hamster-wheel-cli 0.2.0-beta.2 → 0.3.1
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/CHANGELOG.md +29 -2
- package/README.md +56 -19
- package/dist/cli.js +567 -22
- package/dist/cli.js.map +1 -1
- package/dist/index.js +567 -22
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/cli.ts +376 -18
- package/src/global-config.ts +251 -0
- package/src/logs-viewer.ts +13 -1
- package/src/loop.ts +92 -12
- package/src/utils.ts +13 -0
- package/src/webhook.ts +54 -9
- package/tests/e2e/cli.e2e.test.ts +41 -3
- package/tests/global-config.test.ts +89 -1
- package/tests/webhook.test.ts +10 -6
package/dist/index.js
CHANGED
|
@@ -149,6 +149,15 @@ async function runCommand(command, args, options = {}) {
|
|
|
149
149
|
function isoNow() {
|
|
150
150
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
151
151
|
}
|
|
152
|
+
function localTimestamp(date = /* @__PURE__ */ new Date()) {
|
|
153
|
+
const year = date.getFullYear();
|
|
154
|
+
const month = pad2(date.getMonth() + 1);
|
|
155
|
+
const day = pad2(date.getDate());
|
|
156
|
+
const hours = pad2(date.getHours());
|
|
157
|
+
const minutes = pad2(date.getMinutes());
|
|
158
|
+
const seconds = pad2(date.getSeconds());
|
|
159
|
+
return `${year}${month}${day}-${hours}${minutes}${seconds}`;
|
|
160
|
+
}
|
|
152
161
|
function resolvePath(cwd, target) {
|
|
153
162
|
return import_node_path.default.isAbsolute(target) ? target : import_node_path.default.join(cwd, target);
|
|
154
163
|
}
|
|
@@ -392,10 +401,37 @@ function normalizeShortcutName(name) {
|
|
|
392
401
|
function normalizeAliasName(name) {
|
|
393
402
|
return normalizeShortcutName(name);
|
|
394
403
|
}
|
|
404
|
+
function normalizeAgentName(name) {
|
|
405
|
+
return normalizeShortcutName(name);
|
|
406
|
+
}
|
|
395
407
|
function formatTomlString(value) {
|
|
396
408
|
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
397
409
|
return `"${escaped}"`;
|
|
398
410
|
}
|
|
411
|
+
function collectSectionRanges(lines) {
|
|
412
|
+
const ranges = [];
|
|
413
|
+
let current = null;
|
|
414
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
415
|
+
const match = /^\s*\[(.+?)\]\s*$/.exec(lines[i]);
|
|
416
|
+
if (!match) continue;
|
|
417
|
+
if (current) {
|
|
418
|
+
current.end = i;
|
|
419
|
+
ranges.push(current);
|
|
420
|
+
}
|
|
421
|
+
current = {
|
|
422
|
+
name: match[1].trim(),
|
|
423
|
+
start: i,
|
|
424
|
+
end: lines.length
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
if (current) {
|
|
428
|
+
ranges.push(current);
|
|
429
|
+
}
|
|
430
|
+
return ranges;
|
|
431
|
+
}
|
|
432
|
+
function isAgentSection(name) {
|
|
433
|
+
return name === "agent" || name === "agents";
|
|
434
|
+
}
|
|
399
435
|
function updateAliasContent(content, name, command) {
|
|
400
436
|
const lines = content.split(/\r?\n/);
|
|
401
437
|
const entryLine = `${name} = ${formatTomlString(command)}`;
|
|
@@ -439,6 +475,33 @@ ${entryLine}
|
|
|
439
475
|
return output.endsWith("\n") ? output : `${output}
|
|
440
476
|
`;
|
|
441
477
|
}
|
|
478
|
+
function removeAliasContent(content, name) {
|
|
479
|
+
const lines = content.split(/\r?\n/);
|
|
480
|
+
let currentSection = null;
|
|
481
|
+
const removeIndices = [];
|
|
482
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
483
|
+
const sectionMatch = /^\s*\[(.+?)\]\s*$/.exec(lines[i]);
|
|
484
|
+
if (sectionMatch) {
|
|
485
|
+
currentSection = sectionMatch[1].trim();
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (currentSection !== "alias") continue;
|
|
489
|
+
const parsed = parseTomlKeyValue(stripTomlComment(lines[i]).trim());
|
|
490
|
+
if (!parsed) continue;
|
|
491
|
+
if (parsed.key === name) {
|
|
492
|
+
removeIndices.push(i);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (removeIndices.length === 0) {
|
|
496
|
+
return { removed: false, nextContent: ensureTrailingNewline(content) };
|
|
497
|
+
}
|
|
498
|
+
removeIndices.sort((a, b) => b - a).forEach((index) => {
|
|
499
|
+
lines.splice(index, 1);
|
|
500
|
+
});
|
|
501
|
+
const output = lines.join("\n");
|
|
502
|
+
return { removed: true, nextContent: output.endsWith("\n") ? output : `${output}
|
|
503
|
+
` };
|
|
504
|
+
}
|
|
442
505
|
async function upsertAliasEntry(name, command, filePath = getGlobalConfigPath()) {
|
|
443
506
|
const exists = await import_fs_extra2.default.pathExists(filePath);
|
|
444
507
|
const content = exists ? await import_fs_extra2.default.readFile(filePath, "utf8") : "";
|
|
@@ -446,6 +509,104 @@ async function upsertAliasEntry(name, command, filePath = getGlobalConfigPath())
|
|
|
446
509
|
await import_fs_extra2.default.mkdirp(import_node_path3.default.dirname(filePath));
|
|
447
510
|
await import_fs_extra2.default.writeFile(filePath, nextContent, "utf8");
|
|
448
511
|
}
|
|
512
|
+
async function removeAliasEntry(name, filePath = getGlobalConfigPath()) {
|
|
513
|
+
const exists = await import_fs_extra2.default.pathExists(filePath);
|
|
514
|
+
if (!exists) return false;
|
|
515
|
+
const content = await import_fs_extra2.default.readFile(filePath, "utf8");
|
|
516
|
+
const { removed, nextContent } = removeAliasContent(content, name);
|
|
517
|
+
if (!removed) return false;
|
|
518
|
+
await import_fs_extra2.default.writeFile(filePath, nextContent, "utf8");
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
function ensureTrailingNewline(content) {
|
|
522
|
+
if (!content) return "";
|
|
523
|
+
return content.endsWith("\n") ? content : `${content}
|
|
524
|
+
`;
|
|
525
|
+
}
|
|
526
|
+
function findAgentRangeWithEntry(lines, ranges, name) {
|
|
527
|
+
for (const range of ranges) {
|
|
528
|
+
for (let i = range.start + 1; i < range.end; i += 1) {
|
|
529
|
+
const parsed = parseTomlKeyValue(stripTomlComment(lines[i]).trim());
|
|
530
|
+
if (!parsed) continue;
|
|
531
|
+
if (parsed.key === name) return range;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
function updateAgentContent(content, name, command) {
|
|
537
|
+
const lines = content.split(/\r?\n/);
|
|
538
|
+
const entryLine = `${name} = ${formatTomlString(command)}`;
|
|
539
|
+
const ranges = collectSectionRanges(lines);
|
|
540
|
+
const agentRanges = ranges.filter((range) => isAgentSection(range.name));
|
|
541
|
+
let targetRange = findAgentRangeWithEntry(lines, agentRanges, name);
|
|
542
|
+
if (!targetRange) {
|
|
543
|
+
targetRange = agentRanges.find((range) => range.name === "agent") ?? agentRanges.find((range) => range.name === "agents") ?? null;
|
|
544
|
+
}
|
|
545
|
+
if (!targetRange) {
|
|
546
|
+
const trimmed = content.trimEnd();
|
|
547
|
+
const prefix = trimmed.length > 0 ? `${trimmed}
|
|
548
|
+
|
|
549
|
+
` : "";
|
|
550
|
+
return `${prefix}[agent]
|
|
551
|
+
${entryLine}
|
|
552
|
+
`;
|
|
553
|
+
}
|
|
554
|
+
let replaced = false;
|
|
555
|
+
for (let i = targetRange.start + 1; i < targetRange.end; i += 1) {
|
|
556
|
+
const parsed = parseTomlKeyValue(stripTomlComment(lines[i]).trim());
|
|
557
|
+
if (!parsed) continue;
|
|
558
|
+
if (parsed.key === name) {
|
|
559
|
+
lines[i] = entryLine;
|
|
560
|
+
replaced = true;
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (!replaced) {
|
|
565
|
+
lines.splice(targetRange.end, 0, entryLine);
|
|
566
|
+
}
|
|
567
|
+
const output = lines.join("\n");
|
|
568
|
+
return output.endsWith("\n") ? output : `${output}
|
|
569
|
+
`;
|
|
570
|
+
}
|
|
571
|
+
function removeAgentContent(content, name) {
|
|
572
|
+
const lines = content.split(/\r?\n/);
|
|
573
|
+
const ranges = collectSectionRanges(lines).filter((range) => isAgentSection(range.name));
|
|
574
|
+
const removeIndices = [];
|
|
575
|
+
for (const range of ranges) {
|
|
576
|
+
for (let i = range.start + 1; i < range.end; i += 1) {
|
|
577
|
+
const parsed = parseTomlKeyValue(stripTomlComment(lines[i]).trim());
|
|
578
|
+
if (!parsed) continue;
|
|
579
|
+
if (parsed.key === name) {
|
|
580
|
+
removeIndices.push(i);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (removeIndices.length === 0) {
|
|
585
|
+
return { removed: false, nextContent: ensureTrailingNewline(content) };
|
|
586
|
+
}
|
|
587
|
+
removeIndices.sort((a, b) => b - a).forEach((index) => {
|
|
588
|
+
lines.splice(index, 1);
|
|
589
|
+
});
|
|
590
|
+
const output = lines.join("\n");
|
|
591
|
+
return { removed: true, nextContent: output.endsWith("\n") ? output : `${output}
|
|
592
|
+
` };
|
|
593
|
+
}
|
|
594
|
+
async function upsertAgentEntry(name, command, filePath = getGlobalConfigPath()) {
|
|
595
|
+
const exists = await import_fs_extra2.default.pathExists(filePath);
|
|
596
|
+
const content = exists ? await import_fs_extra2.default.readFile(filePath, "utf8") : "";
|
|
597
|
+
const nextContent = updateAgentContent(content, name, command);
|
|
598
|
+
await import_fs_extra2.default.mkdirp(import_node_path3.default.dirname(filePath));
|
|
599
|
+
await import_fs_extra2.default.writeFile(filePath, nextContent, "utf8");
|
|
600
|
+
}
|
|
601
|
+
async function removeAgentEntry(name, filePath = getGlobalConfigPath()) {
|
|
602
|
+
const exists = await import_fs_extra2.default.pathExists(filePath);
|
|
603
|
+
if (!exists) return false;
|
|
604
|
+
const content = await import_fs_extra2.default.readFile(filePath, "utf8");
|
|
605
|
+
const { removed, nextContent } = removeAgentContent(content, name);
|
|
606
|
+
if (!removed) return false;
|
|
607
|
+
await import_fs_extra2.default.writeFile(filePath, nextContent, "utf8");
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
449
610
|
function parseGlobalConfig(content) {
|
|
450
611
|
const lines = content.split(/\r?\n/);
|
|
451
612
|
let currentSection = null;
|
|
@@ -512,6 +673,34 @@ function parseAliasEntries(content) {
|
|
|
512
673
|
}
|
|
513
674
|
return entries;
|
|
514
675
|
}
|
|
676
|
+
function parseAgentEntries(content) {
|
|
677
|
+
const lines = content.split(/\r?\n/);
|
|
678
|
+
let currentSection = null;
|
|
679
|
+
const entries = [];
|
|
680
|
+
const names = /* @__PURE__ */ new Set();
|
|
681
|
+
for (const rawLine of lines) {
|
|
682
|
+
const line = stripTomlComment(rawLine).trim();
|
|
683
|
+
if (!line) continue;
|
|
684
|
+
const sectionMatch = /^\[(.+)\]$/.exec(line);
|
|
685
|
+
if (sectionMatch) {
|
|
686
|
+
currentSection = sectionMatch[1].trim();
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (!isAgentSection(currentSection)) continue;
|
|
690
|
+
const parsed = parseTomlKeyValue(line);
|
|
691
|
+
if (!parsed) continue;
|
|
692
|
+
const name = normalizeShortcutName(parsed.key);
|
|
693
|
+
const command = parsed.value.trim();
|
|
694
|
+
if (!name || !command) continue;
|
|
695
|
+
if (names.has(name)) continue;
|
|
696
|
+
names.add(name);
|
|
697
|
+
entries.push({
|
|
698
|
+
name,
|
|
699
|
+
command
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
return entries;
|
|
703
|
+
}
|
|
515
704
|
async function loadGlobalConfig(logger) {
|
|
516
705
|
const filePath = getGlobalConfigPath();
|
|
517
706
|
const exists = await import_fs_extra2.default.pathExists(filePath);
|
|
@@ -1215,7 +1404,7 @@ async function readLogLines(logFile) {
|
|
|
1215
1404
|
}
|
|
1216
1405
|
function buildListHeader(state, columns) {
|
|
1217
1406
|
const total = state.logs.length;
|
|
1218
|
-
const title = `\u65E5\u5FD7\u5217\u8868\uFF08${total} \u6761\uFF09\uFF5C\u2191/\u2193 \u9009\u62E9 Enter \u67E5\u770B q \u9000\u51FA`;
|
|
1407
|
+
const title = `\u65E5\u5FD7\u5217\u8868\uFF08${total} \u6761\uFF09\uFF5C\u2191/\u2193 \u9009\u62E9 PageUp/PageDown \u7FFB\u9875 Enter \u67E5\u770B q \u9000\u51FA`;
|
|
1219
1408
|
return truncateLine2(title, columns);
|
|
1220
1409
|
}
|
|
1221
1410
|
function buildListStatus(state, columns) {
|
|
@@ -1430,6 +1619,18 @@ async function runLogsViewer() {
|
|
|
1430
1619
|
render2(state);
|
|
1431
1620
|
return;
|
|
1432
1621
|
}
|
|
1622
|
+
if (isPageUp(input)) {
|
|
1623
|
+
const pageSize = getPageSize2(getTerminalSize2().rows);
|
|
1624
|
+
state.selectedIndex = clampIndex2(state.selectedIndex - pageSize, state.logs.length);
|
|
1625
|
+
render2(state);
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
if (isPageDown(input)) {
|
|
1629
|
+
const pageSize = getPageSize2(getTerminalSize2().rows);
|
|
1630
|
+
state.selectedIndex = clampIndex2(state.selectedIndex + pageSize, state.logs.length);
|
|
1631
|
+
render2(state);
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1433
1634
|
if (isEnter(input)) {
|
|
1434
1635
|
void openView();
|
|
1435
1636
|
return;
|
|
@@ -2535,13 +2736,26 @@ function normalizeWebhookUrls(urls) {
|
|
|
2535
2736
|
return urls.map((url) => url.trim()).filter((url) => url.length > 0);
|
|
2536
2737
|
}
|
|
2537
2738
|
function buildWebhookPayload(input) {
|
|
2538
|
-
|
|
2539
|
-
event: input.event,
|
|
2540
|
-
task: input.task,
|
|
2739
|
+
const base = {
|
|
2541
2740
|
branch: input.branch ?? "",
|
|
2542
2741
|
iteration: input.iteration,
|
|
2543
2742
|
stage: input.stage,
|
|
2544
|
-
timestamp: input.timestamp ??
|
|
2743
|
+
timestamp: input.timestamp ?? localTimestamp(),
|
|
2744
|
+
project: input.project,
|
|
2745
|
+
commit: input.commit ?? "",
|
|
2746
|
+
pr: input.pr ?? "",
|
|
2747
|
+
...input.plan !== void 0 ? { plan: input.plan } : {}
|
|
2748
|
+
};
|
|
2749
|
+
if (input.event === "task_start") {
|
|
2750
|
+
return {
|
|
2751
|
+
...base,
|
|
2752
|
+
event: "task_start",
|
|
2753
|
+
task: input.task
|
|
2754
|
+
};
|
|
2755
|
+
}
|
|
2756
|
+
return {
|
|
2757
|
+
...base,
|
|
2758
|
+
event: input.event
|
|
2545
2759
|
};
|
|
2546
2760
|
}
|
|
2547
2761
|
function resolveFetcher(fetcher) {
|
|
@@ -2599,11 +2813,14 @@ function trimOutput(output, limit = MAX_LOG_LENGTH) {
|
|
|
2599
2813
|
return `${output.slice(0, limit)}
|
|
2600
2814
|
\u2026\u2026\uFF08\u8F93\u51FA\u5DF2\u622A\u65AD\uFF0C\u539F\u59CB\u957F\u5EA6 ${output.length} \u5B57\u7B26\uFF09`;
|
|
2601
2815
|
}
|
|
2602
|
-
function truncateText(text, limit =
|
|
2816
|
+
function truncateText(text, limit = 100) {
|
|
2603
2817
|
const trimmed = text.trim();
|
|
2604
2818
|
if (trimmed.length <= limit) return trimmed;
|
|
2605
2819
|
return `${trimmed.slice(0, limit)}...`;
|
|
2606
2820
|
}
|
|
2821
|
+
function normalizePlanForWebhook(plan) {
|
|
2822
|
+
return plan.replace(/\r\n?/g, "\n");
|
|
2823
|
+
}
|
|
2607
2824
|
async function safeCommandOutput(command, args, cwd, logger, label, verboseCommand) {
|
|
2608
2825
|
const result = await runCommand(command, args, {
|
|
2609
2826
|
cwd,
|
|
@@ -2617,6 +2834,41 @@ async function safeCommandOutput(command, args, cwd, logger, label, verboseComma
|
|
|
2617
2834
|
}
|
|
2618
2835
|
return result.stdout.trim();
|
|
2619
2836
|
}
|
|
2837
|
+
function normalizeWebhookUrl(value) {
|
|
2838
|
+
if (!value) return "";
|
|
2839
|
+
const trimmed = value.trim();
|
|
2840
|
+
if (!/^https?:\/\//i.test(trimmed)) return "";
|
|
2841
|
+
return trimmed;
|
|
2842
|
+
}
|
|
2843
|
+
function normalizeRepoUrl(remoteUrl) {
|
|
2844
|
+
const trimmed = remoteUrl.trim();
|
|
2845
|
+
if (!trimmed) return null;
|
|
2846
|
+
const withoutGit = trimmed.replace(/\.git$/i, "");
|
|
2847
|
+
if (withoutGit.includes("://")) {
|
|
2848
|
+
try {
|
|
2849
|
+
const parsed = new URL(withoutGit);
|
|
2850
|
+
const protocol = parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.protocol : "https:";
|
|
2851
|
+
const pathname = parsed.pathname.replace(/\.git$/i, "");
|
|
2852
|
+
return `${protocol}//${parsed.host}${pathname}`.replace(/\/+$/, "");
|
|
2853
|
+
} catch {
|
|
2854
|
+
return null;
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
const scpMatch = withoutGit.match(/^(?:[^@]+@)?([^:]+):(.+)$/);
|
|
2858
|
+
if (!scpMatch) return null;
|
|
2859
|
+
const host = scpMatch[1];
|
|
2860
|
+
const repoPath = scpMatch[2];
|
|
2861
|
+
return `https://${host}/${repoPath}`.replace(/\/+$/, "");
|
|
2862
|
+
}
|
|
2863
|
+
async function resolveCommitLink(cwd, logger) {
|
|
2864
|
+
const sha = await safeCommandOutput("git", ["rev-parse", "HEAD"], cwd, logger, "git", "git rev-parse HEAD");
|
|
2865
|
+
if (!sha) return "";
|
|
2866
|
+
const remote = await safeCommandOutput("git", ["remote", "get-url", "origin"], cwd, logger, "git", "git remote get-url origin");
|
|
2867
|
+
if (!remote) return "";
|
|
2868
|
+
const repoUrl = normalizeRepoUrl(remote);
|
|
2869
|
+
if (!repoUrl) return "";
|
|
2870
|
+
return `${repoUrl}/commit/${sha}`;
|
|
2871
|
+
}
|
|
2620
2872
|
async function runSingleTest(kind, command, cwd, logger) {
|
|
2621
2873
|
const label = kind === "unit" ? "\u5355\u5143\u6D4B\u8BD5" : "e2e \u6D4B\u8BD5";
|
|
2622
2874
|
logger.info(`\u6267\u884C${label}: ${command}`);
|
|
@@ -2809,14 +3061,33 @@ async function runLoop(config) {
|
|
|
2809
3061
|
let prInfo = null;
|
|
2810
3062
|
let prFailed = false;
|
|
2811
3063
|
let sessionIndex = 0;
|
|
3064
|
+
let commitLink = "";
|
|
3065
|
+
let prLink = "";
|
|
3066
|
+
let commitCreated = false;
|
|
3067
|
+
let pushSucceeded = false;
|
|
2812
3068
|
const preWorktreeRecords = [];
|
|
2813
|
-
const
|
|
2814
|
-
|
|
3069
|
+
const resolveProjectName = () => import_node_path9.default.basename(workDir);
|
|
3070
|
+
const notifyWebhook = async (event, iteration, stage, plan) => {
|
|
3071
|
+
const project = resolveProjectName();
|
|
3072
|
+
const payload = event === "task_start" ? buildWebhookPayload({
|
|
2815
3073
|
event,
|
|
2816
3074
|
task: config.task,
|
|
2817
3075
|
branch: branchName,
|
|
2818
3076
|
iteration,
|
|
2819
|
-
stage
|
|
3077
|
+
stage,
|
|
3078
|
+
project,
|
|
3079
|
+
commit: commitLink,
|
|
3080
|
+
pr: prLink,
|
|
3081
|
+
plan
|
|
3082
|
+
}) : buildWebhookPayload({
|
|
3083
|
+
event,
|
|
3084
|
+
branch: branchName,
|
|
3085
|
+
iteration,
|
|
3086
|
+
stage,
|
|
3087
|
+
project,
|
|
3088
|
+
commit: commitLink,
|
|
3089
|
+
pr: prLink,
|
|
3090
|
+
plan
|
|
2820
3091
|
});
|
|
2821
3092
|
await sendWebhookNotifications(config.webhooks, payload, logger);
|
|
2822
3093
|
};
|
|
@@ -2894,7 +3165,8 @@ async function runLoop(config) {
|
|
|
2894
3165
|
});
|
|
2895
3166
|
const runAiSession = async (stage, prompt, extras) => {
|
|
2896
3167
|
sessionIndex += 1;
|
|
2897
|
-
|
|
3168
|
+
const webhookPlan = stage === "\u8BA1\u5212\u751F\u6210" ? normalizePlanForWebhook(await readFileSafe(workflowFiles.planFile)) : void 0;
|
|
3169
|
+
await notifyWebhook("iteration_start", sessionIndex, stage, webhookPlan);
|
|
2898
3170
|
logger.info(`${stage} \u63D0\u793A\u6784\u5EFA\u5B8C\u6210\uFF0C\u8C03\u7528 AI CLI...`);
|
|
2899
3171
|
const aiResult = await runAi(prompt, aiConfig, logger, extras?.cwd ?? workDir);
|
|
2900
3172
|
accumulatedUsage = mergeTokenUsage(accumulatedUsage, aiResult.usage);
|
|
@@ -3110,6 +3382,7 @@ async function runLoop(config) {
|
|
|
3110
3382
|
};
|
|
3111
3383
|
try {
|
|
3112
3384
|
const committed = await commitAll(commitMessage, workDir, logger);
|
|
3385
|
+
commitCreated = committed;
|
|
3113
3386
|
deliveryNotes.push(committed ? `\u81EA\u52A8\u63D0\u4EA4\uFF1A\u5DF2\u63D0\u4EA4\uFF08${commitMessage.title}\uFF09` : "\u81EA\u52A8\u63D0\u4EA4\uFF1A\u672A\u751F\u6210\u63D0\u4EA4\uFF08\u53EF\u80FD\u65E0\u53D8\u66F4\u6216\u63D0\u4EA4\u5931\u8D25\uFF09");
|
|
3114
3387
|
} catch (error) {
|
|
3115
3388
|
deliveryNotes.push(`\u81EA\u52A8\u63D0\u4EA4\uFF1A\u5931\u8D25\uFF08${String(error)}\uFF09`);
|
|
@@ -3126,6 +3399,7 @@ async function runLoop(config) {
|
|
|
3126
3399
|
} else {
|
|
3127
3400
|
try {
|
|
3128
3401
|
await pushBranch(branchName, workDir, logger);
|
|
3402
|
+
pushSucceeded = true;
|
|
3129
3403
|
deliveryNotes.push(`\u81EA\u52A8\u63A8\u9001\uFF1A\u5DF2\u63A8\u9001\uFF08${branchName}\uFF09`);
|
|
3130
3404
|
} catch (error) {
|
|
3131
3405
|
deliveryNotes.push(`\u81EA\u52A8\u63A8\u9001\uFF1A\u5931\u8D25\uFF08${String(error)}\uFF09`);
|
|
@@ -3134,6 +3408,9 @@ async function runLoop(config) {
|
|
|
3134
3408
|
} else {
|
|
3135
3409
|
deliveryNotes.push("\u81EA\u52A8\u63A8\u9001\uFF1A\u672A\u5F00\u542F");
|
|
3136
3410
|
}
|
|
3411
|
+
if (commitCreated && pushSucceeded) {
|
|
3412
|
+
commitLink = await resolveCommitLink(workDir, logger);
|
|
3413
|
+
}
|
|
3137
3414
|
if (config.pr.enable) {
|
|
3138
3415
|
if (lastTestFailed) {
|
|
3139
3416
|
deliveryNotes.push("PR \u521B\u5EFA\uFF1A\u5DF2\u8DF3\u8FC7\uFF08\u6D4B\u8BD5\u672A\u901A\u8FC7\uFF09");
|
|
@@ -3192,6 +3469,7 @@ async function runLoop(config) {
|
|
|
3192
3469
|
} else {
|
|
3193
3470
|
deliveryNotes.push("PR \u521B\u5EFA\uFF1A\u672A\u5F00\u542F\uFF08\u7F3A\u5C11\u5206\u652F\u540D\uFF09");
|
|
3194
3471
|
}
|
|
3472
|
+
prLink = normalizeWebhookUrl(prInfo?.url);
|
|
3195
3473
|
if (deliveryNotes.length > 0) {
|
|
3196
3474
|
const record = formatSystemRecord("\u63D0\u4EA4\u4E0EPR", deliveryNotes.join("\n"), isoNow());
|
|
3197
3475
|
await appendSection(workflowFiles.notesFile, record);
|
|
@@ -3843,6 +4121,8 @@ var RUN_OPTION_SPECS = [
|
|
|
3843
4121
|
var RUN_OPTION_FLAG_MAP = new Map(
|
|
3844
4122
|
RUN_OPTION_SPECS.flatMap((spec) => spec.flags.map((flag) => [flag, spec]))
|
|
3845
4123
|
);
|
|
4124
|
+
var USE_ALIAS_FLAG = "--use-alias";
|
|
4125
|
+
var USE_AGENT_FLAG = "--use-agent";
|
|
3846
4126
|
function parseInteger(value, defaultValue) {
|
|
3847
4127
|
const parsed = Number.parseInt(value, 10);
|
|
3848
4128
|
if (Number.isNaN(parsed)) return defaultValue;
|
|
@@ -3872,7 +4152,21 @@ function buildBackgroundArgs(argv, logFile, branchName, injectBranch = false) {
|
|
|
3872
4152
|
}
|
|
3873
4153
|
function extractAliasCommandArgs(argv, name) {
|
|
3874
4154
|
const args = argv.slice(2);
|
|
3875
|
-
const start = args.findIndex((arg, index) =>
|
|
4155
|
+
const start = args.findIndex((arg, index) => {
|
|
4156
|
+
const legacyMatch = arg === "set" && args[index + 1] === "alias" && args[index + 2] === name;
|
|
4157
|
+
const aliasMatch = isAliasCommandToken(arg) && args[index + 1] === "set" && args[index + 2] === name;
|
|
4158
|
+
return legacyMatch || aliasMatch;
|
|
4159
|
+
});
|
|
4160
|
+
if (start < 0) return [];
|
|
4161
|
+
const rest = args.slice(start + 3);
|
|
4162
|
+
if (rest[0] === "--") return rest.slice(1);
|
|
4163
|
+
return rest;
|
|
4164
|
+
}
|
|
4165
|
+
function extractAgentCommandArgs(argv, action, name) {
|
|
4166
|
+
const args = argv.slice(2);
|
|
4167
|
+
const start = args.findIndex(
|
|
4168
|
+
(arg, index) => arg === "agent" && args[index + 1] === action && args[index + 2] === name
|
|
4169
|
+
);
|
|
3876
4170
|
if (start < 0) return [];
|
|
3877
4171
|
const rest = args.slice(start + 3);
|
|
3878
4172
|
if (rest[0] === "--") return rest.slice(1);
|
|
@@ -3891,7 +4185,7 @@ function extractAliasRunArgs(argv, name) {
|
|
|
3891
4185
|
if (rest[0] === "--") return rest.slice(1);
|
|
3892
4186
|
return rest;
|
|
3893
4187
|
}
|
|
3894
|
-
function
|
|
4188
|
+
function normalizeRunCommandArgs(args) {
|
|
3895
4189
|
let start = 0;
|
|
3896
4190
|
if (args[start] === "wheel-ai") {
|
|
3897
4191
|
start += 1;
|
|
@@ -3962,18 +4256,133 @@ function parseArgSegments(tokens) {
|
|
|
3962
4256
|
}
|
|
3963
4257
|
return segments;
|
|
3964
4258
|
}
|
|
3965
|
-
function
|
|
3966
|
-
const
|
|
4259
|
+
function mergeRunCommandArgs(baseTokens, additionTokens) {
|
|
4260
|
+
const baseSegments = parseArgSegments(baseTokens);
|
|
3967
4261
|
const additionSegments = parseArgSegments(additionTokens);
|
|
3968
4262
|
const overrideNames = new Set(
|
|
3969
4263
|
additionSegments.flatMap((segment) => segment.name ? [segment.name] : [])
|
|
3970
4264
|
);
|
|
3971
4265
|
const merged = [
|
|
3972
|
-
...
|
|
4266
|
+
...baseSegments.filter((segment) => !segment.name || !overrideNames.has(segment.name)),
|
|
3973
4267
|
...additionSegments
|
|
3974
4268
|
];
|
|
3975
4269
|
return merged.flatMap((segment) => segment.tokens);
|
|
3976
4270
|
}
|
|
4271
|
+
function extractRunCommandArgs(argv) {
|
|
4272
|
+
const args = argv.slice(2);
|
|
4273
|
+
const start = args.findIndex((arg) => arg === "run");
|
|
4274
|
+
if (start < 0) return [];
|
|
4275
|
+
return args.slice(start + 1);
|
|
4276
|
+
}
|
|
4277
|
+
function parseUseOptionToken(token, flag) {
|
|
4278
|
+
if (token === flag) {
|
|
4279
|
+
return { matched: true };
|
|
4280
|
+
}
|
|
4281
|
+
if (token.startsWith(`${flag}=`)) {
|
|
4282
|
+
return { matched: true, value: token.slice(flag.length + 1) };
|
|
4283
|
+
}
|
|
4284
|
+
return { matched: false };
|
|
4285
|
+
}
|
|
4286
|
+
function resolveUseOption(tokens, index) {
|
|
4287
|
+
const token = tokens[index];
|
|
4288
|
+
const aliasMatch = parseUseOptionToken(token, USE_ALIAS_FLAG);
|
|
4289
|
+
if (aliasMatch.matched) {
|
|
4290
|
+
const value = aliasMatch.value ?? tokens[index + 1];
|
|
4291
|
+
if (!value) {
|
|
4292
|
+
throw new Error(`${USE_ALIAS_FLAG} \u9700\u8981\u63D0\u4F9B\u540D\u79F0`);
|
|
4293
|
+
}
|
|
4294
|
+
const nextIndex = aliasMatch.value ? index + 1 : index + 2;
|
|
4295
|
+
return { type: "alias", name: value, nextIndex };
|
|
4296
|
+
}
|
|
4297
|
+
const agentMatch = parseUseOptionToken(token, USE_AGENT_FLAG);
|
|
4298
|
+
if (agentMatch.matched) {
|
|
4299
|
+
const value = agentMatch.value ?? tokens[index + 1];
|
|
4300
|
+
if (!value) {
|
|
4301
|
+
throw new Error(`${USE_AGENT_FLAG} \u9700\u8981\u63D0\u4F9B\u540D\u79F0`);
|
|
4302
|
+
}
|
|
4303
|
+
const nextIndex = agentMatch.value ? index + 1 : index + 2;
|
|
4304
|
+
return { type: "agent", name: value, nextIndex };
|
|
4305
|
+
}
|
|
4306
|
+
return null;
|
|
4307
|
+
}
|
|
4308
|
+
function buildAliasRunArgs(entry) {
|
|
4309
|
+
return normalizeRunCommandArgs(splitCommandArgs(entry.command));
|
|
4310
|
+
}
|
|
4311
|
+
function buildAgentRunArgs(entry) {
|
|
4312
|
+
const tokens = normalizeRunCommandArgs(splitCommandArgs(entry.command));
|
|
4313
|
+
if (tokens.length === 0) return [];
|
|
4314
|
+
if (tokens[0].startsWith("-")) {
|
|
4315
|
+
return tokens;
|
|
4316
|
+
}
|
|
4317
|
+
const [command, ...args] = tokens;
|
|
4318
|
+
if (args.length === 0) {
|
|
4319
|
+
return ["--ai-cli", command];
|
|
4320
|
+
}
|
|
4321
|
+
return ["--ai-cli", command, "--ai-args", ...args];
|
|
4322
|
+
}
|
|
4323
|
+
function expandRunTokens(tokens, lookup, stack) {
|
|
4324
|
+
let mergedTokens = [];
|
|
4325
|
+
let buffer = [];
|
|
4326
|
+
let expanded = false;
|
|
4327
|
+
let index = 0;
|
|
4328
|
+
while (index < tokens.length) {
|
|
4329
|
+
const token = tokens[index];
|
|
4330
|
+
if (token === "--") {
|
|
4331
|
+
buffer.push(...tokens.slice(index));
|
|
4332
|
+
break;
|
|
4333
|
+
}
|
|
4334
|
+
const match = resolveUseOption(tokens, index);
|
|
4335
|
+
if (!match) {
|
|
4336
|
+
buffer.push(token);
|
|
4337
|
+
index += 1;
|
|
4338
|
+
continue;
|
|
4339
|
+
}
|
|
4340
|
+
if (buffer.length > 0) {
|
|
4341
|
+
mergedTokens = mergeRunCommandArgs(mergedTokens, buffer);
|
|
4342
|
+
buffer = [];
|
|
4343
|
+
}
|
|
4344
|
+
expanded = true;
|
|
4345
|
+
if (match.type === "alias") {
|
|
4346
|
+
const normalized2 = normalizeAliasName(match.name);
|
|
4347
|
+
if (!normalized2) {
|
|
4348
|
+
throw new Error("alias \u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A\u4E14\u4E0D\u80FD\u5305\u542B\u7A7A\u767D\u5B57\u7B26");
|
|
4349
|
+
}
|
|
4350
|
+
if (stack.alias.has(normalized2)) {
|
|
4351
|
+
throw new Error(`alias \u5FAA\u73AF\u5F15\u7528\uFF1A${normalized2}`);
|
|
4352
|
+
}
|
|
4353
|
+
const entry2 = lookup.aliasEntries.get(normalized2);
|
|
4354
|
+
if (!entry2) {
|
|
4355
|
+
throw new Error(`\u672A\u627E\u5230 alias\uFF1A${normalized2}`);
|
|
4356
|
+
}
|
|
4357
|
+
stack.alias.add(normalized2);
|
|
4358
|
+
const resolved2 = expandRunTokens(buildAliasRunArgs(entry2), lookup, stack);
|
|
4359
|
+
stack.alias.delete(normalized2);
|
|
4360
|
+
mergedTokens = mergeRunCommandArgs(mergedTokens, resolved2.tokens);
|
|
4361
|
+
index = match.nextIndex;
|
|
4362
|
+
continue;
|
|
4363
|
+
}
|
|
4364
|
+
const normalized = normalizeAgentName(match.name);
|
|
4365
|
+
if (!normalized) {
|
|
4366
|
+
throw new Error("agent \u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A\u4E14\u4E0D\u80FD\u5305\u542B\u7A7A\u767D\u5B57\u7B26");
|
|
4367
|
+
}
|
|
4368
|
+
if (stack.agent.has(normalized)) {
|
|
4369
|
+
throw new Error(`agent \u5FAA\u73AF\u5F15\u7528\uFF1A${normalized}`);
|
|
4370
|
+
}
|
|
4371
|
+
const entry = lookup.agentEntries.get(normalized);
|
|
4372
|
+
if (!entry) {
|
|
4373
|
+
throw new Error(`\u672A\u627E\u5230 agent\uFF1A${normalized}`);
|
|
4374
|
+
}
|
|
4375
|
+
stack.agent.add(normalized);
|
|
4376
|
+
const resolved = expandRunTokens(buildAgentRunArgs(entry), lookup, stack);
|
|
4377
|
+
stack.agent.delete(normalized);
|
|
4378
|
+
mergedTokens = mergeRunCommandArgs(mergedTokens, resolved.tokens);
|
|
4379
|
+
index = match.nextIndex;
|
|
4380
|
+
}
|
|
4381
|
+
if (buffer.length > 0) {
|
|
4382
|
+
mergedTokens = mergeRunCommandArgs(mergedTokens, buffer);
|
|
4383
|
+
}
|
|
4384
|
+
return { tokens: mergedTokens, expanded };
|
|
4385
|
+
}
|
|
3977
4386
|
async function runForegroundWithDetach(options) {
|
|
3978
4387
|
const args = buildBackgroundArgs(options.argv, options.logFile, options.branchName, options.injectBranch);
|
|
3979
4388
|
const child = (0, import_node_child_process.spawn)(process.execPath, [...process.execArgv, ...args], {
|
|
@@ -4057,12 +4466,45 @@ async function runCli(argv) {
|
|
|
4057
4466
|
const globalConfig = await loadGlobalConfig(defaultLogger);
|
|
4058
4467
|
const effectiveArgv = applyShortcutArgv(argv, globalConfig);
|
|
4059
4468
|
const program = new import_commander.Command();
|
|
4060
|
-
program.name("wheel-ai").description("\u57FA\u4E8E AI CLI \u7684\u6301\u7EED\u8FED\u4EE3\u5F00\u53D1\u5DE5\u5177").version("
|
|
4469
|
+
program.name("wheel-ai").description("\u57FA\u4E8E AI CLI \u7684\u6301\u7EED\u8FED\u4EE3\u5F00\u53D1\u5DE5\u5177").version("0.2.1");
|
|
4061
4470
|
program.addHelpText(
|
|
4062
4471
|
"after",
|
|
4063
|
-
"\
|
|
4472
|
+
"\nalias \u7BA1\u7406\uFF1A\n wheel-ai alias set <name> <options...>\n wheel-ai alias list\n wheel-ai alias delete <name>\n\nalias/agent \u53E0\u52A0\uFF1A\n wheel-ai run --use-alias <name> [--use-alias <name>...]\n wheel-ai run --use-agent <name> [--use-agent <name>...]\n \u540C\u540D\u9009\u9879\u6309\u51FA\u73B0\u987A\u5E8F\u8986\u76D6\u3002\n"
|
|
4064
4473
|
);
|
|
4065
|
-
program.command("run").option("-t, --task <task>", "\u9700\u8981\u5B8C\u6210\u7684\u4EFB\u52A1\u63CF\u8FF0\uFF08\u53EF\u91CD\u590D\u4F20\u5165\uFF0C\u72EC\u7ACB\u5904\u7406\uFF09", collect, []).option("-i, --iterations <number>", "\u6700\u5927\u8FED\u4EE3\u6B21\u6570", (value) => parseInteger(value, 5), 5).option("--ai-cli <command>", "AI CLI \u547D\u4EE4", "claude").option("--ai-args <args...>", "AI CLI \u53C2\u6570", []).option("--ai-prompt-arg <flag>", "\u7528\u4E8E\u4F20\u5165 prompt \u7684\u53C2\u6570\uFF08\u4E3A\u7A7A\u5219\u4F7F\u7528 stdin\uFF09").option("--notes-file <path>", "\u6301\u4E45\u5316\u8BB0\u5FC6\u6587\u4EF6", defaultNotesPath()).option("--plan-file <path>", "\u8BA1\u5212\u6587\u4EF6", defaultPlanPath()).option("--workflow-doc <path>", "AI \u5DE5\u4F5C\u6D41\u7A0B\u8BF4\u660E\u6587\u4EF6", defaultWorkflowDoc()).option("--worktree", "\u5728\u72EC\u7ACB worktree \u4E0A\u6267\u884C", false).option("--branch <name>", "worktree \u5206\u652F\u540D\uFF08\u9ED8\u8BA4\u81EA\u52A8\u751F\u6210\u6216\u5F53\u524D\u5206\u652F\uFF09").option("--worktree-path <path>", "worktree \u8DEF\u5F84\uFF0C\u9ED8\u8BA4 ../worktrees/<branch>").option("--base-branch <name>", "\u521B\u5EFA\u5206\u652F\u7684\u57FA\u7EBF\u5206\u652F", "main").option("--skip-install", "\u8DF3\u8FC7\u5F00\u59CB\u4EFB\u52A1\u524D\u7684\u4F9D\u8D56\u68C0\u67E5", false).option("--run-tests", "\u8FD0\u884C\u5355\u5143\u6D4B\u8BD5\u547D\u4EE4", false).option("--run-e2e", "\u8FD0\u884C e2e \u6D4B\u8BD5\u547D\u4EE4", false).option("--unit-command <cmd>", "\u5355\u5143\u6D4B\u8BD5\u547D\u4EE4", "yarn test").option("--e2e-command <cmd>", "e2e \u6D4B\u8BD5\u547D\u4EE4", "yarn e2e").option("--auto-commit", "\u81EA\u52A8 git commit", false).option("--auto-push", "\u81EA\u52A8 git push", false).option("--pr", "\u4F7F\u7528 gh \u521B\u5EFA PR", false).option("--pr-title <title>", "PR \u6807\u9898").option("--pr-body <path>", "PR \u63CF\u8FF0\u6587\u4EF6\u8DEF\u5F84\uFF08\u53EF\u7559\u7A7A\u81EA\u52A8\u751F\u6210\uFF09").option("--draft", "\u4EE5\u8349\u7A3F\u5F62\u5F0F\u521B\u5EFA PR", false).option("--reviewer <user...>", "PR reviewers", collect, []).option("--auto-merge", "PR \u68C0\u67E5\u901A\u8FC7\u540E\u81EA\u52A8\u5408\u5E76", false).option("--webhook <url>", "webhook \u901A\u77E5 URL\uFF08\u53EF\u91CD\u590D\uFF09", collect, []).option("--webhook-timeout <ms>", "webhook \u8BF7\u6C42\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09", (value) => parseInteger(value, 8e3)).option("--multi-task-mode <mode>", "\u591A\u4EFB\u52A1\u6267\u884C\u6A21\u5F0F\uFF08relay/serial/serial-continue/parallel\uFF0C\u6216\u4E2D\u6587\u63CF\u8FF0\uFF09", "relay").option("--stop-signal <token>", "AI \u8F93\u51FA\u4E2D\u7684\u505C\u6B62\u6807\u8BB0", "<<DONE>>").option("--log-file <path>", "\u65E5\u5FD7\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84").option("--background", "\u5207\u5165\u540E\u53F0\u8FD0\u884C", false).option("-v, --verbose", "\u8F93\u51FA\u8C03\u8BD5\u65E5\u5FD7", false).option("--skip-quality", "\u8DF3\u8FC7\u4EE3\u7801\u8D28\u91CF\u68C0\u67E5", false).action(async (options) => {
|
|
4474
|
+
program.command("run").option("-t, --task <task>", "\u9700\u8981\u5B8C\u6210\u7684\u4EFB\u52A1\u63CF\u8FF0\uFF08\u53EF\u91CD\u590D\u4F20\u5165\uFF0C\u72EC\u7ACB\u5904\u7406\uFF09", collect, []).option("--use-alias <name>", "\u53E0\u52A0 alias \u914D\u7F6E\uFF08\u53EF\u91CD\u590D\uFF09", collect, []).option("--use-agent <name>", "\u53E0\u52A0 agent \u914D\u7F6E\uFF08\u53EF\u91CD\u590D\uFF09", collect, []).option("-i, --iterations <number>", "\u6700\u5927\u8FED\u4EE3\u6B21\u6570", (value) => parseInteger(value, 5), 5).option("--ai-cli <command>", "AI CLI \u547D\u4EE4", "claude").option("--ai-args <args...>", "AI CLI \u53C2\u6570", []).option("--ai-prompt-arg <flag>", "\u7528\u4E8E\u4F20\u5165 prompt \u7684\u53C2\u6570\uFF08\u4E3A\u7A7A\u5219\u4F7F\u7528 stdin\uFF09").option("--notes-file <path>", "\u6301\u4E45\u5316\u8BB0\u5FC6\u6587\u4EF6", defaultNotesPath()).option("--plan-file <path>", "\u8BA1\u5212\u6587\u4EF6", defaultPlanPath()).option("--workflow-doc <path>", "AI \u5DE5\u4F5C\u6D41\u7A0B\u8BF4\u660E\u6587\u4EF6", defaultWorkflowDoc()).option("--worktree", "\u5728\u72EC\u7ACB worktree \u4E0A\u6267\u884C", false).option("--branch <name>", "worktree \u5206\u652F\u540D\uFF08\u9ED8\u8BA4\u81EA\u52A8\u751F\u6210\u6216\u5F53\u524D\u5206\u652F\uFF09").option("--worktree-path <path>", "worktree \u8DEF\u5F84\uFF0C\u9ED8\u8BA4 ../worktrees/<branch>").option("--base-branch <name>", "\u521B\u5EFA\u5206\u652F\u7684\u57FA\u7EBF\u5206\u652F", "main").option("--skip-install", "\u8DF3\u8FC7\u5F00\u59CB\u4EFB\u52A1\u524D\u7684\u4F9D\u8D56\u68C0\u67E5", false).option("--run-tests", "\u8FD0\u884C\u5355\u5143\u6D4B\u8BD5\u547D\u4EE4", false).option("--run-e2e", "\u8FD0\u884C e2e \u6D4B\u8BD5\u547D\u4EE4", false).option("--unit-command <cmd>", "\u5355\u5143\u6D4B\u8BD5\u547D\u4EE4", "yarn test").option("--e2e-command <cmd>", "e2e \u6D4B\u8BD5\u547D\u4EE4", "yarn e2e").option("--auto-commit", "\u81EA\u52A8 git commit", false).option("--auto-push", "\u81EA\u52A8 git push", false).option("--pr", "\u4F7F\u7528 gh \u521B\u5EFA PR", false).option("--pr-title <title>", "PR \u6807\u9898").option("--pr-body <path>", "PR \u63CF\u8FF0\u6587\u4EF6\u8DEF\u5F84\uFF08\u53EF\u7559\u7A7A\u81EA\u52A8\u751F\u6210\uFF09").option("--draft", "\u4EE5\u8349\u7A3F\u5F62\u5F0F\u521B\u5EFA PR", false).option("--reviewer <user...>", "PR reviewers", collect, []).option("--auto-merge", "PR \u68C0\u67E5\u901A\u8FC7\u540E\u81EA\u52A8\u5408\u5E76", false).option("--webhook <url>", "webhook \u901A\u77E5 URL\uFF08\u53EF\u91CD\u590D\uFF09", collect, []).option("--webhook-timeout <ms>", "webhook \u8BF7\u6C42\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09", (value) => parseInteger(value, 8e3)).option("--multi-task-mode <mode>", "\u591A\u4EFB\u52A1\u6267\u884C\u6A21\u5F0F\uFF08relay/serial/serial-continue/parallel\uFF0C\u6216\u4E2D\u6587\u63CF\u8FF0\uFF09", "relay").option("--stop-signal <token>", "AI \u8F93\u51FA\u4E2D\u7684\u505C\u6B62\u6807\u8BB0", "<<DONE>>").option("--log-file <path>", "\u65E5\u5FD7\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84").option("--background", "\u5207\u5165\u540E\u53F0\u8FD0\u884C", false).option("-v, --verbose", "\u8F93\u51FA\u8C03\u8BD5\u65E5\u5FD7", false).option("--skip-quality", "\u8DF3\u8FC7\u4EE3\u7801\u8D28\u91CF\u68C0\u67E5", false).action(async (options) => {
|
|
4475
|
+
const rawRunArgs = extractRunCommandArgs(effectiveArgv);
|
|
4476
|
+
const hasUseOptions = rawRunArgs.some(
|
|
4477
|
+
(token) => token === USE_ALIAS_FLAG || token.startsWith(`${USE_ALIAS_FLAG}=`) || token === USE_AGENT_FLAG || token.startsWith(`${USE_AGENT_FLAG}=`)
|
|
4478
|
+
);
|
|
4479
|
+
if (hasUseOptions) {
|
|
4480
|
+
const filePath = getGlobalConfigPath();
|
|
4481
|
+
const exists = await import_fs_extra12.default.pathExists(filePath);
|
|
4482
|
+
const content = exists ? await import_fs_extra12.default.readFile(filePath, "utf8") : "";
|
|
4483
|
+
const aliasEntries = parseAliasEntries(content);
|
|
4484
|
+
const agentEntries = parseAgentEntries(content);
|
|
4485
|
+
const resolved = expandRunTokens(
|
|
4486
|
+
rawRunArgs,
|
|
4487
|
+
{
|
|
4488
|
+
aliasEntries: new Map(aliasEntries.map((entry) => [entry.name, entry])),
|
|
4489
|
+
agentEntries: new Map(agentEntries.map((entry) => [entry.name, entry]))
|
|
4490
|
+
},
|
|
4491
|
+
{
|
|
4492
|
+
alias: /* @__PURE__ */ new Set(),
|
|
4493
|
+
agent: /* @__PURE__ */ new Set()
|
|
4494
|
+
}
|
|
4495
|
+
);
|
|
4496
|
+
if (resolved.expanded) {
|
|
4497
|
+
const nextArgv = [process.argv[0], process.argv[1], "run", ...resolved.tokens];
|
|
4498
|
+
const originalArgv = process.argv;
|
|
4499
|
+
process.argv = nextArgv;
|
|
4500
|
+
try {
|
|
4501
|
+
await runCli(nextArgv);
|
|
4502
|
+
} finally {
|
|
4503
|
+
process.argv = originalArgv;
|
|
4504
|
+
}
|
|
4505
|
+
return;
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4066
4508
|
const tasks = normalizeTaskList(options.task);
|
|
4067
4509
|
if (tasks.length === 0) {
|
|
4068
4510
|
throw new Error("\u9700\u8981\u81F3\u5C11\u63D0\u4F9B\u4E00\u4E2A\u4EFB\u52A1\u63CF\u8FF0");
|
|
@@ -4225,7 +4667,78 @@ async function runCli(argv) {
|
|
|
4225
4667
|
program.command("logs").description("\u67E5\u770B\u5386\u53F2\u65E5\u5FD7").action(async () => {
|
|
4226
4668
|
await runLogsViewer();
|
|
4227
4669
|
});
|
|
4228
|
-
program.command("
|
|
4670
|
+
const agentCommand = program.command("agent").description("\u7BA1\u7406 AI CLI agent \u914D\u7F6E");
|
|
4671
|
+
agentCommand.command("add <name> [command...]").description("\u65B0\u589E agent").allowUnknownOption(true).allowExcessArguments(true).action(async (name) => {
|
|
4672
|
+
const normalized = normalizeAgentName(name);
|
|
4673
|
+
if (!normalized) {
|
|
4674
|
+
throw new Error("agent \u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A\u4E14\u4E0D\u80FD\u5305\u542B\u7A7A\u767D\u5B57\u7B26");
|
|
4675
|
+
}
|
|
4676
|
+
const commandArgs = extractAgentCommandArgs(effectiveArgv, "add", name);
|
|
4677
|
+
const commandLine = formatCommandLine(commandArgs);
|
|
4678
|
+
if (!commandLine) {
|
|
4679
|
+
throw new Error("agent \u547D\u4EE4\u4E0D\u80FD\u4E3A\u7A7A");
|
|
4680
|
+
}
|
|
4681
|
+
const filePath = getGlobalConfigPath();
|
|
4682
|
+
const exists = await import_fs_extra12.default.pathExists(filePath);
|
|
4683
|
+
const content = exists ? await import_fs_extra12.default.readFile(filePath, "utf8") : "";
|
|
4684
|
+
const entries = parseAgentEntries(content);
|
|
4685
|
+
if (entries.some((entry) => entry.name === normalized)) {
|
|
4686
|
+
throw new Error(`agent \u5DF2\u5B58\u5728\uFF1A${normalized}`);
|
|
4687
|
+
}
|
|
4688
|
+
await upsertAgentEntry(normalized, commandLine);
|
|
4689
|
+
console.log(`\u5DF2\u65B0\u589E agent\uFF1A${normalized}`);
|
|
4690
|
+
});
|
|
4691
|
+
agentCommand.command("set <name> [command...]").description("\u5199\u5165 agent").allowUnknownOption(true).allowExcessArguments(true).action(async (name) => {
|
|
4692
|
+
const normalized = normalizeAgentName(name);
|
|
4693
|
+
if (!normalized) {
|
|
4694
|
+
throw new Error("agent \u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A\u4E14\u4E0D\u80FD\u5305\u542B\u7A7A\u767D\u5B57\u7B26");
|
|
4695
|
+
}
|
|
4696
|
+
const commandArgs = extractAgentCommandArgs(effectiveArgv, "set", name);
|
|
4697
|
+
const commandLine = formatCommandLine(commandArgs);
|
|
4698
|
+
if (!commandLine) {
|
|
4699
|
+
throw new Error("agent \u547D\u4EE4\u4E0D\u80FD\u4E3A\u7A7A");
|
|
4700
|
+
}
|
|
4701
|
+
await upsertAgentEntry(normalized, commandLine);
|
|
4702
|
+
console.log(`\u5DF2\u5199\u5165 agent\uFF1A${normalized}`);
|
|
4703
|
+
});
|
|
4704
|
+
agentCommand.command("delete <name>").description("\u5220\u9664 agent").action(async (name) => {
|
|
4705
|
+
const normalized = normalizeAgentName(name);
|
|
4706
|
+
if (!normalized) {
|
|
4707
|
+
throw new Error("agent \u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A\u4E14\u4E0D\u80FD\u5305\u542B\u7A7A\u767D\u5B57\u7B26");
|
|
4708
|
+
}
|
|
4709
|
+
const filePath = getGlobalConfigPath();
|
|
4710
|
+
const exists = await import_fs_extra12.default.pathExists(filePath);
|
|
4711
|
+
if (!exists) {
|
|
4712
|
+
throw new Error(`\u672A\u627E\u5230 agent \u914D\u7F6E\u6587\u4EF6\uFF1A${filePath}`);
|
|
4713
|
+
}
|
|
4714
|
+
const removed = await removeAgentEntry(normalized);
|
|
4715
|
+
if (!removed) {
|
|
4716
|
+
throw new Error(`\u672A\u627E\u5230 agent\uFF1A${normalized}`);
|
|
4717
|
+
}
|
|
4718
|
+
console.log(`\u5DF2\u5220\u9664 agent\uFF1A${normalized}`);
|
|
4719
|
+
});
|
|
4720
|
+
agentCommand.command("list").description("\u5217\u51FA agent \u914D\u7F6E").action(async () => {
|
|
4721
|
+
const filePath = getGlobalConfigPath();
|
|
4722
|
+
const exists = await import_fs_extra12.default.pathExists(filePath);
|
|
4723
|
+
if (!exists) {
|
|
4724
|
+
console.log("\u672A\u53D1\u73B0 agent \u914D\u7F6E");
|
|
4725
|
+
return;
|
|
4726
|
+
}
|
|
4727
|
+
const content = await import_fs_extra12.default.readFile(filePath, "utf8");
|
|
4728
|
+
const entries = parseAgentEntries(content);
|
|
4729
|
+
if (entries.length === 0) {
|
|
4730
|
+
console.log("\u672A\u53D1\u73B0 agent \u914D\u7F6E");
|
|
4731
|
+
return;
|
|
4732
|
+
}
|
|
4733
|
+
entries.forEach((entry) => {
|
|
4734
|
+
console.log(`${entry.name}: ${entry.command}`);
|
|
4735
|
+
});
|
|
4736
|
+
});
|
|
4737
|
+
agentCommand.action(() => {
|
|
4738
|
+
agentCommand.help();
|
|
4739
|
+
});
|
|
4740
|
+
const aliasCommand = program.command("alias").alias("aliases").description("\u7BA1\u7406\u5168\u5C40 alias \u914D\u7F6E");
|
|
4741
|
+
aliasCommand.command("set <name> [options...]").description("\u5199\u5165 alias").allowUnknownOption(true).allowExcessArguments(true).action(async (name) => {
|
|
4229
4742
|
const normalized = normalizeAliasName(name);
|
|
4230
4743
|
if (!normalized) {
|
|
4231
4744
|
throw new Error("alias \u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A\u4E14\u4E0D\u80FD\u5305\u542B\u7A7A\u767D\u5B57\u7B26");
|
|
@@ -4238,7 +4751,39 @@ async function runCli(argv) {
|
|
|
4238
4751
|
await upsertAliasEntry(normalized, commandLine);
|
|
4239
4752
|
console.log(`\u5DF2\u5199\u5165 alias\uFF1A${normalized}`);
|
|
4240
4753
|
});
|
|
4241
|
-
|
|
4754
|
+
aliasCommand.command("list").description("\u5217\u51FA alias \u914D\u7F6E").action(async () => {
|
|
4755
|
+
const filePath = getGlobalConfigPath();
|
|
4756
|
+
const exists = await import_fs_extra12.default.pathExists(filePath);
|
|
4757
|
+
if (!exists) {
|
|
4758
|
+
console.log("\u672A\u53D1\u73B0 alias \u914D\u7F6E");
|
|
4759
|
+
return;
|
|
4760
|
+
}
|
|
4761
|
+
const content = await import_fs_extra12.default.readFile(filePath, "utf8");
|
|
4762
|
+
const entries = parseAliasEntries(content).filter((entry) => entry.source === "alias");
|
|
4763
|
+
if (entries.length === 0) {
|
|
4764
|
+
console.log("\u672A\u53D1\u73B0 alias \u914D\u7F6E");
|
|
4765
|
+
return;
|
|
4766
|
+
}
|
|
4767
|
+
entries.forEach((entry) => {
|
|
4768
|
+
console.log(`${entry.name}: ${entry.command}`);
|
|
4769
|
+
});
|
|
4770
|
+
});
|
|
4771
|
+
aliasCommand.command("delete <name>").description("\u5220\u9664 alias").action(async (name) => {
|
|
4772
|
+
const normalized = normalizeAliasName(name);
|
|
4773
|
+
if (!normalized) {
|
|
4774
|
+
throw new Error("alias \u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A\u4E14\u4E0D\u80FD\u5305\u542B\u7A7A\u767D\u5B57\u7B26");
|
|
4775
|
+
}
|
|
4776
|
+
const filePath = getGlobalConfigPath();
|
|
4777
|
+
const exists = await import_fs_extra12.default.pathExists(filePath);
|
|
4778
|
+
if (!exists) {
|
|
4779
|
+
throw new Error(`\u672A\u627E\u5230 alias \u914D\u7F6E\u6587\u4EF6\uFF1A${filePath}`);
|
|
4780
|
+
}
|
|
4781
|
+
const removed = await removeAliasEntry(normalized);
|
|
4782
|
+
if (!removed) {
|
|
4783
|
+
throw new Error(`\u672A\u627E\u5230 alias\uFF1A${normalized}`);
|
|
4784
|
+
}
|
|
4785
|
+
console.log(`\u5DF2\u5220\u9664 alias\uFF1A${normalized}`);
|
|
4786
|
+
});
|
|
4242
4787
|
aliasCommand.command("run <name> [addition...]").description("\u6267\u884C alias \u5E76\u8FFD\u52A0\u547D\u4EE4").allowUnknownOption(true).allowExcessArguments(true).action(async (name) => {
|
|
4243
4788
|
const normalized = normalizeAliasName(name);
|
|
4244
4789
|
if (!normalized) {
|
|
@@ -4255,9 +4800,9 @@ async function runCli(argv) {
|
|
|
4255
4800
|
if (!entry) {
|
|
4256
4801
|
throw new Error(`\u672A\u627E\u5230 alias\uFF1A${normalized}`);
|
|
4257
4802
|
}
|
|
4258
|
-
const aliasTokens =
|
|
4803
|
+
const aliasTokens = normalizeRunCommandArgs(splitCommandArgs(entry.command));
|
|
4259
4804
|
const additionTokens = extractAliasRunArgs(effectiveArgv, normalized);
|
|
4260
|
-
const mergedTokens =
|
|
4805
|
+
const mergedTokens = mergeRunCommandArgs(aliasTokens, additionTokens);
|
|
4261
4806
|
if (mergedTokens.length === 0) {
|
|
4262
4807
|
throw new Error("alias \u547D\u4EE4\u4E0D\u80FD\u4E3A\u7A7A");
|
|
4263
4808
|
}
|