refactor(cli): registry in commands/__init__, dispatcher main() in __main__
tracker-policy-pr / check-pr (pull_request) Successful in 8s
test / integration-docker (pull_request) Successful in 17s
test / unit (pull_request) Successful in 48s
lint / lint (push) Failing after 2m56s
test / integration-firecracker (pull_request) Successful in 3m33s
test / coverage (pull_request) Successful in 21s
test / publish-infra (pull_request) Has been skipped
tracker-policy-pr / check-pr (pull_request) Successful in 8s
test / integration-docker (pull_request) Successful in 17s
test / unit (pull_request) Successful in 48s
lint / lint (push) Failing after 2m56s
test / integration-firecracker (pull_request) Successful in 3m33s
test / coverage (pull_request) Successful in 21s
test / publish-infra (pull_request) Has been skipped
Split the cli package's two responsibilities out of __init__:
* commands/__init__.py now assembles the COMMANDS registry and
NO_MIGRATION_COMMANDS from the per-command modules — the command
surface lives entirely under commands/.
* __main__.py now owns main() (dispatch + migration gate + exit-code
mapping) alongside the runnable entry guard.
cli/__init__.py shrinks to a shim that re-exports main / COMMANDS /
NO_MIGRATION_COMMANDS, so bot_bottle.cli.main (repo-root cli.py entry)
and the tests' bot_bottle.cli.COMMANDS keep working unchanged. Verified
`python -m bot_bottle.cli` (runpy) and `cli.py` both dispatch; full unit
suite green (2243).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,93 +1,15 @@
|
|||||||
"""Main CLI dispatcher.
|
"""bot-bottle CLI package.
|
||||||
|
|
||||||
Commands: backend, cleanup, commit, edit, help, info, init, list, login,
|
The subcommand handlers live in `commands/` and are assembled into the
|
||||||
resume, start, supervise
|
COMMANDS registry by `commands/__init__.py`; the dispatcher `main()` lives
|
||||||
|
in `__main__.py`. They are re-exported here so `bot_bottle.cli.main`,
|
||||||
|
`bot_bottle.cli.COMMANDS`, and `bot_bottle.cli.NO_MIGRATION_COMMANDS` stay
|
||||||
|
importable (the repo-root `cli.py` entry point and the tests use them).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
from .__main__ import main
|
||||||
|
from .commands import COMMANDS, NO_MIGRATION_COMMANDS
|
||||||
|
|
||||||
from ..errors import MissingEnvVarError
|
__all__ = ["main", "COMMANDS", "NO_MIGRATION_COMMANDS"]
|
||||||
from ..log import Die, die, error
|
|
||||||
from ..manifest import ManifestError
|
|
||||||
from ..orchestrator.store.store_manager import StoreManager
|
|
||||||
from .constants import PROG
|
|
||||||
from .commands import list as _list_mod
|
|
||||||
from .commands.backend import cmd_backend
|
|
||||||
from .commands.cleanup import cmd_cleanup
|
|
||||||
from .commands.commit import cmd_commit
|
|
||||||
from .commands.edit import cmd_edit
|
|
||||||
from .commands.help import cmd_help
|
|
||||||
from .commands.info import cmd_info
|
|
||||||
from .commands.init import cmd_init
|
|
||||||
from .commands.login import cmd_login
|
|
||||||
from .commands.resume import cmd_resume
|
|
||||||
from .commands.start import cmd_start
|
|
||||||
from .commands.supervise import cmd_supervise
|
|
||||||
|
|
||||||
cmd_list = _list_mod.cmd_list
|
|
||||||
|
|
||||||
COMMANDS = {
|
|
||||||
"backend": cmd_backend,
|
|
||||||
"cleanup": cmd_cleanup,
|
|
||||||
"commit": cmd_commit,
|
|
||||||
"edit": cmd_edit,
|
|
||||||
"help": cmd_help,
|
|
||||||
"info": cmd_info,
|
|
||||||
"init": cmd_init,
|
|
||||||
"list": cmd_list,
|
|
||||||
"login": cmd_login,
|
|
||||||
"resume": cmd_resume,
|
|
||||||
"start": cmd_start,
|
|
||||||
"supervise": cmd_supervise,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Commands that manage host prerequisites (or are otherwise store-free) and
|
|
||||||
# must run before — or without — a migrated DB. `backend` provisions/probes
|
|
||||||
# the host (TAP pool, /dev/kvm, firecracker) and never opens the store, so
|
|
||||||
# gating it on the schema breaks preflight on a fresh CI runner where stdin
|
|
||||||
# isn't a TTY and the migration prompt can't be answered.
|
|
||||||
NO_MIGRATION_COMMANDS = frozenset({"backend", "help", "login"})
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
|
||||||
if argv is None:
|
|
||||||
argv = sys.argv[1:]
|
|
||||||
if not argv:
|
|
||||||
cmd_help()
|
|
||||||
return 2
|
|
||||||
command = argv[0]
|
|
||||||
rest = argv[1:]
|
|
||||||
if command in ("-h", "--help"):
|
|
||||||
cmd_help()
|
|
||||||
return 0
|
|
||||||
handler = COMMANDS.get(command)
|
|
||||||
if handler is None:
|
|
||||||
cmd_help()
|
|
||||||
die(f"unknown command: {command}")
|
|
||||||
mgr = StoreManager.instance()
|
|
||||||
if command not in NO_MIGRATION_COMMANDS and not mgr.is_migrated():
|
|
||||||
sys.stderr.write("bot-bottle: database schema is out of date\n")
|
|
||||||
sys.stderr.write("Migrate now? [y/N] ")
|
|
||||||
sys.stderr.flush()
|
|
||||||
try:
|
|
||||||
answer = sys.stdin.readline().strip().lower()
|
|
||||||
except EOFError:
|
|
||||||
answer = ""
|
|
||||||
if answer != "y":
|
|
||||||
error("migration required — re-run and confirm to migrate")
|
|
||||||
return 1
|
|
||||||
mgr.migrate()
|
|
||||||
try:
|
|
||||||
return handler(rest) or 0
|
|
||||||
except MissingEnvVarError as e:
|
|
||||||
error(str(e))
|
|
||||||
return 1
|
|
||||||
except ManifestError as e:
|
|
||||||
error(str(e))
|
|
||||||
return 1
|
|
||||||
except Die as e:
|
|
||||||
return e.code if isinstance(e.code, int) else 1
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
return 130
|
|
||||||
|
|||||||
@@ -1,15 +1,65 @@
|
|||||||
"""Entry point for `python -m bot_bottle.cli`.
|
"""Entry point + dispatcher for `python -m bot_bottle.cli`.
|
||||||
|
|
||||||
`cli.py` at the repo root is the usual way in; this makes the package
|
Maps `bot-bottle <command>` to its handler in the COMMANDS registry
|
||||||
runnable too, so the CLI works from an installed copy where there is no
|
(`bot_bottle.cli.commands`), enforces the schema-migration gate, and
|
||||||
`cli.py` on disk to point at.
|
translates handler exceptions into process exit codes. The repo-root
|
||||||
|
`cli.py` is the usual way in; this makes the package runnable too, so the
|
||||||
|
CLI works from an installed copy where there is no `cli.py` on disk.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from . import main
|
from ..errors import MissingEnvVarError
|
||||||
|
from ..log import Die, die, error
|
||||||
|
from ..manifest import ManifestError
|
||||||
|
from ..orchestrator.store.store_manager import StoreManager
|
||||||
|
from .commands import COMMANDS, NO_MIGRATION_COMMANDS
|
||||||
|
from .commands.help import cmd_help
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
if argv is None:
|
||||||
|
argv = sys.argv[1:]
|
||||||
|
if not argv:
|
||||||
|
cmd_help()
|
||||||
|
return 2
|
||||||
|
command = argv[0]
|
||||||
|
rest = argv[1:]
|
||||||
|
if command in ("-h", "--help"):
|
||||||
|
cmd_help()
|
||||||
|
return 0
|
||||||
|
handler = COMMANDS.get(command)
|
||||||
|
if handler is None:
|
||||||
|
cmd_help()
|
||||||
|
die(f"unknown command: {command}")
|
||||||
|
mgr = StoreManager.instance()
|
||||||
|
if command not in NO_MIGRATION_COMMANDS and not mgr.is_migrated():
|
||||||
|
sys.stderr.write("bot-bottle: database schema is out of date\n")
|
||||||
|
sys.stderr.write("Migrate now? [y/N] ")
|
||||||
|
sys.stderr.flush()
|
||||||
|
try:
|
||||||
|
answer = sys.stdin.readline().strip().lower()
|
||||||
|
except EOFError:
|
||||||
|
answer = ""
|
||||||
|
if answer != "y":
|
||||||
|
error("migration required — re-run and confirm to migrate")
|
||||||
|
return 1
|
||||||
|
mgr.migrate()
|
||||||
|
try:
|
||||||
|
return handler(rest) or 0
|
||||||
|
except MissingEnvVarError as e:
|
||||||
|
error(str(e))
|
||||||
|
return 1
|
||||||
|
except ManifestError as e:
|
||||||
|
error(str(e))
|
||||||
|
return 1
|
||||||
|
except Die as e:
|
||||||
|
return e.code if isinstance(e.code, int) else 1
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
return 130
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|||||||
@@ -1,6 +1,47 @@
|
|||||||
"""CLI subcommand handlers — one module per `bot-bottle <command>`.
|
"""CLI subcommand registry.
|
||||||
|
|
||||||
Each module exposes a `cmd_<name>(argv)` handler that the dispatcher
|
One module per `bot-bottle <command>`, each exposing a `cmd_<name>(argv)`
|
||||||
(`bot_bottle.cli`) registers in its COMMANDS table. Shared CLI helpers
|
handler. This package `__init__` assembles them into the COMMANDS table
|
||||||
|
the dispatcher (`bot_bottle.cli.__main__`) reads. Shared CLI helpers
|
||||||
(`constants`, `tui`) stay one level up in the `cli` package.
|
(`constants`, `tui`) stay one level up in the `cli` package.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .backend import cmd_backend
|
||||||
|
from .cleanup import cmd_cleanup
|
||||||
|
from .commit import cmd_commit
|
||||||
|
from .edit import cmd_edit
|
||||||
|
from .help import cmd_help
|
||||||
|
from .info import cmd_info
|
||||||
|
from .init import cmd_init
|
||||||
|
from .list import cmd_list
|
||||||
|
from .login import cmd_login
|
||||||
|
from .resume import cmd_resume
|
||||||
|
from .start import cmd_start
|
||||||
|
from .supervise import cmd_supervise
|
||||||
|
|
||||||
|
COMMANDS = {
|
||||||
|
"backend": cmd_backend,
|
||||||
|
"cleanup": cmd_cleanup,
|
||||||
|
"commit": cmd_commit,
|
||||||
|
"edit": cmd_edit,
|
||||||
|
"help": cmd_help,
|
||||||
|
"info": cmd_info,
|
||||||
|
"init": cmd_init,
|
||||||
|
"list": cmd_list,
|
||||||
|
"login": cmd_login,
|
||||||
|
"resume": cmd_resume,
|
||||||
|
"start": cmd_start,
|
||||||
|
"supervise": cmd_supervise,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Commands that manage host prerequisites (or are otherwise store-free) and
|
||||||
|
# must run before — or without — a migrated DB. `backend` provisions/probes
|
||||||
|
# the host (TAP pool, /dev/kvm, firecracker) and never opens the store, so
|
||||||
|
# gating it on the schema breaks preflight on a fresh CI runner where stdin
|
||||||
|
# isn't a TTY and the migration prompt can't be answered. `help` and `login`
|
||||||
|
# likewise never touch the store.
|
||||||
|
NO_MIGRATION_COMMANDS = frozenset({"backend", "help", "login"})
|
||||||
|
|
||||||
|
__all__ = ["COMMANDS", "NO_MIGRATION_COMMANDS"]
|
||||||
|
|||||||
Reference in New Issue
Block a user