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.
- package/lib/commands/build.js +118 -143
- package/lib/utils/browser.js +200 -34
- package/package.json +1 -1
- package/.claude/settings.local.json +0 -20
- package/.idea/UniappTool.xml +0 -10
- package/.idea/copilot.data.migration.agent.xml +0 -6
- package/.idea/copilot.data.migration.ask.xml +0 -6
- package/.idea/copilot.data.migration.ask2agent.xml +0 -6
- package/.idea/copilot.data.migration.edit.xml +0 -6
- package/.idea/daodou-command.iml +0 -12
- package/.idea/editorJumperProjectSettings.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/vcs.xml +0 -6
- package/.idea/workspace.xml +0 -84
- package/AI_QUICK_REFERENCE.md +0 -202
- package/CHANGELOG.md +0 -210
- package/COMMAND_DEVELOPMENT_GUIDE.md +0 -504
package/lib/commands/build.js
CHANGED
|
@@ -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('
|
|
26
|
-
console.log('
|
|
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(
|
|
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.
|
|
55
|
+
console.log(chalk.dim(` ${key}`) + ' = ' + chalk.cyan(value));
|
|
57
56
|
}
|
|
58
57
|
});
|
|
59
|
-
console.log(chalk.
|
|
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.
|
|
74
|
+
console.log('');
|
|
75
|
+
console.log(chalk.red(' ✖ 缺少必要配置:'));
|
|
77
76
|
missingKeys.forEach(key => {
|
|
78
|
-
console.
|
|
77
|
+
console.log(chalk.dim(` - ${key}`));
|
|
79
78
|
});
|
|
80
|
-
console.
|
|
81
|
-
console.
|
|
79
|
+
console.log('');
|
|
80
|
+
console.log(chalk.dim(' 请在 .daodourc 或 ~/.daodou/config.json 中补充'));
|
|
82
81
|
if (missingKeys.includes('jobName')) {
|
|
83
|
-
console.
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
175
|
-
console.log(chalk.
|
|
176
|
-
console.log(
|
|
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;
|
|
179
|
-
console.log(
|
|
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
|
-
|
|
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(
|
|
220
|
+
console.log(chalk.yellow(' ⚠ 使用指定分支 ') + chalk.cyan(options.branch));
|
|
239
221
|
return options.branch;
|
|
240
222
|
}
|
|
241
223
|
|
|
242
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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('
|
|
307
|
-
console.error(chalk.red('
|
|
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('
|
|
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('
|
|
321
|
+
const spinner = ora({ text: '等待分配构建号...', indent: 2 }).start();
|
|
351
322
|
let buildNumber = null;
|
|
352
323
|
let count = 0;
|
|
353
|
-
while (!buildNumber && count < 60) {
|
|
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.
|
|
332
|
+
spinner.succeed('构建号 ' + chalk.cyan(`#${buildNumber}`));
|
|
362
333
|
break;
|
|
363
334
|
}
|
|
364
|
-
spinner.text = '等待Jenkins分配构建号...';
|
|
365
335
|
} catch (e) {
|
|
366
|
-
|
|
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: '
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
408
|
+
} catch (e) {
|
|
409
|
+
// 请求失败不影响主流程
|
|
435
410
|
}
|
|
436
|
-
|
|
437
|
-
spinner.text = 'Jenkins构建中...';
|
|
411
|
+
await new Promise(r => setTimeout(r, 500));
|
|
438
412
|
}
|
|
439
|
-
|
|
413
|
+
} finally {
|
|
414
|
+
clearInterval(timer);
|
|
440
415
|
}
|
|
441
416
|
}
|
|
442
417
|
}
|