Commit 826e193
refactor: move extension command handlers to extensions/_commands.py (PR-7/8) (#3014)
* refactor: move extension command handlers to extensions/_commands.py (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).
* fix(extensions): preserve per-command path in update backup for skills 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.
* style(extensions): use yaml.safe_dump when writing catalog config
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.
* fix(extensions): correct _print_cli_warning import path in skill registration
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.
* fix(extensions): escape untrusted values in Rich markup output
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.
* Prevent Rich markup injection in extension CLI output
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.
* Prevent markup injection from exception text
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>
* Prevent path and manifest review regressions
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.
---------
Co-authored-by: OmX <omx@oh-my-codex.dev>1 parent 6a3ee9b commit 826e193
6 files changed
Lines changed: 2246 additions & 1472 deletions
File tree
- src/specify_cli
- extensions
- tests
- integrations
0 commit comments