diff --git a/bot_bottle/cli/__init__.py b/bot_bottle/cli/__init__.py index 6a1ea3b..56a73d0 100644 --- a/bot_bottle/cli/__init__.py +++ b/bot_bottle/cli/__init__.py @@ -1,93 +1,15 @@ -"""Main CLI dispatcher. +"""bot-bottle CLI package. -Commands: backend, cleanup, commit, edit, help, info, init, list, login, -resume, start, supervise +The subcommand handlers live in `commands/` and are assembled into the +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 -import sys +from .__main__ import main +from .commands import COMMANDS, NO_MIGRATION_COMMANDS -from ..errors import MissingEnvVarError -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 +__all__ = ["main", "COMMANDS", "NO_MIGRATION_COMMANDS"] diff --git a/bot_bottle/cli/__main__.py b/bot_bottle/cli/__main__.py index 3cf5f19..91a7e5a 100644 --- a/bot_bottle/cli/__main__.py +++ b/bot_bottle/cli/__main__.py @@ -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 -runnable too, so the CLI works from an installed copy where there is no -`cli.py` on disk to point at. +Maps `bot-bottle ` to its handler in the COMMANDS registry +(`bot_bottle.cli.commands`), enforces the schema-migration gate, and +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 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__": sys.exit(main()) diff --git a/bot_bottle/cli/commands/__init__.py b/bot_bottle/cli/commands/__init__.py index 7f34d76..cda230d 100644 --- a/bot_bottle/cli/commands/__init__.py +++ b/bot_bottle/cli/commands/__init__.py @@ -1,6 +1,47 @@ -"""CLI subcommand handlers — one module per `bot-bottle `. +"""CLI subcommand registry. -Each module exposes a `cmd_(argv)` handler that the dispatcher -(`bot_bottle.cli`) registers in its COMMANDS table. Shared CLI helpers +One module per `bot-bottle `, each exposing a `cmd_(argv)` +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. """ + +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"]