fe-build-cli 1.2.4 → 1.5.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/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>}