diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index 21e7cd2a3..7e63203da 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -23,6 +23,10 @@ export const isBunBinary = /** Detect if Bun is the runtime (compiled binary or bun run) */ export const isBunRuntime = !!process.versions.bun; +/** Detect the esbuild-bundled Node.js distribution. */ +declare const PI_BUNDLED_NODE: boolean; +export const isBundledNode = typeof PI_BUNDLED_NODE !== "undefined" && PI_BUNDLED_NODE; + // ============================================================================= // Install Method Detection // ============================================================================= diff --git a/packages/coding-agent/src/core/extensions/loader.ts b/packages/coding-agent/src/core/extensions/loader.ts index 36df2da21..32ca6c952 100644 --- a/packages/coding-agent/src/core/extensions/loader.ts +++ b/packages/coding-agent/src/core/extensions/loader.ts @@ -21,7 +21,7 @@ import { createJiti } from "jiti/static"; import * as _bundledTypebox from "typebox"; import * as _bundledTypeboxCompile from "typebox/compile"; import * as _bundledTypeboxValue from "typebox/value"; -import { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from "../../config.ts"; +import { CONFIG_DIR_NAME, getAgentDir, isBunBinary, isBundledNode } from "../../config.ts"; // NOTE: This import works because loader.ts exports are NOT re-exported from index.ts, // avoiding a circular dependency. Extensions can import from @earendil-works/pi-coding-agent. import * as _bundledPiCodingAgent from "../../index.ts"; @@ -78,8 +78,6 @@ 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"; /** diff --git a/packages/coding-agent/src/experimental/coordinator-entry.ts b/packages/coding-agent/src/experimental/coordinator-entry.ts new file mode 100644 index 000000000..339106295 --- /dev/null +++ b/packages/coding-agent/src/experimental/coordinator-entry.ts @@ -0,0 +1,10 @@ +#!/usr/bin/env node +import { runCoordinatorProcess } from "./coordinator.ts"; +import { consumeInternalProcessRole } from "./process.ts"; + +const role = consumeInternalProcessRole(); +if (role !== "coordinator") throw new Error("Coordinator entrypoint requires an internal coordinator invocation"); +void runCoordinatorProcess(process.argv.slice(2)).catch((error: unknown) => { + console.error(error); + process.exit(1); +}); diff --git a/packages/coding-agent/src/experimental/coordinator.ts b/packages/coding-agent/src/experimental/coordinator.ts index 773c7c8f8..dde1ba003 100644 --- a/packages/coding-agent/src/experimental/coordinator.ts +++ b/packages/coding-agent/src/experimental/coordinator.ts @@ -1,13 +1,12 @@ import { randomUUID } from "node:crypto"; import { chmod, lstat, unlink } from "node:fs/promises"; import { createConnection, createServer, type Server, type Socket } from "node:net"; -import { resolve } from "node:path"; -import { fileURLToPath } from "node:url"; import Type, { type Static } from "typebox"; import { Check } from "typebox/value"; import { consumeInternalProcessRole, encodeControlLine, + isDirectInternalProcessEntry, MAX_CONTROL_LINE_BYTES, spawnInternalProcess, } from "./process.ts"; @@ -603,7 +602,7 @@ async function cleanupSocket(path: string): Promise { }); } -if (process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { +if (isDirectInternalProcessEntry(import.meta.url)) { const role = consumeInternalProcessRole(); if (role !== "coordinator") throw new Error("Coordinator entrypoint requires an internal coordinator invocation"); void runCoordinatorProcess(process.argv.slice(2)).catch((error: unknown) => { diff --git a/packages/coding-agent/src/experimental/process.ts b/packages/coding-agent/src/experimental/process.ts index e3d52ec04..90c4b9eae 100644 --- a/packages/coding-agent/src/experimental/process.ts +++ b/packages/coding-agent/src/experimental/process.ts @@ -1,11 +1,22 @@ import { type ChildProcess, spawn } from "node:child_process"; -import { fileURLToPath } from "node:url"; -import { isBunBinary } from "../config.ts"; +import { join, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { getPackageDir, isBunBinary, isBundledNode } from "../config.ts"; export const INTERNAL_PROCESS_ENV = "__PI_INTERNAL_SPAWN"; export type InternalProcessRole = "coordinator" | "server" | "session-worker"; +/** Detect a directly executed source or unbundled internal-process module. */ +export function isDirectInternalProcessEntry(moduleUrl: string): boolean { + return ( + !isBunBinary && + !isBundledNode && + process.argv[1] !== undefined && + resolve(process.argv[1]) === fileURLToPath(moduleUrl) + ); +} + /** Read and validate an internal process role without consuming it. */ export function getInternalProcessRole(): InternalProcessRole | undefined { const role = process.env[INTERNAL_PROCESS_ENV]; @@ -71,6 +82,10 @@ export async function terminateInternalProcess(child: ChildProcess): Promise { } } -if (process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { +if (isDirectInternalProcessEntry(import.meta.url)) { const role = consumeInternalProcessRole(); if (role !== "server") throw new Error("Server entrypoint requires an internal server invocation"); void runServerProcess(process.argv.slice(2)).catch((error: unknown) => { diff --git a/packages/coding-agent/src/experimental/session-worker.ts b/packages/coding-agent/src/experimental/session-worker.ts index 38f55754d..caff625a1 100644 --- a/packages/coding-agent/src/experimental/session-worker.ts +++ b/packages/coding-agent/src/experimental/session-worker.ts @@ -1,7 +1,6 @@ import { randomUUID } from "node:crypto"; import { createConnection, type Socket } from "node:net"; -import { isAbsolute, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { isAbsolute } from "node:path"; import { AgentHarness, type AgentHarness as AgentHarnessInstance, @@ -48,7 +47,12 @@ import { toWireLaneSnapshot, toWireRunResult, } from "./harness-wire-adapter.ts"; -import { consumeInternalProcessRole, encodeControlLine, MAX_CONTROL_LINE_BYTES } from "./process.ts"; +import { + consumeInternalProcessRole, + encodeControlLine, + isDirectInternalProcessEntry, + MAX_CONTROL_LINE_BYTES, +} from "./process.ts"; import { createSessionWorkerServices, ServiceOperationResultSchema, @@ -952,7 +956,7 @@ export function runSessionWorkerProcess(args: readonly string[]): Promise return runSessionWorkerWithHarness(args, createCodingAgentHarness); } -if (process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { +if (isDirectInternalProcessEntry(import.meta.url)) { const role = consumeInternalProcessRole(); if (role !== "session-worker") { throw new Error("Session worker entrypoint requires an internal session-worker invocation"); diff --git a/scripts/auto-pi.sh b/scripts/auto-pi.sh index 2cab40be1..e8d1db1ec 100755 --- a/scripts/auto-pi.sh +++ b/scripts/auto-pi.sh @@ -65,7 +65,7 @@ if [[ "$use_stable" == true ]]; then exec "$stable_pi" ${args[@]+"${args[@]}"} fi -dev_pi="$repo_dir/packages/coding-agent/dist/cli.js" +dev_pi="$repo_dir/packages/coding-agent/dist/bundle/cli.js" if [[ ! -x "$dev_pi" ]]; then echo "error: development pi build not found; run \`npm run build\` in $repo_dir" >&2 exit 1 diff --git a/scripts/build-coding-agent-bundle.mjs b/scripts/build-coding-agent-bundle.mjs index 1b6d233eb..397141bfa 100644 --- a/scripts/build-coding-agent-bundle.mjs +++ b/scripts/build-coding-agent-bundle.mjs @@ -113,6 +113,7 @@ function outputBytes(metafiles) { for (const entry of [ join(codingAgentDistDir, "cli.js"), + join(codingAgentDistDir, "experimental", "coordinator-entry.js"), join(codingAgentDistDir, "index.js"), join(codingAgentDistDir, "rpc-entry.js"), join(codingAgentDistDir, "client", "index.js"), @@ -134,6 +135,7 @@ const mainResult = await build({ entryPoints: { cli: join(codingAgentDistDir, "cli.js"), client: join(codingAgentDistDir, "client", "index.js"), + coordinator: join(codingAgentDistDir, "experimental", "coordinator-entry.js"), index: join(codingAgentDistDir, "index.js"), "rpc-entry": join(codingAgentDistDir, "rpc-entry.js"), }, @@ -177,6 +179,7 @@ if (dirname(imageResizeOutput) !== dirname(imageResizeWorkerOutput)) { validateExternalImports([mainResult.metafile, lazyResult.metafile]); chmodSync(join(bundleDir, "cli.js"), 0o755); +chmodSync(join(bundleDir, "coordinator.js"), 0o755); chmodSync(join(bundleDir, "rpc-entry.js"), 0o755); const files = new Set([...Object.keys(mainResult.metafile.outputs), ...Object.keys(lazyResult.metafile.outputs)]).size;