Skip to content

Commit fbbcf3d

Browse files
committed
Merge remote-tracking branch 'origin/main' into codex/pr2442-resolve-conflicts
# Conflicts: # src/specify_cli/__init__.py # tests/test_authentication.py
2 parents 000481b + 659a41a commit fbbcf3d

27 files changed

Lines changed: 893 additions & 74 deletions

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
<!-- insert new changelog below this comment -->
44

5+
## [0.9.3] - 2026-06-03
6+
7+
### Changed
8+
9+
- fix: render script command hints with active agent separator (#2649)
10+
- chore(tests): fix ruff lint violations in tests/ (#2827)
11+
- fix(workflows): validate run_id in RunState.load before touching the … (#2813)
12+
- feat(cli): implement specify self upgrade (#2475)
13+
- feat(workflows): allow resume to accept updated workflow inputs (#2815)
14+
- catalog: rename "superpowers-bridge" to "superspec" (v1.0.1) (#2772)
15+
- fix(cli): force UTF-8 stdout/stderr on Windows to prevent UnicodeEncodeError (#2817)
16+
- fix(plan): clarify quickstart validation guide scope (#2805)
17+
- chore: release 0.9.2, begin 0.9.3.dev0 development (#2823)
18+
519
## [0.9.2] - 2026-06-02
620

721
### Changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "specify-cli"
3-
version = "0.9.3.dev0"
3+
version = "0.9.4.dev0"
44
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
55
requires-python = ">=3.11"
66
dependencies = [

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/__init__.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1626,6 +1626,7 @@ def extension_add(
16261626
extension: str = typer.Argument(help="Extension name or path"),
16271627
dev: bool = typer.Option(False, "--dev", help="Install from local directory"),
16281628
from_url: Optional[str] = typer.Option(None, "--from", help="Install from custom URL"),
1629+
force: bool = typer.Option(False, "--force", help="Overwrite if already installed"),
16291630
priority: int = typer.Option(10, "--priority", help="Resolution priority (lower = higher precedence, default 10)"),
16301631
):
16311632
"""Install an extension."""
@@ -1640,6 +1641,9 @@ def extension_add(
16401641
manager = ExtensionManager(project_root)
16411642
speckit_version = get_speckit_version()
16421643

1644+
if force:
1645+
console.print("[yellow]--force:[/yellow] Will overwrite if already installed")
1646+
16431647
# Prompt for URL-based installs BEFORE the spinner so the user can
16441648
# actually see and respond to the confirmation (the Rich status
16451649
# spinner overwrites the typer.confirm prompt line, making it appear
@@ -1690,11 +1694,15 @@ def extension_add(
16901694
console.print(f"[red]Error:[/red] No extension.yml found in {source_path}")
16911695
raise typer.Exit(1)
16921696

1697+
if force:
1698+
console.print(f"[yellow]--force:[/yellow] Installing from [cyan]{source_path}[/cyan] (will overwrite if already installed)...")
1699+
16931700
manifest = manager.install_from_directory(
16941701
source_path,
16951702
speckit_version,
16961703
priority=priority,
16971704
link_commands=True,
1705+
force=force
16981706
)
16991707

17001708
elif from_url:
@@ -1724,7 +1732,7 @@ def extension_add(
17241732
zip_path.write_bytes(zip_data)
17251733

17261734
# Install from downloaded ZIP
1727-
manifest = manager.install_from_zip(zip_path, speckit_version, priority=priority)
1735+
manifest = manager.install_from_zip(zip_path, speckit_version, priority=priority, force=force)
17281736
# ExtensionError covers an oversized body (via error_type) and the
17291737
# ValidationError/ExtensionError raised by install_from_zip; URL
17301738
# scheme is validated above. Catching these instead of a blanket
@@ -1741,7 +1749,9 @@ def extension_add(
17411749
# Try bundled extensions first (shipped with spec-kit)
17421750
bundled_path = _locate_bundled_extension(extension)
17431751
if bundled_path is not None:
1744-
manifest = manager.install_from_directory(bundled_path, speckit_version, priority=priority)
1752+
manifest = manager.install_from_directory(
1753+
bundled_path, speckit_version, priority=priority, force=force
1754+
)
17451755
else:
17461756
# Install from catalog (also resolves display names to IDs)
17471757
catalog = ExtensionCatalog(project_root)
@@ -1762,7 +1772,9 @@ def extension_add(
17621772
if resolved_id != extension:
17631773
bundled_path = _locate_bundled_extension(resolved_id)
17641774
if bundled_path is not None:
1765-
manifest = manager.install_from_directory(bundled_path, speckit_version, priority=priority)
1775+
manifest = manager.install_from_directory(
1776+
bundled_path, speckit_version, priority=priority, force=force
1777+
)
17661778

17671779
if bundled_path is None:
17681780
# Bundled extensions without a download URL must come from the local package
@@ -1798,7 +1810,7 @@ def extension_add(
17981810

17991811
try:
18001812
# Install from downloaded ZIP
1801-
manifest = manager.install_from_zip(zip_path, speckit_version, priority=priority)
1813+
manifest = manager.install_from_zip(zip_path, speckit_version, priority=priority, force=force)
18021814
finally:
18031815
# Clean up downloaded ZIP
18041816
if zip_path.exists():

0 commit comments

Comments
 (0)