#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"