From bf83b816e2a7d5a2aa74af80b8fd68afc883f1a3 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Mon, 2 Feb 2026 11:59:07 -0800 Subject: [PATCH] fix(cli): support auto-updates for nightly versions (#9034) * fix(cli): support auto-updates for nightly versions Previously, the auto-update logic only checked npm's "latest" tag, so users on nightly builds (2.0.0-nightly.X) would never receive nightly updates. The update commands also hardcoded @latest. Changes: - Detect nightly versions by checking for "-nightly." in version string - Query npm "nightly" tag when current version is a nightly build - Use @nightly in update commands for nightly users - Fix compareVersions() to properly parse and compare nightly timestamps (previously it would produce NaN when parsing "2.0.0-nightly.X") * fix: tighten nightly version regex to require valid semver format --- cli/src/utils/update.ts | 95 +++++++++++++++++++++++++++++++++-------- 1 file changed, 77 insertions(+), 18 deletions(-) diff --git a/cli/src/utils/update.ts b/cli/src/utils/update.ts index b54ee6702f..75a28cca01 100644 --- a/cli/src/utils/update.ts +++ b/cli/src/utils/update.ts @@ -19,9 +19,26 @@ interface InstallationInfo { } /** - * Detect how the CLI was installed and return the appropriate update command. + * Check if a version string is a nightly build. */ -function getInstallationInfo(): InstallationInfo { +function isNightlyVersion(version: string): boolean { + return version.includes("-nightly.") +} + +/** + * Get the npm tag to use based on the current version. + */ +function getNpmTag(currentVersion: string): string { + return isNightlyVersion(currentVersion) ? "nightly" : "latest" +} + +/** + * Detect how the CLI was installed and return the appropriate update command. + * Uses the correct npm tag based on whether the current version is nightly. + */ +function getInstallationInfo(currentVersion: string): InstallationInfo { + const tag = getNpmTag(currentVersion) + try { const scriptPath = realpathSync(process.argv[1] || "").replace(/\\/g, "/") @@ -34,7 +51,7 @@ function getInstallationInfo(): InstallationInfo { if (scriptPath.includes("/.pnpm/global") || scriptPath.includes("/pnpm/global")) { return { packageManager: PackageManager.PNPM, - updateCommand: "pnpm add -g cline@latest", + updateCommand: `pnpm add -g cline@${tag}`, } } @@ -42,7 +59,7 @@ function getInstallationInfo(): InstallationInfo { if (scriptPath.includes("/.yarn/") || scriptPath.includes("/yarn/global")) { return { packageManager: PackageManager.YARN, - updateCommand: "yarn global add cline@latest", + updateCommand: `yarn global add cline@${tag}`, } } @@ -50,7 +67,7 @@ function getInstallationInfo(): InstallationInfo { if (scriptPath.includes("/.bun/bin")) { return { packageManager: PackageManager.BUN, - updateCommand: "bun add -g cline@latest", + updateCommand: `bun add -g cline@${tag}`, } } @@ -58,7 +75,7 @@ function getInstallationInfo(): InstallationInfo { if (scriptPath.includes("/node_modules/cline/")) { return { packageManager: PackageManager.NPM, - updateCommand: "npm install -g cline@latest", + updateCommand: `npm install -g cline@${tag}`, } } } catch { @@ -70,10 +87,12 @@ function getInstallationInfo(): InstallationInfo { /** * Fetch the latest version from npm registry. + * Uses the appropriate tag based on whether the current version is nightly. */ -async function getLatestVersion(): Promise { +async function getLatestVersion(currentVersion: string): Promise { try { - const response = await fetch("https://registry.npmjs.org/cline/latest") + const tag = getNpmTag(currentVersion) + const response = await fetch(`https://registry.npmjs.org/cline/${tag}`) if (!response.ok) return null const data = (await response.json()) as { version: string } return data.version || null @@ -102,7 +121,7 @@ export function autoUpdateOnStartup(currentVersion: string): void { return } - const { updateCommand } = getInstallationInfo() + const { updateCommand } = getInstallationInfo(currentVersion) if (!updateCommand) { return } @@ -113,7 +132,7 @@ export function autoUpdateOnStartup(currentVersion: string): void { async function checkAndUpdate(currentVersion: string, updateCommand: string): Promise { try { - const latestVersion = await getLatestVersion() + const latestVersion = await getLatestVersion(currentVersion) if (!latestVersion) return // Only update if latest is newer @@ -138,10 +157,10 @@ async function checkAndUpdate(currentVersion: string, updateCommand: string): Pr export async function checkForUpdates(currentVersion: string, options?: { verbose?: boolean }) { printInfo("Checking for updates...") - const { updateCommand, packageManager } = getInstallationInfo() + const { updateCommand, packageManager } = getInstallationInfo(currentVersion) try { - const latestVersion = await getLatestVersion() + const latestVersion = await getLatestVersion(currentVersion) if (!latestVersion) { printWarning("Failed to check for updates: could not fetch latest version") exit(1) @@ -218,21 +237,61 @@ export async function checkForUpdates(currentVersion: string, options?: { verbos } } +interface ParsedVersion { + base: number[] + isNightly: boolean + timestamp: number +} + /** - * Compare two semantic version strings + * Parse a version string into its components. + * Handles both stable versions (2.0.0) and nightly versions (2.0.0-nightly.1736365200). + */ +function parseVersion(version: string): ParsedVersion { + const nightlyMatch = version.match(/^(\d+\.\d+\.\d+)-nightly\.(\d+)$/) + if (nightlyMatch) { + return { + base: nightlyMatch[1].split(".").map(Number), + isNightly: true, + timestamp: parseInt(nightlyMatch[2], 10), + } + } + return { + base: version.split(".").map(Number), + isNightly: false, + timestamp: 0, + } +} + +/** + * Compare two semantic version strings. + * Handles both stable versions and nightly versions. + * Nightly versions are compared by their timestamps. * Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal */ function compareVersions(v1: string, v2: string): number { - const parts1 = v1.split(".").map(Number) - const parts2 = v2.split(".").map(Number) + const p1 = parseVersion(v1) + const p2 = parseVersion(v2) - for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { - const part1 = parts1[i] || 0 - const part2 = parts2[i] || 0 + // Compare base versions first + for (let i = 0; i < Math.max(p1.base.length, p2.base.length); i++) { + const part1 = p1.base[i] || 0 + const part2 = p2.base[i] || 0 if (part1 > part2) return 1 if (part1 < part2) return -1 } + // Base versions are equal, check nightly status + // Nightly is considered less than stable (it's a pre-release) + if (p1.isNightly && !p2.isNightly) return -1 + if (!p1.isNightly && p2.isNightly) return 1 + + // Both are nightly, compare timestamps + if (p1.isNightly && p2.isNightly) { + if (p1.timestamp > p2.timestamp) return 1 + if (p1.timestamp < p2.timestamp) return -1 + } + return 0 }