From 455856a74529a8f66e8def9f068a4c71dc6b0ff2 Mon Sep 17 00:00:00 2001 From: OneStepAt4time Date: Mon, 1 Jun 2026 10:26:52 +0200 Subject: [PATCH] chore(#4525): deduplicate parseSemver/compareSemver across codebase - Update validation.ts parseSemver to accept optional 'v' prefix - Add compareSemverTuples to validation.ts for tuple-based comparison - Update cc-agents-discovery.ts to import from validation.ts - Update update.ts to import parseSemver from validation.ts - Update cc-agents-discovery-4028.test.ts imports Closes #4525 --- .../cc-agents-discovery-4028.test.ts | 11 +++--- src/commands/update.ts | 36 +++++++------------ src/runners/cc-agents-discovery.ts | 29 +++------------ src/validation.ts | 15 +++++++- 4 files changed, 37 insertions(+), 54 deletions(-) diff --git a/src/__tests__/cc-agents-discovery-4028.test.ts b/src/__tests__/cc-agents-discovery-4028.test.ts index 49f41a85e..1e265cb73 100644 --- a/src/__tests__/cc-agents-discovery-4028.test.ts +++ b/src/__tests__/cc-agents-discovery-4028.test.ts @@ -19,9 +19,8 @@ import { detectCcVersion, isCcAgentsJsonSupported, discoverCcAgents, - parseSemver, - compareSemver, } from '../runners/cc-agents-discovery.js'; +import { parseSemver, compareSemverTuples } from '../validation.js'; const mockExecFile = vi.mocked(execFile); @@ -42,19 +41,19 @@ describe('parseSemver', () => { describe('compareSemver', () => { it('compares equal versions', () => { - expect(compareSemver([2, 1, 145], [2, 1, 145])).toBe(0); + expect(compareSemverTuples([2, 1, 145], [2, 1, 145])).toBe(0); }); it('compares major difference', () => { - expect(compareSemver([3, 0, 0], [2, 1, 145])).toBeGreaterThan(0); + expect(compareSemverTuples([3, 0, 0], [2, 1, 145])).toBeGreaterThan(0); }); it('compares minor difference', () => { - expect(compareSemver([2, 0, 0], [2, 1, 145])).toBeLessThan(0); + expect(compareSemverTuples([2, 0, 0], [2, 1, 145])).toBeLessThan(0); }); it('compares patch difference', () => { - expect(compareSemver([2, 1, 146], [2, 1, 145])).toBeGreaterThan(0); + expect(compareSemverTuples([2, 1, 146], [2, 1, 145])).toBeGreaterThan(0); }); }); diff --git a/src/commands/update.ts b/src/commands/update.ts index 09ef6ba41..cfcfde861 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -12,6 +12,7 @@ import { createRequire } from 'node:module'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { parseSemver } from '../validation.js'; // --------------------------------------------------------------------------- // Version resolution @@ -29,29 +30,6 @@ function getCurrentVersion(): string { } } -// --------------------------------------------------------------------------- -// Semver comparison (lightweight — avoids adding a runtime dependency) -// --------------------------------------------------------------------------- - -/** Parse a semver string (with optional "v" prefix) into [major, minor, patch]. */ -function parseSemver(v: string): [number, number, number] | null { - const s = v.trim().replace(/^v/, ''); - const m = s.match(/^(\d+)\.(\d+)\.(\d+)/); - if (!m) return null; - return [parseInt(m[1]!, 10), parseInt(m[2]!, 10), parseInt(m[3]!, 10)]; -} - -/** Returns true when `latest` is strictly newer than `current`. */ -function isNewer(latest: string, current: string): boolean { - const l = parseSemver(latest); - const c = parseSemver(current); - if (!l || !c) return false; - for (let i = 0; i < 3; i++) { - if (l[i] !== c[i]) return l[i] > c[i]; - } - return false; -} - // --------------------------------------------------------------------------- // GitHub releases API // --------------------------------------------------------------------------- @@ -418,6 +396,18 @@ export async function handleUpdate(argv: string[], io: CliIO): Promise { return 0; } + +/** Returns true when `latest` is strictly newer than `current`. */ +function isNewer(latest: string, current: string): boolean { + const l = parseSemver(latest); + const c = parseSemver(current); + if (!l || !c) return false; + for (let i = 0; i < 3; i++) { + if (l[i] !== c[i]) return l[i] > c[i]; + } + return false; +} + // --------------------------------------------------------------------------- // Exports for testing // --------------------------------------------------------------------------- diff --git a/src/runners/cc-agents-discovery.ts b/src/runners/cc-agents-discovery.ts index 701cc0798..d53838dca 100644 --- a/src/runners/cc-agents-discovery.ts +++ b/src/runners/cc-agents-discovery.ts @@ -11,6 +11,7 @@ */ import { execFile } from 'node:child_process'; +import { parseSemver, compareSemverTuples } from '../validation.js'; import { promisify } from 'node:util'; const execFileAsync = promisify(execFile); @@ -64,28 +65,6 @@ export interface CcAgentsDiscoveryOptions { const DEFAULT_TIMEOUT_MS = 10_000; const MIN_CC_VERSION_FOR_AGENTS_JSON = '2.1.145'; -/** - * Parse a semver string into [major, minor, patch]. - * Returns null if the string is not valid semver. - */ -export function parseSemver(version: string): [number, number, number] | null { - const match = version.match(/^(\d+)\.(\d+)\.(\d+)/); - if (!match) return null; - return [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)]; -} - -/** - * Compare two semver tuples. Returns negative if a < b, 0 if equal, positive if a > b. - */ -export function compareSemver( - a: [number, number, number], - b: [number, number, number], -): number { - if (a[0] !== b[0]) return a[0] - b[0]; - if (a[1] !== b[1]) return a[1] - b[1]; - return a[2] - b[2]; -} - /** * Detect the installed CC version. * @@ -117,7 +96,7 @@ export async function isCcAgentsJsonSupported(claudePathOrVersion?: string, _cla const parsed = parseSemver(version); const minParsed = parseSemver(MIN_CC_VERSION_FOR_AGENTS_JSON); if (!parsed || !minParsed) return false; - return compareSemver(parsed, minParsed) >= 0; + return compareSemverTuples(parsed, minParsed) >= 0; } /** @@ -145,7 +124,9 @@ export async function discoverCcAgents( }; } - if (!await isCcAgentsJsonSupported(ccVersion, bin)) { + const parsed = parseSemver(ccVersion); + const minParsed = parseSemver(MIN_CC_VERSION_FOR_AGENTS_JSON); + if (!parsed || !minParsed || compareSemverTuples(parsed, minParsed) < 0) { return { available: false, sessions: [], diff --git a/src/validation.ts b/src/validation.ts index d5e67927c..450d2566c 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -629,7 +629,7 @@ export const MIN_CC_VERSION = '2.1.80'; /** Parse a semver string into [major, minor, patch], or null if invalid. */ export function parseSemver(v: string): [number, number, number] | null { - const match = v.trim().match(/^(\d+)\.(\d+)\.(\d+)/); + const match = v.trim().replace(/^v/, '').match(/^(\d+)\.(\d+)\.(\d+)/); if (!match) return null; return [Number(match[1]), Number(match[2]), Number(match[3])]; } @@ -649,6 +649,19 @@ export function compareSemver(a: string, b: string): number { return 0; } +/** + * Compare two semver tuples. + * Returns negative if a < b, 0 if equal, positive if a > b. + */ +export function compareSemverTuples( + a: [number, number, number], + b: [number, number, number], +): number { + if (a[0] !== b[0]) return a[0] - b[0]; + if (a[1] !== b[1]) return a[1] - b[1]; + return a[2] - b[2]; +} + /** Extract version number from `claude --version` output. */ export function extractCCVersion(output: string): string | null { const match = output.match(/(\d+\.\d+\.\d+)/);