From fdfec4617d60c2fa64321d32e3b28d524cc7cc85 Mon Sep 17 00:00:00 2001 From: cte Date: Sat, 7 Feb 2026 18:59:41 -0800 Subject: [PATCH] IPC commands --- apps/cli/src/commands/cli/run.ts | 75 ++++++++++++++++++++++++++++++-- apps/cli/src/index.ts | 4 ++ apps/cli/src/types/types.ts | 1 + 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/apps/cli/src/commands/cli/run.ts b/apps/cli/src/commands/cli/run.ts index 881107fb82..15966f52e0 100644 --- a/apps/cli/src/commands/cli/run.ts +++ b/apps/cli/src/commands/cli/run.ts @@ -1,8 +1,10 @@ import fs from "fs" +import os from "os" import path from "path" import { fileURLToPath } from "url" import { createElement } from "react" +import pWaitFor from "p-wait-for" import { setLogger } from "@roo-code/vscode-shim" @@ -64,7 +66,11 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption const effectiveReasoningEffort = flagOptions.reasoningEffort || settings.reasoningEffort || DEFAULT_FLAGS.reasoningEffort const effectiveProvider = flagOptions.provider ?? settings.provider ?? (rooToken ? "roo" : "openrouter") - const effectiveWorkspacePath = flagOptions.workspace ? path.resolve(flagOptions.workspace) : process.cwd() + const effectiveWorkspacePath = flagOptions.workspace + ? path.resolve(flagOptions.workspace) + : process.env.ROO_CODE_WORKSPACE_PATH + ? path.resolve(process.env.ROO_CODE_WORKSPACE_PATH) + : process.cwd() const effectiveDangerouslySkipPermissions = flagOptions.yes || flagOptions.dangerouslySkipPermissions || settings.dangerouslySkipPermissions || false const effectiveExitOnComplete = flagOptions.print || flagOptions.oneshot || settings.oneshot || false @@ -84,6 +90,25 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption exitOnComplete: effectiveExitOnComplete, } + // IPC (headless) mode detection. + // Activates when: --ipc flag is set, or ROO_CODE_IPC_SOCKET_PATH is set without a prompt. + const isIpcMode = !!flagOptions.ipc || (!!process.env.ROO_CODE_IPC_SOCKET_PATH && !prompt && !flagOptions.print) + + // Resolve the IPC socket path: --ipc > ROO_CODE_IPC_SOCKET_PATH > auto-generated temp path. + let ipcSocketPath: string | undefined + if (isIpcMode) { + ipcSocketPath = + (typeof flagOptions.ipc === "string" ? flagOptions.ipc : undefined) || + process.env.ROO_CODE_IPC_SOCKET_PATH || + path.join(os.tmpdir(), `roo-ipc-${process.pid}.sock`) + + // Set the env var so the extension picks it up during activation. + process.env.ROO_CODE_IPC_SOCKET_PATH = ipcSocketPath + + extensionHostOptions.disableOutput = true + extensionHostOptions.exitOnComplete = false + } + // Roo Code Cloud Authentication if (isOnboardingEnabled) { @@ -187,7 +212,7 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption process.exit(1) } - if (!isTuiEnabled) { + if (!isTuiEnabled && !isIpcMode) { if (!prompt) { console.error("[CLI] Error: prompt is required in print mode") console.error("[CLI] Usage: roo --print [options]") @@ -202,7 +227,51 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption // Run! - if (isTuiEnabled) { + if (isIpcMode) { + const host = new ExtensionHost(extensionHostOptions) + + async function shutdown(signal: string, exitCode: number): Promise { + console.error(`\n[IPC] Received ${signal}, shutting down...`) + await host.dispose() + + if (ipcSocketPath && fs.existsSync(ipcSocketPath)) { + try { + fs.unlinkSync(ipcSocketPath) + } catch { + // Socket may already be cleaned up. + } + } + + process.exit(exitCode) + } + + process.on("SIGINT", () => shutdown("SIGINT", 130)) + process.on("SIGTERM", () => shutdown("SIGTERM", 0)) + + try { + await host.activate() + + // Verify the IPC socket is ready. The extension creates the IPC server + // internally during activation, but ipc.serve() is async so we poll + // for the socket file. + await pWaitFor(() => fs.existsSync(ipcSocketPath!), { interval: 100, timeout: 10_000 }) + + console.error(`[IPC] IPC server ready: ${ipcSocketPath}`) + + // Stay alive indefinitely until SIGTERM/SIGINT. + await new Promise(() => {}) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + console.error(`[IPC] Error: ${errorMessage}`) + + if (error instanceof Error) { + console.error(error.stack) + } + + await host.dispose() + process.exit(1) + } + } else if (isTuiEnabled) { try { const { render } = await import("ink") const { App } = await import("../../ui/App.js") diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 51218aa860..efcfa0b726 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -32,6 +32,10 @@ program .option("--exit-on-error", "Exit on API request errors instead of retrying", false) .option("--ephemeral", "Run without persisting state (uses temporary storage)", false) .option("--oneshot", "Exit upon task completion", false) + .option( + "--ipc [socket-path]", + "Run as a headless IPC server (optionally specify socket path, or set ROO_CODE_IPC_SOCKET_PATH)", + ) .option( "--output-format ", 'Output format (only works with --print): "text" (default), "json" (single result), or "stream-json" (realtime streaming)', diff --git a/apps/cli/src/types/types.ts b/apps/cli/src/types/types.ts index 827afad513..d6eb4b6fc2 100644 --- a/apps/cli/src/types/types.ts +++ b/apps/cli/src/types/types.ts @@ -35,6 +35,7 @@ export type FlagOptions = { ephemeral: boolean oneshot: boolean outputFormat?: OutputFormat + ipc?: boolean | string } export enum OnboardingProviderChoice {