#!/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")