mirror of
https://github.com/github/spec-kit.git
synced 2026-03-16 18:33:07 +00:00
fix: correct Copilot extension command registration (#1724)
* fix: correct Copilot extension command registration (#copilot) - Use .agent.md extension for commands in .github/agents/ - Generate companion .prompt.md files in .github/prompts/ - Clean up .prompt.md files on extension removal - Add tests for Copilot-specific registration behavior Bumps version to 0.1.7. * fix: test copilot prompt cleanup via ExtensionManager.remove() instead of manual unlink --------- Co-authored-by: Ismael <ismael.jimenez-martinez@bmw.de>
This commit is contained in:
@@ -7,6 +7,15 @@ Recent changes to the Specify CLI and templates are documented here.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.1.13] - 2026-03-03
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Copilot Extension Commands Not Visible**: Fixed extension commands not appearing in GitHub Copilot when installed via `specify extension add --dev`
|
||||||
|
- Changed Copilot file extension from `.md` to `.agent.md` in `CommandRegistrar.AGENT_CONFIGS` so Copilot recognizes agent files
|
||||||
|
- Added generation of companion `.prompt.md` files in `.github/prompts/` during extension command registration, matching the release packaging behavior
|
||||||
|
- Added cleanup of `.prompt.md` companion files when removing extensions via `specify extension remove`
|
||||||
|
|
||||||
## [0.1.12] - 2026-03-02
|
## [0.1.12] - 2026-03-02
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "specify-cli"
|
name = "specify-cli"
|
||||||
version = "0.1.12"
|
version = "0.1.13"
|
||||||
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -455,6 +455,12 @@ class ExtensionManager:
|
|||||||
if cmd_file.exists():
|
if cmd_file.exists():
|
||||||
cmd_file.unlink()
|
cmd_file.unlink()
|
||||||
|
|
||||||
|
# Also remove companion .prompt.md for Copilot
|
||||||
|
if agent_name == "copilot":
|
||||||
|
prompt_file = self.project_root / ".github" / "prompts" / f"{cmd_name}.prompt.md"
|
||||||
|
if prompt_file.exists():
|
||||||
|
prompt_file.unlink()
|
||||||
|
|
||||||
if keep_config:
|
if keep_config:
|
||||||
# Preserve config files, only remove non-config files
|
# Preserve config files, only remove non-config files
|
||||||
if extension_dir.exists():
|
if extension_dir.exists():
|
||||||
@@ -597,7 +603,7 @@ class CommandRegistrar:
|
|||||||
"dir": ".github/agents",
|
"dir": ".github/agents",
|
||||||
"format": "markdown",
|
"format": "markdown",
|
||||||
"args": "$ARGUMENTS",
|
"args": "$ARGUMENTS",
|
||||||
"extension": ".md"
|
"extension": ".agent.md"
|
||||||
},
|
},
|
||||||
"cursor": {
|
"cursor": {
|
||||||
"dir": ".cursor/commands",
|
"dir": ".cursor/commands",
|
||||||
@@ -871,16 +877,40 @@ class CommandRegistrar:
|
|||||||
dest_file = commands_dir / f"{cmd_name}{agent_config['extension']}"
|
dest_file = commands_dir / f"{cmd_name}{agent_config['extension']}"
|
||||||
dest_file.write_text(output)
|
dest_file.write_text(output)
|
||||||
|
|
||||||
|
# Generate companion .prompt.md for Copilot agents
|
||||||
|
if agent_name == "copilot":
|
||||||
|
self._write_copilot_prompt(project_root, cmd_name)
|
||||||
|
|
||||||
registered.append(cmd_name)
|
registered.append(cmd_name)
|
||||||
|
|
||||||
# Register aliases
|
# Register aliases
|
||||||
for alias in cmd_info.get("aliases", []):
|
for alias in cmd_info.get("aliases", []):
|
||||||
alias_file = commands_dir / f"{alias}{agent_config['extension']}"
|
alias_file = commands_dir / f"{alias}{agent_config['extension']}"
|
||||||
alias_file.write_text(output)
|
alias_file.write_text(output)
|
||||||
|
# Generate companion .prompt.md for alias too
|
||||||
|
if agent_name == "copilot":
|
||||||
|
self._write_copilot_prompt(project_root, alias)
|
||||||
registered.append(alias)
|
registered.append(alias)
|
||||||
|
|
||||||
return registered
|
return registered
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _write_copilot_prompt(project_root: Path, cmd_name: str) -> None:
|
||||||
|
"""Generate a companion .prompt.md file for a Copilot agent command.
|
||||||
|
|
||||||
|
Copilot requires a .prompt.md file in .github/prompts/ that references
|
||||||
|
the corresponding .agent.md file in .github/agents/ via an ``agent:``
|
||||||
|
frontmatter field.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_root: Path to project root
|
||||||
|
cmd_name: Command name (used as the file stem, e.g. 'speckit.my-ext.example')
|
||||||
|
"""
|
||||||
|
prompts_dir = project_root / ".github" / "prompts"
|
||||||
|
prompts_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
prompt_file = prompts_dir / f"{cmd_name}.prompt.md"
|
||||||
|
prompt_file.write_text(f"---\nagent: {cmd_name}\n---\n")
|
||||||
|
|
||||||
def register_commands_for_all_agents(
|
def register_commands_for_all_agents(
|
||||||
self,
|
self,
|
||||||
manifest: ExtensionManifest,
|
manifest: ExtensionManifest,
|
||||||
|
|||||||
@@ -520,6 +520,121 @@ $ARGUMENTS
|
|||||||
assert (claude_dir / "speckit.alias.cmd.md").exists()
|
assert (claude_dir / "speckit.alias.cmd.md").exists()
|
||||||
assert (claude_dir / "speckit.shortcut.md").exists()
|
assert (claude_dir / "speckit.shortcut.md").exists()
|
||||||
|
|
||||||
|
def test_register_commands_for_copilot(self, extension_dir, project_dir):
|
||||||
|
"""Test registering commands for Copilot agent with .agent.md extension."""
|
||||||
|
# Create .github/agents directory (Copilot project)
|
||||||
|
agents_dir = project_dir / ".github" / "agents"
|
||||||
|
agents_dir.mkdir(parents=True)
|
||||||
|
|
||||||
|
manifest = ExtensionManifest(extension_dir / "extension.yml")
|
||||||
|
|
||||||
|
registrar = CommandRegistrar()
|
||||||
|
registered = registrar.register_commands_for_agent(
|
||||||
|
"copilot", manifest, extension_dir, project_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(registered) == 1
|
||||||
|
assert "speckit.test.hello" in registered
|
||||||
|
|
||||||
|
# Verify command file uses .agent.md extension
|
||||||
|
cmd_file = agents_dir / "speckit.test.hello.agent.md"
|
||||||
|
assert cmd_file.exists()
|
||||||
|
|
||||||
|
# Verify NO plain .md file was created
|
||||||
|
plain_md_file = agents_dir / "speckit.test.hello.md"
|
||||||
|
assert not plain_md_file.exists()
|
||||||
|
|
||||||
|
content = cmd_file.read_text()
|
||||||
|
assert "description: Test hello command" in content
|
||||||
|
assert "<!-- Extension: test-ext -->" in content
|
||||||
|
|
||||||
|
def test_copilot_companion_prompt_created(self, extension_dir, project_dir):
|
||||||
|
"""Test that companion .prompt.md files are created in .github/prompts/."""
|
||||||
|
agents_dir = project_dir / ".github" / "agents"
|
||||||
|
agents_dir.mkdir(parents=True)
|
||||||
|
|
||||||
|
manifest = ExtensionManifest(extension_dir / "extension.yml")
|
||||||
|
|
||||||
|
registrar = CommandRegistrar()
|
||||||
|
registrar.register_commands_for_agent(
|
||||||
|
"copilot", manifest, extension_dir, project_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify companion .prompt.md file exists
|
||||||
|
prompt_file = project_dir / ".github" / "prompts" / "speckit.test.hello.prompt.md"
|
||||||
|
assert prompt_file.exists()
|
||||||
|
|
||||||
|
# Verify content has correct agent frontmatter
|
||||||
|
content = prompt_file.read_text()
|
||||||
|
assert content == "---\nagent: speckit.test.hello\n---\n"
|
||||||
|
|
||||||
|
def test_copilot_aliases_get_companion_prompts(self, project_dir, temp_dir):
|
||||||
|
"""Test that aliases also get companion .prompt.md files for Copilot."""
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
ext_dir = temp_dir / "ext-alias-copilot"
|
||||||
|
ext_dir.mkdir()
|
||||||
|
|
||||||
|
manifest_data = {
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"extension": {
|
||||||
|
"id": "ext-alias-copilot",
|
||||||
|
"name": "Extension with Alias",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Test",
|
||||||
|
},
|
||||||
|
"requires": {"speckit_version": ">=0.1.0"},
|
||||||
|
"provides": {
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"name": "speckit.alias-copilot.cmd",
|
||||||
|
"file": "commands/cmd.md",
|
||||||
|
"aliases": ["speckit.shortcut-copilot"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(ext_dir / "extension.yml", "w") as f:
|
||||||
|
yaml.dump(manifest_data, f)
|
||||||
|
|
||||||
|
(ext_dir / "commands").mkdir()
|
||||||
|
(ext_dir / "commands" / "cmd.md").write_text(
|
||||||
|
"---\ndescription: Test\n---\n\nTest"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set up Copilot project
|
||||||
|
(project_dir / ".github" / "agents").mkdir(parents=True)
|
||||||
|
|
||||||
|
manifest = ExtensionManifest(ext_dir / "extension.yml")
|
||||||
|
registrar = CommandRegistrar()
|
||||||
|
registered = registrar.register_commands_for_agent(
|
||||||
|
"copilot", manifest, ext_dir, project_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(registered) == 2
|
||||||
|
|
||||||
|
# Both primary and alias get companion .prompt.md
|
||||||
|
prompts_dir = project_dir / ".github" / "prompts"
|
||||||
|
assert (prompts_dir / "speckit.alias-copilot.cmd.prompt.md").exists()
|
||||||
|
assert (prompts_dir / "speckit.shortcut-copilot.prompt.md").exists()
|
||||||
|
|
||||||
|
def test_non_copilot_agent_no_companion_file(self, extension_dir, project_dir):
|
||||||
|
"""Test that non-copilot agents do NOT create .prompt.md files."""
|
||||||
|
claude_dir = project_dir / ".claude" / "commands"
|
||||||
|
claude_dir.mkdir(parents=True)
|
||||||
|
|
||||||
|
manifest = ExtensionManifest(extension_dir / "extension.yml")
|
||||||
|
|
||||||
|
registrar = CommandRegistrar()
|
||||||
|
registrar.register_commands_for_agent(
|
||||||
|
"claude", manifest, extension_dir, project_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
# No .github/prompts directory should exist
|
||||||
|
prompts_dir = project_dir / ".github" / "prompts"
|
||||||
|
assert not prompts_dir.exists()
|
||||||
|
|
||||||
|
|
||||||
# ===== Utility Function Tests =====
|
# ===== Utility Function Tests =====
|
||||||
|
|
||||||
@@ -596,6 +711,31 @@ class TestIntegration:
|
|||||||
assert not cmd_file.exists()
|
assert not cmd_file.exists()
|
||||||
assert len(manager.list_installed()) == 0
|
assert len(manager.list_installed()) == 0
|
||||||
|
|
||||||
|
def test_copilot_cleanup_removes_prompt_files(self, extension_dir, project_dir):
|
||||||
|
"""Test that removing a Copilot extension also removes .prompt.md files."""
|
||||||
|
agents_dir = project_dir / ".github" / "agents"
|
||||||
|
agents_dir.mkdir(parents=True)
|
||||||
|
|
||||||
|
manager = ExtensionManager(project_dir)
|
||||||
|
manager.install_from_directory(extension_dir, "0.1.0", register_commands=True)
|
||||||
|
|
||||||
|
# Verify copilot was detected and registered
|
||||||
|
metadata = manager.registry.get("test-ext")
|
||||||
|
assert "copilot" in metadata["registered_commands"]
|
||||||
|
|
||||||
|
# Verify files exist before cleanup
|
||||||
|
agent_file = agents_dir / "speckit.test.hello.agent.md"
|
||||||
|
prompt_file = project_dir / ".github" / "prompts" / "speckit.test.hello.prompt.md"
|
||||||
|
assert agent_file.exists()
|
||||||
|
assert prompt_file.exists()
|
||||||
|
|
||||||
|
# Use the extension manager to remove — exercises the copilot prompt cleanup code
|
||||||
|
result = manager.remove("test-ext")
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
assert not agent_file.exists()
|
||||||
|
assert not prompt_file.exists()
|
||||||
|
|
||||||
def test_multiple_extensions(self, temp_dir, project_dir):
|
def test_multiple_extensions(self, temp_dir, project_dir):
|
||||||
"""Test installing multiple extensions."""
|
"""Test installing multiple extensions."""
|
||||||
import yaml
|
import yaml
|
||||||
|
|||||||
Reference in New Issue
Block a user