Skip to content

refactor(cli): decompose the cli.py monolith into a cli/ package#85

Open
donalddellapietra wants to merge 8 commits into
mainfrom
worktree-cli-refactor
Open

refactor(cli): decompose the cli.py monolith into a cli/ package#85
donalddellapietra wants to merge 8 commits into
mainfrom
worktree-cli-refactor

Conversation

@donalddellapietra

Copy link
Copy Markdown
Contributor

Summary

Splits the single 7,542-line sponsio/cli.py into a sponsio/cli/ package — one module per top-level command under commands/, one per command group under groups/, a root group in app.py, and cross-command helpers in _shared.py. Pure restructuring: no behavior change.

Type of change

  • refactor: no behavior change

Before → after

sponsio/cli/
├── __init__.py      # registers all commands, re-exports public + internal API, main()
├── __main__.py      # python -m sponsio.cli
├── app.py           # root `cli` group
├── _shared.py       # cross-command helpers (_contract_guarantee, _resolve_entry, _parse_since, …)
├── commands/        # 18 modules: scan, onboard, validate, export, eval, doctor, init, mode, …
└── groups/          # 5 modules: plugin, host, skill, cursor, daemon

How compatibility is preserved

Many modules and tests do from sponsio.cli import X — the cli group, command callables (onboard, doctor, scan, …), and internal helpers (_resolve_entry, _patch_mode_in_yaml, _filter_invalid_contracts, _packaged_skill_source, _refresh_per_host_bundles, …). __init__ re-exports every one of those from its new home, so no external import changed. The sponsio.cli:main console-script entry and python -m sponsio.cli both still work.

Design notes

  • Eight incremental commits, each independently verified (identical command set + green suite) before the next: scaffold → app → groups (daemon+cursor, skill, plugin, host) → _shared → commands.
  • Two latent bugs fixed in passing: demo's repo_root was computed from __file__ depth (would break on the move) — now resolves via the package location; and a package needs __main__.py for python -m sponsio.cli (the plugin tests shell out via that form).
  • Cross-module coupling is now explicit: onboard imports scan/mode helpers + host's runtime-mode resolver; export_sessions imports export's OTLP converters; host install reuses plugin's bucket bootstrap and skill's packaged-source resolver.
  • Out of scope: plugin.py (1154) and host.py (1037) remain large — they're genuinely big groups (7 subcommands each); per-subcommand splitting is a possible follow-up.

Test plan

pytest -q                                    # 2296 passed, 26 skipped
ruff check sponsio/ tests/ scripts/          # clean
ruff format --check sponsio/ tests/ scripts/ # clean
python -m sponsio.cli --version              # works
python -m sponsio.cli scan --help            # works
python -c "from sponsio.cli import cli, onboard, doctor, scan, _resolve_entry, _patch_mode_in_yaml"  # back-compat

Command set unchanged: 23 top-level commands; subgroups skill(1)/plugin(7)/host(7)/daemon(3)/cursor(2).

Checklist

  • pytest -v passes locally (2296 passed).
  • ruff check / ruff format --check clean.
  • No behavior change; no new dependencies.
  • N/A: no user-visible surface change (CHANGELOG not updated — internal refactor).

Note: commits are not DCO Signed-off-by signed (per your earlier note, DCO isn't enabled on this repo).

🤖 Generated with Claude Code

donalddellapietra and others added 8 commits June 15, 2026 12:59
…hange)

First step of decomposing the 7.5k-line CLI monolith. The module
sponsio/cli.py becomes the sponsio/cli/ package:

- cli.py -> cli/_monolith.py (verbatim; carved into commands/ + groups/
  in following commits).
- cli/__init__.py re-exports every command, group, and the internal
  helpers that other modules and tests import from 'sponsio.cli', so
  'from sponsio.cli import X' is unchanged.
- cli/__main__.py restores 'python -m sponsio.cli' (a package needs an
  explicit __main__; the plugin tests shell out via that form).
- demo's repo_root now resolves via the sponsio package location instead
  of __file__ depth, so it survives the move.

Command set identical (23 top-level + subgroups); 695 CLI tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the top-level 'cli' click.Group into its own module so command and
group modules can register on it without importing the package root.
_monolith now imports cli from app; no behavior change (23 commands, 695
CLI tests pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the two self-contained IDE/daemon groups out of the monolith into
cli/groups/daemon.py and cli/groups/cursor.py. __init__ imports them for
registration and re-exports the names. No behavior change (subcommands
intact; CLI tests pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the skill group, skill_install, and the shared skill-install
verification helpers (_packaged_skill_source, _verify_skill_install_target,
_SKILL_TOOL_DIRS, _SkillInstallHealth, …) out of the monolith. doctor.py
and the host-install path import the shared helpers from here now; __init__
re-exports them for back-compat. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the plugin group, all seven subcommands (init/install/show/append/
scan/prompt/guard), and the plugin-library helpers out of the monolith.
Add cli/_shared.py for genuinely cross-module helpers and relocate
_contract_guarantee there (used by plugin + several commands). host
install imports _bootstrap_default_buckets and _install_one from the
plugin module. No behavior change (7 subcommands intact; tests pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the host group + all subcommands (install/status/trace/list/migrate/
uninstall/guard) and host-lifecycle helpers out of the monolith. host
imports its cross-module deps (skill packaged-source, plugin bucket
bootstrap + _install_one, _shared _contract_guarantee); onboard now imports
_resolve_runtime_mode from the host module. All 5 command groups now live
in cli/groups/. Full suite: 2296 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Relocate _looks_like_sponsio_config, _resolve_entry, _parse_since, and
_parse_existing_contracts (each used by 2+ commands, and _resolve_entry
imported by eval_runner) into _shared.py alongside _contract_guarantee.
__init__ re-exports _resolve_entry from there. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Final decomposition step: move the 18 top-level commands out of the
monolith into one module each under cli/commands/ (demo, patterns, packs,
validate, check, explain, replay, report, serve, scan, export,
export_sessions, eval, init, doctor, onboard, mode, prompt). Command-local
helpers travel with their command (scan's yaml filters, export's OTLP
converters, mode's yaml patcher); cross-command coupling is explicit
(onboard imports scan/mode helpers + host's mode resolver; export_sessions
imports export's OTLP converters).

_monolith.py is deleted. __init__ imports every command/group module (for
registration) and re-exports the full public + internal API, so
'from sponsio.cli import X' is unchanged. main() now lives in __init__.

Command set identical (23 top-level + subgroups); full suite 2296 passed;
python -m sponsio.cli and the console script both work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant