diff --git a/apps/server/src/bin.ts b/apps/server/src/bin.ts index 063093874503..0cecf7595b72 100644 --- a/apps/server/src/bin.ts +++ b/apps/server/src/bin.ts @@ -5,7 +5,8 @@ * terminal-fallback agents run `t3 acp-mcp-call` per tool call, so their * startup sits on first-message latency. Both dispatch here before the full * CLI module graph (seconds of evaluation) loads; everything else defers to - * the real CLI in ./binCli.ts. + * the real CLI in ./binCli.ts. `t3 __run-node-script` runs bundled language + * servers under the single-executable build (see ./nodeScript.ts). */ import { isEntrypoint } from "./entrypoint.ts"; @@ -20,6 +21,9 @@ if ( if (command === "acp-mcp-bridge" || command === "acp-mcp-call") { const { runAcpMcpCliFastPath } = await import("./mcp/AcpMcpStdioBridge.ts"); await runAcpMcpCliFastPath(command, process.argv.slice(3)); + } else if (command === "__run-node-script") { + const { runNodeScript } = await import("./nodeScript.ts"); + runNodeScript(process.argv.slice(3)); } else { const { runCli } = await import("./binCli.ts"); runCli(); diff --git a/apps/server/src/nodeScript.test.ts b/apps/server/src/nodeScript.test.ts new file mode 100644 index 000000000000..b5b49b82dd00 --- /dev/null +++ b/apps/server/src/nodeScript.test.ts @@ -0,0 +1,35 @@ +// @effect-diagnostics nodeBuiltinImport:off - runs a CommonJS script from the filesystem. +import * as NodeFS from "node:fs"; +import * as NodeOS from "node:os"; +import * as NodePath from "node:path"; + +import { afterEach, describe, expect, it } from "vite-plus/test"; + +import { runNodeScript } from "./nodeScript.ts"; + +describe("runNodeScript", () => { + const originalArgv = process.argv; + afterEach(() => { + process.argv = originalArgv; + }); + + it("runs the script with the argv Node would give it", () => { + const directory = NodeFS.realpathSync( + NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-node-script-")), + ); + const script = NodePath.join(directory, "server.cjs"); + const output = NodePath.join(directory, "argv.json"); + NodeFS.writeFileSync( + script, + `require("node:fs").writeFileSync(${JSON.stringify(output)}, JSON.stringify({ argv: process.argv.slice(1), filename: __filename }));`, + ); + + runNodeScript([script, "--stdio"]); + + expect(JSON.parse(NodeFS.readFileSync(output, "utf8"))).toEqual({ + argv: [script, "--stdio"], + filename: script, + }); + NodeFS.rmSync(directory, { recursive: true, force: true }); + }); +}); diff --git a/apps/server/src/nodeScript.ts b/apps/server/src/nodeScript.ts new file mode 100644 index 000000000000..2ab8564dead7 --- /dev/null +++ b/apps/server/src/nodeScript.ts @@ -0,0 +1,23 @@ +// @effect-diagnostics nodeBuiltinImport:off +// Runs before any Effect runtime is built, so it stays on Node built-ins. +import * as NodeModule from "node:module"; +import * as NodeSea from "node:sea"; + +/** + * How to run a JavaScript file shipped with T3 (tsserver, Pyright) on T3's own runtime. + * The single-executable build always starts its embedded entry and ignores a script + * argument, so it goes through `t3 __run-node-script`, which loads the script itself. + */ +export function nodeScriptCommand(script: string, args: ReadonlyArray) { + return { + command: process.execPath, + args: NodeSea.isSea() ? ["__run-node-script", script, ...args] : [script, ...args], + }; +} + +/** Entry for `t3 __run-node-script