fix(extensions): register on enable only when no output exists, and u… · github/spec-kit@629224f · GitHub
Skip to content

Commit 629224f

Browse files
yxsheeclaude
andcommitted
fix(extensions): register on enable only when no output exists, and undo failures
Round-2 review of the enable repair: - Check every output registration writes for the active layout, under the directory registration itself resolves: home-relative (Hermes ~/.hermes/skills) and legacy (OpenCode/Kilo) directories, aliases, and Copilot's companion .prompt.md, not only the primary name. - An unusable skills directory now raises, so enable restores the disabled state instead of reporting success without skills. - Register only when none of the extension's outputs exist, the state an integration switch leaves behind. Registration rewrites every command, so repairing one missing output could discard edits or dev symlinks on the others; partially present outputs are left untouched. - Because every output was absent beforehand, a failed attempt removes the outputs it wrote and restores the registry tracking, so a retry registers the full set instead of exiting as already complete. Assisted-by: Claude Code (model: Claude Opus 5.5, autonomous) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 891bacc commit 629224f

3 files changed

Lines changed: 212 additions & 41 deletions

File tree

‎src/specify_cli/extensions/__init__.py‎

Lines changed: 37 additions & 26 deletions

‎src/specify_cli/extensions/command_enable.py‎

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,42 @@
1212
from . import _commands
1313

1414

15+
def _present(path) -> bool:
16+
return path.exists() or path.is_symlink()
17+
18+
19+
def _register_if_absent(manager, extension_id: str, agent: str, metadata: dict) -> None:
20+
"""Register ``extension_id`` for ``agent`` when none of its outputs exist.
21+
22+
Surviving outputs (customized files, dev symlinks) are never rewritten.
23+
Every output was absent before registration, so if it leaves any missing
24+
or raises, the outputs it did write and its registry tracking are undone,
25+
and a retry starts from the same clean state.
26+
"""
27+
from . import ExtensionError
28+
29+
outputs = manager.invocation_outputs(extension_id, agent)
30+
if not outputs or any(_present(path) for _, path in outputs):
31+
return
32+
try:
33+
manager.register_enabled_extensions_for_agent(agent, extension_ids={extension_id})
34+
missing = sorted(name for name, path in outputs if not _present(path))
35+
if missing:
36+
raise ExtensionError("Missing invocation artifacts: " + ", ".join(missing))
37+
except Exception:
38+
for _, path in outputs:
39+
if path.is_symlink() or path.is_file():
40+
path.unlink()
41+
if path.name == "SKILL.md" and path.parent.is_dir() and not any(path.parent.iterdir()):
42+
path.parent.rmdir()
43+
manager.registry.update(extension_id, {
44+
key: metadata[key]
45+
for key in ("registered_commands", "registered_skills")
46+
if key in metadata
47+
})
48+
raise
49+
50+
1551
@_commands.extension_app.command("enable")
1652
def extension_enable(
1753
extension: str = typer.Argument(help="Extension ID or name to enable"),
@@ -85,24 +121,14 @@ def extension_enable(
85121
# `integration use`/`switch` register only enabled extensions for the
86122
# agent they activate, and `switch` removes a disabled extension's
87123
# commands from the old one, so an extension disabled at that point has
88-
# no commands for the now-active agent. Register them only when files
89-
# are actually missing on disk, so existing files and dev symlinks are
90-
# left untouched, and roll back if registration does not produce them.
124+
# no commands for the now-active agent. Register them only when none
125+
# of the extension's outputs exist for the active layout, so surviving
126+
# files, edits and dev symlinks are never rewritten, and roll back if
127+
# registration leaves any output missing.
91128
active_ai = init_options.get("ai")
92129
if isinstance(active_ai, str) and active_ai:
93130
try:
94-
if manager.missing_invocation_artifacts(extension_id, active_ai):
95-
manager.register_enabled_extensions_for_agent(
96-
active_ai, extension_ids={extension_id}
97-
)
98-
missing = manager.missing_invocation_artifacts(
99-
extension_id, active_ai
100-
)
101-
if missing:
102-
raise ExtensionError(
103-
"Missing invocation artifacts: "
104-
+ ", ".join(sorted(missing))
105-
)
131+
_register_if_absent(manager, extension_id, active_ai, metadata)
106132
except Exception as exc:
107133
manager.registry.update(extension_id, {"enabled": False})
108134
console.print(

‎tests/specify_cli/extensions/test_command_enable.py‎

Lines changed: 134 additions & 0 deletions

0 commit comments

Comments
 (0)