mirror of
https://github.com/earendil-works/pi.git
synced 2026-10-02 08:44:38 +08:00
feat(coding-agent): bundle Node runtime (#8474)
This commit is contained in:
Generated
+1
-1
@@ -5511,7 +5511,7 @@
|
||||
"yaml": "2.9.0"
|
||||
},
|
||||
"bin": {
|
||||
"pi": "dist/cli.js"
|
||||
"pi": "dist/bundle/cli.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed the Node.js CLI and RPC entrypoints to load a bundled runtime, reducing startup filesystem reads while keeping the public library and legacy module paths on the modular runtime for normal dependency identity.
|
||||
- Changed session sharing to render clickable terminal links and Radius shares to display only the artifact's canonical URL.
|
||||
|
||||
### Fixed
|
||||
|
||||
+1
-1
@@ -530,7 +530,7 @@
|
||||
"@mariozechner/clipboard": "0.3.9"
|
||||
},
|
||||
"bin": {
|
||||
"pi": "dist/cli.js"
|
||||
"pi": "dist/bundle/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@
|
||||
"@mariozechner/clipboard": "0.3.9"
|
||||
},
|
||||
"bin": {
|
||||
"pi": "dist/cli.js"
|
||||
"pi": "dist/bundle/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"configDir": ".pi"
|
||||
},
|
||||
"bin": {
|
||||
"pi": "dist/cli.js"
|
||||
"pi": "dist/bundle/cli.js"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -17,7 +17,7 @@
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./rpc-entry": {
|
||||
"import": "./dist/rpc-entry.js"
|
||||
"import": "./dist/bundle/rpc-entry.js"
|
||||
},
|
||||
"./client": {
|
||||
"types": "./dist/client/index.d.ts",
|
||||
@@ -34,8 +34,9 @@
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "shx rm -rf dist",
|
||||
"build": "tsgo -p tsconfig.build.json && shx chmod +x dist/cli.js dist/rpc-entry.js && npm run copy-assets",
|
||||
"build:binary": "npm --prefix ../tui run build && npm --prefix ../telemetry run build && npm --prefix ../ai run build && npm --prefix ../agent run build && npm --prefix ../protocol run build && npm --prefix ../client run build && npm run build && bun build --compile --no-compile-autoload-bunfig ./dist/bun/cli.js ./src/utils/image-resize-worker.ts --outfile dist/pi && npm run copy-binary-assets",
|
||||
"build": "npm run build:unbundled && node ../../scripts/build-coding-agent-bundle.mjs",
|
||||
"build:unbundled": "tsgo -p tsconfig.build.json && shx chmod +x dist/cli.js dist/rpc-entry.js && npm run copy-assets",
|
||||
"build:binary": "npm --prefix ../tui run build && npm --prefix ../telemetry run build && npm --prefix ../ai run build && npm --prefix ../agent run build && npm --prefix ../protocol run build && npm --prefix ../client run build && npm run build && bun build --compile --no-compile-autoload-bunfig ./src/bun/cli.ts ./src/utils/image-resize-worker.ts --outfile dist/pi && npm run copy-binary-assets",
|
||||
"copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/modes/interactive/assets && shx cp src/modes/interactive/assets/*.png dist/modes/interactive/assets/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/",
|
||||
"copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/assets && shx cp src/modes/interactive/assets/*.png dist/assets/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/",
|
||||
"test": "vitest --run",
|
||||
|
||||
@@ -362,9 +362,26 @@ export function getUpdateInstruction(packageName: string): string {
|
||||
/**
|
||||
* Get the base directory for resolving package assets (themes, package.json, README.md, CHANGELOG.md).
|
||||
* - For Bun binary: returns the directory containing the executable
|
||||
* - For Node.js (dist/): returns __dirname (the dist/ directory)
|
||||
* - For tsx (src/): returns parent directory (the package root)
|
||||
* - For Node.js and tsx: returns the package root containing package.json
|
||||
* - Ignores Bun binary metadata copied into dist/ when the package root is available
|
||||
*/
|
||||
export function findNodePackageDir(startDir: string): string {
|
||||
let dir = startDir;
|
||||
while (dir !== dirname(dir)) {
|
||||
if (existsSync(join(dir, "package.json"))) {
|
||||
const parent = dirname(dir);
|
||||
// build:binary places Bun's metadata inside dist/. Node still needs the
|
||||
// package root so its dist-relative asset paths do not become dist/dist/.
|
||||
if (basename(dir) === "dist" && existsSync(join(parent, "package.json"))) {
|
||||
return parent;
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
dir = dirname(dir);
|
||||
}
|
||||
return startDir;
|
||||
}
|
||||
|
||||
export function getPackageDir(): string {
|
||||
// Allow override via environment variable (useful for Nix/Guix where store paths tokenize poorly)
|
||||
const envDir = process.env.PI_PACKAGE_DIR;
|
||||
@@ -376,16 +393,7 @@ export function getPackageDir(): string {
|
||||
// Bun binary: process.execPath points to the compiled executable
|
||||
return dirname(process.execPath);
|
||||
}
|
||||
// Node.js: walk up from __dirname until we find package.json
|
||||
let dir = __dirname;
|
||||
while (dir !== dirname(dir)) {
|
||||
if (existsSync(join(dir, "package.json"))) {
|
||||
return dir;
|
||||
}
|
||||
dir = dirname(dir);
|
||||
}
|
||||
// Fallback (shouldn't happen)
|
||||
return __dirname;
|
||||
return findNodePackageDir(__dirname);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -78,6 +78,8 @@ const require = createRequire(import.meta.url);
|
||||
const isNodeSeaBinary =
|
||||
("sea" in process.features && process.features.sea === true) ||
|
||||
process.getBuiltinModule("node:sea")?.isSea() === true;
|
||||
declare const PI_BUNDLED_NODE: boolean;
|
||||
const isBundledNode = typeof PI_BUNDLED_NODE !== "undefined" && PI_BUNDLED_NODE;
|
||||
const isTypeScriptSourceRuntime = !isBunBinary && path.extname(fileURLToPath(import.meta.url)) === ".ts";
|
||||
|
||||
/**
|
||||
@@ -451,9 +453,10 @@ async function loadExtensionModule(extensionPath: string, cacheToken?: Extension
|
||||
|
||||
const jiti = createJiti(import.meta.url, {
|
||||
moduleCache: false,
|
||||
// Compiled binaries use modules embedded in the executable. Source TypeScript
|
||||
// reuses host modules and root tsconfig paths. Built Node uses dist aliases.
|
||||
...(isBunBinary || isNodeSeaBinary
|
||||
// Compiled binaries and the bundled Node distribution use embedded modules.
|
||||
// Source TypeScript reuses host modules and root tsconfig paths. Unbundled
|
||||
// Node builds use dist aliases.
|
||||
...(isBunBinary || isNodeSeaBinary || isBundledNode
|
||||
? { virtualModules: VIRTUAL_MODULES, tryNative: false }
|
||||
: isTypeScriptSourceRuntime
|
||||
? { virtualModules: VIRTUAL_MODULES, tsconfigPaths: true }
|
||||
|
||||
@@ -4,6 +4,7 @@ import { delimiter, join } from "path";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import {
|
||||
detectInstallMethod,
|
||||
findNodePackageDir,
|
||||
getSelfUpdateCommand,
|
||||
getSelfUpdateUnavailableInstruction,
|
||||
getUpdateInstruction,
|
||||
@@ -145,6 +146,19 @@ function createFakeBunScript(bunBin: string): string {
|
||||
return `#!/bin/sh\nif [ "$1" = "pm" ] && [ "$2" = "bin" ] && [ "$3" = "-g" ]; then\n\tprintf '%s\\n' '${escapedBunBin}'\n\texit 0\nfi\nexit 1\n`;
|
||||
}
|
||||
|
||||
describe("findNodePackageDir", () => {
|
||||
test("skips binary metadata copied into dist", () => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), "pi-package-dir-"));
|
||||
const distDir = join(tempDir, "dist");
|
||||
const bundleDir = join(distDir, "bundle");
|
||||
mkdirSync(bundleDir, { recursive: true });
|
||||
writeFileSync(join(tempDir, "package.json"), "{}");
|
||||
writeFileSync(join(distDir, "package.json"), "{}");
|
||||
|
||||
expect(findNodePackageDir(bundleDir)).toBe(tempDir);
|
||||
});
|
||||
});
|
||||
|
||||
describe("detectInstallMethod", () => {
|
||||
test("detects pnpm from Windows .pnpm install paths", () => {
|
||||
setExecPath(
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
interface CodingAgentPackageJson {
|
||||
bin: { pi: string };
|
||||
main: string;
|
||||
exports: {
|
||||
".": { import: string; types: string };
|
||||
"./client": { import: string; types: string };
|
||||
"./rpc-entry": { import: string };
|
||||
};
|
||||
}
|
||||
|
||||
const packageJson = JSON.parse(
|
||||
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
||||
) as CodingAgentPackageJson;
|
||||
|
||||
describe("package distribution entrypoints", () => {
|
||||
test("uses the bundle for executables and modular output for libraries", () => {
|
||||
expect(packageJson.bin.pi).toBe("dist/bundle/cli.js");
|
||||
expect(packageJson.main).toBe("./dist/index.js");
|
||||
expect(packageJson.exports["."].import).toBe("./dist/index.js");
|
||||
expect(packageJson.exports["./client"].import).toBe("./dist/client/index.js");
|
||||
expect(packageJson.exports["./rpc-entry"].import).toBe("./dist/bundle/rpc-entry.js");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createRequire } from "node:module";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { getNativeModuleCandidates } from "./native-module-path.ts";
|
||||
|
||||
const cjsRequire = createRequire(import.meta.url);
|
||||
|
||||
@@ -33,14 +33,7 @@ function loadNativeModifiersHelper(): NativeModifiersHelper | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const candidates = [
|
||||
path.join(moduleDir, "..", nativePath),
|
||||
path.join(moduleDir, nativePath),
|
||||
path.join(path.dirname(process.execPath), nativePath),
|
||||
];
|
||||
|
||||
for (const modulePath of candidates) {
|
||||
for (const modulePath of getNativeModuleCandidates(nativePath)) {
|
||||
try {
|
||||
const helper = cjsRequire(modulePath) as unknown;
|
||||
if (isNativeModifiersHelper(helper)) {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const moduleRequire = createRequire(import.meta.url);
|
||||
const TUI_PACKAGE_NAME = "@earendil-works/pi-tui";
|
||||
|
||||
export interface NativeModuleCandidateOptions {
|
||||
moduleUrl?: string;
|
||||
execPath?: string;
|
||||
resolvePackage?: (specifier: string) => string;
|
||||
}
|
||||
|
||||
export function getNativeModuleCandidates(nativePath: string, options: NativeModuleCandidateOptions = {}): string[] {
|
||||
const moduleDir = dirname(fileURLToPath(options.moduleUrl ?? import.meta.url));
|
||||
const candidates: string[] = [];
|
||||
|
||||
try {
|
||||
const packageEntry = (options.resolvePackage ?? moduleRequire.resolve)(TUI_PACKAGE_NAME);
|
||||
candidates.push(join(dirname(packageEntry), "..", nativePath));
|
||||
} catch {
|
||||
// Standalone binaries do not have an installed TUI package.
|
||||
}
|
||||
|
||||
candidates.push(
|
||||
join(moduleDir, "..", nativePath),
|
||||
join(moduleDir, nativePath),
|
||||
join(dirname(options.execPath ?? process.execPath), nativePath),
|
||||
);
|
||||
return Array.from(new Set(candidates));
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { setKittyProtocolActive } from "./keys.ts";
|
||||
import { isNativeModifierPressed } from "./native-modifiers.ts";
|
||||
import { getNativeModuleCandidates } from "./native-module-path.ts";
|
||||
import { StdinBuffer } from "./stdin-buffer.ts";
|
||||
|
||||
const cjsRequire = createRequire(import.meta.url);
|
||||
@@ -370,16 +370,10 @@ export class ProcessTerminal implements Terminal {
|
||||
if (arch !== "x64" && arch !== "arm64") return;
|
||||
|
||||
// Dynamic require so non-Windows and bundled/browser paths never load the
|
||||
// native helper. In the npm package native/ is next to dist/; in compiled
|
||||
// binary archives native/ is copied next to the executable.
|
||||
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
// native helper. Installed packages resolve it from pi-tui; standalone
|
||||
// binaries resolve the copy next to the executable.
|
||||
const nativePath = path.join("native", "win32", "prebuilds", `win32-${arch}`, "win32-console-mode.node");
|
||||
const candidates = [
|
||||
path.join(moduleDir, "..", nativePath),
|
||||
path.join(moduleDir, nativePath),
|
||||
path.join(path.dirname(process.execPath), nativePath),
|
||||
];
|
||||
for (const modulePath of candidates) {
|
||||
for (const modulePath of getNativeModuleCandidates(nativePath)) {
|
||||
try {
|
||||
const helper = cjsRequire(modulePath) as { enableVirtualTerminalInput?: () => boolean };
|
||||
helper.enableVirtualTerminalInput?.();
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import assert from "node:assert";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { describe, it } from "node:test";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { getNativeModuleCandidates } from "../src/native-module-path.ts";
|
||||
|
||||
describe("getNativeModuleCandidates", () => {
|
||||
it("resolves native helpers from the installed TUI package when the module is bundled elsewhere", () => {
|
||||
const packageRoot = resolve("virtual", "node_modules", "@earendil-works", "pi-tui");
|
||||
const bundledModule = resolve("virtual", "pi-coding-agent", "dist", "bundle", "chunks", "chunk.js");
|
||||
const nativePath = join("native", "win32", "prebuilds", "win32-arm64", "win32-console-mode.node");
|
||||
|
||||
const candidates = getNativeModuleCandidates(nativePath, {
|
||||
moduleUrl: pathToFileURL(bundledModule).href,
|
||||
execPath: resolve("virtual", "node", "node.exe"),
|
||||
resolvePackage: (specifier) => {
|
||||
assert.equal(specifier, "@earendil-works/pi-tui");
|
||||
return join(packageRoot, "dist", "index.js");
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(candidates[0], join(packageRoot, nativePath));
|
||||
assert.ok(candidates.includes(join(dirname(bundledModule), "..", nativePath)));
|
||||
});
|
||||
|
||||
it("keeps standalone binary fallbacks when the TUI package is unavailable", () => {
|
||||
const bundledModule = resolve("virtual", "pi", "bundle", "chunks", "chunk.js");
|
||||
const execPath = resolve("virtual", "pi", "pi.exe");
|
||||
const nativePath = join("native", "darwin", "prebuilds", "darwin-arm64", "darwin-modifiers.node");
|
||||
|
||||
const candidates = getNativeModuleCandidates(nativePath, {
|
||||
moduleUrl: pathToFileURL(bundledModule).href,
|
||||
execPath,
|
||||
resolvePackage: () => {
|
||||
throw new Error("not installed");
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(candidates, [
|
||||
join(dirname(bundledModule), "..", nativePath),
|
||||
join(dirname(bundledModule), nativePath),
|
||||
join(dirname(execPath), nativePath),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { chmodSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
||||
import { isBuiltin } from "node:module";
|
||||
import { dirname, join, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { build } from "esbuild";
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(scriptDir, "..");
|
||||
const codingAgentDir = join(repoRoot, "packages", "coding-agent");
|
||||
const aiDistDir = join(repoRoot, "packages", "ai", "dist");
|
||||
const codingAgentDistDir = join(codingAgentDir, "dist");
|
||||
const bundleDir = join(codingAgentDistDir, "bundle");
|
||||
const banner = {
|
||||
js: 'import { createRequire as __piCreateRequire } from "node:module"; const require = __piCreateRequire(import.meta.url);',
|
||||
};
|
||||
const allowedExternalPackages = new Set([
|
||||
"@silvia-odwyer/photon-node",
|
||||
// Optional native accelerators. Their callers fall back to JavaScript when absent.
|
||||
"bufferutil",
|
||||
"utf-8-validate",
|
||||
// Optional debug output coloring.
|
||||
"supports-color",
|
||||
]);
|
||||
|
||||
function commonBuildOptions() {
|
||||
return {
|
||||
absWorkingDir: repoRoot,
|
||||
banner,
|
||||
bundle: true,
|
||||
define: { PI_BUNDLED_NODE: "true" },
|
||||
external: ["@silvia-odwyer/photon-node"],
|
||||
format: "esm",
|
||||
legalComments: "none",
|
||||
logLevel: "warning",
|
||||
metafile: true,
|
||||
minifySyntax: true,
|
||||
minifyWhitespace: true,
|
||||
platform: "node",
|
||||
sourcemap: false,
|
||||
target: "node22.19",
|
||||
// Do not apply the monorepo's source-oriented path aliases while bundling
|
||||
// compiled output. Release builds must resolve the same package entries as
|
||||
// an installed npm package.
|
||||
tsconfigRaw: { compilerOptions: {} },
|
||||
};
|
||||
}
|
||||
|
||||
function validateExternalImports(metafiles) {
|
||||
const unexpected = new Set();
|
||||
for (const metafile of metafiles) {
|
||||
for (const input of Object.values(metafile.inputs)) {
|
||||
for (const imported of input.imports) {
|
||||
if (!imported.external || isBuiltin(imported.path) || allowedExternalPackages.has(imported.path)) {
|
||||
continue;
|
||||
}
|
||||
unexpected.add(imported.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unexpected.size > 0) {
|
||||
throw new Error(`Bundle left unexpected external imports: ${Array.from(unexpected).sort().join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
function findContainingOutput(metafile, inputSuffix) {
|
||||
const normalizedSuffix = inputSuffix.replaceAll("\\", "/");
|
||||
for (const [outputPath, output] of Object.entries(metafile.outputs)) {
|
||||
if (Object.keys(output.inputs).some((inputPath) => inputPath.replaceAll("\\", "/").endsWith(normalizedSuffix))) {
|
||||
return resolve(repoRoot, outputPath);
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not locate bundled output containing ${inputSuffix}`);
|
||||
}
|
||||
|
||||
function outputBytes(metafiles) {
|
||||
return metafiles.reduce(
|
||||
(total, metafile) => total + Object.values(metafile.outputs).reduce((subtotal, output) => subtotal + output.bytes, 0),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
for (const entry of [
|
||||
join(codingAgentDistDir, "cli.js"),
|
||||
join(codingAgentDistDir, "index.js"),
|
||||
join(codingAgentDistDir, "rpc-entry.js"),
|
||||
join(codingAgentDistDir, "client", "index.js"),
|
||||
join(codingAgentDistDir, "utils", "image-resize-worker.js"),
|
||||
join(aiDistDir, "api", "bedrock-converse-stream.js"),
|
||||
join(aiDistDir, "auth", "oauth", "anthropic.js"),
|
||||
]) {
|
||||
if (!existsSync(entry)) {
|
||||
throw new Error(`Bundle input is missing: ${relative(repoRoot, entry)}. Build the workspace packages first.`);
|
||||
}
|
||||
}
|
||||
|
||||
rmSync(bundleDir, { force: true, recursive: true });
|
||||
mkdirSync(bundleDir, { recursive: true });
|
||||
|
||||
const mainResult = await build({
|
||||
...commonBuildOptions(),
|
||||
entryNames: "[name]",
|
||||
entryPoints: {
|
||||
cli: join(codingAgentDistDir, "cli.js"),
|
||||
client: join(codingAgentDistDir, "client", "index.js"),
|
||||
index: join(codingAgentDistDir, "index.js"),
|
||||
"rpc-entry": join(codingAgentDistDir, "rpc-entry.js"),
|
||||
},
|
||||
outdir: bundleDir,
|
||||
chunkNames: "chunks/[name]-[hash]",
|
||||
splitting: true,
|
||||
});
|
||||
|
||||
const bedrockLoaderOutput = findContainingOutput(mainResult.metafile, "packages/ai/dist/api/bedrock-converse-stream.lazy.js");
|
||||
const oauthLoaderOutput = findContainingOutput(mainResult.metafile, "packages/ai/dist/auth/oauth/load.js");
|
||||
const imageResizeOutput = findContainingOutput(mainResult.metafile, "packages/coding-agent/dist/utils/image-resize.js");
|
||||
if (dirname(bedrockLoaderOutput) !== dirname(oauthLoaderOutput)) {
|
||||
throw new Error("Bedrock and OAuth lazy loaders were emitted into different directories");
|
||||
}
|
||||
|
||||
// These implementations are reached through variable-specifier imports or a
|
||||
// worker URL, so the main bundle cannot follow them. Emit one self-contained
|
||||
// file per implementation beside the code that resolves it.
|
||||
const lazyResult = await build({
|
||||
...commonBuildOptions(),
|
||||
entryNames: "[name]",
|
||||
entryPoints: {
|
||||
anthropic: join(aiDistDir, "auth", "oauth", "anthropic.js"),
|
||||
"bedrock-converse-stream": join(aiDistDir, "api", "bedrock-converse-stream.js"),
|
||||
"github-copilot": join(aiDistDir, "auth", "oauth", "github-copilot.js"),
|
||||
"image-resize-worker": join(codingAgentDistDir, "utils", "image-resize-worker.js"),
|
||||
"kimi-coding": join(aiDistDir, "auth", "oauth", "kimi-coding.js"),
|
||||
"openai-codex": join(aiDistDir, "auth", "oauth", "openai-codex.js"),
|
||||
openrouter: join(aiDistDir, "auth", "oauth", "openrouter.js"),
|
||||
radius: join(aiDistDir, "auth", "oauth", "radius.js"),
|
||||
xai: join(aiDistDir, "auth", "oauth", "xai.js"),
|
||||
},
|
||||
outdir: dirname(bedrockLoaderOutput),
|
||||
splitting: false,
|
||||
});
|
||||
|
||||
const imageResizeWorkerOutput = resolve(dirname(bedrockLoaderOutput), "image-resize-worker.js");
|
||||
if (dirname(imageResizeOutput) !== dirname(imageResizeWorkerOutput)) {
|
||||
throw new Error("Image resize implementation and worker were emitted into different directories");
|
||||
}
|
||||
|
||||
validateExternalImports([mainResult.metafile, lazyResult.metafile]);
|
||||
chmodSync(join(bundleDir, "cli.js"), 0o755);
|
||||
chmodSync(join(bundleDir, "rpc-entry.js"), 0o755);
|
||||
|
||||
const files = new Set([...Object.keys(mainResult.metafile.outputs), ...Object.keys(lazyResult.metafile.outputs)]).size;
|
||||
const mib = outputBytes([mainResult.metafile, lazyResult.metafile]) / (1024 * 1024);
|
||||
console.log(`Built ${relative(repoRoot, bundleDir)} (${files} files, ${mib.toFixed(1)} MiB)`);
|
||||
@@ -1,4 +1,4 @@
|
||||
import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { spawn } from "node:child_process";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, relative, resolve } from "node:path";
|
||||
@@ -9,6 +9,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(__dirname, "..");
|
||||
const packageDir = join(repoRoot, "packages", "coding-agent");
|
||||
const distCliPath = join(packageDir, "dist", "cli.js");
|
||||
const bundledDistCliPath = join(packageDir, "dist", "bundle", "cli.js");
|
||||
const srcCliPath = join(packageDir, "src", "cli.ts");
|
||||
const defaultNodeProfileDir = join(repoRoot, "profiles-node");
|
||||
const defaultBunProfileDir = join(repoRoot, "profiles-bun");
|
||||
@@ -35,8 +36,9 @@ Options:
|
||||
--runtime <name> node, bun, or auto (default: auto)
|
||||
--agent-dir <dir> Use a specific PI_CODING_AGENT_DIR for the benchmark run
|
||||
--isolated-agent-dir Use a fresh temporary agent dir instead of the normal one
|
||||
--bundle Build and profile the bundled Node entrypoint instead of dist/cli.js
|
||||
--no-offline Do not force PI_OFFLINE=1 / PI_SKIP_VERSION_CHECK=1
|
||||
--skip-build Reuse the current dist/cli.js without rebuilding first (Node only)
|
||||
--skip-build Reuse the selected build output without rebuilding first (Node only)
|
||||
--cpu-profile Write CPU profiles for benchmark runs
|
||||
--help Show this help
|
||||
|
||||
@@ -73,6 +75,7 @@ function parseMode(value) {
|
||||
function parseArgs(argv) {
|
||||
const options = {
|
||||
mode: "tui",
|
||||
bundle: false,
|
||||
runs: 1,
|
||||
warmup: 0,
|
||||
profileDir: undefined,
|
||||
@@ -103,6 +106,11 @@ function parseArgs(argv) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === "--bundle") {
|
||||
options.bundle = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === "--skip-build") {
|
||||
options.build = false;
|
||||
continue;
|
||||
@@ -221,7 +229,7 @@ function parseStartupTimings(stderr) {
|
||||
let inBlock = false;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes("--- Startup Timings ---")) {
|
||||
if (/^--- Startup Timings(?:: [^-]+)? ---$/.test(line.trim())) {
|
||||
inBlock = true;
|
||||
continue;
|
||||
}
|
||||
@@ -278,59 +286,72 @@ async function waitForExit(child, errorPrefix) {
|
||||
});
|
||||
}
|
||||
|
||||
async function runBuild() {
|
||||
process.stdout.write("Building packages/tui, packages/telemetry, packages/ai, packages/agent, and packages/coding-agent...\n");
|
||||
async function runBuild(bundle) {
|
||||
process.stdout.write(
|
||||
`Building dependencies and the ${bundle ? "bundled" : "unbundled"} coding-agent Node entrypoint...\n`,
|
||||
);
|
||||
const startedAt = performance.now();
|
||||
const child = spawn(
|
||||
"npm",
|
||||
[
|
||||
"run",
|
||||
"build",
|
||||
"--workspace",
|
||||
"packages/tui",
|
||||
"--workspace",
|
||||
"packages/telemetry",
|
||||
"--workspace",
|
||||
"packages/ai",
|
||||
"--workspace",
|
||||
"packages/agent",
|
||||
"--workspace",
|
||||
"packages/coding-agent",
|
||||
],
|
||||
const commands = [
|
||||
{
|
||||
label: "Dependency build",
|
||||
args: [
|
||||
"run",
|
||||
"build",
|
||||
"--workspace",
|
||||
"packages/tui",
|
||||
"--workspace",
|
||||
"packages/telemetry",
|
||||
"--workspace",
|
||||
"packages/ai",
|
||||
"--workspace",
|
||||
"packages/agent",
|
||||
"--workspace",
|
||||
"packages/protocol",
|
||||
"--workspace",
|
||||
"packages/client",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Coding-agent build",
|
||||
args: ["run", bundle ? "build" : "build:unbundled", "--workspace", "packages/coding-agent"],
|
||||
},
|
||||
];
|
||||
|
||||
for (const command of commands) {
|
||||
const child = spawn("npm", command.args, {
|
||||
cwd: repoRoot,
|
||||
env: process.env,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
shell: process.platform === "win32",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk;
|
||||
});
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk;
|
||||
});
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
|
||||
const exitCode = await waitForExit(child, "Build");
|
||||
if (exitCode !== 0) {
|
||||
if (stdout.trim()) {
|
||||
process.stdout.write(`${stdout}${stdout.endsWith("\n") ? "" : "\n"}`);
|
||||
const exitCode = await waitForExit(child, command.label);
|
||||
if (exitCode !== 0) {
|
||||
if (stdout.trim()) {
|
||||
process.stdout.write(`${stdout}${stdout.endsWith("\n") ? "" : "\n"}`);
|
||||
}
|
||||
if (stderr.trim()) {
|
||||
process.stderr.write(`${stderr}${stderr.endsWith("\n") ? "" : "\n"}`);
|
||||
}
|
||||
throw new Error(`${command.label} failed with exit code ${exitCode}`);
|
||||
}
|
||||
if (stderr.trim()) {
|
||||
process.stderr.write(`${stderr}${stderr.endsWith("\n") ? "" : "\n"}`);
|
||||
}
|
||||
throw new Error(`Build failed with exit code ${exitCode}`);
|
||||
}
|
||||
|
||||
process.stdout.write(`Build completed in ${formatMs(performance.now() - startedAt)}\n`);
|
||||
}
|
||||
|
||||
function getRuntimeCommand(runtime, mode, profileDir, profileName, cpuProfile) {
|
||||
function getRuntimeCommand(runtime, mode, profileDir, profileName, cpuProfile, nodeEntryPath) {
|
||||
const benchmarkArgs = ["--no-session"];
|
||||
if (mode === "rpc") {
|
||||
benchmarkArgs.push("--mode", "rpc");
|
||||
@@ -352,7 +373,7 @@ function getRuntimeCommand(runtime, mode, profileDir, profileName, cpuProfile) {
|
||||
if (cpuProfile) {
|
||||
args.push("--cpu-prof", `--cpu-prof-dir=${profileDir}`, `--cpu-prof-name=${profileName}`);
|
||||
}
|
||||
args.push(distCliPath, ...benchmarkArgs);
|
||||
args.push(nodeEntryPath, ...benchmarkArgs);
|
||||
return {
|
||||
executable: process.execPath,
|
||||
args,
|
||||
@@ -360,7 +381,7 @@ function getRuntimeCommand(runtime, mode, profileDir, profileName, cpuProfile) {
|
||||
}
|
||||
|
||||
function createBenchmarkEnv(options, isolatedAgentDir) {
|
||||
const env = { ...process.env };
|
||||
const env = { ...process.env, PI_TIMING: "1" };
|
||||
if (options.agentDir) {
|
||||
env[agentDirEnvName] = options.agentDir;
|
||||
} else if (isolatedAgentDir) {
|
||||
@@ -386,11 +407,12 @@ async function runTuiBenchmarkRun({ runtime, runIndex, measuredIndex, options, p
|
||||
mkdirSync(isolatedAgentDir, { recursive: true });
|
||||
}
|
||||
|
||||
const command = getRuntimeCommand(runtime, "tui", profileDir, profileName, options.cpuProfile);
|
||||
const nodeEntryPath = options.bundle ? bundledDistCliPath : distCliPath;
|
||||
const command = getRuntimeCommand(runtime, "tui", profileDir, profileName, options.cpuProfile, nodeEntryPath);
|
||||
const child = spawn(command.executable, command.args, {
|
||||
cwd: packageDir,
|
||||
env: createBenchmarkEnv(options, isolatedAgentDir),
|
||||
stdio: ["inherit", "ignore", "pipe"],
|
||||
stdio: ["inherit", "inherit", "pipe"],
|
||||
shell: process.platform === "win32" && runtime === "bun",
|
||||
});
|
||||
|
||||
@@ -445,7 +467,8 @@ async function runRpcBenchmarkRun({ runtime, runIndex, measuredIndex, options, p
|
||||
mkdirSync(isolatedAgentDir, { recursive: true });
|
||||
}
|
||||
|
||||
const command = getRuntimeCommand(runtime, "rpc", profileDir, profileName, options.cpuProfile);
|
||||
const nodeEntryPath = options.bundle ? bundledDistCliPath : distCliPath;
|
||||
const command = getRuntimeCommand(runtime, "rpc", profileDir, profileName, options.cpuProfile, nodeEntryPath);
|
||||
const child = spawn(command.executable, command.args, {
|
||||
cwd: packageDir,
|
||||
env: createBenchmarkEnv(options, isolatedAgentDir),
|
||||
@@ -547,11 +570,14 @@ async function main() {
|
||||
}
|
||||
|
||||
const runtime = resolveRuntime(options.runtime);
|
||||
if (options.bundle && runtime !== "node") {
|
||||
throw new Error("--bundle only supports the Node runtime");
|
||||
}
|
||||
options.label = resolveLabel(options.mode, options.label);
|
||||
const profileDir = resolveProfileDir(runtime, options.profileDir);
|
||||
|
||||
if (runtime === "node" && options.build) {
|
||||
await runBuild();
|
||||
await runBuild(options.bundle);
|
||||
}
|
||||
if (runtime === "bun") {
|
||||
process.stdout.write(
|
||||
@@ -559,7 +585,16 @@ async function main() {
|
||||
);
|
||||
}
|
||||
|
||||
const entryPath = runtime === "bun" ? srcCliPath : distCliPath;
|
||||
const entryPath = runtime === "bun" ? srcCliPath : options.bundle ? bundledDistCliPath : distCliPath;
|
||||
if (
|
||||
runtime === "node" &&
|
||||
!options.bundle &&
|
||||
!options.build &&
|
||||
existsSync(distCliPath) &&
|
||||
readFileSync(distCliPath, "utf8").includes('import "./bundle/cli.js";')
|
||||
) {
|
||||
throw new Error("dist/cli.js is a bundled facade; rerun without --skip-build for an unbundled profile");
|
||||
}
|
||||
if (!existsSync(entryPath)) {
|
||||
throw new Error(`CLI entrypoint not found: ${entryPath}`);
|
||||
}
|
||||
@@ -597,7 +632,7 @@ async function main() {
|
||||
const maxElapsedRun = measuredRuns.reduce((slowest, run) => (run.elapsedMs > slowest.elapsedMs ? run : slowest));
|
||||
if (measuredRuns.length === 1) {
|
||||
process.stdout.write("\nResult\n");
|
||||
process.stdout.write(` runtime: ${runtime}\n`);
|
||||
process.stdout.write(` runtime: ${runtime}${options.bundle ? " (bundle)" : ""}\n`);
|
||||
process.stdout.write(` mode: ${options.mode}\n`);
|
||||
process.stdout.write(` elapsed: ${formatMs(measuredRuns[0].elapsedMs)}\n`);
|
||||
for (const [label, summary] of timingSummaries.entries()) {
|
||||
@@ -615,7 +650,7 @@ async function main() {
|
||||
}
|
||||
|
||||
process.stdout.write("\nSummary\n");
|
||||
process.stdout.write(` runtime: ${runtime}\n`);
|
||||
process.stdout.write(` runtime: ${runtime}${options.bundle ? " (bundle)" : ""}\n`);
|
||||
process.stdout.write(` mode: ${options.mode}\n`);
|
||||
process.stdout.write(` elapsed min: ${formatMs(elapsedSummary.min)}\n`);
|
||||
process.stdout.write(` elapsed median: ${formatMs(elapsedSummary.median)}\n`);
|
||||
|
||||
Reference in New Issue
Block a user