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/src/cli.js CHANGED
@@ -5,10 +5,12 @@ import { execSync } from 'node:child_process';
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath, pathToFileURL } from 'node:url';
8
- import { deployToServer, rollbackDeployment } from './deploy-core.js';
8
+ import { deployToServer, rollbackDeployment, getServerBackupList, getLocalBackupList, rollbackFromLocal } from './deploy-core.js';
9
+ import SSHClient from './ssh-client.js';
9
10
  import {
10
11
  getCurrentBranch,
11
12
  getGitSha,
13
+ getGitCommitMessage,
12
14
  executeMainBranchFlow,
13
15
  executeCurrentBranchFlow,
14
16
  executeTestBranchFlow,
@@ -20,6 +22,8 @@ import {
20
22
  sendDeployFailureNotification,
21
23
  sendRollbackNotification
22
24
  } from './dingtalk.js';
25
+ import { DeployLogger } from './logger.js';
26
+ import { checkForUpdate, performUpdate, showUpdateInfo, getCurrentVersion } from './update.js';
23
27
 
24
28
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
29
 
@@ -164,7 +168,11 @@ fe-build-cli - 前端项目打包部署工具
164
168
 
165
169
  命令:
166
170
  deploy [环境] 部署到指定环境(默认命令)
167
- rollback [环境] 回滚到上一版本
171
+ rollback [环境] 回滚到指定版本(交互选择备份来源)
172
+ update 检查并更新到最新版本
173
+ update --force 自动更新(无需确认)
174
+ check-update 仅检查是否有新版本
175
+ version 显示当前版本号
168
176
  help 显示帮助信息
169
177
 
170
178
  选项:
@@ -176,6 +184,9 @@ fe-build-cli - 前端项目打包部署工具
176
184
  --no-merge test 发布时不合并,使用 stash 储藏本地改动
177
185
  --skip-build 跳过构建步骤
178
186
  --no-push 发布时不推送到远程
187
+ --server 回滚时使用服务器备份(默认)
188
+ --local 回滚时使用本地备份
189
+ --version <版本号> 回滚到指定版本
179
190
 
180
191
  示例:
181
192
  fe-build # 交互式选择环境部署
@@ -186,7 +197,9 @@ fe-build-cli - 前端项目打包部署工具
186
197
  fe-build --test-branch --no-merge # test 发布,stash 储藏改动
187
198
  fe-build --current-branch # 当前分支发布
188
199
  fe-build --main-branch # 主分支发布流程
189
- fe-build rollback production # 回滚生产环境
200
+ fe-build rollback production # 回滚生产环境(交互选择)
201
+ fe-build rollback production --server # 回滚生产环境(服务器备份)
202
+ fe-build rollback production --local # 回滚生产环境(本地备份)
190
203
 
191
204
  配置文件 (fe-build.config.js):
192
205
  export default {
@@ -295,6 +308,12 @@ async function deployCommand(config) {
295
308
  }
296
309
  }
297
310
 
311
+ // 创建日志记录器(在分支操作之前)
312
+ const logDir = config.logDir || 'logs';
313
+ const localBackupDir = config.localBackupDir || 'D:\\备份';
314
+ const logger = new DeployLogger({ logDir, localBackupDir });
315
+ logger.start();
316
+
298
317
  // 执行分支发布流程
299
318
  let branchResult = null;
300
319
  let originalBranch = getCurrentBranch(); // 记录原始分支
@@ -320,7 +339,8 @@ async function deployCommand(config) {
320
339
  testBranch: branches.test,
321
340
  mergeChanges,
322
341
  pushToRemote: !noPush,
323
- prompt
342
+ prompt,
343
+ logger // 传递 logger
324
344
  });
325
345
  originalBranch = branchResult.originalBranch;
326
346
  needRestore = branchResult.needRestore;
@@ -339,19 +359,21 @@ async function deployCommand(config) {
339
359
  const confirmAnswer = await prompt('确认执行主分支发布流程? (y/n): ');
340
360
  if (confirmAnswer.toLowerCase() !== 'y') {
341
361
  console.log('已取消发布');
362
+ logger.end('cancelled');
342
363
  process.exit(0);
343
364
  }
344
365
 
345
366
  branchResult = executeMainBranchFlow({
346
367
  testBranch: branches.test,
347
368
  mainBranch: branches.main,
348
- pushToRemote: !noPush
369
+ pushToRemote: !noPush,
370
+ logger // 传递 logger
349
371
  });
350
372
  originalBranch = branchResult.originalBranch;
351
373
  needRestore = true;
352
374
  } else if (deployMode === 'current') {
353
375
  // 当前分支发布模式
354
- branchResult = executeCurrentBranchFlow();
376
+ branchResult = executeCurrentBranchFlow(logger);
355
377
  originalBranch = branchResult.currentBranch;
356
378
  needRestore = false;
357
379
  console.log('📌 当前分支发布模式:不切换分支');
@@ -359,7 +381,10 @@ async function deployCommand(config) {
359
381
  } catch (branchError) {
360
382
  // 分支流程失败,发送钉钉通知
361
383
  console.error(`❌ 分支流程失败:`, branchError.message);
384
+ logger.log('ERROR', '分支流程失败', branchError.message);
385
+ logger.end('failed');
362
386
 
387
+ const commitMessage = getGitCommitMessage();
363
388
  if (config.dingtalk && config.dingtalk.enabled && config.dingtalk.webhook) {
364
389
  console.log('\n发送钉钉失败通知...');
365
390
  const envConfig = getServerConfig(config, selectedServers[0] || serverNames[0]);
@@ -368,13 +393,14 @@ async function deployCommand(config) {
368
393
  buildVersion: '未完成',
369
394
  serverHost: envConfig?.sshHost || '未知',
370
395
  branch: originalBranch,
396
+ commitMessage,
371
397
  error: `分支流程失败: ${branchError.message}`,
372
398
  keyword: config.dingtalk.keyword || '部署'
373
399
  });
374
400
  }
375
401
 
376
402
  // 切回原分支
377
- restoreBranch(originalBranch, hasStash);
403
+ restoreBranch(originalBranch, hasStash, logger);
378
404
  process.exit(1);
379
405
  }
380
406
 
@@ -404,18 +430,25 @@ async function deployCommand(config) {
404
430
  }
405
431
  console.log('========================================');
406
432
 
433
+ // 部署到服务器(使用前面创建的 logger)
407
434
  try {
408
435
  await deployToServer({
409
436
  environment: serverName,
410
437
  envConfig,
411
438
  buildVersion,
412
439
  skipBuild: skipBuild || !isFirst,
413
- skipLocalCleanup: i < selectedServers.length - 1
440
+ skipLocalCleanup: i < selectedServers.length - 1,
441
+ logger,
442
+ localBackupDir
414
443
  });
415
444
 
445
+ // 部署成功,结束日志记录
446
+ logger.end('success');
447
+
416
448
  // 部署成功,发送钉钉通知
417
449
  const duration = Math.round((Date.now() - startTime) / 1000);
418
450
  const currentBranch = getCurrentBranch();
451
+ const commitMessage = getGitCommitMessage();
419
452
 
420
453
  if (config.dingtalk && config.dingtalk.enabled && config.dingtalk.webhook) {
421
454
  console.log('\n发送钉钉通知...');
@@ -426,15 +459,21 @@ async function deployCommand(config) {
426
459
  deployUrl: envConfig.deployUrl,
427
460
  branch: currentBranch,
428
461
  deployMode,
462
+ commitMessage,
429
463
  duration: `${duration}秒`,
430
464
  keyword: config.dingtalk.keyword || '部署'
431
465
  });
466
+ logger.logDingTalk(true);
432
467
  }
433
468
  } catch (error) {
434
469
  console.error(`❌ 部署到 ${serverName} 失败:`, error.message);
435
470
 
471
+ // 部署失败,结束日志记录
472
+ logger.end('failed');
473
+
436
474
  // 部署失败,发送钉钉通知
437
475
  const currentBranch = getCurrentBranch();
476
+ const commitMessage = getGitCommitMessage();
438
477
  if (config.dingtalk && config.dingtalk.enabled && config.dingtalk.webhook) {
439
478
  console.log('\n发送钉钉失败通知...');
440
479
  await sendDeployFailureNotification(config.dingtalk.webhook, {
@@ -442,14 +481,16 @@ async function deployCommand(config) {
442
481
  buildVersion,
443
482
  serverHost: envConfig.sshHost,
444
483
  branch: currentBranch,
484
+ commitMessage,
445
485
  error: error.message,
446
486
  keyword: config.dingtalk.keyword || '部署'
447
487
  });
488
+ logger.logDingTalk(false, error.message);
448
489
  }
449
490
 
450
491
  // 出错时切回原分支
451
492
  if (needRestore && originalBranch) {
452
- restoreBranch(originalBranch, hasStash);
493
+ restoreBranch(originalBranch, hasStash, logger);
453
494
  }
454
495
  process.exit(1);
455
496
  }
@@ -460,13 +501,13 @@ async function deployCommand(config) {
460
501
  // 合并模式:自动切回原分支
461
502
  if (autoRestore) {
462
503
  console.log('\n📌 自动切回原分支...');
463
- restoreBranch(originalBranch, false);
504
+ restoreBranch(originalBranch, false, logger);
464
505
  console.log(`✅ 已切回 ${originalBranch},可继续开发`);
465
506
  } else {
466
507
  // stash 模式:询问是否切回
467
508
  const returnAnswer = await prompt('\n是否切回原分支? (y/n): ');
468
509
  if (returnAnswer.toLowerCase() === 'y') {
469
- restoreBranch(originalBranch, hasStash);
510
+ restoreBranch(originalBranch, hasStash, logger);
470
511
  } else if (hasStash) {
471
512
  console.log('\n💡 提示: 本地改动已储藏,执行以下命令恢复:');
472
513
  console.log(' git stash pop');
@@ -491,10 +532,12 @@ async function rollbackCommand(config) {
491
532
  const environment = args.find(arg => arg !== 'rollback' && !arg.startsWith('--'));
492
533
  const versionIndex = args.indexOf('--version');
493
534
  const specifiedVersion = versionIndex !== -1 ? args[versionIndex + 1] : undefined;
535
+ const useLocalBackup = args.includes('--local'); // 是否使用本地备份
536
+ const useServerBackup = args.includes('--server'); // 是否使用服务器备份
494
537
 
495
538
  if (!environment || !serverNames.includes(environment)) {
496
539
  console.error(`❌ 请指定服务器: ${serverNames.join(' 或 ')}`);
497
- console.error(`用法: fe-build rollback [${serverNames.join('|')}] [--version <版本号>]`);
540
+ console.error(`用法: fe-build rollback [${serverNames.join('|')}] [--server|--local] [--version <版本号>]`);
498
541
  process.exit(1);
499
542
  }
500
543
 
@@ -505,22 +548,134 @@ async function rollbackCommand(config) {
505
548
  process.exit(1);
506
549
  }
507
550
 
551
+ // 创建日志记录器
552
+ const logDir = config.logDir || 'logs';
553
+ const localBackupDir = config.localBackupDir || 'D:\\备份';
554
+ const logger = new DeployLogger({ logDir, localBackupDir });
555
+ logger.start();
556
+
557
+ console.log('========================================');
558
+ console.log(`开始回滚 ${environment} 环境`);
559
+ console.log(`服务器: ${envConfig.sshHost}`);
560
+ console.log('========================================');
561
+
562
+ // 连接服务器
563
+ const ssh = new SSHClient(envConfig);
564
+ await ssh.connect();
565
+ logger.logSSHConnect(envConfig.sshHost, true);
566
+
508
567
  let backupFile = '';
509
- let success = false;
568
+ let selectedBackup = null;
569
+ let backupSource = 'server'; // 默认服务器备份
510
570
 
511
571
  try {
512
- // 执行回滚,获取备份文件信息
513
- backupFile = specifiedVersion
514
- ? `${envConfig.backupDir}/${envConfig.backupPrefix}-${specifiedVersion}.tar.gz`
515
- : '';
572
+ // 如果指定了版本号,直接使用
573
+ if (specifiedVersion) {
574
+ backupFile = `${envConfig.backupDir}/${envConfig.backupPrefix}-${specifiedVersion}.tar.gz`;
575
+ console.log(`\n使用指定版本: ${specifiedVersion}`);
576
+ logger.log('INFO', '回滚版本', `指定版本: ${specifiedVersion}`);
577
+ } else {
578
+ // 获取备份列表
579
+ let serverBackups = [];
580
+ let localBackups = [];
581
+
582
+ // 获取服务器备份列表
583
+ console.log('\n[步骤 1] 获取服务器备份列表...');
584
+ serverBackups = await getServerBackupList(ssh, envConfig);
585
+ console.log(`找到 ${serverBackups.length} 个服务器备份`);
586
+
587
+ // 获取本地备份列表
588
+ console.log('\n[步骤 2] 获取本地备份列表...');
589
+ localBackups = getLocalBackupList(localBackupDir, envConfig.backupPrefix);
590
+ console.log(`找到 ${localBackups.length} 个本地备份`);
591
+
592
+ // 如果没有备份
593
+ if (serverBackups.length === 0 && localBackups.length === 0) {
594
+ logger.log('ERROR', '获取备份', '未找到任何备份文件');
595
+ console.error('❌ 未找到任何备份文件!');
596
+ await ssh.disconnect();
597
+ logger.end('failed');
598
+ process.exit(1);
599
+ }
600
+
601
+ // 确定备份来源
602
+ if (useLocalBackup && localBackups.length > 0) {
603
+ backupSource = 'local';
604
+ } else if (useServerBackup && serverBackups.length > 0) {
605
+ backupSource = 'server';
606
+ } else if (!useLocalBackup && !useServerBackup) {
607
+ // 交互选择备份来源(默认服务器)
608
+ console.log('\n========================================');
609
+ console.log(' 📦 选择备份来源');
610
+ console.log('========================================');
611
+ console.log(` 1. 服务器备份 (${serverBackups.length} 个) - 默认`);
612
+ if (localBackups.length > 0) {
613
+ console.log(` 2. 本地备份 (${localBackups.length} 个)`);
614
+ }
615
+ console.log('========================================');
616
+
617
+ const sourceAnswer = await prompt(`请选择备份来源 (1${localBackups.length > 0 ? '/2' : ''}): `);
618
+ if (sourceAnswer === '2' && localBackups.length > 0) {
619
+ backupSource = 'local';
620
+ } else {
621
+ backupSource = 'server';
622
+ }
623
+ }
624
+
625
+ // 显示备份列表供选择
626
+ const backups = backupSource === 'server' ? serverBackups : localBackups;
627
+
628
+ console.log(`\n========================================`);
629
+ console.log(` 📦 ${backupSource === 'server' ? '服务器' : '本地'}备份列表`);
630
+ console.log(`========================================`);
631
+
632
+ backups.forEach((backup, index) => {
633
+ const sizeStr = backup.size ? ` (${formatFileSize(backup.size)})` : '';
634
+ const timeStr = backup.mtime ? ` - ${backup.mtime.toLocaleDateString('zh-CN')}` : '';
635
+ console.log(` ${index + 1}. ${backup.version}${sizeStr}${timeStr}`);
636
+ });
637
+ console.log(`========================================`);
516
638
 
639
+ const backupAnswer = await prompt(`请选择要回滚的备份 (1-${backups.length}): `);
640
+ const selectedIndex = parseInt(backupAnswer, 10) - 1;
641
+
642
+ if (selectedIndex < 0 || selectedIndex >= backups.length) {
643
+ console.error('❌ 无效选择');
644
+ await ssh.disconnect();
645
+ logger.end('failed');
646
+ process.exit(1);
647
+ }
648
+
649
+ selectedBackup = backups[selectedIndex];
650
+ backupFile = selectedBackup.file;
651
+
652
+ console.log(`\n已选择: ${selectedBackup.version}`);
653
+ logger.log('INFO', '选择备份', `来源: ${backupSource}, 版本: ${selectedBackup.version}`);
654
+ }
655
+
656
+ // 如果是本地备份,需要先上传到服务器
657
+ if (backupSource === 'local' && selectedBackup) {
658
+ const remoteFile = await rollbackFromLocal({
659
+ ssh,
660
+ envConfig,
661
+ localBackupFile: backupFile,
662
+ logger
663
+ });
664
+ backupFile = remoteFile;
665
+ }
666
+
667
+ // 执行回滚
517
668
  await rollbackDeployment({
518
669
  environment,
519
670
  envConfig,
520
- specifiedVersion
671
+ specifiedVersion: specifiedVersion || (selectedBackup ? selectedBackup.version : undefined),
672
+ backupFile,
673
+ logger,
674
+ ssh // 传递已连接的 ssh
521
675
  });
522
676
 
523
- success = true;
677
+ await ssh.disconnect();
678
+ logger.end('success');
524
679
 
525
680
  // 回滚成功,发送钉钉通知
526
681
  if (config.dingtalk && config.dingtalk.enabled && config.dingtalk.webhook) {
@@ -533,10 +688,18 @@ async function rollbackCommand(config) {
533
688
  success: true,
534
689
  keyword: config.dingtalk.keyword || '部署'
535
690
  });
691
+ logger.logDingTalk(true);
536
692
  }
537
693
  } catch (error) {
538
694
  console.error('❌ 回滚失败:', error.message);
539
- success = false;
695
+ logger.log('ERROR', '回滚失败', error.message);
696
+ logger.end('failed');
697
+
698
+ try {
699
+ await ssh.disconnect();
700
+ } catch (e) {
701
+ // 忽略
702
+ }
540
703
 
541
704
  // 回滚失败,发送钉钉通知
542
705
  if (config.dingtalk && config.dingtalk.enabled && config.dingtalk.webhook) {
@@ -549,12 +712,42 @@ async function rollbackCommand(config) {
549
712
  success: false,
550
713
  keyword: config.dingtalk.keyword || '部署'
551
714
  });
715
+ logger.logDingTalk(false, error.message);
552
716
  }
553
717
 
554
718
  process.exit(1);
555
719
  }
556
720
  }
557
721
 
722
+ /**
723
+ * 格式化文件大小
724
+ */
725
+ function formatFileSize(bytes) {
726
+ if (bytes < 1024) return bytes + ' B';
727
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
728
+ return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
729
+ }
730
+
731
+ /**
732
+ * 启动时检查更新
733
+ */
734
+ async function checkUpdateOnStart() {
735
+ try {
736
+ const info = await checkForUpdate();
737
+ if (info && info.hasUpdate) {
738
+ console.log('\n========================================');
739
+ console.log(' 🔄 发现新版本');
740
+ console.log('========================================');
741
+ console.log(`当前版本: ${info.currentVersion}`);
742
+ console.log(`最新版本: ${info.latestVersion}`);
743
+ console.log('\n更新命令: fe-build update --force');
744
+ console.log('========================================\n');
745
+ }
746
+ } catch (error) {
747
+ // 检查更新失败,不影响主流程
748
+ }
749
+ }
750
+
558
751
  /**
559
752
  * 主入口
560
753
  */
@@ -568,6 +761,42 @@ async function main() {
568
761
  process.exit(0);
569
762
  }
570
763
 
764
+ // 显示版本
765
+ if (command === 'version' || args.includes('--version') || args.includes('-v')) {
766
+ const version = getCurrentVersion();
767
+ console.log(`fe-build-cli v${version}`);
768
+ process.exit(0);
769
+ }
770
+
771
+ // 检查更新
772
+ if (command === 'check-update') {
773
+ await showUpdateInfo();
774
+ process.exit(0);
775
+ }
776
+
777
+ // 执行更新
778
+ if (command === 'update') {
779
+ const forceUpdate = args.includes('--force') || args.includes('--auto');
780
+ const info = await showUpdateInfo();
781
+
782
+ if (info && info.hasUpdate) {
783
+ if (forceUpdate) {
784
+ // 自动更新,无需确认
785
+ await performUpdate(true);
786
+ } else {
787
+ // 交互确认
788
+ const answer = await prompt('\n是否立即更新? (y/n): ');
789
+ if (answer.toLowerCase() === 'y') {
790
+ await performUpdate(true);
791
+ }
792
+ }
793
+ }
794
+ process.exit(0);
795
+ }
796
+
797
+ // 其他命令启动时检查更新(静默检查)
798
+ await checkUpdateOnStart();
799
+
571
800
  // 加载配置
572
801
  const config = await loadConfig();
573
802
 
@@ -80,5 +80,18 @@ export default {
80
80
  webhook: 'https://oapi.dingtalk.com/robot/send?access_token=your-token', // 钉钉机器人 webhook URL
81
81
  enabled: true, // 是否启用钉钉通知,默认 true
82
82
  keyword: '部署' // 安全设置关键词(如果机器人设置了关键词,必须配置此项)
83
- }
83
+ },
84
+
85
+ /**
86
+ * 日志配置(可选)
87
+ * 部署日志存储目录,默认项目根目录下的 logs 目录
88
+ */
89
+ logDir: 'logs',
90
+
91
+ /**
92
+ * 本地备份目录(可选)
93
+ * 线上备份下载到本地的存储目录,默认 D:\备份
94
+ * 保留 7 天内的备份,自动清理旧备份
95
+ */
96
+ localBackupDir: 'D:\\备份'
84
97
  };