From bc9fa2be8658ba8a772f56ee8c340936889f8b0b Mon Sep 17 00:00:00 2001 From: Advait Paliwal Date: Wed, 25 Mar 2026 14:02:38 -0700 Subject: [PATCH] Fix runtime package resolution and tty shutdown --- scripts/patch-embedded-pi.mjs | 63 +++++++++++++++++++++++++++++++++-- src/pi/runtime.ts | 8 ++++- tests/pi-runtime.test.ts | 9 ++++- 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/scripts/patch-embedded-pi.mjs b/scripts/patch-embedded-pi.mjs index c403262..74afae5 100644 --- a/scripts/patch-embedded-pi.mjs +++ b/scripts/patch-embedded-pi.mjs @@ -51,6 +51,7 @@ const cliPath = piPackageRoot ? resolve(piPackageRoot, "dist", "cli.js") : null; const bunCliPath = piPackageRoot ? resolve(piPackageRoot, "dist", "bun", "cli.js") : null; const interactiveModePath = piPackageRoot ? resolve(piPackageRoot, "dist", "modes", "interactive", "interactive-mode.js") : null; const interactiveThemePath = piPackageRoot ? resolve(piPackageRoot, "dist", "modes", "interactive", "theme", "theme.js") : null; +const terminalPath = piTuiRoot ? resolve(piTuiRoot, "dist", "terminal.js") : null; const editorPath = piTuiRoot ? resolve(piTuiRoot, "dist", "components", "editor.js") : null; const workspaceRoot = resolve(appRoot, ".feynman", "npm", "node_modules"); const webAccessPath = resolve(workspaceRoot, "pi-web-access", "index.ts"); @@ -247,10 +248,68 @@ for (const entryPath of [cliPath, bunCliPath].filter(Boolean)) { continue; } - const cliSource = readFileSync(entryPath, "utf8"); + let cliSource = readFileSync(entryPath, "utf8"); if (cliSource.includes('process.title = "pi";')) { - writeFileSync(entryPath, cliSource.replace('process.title = "pi";', 'process.title = "feynman";'), "utf8"); + cliSource = cliSource.replace('process.title = "pi";', 'process.title = "feynman";'); } + const stdinErrorGuard = [ + "const feynmanHandleStdinError = (error) => {", + ' if (error && typeof error === "object") {', + ' const code = "code" in error ? error.code : undefined;', + ' const syscall = "syscall" in error ? error.syscall : undefined;', + ' if ((code === "EIO" || code === "EBADF") && syscall === "read") {', + " return;", + " }", + " }", + "};", + 'process.stdin?.on?.("error", feynmanHandleStdinError);', + ].join("\n"); + if (!cliSource.includes('process.stdin?.on?.("error", feynmanHandleStdinError);')) { + cliSource = cliSource.replace( + 'process.emitWarning = (() => { });', + `process.emitWarning = (() => { });\n${stdinErrorGuard}`, + ); + } + writeFileSync(entryPath, cliSource, "utf8"); +} + +if (terminalPath && existsSync(terminalPath)) { + let terminalSource = readFileSync(terminalPath, "utf8"); + if (!terminalSource.includes("stdinErrorHandler;")) { + terminalSource = terminalSource.replace( + " stdinBuffer;\n stdinDataHandler;\n", + [ + " stdinBuffer;", + " stdinDataHandler;", + " stdinErrorHandler = (error) => {", + ' if ((error?.code === "EIO" || error?.code === "EBADF") && error?.syscall === "read") {', + " return;", + " }", + " };", + ].join("\n") + "\n", + ); + } + if (!terminalSource.includes('process.stdin.on("error", this.stdinErrorHandler);')) { + terminalSource = terminalSource.replace( + ' process.stdin.resume();\n', + ' process.stdin.resume();\n process.stdin.on("error", this.stdinErrorHandler);\n', + ); + } + if (!terminalSource.includes(' process.stdin.removeListener("error", this.stdinErrorHandler);')) { + terminalSource = terminalSource.replace( + ' process.stdin.removeListener("data", onData);\n this.inputHandler = previousHandler;\n', + [ + ' process.stdin.removeListener("data", onData);', + ' process.stdin.removeListener("error", this.stdinErrorHandler);', + ' this.inputHandler = previousHandler;', + ].join("\n"), + ); + terminalSource = terminalSource.replace( + ' process.stdin.pause();\n', + ' process.stdin.removeListener("error", this.stdinErrorHandler);\n process.stdin.pause();\n', + ); + } + writeFileSync(terminalPath, terminalSource, "utf8"); } if (interactiveModePath && existsSync(interactiveModePath)) { diff --git a/src/pi/runtime.ts b/src/pi/runtime.ts index fee987d..9a85aa3 100644 --- a/src/pi/runtime.ts +++ b/src/pi/runtime.ts @@ -77,9 +77,12 @@ export function buildPiArgs(options: PiRuntimeOptions): string[] { export function buildPiEnv(options: PiRuntimeOptions): NodeJS.ProcessEnv { const paths = resolvePiPaths(options.appRoot); + const feynmanHome = dirname(options.feynmanAgentDir); + const feynmanNpmPrefixPath = resolve(feynmanHome, "npm-global"); + const feynmanNpmBinPath = resolve(feynmanNpmPrefixPath, "bin"); const currentPath = process.env.PATH ?? ""; - const binEntries = [paths.nodeModulesBinPath, resolve(paths.piWorkspaceNodeModulesPath, ".bin")]; + const binEntries = [paths.nodeModulesBinPath, resolve(paths.piWorkspaceNodeModulesPath, ".bin"), feynmanNpmBinPath]; const binPath = binEntries.join(":"); return { @@ -90,11 +93,14 @@ export function buildPiEnv(options: PiRuntimeOptions): NodeJS.ProcessEnv { FEYNMAN_MEMORY_DIR: resolve(dirname(options.feynmanAgentDir), "memory"), FEYNMAN_NODE_EXECUTABLE: process.execPath, FEYNMAN_BIN_PATH: resolve(options.appRoot, "bin", "feynman.js"), + FEYNMAN_NPM_PREFIX: feynmanNpmPrefixPath, PANDOC_PATH: process.env.PANDOC_PATH ?? resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS), PI_HARDWARE_CURSOR: process.env.PI_HARDWARE_CURSOR ?? "1", PI_SKIP_VERSION_CHECK: process.env.PI_SKIP_VERSION_CHECK ?? "1", MERMAID_CLI_PATH: process.env.MERMAID_CLI_PATH ?? resolveExecutable("mmdc", MERMAID_FALLBACK_PATHS), PUPPETEER_EXECUTABLE_PATH: process.env.PUPPETEER_EXECUTABLE_PATH ?? resolveExecutable("google-chrome", BROWSER_FALLBACK_PATHS), + NPM_CONFIG_PREFIX: process.env.NPM_CONFIG_PREFIX ?? feynmanNpmPrefixPath, + npm_config_prefix: process.env.npm_config_prefix ?? feynmanNpmPrefixPath, }; } diff --git a/tests/pi-runtime.test.ts b/tests/pi-runtime.test.ts index 204e4d2..b388d98 100644 --- a/tests/pi-runtime.test.ts +++ b/tests/pi-runtime.test.ts @@ -41,7 +41,14 @@ test("buildPiEnv wires Feynman paths into the Pi environment", () => { assert.equal(env.FEYNMAN_SESSION_DIR, "/sessions"); assert.equal(env.FEYNMAN_BIN_PATH, "/repo/feynman/bin/feynman.js"); assert.equal(env.FEYNMAN_MEMORY_DIR, "/home/.feynman/memory"); - assert.ok(env.PATH?.startsWith("/repo/feynman/node_modules/.bin:/repo/feynman/.feynman/npm/node_modules/.bin:")); + assert.equal(env.FEYNMAN_NPM_PREFIX, "/home/.feynman/npm-global"); + assert.equal(env.NPM_CONFIG_PREFIX, "/home/.feynman/npm-global"); + assert.equal(env.npm_config_prefix, "/home/.feynman/npm-global"); + assert.ok( + env.PATH?.startsWith( + "/repo/feynman/node_modules/.bin:/repo/feynman/.feynman/npm/node_modules/.bin:/home/.feynman/npm-global/bin:", + ), + ); }); test("resolvePiPaths includes the Promise.withResolvers polyfill path", () => {