Skip to content

Commit 0686fb9

Browse files
author
huzijie.sea
committed
feat(版本检查): 添加启动时自动检查版本更新功能
添加版本检查服务 VersionChecker,实现以下功能: 1. 从 npm registry 获取最新版本 2. 本地缓存版本信息避免频繁请求 3. 启动时后台检查并提示更新 4. 提供手动更新命令
1 parent 28011d4 commit 0686fb9

3 files changed

Lines changed: 279 additions & 14 deletions

File tree

src/commands/update.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
* Update 命令 - Yargs 版本
33
*/
44

5+
import { execSync } from 'child_process';
56
import type { CommandModule } from 'yargs';
67
import type { UpdateOptions } from '../cli/types.js';
8+
import { checkVersion } from '../services/VersionChecker.js';
79

810
export const updateCommands: CommandModule<{}, UpdateOptions> = {
911
command: 'update',
@@ -12,23 +14,38 @@ export const updateCommands: CommandModule<{}, UpdateOptions> = {
1214
console.log('🔍 Checking for updates...');
1315

1416
try {
15-
// 读取当前版本
16-
const fs = await import('fs/promises');
17-
const path = await import('path');
18-
const packageJsonPath = path.join(process.cwd(), 'package.json');
19-
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
20-
const currentVersion = packageJson.version;
17+
const result = await checkVersion(true); // 强制检查,忽略缓存
2118

22-
console.log(`📦 Current version: ${currentVersion}`);
19+
console.log(`📦 Current version: ${result.currentVersion}`);
2320

24-
// 模拟检查更新(实际项目中应该检查 npm registry 或 GitHub releases)
25-
console.log('✅ You are running the latest version of Blade');
21+
if (result.error) {
22+
console.log(`⚠️ ${result.error}`);
23+
return;
24+
}
2625

27-
// 实际实现时可以添加:
28-
// 1. 检查 npm registry 的最新版本
29-
// 2. 比较版本号
30-
// 3. 如果有更新,提示用户或自动更新
31-
// 4. 显示更新日志
26+
if (result.latestVersion) {
27+
console.log(`📦 Latest version: ${result.latestVersion}`);
28+
}
29+
30+
if (result.hasUpdate && result.latestVersion) {
31+
console.log('');
32+
console.log(
33+
`\x1b[33m⚠️ Update available: ${result.currentVersion}${result.latestVersion}\x1b[0m`
34+
);
35+
console.log('');
36+
console.log('🚀 Updating...');
37+
try {
38+
execSync('npm install -g blade-code@latest', { stdio: 'inherit' });
39+
console.log('');
40+
console.log('✅ Update complete!');
41+
} catch (_errrr) {
42+
console.error('❌ Update failed. Please run manually:');
43+
console.error(' npm install -g blade-code@latest');
44+
process.exit(1);
45+
}
46+
} else {
47+
console.log('✅ You are running the latest version of Blade');
48+
}
3249
} catch (error) {
3350
console.error(
3451
`❌ Failed to check for updates: ${error instanceof Error ? error.message : '未知错误'}`

src/services/VersionChecker.ts

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
/**
2+
* 版本检查服务
3+
*
4+
* 启动时检查 npm registry 获取最新版本,提示用户更新
5+
*/
6+
7+
import * as fs from 'fs/promises';
8+
import * as path from 'path';
9+
import { fileURLToPath } from 'url';
10+
11+
// 包名
12+
const PACKAGE_NAME = 'blade-code';
13+
14+
// 缓存文件路径
15+
const CACHE_DIR = path.join(
16+
process.env.HOME || process.env.USERPROFILE || '.',
17+
'.blade'
18+
);
19+
const CACHE_FILE = path.join(CACHE_DIR, 'version-cache.json');
20+
21+
// 缓存有效期:24 小时
22+
const CACHE_TTL = 24 * 60 * 60 * 1000;
23+
24+
// npm registry URL
25+
const NPM_REGISTRY_URL = `https://registry.npmmirror.com/${PACKAGE_NAME}/latest`;
26+
27+
interface VersionCache {
28+
latestVersion: string;
29+
checkedAt: number;
30+
}
31+
32+
interface VersionCheckResult {
33+
currentVersion: string;
34+
latestVersion: string | null;
35+
hasUpdate: boolean;
36+
error?: string;
37+
}
38+
39+
/**
40+
* 获取当前安装的版本
41+
*/
42+
async function getCurrentVersion(): Promise<string> {
43+
try {
44+
// 方法1:从环境变量获取(构建时注入)
45+
if (process.env.BLADE_VERSION) {
46+
return process.env.BLADE_VERSION;
47+
}
48+
49+
// 方法2:从 package.json 获取
50+
// 获取当前模块的目录
51+
const __filename = fileURLToPath(import.meta.url);
52+
const __dirname = path.dirname(__filename);
53+
54+
// 尝试多个可能的 package.json 位置
55+
const possiblePaths = [
56+
path.join(__dirname, '..', '..', 'package.json'), // src/services -> root
57+
path.join(__dirname, '..', 'package.json'), // dist -> root
58+
path.join(process.cwd(), 'package.json'), // 当前工作目录
59+
];
60+
61+
for (const pkgPath of possiblePaths) {
62+
try {
63+
const content = await fs.readFile(pkgPath, 'utf-8');
64+
const pkg = JSON.parse(content);
65+
if (pkg.name === PACKAGE_NAME && pkg.version) {
66+
return pkg.version;
67+
}
68+
} catch {
69+
// 继续尝试下一个路径
70+
}
71+
}
72+
73+
return 'unknown';
74+
} catch {
75+
return 'unknown';
76+
}
77+
}
78+
79+
/**
80+
* 从缓存读取版本信息
81+
*/
82+
async function readCache(): Promise<VersionCache | null> {
83+
try {
84+
const content = await fs.readFile(CACHE_FILE, 'utf-8');
85+
const cache: VersionCache = JSON.parse(content);
86+
87+
// 检查缓存是否过期
88+
if (Date.now() - cache.checkedAt < CACHE_TTL) {
89+
return cache;
90+
}
91+
return null;
92+
} catch {
93+
return null;
94+
}
95+
}
96+
97+
/**
98+
* 写入缓存
99+
*/
100+
async function writeCache(cache: VersionCache): Promise<void> {
101+
try {
102+
await fs.mkdir(CACHE_DIR, { recursive: true });
103+
await fs.writeFile(CACHE_FILE, JSON.stringify(cache, null, 2));
104+
} catch {
105+
// 静默失败
106+
}
107+
}
108+
109+
/**
110+
* 从 npm registry 获取最新版本
111+
*/
112+
async function fetchLatestVersion(): Promise<string | null> {
113+
try {
114+
const controller = new AbortController();
115+
const timeout = setTimeout(() => controller.abort(), 5000); // 5 秒超时
116+
117+
const response = await fetch(NPM_REGISTRY_URL, {
118+
signal: controller.signal,
119+
headers: {
120+
Accept: 'application/json',
121+
},
122+
});
123+
124+
clearTimeout(timeout);
125+
126+
if (!response.ok) {
127+
return null;
128+
}
129+
130+
const data = await response.json();
131+
return data.version || null;
132+
} catch {
133+
return null;
134+
}
135+
}
136+
137+
/**
138+
* 比较版本号
139+
* 返回: 1 如果 a > b, -1 如果 a < b, 0 如果相等
140+
*/
141+
function compareVersions(a: string, b: string): number {
142+
const partsA = a.split('.').map(Number);
143+
const partsB = b.split('.').map(Number);
144+
145+
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
146+
const numA = partsA[i] || 0;
147+
const numB = partsB[i] || 0;
148+
149+
if (numA > numB) return 1;
150+
if (numA < numB) return -1;
151+
}
152+
153+
return 0;
154+
}
155+
156+
/**
157+
* 检查版本更新
158+
*
159+
* @param forceCheck - 强制检查(忽略缓存)
160+
* @returns 版本检查结果
161+
*/
162+
export async function checkVersion(forceCheck = false): Promise<VersionCheckResult> {
163+
const currentVersion = await getCurrentVersion();
164+
165+
// 如果无法获取当前版本,跳过检查
166+
if (currentVersion === 'unknown') {
167+
return {
168+
currentVersion,
169+
latestVersion: null,
170+
hasUpdate: false,
171+
error: 'Unable to determine current version',
172+
};
173+
}
174+
175+
// 尝试从缓存读取
176+
if (!forceCheck) {
177+
const cache = await readCache();
178+
if (cache) {
179+
return {
180+
currentVersion,
181+
latestVersion: cache.latestVersion,
182+
hasUpdate: compareVersions(cache.latestVersion, currentVersion) > 0,
183+
};
184+
}
185+
}
186+
187+
// 从 npm 获取最新版本
188+
const latestVersion = await fetchLatestVersion();
189+
190+
if (latestVersion) {
191+
// 更新缓存
192+
await writeCache({
193+
latestVersion,
194+
checkedAt: Date.now(),
195+
});
196+
197+
return {
198+
currentVersion,
199+
latestVersion,
200+
hasUpdate: compareVersions(latestVersion, currentVersion) > 0,
201+
};
202+
}
203+
204+
return {
205+
currentVersion,
206+
latestVersion: null,
207+
hasUpdate: false,
208+
error: 'Unable to check for updates',
209+
};
210+
}
211+
212+
/**
213+
* 格式化版本更新提示消息
214+
*/
215+
export function formatUpdateMessage(result: VersionCheckResult): string | null {
216+
if (!result.hasUpdate || !result.latestVersion) {
217+
return null;
218+
}
219+
220+
return (
221+
`\x1b[33m⚠️ Update available: ${result.currentVersion}${result.latestVersion}\x1b[0m\n` +
222+
` Run \x1b[36mnpm install -g ${PACKAGE_NAME}@latest\x1b[0m to update`
223+
);
224+
}
225+
226+
/**
227+
* 启动时版本检查(后台执行,不阻塞)
228+
*
229+
* @returns Promise<string | null> 更新提示消息,如果没有更新则返回 null
230+
*/
231+
export async function checkVersionOnStartup(): Promise<string | null> {
232+
try {
233+
const result = await checkVersion();
234+
return formatUpdateMessage(result);
235+
} catch {
236+
return null;
237+
}
238+
}

src/ui/App.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from '../config/index.js';
1111
import { HookManager } from '../hooks/HookManager.js';
1212
import { Logger } from '../logging/Logger.js';
13+
import { checkVersionOnStartup } from '../services/VersionChecker.js';
1314
import { appActions, getState } from '../store/vanilla.js';
1415
import { BladeInterface } from './components/BladeInterface.js';
1516
import { ErrorBoundary } from './components/ErrorBoundary.js';
@@ -122,6 +123,15 @@ export const AppWrapper: React.FC<AppProps> = (props) => {
122123
}
123124
}
124125

126+
// 8. 后台检查版本更新(不阻塞启动)
127+
checkVersionOnStartup().then((updateMessage) => {
128+
if (updateMessage) {
129+
console.log('');
130+
console.log(updateMessage);
131+
console.log('');
132+
}
133+
});
134+
125135
setIsInitialized(true);
126136
} catch (error) {
127137
// 静默失败,使用默认配置

0 commit comments

Comments
 (0)