import { KUBER_VERSION } from "../shared/version"; type SemVer = { major: bigint; minor: bigint; patch: bigint; prerelease: string[]; }; const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/; const NUMERIC = /^(0|[1-9]\d*)$/; const INSTALL_TIMEOUT_SECONDS = 30; const gray = (line: string) => `\x1b[90m${line}\x1b[0m\n`; const reportToStderr = (line: string) => process.stderr.write(line); function parseVersion(value: string | null): SemVer | undefined { if (!value || value.length > 128) return; const match = SEMVER.exec(value); if (!match || match[0] !== value) return; const prerelease = match[4]?.split(".") ?? []; if (prerelease.some((part) => /^\d+$/.test(part) && !NUMERIC.test(part))) return; return { major: BigInt(match[1]!), minor: BigInt(match[2]!), patch: BigInt(match[3]!), prerelease, }; } /** A positive result means candidate is newer; build metadata has no precedence. */ export function compareVersions( candidate: string, current: string, ): number | undefined { const a = parseVersion(candidate); const b = parseVersion(current); if (!a || !b) return; for (const field of ["major", "minor", "patch"] as const) { if (a[field] !== b[field]) return a[field] > b[field] ? 1 : -1; } if (!a.prerelease.length || !b.prerelease.length) { return Number(!a.prerelease.length) - Number(!b.prerelease.length); } for (let i = 0; i < Math.max(a.prerelease.length, b.prerelease.length); i++) { const left = a.prerelease[i]; const right = b.prerelease[i]; if (left === undefined || right === undefined) return left === undefined ? -1 : 1; if (left === right) continue; const leftNumeric = NUMERIC.test(left); const rightNumeric = NUMERIC.test(right); if (leftNumeric && rightNumeric) return BigInt(left) > BigInt(right) ? 1 : -1; if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1; return left < right ? -1 : 1; } return 0; } export type VersionInstallRunner = (version: string) => Promise; type InstallProcess = { exited: Promise; kill(signal?: NodeJS.Signals): void; }; type InstallSpawn = ( argv: string[], options: { stdin: "ignore"; stdout: "ignore"; stderr: "ignore"; env: NodeJS.ProcessEnv; }, ) => InstallProcess; export async function installVersion( version: string, spawn: InstallSpawn = Bun.spawn, timeoutSeconds = INSTALL_TIMEOUT_SECONDS, ): Promise { // Defense in depth: never pass an unvalidated header to a subprocess. if ( !parseVersion(version) || !Number.isSafeInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 300 ) return false; try { const child = spawn( [process.execPath, "i", "-g", "--no-cache", `@dmgnr/kuber@${version}`], { stdin: "ignore", stdout: "ignore", stderr: "ignore", env: process.env }, ); return await new Promise((resolve) => { // Allow a short grace period to reap a child that ignores termination. let timedOut = false; const deadline = setTimeout( () => { timedOut = true; try { child.kill(); } catch {} const force = setTimeout(() => { try { child.kill("SIGKILL"); } catch {} resolve(false); }, 1_000); void child.exited.finally(() => clearTimeout(force)).catch(() => {}); }, timeoutSeconds * 1_000 - 1_000, ); void child.exited.then( (code) => { clearTimeout(deadline); resolve(!timedOut && code === 0); }, () => { clearTimeout(deadline); resolve(false); }, ); }); } catch { return false; } } const pendingUpdates = new Set>(); /** Wait for an update observed during this CLI invocation, including its notice. */ export async function waitForVersionUpdate(): Promise { await Promise.all(pendingUpdates); } export function createVersionObserver({ currentVersion = KUBER_VERSION, runner = installVersion, report = reportToStderr, }: { currentVersion?: string; runner?: VersionInstallRunner; report?: (line: string) => void; } = {}): (version: string | null) => void { let attempted = false; return (version) => { if (attempted || !version || compareVersions(version, currentVersion) !== 1) return; attempted = true; // Defer install work beyond the response headers; never block body consumption. const pending = new Promise((resolve) => setTimeout(resolve, 0)).then( async () => { // The global observer must not install packages in tests or source-tree // development commands. Explicitly injected runners remain testable. if ( runner === installVersion && (process.env.NODE_ENV === "test" || process.env.NODE_ENV === "development" || process.argv[1]?.endsWith(".ts")) ) return; let success = false; try { success = await runner(version); } catch {} try { report( gray( `+ ${success ? "Updated to " : "New version available: "}${version}`, ), ); } catch { // A broken stderr must never affect an API request or command exit status. } }, ); pendingUpdates.add(pending); void pending.finally(() => pendingUpdates.delete(pending)); }; } export const observeServerVersion = createVersionObserver();