param( [string]$NodeMajor = "24", [string]$CodexPackage = "@wepulse/codex", [string]$CodexVersion = "latest", [string]$NodeDistBaseUrl = "https://npmmirror.com/mirrors/node", [string]$NpmRegistry = "https://registry.npmmirror.com", [string]$WePulseCodexBaseUrl = "https://agent-dev.wepulse.cn/v1", [string]$WePulseAuthBaseUrl = "https://auth-dev.wepulse.cn", [string]$WePulseCodexReleaseBaseUrl = "https://codex-dev.wepulse.cn/dev" ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" if (-not [string]::IsNullOrWhiteSpace($env:WEPULSE_NODE_DIST_BASE_URL)) { $NodeDistBaseUrl = $env:WEPULSE_NODE_DIST_BASE_URL } if (-not [string]::IsNullOrWhiteSpace($env:WEPULSE_NPM_REGISTRY)) { $NpmRegistry = $env:WEPULSE_NPM_REGISTRY } if (-not [string]::IsNullOrWhiteSpace($env:WEPULSE_CODEX_BASE_URL)) { $WePulseCodexBaseUrl = $env:WEPULSE_CODEX_BASE_URL } if (-not [string]::IsNullOrWhiteSpace($env:WEPULSE_AUTH_BASE_URL)) { $WePulseAuthBaseUrl = $env:WEPULSE_AUTH_BASE_URL } if (-not [string]::IsNullOrWhiteSpace($env:WEPULSE_CODEX_RELEASE_BASE_URL)) { $WePulseCodexReleaseBaseUrl = $env:WEPULSE_CODEX_RELEASE_BASE_URL } $NodeDistBaseUrl = $NodeDistBaseUrl.TrimEnd("/") $NpmRegistry = $NpmRegistry.TrimEnd("/") $WePulseCodexReleaseBaseUrl = $WePulseCodexReleaseBaseUrl.TrimEnd("/") function Write-Step { param([string]$Message) Write-Host "==> $Message" } function Get-NodeMajor { try { $version = & node -p "process.versions.node.split('.')[0]" 2>$null if ($LASTEXITCODE -eq 0) { return [string]$version } } catch { return $null } return $null } function Add-UserPathEntry { param([string]$Entry) $current = [Environment]::GetEnvironmentVariable("Path", "User") $segments = @() if (-not [string]::IsNullOrWhiteSpace($current)) { $segments = $current.Split(";", [System.StringSplitOptions]::RemoveEmptyEntries) } foreach ($segment in $segments) { if ($segment.TrimEnd("\") -ieq $Entry.TrimEnd("\")) { return } } $updated = if ([string]::IsNullOrWhiteSpace($current)) { $Entry } else { "$current;$Entry" } [Environment]::SetEnvironmentVariable("Path", $updated, "User") } function Prompt-YesNo { param([string]$Prompt) $answer = Read-Host "$Prompt [y/N]" $normalized = $answer.Trim().ToLowerInvariant() return ($normalized -eq "y" -or $normalized -eq "yes") } function Get-ExistingCodexCommand { $existing = Get-Command codex -ErrorAction SilentlyContinue if ($null -eq $existing) { return $null } return $existing.Source } function Test-CommandMentionsPackage { param( [string]$Path, [string]$PackageName ) if ([string]::IsNullOrWhiteSpace($Path)) { return $false } $normalizedPackage = $PackageName -replace "/", "[\\/]" if ($Path -match $normalizedPackage) { return $true } if (Test-Path -LiteralPath $Path -PathType Leaf) { try { $content = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop return ($content -match $normalizedPackage) } catch { return $false } } return $false } function Test-WePulseCodexCommand { param([string]$Path) return (Test-CommandMentionsPackage -Path $Path -PackageName "@wepulse/codex") } function Get-OfficialCodexManager { param([string]$Path) if ([string]::IsNullOrWhiteSpace($Path) -or (Test-WePulseCodexCommand -Path $Path)) { return $null } if (Test-CommandMentionsPackage -Path $Path -PackageName "@openai/codex") { if ($Path -match "\\.bun\\") { return "bun" } return "npm" } if ($Path -match "\\OpenAI\\Codex\\bin\\") { return "standalone" } if ($Path -match "\\.bun\\") { return "bun" } return $null } function Get-OfficialNpmCodexPackagePath { $defaultPackagePath = Join-Path $env:APPDATA "npm\node_modules\@openai\codex" if (Test-Path -LiteralPath $defaultPackagePath -PathType Container) { return $defaultPackagePath } $npm = Get-Command npm.cmd -ErrorAction SilentlyContinue if ($null -eq $npm) { return $null } try { $globalRoot = (& npm.cmd root -g 2>$null).Trim() if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($globalRoot)) { return $null } $packagePath = Join-Path $globalRoot "@openai\codex" if (Test-Path -LiteralPath $packagePath -PathType Container) { return $packagePath } } catch { return $null } return $null } function Get-OfficialCodexInstall { $existingPath = Get-ExistingCodexCommand $manager = Get-OfficialCodexManager -Path $existingPath if ($null -ne $manager) { return [PSCustomObject]@{ Manager = $manager Path = $existingPath } } $npmPackagePath = Get-OfficialNpmCodexPackagePath if (-not [string]::IsNullOrWhiteSpace($npmPackagePath)) { return [PSCustomObject]@{ Manager = "npm" Path = $npmPackagePath } } return $null } function Remove-StandaloneCodex { param([string]$CodexPath) $visibleBinDir = Split-Path -Parent $CodexPath if (-not [string]::IsNullOrWhiteSpace($visibleBinDir) -and (Test-Path -LiteralPath $visibleBinDir)) { Remove-Item -LiteralPath $visibleBinDir -Recurse -Force } $codexHome = if ($env:CODEX_HOME) { $env:CODEX_HOME } else { Join-Path $HOME ".codex" } $standaloneRoot = Join-Path $codexHome "packages\standalone" if (Test-Path -LiteralPath $standaloneRoot) { Remove-Item -LiteralPath $standaloneRoot -Recurse -Force } } function Guard-OfficialCodexInstall { $official = Get-OfficialCodexInstall if ($null -eq $official) { return } Write-Step "Detected existing official $($official.Manager)-managed Codex at $($official.Path)" Write-Warning "WePulse Codex intentionally owns the codex command. The official Codex install must be removed first." if (-not (Prompt-YesNo "Uninstall the existing official Codex now?")) { throw "Stopped: official Codex was not uninstalled." } switch ($official.Manager) { "bun" { Write-Step "Running: bun remove -g @openai/codex" & bun remove -g "@openai/codex" if ($LASTEXITCODE -ne 0) { throw "Failed to uninstall official bun-managed Codex." } } "standalone" { Write-Step "Removing standalone Codex at $($official.Path)" Remove-StandaloneCodex -CodexPath $official.Path } default { if (-not (Get-Command npm.cmd -ErrorAction SilentlyContinue)) { Ensure-Node } Write-Step "Running: npm uninstall -g @openai/codex" & npm.cmd uninstall -g "@openai/codex" if ($LASTEXITCODE -ne 0) { throw "Failed to uninstall official npm-managed Codex." } } } } function Ensure-Node { if ((Get-NodeMajor) -eq $NodeMajor -and (Get-Command npm.cmd -ErrorAction SilentlyContinue)) { Write-Step "Using existing Node.js $(& node -v)" return } $arch = switch ($env:PROCESSOR_ARCHITECTURE) { "AMD64" { "x64" } "ARM64" { "arm64" } default { throw "Unsupported Windows architecture: $env:PROCESSOR_ARCHITECTURE" } } $shasumsUrl = "$NodeDistBaseUrl/latest-v$NodeMajor.x/SHASUMS256.txt" $shasums = Invoke-RestMethod -Uri $shasumsUrl $nodeFile = ($shasums -split "`n" | Where-Object { $_ -match "node-v.+-win-$arch\.zip" } | Select-Object -First 1) -replace "^[0-9a-fA-F]{64}\s+", "" if ([string]::IsNullOrWhiteSpace($nodeFile)) { throw "Could not resolve latest Node.js $NodeMajor.x for $arch." } $nodeVersion = $nodeFile -replace "^node-v", "" -replace "-win-.+$", "" $root = Join-Path $env:LOCALAPPDATA "WePulse" $installDir = Join-Path $root "node-v$nodeVersion-win-$arch" $nodeDir = Join-Path $root "node" if (-not (Test-Path -LiteralPath (Join-Path $installDir "node.exe") -PathType Leaf)) { $tmp = Join-Path ([System.IO.Path]::GetTempPath()) "wepulse-node-$([Guid]::NewGuid().ToString('n'))" New-Item -ItemType Directory -Force -Path $tmp | Out-Null try { $archive = Join-Path $tmp $nodeFile Write-Step "Downloading Node.js v$nodeVersion from $NodeDistBaseUrl" Invoke-WebRequest -Uri "$NodeDistBaseUrl/latest-v$NodeMajor.x/$nodeFile" -OutFile $archive New-Item -ItemType Directory -Force -Path $root | Out-Null Expand-Archive -LiteralPath $archive -DestinationPath $root -Force } finally { Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue } } if (Test-Path -LiteralPath $nodeDir) { $nodeItem = Get-Item -LiteralPath $nodeDir -Force if ($nodeItem.Attributes -band [IO.FileAttributes]::ReparsePoint) { Remove-Item -LiteralPath $nodeDir -Force } else { Remove-Item -LiteralPath $nodeDir -Recurse -Force } } New-Item -ItemType Junction -Path $nodeDir -Target $installDir | Out-Null $npmGlobalBin = Join-Path $env:APPDATA "npm" $env:Path = "$nodeDir;$npmGlobalBin;$env:Path" Add-UserPathEntry -Entry $nodeDir Add-UserPathEntry -Entry $npmGlobalBin } function Get-CodexPlatformPackage { switch ($env:PROCESSOR_ARCHITECTURE) { "AMD64" { return [PSCustomObject]@{ NpmTag = "win32-x64" Package = "@wepulse/codex-win32-x64" } } "ARM64" { return [PSCustomObject]@{ NpmTag = "win32-arm64" Package = "@wepulse/codex-win32-arm64" } } default { throw "Unsupported Windows architecture: $env:PROCESSOR_ARCHITECTURE" } } } function ConvertTo-NpmFileSpecPath { param([string]$Path) return ($Path -replace "\\", "/") } function Install-FromReleaseAssets { $platform = Get-CodexPlatformPackage $manifestUrl = "$WePulseCodexReleaseBaseUrl/release-manifest.json" try { $manifest = Invoke-RestMethod -Uri $manifestUrl } catch { Write-Warning "Could not fetch WePulse release manifest from $manifestUrl. $($_.Exception.Message)" return $false } $releaseVersion = [string]$manifest.version if ([string]::IsNullOrWhiteSpace($releaseVersion)) { Write-Warning "WePulse release manifest did not include a version." return $false } $versionedBaseUrl = [string]$manifest.versionedBaseUrl if ([string]::IsNullOrWhiteSpace($versionedBaseUrl)) { $versionedBaseUrl = "$WePulseCodexReleaseBaseUrl/releases/$releaseVersion" } $versionedBaseUrl = $versionedBaseUrl.TrimEnd("/") $npmAssetBaseUrl = "$versionedBaseUrl/npm" $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "wepulse-codex-release-$([Guid]::NewGuid().ToString('n'))" New-Item -ItemType Directory -Force -Path $tempDir | Out-Null try { $rootAsset = "codex-npm-$releaseVersion.tgz" $platformAsset = "codex-npm-$($platform.NpmTag)-$releaseVersion.tgz" $rootArchive = Join-Path $tempDir $rootAsset $platformArchive = Join-Path $tempDir $platformAsset try { Invoke-WebRequest -Uri "$npmAssetBaseUrl/$rootAsset" -OutFile $rootArchive Invoke-WebRequest -Uri "$npmAssetBaseUrl/$platformAsset" -OutFile $platformArchive } catch { Write-Warning "Could not download WePulse release assets from $npmAssetBaseUrl. $($_.Exception.Message)" return $false } $rootSpecPath = ConvertTo-NpmFileSpecPath -Path $rootArchive $platformSpecPath = ConvertTo-NpmFileSpecPath -Path $platformArchive Write-Step "Installing WePulse Codex $releaseVersion from $npmAssetBaseUrl" & npm.cmd install -g "$CodexPackage@file:$rootSpecPath" "$($platform.Package)@file:$platformSpecPath" --registry $NpmRegistry if ($LASTEXITCODE -eq 0) { return $true } Write-Warning "Local release asset install failed." return $false } finally { Remove-Item -LiteralPath $tempDir -Recurse -Force -ErrorAction SilentlyContinue } } function Write-CodexConfig { $codexHome = if ($env:CODEX_HOME) { $env:CODEX_HOME } else { Join-Path $HOME ".codex" } New-Item -ItemType Directory -Force -Path $codexHome | Out-Null $configFile = Join-Path $codexHome "config.toml" $lines = @() if (Test-Path -LiteralPath $configFile -PathType Leaf) { $section = "" foreach ($line in Get-Content -LiteralPath $configFile) { if ($line -match "^\[") { if ($line -eq "[model_providers.wepulse]" -or $line -eq "[model_providers.wepulse.auth]") { $section = "wepulse" continue } $section = $line } if ($section -eq "wepulse") { continue } if ($section -eq "" -and $line -match "^model_provider\s*=") { continue } if ($section -eq "" -and $line -match "^model\s*=") { continue } $lines += $line } } $wepulseConfig = @" model_provider = "wepulse" model = "gpt-5.4" [model_providers.wepulse] name = "WePulse" base_url = "$WePulseCodexBaseUrl" wire_api = "responses" requires_openai_auth = false supports_websockets = true [model_providers.wepulse.auth] command = "codex" args = ["__wepulse-auth", "token"] timeout_ms = 5000 refresh_interval_ms = 300000 "@ $utf8NoBom = [System.Text.UTF8Encoding]::new($false) [System.IO.File]::WriteAllText($configFile, (($lines + $wepulseConfig) -join [Environment]::NewLine) + [Environment]::NewLine, $utf8NoBom) } Guard-OfficialCodexInstall Ensure-Node Guard-OfficialCodexInstall $npmGlobalBin = Join-Path $env:APPDATA "npm" $env:Path = "$npmGlobalBin;$env:Path" Write-Step "Installing $CodexPackage@$CodexVersion" & npm.cmd config set prefix $npmGlobalBin | Out-Null & npm.cmd config set registry $NpmRegistry | Out-Null & npm.cmd config set disturl $NodeDistBaseUrl | Out-Null if (-not (Install-FromReleaseAssets)) { Write-Warning "Falling back to npm registry install." & npm.cmd install -g "$CodexPackage@$CodexVersion" --registry $NpmRegistry if ($LASTEXITCODE -ne 0) { throw "Failed to install $CodexPackage@$CodexVersion from $NpmRegistry." } } Write-Step "Writing Codex WePulse provider config" Write-CodexConfig Write-Host "" Write-Host "WePulse Codex installed." Write-Host "Run this once to sign in: codex login" Write-Host "Then start Codex with: codex"