diff --git a/packages/cli/__tests__/scripts/doctor-script.test.ts b/packages/cli/__tests__/scripts/doctor-script.test.ts index 12744d98c..514100cac 100644 --- a/packages/cli/__tests__/scripts/doctor-script.test.ts +++ b/packages/cli/__tests__/scripts/doctor-script.test.ts @@ -35,7 +35,14 @@ function createHealthyRepo(tempRoot: string): string { mkdirSync(join(fakeRepo, "packages", "core", "dist"), { recursive: true }); mkdirSync(join(fakeRepo, "packages", "cli", "dist"), { recursive: true }); mkdirSync(join(fakeRepo, "packages", "web"), { recursive: true }); - writeFileSync(join(fakeRepo, "packages", "core", "dist", "index.js"), "export {};\n"); + writeFileSync( + join(fakeRepo, "packages", "core", "package.json"), + JSON.stringify({ type: "module", main: "dist/index.js" }, null, 2), + ); + writeFileSync( + join(fakeRepo, "packages", "core", "dist", "index.js"), + 'export function getNodePtyPrebuildsSubdir() { return process.platform + "-" + process.arch; }\n', + ); writeFileSync(join(fakeRepo, "packages", "cli", "dist", "index.js"), "export {};\n"); writeFileSync( join(fakeRepo, "packages", "ao", "bin", "ao.js"), diff --git a/packages/cli/src/assets/scripts/ao-doctor.sh b/packages/cli/src/assets/scripts/ao-doctor.sh index 424daf9e2..2b20735bf 100755 --- a/packages/cli/src/assets/scripts/ao-doctor.sh +++ b/packages/cli/src/assets/scripts/ao-doctor.sh @@ -414,6 +414,7 @@ resolve_node_pty_spawn_helper() { const fs = require("node:fs"); const { createRequire } = require("node:module"); const path = require("node:path"); +const { pathToFileURL } = require("node:url"); const repoRoot = process.argv[2]; @@ -442,34 +443,57 @@ function resolveNodeModulesPackage(fromDir, ...segments) { return fs.existsSync(path.resolve(packageDir, "package.json")) ? packageDir : null; } -const packageJsonPath = - resolvePackageJson(repoRoot) ?? - (() => { - const directNodePtyDir = findPackageUp(repoRoot, "node-pty"); - if (directNodePtyDir) return path.join(directNodePtyDir, "package.json"); +function resolveCoreEntrypoint() { + const sourceCoreDir = path.resolve(repoRoot, "packages", "core"); + const coreDir = + (fs.existsSync(path.join(sourceCoreDir, "package.json")) ? sourceCoreDir : null) ?? + findPackageUp(repoRoot, "@aoagents", "ao-core") ?? + resolveNodeModulesPackage(repoRoot, "@aoagents", "ao-core"); + if (!coreDir) return null; - const sourceWebDir = path.resolve(repoRoot, "packages", "web"); - const webDir = - (fs.existsSync(path.join(sourceWebDir, "package.json")) ? sourceWebDir : null) ?? - findPackageUp(repoRoot, "@aoagents", "ao-web") ?? - resolveNodeModulesPackage(repoRoot, "@aoagents", "ao-web"); - if (!webDir) return null; + try { + const pkg = JSON.parse(fs.readFileSync(path.join(coreDir, "package.json"), "utf8")); + const entry = pkg.exports?.["."]?.import ?? pkg.module ?? pkg.main ?? "dist/index.js"; + return path.resolve(coreDir, entry); + } catch { + return null; + } +} - const webNodePtyDir = - resolveNodeModulesPackage(webDir, "node-pty") ?? findPackageUp(webDir, "node-pty"); - return webNodePtyDir ? path.join(webNodePtyDir, "package.json") : null; - })(); +async function getNodePtyPrebuildsSubdir() { + const coreEntrypoint = resolveCoreEntrypoint(); + if (!coreEntrypoint || !fs.existsSync(coreEntrypoint)) return null; -if (!packageJsonPath) process.exit(0); + const core = await import(pathToFileURL(coreEntrypoint).href); + return typeof core.getNodePtyPrebuildsSubdir === "function" + ? core.getNodePtyPrebuildsSubdir() + : null; +} -console.log( - path.join( - path.dirname(packageJsonPath), - "prebuilds", - `${process.platform}-${process.arch}`, - "spawn-helper", - ), -); +(async () => { + const packageJsonPath = + resolvePackageJson(repoRoot) ?? + (() => { + const directNodePtyDir = findPackageUp(repoRoot, "node-pty"); + if (directNodePtyDir) return path.join(directNodePtyDir, "package.json"); + + const sourceWebDir = path.resolve(repoRoot, "packages", "web"); + const webDir = + (fs.existsSync(path.join(sourceWebDir, "package.json")) ? sourceWebDir : null) ?? + findPackageUp(repoRoot, "@aoagents", "ao-web") ?? + resolveNodeModulesPackage(repoRoot, "@aoagents", "ao-web"); + if (!webDir) return null; + + const webNodePtyDir = + resolveNodeModulesPackage(webDir, "node-pty") ?? findPackageUp(webDir, "node-pty"); + return webNodePtyDir ? path.join(webNodePtyDir, "package.json") : null; + })(); + + const prebuildsSubdir = await getNodePtyPrebuildsSubdir(); + if (!packageJsonPath || !prebuildsSubdir) process.exit(0); + + console.log(path.join(path.dirname(packageJsonPath), "prebuilds", prebuildsSubdir, "spawn-helper")); +})().catch(() => process.exit(0)); NODE } diff --git a/packages/core/src/__tests__/platform.test.ts b/packages/core/src/__tests__/platform.test.ts index 06667db02..0e3fcfb80 100644 --- a/packages/core/src/__tests__/platform.test.ts +++ b/packages/core/src/__tests__/platform.test.ts @@ -44,6 +44,14 @@ describe("platform adapter", () => { }); }); + describe("getNodePtyPrebuildsSubdir", () => { + it("centralizes node-pty prebuild platform/arch naming", async () => { + setPlatform("darwin"); + const mod = await import("../platform.js"); + expect(mod.getNodePtyPrebuildsSubdir()).toBe(`darwin-${process.arch}`); + }); + }); + describe("getShell", () => { it("always returns /bin/sh on unix (ignores $SHELL)", async () => { setPlatform("linux"); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5c5936c82..cbf2024a7 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -366,6 +366,7 @@ export { isMac, isLinux, getDefaultRuntime, + getNodePtyPrebuildsSubdir, getShell, killProcessTree, findPidByPort, diff --git a/packages/core/src/platform.ts b/packages/core/src/platform.ts index 5cde1af87..2f4323555 100644 --- a/packages/core/src/platform.ts +++ b/packages/core/src/platform.ts @@ -28,6 +28,10 @@ export function getDefaultRuntime(): "tmux" | "process" { return isWindows() ? "process" : "tmux"; } +export function getNodePtyPrebuildsSubdir(): string { + return `${process.platform}-${process.arch}`; +} + // -- Shell resolution -- interface ShellInfo { diff --git a/packages/web/server/__tests__/mux-websocket.test.ts b/packages/web/server/__tests__/mux-websocket.test.ts index e33bd8186..3f5cc0216 100644 --- a/packages/web/server/__tests__/mux-websocket.test.ts +++ b/packages/web/server/__tests__/mux-websocket.test.ts @@ -5,7 +5,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import type { Socket } from "node:net"; import { WebSocket } from "ws"; -import { appendDashboardNotification, type OrchestratorEvent } from "@aoagents/ao-core"; +import { appendDashboardNotification, isWindows, type OrchestratorEvent } from "@aoagents/ao-core"; import type { SessionBroadcaster as SessionBroadcasterType } from "../mux-websocket"; // vi.mock factories run before module-level statements. Hoist the mock @@ -967,7 +967,7 @@ describe("TerminalManager.open — tmux target args (regression for #1714)", () expect(args).toEqual(["attach-session", "-t", "=ao-177"]); }); - it("chmods node-pty spawn-helper and retries once after posix_spawnp failure", () => { + it("repairs node-pty spawn-helper when applicable and retries once after posix_spawnp failure", () => { const tempRoot = mkdtempSync(join(tmpdir(), "ao-mux-spawn-helper-")); const helperPath = join(tempRoot, "spawn-helper"); writeFileSync(helperPath, "#!/bin/sh\nexit 0\n"); @@ -993,7 +993,9 @@ describe("TerminalManager.open — tmux target args (regression for #1714)", () mgr.open("ao-177"); expect(mockPtySpawn).toHaveBeenCalledTimes(2); - expect((statSync(helperPath).mode & 0o111) !== 0).toBe(true); + if (!isWindows()) { + expect((statSync(helperPath).mode & 0o111) !== 0).toBe(true); + } } finally { delete process.env.AO_NODE_PTY_SPAWN_HELPER_PATH; rmSync(tempRoot, { recursive: true, force: true }); diff --git a/packages/web/server/mux-websocket.ts b/packages/web/server/mux-websocket.ts index 26df58d18..9be199e2c 100644 --- a/packages/web/server/mux-websocket.ts +++ b/packages/web/server/mux-websocket.ts @@ -16,6 +16,7 @@ import { DEFAULT_DASHBOARD_NOTIFICATION_LIMIT, getEnvDefaults, getDashboardNotificationStorePath, + getNodePtyPrebuildsSubdir, isWindows, loadConfig, normalizeDashboardNotificationLimit, @@ -395,12 +396,7 @@ export function resolveNodePtySpawnHelperPath(): string | null { try { const packageJsonPath = nodePtyRequire.resolve("node-pty/package.json"); - return join( - dirname(packageJsonPath), - "prebuilds", - `${process.platform}-${process.arch}`, - "spawn-helper", - ); + return join(dirname(packageJsonPath), "prebuilds", getNodePtyPrebuildsSubdir(), "spawn-helper"); } catch { return null; }