Skip to content

Commit 1efbabc

Browse files
rootroot
authored andcommitted
fix shared script command refs for integration separators
1 parent cec63d3 commit 1efbabc

10 files changed

Lines changed: 192 additions & 52 deletions

File tree

scripts/bash/check-prerequisites.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,20 +117,20 @@ check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
117117
# Validate required directories and files
118118
if [[ ! -d "$FEATURE_DIR" ]]; then
119119
echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2
120-
echo "Run /speckit.specify first to create the feature structure." >&2
120+
echo "Run __SPECKIT_COMMAND_SPECIFY__ first to create the feature structure." >&2
121121
exit 1
122122
fi
123123

124124
if [[ ! -f "$IMPL_PLAN" ]]; then
125125
echo "ERROR: plan.md not found in $FEATURE_DIR" >&2
126-
echo "Run /speckit.plan first to create the implementation plan." >&2
126+
echo "Run __SPECKIT_COMMAND_PLAN__ first to create the implementation plan." >&2
127127
exit 1
128128
fi
129129

130130
# Check for tasks.md if required
131131
if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then
132132
echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2
133-
echo "Run /speckit.tasks first to create the task list." >&2
133+
echo "Run __SPECKIT_COMMAND_TASKS__ first to create the task list." >&2
134134
exit 1
135135
fi
136136

scripts/bash/common.sh

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ read_feature_json_feature_directory() {
186186
}
187187

188188
# Returns 0 when .specify/feature.json lists feature_directory that exists as a directory
189-
# and matches the resolved active FEATURE_DIR (so /speckit.plan can skip git branch pattern checks).
189+
# and matches the resolved active FEATURE_DIR (so __SPECKIT_COMMAND_PLAN__ can skip git branch pattern checks).
190190
# Delegates parsing to read_feature_json_feature_directory, which is safe under `set -e`.
191191
feature_json_matches_feature_dir() {
192192
local repo_root="$1"
@@ -262,7 +262,7 @@ get_feature_paths() {
262262

263263
# Resolve feature directory. Priority:
264264
# 1. SPECIFY_FEATURE_DIRECTORY env var (explicit override)
265-
# 2. .specify/feature.json "feature_directory" key (persisted by /speckit.specify)
265+
# 2. .specify/feature.json "feature_directory" key (persisted by __SPECKIT_COMMAND_SPECIFY__)
266266
# 3. Branch-name-based prefix lookup (legacy fallback)
267267
local feature_dir
268268
if [[ -n "${SPECIFY_FEATURE_DIRECTORY:-}" ]]; then
@@ -642,4 +642,3 @@ except Exception:
642642
printf '%s' "$content"
643643
return 0
644644
}
645-

scripts/bash/setup-tasks.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ fi
3535

3636
if [[ ! -f "$IMPL_PLAN" ]]; then
3737
echo "ERROR: plan.md not found in $FEATURE_DIR" >&2
38-
echo "Run /speckit.plan first to create the implementation plan." >&2
38+
echo "Run __SPECKIT_COMMAND_PLAN__ first to create the implementation plan." >&2
3939
exit 1
4040
fi
4141

4242
if [[ ! -f "$FEATURE_SPEC" ]]; then
4343
echo "ERROR: spec.md not found in $FEATURE_DIR" >&2
44-
echo "Run /speckit.specify first to create the feature structure." >&2
44+
echo "Run __SPECKIT_COMMAND_SPECIFY__ first to create the feature structure." >&2
4545
exit 1
4646
fi
4747

scripts/powershell/check-prerequisites.ps1

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,20 +89,20 @@ if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit:$paths.HAS_GI
8989
# Validate required directories and files
9090
if (-not (Test-Path $paths.FEATURE_DIR -PathType Container)) {
9191
Write-Output "ERROR: Feature directory not found: $($paths.FEATURE_DIR)"
92-
Write-Output "Run /speckit.specify first to create the feature structure."
92+
Write-Output "Run __SPECKIT_COMMAND_SPECIFY__ first to create the feature structure."
9393
exit 1
9494
}
9595

9696
if (-not (Test-Path $paths.IMPL_PLAN -PathType Leaf)) {
9797
Write-Output "ERROR: plan.md not found in $($paths.FEATURE_DIR)"
98-
Write-Output "Run /speckit.plan first to create the implementation plan."
98+
Write-Output "Run __SPECKIT_COMMAND_PLAN__ first to create the implementation plan."
9999
exit 1
100100
}
101101

102102
# Check for tasks.md if required
103103
if ($RequireTasks -and -not (Test-Path $paths.TASKS -PathType Leaf)) {
104104
Write-Output "ERROR: tasks.md not found in $($paths.FEATURE_DIR)"
105-
Write-Output "Run /speckit.tasks first to create the task list."
105+
Write-Output "Run __SPECKIT_COMMAND_TASKS__ first to create the task list."
106106
exit 1
107107
}
108108

scripts/powershell/common.ps1

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ function Test-FeatureBranch {
165165
}
166166

167167
# True when .specify/feature.json pins an existing feature directory that matches the
168-
# active FEATURE_DIR from Get-FeaturePathsEnv (so /speckit.plan can skip git branch pattern checks).
168+
# active FEATURE_DIR from Get-FeaturePathsEnv (so __SPECKIT_COMMAND_PLAN__ can skip git branch pattern checks).
169169
function Test-FeatureJsonMatchesFeatureDir {
170170
param(
171171
[Parameter(Mandatory = $true)][string]$RepoRoot,
@@ -288,7 +288,7 @@ function Get-FeaturePathsEnv {
288288

289289
# Resolve feature directory. Priority:
290290
# 1. SPECIFY_FEATURE_DIRECTORY env var (explicit override)
291-
# 2. .specify/feature.json "feature_directory" key (persisted by /speckit.specify)
291+
# 2. .specify/feature.json "feature_directory" key (persisted by __SPECKIT_COMMAND_SPECIFY__)
292292
# 3. Branch-name-based prefix lookup (same as scripts/bash/common.sh)
293293
$featureJson = Join-Path $repoRoot '.specify/feature.json'
294294
if ($env:SPECIFY_FEATURE_DIRECTORY) {
@@ -640,4 +640,4 @@ except Exception:
640640
}
641641

642642
return $content
643-
}
643+
}

scripts/powershell/setup-tasks.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ if (-not (Test-FeatureJsonMatchesFeatureDir -RepoRoot $paths.REPO_ROOT -ActiveFe
2828

2929
if (-not (Test-Path $paths.IMPL_PLAN -PathType Leaf)) {
3030
[Console]::Error.WriteLine("ERROR: plan.md not found in $($paths.FEATURE_DIR)")
31-
[Console]::Error.WriteLine("Run /speckit.plan first to create the implementation plan.")
31+
[Console]::Error.WriteLine("Run __SPECKIT_COMMAND_PLAN__ first to create the implementation plan.")
3232
exit 1
3333
}
3434

3535
if (-not (Test-Path $paths.FEATURE_SPEC -PathType Leaf)) {
3636
[Console]::Error.WriteLine("ERROR: spec.md not found in $($paths.FEATURE_DIR)")
37-
[Console]::Error.WriteLine("Run /speckit.specify first to create the feature structure.")
37+
[Console]::Error.WriteLine("Run __SPECKIT_COMMAND_SPECIFY__ first to create the feature structure.")
3838
exit 1
3939
}
4040

src/specify_cli/__init__.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,9 @@ def _install_shared_infra(
161161
``bash`` when *script_type* is ``"sh"`` and ``powershell`` when it is
162162
``"ps"``. Tracks all installed files in ``speckit.manifest.json``.
163163
164-
Page templates are processed to resolve ``__SPECKIT_COMMAND_<NAME>__``
165-
placeholders using *invoke_separator* (``"."`` for markdown agents,
166-
``"-"`` for skills agents).
164+
Shared scripts and page templates are processed to resolve
165+
``__SPECKIT_COMMAND_<NAME>__`` placeholders using *invoke_separator*
166+
(``"."`` for markdown agents, ``"-"`` for skills agents).
167167
168168
Overwrite policy:
169169
@@ -755,16 +755,18 @@ def _set_default_integration(
755755

756756
if refresh_templates:
757757
try:
758-
_refresh_shared_templates(
758+
_install_shared_infra(
759759
project_root,
760+
resolved_script,
760761
invoke_separator=_invoke_separator_for_integration(
761762
integration, {"integration_settings": settings}, key, parsed_options
762763
),
763764
force=refresh_templates_force,
765+
refresh_managed=True,
764766
)
765767
except (ValueError, OSError) as exc:
766768
raise _SharedTemplateRefreshError(
767-
f"Failed to refresh shared templates for '{key}': {exc}"
769+
f"Failed to refresh shared infrastructure for '{key}': {exc}"
768770
) from exc
769771

770772
_write_integration_json(project_root, key, installed_keys, settings)
@@ -1115,7 +1117,7 @@ def _update_init_options_for_integration(
11151117
@integration_app.command("use")
11161118
def integration_use(
11171119
key: str = typer.Argument(help="Installed integration key to make the default"),
1118-
force: bool = typer.Option(False, "--force", help="Overwrite managed shared templates while changing the default"),
1120+
force: bool = typer.Option(False, "--force", help="Overwrite managed shared infrastructure while changing the default"),
11191121
):
11201122
"""Set the default integration without uninstalling other integrations."""
11211123
from .integrations import get_integration
@@ -1312,7 +1314,7 @@ def integration_switch(
13121314
)
13131315
console.print(
13141316
f"\n[green]✓[/green] Default integration remains [bold]{target}[/bold]; "
1315-
"managed shared templates refreshed."
1317+
"managed shared infrastructure refreshed."
13161318
)
13171319
raise typer.Exit(0)
13181320
console.print(f"[yellow]Integration '{target}' is already the default integration. Nothing to switch.[/yellow]")
@@ -1667,16 +1669,18 @@ def integration_upgrade(
16671669
)
16681670
if installed_key == key:
16691671
try:
1670-
_refresh_shared_templates(
1672+
_install_shared_infra(
16711673
project_root,
1674+
selected_script,
16721675
invoke_separator=_invoke_separator_for_integration(
16731676
integration, {"integration_settings": settings}, key, parsed_options
16741677
),
16751678
force=force,
1679+
refresh_managed=True,
16761680
)
16771681
except (ValueError, OSError) as exc:
16781682
raise _SharedTemplateRefreshError(
1679-
f"Failed to refresh shared templates for '{key}': {exc}"
1683+
f"Failed to refresh shared infrastructure for '{key}': {exc}"
16801684
) from exc
16811685
new_manifest.save()
16821686
_write_integration_json(project_root, installed_key, installed_keys, settings)

src/specify_cli/shared_infra.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,16 @@ def _ensure_or_bucket_dir(directory: Path) -> bool:
369369

370370
if not _ensure_or_bucket_dir(dst_path.parent):
371371
continue
372-
planned_copies.append((dst_path, rel, src_path.read_bytes(), src_path.stat().st_mode & 0o777))
372+
content = src_path.read_text(encoding="utf-8")
373+
content = IntegrationBase.resolve_command_refs(content, invoke_separator)
374+
planned_copies.append(
375+
(
376+
dst_path,
377+
rel,
378+
content.encode("utf-8"),
379+
src_path.stat().st_mode & 0o777,
380+
)
381+
)
373382

374383
templates_src = shared_templates_source(core_pack=core_pack, repo_root=repo_root)
375384
if templates_src.is_dir():

tests/integrations/test_cli.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -923,7 +923,23 @@ def test_git_extension_commands_registered(self, tmp_path):
923923

924924

925925
class TestSharedInfraCommandRefs:
926-
"""Verify _install_shared_infra resolves __SPECKIT_COMMAND_*__ in page templates."""
926+
"""Verify _install_shared_infra resolves __SPECKIT_COMMAND_*__ in shared infra."""
927+
928+
@staticmethod
929+
def _combined_script_content(project, script_type):
930+
script_dir = "bash" if script_type == "sh" else "powershell"
931+
suffix = "sh" if script_type == "sh" else "ps1"
932+
names = [
933+
f"check-prerequisites.{suffix}",
934+
f"common.{suffix}",
935+
f"setup-tasks.{suffix}",
936+
]
937+
return "\n".join(
938+
(project / ".specify" / "scripts" / script_dir / name).read_text(
939+
encoding="utf-8"
940+
)
941+
for name in names
942+
)
927943

928944
def test_dot_separator_in_page_templates(self, tmp_path):
929945
"""Markdown agents get /speckit.<name> in page templates."""
@@ -968,6 +984,46 @@ def test_hyphen_separator_in_page_templates(self, tmp_path):
968984
assert "__SPECKIT_COMMAND_" not in content
969985
assert "/speckit-tasks" in content
970986

987+
@pytest.mark.parametrize("script_type", ["sh", "ps"])
988+
def test_dot_separator_in_shared_scripts(self, tmp_path, script_type):
989+
"""Markdown agents get /speckit.<name> in shared script hints."""
990+
from specify_cli import _install_shared_infra
991+
992+
project = tmp_path / f"dot-script-{script_type}"
993+
project.mkdir()
994+
(project / ".specify").mkdir()
995+
996+
_install_shared_infra(project, script_type, invoke_separator=".")
997+
998+
content = self._combined_script_content(project, script_type)
999+
assert "__SPECKIT_COMMAND_" not in content
1000+
assert "/speckit.specify" in content
1001+
assert "/speckit.plan" in content
1002+
assert "/speckit.tasks" in content
1003+
assert "/speckit-specify" not in content
1004+
assert "/speckit-plan" not in content
1005+
assert "/speckit-tasks" not in content
1006+
1007+
@pytest.mark.parametrize("script_type", ["sh", "ps"])
1008+
def test_hyphen_separator_in_shared_scripts(self, tmp_path, script_type):
1009+
"""Skills agents get /speckit-<name> in shared script hints."""
1010+
from specify_cli import _install_shared_infra
1011+
1012+
project = tmp_path / f"hyphen-script-{script_type}"
1013+
project.mkdir()
1014+
(project / ".specify").mkdir()
1015+
1016+
_install_shared_infra(project, script_type, invoke_separator="-")
1017+
1018+
content = self._combined_script_content(project, script_type)
1019+
assert "__SPECKIT_COMMAND_" not in content
1020+
assert "/speckit-specify" in content
1021+
assert "/speckit-plan" in content
1022+
assert "/speckit-tasks" in content
1023+
assert "/speckit.specify" not in content
1024+
assert "/speckit.plan" not in content
1025+
assert "/speckit.tasks" not in content
1026+
9711027
def test_full_init_claude_resolves_page_templates(self, tmp_path):
9721028
"""Full CLI init with Claude (skills agent) produces hyphen refs in page templates."""
9731029
from typer.testing import CliRunner
@@ -995,6 +1051,10 @@ def test_full_init_claude_resolves_page_templates(self, tmp_path):
9951051
assert "/speckit-plan" in content, "Claude (skills) should use /speckit-plan"
9961052
assert "__SPECKIT_COMMAND_" not in content
9971053

1054+
script_content = self._combined_script_content(project, "sh")
1055+
assert "/speckit-specify" in script_content
1056+
assert "/speckit.specify" not in script_content
1057+
9981058
def test_full_init_copilot_resolves_page_templates(self, tmp_path):
9991059
"""Full CLI init with Copilot (markdown agent) produces dot refs in page templates."""
10001060
from typer.testing import CliRunner
@@ -1022,6 +1082,10 @@ def test_full_init_copilot_resolves_page_templates(self, tmp_path):
10221082
assert "/speckit.plan" in content, "Copilot (markdown) should use /speckit.plan"
10231083
assert "__SPECKIT_COMMAND_" not in content
10241084

1085+
script_content = self._combined_script_content(project, "sh")
1086+
assert "/speckit.specify" in script_content
1087+
assert "/speckit-specify" not in script_content
1088+
10251089
def test_full_init_copilot_skills_resolves_page_templates(self, tmp_path):
10261090
"""Full CLI init with Copilot --skills produces hyphen refs in page templates."""
10271091
from typer.testing import CliRunner
@@ -1051,6 +1115,10 @@ def test_full_init_copilot_skills_resolves_page_templates(self, tmp_path):
10511115
assert "/speckit.plan" not in content, "dot-notation leaked into Copilot skills page template"
10521116
assert "__SPECKIT_COMMAND_" not in content
10531117

1118+
script_content = self._combined_script_content(project, "sh")
1119+
assert "/speckit-specify" in script_content
1120+
assert "/speckit.specify" not in script_content
1121+
10541122

10551123
class TestIntegrationCatalogDiscoveryCLI:
10561124
"""End-to-end CLI tests for `integration search`, `info`, and `catalog …`.

0 commit comments

Comments
 (0)