Skip to content

Commit 5c4dd80

Browse files
yuWormclaude
andcommitted
feat: add async npm update check on CLI startup
Non-blocking version check against npm registry with 4-hour cache, displays update notification on exit when a newer version is available. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6d7bd79 commit 5c4dd80

File tree

3 files changed

+107
-0
lines changed

3 files changed

+107
-0
lines changed

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Command } from 'commander'
77
import chalk from 'chalk'
88
import { readGlobalConfig, readProjectConfig, resolveProjectDir } from './lib/config.js'
99
import { initI18nFromConfig, t } from './lib/i18n.js'
10+
import { checkForUpdate } from './lib/update-check.js'
1011

1112
const currentDir = dirname(fileURLToPath(import.meta.url))
1213
const packageJsonPath = resolve(currentDir, '..', 'package.json')
@@ -262,5 +263,8 @@ configCmd
262263
await configSetAction()
263264
})
264265

266+
// ─── Update check (async, non-blocking) ───
267+
checkForUpdate(cliVersion)
268+
265269
// ─── Run ───
266270
program.parse()

src/lib/i18n.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,10 @@ const messages = {
275275
registryDefault: "官方源 (未配置)",
276276
registrySetupTitle: "配置包管理器镜像源",
277277

278+
// Update check
279+
updateAvailable: "发现新版本: {current} → {latest}",
280+
updateHint: "运行 npm i -g @fba/cli 更新",
281+
278282
// Edit
279283
editOpening: "正在使用编辑器打开",
280284

@@ -579,6 +583,10 @@ const messages = {
579583
registryDefault: "Official (not configured)",
580584
registrySetupTitle: "Set up package manager registries",
581585

586+
// Update check
587+
updateAvailable: "New version available: {current} → {latest}",
588+
updateHint: "Run npm i -g @fba/cli to update",
589+
582590
editOpening: "Opening with editor",
583591

584592
// CLI Help

src/lib/update-check.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// update-check — 异步检查 npm 上的最新版本,不阻塞 CLI 正常使用
2+
import { readFileSync, writeFileSync, mkdirSync } from 'fs'
3+
import { join } from 'path'
4+
import { homedir } from 'os'
5+
import chalk from 'chalk'
6+
import { t } from './i18n.js'
7+
8+
const CACHE_DIR = join(homedir(), '.fba-cli')
9+
const CACHE_FILE = join(CACHE_DIR, 'update-check.json')
10+
const CHECK_INTERVAL = 1000 * 60 * 60 * 4 // 4 hours
11+
const PACKAGE_NAME = '@fba/cli'
12+
13+
interface CacheData {
14+
latest: string
15+
checkedAt: number
16+
}
17+
18+
function readCache(): CacheData | null {
19+
try {
20+
return JSON.parse(readFileSync(CACHE_FILE, 'utf-8')) as CacheData
21+
} catch {
22+
return null
23+
}
24+
}
25+
26+
function writeCache(data: CacheData) {
27+
try {
28+
mkdirSync(CACHE_DIR, { recursive: true })
29+
writeFileSync(CACHE_FILE, JSON.stringify(data))
30+
} catch {
31+
// ignore
32+
}
33+
}
34+
35+
async function fetchLatestVersion(): Promise<string | null> {
36+
try {
37+
const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
38+
signal: AbortSignal.timeout(5000),
39+
})
40+
if (!res.ok) return null
41+
const data = (await res.json()) as { version?: string }
42+
return data.version ?? null
43+
} catch {
44+
return null
45+
}
46+
}
47+
48+
function compareVersions(current: string, latest: string): boolean {
49+
const parse = (v: string) => v.split('.').map(Number)
50+
const c = parse(current)
51+
const l = parse(latest)
52+
for (let i = 0; i < 3; i++) {
53+
if ((l[i] ?? 0) > (c[i] ?? 0)) return true
54+
if ((l[i] ?? 0) < (c[i] ?? 0)) return false
55+
}
56+
return false
57+
}
58+
59+
function printNotification(current: string, latest: string) {
60+
const message = t('updateAvailable')
61+
.replace('{current}', current)
62+
.replace('{latest}', latest)
63+
const hint = t('updateHint')
64+
const box =
65+
`\n${chalk.yellow('┌──────────────────────────────────────────┐')}` +
66+
`\n${chalk.yellow('│')} ${message.padEnd(40)}${chalk.yellow('│')}` +
67+
`\n${chalk.yellow('│')} ${hint.padEnd(40)}${chalk.yellow('│')}` +
68+
`\n${chalk.yellow('└──────────────────────────────────────────┘')}\n`
69+
console.error(box)
70+
}
71+
72+
/**
73+
* 异步检查更新。在 CLI 启动时调用,完全不阻塞。
74+
* 如果有缓存命中则同步注册 exit 回调打印通知;
75+
* 否则发起网络请求,请求完成后打印通知(如果 CLI 还没退出的话)。
76+
*/
77+
export function checkForUpdate(currentVersion: string): void {
78+
// 先检查缓存
79+
const cache = readCache()
80+
if (cache && Date.now() - cache.checkedAt < CHECK_INTERVAL) {
81+
if (compareVersions(currentVersion, cache.latest)) {
82+
process.on('exit', () => printNotification(currentVersion, cache.latest))
83+
}
84+
return
85+
}
86+
87+
// 异步获取最新版本,请求完成后注册 exit 钩子
88+
fetchLatestVersion().then((latest) => {
89+
if (!latest) return
90+
writeCache({ latest, checkedAt: Date.now() })
91+
if (compareVersions(currentVersion, latest)) {
92+
process.on('exit', () => printNotification(currentVersion, latest))
93+
}
94+
})
95+
}

0 commit comments

Comments
 (0)