refactor: move extension command handlers to extensions/_commands.py (PR-7/8)#3014
Conversation
5da1bab to
29be5d8
Compare
There was a problem hiding this comment.
Pull request overview
Refactors the Specify CLI by extracting specify extension * and specify extension catalog * command handlers out of the specify_cli/__init__.py monolith into a dedicated extensions/_commands.py module, while converting extensions from a flat module into a package to support the domain-dir layout.
Changes:
- Introduces
src/specify_cli/extensions/_commands.pycontaining the Typer apps (extension,catalog) and all related command handlers, plus aregister(app)entry point. - Converts
extensions.pyintoextensions/__init__.pyand updates intra-package imports accordingly. - Re-attaches the extension command group from
specify_cli/__init__.pyvia_register_extension_cmds(app)to preserve the CLI surface.
Show a summary per file
| File | Description |
|---|---|
| src/specify_cli/extensions/_commands.py | New home for extension / catalog Typer apps and all extension-related CLI handlers (registered via register(app)). |
| src/specify_cli/extensions/init.py | Converts extensions into a package and adjusts relative imports to continue referencing root-package siblings. |
| src/specify_cli/init.py | Removes inline extension command definitions and registers the new extensions command module to keep the same CLI entry points. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 3/3 changed files
- Comments generated: 3
mnriem
left a comment
There was a problem hiding this comment.
Please address Copilot feedback and resolve conflicts
50756f8 to
ddcc165
Compare
|
fixed |
|
Please address Copilot feedback |
ddcc165 to
efe0633
Compare
Heads-up on merge orderingI force-pushed this branch to remove the The extracted change now lives in its own PR, #3051, with test coverage. Merge order matters, because #3051 is stacked on this branch:
If #3051 is reviewed before this merges, please ignore the structural-move commit in its diff — it's this PR, and will disappear on rebase. |
efe0633 to
2d13763
Compare
|
Please address Copilot feedback and resolve conflicts by rebasing on upstream/main |
|
We urgently need a review tool. The review function provided by Copilot is too inefficient. |
19dfad7 to
9f9e9e6
Compare
|
Please address Copilot feedback |
|
Please address Copilot feedback. If not applicable, please explain why |
Getting entangled in trivial matters that don't matter at all and wasting time |
|
Please address Copilot feedback |
d080d5c to
6782384
Compare
…(PR-7/8) Convert the flat extensions.py module into an extensions/ package and extract all extension_app and catalog_app command handlers plus their private helpers (_resolve_installed_extension, _resolve_catalog_extension, _print_extension_info) out of __init__.py into the new extensions/_commands.py, mirroring the domain-dir layout used for presets/_commands.py (PR-6) and integrations/_commands.py (PR-5). - extensions.py -> extensions/__init__.py (pure rename, 99%); intra-module relative imports bumped from `.x` to `..x` since they reference root siblings. - Root helpers (_require_specify_project, _locate_bundled_extension, load_init_options, _display_project_path) are reached through thin shims that re-fetch from the parent package at call time, so test monkeypatching of specify_cli.<helper> keeps working unchanged. - __init__.py drops ~1444 lines (3511 -> 2067); CLI surface preserved via register(app). No behavior change. Full suite failure set is identical before/after (82 pre-existing env failures, 0 new).
…s agents Skills agents (extension == "/SKILL.md") name every command file SKILL.md, each in its own per-command subdir (e.g. speckit-plan/SKILL.md). The update backup keyed the backup path on cmd_file.name alone, so all of an agent's skill files collided onto a single backup path — each shutil.copy2 overwrote the previous one, and rollback restored one skill's content over all the others, corrupting or losing the rest. Mirror the real on-disk layout by using cmd_file.relative_to(commands_dir), keeping each backup path unique. This also makes backed_up_command_files values unique so restore copies the correct content back to each command. Add a regression test asserting two distinct skill files survive a backup -> failed-update -> rollback cycle with their own content.
The catalog add/remove handlers wrote the integration catalog config with yaml.dump. Switch to yaml.safe_dump to align with the SafeDumper used by the presets commands and to refuse emitting !!python/object tags if a non-basic value ever reaches the config dict. Output is unchanged for the current basic-type payload (str/int/bool/dict/ list) — this is a defensive/consistency change, not a behavioral fix.
…stration register_enabled_extensions_for_agent imported _print_cli_warning from `.` (the extensions package), but the helper lives in the parent specify_cli package. The wrong level raised ImportError inside the error handlers, aborting extension/skill registration on the first failure instead of warning and continuing. Use `..` to match the other parent-package imports.
User-provided arguments and extension/catalog metadata (names, descriptions, versions, IDs, paths) were interpolated into Rich markup strings without escaping. Values containing markup sequences (e.g. [red]...) would be parsed as markup, allowing output injection that could corrupt or mislead CLI messages. Wrap all such interpolations with rich.markup.escape across the extension/catalog command handlers: list, search, info (_print_extension_info), add (including --dev paths), remove, enable, disable, set-priority, update, and the ambiguous-match resolvers (error strings and Table rows). Reuse the already-computed safe_extension where available. Escaping is a no-op for benign strings, so normal output is unchanged.
User-controlled catalog URLs and extension IDs are rendered through Rich-enabled console paths, so every remaining output-only interpolation now escapes markup while leaving stored values and filesystem behavior unchanged. Regression tests cover catalog add, install hints, remove hints, and state command messages with bracketed markup-like values.
Rich markup remains enabled for styled CLI messages, so exception text and config path labels must be escaped before rendering. YAML parser errors, URL validation failures, download errors, and extension validation errors can include user-controlled catalog or manifest values. Constraint: Preserve existing exception handling and user-facing error paths Rejected: Disable Rich markup for these messages | existing output intentionally uses markup for labels and styling Confidence: high Scope-risk: narrow Directive: Escape user-controlled exception text before interpolating into Rich-rendered strings Tested: .venv/bin/python -m pytest tests/test_extensions.py -q Co-authored-by: OmX <omx@oh-my-codex.dev>
Catalog path labels are rendered through Rich markup and downloaded update manifests are trusted long enough to validate extension IDs. Escape displayed project paths before rendering, and reject non-mapping extension.yml payloads before ID validation so bad archives fail with a clear rollback reason.
6782384 to
3711804
Compare
|
Thank you! |
Summary
PR-7 of the 8-part effort to break up the
specify_cli/__init__.pymonolith. Continues the domain-dir layout established in PR-5 (integrations/_commands.py) and PR-6 (presets/_commands.py).This PR moves the
extension *andcatalog *command handlers out of__init__.py. Becauseextensionswas a flat module (unlike the already-packagepresets/integrations), it is first converted to a package.This is primarily a structural refactor. It also includes review follow-up fixes in the moved extension command code so the refactor branch stays current with reviewer feedback instead of carrying known issues forward.
Changes
extensions.py→extensions/__init__.py— pure rename (99% identical). The module’s intra-file relative imports are bumped from.xto..x, since they reference root-package siblings (.._init_options,..catalogs,..agents,..integrations, andfrom .. importforload_init_options,_print_cli_warning,AGENT_CONFIG,DEFAULT_SKILLS_DIR,_get_skills_dir).extensions/_commands.py— holdsextension_app,catalog_app, all 12 command handlers (list/add/remove/search/info/update/enable/disable/set-priority+ cataloglist/add/remove), the private helpers (_resolve_installed_extension,_resolve_catalog_extension,_print_extension_info), and aregister(app)entry point.__init__.pydrops ~1444 lines (3511 → 2067). The extension command group is re-attached viaregister(app), preserving the CLI surface.SKILL.mdrestore independently during rollback.extension add <name> --from <zip-url>now writes downloaded ZIPs to a generated tempfile under.specify/extensions/.cache/downloads/, so the extension argument cannot influence the ZIP path._print_cli_warningfrom the package root after the move, and catalog config writes useyaml.safe_dump.Monkeypatch compatibility
Root helpers (
_require_specify_project,_locate_bundled_extension,load_init_options,_display_project_path) are reached through thin shims in_commands.pythat re-fetch from the parent package at call time. This keeps existingspecify_cli.<helper>monkeypatch targets working with no test changes.Verification
extension/catalogcommand tree loads and all--helpsurfaces respond.tests/test_extension_update_hardening.pycovers corrupted update config rollback and the skillsSKILL.mdbackup collision regression.tests/test_extensions.pycovers Rich markup escaping and the generated tempfile path forextension add --fromdownloads.