diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 09aada0d5..f9d7f7b29 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -320,7 +320,7 @@ function ghInstallAttempts(): InstallAttempt[] { { cmd: "sudo", args: ["dnf", "install", "-y", "gh"], label: "sudo dnf install -y gh" }, ]; } - if (process.platform === "win32") { + if (isWindows()) { return [ { cmd: "winget", @@ -1625,7 +1625,7 @@ function isLocalPath(arg: string): boolean { * the parent process — the user explicitly asked us to stop AO. */ async function sweepWindowsPtyHostsBeforeParentKill(): Promise { - if (process.platform !== "win32") return; + if (!isWindows()) return; try { const mod = (await import("@aoagents/ao-plugin-runtime-process")) as { sweepWindowsPtyHosts?: () => Promise<{ diff --git a/packages/core/src/atomic-write.ts b/packages/core/src/atomic-write.ts index deaf076ab..a7f83a582 100644 --- a/packages/core/src/atomic-write.ts +++ b/packages/core/src/atomic-write.ts @@ -1,11 +1,11 @@ import { renameSync, writeFileSync, unlinkSync } from "node:fs"; +import { isWindows } from "./platform.js"; // Windows file-locking workaround. `renameSync` can fail with EPERM/EACCES when // antivirus, the Windows indexer, or another process briefly holds a handle on // the destination path. The failures are transient — a short retry loop is the // standard fix (same pattern Node's own `fs.rm` uses internally). -const IS_WINDOWS = process.platform === "win32"; -const RENAME_RETRIES = IS_WINDOWS ? 10 : 0; +const RENAME_RETRIES = isWindows() ? 10 : 0; const RENAME_RETRY_DELAY_MS = 50; function renameWithRetry(src: string, dest: string): void { diff --git a/packages/web/server/mux-websocket.ts b/packages/web/server/mux-websocket.ts index 03f4f7117..3a31f3923 100644 --- a/packages/web/server/mux-websocket.ts +++ b/packages/web/server/mux-websocket.ts @@ -10,7 +10,7 @@ import { WebSocketServer, WebSocket } from "ws"; import { spawn } from "node:child_process"; import { type Socket, connect as netConnect } from "node:net"; import { findTmux, resolveTmuxSession, resolvePipePath, validateSessionId } from "./tmux-utils.js"; -import { getEnvDefaults } from "@aoagents/ao-core"; +import { getEnvDefaults, isWindows } from "@aoagents/ao-core"; // These types mirror src/lib/mux-protocol.ts exactly. // tsconfig.server.json constrains rootDir to "server/", so we cannot import @@ -503,6 +503,7 @@ export function handleWindowsPipeMessage( pipeSocket.on("error", (err) => { winPipes.delete(pipeKey); winPipeBuffers.delete(pipeKey); + pipeSocket.destroy(); if (ws.readyState === WS_OPEN) { ws.send( JSON.stringify({ @@ -604,14 +605,14 @@ export function handleWindowsPipeMessage( export function createMuxWebSocket(tmuxPath?: string | null): WebSocketServer | null { // On Windows, we use named pipe relay instead of node-pty/tmux. // Allow the server to be created without ptySpawn on Windows. - if (!ptySpawn && process.platform !== "win32") { + if (!ptySpawn && !isWindows()) { console.warn("[MuxServer] node-pty not available — mux WebSocket will be disabled"); return null; } // On Windows, terminal I/O goes through named pipe relay — no TerminalManager needed. const terminalManager = - ptySpawn && process.platform !== "win32" ? new TerminalManager(tmuxPath ?? undefined) : null; + ptySpawn && !isWindows() ? new TerminalManager(tmuxPath ?? undefined) : null; const nextPort = process.env.PORT || "3000"; const broadcaster = new SessionBroadcaster(nextPort); @@ -669,7 +670,7 @@ export function createMuxWebSocket(tmuxPath?: string | null): WebSocketServer | try { if (type === "open") { - if (process.platform === "win32") { + if (isWindows()) { handleWindowsPipeMessage( msg as { id: string; type: string; projectId?: string; data?: string; cols?: number; rows?: number }, ws, @@ -739,7 +740,7 @@ export function createMuxWebSocket(tmuxPath?: string | null): WebSocketServer | } } } else if (type === "data" && "data" in msg) { - if (process.platform === "win32") { + if (isWindows()) { handleWindowsPipeMessage( msg as { id: string; type: string; projectId?: string; data: string }, ws, @@ -751,7 +752,7 @@ export function createMuxWebSocket(tmuxPath?: string | null): WebSocketServer | terminalManager?.write(id, msg.data, projectId); } } else if (type === "resize" && "cols" in msg && "rows" in msg) { - if (process.platform === "win32") { + if (isWindows()) { handleWindowsPipeMessage( msg as { id: string; type: string; projectId?: string; cols: number; rows: number }, ws, @@ -763,7 +764,7 @@ export function createMuxWebSocket(tmuxPath?: string | null): WebSocketServer | terminalManager?.resize(id, msg.cols, msg.rows, projectId); } } else if (type === "close") { - if (process.platform === "win32") { + if (isWindows()) { handleWindowsPipeMessage( msg as { id: string; type: string; projectId?: string; data?: string; cols?: number; rows?: number }, ws, @@ -840,7 +841,7 @@ export function createMuxWebSocket(tmuxPath?: string | null): WebSocketServer | subscriptions.clear(); // Windows: close all open pipe sockets for (const pipeSocket of winPipes.values()) { - pipeSocket.end(); + pipeSocket.destroy(); } winPipes.clear(); winPipeBuffers.clear(); diff --git a/packages/web/server/tmux-utils.ts b/packages/web/server/tmux-utils.ts index ba7d82622..7aa5a5a97 100644 --- a/packages/web/server/tmux-utils.ts +++ b/packages/web/server/tmux-utils.ts @@ -9,6 +9,7 @@ import { execFileSync } from "node:child_process"; import { readdirSync, existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; +import { isWindows } from "@aoagents/ao-core"; /** Session ID validation regex — alphanumeric, hyphens, underscores only */ export const SESSION_ID_PATTERN = /^[a-zA-Z0-9_-]+$/; @@ -110,7 +111,7 @@ export function validateSessionId(sessionId: string): boolean { export function findTmux( execFn: typeof execFileSync = execFileSync, ): string | null { - if (process.platform === "win32") return null; + if (isWindows()) return null; const candidates = [ "/opt/homebrew/bin/tmux", // macOS ARM (Homebrew) "/usr/local/bin/tmux", // macOS Intel (Homebrew) @@ -233,7 +234,7 @@ export function resolvePipePath( readFile?: (path: string) => string; } = defaultFs, ): string | null { - if (process.platform !== "win32") return null; + if (!isWindows()) return null; const readFile = fs.readFile ?? ((p: string) => readFileSync(p, "utf8")); const aoBase = join(fs.homedir(), ".agent-orchestrator");