diff --git a/src/cli/run/server-connection.test.ts b/src/cli/run/server-connection.test.ts index a9e224124..110f9c00d 100644 --- a/src/cli/run/server-connection.test.ts +++ b/src/cli/run/server-connection.test.ts @@ -177,6 +177,28 @@ describe("createServerConnection", () => { expect(mockServerClose).toHaveBeenCalledTimes(1) }) + it("auto mode attaches to default server when port range is exhausted", async () => { + // given + const signal = new AbortController().signal + mockGetAvailableServerPort.mockRejectedValueOnce( + new Error("No available port found in range 4097-4116"), + ) + mockIsPortAvailable.mockResolvedValueOnce(false) + + // when + const result = await createServerConnection({ signal }) + + // then + expect(mockGetAvailableServerPort).toHaveBeenCalledWith(4096, "127.0.0.1") + expect(mockIsPortAvailable).toHaveBeenCalledWith(4096, "127.0.0.1") + expect(mockCreateOpencodeClient).toHaveBeenCalledWith({ + baseUrl: "http://127.0.0.1:4096", + }) + expect(mockCreateOpencode).not.toHaveBeenCalled() + result.cleanup() + expect(mockServerClose).not.toHaveBeenCalled() + }) + it("invalid port throws error", async () => { // given const signal = new AbortController().signal diff --git a/src/cli/run/server-connection.ts b/src/cli/run/server-connection.ts index 949b6de4f..bf658ff05 100644 --- a/src/cli/run/server-connection.ts +++ b/src/cli/run/server-connection.ts @@ -12,6 +12,14 @@ function isPortStartFailure(error: unknown, port: number): boolean { return error.message.includes(`Failed to start server on port ${port}`) } +function isPortRangeExhausted(error: unknown): boolean { + if (!(error instanceof Error)) { + return false + } + + return error.message.includes("No available port found in range") +} + async function startServer(options: { signal: AbortSignal, port: number }): Promise { const { signal, port } = options const { client, server } = await withWorkingOpencodePath(() => @@ -67,7 +75,27 @@ export async function createServerConnection(options: { return { client, cleanup: () => {} } } - const { port: selectedPort, wasAutoSelected } = await getAvailableServerPort(DEFAULT_SERVER_PORT, "127.0.0.1") + let selectedPort: number + let wasAutoSelected: boolean + try { + const selected = await getAvailableServerPort(DEFAULT_SERVER_PORT, "127.0.0.1") + selectedPort = selected.port + wasAutoSelected = selected.wasAutoSelected + } catch (error) { + if (!isPortRangeExhausted(error)) { + throw error + } + + const defaultPortIsAvailable = await isPortAvailable(DEFAULT_SERVER_PORT, "127.0.0.1") + if (defaultPortIsAvailable) { + throw error + } + + console.log(pc.dim("Port range exhausted, attaching to existing server on"), pc.cyan(DEFAULT_SERVER_PORT.toString())) + const client = createOpencodeClient({ baseUrl: `http://127.0.0.1:${DEFAULT_SERVER_PORT}` }) + return { client, cleanup: () => {} } + } + if (wasAutoSelected) { console.log(pc.dim("Auto-selected port"), pc.cyan(selectedPort.toString())) } else {