Skip to content

Commit 4028c50

Browse files
authored
fix: render script command hints with active agent separator (#2649)
* fix script command hints for agent separators * Address command hint review feedback * chore: remove whitespace-only PR churn * test: fix PowerShell command hint invocation * fix: preserve hyphens in script command hints * fix: render managed script command hints
1 parent 67fecd3 commit 4028c50

8 files changed

Lines changed: 441 additions & 14 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_COMMAND_SPECIFY__ first to create the feature structure." >&2
120+
echo "Run $(format_speckit_command specify "$REPO_ROOT") 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_COMMAND_PLAN__ first to create the implementation plan." >&2
126+
echo "Run $(format_speckit_command plan "$REPO_ROOT") 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_COMMAND_TASKS__ first to create the task list." >&2
133+
echo "Run $(format_speckit_command tasks "$REPO_ROOT") first to create the task list." >&2
134134
exit 1
135135
fi
136136

scripts/bash/common.sh

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,83 @@ has_jq() {
307307
command -v jq >/dev/null 2>&1
308308
}
309309

310+
get_invoke_separator() {
311+
local repo_root="${1:-$(get_repo_root)}"
312+
if [[ "${_SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT:-}" == "$repo_root" && -n "${_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE:-}" ]]; then
313+
printf '%s\n' "$_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE"
314+
return 0
315+
fi
316+
317+
local integration_json="$repo_root/.specify/integration.json"
318+
local separator="."
319+
local parsed_with_jq=0
320+
321+
if [[ -f "$integration_json" ]]; then
322+
if command -v jq >/dev/null 2>&1; then
323+
local jq_separator
324+
if jq_separator=$(jq -r '(.default_integration // .integration // "") as $k | if $k == "" then "." else (.integration_settings[$k].invoke_separator // ".") end' "$integration_json" 2>/dev/null); then
325+
parsed_with_jq=1
326+
case "$jq_separator" in
327+
"."|"-") separator="$jq_separator" ;;
328+
esac
329+
fi
330+
fi
331+
332+
if [[ "$parsed_with_jq" -eq 0 ]] && command -v python3 >/dev/null 2>&1; then
333+
if separator=$(python3 - "$integration_json" <<'PY' 2>/dev/null
334+
import json
335+
import sys
336+
337+
try:
338+
with open(sys.argv[1], encoding="utf-8") as fh:
339+
state = json.load(fh)
340+
key = state.get("default_integration") or state.get("integration") or ""
341+
settings = state.get("integration_settings")
342+
separator = "."
343+
if isinstance(key, str) and isinstance(settings, dict):
344+
entry = settings.get(key)
345+
if isinstance(entry, dict) and entry.get("invoke_separator") in {".", "-"}:
346+
separator = entry["invoke_separator"]
347+
print(separator)
348+
except Exception:
349+
print(".")
350+
PY
351+
); then
352+
case "$separator" in
353+
"."|"-") ;;
354+
*) separator="." ;;
355+
esac
356+
else
357+
separator="."
358+
fi
359+
fi
360+
fi
361+
362+
_SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT="$repo_root"
363+
_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE="$separator"
364+
printf '%s\n' "$separator"
365+
}
366+
367+
format_speckit_command() {
368+
local command_name="$1"
369+
local repo_root="${2:-$(get_repo_root)}"
370+
local separator
371+
if [[ "${_SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT:-}" == "$repo_root" && -n "${_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE:-}" ]]; then
372+
separator="$_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE"
373+
else
374+
separator=$(get_invoke_separator "$repo_root")
375+
_SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT="$repo_root"
376+
_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE="$separator"
377+
fi
378+
379+
command_name="${command_name#/}"
380+
command_name="${command_name#speckit.}"
381+
command_name="${command_name#speckit-}"
382+
command_name="${command_name//./$separator}"
383+
384+
printf '/speckit%s%s\n' "$separator" "$command_name"
385+
}
386+
310387
# Escape a string for safe embedding in a JSON value (fallback when jq is unavailable).
311388
# Handles backslash, double-quote, and JSON-required control character escapes (RFC 8259).
312389
json_escape() {

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_COMMAND_PLAN__ first to create the implementation plan." >&2
38+
echo "Run $(format_speckit_command plan "$REPO_ROOT") 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_COMMAND_SPECIFY__ first to create the feature structure." >&2
44+
echo "Run $(format_speckit_command specify "$REPO_ROOT") first to create the feature structure." >&2
4545
exit 1
4646
fi
4747

scripts/powershell/check-prerequisites.ps1

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,20 +89,23 @@ 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_COMMAND_SPECIFY__ first to create the feature structure."
92+
$specifyCommand = Format-SpecKitCommand -CommandName 'specify' -RepoRoot $paths.REPO_ROOT
93+
Write-Output "Run $specifyCommand first to create the feature structure."
9394
exit 1
9495
}
9596

9697
if (-not (Test-Path $paths.IMPL_PLAN -PathType Leaf)) {
9798
Write-Output "ERROR: plan.md not found in $($paths.FEATURE_DIR)"
98-
Write-Output "Run __SPECKIT_COMMAND_PLAN__ first to create the implementation plan."
99+
$planCommand = Format-SpecKitCommand -CommandName 'plan' -RepoRoot $paths.REPO_ROOT
100+
Write-Output "Run $planCommand first to create the implementation plan."
99101
exit 1
100102
}
101103

102104
# Check for tasks.md if required
103105
if ($RequireTasks -and -not (Test-Path $paths.TASKS -PathType Leaf)) {
104106
Write-Output "ERROR: tasks.md not found in $($paths.FEATURE_DIR)"
105-
Write-Output "Run __SPECKIT_COMMAND_TASKS__ first to create the task list."
107+
$tasksCommand = Format-SpecKitCommand -CommandName 'tasks' -RepoRoot $paths.REPO_ROOT
108+
Write-Output "Run $tasksCommand first to create the task list."
106109
exit 1
107110
}
108111

scripts/powershell/common.ps1

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,58 @@ function Test-DirHasFiles {
355355
}
356356
}
357357

358+
function Get-InvokeSeparator {
359+
param([string]$RepoRoot = (Get-RepoRoot))
360+
361+
if ($null -eq $script:SpecKitInvokeSeparatorCache) {
362+
$script:SpecKitInvokeSeparatorCache = @{}
363+
}
364+
if ($script:SpecKitInvokeSeparatorCache.ContainsKey($RepoRoot)) {
365+
return $script:SpecKitInvokeSeparatorCache[$RepoRoot]
366+
}
367+
368+
$separator = '.'
369+
$integrationJson = Join-Path $RepoRoot '.specify/integration.json'
370+
if (Test-Path -LiteralPath $integrationJson -PathType Leaf) {
371+
try {
372+
$state = Get-Content -LiteralPath $integrationJson -Raw | ConvertFrom-Json
373+
$key = if ($state.default_integration) { [string]$state.default_integration } elseif ($state.integration) { [string]$state.integration } else { '' }
374+
if ($key -and $state.integration_settings) {
375+
$settingProperty = $state.integration_settings.PSObject.Properties[$key]
376+
if ($settingProperty) {
377+
$setting = $settingProperty.Value
378+
if ($setting -and ($setting.invoke_separator -eq '.' -or $setting.invoke_separator -eq '-')) {
379+
$separator = [string]$setting.invoke_separator
380+
}
381+
}
382+
}
383+
} catch {
384+
$separator = '.'
385+
}
386+
}
387+
388+
$script:SpecKitInvokeSeparatorCache[$RepoRoot] = $separator
389+
return $separator
390+
}
391+
392+
function Format-SpecKitCommand {
393+
param(
394+
[Parameter(Mandatory = $true)][string]$CommandName,
395+
[string]$RepoRoot = (Get-RepoRoot)
396+
)
397+
398+
$separator = Get-InvokeSeparator -RepoRoot $RepoRoot
399+
$name = $CommandName.TrimStart('/')
400+
if ($name.StartsWith('speckit.')) {
401+
$name = $name.Substring(8)
402+
} elseif ($name.StartsWith('speckit-')) {
403+
$name = $name.Substring(8)
404+
}
405+
$name = $name -replace '\.', $separator
406+
407+
return "/speckit$separator$name"
408+
}
409+
358410
# Find a usable Python 3 executable (python3, python, or py -3).
359411
# Returns the command/arguments as an array, or $null if none found.
360412
function Get-Python3Command {

scripts/powershell/setup-tasks.ps1

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ 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_COMMAND_PLAN__ first to create the implementation plan.")
31+
$planCommand = Format-SpecKitCommand -CommandName 'plan' -RepoRoot $paths.REPO_ROOT
32+
[Console]::Error.WriteLine("Run $planCommand first to create the implementation plan.")
3233
exit 1
3334
}
3435

3536
if (-not (Test-Path $paths.FEATURE_SPEC -PathType Leaf)) {
3637
[Console]::Error.WriteLine("ERROR: spec.md not found in $($paths.FEATURE_DIR)")
37-
[Console]::Error.WriteLine("Run __SPECKIT_COMMAND_SPECIFY__ first to create the feature structure.")
38+
$specifyCommand = Format-SpecKitCommand -CommandName 'specify' -RepoRoot $paths.REPO_ROOT
39+
[Console]::Error.WriteLine("Run $specifyCommand first to create the feature structure.")
3840
exit 1
3941
}
4042

src/specify_cli/shared_infra.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import os
6+
import re
67
import tempfile
78
from pathlib import Path
89
from typing import Any
@@ -194,6 +195,37 @@ def _write_shared_bytes(
194195
temp_path.unlink()
195196

196197

198+
_BASH_FORMAT_COMMAND_RE = re.compile(
199+
r"\$\(\s*format_speckit_command\s+(['\"]?)([A-Za-z0-9_.-]+)\1(?:\s+[^)]*)?\)"
200+
)
201+
_POWERSHELL_FORMAT_COMMAND_RE = re.compile(
202+
r"Format-SpecKitCommand\s+-CommandName\s+(['\"])([A-Za-z0-9_.-]+)\1(?:\s+-RepoRoot\s+[^\r\n]+)?"
203+
)
204+
205+
206+
def _format_speckit_command(command_name: str, separator: str) -> str:
207+
name = command_name.strip().lstrip("/")
208+
if name.startswith("speckit."):
209+
name = name[len("speckit.") :]
210+
elif name.startswith("speckit-"):
211+
name = name[len("speckit-") :]
212+
name = name.replace(".", separator)
213+
return f"/speckit{separator}{name}"
214+
215+
216+
def _resolve_dynamic_command_refs(content: str, separator: str) -> str:
217+
"""Render script runtime command helpers for managed shared infra copies."""
218+
219+
content = _BASH_FORMAT_COMMAND_RE.sub(
220+
lambda match: _format_speckit_command(match.group(2), separator),
221+
content,
222+
)
223+
return _POWERSHELL_FORMAT_COMMAND_RE.sub(
224+
lambda match: f"'{_format_speckit_command(match.group(2), separator)}'",
225+
content,
226+
)
227+
228+
197229
def refresh_shared_templates(
198230
project_path: Path,
199231
*,
@@ -388,6 +420,7 @@ def _ensure_or_bucket_dir(directory: Path) -> bool:
388420
continue
389421
content = src_path.read_text(encoding="utf-8")
390422
content = IntegrationBase.resolve_command_refs(content, invoke_separator)
423+
content = _resolve_dynamic_command_refs(content, invoke_separator)
391424
planned_copies.append(
392425
(
393426
dst_path,

0 commit comments

Comments
 (0)