Files
claude-statusline-kit/install.ps1
T
André BezekandClaude Opus 4.8 104903e29d fix: fall back to Windows PowerShell 5.1 when pwsh is missing
The installer wrote a hardcoded pwsh.exe path into settings.json even when
PowerShell 7 was not installed, so Claude Code ran a nonexistent binary and
the statusline silently failed to render. Now detect the available host
(prefer pwsh 7, fall back to Windows PowerShell 5.1 which ships with every
Windows install) and add an end-to-end self-test so a broken host path is
caught at install time instead of silently at runtime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C4QKwZ7bC7oCMVtnuJdDKm
2026-07-19 12:19:27 +02:00

213 lines
9.7 KiB
PowerShell

#requires -Version 5.1
<#
install.ps1 — Deploys the Claude Code "cship" statusline onto this machine.
Idempotent: safe to run repeatedly. Copies the three assets to their target
locations, auto-detects the local pwsh path, and merges the statusLine block
into an existing ~/.claude/settings.json without touching other settings.
#>
[CmdletBinding()]
param(
# Overwrite an existing cship.toml even if one is already present.
[switch]$Force,
# Skip the Nerd-Font check/install entirely.
[switch]$SkipFont,
# Nerd Font to install if none is present (matches a ryanoasis/nerd-fonts
# release asset name). Default 'Hack' is small (~17 MB); others: FiraCode,
# CascadiaCode, JetBrainsMono (large, ~123 MB), SpaceMono, Meslo.
[string]$FontName = 'Hack'
)
$ErrorActionPreference = 'Stop'
$assets = Join-Path $PSScriptRoot 'assets'
function Write-Step { param($m) Write-Host " $m" -ForegroundColor Cyan }
function Write-Ok { param($m) Write-Host " OK $m" -ForegroundColor Green }
function Write-Warn { param($m) Write-Host " !! $m" -ForegroundColor Yellow }
# Install a Nerd Font for the current user only (no admin rights): drop the .ttf
# files into the per-user font store and register them under HKCU. Available to
# apps launched after a terminal restart. Any failure is non-fatal — the
# statusline renders via the OS emoji fallback regardless.
function Install-NerdFont {
param([string]$FontName)
$installed = @()
foreach ($k in @(
'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts',
'HKCU:\Software\Microsoft\Windows NT\CurrentVersion\Fonts')) {
if (Test-Path $k) { $installed += (Get-Item $k).Property }
}
# Match both "Hack Nerd Font" (double-click install) and "HackNerdFont-..."
# (the per-user name we register from the file stem).
if ($installed | Where-Object { $_ -match 'Nerd\s*Font' }) {
Write-Ok "A Nerd Font is already installed - skipping font download."
return
}
$family = "$FontName Nerd Font"
Write-Step "No Nerd Font found. Installing '$family' for the current user (no admin needed)..."
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
} catch { }
$url = "https://github.com/ryanoasis/nerd-fonts/releases/latest/download/$FontName.zip"
$tmp = Join-Path ([IO.Path]::GetTempPath()) ('nf_' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
$zip = Join-Path $tmp 'font.zip'
try {
Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing
Expand-Archive -Path $zip -DestinationPath $tmp -Force
} catch {
Write-Warn "Font download/extract failed: $($_.Exception.Message)"
Write-Warn "Skipping font install - the statusline still works via emoji fallback."
Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue
return
}
# Prefer the base variant (single-width spacing files carry 'Mono'/'Propo').
$ttfs = Get-ChildItem $tmp -Recurse -Filter '*.ttf' | Where-Object { $_.Name -match 'NerdFont-' }
if (-not $ttfs) { $ttfs = Get-ChildItem $tmp -Recurse -Filter '*.ttf' }
$fontDir = Join-Path $env:LOCALAPPDATA 'Microsoft\Windows\Fonts'
New-Item -ItemType Directory -Force -Path $fontDir | Out-Null
$regKey = 'HKCU:\Software\Microsoft\Windows NT\CurrentVersion\Fonts'
if (-not (Test-Path $regKey)) { New-Item -Path $regKey -Force | Out-Null }
$count = 0
foreach ($ttf in $ttfs) {
$dest = Join-Path $fontDir $ttf.Name
try {
Copy-Item $ttf.FullName $dest -Force -ErrorAction Stop
} catch {
# A file already in use (loaded by a running app) is fine - it exists.
if (-not (Test-Path $dest)) { throw }
}
New-ItemProperty -Path $regKey -Name "$($ttf.BaseName) (TrueType)" -Value $dest -PropertyType String -Force | Out-Null
$count++
}
Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue
if ($count -gt 0) {
Write-Ok "Installed $count '$family' file(s) into the per-user font store."
Write-Warn "To use them, set your terminal font to '$family' and restart the terminal."
} else {
Write-Warn "Archive contained no .ttf files - font skipped."
}
}
Write-Host "`nClaude Code statusline installer" -ForegroundColor White
# --- Target locations -------------------------------------------------------
$localBin = Join-Path $env:USERPROFILE '.local\bin'
$config = Join-Path $env:USERPROFILE '.config'
$claude = Join-Path $env:USERPROFILE '.claude'
foreach ($d in @($localBin, $config, $claude)) {
New-Item -ItemType Directory -Force -Path $d | Out-Null
}
# --- 1. cship.exe -----------------------------------------------------------
Copy-Item (Join-Path $assets 'cship.exe') (Join-Path $localBin 'cship.exe') -Force
Write-Ok "cship.exe -> $localBin"
# --- 2. cship.toml (do not clobber a customised config unless -Force) --------
$tomlTarget = Join-Path $config 'cship.toml'
if ((Test-Path $tomlTarget) -and -not $Force) {
Write-Warn "cship.toml already exists, kept as-is (use -Force to overwrite)"
} else {
Copy-Item (Join-Path $assets 'cship.toml') $tomlTarget -Force
Write-Ok "cship.toml -> $config"
}
# --- 3. Wrapper script ------------------------------------------------------
$wrapperTarget = Join-Path $claude 'cship-statusline.ps1'
Copy-Item (Join-Path $assets 'cship-statusline.ps1') $wrapperTarget -Force
Write-Ok "cship-statusline.ps1 -> $claude"
# --- 4. Locate a PowerShell host --------------------------------------------
# Prefer PowerShell 7 (pwsh). If it isn't installed, fall back to Windows
# PowerShell 5.1 (powershell.exe) which ships with every Windows install and
# runs the wrapper correctly - so the statusline works out of the box instead
# of pointing at a non-existent pwsh.exe and silently rendering nothing.
$pwsh = (Get-Command pwsh -ErrorAction SilentlyContinue).Source
if (-not $pwsh -and (Test-Path 'C:\Program Files\PowerShell\7\pwsh.exe')) {
$pwsh = 'C:\Program Files\PowerShell\7\pwsh.exe'
}
if (-not $pwsh) {
$pwsh = (Get-Command powershell -ErrorAction SilentlyContinue).Source
if (-not $pwsh) {
$pwsh = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
}
Write-Warn "PowerShell 7 (pwsh) not found - using Windows PowerShell 5.1."
Write-Warn " For sharper rendering, install PS7 and re-run this script:"
Write-Warn " winget install --id Microsoft.PowerShell -e"
}
$pwshFwd = $pwsh.Replace('\', '/')
$wrapperFwd = $wrapperTarget.Replace('\', '/')
$command = '"{0}" -NoProfile -ExecutionPolicy Bypass -File "{1}"' -f $pwshFwd, $wrapperFwd
# --- 5. Merge statusLine into settings.json ---------------------------------
$settingsPath = Join-Path $claude 'settings.json'
if (Test-Path $settingsPath) {
Copy-Item $settingsPath "$settingsPath.bak" -Force
$settings = Get-Content $settingsPath -Raw | ConvertFrom-Json
} else {
$settings = [PSCustomObject]@{}
}
$statusLine = [PSCustomObject]@{ type = 'command'; command = $command }
if ($settings.PSObject.Properties.Name -contains 'statusLine') {
$settings.statusLine = $statusLine
} else {
$settings | Add-Member -MemberType NoteProperty -Name 'statusLine' -Value $statusLine
}
$settings | ConvertTo-Json -Depth 20 | Set-Content $settingsPath -Encoding UTF8
Write-Ok "settings.json statusLine wired to $([IO.Path]::GetFileName($pwsh))"
# --- 5b. End-to-end self-test -----------------------------------------------
# Actually run the configured host + wrapper against a sample Status payload so
# a broken host path (the classic "no statusline appears" failure) is caught
# here rather than silently at runtime, where Claude Code just shows nothing.
$sample = '{"hook_event_name":"Status","model":{"display_name":"Opus"},' +
'"workspace":{"current_dir":"' + ($claude -replace '\\','/') + '",' +
'"project_dir":"' + ($claude -replace '\\','/') + '"},' +
'"context_window":{"used_tokens":1000}}'
try {
$probe = $sample | & $pwsh -NoProfile -ExecutionPolicy Bypass -File $wrapperTarget 2>$null
if ([string]::IsNullOrWhiteSpace(($probe -join ''))) {
Write-Warn "Self-test produced no output - the statusline may not render."
Write-Warn " Check that '$pwsh' runs and that cship.exe is present."
} else {
Write-Ok "Self-test passed - statusline renders."
}
} catch {
Write-Warn "Self-test could not run the host: $($_.Exception.Message)"
}
# --- 6. Nerd Font (safety net, per-user) ------------------------------------
if ($SkipFont) {
Write-Warn "Font step skipped (-SkipFont)."
} else {
Install-NerdFont -FontName $FontName
}
# --- Glyph self-test --------------------------------------------------------
# The only thing this script cannot check is whether the terminal actually
# draws the glyphs. Print the exact symbol set so the user can eyeball it: if
# any show as boxes/question marks, the terminal (not the font) is the problem.
Write-Host "`nGlyph self-test - these should all be real symbols, not boxes:" -ForegroundColor White
Write-Host " 🤖 ⚡ 🧠 🔢 ⏰ ⏳ 📅 █ ░" -ForegroundColor Cyan
Write-Host " If any appear as boxes/? , switch to Windows Terminal (Win11 default)."
# --- Post-install notes -----------------------------------------------------
Write-Host "`nDone." -ForegroundColor Green
Write-Host "Notes:" -ForegroundColor White
Write-Host " -> Symbols are standard Unicode (emoji + block chars). Windows Terminal renders"
Write-Host " them via Segoe UI Emoji by default - no special font needed. The Nerd Font"
Write-Host " above is an optional safety net."
Write-Host " Restart Claude Code (or start a new session) to see the statusline.`n"