const CODEX_VERSION_PATTERN = /\bv?(\d+\.\d+(?:\.\d+)?(?:-[0-9A-Za-z.-]+)?)\b/; export const MINIMUM_CODEX_CLI_VERSION = "0.37.0"; interface ParsedSemver { readonly major: number; readonly minor: number; readonly patch: number; readonly prerelease: ReadonlyArray; } function normalizeCodexVersion(version: string): string { const [main, prerelease] = version.trim().split("-", 2); const segments = (main ?? "") .split(".") .map((segment) => segment.trim()) .filter((segment) => segment.length > 0); if (segments.length === 2) { segments.push("0"); } return prerelease ? `${segments.join(".")}-${prerelease}` : segments.join("."); } function parseSemver(version: string): ParsedSemver | null { const normalized = normalizeCodexVersion(version); const [main = "", prerelease] = normalized.split("-", 2); const segments = main.split("."); if (segments.length !== 3) { return null; } const [majorSegment, minorSegment, patchSegment] = segments; if (majorSegment === undefined || minorSegment === undefined || patchSegment === undefined) { return null; } const major = Number.parseInt(majorSegment, 10); const minor = Number.parseInt(minorSegment, 10); const patch = Number.parseInt(patchSegment, 10); if (![major, minor, patch].every(Number.isInteger)) { return null; } return { major, minor, patch, prerelease: prerelease ?.split(".") .map((segment) => segment.trim()) .filter((segment) => segment.length > 0) ?? [], }; } function comparePrereleaseIdentifier(left: string, right: string): number { const leftNumeric = /^\d+$/.test(left); const rightNumeric = /^\d+$/.test(right); if (leftNumeric && rightNumeric) { return Number.parseInt(left, 10) - Number.parseInt(right, 10); } if (leftNumeric) { return -1; } if (rightNumeric) { return 1; } return left.localeCompare(right); } export function compareCodexCliVersions(left: string, right: string): number { const parsedLeft = parseSemver(left); const parsedRight = parseSemver(right); if (!parsedLeft || !parsedRight) { return left.localeCompare(right); } if (parsedLeft.major !== parsedRight.major) { return parsedLeft.major - parsedRight.major; } if (parsedLeft.minor !== parsedRight.minor) { return parsedLeft.minor - parsedRight.minor; } if (parsedLeft.patch !== parsedRight.patch) { return parsedLeft.patch - parsedRight.patch; } if (parsedLeft.prerelease.length === 0 && parsedRight.prerelease.length === 0) { return 0; } if (parsedLeft.prerelease.length === 0) { return 1; } if (parsedRight.prerelease.length === 0) { return -1; } const length = Math.max(parsedLeft.prerelease.length, parsedRight.prerelease.length); for (let index = 0; index < length; index += 1) { const leftIdentifier = parsedLeft.prerelease[index]; const rightIdentifier = parsedRight.prerelease[index]; if (leftIdentifier === undefined) { return -1; } if (rightIdentifier === undefined) { return 1; } const comparison = comparePrereleaseIdentifier(leftIdentifier, rightIdentifier); if (comparison !== 0) { return comparison; } } return 0; } export function parseCodexCliVersion(output: string): string | null { const match = CODEX_VERSION_PATTERN.exec(output); if (!match?.[1]) { return null; } const parsed = parseSemver(match[1]); if (!parsed) { return null; } return normalizeCodexVersion(match[1]); } export function isCodexCliVersionSupported(version: string): boolean { return compareCodexCliVersions(version, MINIMUM_CODEX_CLI_VERSION) >= 0; } export function formatCodexCliUpgradeMessage(version: string | null): string { const versionLabel = version ? `v${version}` : "the installed version"; return `Codex CLI ${versionLabel} is too old for T3 Code. Upgrade to v${MINIMUM_CODEX_CLI_VERSION} or newer and restart T3 Code.`; }