diff --git a/packages/cli/package.json b/packages/cli/package.json index 65b887909..8545797e1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "0.30.0", + "version": "0.30.1", "type": "module", "bin": { "spawn": "cli.js" diff --git a/packages/cli/src/__tests__/sandbox.test.ts b/packages/cli/src/__tests__/sandbox.test.ts index 5f2668d7e..283b95bc8 100644 --- a/packages/cli/src/__tests__/sandbox.test.ts +++ b/packages/cli/src/__tests__/sandbox.test.ts @@ -72,7 +72,7 @@ describe("ensureDocker", () => { spy.mockRestore(); }); - it("attempts brew install on macOS when docker unavailable", async () => { + it("attempts brew install on macOS when docker not installed", async () => { const origPlatform = Object.getOwnPropertyDescriptor(process, "platform"); Object.defineProperty(process, "platform", { value: "darwin", @@ -82,19 +82,7 @@ describe("ensureDocker", () => { let callCount = 0; const spy = spyOn(Bun, "spawnSync").mockImplementation((..._args: unknown[]) => { callCount++; - // First call: docker info → fail, second: brew install → succeed, third: docker info → succeed - if (callCount === 1) { - return { - exitCode: 1, - stdout: new Uint8Array(), - stderr: new Uint8Array(), - success: false, - signalCode: null, - resourceUsage: undefined, - pid: 1234, - } satisfies ReturnType; - } - return { + const ok = { exitCode: 0, stdout: new Uint8Array(), stderr: new Uint8Array(), @@ -103,16 +91,37 @@ describe("ensureDocker", () => { resourceUsage: undefined, pid: 1234, } satisfies ReturnType; + const fail = { + exitCode: 1, + stdout: new Uint8Array(), + stderr: new Uint8Array(), + success: false, + signalCode: null, + resourceUsage: undefined, + pid: 1234, + } satisfies ReturnType; + // 1: docker info → fail, 2: which docker → fail (not installed), + // 3: brew install → ok, 4: open -a OrbStack → ok, 5: docker info → ok + if (callCount <= 2) { + return fail; + } + return ok; }); await ensureDocker(); - // Second call should be brew install orbstack - expect(spy.mock.calls[1][0]).toEqual([ + // Call 1: docker info, 2: which docker, 3: brew install orbstack + expect(spy.mock.calls[2][0]).toEqual([ "brew", "install", "orbstack", ]); + // Call 4: open -a OrbStack (starts daemon) + expect(spy.mock.calls[3][0]).toEqual([ + "open", + "-a", + "OrbStack", + ]); spy.mockRestore(); if (origPlatform) { diff --git a/packages/cli/src/local/local.ts b/packages/cli/src/local/local.ts index 79cded44b..b55e7ccaf 100644 --- a/packages/cli/src/local/local.ts +++ b/packages/cli/src/local/local.ts @@ -68,31 +68,129 @@ export async function interactiveSession(cmd: string): Promise { // ─── Docker Sandbox ───────────────────────────────────────────────────────── -/** Check whether Docker (or OrbStack) is available on the host. */ +/** Check whether the Docker daemon is running and responsive. */ export function isDockerAvailable(): boolean { - const result = Bun.spawnSync( - [ - "docker", - "info", - ], - { - stdio: [ - "ignore", - "ignore", - "ignore", + return ( + Bun.spawnSync( + [ + "docker", + "info", ], - }, + { + stdio: [ + "ignore", + "ignore", + "ignore", + ], + }, + ).exitCode === 0 ); - return result.exitCode === 0; } -/** Install Docker if not present, or exit with guidance if install fails. */ +/** Check whether the docker binary exists (installed but daemon may be stopped). */ +function isDockerInstalled(): boolean { + return ( + Bun.spawnSync( + [ + "which", + "docker", + ], + { + stdio: [ + "ignore", + "ignore", + "ignore", + ], + }, + ).exitCode === 0 + ); +} + +/** Try to start the Docker daemon and wait up to 30s for it to respond. */ +function startAndWaitForDocker(isMac: boolean): void { + if (isMac) { + logStep("Starting OrbStack..."); + Bun.spawnSync( + [ + "open", + "-a", + "OrbStack", + ], + { + stdio: [ + "ignore", + "ignore", + "ignore", + ], + }, + ); + } else { + logStep("Starting Docker daemon..."); + const hasSudo = + Bun.spawnSync( + [ + "which", + "sudo", + ], + { + stdio: [ + "ignore", + "ignore", + "ignore", + ], + }, + ).exitCode === 0; + if (hasSudo) { + Bun.spawnSync( + [ + "sudo", + "systemctl", + "start", + "docker", + ], + { + stdio: [ + "ignore", + "inherit", + "inherit", + ], + }, + ); + } + } + + // Wait up to 30s for the daemon to be ready + logStep("Waiting for Docker daemon..."); + for (let i = 0; i < 30; i++) { + if (isDockerAvailable()) { + logInfo("Docker is ready"); + return; + } + Bun.sleepSync(1000); + } + logInfo("Docker daemon did not start within 30s."); + if (isMac) { + logInfo("Open OrbStack.app manually, then retry."); + } + process.exit(1); +} + +/** Ensure Docker is installed and the daemon is running. Installs and starts if needed. */ export async function ensureDocker(): Promise { + // Fast path: daemon already running if (isDockerAvailable()) { return; } const isMac = process.platform === "darwin"; + + // Docker binary exists but daemon not running — just start it + if (isDockerInstalled()) { + startAndWaitForDocker(isMac); + return; + } + + // Not installed at all — install first if (isMac) { logStep("Docker not found — installing OrbStack..."); const result = Bun.spawnSync( @@ -150,11 +248,8 @@ export async function ensureDocker(): Promise { } } - // Verify Docker works after install - if (!isDockerAvailable()) { - logInfo("Docker installed but not responding. You may need to start the Docker daemon."); - process.exit(1); - } + // Start the daemon after fresh install + startAndWaitForDocker(isMac); } /** Pull the agent Docker image and start a container. */