<# One-time installer for claude-mode. Installs to %USERPROFILE%\.claude-mode, drops a cmd.exe shim on PATH, and wires `claude-mode` + a `claude` wrapper into the PowerShell profile. Idempotent: safe to re-run to upgrade. Existing presets are not overwritten unless -Force is passed. Your ~/.claude/settings.json is NOT touched here - that only happens when you actually run `claude-mode openrouter|anthropic`. #> [CmdletBinding()] param( [switch] $Force, # overwrite existing presets [switch] $SkipKeyPrompt # do not prompt for the OpenRouter key ) Set-StrictMode -Version 1.0 $ErrorActionPreference = 'Stop' $repoUrl = 'https://git.nebulm.com/smoido/claude-mode' # Running from a checkout, the source is the files next to this script. Piped # in (irm ... | iex) there is no script file and no checkout, so fetch the # repository archive and install from that instead. $src = $null if ($PSScriptRoot -and (Test-Path -LiteralPath (Join-Path $PSScriptRoot 'claude-mode.ps1'))) { $src = $PSScriptRoot } else { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 $tmp = Join-Path $env:TEMP ('claude-mode-src-' + [IO.Path]::GetRandomFileName().Replace('.', '')) New-Item -ItemType Directory -Path $tmp -Force | Out-Null $zip = Join-Path $tmp 'repo.zip' Write-Host "no local checkout found - fetching source from $repoUrl" -ForegroundColor Cyan Invoke-WebRequest -Uri "$repoUrl/archive/master.zip" -OutFile $zip -UseBasicParsing Expand-Archive -LiteralPath $zip -DestinationPath $tmp # Forgejo nests everything under a repo-named folder; find it wherever it is. $extracted = Get-ChildItem -LiteralPath $tmp -Directory | Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'claude-mode.ps1') } | Select-Object -First 1 if ($extracted) { $src = $extracted.FullName } elseif (Test-Path -LiteralPath (Join-Path $tmp 'claude-mode.ps1')) { $src = $tmp } else { throw 'archive did not contain claude-mode.ps1 - repo layout changed?' } } $root = Join-Path $env:USERPROFILE '.claude-mode' Write-Host "installing claude-mode -> $root" -ForegroundColor Cyan # --- 1. directories --------------------------------------------------------- foreach ($d in @($root, "$root\presets", "$root\vault", "$root\backups", "$root\bin")) { if (-not (Test-Path -LiteralPath $d)) { New-Item -ItemType Directory -Path $d -Force | Out-Null } } # Lock the whole tree to the current user. The vault is DPAPI-encrypted on top # of this, but there is no reason for the directory to be broadly readable. try { $acl = Get-Acl -LiteralPath $root if ($acl.AreAccessRulesProtected) { # Already locked down by a previous run. Re-applying would need # SeSecurityPrivilege on some systems, so leave it alone. Write-Host ' ok .claude-mode ACL already locked to current user' -ForegroundColor Green throw [System.OperationCanceledException]::new('already-protected') } $acl.SetAccessRuleProtection($true, $false) foreach ($r in @($acl.Access)) { [void]$acl.RemoveAccessRule($r) } $me = New-Object System.Security.Principal.NTAccount($env:USERDOMAIN, $env:USERNAME) $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule( $me, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))) Set-Acl -LiteralPath $root -AclObject $acl Write-Host ' ok locked .claude-mode ACL to current user' -ForegroundColor Green } catch [System.OperationCanceledException] { # already protected - nothing to do } catch { Write-Host " warn ACL hardening failed: $($_.Exception.Message)" -ForegroundColor Yellow } # --- 2. scripts ------------------------------------------------------------- Copy-Item -LiteralPath (Join-Path $src 'claude-mode.ps1') -Destination $root -Force # claude-mode reads VERSION from its own directory to stamp health.json. if (Test-Path -LiteralPath (Join-Path $src 'VERSION')) { Copy-Item -LiteralPath (Join-Path $src 'VERSION') -Destination $root -Force } Copy-Item -LiteralPath (Join-Path $src 'bin\claude-key-helper.ps1') -Destination "$root\bin" -Force Copy-Item -LiteralPath (Join-Path $src 'bin\claude-key-helper.cmd') -Destination "$root\bin" -Force Write-Host ' ok copied claude-mode.ps1 + key helper' -ForegroundColor Green # --- 3. presets ------------------------------------------------------------- foreach ($p in Get-ChildItem -LiteralPath (Join-Path $src 'presets') -Filter '*.json') { $dest = Join-Path "$root\presets" $p.Name if ((Test-Path -LiteralPath $dest) -and -not $Force) { Write-Host " skip preset $($p.BaseName) (exists; -Force to overwrite)" -ForegroundColor DarkGray } else { Copy-Item -LiteralPath $p.FullName -Destination $dest -Force Write-Host " ok preset $($p.BaseName)" -ForegroundColor Green } } # --- 4. initial state (native mode; settings.json untouched) ---------------- $statePath = Join-Path $root 'state.json' if (-not (Test-Path -LiteralPath $statePath)) { @{ mode = 'anthropic'; preset = 'default'; updated = (Get-Date).ToString('o') } | ConvertTo-Json | Set-Content -LiteralPath $statePath -Encoding UTF8 Write-Host ' ok state.json initialised (mode=anthropic)' -ForegroundColor Green } # --- 5. cmd.exe / PATH shim ------------------------------------------------- $binDir = Join-Path $env:USERPROFILE '.local\bin' if (-not (Test-Path -LiteralPath $binDir)) { New-Item -ItemType Directory -Path $binDir -Force | Out-Null } $shim = Join-Path $binDir 'claude-mode.cmd' @" @echo off powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%USERPROFILE%\.claude-mode\claude-mode.ps1" %* "@ | Set-Content -LiteralPath $shim -Encoding ASCII Write-Host " ok shim $shim" -ForegroundColor Green $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') if ($userPath -notlike "*$binDir*") { [Environment]::SetEnvironmentVariable('Path', "$userPath;$binDir", 'User') Write-Host " ok added $binDir to User PATH (new terminals only)" -ForegroundColor Green } # --- 6. PowerShell profile -------------------------------------------------- $profilePath = $PROFILE.CurrentUserAllHosts $profileDir = Split-Path -Parent $profilePath if (-not (Test-Path -LiteralPath $profileDir)) { New-Item -ItemType Directory -Path $profileDir -Force | Out-Null } $snippet = Get-Content -LiteralPath (Join-Path $src 'profile-snippet.ps1') -Raw $current = '' if (Test-Path -LiteralPath $profilePath) { $current = Get-Content -LiteralPath $profilePath -Raw } $startMark = '# >>> claude-mode >>>' $endMark = '# <<< claude-mode <<<' if ($current -match [regex]::Escape($startMark)) { $pattern = '(?s)' + [regex]::Escape($startMark) + '.*?' + [regex]::Escape($endMark) $current = [regex]::Replace($current, $pattern, $snippet.TrimEnd()) Set-Content -LiteralPath $profilePath -Value $current -Encoding UTF8 Write-Host " ok updated claude-mode block in $profilePath" -ForegroundColor Green } else { Add-Content -LiteralPath $profilePath -Value ("`r`n" + $snippet) -Encoding UTF8 Write-Host " ok appended claude-mode block to $profilePath" -ForegroundColor Green } # --- 7. execution policy check --------------------------------------------- $pol = Get-ExecutionPolicy -Scope CurrentUser if ($pol -in @('Restricted', 'Undefined', 'AllSigned')) { Write-Host " warn CurrentUser execution policy is '$pol'; the profile will not load." -ForegroundColor Yellow Write-Host " Fix: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned" -ForegroundColor Yellow } # --- 8. key ---------------------------------------------------------------- if (-not $SkipKeyPrompt) { $vault = Join-Path $root 'vault\openrouter.cred' if ((Test-Path -LiteralPath $vault) -and -not $Force) { Write-Host ' ok OpenRouter key already stored' -ForegroundColor Green } else { Write-Host '' & (Join-Path $root 'claude-mode.ps1') set-key openrouter } } Write-Host '' Write-Host 'done. Open a NEW terminal, then:' -ForegroundColor Cyan Write-Host ' claude-mode status' Write-Host ' claude-mode openrouter default' Write-Host ' claude-mode doctor' Write-Host ' claude-mode anthropic'