582 lines
19 KiB
PowerShell
582 lines
19 KiB
PowerShell
#requires -Version 5.1
|
|
<#
|
|
.SYNOPSIS
|
|
Idempotent Windows setup for Claude Code + CLIProxyAPI + claudex aliases.
|
|
|
|
.DESCRIPTION
|
|
- Installs Claude Code only when the `claude` command is missing.
|
|
- Downloads CLIProxyAPI v7.2.71 only when the expected executable is missing
|
|
(or when -ForceReinstall is supplied).
|
|
- Creates/updates config.yaml, preserving a timestamped backup when replacing it.
|
|
- Adds a managed `claudex` function to Windows PowerShell and PowerShell 7 profiles.
|
|
- Runs Codex OAuth login only when no Codex auth JSON is detected.
|
|
- Creates/updates a Task Scheduler task that starts at boot and restarts the proxy.
|
|
- Starts the task and verifies that TCP port 8317 is listening.
|
|
|
|
Run from PowerShell:
|
|
powershell -ExecutionPolicy Bypass -File .\setup-claudex-proxy.ps1
|
|
|
|
Optional:
|
|
.\setup-claudex-proxy.ps1 -SkipCodexLogin
|
|
.\setup-claudex-proxy.ps1 -ForceReinstall
|
|
.\setup-claudex-proxy.ps1 -NoStart
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[switch]$ForceReinstall,
|
|
[switch]$SkipCodexLogin,
|
|
[switch]$NoStart
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$ProxyVersion = '7.2.71'
|
|
$ProxyArchiveName = "CLIProxyAPI_${ProxyVersion}_windows_amd64.zip"
|
|
$ProxyDownloadUrl = "https://github.com/router-for-me/CLIProxyAPI/releases/download/v$ProxyVersion/$ProxyArchiveName"
|
|
$InstallDir = Join-Path $env:ProgramFiles "CLIProxyAPI_${ProxyVersion}_windows_amd64"
|
|
$ProxyExe = Join-Path $InstallDir 'cli-proxy-api.exe'
|
|
$ConfigPath = Join-Path $InstallDir 'config.yaml'
|
|
$RunnerPath = Join-Path $InstallDir 'run-proxy.ps1'
|
|
$TaskName = 'CLIProxyAPI'
|
|
$ProxyPort = 8317
|
|
$ProxyApiKey = 'sk-dummy'
|
|
$AuthDir = Join-Path $env:USERPROFILE '.cli-proxy-api'
|
|
|
|
function Write-Step {
|
|
param([Parameter(Mandatory)][string]$Message)
|
|
Write-Host "`n==> $Message" -ForegroundColor Cyan
|
|
}
|
|
|
|
function Write-Skip {
|
|
param([Parameter(Mandatory)][string]$Message)
|
|
Write-Host " SKIP: $Message" -ForegroundColor DarkGray
|
|
}
|
|
|
|
function Test-Administrator {
|
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
|
|
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
}
|
|
|
|
function Restart-Elevated {
|
|
Write-Step 'Administrator rights are required; reopening this script with UAC'
|
|
|
|
$forward = @(
|
|
'-NoLogo',
|
|
'-NoProfile',
|
|
'-ExecutionPolicy', 'Bypass',
|
|
'-File', ('"{0}"' -f $PSCommandPath)
|
|
)
|
|
|
|
if ($ForceReinstall) { $forward += '-ForceReinstall' }
|
|
if ($SkipCodexLogin) { $forward += '-SkipCodexLogin' }
|
|
if ($NoStart) { $forward += '-NoStart' }
|
|
|
|
Start-Process -FilePath 'powershell.exe' -Verb RunAs -ArgumentList ($forward -join ' ')
|
|
exit
|
|
}
|
|
|
|
function Refresh-ProcessPath {
|
|
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
|
|
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
|
$env:Path = @($machinePath, $userPath) -join ';'
|
|
}
|
|
|
|
function Write-Utf8NoBom {
|
|
param(
|
|
[Parameter(Mandatory)][string]$Path,
|
|
[Parameter(Mandatory)][string]$Content
|
|
)
|
|
|
|
$parent = Split-Path -Parent $Path
|
|
if ($parent) {
|
|
[IO.Directory]::CreateDirectory($parent) | Out-Null
|
|
}
|
|
|
|
$encoding = New-Object Text.UTF8Encoding($false)
|
|
[IO.File]::WriteAllText($Path, $Content, $encoding)
|
|
}
|
|
|
|
function Set-ManagedFile {
|
|
param(
|
|
[Parameter(Mandatory)][string]$Path,
|
|
[Parameter(Mandatory)][string]$Content,
|
|
[switch]$BackupExisting
|
|
)
|
|
|
|
$normalized = $Content.TrimEnd() + [Environment]::NewLine
|
|
|
|
if (Test-Path -LiteralPath $Path) {
|
|
$existing = [IO.File]::ReadAllText($Path)
|
|
if ($existing -eq $normalized) {
|
|
Write-Skip "$Path is already correct"
|
|
return $false
|
|
}
|
|
|
|
if ($BackupExisting) {
|
|
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
$backup = "$Path.bak-$stamp"
|
|
Copy-Item -LiteralPath $Path -Destination $backup -Force
|
|
Write-Host " Backed up existing file to: $backup"
|
|
}
|
|
}
|
|
|
|
Write-Utf8NoBom -Path $Path -Content $normalized
|
|
Write-Host " Wrote: $Path"
|
|
return $true
|
|
}
|
|
|
|
function Install-ClaudeCodeIfMissing {
|
|
Write-Step 'Checking Claude Code'
|
|
|
|
if (Get-Command 'claude' -ErrorAction SilentlyContinue) {
|
|
$version = try { (& claude --version 2>$null | Select-Object -First 1) } catch { $null }
|
|
Write-Skip ("Claude Code is already installed" + $(if ($version) { ": $version" } else { '' }))
|
|
return
|
|
}
|
|
|
|
Write-Host ' Claude Code was not found. Running the official Windows installer...'
|
|
$installer = Invoke-RestMethod -Uri 'https://claude.ai/install.ps1'
|
|
Invoke-Expression $installer
|
|
Refresh-ProcessPath
|
|
|
|
if (-not (Get-Command 'claude' -ErrorAction SilentlyContinue)) {
|
|
throw 'Claude Code installation finished, but `claude` is still unavailable in PATH. Open a new PowerShell window and rerun this script.'
|
|
}
|
|
|
|
Write-Host ' Claude Code installed.'
|
|
}
|
|
|
|
function Install-ProxyIfMissing {
|
|
Write-Step "Checking CLIProxyAPI v$ProxyVersion"
|
|
|
|
if ((Test-Path -LiteralPath $ProxyExe) -and -not $ForceReinstall) {
|
|
Write-Skip "$ProxyExe already exists"
|
|
return
|
|
}
|
|
|
|
$tempRoot = Join-Path ([IO.Path]::GetTempPath()) ("cliproxy-setup-" + [guid]::NewGuid().ToString('N'))
|
|
$zipPath = Join-Path $tempRoot $ProxyArchiveName
|
|
$extractPath = Join-Path $tempRoot 'extract'
|
|
|
|
try {
|
|
New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null
|
|
New-Item -ItemType Directory -Path $extractPath -Force | Out-Null
|
|
|
|
Write-Host " Downloading: $ProxyDownloadUrl"
|
|
Invoke-WebRequest -Uri $ProxyDownloadUrl -OutFile $zipPath -UseBasicParsing
|
|
|
|
Write-Host ' Extracting archive...'
|
|
Expand-Archive -LiteralPath $zipPath -DestinationPath $extractPath -Force
|
|
|
|
$foundExe = Get-ChildItem -LiteralPath $extractPath -Filter 'cli-proxy-api.exe' -File -Recurse |
|
|
Select-Object -First 1
|
|
|
|
if (-not $foundExe) {
|
|
throw 'The downloaded archive did not contain cli-proxy-api.exe.'
|
|
}
|
|
|
|
$sourceRoot = $foundExe.Directory.FullName
|
|
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
|
|
Copy-Item -Path (Join-Path $sourceRoot '*') -Destination $InstallDir -Recurse -Force
|
|
|
|
if (-not (Test-Path -LiteralPath $ProxyExe)) {
|
|
throw "Extraction completed, but $ProxyExe was not created."
|
|
}
|
|
|
|
Write-Host " Installed to: $InstallDir"
|
|
}
|
|
finally {
|
|
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
function Configure-Proxy {
|
|
Write-Step 'Configuring CLIProxyAPI'
|
|
|
|
New-Item -ItemType Directory -Path $AuthDir -Force | Out-Null
|
|
$authDirYaml = $AuthDir.Replace('\', '/')
|
|
|
|
$config = @"
|
|
host: "127.0.0.1"
|
|
port: $ProxyPort
|
|
|
|
auth-dir: "$authDirYaml"
|
|
|
|
api-keys:
|
|
- "$ProxyApiKey"
|
|
|
|
debug: false
|
|
logging-to-file: true
|
|
logs-max-total-size-mb: 256
|
|
usage-statistics-enabled: false
|
|
"@
|
|
|
|
Set-ManagedFile -Path $ConfigPath -Content $config -BackupExisting | Out-Null
|
|
}
|
|
|
|
function Configure-Runner {
|
|
Write-Step 'Configuring the proxy watchdog script'
|
|
|
|
$escapedInstallDir = $InstallDir.Replace("'", "''")
|
|
$escapedProxyExe = $ProxyExe.Replace("'", "''")
|
|
$escapedConfigPath = $ConfigPath.Replace("'", "''")
|
|
$wrapperLog = (Join-Path $InstallDir 'proxy-watchdog.log').Replace("'", "''")
|
|
|
|
$runner = @"
|
|
`$ErrorActionPreference = 'Continue'
|
|
`$installDir = '$escapedInstallDir'
|
|
`$proxyExe = '$escapedProxyExe'
|
|
`$configPath = '$escapedConfigPath'
|
|
`$wrapperLog = '$wrapperLog'
|
|
|
|
Set-Location -LiteralPath `$installDir
|
|
|
|
while (`$true) {
|
|
`$started = Get-Date
|
|
|
|
try {
|
|
& `$proxyExe -config `$configPath
|
|
`$exitCode = `$LASTEXITCODE
|
|
Add-Content -LiteralPath `$wrapperLog -Value ("{0:u} Proxy exited with code {1}; restarting in 5 seconds." -f (Get-Date), `$exitCode)
|
|
}
|
|
catch {
|
|
Add-Content -LiteralPath `$wrapperLog -Value ("{0:u} Proxy crashed: {1}; restarting in 5 seconds." -f (Get-Date), `$_.Exception.Message)
|
|
}
|
|
|
|
# Avoid a hot restart loop if startup fails instantly.
|
|
`$runtime = (Get-Date) - `$started
|
|
if (`$runtime.TotalSeconds -lt 5) {
|
|
Start-Sleep -Seconds 5
|
|
}
|
|
}
|
|
"@
|
|
|
|
Set-ManagedFile -Path $RunnerPath -Content $runner | Out-Null
|
|
}
|
|
|
|
function Upsert-ManagedProfileBlock {
|
|
param(
|
|
[Parameter(Mandatory)][string]$Path,
|
|
[Parameter(Mandatory)][string]$Block
|
|
)
|
|
|
|
$begin = '# >>> CLIProxyAPI claudex BEGIN >>>'
|
|
$end = '# <<< CLIProxyAPI claudex END <<<'
|
|
$pattern = '(?ms)^[ \t]*' + [regex]::Escape($begin) + '.*?^[ \t]*' + [regex]::Escape($end) + '[ \t]*(?:\r?\n)?'
|
|
|
|
$existing = if (Test-Path -LiteralPath $Path) {
|
|
[IO.File]::ReadAllText($Path)
|
|
}
|
|
else {
|
|
''
|
|
}
|
|
|
|
$withoutManagedBlock = [regex]::Replace($existing, $pattern, '').TrimEnd()
|
|
$newContent = if ([string]::IsNullOrWhiteSpace($withoutManagedBlock)) {
|
|
$Block.TrimEnd() + [Environment]::NewLine
|
|
}
|
|
else {
|
|
$withoutManagedBlock + [Environment]::NewLine + [Environment]::NewLine + $Block.TrimEnd() + [Environment]::NewLine
|
|
}
|
|
|
|
if ($existing -eq $newContent) {
|
|
Write-Skip "$Path already contains the current claudex block"
|
|
return
|
|
}
|
|
|
|
Write-Utf8NoBom -Path $Path -Content $newContent
|
|
Write-Host " Updated: $Path"
|
|
}
|
|
|
|
function Configure-PowerShellProfiles {
|
|
Write-Step 'Configuring the claudex model aliases'
|
|
|
|
$profileBlock = @'
|
|
# >>> CLIProxyAPI claudex BEGIN >>>
|
|
$global:ClaudeModelAliases = @{
|
|
sol = 'gpt-5.6-sol'
|
|
terra = 'gpt-5.6-terra'
|
|
luna = 'gpt-5.6-luna'
|
|
}
|
|
|
|
function claudex {
|
|
param(
|
|
[Parameter(Position = 0)]
|
|
[string]$Model = 'sol',
|
|
|
|
[Parameter(ValueFromRemainingArguments = $true)]
|
|
[string[]]$ClaudeArgs
|
|
)
|
|
|
|
$resolvedModel = if ($global:ClaudeModelAliases.ContainsKey($Model)) {
|
|
$global:ClaudeModelAliases[$Model]
|
|
}
|
|
else {
|
|
$Model
|
|
}
|
|
|
|
$names = @(
|
|
'ANTHROPIC_BASE_URL',
|
|
'ANTHROPIC_AUTH_TOKEN',
|
|
'CLAUDE_CODE_SUBAGENT_MODEL'
|
|
)
|
|
|
|
$previous = @{}
|
|
foreach ($name in $names) {
|
|
$previous[$name] = [Environment]::GetEnvironmentVariable($name, 'Process')
|
|
}
|
|
|
|
[Environment]::SetEnvironmentVariable('ANTHROPIC_BASE_URL', 'http://127.0.0.1:8317', 'Process')
|
|
[Environment]::SetEnvironmentVariable('ANTHROPIC_AUTH_TOKEN', 'sk-dummy', 'Process')
|
|
[Environment]::SetEnvironmentVariable('CLAUDE_CODE_SUBAGENT_MODEL', $resolvedModel, 'Process')
|
|
|
|
try {
|
|
& claude --model $resolvedModel @ClaudeArgs
|
|
}
|
|
finally {
|
|
foreach ($name in $names) {
|
|
[Environment]::SetEnvironmentVariable($name, $previous[$name], 'Process')
|
|
}
|
|
}
|
|
}
|
|
# <<< CLIProxyAPI claudex END <<<
|
|
'@
|
|
|
|
$documents = [Environment]::GetFolderPath([Environment+SpecialFolder]::MyDocuments)
|
|
|
|
# All-host and ConsoleHost profiles for Windows PowerShell 5.1 and PowerShell 7.
|
|
# Writing the managed block last also overrides an older unmarked `claudex`
|
|
# function that may already exist earlier in a profile.
|
|
$profilePaths = @(
|
|
(Join-Path $documents 'WindowsPowerShell\profile.ps1'),
|
|
(Join-Path $documents 'WindowsPowerShell\Microsoft.PowerShell_profile.ps1'),
|
|
(Join-Path $documents 'PowerShell\profile.ps1'),
|
|
(Join-Path $documents 'PowerShell\Microsoft.PowerShell_profile.ps1')
|
|
) | Select-Object -Unique
|
|
|
|
foreach ($profilePath in $profilePaths) {
|
|
Upsert-ManagedProfileBlock -Path $profilePath -Block $profileBlock
|
|
}
|
|
}
|
|
|
|
function Test-CodexAuthentication {
|
|
if (-not (Test-Path -LiteralPath $AuthDir)) {
|
|
return $false
|
|
}
|
|
|
|
foreach ($file in Get-ChildItem -LiteralPath $AuthDir -Filter '*.json' -File -ErrorAction SilentlyContinue) {
|
|
try {
|
|
$data = Get-Content -LiteralPath $file.FullName -Raw | ConvertFrom-Json
|
|
if (($data.type -eq 'codex') -and ($data.refresh_token -or $data.access_token)) {
|
|
return $true
|
|
}
|
|
}
|
|
catch {
|
|
# Ignore unrelated or malformed JSON files.
|
|
}
|
|
}
|
|
|
|
return $false
|
|
}
|
|
|
|
function Ensure-CodexAuthentication {
|
|
Write-Step 'Checking Codex OAuth authentication'
|
|
|
|
if (Test-CodexAuthentication) {
|
|
Write-Skip 'A Codex OAuth credential already exists'
|
|
return
|
|
}
|
|
|
|
if ($SkipCodexLogin) {
|
|
Write-Warning 'No Codex credential was found, and -SkipCodexLogin was supplied.'
|
|
return
|
|
}
|
|
|
|
Write-Host ' No Codex credential found. A browser login will open now.'
|
|
& $ProxyExe -config $ConfigPath -codex-login
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Codex login exited with code $LASTEXITCODE."
|
|
}
|
|
|
|
if (-not (Test-CodexAuthentication)) {
|
|
throw "Codex login completed, but no Codex credential was detected in $AuthDir."
|
|
}
|
|
|
|
Write-Host ' Codex OAuth credential saved.'
|
|
}
|
|
|
|
function Get-ExpectedTaskActionArguments {
|
|
$escapedRunner = $RunnerPath.Replace('"', '\"')
|
|
return "-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$escapedRunner`""
|
|
}
|
|
|
|
function Test-ScheduledTaskMatches {
|
|
param([Parameter(Mandatory)]$Task)
|
|
|
|
$expectedExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
|
$expectedArgs = Get-ExpectedTaskActionArguments
|
|
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent().Name
|
|
|
|
$action = @($Task.Actions) | Select-Object -First 1
|
|
$trigger = @($Task.Triggers) | Select-Object -First 1
|
|
|
|
if (-not $action -or -not $trigger) {
|
|
return $false
|
|
}
|
|
|
|
$executeMatches = ([IO.Path]::GetFullPath($action.Execute) -ieq [IO.Path]::GetFullPath($expectedExe))
|
|
$argumentsMatch = ($action.Arguments -eq $expectedArgs)
|
|
$userMatches = ($Task.Principal.UserId -ieq $currentUser)
|
|
$logonMatches = ($Task.Principal.LogonType -eq 'S4U')
|
|
$startupTrigger = ($trigger.CimClass.CimClassName -eq 'MSFT_TaskBootTrigger')
|
|
|
|
return ($executeMatches -and $argumentsMatch -and $userMatches -and $logonMatches -and $startupTrigger)
|
|
}
|
|
|
|
function Configure-ScheduledTask {
|
|
Write-Step 'Configuring Task Scheduler'
|
|
|
|
Import-Module ScheduledTasks
|
|
|
|
$existingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
|
if ($existingTask -and (Test-ScheduledTaskMatches -Task $existingTask)) {
|
|
Write-Skip "Scheduled task '$TaskName' is already correct"
|
|
return
|
|
}
|
|
|
|
$powershellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
|
$action = New-ScheduledTaskAction `
|
|
-Execute $powershellExe `
|
|
-Argument (Get-ExpectedTaskActionArguments) `
|
|
-WorkingDirectory $InstallDir
|
|
|
|
$trigger = New-ScheduledTaskTrigger -AtStartup
|
|
|
|
# S4U runs under the current Windows user without storing a password.
|
|
# The proxy config uses an explicit auth directory, so it does not depend on
|
|
# the task receiving a normal interactive user profile.
|
|
$principal = New-ScheduledTaskPrincipal `
|
|
-UserId ([Security.Principal.WindowsIdentity]::GetCurrent().Name) `
|
|
-LogonType S4U `
|
|
-RunLevel Highest
|
|
|
|
$settings = New-ScheduledTaskSettingsSet `
|
|
-AllowStartIfOnBatteries `
|
|
-DontStopIfGoingOnBatteries `
|
|
-StartWhenAvailable `
|
|
-RestartCount 999 `
|
|
-RestartInterval (New-TimeSpan -Minutes 1) `
|
|
-ExecutionTimeLimit ([TimeSpan]::Zero) `
|
|
-MultipleInstances IgnoreNew
|
|
|
|
$task = New-ScheduledTask `
|
|
-Action $action `
|
|
-Trigger $trigger `
|
|
-Principal $principal `
|
|
-Settings $settings `
|
|
-Description 'Runs CLIProxyAPI for Claude Code through the local Codex-compatible proxy.'
|
|
|
|
Register-ScheduledTask -TaskName $TaskName -InputObject $task -Force | Out-Null
|
|
Write-Host " Registered scheduled task: $TaskName"
|
|
}
|
|
|
|
function Test-TcpPort {
|
|
param(
|
|
[Parameter(Mandatory)][string]$HostName,
|
|
[Parameter(Mandatory)][int]$Port,
|
|
[int]$TimeoutMs = 1500
|
|
)
|
|
|
|
$client = New-Object Net.Sockets.TcpClient
|
|
try {
|
|
$result = $client.BeginConnect($HostName, $Port, $null, $null)
|
|
if (-not $result.AsyncWaitHandle.WaitOne($TimeoutMs, $false)) {
|
|
return $false
|
|
}
|
|
|
|
$client.EndConnect($result)
|
|
return $true
|
|
}
|
|
catch {
|
|
return $false
|
|
}
|
|
finally {
|
|
$client.Close()
|
|
}
|
|
}
|
|
|
|
function Start-ProxyTask {
|
|
if ($NoStart) {
|
|
Write-Step 'Skipping task startup because -NoStart was supplied'
|
|
return
|
|
}
|
|
|
|
Write-Step 'Starting CLIProxyAPI'
|
|
|
|
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction Stop
|
|
if ($task.State -eq 'Running') {
|
|
Write-Skip "Scheduled task '$TaskName' is already running"
|
|
}
|
|
else {
|
|
# Stop a manually launched copy from this exact installation before
|
|
# starting the managed task, preventing a port-conflict restart loop.
|
|
Get-CimInstance Win32_Process -Filter "Name = 'cli-proxy-api.exe'" -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.ExecutablePath -and ($_.ExecutablePath -ieq $ProxyExe) } |
|
|
ForEach-Object {
|
|
Write-Host " Stopping unmanaged process PID $($_.ProcessId)..."
|
|
Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
Start-ScheduledTask -TaskName $TaskName
|
|
}
|
|
|
|
$listening = $false
|
|
foreach ($attempt in 1..12) {
|
|
Start-Sleep -Seconds 1
|
|
if (Test-TcpPort -HostName '127.0.0.1' -Port $ProxyPort) {
|
|
$listening = $true
|
|
break
|
|
}
|
|
}
|
|
|
|
if ($listening) {
|
|
Write-Host " Proxy is listening on http://127.0.0.1:$ProxyPort" -ForegroundColor Green
|
|
}
|
|
else {
|
|
$info = Get-ScheduledTaskInfo -TaskName $TaskName
|
|
Write-Warning "Task started, but port $ProxyPort is not listening yet. LastTaskResult: $($info.LastTaskResult)"
|
|
Write-Warning "Check: $InstallDir\proxy-watchdog.log and $InstallDir\logs"
|
|
}
|
|
}
|
|
|
|
if (-not (Test-Administrator)) {
|
|
Restart-Elevated
|
|
}
|
|
|
|
try {
|
|
Install-ClaudeCodeIfMissing
|
|
Install-ProxyIfMissing
|
|
Configure-Proxy
|
|
Configure-Runner
|
|
Configure-PowerShellProfiles
|
|
Ensure-CodexAuthentication
|
|
Configure-ScheduledTask
|
|
Start-ProxyTask
|
|
|
|
Write-Host "`nSetup complete." -ForegroundColor Green
|
|
Write-Host 'Open a new PowerShell window, then use:'
|
|
Write-Host ' claudex sol'
|
|
Write-Host ' claudex terra'
|
|
Write-Host ' claudex luna'
|
|
Write-Host ' claudex gpt-5.6-sol --dangerously-skip-permissions'
|
|
}
|
|
catch {
|
|
Write-Host "`nSetup failed: $($_.Exception.Message)" -ForegroundColor Red
|
|
Write-Host $_.ScriptStackTrace -ForegroundColor DarkGray
|
|
exit 1
|
|
}
|