Skip to content

Commit 9bb8835

Browse files
committed
Address command hint review feedback
1 parent 6cfa652 commit 9bb8835

3 files changed

Lines changed: 145 additions & 34 deletions

File tree

scripts/bash/common.sh

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -309,24 +309,28 @@ has_jq() {
309309

310310
get_invoke_separator() {
311311
local repo_root="${1:-$(get_repo_root)}"
312-
local integration_json="$repo_root/.specify/integration.json"
313-
local separator="."
314-
315-
if [[ ! -f "$integration_json" ]]; then
316-
printf '%s\n' "$separator"
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"
317314
return 0
318315
fi
319316

320-
if command -v jq >/dev/null 2>&1; then
321-
if separator=$(jq -r '(.default_integration // .integration // "") as $k | if $k == "" then "." else (.integration_settings[$k].invoke_separator // ".") end' "$integration_json" 2>/dev/null); then
322-
case "$separator" in
323-
"."|"-") printf '%s\n' "$separator"; return 0 ;;
324-
esac
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
325330
fi
326-
fi
327331

328-
if command -v python3 >/dev/null 2>&1; then
329-
if separator=$(python3 - "$integration_json" <<'PY' 2>/dev/null
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
330334
import json
331335
import sys
332336
@@ -345,25 +349,38 @@ except Exception:
345349
print(".")
346350
PY
347351
); then
348-
case "$separator" in
349-
"."|"-") printf '%s\n' "$separator"; return 0 ;;
350-
esac
352+
case "$separator" in
353+
"."|"-") ;;
354+
*) separator="." ;;
355+
esac
356+
else
357+
separator="."
358+
fi
351359
fi
352360
fi
353361

354-
printf '.\n'
362+
_SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT="$repo_root"
363+
_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE="$separator"
364+
printf '%s\n' "$separator"
355365
}
356366

357367
format_speckit_command() {
358368
local command_name="$1"
359369
local repo_root="${2:-$(get_repo_root)}"
360370
local separator
361-
separator=$(get_invoke_separator "$repo_root")
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
362378

363379
command_name="${command_name#/}"
364380
command_name="${command_name#speckit.}"
365381
command_name="${command_name#speckit-}"
366382
command_name="${command_name//./$separator}"
383+
command_name="${command_name//-/$separator}"
367384

368385
printf '/speckit%s%s\n' "$separator" "$command_name"
369386
}

scripts/powershell/common.ps1

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -358,28 +358,35 @@ function Test-DirHasFiles {
358358
function Get-InvokeSeparator {
359359
param([string]$RepoRoot = (Get-RepoRoot))
360360

361-
$integrationJson = Join-Path $RepoRoot '.specify/integration.json'
362-
if (-not (Test-Path -LiteralPath $integrationJson -PathType Leaf)) {
363-
return '.'
361+
if ($null -eq $script:SpecKitInvokeSeparatorCache) {
362+
$script:SpecKitInvokeSeparatorCache = @{}
363+
}
364+
if ($script:SpecKitInvokeSeparatorCache.ContainsKey($RepoRoot)) {
365+
return $script:SpecKitInvokeSeparatorCache[$RepoRoot]
364366
}
365367

366-
try {
367-
$state = Get-Content -LiteralPath $integrationJson -Raw | ConvertFrom-Json
368-
$key = if ($state.default_integration) { [string]$state.default_integration } elseif ($state.integration) { [string]$state.integration } else { '' }
369-
if ($key -and $state.integration_settings) {
370-
$settingProperty = $state.integration_settings.PSObject.Properties[$key]
371-
if ($settingProperty) {
372-
$setting = $settingProperty.Value
373-
if ($setting -and ($setting.invoke_separator -eq '.' -or $setting.invoke_separator -eq '-')) {
374-
return [string]$setting.invoke_separator
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+
}
375381
}
376382
}
383+
} catch {
384+
$separator = '.'
377385
}
378-
} catch {
379-
return '.'
380386
}
381387

382-
return '.'
388+
$script:SpecKitInvokeSeparatorCache[$RepoRoot] = $separator
389+
return $separator
383390
}
384391

385392
function Format-SpecKitCommand {
@@ -395,7 +402,7 @@ function Format-SpecKitCommand {
395402
} elseif ($name.StartsWith('speckit-')) {
396403
$name = $name.Substring(8)
397404
}
398-
$name = $name -replace '\.', $separator
405+
$name = $name -replace '[.-]', $separator
399406

400407
return "/speckit$separator$name"
401408
}

tests/test_setup_tasks.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,26 @@ def _run_bash_format_command(repo: Path, command_name: str) -> subprocess.Comple
106106
)
107107

108108

109+
def _run_powershell_format_command(repo: Path, command_name: str) -> subprocess.CompletedProcess:
110+
script = repo / ".specify" / "scripts" / "powershell" / "common.ps1"
111+
exe = "pwsh" if HAS_PWSH else _POWERSHELL
112+
return subprocess.run(
113+
[
114+
exe,
115+
"-NoProfile",
116+
"-Command",
117+
'$common = $args[0]; $commandName = $args[1]; . $common; Format-SpecKitCommand -CommandName $commandName -RepoRoot (Get-Location).Path',
118+
str(script),
119+
command_name,
120+
],
121+
cwd=repo,
122+
capture_output=True,
123+
text=True,
124+
check=False,
125+
env=_clean_env(),
126+
)
127+
128+
109129
def _git_init(repo: Path) -> None:
110130
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
111131
subprocess.run(
@@ -408,6 +428,54 @@ def test_bash_command_hint_rejects_invalid_invoke_separator(tasks_repo: Path) ->
408428
assert result.stdout.strip() == "/speckit.plan"
409429

410430

431+
@requires_bash
432+
def test_bash_command_hint_normalizes_mixed_separators(tasks_repo: Path) -> None:
433+
_write_integration_state(tasks_repo, "copilot", ".")
434+
435+
result = _run_bash_format_command(tasks_repo, "/speckit-git.commit")
436+
437+
assert result.returncode == 0, result.stderr
438+
assert result.stdout.strip() == "/speckit.git.commit"
439+
440+
_write_integration_state(tasks_repo, "claude", "-")
441+
442+
result = _run_bash_format_command(tasks_repo, "speckit.git-commit")
443+
444+
assert result.returncode == 0, result.stderr
445+
assert result.stdout.strip() == "/speckit-git-commit"
446+
447+
448+
@requires_bash
449+
def test_bash_command_hint_caches_invoke_separator_per_process(tasks_repo: Path) -> None:
450+
_write_integration_state(tasks_repo, "claude", "-")
451+
script = tasks_repo / ".specify" / "scripts" / "bash" / "common.sh"
452+
dot_state = {
453+
"integration": "copilot",
454+
"default_integration": "copilot",
455+
"installed_integrations": ["copilot"],
456+
"integration_settings": {"copilot": {"invoke_separator": "."}},
457+
}
458+
459+
result = subprocess.run(
460+
[
461+
"bash",
462+
"-c",
463+
'source "$1"; format_speckit_command plan "$PWD"; printf "%s" "$2" > .specify/integration.json; format_speckit_command tasks "$PWD"',
464+
"bash",
465+
str(script),
466+
json.dumps(dot_state),
467+
],
468+
cwd=tasks_repo,
469+
capture_output=True,
470+
text=True,
471+
check=False,
472+
env=_clean_env(),
473+
)
474+
475+
assert result.returncode == 0, result.stderr
476+
assert result.stdout.splitlines() == ["/speckit-plan", "/speckit-tasks"]
477+
478+
411479
@requires_bash
412480
def test_setup_tasks_bash_uses_invoke_separator_in_plan_hint(tasks_repo: Path) -> None:
413481
_write_integration_state(tasks_repo, "claude", "-")
@@ -615,6 +683,25 @@ def test_setup_tasks_ps_missing_template_errors(tasks_repo: Path) -> None:
615683
assert "tasks-template" in result.stderr.lower() or "tasks-template" in result.stdout.lower()
616684

617685

686+
@pytest.mark.skipif(not (HAS_PWSH or _POWERSHELL), reason="no PowerShell available")
687+
def test_powershell_command_hint_normalizes_mixed_separators(
688+
tasks_repo: Path,
689+
) -> None:
690+
_write_integration_state(tasks_repo, "copilot", ".")
691+
692+
result = _run_powershell_format_command(tasks_repo, "/speckit-git.commit")
693+
694+
assert result.returncode == 0, result.stderr
695+
assert result.stdout.strip() == "/speckit.git.commit"
696+
697+
_write_integration_state(tasks_repo, "claude", "-")
698+
699+
result = _run_powershell_format_command(tasks_repo, "speckit.git-commit")
700+
701+
assert result.returncode == 0, result.stderr
702+
assert result.stdout.strip() == "/speckit-git-commit"
703+
704+
618705
@pytest.mark.skipif(not (HAS_PWSH or _POWERSHELL), reason="no PowerShell available")
619706
def test_setup_tasks_ps_uses_invoke_separator_in_plan_hint(tasks_repo: Path) -> None:
620707
_write_integration_state(tasks_repo, "claude", "-")

0 commit comments

Comments
 (0)