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