Files
tractorjuice--arckit-gemini/scripts/python/common.py
T
2026-07-13 12:35:03 +08:00

289 lines
8.4 KiB
Python

#!/usr/bin/env python3
"""
Common utilities for ArcKit scripts (plugin version).
Looks for projects/ directory as repo root indicator.
"""
import os
import re
import sys
import json
import shutil
import subprocess
from pathlib import Path
# ANSI color codes
RED = "\033[0;31m"
GREEN = "\033[0;32m"
YELLOW = "\033[1;33m"
BLUE = "\033[0;34m"
NC = "\033[0m" # No Color
# ============================================================================
# Logging Functions
# ============================================================================
def log_info(msg):
print(f"{BLUE}[INFO]{NC} {msg}", file=sys.stderr)
def log_success(msg):
print(f"{GREEN}[SUCCESS]{NC} {msg}", file=sys.stderr)
def log_warning(msg):
print(f"{YELLOW}[WARNING]{NC} {msg}", file=sys.stderr)
def log_error(msg):
print(f"{RED}[ERROR]{NC} {msg}", file=sys.stderr)
# ============================================================================
# Repository Root Detection
# ============================================================================
def find_repo_root(start_dir=None):
"""Find the repository root by looking for projects/ directory."""
current = Path(start_dir or os.getcwd()).resolve()
while current != current.parent:
if (current / "projects").is_dir():
return str(current)
current = current.parent
log_error("Not in an ArcKit project (no projects/ directory found)")
sys.exit(1)
# ============================================================================
# Project Management
# ============================================================================
def get_next_project_number(repo_root):
"""Get the next available project number (zero-padded to 3 digits)."""
projects_dir = Path(repo_root) / "projects"
if not projects_dir.is_dir():
return "001"
max_num = 0
for entry in projects_dir.iterdir():
if entry.is_dir():
m = re.match(r"^(\d{3})-", entry.name)
if m:
num = int(m.group(1))
if num > max_num:
max_num = num
return f"{max_num + 1:03d}"
def slugify(text):
"""Convert text to kebab-case slug."""
text = text.lower()
text = re.sub(r"[^a-z0-9]+", "-", text)
text = text.strip("-")
return text
def create_project_dir(project_dir):
"""Create project directory structure with all required subdirectories."""
subdirs = [
"", "vendors", "external", "final",
"decisions", "diagrams", "wardley-maps",
"data-contracts", "reviews",
]
for sub in subdirs:
(Path(project_dir) / sub).mkdir(parents=True, exist_ok=True)
log_success(f"Created project directory: {project_dir}")
# ============================================================================
# Project Finding
# ============================================================================
def find_project_dir_by_prefix(prefix, repo_root=None):
"""Find project directory by number or prefix (exact then fuzzy match)."""
if repo_root is None:
repo_root = find_repo_root()
projects_dir = Path(repo_root) / "projects"
if not projects_dir.is_dir():
log_error("No projects directory found")
return None
# Exact match first
for entry in sorted(projects_dir.iterdir()):
if entry.is_dir():
name = entry.name
if name == prefix or name.startswith(f"{prefix}-"):
return str(entry)
# Fuzzy match
for entry in sorted(projects_dir.iterdir()):
if entry.is_dir() and prefix in entry.name:
return str(entry)
log_error(f"No project found matching: {prefix}")
return None
def get_project_number_from_dir(dir_path):
"""Extract 3-digit project number from directory name."""
name = Path(dir_path).name
m = re.match(r"^(\d{3})-", name)
return m.group(1) if m else None
def list_projects(repo_root=None):
"""List all projects."""
if repo_root is None:
repo_root = find_repo_root()
projects_dir = Path(repo_root) / "projects"
if not projects_dir.is_dir():
print("No projects found")
return
dirs = sorted(d for d in projects_dir.iterdir() if d.is_dir())
if not dirs:
print("No projects found")
return
print("Available projects:")
for d in dirs:
print(f" - {d.name}")
# ============================================================================
# Git Integration
# ============================================================================
def has_git():
"""Check if git is available."""
return shutil.which("git") is not None
def get_repo_root():
"""Get repository root using git (fallback to find_repo_root)."""
if has_git():
try:
result = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
capture_output=True, text=True, timeout=5,
)
if result.returncode == 0:
return result.stdout.strip()
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
return find_repo_root()
def get_current_branch():
"""Get current git branch."""
if has_git():
try:
result = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
capture_output=True, text=True, timeout=5,
)
if result.returncode == 0:
return result.stdout.strip()
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
return "main"
# ============================================================================
# Validation Helpers
# ============================================================================
def check_file(file_path, description=None):
"""Check if file exists and print status."""
if description is None:
description = Path(file_path).name
if Path(file_path).is_file():
print(f" \u2713 {description}")
return True
print(f" \u2717 {description}")
return False
def check_dir(dir_path, description=None):
"""Check if directory exists and is not empty."""
if description is None:
description = Path(dir_path).name
p = Path(dir_path)
if p.is_dir() and any(p.iterdir()):
print(f" \u2713 {description}")
return True
print(f" \u2717 {description}")
return False
def require_file(file_path, description=None):
"""Require file to exist."""
if description is None:
description = Path(file_path).name
if not Path(file_path).is_file():
log_error(f"Required file not found: {description}")
log_error(f" Path: {file_path}")
return False
log_success(f"Found: {description}")
return True
def require_dir(dir_path, description=None):
"""Require directory to exist."""
if description is None:
description = Path(dir_path).name
if not Path(dir_path).is_dir():
log_error(f"Required directory not found: {description}")
log_error(f" Path: {dir_path}")
return False
log_success(f"Found: {description}")
return True
# ============================================================================
# JSON Helpers
# ============================================================================
def json_escape(s):
"""Escape string for JSON embedding."""
return json.dumps(s)[1:-1] # Strip surrounding quotes
def output_json_array(items):
"""Output a JSON array string from a list."""
return json.dumps(items)
# ============================================================================
# Path Helpers
# ============================================================================
def get_arckit_dir(repo_root=None):
"""Get .arckit directory path."""
if repo_root is None:
repo_root = find_repo_root()
return os.path.join(repo_root, ".arckit")
def get_templates_dir(repo_root=None):
"""Get templates directory path."""
if repo_root is None:
repo_root = find_repo_root()
return os.path.join(repo_root, ".arckit", "templates")
def get_projects_dir(repo_root=None):
"""Get projects directory path."""
if repo_root is None:
repo_root = find_repo_root()
return os.path.join(repo_root, "projects")
def get_memory_dir(repo_root=None):
"""Get memory directory path (000-global)."""
if repo_root is None:
repo_root = find_repo_root()
return os.path.join(repo_root, "projects", "000-global")