Skip to content

Commit a01e05f

Browse files
author
root
committed
fix: constrain skill slash ref rewrites to manifest names
1 parent 749ecdf commit a01e05f

2 files changed

Lines changed: 76 additions & 9 deletions

File tree

src/specify_cli/extensions/__init__.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1608,24 +1608,30 @@ def _replacement(match: re.Match[str]) -> str:
16081608
def _normalize_literal_slash_command_refs(body: str) -> str:
16091609
"""Normalize literal /speckit.foo refs in generated skill bodies."""
16101610

1611+
known_command_names = {
1612+
cmd["name"]
1613+
for cmd in manifest.commands
1614+
if isinstance(cmd.get("name"), str)
1615+
}
1616+
for cmd in manifest.commands:
1617+
aliases = cmd.get("aliases", [])
1618+
if not isinstance(aliases, list):
1619+
continue
1620+
known_command_names.update(
1621+
alias for alias in aliases if isinstance(alias, str)
1622+
)
1623+
16111624
def _replacement(match: re.Match[str]) -> str:
16121625
command_name = match.group("command")
1613-
if command_name.rsplit(".", 1)[-1] in {
1614-
"json",
1615-
"md",
1616-
"toml",
1617-
"txt",
1618-
"yaml",
1619-
"yml",
1620-
}:
1626+
if command_name not in known_command_names:
16211627
return match.group(0)
16221628
return _render_skill_command_invocation(command_name)
16231629

16241630
return re.sub(
16251631
(
16261632
r"(?<![\w$:/-])"
16271633
r"/(?P<command>speckit\.[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+)"
1628-
r"(?![A-Za-z0-9_.-])"
1634+
r"(?!/)"
16291635
),
16301636
_replacement,
16311637
body,

tests/test_extension_skills.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,6 +1202,10 @@ def test_skill_registration_rewrites_literal_slash_command_refs(
12021202
"name": "speckit.literal-ref-ext.run",
12031203
"file": "commands/run.md",
12041204
"description": "Run command",
1205+
"aliases": [
1206+
"speckit.foo.bar",
1207+
"speckit.export.json",
1208+
],
12051209
}
12061210
]
12071211
},
@@ -1215,6 +1219,9 @@ def test_skill_registration_rewrites_literal_slash_command_refs(
12151219
"description: Run command\n"
12161220
"---\n\n"
12171221
"Literal slash form: /speckit.foo.bar --flag value\n"
1222+
"Valid suffix form: /speckit.export.json\n"
1223+
"Path continuation form: /speckit.foo.bar/scripts/run.sh\n"
1224+
"Sentence punctuation form: Run /speckit.foo.bar.\n"
12181225
"Native slash form: /speckit-foo-bar\n"
12191226
"Native dollar form: $speckit-foo-bar\n"
12201227
"Native skill form: /skill:speckit-foo-bar\n"
@@ -1227,15 +1234,69 @@ def test_skill_registration_rewrites_literal_slash_command_refs(
12271234
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
12281235

12291236
content = (skills_dir / "speckit-literal-ref-ext-run" / "SKILL.md").read_text()
1237+
expected_json_invocation = expected_invocation.replace("foo-bar", "export-json")
12301238
assert f"Literal slash form: {expected_invocation} --flag value" in content
12311239
assert "Literal slash form: /speckit.foo.bar --flag value" not in content
1240+
assert f"Valid suffix form: {expected_json_invocation}" in content
1241+
assert "Valid suffix form: /speckit.export.json" not in content
1242+
assert "Path continuation form: /speckit.foo.bar/scripts/run.sh" in content
1243+
assert f"Sentence punctuation form: Run {expected_invocation}." in content
12321244
assert "Native slash form: /speckit-foo-bar" in content
12331245
assert "Native dollar form: $speckit-foo-bar" in content
12341246
assert "Native skill form: /skill:speckit-foo-bar" in content
12351247
assert "speckit.foo.bar" in content
12361248
assert "https://example.com/speckit.foo.bar" in content
12371249
assert "/speckit.foo.bar.md" in content
12381250

1251+
def test_skill_registration_rewrites_multi_segment_alias_with_punctuation(
1252+
self, project_dir, temp_dir
1253+
):
1254+
"""Aliases can be free-form safe names with multiple dotted segments."""
1255+
_create_init_options(project_dir, ai="claude", ai_skills=True)
1256+
skills_dir = _create_skills_dir(project_dir, ai="claude")
1257+
1258+
ext_dir = temp_dir / "multi-segment-alias-ext"
1259+
ext_dir.mkdir()
1260+
manifest_data = {
1261+
"schema_version": "1.0",
1262+
"extension": {
1263+
"id": "multi-segment-alias-ext",
1264+
"name": "Multi Segment Alias Extension",
1265+
"version": "1.0.0",
1266+
"description": "Test",
1267+
},
1268+
"requires": {"speckit_version": ">=0.1.0"},
1269+
"provides": {
1270+
"commands": [
1271+
{
1272+
"name": "speckit.multi-segment-alias-ext.run",
1273+
"file": "commands/run.md",
1274+
"description": "Run command",
1275+
"aliases": ["speckit.foo.bar.baz"],
1276+
}
1277+
]
1278+
},
1279+
}
1280+
with open(ext_dir / "extension.yml", "w") as f:
1281+
yaml.safe_dump(manifest_data, f)
1282+
1283+
(ext_dir / "commands").mkdir()
1284+
(ext_dir / "commands" / "run.md").write_text(
1285+
"---\n"
1286+
"description: Run command\n"
1287+
"---\n\n"
1288+
"Sentence punctuation form: Run /speckit.foo.bar.baz.\n"
1289+
)
1290+
1291+
manager = ExtensionManager(project_dir)
1292+
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
1293+
1294+
content = (
1295+
skills_dir / "speckit-multi-segment-alias-ext-run" / "SKILL.md"
1296+
).read_text()
1297+
assert "Sentence punctuation form: Run /speckit-foo-bar-baz." in content
1298+
assert "/speckit.foo.bar.baz." not in content
1299+
12391300
def test_missing_command_file_skipped(self, skills_project, temp_dir):
12401301
"""Commands with missing source files should be skipped gracefully."""
12411302
project_dir, skills_dir = skills_project

0 commit comments

Comments
 (0)