daodou-command 1.4.7 → 1.4.8

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.
@@ -1,6 +1,5 @@
1
1
  const chalk = require('chalk');
2
2
  const ora = require('ora');
3
- const inquirer = require('inquirer');
4
3
  const path = require('path');
5
4
  const fs = require('fs');
6
5
  const { getCurrentBranch } = require('../utils/git');
@@ -18,20 +17,19 @@ class BuildConfigManager {
18
17
 
19
18
  checkAndCreateConfig() {
20
19
  try {
21
- // 检查是否有配置文件
22
20
  if (!fs.existsSync(this.configManager.projectConfigFile) && !fs.existsSync(this.configManager.configFile)) {
23
21
  this.configManager.createDefaultConfig();
24
- console.log('📝 已为你创建配置文件');
25
- console.log('💡 请编辑该文件,将 your-xxx 替换为你的实际配置信息');
26
- console.log('📍 文件位置:' + this.configManager.projectConfigFile);
27
- console.log('🔄 填写完成后请重新运行命令');
22
+ console.log('');
23
+ console.log(chalk.yellow(' 已创建默认配置文件'));
24
+ console.log(chalk.dim(' 请编辑后重新运行:'));
25
+ console.log(' ' + this.configManager.projectConfigFile);
28
26
  process.exit(0);
29
27
  }
30
-
28
+
31
29
  this.loadConfig();
32
30
  this.checkTemplateValues();
33
31
  } catch (e) {
34
- console.error(' 读取配置文件失败,请检查文件格式或内容是否正确。');
32
+ console.error(chalk.red(' 读取配置文件失败,请检查文件格式'));
35
33
  process.exit(1);
36
34
  }
37
35
  }
@@ -50,14 +48,14 @@ class BuildConfigManager {
50
48
  ];
51
49
  const hasTemplateValues = Object.values(this.localConfig).some(value => templateValues.includes(value));
52
50
  if (hasTemplateValues) {
53
- console.log(chalk.yellow('⚠️ 检测到配置文件包含模板值,请修改以下变量:'));
51
+ console.log('');
52
+ console.log(chalk.yellow(' ⚠ 配置文件包含模板值,请修改:'));
54
53
  Object.entries(this.localConfig).forEach(([key, value]) => {
55
54
  if (templateValues.includes(value)) {
56
- console.log(chalk.cyan(` ${key}=${value}`));
55
+ console.log(chalk.dim(` ${key}`) + ' = ' + chalk.cyan(value));
57
56
  }
58
57
  });
59
- console.log(chalk.green('📍 文件位置:' + this.configManager.projectConfigFile));
60
- console.log(chalk.cyan('🔄 修改完成后请重新运行命令'));
58
+ console.log(chalk.dim(' 文件: ') + this.configManager.projectConfigFile);
61
59
  process.exit(0);
62
60
  }
63
61
  }
@@ -65,34 +63,30 @@ class BuildConfigManager {
65
63
  validateConfig() {
66
64
  const requiredKeys = ['jenkinsBase', 'jenkinsToken', 'jenkinsUrl', 'jobName'];
67
65
  const missingKeys = [];
68
-
66
+
69
67
  for (const key of requiredKeys) {
70
68
  if (!this.localConfig[key]) {
71
69
  missingKeys.push(key);
72
70
  }
73
71
  }
74
-
72
+
75
73
  if (missingKeys.length > 0) {
76
- console.error('❌ 配置信息缺失,请检查以下配置:');
74
+ console.log('');
75
+ console.log(chalk.red(' ✖ 缺少必要配置:'));
77
76
  missingKeys.forEach(key => {
78
- console.error(` - ${key}`);
77
+ console.log(chalk.dim(` - ${key}`));
79
78
  });
80
- console.error('\n💡 提示:');
81
- console.error(' 1. 请在项目配置文件 (.daodourc) 中取消注释并填写上述配置');
79
+ console.log('');
80
+ console.log(chalk.dim(' 请在 .daodourc ~/.daodou/config.json 中补充'));
82
81
  if (missingKeys.includes('jobName')) {
83
- console.error(' 2. jobName 参数必须从本地配置文件设置,不能使用全局配置');
84
- } else {
85
- console.error(' 2. 或者确保全局配置文件 (~/.daodou/config.json) 中包含这些配置');
82
+ console.log(chalk.dim(' jobName 必须在项目配置 (.daodourc) 中设置'));
86
83
  }
87
- console.error(' 3. 项目配置优先于全局配置');
88
84
  process.exit(1);
89
85
  }
90
-
91
- // 验证构建参数配置
86
+
92
87
  if (this.localConfig.buildParams) {
93
- const buildParams = this.localConfig.buildParams;
94
- if (typeof buildParams !== 'object') {
95
- console.error('❌ buildParams 必须是对象格式');
88
+ if (typeof this.localConfig.buildParams !== 'object') {
89
+ console.log(chalk.red(' ✖ buildParams 必须是对象格式'));
96
90
  process.exit(1);
97
91
  }
98
92
  }
@@ -151,48 +145,37 @@ class BuildCommand {
151
145
  this.jenkinsConfig.baseUrl = config.jenkinsUrl;
152
146
  this.jenkinsBase = config.jenkinsBase;
153
147
  this.jenkinsToken = config.jenkinsToken;
154
- console.log(chalk.blue('🔧 刀豆构建工具启动中...\n'));
155
148
 
156
- // 1. 获取分支名称(强制从Git获取,不依赖配置)
149
+ console.log('');
150
+ console.log(chalk.bold(' 🔧 刀豆构建工具'));
151
+ console.log(chalk.dim(' ─────────────────────────'));
152
+
153
+ // 1. 获取分支名称
157
154
  const branch = await this.getBranch(options);
158
- console.log(chalk.green(`✅ 当前分支: ${chalk.cyan(branch)}\n`));
159
155
 
160
- // 2. 浏览器登录Jenkins
156
+ // 2. 登录 & 认证
161
157
  await this.browserAuth.ensureLogin();
162
-
163
- // 3. 设置Jenkins认证
164
158
  this.setupJenkinsAuth();
165
-
166
- // 4. 检查 Jenkins 会话有效性
167
159
  await this.ensureJenkinsSession();
168
160
 
169
- // 5. 构建任务名称和URL
161
+ // 3. 构建参数
170
162
  const jobName = this.buildJobName(config);
171
163
  const jenkinsUrl = this.buildJobUrl(config, jobName);
172
164
  const params = this.buildParams(config, branch);
173
165
 
174
- // 7. 确认参数
175
- console.log(chalk.yellow('📋 构建参数:'));
176
- console.log(` jobName: ${jobName}`);
166
+ console.log('');
167
+ console.log(chalk.dim(' ─────────────────────────'));
168
+ console.log(' ' + chalk.dim('任务') + ' ' + chalk.cyan(jobName));
169
+ console.log(' ' + chalk.dim('分支') + ' ' + chalk.cyan(branch));
177
170
  Object.entries(params).forEach(([key, value]) => {
178
- if (key === 'token') return; // 不显示 token
179
- console.log(` ${chalk.cyan(key)}: ${chalk.magenta(value)}`);
171
+ if (key === 'token' || key === 'GIT_BRANCH') return;
172
+ console.log(' ' + chalk.dim(key) + ' ' + value);
180
173
  });
174
+ console.log(chalk.dim(' ─────────────────────────'));
175
+ console.log('');
181
176
 
182
- await inquirer.prompt([
183
- {
184
- type: 'input',
185
- name: 'confirm',
186
- message: chalk.yellow('按回车确认开始构建,Ctrl+C 取消...'),
187
- default: undefined,
188
- transformer: () => ''
189
- }
190
- ]);
191
- // 只要不是 Ctrl+C 终止就继续
192
-
193
- // 8. 记录本地计时起点
177
+ // 4. 触发构建
194
178
  const buildStartTime = Date.now();
195
- // 9. 触发Jenkins构建并监听进度
196
179
  await this.triggerAndMonitorBuild(jobName, params, buildStartTime);
197
180
  }
198
181
 
@@ -233,33 +216,19 @@ class BuildCommand {
233
216
  * @returns {Promise<string>} 分支名称
234
217
  */
235
218
  async getBranch(options) {
236
- // 如果命令行指定了分支,优先使用命令行参数
237
219
  if (options.branch) {
238
- console.log(chalk.yellow(`⚠️ 使用命令行指定的分支: ${options.branch}`));
220
+ console.log(chalk.yellow(' ⚠ 使用指定分支 ') + chalk.cyan(options.branch));
239
221
  return options.branch;
240
222
  }
241
223
 
242
- // 否则强制从Git获取当前分支
243
- const spinner = ora('检测当前Git分支...').start();
244
- try {
245
- const branch = await getCurrentBranch();
246
- spinner.succeed('分支检测完成');
247
- return branch;
248
- } catch (error) {
249
- spinner.fail('分支检测失败');
250
- throw new Error(`无法检测当前Git分支: ${error.message}\n💡 请确保当前目录是Git仓库,或使用 --branch 参数指定分支`);
251
- }
252
- }
253
-
254
- async detectBranch() {
255
- const spinner = ora('检测当前分支...').start();
224
+ const spinner = ora({ text: '检测 Git 分支...', indent: 2 }).start();
256
225
  try {
257
226
  const branch = await getCurrentBranch();
258
- spinner.succeed('分支检测完成');
227
+ spinner.succeed('分支 ' + chalk.cyan(branch));
259
228
  return branch;
260
229
  } catch (error) {
261
230
  spinner.fail('分支检测失败');
262
- throw new Error(`无法检测当前分支: ${error.message}`);
231
+ throw new Error(`无法检测 Git 分支: ${error.message}\n 请确保当前目录是 Git 仓库,或使用 --branch 指定`);
263
232
  }
264
233
  }
265
234
 
@@ -276,18 +245,24 @@ class BuildCommand {
276
245
  }
277
246
 
278
247
  async ensureJenkinsSession() {
248
+ const spinner = ora({ text: '验证 Jenkins 会话...', indent: 2 }).start();
279
249
  try {
280
- // 使用拦截器自动带上 Cookie
281
250
  const resp = await this.axios.get(this.jenkinsConfig.baseUrl);
282
-
283
- // 302 跳转到登录页、401/403 都视为未登录
284
251
  if (resp.status === 401 || resp.status === 403) throw new Error('未登录');
285
252
  if (resp.status === 302 && resp.headers.location && resp.headers.location.includes('casdoor')) throw new Error('未登录');
286
- // 其它情况视为已登录
253
+ spinner.succeed('认证有效');
287
254
  return true;
288
255
  } catch (e) {
289
- // 自动重新登录
290
- console.log(chalk.yellow('⚠️ Cookie 已失效,自动重新登录...'));
256
+ // 优先用 Casdoor cookie 静默刷新
257
+ spinner.text = 'Session 已过期,正在刷新...';
258
+ const refreshed = await this.browserAuth.refreshSessionViaCasdoor();
259
+ if (refreshed) {
260
+ this.setupJenkinsAuth();
261
+ spinner.succeed('Session 已刷新');
262
+ return true;
263
+ }
264
+ // Casdoor 也过期,启动浏览器重新登录
265
+ spinner.warn('Session 已过期,需要重新登录');
291
266
  await this.browserAuth.login();
292
267
  this.setupJenkinsAuth();
293
268
  return true;
@@ -295,16 +270,14 @@ class BuildCommand {
295
270
  }
296
271
 
297
272
  async triggerAndMonitorBuild(jobName, params, buildStartTime) {
298
- // 1. 触发构建
299
- const spinner = ora('正在触发Jenkins构建...').start();
273
+ const spinner = ora({ text: '触发构建...', indent: 2 }).start();
300
274
  try {
301
275
  const queueId = await this.triggerBuild(jobName, params);
302
- spinner.succeed(chalk.green(`✅ Jenkins构建已触发,队列号: ${queueId}`));
303
- // 2. 监听队列,获取build number
276
+ spinner.succeed('构建已触发 ' + chalk.dim(`队列 #${queueId}`));
304
277
  await this.monitorQueueAndBuild(jobName, queueId, buildStartTime);
305
278
  } catch (error) {
306
- spinner.fail('❌ Jenkins构建触发失败');
307
- console.error(chalk.red('构建失败:'), error.message);
279
+ spinner.fail('构建触发失败');
280
+ console.error(chalk.red(' ' + error.message));
308
281
  }
309
282
  }
310
283
 
@@ -333,24 +306,22 @@ class BuildCommand {
333
306
  const location = response.headers.location;
334
307
  const queueId = location ? location.replace(/\/$/, '').split('/').pop() : null;
335
308
  if (!queueId) {
336
- console.log(chalk.yellow('⚠️ 未能正确获取队列号,可能是Jenkins配置问题。'));
309
+ console.log(chalk.yellow(' 未能获取队列号'));
337
310
  }
338
311
  return queueId;
339
312
  } else {
340
313
  throw new Error('触发构建失败');
341
314
  }
342
315
  } catch (error) {
343
- console.log(chalk.red('❌ Jenkins构建触发失败: ' + error.message));
344
- console.log(chalk.yellow('💡 请检查Jenkins权限、Token、参数设置,或联系管理员。'));
345
316
  throw new Error(`触发构建失败: ${error.message}`);
346
317
  }
347
318
  }
348
319
 
349
320
  async monitorQueueAndBuild(jobName, queueId, buildStartTime) {
350
- const spinner = ora('等待Jenkins分配构建号...').start();
321
+ const spinner = ora({ text: '等待分配构建号...', indent: 2 }).start();
351
322
  let buildNumber = null;
352
323
  let count = 0;
353
- while (!buildNumber && count < 60) { // 最多等2分钟
324
+ while (!buildNumber && count < 60) {
354
325
  try {
355
326
  const response = await this.axios.get(`${this.jenkinsConfig.baseUrl}queue/item/${queueId}/api/json`, {
356
327
  timeout: 10000
@@ -358,85 +329,89 @@ class BuildCommand {
358
329
  const item = response.data;
359
330
  if (item.executable && item.executable.number) {
360
331
  buildNumber = item.executable.number;
361
- spinner.succeed(chalk.green(`✅ 已分配构建号: ${buildNumber}`));
332
+ spinner.succeed('构建号 ' + chalk.cyan(`#${buildNumber}`));
362
333
  break;
363
334
  }
364
- spinner.text = '等待Jenkins分配构建号...';
365
335
  } catch (e) {
366
- spinner.text = '队列查询中...';
336
+ // 队列查询失败不影响主流程
367
337
  }
368
338
  await new Promise(r => setTimeout(r, 2000));
369
339
  count++;
370
340
  }
371
341
  if (!buildNumber) {
372
- spinner.fail('超时未分配构建号');
342
+ spinner.fail('超时未分配构建号');
373
343
  return;
374
344
  }
375
- // 监听构建进度
376
345
  await this.monitorBuild(jobName, buildNumber, buildStartTime);
377
346
  }
378
347
 
379
348
  async monitorBuild(jobName, buildNumber, buildStartTime) {
349
+ const formatTime = (ms) => {
350
+ const s = Math.floor(ms / 1000);
351
+ return s >= 60 ? `${Math.floor(s / 60)}m${s % 60}s` : `${s}s`;
352
+ };
353
+
380
354
  const spinner = ora({
381
- text: 'Jenkins构建中... [BUILDING] 0s',
355
+ text: '构建中 ' + chalk.dim('0s'),
356
+ indent: 2,
382
357
  spinner: 'dots'
383
358
  }).start();
359
+
360
+ // 独立定时器刷新计时,不受 API 请求阻塞
361
+ const timer = setInterval(() => {
362
+ spinner.text = '构建中 ' + chalk.dim(formatTime(Date.now() - buildStartTime));
363
+ }, 200);
364
+
384
365
  let building = true;
385
366
  let lastLog = '';
386
367
  let lastLogTime = 0;
387
- while (building) {
388
- try {
389
- const response = await this.axios.get(`${this.jenkinsConfig.baseUrl}job/${encodeURIComponent(jobName)}/${buildNumber}/api/json`, {
390
- timeout: 10000
391
- });
392
- const buildInfo = response.data;
393
- building = buildInfo.building;
394
- // 计时
395
- const duration = building
396
- ? Math.floor((Date.now() - buildStartTime) / 1000)
397
- : Math.floor((Date.now() - buildStartTime) / 1000);
398
- spinner.text = `Jenkins构建中... [${buildInfo.result || 'BUILDING'}] ${duration}s`;
399
- // 实时输出日志片段(每3秒刷新一次)
400
- const now = Date.now();
401
- if (now - lastLogTime > 2900) {
402
- try {
403
- const logResponse = await this.axios.get(`${this.jenkinsConfig.baseUrl}job/${encodeURIComponent(jobName)}/${buildNumber}/consoleText`, {
404
- timeout: 10000
405
- });
406
- const log = logResponse.data;
407
- const lines = log.split('\n');
408
- const lastLines = lines.slice(-5).join('\n');
409
- if (lastLines !== lastLog) {
410
- spinner.stop();
411
- process.stdout.write(chalk.gray(lastLines) + '\n');
412
- spinner.start();
413
- spinner.text = `Jenkins构建中... [${buildInfo.result || 'BUILDING'}] ${duration}s`;
414
- lastLog = lastLines;
368
+ try {
369
+ while (building) {
370
+ try {
371
+ const response = await this.axios.get(`${this.jenkinsConfig.baseUrl}job/${encodeURIComponent(jobName)}/${buildNumber}/api/json`, {
372
+ timeout: 10000
373
+ });
374
+ const buildInfo = response.data;
375
+ building = buildInfo.building;
376
+
377
+ // 实时输出日志片段
378
+ const now = Date.now();
379
+ if (now - lastLogTime > 2900) {
380
+ try {
381
+ const logResponse = await this.axios.get(`${this.jenkinsConfig.baseUrl}job/${encodeURIComponent(jobName)}/${buildNumber}/consoleText`, {
382
+ timeout: 10000
383
+ });
384
+ const log = logResponse.data;
385
+ const lines = log.split('\n');
386
+ const lastLines = lines.slice(-5).join('\n');
387
+ if (lastLines !== lastLog) {
388
+ spinner.stop();
389
+ process.stdout.write(chalk.dim(lastLines) + '\n');
390
+ spinner.start();
391
+ lastLog = lastLines;
392
+ }
393
+ lastLogTime = now;
394
+ } catch (logError) {
395
+ // 日志获取失败不影响主流程
415
396
  }
416
- lastLogTime = now;
417
- } catch (logError) {
418
- // 日志获取失败不影响主流程
419
397
  }
420
- }
421
- if (!building) {
422
- spinner.stop();
423
- const totalSeconds = Math.floor((Date.now() - buildStartTime) / 1000);
424
- const min = Math.floor(totalSeconds / 60);
425
- const sec = totalSeconds % 60;
426
- const timeStr = min > 0 ? `${min}分${sec}秒` : `${sec}秒`;
427
- if (buildInfo.result === 'SUCCESS') {
428
- console.log(chalk.green('🎉 Jenkins构建成功!'));
429
- console.log(chalk.green(`⏱️ 总用时: ${timeStr}`));
430
- } else {
431
- console.log(chalk.red(`❌ Jenkins构建失败: ${buildInfo.result}`));
432
- console.log(chalk.red(`⏱️ 总用时: ${timeStr}`));
398
+ if (!building) {
399
+ clearInterval(timer);
400
+ const total = formatTime(Date.now() - buildStartTime);
401
+ if (buildInfo.result === 'SUCCESS') {
402
+ spinner.succeed(chalk.green('构建成功') + chalk.dim(` ${total}`));
403
+ } else {
404
+ spinner.fail(chalk.red(`构建失败 ${buildInfo.result}`) + chalk.dim(` ${total}`));
405
+ }
406
+ break;
433
407
  }
434
- break;
408
+ } catch (e) {
409
+ // 请求失败不影响主流程
435
410
  }
436
- } catch (e) {
437
- spinner.text = 'Jenkins构建中...';
411
+ await new Promise(r => setTimeout(r, 500));
438
412
  }
439
- await new Promise(r => setTimeout(r, 500)); // 500ms刷新一次
413
+ } finally {
414
+ clearInterval(timer);
440
415
  }
441
416
  }
442
417
  }