feat: add auto update cli

This commit is contained in:
雷骏
2026-06-24 15:31:24 +08:00
parent 60c49ec1ac
commit ba78d13a52
2 changed files with 128 additions and 7 deletions
+20 -7
View File
@@ -10,7 +10,12 @@ import {
import { ensureApiKey } from "./utils/ensure-key.ts";
import { setupProxyFromEnv } from "./proxy.ts";
import { handleError } from "./error-handler.ts";
import { checkForUpdate, getPendingUpdateNotification } from "./utils/update-checker.ts";
import {
checkForUpdate,
getPendingUpdateNotification,
isMajorUpgrade,
performAutoUpdate,
} from "./utils/update-checker.ts";
import { maybeShowStatusBar } from "./output/status-bar.ts";
import { printWelcomeBanner, printQuickStart } from "./output/banner.ts";
import { CLI_VERSION } from "./version.ts";
@@ -129,12 +134,20 @@ async function main() {
const isUpdateCommand = commandPath.length === 1 && commandPath[0] === "update";
const newVersion = getPendingUpdateNotification();
if (newVersion && !config.quiet && !isUpdateCommand) {
const isTTY = process.stderr.isTTY;
const yellow = isTTY ? "\x1b[33m" : "";
const cyan = isTTY ? "\x1b[36m" : "";
const reset = isTTY ? "\x1b[0m" : "";
process.stderr.write(`\n ${yellow}Update available: ${CLI_VERSION} → ${newVersion}${reset}\n`);
process.stderr.write(` Run ${cyan}bl update${reset} to upgrade\n\n`);
if (isMajorUpgrade(newVersion, CLI_VERSION)) {
// 大版本差距,自动更新
await performAutoUpdate(CLI_VERSION, newVersion);
} else {
// 普通小版本提示
const isTTY = process.stderr.isTTY;
const yellow = isTTY ? "\x1b[33m" : "";
const cyan = isTTY ? "\x1b[36m" : "";
const reset = isTTY ? "\x1b[0m" : "";
process.stderr.write(
`\n ${yellow}Update available: ${CLI_VERSION} → ${newVersion}${reset}\n`,
);
process.stderr.write(` Run ${cyan}bl update${reset} to upgrade\n\n`);
}
}
// 进程退出前尽力等待在途的埋点完成。
+108
View File
@@ -71,6 +71,114 @@ export function getPendingUpdateNotification(): string | null {
return pendingNotification;
}
/**
* Determines if the version gap is large enough to warrant auto-update.
* Conditions (either triggers auto-update):
* 1. New major > current major
* 2. Same major, but new minor - current minor > 3
*/
export function isMajorUpgrade(latest: string, current: string): boolean {
const [latestMajor, latestMinor] = latest.split(".").map(Number);
const [currentMajor, currentMinor] = current.split(".").map(Number);
// Condition 1: major version bump
if (latestMajor > currentMajor) return true;
// Condition 2: same major, minor gap > 3
if (latestMajor === currentMajor && latestMinor - currentMinor > 3) return true;
return false;
}
/**
* Perform auto-update: install latest version globally and update agent skill.
* Returns true if update succeeded, false otherwise.
*/
export async function performAutoUpdate(
currentVersion: string,
latestVersion: string,
): Promise<boolean> {
const isTTY = process.stderr.isTTY;
const green = isTTY ? "\x1b[32m" : "";
const yellow = isTTY ? "\x1b[33m" : "";
const cyan = isTTY ? "\x1b[36m" : "";
const dim = isTTY ? "\x1b[2m" : "";
const reset = isTTY ? "\x1b[0m" : "";
const [latestMajor] = latestVersion.split(".").map(Number);
const [currentMajor] = currentVersion.split(".").map(Number);
const isMajorBump = latestMajor > currentMajor;
process.stderr.write("\n");
process.stderr.write(` ${yellow}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${reset}\n`);
if (isMajorBump) {
process.stderr.write(
` ${yellow}⚡ Major update detected: ${currentVersion} → ${latestVersion}${reset}\n`,
);
} else {
process.stderr.write(
` ${yellow}⚡ Significant update detected: ${currentVersion} → ${latestVersion}${reset}\n`,
);
}
process.stderr.write(` ${dim}Auto-updating to keep your CLI up to date...${reset}\n`);
process.stderr.write(` ${yellow}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${reset}\n\n`);
const cmd = `npm install -g ${NPM_PACKAGE}@latest`;
try {
const { execSync } = await import("child_process");
execSync(cmd, { stdio: "inherit" });
// Verify installed version
let newVer: string | null = null;
try {
const rawVer = execSync("bl --version 2>/dev/null", { encoding: "utf-8" }).trim();
newVer = rawVer.replace(/^bl\s+/, "");
} catch {
/* ignore */
}
// Update cached state
try {
const { writeFileSync } = await import("fs");
const { join } = await import("path");
const { getConfigDir } = await import("bailian-cli-core");
const stateFile = join(getConfigDir(), "update-state.json");
writeFileSync(
stateFile,
JSON.stringify({ lastChecked: Date.now(), latestVersion: newVer ?? latestVersion }),
);
} catch {
/* ignore */
}
process.stderr.write(
` ${green}✓ Update complete: ${currentVersion} → ${newVer ?? latestVersion}${reset}\n`,
);
process.stderr.write(` ${dim}Run ${cyan}bl --version${reset}${dim} to verify.${reset}\n\n`);
// Update agent skill
try {
const { execSync: exec } = await import("child_process");
process.stderr.write(` ${dim}Syncing agent skill...${reset}\n`);
exec(`npx skills add modelstudioai/cli --all -g -y`, { stdio: "inherit" });
process.stderr.write(` ${green}✓ Agent skill updated.${reset}\n\n`);
} catch {
process.stderr.write(
` ${yellow}Agent skill sync skipped (run manually: npx skills add modelstudioai/cli --all -g -y)${reset}\n\n`,
);
}
// Clear pending notification
pendingNotification = null;
return true;
} catch {
process.stderr.write(` ${yellow}⚠ Auto-update failed. Please run manually:${reset}\n`);
process.stderr.write(` ${cyan}${cmd}${reset}\n\n`);
return false;
}
}
export async function checkForUpdate(currentVersion: string): Promise<void> {
// Skip in CI / non-TTY environments
if (process.env.CI || !process.stderr.isTTY) return;