uofa-demo / src /uofa_cli /cli.py
system's picture
system HF Staff
CI: sync Gap-Finder Space (cdc3eff)
0cc7125 verified
Raw
History Blame Contribute Delete
8.64 kB
"""UofA CLI entry point — argparse dispatcher for all subcommands."""
from __future__ import annotations
import argparse
import sys
from uofa_cli import __version__
from uofa_cli.output import set_color, error
from uofa_cli.paths import find_repo_root
def _force_utf8_streams() -> None:
"""Reconfigure stdout/stderr to UTF-8 with replacement.
The CLI emits Unicode characters (`══`, `✓`, `✗`, status emoji) in
its progress output. Windows consoles default to cp1252
("charmap"), which can't encode those bytes and raises
``UnicodeEncodeError``, crashing the CLI mid-run. Python 3.7+
exposes ``reconfigure`` on text streams; we set encoding=utf-8 +
errors='replace' so any truly-unencodable byte falls back to
``?`` rather than crashing.
No-op on streams that don't support reconfigure (PIPEs without
text wrapping, redirected file handles in some environments).
"""
for stream_name in ("stdout", "stderr"):
stream = getattr(sys, stream_name, None)
if stream is None:
continue
reconfigure = getattr(stream, "reconfigure", None)
if reconfigure is None:
continue
try:
reconfigure(encoding="utf-8", errors="replace")
except (OSError, ValueError):
# Stream is closed, redirected to a non-text sink, or the
# codec isn't available; degrade silently.
pass
def main():
_force_utf8_streams()
sys.exit(_run() or 0)
def _run():
# Shared flags inherited by all subcommands
parent = argparse.ArgumentParser(add_help=False)
parent.add_argument("--no-color", action="store_true", help="disable colored output")
parent.add_argument("--verbose", action="store_true", help="show full tracebacks on error")
parent.add_argument("--repo-root", metavar="PATH", help="override repo root auto-detection")
parent.add_argument("--pack", metavar="NAME", action="append",
help="pack(s) to use for shapes, rules, and templates (default: vv40). "
"May be repeated: --pack vv40 --pack nasa-7009b")
parser = argparse.ArgumentParser(
prog="uofa",
description="Create, validate, and sign Unit of Assurance evidence packages.",
parents=[parent],
)
parser.add_argument("--version", action="version", version=f"uofa {__version__}")
parser.add_argument("--help-all", action="store_true",
help="emit markdown documentation for every subcommand to stdout and exit")
sub = parser.add_subparsers(dest="command", title="commands")
# ── Register subcommands ──────────────────────────────────
from uofa_cli.commands import keygen, sign, verify, shacl, rules, check, validate, init, diff, schema, packs, migrate, import_excel, extract_cmd, adversarial, catalog, setup, demo, interrogate, decision, report
from uofa_cli.commands import explain as explain_cmd
from uofa_cli.commands import guardrail as guardrail_cmd
modules = {
"keygen": keygen,
"sign": sign,
"verify": verify,
"shacl": shacl,
"rules": rules,
"check": check,
"report": report,
"validate": validate,
"init": init,
"diff": diff,
"explain": explain_cmd,
"schema": schema,
"packs": packs,
"catalog": catalog,
"migrate": migrate,
"import": import_excel,
"extract": extract_cmd,
"adversarial": adversarial,
"setup": setup,
"demo": demo,
"interrogate": interrogate,
"decision": decision,
"guardrail": guardrail_cmd,
}
subparsers: dict[str, argparse.ArgumentParser] = {}
for name, mod in modules.items():
sp = sub.add_parser(name, help=mod.HELP, parents=[parent])
mod.add_arguments(sp)
subparsers[name] = sp
# Pre-parse --pack so values supplied BEFORE the subcommand are preserved.
# With parents=[parent], subparsers inherit a --pack action whose default
# of None clobbers a top-level --pack value at full-parse time. The pre-
# parse recovers it so `uofa --pack X catalog` and `uofa catalog --pack X`
# behave identically.
_pre_pack = argparse.ArgumentParser(add_help=False)
_pre_pack.add_argument("--pack", action="append")
_pre_args, _ = _pre_pack.parse_known_args()
args = parser.parse_args()
if getattr(args, "help_all", False):
sys.stdout.write(_render_help_all(modules, subparsers))
return 0
if not args.command:
parser.print_help()
return 0
if args.no_color:
set_color(False)
# Resolve active pack(s) and thread them explicitly on args (P2d-3). The
# process global is gone; commands read args.active_packs (via
# paths.resolve_active_packs) and pass it down explicitly.
args.active_packs = _pre_args.pack or args.pack or ["vv40"]
# Resolve repo root early so commands can use paths.*
try:
find_repo_root(args.repo_root)
except FileNotFoundError as exc:
error(str(exc))
return 1
# Dispatch
mod = modules[args.command]
try:
return mod.run(args)
except FileNotFoundError as exc:
error(str(exc))
if args.verbose:
raise
return 1
except Exception as exc:
error(str(exc))
if args.verbose:
raise
return 1
def _render_help_all(
modules: dict,
subparsers: dict[str, argparse.ArgumentParser],
) -> str:
"""Emit a markdown reference for every subcommand.
One section per command. Each section: heading, one-line summary,
``Usage`` block (from argparse format_usage), then a table of
options when the parser has any beyond the inherited parent flags.
"""
from datetime import datetime, timezone
generated = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
lines: list[str] = [
"---",
f"title: '`uofa` CLI reference (v{__version__})'",
"description: 'Auto-generated CLI reference covering every subcommand and flag. Re-runs on every site build via `uofa --help-all`.'",
f"generated: {generated}",
f"cli_version: v{__version__}",
f"command_count: {len(modules)}",
"---",
"",
f"Generated from `uofa --help-all` at {generated}. ",
f"{len(modules)} subcommand{'s' if len(modules) != 1 else ''} available.",
"",
"## Synopsis",
"",
"```text",
"uofa [--no-color] [--verbose] [--repo-root PATH] [--pack NAME] <command> [...]",
"```",
"",
"Global flags inherited by every subcommand: `--no-color`, `--verbose`, "
"`--repo-root`, `--pack`. See `uofa --help` for details.",
"",
"## Commands",
"",
]
for name in modules:
mod = modules[name]
sp = subparsers[name]
usage = sp.format_usage().strip().replace("usage: ", "")
lines.extend([
f"### `uofa {name}`",
"",
f"_{getattr(mod, 'HELP', '').strip()}_",
"",
"```text",
usage,
"```",
"",
])
rows = _option_rows(sp)
if rows:
lines.extend([
"| Flag | Description |",
"|---|---|",
])
lines.extend(rows)
lines.append("")
return "\n".join(lines) + "\n"
def _option_rows(sp: argparse.ArgumentParser) -> list[str]:
"""Return one markdown table row per command-specific option.
Excludes inherited parent flags (--no-color, --verbose, --repo-root,
--pack, -h/--help) since those are documented once in the synopsis.
"""
skip = {"--no-color", "--verbose", "--repo-root", "--pack", "-h", "--help"}
rows: list[str] = []
for action in sp._actions:
flags = action.option_strings
if not flags or any(f in skip for f in flags):
continue
flag_str = ", ".join(f"`{f}`" for f in flags)
if action.metavar:
flag_str += f" `{action.metavar}`"
elif action.choices:
flag_str += f" {{{', '.join(str(c) for c in action.choices)}}}"
help_text = (action.help or "").replace("|", "\\|").replace("\n", " ")
rows.append(f"| {flag_str} | {help_text} |")
return rows
if __name__ == "__main__":
main()