mirror of
https://github.com/BillyOutlast/CLI-Anything-WEB.git
synced 2026-10-12 02:24:59 +00:00
The repo "About" description embeds a count ("... N CLIs and counting.")
that was hand-maintained and had drifted (said 17; the registry has 19).
registry.json is the fleet source of truth, so the count should derive
from it.
- cli-web-devkit about: renders the count from registry.json and reconciles
it with the live GitHub description (gh API). `--check` fails on drift
(read-only, CI-safe); `--apply` rewrites just the count, preserving any
reworded prose. Editing repo metadata needs Administration:write, which
the default GITHUB_TOKEN lacks, so --apply is a maintainer/PAT step while
--check is the CI guardrail.
- tests.yml lint job runs `about --check` (no-ops if gh metadata is
unavailable, so it never flakes CI).
- standards Step 6 runs `about --apply` alongside the other registry-driven
generators when a CLI is added.
- unit tests cover the count templating (replace/idempotent/append/
reworded-prose/first-only) and registry counting.
Also corrected the live description to 19 CLIs.
211 lines
7.4 KiB
Python
211 lines
7.4 KiB
Python
"""``cli-web-devkit`` — fleet tooling entry point."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from .about import apply_description, desired_description, fleet_count, live_description
|
|
from .canary import run_canaries
|
|
from .docs import generate as generate_docs
|
|
from .gaps import analyze as analyze_gaps
|
|
from .matrix import render_matrix
|
|
from .paths import repo_root
|
|
from .registry import validate as validate_registry
|
|
from .spec import validate_file as validate_spec_file
|
|
from .sync import drift, resync
|
|
|
|
|
|
def _cmd_registry_validate(args: argparse.Namespace) -> int:
|
|
problems = validate_registry(args.root)
|
|
if problems:
|
|
print("registry.json validation FAILED:", file=sys.stderr)
|
|
for p in problems:
|
|
print(f" - {p}", file=sys.stderr)
|
|
return 1
|
|
print("registry.json OK")
|
|
return 0
|
|
|
|
|
|
def _cmd_matrix(args: argparse.Namespace) -> int:
|
|
print(render_matrix(args.root))
|
|
return 0
|
|
|
|
|
|
def _cmd_docs(args: argparse.Namespace) -> int:
|
|
fresh = generate_docs(args.root, check=args.check)
|
|
if args.check:
|
|
if fresh:
|
|
print("README.md fleet sections are up to date")
|
|
return 0
|
|
print(
|
|
"README.md fleet sections are STALE — run: cli-web-devkit docs",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
print("README.md regenerated" if not fresh else "README.md already up to date")
|
|
return 0
|
|
|
|
|
|
def _cmd_about(args: argparse.Namespace) -> int:
|
|
count = fleet_count(args.root)
|
|
try:
|
|
live = live_description()
|
|
except RuntimeError as exc:
|
|
# No gh / not authenticated. Don't fail CI for a metadata nicety;
|
|
# report and move on (the count is enforced when gh is available).
|
|
print(f"about: skipped — {exc}", file=sys.stderr)
|
|
return 0
|
|
desired = desired_description(live, count)
|
|
if live == desired:
|
|
print(f"About description in sync ({count} CLIs)")
|
|
return 0
|
|
if args.apply:
|
|
apply_description(desired)
|
|
print(f"About description updated -> {count} CLIs")
|
|
return 0
|
|
if args.check:
|
|
print(
|
|
f"About description STALE (fleet has {count} CLIs) — run: cli-web-devkit about --apply",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
print(f"current: {live}")
|
|
print(f"desired: {desired}")
|
|
print("run: cli-web-devkit about --apply")
|
|
return 0
|
|
|
|
|
|
def _cmd_drift(args: argparse.Namespace) -> int:
|
|
items = drift(args.root)
|
|
bad = [i for i in items if i.status in ("drifted", "missing")]
|
|
for item in items:
|
|
if args.verbose or item.status != "ok":
|
|
detail = f" ({item.detail})" if item.detail else ""
|
|
print(f"{item.status.upper():9} {item.cli:24} {item.file}{detail}")
|
|
print(f"\n{len(items) - len(bad)} ok, {len(bad)} drifted/missing")
|
|
return 1 if bad else 0
|
|
|
|
|
|
def _cmd_canary(args: argparse.Namespace) -> int:
|
|
import json as _json
|
|
|
|
report = run_canaries(args.root, names=args.app or None, timeout=args.timeout)
|
|
if args.json_out:
|
|
print(_json.dumps(report.to_dict(), indent=2))
|
|
else:
|
|
for r in report.results:
|
|
mark = "PASS" if r.ok else "FAIL"
|
|
detail = f" ({r.detail})" if r.detail else ""
|
|
print(f"{mark} {r.cli:24} {' '.join(r.argv)}{detail}")
|
|
print(
|
|
f"\n{len(report.results) - len(report.failures)} passed, {len(report.failures)} failed"
|
|
)
|
|
return 1 if report.failures else 0
|
|
|
|
|
|
def _cmd_gaps(args: argparse.Namespace) -> int:
|
|
import json as _json
|
|
|
|
report = analyze_gaps(args.root, args.app)
|
|
print(_json.dumps(report.to_dict(), indent=2))
|
|
return 1 if (report.unimplemented_endpoints or report.unexposed_methods) else 0
|
|
|
|
|
|
def _cmd_spec_validate(args: argparse.Namespace) -> int:
|
|
problems = validate_spec_file(Path(args.spec))
|
|
if problems:
|
|
print("api-spec validation FAILED:", file=sys.stderr)
|
|
for p in problems:
|
|
print(f" - {p}", file=sys.stderr)
|
|
return 1
|
|
print("api-spec OK")
|
|
return 0
|
|
|
|
|
|
def _cmd_resync(args: argparse.Namespace) -> int:
|
|
changed = resync(args.root, apps=args.app or None)
|
|
if args.app:
|
|
print(
|
|
"note: --app skips the plugin-internal copies "
|
|
"(cli-anything-web-plugin/scripts/) — run a full resync to update them"
|
|
)
|
|
if changed:
|
|
print(f"resynced {len(changed)} file(s):")
|
|
for path in changed:
|
|
print(f" {path}")
|
|
else:
|
|
print("fleet already in sync")
|
|
return 0
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(prog="cli-web-devkit", description=__doc__)
|
|
parser.add_argument(
|
|
"--root",
|
|
type=Path,
|
|
default=None,
|
|
help="Repo root (auto-detected from cwd by default)",
|
|
)
|
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
|
|
p_reg = sub.add_parser("registry", help="Registry operations")
|
|
reg_sub = p_reg.add_subparsers(dest="registry_command", required=True)
|
|
p_val = reg_sub.add_parser("validate", help="Validate registry.json against the fleet")
|
|
p_val.set_defaults(func=_cmd_registry_validate)
|
|
|
|
p_matrix = sub.add_parser("matrix", help="Emit GitHub Actions test matrix JSON")
|
|
p_matrix.set_defaults(func=_cmd_matrix)
|
|
|
|
p_drift = sub.add_parser("drift", help="Detect vendored shared files diverging from canon")
|
|
p_drift.add_argument("--verbose", action="store_true", help="Also list in-sync files")
|
|
p_drift.set_defaults(func=_cmd_drift)
|
|
|
|
p_resync = sub.add_parser("resync", help="Rewrite vendored shared files from canon")
|
|
p_resync.add_argument(
|
|
"--app",
|
|
action="append",
|
|
help="Limit to specific app(s); default: whole fleet + plugin copy",
|
|
)
|
|
p_resync.set_defaults(func=_cmd_resync)
|
|
|
|
p_canary = sub.add_parser("canary", help="Run registered live-site canary commands")
|
|
p_canary.add_argument("--app", action="append", help="Limit to specific app(s)")
|
|
p_canary.add_argument("--json", action="store_true", dest="json_out", help="JSON report")
|
|
p_canary.add_argument("--timeout", type=float, default=120.0)
|
|
p_canary.set_defaults(func=_cmd_canary)
|
|
|
|
p_docs = sub.add_parser("docs", help="Regenerate README fleet sections from registry.json")
|
|
p_docs.add_argument("--check", action="store_true", help="Fail if README is stale")
|
|
p_docs.set_defaults(func=_cmd_docs)
|
|
|
|
p_about = sub.add_parser(
|
|
"about", help="Sync the GitHub repo description's CLI count with registry.json"
|
|
)
|
|
p_about.add_argument("--check", action="store_true", help="Fail if the count is stale (CI)")
|
|
p_about.add_argument(
|
|
"--apply", action="store_true", help="Update the live description (needs gh admin)"
|
|
)
|
|
p_about.set_defaults(func=_cmd_about)
|
|
|
|
p_gaps = sub.add_parser("gaps", help="Captured-vs-implemented-vs-exposed gap report")
|
|
p_gaps.add_argument("app", help="App directory name (e.g. hackernews)")
|
|
p_gaps.set_defaults(func=_cmd_gaps)
|
|
|
|
p_spec = sub.add_parser("spec", help="api-spec.json operations")
|
|
spec_sub = p_spec.add_subparsers(dest="spec_command", required=True)
|
|
p_spec_val = spec_sub.add_parser("validate", help="Validate an api-spec.json file")
|
|
p_spec_val.add_argument("spec", help="Path to api-spec.json")
|
|
p_spec_val.set_defaults(func=_cmd_spec_validate)
|
|
|
|
args = parser.parse_args(argv)
|
|
if args.root is None:
|
|
args.root = repo_root(Path.cwd())
|
|
result: int = args.func(args)
|
|
return result
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|