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
This commit is contained in:
Saoud Rizwan
2026-02-02 11:59:07 -08:00
committed by GitHub
parent 24033613cd
commit bf83b816e2
+77 -18
View File
@@ -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<string | null> {
async function getLatestVersion(currentVersion: string): Promise<string | null> {
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<void> {
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
}