diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index b41d17dec3..f97229c8a9 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -372,4 +372,3 @@ except Exception: # Callers running under set -e should use: TEMPLATE=$(resolve_template ...) || true return 1 } - diff --git a/scripts/bash/create-new-feature.sh b/scripts/bash/create-new-feature.sh index 1879647026..9290525b90 100644 --- a/scripts/bash/create-new-feature.sh +++ b/scripts/bash/create-new-feature.sh @@ -324,6 +324,7 @@ fi FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME" SPEC_FILE="$FEATURE_DIR/spec.md" +FEATURE_METADATA_FILE="$REPO_ROOT/.specify/feature.json" if [ "$DRY_RUN" != true ]; then if [ "$HAS_GIT" = true ]; then @@ -366,6 +367,7 @@ if [ "$DRY_RUN" != true ]; then fi mkdir -p "$FEATURE_DIR" + mkdir -p "$(dirname "$FEATURE_METADATA_FILE")" if [ ! -f "$SPEC_FILE" ]; then TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT") || true @@ -377,6 +379,14 @@ if [ "$DRY_RUN" != true ]; then fi fi + if command -v jq >/dev/null 2>&1; then + jq -cn \ + --arg feature_directory "specs/$BRANCH_NAME" \ + '{feature_directory:$feature_directory}' >"$FEATURE_METADATA_FILE" + else + printf '{"feature_directory":"%s"}\n' "$(json_escape "specs/$BRANCH_NAME")" >"$FEATURE_METADATA_FILE" + fi + # Inform the user how to persist the feature variable in their own shell printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2 fi diff --git a/scripts/powershell/common.ps1 b/scripts/powershell/common.ps1 index 0d6544aaf4..3ffe46264c 100644 --- a/scripts/powershell/common.ps1 +++ b/scripts/powershell/common.ps1 @@ -353,4 +353,3 @@ function Resolve-Template { return $null } - diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 2f23283fc4..8ba34be852 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -289,6 +289,7 @@ if ($branchName.Length -gt $maxBranchLength) { $featureDir = Join-Path $specsDir $branchName $specFile = Join-Path $featureDir 'spec.md' +$featureMetadataFile = Join-Path $repoRoot '.specify/feature.json' if (-not $DryRun) { if ($hasGit) { @@ -346,6 +347,7 @@ if (-not $DryRun) { } New-Item -ItemType Directory -Path $featureDir -Force | Out-Null + New-Item -ItemType Directory -Path (Split-Path $featureMetadataFile -Parent) -Force | Out-Null if (-not (Test-Path -PathType Leaf $specFile)) { $template = Resolve-Template -TemplateName 'spec-template' -RepoRoot $repoRoot @@ -356,6 +358,10 @@ if (-not $DryRun) { } } + [PSCustomObject]@{ + feature_directory = "specs/$branchName" + } | ConvertTo-Json -Compress | Set-Content -Path $featureMetadataFile -Encoding utf8 + # Set the SPECIFY_FEATURE environment variable for the current session $env:SPECIFY_FEATURE = $branchName } diff --git a/tests/test_timestamp_branches.py b/tests/test_timestamp_branches.py index 39228d9455..0f06b74085 100644 --- a/tests/test_timestamp_branches.py +++ b/tests/test_timestamp_branches.py @@ -180,6 +180,19 @@ def test_json_output_keys(self, git_repo: Path): assert key in data, f"missing {key} in JSON: {data}" assert re.match(r"^\d{8}-\d{6}$", data["FEATURE_NUM"]) + def test_writes_feature_metadata_file(self, git_repo: Path): + """Feature creation persists .specify/feature.json with the created spec dir.""" + import json + + result = run_script(git_repo, "--json", "--short-name", "meta-test", "Metadata test") + assert result.returncode == 0, result.stderr + data = json.loads(result.stdout) + + metadata_file = git_repo / ".specify" / "feature.json" + assert metadata_file.exists(), "feature metadata file was not created" + metadata = json.loads(metadata_file.read_text(encoding="utf-8")) + assert metadata == {"feature_directory": f"specs/{data['BRANCH_NAME']}"} + def test_long_name_truncation(self, git_repo: Path): """Test 5: Long branch name is truncated to <= 244 chars.""" long_name = "a-" * 150 + "end" @@ -644,6 +657,12 @@ def test_powershell_supports_allow_existing_branch_flag(self): # Ensure the flag is referenced in script logic, not just declared assert "AllowExistingBranch" in contents.replace("-AllowExistingBranch", "") + def test_powershell_persists_feature_metadata(self): + """Static guard: PS script writes .specify/feature.json.""" + contents = CREATE_FEATURE_PS.read_text(encoding="utf-8") + assert "feature_directory = \"specs/$branchName\"" in contents + assert "feature.json" in contents + def test_powershell_surfaces_checkout_errors(self): """Static guard: PS script preserves checkout stderr on existing-branch failures.""" contents = CREATE_FEATURE_PS.read_text(encoding="utf-8") @@ -993,7 +1012,19 @@ def test_ps_dry_run_json_absent_without_flag(self, ps_git_repo: Path): assert result.returncode == 0, result.stderr data = json.loads(result.stdout) assert "DRY_RUN" not in data, f"DRY_RUN should not be in normal JSON: {data}" + + def test_ps_writes_feature_metadata_file(self, ps_git_repo: Path): + """PowerShell create script persists .specify/feature.json.""" + result = run_ps_script( + ps_git_repo, "-Json", "-ShortName", "ps-meta", "PowerShell metadata" + ) + assert result.returncode == 0, result.stderr + data = json.loads(result.stdout) + metadata_file = ps_git_repo / ".specify" / "feature.json" + assert metadata_file.exists(), "feature metadata file was not created" + metadata = json.loads(metadata_file.read_text(encoding="utf-8-sig")) + assert metadata == {"feature_directory": f"specs/{data['BRANCH_NAME']}"} # ── GIT_BRANCH_NAME Override Tests ──────────────────────────────────────────