authorgravatar for 71512463+NikodemNowak@users.noreply.github.comNikodemNowak <71512463+NikodemNowak@users.noreply.github.com> 2026-09-14 18:15:45+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-14 09:15:45-07:00
log955b787e6b9fb8666e84abfa6868671859d72f7e
tree16f5862de03cffddf3038e726e7259443b10cb5a
parentae67c5b8159cb49a68c16f96f7bb64a2e3c6b1c6
signature Signed by PGP key B5690EEEBB952194

fix(server): parse CLI versions with a "v" prefix (#11738)


2 files changed, 31 insertions(+), 1 deletions(-)

apps/server/src/provider/providerSnapshot.test.ts+24
...@@ -11,6 +11,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";...@@ -11,6 +11,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1111
12import {12import {
13 isCommandMissingCause,13 isCommandMissingCause,
14 parseGenericCliVersion,
14 providerModelsFromSettings,15 providerModelsFromSettings,
15 spawnAndCollect,16 spawnAndCollect,
16} from "./providerSnapshot.ts";17} from "./providerSnapshot.ts";
...@@ -93,6 +94,29 @@ describe("providerModelsFromSettings", () => {...@@ -93,6 +94,29 @@ describe("providerModelsFromSettings", () => {
93 });94 });
94});95});
9596
97describe("parseGenericCliVersion", () => {
98 it("parses a bare version", () => {
99 expect(parseGenericCliVersion("1.14.19")).toBe("1.14.19");
100 });
101
102 it("parses a v-prefixed version", () => {
103 expect(parseGenericCliVersion("opencode v2.0.3")).toBe("2.0.3");
104 expect(parseGenericCliVersion("v22.19.0")).toBe("22.19.0");
105 });
106
107 it("parses a version embedded in other output", () => {
108 expect(parseGenericCliVersion("codex-cli 0.53.0 (build abc)")).toBe("0.53.0");
109 });
110
111 it("returns null when no version is present", () => {
112 expect(parseGenericCliVersion("no version here")).toBeNull();
113 });
114
115 it("ignores versions glued to other word characters", () => {
116 expect(parseGenericCliVersion("build2.0.3artifact")).toBeNull();
117 });
118});
119
96describe("ProviderCommandNotFoundError", () => {120describe("ProviderCommandNotFoundError", () => {
97 it("classifies normalized platform failures without parsing messages", () => {121 it("classifies normalized platform failures without parsing messages", () => {
98 expect(122 expect(
apps/server/src/provider/providerSnapshot.ts+7-1
...@@ -107,8 +107,14 @@ export const spawnAndCollect = (binaryPath: string, command: ChildProcess.Comman...@@ -107,8 +107,14 @@ export const spawnAndCollect = (binaryPath: string, command: ChildProcess.Comman
107 return result;107 return result;
108 }).pipe(Effect.scoped);108 }).pipe(Effect.scoped);
109109
110/**
111 * Return the first semantic version found in CLI output, or null. Accepts a
112 * leading "v" (for example `opencode v2.0.3`).
113 */
110export function parseGenericCliVersion(output: string): string | null {114export function parseGenericCliVersion(output: string): string | null {
111 const match = output.match(/\b(\d+\.\d+\.\d+)\b/);115 // "opencode v2.0.3"-style output: the optional "v" has to be consumed first,
116 // since "v2" itself contains no word boundary.
117 const match = output.match(/\bv?(\d+\.\d+\.\d+)\b/);
112 return match?.[1] ?? null;118 return match?.[1] ?? null;
113}119}
114120