Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
34ea817
Add failing tests for agent installs when npm is missing
brsbl Oct 20, 2026
726cda3
Ask for Node.js instead of offering an npm install that cannot run
brsbl Oct 20, 2026
a05667c
Install Codex with OpenAI's native installer
brsbl Oct 20, 2026
1260156
Run Codex's installer without prompts and resolve its native binary e…
brsbl Oct 20, 2026
0f61193
Call a native Codex path with PowerShell's call operator when signing in
brsbl Oct 21, 2026
577ca55
Merge remote-tracking branch 'origin/main' into bb/windows-agent-inst…
brsbl Oct 27, 2026
48ad965
Verify native Codex installation without Node on Windows
brsbl Oct 27, 2026
7eec50c
Align protocol assertions and retain safe install diagnostics
brsbl Oct 27, 2026
ca3f2e0
Preserve Windows OS detection in native installer CI
brsbl Oct 27, 2026
61b5151
Keep Codex sign-in commands usable before terminal PATH refresh
brsbl Oct 27, 2026
d2226d5
Expect resolved Codex login paths in health regression
brsbl Oct 28, 2026
60ebc14
Wait for executable discovery before removing native install fixtures
brsbl Oct 28, 2026
f56478d
Test Pi installation from setup without system Node.js
brsbl Oct 28, 2026
7023da6
Install Pi privately using the runtime and npm bundled with bb
brsbl Oct 28, 2026
4a3abbe
Fix bundled Pi installer serialization and preserve launch context
brsbl Oct 28, 2026
c5e87a8
Start AppImage launcher regression with no existing executable
brsbl Oct 28, 2026
f77b0bb
Format Pi installer regression fixture
brsbl Oct 28, 2026
533c77c
Keep isolated Pi tests from starting host environment refreshes
brsbl Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ jobs:

windows-app-smoke:
needs: plan
if: ${{ fromJSON(needs.plan.outputs.selection).packaging }}
if: ${{ fromJSON(needs.plan.outputs.selection).packaging || fromJSON(needs.plan.outputs.selection).providers }}
name: Windows app smoke (Node 22.x)
runs-on: blacksmith-4vcpu-windows-2025
timeout-minutes: 40
Expand Down Expand Up @@ -240,6 +240,21 @@ jobs:
shell: bash
run: pnpm exec turbo run smoke:boot --filter=bb-app --cache-dir="${TURBO_CACHE_DIR:-.turbo/cache}" --output-logs=new-only

- name: Install Codex without Node.js through setup
shell: bash
env:
CODEX_INSTALL_RESULTS: ${{ github.workspace }}/codex-install-results
run: pnpm exec turbo run smoke:codex-install --filter=bb-app --cache-dir="${TURBO_CACHE_DIR:-.turbo/cache}" --output-logs=new-only

- name: Upload Codex first-run evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: codex-install-without-node-windows
path: codex-install-results/
if-no-files-found: error
retention-days: 14

- name: Restore npm package downloads
id: npm-downloads
timeout-minutes: 1
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/scripts/smoke-packaged-app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "./desktop-release-channel.mjs";
import { createPackagedAppLaunchArguments } from "./packaged-app-launch.mjs";
import { resolvePackagedAppBinary } from "./packaged-app-paths.mjs";
import { smokePackagedPi } from "./smoke-packaged-pi.mjs";
import { smokePackagedNpm } from "./smoke-packaged-npm.mjs";

const scriptDirectory = dirname(fileURLToPath(import.meta.url));
Expand Down Expand Up @@ -313,7 +314,8 @@ async function smokePackagedApp() {
productName: releaseConfig.applicationName,
releaseDir,
});
await smokePackagedNpm(appBinary);
const npmCli = await smokePackagedNpm(appBinary);
await smokePackagedPi(appBinary, npmCli);
const smokeRoot = await mkdtemp(join(tmpdir(), "bb-desktop-packaged-smoke-"));
const dataDir = join(smokeRoot, "data");
const userDataDir = join(smokeRoot, "user-data");
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/scripts/smoke-packaged-npm.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ export async function smokePackagedNpm(appBinary) {
console.log(
`Packaged npm ${result.version}: ${result.directDependencies} direct dependencies, ${result.packages} bundled packages, ${result.resolutions} CJS/ESM dependency edges; offline plugin dependency install/update passed with Electron ${result.electron} (Node ${result.node}) and empty PATH; packaged resources unchanged.`,
);
return result.npmCli;
} finally {
await rm(fixture, { recursive: true, force: true });
}
Expand Down
107 changes: 107 additions & 0 deletions apps/desktop/scripts/smoke-packaged-pi.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import assert from "node:assert/strict";
import { execFile, spawn } from "node:child_process";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { promisify } from "node:util";

export async function smokePackagedPi(runtime, npmCli) {
const root = await mkdtemp(join(tmpdir(), "bb-pi-runtime-"));
let child;
try {
const env = {
HOME: root,
USERPROFILE: root,
PATH: "",
ELECTRON_RUN_AS_NODE: "1",
SystemRoot: process.env.SystemRoot,
TMP: root,
TEMP: root,
TMPDIR: root,
npm_config_cache: join(root, "cache"),
};
const exec = promisify(execFile);
await exec(
runtime,
[
npmCli,
"install",
"--prefix",
root,
"--ignore-scripts",
"--no-audit",
"--no-fund",
"@earendil-works/pi-coding-agent@latest",
],
{ env, cwd: root, timeout: 180_000, maxBuffer: 4 * 1024 * 1024 },
);
const packageDir = join(
root,
"node_modules",
"@earendil-works",
"pi-coding-agent",
);
const manifest = JSON.parse(
await readFile(join(packageDir, "package.json"), "utf8"),
);
const entry = resolve(packageDir, manifest.bin.pi);
const version = await exec(runtime, [entry, "--version"], {
env,
cwd: root,
timeout: 30_000,
});
assert(version.stdout.includes(manifest.version));
child = spawn(runtime, [entry, "--mode", "rpc", "--no-session"], {
env,
cwd: root,
stdio: ["pipe", "pipe", "pipe"],
windowsHide: true,
});
await new Promise((resolve, reject) => {
const timer = setTimeout(
() => reject(new Error("Pi did not answer RPC using bundled Electron")),
30_000,
);
let output = "";
child.on("error", reject);
child.on("exit", () => {
clearTimeout(timer);
reject(new Error("Pi exited before answering RPC"));
});
child.stdout.on("data", (data) => {
output += data;
for (const line of output.split("\n").slice(0, -1)) {
let message;
try {
message = JSON.parse(line);
} catch {
continue;
}
if (message.id !== "smoke") continue;
clearTimeout(timer);
if (!message.success)
reject(new Error("Pi RPC failed using bundled Electron"));
else resolve();
}
});
child.stdin.write(
JSON.stringify({ id: "smoke", type: "get_state" }) + "\n",
);
});
console.log(
"Pi installation, version and RPC passed with packaged Electron and empty PATH",
);
} finally {
if (child && child.exitCode === null && child.signalCode === null) {
const exited = new Promise((resolve) => child.once("exit", resolve));
child.kill();
await exited;
}
await rm(root, {
recursive: true,
force: true,
maxRetries: 5,
retryDelay: 500,
});
}
}
11 changes: 11 additions & 0 deletions docs/codex-install-ci.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Agent installation without Node.js

The Windows app smoke job runs `pnpm exec turbo run smoke:codex-install --filter=bb-app` after building the bundled app. It is a required, uncached check; a failing install fails the job.

The disposable runner uses a fresh home containing spaces and an apostrophe, a fresh bb data directory, and separate server and daemon endpoints. Node.js, npm, Pi and Codex are removed from its process and registry PATH. A copy of the runtime named `bb-runtime.exe` starts bb by absolute path without making a `node` executable available to installers. The original registry PATH is restored during cleanup.

The check proves that the previous Codex npm install fails without npm, then runs the native installer through setup's endpoint. Codex must report a version and unauthenticated health, and its generated sign-in command must run before PATH refresh. A browser then clicks Pi's Install button in setup. Pi's real executable must report a version, provider health must be unauthenticated, and setup must show installed and not signed in. The Pi browser assertion also fails against the prerequisite-link implementation because that revision offers no Install button.

Pi uses bb's existing runtime and bundled npm to install into a private user directory, with lifecycle scripts disabled as supported by Pi's distribution. Desktop uses Electron in Node mode; headless bb uses its existing Node runtime. Both invoke the package's manifest-declared executable directly, without requiring Node or npm on PATH. Windows RPC launches avoid shell shims; the sign-in launcher handles quoted paths and Electron mode. Linux AppImage launchers use the stable AppImage path instead of its transient mount.

The job publishes a result summary and JSON artifact with the tested revision. It does not authenticate an account or send an agent turn. Linux browser QA separately covers both setup Install interactions and desktop/mobile screenshots.
Loading
Loading