From d5f3ee0ce1a6c33d8934e24491cd0882b80dd0ed Mon Sep 17 00:00:00 2001 From: j-token Date: Mon, 10 Aug 2026 11:52:33 +0900 Subject: [PATCH 1/4] feat(install): add Windows PowerShell installer and Windows kernel bootstrap Adds install.ps1, a PowerShell installer that mirrors install.sh: it resolves the release channel, downloads the tarball and SHA256SUMS, verifies the SHA-256 checksum, installs with npm, and can prepare the IPython runtime. The installer body runs inside a script block so `irm ... | iex` does not leak helpers or preferences into the caller's session. Makes the kernel bootstrap work on Windows: uv is installed with the official PowerShell installer and looked up in its Windows install locations, and the managed venv interpreter resolves to Scripts/python.exe. Non-Windows behavior is unchanged. The release workflow now renders and publishes install.ps1 and install-beta.ps1 alongside the shell installers, and check-installer-render.mjs validates both. --- .github/workflows/build-binaries.yml | 31 +- README.md | 6 + install.ps1 | 468 ++++++++++++++++++ packages/coding-agent/CHANGELOG.md | 2 + packages/coding-agent/README.md | 6 + packages/coding-agent/docs/index.md | 6 + packages/coding-agent/docs/quickstart.md | 8 +- packages/coding-agent/docs/windows.md | 108 +++- .../coding-agent/src/core/kernel/bootstrap.ts | 67 ++- scripts/check-installer-render.mjs | 30 ++ 10 files changed, 714 insertions(+), 18 deletions(-) create mode 100644 install.ps1 diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index d7022d365..4c0fe135a 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -236,12 +236,15 @@ jobs: const fs = require("node:fs"); const baseUrl = process.env.INSTALL_BASE_URL; if (!baseUrl) throw new Error("INSTALL_BASE_URL is required"); - const installer = fs.readFileSync("install.sh", "utf8"); - const renderInstaller = (channel) => installer + const renderInstaller = (source, channel) => source .replaceAll("__PRIME_AGENT_DOWNLOAD_BASE_URL__", baseUrl) .replaceAll("__PRIME_AGENT_DEFAULT_RELEASE_CHANNEL__", channel); - fs.writeFileSync("/tmp/prime-agent-install.sh", renderInstaller("stable")); - fs.writeFileSync("/tmp/prime-agent-install-beta.sh", renderInstaller("beta")); + const shellInstaller = fs.readFileSync("install.sh", "utf8"); + fs.writeFileSync("/tmp/prime-agent-install.sh", renderInstaller(shellInstaller, "stable")); + fs.writeFileSync("/tmp/prime-agent-install-beta.sh", renderInstaller(shellInstaller, "beta")); + const powershellInstaller = fs.readFileSync("install.ps1", "utf8"); + fs.writeFileSync("/tmp/prime-agent-install.ps1", renderInstaller(powershellInstaller, "stable")); + fs.writeFileSync("/tmp/prime-agent-install-beta.ps1", renderInstaller(powershellInstaller, "beta")); NODE - name: Extract production release notes @@ -298,6 +301,16 @@ jobs: --content-type text/x-shellscript \ --cache-control no-cache + aws s3 cp /tmp/prime-agent-install.ps1 "s3://${R2_BUCKET}/install.ps1" \ + --endpoint-url "$R2_ENDPOINT_URL" \ + --content-type text/plain \ + --cache-control no-cache + + aws s3 cp /tmp/prime-agent-install-beta.ps1 "s3://${R2_BUCKET}/install-beta.ps1" \ + --endpoint-url "$R2_ENDPOINT_URL" \ + --content-type text/plain \ + --cache-control no-cache + - name: Create production GitHub release if: env.PUBLISH_PRODUCTION == 'true' env: @@ -379,6 +392,16 @@ jobs: --content-type text/x-shellscript \ --cache-control no-cache + aws s3 cp /tmp/prime-agent-install.ps1 "s3://${R2_BUCKET}/install.ps1" \ + --endpoint-url "$R2_ENDPOINT_URL" \ + --content-type text/plain \ + --cache-control no-cache + + aws s3 cp /tmp/prime-agent-install-beta.ps1 "s3://${R2_BUCKET}/install-beta.ps1" \ + --endpoint-url "$R2_ENDPOINT_URL" \ + --content-type text/plain \ + --cache-control no-cache + printf 'Automated beta build from `%s` (`%s`).\n' "$DEFAULT_BRANCH" "$BUILD_REF" > /tmp/beta-release-notes.md if gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/beta" >/dev/null 2>&1; then diff --git a/README.md b/README.md index 1d6f850c5..cc4241aa9 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,12 @@ Install the latest stable release on macOS or Linux: curl -fsSL https://app.primeintellect.ai/prime-agent/install.sh | sh ``` +On Windows, install from PowerShell (see [Windows setup](packages/coding-agent/docs/windows.md)): + +```powershell +irm https://app.primeintellect.ai/prime-agent/install.ps1 | iex +``` + The installer downloads a versioned release, verifies its SHA-256 checksum, installs the `prime-agent` command, and can prepare the IPython runtime used by the agent. Start Prime Agent from the repository or directory you want it to work in: diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 000000000..7afd68f21 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,468 @@ +[CmdletBinding()] +param( + [Parameter(Position = 0)] + [string] $ChannelOrVersion +) + +# The installer body runs inside this script block so `irm ... | iex` cannot leak +# its helpers, variables, strict mode, or preference changes into the caller's session. +& { + param([string] $ChannelOrVersion) + + Set-StrictMode -Version Latest + $ErrorActionPreference = 'Stop' + $ProgressPreference = 'SilentlyContinue' + + if ($PSVersionTable.PSVersion.Major -lt 6) { + [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + } + + # Keep these sentinels split so release publishing only rewrites the configured + # values below; local or unpublished copies still need unreplaced values to compare. + $PrimeAgentUnconfiguredBaseUrl = '__PRIME_AGENT_DOWNLOAD_BASE' + '_URL__' + $PrimeAgentUnconfiguredDefaultReleaseChannel = '__PRIME_AGENT_DEFAULT_RELEASE_' + 'CHANNEL__' + $PrimeAgentBaseUrl = '__PRIME_AGENT_DOWNLOAD_BASE_URL__' + $PrimeAgentDefaultReleaseChannel = '__PRIME_AGENT_DEFAULT_RELEASE_CHANNEL__' + + if ($env:PRIME_AGENT_DOWNLOAD_BASE_URL) { + $PrimeAgentBaseUrl = $env:PRIME_AGENT_DOWNLOAD_BASE_URL + } + $PrimeAgentBaseUrl = $PrimeAgentBaseUrl.TrimEnd('/') + + if ($PrimeAgentDefaultReleaseChannel -eq $PrimeAgentUnconfiguredDefaultReleaseChannel) { + $PrimeAgentDefaultReleaseChannel = 'stable' + } + $PrimeAgentReleaseChannel = $PrimeAgentDefaultReleaseChannel + if ($env:PRIME_AGENT_RELEASE_CHANNEL) { + $PrimeAgentReleaseChannel = $env:PRIME_AGENT_RELEASE_CHANNEL + } + + $PrimeAgentPackage = 'prime-agent' + if ($env:PRIME_AGENT_PACKAGE) { + $PrimeAgentPackage = $env:PRIME_AGENT_PACKAGE + } + + $PrimeAgentCmd = 'prime-agent' + if ($env:PRIME_AGENT_CMD) { + $PrimeAgentCmd = $env:PRIME_AGENT_CMD + } + + $PrimeAgentMinimumNodeVersion = [Version]'20.6.0' + + function Write-PrimeAgentLine { + param([string] $Text = '') + + Write-Host $Text + } + + function Write-PrimeAgentStep { + param([string] $Text = '') + + Write-Host " $Text" + } + + function Write-PrimeAgentNote { + param([string] $Text = '') + + Write-Host " $Text" -ForegroundColor DarkGray + } + + function Write-PrimeAgentWarning { + param([string] $Text = '') + + Write-Host " $Text" -ForegroundColor Yellow + } + + function Write-PrimeAgentHeader { + Write-PrimeAgentLine + Write-Host ' Installing Prime Agent' -ForegroundColor Magenta + Write-PrimeAgentNote 'npm global install' + Write-PrimeAgentLine + } + + function Test-PrimeAgentCanPrompt { + try { + if (-not [Environment]::UserInteractive) { + return $false + } + if ([Console]::IsInputRedirected) { + return $false + } + } catch { + return $false + } + return $null -ne $Host.UI + } + + function Read-PrimeAgentYesNo { + param( + [Parameter(Mandatory = $true)][string] $Question, + [Parameter(Mandatory = $true)][string] $Detail + ) + + Write-PrimeAgentLine + Write-PrimeAgentNote $Detail + + if (-not (Test-PrimeAgentCanPrompt)) { + Write-PrimeAgentNote 'No terminal detected; continuing without confirmation.' + return $true + } + + $answer = $null + try { + $answer = Read-Host " $Question [Y/n]" + } catch { + Write-PrimeAgentNote 'No terminal detected; continuing without confirmation.' + return $true + } + + if ($null -eq $answer) { + $answer = '' + } + $answer = $answer.Trim().ToLowerInvariant() + return -not ($answer -eq 'n' -or $answer -eq 'no') + } + + function Get-PrimeAgentCommandPath { + param([Parameter(Mandatory = $true)][string] $Name) + + $command = Get-Command -Name $Name -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($command -and $command.Source) { + return [string]$command.Source + } + return $null + } + + function Get-PrimeAgentNpmPath { + # npm ships as npm.cmd on Windows; the bare npm shim is a shell script that + # PowerShell cannot execute, and npm.ps1 depends on the execution policy. + foreach ($name in @('npm.cmd', 'npm.exe')) { + $command = Get-Command -Name $name -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($command -and $command.Source) { + return [string]$command.Source + } + } + + $command = Get-Command -Name 'npm' -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($command -and $command.Source) { + return [string]$command.Source + } + + $command = Get-Command -Name 'npm' -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($command -and $command.Source) { + $sibling = Join-Path (Split-Path -Parent ([string]$command.Source)) 'npm.cmd' + if (Test-Path -LiteralPath $sibling) { + return $sibling + } + return [string]$command.Source + } + + return $null + } + + function Get-PrimeAgentNodeVersion { + $nodePath = Get-PrimeAgentCommandPath -Name 'node' + if (-not $nodePath) { + return $null + } + + $output = $null + try { + $output = & $nodePath --version 2>&1 | Select-Object -First 1 + } catch { + return $null + } + if (-not $output) { + return $null + } + + $match = [regex]::Match([string]$output, '(\d+)\.(\d+)\.(\d+)') + if (-not $match.Success) { + return $null + } + return [Version]::new([int]$match.Groups[1].Value, [int]$match.Groups[2].Value, [int]$match.Groups[3].Value) + } + + function Write-PrimeAgentNodeInstallHelp { + Write-PrimeAgentWarning 'Install Node.js 20.6.0 or newer, then run this installer again.' + Write-PrimeAgentLine + Write-PrimeAgentStep 'With winget:' + Write-PrimeAgentStep ' winget install OpenJS.NodeJS.LTS' + Write-PrimeAgentLine + Write-PrimeAgentStep 'Or download an installer from https://nodejs.org' + Write-PrimeAgentNote 'Open a new terminal after installing so PATH picks up node and npm.' + Write-PrimeAgentLine + } + + function Test-PrimeAgentPrerequisites { + $nodeVersion = Get-PrimeAgentNodeVersion + if (-not $nodeVersion) { + Write-PrimeAgentNodeInstallHelp + throw 'Node.js 20.6.0 or newer is required to install Prime Agent.' + } + if ($nodeVersion -lt $PrimeAgentMinimumNodeVersion) { + Write-PrimeAgentNodeInstallHelp + throw "Prime Agent requires Node.js 20.6.0 or newer. Found v$nodeVersion." + } + + if (-not (Get-PrimeAgentNpmPath)) { + Write-PrimeAgentNodeInstallHelp + throw 'npm is required to install Prime Agent.' + } + + Write-PrimeAgentStep "Node.js v$nodeVersion detected." + + $existing = Get-PrimeAgentCommandPath -Name $PrimeAgentCmd + if ($existing) { + Write-PrimeAgentWarning "Existing $($PrimeAgentCmd) found at: $existing" + } + } + + function New-PrimeAgentTempDirectory { + $path = Join-Path ([System.IO.Path]::GetTempPath()) "prime-agent-install-$([guid]::NewGuid().ToString('N'))" + $null = New-Item -ItemType Directory -Path $path -Force + return $path + } + + function Save-PrimeAgentFile { + param( + [Parameter(Mandatory = $true)][string] $Uri, + [Parameter(Mandatory = $true)][string] $Path + ) + + try { + Invoke-WebRequest -Uri $Uri -OutFile $Path -UseBasicParsing + } catch { + throw "could not download $Uri`: $($_.Exception.Message)" + } + } + + function ConvertTo-PrimeAgentVersion { + param([Parameter(Mandatory = $true)][AllowEmptyString()][string] $Version) + + $normalized = $Version.Trim() + if ($normalized.StartsWith('v')) { + $normalized = $normalized.Substring(1) + } + if (-not $normalized) { + throw 'empty Prime Agent version.' + } + if ($normalized -notmatch '^[0-9A-Za-z.-]+$') { + throw "invalid Prime Agent version: $Version" + } + return $normalized + } + + function Resolve-PrimeAgentVersion { + param([string] $ChannelOrVersion) + + $channel = $PrimeAgentReleaseChannel + if ($ChannelOrVersion) { + if ($ChannelOrVersion -eq 'stable' -or $ChannelOrVersion -eq 'beta') { + $channel = $ChannelOrVersion + } else { + return ConvertTo-PrimeAgentVersion -Version $ChannelOrVersion + } + } + + if ($env:PRIME_AGENT_VERSION) { + return ConvertTo-PrimeAgentVersion -Version $env:PRIME_AGENT_VERSION + } + + if ($channel -ne 'stable' -and $channel -ne 'beta') { + throw "invalid Prime Agent release channel: $channel" + } + + $channelUrl = "$($PrimeAgentBaseUrl)/$channel" + Write-PrimeAgentStep "Resolving the latest $channel release." + + $channelDir = New-PrimeAgentTempDirectory + try { + $channelPath = Join-Path $channelDir $channel + Save-PrimeAgentFile -Uri $channelUrl -Path $channelPath + $channelVersion = (Get-Content -LiteralPath $channelPath -Raw) -replace '\s', '' + } finally { + Remove-Item -LiteralPath $channelDir -Recurse -Force -ErrorAction SilentlyContinue + } + + if (-not $channelVersion) { + throw "could not resolve the latest Prime Agent version from $channelUrl" + } + return ConvertTo-PrimeAgentVersion -Version $channelVersion + } + + function Test-PrimeAgentChecksum { + param( + [Parameter(Mandatory = $true)][string] $ChecksumsPath, + [Parameter(Mandatory = $true)][string] $FilePath + ) + + $fileName = Split-Path -Leaf $FilePath + $expected = $null + foreach ($line in Get-Content -LiteralPath $ChecksumsPath) { + $match = [regex]::Match($line, '^([0-9a-fA-F]{64})\s+\*?(.+)$') + if ($match.Success -and $match.Groups[2].Value.Trim() -eq $fileName) { + $expected = $match.Groups[1].Value + break + } + } + + if (-not $expected) { + throw "checksum for $fileName was not found in $ChecksumsPath" + } + + $actual = (Get-FileHash -LiteralPath $FilePath -Algorithm SHA256).Hash + if ($actual -ne $expected.ToUpperInvariant()) { + throw "checksum mismatch for $fileName. Expected $($expected.ToLowerInvariant()), got $($actual.ToLowerInvariant())." + } + Write-PrimeAgentStep 'Verified the SHA-256 checksum.' + } + + function Get-PrimeAgentKernelChoice { + if ($env:PRIME_AGENT_BOOTSTRAP_KERNEL_ON_INSTALL -eq '1') { + return $true + } + if ($env:PRIME_AGENT_BOOTSTRAP_KERNEL_ON_INSTALL -eq '0') { + return $false + } + + $prepare = Read-PrimeAgentYesNo ` + -Question 'Prepare IPython runtime now?' ` + -Detail 'Installs uv, Python 3.11, ipykernel, and the Prime Agent runtime.' + if (-not $prepare) { + Write-PrimeAgentNote 'Skipping IPython runtime setup; it is prepared on first ipython use.' + } + return $prepare + } + + function Install-PrimeAgentPackage { + param( + [Parameter(Mandatory = $true)][string] $TarballPath, + [switch] $BootstrapKernel + ) + + $npm = Get-PrimeAgentNpmPath + if (-not $npm) { + throw 'npm is required to install Prime Agent.' + } + + $installEnv = @{ 'PRIME_AGENT_BOOTSTRAP_TOOLS_ON_INSTALL' = '1' } + if ($BootstrapKernel) { + $installEnv['PRIME_AGENT_BOOTSTRAP_KERNEL_ON_INSTALL'] = '1' + $installEnv['PRIME_AGENT_INSTALL_UV'] = '1' + } + + $previousEnv = @{} + foreach ($name in @($installEnv.Keys)) { + $previousEnv[$name] = [Environment]::GetEnvironmentVariable($name, 'Process') + [Environment]::SetEnvironmentVariable($name, $installEnv[$name], 'Process') + } + + Write-PrimeAgentStep 'Installing Prime Agent with npm.' + $exitCode = 0 + try { + & $npm install -g --no-fund --no-audit --loglevel=error --progress=false $TarballPath + $exitCode = $LASTEXITCODE + } finally { + foreach ($name in @($previousEnv.Keys)) { + [Environment]::SetEnvironmentVariable($name, $previousEnv[$name], 'Process') + } + } + + if ($exitCode -ne 0) { + throw "npm install -g failed with exit code $exitCode." + } + } + + function Get-PrimeAgentNpmPrefix { + $npm = Get-PrimeAgentNpmPath + if (-not $npm) { + return $null + } + + try { + $prefix = & $npm config get prefix 2>$null | Select-Object -First 1 + } catch { + return $null + } + if (-not $prefix) { + return $null + } + return ([string]$prefix).Trim() + } + + function Write-PrimeAgentCompletion { + Write-PrimeAgentLine + Write-PrimeAgentStep 'Prime Agent was installed successfully.' + + if (Get-PrimeAgentCommandPath -Name $PrimeAgentCmd) { + Write-PrimeAgentStep "Run it with: $($PrimeAgentCmd)" + Write-PrimeAgentLine + return + } + + $prefix = Get-PrimeAgentNpmPrefix + Write-PrimeAgentLine + Write-PrimeAgentWarning "The $($PrimeAgentCmd) command is not on your PATH yet." + if ($prefix) { + Write-PrimeAgentStep 'Add npm''s global bin directory to your PATH:' + Write-PrimeAgentStep " $prefix" + Write-PrimeAgentLine + Write-PrimeAgentStep 'For the current session:' + Write-PrimeAgentStep " `$env:Path = '$prefix;' + `$env:Path" + } else { + Write-PrimeAgentStep 'Find npm''s global bin directory with:' + Write-PrimeAgentStep ' npm config get prefix' + Write-PrimeAgentStep 'Then add that directory to your PATH.' + } + Write-PrimeAgentNote 'A new terminal also picks up PATH changes made by the Node.js installer.' + Write-PrimeAgentLine + } + + function Invoke-PrimeAgentInstall { + param([string] $ChannelOrVersion) + + if ($PrimeAgentBaseUrl -eq $PrimeAgentUnconfiguredBaseUrl) { + throw 'installer download URL is not configured. Set PRIME_AGENT_DOWNLOAD_BASE_URL or use the installer published by the release workflow.' + } + + Write-PrimeAgentHeader + Test-PrimeAgentPrerequisites + + $version = Resolve-PrimeAgentVersion -ChannelOrVersion $ChannelOrVersion + $tarballName = "$($PrimeAgentPackage)-$version.tgz" + $releaseUrl = "$($PrimeAgentBaseUrl)/releases/v$version" + $tarballUrl = "$releaseUrl/$tarballName" + + $install = Read-PrimeAgentYesNo ` + -Question "Install Prime Agent v$version globally with npm?" ` + -Detail "Downloads and verifies $tarballUrl, then runs npm install -g." + if (-not $install) { + Write-PrimeAgentLine + Write-PrimeAgentStep 'Installation cancelled. No changes were made.' + Write-PrimeAgentLine + return + } + + $bootstrapKernel = Get-PrimeAgentKernelChoice + + Write-PrimeAgentLine + $downloadDir = New-PrimeAgentTempDirectory + try { + $tarballPath = Join-Path $downloadDir $tarballName + $checksumsPath = Join-Path $downloadDir 'SHA256SUMS' + + Write-PrimeAgentStep "Downloading Prime Agent v$version." + Save-PrimeAgentFile -Uri "$releaseUrl/SHA256SUMS" -Path $checksumsPath + Save-PrimeAgentFile -Uri $tarballUrl -Path $tarballPath + Test-PrimeAgentChecksum -ChecksumsPath $checksumsPath -FilePath $tarballPath + Install-PrimeAgentPackage -TarballPath $tarballPath -BootstrapKernel:$bootstrapKernel + } finally { + Remove-Item -LiteralPath $downloadDir -Recurse -Force -ErrorAction SilentlyContinue + } + + Write-PrimeAgentCompletion + } + + Invoke-PrimeAgentInstall -ChannelOrVersion $ChannelOrVersion +} $ChannelOrVersion diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index f7100b579..5c1e5e291 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,8 @@ - Added privacy-safe pseudonymous product analytics for onboarding, command use, execution modes, run outcomes, TTFT, latency, usage, tools, retries, and compactions, with disclosure and opt-out controls ([ENG-4682](https://linear.app/primeintellect/issue/ENG-4682/add-privacy-safe-posthog-analytics-to-prime-agent)). - Changed sent agent messages in the IPython cell UI to show only the message text with a `╰─` gutter when expanded, matching received messages, and hid the raw `agent_message.send` receipt dictionary. - Fixed Homebrew installs attempting to self-update their versioned Cellar keg instead of directing users to `brew upgrade prime-agent` ([#844](https://github.com/PrimeIntellect-ai/prime-agent/issues/844)) +- Added a Windows PowerShell installer (`irm .../install.ps1 | iex`) that verifies the release checksum, installs with npm, and can prepare the IPython runtime. +- Added Windows support to the kernel bootstrap, installing uv with the official PowerShell installer and using the venv's `Scripts/python.exe`. ## [0.7.1] - 2026-08-07 diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 565e7f742..ae76fcb7f 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -52,6 +52,12 @@ To install the beta built from the latest commit on `main`: curl -fsSL https://app.primeintellect.ai/prime-agent/install.sh | sh -s -- beta ``` +On Windows, install from PowerShell (see [Windows setup](docs/windows.md)): + +```powershell +irm https://app.primeintellect.ai/prime-agent/install.ps1 | iex +``` + Authenticate with an API key: ```bash diff --git a/packages/coding-agent/docs/index.md b/packages/coding-agent/docs/index.md index 847fd73b6..d19adf86e 100644 --- a/packages/coding-agent/docs/index.md +++ b/packages/coding-agent/docs/index.md @@ -10,6 +10,12 @@ Install the latest stable release on Linux or macOS: curl -fsSL https://app.primeintellect.ai/prime-agent/install.sh | sh ``` +On Windows, install from PowerShell (see [Windows](windows.md)): + +```powershell +irm https://app.primeintellect.ai/prime-agent/install.ps1 | iex +``` + Then run it in a project directory: ```bash diff --git a/packages/coding-agent/docs/quickstart.md b/packages/coding-agent/docs/quickstart.md index 95fd940ea..7c4edf1d8 100644 --- a/packages/coding-agent/docs/quickstart.md +++ b/packages/coding-agent/docs/quickstart.md @@ -16,7 +16,13 @@ To try the latest beta built from `main`: curl -fsSL https://app.primeintellect.ai/prime-agent/install.sh | sh -s -- beta ``` -Both commands fetch versioned Prime Agent release artifacts and install the `prime-agent` command. The inherited npm workspace identifiers in the source tree are not the public install path. +On Windows, install from PowerShell instead: + +```powershell +irm https://app.primeintellect.ai/prime-agent/install.ps1 | iex +``` + +These commands fetch versioned Prime Agent release artifacts and install the `prime-agent` command. The inherited npm workspace identifiers in the source tree are not the public install path. See [Windows](windows.md) for requirements, PATH notes, and the bash shell Prime Agent needs on Windows. Then start Prime Agent in the project directory you want it to work on: diff --git a/packages/coding-agent/docs/windows.md b/packages/coding-agent/docs/windows.md index 3f7da6c61..cfa81fca7 100644 --- a/packages/coding-agent/docs/windows.md +++ b/packages/coding-agent/docs/windows.md @@ -1,17 +1,117 @@ # Windows Setup -Prime Agent requires a bash shell on Windows. Checked locations (in order): +Prime Agent installs and runs natively on Windows with PowerShell. WSL and Git Bash are not required for the install itself, but a bash shell is required at runtime (see [Bash requirement](#bash-requirement)). -1. Custom path from `~/.prime/agent/settings.json` -2. Git Bash (`C:\Program Files\Git\bin\bash.exe`) +## Install + +Run this in Windows PowerShell 5.1 or PowerShell 7+: + +```powershell +irm https://app.primeintellect.ai/prime-agent/install.ps1 | iex +``` + +To try the latest beta built from `main`: + +```powershell +irm https://app.primeintellect.ai/prime-agent/install-beta.ps1 | iex +``` + +The installer resolves the current release, downloads the release tarball and `SHA256SUMS`, verifies the SHA-256 checksum, and installs the `prime-agent` command with `npm install -g`. It asks before installing and before preparing the IPython runtime; both default to yes, and both are assumed when no terminal is attached. + +Then start Prime Agent in the directory you want it to work on: + +```powershell +cd C:\path\to\project +prime-agent +``` + +## Requirements + +- Windows PowerShell 5.1 or PowerShell 7+ +- Node.js 20.6.0 or newer and npm on PATH +- A bash shell for the agent's shell commands + +The installer does not install Node.js for you. If it is missing: + +```powershell +winget install OpenJS.NodeJS.LTS +``` + +Or download an installer from [nodejs.org](https://nodejs.org). Open a new terminal afterwards so `node` and `npm` are on PATH. + +## Installer Options + +Environment variables, set before piping the installer to `iex`: + +| Variable | Purpose | +| --- | --- | +| `PRIME_AGENT_RELEASE_CHANNEL` | `stable` or `beta` | +| `PRIME_AGENT_VERSION` | Install an explicit version instead of the channel's current release | +| `PRIME_AGENT_DOWNLOAD_BASE_URL` | Alternate release host | +| `PRIME_AGENT_BOOTSTRAP_KERNEL_ON_INSTALL` | `1` prepares the IPython runtime during install, `0` skips it | +| `PRIME_AGENT_PACKAGE`, `PRIME_AGENT_CMD` | Package and command name overrides | + +```powershell +$env:PRIME_AGENT_RELEASE_CHANNEL = 'beta' +$env:PRIME_AGENT_BOOTSTRAP_KERNEL_ON_INSTALL = '1' +irm https://app.primeintellect.ai/prime-agent/install.ps1 | iex +``` + +A downloaded copy of the installer also accepts a channel or version as its first argument: + +```powershell +Invoke-WebRequest https://app.primeintellect.ai/prime-agent/install.ps1 -OutFile install.ps1 +./install.ps1 beta +./install.ps1 0.7.1 +``` + +## PATH + +`npm install -g` puts `prime-agent.cmd` in npm's global prefix. If the command is not found after installing, print the prefix and add it to PATH: + +```powershell +npm config get prefix +``` + +For the current session only: + +```powershell +$env:Path = "$(npm config get prefix);" + $env:Path +``` + +Make it permanent through System Settings, `Environment Variables`, or `setx PATH`. A newly opened terminal also picks up PATH changes made by the Node.js installer. + +## Bash requirement + +The agent runs shell commands, `%%bash` cells, and some tools through bash. Checked locations (in order): + +1. Custom path from `shellPath` in `~/.prime/agent/settings.json` +2. Git Bash (`C:\Program Files\Git\bin\bash.exe`, then the 32-bit Program Files location) 3. `bash.exe` on PATH (Cygwin, MSYS2, WSL) For most users, [Git for Windows](https://git-scm.com/download/win) is sufficient. -## Custom Shell Path +### Custom Shell Path ```json { "shellPath": "C:\\cygwin64\\bin\\bash.exe" } ``` + +## IPython Runtime + +The first IPython use (or the install-time prompt) bootstraps the kernel runtime: + +- `uv` is installed with the official PowerShell installer to `%USERPROFILE%\.local\bin\uv.exe` when it is not already available +- `uv` installs Python 3.11, `ipykernel`, `prime-agent-runtime`, and the default Python packages +- The kernel virtual environment lives in `%USERPROFILE%\.prime\agent\kernel-venv` + +The uv installer only adds `%USERPROFILE%\.local\bin` to the persisted user PATH, so `uv` is not on PATH in the current terminal; Prime Agent looks for the binary in that directory directly. Set `PRIME_AGENT_KERNEL_PYTHON` to an existing Python with `ipykernel` and a current `prime-agent-runtime` to skip the bootstrap entirely. + +## Known Limitations + +- A bash shell must be installed for shell commands to work; Prime Agent does not fall back to `cmd.exe` or PowerShell. +- Image paste uses Alt+V instead of Ctrl+V. +- Windows Terminal is recommended; older console hosts render the TUI poorly. +- Running the Linux installer inside WSL is still supported and is the better option if a project needs a Linux toolchain. diff --git a/packages/coding-agent/src/core/kernel/bootstrap.ts b/packages/coding-agent/src/core/kernel/bootstrap.ts index 9b12b4b41..178eb25e6 100644 --- a/packages/coding-agent/src/core/kernel/bootstrap.ts +++ b/packages/coding-agent/src/core/kernel/bootstrap.ts @@ -36,6 +36,8 @@ export const DEFAULT_RLM_EXTRA_UV_ARGS = DEFAULT_RLM_EXTRA_PACKAGES.map((pkg) => export const DEFAULT_RLM_EXTRA_IMPORT_NAMES = DEFAULT_RLM_EXTRA_PACKAGES.map((pkg) => pkg.importName); export const DEFAULT_RLM_EXTRA_IMPORT_LABELS = DEFAULT_RLM_EXTRA_PACKAGES.map((pkg) => pkg.promptLabel); const UV_INSTALL_COMMAND = "curl -LsSf https://astral.sh/uv/install.sh | sh"; +const UV_INSTALL_COMMAND_WINDOWS = + 'powershell -ExecutionPolicy ByPass -NoProfile -Command "irm https://astral.sh/uv/install.ps1 | iex"'; const REQUIRED_HARNESS_METHODS = [ "create_memory", "update_memory", @@ -336,6 +338,11 @@ function ensureKernelPythonKey(pythonSkills: readonly BootstrapPythonSkill[]): s ].join("\0"); } +// uv (like every venv tool) puts the interpreter in Scripts/python.exe on Windows. +function getKernelVenvPython(venv: string): string { + return process.platform === "win32" ? path.join(venv, "Scripts", "python.exe") : path.join(venv, "bin", "python"); +} + export function getKernelVenvDir(): string { const override = process.env.PRIME_AGENT_KERNEL_VENV; if (override) return path.resolve(expandHome(override)); @@ -511,35 +518,77 @@ async function findExecutable(name: string): Promise { return null; } +function uvInstallCommand(): string { + return process.platform === "win32" ? UV_INSTALL_COMMAND_WINDOWS : UV_INSTALL_COMMAND; +} + +function uvInstaller(): { command: string; args: string[] } { + if (process.platform === "win32") { + return { + command: "powershell", + args: ["-ExecutionPolicy", "ByPass", "-NoProfile", "-Command", "irm https://astral.sh/uv/install.ps1 | iex"], + }; + } + return { command: "sh", args: ["-c", UV_INSTALL_COMMAND] }; +} + +// Both uv installers drop the binary into the first of XDG_BIN_HOME, +// XDG_DATA_HOME/../bin, or ~/.local/bin. On Windows the installer only adds that +// directory to the persisted user PATH, so this process never sees it. +function uvInstallLocations(): string[] { + if (process.platform !== "win32") { + return [path.join(os.homedir(), ".local", "bin", "uv")]; + } + const locations: string[] = []; + if (process.env.XDG_BIN_HOME) locations.push(path.join(process.env.XDG_BIN_HOME, "uv.exe")); + if (process.env.XDG_DATA_HOME) locations.push(path.join(process.env.XDG_DATA_HOME, "..", "bin", "uv.exe")); + locations.push(path.join(os.homedir(), ".local", "bin", "uv.exe")); + return locations; +} + +function uvInstallLocationHint(): string { + return process.platform === "win32" ? "%USERPROFILE%\\.local\\bin\\uv.exe" : "~/.local/bin/uv"; +} + +async function findInstalledUv(): Promise { + for (const location of uvInstallLocations()) { + if (await isExecutable(location)) return location; + } + return null; +} + async function ensureUv(options: EnsureKernelPythonOptions): Promise { const fromPath = await findExecutable("uv"); if (fromPath) return fromPath; - const localUv = path.join(os.homedir(), ".local", "bin", process.platform === "win32" ? "uv.exe" : "uv"); - if (await isExecutable(localUv)) return localUv; + const localUv = await findInstalledUv(); + if (localUv) return localUv; + const installCommand = uvInstallCommand(); const shouldInstallUv = process.env.PRIME_AGENT_INSTALL_UV === "1" || (!options.onProgress && (await confirmUvInstall())); if (!shouldInstallUv) { throw new Error( - `uv is required to set up the Python kernel. Install uv yourself: ${UV_INSTALL_COMMAND}, ` + + `uv is required to set up the Python kernel. Install uv yourself: ${installCommand}, ` + "or set PRIME_AGENT_INSTALL_UV=1 to let prime-agent run that installer.", ); } reportProgress(options, "› installing uv (one-time)…"); + const installer = uvInstaller(); try { - await run("sh", ["-c", UV_INSTALL_COMMAND], { stdio: options.onProgress ? "ignore" : "inherit" }); + await run(installer.command, installer.args, { stdio: options.onProgress ? "ignore" : "inherit" }); } catch (error) { throw new Error( - `couldn't install uv from astral.sh; install it yourself: ${UV_INSTALL_COMMAND}, then re-run prime-agent. ${errorMessage(error)}`, + `couldn't install uv from astral.sh; install it yourself: ${installCommand}, then re-run prime-agent. ${errorMessage(error)}`, ); } - if (await isExecutable(localUv)) return localUv; + const installedUv = await findInstalledUv(); + if (installedUv) return installedUv; const installedFromPath = await findExecutable("uv"); if (installedFromPath) return installedFromPath; - throw new Error("uv install completed but binary not found at ~/.local/bin/uv"); + throw new Error(`uv install completed but binary not found at ${uvInstallLocationHint()}`); } async function confirmUvInstall(): Promise { @@ -725,7 +774,7 @@ async function bootstrapVenv( ): Promise { await mkdir(path.dirname(venv), { recursive: true }); const uv = await ensureUv(options); - const python = path.join(venv, "bin", "python"); + const python = getKernelVenvPython(venv); const sourceDir = await resolveRuntimeSourceDir(); const runtimeRequirement = sourceDir ?? RUNTIME_REQUIREMENT; const runtimeIdentity = await resolveRuntimeIdentity(); @@ -886,7 +935,7 @@ async function ensureKernelPythonUncached( } const venv = await resolveWritableKernelVenvDir(); - const python = path.join(venv, "bin", "python"); + const python = getKernelVenvPython(venv); const runtimeIdentity = await resolveRuntimeIdentity(); if (await kernelReady(python, venv, runtimeIdentity, pythonSkills)) return python; diff --git a/scripts/check-installer-render.mjs b/scripts/check-installer-render.mjs index 835f306bb..4fe8af51c 100644 --- a/scripts/check-installer-render.mjs +++ b/scripts/check-installer-render.mjs @@ -4,6 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; const installerSource = readFileSync("install.sh", "utf-8"); +const powershellInstallerSource = readFileSync("install.ps1", "utf-8"); const mainCall = '\nmain "$@"'; const mainCallIndex = installerSource.lastIndexOf(mainCall); const ansiPattern = /\x1b\[[0-?]*[ -/]*[@-~]/g; @@ -156,6 +157,9 @@ try { rmSync(tempDir, { recursive: true, force: true }); } +checkInstallerRelease("install.sh", installerSource); +checkInstallerRelease("install.ps1", powershellInstallerSource); + if (failures.length > 0) { console.error(["Installer render check failed:", ...failures.map((failure) => `- ${failure}`)].join("\n")); process.exit(1); @@ -163,6 +167,32 @@ if (failures.length > 0) { console.log("Installer render check passed."); +// The release workflow rewrites the two configured sentinels in place. Unrendered +// copies must keep the split literals so they can still detect "not configured yet". +function checkInstallerRelease(name, source) { + const baseUrlSentinel = "__PRIME_AGENT_DOWNLOAD_BASE_URL__"; + const channelSentinel = "__PRIME_AGENT_DEFAULT_RELEASE_CHANNEL__"; + const baseUrl = "https://example.invalid/prime-agent"; + + check(source.includes(baseUrlSentinel), `${name}: expected a ${baseUrlSentinel} placeholder`); + check(source.includes(channelSentinel), `${name}: expected a ${channelSentinel} placeholder`); + + for (const channel of ["stable", "beta"]) { + const rendered = source.replaceAll(baseUrlSentinel, baseUrl).replaceAll(channelSentinel, channel); + check(!rendered.includes(baseUrlSentinel), `${name}: ${channel} render left a ${baseUrlSentinel} placeholder`); + check(!rendered.includes(channelSentinel), `${name}: ${channel} render left a ${channelSentinel} placeholder`); + check(rendered.includes(baseUrl), `${name}: ${channel} render did not apply the download base URL`); + check( + rendered.includes('__PRIME_AGENT_DOWNLOAD_BASE"') || rendered.includes("__PRIME_AGENT_DOWNLOAD_BASE'"), + `${name}: ${channel} render lost the split unconfigured base URL sentinel`, + ); + check( + rendered.includes('__PRIME_AGENT_DEFAULT_RELEASE_"') || rendered.includes("__PRIME_AGENT_DEFAULT_RELEASE_'"), + `${name}: ${channel} render lost the split unconfigured channel sentinel`, + ); + } +} + function runCase(name, initialCols, initialRows, resizedCols, resizedRows) { const result = spawnSync("sh", [harnessPath, String(initialCols), String(initialRows), String(resizedCols), String(resizedRows)], { detached: true, From 4814cb4b59743b04c969390d31474ff4bdcc9c4f Mon Sep 17 00:00:00 2001 From: j-token Date: Mon, 10 Aug 2026 12:33:11 +0900 Subject: [PATCH 2/4] fix(windows): hide spawned process console windows --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/cli/daemon-command.ts | 1 + packages/coding-agent/src/cli/daemon-ps.ts | 17 ++++++++++++----- .../src/cli/daemon-update-restart.ts | 1 + .../src/cli/owned-session-worker.ts | 1 + packages/coding-agent/src/config.ts | 1 + packages/coding-agent/src/core/autonomous.ts | 1 + packages/coding-agent/src/core/exec.ts | 1 + .../src/core/footer-data-provider.ts | 2 ++ .../coding-agent/src/core/kernel/bootstrap.ts | 1 + .../coding-agent/src/core/kernel/fork-server.ts | 1 + packages/coding-agent/src/core/kernel/index.ts | 1 + .../coding-agent/src/core/package-manager.ts | 3 +++ .../src/core/resolve-config-value.ts | 1 + .../src/core/session-file-actions.ts | 2 +- packages/coding-agent/src/core/session-lease.ts | 1 + packages/coding-agent/src/core/tools/bash.ts | 1 + .../src/modes/daemon/daemon-catalog-process.ts | 1 + .../src/modes/daemon/daemon-mode.ts | 1 + .../src/modes/daemon/daemon-supervisor.ts | 2 ++ .../interactive/components/extension-editor.ts | 1 + .../interactive/components/login-dialog.ts | 2 +- .../src/modes/interactive/interactive-mode.ts | 7 +++++-- .../coding-agent/src/modes/rpc/rpc-client.ts | 1 + .../src/modes/shared/startup-notices.ts | 1 + .../coding-agent/src/package-manager-cli.ts | 1 + .../coding-agent/src/utils/clipboard-image.ts | 1 + packages/coding-agent/src/utils/clipboard.ts | 12 +++++++++--- packages/coding-agent/src/utils/git.ts | 1 + packages/coding-agent/src/utils/shell.ts | 5 +++-- .../coding-agent/src/utils/tools-manager.ts | 7 +++++-- packages/tui/CHANGELOG.md | 2 ++ packages/tui/src/autocomplete.ts | 1 + 33 files changed, 67 insertions(+), 16 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5c1e5e291..a3872dc0f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ - Added privacy-safe pseudonymous product analytics for onboarding, command use, execution modes, run outcomes, TTFT, latency, usage, tools, retries, and compactions, with disclosure and opt-out controls ([ENG-4682](https://linear.app/primeintellect/issue/ENG-4682/add-privacy-safe-posthog-analytics-to-prime-agent)). - Changed sent agent messages in the IPython cell UI to show only the message text with a `╰─` gutter when expanded, matching received messages, and hid the raw `agent_message.send` receipt dictionary. +- Fixed Windows child processes opening transient console windows. - Fixed Homebrew installs attempting to self-update their versioned Cellar keg instead of directing users to `brew upgrade prime-agent` ([#844](https://github.com/PrimeIntellect-ai/prime-agent/issues/844)) - Added a Windows PowerShell installer (`irm .../install.ps1 | iex`) that verifies the release checksum, installs with npm, and can prepare the IPython runtime. - Added Windows support to the kernel bootstrap, installing uv with the official PowerShell installer and using the venv's `Scripts/python.exe`. diff --git a/packages/coding-agent/src/cli/daemon-command.ts b/packages/coding-agent/src/cli/daemon-command.ts index 9883707bd..f87bf7116 100644 --- a/packages/coding-agent/src/cli/daemon-command.ts +++ b/packages/coding-agent/src/cli/daemon-command.ts @@ -692,6 +692,7 @@ async function runStart(parsed: ParsedDaemonClientCommand): Promise { ...sessionArgs.daemonArgs.filter((arg) => arg !== "--background" && arg !== "-d"), ]; const child = spawn(process.execPath, daemonArgs, { + windowsHide: true, cwd: sessionArgs.config?.cwd ?? process.cwd(), detached: true, env: process.env, diff --git a/packages/coding-agent/src/cli/daemon-ps.ts b/packages/coding-agent/src/cli/daemon-ps.ts index 6f22f823e..4a9bd4416 100644 --- a/packages/coding-agent/src/cli/daemon-ps.ts +++ b/packages/coding-agent/src/cli/daemon-ps.ts @@ -178,18 +178,22 @@ function scanListeningDaemons(): DiscoveredDaemonProcess[] { if (process.platform === "win32") { return []; } - const ss = spawnSync("ss", ["-lxp"], { encoding: "utf8" }); + const ss = spawnSync("ss", ["-lxp"], { windowsHide: true, encoding: "utf8" }); if (!ss.error && ss.status === 0 && typeof ss.stdout === "string") { return enrichUptimes(parseSsListeners(ss.stdout, APP_NAME)); } - const lsof = spawnSync("lsof", ["-nP", "-F", "pn", "-U", "-a", "-c", APP_NAME], { encoding: "utf8" }); + const lsof = spawnSync("lsof", ["-nP", "-F", "pn", "-U", "-a", "-c", APP_NAME], { + windowsHide: true, + encoding: "utf8", + }); const byName = !lsof.error && typeof lsof.stdout === "string" ? parseLsofListeners(lsof.stdout) : []; let byPid: DiscoveredDaemonProcess[] = []; - const ps = spawnSync("ps", ["-axo", "pid=,comm=,args="], { encoding: "utf8" }); + const ps = spawnSync("ps", ["-axo", "pid=,comm=,args="], { windowsHide: true, encoding: "utf8" }); if (!ps.error && ps.status === 0 && typeof ps.stdout === "string") { const pids = parsePrimeAgentProcessIds(ps.stdout, APP_NAME); if (pids.length > 0) { const lsofByPid = spawnSync("lsof", ["-nP", "-F", "pn", "-U", "-a", "-p", pids.join(",")], { + windowsHide: true, encoding: "utf8", }); if (!lsofByPid.error && typeof lsofByPid.stdout === "string") { @@ -210,7 +214,7 @@ function enrichUptimes(daemons: DiscoveredDaemonProcess[]): DiscoveredDaemonProc if (pids.length === 0) { return daemons; } - const ps = spawnSync("ps", ["-o", "pid=,etimes=", "-p", pids.join(",")], { encoding: "utf8" }); + const ps = spawnSync("ps", ["-o", "pid=,etimes=", "-p", pids.join(",")], { windowsHide: true, encoding: "utf8" }); if (ps.error || typeof ps.stdout !== "string") { return daemons; } @@ -806,7 +810,10 @@ function recordResidualListenerFailures( } } function describeDaemonParent(pid: number): string { - const result = spawnSync("ps", ["-o", "ppid=,tty=,command=", "-p", String(pid)], { encoding: "utf8" }); + const result = spawnSync("ps", ["-o", "ppid=,tty=,command=", "-p", String(pid)], { + windowsHide: true, + encoding: "utf8", + }); if (result.error || result.status !== 0 || typeof result.stdout !== "string") { return ""; } diff --git a/packages/coding-agent/src/cli/daemon-update-restart.ts b/packages/coding-agent/src/cli/daemon-update-restart.ts index 7dc44ed57..8b007ee3d 100644 --- a/packages/coding-agent/src/cli/daemon-update-restart.ts +++ b/packages/coding-agent/src/cli/daemon-update-restart.ts @@ -548,6 +548,7 @@ export async function launchDaemonUpdateRestartCoordinator( ...(originActiveSessionId ? [DAEMON_UPDATE_RESTART_ORIGIN_FLAG, originActiveSessionId] : []), ]); const child = spawn(launch.command, launch.args, { + windowsHide: true, cwd: options.cwd ?? process.cwd(), detached: true, env: coordinatorEnvironment(agentDir), diff --git a/packages/coding-agent/src/cli/owned-session-worker.ts b/packages/coding-agent/src/cli/owned-session-worker.ts index f7ab37535..b8d148fc4 100644 --- a/packages/coding-agent/src/cli/owned-session-worker.ts +++ b/packages/coding-agent/src/cli/owned-session-worker.ts @@ -344,6 +344,7 @@ export async function runOwnedSessionWorkerFrontend( ? ["inherit", "inherit", "inherit", "ipc"] : [bridgeStdin ? "pipe" : "inherit", "pipe", "pipe", "ipc"]; const child = spawn(launch.command, launch.args, { + windowsHide: true, cwd: process.cwd(), detached: process.platform !== "win32", env: { diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index b709ab10e..40ae31043 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -207,6 +207,7 @@ function readCommandOutput( options: { requireSuccess?: boolean } = {}, ): string | undefined { const result = spawnSync(command, args, { + windowsHide: true, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], shell: shouldUseWindowsShell(command), diff --git a/packages/coding-agent/src/core/autonomous.ts b/packages/coding-agent/src/core/autonomous.ts index 75930d415..3215ebff1 100644 --- a/packages/coding-agent/src/core/autonomous.ts +++ b/packages/coding-agent/src/core/autonomous.ts @@ -492,6 +492,7 @@ function runChildProcess( options.signal?.throwIfAborted(); return new Promise((resolve) => { const child = spawn(command, args, { + windowsHide: true, cwd: options.cwd, detached: process.platform !== "win32", shell: options.shell === true, diff --git a/packages/coding-agent/src/core/exec.ts b/packages/coding-agent/src/core/exec.ts index bd62eb5a0..e2088ffcd 100644 --- a/packages/coding-agent/src/core/exec.ts +++ b/packages/coding-agent/src/core/exec.ts @@ -59,6 +59,7 @@ export async function execCommand( ): Promise { return new Promise((resolve) => { const proc = spawn(command, args, { + windowsHide: true, cwd, shell: false, stdio: ["ignore", "pipe", "pipe"], diff --git a/packages/coding-agent/src/core/footer-data-provider.ts b/packages/coding-agent/src/core/footer-data-provider.ts index 2aff8ac32..f759dfe7d 100644 --- a/packages/coding-agent/src/core/footer-data-provider.ts +++ b/packages/coding-agent/src/core/footer-data-provider.ts @@ -7,6 +7,7 @@ import { findGitPaths, type GitPaths } from "../utils/git.js"; /** Ask git for the current branch. Returns null on detached HEAD or if git is unavailable. */ function resolveBranchWithGitSync(repoDir: string): string | null { const result = spawnSync("git", ["--no-optional-locks", "symbolic-ref", "--quiet", "--short", "HEAD"], { + windowsHide: true, cwd: repoDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], @@ -22,6 +23,7 @@ function resolveBranchWithGitAsync(repoDir: string): Promise { "git", ["--no-optional-locks", "symbolic-ref", "--quiet", "--short", "HEAD"], { + windowsHide: true, cwd: repoDir, encoding: "utf8", }, diff --git a/packages/coding-agent/src/core/kernel/bootstrap.ts b/packages/coding-agent/src/core/kernel/bootstrap.ts index 178eb25e6..d4a5133c6 100644 --- a/packages/coding-agent/src/core/kernel/bootstrap.ts +++ b/packages/coding-agent/src/core/kernel/bootstrap.ts @@ -381,6 +381,7 @@ async function resolveWritableKernelVenvDir(): Promise { function run(command: string, args: string[], options: { stdio?: "ignore" | "inherit" } = {}): Promise { return new Promise((resolve, reject) => { const child = spawn(command, args, { + windowsHide: true, env: process.env, stdio: options.stdio ?? "ignore", }); diff --git a/packages/coding-agent/src/core/kernel/fork-server.ts b/packages/coding-agent/src/core/kernel/fork-server.ts index d5b5423ac..efaa8778d 100644 --- a/packages/coding-agent/src/core/kernel/fork-server.ts +++ b/packages/coding-agent/src/core/kernel/fork-server.ts @@ -173,6 +173,7 @@ class ForkServer { // The template only imports; its own cwd/env are irrelevant since each // forked child applies the per-kernel cwd/env itself. Inherit the daemon's. const proc = spawn(this.params.python, ["-c", FORK_SERVER_SCRIPT, socketPath], { + windowsHide: true, env: this.launchEnv, stdio: ["ignore", "ignore", "pipe"], }); diff --git a/packages/coding-agent/src/core/kernel/index.ts b/packages/coding-agent/src/core/kernel/index.ts index b760a2e1e..86b7e9802 100644 --- a/packages/coding-agent/src/core/kernel/index.ts +++ b/packages/coding-agent/src/core/kernel/index.ts @@ -645,6 +645,7 @@ export class KernelManager { if (!forked) { const kernel = spawn(python, ["-m", "ipykernel_launcher", "-f", connection.path], { + windowsHide: true, cwd: this.options.cwd, env: this.options.env ? { ...process.env, ...this.options.env } : process.env, stdio: ["ignore", "pipe", "pipe"], diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index fd818595f..d9dbeee50 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -2379,6 +2379,7 @@ export class DefaultPackageManager implements PackageManager { private spawnCommand(command: string, args: string[], options?: { cwd?: string }): ChildProcess { return spawn(command, args, { + windowsHide: true, cwd: options?.cwd, stdio: isStdoutTakenOver() ? ["ignore", 2, 2] : "inherit", shell: shouldUseWindowsShell(command), @@ -2393,6 +2394,7 @@ export class DefaultPackageManager implements PackageManager { ): ChildProcessByStdio { const baseEnv = getEnv(); return spawn(command, args, { + windowsHide: true, cwd: options?.cwd, stdio: ["ignore", "pipe", "pipe"], shell: shouldUseWindowsShell(command), @@ -2460,6 +2462,7 @@ export class DefaultPackageManager implements PackageManager { private runCommandSync(command: string, args: string[]): string { const result = spawnSync(command, args, { + windowsHide: true, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8", shell: shouldUseWindowsShell(command), diff --git a/packages/coding-agent/src/core/resolve-config-value.ts b/packages/coding-agent/src/core/resolve-config-value.ts index 646042e1c..32c00d6f0 100644 --- a/packages/coding-agent/src/core/resolve-config-value.ts +++ b/packages/coding-agent/src/core/resolve-config-value.ts @@ -55,6 +55,7 @@ function executeWithConfiguredShell(command: string): { executed: boolean; value function executeWithDefaultShell(command: string): string | undefined { try { const output = execSync(command, { + windowsHide: true, encoding: "utf-8", timeout: 10000, stdio: ["ignore", "pipe", "ignore"], diff --git a/packages/coding-agent/src/core/session-file-actions.ts b/packages/coding-agent/src/core/session-file-actions.ts index adcec4c01..cdc6f1b0c 100644 --- a/packages/coding-agent/src/core/session-file-actions.ts +++ b/packages/coding-agent/src/core/session-file-actions.ts @@ -25,7 +25,7 @@ async function deleteSessionArtifacts(sessionPath: string): Promise { /** Remove the session `.jsonl`, trying the `trash` CLI first, then falling back to unlink. */ async function removeSessionFile(sessionPath: string): Promise { const trashArgs = sessionPath.startsWith("-") ? ["--", sessionPath] : [sessionPath]; - const trashResult = spawnSync("trash", trashArgs, { encoding: "utf-8" }); + const trashResult = spawnSync("trash", trashArgs, { windowsHide: true, encoding: "utf-8" }); const getTrashErrorHint = (): string | null => { const parts: string[] = []; diff --git a/packages/coding-agent/src/core/session-lease.ts b/packages/coding-agent/src/core/session-lease.ts index 6c4e2975c..763a6300a 100644 --- a/packages/coding-agent/src/core/session-lease.ts +++ b/packages/coding-agent/src/core/session-lease.ts @@ -114,6 +114,7 @@ type ProcessQuery = (command: string, args: string[]) => string; function runProcessQuery(command: string, args: string[]): string { return execFileSync(command, args, { + windowsHide: true, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], }); diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index efc260eae..6b75c4051 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -73,6 +73,7 @@ export function createLocalBashOperations(options?: { shellPath?: string }): Bas return; } const child = spawn(shell, [...args, command], { + windowsHide: true, cwd, detached: process.platform !== "win32", env: env ?? getShellEnv(), diff --git a/packages/coding-agent/src/modes/daemon/daemon-catalog-process.ts b/packages/coding-agent/src/modes/daemon/daemon-catalog-process.ts index c841b5b79..13ec7edb0 100644 --- a/packages/coding-agent/src/modes/daemon/daemon-catalog-process.ts +++ b/packages/coding-agent/src/modes/daemon/daemon-catalog-process.ts @@ -393,6 +393,7 @@ export class DaemonCatalogClient { private async spawnCatalog(): Promise { const launch = createCliSubprocessLaunchSpec(["--version"]); const child = spawn(launch.command, launch.args, { + windowsHide: true, cwd: process.cwd(), env: createCliSubprocessEnv({ ...process.env, [DAEMON_CATALOG_ROLE_ENV]: "1" }), stdio: ["ignore", "ignore", "ignore", "ipc"], diff --git a/packages/coding-agent/src/modes/daemon/daemon-mode.ts b/packages/coding-agent/src/modes/daemon/daemon-mode.ts index dfebcdf61..5a7a3328d 100644 --- a/packages/coding-agent/src/modes/daemon/daemon-mode.ts +++ b/packages/coding-agent/src/modes/daemon/daemon-mode.ts @@ -816,6 +816,7 @@ export class AgentDaemon { delete environment[SESSION_LEASES_ENABLED_ENV]; delete environment[SESSION_LEASE_OWNER_ID_ENV]; const child = spawn(launch.command, launch.args, { + windowsHide: true, cwd: this.options.defaultSessionConfig.cwd ?? process.cwd(), detached: true, env: environment, diff --git a/packages/coding-agent/src/modes/daemon/daemon-supervisor.ts b/packages/coding-agent/src/modes/daemon/daemon-supervisor.ts index b22701bbc..0abf81da3 100644 --- a/packages/coding-agent/src/modes/daemon/daemon-supervisor.ts +++ b/packages/coding-agent/src/modes/daemon/daemon-supervisor.ts @@ -2115,6 +2115,7 @@ export class DaemonSupervisor { const launch = createCliSubprocessLaunchSpec(["--mode", "daemon", "--daemon-socket", socketPath]); await this.assertRecoveryAllowed(); const child: ChildProcess = spawn(launch.command, launch.args, { + windowsHide: true, cwd: createCommand.config?.cwd ?? process.cwd(), detached: true, env: createCliSubprocessEnv({ @@ -4860,6 +4861,7 @@ export class DaemonSupervisor { delete environment[SESSION_LEASES_ENABLED_ENV]; delete environment[SESSION_LEASE_OWNER_ID_ENV]; const replacement = spawn(launch.command, launch.args, { + windowsHide: true, cwd: this.defaultSessionConfig.cwd ?? process.cwd(), detached: true, env: environment, diff --git a/packages/coding-agent/src/modes/interactive/components/extension-editor.ts b/packages/coding-agent/src/modes/interactive/components/extension-editor.ts index e1134ce96..2c6b71990 100644 --- a/packages/coding-agent/src/modes/interactive/components/extension-editor.ts +++ b/packages/coding-agent/src/modes/interactive/components/extension-editor.ts @@ -125,6 +125,7 @@ export class ExtensionEditorComponent extends Container implements Focusable { const [editor, ...editorArgs] = editorCmd.split(" "); const result = spawnSync(editor, [...editorArgs, tmpFile], { + windowsHide: true, stdio: "inherit", shell: process.platform === "win32", }); diff --git a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts index 60934fbcd..9679f5cc6 100644 --- a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts +++ b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts @@ -167,7 +167,7 @@ export class LoginDialogComponent extends Container implements Focusable { url, ] : ["xdg-open", url]; - execFile(command, args, () => {}); + execFile(command, args, { windowsHide: true }, () => {}); this.tui.requestRender(); } diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 0ec1d8bcd..0a755dbbc 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -7047,6 +7047,7 @@ export class InteractiveMode { // Spawn editor synchronously with inherited stdio for interactive editing const result = spawnSync(editor, [...editorArgs, tmpFile], { + windowsHide: true, stdio: "inherit", shell: process.platform === "win32", }); @@ -8436,6 +8437,7 @@ export class InteractiveMode { process.execPath, [...process.execArgv, entrypoint, "update", ...updateChildArgs], { + windowsHide: true, stdio: "inherit", cwd: updateCwd, env: updateEnv, @@ -8487,6 +8489,7 @@ export class InteractiveMode { } } const relaunchResult = spawnSync(process.execPath, [...process.execArgv, entrypoint, ...relaunchArgs], { + windowsHide: true, stdio: "inherit", cwd: updateCwd, env: process.env, @@ -8703,7 +8706,7 @@ export class InteractiveMode { private async handleShareCommand(): Promise { // Check if gh is available and logged in try { - const authResult = spawnSync("gh", ["auth", "status"], { encoding: "utf-8" }); + const authResult = spawnSync("gh", ["auth", "status"], { windowsHide: true, encoding: "utf-8" }); if (authResult.status !== 0) { this.showError("GitHub CLI is not logged in. Run 'gh auth login' first."); return; @@ -8752,7 +8755,7 @@ export class InteractiveMode { try { const result = await new Promise<{ stdout: string; stderr: string; code: number | null }>((resolve) => { - proc = spawn("gh", ["gist", "create", "--public=false", tmpFile]); + proc = spawn("gh", ["gist", "create", "--public=false", tmpFile], { windowsHide: true }); let stdout = ""; let stderr = ""; proc.stdout?.on("data", (data) => { diff --git a/packages/coding-agent/src/modes/rpc/rpc-client.ts b/packages/coding-agent/src/modes/rpc/rpc-client.ts index 7c3278816..5e14038b3 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-client.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-client.ts @@ -104,6 +104,7 @@ export class RpcClient { } this.process = spawn("node", [cliPath, ...args], { + windowsHide: true, cwd: this.options.cwd, env: { ...process.env, ...this.options.env }, stdio: ["pipe", "pipe", "pipe"], diff --git a/packages/coding-agent/src/modes/shared/startup-notices.ts b/packages/coding-agent/src/modes/shared/startup-notices.ts index 1fe9ea1ad..c16e0942d 100644 --- a/packages/coding-agent/src/modes/shared/startup-notices.ts +++ b/packages/coding-agent/src/modes/shared/startup-notices.ts @@ -67,6 +67,7 @@ export async function checkTmuxKeyboardSetup(): Promise { const runTmuxShow = (option: string): Promise => { return new Promise((resolve) => { const proc = spawn("tmux", ["show", "-gv", option], { + windowsHide: true, stdio: ["ignore", "pipe", "ignore"], }); let stdout = ""; diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index a9f4bd6cb..01e1ad044 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -464,6 +464,7 @@ async function runSelfUpdate(command: SelfUpdateCommand): Promise { await new Promise((resolve, reject) => { // Windows package managers are commonly .cmd shims. Use the shell so Node can execute them. const child = spawn(step.command, step.args, { + windowsHide: true, stdio: "inherit", shell: shouldUseWindowsShell(step.command), }); diff --git a/packages/coding-agent/src/utils/clipboard-image.ts b/packages/coding-agent/src/utils/clipboard-image.ts index 4cf44908f..1de85a275 100644 --- a/packages/coding-agent/src/utils/clipboard-image.ts +++ b/packages/coding-agent/src/utils/clipboard-image.ts @@ -95,6 +95,7 @@ function runCommand( const maxBufferBytes = options?.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES; const result = spawnSync(command, args, { + windowsHide: true, timeout: timeoutMs, maxBuffer: maxBufferBytes, env: options?.env, diff --git a/packages/coding-agent/src/utils/clipboard.ts b/packages/coding-agent/src/utils/clipboard.ts index 84166e95e..e880b61bc 100644 --- a/packages/coding-agent/src/utils/clipboard.ts +++ b/packages/coding-agent/src/utils/clipboard.ts @@ -7,6 +7,7 @@ type NativeClipboardExecOptions = { input: string; timeout: number; stdio: ["pipe", "ignore", "ignore"]; + windowsHide: true; }; function copyToX11Clipboard(options: NativeClipboardExecOptions): void { @@ -61,7 +62,12 @@ export async function copyToClipboard(text: string): Promise { return; } - const options: NativeClipboardExecOptions = { input: text, timeout: 5000, stdio: ["pipe", "ignore", "ignore"] }; + const options: NativeClipboardExecOptions = { + input: text, + timeout: 5000, + stdio: ["pipe", "ignore", "ignore"], + windowsHide: true, + }; if (!copied) { try { @@ -89,9 +95,9 @@ export async function copyToClipboard(text: string): Promise { if (isWayland && hasWaylandDisplay) { try { // Verify wl-copy exists (spawn errors are async and won't be caught) - execSync("which wl-copy", { stdio: "ignore" }); + execSync("which wl-copy", { windowsHide: true, stdio: "ignore" }); // wl-copy with execSync hangs due to fork behavior; use spawn instead - const proc = spawn("wl-copy", [], { stdio: ["pipe", "ignore", "ignore"] }); + const proc = spawn("wl-copy", [], { windowsHide: true, stdio: ["pipe", "ignore", "ignore"] }); proc.stdin.on("error", () => { // Ignore EPIPE errors if wl-copy exits early }); diff --git a/packages/coding-agent/src/utils/git.ts b/packages/coding-agent/src/utils/git.ts index b60d98a00..1ad87bf2f 100644 --- a/packages/coding-agent/src/utils/git.ts +++ b/packages/coding-agent/src/utils/git.ts @@ -250,6 +250,7 @@ export function gitContextsEqual(a: GitContext, b: GitContext): boolean { function runGit(cwd: string, args: string[]): string | null { const result = spawnSync("git", ["--no-optional-locks", ...args], { + windowsHide: true, cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], diff --git a/packages/coding-agent/src/utils/shell.ts b/packages/coding-agent/src/utils/shell.ts index cbf289d86..5ebe01324 100644 --- a/packages/coding-agent/src/utils/shell.ts +++ b/packages/coding-agent/src/utils/shell.ts @@ -16,7 +16,7 @@ function findBashOnPath(): string | null { if (process.platform === "win32") { // Windows: Use 'where' and verify file exists (where can return non-existent paths) try { - const result = spawnSync("where", ["bash.exe"], { encoding: "utf-8", timeout: 5000 }); + const result = spawnSync("where", ["bash.exe"], { windowsHide: true, encoding: "utf-8", timeout: 5000 }); if (result.status === 0 && result.stdout) { const firstMatch = result.stdout.trim().split(/\r?\n/)[0]; if (firstMatch && existsSync(firstMatch)) { @@ -31,7 +31,7 @@ function findBashOnPath(): string | null { // Unix: Use 'which' and trust its output (handles Termux and special filesystems) try { - const result = spawnSync("which", ["bash"], { encoding: "utf-8", timeout: 5000 }); + const result = spawnSync("which", ["bash"], { windowsHide: true, encoding: "utf-8", timeout: 5000 }); if (result.status === 0 && result.stdout) { const firstMatch = result.stdout.trim().split(/\r?\n/)[0]; if (firstMatch) { @@ -192,6 +192,7 @@ export function killProcessTree(pid: number): void { // Use taskkill on Windows to kill process tree try { spawn("taskkill", ["/F", "/T", "/PID", String(pid)], { + windowsHide: true, stdio: "ignore", detached: true, }); diff --git a/packages/coding-agent/src/utils/tools-manager.ts b/packages/coding-agent/src/utils/tools-manager.ts index c3da7045e..50914fd34 100644 --- a/packages/coding-agent/src/utils/tools-manager.ts +++ b/packages/coding-agent/src/utils/tools-manager.ts @@ -100,7 +100,7 @@ const TOOLS: Record = { // Check that a command both launches and reports a successful version. function commandWorks(cmd: string): boolean { try { - const result = spawnSync(cmd, ["--version"], { stdio: "pipe", timeout: COMMAND_TIMEOUT_MS }); + const result = spawnSync(cmd, ["--version"], { windowsHide: true, stdio: "pipe", timeout: COMMAND_TIMEOUT_MS }); return !result.error && result.status === 0; } catch { return false; @@ -224,7 +224,10 @@ async function downloadTool(tool: ManagedTool): Promise { try { if (assetName.endsWith(".tar.gz")) { - const extractResult = spawnSync("tar", ["xzf", archivePath, "-C", extractDir], { stdio: "pipe" }); + const extractResult = spawnSync("tar", ["xzf", archivePath, "-C", extractDir], { + windowsHide: true, + stdio: "pipe", + }); if (extractResult.error || extractResult.status !== 0) { const errMsg = extractResult.error?.message ?? extractResult.stderr?.toString().trim() ?? "unknown error"; throw new Error(`Failed to extract ${assetName}: ${errMsg}`); diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index abf78757a..a73c655a7 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +- Fixed Windows file autocomplete opening transient console windows. + ## [0.7.1] - 2026-08-07 ## [0.7.0] - 2026-08-05 diff --git a/packages/tui/src/autocomplete.ts b/packages/tui/src/autocomplete.ts index bc11f530e..985f5ba63 100644 --- a/packages/tui/src/autocomplete.ts +++ b/packages/tui/src/autocomplete.ts @@ -163,6 +163,7 @@ async function walkDirectoryWithFd( } const child = spawn(fdPath, args, { + windowsHide: true, stdio: ["ignore", "pipe", "pipe"], }); let stdout = ""; From e2777d2712ab2dfa0c45f5e582ec193c36c49571 Mon Sep 17 00:00:00 2001 From: skulitom Date: Thu, 6 Aug 2026 01:15:12 +0100 Subject: [PATCH 3/4] fix(coding-agent): resolve kernel venv interpreter for Windows layout bootstrapVenv and ensureKernelPythonUncached built the interpreter path as /bin/python, which only exists on POSIX. On Windows the interpreter is at /Scripts/python.exe, so uv pip install --python got a path that does not exist and exited 2, leaving the kernel unusable. The same path feeds kernelReady, so on Windows it was always false: every launch fell through to bootstrapVenv, which runs uv venv --seed and recreates the venv. A manually provisioned venv was therefore discarded on the next run. Add venvPython to pick the layout from process.platform, matching the existing win32 branch in findExecutable, and use it at both sites. Update the two integration tests that hardcoded the POSIX candidate path. Fixes #660 Co-Authored-By: Claude Opus 5 --- packages/coding-agent/docs/rlm-runtime.md | 2 +- .../coding-agent/src/core/kernel/bootstrap.ts | 13 ++++++------- .../coding-agent/test/ipython-bootstrap.test.ts | 3 ++- .../coding-agent/test/kernel-bootstrap.test.ts | 16 ++++++++++++++++ .../test/kernel-state-roundtrip.test.ts | 3 ++- 5 files changed, 27 insertions(+), 10 deletions(-) diff --git a/packages/coding-agent/docs/rlm-runtime.md b/packages/coding-agent/docs/rlm-runtime.md index 4de095cf0..a3ceaf35d 100644 --- a/packages/coding-agent/docs/rlm-runtime.md +++ b/packages/coding-agent/docs/rlm-runtime.md @@ -76,7 +76,7 @@ The Python side does not call providers or implement an agent loop. The kernel is created lazily on first IPython use. Python resolution is: 1. `PRIME_AGENT_KERNEL_PYTHON`, when it can import `ipykernel`; -2. `~/.prime/agent/kernel-venv/bin/python`, bootstrapped with `uv`; or +2. `~/.prime/agent/kernel-venv/bin/python` (`Scripts\python.exe` on Windows), bootstrapped with `uv`; or 3. the XDG data location when `~/.prime` is not writable. The managed environment includes Python 3.11, `ipykernel`, and `prime-agent-runtime`. A bootstrap marker detects stale environments. diff --git a/packages/coding-agent/src/core/kernel/bootstrap.ts b/packages/coding-agent/src/core/kernel/bootstrap.ts index d4a5133c6..18304ae8e 100644 --- a/packages/coding-agent/src/core/kernel/bootstrap.ts +++ b/packages/coding-agent/src/core/kernel/bootstrap.ts @@ -121,6 +121,10 @@ function expandHome(filePath: string): string { return filePath; } +export function venvPython(venv: string): string { + return process.platform === "win32" ? path.join(venv, "Scripts", "python.exe") : path.join(venv, "bin", "python"); +} + function fileContentHash(filePath: string): string { try { return `sha256:${createHash("sha256").update(readFileSync(filePath)).digest("hex")}`; @@ -338,11 +342,6 @@ function ensureKernelPythonKey(pythonSkills: readonly BootstrapPythonSkill[]): s ].join("\0"); } -// uv (like every venv tool) puts the interpreter in Scripts/python.exe on Windows. -function getKernelVenvPython(venv: string): string { - return process.platform === "win32" ? path.join(venv, "Scripts", "python.exe") : path.join(venv, "bin", "python"); -} - export function getKernelVenvDir(): string { const override = process.env.PRIME_AGENT_KERNEL_VENV; if (override) return path.resolve(expandHome(override)); @@ -775,7 +774,7 @@ async function bootstrapVenv( ): Promise { await mkdir(path.dirname(venv), { recursive: true }); const uv = await ensureUv(options); - const python = getKernelVenvPython(venv); + const python = venvPython(venv); const sourceDir = await resolveRuntimeSourceDir(); const runtimeRequirement = sourceDir ?? RUNTIME_REQUIREMENT; const runtimeIdentity = await resolveRuntimeIdentity(); @@ -936,7 +935,7 @@ async function ensureKernelPythonUncached( } const venv = await resolveWritableKernelVenvDir(); - const python = getKernelVenvPython(venv); + const python = venvPython(venv); const runtimeIdentity = await resolveRuntimeIdentity(); if (await kernelReady(python, venv, runtimeIdentity, pythonSkills)) return python; diff --git a/packages/coding-agent/test/ipython-bootstrap.test.ts b/packages/coding-agent/test/ipython-bootstrap.test.ts index 8aba99b46..fa5c23ba2 100644 --- a/packages/coding-agent/test/ipython-bootstrap.test.ts +++ b/packages/coding-agent/test/ipython-bootstrap.test.ts @@ -3,6 +3,7 @@ import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; import { afterAll, describe, expect, it } from "vitest"; +import { venvPython } from "../src/core/kernel/bootstrap.js"; import { KernelManager } from "../src/core/kernel/index.js"; import { buildRlmBootstrapCode } from "../src/core/tools/ipython.js"; @@ -44,7 +45,7 @@ describe("IPython RLM bootstrap", () => { function resolveKernelPython(): string | null { const candidates = [ process.env.PRIME_AGENT_KERNEL_PYTHON, - join(homedir(), ".prime", "agent", "kernel-venv", "bin", "python"), + venvPython(join(homedir(), ".prime", "agent", "kernel-venv")), ].filter((p): p is string => Boolean(p)); for (const python of candidates) { if (!existsSync(python)) continue; diff --git a/packages/coding-agent/test/kernel-bootstrap.test.ts b/packages/coding-agent/test/kernel-bootstrap.test.ts index eeb2fc0d1..bed16ba12 100644 --- a/packages/coding-agent/test/kernel-bootstrap.test.ts +++ b/packages/coding-agent/test/kernel-bootstrap.test.ts @@ -10,6 +10,7 @@ import { getKernelVenvDir, type KernelPythonSkill, resolveRuntimeIdentity, + venvPython, } from "../src/core/kernel/bootstrap.js"; let tempDir = ""; @@ -174,6 +175,21 @@ describe("kernel bootstrap", () => { expect(getKernelVenvDir()).toBe(venv); }); + it("resolves the venv interpreter using the host platform layout", () => { + const originalPlatform = process.platform; + const setPlatform = (value: NodeJS.Platform) => + Object.defineProperty(process, "platform", { value, configurable: true }); + try { + setPlatform("win32"); + expect(venvPython("C:\\venv")).toBe(join("C:\\venv", "Scripts", "python.exe")); + + setPlatform("linux"); + expect(venvPython("/venv")).toBe(join("/venv", "bin", "python")); + } finally { + setPlatform(originalPlatform); + } + }); + it("bootstraps a missing venv with uv, ipykernel, prime-agent-runtime, and default extra packages", async () => { const logPath = installFakeUv(); const venv = join(tempDir, "kernel-venv"); diff --git a/packages/coding-agent/test/kernel-state-roundtrip.test.ts b/packages/coding-agent/test/kernel-state-roundtrip.test.ts index b4d767c4a..0e64dbd21 100644 --- a/packages/coding-agent/test/kernel-state-roundtrip.test.ts +++ b/packages/coding-agent/test/kernel-state-roundtrip.test.ts @@ -3,13 +3,14 @@ import { existsSync, mkdtempSync, rmSync } from "node:fs"; import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { venvPython } from "../src/core/kernel/bootstrap.js"; import { KernelManager } from "../src/core/kernel/index.js"; /** Find a python that can launch an ipykernel and has dill, or null to skip. */ function resolveKernelPython(): string | null { const candidates = [ process.env.PRIME_AGENT_KERNEL_PYTHON, - join(homedir(), ".prime", "agent", "kernel-venv", "bin", "python"), + venvPython(join(homedir(), ".prime", "agent", "kernel-venv")), ].filter((p): p is string => Boolean(p)); for (const python of candidates) { if (!existsSync(python)) continue; From 9f36fad74b15dc809053795e378b9e215872a07f Mon Sep 17 00:00:00 2001 From: j-token Date: Mon, 10 Aug 2026 12:41:35 +0900 Subject: [PATCH 4/4] fix(windows): harden installer and daemon launch --- install.ps1 | 13 ++++++++++++- packages/coding-agent/CHANGELOG.md | 3 ++- packages/coding-agent/postinstall.cjs | 2 +- packages/coding-agent/src/cli/daemon-launch.ts | 1 + .../interactive/components/extension-editor.ts | 1 - .../modes/interactive/components/login-dialog.ts | 2 +- .../src/modes/interactive/interactive-mode.ts | 1 - packages/coding-agent/test/clipboard.test.ts | 1 + 8 files changed, 18 insertions(+), 6 deletions(-) diff --git a/install.ps1 b/install.ps1 index 7afd68f21..bbbea3b64 100644 --- a/install.ps1 +++ b/install.ps1 @@ -358,10 +358,21 @@ param( [Environment]::SetEnvironmentVariable($name, $installEnv[$name], 'Process') } + $npmArguments = @('install', '-g', '--no-fund', '--no-audit', '--loglevel=error', '--progress=false') + try { + $npmHelp = (& $npm install --help 2>$null | Out-String) + if ($npmHelp -match '--allow-scripts') { + $npmArguments += '--allow-scripts=prime-agent,zeromq,@google/genai,koffi,protobufjs' + } + } catch { + # Older npm versions do not support per-package lifecycle-script approvals. + } + $npmArguments += $TarballPath + Write-PrimeAgentStep 'Installing Prime Agent with npm.' $exitCode = 0 try { - & $npm install -g --no-fund --no-audit --loglevel=error --progress=false $TarballPath + & $npm @npmArguments $exitCode = $LASTEXITCODE } finally { foreach ($name in @($previousEnv.Keys)) { diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a3872dc0f..3479084dd 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -7,7 +7,8 @@ - Fixed Windows child processes opening transient console windows. - Fixed Homebrew installs attempting to self-update their versioned Cellar keg instead of directing users to `brew upgrade prime-agent` ([#844](https://github.com/PrimeIntellect-ai/prime-agent/issues/844)) - Added a Windows PowerShell installer (`irm .../install.ps1 | iex`) that verifies the release checksum, installs with npm, and can prepare the IPython runtime. -- Added Windows support to the kernel bootstrap, installing uv with the official PowerShell installer and using the venv's `Scripts/python.exe`. +- Added Windows support to the kernel bootstrap, installing uv with the official PowerShell installer and using the venv's `Scripts/python.exe` ([#663](https://github.com/PrimeIntellect-ai/prime-agent/pull/663) by [@skulitom](https://github.com/skulitom)). +- Fixed the Windows PowerShell installer skipping required lifecycle scripts with npm versions that require explicit approvals. ## [0.7.1] - 2026-08-07 diff --git a/packages/coding-agent/postinstall.cjs b/packages/coding-agent/postinstall.cjs index 32c99b797..6eb97f41c 100644 --- a/packages/coding-agent/postinstall.cjs +++ b/packages/coding-agent/postinstall.cjs @@ -7,7 +7,7 @@ if (!existsSync(script)) { process.exit(0); } -const result = spawnSync(process.execPath, [script], { stdio: "inherit" }); +const result = spawnSync(process.execPath, [script], { windowsHide: true, stdio: "inherit" }); if (result.error) { console.error(`prime-agent: postinstall setup skipped: ${result.error.message}`); } diff --git a/packages/coding-agent/src/cli/daemon-launch.ts b/packages/coding-agent/src/cli/daemon-launch.ts index 6de9285e9..0d554635d 100644 --- a/packages/coding-agent/src/cli/daemon-launch.ts +++ b/packages/coding-agent/src/cli/daemon-launch.ts @@ -372,6 +372,7 @@ async function ensureDaemonRunning(socketPath: string, spawnCwd?: string): Promi process.execPath, [...process.execArgv, entrypoint, "--mode", "daemon", "--daemon-socket", socketPath], { + windowsHide: true, cwd: spawnCwd ?? process.cwd(), detached: true, env, diff --git a/packages/coding-agent/src/modes/interactive/components/extension-editor.ts b/packages/coding-agent/src/modes/interactive/components/extension-editor.ts index 2c6b71990..e1134ce96 100644 --- a/packages/coding-agent/src/modes/interactive/components/extension-editor.ts +++ b/packages/coding-agent/src/modes/interactive/components/extension-editor.ts @@ -125,7 +125,6 @@ export class ExtensionEditorComponent extends Container implements Focusable { const [editor, ...editorArgs] = editorCmd.split(" "); const result = spawnSync(editor, [...editorArgs, tmpFile], { - windowsHide: true, stdio: "inherit", shell: process.platform === "win32", }); diff --git a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts index 9679f5cc6..60934fbcd 100644 --- a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts +++ b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts @@ -167,7 +167,7 @@ export class LoginDialogComponent extends Container implements Focusable { url, ] : ["xdg-open", url]; - execFile(command, args, { windowsHide: true }, () => {}); + execFile(command, args, () => {}); this.tui.requestRender(); } diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 0a755dbbc..0169e8d5c 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -7047,7 +7047,6 @@ export class InteractiveMode { // Spawn editor synchronously with inherited stdio for interactive editing const result = spawnSync(editor, [...editorArgs, tmpFile], { - windowsHide: true, stdio: "inherit", shell: process.platform === "win32", }); diff --git a/packages/coding-agent/test/clipboard.test.ts b/packages/coding-agent/test/clipboard.test.ts index ea231b572..a8c52bcf5 100644 --- a/packages/coding-agent/test/clipboard.test.ts +++ b/packages/coding-agent/test/clipboard.test.ts @@ -121,6 +121,7 @@ describe("copyToClipboard", () => { input: "hello", stdio: ["pipe", "ignore", "ignore"], timeout: 5000, + windowsHide: true, }); expect(osc52Writes()).toHaveLength(0); });