Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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,
)
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