Skip to content

Commit da6e7d2

Browse files
committed
fix: address Copilot PR review comments (round 2)
- Fix init --preset error masking: distinguish "not found" from real errors - Fix bash resolve_template: skip hidden dirs in extensions (match Python/PS) - Fix temp dir leaks in tests: use temp_dir fixture instead of mkdtemp - Fix self-test catalog entry: add note that it's local-only (no download_url) - Fix Windows path issue in resolve_with_source: use Path.relative_to() - Fix skill restore path: use project's .specify/templates/commands/ not source tree - Add encoding="utf-8" to all file read/write in agents.py - Update test to set up core command templates for skill restoration
1 parent 3ffef55 commit da6e7d2

6 files changed

Lines changed: 45 additions & 25 deletions

File tree

presets/catalog.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
"presets": {
66
"self-test": {
77
"name": "Self-Test Preset",
8-
"description": "A preset that overrides all core templates for testing purposes",
8+
"description": "A preset that overrides all core templates for testing purposes. Install with --dev from the repo.",
99
"author": "github",
1010
"version": "1.0.0",
1111
"repository": "https://github.com/github/spec-kit",
1212
"license": "MIT",
13+
"note": "This preset is bundled with the repo and intended for local testing only. Install via: specify preset add --dev presets/self-test",
1314
"requires": {
1415
"speckit_version": ">=0.1.0"
1516
},

scripts/bash/common.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ except Exception:
213213
if [ -d "$ext_dir" ]; then
214214
for ext in "$ext_dir"/*/; do
215215
[ -d "$ext" ] || continue
216+
# Skip hidden directories (e.g. .backup, .cache)
217+
case "$(basename "$ext")" in .*) continue;; esac
216218
local candidate="$ext/templates/${template_name}.md"
217219
[ -f "$candidate" ] && echo "$candidate" && return 0
218220
done

src/specify_cli/__init__.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1600,17 +1600,21 @@ def init(
16001600
preset_manager.install_from_directory(local_path, speckit_ver)
16011601
else:
16021602
preset_catalog = PresetCatalog(project_path)
1603-
try:
1604-
zip_path = preset_catalog.download_pack(preset)
1605-
preset_manager.install_from_zip(zip_path, speckit_ver)
1606-
# Clean up downloaded ZIP to avoid cache accumulation
1607-
try:
1608-
zip_path.unlink(missing_ok=True)
1609-
except OSError:
1610-
# Best-effort cleanup; failure to delete is non-fatal
1611-
pass
1612-
except PresetError:
1603+
pack_info = preset_catalog.get_pack_info(preset)
1604+
if not pack_info:
16131605
console.print(f"[yellow]Warning:[/yellow] Preset '{preset}' not found in catalog. Skipping.")
1606+
else:
1607+
try:
1608+
zip_path = preset_catalog.download_pack(preset)
1609+
preset_manager.install_from_zip(zip_path, speckit_ver)
1610+
# Clean up downloaded ZIP to avoid cache accumulation
1611+
try:
1612+
zip_path.unlink(missing_ok=True)
1613+
except OSError:
1614+
# Best-effort cleanup; failure to delete is non-fatal
1615+
pass
1616+
except PresetError as preset_err:
1617+
console.print(f"[yellow]Warning:[/yellow] Failed to install preset '{preset}': {preset_err}")
16141618
except Exception as preset_err:
16151619
console.print(f"[yellow]Warning:[/yellow] Failed to install preset: {preset_err}")
16161620

src/specify_cli/agents.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ def register_commands(
301301
if not source_file.exists():
302302
continue
303303

304-
content = source_file.read_text()
304+
content = source_file.read_text(encoding="utf-8")
305305
frontmatter, body = self.parse_frontmatter(content)
306306

307307
frontmatter = self._adjust_script_paths(frontmatter)
@@ -318,7 +318,7 @@ def register_commands(
318318
raise ValueError(f"Unsupported format: {agent_config['format']}")
319319

320320
dest_file = commands_dir / f"{cmd_name}{agent_config['extension']}"
321-
dest_file.write_text(output)
321+
dest_file.write_text(output, encoding="utf-8")
322322

323323
if agent_name == "copilot":
324324
self.write_copilot_prompt(project_root, cmd_name)
@@ -327,7 +327,7 @@ def register_commands(
327327

328328
for alias in cmd_info.get("aliases", []):
329329
alias_file = commands_dir / f"{alias}{agent_config['extension']}"
330-
alias_file.write_text(output)
330+
alias_file.write_text(output, encoding="utf-8")
331331
if agent_name == "copilot":
332332
self.write_copilot_prompt(project_root, alias)
333333
registered.append(alias)
@@ -345,7 +345,7 @@ def write_copilot_prompt(project_root: Path, cmd_name: str) -> None:
345345
prompts_dir = project_root / ".github" / "prompts"
346346
prompts_dir.mkdir(parents=True, exist_ok=True)
347347
prompt_file = prompts_dir / f"{cmd_name}.prompt.md"
348-
prompt_file.write_text(f"---\nagent: {cmd_name}\n---\n")
348+
prompt_file.write_text(f"---\nagent: {cmd_name}\n---\n", encoding="utf-8")
349349

350350
def register_commands_for_all_agents(
351351
self,

src/specify_cli/presets.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -565,9 +565,8 @@ def _unregister_skills(self, skill_names: List[str], preset_dir: Path) -> None:
565565

566566
from . import SKILL_DESCRIPTIONS
567567

568-
# Locate core command templates
569-
script_dir = Path(__file__).parent.parent.parent # up from src/specify_cli/
570-
core_templates_dir = script_dir / "templates" / "commands"
568+
# Locate core command templates from the project's installed templates
569+
core_templates_dir = self.project_root / ".specify" / "templates" / "commands"
571570

572571
for skill_name in skill_names:
573572
# Derive command name from skill name (speckit-specify -> specify)
@@ -1458,20 +1457,29 @@ def resolve_with_source(
14581457
if str(self.presets_dir) in resolved_str and self.presets_dir.exists():
14591458
registry = PresetRegistry(self.presets_dir)
14601459
for pack_id, _metadata in registry.list_by_priority():
1461-
if f"/{pack_id}/" in resolved_str:
1460+
pack_dir = self.presets_dir / pack_id
1461+
try:
1462+
resolved.relative_to(pack_dir)
14621463
meta = registry.get(pack_id)
14631464
version = meta.get("version", "?") if meta else "?"
14641465
return {
14651466
"path": resolved_str,
14661467
"source": f"{pack_id} v{version}",
14671468
}
1469+
except ValueError:
1470+
continue
14681471

1469-
if str(self.extensions_dir) in resolved_str and self.extensions_dir.exists():
1472+
if self.extensions_dir.exists():
14701473
for ext_dir in sorted(self.extensions_dir.iterdir()):
1471-
if ext_dir.is_dir() and f"/{ext_dir.name}/" in resolved_str:
1474+
if not ext_dir.is_dir() or ext_dir.name.startswith("."):
1475+
continue
1476+
try:
1477+
resolved.relative_to(ext_dir)
14721478
return {
14731479
"path": resolved_str,
14741480
"source": f"extension:{ext_dir.name}",
14751481
}
1482+
except ValueError:
1483+
continue
14761484

14771485
return {"path": resolved_str, "source": "core"}

tests/test_presets.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -499,15 +499,15 @@ def test_get_pack_not_installed(self, project_dir):
499499
manager = PresetManager(project_dir)
500500
assert manager.get_pack("nonexistent") is None
501501

502-
def test_check_compatibility_valid(self, pack_dir):
502+
def test_check_compatibility_valid(self, pack_dir, temp_dir):
503503
"""Test compatibility check with valid version."""
504-
manager = PresetManager(Path(tempfile.mkdtemp()))
504+
manager = PresetManager(temp_dir)
505505
manifest = PresetManifest(pack_dir / "preset.yml")
506506
assert manager.check_compatibility(manifest, "0.1.5") is True
507507

508-
def test_check_compatibility_invalid(self, pack_dir):
508+
def test_check_compatibility_invalid(self, pack_dir, temp_dir):
509509
"""Test compatibility check with invalid specifier."""
510-
manager = PresetManager(Path(tempfile.mkdtemp()))
510+
manager = PresetManager(temp_dir)
511511
manifest = PresetManifest(pack_dir / "preset.yml")
512512
manifest.data["requires"]["speckit_version"] = "not-a-specifier"
513513
with pytest.raises(PresetCompatibilityError, match="Invalid version specifier"):
@@ -1678,6 +1678,11 @@ def test_skill_restored_on_preset_remove(self, project_dir, temp_dir):
16781678

16791679
(project_dir / ".claude" / "commands").mkdir(parents=True, exist_ok=True)
16801680

1681+
# Set up core command template in the project so restoration works
1682+
core_cmds = project_dir / ".specify" / "templates" / "commands"
1683+
core_cmds.mkdir(parents=True, exist_ok=True)
1684+
(core_cmds / "specify.md").write_text("---\ndescription: Core specify command\n---\n\nCore specify body\n")
1685+
16811686
manager = PresetManager(project_dir)
16821687
SELF_TEST_DIR = Path(__file__).parent.parent / "presets" / "self-test"
16831688
manager.install_from_directory(SELF_TEST_DIR, "0.1.5")

0 commit comments

Comments
 (0)