Skip to content

Commit e6a3ccf

Browse files
fix(extensions): hyphenate command names in 'extension info' listing (#3744)
The 'Commands:' section of 'specify extension info' for a locally installed extension printed each command in its manifest dotted form (e.g. speckit.jira.sync). Cline and Forge register hyphenated command names (/speckit-jira-sync), so on those projects the displayed names did not match what the user actually invokes. Format each name through the active agent's command-name formatter, mirroring the parity 'extension add' already applies to its 'Provided commands' listing (#3669) and completing the Forge/Cline command-name parity from #3641/#3642. Adds a regression test asserting the hyphenated form appears (and the dotted form does not) for a Forge project.
1 parent 962f9f0 commit e6a3ccf

2 files changed

Lines changed: 75 additions & 1 deletion

File tree

‎src/specify_cli/extensions/_commands.py‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -893,9 +893,30 @@ def extension_info(
893893
console.print()
894894

895895
if ext_manifest.commands:
896+
# Print each command the way the active agent registers it.
897+
# Cline and Forge hyphenate command names (e.g. Forge invokes
898+
# `/speckit-jira-sync`, not the manifest's dotted
899+
# `speckit.jira.sync`), so mirror the same formatting used by
900+
# `extension add`'s "Provided commands" listing — otherwise the
901+
# names shown here don't match what the user actually types.
902+
selected_ai = load_init_options(project_root).get("ai")
903+
if selected_ai == "cline":
904+
from specify_cli.integrations.cline import (
905+
format_cline_command_name as _format_command_name,
906+
)
907+
elif selected_ai == "forge":
908+
from specify_cli.integrations.forge import (
909+
format_forge_command_name as _format_command_name,
910+
)
911+
else:
912+
_format_command_name = None
913+
896914
console.print("[bold]Commands:[/bold]")
897915
for cmd in ext_manifest.commands:
898-
console.print(f" • {_escape_markup(str(cmd['name']))}: {_escape_markup(str(cmd.get('description', '')))}")
916+
cmd_name = cmd['name']
917+
if _format_command_name is not None:
918+
cmd_name = _format_command_name(cmd_name)
919+
console.print(f" • {_escape_markup(str(cmd_name))}: {_escape_markup(str(cmd.get('description', '')))}")
899920
console.print()
900921

901922
# Show catalog status

‎tests/test_extensions.py‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9290,3 +9290,56 @@ def test_forge_extension_install_listing_hyphenates_command_names(
92909290
# Forge registers hyphenated command names, so the summary must match.
92919291
assert "speckit-test-ext-hello" in result.output
92929292
assert "speckit.test-ext.hello" not in result.output
9293+
9294+
9295+
def test_forge_extension_info_hyphenates_command_names(
9296+
extension_dir, project_dir, monkeypatch
9297+
):
9298+
"""`extension info` for an installed extension must show hyphenated
9299+
/speckit-<name> command names on a Forge project, matching the names Forge
9300+
actually registers — the same parity `extension add`'s listing already has.
9301+
"""
9302+
import io
9303+
import json
9304+
import os
9305+
9306+
from rich.console import Console
9307+
9308+
from specify_cli.extensions import _commands
9309+
9310+
init_options = project_dir / ".specify" / "init-options.json"
9311+
init_options.write_text(json.dumps({"ai": "forge", "script": "sh"}))
9312+
9313+
manager = ExtensionManager(project_dir)
9314+
manager.install_from_directory(
9315+
extension_dir, "1.0.0", register_commands=False
9316+
)
9317+
9318+
# Force the "installed locally, not in catalog" branch (the one that prints
9319+
# the local manifest's Commands section) and avoid any network catalog
9320+
# lookup.
9321+
monkeypatch.setattr(
9322+
_commands, "_resolve_catalog_extension", lambda *a, **k: (None, None)
9323+
)
9324+
9325+
# Call the handler directly against a plain captured Console. (Driving it
9326+
# through CliRunner reformats output via Rich's live console, which
9327+
# recurses under pytest's captured stdout — unrelated to this code path.)
9328+
buf = io.StringIO()
9329+
original_console = _commands.console
9330+
_commands.console = Console(file=buf, force_terminal=False, width=200)
9331+
old_cwd = os.getcwd()
9332+
try:
9333+
os.chdir(project_dir)
9334+
_commands.extension_info("test-ext")
9335+
except SystemExit:
9336+
pass
9337+
finally:
9338+
os.chdir(old_cwd)
9339+
_commands.console = original_console
9340+
9341+
output = buf.getvalue()
9342+
# The Commands section must render the hyphenated form Forge registers,
9343+
# not the manifest's dotted name.
9344+
assert "speckit-test-ext-hello" in output, output
9345+
assert "speckit.test-ext.hello" not in output, output

0 commit comments

Comments
 (0)