From d162b6a0fd11004f966744dff6750c2ad88451d0 Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Sun, 26 Jul 2026 21:22:37 +0500 Subject: [PATCH 1/2] fix(powershell): resolve SPECIFY_INIT_DIR without .NET Core-only API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit common.ps1 resolved the SPECIFY_INIT_DIR root with [System.IO.Path]::TrimEndingDirectorySeparator, which only exists on .NET Core. On Windows PowerShell 5.1 (.NET Framework) the call throws 'Method invocation failed ... does not contain a method named TrimEndingDirectorySeparator', so root resolution fails before any command runs whenever SPECIFY_INIT_DIR is set. PowerShell 7+ users are unaffected, which is why it went unnoticed. The same file already documents this incompatibility ~150 lines later and uses .TrimEnd('/','\') there. Apply the same approach here, guarding the one case where the two differ — a bare drive root ('C:\'), where TrimEnd would strip the separator and yield an invalid 'C:' — by preserving the original. The existing @requires_pwsh tests in tests/test_init_dir.py already exercise this path (trailing-slash, relative, precedence, compose); they now pass on Windows PowerShell 5.1 as well as pwsh 7+. Fixes #3749 --- scripts/powershell/common.ps1 | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/powershell/common.ps1 b/scripts/powershell/common.ps1 index afc226ea00..7e408e3a82 100644 --- a/scripts/powershell/common.ps1 +++ b/scripts/powershell/common.ps1 @@ -55,9 +55,15 @@ function Resolve-SpecifyInitDir { } # Resolve-Path echoes back any trailing separator from the input; trim it so # the returned root matches the bash resolver, whose `cd && pwd` never yields - # one. TrimEndingDirectorySeparator is a no-op on a bare root and on a path - # that already has no trailing separator. - $initRoot = [System.IO.Path]::TrimEndingDirectorySeparator($resolved.Path) + # one. Use TrimEnd (not [Path]::TrimEndingDirectorySeparator, which is .NET + # Core only and throws on Windows PowerShell 5.1 / .NET Framework — see the + # matching note further below). TrimEnd is a no-op on a path with no trailing + # separator; the one difference is a bare drive root (`C:\` -> `C:`), so keep + # that separator to preserve a valid rooted path. + $initRoot = $resolved.Path.TrimEnd('/', '\') + if ($initRoot -match '^[A-Za-z]:$') { + $initRoot = $resolved.Path + } if (-not (Test-Path -LiteralPath (Join-Path $initRoot '.specify') -PathType Container)) { [Console]::Error.WriteLine("ERROR: SPECIFY_INIT_DIR is not a Spec Kit project (no .specify/ directory): $initRoot") if ($ReturnNullOnError) { return $null } From bcb6eaf58742d0a4abfb677f3f16342477fc66b0 Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Tue, 28 Jul 2026 12:53:45 +0500 Subject: [PATCH 2/2] fix(powershell): preserve a filesystem root when trimming SPECIFY_INIT_DIR Address review feedback: the drive-root guard was Windows-specific. Under PowerShell on Linux/macOS, Resolve-Path '/' returns '/', so TrimEnd('/','\') produced an empty string and the following Join-Path failed. Treat any all-separator root as a no-op, covering both 'C:\' and '/'. --- scripts/powershell/common.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/powershell/common.ps1 b/scripts/powershell/common.ps1 index 7e408e3a82..b965aab301 100644 --- a/scripts/powershell/common.ps1 +++ b/scripts/powershell/common.ps1 @@ -60,8 +60,12 @@ function Resolve-SpecifyInitDir { # matching note further below). TrimEnd is a no-op on a path with no trailing # separator; the one difference is a bare drive root (`C:\` -> `C:`), so keep # that separator to preserve a valid rooted path. + # A filesystem root is all separator: trimming it would leave an empty (or + # drive-only) string and break the Join-Path below. Covers the Windows + # drive root (`C:\` -> `C:`) and the POSIX root (`/` -> ``), which + # PowerShell on Linux/macOS can return. $initRoot = $resolved.Path.TrimEnd('/', '\') - if ($initRoot -match '^[A-Za-z]:$') { + if (-not $initRoot -or $initRoot -match '^[A-Za-z]:$') { $initRoot = $resolved.Path } if (-not (Test-Path -LiteralPath (Join-Path $initRoot '.specify') -PathType Container)) {