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<string>;} 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.`;}
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<string>;
}
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.`;
}