diff --git a/apps/server/src/provider/providerSnapshot.test.ts b/apps/server/src/provider/providerSnapshot.test.ts index 399d86f7a133..5c45362daa74 100644 --- a/apps/server/src/provider/providerSnapshot.test.ts +++ b/apps/server/src/provider/providerSnapshot.test.ts @@ -11,6 +11,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { isCommandMissingCause, + parseGenericCliVersion, providerModelsFromSettings, spawnAndCollect, } from "./providerSnapshot.ts"; @@ -93,6 +94,29 @@ describe("providerModelsFromSettings", () => { }); }); +describe("parseGenericCliVersion", () => { + it("parses a bare version", () => { + expect(parseGenericCliVersion("1.14.19")).toBe("1.14.19"); + }); + + it("parses a v-prefixed version", () => { + expect(parseGenericCliVersion("opencode v2.0.3")).toBe("2.0.3"); + expect(parseGenericCliVersion("v22.19.0")).toBe("22.19.0"); + }); + + it("parses a version embedded in other output", () => { + expect(parseGenericCliVersion("codex-cli 0.53.0 (build abc)")).toBe("0.53.0"); + }); + + it("returns null when no version is present", () => { + expect(parseGenericCliVersion("no version here")).toBeNull(); + }); + + it("ignores versions glued to other word characters", () => { + expect(parseGenericCliVersion("build2.0.3artifact")).toBeNull(); + }); +}); + describe("ProviderCommandNotFoundError", () => { it("classifies normalized platform failures without parsing messages", () => { expect( diff --git a/apps/server/src/provider/providerSnapshot.ts b/apps/server/src/provider/providerSnapshot.ts index 40ae0eeefde0..c5503a175498 100644 --- a/apps/server/src/provider/providerSnapshot.ts +++ b/apps/server/src/provider/providerSnapshot.ts @@ -107,8 +107,14 @@ export const spawnAndCollect = (binaryPath: string, command: ChildProcess.Comman return result; }).pipe(Effect.scoped); +/** + * Return the first semantic version found in CLI output, or null. Accepts a + * leading "v" (for example `opencode v2.0.3`). + */ export function parseGenericCliVersion(output: string): string | null { - const match = output.match(/\b(\d+\.\d+\.\d+)\b/); + // "opencode v2.0.3"-style output: the optional "v" has to be consumed first, + // since "v2" itself contains no word boundary. + const match = output.match(/\bv?(\d+\.\d+\.\d+)\b/); return match?.[1] ?? null; }