daodou-command 1.4.6 → 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
  }
@@ -107,6 +101,40 @@ class BuildCommand {
107
101
  baseUrl: null,
108
102
  auth: null
109
103
  };
104
+
105
+ // 创建 axios 实例
106
+ this.axios = axios.create({
107
+ maxRedirects: 0,
108
+ validateStatus: s => s < 400 || s === 401 || s === 403 || s === 302
109
+ });
110
+
111
+ // 请求拦截器:自动添加 Cookie
112
+ this.axios.interceptors.request.use(config => {
113
+ const cookieString = this.browserAuth.getCookieString();
114
+ if (cookieString) {
115
+ config.headers.Cookie = cookieString;
116
+ }
117
+ // 确保有 User-Agent
118
+ if (!config.headers['User-Agent']) {
119
+ config.headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36';
120
+ }
121
+ return config;
122
+ });
123
+
124
+ // 响应拦截器:自动更新 Cookie
125
+ this.axios.interceptors.response.use(async response => {
126
+ const setCookie = response.headers['set-cookie'];
127
+ if (setCookie) {
128
+ await this.browserAuth.updateCookiesFromSetCookie(setCookie);
129
+ }
130
+ return response;
131
+ }, async error => {
132
+ // 即使是错误响应,也可能包含 set-cookie
133
+ if (error.response && error.response.headers && error.response.headers['set-cookie']) {
134
+ await this.browserAuth.updateCookiesFromSetCookie(error.response.headers['set-cookie']);
135
+ }
136
+ return Promise.reject(error);
137
+ });
110
138
  }
111
139
 
112
140
  async execute(options = {}) {
@@ -117,48 +145,37 @@ class BuildCommand {
117
145
  this.jenkinsConfig.baseUrl = config.jenkinsUrl;
118
146
  this.jenkinsBase = config.jenkinsBase;
119
147
  this.jenkinsToken = config.jenkinsToken;
120
- console.log(chalk.blue('🔧 刀豆构建工具启动中...\n'));
121
148
 
122
- // 1. 获取分支名称(强制从Git获取,不依赖配置)
149
+ console.log('');
150
+ console.log(chalk.bold(' 🔧 刀豆构建工具'));
151
+ console.log(chalk.dim(' ─────────────────────────'));
152
+
153
+ // 1. 获取分支名称
123
154
  const branch = await this.getBranch(options);
124
- console.log(chalk.green(`✅ 当前分支: ${chalk.cyan(branch)}\n`));
125
155
 
126
- // 2. 浏览器登录Jenkins
156
+ // 2. 登录 & 认证
127
157
  await this.browserAuth.ensureLogin();
128
-
129
- // 3. 设置Jenkins认证
130
158
  this.setupJenkinsAuth();
131
-
132
- // 4. 检查 Jenkins 会话有效性
133
159
  await this.ensureJenkinsSession();
134
160
 
135
- // 5. 构建任务名称和URL
161
+ // 3. 构建参数
136
162
  const jobName = this.buildJobName(config);
137
163
  const jenkinsUrl = this.buildJobUrl(config, jobName);
138
164
  const params = this.buildParams(config, branch);
139
165
 
140
- // 7. 确认参数
141
- console.log(chalk.yellow('📋 构建参数:'));
142
- 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));
143
170
  Object.entries(params).forEach(([key, value]) => {
144
- if (key === 'token') return; // 不显示 token
145
- console.log(` ${chalk.cyan(key)}: ${chalk.magenta(value)}`);
171
+ if (key === 'token' || key === 'GIT_BRANCH') return;
172
+ console.log(' ' + chalk.dim(key) + ' ' + value);
146
173
  });
174
+ console.log(chalk.dim(' ─────────────────────────'));
175
+ console.log('');
147
176
 
148
- await inquirer.prompt([
149
- {
150
- type: 'input',
151
- name: 'confirm',
152
- message: chalk.yellow('按回车确认开始构建,Ctrl+C 取消...'),
153
- default: undefined,
154
- transformer: () => ''
155
- }
156
- ]);
157
- // 只要不是 Ctrl+C 终止就继续
158
-
159
- // 8. 记录本地计时起点
177
+ // 4. 触发构建
160
178
  const buildStartTime = Date.now();
161
- // 9. 触发Jenkins构建并监听进度
162
179
  await this.triggerAndMonitorBuild(jobName, params, buildStartTime);
163
180
  }
164
181
 
@@ -199,33 +216,19 @@ class BuildCommand {
199
216
  * @returns {Promise<string>} 分支名称
200
217
  */
201
218
  async getBranch(options) {
202
- // 如果命令行指定了分支,优先使用命令行参数
203
219
  if (options.branch) {
204
- console.log(chalk.yellow(`⚠️ 使用命令行指定的分支: ${options.branch}`));
220
+ console.log(chalk.yellow(' ⚠ 使用指定分支 ') + chalk.cyan(options.branch));
205
221
  return options.branch;
206
222
  }
207
223
 
208
- // 否则强制从Git获取当前分支
209
- const spinner = ora('检测当前Git分支...').start();
224
+ const spinner = ora({ text: '检测 Git 分支...', indent: 2 }).start();
210
225
  try {
211
226
  const branch = await getCurrentBranch();
212
- spinner.succeed('分支检测完成');
227
+ spinner.succeed('分支 ' + chalk.cyan(branch));
213
228
  return branch;
214
229
  } catch (error) {
215
230
  spinner.fail('分支检测失败');
216
- throw new Error(`无法检测当前Git分支: ${error.message}\n💡 请确保当前目录是Git仓库,或使用 --branch 参数指定分支`);
217
- }
218
- }
219
-
220
- async detectBranch() {
221
- const spinner = ora('检测当前分支...').start();
222
- try {
223
- const branch = await getCurrentBranch();
224
- spinner.succeed('分支检测完成');
225
- return branch;
226
- } catch (error) {
227
- spinner.fail('分支检测失败');
228
- throw new Error(`无法检测当前分支: ${error.message}`);
231
+ throw new Error(`无法检测 Git 分支: ${error.message}\n 请确保当前目录是 Git 仓库,或使用 --branch 指定`);
229
232
  }
230
233
  }
231
234
 
@@ -242,27 +245,24 @@ class BuildCommand {
242
245
  }
243
246
 
244
247
  async ensureJenkinsSession() {
245
- // 先用当前 cookie 访问 Jenkins 首页,确认是否有效
246
- const cookieString = this.browserAuth.cookies
247
- ? this.browserAuth.cookies.map(c => `${c.name}=${c.value}`).join('; ')
248
- : '';
248
+ const spinner = ora({ text: '验证 Jenkins 会话...', indent: 2 }).start();
249
249
  try {
250
- const resp = await axios.get(this.jenkinsConfig.baseUrl, {
251
- headers: {
252
- Cookie: cookieString,
253
- 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
254
- },
255
- maxRedirects: 0,
256
- validateStatus: s => s < 400 || s === 401 || s === 403 || s === 302
257
- });
258
- // 302 跳转到登录页、401/403 都视为未登录
250
+ const resp = await this.axios.get(this.jenkinsConfig.baseUrl);
259
251
  if (resp.status === 401 || resp.status === 403) throw new Error('未登录');
260
252
  if (resp.status === 302 && resp.headers.location && resp.headers.location.includes('casdoor')) throw new Error('未登录');
261
- // 其它情况视为已登录
253
+ spinner.succeed('认证有效');
262
254
  return true;
263
255
  } catch (e) {
264
- // 自动重新登录
265
- 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 已过期,需要重新登录');
266
266
  await this.browserAuth.login();
267
267
  this.setupJenkinsAuth();
268
268
  return true;
@@ -270,27 +270,24 @@ class BuildCommand {
270
270
  }
271
271
 
272
272
  async triggerAndMonitorBuild(jobName, params, buildStartTime) {
273
- // 1. 触发构建
274
- const spinner = ora('正在触发Jenkins构建...').start();
273
+ const spinner = ora({ text: '触发构建...', indent: 2 }).start();
275
274
  try {
276
275
  const queueId = await this.triggerBuild(jobName, params);
277
- spinner.succeed(chalk.green(`✅ Jenkins构建已触发,队列号: ${queueId}`));
278
- // 2. 监听队列,获取build number
276
+ spinner.succeed('构建已触发 ' + chalk.dim(`队列 #${queueId}`));
279
277
  await this.monitorQueueAndBuild(jobName, queueId, buildStartTime);
280
278
  } catch (error) {
281
- spinner.fail('❌ Jenkins构建触发失败');
282
- console.error(chalk.red('构建失败:'), error.message);
279
+ spinner.fail('构建触发失败');
280
+ console.error(chalk.red(' ' + error.message));
283
281
  }
284
282
  }
285
283
 
286
284
  async triggerBuild(jobName, params) {
287
285
  try {
288
286
  const url = `${this.jenkinsConfig.baseUrl}job/${encodeURIComponent(jobName)}/buildWithParameters`;
289
- let headers = { ...this.jenkinsConfig.auth.headers };
287
+ let headers = {};
290
288
  // 1. 获取 crumb(防止 403)
291
289
  try {
292
- const crumbResp = await axios.get(`${this.jenkinsConfig.baseUrl}crumbIssuer/api/json`, {
293
- headers,
290
+ const crumbResp = await this.axios.get(`${this.jenkinsConfig.baseUrl}crumbIssuer/api/json`, {
294
291
  timeout: 10000
295
292
  });
296
293
  const { crumb, crumbRequestField } = crumbResp.data;
@@ -299,7 +296,7 @@ class BuildCommand {
299
296
  // 如果 crumb 获取失败,继续尝试(部分 Jenkins 关闭了 CSRF 防护)
300
297
  }
301
298
  // 2. 触发构建
302
- const response = await axios.post(url, null, {
299
+ const response = await this.axios.post(url, null, {
303
300
  headers,
304
301
  params: params,
305
302
  timeout: 30000
@@ -309,113 +306,112 @@ class BuildCommand {
309
306
  const location = response.headers.location;
310
307
  const queueId = location ? location.replace(/\/$/, '').split('/').pop() : null;
311
308
  if (!queueId) {
312
- console.log(chalk.yellow('⚠️ 未能正确获取队列号,可能是Jenkins配置问题。'));
309
+ console.log(chalk.yellow(' 未能获取队列号'));
313
310
  }
314
311
  return queueId;
315
312
  } else {
316
313
  throw new Error('触发构建失败');
317
314
  }
318
315
  } catch (error) {
319
- console.log(chalk.red('❌ Jenkins构建触发失败: ' + error.message));
320
- console.log(chalk.yellow('💡 请检查Jenkins权限、Token、参数设置,或联系管理员。'));
321
316
  throw new Error(`触发构建失败: ${error.message}`);
322
317
  }
323
318
  }
324
319
 
325
320
  async monitorQueueAndBuild(jobName, queueId, buildStartTime) {
326
- const spinner = ora('等待Jenkins分配构建号...').start();
321
+ const spinner = ora({ text: '等待分配构建号...', indent: 2 }).start();
327
322
  let buildNumber = null;
328
323
  let count = 0;
329
- while (!buildNumber && count < 60) { // 最多等2分钟
324
+ while (!buildNumber && count < 60) {
330
325
  try {
331
- const response = await axios.get(`${this.jenkinsConfig.baseUrl}queue/item/${queueId}/api/json`, {
332
- ...this.jenkinsConfig.auth,
326
+ const response = await this.axios.get(`${this.jenkinsConfig.baseUrl}queue/item/${queueId}/api/json`, {
333
327
  timeout: 10000
334
328
  });
335
329
  const item = response.data;
336
330
  if (item.executable && item.executable.number) {
337
331
  buildNumber = item.executable.number;
338
- spinner.succeed(chalk.green(`✅ 已分配构建号: ${buildNumber}`));
332
+ spinner.succeed('构建号 ' + chalk.cyan(`#${buildNumber}`));
339
333
  break;
340
334
  }
341
- spinner.text = '等待Jenkins分配构建号...';
342
335
  } catch (e) {
343
- spinner.text = '队列查询中...';
336
+ // 队列查询失败不影响主流程
344
337
  }
345
338
  await new Promise(r => setTimeout(r, 2000));
346
339
  count++;
347
340
  }
348
341
  if (!buildNumber) {
349
- spinner.fail('超时未分配构建号');
342
+ spinner.fail('超时未分配构建号');
350
343
  return;
351
344
  }
352
- // 监听构建进度
353
345
  await this.monitorBuild(jobName, buildNumber, buildStartTime);
354
346
  }
355
347
 
356
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
+
357
354
  const spinner = ora({
358
- text: 'Jenkins构建中... [BUILDING] 0s',
355
+ text: '构建中 ' + chalk.dim('0s'),
356
+ indent: 2,
359
357
  spinner: 'dots'
360
358
  }).start();
359
+
360
+ // 独立定时器刷新计时,不受 API 请求阻塞
361
+ const timer = setInterval(() => {
362
+ spinner.text = '构建中 ' + chalk.dim(formatTime(Date.now() - buildStartTime));
363
+ }, 200);
364
+
361
365
  let building = true;
362
366
  let lastLog = '';
363
367
  let lastLogTime = 0;
364
- while (building) {
365
- try {
366
- const response = await axios.get(`${this.jenkinsConfig.baseUrl}job/${encodeURIComponent(jobName)}/${buildNumber}/api/json`, {
367
- ...this.jenkinsConfig.auth,
368
- timeout: 10000
369
- });
370
- const buildInfo = response.data;
371
- building = buildInfo.building;
372
- // 计时
373
- const duration = building
374
- ? Math.floor((Date.now() - buildStartTime) / 1000)
375
- : Math.floor((Date.now() - buildStartTime) / 1000);
376
- spinner.text = `Jenkins构建中... [${buildInfo.result || 'BUILDING'}] ${duration}s`;
377
- // 实时输出日志片段(每3秒刷新一次)
378
- const now = Date.now();
379
- if (now - lastLogTime > 2900) {
380
- try {
381
- const logResponse = await axios.get(`${this.jenkinsConfig.baseUrl}job/${encodeURIComponent(jobName)}/${buildNumber}/consoleText`, {
382
- ...this.jenkinsConfig.auth,
383
- timeout: 10000
384
- });
385
- const log = logResponse.data;
386
- const lines = log.split('\n');
387
- const lastLines = lines.slice(-5).join('\n');
388
- if (lastLines !== lastLog) {
389
- spinner.stop();
390
- process.stdout.write(chalk.gray(lastLines) + '\n');
391
- spinner.start();
392
- spinner.text = `Jenkins构建中... [${buildInfo.result || 'BUILDING'}] ${duration}s`;
393
- 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
+ // 日志获取失败不影响主流程
394
396
  }
395
- lastLogTime = now;
396
- } catch (logError) {
397
- // 日志获取失败不影响主流程
398
397
  }
399
- }
400
- if (!building) {
401
- spinner.stop();
402
- const totalSeconds = Math.floor((Date.now() - buildStartTime) / 1000);
403
- const min = Math.floor(totalSeconds / 60);
404
- const sec = totalSeconds % 60;
405
- const timeStr = min > 0 ? `${min}分${sec}秒` : `${sec}秒`;
406
- if (buildInfo.result === 'SUCCESS') {
407
- console.log(chalk.green('🎉 Jenkins构建成功!'));
408
- console.log(chalk.green(`⏱️ 总用时: ${timeStr}`));
409
- } else {
410
- console.log(chalk.red(`❌ Jenkins构建失败: ${buildInfo.result}`));
411
- 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;
412
407
  }
413
- break;
408
+ } catch (e) {
409
+ // 请求失败不影响主流程
414
410
  }
415
- } catch (e) {
416
- spinner.text = 'Jenkins构建中...';
411
+ await new Promise(r => setTimeout(r, 500));
417
412
  }
418
- await new Promise(r => setTimeout(r, 500)); // 500ms刷新一次
413
+ } finally {
414
+ clearInterval(timer);
419
415
  }
420
416
  }
421
417
  }