generated from coulomb/repo-seed
227 lines
8.1 KiB
Python
227 lines
8.1 KiB
Python
"""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())
|