From 7fb09c9945e811ecb121f89701daf1d66dd46c0e Mon Sep 17 00:00:00 2001 From: MasonStation Date: Sun, 26 Apr 2026 16:02:30 -0400 Subject: [PATCH 1/2] fix(install): hand off Phantom install to phantom's own one-liner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous code shelled out to `bun add -g phantom-secrets` / `npm i -g phantom-secrets`. On a fresh Windows machine that path silently misbehaves in two ways: 1. PS5.1 + native-stderr-as-error: Stack invoked bun under `try/catch` with `EAP=Stop` and `*> $null`. In Windows PowerShell 5.1 that turns bun's normal "Resolving dependencies" stderr line into a `NativeCommandError` that throws — so even though bun exits 0 and installs the package, Stack reports failure. 2. Bun-on-Windows shim non-generation: even when bun *does* succeed, `~\.bun\bin` ends up containing only `bun.exe` — the `phantom` shim from phantom-secrets' `bin` field never lands. Net effect: `phantom` is not on PATH after a "successful" install. Switching to phantom's own curl/irm installer: - Downloads the signed GitHub release directly (no lazy npm wrapper) - Wires User PATH itself - Sidesteps both PS5.1 and bun-on-Windows quirks - Keeps Homebrew preference on macOS (it's still the canonical path there, and gives users `brew upgrade` for free) - Refreshes `$env:Path` from the registry after install so subsequent Stack steps see the new entries Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/site/public/install.ps1 | 29 +++++++++++++++++++---------- packages/site/public/install.sh | 23 +++++++++++++++++------ scripts/install.ps1 | 29 +++++++++++++++++++---------- scripts/install.sh | 23 +++++++++++++++++------ 4 files changed, 72 insertions(+), 32 deletions(-) diff --git a/packages/site/public/install.ps1 b/packages/site/public/install.ps1 index cfeb04b..da4b4b9 100644 --- a/packages/site/public/install.ps1 +++ b/packages/site/public/install.ps1 @@ -66,18 +66,27 @@ function Install-AshlrStack { # ----------------------------------------------------------------------- if (-not (Test-CommandExists 'phantom')) { - Write-StackSay 'Phantom Secrets not found -- installing...' - # No Homebrew on Windows. Fall back to npm/bun global install. + Write-StackSay 'Phantom Secrets not found -- installing via phantom installer...' + # Use phantom's own one-liner. This sidesteps two real bugs in the + # bun/npm path on Windows: + # 1. Bun-on-Windows doesn't reliably materialize the `phantom` shim + # from the npm package's `bin` field, so even a successful + # `bun add -g phantom-secrets` leaves nothing on PATH. + # 2. The PS5.1 native-stderr-as-error trap: redirecting bun's stderr + # with `*>` inside try/catch + EAP=Stop turns benign progress + # output into spurious throws. + # Phantom's installer downloads the signed release directly and wires + # User PATH itself, so it works whether bun is healthy or not. try { - if ($pkgMgr -eq 'bun') { - & bun add -g phantom-secrets *> $null - if ($LASTEXITCODE -ne 0) { throw 'bun add -g phantom-secrets failed' } - } else { - & npm i -g phantom-secrets *> $null - if ($LASTEXITCODE -ne 0) { throw 'npm i -g phantom-secrets failed' } - } + $phantomScript = Invoke-RestMethod 'https://phm.dev/install.ps1' -UseBasicParsing + $phantomScript | & powershell.exe -NoProfile -ExecutionPolicy Bypass -Command - + if ($LASTEXITCODE -ne 0) { throw "phantom installer exited $LASTEXITCODE" } + # phantom's installer modified User PATH; refresh this session so + # subsequent commands can see the new entries. + $env:Path = ([Environment]::GetEnvironmentVariable('Path','User')) + ';' + + ([Environment]::GetEnvironmentVariable('Path','Machine')) } catch { - Write-StackWarn "phantom-secrets install failed -- continuing; install manually later. ($_)" + Write-StackWarn "phantom-secrets install failed -- continuing; install manually from https://phm.dev. ($_)" } } else { Write-StackSay 'phantom already installed -- good.' diff --git a/packages/site/public/install.sh b/packages/site/public/install.sh index 5dba9fa..f9ea595 100755 --- a/packages/site/public/install.sh +++ b/packages/site/public/install.sh @@ -76,13 +76,24 @@ say "using $PKG_MGR" if ! command -v phantom >/dev/null 2>&1; then say "Phantom Secrets not found — installing…" + installed=0 + # Prefer Homebrew on systems that have it (best UX for updates). if command -v brew >/dev/null 2>&1; then - brew tap ashlrai/phantom 2>/dev/null || true - brew install phantom || warn "brew install failed — continuing; install manually later." - elif [ "$PKG_MGR" = "bun" ]; then - bun add -g phantom-secrets 2>/dev/null || warn "bun add -g phantom-secrets failed — install manually later." - else - npm i -g phantom-secrets 2>/dev/null || warn "npm i -g phantom-secrets failed — install manually later." + if brew tap ashlrai/phantom >/dev/null 2>&1 && brew install phantom >/dev/null 2>&1; then + installed=1 + fi + fi + # Fall back to phantom's official one-liner. This avoids `bun add -g` + # /`npm i -g` because the npm package lazy-downloads on first run and + # bun on Windows doesn't reliably materialize the shim either way. + if [ "$installed" -ne 1 ]; then + if curl -fsSL https://phm.dev/install.sh | bash; then + [ -d "$HOME/.phantom-secrets/bin" ] && export PATH="$HOME/.phantom-secrets/bin:$PATH" + installed=1 + fi + fi + if [ "$installed" -ne 1 ]; then + warn "phantom-secrets install failed — install manually from https://phm.dev" fi else say "phantom already installed — good." diff --git a/scripts/install.ps1 b/scripts/install.ps1 index cfeb04b..da4b4b9 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -66,18 +66,27 @@ function Install-AshlrStack { # ----------------------------------------------------------------------- if (-not (Test-CommandExists 'phantom')) { - Write-StackSay 'Phantom Secrets not found -- installing...' - # No Homebrew on Windows. Fall back to npm/bun global install. + Write-StackSay 'Phantom Secrets not found -- installing via phantom installer...' + # Use phantom's own one-liner. This sidesteps two real bugs in the + # bun/npm path on Windows: + # 1. Bun-on-Windows doesn't reliably materialize the `phantom` shim + # from the npm package's `bin` field, so even a successful + # `bun add -g phantom-secrets` leaves nothing on PATH. + # 2. The PS5.1 native-stderr-as-error trap: redirecting bun's stderr + # with `*>` inside try/catch + EAP=Stop turns benign progress + # output into spurious throws. + # Phantom's installer downloads the signed release directly and wires + # User PATH itself, so it works whether bun is healthy or not. try { - if ($pkgMgr -eq 'bun') { - & bun add -g phantom-secrets *> $null - if ($LASTEXITCODE -ne 0) { throw 'bun add -g phantom-secrets failed' } - } else { - & npm i -g phantom-secrets *> $null - if ($LASTEXITCODE -ne 0) { throw 'npm i -g phantom-secrets failed' } - } + $phantomScript = Invoke-RestMethod 'https://phm.dev/install.ps1' -UseBasicParsing + $phantomScript | & powershell.exe -NoProfile -ExecutionPolicy Bypass -Command - + if ($LASTEXITCODE -ne 0) { throw "phantom installer exited $LASTEXITCODE" } + # phantom's installer modified User PATH; refresh this session so + # subsequent commands can see the new entries. + $env:Path = ([Environment]::GetEnvironmentVariable('Path','User')) + ';' + + ([Environment]::GetEnvironmentVariable('Path','Machine')) } catch { - Write-StackWarn "phantom-secrets install failed -- continuing; install manually later. ($_)" + Write-StackWarn "phantom-secrets install failed -- continuing; install manually from https://phm.dev. ($_)" } } else { Write-StackSay 'phantom already installed -- good.' diff --git a/scripts/install.sh b/scripts/install.sh index 5dba9fa..f9ea595 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -76,13 +76,24 @@ say "using $PKG_MGR" if ! command -v phantom >/dev/null 2>&1; then say "Phantom Secrets not found — installing…" + installed=0 + # Prefer Homebrew on systems that have it (best UX for updates). if command -v brew >/dev/null 2>&1; then - brew tap ashlrai/phantom 2>/dev/null || true - brew install phantom || warn "brew install failed — continuing; install manually later." - elif [ "$PKG_MGR" = "bun" ]; then - bun add -g phantom-secrets 2>/dev/null || warn "bun add -g phantom-secrets failed — install manually later." - else - npm i -g phantom-secrets 2>/dev/null || warn "npm i -g phantom-secrets failed — install manually later." + if brew tap ashlrai/phantom >/dev/null 2>&1 && brew install phantom >/dev/null 2>&1; then + installed=1 + fi + fi + # Fall back to phantom's official one-liner. This avoids `bun add -g` + # /`npm i -g` because the npm package lazy-downloads on first run and + # bun on Windows doesn't reliably materialize the shim either way. + if [ "$installed" -ne 1 ]; then + if curl -fsSL https://phm.dev/install.sh | bash; then + [ -d "$HOME/.phantom-secrets/bin" ] && export PATH="$HOME/.phantom-secrets/bin:$PATH" + installed=1 + fi + fi + if [ "$installed" -ne 1 ]; then + warn "phantom-secrets install failed — install manually from https://phm.dev" fi else say "phantom already installed — good." From e16e3703a1954cd2d11e8772d46ea5ede24dc3d2 Mon Sep 17 00:00:00 2001 From: MasonStation Date: Mon, 27 Apr 2026 14:03:08 -0400 Subject: [PATCH 2/2] fix(install): make Windows install work in Git Bash / Claude Code Three bugs surfaced when running `irm stack.ashlr.ai/install.ps1 | iex` on Windows from Claude Code (which shells out through Git Bash): 1. Bare-name lookup failed. We only wrote `stack.cmd` / `ashlr-stack-mcp.cmd` shims. cmd.exe and PowerShell resolve those, but MSYS2 bash's PATH lookup does NOT auto-append .cmd, so `stack` came back as command-not-found even with the bin dir on PATH. Now write a sibling extensionless shim per command -- a tiny `#!/usr/bin/env bash` script (LF line endings; CRLF after the shebang would make /usr/bin/env try to exec Assisted-By: ashlr-plugin "bash\r" and fail). 2. Already-running bash shells didn't pick up the new Windows User PATH until restart. Append an idempotent `export PATH=...` to ~/.bashrc with the unix-style path so Git Bash sees it on next session-start. 3. README one-liners used `irm stack.ashlr.ai/install.ps1` (no scheme). PowerShell 5.1 defaults to http://, the host 308-redirects to https://, and Invoke-RestMethod refuses cross-scheme redirects. Match the working install.ps1 banner: explicit https://. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 2 +- packages/cli/README.md | 2 +- packages/mcp/README.md | 2 +- scripts/install.ps1 | 109 +++++++++++++++++++++++++++++++++++------ 4 files changed, 96 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 30b97ef..ef43327 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ curl -fsSL stack.ashlr.ai/install.sh | bash ```powershell # One-liner, Windows (PowerShell) -irm stack.ashlr.ai/install.ps1 | iex +irm https://stack.ashlr.ai/install.ps1 | iex ``` ```bash diff --git a/packages/cli/README.md b/packages/cli/README.md index 12f4b34..c99678a 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -25,7 +25,7 @@ One-liner (recommended): curl -fsSL stack.ashlr.ai/install.sh | bash # Windows (PowerShell) -irm stack.ashlr.ai/install.ps1 | iex +irm https://stack.ashlr.ai/install.ps1 | iex ``` Or install manually from the registry: diff --git a/packages/mcp/README.md b/packages/mcp/README.md index 4421ecd..e704bdf 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -15,7 +15,7 @@ One-liner (installs both `stack` and `ashlr-stack-mcp`): curl -fsSL stack.ashlr.ai/install.sh | bash # Windows (PowerShell) -irm stack.ashlr.ai/install.ps1 | iex +irm https://stack.ashlr.ai/install.ps1 | iex ``` Or manually from the registry: diff --git a/scripts/install.ps1 b/scripts/install.ps1 index da4b4b9..44e6f61 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -28,6 +28,45 @@ function Test-CommandExists { return [bool](Get-Command $Name -ErrorAction SilentlyContinue) } +# Convert a Windows path (C:\Users\foo\bin) to a Git Bash / MSYS path +# (/c/Users/foo/bin). Git Bash inherits the Windows user PATH, but only at +# shell-start time -- so an already-running bash (e.g. inside Claude Code on +# Windows) won't see new entries until the session restarts. Writing an +# explicit export to ~/.bashrc with the unix-style path covers that gap and +# also handles users whose Git Bash was launched with a sanitized PATH. +function ConvertTo-BashPath { + param([Parameter(Mandatory)][string]$WinPath) + $p = $WinPath -replace '\\', '/' + if ($p -match '^([A-Za-z]):/(.*)$') { + $drive = $Matches[1].ToLower() + return "/$drive/$($Matches[2])" + } + return $p +} + +# Append `export PATH=":$PATH"` to the user's ~/.bashrc (idempotent +# via a marker comment) so Git Bash sessions pick up the stack bin dir. +# Best-effort: skips silently if no HOME / no writable bashrc. +function Add-ToBashrcPath { + param([Parameter(Mandatory)][string]$WinBinDir) + # NB: $home is a PowerShell read-only automatic variable -- using a different name. + $homeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE } + if (-not $homeDir) { return } + $bashrc = Join-Path $homeDir '.bashrc' + $bashPath = ConvertTo-BashPath -WinPath $WinBinDir + $marker = "# ashlr-stack PATH ($bashPath)" + try { + if ((Test-Path $bashrc) -and (Select-String -Path $bashrc -SimpleMatch $marker -Quiet -ErrorAction SilentlyContinue)) { + return + } + $line = "`n$marker`nexport PATH=`"$bashPath`:`$PATH`"`n" + Add-Content -LiteralPath $bashrc -Value $line -Encoding UTF8 + Write-StackSay "wired $bashPath into $bashrc (for Git Bash / Claude Code)" + } catch { + Write-StackWarn "could not update $bashrc -- add '$bashPath' to your bash PATH manually. ($_)" + } +} + function Install-AshlrStack { $RepoUrl = if ($env:STACK_REPO_URL) { $env:STACK_REPO_URL } else { 'https://github.com/ashlrai/ashlr-stack.git' } $InstallDir = if ($env:STACK_INSTALL_DIR) { $env:STACK_INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA 'ashlr-stack' } @@ -103,7 +142,22 @@ function Install-AshlrStack { } else { & npm i -g '@ashlr/stack' 'ashlr-stack-mcp' *> $null } - return ($LASTEXITCODE -eq 0) + if ($LASTEXITCODE -ne 0) { return $false } + + # Where the freshly-globally-installed shim lands. bun -> ~/.bun/bin, + # npm -> %APPDATA%\npm. Both put themselves on Windows User PATH at + # tool-install time, but already-running shells (Git Bash, the + # Claude Code session that just kicked this off) won't see it until + # restart. Mirror the entry into ~/.bashrc so bash picks it up. + $globalBin = if ($pkgMgr -eq 'bun') { + Join-Path $env:USERPROFILE '.bun\bin' + } else { + Join-Path $env:APPDATA 'npm' + } + if (Test-Path $globalBin) { + Add-ToBashrcPath -WinBinDir $globalBin + } + return $true } catch { return $false } @@ -167,21 +221,43 @@ function Install-AshlrStack { New-Item -ItemType Directory -Path $binDir -Force | Out-Null } - # stack.cmd shim -- .cmd so it's picked up by cmd.exe AND PowerShell. - $stackShim = Join-Path $binDir 'stack.cmd' - $cliEntry = Join-Path $InstallDir 'packages\cli\src\index.ts' - @" -@echo off -bun run "$cliEntry" %* -"@ | Set-Content -LiteralPath $stackShim -Encoding ASCII - - # Same deal for the MCP server. - $mcpShim = Join-Path $binDir 'ashlr-stack-mcp.cmd' + # We write TWO shims per command: + # 1. .cmd -- picked up by cmd.exe and PowerShell. + # 2. -- bare-name shell script with a bash shebang, picked + # up by Git Bash. MSYS2 bash's PATH lookup does NOT + # auto-append .cmd, so a user (or Claude Code, which + # shells out through Git Bash on Windows) typing + # `stack` would otherwise get "command not found" + # even though the bin dir is on PATH. + # The bash shim MUST use LF line endings -- a CRLF after the shebang + # makes /usr/bin/env try to exec "bash\r" and fail with ENOENT. + + function Write-Shim { + param( + [Parameter(Mandatory)][string]$BinDir, + [Parameter(Mandatory)][string]$Name, + [Parameter(Mandatory)][string]$EntryWinPath + ) + $cmdShim = Join-Path $BinDir "$Name.cmd" + $cmdBody = "@echo off`r`nbun run `"$EntryWinPath`" %*`r`n" + [System.IO.File]::WriteAllText($cmdShim, $cmdBody, [System.Text.UTF8Encoding]::new($false)) + + $bashShim = Join-Path $BinDir $Name + $entryBash = ConvertTo-BashPath -WinPath $EntryWinPath + $bashBody = "#!/usr/bin/env bash`nexec bun run `"$entryBash`" `"`$@`"`n" + [System.IO.File]::WriteAllText($bashShim, $bashBody, [System.Text.UTF8Encoding]::new($false)) + } + + $cliEntry = Join-Path $InstallDir 'packages\cli\src\index.ts' $mcpEntry = Join-Path $InstallDir 'packages\mcp\src\server.ts' - @" -@echo off -bun run "$mcpEntry" %* -"@ | Set-Content -LiteralPath $mcpShim -Encoding ASCII + Write-Shim -BinDir $binDir -Name 'stack' -EntryWinPath $cliEntry + Write-Shim -BinDir $binDir -Name 'ashlr-stack-mcp' -EntryWinPath $mcpEntry + $stackShim = Join-Path $binDir 'stack.cmd' + + # Mirror the bin dir into Git Bash's PATH too, so an already-running + # bash (Claude Code on Windows runs commands through Git Bash) picks it + # up on next session start. + Add-ToBashrcPath -WinBinDir $binDir Write-StackSay "stack shim written to $stackShim" } @@ -199,7 +275,8 @@ bun run "$mcpEntry" %* # ----------------------------------------------------------------------- if (-not (Test-CommandExists 'stack')) { - Write-StackWarn 'stack binary installed and PATH updated. Open a new shell and re-run `stack --help`.' + Write-StackWarn 'stack installed and PATH updated, but not yet visible in this shell.' + Write-StackWarn 'Restart your terminal -- and if you use Claude Code, restart that session too -- then run: stack --help' return }