fe-build-cli 1.2.5 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +310 -11
- package/package.json +1 -1
- package/src/cli.js +249 -20
- package/src/config-template.js +14 -1
- package/src/deploy-core.js +310 -64
- package/src/dingtalk.js +31 -17
- package/src/git-branch.js +191 -34
- package/src/index.js +22 -1
- package/src/logger.js +381 -0
- package/src/ssh-client.js +69 -0
- package/src/update.js +136 -0
package/src/logger.js
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 日志记录模块
|
|
7
|
+
* 记录部署过程中的每一步操作及状态
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 日志记录器类
|
|
12
|
+
*/
|
|
13
|
+
export class DeployLogger {
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.logDir = options.logDir || 'logs';
|
|
16
|
+
this.backupDir = options.backupDir || 'D:\\备份';
|
|
17
|
+
this.logs = [];
|
|
18
|
+
this.startTime = null;
|
|
19
|
+
this.endTime = null;
|
|
20
|
+
this.hasError = false;
|
|
21
|
+
this.errorLog = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 开始记录
|
|
26
|
+
*/
|
|
27
|
+
start() {
|
|
28
|
+
this.startTime = new Date();
|
|
29
|
+
this.logs = [];
|
|
30
|
+
this.hasError = false;
|
|
31
|
+
this.log('INFO', '部署开始', `开始时间: ${this.formatTime(this.startTime)}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 结束记录
|
|
36
|
+
*/
|
|
37
|
+
end(status = 'success') {
|
|
38
|
+
this.endTime = new Date();
|
|
39
|
+
const duration = Math.round((this.endTime - this.startTime) / 1000);
|
|
40
|
+
this.log('INFO', '部署结束', `结束时间: ${this.formatTime(this.endTime)}, 总耗时: ${duration}秒, 状态: ${status}`);
|
|
41
|
+
|
|
42
|
+
// 保存日志文件
|
|
43
|
+
this.saveLog();
|
|
44
|
+
|
|
45
|
+
// 如果有错误,单独保存错误日志
|
|
46
|
+
if (this.hasError) {
|
|
47
|
+
this.saveErrorLog();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 记录日志
|
|
53
|
+
* @param {string} level - 日志级别: INFO, SUCCESS, ERROR, WARN
|
|
54
|
+
* @param {string} step - 操作步骤名称
|
|
55
|
+
* @param {string} message - 详细信息
|
|
56
|
+
* @param {object} data - 附加数据
|
|
57
|
+
*/
|
|
58
|
+
log(level, step, message, data = null) {
|
|
59
|
+
const timestamp = new Date();
|
|
60
|
+
const logEntry = {
|
|
61
|
+
timestamp: this.formatTime(timestamp),
|
|
62
|
+
level,
|
|
63
|
+
step,
|
|
64
|
+
message,
|
|
65
|
+
data,
|
|
66
|
+
success: level !== 'ERROR'
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
this.logs.push(logEntry);
|
|
70
|
+
|
|
71
|
+
// 如果是错误,标记
|
|
72
|
+
if (level === 'ERROR') {
|
|
73
|
+
this.hasError = true;
|
|
74
|
+
this.errorLog = logEntry;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 同时输出到控制台
|
|
78
|
+
const prefix = this.getLevelPrefix(level);
|
|
79
|
+
console.log(`${prefix} [${step}] ${message}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 获取日志级别前缀
|
|
84
|
+
*/
|
|
85
|
+
getLevelPrefix(level) {
|
|
86
|
+
switch (level) {
|
|
87
|
+
case 'INFO':
|
|
88
|
+
return '📋';
|
|
89
|
+
case 'SUCCESS':
|
|
90
|
+
return '✅';
|
|
91
|
+
case 'ERROR':
|
|
92
|
+
return '❌';
|
|
93
|
+
case 'WARN':
|
|
94
|
+
return '⚠️';
|
|
95
|
+
default:
|
|
96
|
+
return '📝';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 格式化时间
|
|
102
|
+
*/
|
|
103
|
+
formatTime(date) {
|
|
104
|
+
return date.toLocaleString('zh-CN', {
|
|
105
|
+
year: 'numeric',
|
|
106
|
+
month: '2-digit',
|
|
107
|
+
day: '2-digit',
|
|
108
|
+
hour: '2-digit',
|
|
109
|
+
minute: '2-digit',
|
|
110
|
+
second: '2-digit'
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 记录分支操作
|
|
116
|
+
*/
|
|
117
|
+
logBranch(action, fromBranch, toBranch, success, message = '') {
|
|
118
|
+
this.log(
|
|
119
|
+
success ? 'SUCCESS' : 'ERROR',
|
|
120
|
+
'分支操作',
|
|
121
|
+
`${action}: ${fromBranch} → ${toBranch}, ${message || (success ? '成功' : '失败')}`,
|
|
122
|
+
{ action, fromBranch, toBranch }
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 记录合并操作
|
|
128
|
+
*/
|
|
129
|
+
logMerge(sourceBranch, targetBranch, success, hasConflict = false) {
|
|
130
|
+
const message = hasConflict
|
|
131
|
+
? `合并冲突: ${sourceBranch} → ${targetBranch}`
|
|
132
|
+
: `合并: ${sourceBranch} → ${targetBranch}, ${success ? '成功' : '失败'}`;
|
|
133
|
+
this.log(
|
|
134
|
+
success ? 'SUCCESS' : 'ERROR',
|
|
135
|
+
'代码合并',
|
|
136
|
+
message,
|
|
137
|
+
{ sourceBranch, targetBranch, hasConflict }
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 记录 stash 操作
|
|
143
|
+
*/
|
|
144
|
+
logStash(action, success, message = '') {
|
|
145
|
+
this.log(
|
|
146
|
+
success ? 'SUCCESS' : 'ERROR',
|
|
147
|
+
'Stash操作',
|
|
148
|
+
`${action}: ${message || (success ? '成功' : '失败')}`,
|
|
149
|
+
{ action }
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 记录构建操作
|
|
155
|
+
*/
|
|
156
|
+
logBuild(buildMode, buildVersion, success, duration = 0) {
|
|
157
|
+
this.log(
|
|
158
|
+
success ? 'SUCCESS' : 'ERROR',
|
|
159
|
+
'项目构建',
|
|
160
|
+
`构建模式: ${buildMode}, 版本: ${buildVersion}, 耗时: ${duration}秒, ${success ? '成功' : '失败'}`,
|
|
161
|
+
{ buildMode, buildVersion, duration }
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 记录压缩操作
|
|
167
|
+
*/
|
|
168
|
+
logCompress(fileSize, success) {
|
|
169
|
+
this.log(
|
|
170
|
+
success ? 'SUCCESS' : 'ERROR',
|
|
171
|
+
'文件压缩',
|
|
172
|
+
`压缩包大小: ${this.formatFileSize(fileSize)}, ${success ? '成功' : '失败'}`,
|
|
173
|
+
{ fileSize }
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 记录 SSH 连接
|
|
179
|
+
*/
|
|
180
|
+
logSSHConnect(host, success) {
|
|
181
|
+
this.log(
|
|
182
|
+
success ? 'SUCCESS' : 'ERROR',
|
|
183
|
+
'SSH连接',
|
|
184
|
+
`服务器: ${host}, ${success ? '连接成功' : '连接失败'}`,
|
|
185
|
+
{ host }
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 记录上传操作
|
|
191
|
+
*/
|
|
192
|
+
logUpload(localFile, remoteFile, fileSize, duration, success) {
|
|
193
|
+
const speed = duration > 0 ? Math.round(fileSize / duration) : 0;
|
|
194
|
+
this.log(
|
|
195
|
+
success ? 'SUCCESS' : 'ERROR',
|
|
196
|
+
'文件上传',
|
|
197
|
+
`本地: ${localFile}, 远程: ${remoteFile}, 大小: ${this.formatFileSize(fileSize)}, 耗时: ${duration}秒, 速度: ${this.formatFileSize(speed)}/s`,
|
|
198
|
+
{ localFile, remoteFile, fileSize, duration, speed }
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 记录备份操作
|
|
204
|
+
*/
|
|
205
|
+
logBackup(backupFile, success, isDownload = false) {
|
|
206
|
+
const action = isDownload ? '备份下载' : '服务器备份';
|
|
207
|
+
this.log(
|
|
208
|
+
success ? 'SUCCESS' : 'ERROR',
|
|
209
|
+
action,
|
|
210
|
+
`备份文件: ${backupFile}, ${success ? '成功' : '失败'}`,
|
|
211
|
+
{ backupFile, isDownload }
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 记录部署操作
|
|
217
|
+
*/
|
|
218
|
+
logDeploy(deployDir, success) {
|
|
219
|
+
this.log(
|
|
220
|
+
success ? 'SUCCESS' : 'ERROR',
|
|
221
|
+
'部署解压',
|
|
222
|
+
`部署目录: ${deployDir}, ${success ? '成功' : '失败'}`,
|
|
223
|
+
{ deployDir }
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 记录钉钉通知
|
|
229
|
+
*/
|
|
230
|
+
logDingTalk(success, message = '') {
|
|
231
|
+
this.log(
|
|
232
|
+
success ? 'SUCCESS' : 'WARN',
|
|
233
|
+
'钉钉通知',
|
|
234
|
+
`${success ? '发送成功' : '发送失败'}: ${message}`,
|
|
235
|
+
{ success }
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 格式化文件大小
|
|
241
|
+
*/
|
|
242
|
+
formatFileSize(bytes) {
|
|
243
|
+
if (bytes < 1024) return bytes + ' B';
|
|
244
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
|
|
245
|
+
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 保存日志文件
|
|
250
|
+
*/
|
|
251
|
+
saveLog() {
|
|
252
|
+
// 确保日志目录存在
|
|
253
|
+
if (!fs.existsSync(this.logDir)) {
|
|
254
|
+
fs.mkdirSync(this.logDir, { recursive: true });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 生成日志文件名
|
|
258
|
+
const dateStr = this.startTime.toISOString().slice(0, 10);
|
|
259
|
+
const timeStr = this.startTime.toISOString().slice(11, 19).replace(/:/g, '-');
|
|
260
|
+
const status = this.hasError ? 'failed' : 'success';
|
|
261
|
+
const logFileName = `deploy-${dateStr}-${timeStr}-${status}.json`;
|
|
262
|
+
const logFilePath = path.join(this.logDir, logFileName);
|
|
263
|
+
|
|
264
|
+
// 构建完整日志对象
|
|
265
|
+
const fullLog = {
|
|
266
|
+
summary: {
|
|
267
|
+
startTime: this.formatTime(this.startTime),
|
|
268
|
+
endTime: this.formatTime(this.endTime),
|
|
269
|
+
duration: Math.round((this.endTime - this.startTime) / 1000),
|
|
270
|
+
status: this.hasError ? 'failed' : 'success',
|
|
271
|
+
totalSteps: this.logs.length,
|
|
272
|
+
successSteps: this.logs.filter(l => l.success).length,
|
|
273
|
+
failedSteps: this.logs.filter(l => !l.success).length
|
|
274
|
+
},
|
|
275
|
+
logs: this.logs
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// 保存日志
|
|
279
|
+
fs.writeFileSync(logFilePath, JSON.stringify(fullLog, null, 2));
|
|
280
|
+
console.log(`\n📝 日志已保存: ${logFilePath}`);
|
|
281
|
+
|
|
282
|
+
return logFilePath;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 保存错误日志(单独一份)
|
|
287
|
+
*/
|
|
288
|
+
saveErrorLog() {
|
|
289
|
+
if (!this.hasError) return;
|
|
290
|
+
|
|
291
|
+
// 确保日志目录存在
|
|
292
|
+
if (!fs.existsSync(this.logDir)) {
|
|
293
|
+
fs.mkdirSync(this.logDir, { recursive: true });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 生成错误日志文件名
|
|
297
|
+
const dateStr = this.startTime.toISOString().slice(0, 10);
|
|
298
|
+
const timeStr = this.startTime.toISOString().slice(11, 19).replace(/:/g, '-');
|
|
299
|
+
const errorLogFileName = `error-${dateStr}-${timeStr}.json`;
|
|
300
|
+
const errorLogFilePath = path.join(this.logDir, errorLogFileName);
|
|
301
|
+
|
|
302
|
+
// 构建错误日志对象
|
|
303
|
+
const errorLogData = {
|
|
304
|
+
summary: {
|
|
305
|
+
startTime: this.formatTime(this.startTime),
|
|
306
|
+
endTime: this.formatTime(this.endTime),
|
|
307
|
+
status: 'failed'
|
|
308
|
+
},
|
|
309
|
+
error: this.errorLog,
|
|
310
|
+
allLogs: this.logs
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// 保存错误日志
|
|
314
|
+
fs.writeFileSync(errorLogFilePath, JSON.stringify(errorLogData, null, 2));
|
|
315
|
+
console.log(`\n❌ 错误日志已保存: ${errorLogFilePath}`);
|
|
316
|
+
|
|
317
|
+
return errorLogFilePath;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* 获取日志摘要
|
|
322
|
+
*/
|
|
323
|
+
getSummary() {
|
|
324
|
+
return {
|
|
325
|
+
startTime: this.formatTime(this.startTime),
|
|
326
|
+
endTime: this.formatTime(this.endTime),
|
|
327
|
+
duration: Math.round((this.endTime - this.startTime) / 1000),
|
|
328
|
+
status: this.hasError ? 'failed' : 'success',
|
|
329
|
+
totalSteps: this.logs.length,
|
|
330
|
+
successSteps: this.logs.filter(l => l.success).length,
|
|
331
|
+
failedSteps: this.logs.filter(l => !l.success).length
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* 清理本地备份(保留7天)
|
|
338
|
+
* @param {string} backupDir - 本地备份目录
|
|
339
|
+
* @param {number} retentionDays - 保留天数
|
|
340
|
+
*/
|
|
341
|
+
export function cleanLocalBackups(backupDir, retentionDays = 7) {
|
|
342
|
+
if (!fs.existsSync(backupDir)) {
|
|
343
|
+
console.log(`备份目录不存在: ${backupDir}`);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const now = new Date();
|
|
348
|
+
const cutoffDate = new Date(now.getTime() - retentionDays * 24 * 60 * 60 * 1000);
|
|
349
|
+
|
|
350
|
+
console.log(`\n🗑️ 清理本地备份(保留 ${retentionDays} 天)...`);
|
|
351
|
+
console.log(`截止日期: ${cutoffDate.toLocaleDateString('zh-CN')}`);
|
|
352
|
+
|
|
353
|
+
// 获取所有备份文件
|
|
354
|
+
const files = fs.readdirSync(backupDir);
|
|
355
|
+
const backupFiles = files.filter(f => f.endsWith('.tar.gz') || f.endsWith('.tgz'));
|
|
356
|
+
|
|
357
|
+
let deletedCount = 0;
|
|
358
|
+
let keptCount = 0;
|
|
359
|
+
|
|
360
|
+
backupFiles.forEach(file => {
|
|
361
|
+
const filePath = path.join(backupDir, file);
|
|
362
|
+
const stats = fs.statSync(filePath);
|
|
363
|
+
const fileDate = new Date(stats.mtime);
|
|
364
|
+
|
|
365
|
+
if (fileDate < cutoffDate) {
|
|
366
|
+
// 删除旧备份
|
|
367
|
+
fs.unlinkSync(filePath);
|
|
368
|
+
console.log(` 已删除: ${file} (${fileDate.toLocaleDateString('zh-CN')})`);
|
|
369
|
+
deletedCount++;
|
|
370
|
+
} else {
|
|
371
|
+
keptCount++;
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
console.log(`✅ 清理完成: 删除 ${deletedCount} 个, 保留 ${keptCount} 个`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export default {
|
|
379
|
+
DeployLogger,
|
|
380
|
+
cleanLocalBackups
|
|
381
|
+
};
|
package/src/ssh-client.js
CHANGED
|
@@ -137,6 +137,75 @@ export class SSHClient {
|
|
|
137
137
|
});
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* 从远程服务器下载文件(带进度条)
|
|
142
|
+
* @param {string} remotePath - 远程文件路径
|
|
143
|
+
* @param {string} localPath - 本地文件路径
|
|
144
|
+
* @returns {Promise<void>}
|
|
145
|
+
*/
|
|
146
|
+
async downloadFile(remotePath, localPath) {
|
|
147
|
+
const startTime = Date.now();
|
|
148
|
+
|
|
149
|
+
// 格式化字节数为可读格式
|
|
150
|
+
function formatBytes(bytes) {
|
|
151
|
+
if (bytes < 1024) return bytes + ' B';
|
|
152
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
153
|
+
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 渲染进度条
|
|
157
|
+
function renderBar(transferred, total) {
|
|
158
|
+
const percent = Math.round((transferred / total) * 100);
|
|
159
|
+
const barWidth = 30;
|
|
160
|
+
const filled = Math.round((percent / 100) * barWidth);
|
|
161
|
+
const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
|
|
162
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
163
|
+
const speed = elapsed > 0 ? transferred / elapsed : 0;
|
|
164
|
+
process.stdout.write(
|
|
165
|
+
`\r下载进度: [${bar}] ${percent}% ${formatBytes(transferred)}/${formatBytes(total)} ${formatBytes(speed)}/s`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return new Promise((resolve, reject) => {
|
|
170
|
+
this.client.sftp((err, sftp) => {
|
|
171
|
+
if (err) {
|
|
172
|
+
reject(err);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 先获取文件大小
|
|
177
|
+
sftp.stat(remotePath, (err, stats) => {
|
|
178
|
+
if (err) {
|
|
179
|
+
reject(err);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const totalBytes = stats.size;
|
|
184
|
+
|
|
185
|
+
sftp.fastGet(
|
|
186
|
+
remotePath,
|
|
187
|
+
localPath,
|
|
188
|
+
{
|
|
189
|
+
step: (transferred, _chunk, total) => {
|
|
190
|
+
renderBar(transferred, total);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
err => {
|
|
194
|
+
if (err) {
|
|
195
|
+
reject(err);
|
|
196
|
+
} else {
|
|
197
|
+
renderBar(totalBytes, totalBytes);
|
|
198
|
+
process.stdout.write('\n');
|
|
199
|
+
console.log(`✅ 下载成功: ${remotePath} -> ${localPath}`);
|
|
200
|
+
resolve();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
140
209
|
/**
|
|
141
210
|
* 断开 SSH 连接
|
|
142
211
|
* @returns {Promise<void>}
|
package/src/update.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 更新模块
|
|
9
|
+
* 支持检查更新和自动更新
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 获取当前版本
|
|
16
|
+
* @returns {string} 当前版本号
|
|
17
|
+
*/
|
|
18
|
+
export function getCurrentVersion() {
|
|
19
|
+
try {
|
|
20
|
+
// 从 package.json 获取版本
|
|
21
|
+
const packageJsonPath = path.join(__dirname, '..', 'package.json');
|
|
22
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
23
|
+
return packageJson.version;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
// 通过 npm 命令获取
|
|
26
|
+
try {
|
|
27
|
+
const version = execSync('npm list fe-build-cli --global --depth=0', { encoding: 'utf-8' });
|
|
28
|
+
const match = version.match(/fe-build-cli@(\d+\.\d+\.\d+)/);
|
|
29
|
+
return match ? match[1] : '未知';
|
|
30
|
+
} catch (e) {
|
|
31
|
+
return '未知';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 获取 npm 最新版本
|
|
38
|
+
* @returns {Promise<string>} 最新版本号
|
|
39
|
+
*/
|
|
40
|
+
export async function getLatestVersion() {
|
|
41
|
+
try {
|
|
42
|
+
const result = execSync('npm view fe-build-cli version', { encoding: 'utf-8', timeout: 10000 });
|
|
43
|
+
return result.trim();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw new Error('无法获取最新版本,请检查网络连接或 npm 源');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 检查是否有更新
|
|
51
|
+
* @returns {Promise<object>} 更新信息
|
|
52
|
+
*/
|
|
53
|
+
export async function checkForUpdate() {
|
|
54
|
+
const currentVersion = await getCurrentVersion();
|
|
55
|
+
const latestVersion = await getLatestVersion();
|
|
56
|
+
|
|
57
|
+
const hasUpdate = currentVersion !== latestVersion && latestVersion !== '未知';
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
currentVersion,
|
|
61
|
+
latestVersion,
|
|
62
|
+
hasUpdate
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 执行更新
|
|
68
|
+
* @param {boolean} global - 是否全局更新
|
|
69
|
+
* @returns {Promise<boolean>} 是否成功
|
|
70
|
+
*/
|
|
71
|
+
export async function performUpdate(global = true) {
|
|
72
|
+
try {
|
|
73
|
+
console.log('\n正在更新 fe-build-cli...');
|
|
74
|
+
|
|
75
|
+
const command = global
|
|
76
|
+
? 'npm update fe-build-cli --global'
|
|
77
|
+
: 'npm update fe-build-cli';
|
|
78
|
+
|
|
79
|
+
execSync(command, { stdio: 'inherit', timeout: 60000 });
|
|
80
|
+
|
|
81
|
+
console.log('\n✅ 更新完成!');
|
|
82
|
+
|
|
83
|
+
// 显示更新后的版本
|
|
84
|
+
const newVersion = await getLatestVersion();
|
|
85
|
+
console.log(`当前版本: ${newVersion}`);
|
|
86
|
+
|
|
87
|
+
return true;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('\n❌ 更新失败:', error.message);
|
|
90
|
+
console.log('\n请尝试手动更新:');
|
|
91
|
+
console.log(' npm update fe-build-cli --global');
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 显示更新信息
|
|
98
|
+
*/
|
|
99
|
+
export async function showUpdateInfo() {
|
|
100
|
+
console.log('\n========================================');
|
|
101
|
+
console.log(' 🔄 fe-build-cli 版本检查');
|
|
102
|
+
console.log('========================================');
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const info = await checkForUpdate();
|
|
106
|
+
|
|
107
|
+
console.log(`当前版本: ${info.currentVersion}`);
|
|
108
|
+
console.log(`最新版本: ${info.latestVersion}`);
|
|
109
|
+
|
|
110
|
+
if (info.hasUpdate) {
|
|
111
|
+
console.log('\n📌 发现新版本!');
|
|
112
|
+
console.log('\n更新方法:');
|
|
113
|
+
console.log(' fe-build update # 自动更新');
|
|
114
|
+
console.log(' npm update fe-build-cli --global # 手动更新');
|
|
115
|
+
} else {
|
|
116
|
+
console.log('\n✅ 已是最新版本');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log('========================================');
|
|
120
|
+
|
|
121
|
+
return info;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('❌ 检查更新失败:', error.message);
|
|
124
|
+
console.log('\n请检查网络连接或手动更新:');
|
|
125
|
+
console.log(' npm update fe-build-cli --global');
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export default {
|
|
131
|
+
getCurrentVersion,
|
|
132
|
+
getLatestVersion,
|
|
133
|
+
checkForUpdate,
|
|
134
|
+
performUpdate,
|
|
135
|
+
showUpdateInfo
|
|
136
|
+
};
|