Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions src/__tests__/cc-agents-discovery-4028.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);
});
});

Expand Down
36 changes: 13 additions & 23 deletions src/commands/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -418,6 +396,18 @@ export async function handleUpdate(argv: string[], io: CliIO): Promise<number> {
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
// ---------------------------------------------------------------------------
Expand Down
29 changes: 5 additions & 24 deletions src/runners/cc-agents-discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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: [],
Expand Down
15 changes: 14 additions & 1 deletion src/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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])];
}
Expand All @@ -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+)/);
Expand Down
Loading