diff --git a/src/cli/dispatch.ts b/src/cli/dispatch.ts index d84d6b7151..6e516d21fc 100644 --- a/src/cli/dispatch.ts +++ b/src/cli/dispatch.ts @@ -21,7 +21,6 @@ import { restoreNativeCodexAsync } from "../codex/inject"; import { stripGrokConfig } from "../grok/inject"; import { afterCatalogWriteHandleAppServers } from "../codex/app-server-processes"; import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job"; -import { serviceCommand } from "../service"; export interface CliDispatchDeps { args: string[]; @@ -45,6 +44,7 @@ export interface CliDispatchDeps { handleStatus: () => Promise; handleRecoverHistory: () => Promise; handleReady: (args: ReadyArgs) => Promise; + serviceCommand: (...args: string[]) => Promise; } type CommandRunner = (deps: CliDispatchDeps) => Promise; @@ -261,8 +261,11 @@ const commandRunners: Record = { return 0; }, service: async deps => { - await serviceCommand(...deps.args.slice(1)); - return 0; + process.exitCode = 0; + await deps.serviceCommand(...deps.args.slice(1)); + // serviceCommand uses process.exitCode for recoverable install/stop failures + // that must finish cleanup before the single top-level process.exit runs. + return Number(process.exitCode ?? 0); }, tray: async deps => { const { windowsTrayCommand } = await import("../tray/windows"); diff --git a/src/cli/index.ts b/src/cli/index.ts index e67f7a8b8c..940201ab21 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -941,4 +941,5 @@ process.exit(await dispatchCommand(head, { handleStatus, handleRecoverHistory, handleReady, + serviceCommand, })); diff --git a/tests/cli-dispatch.test.ts b/tests/cli-dispatch.test.ts index 0fa501494d..0396dc29e2 100644 --- a/tests/cli-dispatch.test.ts +++ b/tests/cli-dispatch.test.ts @@ -76,4 +76,45 @@ describe("dispatchCommand exit codes", () => { expect(await dispatchCommand(head, fakeDeps), `${name} must be unknown`).toBe(1); } }); + + test("forwards service arguments and preserves handler exit codes", async () => { + const previousExitCode = process.exitCode; + try { + process.exitCode = 7; + const successCalls: string[][] = []; + const successDeps = { + ...fakeDeps, + args: ["service", "install", "--scheduler"], + serviceCommand: async (...args: string[]) => { + successCalls.push(args); + }, + }; + + expect(await dispatchCommand( + { kind: "command", command: "service", args: successDeps.args }, + successDeps, + )).toBe(0); + expect(successCalls).toEqual([["install", "--scheduler"]]); + + for (const expected of [1, 2]) { + const calls: string[][] = []; + const deps = { + ...fakeDeps, + args: ["service", "install", "--scheduler"], + serviceCommand: async (...args: string[]) => { + calls.push(args); + process.exitCode = expected; + }, + }; + + expect(await dispatchCommand( + { kind: "command", command: "service", args: deps.args }, + deps, + )).toBe(expected); + expect(calls).toEqual([["install", "--scheduler"]]); + } + } finally { + process.exitCode = previousExitCode ?? 0; + } + }); });