"""Guide Board command line interface.""" from __future__ import annotations import argparse import json import sys from pathlib import Path from typing import Any from guide_board.discovery import discover_extensions from guide_board.errors import GuideBoardError from guide_board.execution import run_assessment from guide_board.gates import evaluate_trend_gates from guide_board.io import load_json, write_json from guide_board.planning import ( build_run_plan, validate_assessment_profile, validate_target_profile, ) from guide_board.retention import build_trend_summary, list_retained_runs from guide_board.schema import assert_valid from guide_board.service import build_server def main(argv: list[str] | None = None) -> int: parser = build_parser() args = parser.parse_args(argv) try: result = args.func(args) except GuideBoardError as exc: print(f"guide-board: {exc}", file=sys.stderr) return 2 except (OSError, ValueError) as exc: print(f"guide-board: {exc}", file=sys.stderr) return 1 if result is not None: print_json(result) return 0 def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(prog="guide-board") parser.add_argument("--root", type=Path, default=Path.cwd(), help="repository root") parser.add_argument( "--extension-dir", action="append", type=Path, help="external extension repo or directory containing extension repos", ) subcommands = parser.add_subparsers(required=True) extensions = subcommands.add_parser("extensions", help="extension operations") extension_commands = extensions.add_subparsers(required=True) list_extensions = extension_commands.add_parser("list", help="list discovered extensions") list_extensions.set_defaults(func=cmd_extensions_list) validate_extensions = extension_commands.add_parser( "validate", help="validate discovered extension manifests" ) validate_extensions.set_defaults(func=cmd_extensions_validate) profile = subcommands.add_parser("profile", help="profile validation") profile_commands = profile.add_subparsers(required=True) target = profile_commands.add_parser("validate-target", help="validate a target profile") target.add_argument("path", type=Path) target.set_defaults(func=cmd_validate_target) assessment = profile_commands.add_parser( "validate-assessment", help="validate an assessment profile" ) assessment.add_argument("path", type=Path) assessment.set_defaults(func=cmd_validate_assessment) plan = subcommands.add_parser("plan", help="build a run plan") plan.add_argument("--target", type=Path, required=True) plan.add_argument("--assessment", type=Path, required=True) plan.add_argument("--output", type=Path) plan.set_defaults(func=cmd_plan) run = subcommands.add_parser("run", help="run the baseline assessment executor") run.add_argument("--target", type=Path, required=True) run.add_argument("--assessment", type=Path, required=True) run.add_argument("--output-dir", type=Path) run.set_defaults(func=cmd_run) serve = subcommands.add_parser("serve", help="serve the local HTTP API") serve.add_argument("--host", default="127.0.0.1") serve.add_argument("--port", type=int, default=8080) serve.set_defaults(func=cmd_serve) runs = subcommands.add_parser("runs", help="run history operations") runs_commands = runs.add_subparsers(required=True) list_runs = runs_commands.add_parser("list", help="list retained run summaries") list_runs.add_argument("--runs-dir", type=Path) list_runs.set_defaults(func=cmd_runs_list) trend_runs = runs_commands.add_parser("trend", help="summarize retained run trends") trend_runs.add_argument("--runs-dir", type=Path) trend_runs.set_defaults(func=cmd_runs_trend) gate_runs = runs_commands.add_parser("gate", help="evaluate retained run quality gates") gate_runs.add_argument("--runs-dir", type=Path) gate_runs.add_argument("--target") gate_runs.add_argument("--assessment") gate_runs.add_argument("--allowed-status", action="append") gate_runs.add_argument("--max-unexpected-findings", type=int, default=0) gate_runs.add_argument("--allow-regression", action="store_true") gate_runs.set_defaults(func=cmd_runs_gate) schema = subcommands.add_parser("schema", help="schema validation") schema.add_argument("schema_name") schema.add_argument("path", type=Path) schema.set_defaults(func=cmd_schema_validate) return parser def cmd_extensions_list(args: argparse.Namespace) -> dict[str, Any]: extensions = discover_extensions(args.root, args.extension_dir) return { "extensions": [ { "id": extension.id, "name": extension.manifest["name"], "version": extension.manifest["version"], "type": extension.manifest["extension_type"], "path": _display_path(args.root, extension.path), "source": extension.source, } for extension in extensions ] } def cmd_extensions_validate(args: argparse.Namespace) -> dict[str, Any]: extensions = discover_extensions(args.root, args.extension_dir) return { "status": "valid", "extensions": [extension.id for extension in extensions], } def cmd_validate_target(args: argparse.Namespace) -> dict[str, Any]: profile = validate_target_profile(args.path) return {"status": "valid", "target_profile": profile["id"]} def cmd_validate_assessment(args: argparse.Namespace) -> dict[str, Any]: profile = validate_assessment_profile(args.path) return {"status": "valid", "assessment_profile": profile["id"]} def cmd_plan(args: argparse.Namespace) -> dict[str, Any] | None: plan = build_run_plan(args.root, args.target, args.assessment, args.extension_dir) if args.output: write_json(args.output, plan) return {"status": "written", "path": str(args.output)} return plan def cmd_run(args: argparse.Namespace) -> dict[str, Any]: return run_assessment( args.root, args.target, args.assessment, args.output_dir, args.extension_dir, ) def cmd_serve(args: argparse.Namespace) -> None: server = build_server(args.root, args.extension_dir, args.host, args.port) host, port = server.server_address print(f"guide-board: serving local API on http://{host}:{port}", file=sys.stderr) try: server.serve_forever() except KeyboardInterrupt: print("guide-board: stopping local API", file=sys.stderr) finally: server.server_close() return None def cmd_runs_list(args: argparse.Namespace) -> dict[str, Any]: runs_dir = args.runs_dir or args.root / "runs" return { "runs_dir": str(runs_dir), "runs": list_retained_runs(runs_dir), } def cmd_runs_trend(args: argparse.Namespace) -> dict[str, Any]: runs_dir = args.runs_dir or args.root / "runs" summary = build_trend_summary(runs_dir) assert_valid(summary, "trend-summary") return summary def cmd_runs_gate(args: argparse.Namespace) -> dict[str, Any]: runs_dir = args.runs_dir or args.root / "runs" trend_summary = build_trend_summary(runs_dir) gate_summary = evaluate_trend_gates( trend_summary, allowed_statuses=args.allowed_status, max_unexpected_findings=args.max_unexpected_findings, fail_on_regression=not args.allow_regression, target_profile_ref=args.target, assessment_profile_ref=args.assessment, ) assert_valid(gate_summary, "gate-summary") return gate_summary def cmd_schema_validate(args: argparse.Namespace) -> dict[str, Any]: document = load_json(args.path) assert_valid(document, args.schema_name) return {"status": "valid", "schema": args.schema_name, "path": str(args.path)} def print_json(value: Any) -> None: print(json.dumps(value, indent=2, sort_keys=True)) def _display_path(root: Path, path: Path) -> str: try: return str(path.resolve().relative_to(root.resolve())) except ValueError: return str(path.resolve())