hamster-wheel-cli 0.1.0 → 0.2.0-beta.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 +27 -1
- package/README.md +29 -0
- package/dist/cli.js +1747 -309
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +1747 -309
- package/dist/index.js.map +1 -1
- package/docs/ai-workflow.md +27 -36
- package/package.json +1 -1
- package/src/ai.ts +346 -1
- package/src/alias-viewer.ts +221 -0
- package/src/cli.ts +263 -29
- package/src/config.ts +6 -2
- package/src/gh.ts +20 -0
- package/src/git.ts +38 -3
- package/src/global-config.ts +149 -11
- package/src/log-tailer.ts +103 -0
- package/src/logs-viewer.ts +33 -11
- package/src/logs.ts +1 -0
- package/src/loop.ts +458 -120
- package/src/monitor.ts +240 -23
- package/src/multi-task.ts +117 -0
- package/src/plan.ts +61 -0
- package/src/quality.ts +48 -0
- package/src/runtime-tracker.ts +2 -1
- package/src/types.ts +23 -0
- package/tests/branch-name.test.ts +28 -0
- package/tests/e2e/cli.e2e.test.ts +41 -0
- package/tests/global-config.test.ts +52 -1
- package/tests/monitor.test.ts +17 -0
- package/tests/multi-task.test.ts +77 -0
package/dist/index.js
CHANGED
|
@@ -38,6 +38,7 @@ module.exports = __toCommonJS(src_exports);
|
|
|
38
38
|
|
|
39
39
|
// src/cli.ts
|
|
40
40
|
var import_node_child_process = require("child_process");
|
|
41
|
+
var import_fs_extra12 = __toESM(require("fs-extra"));
|
|
41
42
|
var import_commander = require("commander");
|
|
42
43
|
|
|
43
44
|
// src/config.ts
|
|
@@ -204,7 +205,8 @@ function buildPrConfig(options) {
|
|
|
204
205
|
title: options.prTitle,
|
|
205
206
|
bodyPath: options.prBody,
|
|
206
207
|
draft: options.draft,
|
|
207
|
-
reviewers: options.reviewers
|
|
208
|
+
reviewers: options.reviewers,
|
|
209
|
+
autoMerge: options.autoMerge
|
|
208
210
|
};
|
|
209
211
|
}
|
|
210
212
|
function buildWebhookConfig(options) {
|
|
@@ -239,7 +241,8 @@ function buildLoopConfig(options, cwd) {
|
|
|
239
241
|
runE2e: options.runE2e,
|
|
240
242
|
autoCommit: options.autoCommit,
|
|
241
243
|
autoPush: options.autoPush,
|
|
242
|
-
skipInstall: options.skipInstall
|
|
244
|
+
skipInstall: options.skipInstall,
|
|
245
|
+
skipQuality: options.skipQuality
|
|
243
246
|
};
|
|
244
247
|
}
|
|
245
248
|
function defaultNotesPath() {
|
|
@@ -370,12 +373,79 @@ function parseTomlString(raw) {
|
|
|
370
373
|
}
|
|
371
374
|
return null;
|
|
372
375
|
}
|
|
376
|
+
function parseTomlKeyValue(line) {
|
|
377
|
+
const equalIndex = findUnquotedIndex(line, "=");
|
|
378
|
+
if (equalIndex <= 0) return null;
|
|
379
|
+
const key = line.slice(0, equalIndex).trim();
|
|
380
|
+
const valuePart = line.slice(equalIndex + 1).trim();
|
|
381
|
+
if (!key || !valuePart) return null;
|
|
382
|
+
const parsedValue = parseTomlString(valuePart);
|
|
383
|
+
if (parsedValue === null) return null;
|
|
384
|
+
return { key, value: parsedValue };
|
|
385
|
+
}
|
|
373
386
|
function normalizeShortcutName(name) {
|
|
374
387
|
const trimmed = name.trim();
|
|
375
388
|
if (!trimmed) return null;
|
|
376
389
|
if (/\s/.test(trimmed)) return null;
|
|
377
390
|
return trimmed;
|
|
378
391
|
}
|
|
392
|
+
function normalizeAliasName(name) {
|
|
393
|
+
return normalizeShortcutName(name);
|
|
394
|
+
}
|
|
395
|
+
function formatTomlString(value) {
|
|
396
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
397
|
+
return `"${escaped}"`;
|
|
398
|
+
}
|
|
399
|
+
function updateAliasContent(content, name, command) {
|
|
400
|
+
const lines = content.split(/\r?\n/);
|
|
401
|
+
const entryLine = `${name} = ${formatTomlString(command)}`;
|
|
402
|
+
let currentSection = null;
|
|
403
|
+
let aliasStart = -1;
|
|
404
|
+
let aliasEnd = lines.length;
|
|
405
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
406
|
+
const match = /^\s*\[(.+?)\]\s*$/.exec(lines[i]);
|
|
407
|
+
if (!match) continue;
|
|
408
|
+
if (currentSection === "alias" && aliasStart >= 0 && aliasEnd === lines.length) {
|
|
409
|
+
aliasEnd = i;
|
|
410
|
+
}
|
|
411
|
+
currentSection = match[1].trim();
|
|
412
|
+
if (currentSection === "alias") {
|
|
413
|
+
aliasStart = i;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (aliasStart < 0) {
|
|
417
|
+
const trimmed = content.trimEnd();
|
|
418
|
+
const prefix = trimmed.length > 0 ? `${trimmed}
|
|
419
|
+
|
|
420
|
+
` : "";
|
|
421
|
+
return `${prefix}[alias]
|
|
422
|
+
${entryLine}
|
|
423
|
+
`;
|
|
424
|
+
}
|
|
425
|
+
let replaced = false;
|
|
426
|
+
for (let i = aliasStart + 1; i < aliasEnd; i += 1) {
|
|
427
|
+
const parsed = parseTomlKeyValue(stripTomlComment(lines[i]).trim());
|
|
428
|
+
if (!parsed) continue;
|
|
429
|
+
if (parsed.key === name) {
|
|
430
|
+
lines[i] = entryLine;
|
|
431
|
+
replaced = true;
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (!replaced) {
|
|
436
|
+
lines.splice(aliasEnd, 0, entryLine);
|
|
437
|
+
}
|
|
438
|
+
const output = lines.join("\n");
|
|
439
|
+
return output.endsWith("\n") ? output : `${output}
|
|
440
|
+
`;
|
|
441
|
+
}
|
|
442
|
+
async function upsertAliasEntry(name, command, filePath = getGlobalConfigPath()) {
|
|
443
|
+
const exists = await import_fs_extra2.default.pathExists(filePath);
|
|
444
|
+
const content = exists ? await import_fs_extra2.default.readFile(filePath, "utf8") : "";
|
|
445
|
+
const nextContent = updateAliasContent(content, name, command);
|
|
446
|
+
await import_fs_extra2.default.mkdirp(import_node_path3.default.dirname(filePath));
|
|
447
|
+
await import_fs_extra2.default.writeFile(filePath, nextContent, "utf8");
|
|
448
|
+
}
|
|
379
449
|
function parseGlobalConfig(content) {
|
|
380
450
|
const lines = content.split(/\r?\n/);
|
|
381
451
|
let currentSection = null;
|
|
@@ -389,14 +459,9 @@ function parseGlobalConfig(content) {
|
|
|
389
459
|
continue;
|
|
390
460
|
}
|
|
391
461
|
if (currentSection !== "shortcut") continue;
|
|
392
|
-
const
|
|
393
|
-
if (
|
|
394
|
-
|
|
395
|
-
const valuePart = line.slice(equalIndex + 1).trim();
|
|
396
|
-
if (!key || !valuePart) continue;
|
|
397
|
-
const parsedValue = parseTomlString(valuePart);
|
|
398
|
-
if (parsedValue === null) continue;
|
|
399
|
-
shortcut[key] = parsedValue;
|
|
462
|
+
const parsed = parseTomlKeyValue(line);
|
|
463
|
+
if (!parsed) continue;
|
|
464
|
+
shortcut[parsed.key] = parsed.value;
|
|
400
465
|
}
|
|
401
466
|
const name = normalizeShortcutName(shortcut.name ?? "");
|
|
402
467
|
const command = (shortcut.command ?? "").trim();
|
|
@@ -410,6 +475,43 @@ function parseGlobalConfig(content) {
|
|
|
410
475
|
}
|
|
411
476
|
};
|
|
412
477
|
}
|
|
478
|
+
function parseAliasEntries(content) {
|
|
479
|
+
const lines = content.split(/\r?\n/);
|
|
480
|
+
let currentSection = null;
|
|
481
|
+
const entries = [];
|
|
482
|
+
const names = /* @__PURE__ */ new Set();
|
|
483
|
+
for (const rawLine of lines) {
|
|
484
|
+
const line = stripTomlComment(rawLine).trim();
|
|
485
|
+
if (!line) continue;
|
|
486
|
+
const sectionMatch = /^\[(.+)\]$/.exec(line);
|
|
487
|
+
if (sectionMatch) {
|
|
488
|
+
currentSection = sectionMatch[1].trim();
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (currentSection !== "alias") continue;
|
|
492
|
+
const parsed = parseTomlKeyValue(line);
|
|
493
|
+
if (!parsed) continue;
|
|
494
|
+
const name = normalizeShortcutName(parsed.key);
|
|
495
|
+
const command = parsed.value.trim();
|
|
496
|
+
if (!name || !command) continue;
|
|
497
|
+
if (names.has(name)) continue;
|
|
498
|
+
names.add(name);
|
|
499
|
+
entries.push({
|
|
500
|
+
name,
|
|
501
|
+
command,
|
|
502
|
+
source: "alias"
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
const shortcut = parseGlobalConfig(content).shortcut;
|
|
506
|
+
if (shortcut && !names.has(shortcut.name)) {
|
|
507
|
+
entries.push({
|
|
508
|
+
name: shortcut.name,
|
|
509
|
+
command: shortcut.command,
|
|
510
|
+
source: "shortcut"
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
return entries;
|
|
514
|
+
}
|
|
413
515
|
async function loadGlobalConfig(logger) {
|
|
414
516
|
const filePath = getGlobalConfigPath();
|
|
415
517
|
const exists = await import_fs_extra2.default.pathExists(filePath);
|
|
@@ -672,9 +774,10 @@ async function commitAll(message, cwd, logger) {
|
|
|
672
774
|
});
|
|
673
775
|
if (commit.exitCode !== 0) {
|
|
674
776
|
logger.warn(`git commit \u8DF3\u8FC7\u6216\u5931\u8D25: ${commit.stderr}`);
|
|
675
|
-
return;
|
|
777
|
+
return false;
|
|
676
778
|
}
|
|
677
779
|
logger.success("\u5DF2\u63D0\u4EA4\u5F53\u524D\u53D8\u66F4");
|
|
780
|
+
return true;
|
|
678
781
|
}
|
|
679
782
|
async function pushBranch(branchName, cwd, logger) {
|
|
680
783
|
const push = await runCommand("git", ["push", "-u", "origin", branchName], {
|
|
@@ -712,7 +815,30 @@ async function removeWorktree(worktreePath, repoRoot, logger) {
|
|
|
712
815
|
function generateBranchName() {
|
|
713
816
|
const now = /* @__PURE__ */ new Date();
|
|
714
817
|
const stamp = `${now.getFullYear()}${(now.getMonth() + 1).toString().padStart(2, "0")}${now.getDate().toString().padStart(2, "0")}-${now.getHours().toString().padStart(2, "0")}${now.getMinutes().toString().padStart(2, "0")}`;
|
|
715
|
-
return `wheel-
|
|
818
|
+
return `wheel-ai/${stamp}`;
|
|
819
|
+
}
|
|
820
|
+
function guessBranchType(task) {
|
|
821
|
+
const text = task.toLowerCase();
|
|
822
|
+
if (/fix|bug|修复|错误|异常|问题/.test(text)) return "fix";
|
|
823
|
+
if (/docs|readme|changelog|文档/.test(text)) return "docs";
|
|
824
|
+
if (/test|e2e|单测|测试/.test(text)) return "test";
|
|
825
|
+
if (/refactor|重构/.test(text)) return "refactor";
|
|
826
|
+
if (/chore|构建|依赖|配置/.test(text)) return "chore";
|
|
827
|
+
return "feat";
|
|
828
|
+
}
|
|
829
|
+
function slugifyTask(task) {
|
|
830
|
+
const slug = task.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
831
|
+
return slug.slice(0, 40);
|
|
832
|
+
}
|
|
833
|
+
function buildTimestampSlug(now) {
|
|
834
|
+
const stamp = `${now.getFullYear()}${(now.getMonth() + 1).toString().padStart(2, "0")}${now.getDate().toString().padStart(2, "0")}-${now.getHours().toString().padStart(2, "0")}${now.getMinutes().toString().padStart(2, "0")}`;
|
|
835
|
+
return `auto-${stamp}`;
|
|
836
|
+
}
|
|
837
|
+
function generateBranchNameFromTask(task, now = /* @__PURE__ */ new Date()) {
|
|
838
|
+
const slug = slugifyTask(task);
|
|
839
|
+
const type = guessBranchType(task);
|
|
840
|
+
const suffix = slug || buildTimestampSlug(now);
|
|
841
|
+
return `${type}/${suffix}`;
|
|
716
842
|
}
|
|
717
843
|
|
|
718
844
|
// src/logs.ts
|
|
@@ -800,8 +926,192 @@ async function removeCurrentRegistry(logFile) {
|
|
|
800
926
|
await writeJsonFile(getCurrentRegistryPath(), registry);
|
|
801
927
|
}
|
|
802
928
|
|
|
803
|
-
// src/
|
|
929
|
+
// src/alias-viewer.ts
|
|
804
930
|
var import_fs_extra4 = __toESM(require("fs-extra"));
|
|
931
|
+
function getTerminalSize() {
|
|
932
|
+
const rows = process.stdout.rows ?? 24;
|
|
933
|
+
const columns = process.stdout.columns ?? 80;
|
|
934
|
+
return { rows, columns };
|
|
935
|
+
}
|
|
936
|
+
function truncateLine(line, width) {
|
|
937
|
+
if (width <= 0) return "";
|
|
938
|
+
if (line.length <= width) return line;
|
|
939
|
+
return line.slice(0, width);
|
|
940
|
+
}
|
|
941
|
+
function getPageSize(rows) {
|
|
942
|
+
return Math.max(1, rows - 2);
|
|
943
|
+
}
|
|
944
|
+
function buildAliasLabel(entry) {
|
|
945
|
+
if (entry.source === "shortcut") {
|
|
946
|
+
return `${entry.name}\uFF08shortcut\uFF09`;
|
|
947
|
+
}
|
|
948
|
+
return entry.name;
|
|
949
|
+
}
|
|
950
|
+
function buildHeader(state, columns) {
|
|
951
|
+
const total = state.aliases.length;
|
|
952
|
+
const title = `\u522B\u540D\u5217\u8868\uFF08${total} \u6761\uFF09\uFF5C\u2191/\u2193 \u9009\u62E9 q \u9000\u51FA`;
|
|
953
|
+
return truncateLine(title, columns);
|
|
954
|
+
}
|
|
955
|
+
function buildStatus(state, columns) {
|
|
956
|
+
if (state.aliases.length === 0) {
|
|
957
|
+
if (state.lastError) {
|
|
958
|
+
return truncateLine(`\u8BFB\u53D6\u5931\u8D25\uFF1A${state.lastError}`, columns);
|
|
959
|
+
}
|
|
960
|
+
if (state.missingConfig) {
|
|
961
|
+
return truncateLine(`\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6\uFF1A${getGlobalConfigPath()}`, columns);
|
|
962
|
+
}
|
|
963
|
+
return truncateLine("\u672A\u53D1\u73B0 alias \u914D\u7F6E", columns);
|
|
964
|
+
}
|
|
965
|
+
const entry = state.aliases[state.selectedIndex];
|
|
966
|
+
const sourceText = entry.source === "shortcut" ? "\uFF08shortcut\uFF09" : "";
|
|
967
|
+
return truncateLine(`\u547D\u4EE4${sourceText}\uFF1A${entry.command}`, columns);
|
|
968
|
+
}
|
|
969
|
+
function buildListLine(entry, selected, columns) {
|
|
970
|
+
const marker = selected ? ">" : " ";
|
|
971
|
+
return truncateLine(`${marker} ${buildAliasLabel(entry)}`, columns);
|
|
972
|
+
}
|
|
973
|
+
function ensureListOffset(state, pageSize) {
|
|
974
|
+
const total = state.aliases.length;
|
|
975
|
+
if (total === 0) {
|
|
976
|
+
state.listOffset = 0;
|
|
977
|
+
state.selectedIndex = 0;
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
const maxOffset = Math.max(0, total - pageSize);
|
|
981
|
+
if (state.selectedIndex < state.listOffset) {
|
|
982
|
+
state.listOffset = state.selectedIndex;
|
|
983
|
+
}
|
|
984
|
+
if (state.selectedIndex >= state.listOffset + pageSize) {
|
|
985
|
+
state.listOffset = state.selectedIndex - pageSize + 1;
|
|
986
|
+
}
|
|
987
|
+
state.listOffset = Math.min(Math.max(state.listOffset, 0), maxOffset);
|
|
988
|
+
}
|
|
989
|
+
function render(state) {
|
|
990
|
+
const { rows, columns } = getTerminalSize();
|
|
991
|
+
const pageSize = getPageSize(rows);
|
|
992
|
+
const header = buildHeader(state, columns);
|
|
993
|
+
ensureListOffset(state, pageSize);
|
|
994
|
+
if (state.aliases.length === 0) {
|
|
995
|
+
const filler = Array.from({ length: pageSize }, () => "");
|
|
996
|
+
const status2 = buildStatus(state, columns);
|
|
997
|
+
const content2 = [header, ...filler, status2].join("\n");
|
|
998
|
+
process.stdout.write(`\x1B[2J\x1B[H${content2}`);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
const start = state.listOffset;
|
|
1002
|
+
const slice = state.aliases.slice(start, start + pageSize);
|
|
1003
|
+
const lines = slice.map((entry, index) => {
|
|
1004
|
+
const selected = start + index === state.selectedIndex;
|
|
1005
|
+
return buildListLine(entry, selected, columns);
|
|
1006
|
+
});
|
|
1007
|
+
while (lines.length < pageSize) {
|
|
1008
|
+
lines.push("");
|
|
1009
|
+
}
|
|
1010
|
+
const status = buildStatus(state, columns);
|
|
1011
|
+
const content = [header, ...lines, status].join("\n");
|
|
1012
|
+
process.stdout.write(`\x1B[2J\x1B[H${content}`);
|
|
1013
|
+
}
|
|
1014
|
+
function shouldExit(input) {
|
|
1015
|
+
if (input === "") return true;
|
|
1016
|
+
if (input.toLowerCase() === "q") return true;
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
function isArrowUp(input) {
|
|
1020
|
+
return input.includes("\x1B[A");
|
|
1021
|
+
}
|
|
1022
|
+
function isArrowDown(input) {
|
|
1023
|
+
return input.includes("\x1B[B");
|
|
1024
|
+
}
|
|
1025
|
+
function setupCleanup(cleanup) {
|
|
1026
|
+
const exitHandler = () => {
|
|
1027
|
+
cleanup();
|
|
1028
|
+
};
|
|
1029
|
+
const signalHandler = () => {
|
|
1030
|
+
cleanup();
|
|
1031
|
+
process.exit(0);
|
|
1032
|
+
};
|
|
1033
|
+
process.on("SIGINT", signalHandler);
|
|
1034
|
+
process.on("SIGTERM", signalHandler);
|
|
1035
|
+
process.on("exit", exitHandler);
|
|
1036
|
+
}
|
|
1037
|
+
function clampIndex(value, total) {
|
|
1038
|
+
if (total <= 0) return 0;
|
|
1039
|
+
return Math.min(Math.max(value, 0), total - 1);
|
|
1040
|
+
}
|
|
1041
|
+
async function runAliasViewer() {
|
|
1042
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
1043
|
+
console.log("\u5F53\u524D\u7EC8\u7AEF\u4E0D\u652F\u6301\u4EA4\u4E92\u5F0F alias\u3002");
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
const state = {
|
|
1047
|
+
aliases: [],
|
|
1048
|
+
selectedIndex: 0,
|
|
1049
|
+
listOffset: 0,
|
|
1050
|
+
missingConfig: false
|
|
1051
|
+
};
|
|
1052
|
+
let cleaned = false;
|
|
1053
|
+
const cleanup = () => {
|
|
1054
|
+
if (cleaned) return;
|
|
1055
|
+
cleaned = true;
|
|
1056
|
+
if (process.stdin.isTTY) {
|
|
1057
|
+
process.stdin.setRawMode(false);
|
|
1058
|
+
process.stdin.pause();
|
|
1059
|
+
}
|
|
1060
|
+
process.stdout.write("\x1B[?25h");
|
|
1061
|
+
};
|
|
1062
|
+
setupCleanup(cleanup);
|
|
1063
|
+
process.stdout.write("\x1B[?25l");
|
|
1064
|
+
process.stdin.setRawMode(true);
|
|
1065
|
+
process.stdin.resume();
|
|
1066
|
+
const loadAliases = async () => {
|
|
1067
|
+
const filePath = getGlobalConfigPath();
|
|
1068
|
+
const exists = await import_fs_extra4.default.pathExists(filePath);
|
|
1069
|
+
if (!exists) {
|
|
1070
|
+
state.aliases = [];
|
|
1071
|
+
state.selectedIndex = 0;
|
|
1072
|
+
state.lastError = void 0;
|
|
1073
|
+
state.missingConfig = true;
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
try {
|
|
1077
|
+
const content = await import_fs_extra4.default.readFile(filePath, "utf8");
|
|
1078
|
+
state.aliases = parseAliasEntries(content);
|
|
1079
|
+
state.selectedIndex = clampIndex(state.selectedIndex, state.aliases.length);
|
|
1080
|
+
state.lastError = void 0;
|
|
1081
|
+
state.missingConfig = false;
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1084
|
+
state.aliases = [];
|
|
1085
|
+
state.selectedIndex = 0;
|
|
1086
|
+
state.lastError = message;
|
|
1087
|
+
state.missingConfig = false;
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
await loadAliases();
|
|
1091
|
+
render(state);
|
|
1092
|
+
process.stdin.on("data", (data) => {
|
|
1093
|
+
const input = data.toString("utf8");
|
|
1094
|
+
if (shouldExit(input)) {
|
|
1095
|
+
cleanup();
|
|
1096
|
+
process.exit(0);
|
|
1097
|
+
}
|
|
1098
|
+
if (isArrowUp(input)) {
|
|
1099
|
+
state.selectedIndex = clampIndex(state.selectedIndex - 1, state.aliases.length);
|
|
1100
|
+
render(state);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
if (isArrowDown(input)) {
|
|
1104
|
+
state.selectedIndex = clampIndex(state.selectedIndex + 1, state.aliases.length);
|
|
1105
|
+
render(state);
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
process.stdout.on("resize", () => {
|
|
1109
|
+
render(state);
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// src/logs-viewer.ts
|
|
1114
|
+
var import_fs_extra5 = __toESM(require("fs-extra"));
|
|
805
1115
|
var import_node_path6 = __toESM(require("path"));
|
|
806
1116
|
function isRunMetadata(value) {
|
|
807
1117
|
if (!value || typeof value !== "object") return false;
|
|
@@ -814,10 +1124,10 @@ function buildLogMetaPath(logsDir, logFile) {
|
|
|
814
1124
|
}
|
|
815
1125
|
async function readLogMetadata(logsDir, logFile) {
|
|
816
1126
|
const metaPath = buildLogMetaPath(logsDir, logFile);
|
|
817
|
-
const exists = await
|
|
1127
|
+
const exists = await import_fs_extra5.default.pathExists(metaPath);
|
|
818
1128
|
if (!exists) return void 0;
|
|
819
1129
|
try {
|
|
820
|
-
const content = await
|
|
1130
|
+
const content = await import_fs_extra5.default.readFile(metaPath, "utf8");
|
|
821
1131
|
const parsed = JSON.parse(content);
|
|
822
1132
|
return isRunMetadata(parsed) ? parsed : void 0;
|
|
823
1133
|
} catch {
|
|
@@ -835,10 +1145,10 @@ function buildRunningLogKeys(registry) {
|
|
|
835
1145
|
return keys;
|
|
836
1146
|
}
|
|
837
1147
|
async function loadLogEntries(logsDir, registry) {
|
|
838
|
-
const exists = await
|
|
1148
|
+
const exists = await import_fs_extra5.default.pathExists(logsDir);
|
|
839
1149
|
if (!exists) return [];
|
|
840
1150
|
const running = buildRunningLogKeys(registry);
|
|
841
|
-
const names = await
|
|
1151
|
+
const names = await import_fs_extra5.default.readdir(logsDir);
|
|
842
1152
|
const entries = [];
|
|
843
1153
|
for (const name of names) {
|
|
844
1154
|
if (import_node_path6.default.extname(name).toLowerCase() !== ".log") continue;
|
|
@@ -846,7 +1156,7 @@ async function loadLogEntries(logsDir, registry) {
|
|
|
846
1156
|
const filePath = import_node_path6.default.join(logsDir, name);
|
|
847
1157
|
let stat;
|
|
848
1158
|
try {
|
|
849
|
-
stat = await
|
|
1159
|
+
stat = await import_fs_extra5.default.stat(filePath);
|
|
850
1160
|
} catch {
|
|
851
1161
|
continue;
|
|
852
1162
|
}
|
|
@@ -862,12 +1172,12 @@ async function loadLogEntries(logsDir, registry) {
|
|
|
862
1172
|
}
|
|
863
1173
|
return entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
864
1174
|
}
|
|
865
|
-
function
|
|
1175
|
+
function getTerminalSize2() {
|
|
866
1176
|
const rows = process.stdout.rows ?? 24;
|
|
867
1177
|
const columns = process.stdout.columns ?? 80;
|
|
868
1178
|
return { rows, columns };
|
|
869
1179
|
}
|
|
870
|
-
function
|
|
1180
|
+
function truncateLine2(line, width) {
|
|
871
1181
|
if (width <= 0) return "";
|
|
872
1182
|
if (line.length <= width) return line;
|
|
873
1183
|
return line.slice(0, width);
|
|
@@ -889,12 +1199,12 @@ function formatBytes(size) {
|
|
|
889
1199
|
const mb = kb / 1024;
|
|
890
1200
|
return `${mb.toFixed(1)}MB`;
|
|
891
1201
|
}
|
|
892
|
-
function
|
|
1202
|
+
function getPageSize2(rows) {
|
|
893
1203
|
return Math.max(1, rows - 2);
|
|
894
1204
|
}
|
|
895
1205
|
async function readLogLines(logFile) {
|
|
896
1206
|
try {
|
|
897
|
-
const content = await
|
|
1207
|
+
const content = await import_fs_extra5.default.readFile(logFile, "utf8");
|
|
898
1208
|
const normalized = content.replace(/\r\n?/g, "\n");
|
|
899
1209
|
const lines = normalized.split("\n");
|
|
900
1210
|
return lines.length > 0 ? lines : [""];
|
|
@@ -906,36 +1216,36 @@ async function readLogLines(logFile) {
|
|
|
906
1216
|
function buildListHeader(state, columns) {
|
|
907
1217
|
const total = state.logs.length;
|
|
908
1218
|
const title = `\u65E5\u5FD7\u5217\u8868\uFF08${total} \u6761\uFF09\uFF5C\u2191/\u2193 \u9009\u62E9 Enter \u67E5\u770B q \u9000\u51FA`;
|
|
909
|
-
return
|
|
1219
|
+
return truncateLine2(title, columns);
|
|
910
1220
|
}
|
|
911
1221
|
function buildListStatus(state, columns) {
|
|
912
1222
|
if (state.logs.length === 0) {
|
|
913
1223
|
const text = state.lastError ? `\u52A0\u8F7D\u5931\u8D25\uFF1A${state.lastError}` : "\u6682\u65E0\u53EF\u67E5\u770B\u7684\u65E5\u5FD7";
|
|
914
|
-
return
|
|
1224
|
+
return truncateLine2(text, columns);
|
|
915
1225
|
}
|
|
916
1226
|
const entry = state.logs[state.selectedIndex];
|
|
917
1227
|
const meta = entry.meta;
|
|
918
1228
|
const detail = meta ? `\u9879\u76EE ${meta.path}` : `\u6587\u4EF6 ${entry.fileName}`;
|
|
919
1229
|
const suffix = state.lastError ? ` \uFF5C \u52A0\u8F7D\u5931\u8D25\uFF1A${state.lastError}` : "";
|
|
920
|
-
return
|
|
1230
|
+
return truncateLine2(`${detail}${suffix}`, columns);
|
|
921
1231
|
}
|
|
922
|
-
function
|
|
1232
|
+
function buildListLine2(entry, selected, columns) {
|
|
923
1233
|
const marker = selected ? ">" : " ";
|
|
924
1234
|
const time = formatTimestamp(entry.mtimeMs);
|
|
925
1235
|
const metaInfo = entry.meta ? `\u8F6E\u6B21 ${entry.meta.round} \uFF5C Token ${entry.meta.tokenUsed}` : `\u5927\u5C0F ${formatBytes(entry.size)}`;
|
|
926
|
-
return
|
|
1236
|
+
return truncateLine2(`${marker} ${entry.fileName} \uFF5C ${time} \uFF5C ${metaInfo}`, columns);
|
|
927
1237
|
}
|
|
928
1238
|
function buildViewHeader(entry, columns) {
|
|
929
|
-
const title = `\u65E5\u5FD7\u67E5\u770B\uFF5C${entry.fileName}\uFF5C\u2191/\u2193 \u7FFB\u9875 b \u8FD4\u56DE q \u9000\u51FA`;
|
|
930
|
-
return
|
|
1239
|
+
const title = `\u65E5\u5FD7\u67E5\u770B\uFF5C${entry.fileName}\uFF5C\u2191/\u2193 \u4E0A\u4E0B 1 \u884C PageUp/PageDown \u7FFB\u9875 b \u8FD4\u56DE q \u9000\u51FA`;
|
|
1240
|
+
return truncateLine2(title, columns);
|
|
931
1241
|
}
|
|
932
1242
|
function buildViewStatus(entry, page, columns) {
|
|
933
1243
|
const meta = entry.meta;
|
|
934
1244
|
const metaInfo = meta ? `\u8F6E\u6B21 ${meta.round} \uFF5C Token ${meta.tokenUsed} \uFF5C \u9879\u76EE ${meta.path}` : `\u6587\u4EF6 ${entry.fileName}`;
|
|
935
1245
|
const status = `\u9875 ${page.current}/${page.total} \uFF5C ${metaInfo}`;
|
|
936
|
-
return
|
|
1246
|
+
return truncateLine2(status, columns);
|
|
937
1247
|
}
|
|
938
|
-
function
|
|
1248
|
+
function ensureListOffset2(state, pageSize) {
|
|
939
1249
|
const total = state.logs.length;
|
|
940
1250
|
if (total === 0) {
|
|
941
1251
|
state.listOffset = 0;
|
|
@@ -952,10 +1262,10 @@ function ensureListOffset(state, pageSize) {
|
|
|
952
1262
|
state.listOffset = Math.min(Math.max(state.listOffset, 0), maxOffset);
|
|
953
1263
|
}
|
|
954
1264
|
function renderList(state) {
|
|
955
|
-
const { rows, columns } =
|
|
956
|
-
const pageSize =
|
|
1265
|
+
const { rows, columns } = getTerminalSize2();
|
|
1266
|
+
const pageSize = getPageSize2(rows);
|
|
957
1267
|
const header = buildListHeader(state, columns);
|
|
958
|
-
|
|
1268
|
+
ensureListOffset2(state, pageSize);
|
|
959
1269
|
if (state.logs.length === 0) {
|
|
960
1270
|
const filler = Array.from({ length: pageSize }, () => "");
|
|
961
1271
|
const status2 = buildListStatus(state, columns);
|
|
@@ -967,7 +1277,7 @@ function renderList(state) {
|
|
|
967
1277
|
const slice = state.logs.slice(start, start + pageSize);
|
|
968
1278
|
const lines = slice.map((entry, index) => {
|
|
969
1279
|
const selected = start + index === state.selectedIndex;
|
|
970
|
-
return
|
|
1280
|
+
return buildListLine2(entry, selected, columns);
|
|
971
1281
|
});
|
|
972
1282
|
while (lines.length < pageSize) {
|
|
973
1283
|
lines.push("");
|
|
@@ -977,28 +1287,30 @@ function renderList(state) {
|
|
|
977
1287
|
process.stdout.write(`\x1B[2J\x1B[H${content}`);
|
|
978
1288
|
}
|
|
979
1289
|
function renderView(view) {
|
|
980
|
-
const { rows, columns } =
|
|
981
|
-
const pageSize =
|
|
1290
|
+
const { rows, columns } = getTerminalSize2();
|
|
1291
|
+
const pageSize = getPageSize2(rows);
|
|
982
1292
|
const header = buildViewHeader(view.entry, columns);
|
|
983
|
-
const maxOffset = Math.max(0,
|
|
984
|
-
view.
|
|
985
|
-
const start = view.
|
|
986
|
-
const pageLines = view.lines.slice(start, start + pageSize).map((line) =>
|
|
1293
|
+
const maxOffset = Math.max(0, view.lines.length - pageSize);
|
|
1294
|
+
view.lineOffset = Math.min(Math.max(view.lineOffset, 0), maxOffset);
|
|
1295
|
+
const start = view.lineOffset;
|
|
1296
|
+
const pageLines = view.lines.slice(start, start + pageSize).map((line) => truncateLine2(line, columns));
|
|
987
1297
|
while (pageLines.length < pageSize) {
|
|
988
1298
|
pageLines.push("");
|
|
989
1299
|
}
|
|
990
|
-
const
|
|
1300
|
+
const totalPages = Math.max(1, Math.ceil(view.lines.length / pageSize));
|
|
1301
|
+
const currentPage = Math.min(totalPages, Math.floor(view.lineOffset / pageSize) + 1);
|
|
1302
|
+
const status = buildViewStatus(view.entry, { current: currentPage, total: totalPages }, columns);
|
|
991
1303
|
const content = [header, ...pageLines, status].join("\n");
|
|
992
1304
|
process.stdout.write(`\x1B[2J\x1B[H${content}`);
|
|
993
1305
|
}
|
|
994
|
-
function
|
|
1306
|
+
function render2(state) {
|
|
995
1307
|
if (state.mode === "view" && state.view) {
|
|
996
1308
|
renderView(state.view);
|
|
997
1309
|
return;
|
|
998
1310
|
}
|
|
999
1311
|
renderList(state);
|
|
1000
1312
|
}
|
|
1001
|
-
function
|
|
1313
|
+
function shouldExit2(input) {
|
|
1002
1314
|
if (input === "") return true;
|
|
1003
1315
|
if (input.toLowerCase() === "q") return true;
|
|
1004
1316
|
return false;
|
|
@@ -1006,16 +1318,22 @@ function shouldExit(input) {
|
|
|
1006
1318
|
function isEnter(input) {
|
|
1007
1319
|
return input.includes("\r") || input.includes("\n");
|
|
1008
1320
|
}
|
|
1009
|
-
function
|
|
1321
|
+
function isArrowUp2(input) {
|
|
1010
1322
|
return input.includes("\x1B[A");
|
|
1011
1323
|
}
|
|
1012
|
-
function
|
|
1324
|
+
function isArrowDown2(input) {
|
|
1013
1325
|
return input.includes("\x1B[B");
|
|
1014
1326
|
}
|
|
1327
|
+
function isPageUp(input) {
|
|
1328
|
+
return input.includes("\x1B[5~");
|
|
1329
|
+
}
|
|
1330
|
+
function isPageDown(input) {
|
|
1331
|
+
return input.includes("\x1B[6~");
|
|
1332
|
+
}
|
|
1015
1333
|
function isEscape(input) {
|
|
1016
1334
|
return input === "\x1B";
|
|
1017
1335
|
}
|
|
1018
|
-
function
|
|
1336
|
+
function setupCleanup2(cleanup) {
|
|
1019
1337
|
const exitHandler = () => {
|
|
1020
1338
|
cleanup();
|
|
1021
1339
|
};
|
|
@@ -1027,7 +1345,7 @@ function setupCleanup(cleanup) {
|
|
|
1027
1345
|
process.on("SIGTERM", signalHandler);
|
|
1028
1346
|
process.on("exit", exitHandler);
|
|
1029
1347
|
}
|
|
1030
|
-
function
|
|
1348
|
+
function clampIndex2(value, total) {
|
|
1031
1349
|
if (total <= 0) return 0;
|
|
1032
1350
|
return Math.min(Math.max(value, 0), total - 1);
|
|
1033
1351
|
}
|
|
@@ -1053,7 +1371,7 @@ async function runLogsViewer() {
|
|
|
1053
1371
|
}
|
|
1054
1372
|
process.stdout.write("\x1B[?25h");
|
|
1055
1373
|
};
|
|
1056
|
-
|
|
1374
|
+
setupCleanup2(cleanup);
|
|
1057
1375
|
process.stdout.write("\x1B[?25l");
|
|
1058
1376
|
process.stdin.setRawMode(true);
|
|
1059
1377
|
process.stdin.resume();
|
|
@@ -1062,7 +1380,7 @@ async function runLogsViewer() {
|
|
|
1062
1380
|
try {
|
|
1063
1381
|
const registry = await readCurrentRegistry();
|
|
1064
1382
|
state.logs = await loadLogEntries(logsDir, registry);
|
|
1065
|
-
state.selectedIndex =
|
|
1383
|
+
state.selectedIndex = clampIndex2(state.selectedIndex, state.logs.length);
|
|
1066
1384
|
state.lastError = void 0;
|
|
1067
1385
|
} catch (error) {
|
|
1068
1386
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1079,37 +1397,37 @@ async function runLogsViewer() {
|
|
|
1079
1397
|
state.view = {
|
|
1080
1398
|
entry,
|
|
1081
1399
|
lines: ["\u52A0\u8F7D\u4E2D\u2026"],
|
|
1082
|
-
|
|
1400
|
+
lineOffset: 0
|
|
1083
1401
|
};
|
|
1084
|
-
|
|
1402
|
+
render2(state);
|
|
1085
1403
|
const lines = await readLogLines(entry.filePath);
|
|
1086
|
-
const pageSize =
|
|
1087
|
-
const maxOffset = Math.max(0,
|
|
1404
|
+
const pageSize = getPageSize2(getTerminalSize2().rows);
|
|
1405
|
+
const maxOffset = Math.max(0, lines.length - pageSize);
|
|
1088
1406
|
state.view = {
|
|
1089
1407
|
entry,
|
|
1090
1408
|
lines,
|
|
1091
|
-
|
|
1409
|
+
lineOffset: maxOffset
|
|
1092
1410
|
};
|
|
1093
1411
|
loading = false;
|
|
1094
|
-
|
|
1412
|
+
render2(state);
|
|
1095
1413
|
};
|
|
1096
1414
|
await loadLogs();
|
|
1097
|
-
|
|
1415
|
+
render2(state);
|
|
1098
1416
|
process.stdin.on("data", (data) => {
|
|
1099
1417
|
const input = data.toString("utf8");
|
|
1100
|
-
if (
|
|
1418
|
+
if (shouldExit2(input)) {
|
|
1101
1419
|
cleanup();
|
|
1102
1420
|
process.exit(0);
|
|
1103
1421
|
}
|
|
1104
1422
|
if (state.mode === "list") {
|
|
1105
|
-
if (
|
|
1106
|
-
state.selectedIndex =
|
|
1107
|
-
|
|
1423
|
+
if (isArrowUp2(input)) {
|
|
1424
|
+
state.selectedIndex = clampIndex2(state.selectedIndex - 1, state.logs.length);
|
|
1425
|
+
render2(state);
|
|
1108
1426
|
return;
|
|
1109
1427
|
}
|
|
1110
|
-
if (
|
|
1111
|
-
state.selectedIndex =
|
|
1112
|
-
|
|
1428
|
+
if (isArrowDown2(input)) {
|
|
1429
|
+
state.selectedIndex = clampIndex2(state.selectedIndex + 1, state.logs.length);
|
|
1430
|
+
render2(state);
|
|
1113
1431
|
return;
|
|
1114
1432
|
}
|
|
1115
1433
|
if (isEnter(input)) {
|
|
@@ -1119,63 +1437,267 @@ async function runLogsViewer() {
|
|
|
1119
1437
|
return;
|
|
1120
1438
|
}
|
|
1121
1439
|
if (state.mode === "view" && state.view) {
|
|
1122
|
-
if (
|
|
1123
|
-
state.view.
|
|
1124
|
-
|
|
1440
|
+
if (isArrowUp2(input)) {
|
|
1441
|
+
state.view.lineOffset -= 1;
|
|
1442
|
+
render2(state);
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
if (isArrowDown2(input)) {
|
|
1446
|
+
state.view.lineOffset += 1;
|
|
1447
|
+
render2(state);
|
|
1125
1448
|
return;
|
|
1126
1449
|
}
|
|
1127
|
-
if (
|
|
1128
|
-
|
|
1129
|
-
|
|
1450
|
+
if (isPageUp(input)) {
|
|
1451
|
+
const pageSize = getPageSize2(getTerminalSize2().rows);
|
|
1452
|
+
state.view.lineOffset -= pageSize;
|
|
1453
|
+
render2(state);
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
if (isPageDown(input)) {
|
|
1457
|
+
const pageSize = getPageSize2(getTerminalSize2().rows);
|
|
1458
|
+
state.view.lineOffset += pageSize;
|
|
1459
|
+
render2(state);
|
|
1130
1460
|
return;
|
|
1131
1461
|
}
|
|
1132
1462
|
if (input.toLowerCase() === "b" || isEscape(input)) {
|
|
1133
1463
|
state.mode = "list";
|
|
1134
1464
|
state.view = void 0;
|
|
1135
|
-
|
|
1465
|
+
render2(state);
|
|
1136
1466
|
return;
|
|
1137
1467
|
}
|
|
1138
1468
|
}
|
|
1139
1469
|
});
|
|
1140
1470
|
process.stdout.on("resize", () => {
|
|
1141
|
-
|
|
1471
|
+
render2(state);
|
|
1142
1472
|
});
|
|
1143
1473
|
}
|
|
1144
1474
|
|
|
1145
1475
|
// src/loop.ts
|
|
1146
|
-
var
|
|
1147
|
-
var
|
|
1476
|
+
var import_fs_extra9 = __toESM(require("fs-extra"));
|
|
1477
|
+
var import_node_path9 = __toESM(require("path"));
|
|
1148
1478
|
|
|
1149
1479
|
// src/ai.ts
|
|
1150
|
-
function
|
|
1151
|
-
|
|
1480
|
+
function compactLine(text) {
|
|
1481
|
+
return text.replace(/\s+/g, " ").trim();
|
|
1482
|
+
}
|
|
1483
|
+
function buildBranchNamePrompt(input) {
|
|
1484
|
+
return [
|
|
1485
|
+
"# \u89D2\u8272",
|
|
1486
|
+
"\u4F60\u662F\u8D44\u6DF1\u5DE5\u7A0B\u5E08\uFF0C\u9700\u8981\u6839\u636E\u4EFB\u52A1\u751F\u6210\u89C4\u8303\u7684 git \u5206\u652F\u540D\u3002",
|
|
1487
|
+
"# \u89C4\u5219",
|
|
1488
|
+
"- \u8F93\u51FA\u683C\u5F0F\u4EC5\u9650\u4E25\u683C JSON\uFF08\u4E0D\u8981 markdown\u3001\u4E0D\u8981\u4EE3\u7801\u5757\u3001\u4E0D\u8981\u89E3\u91CA\uFF09\u3002",
|
|
1489
|
+
"- \u5206\u652F\u540D\u683C\u5F0F\uFF1A<type>/<slug>\u3002",
|
|
1490
|
+
"- type \u53EF\u9009\uFF1Afeat\u3001fix\u3001docs\u3001refactor\u3001chore\u3001test\u3002",
|
|
1491
|
+
"- slug \u4F7F\u7528\u5C0F\u5199\u82F1\u6587\u3001\u6570\u5B57\u3001\u8FDE\u5B57\u7B26\uFF0C\u957F\u5EA6 3~40\uFF0C\u907F\u514D\u7A7A\u683C\u4E0E\u4E2D\u6587\u3002",
|
|
1492
|
+
"# \u8F93\u51FA JSON",
|
|
1493
|
+
'{"branch":"..."}',
|
|
1494
|
+
"# \u4EFB\u52A1\u63CF\u8FF0",
|
|
1495
|
+
compactLine(input.task) || "\uFF08\u7A7A\uFF09"
|
|
1496
|
+
].join("\n\n");
|
|
1497
|
+
}
|
|
1498
|
+
function buildPlanningPrompt(input) {
|
|
1499
|
+
return [
|
|
1152
1500
|
"# \u80CC\u666F\u4EFB\u52A1",
|
|
1153
1501
|
input.task,
|
|
1502
|
+
"# \u5206\u652F\u4FE1\u606F",
|
|
1503
|
+
input.branchName ? `\u8BA1\u5212\u4F7F\u7528\u5206\u652F\uFF1A${input.branchName}` : "\u672A\u6307\u5B9A\u5206\u652F\u540D\uFF0C\u8BF7\u6309\u4EFB\u52A1\u8BED\u4E49\u7ED9\u51FA\u5EFA\u8BAE",
|
|
1154
1504
|
"# \u5DE5\u4F5C\u6D41\u7A0B\u57FA\u7EBF\uFF08\u4F9B AI \u81EA\u4E3B\u6267\u884C\uFF09",
|
|
1155
1505
|
input.workflowGuide,
|
|
1156
|
-
"# \u5F53\u524D\
|
|
1157
|
-
input.plan || "\uFF08\u6682\u65E0\u8BA1\u5212\
|
|
1158
|
-
"# \u5386\u53F2\
|
|
1159
|
-
input.notes || "\uFF08\
|
|
1506
|
+
"# \u5F53\u524D\u8BA1\u5212",
|
|
1507
|
+
input.plan || "\uFF08\u6682\u65E0\u8BA1\u5212\uFF09",
|
|
1508
|
+
"# \u5386\u53F2\u8BB0\u5FC6",
|
|
1509
|
+
input.notes || "\uFF08\u6682\u65E0\u5386\u53F2\uFF09",
|
|
1160
1510
|
"# \u672C\u8F6E\u6267\u884C\u8981\u6C42",
|
|
1161
1511
|
[
|
|
1162
|
-
"1. \
|
|
1163
|
-
"2. \
|
|
1164
|
-
"3. \
|
|
1165
|
-
"4. \
|
|
1166
|
-
"5. \
|
|
1167
|
-
"6. \u7EF4\u62A4\u6301\u4E45\u5316\u8BB0\u5FC6\u6587\u4EF6\uFF1A\u6458\u8981\u672C\u8F6E\u5173\u952E\u7ED3\u8BBA\u3001\u9057\u7559\u95EE\u9898\u3001\u4E0B\u4E00\u6B65\u5EFA\u8BAE\u3002",
|
|
1168
|
-
"7. \u51C6\u5907\u63D0\u4EA4 PR \u6240\u9700\u7684\u6807\u9898\u4E0E\u63CF\u8FF0\uFF08\u542B\u53D8\u66F4\u6458\u8981\u3001\u6D4B\u8BD5\u7ED3\u679C\u3001\u98CE\u9669\uFF09\u3002",
|
|
1169
|
-
"8. \u5F53\u6240\u6709\u76EE\u6807\u5B8C\u6210\u65F6\uFF0C\u5728\u8F93\u51FA\u4E2D\u52A0\u5165\u6807\u8BB0 <<DONE>> \u4EE5\u4FBF\u5916\u5C42\u505C\u6B62\u5FAA\u73AF\u3002"
|
|
1512
|
+
"1. \u5206\u6790\u4EFB\u52A1\u8F93\u5165/\u8F93\u51FA/\u7EA6\u675F/\u9A8C\u6536\u6807\u51C6\uFF0C\u5FC5\u8981\u65F6\u8865\u5145\u5408\u7406\u5047\u8BBE\uFF08\u5199\u5165 notes\uFF09\u3002",
|
|
1513
|
+
"2. \u82E5 plan.md \u5DF2\u5B58\u5728\uFF0C\u8BF7\u5224\u65AD\u662F\u5426\u5408\u7406\uFF1B\u5408\u7406\u5219\u4E0D\u4FEE\u6539\uFF0C\u4E0D\u5408\u7406\u5219\u4F18\u5316\u6216\u91CD\u5199\u3002",
|
|
1514
|
+
"3. \u8BA1\u5212\u53EA\u5305\u542B\u5F00\u53D1\u76F8\u5173\u4EFB\u52A1\uFF08\u8BBE\u8BA1/\u5B9E\u73B0/\u91CD\u6784/\u914D\u7F6E/\u6587\u6863\u66F4\u65B0\uFF09\uFF0C\u4E0D\u8981\u5305\u542B\u6D4B\u8BD5\u3001\u81EA\u5BA1\u3001PR\u3001\u63D0\u4EA4\u7B49\u5185\u5BB9\u3002",
|
|
1515
|
+
"4. \u8BA1\u5212\u9879\u9700\u53EF\u6267\u884C\u3001\u9897\u7C92\u5EA6\u6E05\u6670\uFF0C\u5DF2\u5B8C\u6210\u9879\u4F7F\u7528 \u2705 \u6807\u8BB0\u3002",
|
|
1516
|
+
"5. \u66F4\u65B0 memory/plan.md \u4E0E memory/notes.md \u540E\u7ED3\u675F\u672C\u8F6E\u3002"
|
|
1170
1517
|
].join("\n")
|
|
1171
|
-
];
|
|
1172
|
-
return sections.join("\n\n");
|
|
1518
|
+
].join("\n\n");
|
|
1173
1519
|
}
|
|
1174
|
-
function
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1520
|
+
function buildPlanItemPrompt(input) {
|
|
1521
|
+
return [
|
|
1522
|
+
"# \u80CC\u666F\u4EFB\u52A1",
|
|
1523
|
+
input.task,
|
|
1524
|
+
"# \u5DE5\u4F5C\u6D41\u7A0B\u57FA\u7EBF\uFF08\u4F9B AI \u81EA\u4E3B\u6267\u884C\uFF09",
|
|
1525
|
+
input.workflowGuide,
|
|
1526
|
+
"# \u5F53\u524D\u8BA1\u5212",
|
|
1527
|
+
input.plan || "\uFF08\u6682\u65E0\u8BA1\u5212\uFF09",
|
|
1528
|
+
"# \u5386\u53F2\u8BB0\u5FC6",
|
|
1529
|
+
input.notes || "\uFF08\u6682\u65E0\u5386\u53F2\uFF09",
|
|
1530
|
+
"# \u672C\u8F6E\u8981\u6267\u884C\u7684\u8BA1\u5212\u9879\uFF08\u4EC5\u6B64\u4E00\u6761\uFF09",
|
|
1531
|
+
input.item,
|
|
1532
|
+
"# \u672C\u8F6E\u6267\u884C\u8981\u6C42",
|
|
1533
|
+
[
|
|
1534
|
+
"1. \u53EA\u6267\u884C\u4E0A\u8FF0\u8BA1\u5212\u9879\uFF0C\u907F\u514D\u63D0\u524D\u5904\u7406\u5176\u5B83\u8BA1\u5212\u9879\u3002",
|
|
1535
|
+
"2. \u5B8C\u6210\u540E\u7ACB\u5373\u5728 plan.md \u4E2D\u5C06\u8BE5\u9879\u6807\u8BB0\u4E3A \u2705\u3002",
|
|
1536
|
+
"3. \u5FC5\u8981\u65F6\u53EF\u5BF9\u8BA1\u5212\u9879\u8FDB\u884C\u5FAE\u8C03\uFF0C\u4F46\u4ECD\u9700\u786E\u4FDD\u5F53\u524D\u9879\u5B8C\u6210\u3002",
|
|
1537
|
+
"4. \u672C\u8F6E\u4E0D\u6267\u884C\u6D4B\u8BD5\u6216\u8D28\u91CF\u68C0\u67E5\u3002",
|
|
1538
|
+
"5. \u5C06\u8FDB\u5C55\u3001\u5173\u952E\u6539\u52A8\u4E0E\u98CE\u9669\u5199\u5165 notes\u3002"
|
|
1539
|
+
].join("\n")
|
|
1540
|
+
].join("\n\n");
|
|
1541
|
+
}
|
|
1542
|
+
function buildQualityPrompt(input) {
|
|
1543
|
+
return [
|
|
1544
|
+
"# \u80CC\u666F\u4EFB\u52A1",
|
|
1545
|
+
input.task,
|
|
1546
|
+
"# \u5DE5\u4F5C\u6D41\u7A0B\u57FA\u7EBF\uFF08\u4F9B AI \u81EA\u4E3B\u6267\u884C\uFF09",
|
|
1547
|
+
input.workflowGuide,
|
|
1548
|
+
"# \u5F53\u524D\u8BA1\u5212",
|
|
1549
|
+
input.plan || "\uFF08\u6682\u65E0\u8BA1\u5212\uFF09",
|
|
1550
|
+
"# \u5386\u53F2\u8BB0\u5FC6",
|
|
1551
|
+
input.notes || "\uFF08\u6682\u65E0\u5386\u53F2\uFF09",
|
|
1552
|
+
"# \u672C\u8F6E\u4EE3\u7801\u8D28\u91CF\u68C0\u67E5",
|
|
1553
|
+
input.commands.length > 0 ? input.commands.map((cmd) => `- ${cmd}`).join("\n") : "\u672A\u68C0\u6D4B\u5230\u53EF\u6267\u884C\u7684\u8D28\u91CF\u68C0\u67E5\u547D\u4EE4\u3002",
|
|
1554
|
+
input.results ? `# \u547D\u4EE4\u6267\u884C\u7ED3\u679C
|
|
1555
|
+
${input.results}` : "",
|
|
1556
|
+
"# \u672C\u8F6E\u6267\u884C\u8981\u6C42",
|
|
1557
|
+
[
|
|
1558
|
+
"1. \u672C\u8F6E\u4EC5\u8FDB\u884C\u4EE3\u7801\u8D28\u91CF\u68C0\u67E5\uFF0C\u4E0D\u8981\u4FEE\u590D\u95EE\u9898\u3002",
|
|
1559
|
+
"2. \u82E5\u51FA\u73B0\u5931\u8D25\uFF0C\u8BB0\u5F55\u5931\u8D25\u8981\u70B9\uFF0C\u7B49\u5F85\u4E0B\u4E00\u8F6E\u4FEE\u590D\u3002",
|
|
1560
|
+
"3. \u5C06\u7ED3\u8BBA\u4E0E\u98CE\u9669\u5199\u5165 notes\u3002"
|
|
1561
|
+
].join("\n")
|
|
1562
|
+
].filter(Boolean).join("\n\n");
|
|
1563
|
+
}
|
|
1564
|
+
function buildFixPrompt(input) {
|
|
1565
|
+
return [
|
|
1566
|
+
"# \u80CC\u666F\u4EFB\u52A1",
|
|
1567
|
+
input.task,
|
|
1568
|
+
"# \u5DE5\u4F5C\u6D41\u7A0B\u57FA\u7EBF\uFF08\u4F9B AI \u81EA\u4E3B\u6267\u884C\uFF09",
|
|
1569
|
+
input.workflowGuide,
|
|
1570
|
+
"# \u5F53\u524D\u8BA1\u5212",
|
|
1571
|
+
input.plan || "\uFF08\u6682\u65E0\u8BA1\u5212\uFF09",
|
|
1572
|
+
"# \u5386\u53F2\u8BB0\u5FC6",
|
|
1573
|
+
input.notes || "\uFF08\u6682\u65E0\u5386\u53F2\uFF09",
|
|
1574
|
+
`# \u9700\u8981\u4FEE\u590D\u7684\u95EE\u9898\uFF08${input.stage}\uFF09`,
|
|
1575
|
+
input.errors || "\uFF08\u65E0\u9519\u8BEF\u4FE1\u606F\uFF09",
|
|
1576
|
+
"# \u672C\u8F6E\u6267\u884C\u8981\u6C42",
|
|
1577
|
+
[
|
|
1578
|
+
"1. \u805A\u7126\u4FEE\u590D\u5F53\u524D\u95EE\u9898\uFF0C\u4E0D\u8981\u6269\u5C55\u8303\u56F4\u3002",
|
|
1579
|
+
"2. \u4FEE\u590D\u5B8C\u6210\u540E\u66F4\u65B0 notes\uFF0C\u8BF4\u660E\u4FEE\u6539\u70B9\u4E0E\u5F71\u54CD\u3002",
|
|
1580
|
+
"3. \u5982\u9700\u8C03\u6574\u8BA1\u5212\uFF0C\u8BF7\u540C\u6B65\u66F4\u65B0 plan.md\u3002"
|
|
1581
|
+
].join("\n")
|
|
1582
|
+
].join("\n\n");
|
|
1583
|
+
}
|
|
1584
|
+
function buildTestPrompt(input) {
|
|
1585
|
+
return [
|
|
1586
|
+
"# \u80CC\u666F\u4EFB\u52A1",
|
|
1587
|
+
input.task,
|
|
1588
|
+
"# \u5DE5\u4F5C\u6D41\u7A0B\u57FA\u7EBF\uFF08\u4F9B AI \u81EA\u4E3B\u6267\u884C\uFF09",
|
|
1589
|
+
input.workflowGuide,
|
|
1590
|
+
"# \u5F53\u524D\u8BA1\u5212",
|
|
1591
|
+
input.plan || "\uFF08\u6682\u65E0\u8BA1\u5212\uFF09",
|
|
1592
|
+
"# \u5386\u53F2\u8BB0\u5FC6",
|
|
1593
|
+
input.notes || "\uFF08\u6682\u65E0\u5386\u53F2\uFF09",
|
|
1594
|
+
"# \u672C\u8F6E\u6D4B\u8BD5\u547D\u4EE4",
|
|
1595
|
+
input.commands.length > 0 ? input.commands.map((cmd) => `- ${cmd}`).join("\n") : "\u672A\u914D\u7F6E\u6D4B\u8BD5\u547D\u4EE4\u3002",
|
|
1596
|
+
input.results ? `# \u6D4B\u8BD5\u7ED3\u679C
|
|
1597
|
+
${input.results}` : "",
|
|
1598
|
+
"# \u672C\u8F6E\u6267\u884C\u8981\u6C42",
|
|
1599
|
+
[
|
|
1600
|
+
"1. \u672C\u8F6E\u4EC5\u6267\u884C\u6D4B\u8BD5\uFF0C\u4E0D\u8981\u4FEE\u590D\u95EE\u9898\u3002",
|
|
1601
|
+
"2. \u82E5\u51FA\u73B0\u5931\u8D25\uFF0C\u8BB0\u5F55\u5931\u8D25\u8981\u70B9\uFF0C\u7B49\u5F85\u4E0B\u4E00\u8F6E\u4FEE\u590D\u3002",
|
|
1602
|
+
"3. \u5C06\u6D4B\u8BD5\u7ED3\u8BBA\u5199\u5165 notes\u3002"
|
|
1603
|
+
].join("\n")
|
|
1604
|
+
].filter(Boolean).join("\n\n");
|
|
1605
|
+
}
|
|
1606
|
+
function buildDocsPrompt(input) {
|
|
1607
|
+
return [
|
|
1608
|
+
"# \u80CC\u666F\u4EFB\u52A1",
|
|
1609
|
+
input.task,
|
|
1610
|
+
"# \u5DE5\u4F5C\u6D41\u7A0B\u57FA\u7EBF\uFF08\u4F9B AI \u81EA\u4E3B\u6267\u884C\uFF09",
|
|
1611
|
+
input.workflowGuide,
|
|
1612
|
+
"# \u5F53\u524D\u8BA1\u5212",
|
|
1613
|
+
input.plan || "\uFF08\u6682\u65E0\u8BA1\u5212\uFF09",
|
|
1614
|
+
"# \u5386\u53F2\u8BB0\u5FC6",
|
|
1615
|
+
input.notes || "\uFF08\u6682\u65E0\u5386\u53F2\uFF09",
|
|
1616
|
+
"# \u672C\u8F6E\u6267\u884C\u8981\u6C42",
|
|
1617
|
+
[
|
|
1618
|
+
"1. \u6839\u636E\u672C\u6B21\u6539\u52A8\u66F4\u65B0\u7248\u672C\u53F7\u3001CHANGELOG\u3001README\u3001docs \u7B49\u76F8\u5173\u6587\u6863\u3002",
|
|
1619
|
+
"2. \u4EC5\u66F4\u65B0\u786E\u6709\u53D8\u5316\u7684\u6587\u6863\uFF0C\u4FDD\u6301\u4E2D\u6587\u8BF4\u660E\u3002",
|
|
1620
|
+
"3. \u5C06\u66F4\u65B0\u6458\u8981\u5199\u5165 notes\u3002"
|
|
1621
|
+
].join("\n")
|
|
1622
|
+
].join("\n\n");
|
|
1623
|
+
}
|
|
1624
|
+
function extractJson(text) {
|
|
1625
|
+
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
1626
|
+
if (fenced?.[1]) return fenced[1].trim();
|
|
1627
|
+
const start = text.indexOf("{");
|
|
1628
|
+
const end = text.lastIndexOf("}");
|
|
1629
|
+
if (start >= 0 && end > start) {
|
|
1630
|
+
return text.slice(start, end + 1).trim();
|
|
1631
|
+
}
|
|
1632
|
+
return null;
|
|
1633
|
+
}
|
|
1634
|
+
var BRANCH_TYPES = ["feat", "fix", "docs", "refactor", "chore", "test"];
|
|
1635
|
+
var BRANCH_TYPE_ALIASES = {
|
|
1636
|
+
feature: "feat",
|
|
1637
|
+
features: "feat",
|
|
1638
|
+
bugfix: "fix",
|
|
1639
|
+
hotfix: "fix",
|
|
1640
|
+
doc: "docs",
|
|
1641
|
+
documentation: "docs",
|
|
1642
|
+
refactoring: "refactor",
|
|
1643
|
+
chores: "chore",
|
|
1644
|
+
tests: "test"
|
|
1645
|
+
};
|
|
1646
|
+
function isBranchType(value) {
|
|
1647
|
+
return BRANCH_TYPES.includes(value);
|
|
1648
|
+
}
|
|
1649
|
+
function normalizeBranchType(value) {
|
|
1650
|
+
const trimmed = value.trim().toLowerCase();
|
|
1651
|
+
if (!trimmed) return null;
|
|
1652
|
+
if (isBranchType(trimmed)) return trimmed;
|
|
1653
|
+
return BRANCH_TYPE_ALIASES[trimmed] ?? null;
|
|
1654
|
+
}
|
|
1655
|
+
function normalizeBranchSlug(value) {
|
|
1656
|
+
const cleaned = value.toLowerCase().replace(/\s+/g, "-").replace(/_/g, "-").replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
1657
|
+
if (!cleaned) return null;
|
|
1658
|
+
const trimmed = cleaned.slice(0, 40);
|
|
1659
|
+
if (trimmed.length < 3) return null;
|
|
1660
|
+
return trimmed;
|
|
1661
|
+
}
|
|
1662
|
+
function normalizeBranchNameCandidate(value) {
|
|
1663
|
+
const trimmed = value.trim();
|
|
1664
|
+
if (!trimmed) return null;
|
|
1665
|
+
const lowered = trimmed.toLowerCase();
|
|
1666
|
+
const parts = lowered.split("/").filter((part) => part.length > 0);
|
|
1667
|
+
const hasExplicitType = lowered.includes("/") && parts.length >= 2;
|
|
1668
|
+
const rawType = hasExplicitType ? parts.shift() ?? "" : "";
|
|
1669
|
+
const rawSlug = hasExplicitType ? parts.join("-") : lowered;
|
|
1670
|
+
const type = rawType ? normalizeBranchType(rawType) : "feat";
|
|
1671
|
+
if (!type) return null;
|
|
1672
|
+
const slug = normalizeBranchSlug(rawSlug);
|
|
1673
|
+
if (!slug) return null;
|
|
1674
|
+
return `${type}/${slug}`;
|
|
1675
|
+
}
|
|
1676
|
+
function parseBranchName(output) {
|
|
1677
|
+
const jsonText = extractJson(output);
|
|
1678
|
+
if (jsonText) {
|
|
1679
|
+
try {
|
|
1680
|
+
const parsed = JSON.parse(jsonText);
|
|
1681
|
+
const raw = typeof parsed.branch === "string" ? parsed.branch : typeof parsed.branchName === "string" ? parsed.branchName : typeof parsed["\u5206\u652F"] === "string" ? parsed["\u5206\u652F"] : typeof parsed["\u5206\u652F\u540D"] === "string" ? parsed["\u5206\u652F\u540D"] : null;
|
|
1682
|
+
if (raw) {
|
|
1683
|
+
const normalized = normalizeBranchNameCandidate(raw);
|
|
1684
|
+
if (normalized) return normalized;
|
|
1685
|
+
}
|
|
1686
|
+
} catch {
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
const lineMatch = output.match(/(?:branch(?:name)?|分支名|分支)\s*[::]\s*([^\s]+)/i);
|
|
1690
|
+
if (lineMatch?.[1]) {
|
|
1691
|
+
const normalized = normalizeBranchNameCandidate(lineMatch[1]);
|
|
1692
|
+
if (normalized) return normalized;
|
|
1693
|
+
}
|
|
1694
|
+
return null;
|
|
1695
|
+
}
|
|
1696
|
+
function pickNumber(pattern, text) {
|
|
1697
|
+
const match = pattern.exec(text);
|
|
1698
|
+
if (!match || match.length < 2) return void 0;
|
|
1699
|
+
const value = Number.parseInt(match[match.length - 1], 10);
|
|
1700
|
+
return Number.isNaN(value) ? void 0 : value;
|
|
1179
1701
|
}
|
|
1180
1702
|
function parseTokenUsage(logs) {
|
|
1181
1703
|
const total = pickNumber(/total[_\s]tokens:\s*(\d+)/i, logs);
|
|
@@ -1247,8 +1769,9 @@ async function runAi(prompt, ai, logger, cwd) {
|
|
|
1247
1769
|
};
|
|
1248
1770
|
}
|
|
1249
1771
|
function formatIterationRecord(record) {
|
|
1772
|
+
const title = record.stage ? `### \u8FED\u4EE3 ${record.iteration} \uFF5C ${record.timestamp} \uFF5C ${record.stage}` : `### \u8FED\u4EE3 ${record.iteration} \uFF5C ${record.timestamp}`;
|
|
1250
1773
|
const lines = [
|
|
1251
|
-
|
|
1774
|
+
title,
|
|
1252
1775
|
"",
|
|
1253
1776
|
"#### \u63D0\u793A\u4E0A\u4E0B\u6587",
|
|
1254
1777
|
"```",
|
|
@@ -1261,6 +1784,19 @@ function formatIterationRecord(record) {
|
|
|
1261
1784
|
"```",
|
|
1262
1785
|
""
|
|
1263
1786
|
];
|
|
1787
|
+
if (record.checkResults && record.checkResults.length > 0) {
|
|
1788
|
+
lines.push("#### \u8D28\u91CF\u68C0\u67E5\u7ED3\u679C");
|
|
1789
|
+
record.checkResults.forEach((result) => {
|
|
1790
|
+
const status = result.success ? "\u2705 \u901A\u8FC7" : "\u274C \u5931\u8D25";
|
|
1791
|
+
lines.push(`${status} \uFF5C ${result.name} \uFF5C \u547D\u4EE4: ${result.command} \uFF5C \u9000\u51FA\u7801: ${result.exitCode}`);
|
|
1792
|
+
if (!result.success) {
|
|
1793
|
+
lines.push("```");
|
|
1794
|
+
lines.push(result.stderr || result.stdout || "\uFF08\u65E0\u8F93\u51FA\uFF09");
|
|
1795
|
+
lines.push("```");
|
|
1796
|
+
lines.push("");
|
|
1797
|
+
}
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1264
1800
|
if (record.testResults && record.testResults.length > 0) {
|
|
1265
1801
|
lines.push("#### \u6D4B\u8BD5\u7ED3\u679C");
|
|
1266
1802
|
record.testResults.forEach((result) => {
|
|
@@ -1280,7 +1816,7 @@ function formatIterationRecord(record) {
|
|
|
1280
1816
|
|
|
1281
1817
|
// src/deps.ts
|
|
1282
1818
|
var import_node_path7 = __toESM(require("path"));
|
|
1283
|
-
var
|
|
1819
|
+
var import_fs_extra6 = __toESM(require("fs-extra"));
|
|
1284
1820
|
function parsePackageManagerField(value) {
|
|
1285
1821
|
if (!value) return null;
|
|
1286
1822
|
const normalized = value.trim().toLowerCase();
|
|
@@ -1378,20 +1914,20 @@ function extractPackageManagerField(value) {
|
|
|
1378
1914
|
}
|
|
1379
1915
|
async function readPackageManagerHints(cwd, logger) {
|
|
1380
1916
|
const packageJsonPath = import_node_path7.default.join(cwd, "package.json");
|
|
1381
|
-
const hasPackageJson = await
|
|
1917
|
+
const hasPackageJson = await import_fs_extra6.default.pathExists(packageJsonPath);
|
|
1382
1918
|
if (!hasPackageJson) return null;
|
|
1383
1919
|
let packageManagerField;
|
|
1384
1920
|
try {
|
|
1385
|
-
const packageJson = await
|
|
1921
|
+
const packageJson = await import_fs_extra6.default.readJson(packageJsonPath);
|
|
1386
1922
|
packageManagerField = extractPackageManagerField(packageJson);
|
|
1387
1923
|
} catch (error) {
|
|
1388
1924
|
logger.warn(`\u8BFB\u53D6 package.json \u5931\u8D25\uFF0C\u5C06\u6539\u7528\u9501\u6587\u4EF6\u5224\u65AD\u5305\u7BA1\u7406\u5668: ${String(error)}`);
|
|
1389
1925
|
}
|
|
1390
1926
|
const [hasYarnLock, hasPnpmLock, hasNpmLock, hasNpmShrinkwrap] = await Promise.all([
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1927
|
+
import_fs_extra6.default.pathExists(import_node_path7.default.join(cwd, "yarn.lock")),
|
|
1928
|
+
import_fs_extra6.default.pathExists(import_node_path7.default.join(cwd, "pnpm-lock.yaml")),
|
|
1929
|
+
import_fs_extra6.default.pathExists(import_node_path7.default.join(cwd, "package-lock.json")),
|
|
1930
|
+
import_fs_extra6.default.pathExists(import_node_path7.default.join(cwd, "npm-shrinkwrap.json"))
|
|
1395
1931
|
]);
|
|
1396
1932
|
return {
|
|
1397
1933
|
packageManagerField,
|
|
@@ -1595,9 +2131,25 @@ async function listFailedRuns(branch, cwd, logger) {
|
|
|
1595
2131
|
return [];
|
|
1596
2132
|
}
|
|
1597
2133
|
}
|
|
2134
|
+
async function enableAutoMerge(target, cwd, logger) {
|
|
2135
|
+
const targetValue = String(target);
|
|
2136
|
+
const args = ["pr", "merge", targetValue, "--auto", "--merge"];
|
|
2137
|
+
const result = await runCommand("gh", args, {
|
|
2138
|
+
cwd,
|
|
2139
|
+
logger,
|
|
2140
|
+
verboseLabel: "gh",
|
|
2141
|
+
verboseCommand: `gh ${args.join(" ")}`
|
|
2142
|
+
});
|
|
2143
|
+
if (result.exitCode !== 0) {
|
|
2144
|
+
logger.warn(`\u542F\u7528\u81EA\u52A8\u5408\u5E76\u5931\u8D25: ${result.stderr || result.stdout}`);
|
|
2145
|
+
return false;
|
|
2146
|
+
}
|
|
2147
|
+
logger.success("\u5DF2\u542F\u7528 PR \u81EA\u52A8\u5408\u5E76");
|
|
2148
|
+
return true;
|
|
2149
|
+
}
|
|
1598
2150
|
|
|
1599
2151
|
// src/logger.ts
|
|
1600
|
-
var
|
|
2152
|
+
var import_fs_extra7 = __toESM(require("fs-extra"));
|
|
1601
2153
|
var wrap = (code) => (value) => `\x1B[${code}m${value}\x1B[0m`;
|
|
1602
2154
|
var colors = {
|
|
1603
2155
|
blue: wrap("34"),
|
|
@@ -1616,7 +2168,7 @@ var Logger = class {
|
|
|
1616
2168
|
this.logFileErrored = false;
|
|
1617
2169
|
if (this.logFile) {
|
|
1618
2170
|
try {
|
|
1619
|
-
|
|
2171
|
+
import_fs_extra7.default.ensureFileSync(this.logFile);
|
|
1620
2172
|
} catch (error) {
|
|
1621
2173
|
this.disableFileWithError(error);
|
|
1622
2174
|
}
|
|
@@ -1656,7 +2208,7 @@ var Logger = class {
|
|
|
1656
2208
|
writeFileLine(line) {
|
|
1657
2209
|
if (!this.logFileEnabled || !this.logFile) return;
|
|
1658
2210
|
try {
|
|
1659
|
-
|
|
2211
|
+
import_fs_extra7.default.appendFileSync(this.logFile, `${line}
|
|
1660
2212
|
`, "utf8");
|
|
1661
2213
|
} catch (error) {
|
|
1662
2214
|
this.disableFileWithError(error);
|
|
@@ -1682,6 +2234,70 @@ var Logger = class {
|
|
|
1682
2234
|
};
|
|
1683
2235
|
var defaultLogger = new Logger();
|
|
1684
2236
|
|
|
2237
|
+
// src/plan.ts
|
|
2238
|
+
var ITEM_PATTERN = /^(\s*)([-*+]|\d+\.)\s+(.*)$/;
|
|
2239
|
+
function isCompleted(content) {
|
|
2240
|
+
if (content.includes("\u2705")) return true;
|
|
2241
|
+
if (/\[[xX]\]/.test(content)) return true;
|
|
2242
|
+
return false;
|
|
2243
|
+
}
|
|
2244
|
+
function normalizeText(content) {
|
|
2245
|
+
return content.replace(/\[[xX ]\]\s*/g, "").replace(/✅/g, "").trim();
|
|
2246
|
+
}
|
|
2247
|
+
function parsePlanItems(plan) {
|
|
2248
|
+
const lines = plan.split(/\r?\n/);
|
|
2249
|
+
const items = [];
|
|
2250
|
+
lines.forEach((line, index) => {
|
|
2251
|
+
const match = line.match(ITEM_PATTERN);
|
|
2252
|
+
if (!match) return;
|
|
2253
|
+
const content = match[3] ?? "";
|
|
2254
|
+
const text = normalizeText(content);
|
|
2255
|
+
if (!text) return;
|
|
2256
|
+
items.push({
|
|
2257
|
+
index,
|
|
2258
|
+
raw: line,
|
|
2259
|
+
text,
|
|
2260
|
+
completed: isCompleted(content)
|
|
2261
|
+
});
|
|
2262
|
+
});
|
|
2263
|
+
return items;
|
|
2264
|
+
}
|
|
2265
|
+
function getPendingPlanItems(plan) {
|
|
2266
|
+
return parsePlanItems(plan).filter((item) => !item.completed);
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
// src/quality.ts
|
|
2270
|
+
var import_fs_extra8 = __toESM(require("fs-extra"));
|
|
2271
|
+
var import_node_path8 = __toESM(require("path"));
|
|
2272
|
+
function hasScript(scripts, name) {
|
|
2273
|
+
return typeof scripts[name] === "string" && scripts[name].trim().length > 0;
|
|
2274
|
+
}
|
|
2275
|
+
async function detectQualityCommands(workDir) {
|
|
2276
|
+
const packagePath = import_node_path8.default.join(workDir, "package.json");
|
|
2277
|
+
const exists = await import_fs_extra8.default.pathExists(packagePath);
|
|
2278
|
+
if (!exists) return [];
|
|
2279
|
+
const pkg = await import_fs_extra8.default.readJson(packagePath);
|
|
2280
|
+
const scripts = typeof pkg === "object" && pkg && typeof pkg.scripts === "object" ? pkg.scripts ?? {} : {};
|
|
2281
|
+
const commands = [];
|
|
2282
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2283
|
+
const append = (name, command) => {
|
|
2284
|
+
if (seen.has(name)) return;
|
|
2285
|
+
if (!hasScript(scripts, name)) return;
|
|
2286
|
+
commands.push({ name, command });
|
|
2287
|
+
seen.add(name);
|
|
2288
|
+
};
|
|
2289
|
+
append("lint", "yarn lint");
|
|
2290
|
+
append("lint:ci", "yarn lint:ci");
|
|
2291
|
+
append("lint:check", "yarn lint:check");
|
|
2292
|
+
append("typecheck", "yarn typecheck");
|
|
2293
|
+
append("format:check", "yarn format:check");
|
|
2294
|
+
append("format:ci", "yarn format:ci");
|
|
2295
|
+
if (!hasScript(scripts, "format:check") && !hasScript(scripts, "format:ci")) {
|
|
2296
|
+
append("format", "yarn format");
|
|
2297
|
+
}
|
|
2298
|
+
return commands;
|
|
2299
|
+
}
|
|
2300
|
+
|
|
1685
2301
|
// src/runtime-tracker.ts
|
|
1686
2302
|
async function safeWrite(logFile, metadata, logger) {
|
|
1687
2303
|
try {
|
|
@@ -1706,14 +2322,15 @@ async function safeRemove(logFile, logger) {
|
|
|
1706
2322
|
}
|
|
1707
2323
|
}
|
|
1708
2324
|
async function createRunTracker(options) {
|
|
1709
|
-
const { logFile, command, path:
|
|
2325
|
+
const { logFile, command, path: path12, logger } = options;
|
|
1710
2326
|
if (!logFile) return null;
|
|
1711
2327
|
const update = async (round, tokenUsed) => {
|
|
1712
2328
|
const metadata = {
|
|
1713
2329
|
command,
|
|
1714
2330
|
round,
|
|
1715
2331
|
tokenUsed,
|
|
1716
|
-
path:
|
|
2332
|
+
path: path12,
|
|
2333
|
+
pid: process.pid
|
|
1717
2334
|
};
|
|
1718
2335
|
await safeWrite(logFile, metadata, logger);
|
|
1719
2336
|
};
|
|
@@ -1728,14 +2345,14 @@ async function createRunTracker(options) {
|
|
|
1728
2345
|
|
|
1729
2346
|
// src/summary.ts
|
|
1730
2347
|
var REQUIRED_SECTIONS = ["# \u53D8\u66F4\u6458\u8981", "# \u6D4B\u8BD5\u7ED3\u679C", "# \u98CE\u9669\u4E0E\u56DE\u6EDA"];
|
|
1731
|
-
function
|
|
2348
|
+
function normalizeText2(text) {
|
|
1732
2349
|
return text.replace(/\r\n?/g, "\n");
|
|
1733
2350
|
}
|
|
1734
|
-
function
|
|
2351
|
+
function compactLine2(text) {
|
|
1735
2352
|
return text.replace(/\s+/g, " ").trim();
|
|
1736
2353
|
}
|
|
1737
2354
|
function trimTail(text, limit, emptyFallback) {
|
|
1738
|
-
const normalized =
|
|
2355
|
+
const normalized = normalizeText2(text).trim();
|
|
1739
2356
|
if (!normalized) return emptyFallback;
|
|
1740
2357
|
if (normalized.length <= limit) return normalized;
|
|
1741
2358
|
return `\uFF08\u5185\u5BB9\u8FC7\u957F\uFF0C\u4FDD\u7559\u6700\u540E ${limit} \u5B57\u7B26\uFF09
|
|
@@ -1762,7 +2379,7 @@ function buildSummaryLinesFromCommit(commitTitle, commitBody) {
|
|
|
1762
2379
|
return [`- ${summary}`];
|
|
1763
2380
|
}
|
|
1764
2381
|
function stripCommitType(title) {
|
|
1765
|
-
const trimmed =
|
|
2382
|
+
const trimmed = compactLine2(title);
|
|
1766
2383
|
if (!trimmed) return "\u66F4\u65B0\u8FED\u4EE3\u4EA7\u51FA";
|
|
1767
2384
|
const match = trimmed.match(/^[^:]+:\s*(.+)$/);
|
|
1768
2385
|
return match?.[1]?.trim() || trimmed;
|
|
@@ -1801,7 +2418,7 @@ function buildSummaryPrompt(input) {
|
|
|
1801
2418
|
"# \u8F93\u51FA JSON",
|
|
1802
2419
|
'{"commitTitle":"...","commitBody":"...","prTitle":"...","prBody":"..."}',
|
|
1803
2420
|
"# \u8F93\u5165\u4FE1\u606F",
|
|
1804
|
-
`\u4EFB\u52A1: ${
|
|
2421
|
+
`\u4EFB\u52A1: ${compactLine2(input.task) || "\uFF08\u7A7A\uFF09"}`,
|
|
1805
2422
|
`\u5206\u652F: ${input.branchName ?? "\uFF08\u672A\u77E5\uFF09"}`,
|
|
1806
2423
|
"\u8BA1\u5212\uFF08\u8282\u9009\uFF09:",
|
|
1807
2424
|
planSnippet,
|
|
@@ -1826,7 +2443,7 @@ function pickString(record, keys) {
|
|
|
1826
2443
|
}
|
|
1827
2444
|
return null;
|
|
1828
2445
|
}
|
|
1829
|
-
function
|
|
2446
|
+
function extractJson2(text) {
|
|
1830
2447
|
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
1831
2448
|
if (fenced?.[1]) return fenced[1].trim();
|
|
1832
2449
|
const start = text.indexOf("{");
|
|
@@ -1837,21 +2454,21 @@ function extractJson(text) {
|
|
|
1837
2454
|
return null;
|
|
1838
2455
|
}
|
|
1839
2456
|
function normalizeTitle(title) {
|
|
1840
|
-
return
|
|
2457
|
+
return compactLine2(title);
|
|
1841
2458
|
}
|
|
1842
2459
|
function normalizeBody(body) {
|
|
1843
2460
|
if (!body) return void 0;
|
|
1844
|
-
const normalized =
|
|
2461
|
+
const normalized = normalizeText2(body).trim();
|
|
1845
2462
|
return normalized.length > 0 ? normalized : void 0;
|
|
1846
2463
|
}
|
|
1847
2464
|
function extractBulletLines(text) {
|
|
1848
2465
|
if (!text) return [];
|
|
1849
|
-
const lines =
|
|
2466
|
+
const lines = normalizeText2(text).split("\n").map((line) => line.trim()).filter(Boolean);
|
|
1850
2467
|
const bullets = lines.filter((line) => line.startsWith("- ") || line.startsWith("* "));
|
|
1851
2468
|
return bullets.map((line) => line.startsWith("* ") ? `- ${line.slice(2).trim()}` : line);
|
|
1852
2469
|
}
|
|
1853
2470
|
function parseDeliverySummary(output) {
|
|
1854
|
-
const jsonText =
|
|
2471
|
+
const jsonText = extractJson2(output);
|
|
1855
2472
|
if (!jsonText) return null;
|
|
1856
2473
|
try {
|
|
1857
2474
|
const parsed = JSON.parse(jsonText);
|
|
@@ -1875,7 +2492,7 @@ function parseDeliverySummary(output) {
|
|
|
1875
2492
|
const normalizedCommitTitle = normalizeTitle(commitTitle);
|
|
1876
2493
|
const normalizedPrTitle = normalizeTitle(prTitle);
|
|
1877
2494
|
const normalizedCommitBody = normalizeBody(commitBody);
|
|
1878
|
-
const normalizedPrBody =
|
|
2495
|
+
const normalizedPrBody = normalizeText2(prBody).trim();
|
|
1879
2496
|
if (!normalizedCommitTitle || !normalizedPrTitle || !normalizedPrBody) return null;
|
|
1880
2497
|
return {
|
|
1881
2498
|
commitTitle: normalizedCommitTitle,
|
|
@@ -1888,7 +2505,7 @@ function parseDeliverySummary(output) {
|
|
|
1888
2505
|
}
|
|
1889
2506
|
}
|
|
1890
2507
|
function buildFallbackSummary(input) {
|
|
1891
|
-
const taskLine =
|
|
2508
|
+
const taskLine = compactLine2(input.task);
|
|
1892
2509
|
const shortTask = taskLine.length > 50 ? `${taskLine.slice(0, 50)}...` : taskLine;
|
|
1893
2510
|
const baseTitle = shortTask || "\u66F4\u65B0\u8FED\u4EE3\u4EA7\u51FA";
|
|
1894
2511
|
const title = `chore: ${baseTitle}`;
|
|
@@ -1903,7 +2520,7 @@ function buildFallbackSummary(input) {
|
|
|
1903
2520
|
};
|
|
1904
2521
|
}
|
|
1905
2522
|
function ensurePrBodySections(prBody, fallback) {
|
|
1906
|
-
const normalized =
|
|
2523
|
+
const normalized = normalizeText2(prBody).trim();
|
|
1907
2524
|
const hasAll = REQUIRED_SECTIONS.every((section) => normalized.includes(section));
|
|
1908
2525
|
if (hasAll) return normalized;
|
|
1909
2526
|
const summaryLines = buildSummaryLinesFromCommit(fallback.commitTitle, fallback.commitBody);
|
|
@@ -1975,13 +2592,18 @@ async function ensureWorkflowFiles(workflowFiles) {
|
|
|
1975
2592
|
await ensureFile(workflowFiles.planFile, "# \u8BA1\u5212\n");
|
|
1976
2593
|
await ensureFile(workflowFiles.notesFile, "# \u6301\u4E45\u5316\u8BB0\u5FC6\n");
|
|
1977
2594
|
}
|
|
1978
|
-
var
|
|
1979
|
-
function trimOutput(output, limit =
|
|
2595
|
+
var MAX_LOG_LENGTH = 4e3;
|
|
2596
|
+
function trimOutput(output, limit = MAX_LOG_LENGTH) {
|
|
1980
2597
|
if (!output) return "";
|
|
1981
2598
|
if (output.length <= limit) return output;
|
|
1982
2599
|
return `${output.slice(0, limit)}
|
|
1983
2600
|
\u2026\u2026\uFF08\u8F93\u51FA\u5DF2\u622A\u65AD\uFF0C\u539F\u59CB\u957F\u5EA6 ${output.length} \u5B57\u7B26\uFF09`;
|
|
1984
2601
|
}
|
|
2602
|
+
function truncateText(text, limit = 24) {
|
|
2603
|
+
const trimmed = text.trim();
|
|
2604
|
+
if (trimmed.length <= limit) return trimmed;
|
|
2605
|
+
return `${trimmed.slice(0, limit)}...`;
|
|
2606
|
+
}
|
|
1985
2607
|
async function safeCommandOutput(command, args, cwd, logger, label, verboseCommand) {
|
|
1986
2608
|
const result = await runCommand(command, args, {
|
|
1987
2609
|
cwd,
|
|
@@ -2019,6 +2641,66 @@ async function runSingleTest(kind, command, cwd, logger) {
|
|
|
2019
2641
|
stderr: trimOutput(result.stderr.trim())
|
|
2020
2642
|
};
|
|
2021
2643
|
}
|
|
2644
|
+
async function runQualityChecks(commands, cwd, logger) {
|
|
2645
|
+
const results = [];
|
|
2646
|
+
for (const item of commands) {
|
|
2647
|
+
logger.info(`\u6267\u884C\u8D28\u91CF\u68C0\u67E5: ${item.command}`);
|
|
2648
|
+
const result = await runCommand("bash", ["-lc", item.command], {
|
|
2649
|
+
cwd,
|
|
2650
|
+
logger,
|
|
2651
|
+
verboseLabel: "shell",
|
|
2652
|
+
verboseCommand: `bash -lc "${item.command}"`
|
|
2653
|
+
});
|
|
2654
|
+
results.push({
|
|
2655
|
+
name: item.name,
|
|
2656
|
+
command: item.command,
|
|
2657
|
+
success: result.exitCode === 0,
|
|
2658
|
+
exitCode: result.exitCode,
|
|
2659
|
+
stdout: trimOutput(result.stdout.trim()),
|
|
2660
|
+
stderr: trimOutput(result.stderr.trim())
|
|
2661
|
+
});
|
|
2662
|
+
}
|
|
2663
|
+
return results;
|
|
2664
|
+
}
|
|
2665
|
+
function buildCheckResultSummary(results) {
|
|
2666
|
+
if (results.length === 0) return "\uFF08\u672A\u6267\u884C\u8D28\u91CF\u68C0\u67E5\uFF09";
|
|
2667
|
+
return results.map((result) => {
|
|
2668
|
+
const status = result.success ? "\u901A\u8FC7" : `\u5931\u8D25\uFF08\u9000\u51FA\u7801 ${result.exitCode}\uFF09`;
|
|
2669
|
+
const output = result.success ? "" : `
|
|
2670
|
+
${result.stderr || result.stdout || "\uFF08\u65E0\u8F93\u51FA\uFF09"}`;
|
|
2671
|
+
return `- ${result.name}: ${status}\uFF5C\u547D\u4EE4: ${result.command}${output}`;
|
|
2672
|
+
}).join("\n");
|
|
2673
|
+
}
|
|
2674
|
+
function buildFailedCheckSummary(results) {
|
|
2675
|
+
return buildCheckResultSummary(results.filter((result) => !result.success));
|
|
2676
|
+
}
|
|
2677
|
+
function buildTestResultSummary(results) {
|
|
2678
|
+
if (results.length === 0) return "\uFF08\u672A\u6267\u884C\u6D4B\u8BD5\uFF09";
|
|
2679
|
+
return results.map((result) => {
|
|
2680
|
+
const label = result.kind === "unit" ? "\u5355\u5143\u6D4B\u8BD5" : "e2e \u6D4B\u8BD5";
|
|
2681
|
+
const status = result.success ? "\u901A\u8FC7" : `\u5931\u8D25\uFF08\u9000\u51FA\u7801 ${result.exitCode}\uFF09`;
|
|
2682
|
+
const output = result.success ? "" : `
|
|
2683
|
+
${result.stderr || result.stdout || "\uFF08\u65E0\u8F93\u51FA\uFF09"}`;
|
|
2684
|
+
return `- ${label}: ${status}\uFF5C\u547D\u4EE4: ${result.command}${output}`;
|
|
2685
|
+
}).join("\n");
|
|
2686
|
+
}
|
|
2687
|
+
function buildFailedTestSummary(results) {
|
|
2688
|
+
return buildTestResultSummary(results.filter((result) => !result.success));
|
|
2689
|
+
}
|
|
2690
|
+
function formatSystemRecord(stage, detail, timestamp) {
|
|
2691
|
+
return [
|
|
2692
|
+
`### \u8BB0\u5F55 \uFF5C ${timestamp} \uFF5C ${stage}`,
|
|
2693
|
+
"",
|
|
2694
|
+
detail,
|
|
2695
|
+
""
|
|
2696
|
+
].join("\n");
|
|
2697
|
+
}
|
|
2698
|
+
function shouldSkipQuality(content, cliSkip) {
|
|
2699
|
+
if (cliSkip) return true;
|
|
2700
|
+
const normalized = content.replace(/\s+/g, "");
|
|
2701
|
+
if (!normalized) return false;
|
|
2702
|
+
return normalized.includes("\u4E0D\u8981\u68C0\u67E5\u4EE3\u7801\u8D28\u91CF") || normalized.includes("\u4E0D\u68C0\u67E5\u4EE3\u7801\u8D28\u91CF") || normalized.includes("\u8DF3\u8FC7\u4EE3\u7801\u8D28\u91CF");
|
|
2703
|
+
}
|
|
2022
2704
|
async function runTests(config, workDir, logger) {
|
|
2023
2705
|
const results = [];
|
|
2024
2706
|
if (config.runTests && config.tests.unitCommand) {
|
|
@@ -2031,10 +2713,26 @@ async function runTests(config, workDir, logger) {
|
|
|
2031
2713
|
}
|
|
2032
2714
|
return results;
|
|
2033
2715
|
}
|
|
2716
|
+
async function runTestsSafely(config, workDir, logger) {
|
|
2717
|
+
try {
|
|
2718
|
+
return await runTests(config, workDir, logger);
|
|
2719
|
+
} catch (error) {
|
|
2720
|
+
const errorMessage = String(error);
|
|
2721
|
+
logger.warn(`\u6D4B\u8BD5\u6267\u884C\u5F02\u5E38: ${errorMessage}`);
|
|
2722
|
+
return [{
|
|
2723
|
+
kind: "unit",
|
|
2724
|
+
command: config.tests.unitCommand ?? "\u672A\u77E5\u6D4B\u8BD5\u547D\u4EE4",
|
|
2725
|
+
success: false,
|
|
2726
|
+
exitCode: -1,
|
|
2727
|
+
stdout: "",
|
|
2728
|
+
stderr: trimOutput(errorMessage)
|
|
2729
|
+
}];
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2034
2732
|
function reRootPath(filePath, repoRoot, workDir) {
|
|
2035
|
-
const relative =
|
|
2733
|
+
const relative = import_node_path9.default.relative(repoRoot, filePath);
|
|
2036
2734
|
if (relative.startsWith("..")) return filePath;
|
|
2037
|
-
return
|
|
2735
|
+
return import_node_path9.default.join(workDir, relative);
|
|
2038
2736
|
}
|
|
2039
2737
|
function reRootWorkflowFiles(workflowFiles, repoRoot, workDir) {
|
|
2040
2738
|
if (repoRoot === workDir) return workflowFiles;
|
|
@@ -2045,10 +2743,10 @@ function reRootWorkflowFiles(workflowFiles, repoRoot, workDir) {
|
|
|
2045
2743
|
};
|
|
2046
2744
|
}
|
|
2047
2745
|
function buildBodyFile(workDir) {
|
|
2048
|
-
return
|
|
2746
|
+
return import_node_path9.default.join(workDir, "memory", "pr-body.md");
|
|
2049
2747
|
}
|
|
2050
2748
|
async function writePrBody(bodyPath, content, appendExisting) {
|
|
2051
|
-
await
|
|
2749
|
+
await import_fs_extra9.default.mkdirp(import_node_path9.default.dirname(bodyPath));
|
|
2052
2750
|
let finalContent = content.trim();
|
|
2053
2751
|
if (appendExisting) {
|
|
2054
2752
|
const existing = await readFileSafe(bodyPath);
|
|
@@ -2061,7 +2759,7 @@ async function writePrBody(bodyPath, content, appendExisting) {
|
|
|
2061
2759
|
${finalContent}`;
|
|
2062
2760
|
}
|
|
2063
2761
|
}
|
|
2064
|
-
await
|
|
2762
|
+
await import_fs_extra9.default.writeFile(bodyPath, `${finalContent}
|
|
2065
2763
|
`, "utf8");
|
|
2066
2764
|
}
|
|
2067
2765
|
async function cleanupWorktreeIfSafe(context) {
|
|
@@ -2098,20 +2796,20 @@ async function runLoop(config) {
|
|
|
2098
2796
|
const logger = new Logger({ verbose: config.verbose, logFile: config.logFile });
|
|
2099
2797
|
const repoRoot = await getRepoRoot(config.cwd, logger);
|
|
2100
2798
|
logger.debug(`\u4ED3\u5E93\u6839\u76EE\u5F55: ${repoRoot}`);
|
|
2101
|
-
const worktreeResult = config.git.useWorktree ? await ensureWorktree(config.git, repoRoot, logger) : { path: repoRoot, created: false };
|
|
2102
|
-
const workDir = worktreeResult.path;
|
|
2103
|
-
const worktreeCreated = worktreeResult.created;
|
|
2104
|
-
logger.debug(`\u5DE5\u4F5C\u76EE\u5F55: ${workDir}`);
|
|
2105
|
-
const commandLine = formatCommandLine(process.argv);
|
|
2106
|
-
const runTracker = await createRunTracker({
|
|
2107
|
-
logFile: config.logFile,
|
|
2108
|
-
command: commandLine,
|
|
2109
|
-
path: workDir,
|
|
2110
|
-
logger
|
|
2111
|
-
});
|
|
2112
2799
|
let branchName = config.git.branchName;
|
|
2800
|
+
let workDir = repoRoot;
|
|
2801
|
+
let worktreeCreated = false;
|
|
2802
|
+
const commandLine = formatCommandLine(process.argv);
|
|
2803
|
+
let runTracker = null;
|
|
2804
|
+
let accumulatedUsage = null;
|
|
2805
|
+
let lastTestResults = null;
|
|
2806
|
+
let lastAiOutput = "";
|
|
2113
2807
|
let lastRound = 0;
|
|
2114
2808
|
let runError = null;
|
|
2809
|
+
let prInfo = null;
|
|
2810
|
+
let prFailed = false;
|
|
2811
|
+
let sessionIndex = 0;
|
|
2812
|
+
const preWorktreeRecords = [];
|
|
2115
2813
|
const notifyWebhook = async (event, iteration, stage) => {
|
|
2116
2814
|
const payload = buildWebhookPayload({
|
|
2117
2815
|
event,
|
|
@@ -2123,15 +2821,46 @@ async function runLoop(config) {
|
|
|
2123
2821
|
await sendWebhookNotifications(config.webhooks, payload, logger);
|
|
2124
2822
|
};
|
|
2125
2823
|
try {
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2824
|
+
await notifyWebhook("task_start", 0, "\u4EFB\u52A1\u5F00\u59CB");
|
|
2825
|
+
if (config.git.useWorktree && !branchName) {
|
|
2826
|
+
const branchPrompt = buildBranchNamePrompt({ task: config.task });
|
|
2827
|
+
await notifyWebhook("iteration_start", sessionIndex + 1, "\u5206\u652F\u540D\u751F\u6210");
|
|
2828
|
+
logger.info("\u5206\u652F\u540D\u751F\u6210\u63D0\u793A\u6784\u5EFA\u5B8C\u6210\uFF0C\u8C03\u7528 AI CLI...");
|
|
2829
|
+
const aiResult = await runAi(branchPrompt, config.ai, logger, repoRoot);
|
|
2830
|
+
accumulatedUsage = mergeTokenUsage(accumulatedUsage, aiResult.usage);
|
|
2831
|
+
lastAiOutput = aiResult.output;
|
|
2832
|
+
sessionIndex += 1;
|
|
2833
|
+
lastRound = sessionIndex;
|
|
2834
|
+
const record = formatIterationRecord({
|
|
2835
|
+
iteration: sessionIndex,
|
|
2836
|
+
stage: "\u5206\u652F\u540D\u751F\u6210",
|
|
2837
|
+
prompt: branchPrompt,
|
|
2838
|
+
aiOutput: aiResult.output,
|
|
2839
|
+
timestamp: isoNow()
|
|
2840
|
+
});
|
|
2841
|
+
preWorktreeRecords.push(record);
|
|
2842
|
+
const parsed = parseBranchName(aiResult.output);
|
|
2843
|
+
if (parsed) {
|
|
2844
|
+
branchName = parsed;
|
|
2845
|
+
logger.info(`AI \u751F\u6210\u5206\u652F\u540D\uFF1A${branchName}`);
|
|
2846
|
+
} else {
|
|
2847
|
+
branchName = generateBranchNameFromTask(config.task);
|
|
2848
|
+
logger.warn(`\u672A\u89E3\u6790\u5230 AI \u5206\u652F\u540D\uFF0C\u4F7F\u7528\u515C\u5E95\u5206\u652F\uFF1A${branchName}`);
|
|
2132
2849
|
}
|
|
2133
2850
|
}
|
|
2134
|
-
await
|
|
2851
|
+
const worktreeResult = config.git.useWorktree ? await ensureWorktree({ ...config.git, branchName }, repoRoot, logger) : { path: repoRoot, created: false };
|
|
2852
|
+
workDir = worktreeResult.path;
|
|
2853
|
+
worktreeCreated = worktreeResult.created;
|
|
2854
|
+
logger.debug(`\u5DE5\u4F5C\u76EE\u5F55: ${workDir}`);
|
|
2855
|
+
runTracker = await createRunTracker({
|
|
2856
|
+
logFile: config.logFile,
|
|
2857
|
+
command: commandLine,
|
|
2858
|
+
path: workDir,
|
|
2859
|
+
logger
|
|
2860
|
+
});
|
|
2861
|
+
if (runTracker && sessionIndex > 0) {
|
|
2862
|
+
await runTracker.update(sessionIndex, accumulatedUsage?.totalTokens ?? 0);
|
|
2863
|
+
}
|
|
2135
2864
|
if (config.skipInstall) {
|
|
2136
2865
|
logger.info("\u5DF2\u8DF3\u8FC7\u4F9D\u8D56\u68C0\u67E5");
|
|
2137
2866
|
} else {
|
|
@@ -2139,79 +2868,203 @@ async function runLoop(config) {
|
|
|
2139
2868
|
}
|
|
2140
2869
|
const workflowFiles = reRootWorkflowFiles(config.workflowFiles, repoRoot, workDir);
|
|
2141
2870
|
await ensureWorkflowFiles(workflowFiles);
|
|
2871
|
+
if (preWorktreeRecords.length > 0) {
|
|
2872
|
+
for (const record of preWorktreeRecords) {
|
|
2873
|
+
await appendSection(workflowFiles.notesFile, record);
|
|
2874
|
+
}
|
|
2875
|
+
logger.success(`\u5DF2\u5199\u5165\u5206\u652F\u540D\u751F\u6210\u8BB0\u5F55\u81F3 ${workflowFiles.notesFile}`);
|
|
2876
|
+
}
|
|
2142
2877
|
const planContent = await readFileSafe(workflowFiles.planFile);
|
|
2143
2878
|
if (planContent.trim().length === 0) {
|
|
2144
2879
|
logger.warn("plan \u6587\u4EF6\u4E3A\u7A7A\uFF0C\u5EFA\u8BAE AI \u9996\u8F6E\u751F\u6210\u8BA1\u5212");
|
|
2145
2880
|
}
|
|
2881
|
+
if (!branchName) {
|
|
2882
|
+
try {
|
|
2883
|
+
branchName = await getCurrentBranch(workDir, logger);
|
|
2884
|
+
} catch (error) {
|
|
2885
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2886
|
+
logger.warn(`\u8BFB\u53D6\u5206\u652F\u540D\u5931\u8D25\uFF0Cwebhook \u4E2D\u5C06\u7F3A\u5931\u5206\u652F\u4FE1\u606F\uFF1A${message}`);
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2146
2889
|
const aiConfig = config.ai;
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
const
|
|
2157
|
-
logger.debug(`\u52A0\u8F7D\u63D0\u793A\u4E0A\u4E0B\u6587\uFF0C\u957F\u5EA6\uFF1Aworkflow=${workflowGuide.length}, plan=${plan.length}, notes=${notes.length}`);
|
|
2158
|
-
const prompt = buildPrompt({
|
|
2159
|
-
task: config.task,
|
|
2160
|
-
workflowGuide,
|
|
2161
|
-
plan,
|
|
2162
|
-
notes,
|
|
2163
|
-
iteration: i
|
|
2164
|
-
});
|
|
2165
|
-
logger.debug(`\u7B2C ${i} \u8F6E\u63D0\u793A\u957F\u5EA6: ${prompt.length}`);
|
|
2166
|
-
logger.info(`\u7B2C ${i} \u8F6E\u63D0\u793A\u6784\u5EFA\u5B8C\u6210\uFF0C\u8C03\u7528 AI CLI...`);
|
|
2167
|
-
const aiResult = await runAi(prompt, aiConfig, logger, workDir);
|
|
2890
|
+
const loadContext = async () => ({
|
|
2891
|
+
workflowGuide: await readFileSafe(workflowFiles.workflowDoc),
|
|
2892
|
+
plan: await readFileSafe(workflowFiles.planFile),
|
|
2893
|
+
notes: await readFileSafe(workflowFiles.notesFile)
|
|
2894
|
+
});
|
|
2895
|
+
const runAiSession = async (stage, prompt, extras) => {
|
|
2896
|
+
sessionIndex += 1;
|
|
2897
|
+
await notifyWebhook("iteration_start", sessionIndex, stage);
|
|
2898
|
+
logger.info(`${stage} \u63D0\u793A\u6784\u5EFA\u5B8C\u6210\uFF0C\u8C03\u7528 AI CLI...`);
|
|
2899
|
+
const aiResult = await runAi(prompt, aiConfig, logger, extras?.cwd ?? workDir);
|
|
2168
2900
|
accumulatedUsage = mergeTokenUsage(accumulatedUsage, aiResult.usage);
|
|
2169
2901
|
lastAiOutput = aiResult.output;
|
|
2170
|
-
const hitStop = aiResult.output.includes(config.stopSignal);
|
|
2171
|
-
let testResults = [];
|
|
2172
|
-
const shouldRunTests = config.runTests || config.runE2e;
|
|
2173
|
-
if (shouldRunTests) {
|
|
2174
|
-
try {
|
|
2175
|
-
testResults = await runTests(config, workDir, logger);
|
|
2176
|
-
} catch (error) {
|
|
2177
|
-
const errorMessage = String(error);
|
|
2178
|
-
logger.warn(`\u6D4B\u8BD5\u6267\u884C\u5F02\u5E38: ${errorMessage}`);
|
|
2179
|
-
testResults = [{
|
|
2180
|
-
kind: "unit",
|
|
2181
|
-
command: config.tests.unitCommand ?? "\u672A\u77E5\u6D4B\u8BD5\u547D\u4EE4",
|
|
2182
|
-
success: false,
|
|
2183
|
-
exitCode: -1,
|
|
2184
|
-
stdout: "",
|
|
2185
|
-
stderr: trimOutput(errorMessage)
|
|
2186
|
-
}];
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
2902
|
const record = formatIterationRecord({
|
|
2190
|
-
iteration:
|
|
2903
|
+
iteration: sessionIndex,
|
|
2904
|
+
stage,
|
|
2191
2905
|
prompt,
|
|
2192
2906
|
aiOutput: aiResult.output,
|
|
2193
2907
|
timestamp: isoNow(),
|
|
2194
|
-
testResults
|
|
2908
|
+
testResults: extras?.testResults,
|
|
2909
|
+
checkResults: extras?.checkResults
|
|
2195
2910
|
});
|
|
2196
2911
|
await appendSection(workflowFiles.notesFile, record);
|
|
2197
|
-
logger.success(`\u5DF2\u5C06
|
|
2912
|
+
logger.success(`\u5DF2\u5C06${stage}\u8F93\u51FA\u5199\u5165 ${workflowFiles.notesFile}`);
|
|
2913
|
+
lastRound = sessionIndex;
|
|
2914
|
+
await runTracker?.update(sessionIndex, accumulatedUsage?.totalTokens ?? 0);
|
|
2915
|
+
};
|
|
2916
|
+
{
|
|
2917
|
+
const { workflowGuide, plan, notes } = await loadContext();
|
|
2918
|
+
const planningPrompt = buildPlanningPrompt({
|
|
2919
|
+
task: config.task,
|
|
2920
|
+
workflowGuide,
|
|
2921
|
+
plan,
|
|
2922
|
+
notes,
|
|
2923
|
+
branchName
|
|
2924
|
+
});
|
|
2925
|
+
await runAiSession("\u8BA1\u5212\u751F\u6210", planningPrompt);
|
|
2926
|
+
}
|
|
2927
|
+
let refreshedPlan = await readFileSafe(workflowFiles.planFile);
|
|
2928
|
+
if (/(测试|test|e2e|单测)/i.test(refreshedPlan)) {
|
|
2929
|
+
logger.warn("\u68C0\u6D4B\u5230 plan \u4E2D\u53EF\u80FD\u5305\u542B\u6D4B\u8BD5\u76F8\u5173\u4E8B\u9879\uFF0C\u5EFA\u8BAE\u4FDD\u7559\u5F00\u53D1\u5185\u5BB9\u5E76\u79FB\u9664\u6D4B\u8BD5\u9879\u3002");
|
|
2930
|
+
}
|
|
2931
|
+
let pendingItems = getPendingPlanItems(refreshedPlan);
|
|
2932
|
+
if (pendingItems.length === 0) {
|
|
2933
|
+
logger.info("\u8BA1\u5212\u6682\u65E0\u5F85\u6267\u884C\u9879\uFF0C\u8DF3\u8FC7\u8BA1\u5212\u6267\u884C\u5FAA\u73AF");
|
|
2934
|
+
const record = formatSystemRecord("\u8BA1\u5212\u6267\u884C", "\u672A\u53D1\u73B0\u5F85\u6267\u884C\u8BA1\u5212\u9879\uFF0C\u5DF2\u8DF3\u8FC7\u6267\u884C\u5FAA\u73AF\u3002", isoNow());
|
|
2935
|
+
await appendSection(workflowFiles.notesFile, record);
|
|
2936
|
+
}
|
|
2937
|
+
let planRounds = 0;
|
|
2938
|
+
while (pendingItems.length > 0) {
|
|
2939
|
+
if (planRounds >= config.iterations) {
|
|
2940
|
+
throw new Error("\u8BA1\u5212\u6267\u884C\u8FBE\u5230\u6700\u5927\u8FED\u4EE3\u6B21\u6570\uFF0C\u4ECD\u6709\u672A\u5B8C\u6210\u9879");
|
|
2941
|
+
}
|
|
2942
|
+
const lastItem = pendingItems[pendingItems.length - 1];
|
|
2943
|
+
const { workflowGuide, plan, notes } = await loadContext();
|
|
2944
|
+
const itemPrompt = buildPlanItemPrompt({
|
|
2945
|
+
task: config.task,
|
|
2946
|
+
workflowGuide,
|
|
2947
|
+
plan,
|
|
2948
|
+
notes,
|
|
2949
|
+
item: lastItem.text
|
|
2950
|
+
});
|
|
2951
|
+
await runAiSession(`\u6267\u884C\u8BA1\u5212\u9879\uFF1A${truncateText(lastItem.text)}`, itemPrompt);
|
|
2952
|
+
planRounds += 1;
|
|
2953
|
+
refreshedPlan = await readFileSafe(workflowFiles.planFile);
|
|
2954
|
+
pendingItems = getPendingPlanItems(refreshedPlan);
|
|
2955
|
+
}
|
|
2956
|
+
const agentsContent = await readFileSafe(import_node_path9.default.join(workDir, "AGENTS.md"));
|
|
2957
|
+
const skipQuality = shouldSkipQuality(agentsContent, config.skipQuality);
|
|
2958
|
+
if (skipQuality) {
|
|
2959
|
+
const record = formatSystemRecord("\u4EE3\u7801\u8D28\u91CF\u68C0\u67E5", "\u5DF2\u6309\u914D\u7F6E/AGENTS.md \u8DF3\u8FC7\u4EE3\u7801\u8D28\u91CF\u68C0\u67E5\u3002", isoNow());
|
|
2960
|
+
await appendSection(workflowFiles.notesFile, record);
|
|
2961
|
+
logger.info("\u5DF2\u8DF3\u8FC7\u4EE3\u7801\u8D28\u91CF\u68C0\u67E5");
|
|
2962
|
+
} else {
|
|
2963
|
+
const qualityCommands = await detectQualityCommands(workDir);
|
|
2964
|
+
if (qualityCommands.length === 0) {
|
|
2965
|
+
const record = formatSystemRecord("\u4EE3\u7801\u8D28\u91CF\u68C0\u67E5", "\u672A\u68C0\u6D4B\u5230\u53EF\u6267\u884C\u7684\u8D28\u91CF\u68C0\u67E5\u547D\u4EE4\uFF0C\u5DF2\u8DF3\u8FC7\u3002", isoNow());
|
|
2966
|
+
await appendSection(workflowFiles.notesFile, record);
|
|
2967
|
+
logger.info("\u672A\u68C0\u6D4B\u5230\u8D28\u91CF\u68C0\u67E5\u547D\u4EE4\uFF0C\u8DF3\u8FC7\u8BE5\u9636\u6BB5");
|
|
2968
|
+
} else {
|
|
2969
|
+
let qualityResults = await runQualityChecks(qualityCommands, workDir, logger);
|
|
2970
|
+
const { workflowGuide, plan, notes } = await loadContext();
|
|
2971
|
+
const qualityPrompt = buildQualityPrompt({
|
|
2972
|
+
task: config.task,
|
|
2973
|
+
workflowGuide,
|
|
2974
|
+
plan,
|
|
2975
|
+
notes,
|
|
2976
|
+
commands: qualityCommands.map((item) => item.command),
|
|
2977
|
+
results: buildCheckResultSummary(qualityResults)
|
|
2978
|
+
});
|
|
2979
|
+
await runAiSession("\u4EE3\u7801\u8D28\u91CF\u68C0\u67E5", qualityPrompt, { checkResults: qualityResults });
|
|
2980
|
+
let hasQualityFailure = qualityResults.some((result) => !result.success);
|
|
2981
|
+
let fixRounds = 0;
|
|
2982
|
+
while (hasQualityFailure) {
|
|
2983
|
+
if (fixRounds >= config.iterations) {
|
|
2984
|
+
throw new Error("\u4EE3\u7801\u8D28\u91CF\u4FEE\u590D\u8FBE\u5230\u6700\u5927\u8F6E\u6B21\uFF0C\u4ECD\u672A\u901A\u8FC7");
|
|
2985
|
+
}
|
|
2986
|
+
const latest = await loadContext();
|
|
2987
|
+
const fixPrompt = buildFixPrompt({
|
|
2988
|
+
task: config.task,
|
|
2989
|
+
workflowGuide: latest.workflowGuide,
|
|
2990
|
+
plan: latest.plan,
|
|
2991
|
+
notes: latest.notes,
|
|
2992
|
+
stage: "\u4EE3\u7801\u8D28\u91CF",
|
|
2993
|
+
errors: buildFailedCheckSummary(qualityResults)
|
|
2994
|
+
});
|
|
2995
|
+
await runAiSession("\u4EE3\u7801\u8D28\u91CF\u4FEE\u590D", fixPrompt);
|
|
2996
|
+
fixRounds += 1;
|
|
2997
|
+
qualityResults = await runQualityChecks(qualityCommands, workDir, logger);
|
|
2998
|
+
hasQualityFailure = qualityResults.some((result) => !result.success);
|
|
2999
|
+
const recheckRecord = formatSystemRecord("\u4EE3\u7801\u8D28\u91CF\u590D\u6838", buildCheckResultSummary(qualityResults), isoNow());
|
|
3000
|
+
await appendSection(workflowFiles.notesFile, recheckRecord);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
if (config.runTests || config.runE2e) {
|
|
3005
|
+
let testResults = await runTestsSafely(config, workDir, logger);
|
|
2198
3006
|
lastTestResults = testResults;
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
if (hitStop && !hasTestFailure) {
|
|
2203
|
-
logger.info(`\u68C0\u6D4B\u5230\u505C\u6B62\u6807\u8BB0 ${config.stopSignal}\uFF0C\u63D0\u524D\u7ED3\u675F\u5FAA\u73AF`);
|
|
2204
|
-
break;
|
|
3007
|
+
const testCommands = [];
|
|
3008
|
+
if (config.runTests && config.tests.unitCommand) {
|
|
3009
|
+
testCommands.push(config.tests.unitCommand);
|
|
2205
3010
|
}
|
|
2206
|
-
if (
|
|
2207
|
-
|
|
3011
|
+
if (config.runE2e && config.tests.e2eCommand) {
|
|
3012
|
+
testCommands.push(config.tests.e2eCommand);
|
|
2208
3013
|
}
|
|
3014
|
+
const { workflowGuide, plan, notes } = await loadContext();
|
|
3015
|
+
const testPrompt = buildTestPrompt({
|
|
3016
|
+
task: config.task,
|
|
3017
|
+
workflowGuide,
|
|
3018
|
+
plan,
|
|
3019
|
+
notes,
|
|
3020
|
+
commands: testCommands,
|
|
3021
|
+
results: buildTestResultSummary(testResults)
|
|
3022
|
+
});
|
|
3023
|
+
await runAiSession("\u6D4B\u8BD5\u6267\u884C", testPrompt, { testResults });
|
|
3024
|
+
let hasTestFailure = testResults.some((result) => !result.success);
|
|
3025
|
+
let fixRounds = 0;
|
|
3026
|
+
while (hasTestFailure) {
|
|
3027
|
+
if (fixRounds >= config.iterations) {
|
|
3028
|
+
throw new Error("\u6D4B\u8BD5\u4FEE\u590D\u8FBE\u5230\u6700\u5927\u8F6E\u6B21\uFF0C\u4ECD\u672A\u901A\u8FC7");
|
|
3029
|
+
}
|
|
3030
|
+
const latest = await loadContext();
|
|
3031
|
+
const fixPrompt = buildFixPrompt({
|
|
3032
|
+
task: config.task,
|
|
3033
|
+
workflowGuide: latest.workflowGuide,
|
|
3034
|
+
plan: latest.plan,
|
|
3035
|
+
notes: latest.notes,
|
|
3036
|
+
stage: "\u6D4B\u8BD5",
|
|
3037
|
+
errors: buildFailedTestSummary(testResults)
|
|
3038
|
+
});
|
|
3039
|
+
await runAiSession("\u6D4B\u8BD5\u4FEE\u590D", fixPrompt, { testResults });
|
|
3040
|
+
fixRounds += 1;
|
|
3041
|
+
testResults = await runTestsSafely(config, workDir, logger);
|
|
3042
|
+
lastTestResults = testResults;
|
|
3043
|
+
hasTestFailure = testResults.some((result) => !result.success);
|
|
3044
|
+
const recheckRecord = formatSystemRecord("\u6D4B\u8BD5\u590D\u6838", buildTestResultSummary(testResults), isoNow());
|
|
3045
|
+
await appendSection(workflowFiles.notesFile, recheckRecord);
|
|
3046
|
+
}
|
|
3047
|
+
} else {
|
|
3048
|
+
const record = formatSystemRecord("\u6D4B\u8BD5\u6267\u884C", "\u672A\u5F00\u542F\u5355\u5143\u6D4B\u8BD5\u6216 e2e \u6D4B\u8BD5\uFF0C\u5DF2\u8DF3\u8FC7\u3002", isoNow());
|
|
3049
|
+
await appendSection(workflowFiles.notesFile, record);
|
|
3050
|
+
logger.info("\u672A\u5F00\u542F\u6D4B\u8BD5\u9636\u6BB5");
|
|
3051
|
+
}
|
|
3052
|
+
{
|
|
3053
|
+
const { workflowGuide, plan, notes } = await loadContext();
|
|
3054
|
+
const docsPrompt = buildDocsPrompt({
|
|
3055
|
+
task: config.task,
|
|
3056
|
+
workflowGuide,
|
|
3057
|
+
plan,
|
|
3058
|
+
notes
|
|
3059
|
+
});
|
|
3060
|
+
await runAiSession("\u6587\u6863\u66F4\u65B0", docsPrompt);
|
|
2209
3061
|
}
|
|
2210
3062
|
const lastTestFailed = lastTestResults?.some((result) => !result.success) ?? false;
|
|
2211
3063
|
if (lastTestFailed) {
|
|
2212
3064
|
logger.warn("\u5B58\u5728\u672A\u901A\u8FC7\u7684\u6D4B\u8BD5\uFF0C\u5DF2\u8DF3\u8FC7\u81EA\u52A8\u63D0\u4EA4/\u63A8\u9001/PR");
|
|
2213
3065
|
}
|
|
2214
3066
|
let deliverySummary = null;
|
|
3067
|
+
const deliveryNotes = [];
|
|
2215
3068
|
const shouldPrepareDelivery = !lastTestFailed && (config.autoCommit || config.pr.enable);
|
|
2216
3069
|
if (shouldPrepareDelivery) {
|
|
2217
3070
|
const [gitStatus, diffStat] = await Promise.all([
|
|
@@ -2241,53 +3094,107 @@ async function runLoop(config) {
|
|
|
2241
3094
|
if (!deliverySummary) {
|
|
2242
3095
|
deliverySummary = buildFallbackSummary({ task: config.task, testResults: lastTestResults });
|
|
2243
3096
|
}
|
|
3097
|
+
if (deliverySummary) {
|
|
3098
|
+
deliveryNotes.push(`\u4EA4\u4ED8\u6458\u8981\uFF1A\u63D0\u4EA4 ${deliverySummary.commitTitle}\uFF5CPR ${deliverySummary.prTitle}`);
|
|
3099
|
+
}
|
|
2244
3100
|
}
|
|
2245
3101
|
await runTracker?.update(lastRound, accumulatedUsage?.totalTokens ?? 0);
|
|
2246
|
-
if (config.autoCommit
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
3102
|
+
if (config.autoCommit) {
|
|
3103
|
+
if (lastTestFailed) {
|
|
3104
|
+
deliveryNotes.push("\u81EA\u52A8\u63D0\u4EA4\uFF1A\u5DF2\u8DF3\u8FC7\uFF08\u6D4B\u8BD5\u672A\u901A\u8FC7\uFF09");
|
|
3105
|
+
} else {
|
|
3106
|
+
const summary = deliverySummary ?? buildFallbackSummary({ task: config.task, testResults: lastTestResults });
|
|
3107
|
+
const commitMessage = {
|
|
3108
|
+
title: summary.commitTitle,
|
|
3109
|
+
body: summary.commitBody
|
|
3110
|
+
};
|
|
3111
|
+
try {
|
|
3112
|
+
const committed = await commitAll(commitMessage, workDir, logger);
|
|
3113
|
+
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
|
+
} catch (error) {
|
|
3115
|
+
deliveryNotes.push(`\u81EA\u52A8\u63D0\u4EA4\uFF1A\u5931\u8D25\uFF08${String(error)}\uFF09`);
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
} else {
|
|
3119
|
+
deliveryNotes.push("\u81EA\u52A8\u63D0\u4EA4\uFF1A\u672A\u5F00\u542F");
|
|
2260
3120
|
}
|
|
2261
|
-
if (config.
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
const createdPr = await createPr(branchName, { ...config.pr, title: prTitleCandidate, bodyPath: bodyFile }, workDir, logger);
|
|
2273
|
-
prInfo = createdPr;
|
|
2274
|
-
if (createdPr) {
|
|
2275
|
-
logger.success(`PR \u5DF2\u521B\u5EFA: ${createdPr.url}`);
|
|
2276
|
-
const failedRuns = await listFailedRuns(branchName, workDir, logger);
|
|
2277
|
-
if (failedRuns.length > 0) {
|
|
2278
|
-
failedRuns.forEach((run) => {
|
|
2279
|
-
logger.warn(`Actions \u5931\u8D25: ${run.name} (${run.status}/${run.conclusion ?? "unknown"}) ${run.url}`);
|
|
2280
|
-
});
|
|
3121
|
+
if (config.autoPush) {
|
|
3122
|
+
if (lastTestFailed) {
|
|
3123
|
+
deliveryNotes.push("\u81EA\u52A8\u63A8\u9001\uFF1A\u5DF2\u8DF3\u8FC7\uFF08\u6D4B\u8BD5\u672A\u901A\u8FC7\uFF09");
|
|
3124
|
+
} else if (!branchName) {
|
|
3125
|
+
deliveryNotes.push("\u81EA\u52A8\u63A8\u9001\uFF1A\u5DF2\u8DF3\u8FC7\uFF08\u7F3A\u5C11\u5206\u652F\u540D\uFF09");
|
|
3126
|
+
} else {
|
|
3127
|
+
try {
|
|
3128
|
+
await pushBranch(branchName, workDir, logger);
|
|
3129
|
+
deliveryNotes.push(`\u81EA\u52A8\u63A8\u9001\uFF1A\u5DF2\u63A8\u9001\uFF08${branchName}\uFF09`);
|
|
3130
|
+
} catch (error) {
|
|
3131
|
+
deliveryNotes.push(`\u81EA\u52A8\u63A8\u9001\uFF1A\u5931\u8D25\uFF08${String(error)}\uFF09`);
|
|
2281
3132
|
}
|
|
3133
|
+
}
|
|
3134
|
+
} else {
|
|
3135
|
+
deliveryNotes.push("\u81EA\u52A8\u63A8\u9001\uFF1A\u672A\u5F00\u542F");
|
|
3136
|
+
}
|
|
3137
|
+
if (config.pr.enable) {
|
|
3138
|
+
if (lastTestFailed) {
|
|
3139
|
+
deliveryNotes.push("PR \u521B\u5EFA\uFF1A\u5DF2\u8DF3\u8FC7\uFF08\u6D4B\u8BD5\u672A\u901A\u8FC7\uFF09");
|
|
3140
|
+
} else if (!branchName) {
|
|
3141
|
+
deliveryNotes.push("PR \u521B\u5EFA\uFF1A\u5DF2\u8DF3\u8FC7\uFF08\u7F3A\u5C11\u5206\u652F\u540D\uFF09");
|
|
2282
3142
|
} else {
|
|
2283
|
-
|
|
2284
|
-
|
|
3143
|
+
logger.info("\u5F00\u59CB\u521B\u5EFA PR...");
|
|
3144
|
+
const summary = deliverySummary ?? buildFallbackSummary({ task: config.task, testResults: lastTestResults });
|
|
3145
|
+
const prTitleCandidate = config.pr.title?.trim() || summary.prTitle;
|
|
3146
|
+
const prBodyContent = ensurePrBodySections(summary.prBody, {
|
|
3147
|
+
commitTitle: summary.commitTitle,
|
|
3148
|
+
commitBody: summary.commitBody,
|
|
3149
|
+
testResults: lastTestResults
|
|
3150
|
+
});
|
|
3151
|
+
const bodyFile = config.pr.bodyPath ?? buildBodyFile(workDir);
|
|
3152
|
+
await writePrBody(bodyFile, prBodyContent, Boolean(config.pr.bodyPath));
|
|
3153
|
+
const createdPr = await createPr(branchName, { ...config.pr, title: prTitleCandidate, bodyPath: bodyFile }, workDir, logger);
|
|
3154
|
+
prInfo = createdPr;
|
|
3155
|
+
if (createdPr) {
|
|
3156
|
+
logger.success(`PR \u5DF2\u521B\u5EFA: ${createdPr.url}`);
|
|
3157
|
+
deliveryNotes.push(`PR \u521B\u5EFA\uFF1A\u5DF2\u5B8C\u6210\uFF08${createdPr.url}\uFF09`);
|
|
3158
|
+
if (config.pr.autoMerge) {
|
|
3159
|
+
const target = createdPr.number > 0 ? createdPr.number : createdPr.url;
|
|
3160
|
+
const merged = await enableAutoMerge(target, workDir, logger);
|
|
3161
|
+
if (merged) {
|
|
3162
|
+
deliveryNotes.push("PR \u81EA\u52A8\u5408\u5E76\uFF1A\u5DF2\u542F\u7528");
|
|
3163
|
+
} else {
|
|
3164
|
+
deliveryNotes.push("PR \u81EA\u52A8\u5408\u5E76\uFF1A\u542F\u7528\u5931\u8D25");
|
|
3165
|
+
prFailed = true;
|
|
3166
|
+
}
|
|
3167
|
+
} else {
|
|
3168
|
+
deliveryNotes.push("PR \u81EA\u52A8\u5408\u5E76\uFF1A\u672A\u5F00\u542F");
|
|
3169
|
+
}
|
|
3170
|
+
const failedRuns = await listFailedRuns(branchName, workDir, logger);
|
|
3171
|
+
if (failedRuns.length > 0) {
|
|
3172
|
+
failedRuns.forEach((run) => {
|
|
3173
|
+
logger.warn(`Actions \u5931\u8D25: ${run.name} (${run.status}/${run.conclusion ?? "unknown"}) ${run.url}`);
|
|
3174
|
+
});
|
|
3175
|
+
}
|
|
3176
|
+
} else {
|
|
3177
|
+
prFailed = true;
|
|
3178
|
+
deliveryNotes.push("PR \u521B\u5EFA\uFF1A\u5931\u8D25\uFF08\u8BE6\u89C1 gh \u8F93\u51FA\uFF09");
|
|
3179
|
+
logger.error("PR \u521B\u5EFA\u5931\u8D25\uFF0C\u8BE6\u89C1\u4E0A\u65B9 gh \u8F93\u51FA");
|
|
3180
|
+
}
|
|
2285
3181
|
}
|
|
2286
|
-
} else if (branchName
|
|
3182
|
+
} else if (branchName) {
|
|
2287
3183
|
logger.info("\u672A\u5F00\u542F PR \u521B\u5EFA\uFF08--pr \u672A\u4F20\uFF09\uFF0C\u5C1D\u8BD5\u67E5\u770B\u5DF2\u6709 PR");
|
|
2288
3184
|
const existingPr = await viewPr(branchName, workDir, logger);
|
|
2289
3185
|
prInfo = existingPr;
|
|
2290
|
-
if (existingPr)
|
|
3186
|
+
if (existingPr) {
|
|
3187
|
+
logger.info(`\u5DF2\u6709 PR: ${existingPr.url}`);
|
|
3188
|
+
deliveryNotes.push(`PR \u521B\u5EFA\uFF1A\u672A\u5F00\u542F\uFF08\u5DF2\u5B58\u5728 PR\uFF1A${existingPr.url}\uFF09`);
|
|
3189
|
+
} else {
|
|
3190
|
+
deliveryNotes.push("PR \u521B\u5EFA\uFF1A\u672A\u5F00\u542F\uFF08\u672A\u68C0\u6D4B\u5230\u5DF2\u6709 PR\uFF09");
|
|
3191
|
+
}
|
|
3192
|
+
} else {
|
|
3193
|
+
deliveryNotes.push("PR \u521B\u5EFA\uFF1A\u672A\u5F00\u542F\uFF08\u7F3A\u5C11\u5206\u652F\u540D\uFF09");
|
|
3194
|
+
}
|
|
3195
|
+
if (deliveryNotes.length > 0) {
|
|
3196
|
+
const record = formatSystemRecord("\u63D0\u4EA4\u4E0EPR", deliveryNotes.join("\n"), isoNow());
|
|
3197
|
+
await appendSection(workflowFiles.notesFile, record);
|
|
2291
3198
|
}
|
|
2292
3199
|
if (accumulatedUsage) {
|
|
2293
3200
|
const input = accumulatedUsage.inputTokens ?? "-";
|
|
@@ -2310,6 +3217,7 @@ async function runLoop(config) {
|
|
|
2310
3217
|
});
|
|
2311
3218
|
}
|
|
2312
3219
|
logger.success(`wheel-ai \u8FED\u4EE3\u6D41\u7A0B\u7ED3\u675F\uFF5CToken \u603B\u8BA1 ${accumulatedUsage?.totalTokens ?? "\u672A\u77E5"}`);
|
|
3220
|
+
return { branchName };
|
|
2313
3221
|
} catch (error) {
|
|
2314
3222
|
runError = error instanceof Error ? error.message : String(error);
|
|
2315
3223
|
throw error;
|
|
@@ -2320,23 +3228,164 @@ async function runLoop(config) {
|
|
|
2320
3228
|
}
|
|
2321
3229
|
}
|
|
2322
3230
|
|
|
3231
|
+
// src/multi-task.ts
|
|
3232
|
+
var import_node_path10 = __toESM(require("path"));
|
|
3233
|
+
var MODE_ALIASES = {
|
|
3234
|
+
relay: "relay",
|
|
3235
|
+
serial: "serial",
|
|
3236
|
+
"serial-continue": "serial-continue",
|
|
3237
|
+
parallel: "parallel",
|
|
3238
|
+
\u63A5\u529B\u6A21\u5F0F: "relay",
|
|
3239
|
+
\u63A5\u529B: "relay",
|
|
3240
|
+
\u4E32\u884C\u6267\u884C: "serial",
|
|
3241
|
+
\u4E32\u884C: "serial",
|
|
3242
|
+
\u4E32\u884C\u6267\u884C\u4F46\u662F\u5931\u8D25\u4E5F\u7EE7\u7EED: "serial-continue",
|
|
3243
|
+
\u4E32\u884C\u7EE7\u7EED: "serial-continue",
|
|
3244
|
+
\u5E76\u884C\u6267\u884C: "parallel",
|
|
3245
|
+
\u5E76\u884C: "parallel"
|
|
3246
|
+
};
|
|
3247
|
+
function parseMultiTaskMode(raw) {
|
|
3248
|
+
if (!raw) return "relay";
|
|
3249
|
+
const trimmed = raw.trim();
|
|
3250
|
+
if (!trimmed) return "relay";
|
|
3251
|
+
const resolved = MODE_ALIASES[trimmed];
|
|
3252
|
+
if (!resolved) {
|
|
3253
|
+
throw new Error(`\u672A\u77E5 multi-task \u6A21\u5F0F: ${raw}`);
|
|
3254
|
+
}
|
|
3255
|
+
return resolved;
|
|
3256
|
+
}
|
|
3257
|
+
function normalizeTaskList(input) {
|
|
3258
|
+
if (Array.isArray(input)) {
|
|
3259
|
+
return input.map((task) => task.trim()).filter((task) => task.length > 0);
|
|
3260
|
+
}
|
|
3261
|
+
if (typeof input === "string") {
|
|
3262
|
+
const trimmed = input.trim();
|
|
3263
|
+
return trimmed.length > 0 ? [trimmed] : [];
|
|
3264
|
+
}
|
|
3265
|
+
return [];
|
|
3266
|
+
}
|
|
3267
|
+
function buildBranchNameSeries(branchInput, total) {
|
|
3268
|
+
if (total <= 0) return [];
|
|
3269
|
+
if (!branchInput) {
|
|
3270
|
+
return Array.from({ length: total }, () => void 0);
|
|
3271
|
+
}
|
|
3272
|
+
const baseName = branchInput;
|
|
3273
|
+
const names = [baseName];
|
|
3274
|
+
for (let i = 1; i < total; i += 1) {
|
|
3275
|
+
names.push(`${baseName}-${i + 1}`);
|
|
3276
|
+
}
|
|
3277
|
+
return names;
|
|
3278
|
+
}
|
|
3279
|
+
function appendPathSuffix(filePath, suffix) {
|
|
3280
|
+
const parsed = import_node_path10.default.parse(filePath);
|
|
3281
|
+
const nextName = parsed.name ? `${parsed.name}-${suffix}` : suffix;
|
|
3282
|
+
return import_node_path10.default.join(parsed.dir, `${nextName}${parsed.ext}`);
|
|
3283
|
+
}
|
|
3284
|
+
function deriveIndexedPath(basePath, index, total, label) {
|
|
3285
|
+
if (!basePath) return void 0;
|
|
3286
|
+
if (total <= 1 || index === 0) return basePath;
|
|
3287
|
+
return appendPathSuffix(basePath, `${label}-${index + 1}`);
|
|
3288
|
+
}
|
|
3289
|
+
function buildTaskPlans(input) {
|
|
3290
|
+
const total = input.tasks.length;
|
|
3291
|
+
if (total === 0) return [];
|
|
3292
|
+
const branchNames = input.useWorktree ? buildBranchNameSeries(input.branchInput, total) : input.tasks.map(() => input.branchInput);
|
|
3293
|
+
return input.tasks.map((task, index) => {
|
|
3294
|
+
const relayBaseBranch = input.useWorktree && input.mode === "relay" && index > 0 ? branchNames[index - 1] ?? input.baseBranch : input.baseBranch;
|
|
3295
|
+
return {
|
|
3296
|
+
task,
|
|
3297
|
+
index,
|
|
3298
|
+
branchName: branchNames[index],
|
|
3299
|
+
baseBranch: relayBaseBranch,
|
|
3300
|
+
worktreePath: deriveIndexedPath(input.worktreePath, index, total, "task"),
|
|
3301
|
+
logFile: deriveIndexedPath(input.logFile, index, total, "task")
|
|
3302
|
+
};
|
|
3303
|
+
});
|
|
3304
|
+
}
|
|
3305
|
+
|
|
2323
3306
|
// src/monitor.ts
|
|
2324
|
-
var
|
|
2325
|
-
var
|
|
3307
|
+
var import_fs_extra10 = __toESM(require("fs-extra"));
|
|
3308
|
+
var import_node_path11 = __toESM(require("path"));
|
|
2326
3309
|
var REFRESH_INTERVAL = 1e3;
|
|
2327
|
-
|
|
3310
|
+
var TERMINATE_GRACE_MS = 800;
|
|
3311
|
+
function getTerminalSize3() {
|
|
2328
3312
|
const rows = process.stdout.rows ?? 24;
|
|
2329
3313
|
const columns = process.stdout.columns ?? 80;
|
|
2330
3314
|
return { rows, columns };
|
|
2331
3315
|
}
|
|
2332
|
-
function
|
|
3316
|
+
function truncateLine3(line, width) {
|
|
2333
3317
|
if (width <= 0) return "";
|
|
2334
3318
|
if (line.length <= width) return line;
|
|
2335
3319
|
return line.slice(0, width);
|
|
2336
3320
|
}
|
|
3321
|
+
function padLine(text, width) {
|
|
3322
|
+
if (width <= 0) return "";
|
|
3323
|
+
const truncated = text.length > width ? text.slice(0, width) : text;
|
|
3324
|
+
const padding = width - truncated.length;
|
|
3325
|
+
const left = Math.floor(padding / 2);
|
|
3326
|
+
const right = padding - left;
|
|
3327
|
+
return `${" ".repeat(left)}${truncated}${" ".repeat(right)}`;
|
|
3328
|
+
}
|
|
3329
|
+
function buildConfirmDialogLines(taskKey, columns) {
|
|
3330
|
+
if (columns <= 0) return [];
|
|
3331
|
+
const message = `\u786E\u8BA4\u7EC8\u6B62\u4EFB\u52A1 ${taskKey}?`;
|
|
3332
|
+
const hint = "y \u786E\u8BA4 / n \u53D6\u6D88";
|
|
3333
|
+
const minWidth = Math.max(message.length, hint.length, 4);
|
|
3334
|
+
const innerWidth = Math.min(columns - 2, minWidth);
|
|
3335
|
+
if (innerWidth <= 0) {
|
|
3336
|
+
return [truncateLine3(message, columns), truncateLine3(hint, columns)];
|
|
3337
|
+
}
|
|
3338
|
+
const border = `+${"-".repeat(innerWidth)}+`;
|
|
3339
|
+
const lines = [
|
|
3340
|
+
border,
|
|
3341
|
+
`|${padLine(message, innerWidth)}|`,
|
|
3342
|
+
`|${padLine(hint, innerWidth)}|`,
|
|
3343
|
+
border
|
|
3344
|
+
];
|
|
3345
|
+
return lines.map((line) => truncateLine3(line, columns));
|
|
3346
|
+
}
|
|
3347
|
+
function applyDialogOverlay(lines, dialogLines) {
|
|
3348
|
+
if (lines.length === 0 || dialogLines.length === 0) return;
|
|
3349
|
+
const start = Math.max(0, Math.floor((lines.length - dialogLines.length) / 2));
|
|
3350
|
+
for (let i = 0; i < dialogLines.length && start + i < lines.length; i += 1) {
|
|
3351
|
+
lines[start + i] = dialogLines[i];
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
function resolveTerminationTarget(pid, platform = process.platform) {
|
|
3355
|
+
return platform === "win32" ? pid : -pid;
|
|
3356
|
+
}
|
|
3357
|
+
function isProcessAlive(pid) {
|
|
3358
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
3359
|
+
try {
|
|
3360
|
+
process.kill(pid, 0);
|
|
3361
|
+
return true;
|
|
3362
|
+
} catch (error) {
|
|
3363
|
+
const err = error;
|
|
3364
|
+
if (err?.code === "ESRCH") return false;
|
|
3365
|
+
return true;
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
function sleep(ms) {
|
|
3369
|
+
return new Promise((resolve) => {
|
|
3370
|
+
setTimeout(resolve, ms);
|
|
3371
|
+
});
|
|
3372
|
+
}
|
|
3373
|
+
function setStatus(state, message, isError = false) {
|
|
3374
|
+
state.statusMessage = message;
|
|
3375
|
+
state.statusIsError = message ? isError : false;
|
|
3376
|
+
}
|
|
3377
|
+
function findTaskByKey(state, key) {
|
|
3378
|
+
return state.tasks.find((task) => task.key === key);
|
|
3379
|
+
}
|
|
3380
|
+
async function safeRemoveRegistry(logFile) {
|
|
3381
|
+
try {
|
|
3382
|
+
await removeCurrentRegistry(logFile);
|
|
3383
|
+
} catch {
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
2337
3386
|
async function readLogLines2(logFile) {
|
|
2338
3387
|
try {
|
|
2339
|
-
const content = await
|
|
3388
|
+
const content = await import_fs_extra10.default.readFile(logFile, "utf8");
|
|
2340
3389
|
const normalized = content.replace(/\r\n?/g, "\n");
|
|
2341
3390
|
const lines = normalized.split("\n");
|
|
2342
3391
|
return lines.length > 0 ? lines : [""];
|
|
@@ -2348,8 +3397,18 @@ async function readLogLines2(logFile) {
|
|
|
2348
3397
|
async function loadTasks(logsDir) {
|
|
2349
3398
|
const registry = await readCurrentRegistry();
|
|
2350
3399
|
const entries = Object.entries(registry).sort(([a], [b]) => a.localeCompare(b));
|
|
2351
|
-
const
|
|
2352
|
-
|
|
3400
|
+
const aliveEntries = [];
|
|
3401
|
+
for (const [key, meta] of entries) {
|
|
3402
|
+
const pid = typeof meta.pid === "number" ? meta.pid : void 0;
|
|
3403
|
+
if (pid && !isProcessAlive(pid)) {
|
|
3404
|
+
const logFile = meta.logFile ?? import_node_path11.default.join(logsDir, key);
|
|
3405
|
+
await safeRemoveRegistry(logFile);
|
|
3406
|
+
continue;
|
|
3407
|
+
}
|
|
3408
|
+
aliveEntries.push([key, meta]);
|
|
3409
|
+
}
|
|
3410
|
+
const tasks = await Promise.all(aliveEntries.map(async ([key, meta]) => {
|
|
3411
|
+
const logFile = meta.logFile ?? import_node_path11.default.join(logsDir, key);
|
|
2353
3412
|
const lines = await readLogLines2(logFile);
|
|
2354
3413
|
return {
|
|
2355
3414
|
key,
|
|
@@ -2360,56 +3419,71 @@ async function loadTasks(logsDir) {
|
|
|
2360
3419
|
}));
|
|
2361
3420
|
return tasks;
|
|
2362
3421
|
}
|
|
2363
|
-
function
|
|
3422
|
+
function buildHeader2(state, columns) {
|
|
2364
3423
|
if (state.tasks.length === 0) {
|
|
2365
|
-
return
|
|
3424
|
+
return truncateLine3("\u6682\u65E0\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\uFF0C\u6309 q \u9000\u51FA", columns);
|
|
2366
3425
|
}
|
|
2367
3426
|
const current = state.tasks[state.selectedIndex];
|
|
2368
3427
|
const total = state.tasks.length;
|
|
2369
3428
|
const index = state.selectedIndex + 1;
|
|
2370
|
-
const title = `\u4EFB\u52A1 ${index}/${total} \uFF5C ${current.key} \uFF5C \u2190/\u2192 \u5207\u6362\u4EFB\u52A1 \u2191/\u2193 \u7FFB\u9875 q \u9000\u51FA`;
|
|
2371
|
-
return
|
|
3429
|
+
const title = `\u4EFB\u52A1 ${index}/${total} \uFF5C ${current.key} \uFF5C \u2190/\u2192 \u5207\u6362\u4EFB\u52A1 \u2191/\u2193 \u4E0A\u4E0B 1 \u884C PageUp/PageDown \u7FFB\u9875 t \u7EC8\u6B62 q \u9000\u51FA`;
|
|
3430
|
+
return truncateLine3(title, columns);
|
|
2372
3431
|
}
|
|
2373
|
-
function
|
|
3432
|
+
function buildStatus2(task, page, columns, errorMessage, statusMessage, statusIsError) {
|
|
2374
3433
|
const meta = task.meta;
|
|
2375
3434
|
const status = `\u8F6E\u6B21 ${meta.round} \uFF5C Token ${meta.tokenUsed} \uFF5C \u9875 ${page.current}/${page.total} \uFF5C \u9879\u76EE ${meta.path}`;
|
|
2376
|
-
const
|
|
2377
|
-
|
|
3435
|
+
const extras = [];
|
|
3436
|
+
if (errorMessage) {
|
|
3437
|
+
extras.push(`\u5237\u65B0\u5931\u8D25\uFF1A${errorMessage}`);
|
|
3438
|
+
}
|
|
3439
|
+
if (statusMessage) {
|
|
3440
|
+
extras.push(statusIsError ? `\u64CD\u4F5C\u5931\u8D25\uFF1A${statusMessage}` : statusMessage);
|
|
3441
|
+
}
|
|
3442
|
+
const suffix = extras.length > 0 ? ` \uFF5C ${extras.join(" \uFF5C ")}` : "";
|
|
3443
|
+
return truncateLine3(`${status}${suffix}`, columns);
|
|
2378
3444
|
}
|
|
2379
|
-
function
|
|
3445
|
+
function getPageSize3(rows) {
|
|
2380
3446
|
return Math.max(1, rows - 2);
|
|
2381
3447
|
}
|
|
2382
|
-
function
|
|
2383
|
-
const { rows, columns } =
|
|
2384
|
-
const pageSize =
|
|
2385
|
-
const header =
|
|
3448
|
+
function render3(state) {
|
|
3449
|
+
const { rows, columns } = getTerminalSize3();
|
|
3450
|
+
const pageSize = getPageSize3(rows);
|
|
3451
|
+
const header = buildHeader2(state, columns);
|
|
2386
3452
|
if (state.tasks.length === 0) {
|
|
2387
3453
|
const filler = Array.from({ length: pageSize }, () => "");
|
|
2388
|
-
const statusText = state.lastError ? `\u5237\u65B0\u5931\u8D25\uFF1A${state.lastError}` : "\u7B49\u5F85\u540E\u53F0\u4EFB\u52A1\u542F\u52A8\u2026";
|
|
2389
|
-
const status2 =
|
|
3454
|
+
const statusText = state.lastError ? `\u5237\u65B0\u5931\u8D25\uFF1A${state.lastError}` : state.statusMessage ? state.statusIsError ? `\u64CD\u4F5C\u5931\u8D25\uFF1A${state.statusMessage}` : state.statusMessage : "\u7B49\u5F85\u540E\u53F0\u4EFB\u52A1\u542F\u52A8\u2026";
|
|
3455
|
+
const status2 = truncateLine3(statusText, columns);
|
|
2390
3456
|
const content2 = [header, ...filler, status2].join("\n");
|
|
2391
3457
|
process.stdout.write(`\x1B[2J\x1B[H${content2}`);
|
|
2392
3458
|
return;
|
|
2393
3459
|
}
|
|
2394
3460
|
const current = state.tasks[state.selectedIndex];
|
|
2395
3461
|
const lines = current.lines;
|
|
2396
|
-
const maxOffset = Math.max(0,
|
|
2397
|
-
const offset = state.
|
|
3462
|
+
const maxOffset = Math.max(0, lines.length - pageSize);
|
|
3463
|
+
const offset = state.lineOffsets.get(current.key) ?? maxOffset;
|
|
2398
3464
|
const stick = state.stickToBottom.get(current.key) ?? true;
|
|
2399
3465
|
const nextOffset = Math.min(Math.max(stick ? maxOffset : offset, 0), maxOffset);
|
|
2400
|
-
state.
|
|
3466
|
+
state.lineOffsets.set(current.key, nextOffset);
|
|
2401
3467
|
state.stickToBottom.set(current.key, nextOffset === maxOffset);
|
|
2402
|
-
const start = nextOffset
|
|
2403
|
-
const pageLines = lines.slice(start, start + pageSize).map((line) =>
|
|
3468
|
+
const start = nextOffset;
|
|
3469
|
+
const pageLines = lines.slice(start, start + pageSize).map((line) => truncateLine3(line, columns));
|
|
2404
3470
|
while (pageLines.length < pageSize) {
|
|
2405
3471
|
pageLines.push("");
|
|
2406
3472
|
}
|
|
2407
|
-
const
|
|
3473
|
+
const totalPages = Math.max(1, Math.ceil(lines.length / pageSize));
|
|
3474
|
+
const currentPage = Math.min(totalPages, Math.floor(nextOffset / pageSize) + 1);
|
|
3475
|
+
const status = buildStatus2(
|
|
2408
3476
|
current,
|
|
2409
|
-
{ current:
|
|
3477
|
+
{ current: currentPage, total: totalPages },
|
|
2410
3478
|
columns,
|
|
2411
|
-
state.lastError
|
|
3479
|
+
state.lastError,
|
|
3480
|
+
state.statusMessage,
|
|
3481
|
+
state.statusIsError
|
|
2412
3482
|
);
|
|
3483
|
+
if (state.confirm) {
|
|
3484
|
+
const dialogLines = buildConfirmDialogLines(state.confirm.key, columns);
|
|
3485
|
+
applyDialogOverlay(pageLines, dialogLines);
|
|
3486
|
+
}
|
|
2413
3487
|
const content = [header, ...pageLines, status].join("\n");
|
|
2414
3488
|
process.stdout.write(`\x1B[2J\x1B[H${content}`);
|
|
2415
3489
|
}
|
|
@@ -2432,9 +3506,9 @@ function updateSelection(state, tasks) {
|
|
|
2432
3506
|
}
|
|
2433
3507
|
state.selectedKey = tasks[state.selectedIndex]?.key;
|
|
2434
3508
|
const existing = new Set(tasks.map((task) => task.key));
|
|
2435
|
-
for (const key of state.
|
|
3509
|
+
for (const key of state.lineOffsets.keys()) {
|
|
2436
3510
|
if (!existing.has(key)) {
|
|
2437
|
-
state.
|
|
3511
|
+
state.lineOffsets.delete(key);
|
|
2438
3512
|
}
|
|
2439
3513
|
}
|
|
2440
3514
|
for (const key of state.stickToBottom.keys()) {
|
|
@@ -2442,6 +3516,9 @@ function updateSelection(state, tasks) {
|
|
|
2442
3516
|
state.stickToBottom.delete(key);
|
|
2443
3517
|
}
|
|
2444
3518
|
}
|
|
3519
|
+
if (state.confirm && !existing.has(state.confirm.key)) {
|
|
3520
|
+
state.confirm = void 0;
|
|
3521
|
+
}
|
|
2445
3522
|
}
|
|
2446
3523
|
function moveSelection(state, direction) {
|
|
2447
3524
|
if (state.tasks.length === 0) return;
|
|
@@ -2449,24 +3526,92 @@ function moveSelection(state, direction) {
|
|
|
2449
3526
|
state.selectedIndex = (state.selectedIndex + direction + total) % total;
|
|
2450
3527
|
state.selectedKey = state.tasks[state.selectedIndex]?.key;
|
|
2451
3528
|
}
|
|
2452
|
-
function
|
|
3529
|
+
function moveLine(state, direction) {
|
|
2453
3530
|
if (state.tasks.length === 0) return;
|
|
2454
|
-
const { rows } = getTerminalSize2();
|
|
2455
|
-
const pageSize = getPageSize2(rows);
|
|
2456
3531
|
const current = state.tasks[state.selectedIndex];
|
|
2457
3532
|
const lines = current.lines;
|
|
2458
|
-
const
|
|
2459
|
-
const
|
|
3533
|
+
const { rows } = getTerminalSize3();
|
|
3534
|
+
const pageSize = getPageSize3(rows);
|
|
3535
|
+
const maxOffset = Math.max(0, lines.length - pageSize);
|
|
3536
|
+
const offset = state.lineOffsets.get(current.key) ?? maxOffset;
|
|
2460
3537
|
const nextOffset = Math.min(Math.max(offset + direction, 0), maxOffset);
|
|
2461
|
-
state.
|
|
3538
|
+
state.lineOffsets.set(current.key, nextOffset);
|
|
2462
3539
|
state.stickToBottom.set(current.key, nextOffset === maxOffset);
|
|
2463
3540
|
}
|
|
2464
|
-
function
|
|
3541
|
+
function movePage(state, direction) {
|
|
3542
|
+
if (state.tasks.length === 0) return;
|
|
3543
|
+
const { rows } = getTerminalSize3();
|
|
3544
|
+
const pageSize = getPageSize3(rows);
|
|
3545
|
+
const current = state.tasks[state.selectedIndex];
|
|
3546
|
+
const lines = current.lines;
|
|
3547
|
+
const maxOffset = Math.max(0, lines.length - pageSize);
|
|
3548
|
+
const offset = state.lineOffsets.get(current.key) ?? maxOffset;
|
|
3549
|
+
const nextOffset = Math.min(Math.max(offset + direction * pageSize, 0), maxOffset);
|
|
3550
|
+
state.lineOffsets.set(current.key, nextOffset);
|
|
3551
|
+
state.stickToBottom.set(current.key, nextOffset === maxOffset);
|
|
3552
|
+
}
|
|
3553
|
+
async function terminateTask(task) {
|
|
3554
|
+
const pid = typeof task.meta.pid === "number" ? task.meta.pid : void 0;
|
|
3555
|
+
if (!pid || pid <= 0) {
|
|
3556
|
+
return { message: "\u4EFB\u52A1\u672A\u8BB0\u5F55 PID\uFF0C\u65E0\u6CD5\u7EC8\u6B62", isError: true, removed: false };
|
|
3557
|
+
}
|
|
3558
|
+
const target = resolveTerminationTarget(pid);
|
|
3559
|
+
try {
|
|
3560
|
+
process.kill(target, "SIGTERM");
|
|
3561
|
+
} catch (error) {
|
|
3562
|
+
const err = error;
|
|
3563
|
+
if (err?.code === "ESRCH") {
|
|
3564
|
+
await safeRemoveRegistry(task.logFile);
|
|
3565
|
+
return { message: `\u4EFB\u52A1 ${task.key} \u5DF2\u7ED3\u675F`, isError: false, removed: true };
|
|
3566
|
+
}
|
|
3567
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3568
|
+
return { message: `\u53D1\u9001\u7EC8\u6B62\u4FE1\u53F7\u5931\u8D25\uFF1A${message}`, isError: true, removed: false };
|
|
3569
|
+
}
|
|
3570
|
+
await sleep(TERMINATE_GRACE_MS);
|
|
3571
|
+
if (!isProcessAlive(pid)) {
|
|
3572
|
+
await safeRemoveRegistry(task.logFile);
|
|
3573
|
+
return { message: `\u4EFB\u52A1 ${task.key} \u5DF2\u7EC8\u6B62`, isError: false, removed: true };
|
|
3574
|
+
}
|
|
3575
|
+
return { message: `\u5DF2\u53D1\u9001\u7EC8\u6B62\u4FE1\u53F7\uFF0C\u4EFB\u52A1\u4ECD\u5728\u8FD0\u884C`, isError: false, removed: false };
|
|
3576
|
+
}
|
|
3577
|
+
async function terminateTaskByKey(state, key, refresh) {
|
|
3578
|
+
const task = findTaskByKey(state, key);
|
|
3579
|
+
if (!task) {
|
|
3580
|
+
setStatus(state, `\u4EFB\u52A1 ${key} \u5DF2\u4E0D\u5B58\u5728`, true);
|
|
3581
|
+
render3(state);
|
|
3582
|
+
return;
|
|
3583
|
+
}
|
|
3584
|
+
const result = await terminateTask(task);
|
|
3585
|
+
setStatus(state, result.message, result.isError);
|
|
3586
|
+
if (result.removed) {
|
|
3587
|
+
await refresh();
|
|
3588
|
+
render3(state);
|
|
3589
|
+
return;
|
|
3590
|
+
}
|
|
3591
|
+
render3(state);
|
|
3592
|
+
}
|
|
3593
|
+
function shouldExit3(input) {
|
|
2465
3594
|
if (input === "") return true;
|
|
2466
3595
|
if (input.toLowerCase() === "q") return true;
|
|
2467
3596
|
return false;
|
|
2468
3597
|
}
|
|
2469
|
-
function handleInput(state, input) {
|
|
3598
|
+
function handleInput(state, input, refresh) {
|
|
3599
|
+
const lower = input.toLowerCase();
|
|
3600
|
+
if (state.confirm) {
|
|
3601
|
+
if (lower.includes("y")) {
|
|
3602
|
+
const key = state.confirm.key;
|
|
3603
|
+
state.confirm = void 0;
|
|
3604
|
+
setStatus(state, `\u6B63\u5728\u7EC8\u6B62\u4EFB\u52A1 ${key}...`);
|
|
3605
|
+
void terminateTaskByKey(state, key, refresh);
|
|
3606
|
+
return;
|
|
3607
|
+
}
|
|
3608
|
+
if (lower.includes("n") || input === "\x1B") {
|
|
3609
|
+
state.confirm = void 0;
|
|
3610
|
+
setStatus(state, "\u5DF2\u53D6\u6D88\u7EC8\u6B62");
|
|
3611
|
+
return;
|
|
3612
|
+
}
|
|
3613
|
+
return;
|
|
3614
|
+
}
|
|
2470
3615
|
if (input.includes("\x1B[D")) {
|
|
2471
3616
|
moveSelection(state, -1);
|
|
2472
3617
|
return;
|
|
@@ -2476,15 +3621,36 @@ function handleInput(state, input) {
|
|
|
2476
3621
|
return;
|
|
2477
3622
|
}
|
|
2478
3623
|
if (input.includes("\x1B[A")) {
|
|
2479
|
-
|
|
3624
|
+
moveLine(state, -1);
|
|
2480
3625
|
return;
|
|
2481
3626
|
}
|
|
2482
3627
|
if (input.includes("\x1B[B")) {
|
|
3628
|
+
moveLine(state, 1);
|
|
3629
|
+
return;
|
|
3630
|
+
}
|
|
3631
|
+
if (input.includes("\x1B[5~")) {
|
|
3632
|
+
movePage(state, -1);
|
|
3633
|
+
return;
|
|
3634
|
+
}
|
|
3635
|
+
if (input.includes("\x1B[6~")) {
|
|
2483
3636
|
movePage(state, 1);
|
|
2484
3637
|
return;
|
|
2485
3638
|
}
|
|
3639
|
+
if (lower.includes("t")) {
|
|
3640
|
+
if (state.tasks.length === 0) {
|
|
3641
|
+
setStatus(state, "\u6682\u65E0\u53EF\u7EC8\u6B62\u7684\u4EFB\u52A1", true);
|
|
3642
|
+
return;
|
|
3643
|
+
}
|
|
3644
|
+
const current = state.tasks[state.selectedIndex];
|
|
3645
|
+
if (typeof current.meta.pid !== "number" || current.meta.pid <= 0) {
|
|
3646
|
+
setStatus(state, "\u4EFB\u52A1\u672A\u8BB0\u5F55 PID\uFF0C\u65E0\u6CD5\u7EC8\u6B62", true);
|
|
3647
|
+
return;
|
|
3648
|
+
}
|
|
3649
|
+
state.confirm = { key: current.key };
|
|
3650
|
+
setStatus(state, void 0);
|
|
3651
|
+
}
|
|
2486
3652
|
}
|
|
2487
|
-
function
|
|
3653
|
+
function setupCleanup3(cleanup) {
|
|
2488
3654
|
const exitHandler = () => {
|
|
2489
3655
|
cleanup();
|
|
2490
3656
|
};
|
|
@@ -2505,7 +3671,7 @@ async function runMonitor() {
|
|
|
2505
3671
|
const state = {
|
|
2506
3672
|
tasks: [],
|
|
2507
3673
|
selectedIndex: 0,
|
|
2508
|
-
|
|
3674
|
+
lineOffsets: /* @__PURE__ */ new Map(),
|
|
2509
3675
|
stickToBottom: /* @__PURE__ */ new Map()
|
|
2510
3676
|
};
|
|
2511
3677
|
let cleaned = false;
|
|
@@ -2518,7 +3684,7 @@ async function runMonitor() {
|
|
|
2518
3684
|
}
|
|
2519
3685
|
process.stdout.write("\x1B[?25h");
|
|
2520
3686
|
};
|
|
2521
|
-
|
|
3687
|
+
setupCleanup3(cleanup);
|
|
2522
3688
|
process.stdout.write("\x1B[?25l");
|
|
2523
3689
|
process.stdin.setRawMode(true);
|
|
2524
3690
|
process.stdin.resume();
|
|
@@ -2530,11 +3696,11 @@ async function runMonitor() {
|
|
|
2530
3696
|
const tasks = await loadTasks(logsDir);
|
|
2531
3697
|
state.lastError = void 0;
|
|
2532
3698
|
updateSelection(state, tasks);
|
|
2533
|
-
|
|
3699
|
+
render3(state);
|
|
2534
3700
|
} catch (error) {
|
|
2535
3701
|
const message = error instanceof Error ? error.message : String(error);
|
|
2536
3702
|
state.lastError = message;
|
|
2537
|
-
|
|
3703
|
+
render3(state);
|
|
2538
3704
|
} finally {
|
|
2539
3705
|
refreshing = false;
|
|
2540
3706
|
}
|
|
@@ -2543,20 +3709,102 @@ async function runMonitor() {
|
|
|
2543
3709
|
const timer = setInterval(refresh, REFRESH_INTERVAL);
|
|
2544
3710
|
process.stdin.on("data", (data) => {
|
|
2545
3711
|
const input = data.toString("utf8");
|
|
2546
|
-
if (
|
|
3712
|
+
if (shouldExit3(input)) {
|
|
2547
3713
|
clearInterval(timer);
|
|
2548
3714
|
cleanup();
|
|
2549
3715
|
process.exit(0);
|
|
2550
3716
|
}
|
|
2551
|
-
handleInput(state, input);
|
|
2552
|
-
|
|
3717
|
+
handleInput(state, input, refresh);
|
|
3718
|
+
render3(state);
|
|
2553
3719
|
});
|
|
2554
3720
|
process.stdout.on("resize", () => {
|
|
2555
|
-
|
|
3721
|
+
render3(state);
|
|
2556
3722
|
});
|
|
2557
3723
|
}
|
|
2558
3724
|
|
|
3725
|
+
// src/log-tailer.ts
|
|
3726
|
+
var import_promises = require("fs/promises");
|
|
3727
|
+
var import_fs_extra11 = __toESM(require("fs-extra"));
|
|
3728
|
+
function normalizeChunk(chunk) {
|
|
3729
|
+
return chunk.replace(/\r\n?/g, "\n");
|
|
3730
|
+
}
|
|
3731
|
+
async function tailLogFile(options) {
|
|
3732
|
+
const intervalMs = options.pollIntervalMs ?? 200;
|
|
3733
|
+
let offset = 0;
|
|
3734
|
+
let buffer = "";
|
|
3735
|
+
let reading = false;
|
|
3736
|
+
let stopped = false;
|
|
3737
|
+
if (options.startFromEnd) {
|
|
3738
|
+
try {
|
|
3739
|
+
const stat = await import_fs_extra11.default.stat(options.filePath);
|
|
3740
|
+
offset = stat.size;
|
|
3741
|
+
} catch {
|
|
3742
|
+
offset = 0;
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
const flushBuffer = () => {
|
|
3746
|
+
if (!buffer) return;
|
|
3747
|
+
options.onLine(buffer);
|
|
3748
|
+
buffer = "";
|
|
3749
|
+
};
|
|
3750
|
+
const emitChunk = (chunk) => {
|
|
3751
|
+
buffer += normalizeChunk(chunk);
|
|
3752
|
+
const parts = buffer.split("\n");
|
|
3753
|
+
buffer = parts.pop() ?? "";
|
|
3754
|
+
for (const line of parts) {
|
|
3755
|
+
options.onLine(line);
|
|
3756
|
+
}
|
|
3757
|
+
};
|
|
3758
|
+
const readNew = async () => {
|
|
3759
|
+
if (reading || stopped) return;
|
|
3760
|
+
reading = true;
|
|
3761
|
+
try {
|
|
3762
|
+
const stat = await import_fs_extra11.default.stat(options.filePath);
|
|
3763
|
+
if (stat.size < offset) {
|
|
3764
|
+
offset = stat.size;
|
|
3765
|
+
buffer = "";
|
|
3766
|
+
}
|
|
3767
|
+
if (stat.size > offset) {
|
|
3768
|
+
const length = stat.size - offset;
|
|
3769
|
+
const handle = await (0, import_promises.open)(options.filePath, "r");
|
|
3770
|
+
try {
|
|
3771
|
+
const payload = Buffer.alloc(length);
|
|
3772
|
+
const result = await handle.read(payload, 0, length, offset);
|
|
3773
|
+
offset += result.bytesRead;
|
|
3774
|
+
if (result.bytesRead > 0) {
|
|
3775
|
+
const text = payload.subarray(0, result.bytesRead).toString("utf8");
|
|
3776
|
+
emitChunk(text);
|
|
3777
|
+
}
|
|
3778
|
+
} finally {
|
|
3779
|
+
await handle.close();
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
} catch (error) {
|
|
3783
|
+
const err = error;
|
|
3784
|
+
if (err?.code !== "ENOENT") {
|
|
3785
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3786
|
+
options.onError?.(message);
|
|
3787
|
+
}
|
|
3788
|
+
} finally {
|
|
3789
|
+
reading = false;
|
|
3790
|
+
}
|
|
3791
|
+
};
|
|
3792
|
+
const timer = setInterval(() => {
|
|
3793
|
+
void readNew();
|
|
3794
|
+
}, intervalMs);
|
|
3795
|
+
await readNew();
|
|
3796
|
+
return {
|
|
3797
|
+
stop: async () => {
|
|
3798
|
+
if (stopped) return;
|
|
3799
|
+
stopped = true;
|
|
3800
|
+
clearInterval(timer);
|
|
3801
|
+
flushBuffer();
|
|
3802
|
+
}
|
|
3803
|
+
};
|
|
3804
|
+
}
|
|
3805
|
+
|
|
2559
3806
|
// src/cli.ts
|
|
3807
|
+
var FOREGROUND_CHILD_ENV = "WHEEL_AI_FOREGROUND_CHILD";
|
|
2560
3808
|
function parseInteger(value, defaultValue) {
|
|
2561
3809
|
const parsed = Number.parseInt(value, 10);
|
|
2562
3810
|
if (Number.isNaN(parsed)) return defaultValue;
|
|
@@ -2584,30 +3832,132 @@ function buildBackgroundArgs(argv, logFile, branchName, injectBranch = false) {
|
|
|
2584
3832
|
}
|
|
2585
3833
|
return filtered;
|
|
2586
3834
|
}
|
|
3835
|
+
function extractAliasCommandArgs(argv, name) {
|
|
3836
|
+
const args = argv.slice(2);
|
|
3837
|
+
const start = args.findIndex((arg, index) => arg === "set" && args[index + 1] === "alias" && args[index + 2] === name);
|
|
3838
|
+
if (start < 0) return [];
|
|
3839
|
+
const rest = args.slice(start + 3);
|
|
3840
|
+
if (rest[0] === "--") return rest.slice(1);
|
|
3841
|
+
return rest;
|
|
3842
|
+
}
|
|
3843
|
+
async function runForegroundWithDetach(options) {
|
|
3844
|
+
const args = buildBackgroundArgs(options.argv, options.logFile, options.branchName, options.injectBranch);
|
|
3845
|
+
const child = (0, import_node_child_process.spawn)(process.execPath, [...process.execArgv, ...args], {
|
|
3846
|
+
detached: true,
|
|
3847
|
+
stdio: "ignore",
|
|
3848
|
+
env: {
|
|
3849
|
+
...process.env,
|
|
3850
|
+
[FOREGROUND_CHILD_ENV]: "1"
|
|
3851
|
+
}
|
|
3852
|
+
});
|
|
3853
|
+
child.unref();
|
|
3854
|
+
const resolvedLogFile = resolvePath(process.cwd(), options.logFile);
|
|
3855
|
+
const existed = await import_fs_extra12.default.pathExists(resolvedLogFile);
|
|
3856
|
+
const tailer = await tailLogFile({
|
|
3857
|
+
filePath: resolvedLogFile,
|
|
3858
|
+
startFromEnd: existed,
|
|
3859
|
+
onLine: (line) => {
|
|
3860
|
+
process.stdout.write(`${line}
|
|
3861
|
+
`);
|
|
3862
|
+
},
|
|
3863
|
+
onError: (message) => {
|
|
3864
|
+
defaultLogger.warn(`\u65E5\u5FD7\u8BFB\u53D6\u5931\u8D25\uFF1A${message}`);
|
|
3865
|
+
}
|
|
3866
|
+
});
|
|
3867
|
+
const suffixNote = options.isMultiTask ? "\uFF08\u591A\u4EFB\u52A1\u5C06\u8FFD\u52A0\u5E8F\u53F7\uFF09" : "";
|
|
3868
|
+
console.log(`\u5DF2\u8FDB\u5165\u524D\u53F0\u65E5\u5FD7\u67E5\u770B\uFF0C\u6309 Esc \u5207\u5230\u540E\u53F0\u8FD0\u884C\uFF0C\u65E5\u5FD7\u8F93\u51FA\u81F3 ${resolvedLogFile}${suffixNote}`);
|
|
3869
|
+
let cleaned = false;
|
|
3870
|
+
const cleanup = async () => {
|
|
3871
|
+
if (cleaned) return;
|
|
3872
|
+
cleaned = true;
|
|
3873
|
+
await tailer.stop();
|
|
3874
|
+
if (process.stdin.isTTY) {
|
|
3875
|
+
process.stdin.setRawMode(false);
|
|
3876
|
+
process.stdin.pause();
|
|
3877
|
+
}
|
|
3878
|
+
};
|
|
3879
|
+
const detach = async () => {
|
|
3880
|
+
await cleanup();
|
|
3881
|
+
console.log(`\u5DF2\u5207\u5165\u540E\u53F0\u8FD0\u884C\uFF0C\u65E5\u5FD7\u8F93\u51FA\u81F3 ${resolvedLogFile}${suffixNote}`);
|
|
3882
|
+
process.exit(0);
|
|
3883
|
+
};
|
|
3884
|
+
const terminate = async () => {
|
|
3885
|
+
if (child.pid) {
|
|
3886
|
+
try {
|
|
3887
|
+
const target = resolveTerminationTarget(child.pid);
|
|
3888
|
+
process.kill(target, "SIGTERM");
|
|
3889
|
+
} catch (error) {
|
|
3890
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3891
|
+
defaultLogger.warn(`\u7EC8\u6B62\u5B50\u8FDB\u7A0B\u5931\u8D25\uFF1A${message}`);
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
await cleanup();
|
|
3895
|
+
process.exit(0);
|
|
3896
|
+
};
|
|
3897
|
+
if (process.stdin.isTTY) {
|
|
3898
|
+
process.stdin.setRawMode(true);
|
|
3899
|
+
process.stdin.resume();
|
|
3900
|
+
process.stdin.on("data", (data) => {
|
|
3901
|
+
const input = data.toString("utf8");
|
|
3902
|
+
if (input === "\x1B") {
|
|
3903
|
+
void detach();
|
|
3904
|
+
return;
|
|
3905
|
+
}
|
|
3906
|
+
if (input === "") {
|
|
3907
|
+
void terminate();
|
|
3908
|
+
}
|
|
3909
|
+
});
|
|
3910
|
+
}
|
|
3911
|
+
process.on("SIGINT", () => {
|
|
3912
|
+
void terminate();
|
|
3913
|
+
});
|
|
3914
|
+
process.on("SIGTERM", () => {
|
|
3915
|
+
void terminate();
|
|
3916
|
+
});
|
|
3917
|
+
child.on("exit", async (code) => {
|
|
3918
|
+
await cleanup();
|
|
3919
|
+
process.exit(code ?? 0);
|
|
3920
|
+
});
|
|
3921
|
+
}
|
|
2587
3922
|
async function runCli(argv) {
|
|
2588
3923
|
const globalConfig = await loadGlobalConfig(defaultLogger);
|
|
2589
3924
|
const effectiveArgv = applyShortcutArgv(argv, globalConfig);
|
|
2590
3925
|
const program = new import_commander.Command();
|
|
2591
3926
|
program.name("wheel-ai").description("\u57FA\u4E8E AI CLI \u7684\u6301\u7EED\u8FED\u4EE3\u5F00\u53D1\u5DE5\u5177").version("1.0.0");
|
|
2592
|
-
program.command("run").
|
|
3927
|
+
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) => {
|
|
3928
|
+
const tasks = normalizeTaskList(options.task);
|
|
3929
|
+
if (tasks.length === 0) {
|
|
3930
|
+
throw new Error("\u9700\u8981\u81F3\u5C11\u63D0\u4F9B\u4E00\u4E2A\u4EFB\u52A1\u63CF\u8FF0");
|
|
3931
|
+
}
|
|
3932
|
+
const multiTaskMode = parseMultiTaskMode(options.multiTaskMode);
|
|
2593
3933
|
const useWorktree = Boolean(options.worktree);
|
|
3934
|
+
if (multiTaskMode === "parallel" && !useWorktree) {
|
|
3935
|
+
throw new Error("\u5E76\u884C\u6A21\u5F0F\u5FC5\u987B\u542F\u7528 --worktree");
|
|
3936
|
+
}
|
|
2594
3937
|
const branchInput = normalizeOptional(options.branch);
|
|
2595
3938
|
const logFileInput = normalizeOptional(options.logFile);
|
|
3939
|
+
const worktreePathInput = normalizeOptional(options.worktreePath);
|
|
2596
3940
|
const background = Boolean(options.background);
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
3941
|
+
const isMultiTask = tasks.length > 1;
|
|
3942
|
+
const isForegroundChild = process.env[FOREGROUND_CHILD_ENV] === "1";
|
|
3943
|
+
const canForegroundDetach = !background && !isForegroundChild && process.stdout.isTTY && process.stdin.isTTY;
|
|
3944
|
+
const shouldInjectBranch = Boolean(useWorktree && branchInput && !isMultiTask);
|
|
3945
|
+
const branchNameForBackground = branchInput;
|
|
2601
3946
|
let logFile = logFileInput;
|
|
2602
|
-
if (background && !logFile) {
|
|
2603
|
-
let branchForLog =
|
|
2604
|
-
if (!
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
3947
|
+
if ((background || canForegroundDetach) && !logFile) {
|
|
3948
|
+
let branchForLog = "multi-task";
|
|
3949
|
+
if (!isMultiTask) {
|
|
3950
|
+
branchForLog = branchNameForBackground ?? "";
|
|
3951
|
+
if (!branchForLog) {
|
|
3952
|
+
try {
|
|
3953
|
+
const current = await getCurrentBranch(process.cwd(), defaultLogger);
|
|
3954
|
+
branchForLog = current || "detached";
|
|
3955
|
+
} catch {
|
|
3956
|
+
branchForLog = "unknown";
|
|
3957
|
+
}
|
|
2610
3958
|
}
|
|
3959
|
+
} else if (branchInput) {
|
|
3960
|
+
branchForLog = `${branchInput}-multi`;
|
|
2611
3961
|
}
|
|
2612
3962
|
logFile = buildAutoLogFilePath(branchForLog);
|
|
2613
3963
|
}
|
|
@@ -2615,18 +3965,40 @@ async function runCli(argv) {
|
|
|
2615
3965
|
if (!logFile) {
|
|
2616
3966
|
throw new Error("\u540E\u53F0\u8FD0\u884C\u9700\u8981\u6307\u5B9A\u65E5\u5FD7\u6587\u4EF6");
|
|
2617
3967
|
}
|
|
2618
|
-
const args = buildBackgroundArgs(effectiveArgv, logFile,
|
|
3968
|
+
const args = buildBackgroundArgs(effectiveArgv, logFile, branchNameForBackground, shouldInjectBranch);
|
|
2619
3969
|
const child = (0, import_node_child_process.spawn)(process.execPath, [...process.execArgv, ...args], {
|
|
2620
3970
|
detached: true,
|
|
2621
3971
|
stdio: "ignore"
|
|
2622
3972
|
});
|
|
2623
3973
|
child.unref();
|
|
2624
3974
|
const displayLogFile = resolvePath(process.cwd(), logFile);
|
|
2625
|
-
|
|
3975
|
+
const suffixNote = isMultiTask ? "\uFF08\u591A\u4EFB\u52A1\u5C06\u8FFD\u52A0\u5E8F\u53F7\uFF09" : "";
|
|
3976
|
+
console.log(`\u5DF2\u5207\u5165\u540E\u53F0\u8FD0\u884C\uFF0C\u65E5\u5FD7\u8F93\u51FA\u81F3 ${displayLogFile}${suffixNote}`);
|
|
2626
3977
|
return;
|
|
2627
3978
|
}
|
|
2628
|
-
|
|
2629
|
-
|
|
3979
|
+
if (canForegroundDetach) {
|
|
3980
|
+
if (!logFile) {
|
|
3981
|
+
throw new Error("\u5207\u5165\u540E\u53F0\u9700\u8981\u6307\u5B9A\u65E5\u5FD7\u6587\u4EF6");
|
|
3982
|
+
}
|
|
3983
|
+
await runForegroundWithDetach({
|
|
3984
|
+
argv: effectiveArgv,
|
|
3985
|
+
logFile,
|
|
3986
|
+
branchName: branchNameForBackground,
|
|
3987
|
+
injectBranch: shouldInjectBranch,
|
|
3988
|
+
isMultiTask
|
|
3989
|
+
});
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3992
|
+
const taskPlans = buildTaskPlans({
|
|
3993
|
+
tasks,
|
|
3994
|
+
mode: multiTaskMode,
|
|
3995
|
+
useWorktree,
|
|
3996
|
+
baseBranch: options.baseBranch,
|
|
3997
|
+
branchInput,
|
|
3998
|
+
worktreePath: worktreePathInput,
|
|
3999
|
+
logFile: logFileInput
|
|
4000
|
+
});
|
|
4001
|
+
const baseOptions = {
|
|
2630
4002
|
iterations: options.iterations,
|
|
2631
4003
|
aiCli: options.aiCli,
|
|
2632
4004
|
aiArgs: options.aiArgs ?? [],
|
|
@@ -2635,9 +4007,6 @@ async function runCli(argv) {
|
|
|
2635
4007
|
planFile: options.planFile,
|
|
2636
4008
|
workflowDoc: options.workflowDoc,
|
|
2637
4009
|
useWorktree,
|
|
2638
|
-
branch: branchName,
|
|
2639
|
-
worktreePath: options.worktreePath,
|
|
2640
|
-
baseBranch: options.baseBranch,
|
|
2641
4010
|
runTests: Boolean(options.runTests),
|
|
2642
4011
|
runE2e: Boolean(options.runE2e),
|
|
2643
4012
|
unitCommand: options.unitCommand,
|
|
@@ -2649,22 +4018,91 @@ async function runCli(argv) {
|
|
|
2649
4018
|
prBody: options.prBody,
|
|
2650
4019
|
draft: Boolean(options.draft),
|
|
2651
4020
|
reviewers: options.reviewer ?? [],
|
|
4021
|
+
autoMerge: Boolean(options.autoMerge),
|
|
2652
4022
|
webhookUrls: options.webhook ?? [],
|
|
2653
4023
|
webhookTimeout: options.webhookTimeout,
|
|
2654
4024
|
stopSignal: options.stopSignal,
|
|
2655
|
-
logFile,
|
|
2656
4025
|
verbose: Boolean(options.verbose),
|
|
2657
|
-
skipInstall: Boolean(options.skipInstall)
|
|
4026
|
+
skipInstall: Boolean(options.skipInstall),
|
|
4027
|
+
skipQuality: Boolean(options.skipQuality)
|
|
2658
4028
|
};
|
|
2659
|
-
const
|
|
2660
|
-
|
|
4029
|
+
const dynamicRelay = useWorktree && multiTaskMode === "relay" && !branchInput;
|
|
4030
|
+
let relayBaseBranch = options.baseBranch;
|
|
4031
|
+
const runPlan = async (plan, baseBranchOverride) => {
|
|
4032
|
+
const cliOptions = {
|
|
4033
|
+
task: plan.task,
|
|
4034
|
+
...baseOptions,
|
|
4035
|
+
branch: plan.branchName,
|
|
4036
|
+
worktreePath: plan.worktreePath,
|
|
4037
|
+
baseBranch: baseBranchOverride ?? plan.baseBranch,
|
|
4038
|
+
logFile: plan.logFile
|
|
4039
|
+
};
|
|
4040
|
+
const config = buildLoopConfig(cliOptions, process.cwd());
|
|
4041
|
+
return runLoop(config);
|
|
4042
|
+
};
|
|
4043
|
+
if (multiTaskMode === "parallel") {
|
|
4044
|
+
const results = await Promise.allSettled(taskPlans.map((plan) => runPlan(plan)));
|
|
4045
|
+
const errors = results.flatMap((result, index) => {
|
|
4046
|
+
if (result.status === "fulfilled") return [];
|
|
4047
|
+
const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
4048
|
+
return [`\u4EFB\u52A1 ${index + 1} \u5931\u8D25: ${reason}`];
|
|
4049
|
+
});
|
|
4050
|
+
if (errors.length > 0) {
|
|
4051
|
+
errors.forEach((message) => defaultLogger.warn(message));
|
|
4052
|
+
throw new Error(errors.join("\n"));
|
|
4053
|
+
}
|
|
4054
|
+
return;
|
|
4055
|
+
}
|
|
4056
|
+
if (multiTaskMode === "serial-continue") {
|
|
4057
|
+
const errors = [];
|
|
4058
|
+
for (const plan of taskPlans) {
|
|
4059
|
+
try {
|
|
4060
|
+
const baseBranch = dynamicRelay ? relayBaseBranch : plan.baseBranch;
|
|
4061
|
+
const result = await runPlan(plan, baseBranch);
|
|
4062
|
+
if (dynamicRelay && result.branchName) {
|
|
4063
|
+
relayBaseBranch = result.branchName;
|
|
4064
|
+
}
|
|
4065
|
+
} catch (error) {
|
|
4066
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4067
|
+
errors.push(`\u4EFB\u52A1 ${plan.index + 1} \u5931\u8D25: ${message}`);
|
|
4068
|
+
defaultLogger.warn(`\u4EFB\u52A1 ${plan.index + 1} \u6267\u884C\u5931\u8D25\uFF0C\u7EE7\u7EED\u4E0B\u4E00\u4EFB\u52A1\uFF1A${message}`);
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
if (errors.length > 0) {
|
|
4072
|
+
throw new Error(errors.join("\n"));
|
|
4073
|
+
}
|
|
4074
|
+
return;
|
|
4075
|
+
}
|
|
4076
|
+
for (const plan of taskPlans) {
|
|
4077
|
+
const baseBranch = dynamicRelay ? relayBaseBranch : plan.baseBranch;
|
|
4078
|
+
const result = await runPlan(plan, baseBranch);
|
|
4079
|
+
if (dynamicRelay && result.branchName) {
|
|
4080
|
+
relayBaseBranch = result.branchName;
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
2661
4083
|
});
|
|
2662
|
-
program.command("monitor").description("\u67E5\u770B\u540E\u53F0\u8FD0\u884C\u65E5\u5FD7").action(async () => {
|
|
4084
|
+
program.command("monitor").description("\u67E5\u770B\u540E\u53F0\u8FD0\u884C\u65E5\u5FD7\uFF08t \u7EC8\u6B62\u4EFB\u52A1\uFF09").action(async () => {
|
|
2663
4085
|
await runMonitor();
|
|
2664
4086
|
});
|
|
2665
4087
|
program.command("logs").description("\u67E5\u770B\u5386\u53F2\u65E5\u5FD7").action(async () => {
|
|
2666
4088
|
await runLogsViewer();
|
|
2667
4089
|
});
|
|
4090
|
+
program.command("set").description("\u5199\u5165\u5168\u5C40\u914D\u7F6E").command("alias <name> [options...]").description("\u8BBE\u7F6E alias").allowUnknownOption(true).action(async (name) => {
|
|
4091
|
+
const normalized = normalizeAliasName(name);
|
|
4092
|
+
if (!normalized) {
|
|
4093
|
+
throw new Error("alias \u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A\u4E14\u4E0D\u80FD\u5305\u542B\u7A7A\u767D\u5B57\u7B26");
|
|
4094
|
+
}
|
|
4095
|
+
const commandArgs = extractAliasCommandArgs(effectiveArgv, name);
|
|
4096
|
+
const commandLine = formatCommandLine(commandArgs);
|
|
4097
|
+
if (!commandLine) {
|
|
4098
|
+
throw new Error("alias \u547D\u4EE4\u4E0D\u80FD\u4E3A\u7A7A");
|
|
4099
|
+
}
|
|
4100
|
+
await upsertAliasEntry(normalized, commandLine);
|
|
4101
|
+
console.log(`\u5DF2\u5199\u5165 alias\uFF1A${normalized}`);
|
|
4102
|
+
});
|
|
4103
|
+
program.command("alias").alias("aliases").description("\u6D4F\u89C8\u5168\u5C40 alias \u914D\u7F6E").action(async () => {
|
|
4104
|
+
await runAliasViewer();
|
|
4105
|
+
});
|
|
2668
4106
|
await program.parseAsync(effectiveArgv);
|
|
2669
4107
|
}
|
|
2670
4108
|
if (require.main === module) {
|