diff --git a/.claude/agent-memory/archgate-developer/MEMORY.md b/.claude/agent-memory/archgate-developer/MEMORY.md index b97f417d..50d7b1d1 100644 --- a/.claude/agent-memory/archgate-developer/MEMORY.md +++ b/.claude/agent-memory/archgate-developer/MEMORY.md @@ -40,6 +40,7 @@ Skipping steps 2 or 3 is a workflow violation. The user should NEVER have to inv - **GCM prompt suppression requires 5 env vars** — `GIT_TERMINAL_PROMPT=0` alone does NOT prevent Git Credential Manager (GCM) from showing GUI prompts on Windows or askpass prompts on Linux. The full set for `gitCredentialEnv()` in `src/helpers/credential-store.ts` is: `GIT_TERMINAL_PROMPT=0`, `GCM_INTERACTIVE=never`, `GCM_GUI_PROMPT=false`, `GIT_ASKPASS=""`, `SSH_ASKPASS=""`. Omitting any one can trigger unexpected prompts in editor contexts where the CLI runs as a subprocess. - **Module-level `{ ...Bun.env }` captures env at import time** — Spreading `Bun.env` into a module-level constant freezes the env snapshot. Tests that override `Bun.env.HOME` after import won't affect the constant. Fix: use a function that returns `{ ...Bun.env, ... }` on each call so it picks up test-time overrides. Applied in `src/helpers/credential-store.ts`. - **`Bun.Glob.scan({ dot: false })` silently drops dot-prefixed segments — even on explicit paths** — `dot: false` (the default) skips matches whose path contains a `.`-prefixed segment, including patterns that explicitly name the dir (e.g. `.github/workflows/release.yml`). Behavior also varies across platforms — Windows reliably drops the match while Linux can match the same pattern, so a rule appears to "work in CI" but no-ops locally. For code repos where `.github/`, `.husky/`, `.vscode/` are first-class source dirs, ALWAYS pass `dot: true` to `Bun.Glob.scan()`. Applied in `src/engine/runner.ts` (`ctx.glob`, `ctx.grepFiles`) and `src/engine/git-files.ts` (`resolveScopedFiles`). See archgate/cli#222. +- **PowerShell 5.1 reads BOM-less `.ps1` files as ANSI — never use non-ASCII chars in `install.ps1`** — `install.ps1` has no UTF-8 BOM (first bytes are `# A...`). On Windows PowerShell 5.1, this means the file is decoded as the system codepage (Windows-1252), so multi-byte UTF-8 characters like em-dash (`—`, `\xE2\x80\x94`) get split into garbage bytes that break later string parsing — the parser reports cryptic errors like "string is missing the terminator" on lines that look fine. ALWAYS stick to ASCII (`-`, `--`, straight quotes) in comments and string literals in `install.ps1`. To verify after editing: `[System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path .\install.ps1).Path, [ref]$null, [ref]$errs)`. Same caveat applies to any unsigned `.ps1` file the project distributes. ## Validation Pipeline diff --git a/install.ps1 b/install.ps1 index eb093bec..89c83206 100644 --- a/install.ps1 +++ b/install.ps1 @@ -109,50 +109,105 @@ if ($CurrentPath -notlike "*$InstallDir*") { Write-Host "Restart your terminal for the change to take effect in new sessions." } -# --- Update Git Bash / MSYS2 shell profiles --- +# --- Update shell profiles (Git Bash + PowerShell) --- +# +# These shells share the Windows ecosystem but each have their own profile +# system. We detect existing profiles only (never create new ones) and +# prompt once for the whole batch. -$GitBashProfiles = @() +$ProfileUpdates = @() + +# Git Bash / MSYS2 - first matching rc file wins +$InstallDirPosix = $InstallDir -replace '\\', '/' +if ($InstallDirPosix -match '^([A-Za-z]):') { + $InstallDirPosix = '/' + $Matches[1].ToLower() + $InstallDirPosix.Substring(2) +} +$BashLine = "export PATH=`"${InstallDirPosix}:`$PATH`"" foreach ($f in @("$HOME\.bashrc", "$HOME\.bash_profile", "$HOME\.profile")) { if (Test-Path $f) { - $GitBashProfiles += $f + $ProfileUpdates += [pscustomobject]@{ + Path = $f + Line = $BashLine + Label = "Git Bash" + Match = $InstallDirPosix + } break } } -$InstallDirPosix = $InstallDir -replace '\\', '/' -if ($InstallDirPosix -match '^([A-Za-z]):') { - $InstallDirPosix = '/' + $Matches[1].ToLower() + $InstallDirPosix.Substring(2) +# PowerShell 5.1 (Windows PowerShell) and PowerShell 7+ have separate profile +# paths and can coexist on the same machine - detect both. Use +# GetFolderPath('MyDocuments') so OneDrive-redirected Documents folders work. +try { + $DocsDir = [Environment]::GetFolderPath('MyDocuments') +} catch { + $DocsDir = "$HOME\Documents" +} +$PSLine = "`$env:PATH = `"$InstallDir;`$env:PATH`"" +$PSProfileCandidates = @( + @{ Path = Join-Path $DocsDir "WindowsPowerShell\Microsoft.PowerShell_profile.ps1"; Label = "PowerShell 5.1" }, + @{ Path = Join-Path $DocsDir "PowerShell\Microsoft.PowerShell_profile.ps1"; Label = "PowerShell 7+" } +) +foreach ($entry in $PSProfileCandidates) { + if (Test-Path $entry.Path) { + $ProfileUpdates += [pscustomobject]@{ + Path = $entry.Path + Line = $PSLine + Label = $entry.Label + Match = $InstallDir + } + } } -$PathLine = "export PATH=`"${InstallDirPosix}:`$PATH`"" +# Filter to profiles that don't already reference the install dir. +# Read failures are warnings, not fatal - skip the file and continue. $NeedsUpdate = @() -foreach ($f in $GitBashProfiles) { - if (-not (Select-String -Path $f -SimpleMatch $InstallDirPosix -Quiet)) { - $NeedsUpdate += $f +foreach ($entry in $ProfileUpdates) { + try { + $alreadyHas = Select-String -Path $entry.Path -SimpleMatch $entry.Match -Quiet -ErrorAction Stop + if (-not $alreadyHas) { + $NeedsUpdate += $entry + } + } catch { + Write-Warning "Could not read $($entry.Path): $($_.Exception.Message)" } } if ($NeedsUpdate.Count -gt 0) { Write-Host "" - Write-Host "Detected Git Bash shell profiles to update:" - foreach ($f in $NeedsUpdate) { - Write-Host " $f -> $PathLine" + Write-Host "Detected shell profiles to update:" + foreach ($entry in $NeedsUpdate) { + Write-Host " [$($entry.Label)] $($entry.Path)" + Write-Host " $($entry.Line)" } Write-Host "" $answer = Read-Host "Update these files now? [Y/n]" if ($answer -match '^[nN]') { Write-Host "" - Write-Host "Skipped. To add manually, append this line to your shell profile:" - Write-Host "" - Write-Host " $PathLine" + Write-Host "Skipped. To add manually, append the corresponding line above to each profile." } else { - foreach ($f in $NeedsUpdate) { - Add-Content -Path $f -Value "`n# archgate`n$PathLine" - Write-Host " Updated: $f" + $updated = 0 + $failed = 0 + foreach ($entry in $NeedsUpdate) { + try { + Add-Content -Path $entry.Path -Value "`n# archgate`n$($entry.Line)" -ErrorAction Stop + Write-Host " Updated: $($entry.Path)" + $updated++ + } catch { + Write-Warning "Failed to update $($entry.Path): $($_.Exception.Message)" + Write-Host " Manually append to that file:" + Write-Host " $($entry.Line)" + $failed++ + } } Write-Host "" - Write-Host "Restart Git Bash for the change to take effect." + if ($updated -gt 0) { + Write-Host "Restart the relevant shell(s) for the change to take effect." + } + if ($failed -gt 0) { + Write-Host "$failed profile(s) could not be updated automatically - see warnings above." + } } }