From e3e47bd5659be22401728053a35ec43d7a3cf95a Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Mon, 20 Apr 2026 22:28:59 +0500 Subject: [PATCH 1/7] fix(powershell): strip BOM from templates and ensure No-BOM output --- scripts/powershell/create-new-feature.ps1 | 5 ++++- scripts/powershell/setup-plan.ps1 | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 2f23283fc4..11441a34ca 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -350,7 +350,10 @@ if (-not $DryRun) { if (-not (Test-Path -PathType Leaf $specFile)) { $template = Resolve-Template -TemplateName 'spec-template' -RepoRoot $repoRoot if ($template -and (Test-Path $template)) { - Copy-Item $template $specFile -Force + # Read the template content and write it to the spec file with UTF-8 encoding without BOM + $content = Get-Content -Raw -Path $template + $Utf8NoBom = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($specFile, $content, $Utf8NoBom) } else { New-Item -ItemType File -Path $specFile -Force | Out-Null } diff --git a/scripts/powershell/setup-plan.ps1 b/scripts/powershell/setup-plan.ps1 index ee09094bf7..1c57612cd4 100644 --- a/scripts/powershell/setup-plan.ps1 +++ b/scripts/powershell/setup-plan.ps1 @@ -34,7 +34,10 @@ New-Item -ItemType Directory -Path $paths.FEATURE_DIR -Force | Out-Null # Copy plan template if it exists, otherwise note it or create empty file $template = Resolve-Template -TemplateName 'plan-template' -RepoRoot $paths.REPO_ROOT if ($template -and (Test-Path $template)) { - Copy-Item $template $paths.IMPL_PLAN -Force + # Read the template content and write it to the implementation plan file with UTF-8 encoding without BOM + $content = Get-Content -Raw -Path $template + $Utf8NoBom = New-Object System.Text.UTF8Encoding($false) +[System.IO.File]::WriteAllText($paths.IMPL_PLAN, $content, $Utf8NoBom) Write-Output "Copied plan template to $($paths.IMPL_PLAN)" } else { Write-Warning "Plan template not found" From a6cbcad4fb63718661ee97c769b36112397f7738 Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Tue, 21 Apr 2026 22:51:40 +0500 Subject: [PATCH 2/7] fix: address review feedback on encoding and naming for all ps scripts --- scripts/powershell/create-new-feature.ps1 | 6 +++--- scripts/powershell/setup-plan.ps1 | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 11441a34ca..bf2c9ffdc7 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -351,9 +351,9 @@ if (-not $DryRun) { $template = Resolve-Template -TemplateName 'spec-template' -RepoRoot $repoRoot if ($template -and (Test-Path $template)) { # Read the template content and write it to the spec file with UTF-8 encoding without BOM - $content = Get-Content -Raw -Path $template - $Utf8NoBom = New-Object System.Text.UTF8Encoding($false) - [System.IO.File]::WriteAllText($specFile, $content, $Utf8NoBom) + $content = Get-Content -Raw -Encoding UTF8 -Path $template + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($targetPath, $content, $utf8NoBom) } else { New-Item -ItemType File -Path $specFile -Force | Out-Null } diff --git a/scripts/powershell/setup-plan.ps1 b/scripts/powershell/setup-plan.ps1 index 1c57612cd4..913fd2a9e9 100644 --- a/scripts/powershell/setup-plan.ps1 +++ b/scripts/powershell/setup-plan.ps1 @@ -35,9 +35,10 @@ New-Item -ItemType Directory -Path $paths.FEATURE_DIR -Force | Out-Null $template = Resolve-Template -TemplateName 'plan-template' -RepoRoot $paths.REPO_ROOT if ($template -and (Test-Path $template)) { # Read the template content and write it to the implementation plan file with UTF-8 encoding without BOM - $content = Get-Content -Raw -Path $template - $Utf8NoBom = New-Object System.Text.UTF8Encoding($false) -[System.IO.File]::WriteAllText($paths.IMPL_PLAN, $content, $Utf8NoBom) + # Read the template content as UTF-8 and write it to the implementation plan file with UTF-8 encoding without BOM + $content = Get-Content -Raw -Encoding UTF8 -Path $template + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($paths.IMPL_PLAN, $content, $utf8NoBom) Write-Output "Copied plan template to $($paths.IMPL_PLAN)" } else { Write-Warning "Plan template not found" From 059adbc404670d3d6ac96d4dbc2393d9aecfc34e Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Wed, 22 Apr 2026 00:00:36 +0500 Subject: [PATCH 3/7] fix: address copilot feedback (encoding detection and variable naming) --- scripts/powershell/create-new-feature.ps1 | 4 ++-- scripts/powershell/setup-plan.ps1 | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index bf2c9ffdc7..2835c3ee4e 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -351,9 +351,9 @@ if (-not $DryRun) { $template = Resolve-Template -TemplateName 'spec-template' -RepoRoot $repoRoot if ($template -and (Test-Path $template)) { # Read the template content and write it to the spec file with UTF-8 encoding without BOM - $content = Get-Content -Raw -Encoding UTF8 -Path $template + $content = [System.IO.File]::ReadAllText($template) $utf8NoBom = New-Object System.Text.UTF8Encoding($false) - [System.IO.File]::WriteAllText($targetPath, $content, $utf8NoBom) + [System.IO.File]::WriteAllText($specFile, $content, $utf8NoBom) } else { New-Item -ItemType File -Path $specFile -Force | Out-Null } diff --git a/scripts/powershell/setup-plan.ps1 b/scripts/powershell/setup-plan.ps1 index 913fd2a9e9..0dcb4f81e1 100644 --- a/scripts/powershell/setup-plan.ps1 +++ b/scripts/powershell/setup-plan.ps1 @@ -36,10 +36,10 @@ $template = Resolve-Template -TemplateName 'plan-template' -RepoRoot $paths.REPO if ($template -and (Test-Path $template)) { # Read the template content and write it to the implementation plan file with UTF-8 encoding without BOM # Read the template content as UTF-8 and write it to the implementation plan file with UTF-8 encoding without BOM - $content = Get-Content -Raw -Encoding UTF8 -Path $template + # Read the template content and write it to the implementation plan file with UTF-8 encoding without BOM + $content = [System.IO.File]::ReadAllText($template) $utf8NoBom = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::WriteAllText($paths.IMPL_PLAN, $content, $utf8NoBom) - Write-Output "Copied plan template to $($paths.IMPL_PLAN)" } else { Write-Warning "Plan template not found" # Create a basic plan file if template doesn't exist From edc7f2c5a80f77fbc905183bd7c95867dab4acc2 Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Mon, 4 May 2026 18:20:14 +0500 Subject: [PATCH 4/7] fix: remove duplicate comments in setup-plan.ps1 --- scripts/powershell/setup-plan.ps1 | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/powershell/setup-plan.ps1 b/scripts/powershell/setup-plan.ps1 index 0dcb4f81e1..2881c9fc8a 100644 --- a/scripts/powershell/setup-plan.ps1 +++ b/scripts/powershell/setup-plan.ps1 @@ -35,8 +35,6 @@ New-Item -ItemType Directory -Path $paths.FEATURE_DIR -Force | Out-Null $template = Resolve-Template -TemplateName 'plan-template' -RepoRoot $paths.REPO_ROOT if ($template -and (Test-Path $template)) { # Read the template content and write it to the implementation plan file with UTF-8 encoding without BOM - # Read the template content as UTF-8 and write it to the implementation plan file with UTF-8 encoding without BOM - # Read the template content and write it to the implementation plan file with UTF-8 encoding without BOM $content = [System.IO.File]::ReadAllText($template) $utf8NoBom = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::WriteAllText($paths.IMPL_PLAN, $content, $utf8NoBom) From 57f06f6ce29ade99508669116ce3986d245ba6fd Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Mon, 4 May 2026 18:32:36 +0500 Subject: [PATCH 5/7] test: verify spec.md is written without UTF-8 BOM --- tests/test_timestamp_branches.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_timestamp_branches.py b/tests/test_timestamp_branches.py index 39228d9455..078088b4c5 100644 --- a/tests/test_timestamp_branches.py +++ b/tests/test_timestamp_branches.py @@ -649,7 +649,25 @@ def test_powershell_surfaces_checkout_errors(self): contents = CREATE_FEATURE_PS.read_text(encoding="utf-8") assert "$switchBranchError = git checkout -q $branchName 2>&1 | Out-String" in contents assert "exists but could not be checked out.`n$($switchBranchError.Trim())" in contents + + @pytest.mark.skipif(not _has_pwsh(), reason="pwsh not installed") + def test_ps_spec_file_written_without_bom(self, ps_git_repo: Path): + """spec.md generated by create-new-feature.ps1 must not contain a UTF-8 BOM.""" + result = run_ps_script( + ps_git_repo, "-ShortName", "bom-check", "BOM check feature" + ) + assert result.returncode == 0, result.stderr + + # Find the generated spec.md + spec_file = next((ps_git_repo / "specs").rglob("spec.md"), None) + assert spec_file is not None, "spec.md was not created" + # Read the first 3 raw bytes and assert no BOM present + with open(spec_file, "rb") as f: + raw_bytes = f.read(3) + assert raw_bytes != b"\xef\xbb\xbf", ( + f"spec.md contains a UTF-8 BOM — found {raw_bytes!r} at start of file" + ) class TestGitExtensionParity: def test_bash_extension_surfaces_checkout_errors(self): From 72e5d756a5d2ec33769fcd6ab3e6d85ff12dd71c Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Tue, 5 May 2026 19:04:59 +0500 Subject: [PATCH 6/7] test: also verify BOM-free output under Windows PowerShell 5.1 --- tests/test_timestamp_branches.py | 34 ++++++++++++++------------------ 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/tests/test_timestamp_branches.py b/tests/test_timestamp_branches.py index 078088b4c5..39a29775c6 100644 --- a/tests/test_timestamp_branches.py +++ b/tests/test_timestamp_branches.py @@ -382,28 +382,24 @@ def test_bash_specify_feature_prefixed_resolves_by_prefix(self, tmp_path: Path): assert result.stdout.strip() == str(tmp_path / "specs" / "001-target-spec") @pytest.mark.skipif(not _has_pwsh(), reason="pwsh not installed") - def test_ps_specify_feature_prefixed_resolves_by_prefix(self, git_repo: Path): - """PowerShell Get-FeaturePathsEnv: same prefix stripping as bash.""" - common_ps = PROJECT_ROOT / "scripts" / "powershell" / "common.ps1" - spec_dir = git_repo / "specs" / "001-ps-prefix-spec" - spec_dir.mkdir(parents=True) - ps_cmd = f'. "{common_ps}"; $r = Get-FeaturePathsEnv; Write-Output "FEATURE_DIR=$($r.FEATURE_DIR)"' - result = subprocess.run( - ["pwsh", "-NoProfile", "-Command", ps_cmd], - cwd=git_repo, - capture_output=True, - text=True, - env={**os.environ, "SPECIFY_FEATURE": "feat/001-other"}, + def test_ps_spec_file_written_without_bom(self, ps_git_repo: Path): + """spec.md generated by create-new-feature.ps1 must not contain a UTF-8 BOM.""" + result = run_ps_script( + ps_git_repo, "-ShortName", "bom-check", "BOM check feature" ) assert result.returncode == 0, result.stderr - for line in result.stdout.splitlines(): - if line.startswith("FEATURE_DIR="): - val = line.split("=", 1)[1].strip() - assert val == str(spec_dir) - break - else: - pytest.fail("FEATURE_DIR not found in PowerShell output") + specs_root = ps_git_repo / "specs" + spec_file = next(specs_root.rglob("spec.md"), None) if specs_root.exists() else None + assert spec_file is not None, ( + f"spec.md was not created.\nstdout: {result.stdout}\nstderr: {result.stderr}" + ) + + with open(spec_file, "rb") as f: + raw_bytes = f.read(3) + assert raw_bytes != b"\xef\xbb\xbf", ( + f"spec.md contains a UTF-8 BOM — found {raw_bytes!r} at start of file" + ) # ── get_current_branch Tests ───────────────────────────────────────────────── From 46bac7dd0b4f3cde582a64ad103f9b44c845b63b Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Wed, 6 May 2026 23:06:51 +0500 Subject: [PATCH 7/7] fix --- tests/test_timestamp_branches.py | 32 +++++++------------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/tests/test_timestamp_branches.py b/tests/test_timestamp_branches.py index 39a29775c6..1562c7bcd2 100644 --- a/tests/test_timestamp_branches.py +++ b/tests/test_timestamp_branches.py @@ -381,25 +381,6 @@ def test_bash_specify_feature_prefixed_resolves_by_prefix(self, tmp_path: Path): assert result.returncode == 0, result.stderr assert result.stdout.strip() == str(tmp_path / "specs" / "001-target-spec") - @pytest.mark.skipif(not _has_pwsh(), reason="pwsh not installed") - def test_ps_spec_file_written_without_bom(self, ps_git_repo: Path): - """spec.md generated by create-new-feature.ps1 must not contain a UTF-8 BOM.""" - result = run_ps_script( - ps_git_repo, "-ShortName", "bom-check", "BOM check feature" - ) - assert result.returncode == 0, result.stderr - - specs_root = ps_git_repo / "specs" - spec_file = next(specs_root.rglob("spec.md"), None) if specs_root.exists() else None - assert spec_file is not None, ( - f"spec.md was not created.\nstdout: {result.stdout}\nstderr: {result.stderr}" - ) - - with open(spec_file, "rb") as f: - raw_bytes = f.read(3) - assert raw_bytes != b"\xef\xbb\xbf", ( - f"spec.md contains a UTF-8 BOM — found {raw_bytes!r} at start of file" - ) # ── get_current_branch Tests ───────────────────────────────────────────────── @@ -645,7 +626,7 @@ def test_powershell_surfaces_checkout_errors(self): contents = CREATE_FEATURE_PS.read_text(encoding="utf-8") assert "$switchBranchError = git checkout -q $branchName 2>&1 | Out-String" in contents assert "exists but could not be checked out.`n$($switchBranchError.Trim())" in contents - + @pytest.mark.skipif(not _has_pwsh(), reason="pwsh not installed") def test_ps_spec_file_written_without_bom(self, ps_git_repo: Path): """spec.md generated by create-new-feature.ps1 must not contain a UTF-8 BOM.""" @@ -654,17 +635,18 @@ def test_ps_spec_file_written_without_bom(self, ps_git_repo: Path): ) assert result.returncode == 0, result.stderr - # Find the generated spec.md spec_file = next((ps_git_repo / "specs").rglob("spec.md"), None) - assert spec_file is not None, "spec.md was not created" + assert spec_file is not None, ( + f"spec.md was not created.\nstdout: {result.stdout}\nstderr: {result.stderr}" + ) - # Read the first 3 raw bytes and assert no BOM present with open(spec_file, "rb") as f: raw_bytes = f.read(3) assert raw_bytes != b"\xef\xbb\xbf", ( - f"spec.md contains a UTF-8 BOM — found {raw_bytes!r} at start of file" + f"spec.md must not start with a UTF-8 BOM — got first 3 bytes: {raw_bytes!r}" ) + class TestGitExtensionParity: def test_bash_extension_surfaces_checkout_errors(self): """Static guard: git extension bash script preserves checkout stderr.""" @@ -1270,4 +1252,4 @@ def test_ps_feature_json_overrides_branch_lookup(self, git_repo: Path): assert val == str(custom_dir) break else: - pytest.fail("FEATURE_DIR not found in PowerShell output") + pytest.fail("FEATURE_DIR not found in PowerShell output") \ No newline at end of file