mirror of
https://github.com/github/spec-kit.git
synced 2026-07-07 06:35:06 +08:00
fix: resolve command references per integration type (dot vs hyphen) (#2354)
* fix: resolve command references per integration type (dot vs hyphen) Replace hardcoded /speckit.<cmd> references in templates with __SPECKIT_COMMAND_<NAME>__ placeholders that are resolved at setup time based on the integration type: - Markdown/TOML/YAML agents: separator='.' → /speckit.plan - Skills agents: separator='-' → /speckit-plan Changes: - Add resolve_command_refs() static method to IntegrationBase - Add invoke_separator class attribute (. for base, - for skills) - Wire into process_template() as step 8 - Update _install_shared_infra() to process page templates - Replace /speckit.* in 5 command templates and 3 page templates - Add unit tests for resolve_command_refs (positive + negative) - Add integration tests verifying on-disk content for all agents - Add end-to-end CLI tests for Claude (skills) and Copilot (markdown) Fixes #2347 * review: use effective_invoke_separator() for Copilot skills mode Address PR review feedback: instead of bleeding _skills_mode knowledge into the CLI layer, add effective_invoke_separator() method to IntegrationBase that accepts parsed_options. CopilotIntegration overrides it to return "-" when skills mode is requested. The CLI layer simply asks the integration for its separator — no hasattr or _skills_mode coupling. Also adds tests for the new method on both base and Copilot, plus an end-to-end test for 'specify init --integration copilot --integration-options --skills' verifying page templates get hyphen refs. * fix: build_command_invocation preserves full suffix for extension commands Previously rsplit('.', 1)[-1] on 'speckit.git.commit' yielded just 'commit', producing /speckit.commit instead of /speckit.git.commit (or /speckit-git-commit for skills). Fix: strip only the 'speckit.' prefix when present, then join remaining segments with the appropriate separator. Updated in IntegrationBase, SkillsIntegration, and CopilotIntegration. Added tests for extension commands in build_command_invocation across all three. * fix: Copilot dispatch_command() preserves full extension command suffix dispatch_command() had the same rsplit('.', 1)[-1] bug as build_command_invocation() — speckit.git.commit would dispatch as /speckit-commit instead of /speckit-git-commit in skills mode, or --agent speckit.commit instead of speckit.git.commit in default mode.
This commit is contained in:
@@ -723,6 +723,7 @@ def _install_shared_infra(
|
||||
script_type: str,
|
||||
tracker: StepTracker | None = None,
|
||||
force: bool = False,
|
||||
invoke_separator: str = ".",
|
||||
) -> bool:
|
||||
"""Install shared infrastructure files into *project_path*.
|
||||
|
||||
@@ -730,12 +731,17 @@ def _install_shared_infra(
|
||||
bundled core_pack or source checkout. Tracks all installed files
|
||||
in ``speckit.manifest.json``.
|
||||
|
||||
Page templates are processed to resolve ``__SPECKIT_COMMAND_<NAME>__``
|
||||
placeholders using *invoke_separator* (``"."`` for markdown agents,
|
||||
``"-"`` for skills agents).
|
||||
|
||||
When *force* is ``True``, existing files are overwritten with the
|
||||
latest bundled versions. When ``False`` (default), only missing
|
||||
files are added and existing ones are skipped.
|
||||
|
||||
Returns ``True`` on success.
|
||||
"""
|
||||
from .integrations.base import IntegrationBase
|
||||
from .integrations.manifest import IntegrationManifest
|
||||
|
||||
core = _locate_core_pack()
|
||||
@@ -786,7 +792,11 @@ def _install_shared_infra(
|
||||
if dst.exists() and not force:
|
||||
skipped_files.append(str(dst.relative_to(project_path)))
|
||||
else:
|
||||
shutil.copy2(f, dst)
|
||||
content = f.read_text(encoding="utf-8")
|
||||
content = IntegrationBase.resolve_command_refs(
|
||||
content, invoke_separator
|
||||
)
|
||||
dst.write_text(content, encoding="utf-8")
|
||||
rel = dst.relative_to(project_path).as_posix()
|
||||
manifest.record_existing(rel)
|
||||
|
||||
@@ -1295,7 +1305,7 @@ def init(
|
||||
|
||||
# Install shared infrastructure (scripts, templates)
|
||||
tracker.start("shared-infra")
|
||||
_install_shared_infra(project_path, selected_script, tracker=tracker, force=force)
|
||||
_install_shared_infra(project_path, selected_script, tracker=tracker, force=force, invoke_separator=resolved_integration.effective_invoke_separator(integration_parsed_options))
|
||||
tracker.complete("shared-infra", f"scripts ({selected_script}) + templates")
|
||||
|
||||
ensure_constitution_from_template(project_path, tracker=tracker)
|
||||
@@ -2072,9 +2082,16 @@ def integration_install(
|
||||
|
||||
selected_script = _resolve_script_type(project_root, script)
|
||||
|
||||
# Build parsed options from --integration-options so the integration
|
||||
# can determine its effective invoke separator before shared infra
|
||||
# is installed.
|
||||
parsed_options: dict[str, Any] | None = None
|
||||
if integration_options:
|
||||
parsed_options = _parse_integration_options(integration, integration_options)
|
||||
|
||||
# Ensure shared infrastructure is present (safe to run unconditionally;
|
||||
# _install_shared_infra merges missing files without overwriting).
|
||||
_install_shared_infra(project_root, selected_script)
|
||||
_install_shared_infra(project_root, selected_script, invoke_separator=integration.effective_invoke_separator(parsed_options))
|
||||
if os.name != "nt":
|
||||
ensure_executable_scripts(project_root)
|
||||
|
||||
@@ -2082,11 +2099,6 @@ def integration_install(
|
||||
integration.key, project_root, version=get_speckit_version()
|
||||
)
|
||||
|
||||
# Build parsed options from --integration-options
|
||||
parsed_options: dict[str, Any] | None = None
|
||||
if integration_options:
|
||||
parsed_options = _parse_integration_options(integration, integration_options)
|
||||
|
||||
try:
|
||||
integration.setup(
|
||||
project_root, manifest,
|
||||
@@ -2356,9 +2368,16 @@ def integration_switch(
|
||||
opts.pop("context_file", None)
|
||||
save_init_options(project_root, opts)
|
||||
|
||||
# Build parsed options from --integration-options so the integration
|
||||
# can determine its effective invoke separator before shared infra
|
||||
# is installed.
|
||||
parsed_options: dict[str, Any] | None = None
|
||||
if integration_options:
|
||||
parsed_options = _parse_integration_options(target_integration, integration_options)
|
||||
|
||||
# Ensure shared infrastructure is present (safe to run unconditionally;
|
||||
# _install_shared_infra merges missing files without overwriting).
|
||||
_install_shared_infra(project_root, selected_script)
|
||||
_install_shared_infra(project_root, selected_script, invoke_separator=target_integration.effective_invoke_separator(parsed_options))
|
||||
if os.name != "nt":
|
||||
ensure_executable_scripts(project_root)
|
||||
|
||||
@@ -2368,10 +2387,6 @@ def integration_switch(
|
||||
target_integration.key, project_root, version=get_speckit_version()
|
||||
)
|
||||
|
||||
parsed_options: dict[str, Any] | None = None
|
||||
if integration_options:
|
||||
parsed_options = _parse_integration_options(target_integration, integration_options)
|
||||
|
||||
try:
|
||||
target_integration.setup(
|
||||
project_root, manifest,
|
||||
@@ -2465,8 +2480,15 @@ def integration_upgrade(
|
||||
|
||||
selected_script = _resolve_script_type(project_root, script)
|
||||
|
||||
# Build parsed options from --integration-options so the integration
|
||||
# can determine its effective invoke separator before shared infra
|
||||
# is installed.
|
||||
parsed_options: dict[str, Any] | None = None
|
||||
if integration_options:
|
||||
parsed_options = _parse_integration_options(integration, integration_options)
|
||||
|
||||
# Ensure shared infrastructure is up to date; --force overwrites existing files.
|
||||
_install_shared_infra(project_root, selected_script, force=force)
|
||||
_install_shared_infra(project_root, selected_script, force=force, invoke_separator=integration.effective_invoke_separator(parsed_options))
|
||||
if os.name != "nt":
|
||||
ensure_executable_scripts(project_root)
|
||||
|
||||
@@ -2474,10 +2496,6 @@ def integration_upgrade(
|
||||
console.print(f"Upgrading integration: [cyan]{key}[/cyan]")
|
||||
new_manifest = IntegrationManifest(key, project_root, version=get_speckit_version())
|
||||
|
||||
parsed_options: dict[str, Any] | None = None
|
||||
if integration_options:
|
||||
parsed_options = _parse_integration_options(integration, integration_options)
|
||||
|
||||
try:
|
||||
integration.setup(
|
||||
project_root,
|
||||
|
||||
Reference in New Issue
Block a user