Skip to content

Commit 4aeaf5b

Browse files
committed
Avoid deleting unmanaged preset skill dirs
1 parent 734efdf commit 4aeaf5b

2 files changed

Lines changed: 73 additions & 5 deletions

File tree

src/specify_cli/presets.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,9 @@ def _unregister_skills(self, skill_names: List[str], preset_dir: Path) -> None:
817817
skill_file = skill_subdir / "SKILL.md"
818818
if not skill_subdir.is_dir():
819819
continue
820+
if not skill_file.is_file():
821+
# Only manage directories that contain the expected skill entrypoint.
822+
continue
820823

821824
# Try to find the core command template
822825
core_file = core_templates_dir / f"{short_name}.md" if core_templates_dir.exists() else None
@@ -1022,17 +1025,26 @@ def remove(self, pack_id: str) -> bool:
10221025
if not self.registry.is_installed(pack_id):
10231026
return False
10241027

1025-
# Unregister commands from AI agents
10261028
metadata = self.registry.get(pack_id)
1027-
registered_commands = metadata.get("registered_commands", {}) if metadata else {}
1028-
if registered_commands:
1029-
self._unregister_commands(registered_commands)
1030-
10311029
# Restore original skills when preset is removed
10321030
registered_skills = metadata.get("registered_skills", []) if metadata else []
1031+
registered_commands = metadata.get("registered_commands", {}) if metadata else {}
10331032
pack_dir = self.presets_dir / pack_id
10341033
if registered_skills:
10351034
self._unregister_skills(registered_skills, pack_dir)
1035+
try:
1036+
from . import NATIVE_SKILLS_AGENTS
1037+
except ImportError:
1038+
NATIVE_SKILLS_AGENTS = set()
1039+
registered_commands = {
1040+
agent_name: cmd_names
1041+
for agent_name, cmd_names in registered_commands.items()
1042+
if agent_name not in NATIVE_SKILLS_AGENTS
1043+
}
1044+
1045+
# Unregister non-skill command files from AI agents.
1046+
if registered_commands:
1047+
self._unregister_commands(registered_commands)
10361048

10371049
if pack_dir.exists():
10381050
shutil.rmtree(pack_dir)

tests/test_presets.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2249,6 +2249,62 @@ def test_extension_skill_restored_on_preset_remove(self, project_dir, temp_dir):
22492249
assert "extension:fakeext" in content
22502250
assert '.specify/scripts/bash/setup-plan.sh --json "$ARGUMENTS"' in content
22512251

2252+
def test_preset_remove_skips_skill_dir_without_skill_file(self, project_dir, temp_dir):
2253+
"""Preset removal should not delete arbitrary directories missing SKILL.md."""
2254+
self._write_init_options(project_dir, ai="codex")
2255+
skills_dir = project_dir / ".agents" / "skills"
2256+
stray_skill_dir = skills_dir / "speckit-fakeext-cmd"
2257+
stray_skill_dir.mkdir(parents=True, exist_ok=True)
2258+
note_file = stray_skill_dir / "notes.txt"
2259+
note_file.write_text("user content", encoding="utf-8")
2260+
2261+
preset_dir = temp_dir / "ext-skill-missing-file"
2262+
preset_dir.mkdir()
2263+
(preset_dir / "commands").mkdir()
2264+
(preset_dir / "commands" / "speckit.fakeext.cmd.md").write_text(
2265+
"---\ndescription: Override fakeext cmd\n---\n\npreset:ext-skill-missing-file\n"
2266+
)
2267+
preset_manifest = {
2268+
"schema_version": "1.0",
2269+
"preset": {
2270+
"id": "ext-skill-missing-file",
2271+
"name": "Ext Skill Missing File",
2272+
"version": "1.0.0",
2273+
"description": "Test",
2274+
},
2275+
"requires": {"speckit_version": ">=0.1.0"},
2276+
"provides": {
2277+
"templates": [
2278+
{
2279+
"type": "command",
2280+
"name": "speckit.fakeext.cmd",
2281+
"file": "commands/speckit.fakeext.cmd.md",
2282+
}
2283+
]
2284+
},
2285+
}
2286+
with open(preset_dir / "preset.yml", "w") as f:
2287+
yaml.dump(preset_manifest, f)
2288+
2289+
manager = PresetManager(project_dir)
2290+
installed_preset_dir = manager.presets_dir / "ext-skill-missing-file"
2291+
shutil.copytree(preset_dir, installed_preset_dir)
2292+
manager.registry.add(
2293+
"ext-skill-missing-file",
2294+
{
2295+
"version": "1.0.0",
2296+
"source": str(preset_dir),
2297+
"provides_templates": ["speckit.fakeext.cmd"],
2298+
"registered_skills": ["speckit-fakeext-cmd"],
2299+
"priority": 10,
2300+
},
2301+
)
2302+
2303+
manager.remove("ext-skill-missing-file")
2304+
2305+
assert stray_skill_dir.is_dir()
2306+
assert note_file.read_text(encoding="utf-8") == "user content"
2307+
22522308
def test_kimi_legacy_dotted_skill_override_still_applies(self, project_dir, temp_dir):
22532309
"""Preset overrides should still target legacy dotted Kimi skill directories."""
22542310
self._write_init_options(project_dir, ai="kimi")

0 commit comments

Comments
 (0)