feat: CLI consistency pass — unified args, verbs, and status

#1 Positional dest unified to `name` across info + dev verbs (was `project`).
#2 install/uninstall help now states the activate semantics (tool→PATH,
   service/job→systemd, frontend→served) rather than 'to PATH'.
#4 `service status` (a plural op) moved to `services status`; the singular
   `service` group is enable/disable only.
#5 `list --behavior` now filters the program *catalog* by its real behavior
   field. behavior (what a program is) and service/job (how it's deployed) are
   separate axes: `list` shows a Programs catalog plus Services/Jobs deployment
   views; a behavior filter scopes the catalog only.
#6 `list` uses lifecycle.is_active uniformly for the status dot (tool on PATH /
   service running / job timer / static frontend served) instead of registry
   presence.
#7 New `castle restart <name>` — restart a service (unit) or job (timer).
#8 New `castle status` — unified services + jobs + programs activation view.
#9 New `castle format [name]` dev verb (ruff format / pnpm format); wired into
   stacks DEV_ACTIONS + handlers.
#10 New `castle up` — deploy + start everything in one shot.

Docs: CLAUDE.md command reference refreshed. Tests updated to the two-axis
list model (+ a daemon-behavior program fixture). core 92 · cli 24 · api 52
green; ruff clean.
This commit is contained in:
2026-06-14 11:57:58 -07:00
parent 3f3b88f17b
commit a5dcb6b346
10 changed files with 245 additions and 156 deletions

View File

@@ -63,34 +63,42 @@ def build_parser() -> argparse.ArgumentParser:
# castle info
info_parser = subparsers.add_parser("info", help="Show program details")
info_parser.add_argument("project", help="Program, service, or job name")
info_parser.add_argument("name", help="Program, service, or job name")
info_parser.add_argument("--json", action="store_true", help="Output as JSON")
# castle test
test_parser = subparsers.add_parser("test", help="Run tests")
test_parser.add_argument("project", nargs="?", help="Project to test (default: all)")
test_parser.add_argument("name", nargs="?", help="Program to test (default: all)")
# castle lint
lint_parser = subparsers.add_parser("lint", help="Run linter")
lint_parser.add_argument("project", nargs="?", help="Project to lint (default: all)")
lint_parser.add_argument("name", nargs="?", help="Program to lint (default: all)")
# castle format
format_parser = subparsers.add_parser("format", help="Format source code")
format_parser.add_argument("name", nargs="?", help="Program to format (default: all)")
# castle build
build_parser = subparsers.add_parser("build", help="Build projects")
build_parser.add_argument("project", nargs="?", help="Project to build (default: all)")
build_parser = subparsers.add_parser("build", help="Build programs")
build_parser.add_argument("name", nargs="?", help="Program to build (default: all)")
# castle type-check
tc_parser = subparsers.add_parser("type-check", help="Run type checker")
tc_parser.add_argument("project", nargs="?", help="Project to type-check (default: all)")
tc_parser.add_argument("name", nargs="?", help="Program to type-check (default: all)")
# castle check (composite: lint + type-check + test)
check_parser = subparsers.add_parser("check", help="Run lint + type-check + test")
check_parser.add_argument("project", nargs="?", help="Project to check (default: all)")
check_parser.add_argument("name", nargs="?", help="Program to check (default: all)")
# castle install / uninstall
install_parser = subparsers.add_parser("install", help="Install a program to PATH")
install_parser.add_argument("project", nargs="?", help="Program to install (default: all)")
uninstall_parser = subparsers.add_parser("uninstall", help="Uninstall a program")
uninstall_parser.add_argument("project", nargs="?", help="Program to uninstall (default: all)")
# castle install / uninstall — activate / deactivate a program in its mode
install_parser = subparsers.add_parser(
"install", help="Activate a program (tool→PATH, service/job→systemd, frontend→served)"
)
install_parser.add_argument("name", nargs="?", help="Program to activate (default: all)")
uninstall_parser = subparsers.add_parser(
"uninstall", help="Deactivate a program (reverse of install)"
)
uninstall_parser.add_argument("name", nargs="?", help="Program to deactivate")
# castle gateway
gateway_parser = subparsers.add_parser("gateway", help="Manage the Caddy gateway")
@@ -116,13 +124,23 @@ def build_parser() -> argparse.ArgumentParser:
)
disable_parser = service_sub.add_parser("disable", help="Stop and disable a service")
disable_parser.add_argument("name", help="Service name")
service_sub.add_parser("status", help="Show status of all services")
# castle services (plural - manage all services)
services_parser = subparsers.add_parser("services", help="Manage all services together")
services_sub = services_parser.add_subparsers(dest="services_command")
services_sub.add_parser("start", help="Start all services and gateway")
services_sub.add_parser("stop", help="Stop all services and gateway")
services_sub.add_parser("status", help="Show status of all services and jobs")
# castle restart — restart a single deployed service or job
restart_parser = subparsers.add_parser("restart", help="Restart a service or job")
restart_parser.add_argument("name", help="Service or job name")
# castle status — unified status (gateway + services + jobs + programs)
subparsers.add_parser("status", help="Show overall status across the platform")
# castle up — bring everything online (deploy + start all services)
subparsers.add_parser("up", help="Deploy and start all services and the gateway")
# castle logs
logs_parser = subparsers.add_parser("logs", help="View service/job logs")
@@ -196,6 +214,11 @@ def main() -> int:
return run_lint(args)
elif args.command == "format":
from castle_cli.commands.dev import run_format
return run_format(args)
elif args.command == "build":
from castle_cli.commands.dev import run_build
@@ -236,6 +259,21 @@ def main() -> int:
return run_services(args)
elif args.command == "restart":
from castle_cli.commands.service import run_restart
return run_restart(args)
elif args.command == "status":
from castle_cli.commands.service import run_status
return run_status(args)
elif args.command == "up":
from castle_cli.commands.service import run_up
return run_up(args)
elif args.command == "logs":
from castle_cli.commands.logs import run_logs