"""A compact command catalog over maintained project tools.""" from __future__ import annotations import argparse import importlib import json from pathlib import Path import sys from . import __version__ from .common import META_ROOT, redact, safe_output COMMAND_MODULES = ( "context", "doctor", "runner", "review", "docs", "issues", "release", "maintenance", ) def main(argv: list[str] | None = None) -> int: argv = sys.argv[1:] if argv is None else argv common = argparse.ArgumentParser(add_help=False, allow_abbrev=False) common.add_argument( "--workspace-root", type=Path, default=META_ROOT.parent, help="Directory containing registered repositories", ) common.add_argument( "--project", type=Path, help="Explicit trusted portable-project JSON manifest" ) common.add_argument( "--state-dir", type=Path, help="Private evidence-state base (scoped again by workspace)", ) common.add_argument("--format", choices=("summary", "json"), default="summary") common.add_argument( "--quiet", action="store_true", help="Suppress live progress on stderr; retain the final result", ) common.add_argument( "--json", dest="format", action="store_const", const="json", help="Alias for --format json", ) parser = argparse.ArgumentParser( prog="devkit", description="Deterministic development workflows. Read-only previews by default for remote writes.", parents=[common], allow_abbrev=False, ) parser.add_argument("--version", action="version", version=f"devkit {__version__}") subparsers = parser.add_subparsers(dest="command", required=True) for name in COMMAND_MODULES: importlib.import_module("." + name, __package__).register(subparsers) catalog = subparsers.add_parser("commands", help="Show the compact command catalog") def commands(_): entries = [ { "command": "context", "purpose": "Offline repository changes, ownership and instructions", "effects": "read only", }, { "command": "doctor", "purpose": "Local tool/dependency preflight and repair guidance", "effects": "read only", }, { "command": "check", "purpose": "Registered verification profiles with logs and source-bound receipts", "effects": "tests/builds; --dry-run previews", }, { "command": "runs / latest / status / summary / logs", "purpose": "Find runs and read progress, compact results and bounded live/final logs", "effects": "read only", }, { "command": "coverage", "purpose": "Explain declared suite coverage and exclusions for a check profile", "effects": "read only", }, { "command": "resume / recover", "purpose": "Resume verified identical work or recover an abandoned check run", "effects": "local checks/state only", }, { "command": "review", "purpose": "Module UI-review inventory and manual evidence checklist", "effects": "local bundle only", }, { "command": "docs", "purpose": "Existing documentation and translation audits", "effects": "local checks/evidence", }, { "command": "issues", "purpose": "Preview and explicitly publish deduplicated issue evidence", "effects": "remote only with --apply", }, { "command": "release", "purpose": "Existing durable release lifecycle, receipts and confirmations", "effects": "explicit --apply and step confirmation", }, { "command": "git", "purpose": "Frozen explicit-path commit and branch-push maintenance", "effects": "explicit --apply; no bulk staging, force or tags", }, ] return { "commands": entries, "summary": [ f"{item['command']}: {item['purpose']} ({item['effects']})" for item in entries ], } catalog.set_defaults(handler=commands) # Global flags work before or after the command, without copying defaults to every parser. global_args, remaining = common.parse_known_args(argv) args = parser.parse_args(remaining, namespace=global_args) args.workspace_root = args.workspace_root.expanduser().resolve() if args.project: args.project = args.project.expanduser().absolute() def progress(event): if args.quiet: return if args.format == "json": print( json.dumps(safe_output(event), sort_keys=True, allow_nan=False), file=sys.stderr, flush=True, ) else: counts = event["counts"] completed = sum( count for state, count in counts.items() if state not in {"pending", "running"} ) active = ", ".join(event["active_stages"]) print( redact( f"Run {event['run_id']}: {event['phase']} · {completed}/{event['total_stages']} stages · {event['elapsed_seconds']}s" + (f" · {active}" if active else "") ), file=sys.stderr, flush=True, ) args.on_progress = progress try: result = args.handler(args) if not isinstance(result, dict): raise ValueError("Command did not return a result object") code = int(result.pop("_exit_code", 0)) if args.format == "json": print( json.dumps( safe_output(result), sort_keys=True, indent=2, ensure_ascii=True, allow_nan=False, ) ) else: summary = result.get("summary", [str(result.get("status", "Completed"))]) print( "\n".join(redact(str(line)) for line in summary) if isinstance(summary, list) else redact(str(summary)) ) return code except (ValueError, OSError, RuntimeError, ImportError) as exc: message = redact(str(exc)) if args.format == "json": print( json.dumps( { "status": "error", "error": message, "error_type": type(exc).__name__, } ) ) else: print(f"devkit: {message}", file=sys.stderr) return 2 except KeyboardInterrupt: print( "devkit: interrupted; inspect the saved run before retrying", file=sys.stderr, ) return 130