Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions scripts/bash/check-prerequisites.sh
Original file line number Diff line number Diff line change
Expand Up @@ -115,20 +115,20 @@ fi
# Validate required directories and files
if [[ ! -d "$FEATURE_DIR" ]]; then
echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2
echo "Run /speckit.specify first to create the feature structure." >&2
echo "Run __SPECKIT_COMMAND_SPECIFY__ first to create the feature structure." >&2
exit 1
fi

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

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

Expand Down
5 changes: 2 additions & 3 deletions scripts/bash/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ read_feature_json_feature_directory() {
}

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

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

4 changes: 2 additions & 2 deletions scripts/bash/setup-tasks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ fi

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

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

Expand Down
6 changes: 3 additions & 3 deletions scripts/powershell/check-prerequisites.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,20 @@ if ($PathsOnly) {
# Validate required directories and files
if (-not (Test-Path $paths.FEATURE_DIR -PathType Container)) {
Write-Output "ERROR: Feature directory not found: $($paths.FEATURE_DIR)"
Write-Output "Run /speckit.specify first to create the feature structure."
Write-Output "Run __SPECKIT_COMMAND_SPECIFY__ first to create the feature structure."
exit 1
}

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

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

Expand Down
6 changes: 3 additions & 3 deletions scripts/powershell/common.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ function Test-FeatureBranch {
}

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

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

return $content
}
}
4 changes: 2 additions & 2 deletions scripts/powershell/setup-tasks.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ if (-not (Test-FeatureJsonMatchesFeatureDir -RepoRoot $paths.REPO_ROOT -ActiveFe

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

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

Expand Down
22 changes: 13 additions & 9 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,9 @@ def _install_shared_infra(
``bash`` when *script_type* is ``"sh"`` and ``powershell`` when it is
``"ps"``. 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).
Shared scripts and page templates are processed to resolve
``__SPECKIT_COMMAND_<NAME>__`` placeholders using *invoke_separator*
(``"."`` for markdown agents, ``"-"`` for skills agents).

Overwrite policy:

Expand Down Expand Up @@ -1439,16 +1439,18 @@ def _set_default_integration(

if refresh_templates:
try:
_refresh_shared_templates(
_install_shared_infra(
project_root,
resolved_script,
invoke_separator=_invoke_separator_for_integration(
integration, {"integration_settings": settings}, key, parsed_options
),
force=refresh_templates_force,
refresh_managed=True,
)
Comment on lines +1443 to 1452
except (ValueError, OSError) as exc:
raise _SharedTemplateRefreshError(
f"Failed to refresh shared templates for '{key}': {exc}"
f"Failed to refresh shared infrastructure for '{key}': {exc}"
) from exc

_write_integration_json(project_root, key, installed_keys, settings)
Expand Down Expand Up @@ -1787,7 +1789,7 @@ def _update_init_options_for_integration(
@integration_app.command("use")
def integration_use(
key: str = typer.Argument(help="Installed integration key to make the default"),
force: bool = typer.Option(False, "--force", help="Overwrite managed shared templates while changing the default"),
force: bool = typer.Option(False, "--force", help="Overwrite managed shared infrastructure while changing the default"),
):
"""Set the default integration without uninstalling other integrations."""
from .integrations import get_integration
Expand Down Expand Up @@ -1984,7 +1986,7 @@ def integration_switch(
)
console.print(
f"\n[green]✓[/green] Default integration remains [bold]{target}[/bold]; "
"managed shared templates refreshed."
"managed shared infrastructure refreshed."
)
raise typer.Exit(0)
console.print(f"[yellow]Integration '{target}' is already the default integration. Nothing to switch.[/yellow]")
Expand Down Expand Up @@ -2324,16 +2326,18 @@ def integration_upgrade(
)
if installed_key == key:
try:
_refresh_shared_templates(
_install_shared_infra(
project_root,
selected_script,
invoke_separator=_invoke_separator_for_integration(
integration, {"integration_settings": settings}, key, parsed_options
),
force=force,
refresh_managed=True,
)
except (ValueError, OSError) as exc:
raise _SharedTemplateRefreshError(
f"Failed to refresh shared templates for '{key}': {exc}"
f"Failed to refresh shared infrastructure for '{key}': {exc}"
) from exc
new_manifest.save()
_write_integration_json(project_root, installed_key, installed_keys, settings)
Expand Down
11 changes: 10 additions & 1 deletion src/specify_cli/shared_infra.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,16 @@ def _ensure_or_bucket_dir(directory: Path) -> bool:

if not _ensure_or_bucket_dir(dst_path.parent):
continue
planned_copies.append((dst_path, rel, src_path.read_bytes(), src_path.stat().st_mode & 0o777))
content = src_path.read_text(encoding="utf-8")
content = IntegrationBase.resolve_command_refs(content, invoke_separator)
planned_copies.append(
(
dst_path,
rel,
content.encode("utf-8"),
src_path.stat().st_mode & 0o777,
)
)

templates_src = shared_templates_source(core_pack=core_pack, repo_root=repo_root)
if templates_src.is_dir():
Expand Down
70 changes: 69 additions & 1 deletion tests/integrations/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -866,7 +866,23 @@ def test_git_extension_commands_registered(self, tmp_path):


class TestSharedInfraCommandRefs:
"""Verify _install_shared_infra resolves __SPECKIT_COMMAND_*__ in page templates."""
"""Verify _install_shared_infra resolves __SPECKIT_COMMAND_*__ in shared infra."""

@staticmethod
def _combined_script_content(project, script_type):
script_dir = "bash" if script_type == "sh" else "powershell"
suffix = "sh" if script_type == "sh" else "ps1"
names = [
f"check-prerequisites.{suffix}",
f"common.{suffix}",
f"setup-tasks.{suffix}",
]
return "\n".join(
(project / ".specify" / "scripts" / script_dir / name).read_text(
encoding="utf-8"
)
for name in names
)

def test_dot_separator_in_page_templates(self, tmp_path):
"""Markdown agents get /speckit.<name> in page templates."""
Expand Down Expand Up @@ -911,6 +927,46 @@ def test_hyphen_separator_in_page_templates(self, tmp_path):
assert "__SPECKIT_COMMAND_" not in content
assert "/speckit-tasks" in content

@pytest.mark.parametrize("script_type", ["sh", "ps"])
def test_dot_separator_in_shared_scripts(self, tmp_path, script_type):
"""Markdown agents get /speckit.<name> in shared script hints."""
from specify_cli import _install_shared_infra

project = tmp_path / f"dot-script-{script_type}"
project.mkdir()
(project / ".specify").mkdir()

_install_shared_infra(project, script_type, invoke_separator=".")

content = self._combined_script_content(project, script_type)
assert "__SPECKIT_COMMAND_" not in content
assert "/speckit.specify" in content
assert "/speckit.plan" in content
assert "/speckit.tasks" in content
assert "/speckit-specify" not in content
assert "/speckit-plan" not in content
assert "/speckit-tasks" not in content

@pytest.mark.parametrize("script_type", ["sh", "ps"])
def test_hyphen_separator_in_shared_scripts(self, tmp_path, script_type):
"""Skills agents get /speckit-<name> in shared script hints."""
from specify_cli import _install_shared_infra

project = tmp_path / f"hyphen-script-{script_type}"
project.mkdir()
(project / ".specify").mkdir()

_install_shared_infra(project, script_type, invoke_separator="-")

content = self._combined_script_content(project, script_type)
assert "__SPECKIT_COMMAND_" not in content
assert "/speckit-specify" in content
assert "/speckit-plan" in content
assert "/speckit-tasks" in content
assert "/speckit.specify" not in content
assert "/speckit.plan" not in content
assert "/speckit.tasks" not in content

def test_full_init_claude_resolves_page_templates(self, tmp_path):
"""Full CLI init with Claude (skills agent) produces hyphen refs in page templates."""
from typer.testing import CliRunner
Expand Down Expand Up @@ -938,6 +994,10 @@ def test_full_init_claude_resolves_page_templates(self, tmp_path):
assert "/speckit-plan" in content, "Claude (skills) should use /speckit-plan"
assert "__SPECKIT_COMMAND_" not in content

script_content = self._combined_script_content(project, "sh")
assert "/speckit-specify" in script_content
assert "/speckit.specify" not in script_content

def test_full_init_copilot_resolves_page_templates(self, tmp_path):
"""Full CLI init with Copilot (markdown agent) produces dot refs in page templates."""
from typer.testing import CliRunner
Expand Down Expand Up @@ -965,6 +1025,10 @@ def test_full_init_copilot_resolves_page_templates(self, tmp_path):
assert "/speckit.plan" in content, "Copilot (markdown) should use /speckit.plan"
assert "__SPECKIT_COMMAND_" not in content

script_content = self._combined_script_content(project, "sh")
assert "/speckit.specify" in script_content
assert "/speckit-specify" not in script_content

def test_full_init_copilot_skills_resolves_page_templates(self, tmp_path):
"""Full CLI init with Copilot --skills produces hyphen refs in page templates."""
from typer.testing import CliRunner
Expand Down Expand Up @@ -994,6 +1058,10 @@ def test_full_init_copilot_skills_resolves_page_templates(self, tmp_path):
assert "/speckit.plan" not in content, "dot-notation leaked into Copilot skills page template"
assert "__SPECKIT_COMMAND_" not in content

script_content = self._combined_script_content(project, "sh")
assert "/speckit-specify" in script_content
assert "/speckit.specify" not in script_content


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