Skip to content

Commit e5b1a1a

Browse files
iamaeroplaneclaude
andcommitted
fix: address additional review feedback
- PresetResolver: add fallback to directory scanning when registry is empty/corrupted for robustness and backwards compatibility - PresetRegistry.update(): add guard to prevent injecting installed_at when absent in existing entry (mirrors ExtensionRegistry behavior) - RFC: update extension list example to match actual CLI output format Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9f12188 commit e5b1a1a

2 files changed

Lines changed: 73 additions & 30 deletions

File tree

extensions/RFC-EXTENSION-SYSTEM.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,11 +1087,15 @@ List installed extensions in current project.
10871087
$ specify extension list
10881088
10891089
Installed Extensions:
1090-
✓ jira (v1.0.0) - Jira Integration
1091-
Commands: 3 | Hooks: 2 | Priority: 10 | Status: Enabled
1092-
1093-
✓ linear (v0.9.0) - Linear Integration
1094-
Commands: 1 | Hooks: 1 | Priority: 10 | Status: Enabled
1090+
✓ Jira Integration (v1.0.0)
1091+
jira
1092+
Create Jira issues from spec-kit artifacts
1093+
Commands: 3 | Hooks: 2 | Priority: 10 | Status: Enabled
1094+
1095+
✓ Linear Integration (v0.9.0)
1096+
linear
1097+
Create Linear issues from spec-kit artifacts
1098+
Commands: 1 | Hooks: 1 | Priority: 10 | Status: Enabled
10951099
```
10961100

10971101
**Options:**

src/specify_cli/presets.py

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -292,9 +292,13 @@ def update(self, pack_id: str, updates: dict):
292292
existing = self.data["presets"][pack_id]
293293
# Merge: existing fields preserved, new fields override
294294
merged = {**existing, **updates}
295-
# Always preserve original installed_at
295+
# Always preserve original installed_at based on key existence, not truthiness,
296+
# to handle cases where the field exists but may be falsy (legacy/corruption)
296297
if "installed_at" in existing:
297298
merged["installed_at"] = existing["installed_at"]
299+
else:
300+
# If not present in existing, explicitly remove from merged if caller provided it
301+
merged.pop("installed_at", None)
298302
self.data["presets"][pack_id] = merged
299303
self._save()
300304

@@ -1488,17 +1492,35 @@ def resolve(
14881492
# Priority 3: Extension-provided templates (sorted by priority — lower number wins)
14891493
if self.extensions_dir.exists():
14901494
registry = ExtensionRegistry(self.extensions_dir)
1491-
for ext_id, _metadata in registry.list_by_priority():
1492-
ext_dir = self.extensions_dir / ext_id
1493-
if not ext_dir.is_dir():
1494-
continue
1495-
for subdir in subdirs:
1496-
if subdir:
1497-
candidate = ext_dir / subdir / f"{template_name}{ext}"
1498-
else:
1499-
candidate = ext_dir / f"{template_name}{ext}"
1500-
if candidate.exists():
1501-
return candidate
1495+
registered_extensions = registry.list_by_priority()
1496+
1497+
# If registry is empty but extension directories exist, fall back to
1498+
# directory scanning for robustness (handles corrupted/missing registry)
1499+
if not registered_extensions:
1500+
# Scan directories alphabetically with implicit priority=10
1501+
for ext_dir in sorted(self.extensions_dir.iterdir()):
1502+
if not ext_dir.is_dir() or ext_dir.name.startswith("."):
1503+
continue
1504+
for subdir in subdirs:
1505+
if subdir:
1506+
candidate = ext_dir / subdir / f"{template_name}{ext}"
1507+
else:
1508+
candidate = ext_dir / f"{template_name}{ext}"
1509+
if candidate.exists():
1510+
return candidate
1511+
else:
1512+
# Use registry-based resolution with priority ordering
1513+
for ext_id, _metadata in registered_extensions:
1514+
ext_dir = self.extensions_dir / ext_id
1515+
if not ext_dir.is_dir():
1516+
continue
1517+
for subdir in subdirs:
1518+
if subdir:
1519+
candidate = ext_dir / subdir / f"{template_name}{ext}"
1520+
else:
1521+
candidate = ext_dir / f"{template_name}{ext}"
1522+
if candidate.exists():
1523+
return candidate
15021524

15031525
# Priority 4: Core templates
15041526
if template_type == "template":
@@ -1558,18 +1580,35 @@ def resolve_with_source(
15581580

15591581
if self.extensions_dir.exists():
15601582
ext_registry = ExtensionRegistry(self.extensions_dir)
1561-
for ext_id, ext_meta in ext_registry.list_by_priority():
1562-
ext_dir = self.extensions_dir / ext_id
1563-
if not ext_dir.is_dir():
1564-
continue
1565-
try:
1566-
resolved.relative_to(ext_dir)
1567-
version = ext_meta.get("version", "?") if ext_meta else "?"
1568-
return {
1569-
"path": resolved_str,
1570-
"source": f"extension:{ext_id} v{version}",
1571-
}
1572-
except ValueError:
1573-
continue
1583+
registered_extensions = ext_registry.list_by_priority()
1584+
1585+
if registered_extensions:
1586+
# Use registry-based attribution
1587+
for ext_id, ext_meta in registered_extensions:
1588+
ext_dir = self.extensions_dir / ext_id
1589+
if not ext_dir.is_dir():
1590+
continue
1591+
try:
1592+
resolved.relative_to(ext_dir)
1593+
version = ext_meta.get("version", "?") if ext_meta else "?"
1594+
return {
1595+
"path": resolved_str,
1596+
"source": f"extension:{ext_id} v{version}",
1597+
}
1598+
except ValueError:
1599+
continue
1600+
else:
1601+
# Fallback: scan directories when registry is empty/corrupted
1602+
for ext_dir in sorted(self.extensions_dir.iterdir()):
1603+
if not ext_dir.is_dir() or ext_dir.name.startswith("."):
1604+
continue
1605+
try:
1606+
resolved.relative_to(ext_dir)
1607+
return {
1608+
"path": resolved_str,
1609+
"source": f"extension:{ext_dir.name} (unregistered)",
1610+
}
1611+
except ValueError:
1612+
continue
15741613

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

0 commit comments

Comments
 (0)