hamster-wheel-cli 0.1.0
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/.github/ISSUE_TEMPLATE/bug_report.yml +107 -0
- package/.github/ISSUE_TEMPLATE/config.yml +15 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +56 -0
- package/.github/workflows/ci-pr.yml +50 -0
- package/.github/workflows/publish.yml +121 -0
- package/.github/workflows/sync-master-to-dev.yml +100 -0
- package/AGENTS.md +20 -0
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +90 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +2678 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +2682 -0
- package/dist/index.js.map +1 -0
- package/docs/ai-workflow.md +58 -0
- package/package.json +44 -0
- package/src/ai.ts +173 -0
- package/src/cli.ts +189 -0
- package/src/config.ts +134 -0
- package/src/deps.ts +210 -0
- package/src/gh.ts +228 -0
- package/src/git.ts +285 -0
- package/src/global-config.ts +296 -0
- package/src/index.ts +3 -0
- package/src/logger.ts +122 -0
- package/src/logs-viewer.ts +420 -0
- package/src/logs.ts +132 -0
- package/src/loop.ts +422 -0
- package/src/monitor.ts +291 -0
- package/src/runtime-tracker.ts +65 -0
- package/src/summary.ts +255 -0
- package/src/types.ts +176 -0
- package/src/utils.ts +179 -0
- package/src/webhook.ts +107 -0
- package/tests/deps.test.ts +72 -0
- package/tests/e2e/cli.e2e.test.ts +77 -0
- package/tests/e2e/gh-pr-create.e2e.test.ts +55 -0
- package/tests/e2e/gh-run-list.e2e.test.ts +47 -0
- package/tests/gh-pr-create.test.ts +55 -0
- package/tests/gh-run-list.test.ts +35 -0
- package/tests/global-config.test.ts +52 -0
- package/tests/logger-file.test.ts +56 -0
- package/tests/logger.test.ts +72 -0
- package/tests/logs-viewer.test.ts +57 -0
- package/tests/logs.test.ts +33 -0
- package/tests/prompt.test.ts +20 -0
- package/tests/run-command-stream.test.ts +60 -0
- package/tests/summary.test.ts +58 -0
- package/tests/token-usage.test.ts +33 -0
- package/tests/utils.test.ts +8 -0
- package/tests/webhook.test.ts +89 -0
- package/tsconfig.json +18 -0
- package/tsup.config.ts +18 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2678 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/cli.ts
|
|
32
|
+
var cli_exports = {};
|
|
33
|
+
__export(cli_exports, {
|
|
34
|
+
runCli: () => runCli
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(cli_exports);
|
|
37
|
+
var import_node_child_process = require("child_process");
|
|
38
|
+
var import_commander = require("commander");
|
|
39
|
+
|
|
40
|
+
// src/config.ts
|
|
41
|
+
var import_node_path2 = __toESM(require("path"));
|
|
42
|
+
|
|
43
|
+
// src/utils.ts
|
|
44
|
+
var import_node_path = __toESM(require("path"));
|
|
45
|
+
var import_fs_extra = __toESM(require("fs-extra"));
|
|
46
|
+
var importExeca = async () => {
|
|
47
|
+
const importer = new Function("specifier", "return import(specifier)");
|
|
48
|
+
return importer("execa");
|
|
49
|
+
};
|
|
50
|
+
async function runCommand(command, args, options = {}) {
|
|
51
|
+
const label = options.verboseLabel ?? "cmd";
|
|
52
|
+
const displayCmd = options.verboseCommand ?? [command, ...args].join(" ");
|
|
53
|
+
const cwd = options.cwd ?? process.cwd();
|
|
54
|
+
options.logger?.debug(`[${label}] ${displayCmd} (cwd: ${cwd})`);
|
|
55
|
+
const logger = options.logger;
|
|
56
|
+
const streamEnabled = Boolean(options.stream?.enabled && logger);
|
|
57
|
+
const stdoutPrefix = options.stream?.stdoutPrefix ?? `[${label}] `;
|
|
58
|
+
const stderrPrefix = options.stream?.stderrPrefix ?? `[${label} stderr] `;
|
|
59
|
+
const createLineStreamer = (prefix) => {
|
|
60
|
+
let buffer = "";
|
|
61
|
+
const emit = (line) => {
|
|
62
|
+
logger?.info(`${prefix}${line}`);
|
|
63
|
+
};
|
|
64
|
+
const push = (chunk) => {
|
|
65
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
66
|
+
buffer += text.replace(/\r/g, "\n");
|
|
67
|
+
const parts = buffer.split("\n");
|
|
68
|
+
buffer = parts.pop() ?? "";
|
|
69
|
+
parts.forEach(emit);
|
|
70
|
+
};
|
|
71
|
+
const flush = () => {
|
|
72
|
+
if (buffer.length === 0) return;
|
|
73
|
+
emit(buffer);
|
|
74
|
+
buffer = "";
|
|
75
|
+
};
|
|
76
|
+
return { push, flush };
|
|
77
|
+
};
|
|
78
|
+
const attachStream = (stream, streamer) => {
|
|
79
|
+
if (!stream) return;
|
|
80
|
+
if (typeof stream.setEncoding === "function") {
|
|
81
|
+
stream.setEncoding("utf8");
|
|
82
|
+
}
|
|
83
|
+
stream.on("data", streamer.push);
|
|
84
|
+
stream.on("end", streamer.flush);
|
|
85
|
+
};
|
|
86
|
+
const stdoutStreamer = streamEnabled ? createLineStreamer(stdoutPrefix) : null;
|
|
87
|
+
const stderrStreamer = streamEnabled ? createLineStreamer(stderrPrefix) : null;
|
|
88
|
+
try {
|
|
89
|
+
const { execa } = await importExeca();
|
|
90
|
+
const subprocess = execa(command, args, {
|
|
91
|
+
cwd: options.cwd,
|
|
92
|
+
env: options.env,
|
|
93
|
+
input: options.input,
|
|
94
|
+
all: false
|
|
95
|
+
});
|
|
96
|
+
if (stdoutStreamer) {
|
|
97
|
+
attachStream(subprocess.stdout, stdoutStreamer);
|
|
98
|
+
}
|
|
99
|
+
if (stderrStreamer) {
|
|
100
|
+
attachStream(subprocess.stderr, stderrStreamer);
|
|
101
|
+
}
|
|
102
|
+
const result = await subprocess;
|
|
103
|
+
stdoutStreamer?.flush();
|
|
104
|
+
stderrStreamer?.flush();
|
|
105
|
+
const commandResult = {
|
|
106
|
+
stdout: String(result.stdout ?? ""),
|
|
107
|
+
stderr: String(result.stderr ?? ""),
|
|
108
|
+
exitCode: result.exitCode ?? 0
|
|
109
|
+
};
|
|
110
|
+
if (logger) {
|
|
111
|
+
const stdout = commandResult.stdout.trim();
|
|
112
|
+
const stderr = commandResult.stderr.trim();
|
|
113
|
+
if (stdout.length > 0) {
|
|
114
|
+
logger.debug(`[${label}] stdout: ${stdout}`);
|
|
115
|
+
}
|
|
116
|
+
if (stderr.length > 0) {
|
|
117
|
+
logger.debug(`[${label}] stderr: ${stderr}`);
|
|
118
|
+
}
|
|
119
|
+
logger.debug(`[${label}] exit ${commandResult.exitCode}`);
|
|
120
|
+
}
|
|
121
|
+
return commandResult;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const execaError = error;
|
|
124
|
+
stdoutStreamer?.flush();
|
|
125
|
+
stderrStreamer?.flush();
|
|
126
|
+
const commandResult = {
|
|
127
|
+
stdout: String(execaError.stdout ?? ""),
|
|
128
|
+
stderr: String(execaError.stderr ?? String(error)),
|
|
129
|
+
exitCode: execaError.exitCode ?? 1
|
|
130
|
+
};
|
|
131
|
+
if (logger) {
|
|
132
|
+
const stdout = commandResult.stdout.trim();
|
|
133
|
+
const stderr = commandResult.stderr.trim();
|
|
134
|
+
if (stdout.length > 0) {
|
|
135
|
+
logger.debug(`[${label}] stdout: ${stdout}`);
|
|
136
|
+
}
|
|
137
|
+
if (stderr.length > 0) {
|
|
138
|
+
logger.debug(`[${label}] stderr: ${stderr}`);
|
|
139
|
+
}
|
|
140
|
+
logger.debug(`[${label}] exit ${commandResult.exitCode}`);
|
|
141
|
+
}
|
|
142
|
+
return commandResult;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function isoNow() {
|
|
146
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
147
|
+
}
|
|
148
|
+
function resolvePath(cwd, target) {
|
|
149
|
+
return import_node_path.default.isAbsolute(target) ? target : import_node_path.default.join(cwd, target);
|
|
150
|
+
}
|
|
151
|
+
async function ensureDir(dirPath) {
|
|
152
|
+
await import_fs_extra.default.mkdirp(dirPath);
|
|
153
|
+
}
|
|
154
|
+
async function ensureFile(filePath, initialContent = "") {
|
|
155
|
+
await ensureDir(import_node_path.default.dirname(filePath));
|
|
156
|
+
const exists = await import_fs_extra.default.pathExists(filePath);
|
|
157
|
+
if (!exists) {
|
|
158
|
+
await import_fs_extra.default.writeFile(filePath, initialContent, "utf8");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function appendSection(filePath, content) {
|
|
162
|
+
await ensureDir(import_node_path.default.dirname(filePath));
|
|
163
|
+
await import_fs_extra.default.appendFile(filePath, `
|
|
164
|
+
${content}
|
|
165
|
+
`, "utf8");
|
|
166
|
+
}
|
|
167
|
+
async function readFileSafe(filePath) {
|
|
168
|
+
const exists = await import_fs_extra.default.pathExists(filePath);
|
|
169
|
+
if (!exists) return "";
|
|
170
|
+
return import_fs_extra.default.readFile(filePath, "utf8");
|
|
171
|
+
}
|
|
172
|
+
function pad2(value) {
|
|
173
|
+
return String(value).padStart(2, "0");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/config.ts
|
|
177
|
+
function buildAiConfig(options) {
|
|
178
|
+
return {
|
|
179
|
+
command: options.aiCli,
|
|
180
|
+
args: options.aiArgs,
|
|
181
|
+
promptArg: options.aiPromptArg
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function buildWorktreeConfig(options) {
|
|
185
|
+
return {
|
|
186
|
+
useWorktree: options.useWorktree,
|
|
187
|
+
branchName: options.branch,
|
|
188
|
+
worktreePath: options.worktreePath,
|
|
189
|
+
baseBranch: options.baseBranch
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function buildTestConfig(options) {
|
|
193
|
+
return {
|
|
194
|
+
unitCommand: options.unitCommand,
|
|
195
|
+
e2eCommand: options.e2eCommand
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function buildPrConfig(options) {
|
|
199
|
+
return {
|
|
200
|
+
enable: options.pr,
|
|
201
|
+
title: options.prTitle,
|
|
202
|
+
bodyPath: options.prBody,
|
|
203
|
+
draft: options.draft,
|
|
204
|
+
reviewers: options.reviewers
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function buildWebhookConfig(options) {
|
|
208
|
+
if (!options.webhookUrls || options.webhookUrls.length === 0) return void 0;
|
|
209
|
+
return {
|
|
210
|
+
urls: options.webhookUrls,
|
|
211
|
+
timeoutMs: options.webhookTimeout
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function buildWorkflowFiles(options, cwd) {
|
|
215
|
+
return {
|
|
216
|
+
workflowDoc: resolvePath(cwd, options.workflowDoc),
|
|
217
|
+
notesFile: resolvePath(cwd, options.notesFile),
|
|
218
|
+
planFile: resolvePath(cwd, options.planFile)
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function buildLoopConfig(options, cwd) {
|
|
222
|
+
return {
|
|
223
|
+
task: options.task,
|
|
224
|
+
iterations: options.iterations,
|
|
225
|
+
stopSignal: options.stopSignal,
|
|
226
|
+
ai: buildAiConfig(options),
|
|
227
|
+
workflowFiles: buildWorkflowFiles(options, cwd),
|
|
228
|
+
git: buildWorktreeConfig(options),
|
|
229
|
+
tests: buildTestConfig(options),
|
|
230
|
+
pr: buildPrConfig(options),
|
|
231
|
+
webhooks: buildWebhookConfig(options),
|
|
232
|
+
cwd,
|
|
233
|
+
logFile: options.logFile ? resolvePath(cwd, options.logFile) : void 0,
|
|
234
|
+
verbose: options.verbose,
|
|
235
|
+
runTests: options.runTests,
|
|
236
|
+
runE2e: options.runE2e,
|
|
237
|
+
autoCommit: options.autoCommit,
|
|
238
|
+
autoPush: options.autoPush,
|
|
239
|
+
skipInstall: options.skipInstall
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function defaultNotesPath() {
|
|
243
|
+
return import_node_path2.default.join("memory", "notes.md");
|
|
244
|
+
}
|
|
245
|
+
function defaultPlanPath() {
|
|
246
|
+
return import_node_path2.default.join("memory", "plan.md");
|
|
247
|
+
}
|
|
248
|
+
function defaultWorkflowDoc() {
|
|
249
|
+
return import_node_path2.default.join("docs", "ai-workflow.md");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/global-config.ts
|
|
253
|
+
var import_node_os = __toESM(require("os"));
|
|
254
|
+
var import_node_path3 = __toESM(require("path"));
|
|
255
|
+
var import_fs_extra2 = __toESM(require("fs-extra"));
|
|
256
|
+
function getGlobalConfigPath() {
|
|
257
|
+
return import_node_path3.default.join(import_node_os.default.homedir(), ".wheel-ai", "config.toml");
|
|
258
|
+
}
|
|
259
|
+
function stripTomlComment(line) {
|
|
260
|
+
let quote = null;
|
|
261
|
+
let escaped = false;
|
|
262
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
263
|
+
const char = line[i];
|
|
264
|
+
if (escaped) {
|
|
265
|
+
escaped = false;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (quote) {
|
|
269
|
+
if (quote === '"' && char === "\\") {
|
|
270
|
+
escaped = true;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (char === quote) {
|
|
274
|
+
quote = null;
|
|
275
|
+
}
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (char === '"' || char === "'") {
|
|
279
|
+
quote = char;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (char === "#" || char === ";") {
|
|
283
|
+
return line.slice(0, i);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return line;
|
|
287
|
+
}
|
|
288
|
+
function findUnquotedIndex(text, target) {
|
|
289
|
+
let quote = null;
|
|
290
|
+
let escaped = false;
|
|
291
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
292
|
+
const char = text[i];
|
|
293
|
+
if (escaped) {
|
|
294
|
+
escaped = false;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (quote) {
|
|
298
|
+
if (quote === '"' && char === "\\") {
|
|
299
|
+
escaped = true;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (char === quote) {
|
|
303
|
+
quote = null;
|
|
304
|
+
}
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (char === '"' || char === "'") {
|
|
308
|
+
quote = char;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (char === target) {
|
|
312
|
+
return i;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return -1;
|
|
316
|
+
}
|
|
317
|
+
function parseTomlString(raw) {
|
|
318
|
+
const value = raw.trim();
|
|
319
|
+
if (value.length < 2) return null;
|
|
320
|
+
const quote = value[0];
|
|
321
|
+
if (quote !== '"' && quote !== "'") return null;
|
|
322
|
+
let result = "";
|
|
323
|
+
let escaped = false;
|
|
324
|
+
for (let i = 1; i < value.length; i += 1) {
|
|
325
|
+
const char = value[i];
|
|
326
|
+
if (quote === '"') {
|
|
327
|
+
if (escaped) {
|
|
328
|
+
switch (char) {
|
|
329
|
+
case "n":
|
|
330
|
+
result += "\n";
|
|
331
|
+
break;
|
|
332
|
+
case "t":
|
|
333
|
+
result += " ";
|
|
334
|
+
break;
|
|
335
|
+
case "r":
|
|
336
|
+
result += "\r";
|
|
337
|
+
break;
|
|
338
|
+
case '"':
|
|
339
|
+
case "\\":
|
|
340
|
+
result += char;
|
|
341
|
+
break;
|
|
342
|
+
default:
|
|
343
|
+
result += char;
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
escaped = false;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (char === "\\") {
|
|
350
|
+
escaped = true;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (char === quote) {
|
|
354
|
+
const rest = value.slice(i + 1).trim();
|
|
355
|
+
if (rest.length > 0) return null;
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
result += char;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (char === quote) {
|
|
362
|
+
const rest = value.slice(i + 1).trim();
|
|
363
|
+
if (rest.length > 0) return null;
|
|
364
|
+
return result;
|
|
365
|
+
}
|
|
366
|
+
result += char;
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
function normalizeShortcutName(name) {
|
|
371
|
+
const trimmed = name.trim();
|
|
372
|
+
if (!trimmed) return null;
|
|
373
|
+
if (/\s/.test(trimmed)) return null;
|
|
374
|
+
return trimmed;
|
|
375
|
+
}
|
|
376
|
+
function parseGlobalConfig(content) {
|
|
377
|
+
const lines = content.split(/\r?\n/);
|
|
378
|
+
let currentSection = null;
|
|
379
|
+
const shortcut = {};
|
|
380
|
+
for (const rawLine of lines) {
|
|
381
|
+
const line = stripTomlComment(rawLine).trim();
|
|
382
|
+
if (!line) continue;
|
|
383
|
+
const sectionMatch = /^\[(.+)\]$/.exec(line);
|
|
384
|
+
if (sectionMatch) {
|
|
385
|
+
currentSection = sectionMatch[1].trim();
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (currentSection !== "shortcut") continue;
|
|
389
|
+
const equalIndex = findUnquotedIndex(line, "=");
|
|
390
|
+
if (equalIndex <= 0) continue;
|
|
391
|
+
const key = line.slice(0, equalIndex).trim();
|
|
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;
|
|
397
|
+
}
|
|
398
|
+
const name = normalizeShortcutName(shortcut.name ?? "");
|
|
399
|
+
const command = (shortcut.command ?? "").trim();
|
|
400
|
+
if (!name || !command) {
|
|
401
|
+
return {};
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
shortcut: {
|
|
405
|
+
name,
|
|
406
|
+
command
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
async function loadGlobalConfig(logger) {
|
|
411
|
+
const filePath = getGlobalConfigPath();
|
|
412
|
+
const exists = await import_fs_extra2.default.pathExists(filePath);
|
|
413
|
+
if (!exists) return null;
|
|
414
|
+
try {
|
|
415
|
+
const content = await import_fs_extra2.default.readFile(filePath, "utf8");
|
|
416
|
+
return parseGlobalConfig(content);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
419
|
+
logger?.warn(`\u8BFB\u53D6\u5168\u5C40\u914D\u7F6E\u5931\u8D25\uFF0C\u5DF2\u5FFD\u7565\uFF1A${message}`);
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function splitCommandArgs(command) {
|
|
424
|
+
const args = [];
|
|
425
|
+
let current = "";
|
|
426
|
+
let quote = null;
|
|
427
|
+
let escaped = false;
|
|
428
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
429
|
+
const char = command[i];
|
|
430
|
+
if (escaped) {
|
|
431
|
+
current += char;
|
|
432
|
+
escaped = false;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (quote) {
|
|
436
|
+
if (quote === '"' && char === "\\") {
|
|
437
|
+
escaped = true;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (char === quote) {
|
|
441
|
+
quote = null;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
current += char;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (char === '"' || char === "'") {
|
|
448
|
+
quote = char;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (/\s/.test(char)) {
|
|
452
|
+
if (current.length > 0) {
|
|
453
|
+
args.push(current);
|
|
454
|
+
current = "";
|
|
455
|
+
}
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
if (char === "\\") {
|
|
459
|
+
escaped = true;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
current += char;
|
|
463
|
+
}
|
|
464
|
+
if (current.length > 0) {
|
|
465
|
+
args.push(current);
|
|
466
|
+
}
|
|
467
|
+
return args;
|
|
468
|
+
}
|
|
469
|
+
function normalizeShortcutArgs(args) {
|
|
470
|
+
if (args.length > 0 && args[0] === "run") {
|
|
471
|
+
return args.slice(1);
|
|
472
|
+
}
|
|
473
|
+
return args;
|
|
474
|
+
}
|
|
475
|
+
function applyShortcutArgv(argv, config) {
|
|
476
|
+
if (!config?.shortcut) return argv;
|
|
477
|
+
if (argv.length < 3) return argv;
|
|
478
|
+
const commandIndex = 2;
|
|
479
|
+
if (argv[commandIndex] !== config.shortcut.name) return argv;
|
|
480
|
+
const shortcutArgs = normalizeShortcutArgs(splitCommandArgs(config.shortcut.command));
|
|
481
|
+
return [
|
|
482
|
+
...argv.slice(0, commandIndex),
|
|
483
|
+
"run",
|
|
484
|
+
...shortcutArgs,
|
|
485
|
+
...argv.slice(commandIndex + 1)
|
|
486
|
+
];
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/git.ts
|
|
490
|
+
var import_node_path4 = __toESM(require("path"));
|
|
491
|
+
async function branchExists(branch, cwd, logger) {
|
|
492
|
+
const result = await runCommand("git", ["rev-parse", "--verify", branch], {
|
|
493
|
+
cwd,
|
|
494
|
+
logger,
|
|
495
|
+
verboseLabel: "git",
|
|
496
|
+
verboseCommand: `git rev-parse --verify ${branch}`
|
|
497
|
+
});
|
|
498
|
+
return result.exitCode === 0;
|
|
499
|
+
}
|
|
500
|
+
async function resolveBaseBranch(baseBranch, repoRoot, logger) {
|
|
501
|
+
const baseExists = await branchExists(baseBranch, repoRoot, logger);
|
|
502
|
+
if (baseExists) return baseBranch;
|
|
503
|
+
const current = await getCurrentBranch(repoRoot, logger);
|
|
504
|
+
const currentExists = await branchExists(current, repoRoot, logger);
|
|
505
|
+
if (currentExists) {
|
|
506
|
+
logger.warn(`\u57FA\u7EBF\u5206\u652F ${baseBranch} \u4E0D\u5B58\u5728\uFF0C\u6539\u7528\u5F53\u524D\u5206\u652F ${current} \u4F5C\u4E3A\u57FA\u7EBF`);
|
|
507
|
+
return current;
|
|
508
|
+
}
|
|
509
|
+
throw new Error(`\u57FA\u7EBF\u5206\u652F ${baseBranch} \u4E0D\u5B58\u5728\uFF0C\u4E14\u65E0\u6CD5\u786E\u5B9A\u53EF\u7528\u7684\u5F53\u524D\u5206\u652F`);
|
|
510
|
+
}
|
|
511
|
+
async function getRepoRoot(cwd, logger) {
|
|
512
|
+
const result = await runCommand("git", ["rev-parse", "--show-toplevel"], {
|
|
513
|
+
cwd,
|
|
514
|
+
logger,
|
|
515
|
+
verboseLabel: "git",
|
|
516
|
+
verboseCommand: "git rev-parse --show-toplevel"
|
|
517
|
+
});
|
|
518
|
+
if (result.exitCode !== 0) {
|
|
519
|
+
throw new Error("\u5F53\u524D\u76EE\u5F55\u4E0D\u662F git \u4ED3\u5E93\uFF0C\u65E0\u6CD5\u7EE7\u7EED");
|
|
520
|
+
}
|
|
521
|
+
return result.stdout.trim();
|
|
522
|
+
}
|
|
523
|
+
async function getCurrentBranch(cwd, logger) {
|
|
524
|
+
const result = await runCommand("git", ["branch", "--show-current"], {
|
|
525
|
+
cwd,
|
|
526
|
+
logger,
|
|
527
|
+
verboseLabel: "git",
|
|
528
|
+
verboseCommand: "git branch --show-current"
|
|
529
|
+
});
|
|
530
|
+
if (result.exitCode !== 0) {
|
|
531
|
+
throw new Error(`\u65E0\u6CD5\u83B7\u53D6\u5F53\u524D\u5206\u652F: ${result.stderr}`);
|
|
532
|
+
}
|
|
533
|
+
return result.stdout.trim();
|
|
534
|
+
}
|
|
535
|
+
async function getUpstreamBranch(branchName, cwd, logger) {
|
|
536
|
+
const result = await runCommand("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", `${branchName}@{u}`], {
|
|
537
|
+
cwd,
|
|
538
|
+
logger,
|
|
539
|
+
verboseLabel: "git",
|
|
540
|
+
verboseCommand: `git rev-parse --abbrev-ref --symbolic-full-name ${branchName}@{u}`
|
|
541
|
+
});
|
|
542
|
+
if (result.exitCode !== 0) {
|
|
543
|
+
logger?.warn(`\u5206\u652F ${branchName} \u6CA1\u6709\u5173\u8054\u7684 upstream: ${result.stderr || result.stdout}`);
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
return result.stdout.trim();
|
|
547
|
+
}
|
|
548
|
+
function defaultWorktreePath(repoRoot, branchName) {
|
|
549
|
+
return import_node_path4.default.join(repoRoot, "..", "worktrees", branchName);
|
|
550
|
+
}
|
|
551
|
+
async function ensureBranchExists(branchName, baseBranch, repoRoot, logger) {
|
|
552
|
+
const exists = await branchExists(branchName, repoRoot, logger);
|
|
553
|
+
if (exists) return;
|
|
554
|
+
const create = await runCommand("git", ["branch", branchName, baseBranch], {
|
|
555
|
+
cwd: repoRoot,
|
|
556
|
+
logger,
|
|
557
|
+
verboseLabel: "git",
|
|
558
|
+
verboseCommand: `git branch ${branchName} ${baseBranch}`
|
|
559
|
+
});
|
|
560
|
+
if (create.exitCode !== 0) {
|
|
561
|
+
throw new Error(`\u521B\u5EFA\u5206\u652F\u5931\u8D25: ${create.stderr}`);
|
|
562
|
+
}
|
|
563
|
+
logger.info(`\u5DF2\u57FA\u4E8E ${baseBranch} \u521B\u5EFA\u5206\u652F ${branchName}`);
|
|
564
|
+
}
|
|
565
|
+
async function ensureWorktree(config, repoRoot, logger) {
|
|
566
|
+
if (!config.useWorktree) {
|
|
567
|
+
return { path: repoRoot, created: false };
|
|
568
|
+
}
|
|
569
|
+
const branchName = config.branchName ?? generateBranchName();
|
|
570
|
+
const baseBranch = await resolveBaseBranch(config.baseBranch, repoRoot, logger);
|
|
571
|
+
const worktreePath = resolvePath(repoRoot, config.worktreePath ?? defaultWorktreePath(repoRoot, branchName));
|
|
572
|
+
await ensureBranchExists(branchName, baseBranch, repoRoot, logger);
|
|
573
|
+
const addResult = await runCommand("git", ["worktree", "add", worktreePath, branchName], {
|
|
574
|
+
cwd: repoRoot,
|
|
575
|
+
logger,
|
|
576
|
+
verboseLabel: "git",
|
|
577
|
+
verboseCommand: `git worktree add ${worktreePath} ${branchName}`
|
|
578
|
+
});
|
|
579
|
+
if (addResult.exitCode !== 0) {
|
|
580
|
+
const alreadyExists = addResult.stderr.includes("already exists") || addResult.stdout.includes("already exists");
|
|
581
|
+
if (alreadyExists) {
|
|
582
|
+
logger.warn(`worktree \u8DEF\u5F84\u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7\u521B\u5EFA: ${worktreePath}`);
|
|
583
|
+
return { path: worktreePath, created: false };
|
|
584
|
+
}
|
|
585
|
+
throw new Error(`\u521B\u5EFA worktree \u5931\u8D25: ${addResult.stderr || addResult.stdout}`);
|
|
586
|
+
}
|
|
587
|
+
logger.success(`\u5DF2\u5728 ${worktreePath} \u521B\u5EFA\u5E76\u6302\u8F7D worktree (${branchName})`);
|
|
588
|
+
return { path: worktreePath, created: true };
|
|
589
|
+
}
|
|
590
|
+
async function isWorktreeClean(cwd, logger) {
|
|
591
|
+
const status = await runCommand("git", ["status", "--porcelain"], {
|
|
592
|
+
cwd,
|
|
593
|
+
logger,
|
|
594
|
+
verboseLabel: "git",
|
|
595
|
+
verboseCommand: "git status --porcelain"
|
|
596
|
+
});
|
|
597
|
+
if (status.exitCode !== 0) {
|
|
598
|
+
logger?.warn(`\u65E0\u6CD5\u83B7\u53D6 git \u72B6\u6001: ${status.stderr || status.stdout}`);
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
return status.stdout.trim().length === 0;
|
|
602
|
+
}
|
|
603
|
+
async function isBranchPushed(branchName, cwd, logger) {
|
|
604
|
+
const upstream = await getUpstreamBranch(branchName, cwd, logger);
|
|
605
|
+
if (!upstream) return false;
|
|
606
|
+
const countResult = await runCommand("git", ["rev-list", "--left-right", "--count", `${upstream}...${branchName}`], {
|
|
607
|
+
cwd,
|
|
608
|
+
logger,
|
|
609
|
+
verboseLabel: "git",
|
|
610
|
+
verboseCommand: `git rev-list --left-right --count ${upstream}...${branchName}`
|
|
611
|
+
});
|
|
612
|
+
if (countResult.exitCode !== 0) {
|
|
613
|
+
logger.warn(`\u65E0\u6CD5\u6BD4\u8F83\u5206\u652F ${branchName} \u4E0E ${upstream}: ${countResult.stderr || countResult.stdout}`);
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
const [behindStr, aheadStr] = countResult.stdout.trim().split(/\s+/);
|
|
617
|
+
const ahead = Number.parseInt(aheadStr ?? "0", 10);
|
|
618
|
+
if (Number.isNaN(ahead)) {
|
|
619
|
+
logger.warn(`\u65E0\u6CD5\u89E3\u6790\u5206\u652F\u63A8\u9001\u72B6\u6001: ${countResult.stdout}`);
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
if (ahead > 0) {
|
|
623
|
+
logger.warn(`\u5206\u652F ${branchName} \u4ECD\u6709 ${ahead} \u4E2A\u672C\u5730\u63D0\u4EA4\u672A\u63A8\u9001`);
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
function normalizeCommitTitle(title) {
|
|
629
|
+
return title.replace(/\s+/g, " ").trim();
|
|
630
|
+
}
|
|
631
|
+
function normalizeCommitBody(body) {
|
|
632
|
+
if (!body) return void 0;
|
|
633
|
+
const normalized = body.replace(/\r\n?/g, "\n").trim();
|
|
634
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
635
|
+
}
|
|
636
|
+
function formatCommitCommand(message) {
|
|
637
|
+
const title = normalizeCommitTitle(message.title) || "chore: \u66F4\u65B0\u8FED\u4EE3\u4EA7\u51FA";
|
|
638
|
+
const parts = ["git commit -m", JSON.stringify(title)];
|
|
639
|
+
const body = normalizeCommitBody(message.body);
|
|
640
|
+
if (body) {
|
|
641
|
+
parts.push("-m", JSON.stringify(body));
|
|
642
|
+
}
|
|
643
|
+
return parts.join(" ");
|
|
644
|
+
}
|
|
645
|
+
function buildCommitArgs(message) {
|
|
646
|
+
const title = normalizeCommitTitle(message.title) || "chore: \u66F4\u65B0\u8FED\u4EE3\u4EA7\u51FA";
|
|
647
|
+
const args = ["commit", "-m", title];
|
|
648
|
+
const body = normalizeCommitBody(message.body);
|
|
649
|
+
if (body) {
|
|
650
|
+
args.push("-m", body);
|
|
651
|
+
}
|
|
652
|
+
return args;
|
|
653
|
+
}
|
|
654
|
+
async function commitAll(message, cwd, logger) {
|
|
655
|
+
const add = await runCommand("git", ["add", "-A"], {
|
|
656
|
+
cwd,
|
|
657
|
+
logger,
|
|
658
|
+
verboseLabel: "git",
|
|
659
|
+
verboseCommand: "git add -A"
|
|
660
|
+
});
|
|
661
|
+
if (add.exitCode !== 0) {
|
|
662
|
+
throw new Error(`git add \u5931\u8D25: ${add.stderr}`);
|
|
663
|
+
}
|
|
664
|
+
const commit = await runCommand("git", buildCommitArgs(message), {
|
|
665
|
+
cwd,
|
|
666
|
+
logger,
|
|
667
|
+
verboseLabel: "git",
|
|
668
|
+
verboseCommand: formatCommitCommand(message)
|
|
669
|
+
});
|
|
670
|
+
if (commit.exitCode !== 0) {
|
|
671
|
+
logger.warn(`git commit \u8DF3\u8FC7\u6216\u5931\u8D25: ${commit.stderr}`);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
logger.success("\u5DF2\u63D0\u4EA4\u5F53\u524D\u53D8\u66F4");
|
|
675
|
+
}
|
|
676
|
+
async function pushBranch(branchName, cwd, logger) {
|
|
677
|
+
const push = await runCommand("git", ["push", "-u", "origin", branchName], {
|
|
678
|
+
cwd,
|
|
679
|
+
logger,
|
|
680
|
+
verboseLabel: "git",
|
|
681
|
+
verboseCommand: `git push -u origin ${branchName}`
|
|
682
|
+
});
|
|
683
|
+
if (push.exitCode !== 0) {
|
|
684
|
+
throw new Error(`git push \u5931\u8D25: ${push.stderr}`);
|
|
685
|
+
}
|
|
686
|
+
logger.success(`\u5DF2\u63A8\u9001\u5206\u652F ${branchName}`);
|
|
687
|
+
}
|
|
688
|
+
async function removeWorktree(worktreePath, repoRoot, logger) {
|
|
689
|
+
const remove = await runCommand("git", ["worktree", "remove", "--force", worktreePath], {
|
|
690
|
+
cwd: repoRoot,
|
|
691
|
+
logger,
|
|
692
|
+
verboseLabel: "git",
|
|
693
|
+
verboseCommand: `git worktree remove --force ${worktreePath}`
|
|
694
|
+
});
|
|
695
|
+
if (remove.exitCode !== 0) {
|
|
696
|
+
throw new Error(`\u5220\u9664 worktree \u5931\u8D25: ${remove.stderr || remove.stdout}`);
|
|
697
|
+
}
|
|
698
|
+
const prune = await runCommand("git", ["worktree", "prune"], {
|
|
699
|
+
cwd: repoRoot,
|
|
700
|
+
logger,
|
|
701
|
+
verboseLabel: "git",
|
|
702
|
+
verboseCommand: "git worktree prune"
|
|
703
|
+
});
|
|
704
|
+
if (prune.exitCode !== 0) {
|
|
705
|
+
logger.warn(`worktree prune \u5931\u8D25: ${prune.stderr || prune.stdout}`);
|
|
706
|
+
}
|
|
707
|
+
logger.success(`\u5DF2\u5220\u9664 worktree: ${worktreePath}`);
|
|
708
|
+
}
|
|
709
|
+
function generateBranchName() {
|
|
710
|
+
const now = /* @__PURE__ */ new Date();
|
|
711
|
+
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-aii/${stamp}`;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/logs.ts
|
|
716
|
+
var import_node_os2 = __toESM(require("os"));
|
|
717
|
+
var import_node_path5 = __toESM(require("path"));
|
|
718
|
+
var import_fs_extra3 = __toESM(require("fs-extra"));
|
|
719
|
+
var LOGS_DIR = import_node_path5.default.join(import_node_os2.default.homedir(), ".wheel-ai", "logs");
|
|
720
|
+
function getLogsDir() {
|
|
721
|
+
return LOGS_DIR;
|
|
722
|
+
}
|
|
723
|
+
function getCurrentRegistryPath() {
|
|
724
|
+
return import_node_path5.default.join(LOGS_DIR, "current.json");
|
|
725
|
+
}
|
|
726
|
+
async function ensureLogsDir() {
|
|
727
|
+
await import_fs_extra3.default.mkdirp(LOGS_DIR);
|
|
728
|
+
}
|
|
729
|
+
function formatTimeString(date = /* @__PURE__ */ new Date()) {
|
|
730
|
+
const year = date.getFullYear();
|
|
731
|
+
const month = pad2(date.getMonth() + 1);
|
|
732
|
+
const day = pad2(date.getDate());
|
|
733
|
+
const hours = pad2(date.getHours());
|
|
734
|
+
const minutes = pad2(date.getMinutes());
|
|
735
|
+
const seconds = pad2(date.getSeconds());
|
|
736
|
+
return `${year}${month}${day}${hours}${minutes}${seconds}`;
|
|
737
|
+
}
|
|
738
|
+
function sanitizeBranchName(branchName) {
|
|
739
|
+
const normalized = branchName.trim();
|
|
740
|
+
if (!normalized) return "";
|
|
741
|
+
const replaced = normalized.replace(/[\\/:*?"<>|\s]+/g, "-");
|
|
742
|
+
return replaced.replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
743
|
+
}
|
|
744
|
+
function buildAutoLogFilePath(branchName, date = /* @__PURE__ */ new Date()) {
|
|
745
|
+
const safeBranch = sanitizeBranchName(branchName) || "unknown";
|
|
746
|
+
return import_node_path5.default.join(LOGS_DIR, `wheel-ai-auto-log-${formatTimeString(date)}-${safeBranch}.log`);
|
|
747
|
+
}
|
|
748
|
+
function getLogMetaPath(logFile) {
|
|
749
|
+
const baseName = import_node_path5.default.basename(logFile, import_node_path5.default.extname(logFile));
|
|
750
|
+
return import_node_path5.default.join(LOGS_DIR, `${baseName}.json`);
|
|
751
|
+
}
|
|
752
|
+
function buildLogKey(logFile) {
|
|
753
|
+
return import_node_path5.default.basename(logFile);
|
|
754
|
+
}
|
|
755
|
+
function formatCommandLine(argv) {
|
|
756
|
+
const quote = (value) => {
|
|
757
|
+
if (/[\s"'\\]/.test(value)) {
|
|
758
|
+
return JSON.stringify(value);
|
|
759
|
+
}
|
|
760
|
+
return value;
|
|
761
|
+
};
|
|
762
|
+
return argv.map(quote).join(" ").trim();
|
|
763
|
+
}
|
|
764
|
+
async function writeJsonFile(filePath, data) {
|
|
765
|
+
await ensureLogsDir();
|
|
766
|
+
await import_fs_extra3.default.writeFile(filePath, `${JSON.stringify(data, null, 2)}
|
|
767
|
+
`, "utf8");
|
|
768
|
+
}
|
|
769
|
+
async function writeRunMetadata(logFile, metadata) {
|
|
770
|
+
const metaPath = getLogMetaPath(logFile);
|
|
771
|
+
await writeJsonFile(metaPath, metadata);
|
|
772
|
+
}
|
|
773
|
+
async function readCurrentRegistry() {
|
|
774
|
+
const filePath = getCurrentRegistryPath();
|
|
775
|
+
const exists = await import_fs_extra3.default.pathExists(filePath);
|
|
776
|
+
if (!exists) return {};
|
|
777
|
+
try {
|
|
778
|
+
const content = await import_fs_extra3.default.readFile(filePath, "utf8");
|
|
779
|
+
const parsed = JSON.parse(content);
|
|
780
|
+
if (!parsed || typeof parsed !== "object") return {};
|
|
781
|
+
return parsed;
|
|
782
|
+
} catch {
|
|
783
|
+
return {};
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
async function upsertCurrentRegistry(logFile, metadata) {
|
|
787
|
+
const registry = await readCurrentRegistry();
|
|
788
|
+
const key = buildLogKey(logFile);
|
|
789
|
+
registry[key] = { ...metadata, logFile };
|
|
790
|
+
await writeJsonFile(getCurrentRegistryPath(), registry);
|
|
791
|
+
}
|
|
792
|
+
async function removeCurrentRegistry(logFile) {
|
|
793
|
+
const registry = await readCurrentRegistry();
|
|
794
|
+
const key = buildLogKey(logFile);
|
|
795
|
+
if (!(key in registry)) return;
|
|
796
|
+
delete registry[key];
|
|
797
|
+
await writeJsonFile(getCurrentRegistryPath(), registry);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// src/logs-viewer.ts
|
|
801
|
+
var import_fs_extra4 = __toESM(require("fs-extra"));
|
|
802
|
+
var import_node_path6 = __toESM(require("path"));
|
|
803
|
+
function isRunMetadata(value) {
|
|
804
|
+
if (!value || typeof value !== "object") return false;
|
|
805
|
+
const record = value;
|
|
806
|
+
return typeof record.command === "string" && typeof record.round === "number" && typeof record.tokenUsed === "number" && typeof record.path === "string";
|
|
807
|
+
}
|
|
808
|
+
function buildLogMetaPath(logsDir, logFile) {
|
|
809
|
+
const baseName = import_node_path6.default.basename(logFile, import_node_path6.default.extname(logFile));
|
|
810
|
+
return import_node_path6.default.join(logsDir, `${baseName}.json`);
|
|
811
|
+
}
|
|
812
|
+
async function readLogMetadata(logsDir, logFile) {
|
|
813
|
+
const metaPath = buildLogMetaPath(logsDir, logFile);
|
|
814
|
+
const exists = await import_fs_extra4.default.pathExists(metaPath);
|
|
815
|
+
if (!exists) return void 0;
|
|
816
|
+
try {
|
|
817
|
+
const content = await import_fs_extra4.default.readFile(metaPath, "utf8");
|
|
818
|
+
const parsed = JSON.parse(content);
|
|
819
|
+
return isRunMetadata(parsed) ? parsed : void 0;
|
|
820
|
+
} catch {
|
|
821
|
+
return void 0;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function buildRunningLogKeys(registry) {
|
|
825
|
+
const keys = /* @__PURE__ */ new Set();
|
|
826
|
+
for (const [key, entry] of Object.entries(registry)) {
|
|
827
|
+
keys.add(key);
|
|
828
|
+
if (entry.logFile) {
|
|
829
|
+
keys.add(import_node_path6.default.basename(entry.logFile));
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return keys;
|
|
833
|
+
}
|
|
834
|
+
async function loadLogEntries(logsDir, registry) {
|
|
835
|
+
const exists = await import_fs_extra4.default.pathExists(logsDir);
|
|
836
|
+
if (!exists) return [];
|
|
837
|
+
const running = buildRunningLogKeys(registry);
|
|
838
|
+
const names = await import_fs_extra4.default.readdir(logsDir);
|
|
839
|
+
const entries = [];
|
|
840
|
+
for (const name of names) {
|
|
841
|
+
if (import_node_path6.default.extname(name).toLowerCase() !== ".log") continue;
|
|
842
|
+
if (running.has(name)) continue;
|
|
843
|
+
const filePath = import_node_path6.default.join(logsDir, name);
|
|
844
|
+
let stat;
|
|
845
|
+
try {
|
|
846
|
+
stat = await import_fs_extra4.default.stat(filePath);
|
|
847
|
+
} catch {
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
if (!stat.isFile()) continue;
|
|
851
|
+
const meta = await readLogMetadata(logsDir, filePath);
|
|
852
|
+
entries.push({
|
|
853
|
+
fileName: name,
|
|
854
|
+
filePath,
|
|
855
|
+
size: stat.size,
|
|
856
|
+
mtimeMs: stat.mtimeMs,
|
|
857
|
+
meta
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
return entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
861
|
+
}
|
|
862
|
+
function getTerminalSize() {
|
|
863
|
+
const rows = process.stdout.rows ?? 24;
|
|
864
|
+
const columns = process.stdout.columns ?? 80;
|
|
865
|
+
return { rows, columns };
|
|
866
|
+
}
|
|
867
|
+
function truncateLine(line, width) {
|
|
868
|
+
if (width <= 0) return "";
|
|
869
|
+
if (line.length <= width) return line;
|
|
870
|
+
return line.slice(0, width);
|
|
871
|
+
}
|
|
872
|
+
function formatTimestamp(ms) {
|
|
873
|
+
const date = new Date(ms);
|
|
874
|
+
const year = date.getFullYear();
|
|
875
|
+
const month = pad2(date.getMonth() + 1);
|
|
876
|
+
const day = pad2(date.getDate());
|
|
877
|
+
const hours = pad2(date.getHours());
|
|
878
|
+
const minutes = pad2(date.getMinutes());
|
|
879
|
+
const seconds = pad2(date.getSeconds());
|
|
880
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
881
|
+
}
|
|
882
|
+
function formatBytes(size) {
|
|
883
|
+
if (size < 1024) return `${size}B`;
|
|
884
|
+
const kb = size / 1024;
|
|
885
|
+
if (kb < 1024) return `${kb.toFixed(1)}KB`;
|
|
886
|
+
const mb = kb / 1024;
|
|
887
|
+
return `${mb.toFixed(1)}MB`;
|
|
888
|
+
}
|
|
889
|
+
function getPageSize(rows) {
|
|
890
|
+
return Math.max(1, rows - 2);
|
|
891
|
+
}
|
|
892
|
+
async function readLogLines(logFile) {
|
|
893
|
+
try {
|
|
894
|
+
const content = await import_fs_extra4.default.readFile(logFile, "utf8");
|
|
895
|
+
const normalized = content.replace(/\r\n?/g, "\n");
|
|
896
|
+
const lines = normalized.split("\n");
|
|
897
|
+
return lines.length > 0 ? lines : [""];
|
|
898
|
+
} catch (error) {
|
|
899
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
900
|
+
return [`\uFF08\u65E0\u6CD5\u8BFB\u53D6\u65E5\u5FD7\u6587\u4EF6\uFF1A${message}\uFF09`];
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
function buildListHeader(state, columns) {
|
|
904
|
+
const total = state.logs.length;
|
|
905
|
+
const title = `\u65E5\u5FD7\u5217\u8868\uFF08${total} \u6761\uFF09\uFF5C\u2191/\u2193 \u9009\u62E9 Enter \u67E5\u770B q \u9000\u51FA`;
|
|
906
|
+
return truncateLine(title, columns);
|
|
907
|
+
}
|
|
908
|
+
function buildListStatus(state, columns) {
|
|
909
|
+
if (state.logs.length === 0) {
|
|
910
|
+
const text = state.lastError ? `\u52A0\u8F7D\u5931\u8D25\uFF1A${state.lastError}` : "\u6682\u65E0\u53EF\u67E5\u770B\u7684\u65E5\u5FD7";
|
|
911
|
+
return truncateLine(text, columns);
|
|
912
|
+
}
|
|
913
|
+
const entry = state.logs[state.selectedIndex];
|
|
914
|
+
const meta = entry.meta;
|
|
915
|
+
const detail = meta ? `\u9879\u76EE ${meta.path}` : `\u6587\u4EF6 ${entry.fileName}`;
|
|
916
|
+
const suffix = state.lastError ? ` \uFF5C \u52A0\u8F7D\u5931\u8D25\uFF1A${state.lastError}` : "";
|
|
917
|
+
return truncateLine(`${detail}${suffix}`, columns);
|
|
918
|
+
}
|
|
919
|
+
function buildListLine(entry, selected, columns) {
|
|
920
|
+
const marker = selected ? ">" : " ";
|
|
921
|
+
const time = formatTimestamp(entry.mtimeMs);
|
|
922
|
+
const metaInfo = entry.meta ? `\u8F6E\u6B21 ${entry.meta.round} \uFF5C Token ${entry.meta.tokenUsed}` : `\u5927\u5C0F ${formatBytes(entry.size)}`;
|
|
923
|
+
return truncateLine(`${marker} ${entry.fileName} \uFF5C ${time} \uFF5C ${metaInfo}`, columns);
|
|
924
|
+
}
|
|
925
|
+
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 truncateLine(title, columns);
|
|
928
|
+
}
|
|
929
|
+
function buildViewStatus(entry, page, columns) {
|
|
930
|
+
const meta = entry.meta;
|
|
931
|
+
const metaInfo = meta ? `\u8F6E\u6B21 ${meta.round} \uFF5C Token ${meta.tokenUsed} \uFF5C \u9879\u76EE ${meta.path}` : `\u6587\u4EF6 ${entry.fileName}`;
|
|
932
|
+
const status = `\u9875 ${page.current}/${page.total} \uFF5C ${metaInfo}`;
|
|
933
|
+
return truncateLine(status, columns);
|
|
934
|
+
}
|
|
935
|
+
function ensureListOffset(state, pageSize) {
|
|
936
|
+
const total = state.logs.length;
|
|
937
|
+
if (total === 0) {
|
|
938
|
+
state.listOffset = 0;
|
|
939
|
+
state.selectedIndex = 0;
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const maxOffset = Math.max(0, total - pageSize);
|
|
943
|
+
if (state.selectedIndex < state.listOffset) {
|
|
944
|
+
state.listOffset = state.selectedIndex;
|
|
945
|
+
}
|
|
946
|
+
if (state.selectedIndex >= state.listOffset + pageSize) {
|
|
947
|
+
state.listOffset = state.selectedIndex - pageSize + 1;
|
|
948
|
+
}
|
|
949
|
+
state.listOffset = Math.min(Math.max(state.listOffset, 0), maxOffset);
|
|
950
|
+
}
|
|
951
|
+
function renderList(state) {
|
|
952
|
+
const { rows, columns } = getTerminalSize();
|
|
953
|
+
const pageSize = getPageSize(rows);
|
|
954
|
+
const header = buildListHeader(state, columns);
|
|
955
|
+
ensureListOffset(state, pageSize);
|
|
956
|
+
if (state.logs.length === 0) {
|
|
957
|
+
const filler = Array.from({ length: pageSize }, () => "");
|
|
958
|
+
const status2 = buildListStatus(state, columns);
|
|
959
|
+
const content2 = [header, ...filler, status2].join("\n");
|
|
960
|
+
process.stdout.write(`\x1B[2J\x1B[H${content2}`);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
const start = state.listOffset;
|
|
964
|
+
const slice = state.logs.slice(start, start + pageSize);
|
|
965
|
+
const lines = slice.map((entry, index) => {
|
|
966
|
+
const selected = start + index === state.selectedIndex;
|
|
967
|
+
return buildListLine(entry, selected, columns);
|
|
968
|
+
});
|
|
969
|
+
while (lines.length < pageSize) {
|
|
970
|
+
lines.push("");
|
|
971
|
+
}
|
|
972
|
+
const status = buildListStatus(state, columns);
|
|
973
|
+
const content = [header, ...lines, status].join("\n");
|
|
974
|
+
process.stdout.write(`\x1B[2J\x1B[H${content}`);
|
|
975
|
+
}
|
|
976
|
+
function renderView(view) {
|
|
977
|
+
const { rows, columns } = getTerminalSize();
|
|
978
|
+
const pageSize = getPageSize(rows);
|
|
979
|
+
const header = buildViewHeader(view.entry, columns);
|
|
980
|
+
const maxOffset = Math.max(0, Math.ceil(view.lines.length / pageSize) - 1);
|
|
981
|
+
view.pageOffset = Math.min(Math.max(view.pageOffset, 0), maxOffset);
|
|
982
|
+
const start = view.pageOffset * pageSize;
|
|
983
|
+
const pageLines = view.lines.slice(start, start + pageSize).map((line) => truncateLine(line, columns));
|
|
984
|
+
while (pageLines.length < pageSize) {
|
|
985
|
+
pageLines.push("");
|
|
986
|
+
}
|
|
987
|
+
const status = buildViewStatus(view.entry, { current: view.pageOffset + 1, total: Math.max(1, maxOffset + 1) }, columns);
|
|
988
|
+
const content = [header, ...pageLines, status].join("\n");
|
|
989
|
+
process.stdout.write(`\x1B[2J\x1B[H${content}`);
|
|
990
|
+
}
|
|
991
|
+
function render(state) {
|
|
992
|
+
if (state.mode === "view" && state.view) {
|
|
993
|
+
renderView(state.view);
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
renderList(state);
|
|
997
|
+
}
|
|
998
|
+
function shouldExit(input) {
|
|
999
|
+
if (input === "") return true;
|
|
1000
|
+
if (input.toLowerCase() === "q") return true;
|
|
1001
|
+
return false;
|
|
1002
|
+
}
|
|
1003
|
+
function isEnter(input) {
|
|
1004
|
+
return input.includes("\r") || input.includes("\n");
|
|
1005
|
+
}
|
|
1006
|
+
function isArrowUp(input) {
|
|
1007
|
+
return input.includes("\x1B[A");
|
|
1008
|
+
}
|
|
1009
|
+
function isArrowDown(input) {
|
|
1010
|
+
return input.includes("\x1B[B");
|
|
1011
|
+
}
|
|
1012
|
+
function isEscape(input) {
|
|
1013
|
+
return input === "\x1B";
|
|
1014
|
+
}
|
|
1015
|
+
function setupCleanup(cleanup) {
|
|
1016
|
+
const exitHandler = () => {
|
|
1017
|
+
cleanup();
|
|
1018
|
+
};
|
|
1019
|
+
const signalHandler = () => {
|
|
1020
|
+
cleanup();
|
|
1021
|
+
process.exit(0);
|
|
1022
|
+
};
|
|
1023
|
+
process.on("SIGINT", signalHandler);
|
|
1024
|
+
process.on("SIGTERM", signalHandler);
|
|
1025
|
+
process.on("exit", exitHandler);
|
|
1026
|
+
}
|
|
1027
|
+
function clampIndex(value, total) {
|
|
1028
|
+
if (total <= 0) return 0;
|
|
1029
|
+
return Math.min(Math.max(value, 0), total - 1);
|
|
1030
|
+
}
|
|
1031
|
+
async function runLogsViewer() {
|
|
1032
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
1033
|
+
console.log("\u5F53\u524D\u7EC8\u7AEF\u4E0D\u652F\u6301\u4EA4\u4E92\u5F0F logs\u3002");
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
const logsDir = getLogsDir();
|
|
1037
|
+
const state = {
|
|
1038
|
+
mode: "list",
|
|
1039
|
+
logs: [],
|
|
1040
|
+
selectedIndex: 0,
|
|
1041
|
+
listOffset: 0
|
|
1042
|
+
};
|
|
1043
|
+
let cleaned = false;
|
|
1044
|
+
const cleanup = () => {
|
|
1045
|
+
if (cleaned) return;
|
|
1046
|
+
cleaned = true;
|
|
1047
|
+
if (process.stdin.isTTY) {
|
|
1048
|
+
process.stdin.setRawMode(false);
|
|
1049
|
+
process.stdin.pause();
|
|
1050
|
+
}
|
|
1051
|
+
process.stdout.write("\x1B[?25h");
|
|
1052
|
+
};
|
|
1053
|
+
setupCleanup(cleanup);
|
|
1054
|
+
process.stdout.write("\x1B[?25l");
|
|
1055
|
+
process.stdin.setRawMode(true);
|
|
1056
|
+
process.stdin.resume();
|
|
1057
|
+
let loading = false;
|
|
1058
|
+
const loadLogs = async () => {
|
|
1059
|
+
try {
|
|
1060
|
+
const registry = await readCurrentRegistry();
|
|
1061
|
+
state.logs = await loadLogEntries(logsDir, registry);
|
|
1062
|
+
state.selectedIndex = clampIndex(state.selectedIndex, state.logs.length);
|
|
1063
|
+
state.lastError = void 0;
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1066
|
+
state.lastError = message;
|
|
1067
|
+
state.logs = [];
|
|
1068
|
+
state.selectedIndex = 0;
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
const openView = async () => {
|
|
1072
|
+
if (loading || state.logs.length === 0) return;
|
|
1073
|
+
loading = true;
|
|
1074
|
+
const entry = state.logs[state.selectedIndex];
|
|
1075
|
+
state.mode = "view";
|
|
1076
|
+
state.view = {
|
|
1077
|
+
entry,
|
|
1078
|
+
lines: ["\u52A0\u8F7D\u4E2D\u2026"],
|
|
1079
|
+
pageOffset: 0
|
|
1080
|
+
};
|
|
1081
|
+
render(state);
|
|
1082
|
+
const lines = await readLogLines(entry.filePath);
|
|
1083
|
+
const pageSize = getPageSize(getTerminalSize().rows);
|
|
1084
|
+
const maxOffset = Math.max(0, Math.ceil(lines.length / pageSize) - 1);
|
|
1085
|
+
state.view = {
|
|
1086
|
+
entry,
|
|
1087
|
+
lines,
|
|
1088
|
+
pageOffset: maxOffset
|
|
1089
|
+
};
|
|
1090
|
+
loading = false;
|
|
1091
|
+
render(state);
|
|
1092
|
+
};
|
|
1093
|
+
await loadLogs();
|
|
1094
|
+
render(state);
|
|
1095
|
+
process.stdin.on("data", (data) => {
|
|
1096
|
+
const input = data.toString("utf8");
|
|
1097
|
+
if (shouldExit(input)) {
|
|
1098
|
+
cleanup();
|
|
1099
|
+
process.exit(0);
|
|
1100
|
+
}
|
|
1101
|
+
if (state.mode === "list") {
|
|
1102
|
+
if (isArrowUp(input)) {
|
|
1103
|
+
state.selectedIndex = clampIndex(state.selectedIndex - 1, state.logs.length);
|
|
1104
|
+
render(state);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
if (isArrowDown(input)) {
|
|
1108
|
+
state.selectedIndex = clampIndex(state.selectedIndex + 1, state.logs.length);
|
|
1109
|
+
render(state);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
if (isEnter(input)) {
|
|
1113
|
+
void openView();
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
if (state.mode === "view" && state.view) {
|
|
1119
|
+
if (isArrowUp(input)) {
|
|
1120
|
+
state.view.pageOffset -= 1;
|
|
1121
|
+
render(state);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
if (isArrowDown(input)) {
|
|
1125
|
+
state.view.pageOffset += 1;
|
|
1126
|
+
render(state);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
if (input.toLowerCase() === "b" || isEscape(input)) {
|
|
1130
|
+
state.mode = "list";
|
|
1131
|
+
state.view = void 0;
|
|
1132
|
+
render(state);
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
process.stdout.on("resize", () => {
|
|
1138
|
+
render(state);
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// src/loop.ts
|
|
1143
|
+
var import_fs_extra7 = __toESM(require("fs-extra"));
|
|
1144
|
+
var import_node_path8 = __toESM(require("path"));
|
|
1145
|
+
|
|
1146
|
+
// src/ai.ts
|
|
1147
|
+
function buildPrompt(input) {
|
|
1148
|
+
const sections = [
|
|
1149
|
+
"# \u80CC\u666F\u4EFB\u52A1",
|
|
1150
|
+
input.task,
|
|
1151
|
+
"# \u5DE5\u4F5C\u6D41\u7A0B\u57FA\u7EBF\uFF08\u4F9B AI \u81EA\u4E3B\u6267\u884C\uFF09",
|
|
1152
|
+
input.workflowGuide,
|
|
1153
|
+
"# \u5F53\u524D\u6301\u4E45\u5316\u8BA1\u5212",
|
|
1154
|
+
input.plan || "\uFF08\u6682\u65E0\u8BA1\u5212\uFF0C\u9996\u8F6E\u8BF7\u751F\u6210\u53EF\u6267\u884C\u8BA1\u5212\u5E76\u5199\u5165 plan \u6587\u4EF6\uFF09",
|
|
1155
|
+
"# \u5386\u53F2\u8FED\u4EE3\u4E0E\u8BB0\u5FC6",
|
|
1156
|
+
input.notes || "\uFF08\u9996\u6B21\u6267\u884C\uFF0C\u6682\u65E0\u5386\u53F2\uFF09",
|
|
1157
|
+
"# \u672C\u8F6E\u6267\u884C\u8981\u6C42",
|
|
1158
|
+
[
|
|
1159
|
+
"1. \u81EA\u6211\u68C0\u67E5\u5E76\u8865\u5168\u9700\u6C42\uFF1B\u660E\u786E\u4EA4\u4ED8\u7269\u4E0E\u9A8C\u6536\u6807\u51C6\u3002",
|
|
1160
|
+
"2. \u66F4\u65B0/\u7EC6\u5316\u8BA1\u5212\uFF0C\u5FC5\u8981\u65F6\u5728 plan \u6587\u4EF6\u4E2D\u91CD\u5199\u4EFB\u52A1\u6811\u4E0E\u4F18\u5148\u7EA7\u3002",
|
|
1161
|
+
"3. \u8BBE\u8BA1\u5F00\u53D1\u6B65\u9AA4\u5E76\u76F4\u63A5\u751F\u6210\u4EE3\u7801\uFF08\u65E0\u9700\u518D\u6B21\u8BF7\u6C42\u786E\u8BA4\uFF09\u3002",
|
|
1162
|
+
"4. \u8FDB\u884C\u4EE3\u7801\u81EA\u5BA1\uFF0C\u7ED9\u51FA\u98CE\u9669\u4E0E\u6539\u8FDB\u6E05\u5355\u3002",
|
|
1163
|
+
"5. \u751F\u6210\u5355\u5143\u6D4B\u8BD5\u4E0E e2e \u6D4B\u8BD5\u4EE3\u7801\u5E76\u7ED9\u51FA\u8FD0\u884C\u547D\u4EE4\uFF1B\u5982\u679C\u73AF\u5883\u5141\u8BB8\u53EF\u76F4\u63A5\u8FD0\u884C\u547D\u4EE4\u3002",
|
|
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"
|
|
1167
|
+
].join("\n")
|
|
1168
|
+
];
|
|
1169
|
+
return sections.join("\n\n");
|
|
1170
|
+
}
|
|
1171
|
+
function pickNumber(pattern, text) {
|
|
1172
|
+
const match = pattern.exec(text);
|
|
1173
|
+
if (!match || match.length < 2) return void 0;
|
|
1174
|
+
const value = Number.parseInt(match[match.length - 1], 10);
|
|
1175
|
+
return Number.isNaN(value) ? void 0 : value;
|
|
1176
|
+
}
|
|
1177
|
+
function parseTokenUsage(logs) {
|
|
1178
|
+
const total = pickNumber(/total[_\s]tokens:\s*(\d+)/i, logs);
|
|
1179
|
+
const input = pickNumber(/(input|prompt)[_\s]tokens:\s*(\d+)/i, logs);
|
|
1180
|
+
const output = pickNumber(/(output|completion)[_\s]tokens:\s*(\d+)/i, logs);
|
|
1181
|
+
const consumed = pickNumber(/tokens?\s+used:\s*(\d+)/i, logs) ?? pickNumber(/consumed\s+(\d+)\s+tokens?/i, logs);
|
|
1182
|
+
const totalTokens = total ?? (input !== void 0 || output !== void 0 ? (input ?? 0) + (output ?? 0) : consumed);
|
|
1183
|
+
if (totalTokens === void 0) return null;
|
|
1184
|
+
return {
|
|
1185
|
+
inputTokens: input,
|
|
1186
|
+
outputTokens: output,
|
|
1187
|
+
totalTokens
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
function addOptional(a, b) {
|
|
1191
|
+
if (typeof a !== "number" && typeof b !== "number") return void 0;
|
|
1192
|
+
return (a ?? 0) + (b ?? 0);
|
|
1193
|
+
}
|
|
1194
|
+
function mergeTokenUsage(previous, current) {
|
|
1195
|
+
if (!current) return previous;
|
|
1196
|
+
if (!previous) return { ...current };
|
|
1197
|
+
return {
|
|
1198
|
+
inputTokens: addOptional(previous.inputTokens, current.inputTokens),
|
|
1199
|
+
outputTokens: addOptional(previous.outputTokens, current.outputTokens),
|
|
1200
|
+
totalTokens: previous.totalTokens + current.totalTokens
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
async function runAi(prompt, ai, logger, cwd) {
|
|
1204
|
+
const args = [...ai.args];
|
|
1205
|
+
const verboseCommand = ai.promptArg ? [ai.command, ...ai.args, ai.promptArg, "<prompt>"].join(" ") : [ai.command, ...ai.args, "<stdin>"].join(" ");
|
|
1206
|
+
const streamPrefix = `[${ai.command}] `;
|
|
1207
|
+
const streamErrorPrefix = `[${ai.command} stderr] `;
|
|
1208
|
+
let result;
|
|
1209
|
+
if (ai.promptArg) {
|
|
1210
|
+
args.push(ai.promptArg, prompt);
|
|
1211
|
+
result = await runCommand(ai.command, args, {
|
|
1212
|
+
cwd,
|
|
1213
|
+
logger,
|
|
1214
|
+
verboseLabel: "ai",
|
|
1215
|
+
verboseCommand,
|
|
1216
|
+
stream: {
|
|
1217
|
+
enabled: true,
|
|
1218
|
+
stdoutPrefix: streamPrefix,
|
|
1219
|
+
stderrPrefix: streamErrorPrefix
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
} else {
|
|
1223
|
+
result = await runCommand(ai.command, args, {
|
|
1224
|
+
cwd,
|
|
1225
|
+
input: prompt,
|
|
1226
|
+
logger,
|
|
1227
|
+
verboseLabel: "ai",
|
|
1228
|
+
verboseCommand,
|
|
1229
|
+
stream: {
|
|
1230
|
+
enabled: true,
|
|
1231
|
+
stdoutPrefix: streamPrefix,
|
|
1232
|
+
stderrPrefix: streamErrorPrefix
|
|
1233
|
+
}
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
if (result.exitCode !== 0) {
|
|
1237
|
+
throw new Error(`AI CLI \u6267\u884C\u5931\u8D25: ${result.stderr || result.stdout}`);
|
|
1238
|
+
}
|
|
1239
|
+
logger.success("AI \u8F93\u51FA\u5B8C\u6210");
|
|
1240
|
+
const usage = parseTokenUsage([result.stdout, result.stderr].filter(Boolean).join("\n"));
|
|
1241
|
+
return {
|
|
1242
|
+
output: result.stdout.trim(),
|
|
1243
|
+
usage
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
function formatIterationRecord(record) {
|
|
1247
|
+
const lines = [
|
|
1248
|
+
`### \u8FED\u4EE3 ${record.iteration} \uFF5C ${record.timestamp}`,
|
|
1249
|
+
"",
|
|
1250
|
+
"#### \u63D0\u793A\u4E0A\u4E0B\u6587",
|
|
1251
|
+
"```",
|
|
1252
|
+
record.prompt,
|
|
1253
|
+
"```",
|
|
1254
|
+
"",
|
|
1255
|
+
"#### AI \u8F93\u51FA",
|
|
1256
|
+
"```",
|
|
1257
|
+
record.aiOutput,
|
|
1258
|
+
"```",
|
|
1259
|
+
""
|
|
1260
|
+
];
|
|
1261
|
+
if (record.testResults && record.testResults.length > 0) {
|
|
1262
|
+
lines.push("#### \u6D4B\u8BD5\u7ED3\u679C");
|
|
1263
|
+
record.testResults.forEach((result) => {
|
|
1264
|
+
const label = result.kind === "unit" ? "\u5355\u5143\u6D4B\u8BD5" : "e2e \u6D4B\u8BD5";
|
|
1265
|
+
const status = result.success ? "\u2705 \u901A\u8FC7" : "\u274C \u5931\u8D25";
|
|
1266
|
+
lines.push(`${status} \uFF5C ${label} \uFF5C \u547D\u4EE4: ${result.command} \uFF5C \u9000\u51FA\u7801: ${result.exitCode}`);
|
|
1267
|
+
if (!result.success) {
|
|
1268
|
+
lines.push("```");
|
|
1269
|
+
lines.push(result.stderr || result.stdout || "\uFF08\u65E0\u8F93\u51FA\uFF09");
|
|
1270
|
+
lines.push("```");
|
|
1271
|
+
lines.push("");
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
return lines.join("\n");
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// src/deps.ts
|
|
1279
|
+
var import_node_path7 = __toESM(require("path"));
|
|
1280
|
+
var import_fs_extra5 = __toESM(require("fs-extra"));
|
|
1281
|
+
function parsePackageManagerField(value) {
|
|
1282
|
+
if (!value) return null;
|
|
1283
|
+
const normalized = value.trim().toLowerCase();
|
|
1284
|
+
if (normalized === "yarn" || normalized.startsWith("yarn@")) return "yarn";
|
|
1285
|
+
if (normalized === "pnpm" || normalized.startsWith("pnpm@")) return "pnpm";
|
|
1286
|
+
if (normalized === "npm" || normalized.startsWith("npm@")) return "npm";
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
function hasLockForManager(manager, hints) {
|
|
1290
|
+
if (manager === "yarn") return hints.hasYarnLock;
|
|
1291
|
+
if (manager === "pnpm") return hints.hasPnpmLock;
|
|
1292
|
+
return hints.hasNpmLock || hints.hasNpmShrinkwrap;
|
|
1293
|
+
}
|
|
1294
|
+
function resolvePackageManager(hints) {
|
|
1295
|
+
const fromField = parsePackageManagerField(hints.packageManagerField);
|
|
1296
|
+
if (fromField) {
|
|
1297
|
+
return {
|
|
1298
|
+
manager: fromField,
|
|
1299
|
+
source: "packageManager",
|
|
1300
|
+
hasLock: hasLockForManager(fromField, hints)
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
if (hints.hasYarnLock) {
|
|
1304
|
+
return {
|
|
1305
|
+
manager: "yarn",
|
|
1306
|
+
source: "lockfile",
|
|
1307
|
+
hasLock: true
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
if (hints.hasPnpmLock) {
|
|
1311
|
+
return {
|
|
1312
|
+
manager: "pnpm",
|
|
1313
|
+
source: "lockfile",
|
|
1314
|
+
hasLock: true
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
if (hints.hasNpmLock || hints.hasNpmShrinkwrap) {
|
|
1318
|
+
return {
|
|
1319
|
+
manager: "npm",
|
|
1320
|
+
source: "lockfile",
|
|
1321
|
+
hasLock: true
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
return {
|
|
1325
|
+
manager: "yarn",
|
|
1326
|
+
source: "default",
|
|
1327
|
+
hasLock: false
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
function buildInstallCommand(resolution) {
|
|
1331
|
+
switch (resolution.manager) {
|
|
1332
|
+
case "yarn": {
|
|
1333
|
+
const args = ["yarn", "install"];
|
|
1334
|
+
if (resolution.hasLock) {
|
|
1335
|
+
args.push("--frozen-lockfile");
|
|
1336
|
+
} else {
|
|
1337
|
+
args.push("--no-lockfile");
|
|
1338
|
+
}
|
|
1339
|
+
return args.join(" ");
|
|
1340
|
+
}
|
|
1341
|
+
case "pnpm": {
|
|
1342
|
+
const args = ["pnpm", "install"];
|
|
1343
|
+
if (resolution.hasLock) {
|
|
1344
|
+
args.push("--frozen-lockfile");
|
|
1345
|
+
}
|
|
1346
|
+
return args.join(" ");
|
|
1347
|
+
}
|
|
1348
|
+
case "npm": {
|
|
1349
|
+
const args = resolution.hasLock ? ["npm", "ci"] : ["npm", "install"];
|
|
1350
|
+
args.push("--no-audit", "--no-fund");
|
|
1351
|
+
return args.join(" ");
|
|
1352
|
+
}
|
|
1353
|
+
default: {
|
|
1354
|
+
return "yarn install";
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
function resolveSourceLabel(source) {
|
|
1359
|
+
switch (source) {
|
|
1360
|
+
case "packageManager":
|
|
1361
|
+
return "packageManager \u5B57\u6BB5";
|
|
1362
|
+
case "lockfile":
|
|
1363
|
+
return "\u9501\u6587\u4EF6";
|
|
1364
|
+
case "default":
|
|
1365
|
+
return "\u9ED8\u8BA4\u7B56\u7565";
|
|
1366
|
+
default:
|
|
1367
|
+
return "\u672A\u77E5\u6765\u6E90";
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
function extractPackageManagerField(value) {
|
|
1371
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
1372
|
+
const candidate = value;
|
|
1373
|
+
const field = candidate.packageManager;
|
|
1374
|
+
return typeof field === "string" ? field : void 0;
|
|
1375
|
+
}
|
|
1376
|
+
async function readPackageManagerHints(cwd, logger) {
|
|
1377
|
+
const packageJsonPath = import_node_path7.default.join(cwd, "package.json");
|
|
1378
|
+
const hasPackageJson = await import_fs_extra5.default.pathExists(packageJsonPath);
|
|
1379
|
+
if (!hasPackageJson) return null;
|
|
1380
|
+
let packageManagerField;
|
|
1381
|
+
try {
|
|
1382
|
+
const packageJson = await import_fs_extra5.default.readJson(packageJsonPath);
|
|
1383
|
+
packageManagerField = extractPackageManagerField(packageJson);
|
|
1384
|
+
} catch (error) {
|
|
1385
|
+
logger.warn(`\u8BFB\u53D6 package.json \u5931\u8D25\uFF0C\u5C06\u6539\u7528\u9501\u6587\u4EF6\u5224\u65AD\u5305\u7BA1\u7406\u5668: ${String(error)}`);
|
|
1386
|
+
}
|
|
1387
|
+
const [hasYarnLock, hasPnpmLock, hasNpmLock, hasNpmShrinkwrap] = await Promise.all([
|
|
1388
|
+
import_fs_extra5.default.pathExists(import_node_path7.default.join(cwd, "yarn.lock")),
|
|
1389
|
+
import_fs_extra5.default.pathExists(import_node_path7.default.join(cwd, "pnpm-lock.yaml")),
|
|
1390
|
+
import_fs_extra5.default.pathExists(import_node_path7.default.join(cwd, "package-lock.json")),
|
|
1391
|
+
import_fs_extra5.default.pathExists(import_node_path7.default.join(cwd, "npm-shrinkwrap.json"))
|
|
1392
|
+
]);
|
|
1393
|
+
return {
|
|
1394
|
+
packageManagerField,
|
|
1395
|
+
hasYarnLock,
|
|
1396
|
+
hasPnpmLock,
|
|
1397
|
+
hasNpmLock,
|
|
1398
|
+
hasNpmShrinkwrap
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
async function ensureDependencies(cwd, logger) {
|
|
1402
|
+
const hints = await readPackageManagerHints(cwd, logger);
|
|
1403
|
+
if (!hints) {
|
|
1404
|
+
logger.info("\u672A\u68C0\u6D4B\u5230 package.json\uFF0C\u8DF3\u8FC7\u4F9D\u8D56\u68C0\u67E5");
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
const resolution = resolvePackageManager(hints);
|
|
1408
|
+
const sourceLabel = resolveSourceLabel(resolution.source);
|
|
1409
|
+
logger.info(`\u4F9D\u8D56\u68C0\u67E5\uFF1A\u4F7F\u7528 ${resolution.manager}\uFF08\u6765\u6E90\uFF1A${sourceLabel}\uFF09`);
|
|
1410
|
+
if (resolution.source === "default") {
|
|
1411
|
+
logger.warn("\u672A\u68C0\u6D4B\u5230 packageManager \u914D\u7F6E\u6216\u9501\u6587\u4EF6\uFF0C\u5C06\u6309\u9ED8\u8BA4\u7B56\u7565\u5B89\u88C5\u4F9D\u8D56");
|
|
1412
|
+
}
|
|
1413
|
+
const command = buildInstallCommand(resolution);
|
|
1414
|
+
logger.info(`\u5F00\u59CB\u5B89\u88C5\u4F9D\u8D56: ${command}`);
|
|
1415
|
+
const result = await runCommand("bash", ["-lc", command], {
|
|
1416
|
+
cwd,
|
|
1417
|
+
logger,
|
|
1418
|
+
verboseLabel: "deps",
|
|
1419
|
+
verboseCommand: `bash -lc "${command}"`,
|
|
1420
|
+
stream: {
|
|
1421
|
+
enabled: true,
|
|
1422
|
+
stdoutPrefix: "[deps] ",
|
|
1423
|
+
stderrPrefix: "[deps err] "
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
if (result.exitCode !== 0) {
|
|
1427
|
+
const details = result.stderr || result.stdout || "\u65E0\u8F93\u51FA";
|
|
1428
|
+
throw new Error(`\u4F9D\u8D56\u5B89\u88C5\u5931\u8D25: ${details}`);
|
|
1429
|
+
}
|
|
1430
|
+
logger.success("\u4F9D\u8D56\u68C0\u67E5\u5B8C\u6210");
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// src/gh.ts
|
|
1434
|
+
function isGhPrInfo(input) {
|
|
1435
|
+
if (typeof input !== "object" || input === null) return false;
|
|
1436
|
+
const candidate = input;
|
|
1437
|
+
return typeof candidate.number === "number" && typeof candidate.url === "string" && typeof candidate.title === "string" && typeof candidate.state === "string" && typeof candidate.headRefName === "string";
|
|
1438
|
+
}
|
|
1439
|
+
function resolveRunDatabaseId(candidate) {
|
|
1440
|
+
const databaseId = candidate.databaseId;
|
|
1441
|
+
if (typeof databaseId === "number" && Number.isFinite(databaseId)) return databaseId;
|
|
1442
|
+
const id = candidate.id;
|
|
1443
|
+
if (typeof id === "number" && Number.isFinite(id)) return id;
|
|
1444
|
+
if (typeof id === "string") {
|
|
1445
|
+
const parsed = Number(id);
|
|
1446
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
1447
|
+
}
|
|
1448
|
+
return null;
|
|
1449
|
+
}
|
|
1450
|
+
function parseGhRunInfo(input) {
|
|
1451
|
+
if (typeof input !== "object" || input === null) return null;
|
|
1452
|
+
const candidate = input;
|
|
1453
|
+
const databaseId = resolveRunDatabaseId(candidate);
|
|
1454
|
+
if (databaseId === null) return null;
|
|
1455
|
+
if (typeof candidate.name !== "string") return null;
|
|
1456
|
+
if (typeof candidate.status !== "string") return null;
|
|
1457
|
+
if (typeof candidate.url !== "string") return null;
|
|
1458
|
+
const conclusion = candidate.conclusion;
|
|
1459
|
+
const hasValidConclusion = conclusion === void 0 || conclusion === null || typeof conclusion === "string";
|
|
1460
|
+
if (!hasValidConclusion) return null;
|
|
1461
|
+
return {
|
|
1462
|
+
databaseId,
|
|
1463
|
+
name: candidate.name,
|
|
1464
|
+
status: candidate.status,
|
|
1465
|
+
conclusion: conclusion ?? null,
|
|
1466
|
+
url: candidate.url
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
function parseGhRunList(output) {
|
|
1470
|
+
try {
|
|
1471
|
+
const parsed = JSON.parse(output);
|
|
1472
|
+
if (!Array.isArray(parsed)) return [];
|
|
1473
|
+
return parsed.map(parseGhRunInfo).filter((run) => run !== null);
|
|
1474
|
+
} catch {
|
|
1475
|
+
return [];
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
function resolvePrTitle(branch, title) {
|
|
1479
|
+
const trimmed = title?.trim();
|
|
1480
|
+
if (trimmed) return trimmed;
|
|
1481
|
+
return `chore: \u81EA\u52A8 PR (${branch})`;
|
|
1482
|
+
}
|
|
1483
|
+
function buildPrCreateArgs(branch, config) {
|
|
1484
|
+
const args = ["pr", "create", "--head", branch, "--title", resolvePrTitle(branch, config.title)];
|
|
1485
|
+
if (config.bodyPath) {
|
|
1486
|
+
args.push("--body-file", config.bodyPath);
|
|
1487
|
+
} else {
|
|
1488
|
+
args.push("--body", "\u81EA\u52A8\u751F\u6210 PR\uFF08\u672A\u63D0\u4F9B body \u6587\u4EF6\uFF09");
|
|
1489
|
+
}
|
|
1490
|
+
if (config.draft) {
|
|
1491
|
+
args.push("--draft");
|
|
1492
|
+
}
|
|
1493
|
+
if (config.reviewers && config.reviewers.length > 0) {
|
|
1494
|
+
args.push("--reviewer", config.reviewers.join(","));
|
|
1495
|
+
}
|
|
1496
|
+
return args;
|
|
1497
|
+
}
|
|
1498
|
+
function isPrAlreadyExistsMessage(output) {
|
|
1499
|
+
const trimmed = output.trim();
|
|
1500
|
+
if (!trimmed) return false;
|
|
1501
|
+
const ghPattern = /a pull request for branch ["']?[^"']+["']? into branch ["']?[^"']+["']? already exists/i;
|
|
1502
|
+
if (ghPattern.test(trimmed)) return true;
|
|
1503
|
+
const hasAlreadyExists = /already exists/i.test(trimmed);
|
|
1504
|
+
const hasPrKeyword = /\b(pull request|pr)\b/i.test(trimmed);
|
|
1505
|
+
const hasBranch = /\bbranch\b/i.test(trimmed);
|
|
1506
|
+
if (hasAlreadyExists && hasPrKeyword && hasBranch) return true;
|
|
1507
|
+
const hasChineseExists = trimmed.includes("\u5DF2\u5B58\u5728");
|
|
1508
|
+
const hasChinesePr = trimmed.includes("\u62C9\u53D6\u8BF7\u6C42") || trimmed.includes("\u5408\u5E76\u8BF7\u6C42") || /\bPR\b/i.test(trimmed);
|
|
1509
|
+
const hasChineseBranch = trimmed.includes("\u5206\u652F");
|
|
1510
|
+
if (hasChineseExists && hasChinesePr && hasChineseBranch) return true;
|
|
1511
|
+
return false;
|
|
1512
|
+
}
|
|
1513
|
+
function extractPrUrl(output) {
|
|
1514
|
+
const match = output.match(/https?:\/\/\S+/);
|
|
1515
|
+
if (!match) return null;
|
|
1516
|
+
return match[0].replace(/[),.]+$/, "");
|
|
1517
|
+
}
|
|
1518
|
+
async function viewPr(branch, cwd, logger) {
|
|
1519
|
+
const result = await runCommand("gh", ["pr", "view", branch, "--json", "number,title,url,state,headRefName"], {
|
|
1520
|
+
cwd,
|
|
1521
|
+
logger,
|
|
1522
|
+
verboseLabel: "gh",
|
|
1523
|
+
verboseCommand: `gh pr view ${branch} --json number,title,url,state,headRefName`
|
|
1524
|
+
});
|
|
1525
|
+
if (result.exitCode !== 0) {
|
|
1526
|
+
logger.warn(`gh pr view \u5931\u8D25: ${result.stderr}`);
|
|
1527
|
+
return null;
|
|
1528
|
+
}
|
|
1529
|
+
try {
|
|
1530
|
+
const parsed = JSON.parse(result.stdout);
|
|
1531
|
+
if (isGhPrInfo(parsed)) return parsed;
|
|
1532
|
+
logger.warn("gh pr view \u8FD4\u56DE\u683C\u5F0F\u5F02\u5E38");
|
|
1533
|
+
return null;
|
|
1534
|
+
} catch (error) {
|
|
1535
|
+
logger.warn(`\u89E3\u6790 gh pr view \u8F93\u51FA\u5931\u8D25: ${String(error)}`);
|
|
1536
|
+
return null;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
async function createPr(branch, config, cwd, logger) {
|
|
1540
|
+
if (!config.enable) return null;
|
|
1541
|
+
const args = buildPrCreateArgs(branch, config);
|
|
1542
|
+
const result = await runCommand("gh", args, {
|
|
1543
|
+
cwd,
|
|
1544
|
+
logger,
|
|
1545
|
+
verboseLabel: "gh",
|
|
1546
|
+
verboseCommand: `gh ${args.join(" ")}`
|
|
1547
|
+
});
|
|
1548
|
+
if (result.exitCode !== 0) {
|
|
1549
|
+
const output = `${result.stderr}
|
|
1550
|
+
${result.stdout}`.trim();
|
|
1551
|
+
if (isPrAlreadyExistsMessage(output)) {
|
|
1552
|
+
const existingPr = await viewPr(branch, cwd, logger);
|
|
1553
|
+
if (existingPr) {
|
|
1554
|
+
logger.warn(`\u521B\u5EFA PR \u5931\u8D25\uFF0C\u4F46\u68C0\u6D4B\u5230\u5DF2\u6709 PR\uFF0C\u89C6\u4E3A\u521B\u5EFA\u5B8C\u6210: ${existingPr.url}`);
|
|
1555
|
+
return existingPr;
|
|
1556
|
+
}
|
|
1557
|
+
const fallbackUrl = extractPrUrl(output);
|
|
1558
|
+
logger.warn("\u521B\u5EFA PR \u5931\u8D25\uFF0C\u63D0\u793A\u5DF2\u5B58\u5728 PR\uFF0C\u4F46\u8BFB\u53D6 PR \u4FE1\u606F\u5931\u8D25\uFF0C\u89C6\u4E3A\u521B\u5EFA\u5B8C\u6210");
|
|
1559
|
+
return {
|
|
1560
|
+
number: 0,
|
|
1561
|
+
url: fallbackUrl ?? "\u672A\u83B7\u53D6\u5230\u94FE\u63A5",
|
|
1562
|
+
title: "\u5DF2\u5B58\u5728 PR\uFF08\u672A\u83B7\u53D6\u8BE6\u60C5\uFF09",
|
|
1563
|
+
state: "unknown",
|
|
1564
|
+
headRefName: branch
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
logger.warn(`\u521B\u5EFA PR \u5931\u8D25: ${output || "\u672A\u77E5\u9519\u8BEF"}`);
|
|
1568
|
+
return null;
|
|
1569
|
+
}
|
|
1570
|
+
return viewPr(branch, cwd, logger);
|
|
1571
|
+
}
|
|
1572
|
+
async function listFailedRuns(branch, cwd, logger) {
|
|
1573
|
+
const result = await runCommand("gh", ["run", "list", "--branch", branch, "--json", "databaseId,name,status,conclusion,url", "--limit", "5"], {
|
|
1574
|
+
cwd,
|
|
1575
|
+
logger,
|
|
1576
|
+
verboseLabel: "gh",
|
|
1577
|
+
verboseCommand: `gh run list --branch ${branch} --json databaseId,name,status,conclusion,url --limit 5`
|
|
1578
|
+
});
|
|
1579
|
+
if (result.exitCode !== 0) {
|
|
1580
|
+
logger.warn(`\u83B7\u53D6 Actions \u8FD0\u884C\u5931\u8D25: ${result.stderr}`);
|
|
1581
|
+
return [];
|
|
1582
|
+
}
|
|
1583
|
+
try {
|
|
1584
|
+
const runs = parseGhRunList(result.stdout);
|
|
1585
|
+
const failed = runs.filter((run) => run.conclusion && run.conclusion !== "success");
|
|
1586
|
+
if (failed.length === 0) {
|
|
1587
|
+
logger.info("\u6700\u8FD1 5 \u6B21 Actions \u8FD0\u884C\u65E0\u5931\u8D25");
|
|
1588
|
+
}
|
|
1589
|
+
return failed;
|
|
1590
|
+
} catch (error) {
|
|
1591
|
+
logger.warn(`\u89E3\u6790 Actions \u8F93\u51FA\u5931\u8D25: ${String(error)}`);
|
|
1592
|
+
return [];
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// src/logger.ts
|
|
1597
|
+
var import_fs_extra6 = __toESM(require("fs-extra"));
|
|
1598
|
+
var wrap = (code) => (value) => `\x1B[${code}m${value}\x1B[0m`;
|
|
1599
|
+
var colors = {
|
|
1600
|
+
blue: wrap("34"),
|
|
1601
|
+
green: wrap("32"),
|
|
1602
|
+
yellow: wrap("33"),
|
|
1603
|
+
red: wrap("31"),
|
|
1604
|
+
magenta: wrap("35"),
|
|
1605
|
+
gray: wrap("90")
|
|
1606
|
+
};
|
|
1607
|
+
var Logger = class {
|
|
1608
|
+
constructor(options = {}) {
|
|
1609
|
+
this.verbose = options.verbose ?? false;
|
|
1610
|
+
const trimmedPath = options.logFile?.trim();
|
|
1611
|
+
this.logFile = trimmedPath && trimmedPath.length > 0 ? trimmedPath : void 0;
|
|
1612
|
+
this.logFileEnabled = Boolean(this.logFile);
|
|
1613
|
+
this.logFileErrored = false;
|
|
1614
|
+
if (this.logFile) {
|
|
1615
|
+
try {
|
|
1616
|
+
import_fs_extra6.default.ensureFileSync(this.logFile);
|
|
1617
|
+
} catch (error) {
|
|
1618
|
+
this.disableFileWithError(error);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
info(message) {
|
|
1623
|
+
this.emit("log", colors.blue, "info", " ", message);
|
|
1624
|
+
}
|
|
1625
|
+
success(message) {
|
|
1626
|
+
this.emit("log", colors.green, "ok", " ", message);
|
|
1627
|
+
}
|
|
1628
|
+
warn(message) {
|
|
1629
|
+
this.emit("warn", colors.yellow, "warn", " ", message);
|
|
1630
|
+
}
|
|
1631
|
+
error(message) {
|
|
1632
|
+
this.emit("error", colors.red, "err", " ", message);
|
|
1633
|
+
}
|
|
1634
|
+
debug(message) {
|
|
1635
|
+
if (!this.verbose) return;
|
|
1636
|
+
this.emit("log", colors.magenta, "dbg", " ", message);
|
|
1637
|
+
}
|
|
1638
|
+
emit(method, colorizer, label, padding, message) {
|
|
1639
|
+
const now = /* @__PURE__ */ new Date();
|
|
1640
|
+
const consoleLine = this.formatConsoleLine(now, colorizer(label), padding, message);
|
|
1641
|
+
const fileLine = this.formatFileLine(now, label, padding, message);
|
|
1642
|
+
console[method](consoleLine);
|
|
1643
|
+
this.writeFileLine(fileLine);
|
|
1644
|
+
}
|
|
1645
|
+
formatConsoleLine(date, label, padding, message) {
|
|
1646
|
+
const timestamp = this.formatTimestamp(date);
|
|
1647
|
+
return `${colors.gray(timestamp)} ${label}${padding}${message}`;
|
|
1648
|
+
}
|
|
1649
|
+
formatFileLine(date, label, padding, message) {
|
|
1650
|
+
const timestamp = this.formatTimestamp(date);
|
|
1651
|
+
return `${timestamp} ${label}${padding}${message}`;
|
|
1652
|
+
}
|
|
1653
|
+
writeFileLine(line) {
|
|
1654
|
+
if (!this.logFileEnabled || !this.logFile) return;
|
|
1655
|
+
try {
|
|
1656
|
+
import_fs_extra6.default.appendFileSync(this.logFile, `${line}
|
|
1657
|
+
`, "utf8");
|
|
1658
|
+
} catch (error) {
|
|
1659
|
+
this.disableFileWithError(error);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
disableFileWithError(error) {
|
|
1663
|
+
this.logFileEnabled = false;
|
|
1664
|
+
if (this.logFileErrored) return;
|
|
1665
|
+
this.logFileErrored = true;
|
|
1666
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1667
|
+
const target = this.logFile ? ` (${this.logFile})` : "";
|
|
1668
|
+
console.warn(`\u65E5\u5FD7\u6587\u4EF6\u5199\u5165\u5931\u8D25${target}\uFF0C\u5DF2\u505C\u6B62\u5199\u5165\uFF1A${message}`);
|
|
1669
|
+
}
|
|
1670
|
+
formatTimestamp(date) {
|
|
1671
|
+
const year = date.getFullYear();
|
|
1672
|
+
const month = pad2(date.getMonth() + 1);
|
|
1673
|
+
const day = pad2(date.getDate());
|
|
1674
|
+
const hours = pad2(date.getHours());
|
|
1675
|
+
const minutes = pad2(date.getMinutes());
|
|
1676
|
+
const seconds = pad2(date.getSeconds());
|
|
1677
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1680
|
+
var defaultLogger = new Logger();
|
|
1681
|
+
|
|
1682
|
+
// src/runtime-tracker.ts
|
|
1683
|
+
async function safeWrite(logFile, metadata, logger) {
|
|
1684
|
+
try {
|
|
1685
|
+
await writeRunMetadata(logFile, metadata);
|
|
1686
|
+
} catch (error) {
|
|
1687
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1688
|
+
logger?.warn(`\u5199\u5165\u8FD0\u884C\u5143\u4FE1\u606F\u5931\u8D25: ${message}`);
|
|
1689
|
+
}
|
|
1690
|
+
try {
|
|
1691
|
+
await upsertCurrentRegistry(logFile, metadata);
|
|
1692
|
+
} catch (error) {
|
|
1693
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1694
|
+
logger?.warn(`\u66F4\u65B0 current.json \u5931\u8D25: ${message}`);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
async function safeRemove(logFile, logger) {
|
|
1698
|
+
try {
|
|
1699
|
+
await removeCurrentRegistry(logFile);
|
|
1700
|
+
} catch (error) {
|
|
1701
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1702
|
+
logger?.warn(`\u6E05\u7406 current.json \u5931\u8D25: ${message}`);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
async function createRunTracker(options) {
|
|
1706
|
+
const { logFile, command, path: path10, logger } = options;
|
|
1707
|
+
if (!logFile) return null;
|
|
1708
|
+
const update = async (round, tokenUsed) => {
|
|
1709
|
+
const metadata = {
|
|
1710
|
+
command,
|
|
1711
|
+
round,
|
|
1712
|
+
tokenUsed,
|
|
1713
|
+
path: path10
|
|
1714
|
+
};
|
|
1715
|
+
await safeWrite(logFile, metadata, logger);
|
|
1716
|
+
};
|
|
1717
|
+
await update(0, 0);
|
|
1718
|
+
return {
|
|
1719
|
+
update,
|
|
1720
|
+
finalize: async () => {
|
|
1721
|
+
await safeRemove(logFile, logger);
|
|
1722
|
+
}
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// src/summary.ts
|
|
1727
|
+
var REQUIRED_SECTIONS = ["# \u53D8\u66F4\u6458\u8981", "# \u6D4B\u8BD5\u7ED3\u679C", "# \u98CE\u9669\u4E0E\u56DE\u6EDA"];
|
|
1728
|
+
function normalizeText(text) {
|
|
1729
|
+
return text.replace(/\r\n?/g, "\n");
|
|
1730
|
+
}
|
|
1731
|
+
function compactLine(text) {
|
|
1732
|
+
return text.replace(/\s+/g, " ").trim();
|
|
1733
|
+
}
|
|
1734
|
+
function trimTail(text, limit, emptyFallback) {
|
|
1735
|
+
const normalized = normalizeText(text).trim();
|
|
1736
|
+
if (!normalized) return emptyFallback;
|
|
1737
|
+
if (normalized.length <= limit) return normalized;
|
|
1738
|
+
return `\uFF08\u5185\u5BB9\u8FC7\u957F\uFF0C\u4FDD\u7559\u6700\u540E ${limit} \u5B57\u7B26\uFF09
|
|
1739
|
+
${normalized.slice(-limit)}`;
|
|
1740
|
+
}
|
|
1741
|
+
function formatTestResultLines(testResults) {
|
|
1742
|
+
if (!testResults || testResults.length === 0) {
|
|
1743
|
+
return ["- \u672A\u8FD0\u884C\uFF08\u672C\u6B21\u672A\u6267\u884C\u6D4B\u8BD5\uFF09"];
|
|
1744
|
+
}
|
|
1745
|
+
return testResults.map((result) => {
|
|
1746
|
+
const label = result.kind === "unit" ? "\u5355\u5143\u6D4B\u8BD5" : "e2e \u6D4B\u8BD5";
|
|
1747
|
+
const status = result.success ? "\u901A\u8FC7" : `\u5931\u8D25\uFF08\u9000\u51FA\u7801 ${result.exitCode}\uFF09`;
|
|
1748
|
+
const command = result.command ? `\uFF5C\u547D\u4EE4: ${result.command}` : "";
|
|
1749
|
+
return `- ${label}: ${status} ${command}`.trim();
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
function formatTestResultsForPrompt(testResults) {
|
|
1753
|
+
return formatTestResultLines(testResults).join("\n");
|
|
1754
|
+
}
|
|
1755
|
+
function buildSummaryLinesFromCommit(commitTitle, commitBody) {
|
|
1756
|
+
const bulletLines = extractBulletLines(commitBody);
|
|
1757
|
+
if (bulletLines.length > 0) return bulletLines;
|
|
1758
|
+
const summary = stripCommitType(commitTitle);
|
|
1759
|
+
return [`- ${summary}`];
|
|
1760
|
+
}
|
|
1761
|
+
function stripCommitType(title) {
|
|
1762
|
+
const trimmed = compactLine(title);
|
|
1763
|
+
if (!trimmed) return "\u66F4\u65B0\u8FED\u4EE3\u4EA7\u51FA";
|
|
1764
|
+
const match = trimmed.match(/^[^:]+:\s*(.+)$/);
|
|
1765
|
+
return match?.[1]?.trim() || trimmed;
|
|
1766
|
+
}
|
|
1767
|
+
function buildPrBody(summaryLines, testLines) {
|
|
1768
|
+
const riskLines = ["- \u98CE\u9669\uFF1A\u5F85\u8BC4\u4F30", "- \u56DE\u6EDA\uFF1Agit revert \u5BF9\u5E94\u63D0\u4EA4\u6216\u5173\u95ED PR"];
|
|
1769
|
+
return [
|
|
1770
|
+
"# \u53D8\u66F4\u6458\u8981",
|
|
1771
|
+
summaryLines.join("\n"),
|
|
1772
|
+
"",
|
|
1773
|
+
"# \u6D4B\u8BD5\u7ED3\u679C",
|
|
1774
|
+
testLines.join("\n"),
|
|
1775
|
+
"",
|
|
1776
|
+
"# \u98CE\u9669\u4E0E\u56DE\u6EDA",
|
|
1777
|
+
riskLines.join("\n")
|
|
1778
|
+
].join("\n");
|
|
1779
|
+
}
|
|
1780
|
+
function buildSummaryPrompt(input) {
|
|
1781
|
+
const planSnippet = trimTail(input.plan, 2e3, "\uFF08\u8BA1\u5212\u4E3A\u7A7A\uFF09");
|
|
1782
|
+
const notesSnippet = trimTail(input.notes, 4e3, "\uFF08notes \u4E3A\u7A7A\uFF09");
|
|
1783
|
+
const aiSnippet = trimTail(input.lastAiOutput, 3e3, "\uFF08\u672C\u8F6E\u65E0 AI \u8F93\u51FA\uFF09");
|
|
1784
|
+
const statusSnippet = trimTail(input.gitStatus, 1e3, "\uFF08git status \u4E3A\u7A7A\uFF09");
|
|
1785
|
+
const diffSnippet = trimTail(input.diffStat, 1e3, "\uFF08diff \u7EDF\u8BA1\u4E3A\u7A7A\uFF09");
|
|
1786
|
+
const testSummary = formatTestResultsForPrompt(input.testResults);
|
|
1787
|
+
return [
|
|
1788
|
+
"# \u89D2\u8272",
|
|
1789
|
+
"\u4F60\u662F\u8D44\u6DF1\u5DE5\u7A0B\u5E08\uFF0C\u9700\u8981\u4E3A\u672C\u6B21\u8FED\u4EE3\u751F\u6210\u63D0\u4EA4\u4FE1\u606F\u4E0E PR \u6587\u6848\u3002",
|
|
1790
|
+
"# \u4EFB\u52A1",
|
|
1791
|
+
"\u57FA\u4E8E\u8F93\u5165\u4FE1\u606F\u8F93\u51FA\u4E25\u683C JSON\uFF08\u4E0D\u8981 markdown\u3001\u4E0D\u8981\u4EE3\u7801\u5757\u3001\u4E0D\u8981\u591A\u4F59\u6587\u5B57\uFF09\u3002",
|
|
1792
|
+
"\u8981\u6C42\uFF1A",
|
|
1793
|
+
"- \u5168\u90E8\u4E2D\u6587\u3002",
|
|
1794
|
+
"- commitTitle / prTitle \u4F7F\u7528 Conventional Commits \u683C\u5F0F\uFF1A<type>: <\u6982\u8981>\uFF0C\u7B80\u6D01\u5177\u4F53\uFF0C\u4E0D\u8981\u51FA\u73B0\u201C\u81EA\u52A8\u8FED\u4EE3\u63D0\u4EA4/\u81EA\u52A8 PR\u201D\u7B49\u5B57\u6837\u3002",
|
|
1795
|
+
"- commitBody \u4E3A\u591A\u884C\u8981\u70B9\u5217\u8868\uFF08\u53EF\u4E3A\u7A7A\u5B57\u7B26\u4E32\uFF09\u3002",
|
|
1796
|
+
"- prBody \u4E3A Markdown\uFF0C\u5FC5\u987B\u5305\u542B\u6807\u9898\uFF1A# \u53D8\u66F4\u6458\u8981\u3001# \u6D4B\u8BD5\u7ED3\u679C\u3001# \u98CE\u9669\u4E0E\u56DE\u6EDA\uFF0C\u5E76\u5728\u53D8\u66F4\u6458\u8981\u4E2D\u4F53\u73B0\u5DE5\u4F5C\u603B\u7ED3\u3002",
|
|
1797
|
+
"- \u4E0D\u786E\u5B9A\u5904\u53EF\u57FA\u4E8E\u73B0\u6709\u4FE1\u606F\u5408\u7406\u63A8\u65AD\uFF0C\u4F46\u4E0D\u8981\u7F16\u9020\u6D4B\u8BD5\u7ED3\u679C\u3002",
|
|
1798
|
+
"# \u8F93\u51FA JSON",
|
|
1799
|
+
'{"commitTitle":"...","commitBody":"...","prTitle":"...","prBody":"..."}',
|
|
1800
|
+
"# \u8F93\u5165\u4FE1\u606F",
|
|
1801
|
+
`\u4EFB\u52A1: ${compactLine(input.task) || "\uFF08\u7A7A\uFF09"}`,
|
|
1802
|
+
`\u5206\u652F: ${input.branchName ?? "\uFF08\u672A\u77E5\uFF09"}`,
|
|
1803
|
+
"\u8BA1\u5212\uFF08\u8282\u9009\uFF09:",
|
|
1804
|
+
planSnippet,
|
|
1805
|
+
"notes\uFF08\u8282\u9009\uFF09:",
|
|
1806
|
+
notesSnippet,
|
|
1807
|
+
"\u6700\u8FD1\u4E00\u6B21 AI \u8F93\u51FA\uFF08\u8282\u9009\uFF09:",
|
|
1808
|
+
aiSnippet,
|
|
1809
|
+
"\u6D4B\u8BD5\u7ED3\u679C:",
|
|
1810
|
+
testSummary,
|
|
1811
|
+
"git status --short:",
|
|
1812
|
+
statusSnippet,
|
|
1813
|
+
"git diff --stat:",
|
|
1814
|
+
diffSnippet
|
|
1815
|
+
].join("\n\n");
|
|
1816
|
+
}
|
|
1817
|
+
function pickString(record, keys) {
|
|
1818
|
+
for (const key of keys) {
|
|
1819
|
+
const value = record[key];
|
|
1820
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
1821
|
+
return value.trim();
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
return null;
|
|
1825
|
+
}
|
|
1826
|
+
function extractJson(text) {
|
|
1827
|
+
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
1828
|
+
if (fenced?.[1]) return fenced[1].trim();
|
|
1829
|
+
const start = text.indexOf("{");
|
|
1830
|
+
const end = text.lastIndexOf("}");
|
|
1831
|
+
if (start >= 0 && end > start) {
|
|
1832
|
+
return text.slice(start, end + 1).trim();
|
|
1833
|
+
}
|
|
1834
|
+
return null;
|
|
1835
|
+
}
|
|
1836
|
+
function normalizeTitle(title) {
|
|
1837
|
+
return compactLine(title);
|
|
1838
|
+
}
|
|
1839
|
+
function normalizeBody(body) {
|
|
1840
|
+
if (!body) return void 0;
|
|
1841
|
+
const normalized = normalizeText(body).trim();
|
|
1842
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
1843
|
+
}
|
|
1844
|
+
function extractBulletLines(text) {
|
|
1845
|
+
if (!text) return [];
|
|
1846
|
+
const lines = normalizeText(text).split("\n").map((line) => line.trim()).filter(Boolean);
|
|
1847
|
+
const bullets = lines.filter((line) => line.startsWith("- ") || line.startsWith("* "));
|
|
1848
|
+
return bullets.map((line) => line.startsWith("* ") ? `- ${line.slice(2).trim()}` : line);
|
|
1849
|
+
}
|
|
1850
|
+
function parseDeliverySummary(output) {
|
|
1851
|
+
const jsonText = extractJson(output);
|
|
1852
|
+
if (!jsonText) return null;
|
|
1853
|
+
try {
|
|
1854
|
+
const parsed = JSON.parse(jsonText);
|
|
1855
|
+
let commitTitle = pickString(parsed, ["commitTitle", "commit_message", "commitMessage", "commit_title"]);
|
|
1856
|
+
let commitBody = pickString(parsed, ["commitBody", "commit_body"]);
|
|
1857
|
+
let prTitle = pickString(parsed, ["prTitle", "pr_title"]);
|
|
1858
|
+
let prBody = pickString(parsed, ["prBody", "pr_body"]);
|
|
1859
|
+
const commitObj = parsed.commit;
|
|
1860
|
+
if ((!commitTitle || !commitBody) && typeof commitObj === "object" && commitObj !== null) {
|
|
1861
|
+
const commitRecord = commitObj;
|
|
1862
|
+
commitTitle = commitTitle ?? pickString(commitRecord, ["title", "commitTitle"]);
|
|
1863
|
+
commitBody = commitBody ?? pickString(commitRecord, ["body", "commitBody"]);
|
|
1864
|
+
}
|
|
1865
|
+
const prObj = parsed.pr;
|
|
1866
|
+
if ((!prTitle || !prBody) && typeof prObj === "object" && prObj !== null) {
|
|
1867
|
+
const prRecord = prObj;
|
|
1868
|
+
prTitle = prTitle ?? pickString(prRecord, ["title", "prTitle"]);
|
|
1869
|
+
prBody = prBody ?? pickString(prRecord, ["body", "prBody"]);
|
|
1870
|
+
}
|
|
1871
|
+
if (!commitTitle || !prTitle || !prBody) return null;
|
|
1872
|
+
const normalizedCommitTitle = normalizeTitle(commitTitle);
|
|
1873
|
+
const normalizedPrTitle = normalizeTitle(prTitle);
|
|
1874
|
+
const normalizedCommitBody = normalizeBody(commitBody);
|
|
1875
|
+
const normalizedPrBody = normalizeText(prBody).trim();
|
|
1876
|
+
if (!normalizedCommitTitle || !normalizedPrTitle || !normalizedPrBody) return null;
|
|
1877
|
+
return {
|
|
1878
|
+
commitTitle: normalizedCommitTitle,
|
|
1879
|
+
commitBody: normalizedCommitBody,
|
|
1880
|
+
prTitle: normalizedPrTitle,
|
|
1881
|
+
prBody: normalizedPrBody
|
|
1882
|
+
};
|
|
1883
|
+
} catch {
|
|
1884
|
+
return null;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
function buildFallbackSummary(input) {
|
|
1888
|
+
const taskLine = compactLine(input.task);
|
|
1889
|
+
const shortTask = taskLine.length > 50 ? `${taskLine.slice(0, 50)}...` : taskLine;
|
|
1890
|
+
const baseTitle = shortTask || "\u66F4\u65B0\u8FED\u4EE3\u4EA7\u51FA";
|
|
1891
|
+
const title = `chore: ${baseTitle}`;
|
|
1892
|
+
const summaryLines = [`- ${baseTitle}`];
|
|
1893
|
+
const testLines = formatTestResultLines(input.testResults);
|
|
1894
|
+
const prBody = buildPrBody(summaryLines, testLines);
|
|
1895
|
+
return {
|
|
1896
|
+
commitTitle: title,
|
|
1897
|
+
commitBody: summaryLines.join("\n"),
|
|
1898
|
+
prTitle: title,
|
|
1899
|
+
prBody
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
function ensurePrBodySections(prBody, fallback) {
|
|
1903
|
+
const normalized = normalizeText(prBody).trim();
|
|
1904
|
+
const hasAll = REQUIRED_SECTIONS.every((section) => normalized.includes(section));
|
|
1905
|
+
if (hasAll) return normalized;
|
|
1906
|
+
const summaryLines = buildSummaryLinesFromCommit(fallback.commitTitle, fallback.commitBody);
|
|
1907
|
+
const testLines = formatTestResultLines(fallback.testResults);
|
|
1908
|
+
return buildPrBody(summaryLines, testLines);
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// src/webhook.ts
|
|
1912
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
1913
|
+
function normalizeWebhookUrls(urls) {
|
|
1914
|
+
if (!urls || urls.length === 0) return [];
|
|
1915
|
+
return urls.map((url) => url.trim()).filter((url) => url.length > 0);
|
|
1916
|
+
}
|
|
1917
|
+
function buildWebhookPayload(input) {
|
|
1918
|
+
return {
|
|
1919
|
+
event: input.event,
|
|
1920
|
+
task: input.task,
|
|
1921
|
+
branch: input.branch ?? "",
|
|
1922
|
+
iteration: input.iteration,
|
|
1923
|
+
stage: input.stage,
|
|
1924
|
+
timestamp: input.timestamp ?? isoNow()
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
function resolveFetcher(fetcher) {
|
|
1928
|
+
if (fetcher) return fetcher;
|
|
1929
|
+
const globalFetcher = globalThis.fetch;
|
|
1930
|
+
if (typeof globalFetcher !== "function") return null;
|
|
1931
|
+
return globalFetcher;
|
|
1932
|
+
}
|
|
1933
|
+
async function postWebhook(url, payload, timeoutMs, logger, fetcher) {
|
|
1934
|
+
const controller = new AbortController();
|
|
1935
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1936
|
+
try {
|
|
1937
|
+
const response = await fetcher(url, {
|
|
1938
|
+
method: "POST",
|
|
1939
|
+
headers: {
|
|
1940
|
+
"content-type": "application/json"
|
|
1941
|
+
},
|
|
1942
|
+
body: JSON.stringify(payload),
|
|
1943
|
+
signal: controller.signal
|
|
1944
|
+
});
|
|
1945
|
+
if (!response.ok) {
|
|
1946
|
+
logger.warn(`webhook \u8BF7\u6C42\u5931\u8D25\uFF08${response.status} ${response.statusText}\uFF09\uFF1A${url}`);
|
|
1947
|
+
} else {
|
|
1948
|
+
logger.debug(`webhook \u901A\u77E5\u6210\u529F\uFF1A${url}`);
|
|
1949
|
+
}
|
|
1950
|
+
} catch (error) {
|
|
1951
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1952
|
+
logger.warn(`webhook \u901A\u77E5\u5F02\u5E38\uFF1A${url}\uFF5C${message}`);
|
|
1953
|
+
} finally {
|
|
1954
|
+
clearTimeout(timer);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
async function sendWebhookNotifications(config, payload, logger, fetcher) {
|
|
1958
|
+
const urls = normalizeWebhookUrls(config?.urls);
|
|
1959
|
+
if (urls.length === 0) return;
|
|
1960
|
+
const resolvedFetcher = resolveFetcher(fetcher);
|
|
1961
|
+
if (!resolvedFetcher) {
|
|
1962
|
+
logger.warn("\u5F53\u524D Node \u73AF\u5883\u4E0D\u652F\u6301 fetch\uFF0C\u5DF2\u8DF3\u8FC7 webhook \u901A\u77E5");
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
const timeoutMs = config?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1966
|
+
await Promise.all(urls.map((url) => postWebhook(url, payload, timeoutMs, logger, resolvedFetcher)));
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
// src/loop.ts
|
|
1970
|
+
async function ensureWorkflowFiles(workflowFiles) {
|
|
1971
|
+
await ensureFile(workflowFiles.workflowDoc, "# AI \u5DE5\u4F5C\u6D41\u7A0B\u57FA\u7EBF\n");
|
|
1972
|
+
await ensureFile(workflowFiles.planFile, "# \u8BA1\u5212\n");
|
|
1973
|
+
await ensureFile(workflowFiles.notesFile, "# \u6301\u4E45\u5316\u8BB0\u5FC6\n");
|
|
1974
|
+
}
|
|
1975
|
+
var MAX_TEST_LOG_LENGTH = 4e3;
|
|
1976
|
+
function trimOutput(output, limit = MAX_TEST_LOG_LENGTH) {
|
|
1977
|
+
if (!output) return "";
|
|
1978
|
+
if (output.length <= limit) return output;
|
|
1979
|
+
return `${output.slice(0, limit)}
|
|
1980
|
+
\u2026\u2026\uFF08\u8F93\u51FA\u5DF2\u622A\u65AD\uFF0C\u539F\u59CB\u957F\u5EA6 ${output.length} \u5B57\u7B26\uFF09`;
|
|
1981
|
+
}
|
|
1982
|
+
async function safeCommandOutput(command, args, cwd, logger, label, verboseCommand) {
|
|
1983
|
+
const result = await runCommand(command, args, {
|
|
1984
|
+
cwd,
|
|
1985
|
+
logger,
|
|
1986
|
+
verboseLabel: label,
|
|
1987
|
+
verboseCommand
|
|
1988
|
+
});
|
|
1989
|
+
if (result.exitCode !== 0) {
|
|
1990
|
+
logger.warn(`${label} \u547D\u4EE4\u5931\u8D25: ${result.stderr || result.stdout}`);
|
|
1991
|
+
return "";
|
|
1992
|
+
}
|
|
1993
|
+
return result.stdout.trim();
|
|
1994
|
+
}
|
|
1995
|
+
async function runSingleTest(kind, command, cwd, logger) {
|
|
1996
|
+
const label = kind === "unit" ? "\u5355\u5143\u6D4B\u8BD5" : "e2e \u6D4B\u8BD5";
|
|
1997
|
+
logger.info(`\u6267\u884C${label}: ${command}`);
|
|
1998
|
+
const result = await runCommand("bash", ["-lc", command], {
|
|
1999
|
+
cwd,
|
|
2000
|
+
logger,
|
|
2001
|
+
verboseLabel: "shell",
|
|
2002
|
+
verboseCommand: `bash -lc "${command}"`
|
|
2003
|
+
});
|
|
2004
|
+
const success = result.exitCode === 0;
|
|
2005
|
+
if (success) {
|
|
2006
|
+
logger.success(`${label}\u5B8C\u6210`);
|
|
2007
|
+
} else {
|
|
2008
|
+
logger.warn(`${label}\u5931\u8D25\uFF08\u9000\u51FA\u7801 ${result.exitCode}\uFF09`);
|
|
2009
|
+
}
|
|
2010
|
+
return {
|
|
2011
|
+
kind,
|
|
2012
|
+
command,
|
|
2013
|
+
success,
|
|
2014
|
+
exitCode: result.exitCode,
|
|
2015
|
+
stdout: trimOutput(result.stdout.trim()),
|
|
2016
|
+
stderr: trimOutput(result.stderr.trim())
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
async function runTests(config, workDir, logger) {
|
|
2020
|
+
const results = [];
|
|
2021
|
+
if (config.runTests && config.tests.unitCommand) {
|
|
2022
|
+
const unitResult = await runSingleTest("unit", config.tests.unitCommand, workDir, logger);
|
|
2023
|
+
results.push(unitResult);
|
|
2024
|
+
}
|
|
2025
|
+
if (config.runE2e && config.tests.e2eCommand) {
|
|
2026
|
+
const e2eResult = await runSingleTest("e2e", config.tests.e2eCommand, workDir, logger);
|
|
2027
|
+
results.push(e2eResult);
|
|
2028
|
+
}
|
|
2029
|
+
return results;
|
|
2030
|
+
}
|
|
2031
|
+
function reRootPath(filePath, repoRoot, workDir) {
|
|
2032
|
+
const relative = import_node_path8.default.relative(repoRoot, filePath);
|
|
2033
|
+
if (relative.startsWith("..")) return filePath;
|
|
2034
|
+
return import_node_path8.default.join(workDir, relative);
|
|
2035
|
+
}
|
|
2036
|
+
function reRootWorkflowFiles(workflowFiles, repoRoot, workDir) {
|
|
2037
|
+
if (repoRoot === workDir) return workflowFiles;
|
|
2038
|
+
return {
|
|
2039
|
+
workflowDoc: reRootPath(workflowFiles.workflowDoc, repoRoot, workDir),
|
|
2040
|
+
notesFile: reRootPath(workflowFiles.notesFile, repoRoot, workDir),
|
|
2041
|
+
planFile: reRootPath(workflowFiles.planFile, repoRoot, workDir)
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
function buildBodyFile(workDir) {
|
|
2045
|
+
return import_node_path8.default.join(workDir, "memory", "pr-body.md");
|
|
2046
|
+
}
|
|
2047
|
+
async function writePrBody(bodyPath, content, appendExisting) {
|
|
2048
|
+
await import_fs_extra7.default.mkdirp(import_node_path8.default.dirname(bodyPath));
|
|
2049
|
+
let finalContent = content.trim();
|
|
2050
|
+
if (appendExisting) {
|
|
2051
|
+
const existing = await readFileSafe(bodyPath);
|
|
2052
|
+
const trimmedExisting = existing.trim();
|
|
2053
|
+
if (trimmedExisting.length > 0) {
|
|
2054
|
+
finalContent = `${trimmedExisting}
|
|
2055
|
+
|
|
2056
|
+
---
|
|
2057
|
+
|
|
2058
|
+
${finalContent}`;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
await import_fs_extra7.default.writeFile(bodyPath, `${finalContent}
|
|
2062
|
+
`, "utf8");
|
|
2063
|
+
}
|
|
2064
|
+
async function cleanupWorktreeIfSafe(context) {
|
|
2065
|
+
const { repoRoot, workDir, branchName, prInfo, worktreeCreated, logger } = context;
|
|
2066
|
+
if (!worktreeCreated) {
|
|
2067
|
+
logger.debug("worktree \u5E76\u975E\u672C\u6B21\u521B\u5EFA\uFF0C\u8DF3\u8FC7\u81EA\u52A8\u6E05\u7406");
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
if (workDir === repoRoot) {
|
|
2071
|
+
logger.debug("\u5F53\u524D\u672A\u4F7F\u7528\u72EC\u7ACB worktree\uFF0C\u8DF3\u8FC7\u81EA\u52A8\u6E05\u7406");
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
if (!branchName) {
|
|
2075
|
+
logger.warn("\u672A\u80FD\u786E\u5B9A worktree \u5206\u652F\u540D\uFF0C\u4FDD\u7559\u5DE5\u4F5C\u76EE\u5F55\u4EE5\u514D\u4E22\u5931\u8FDB\u5EA6");
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
const clean = await isWorktreeClean(workDir, logger);
|
|
2079
|
+
if (!clean) {
|
|
2080
|
+
logger.warn("worktree \u4ECD\u6709\u672A\u63D0\u4EA4\u53D8\u66F4\uFF0C\u5DF2\u4FDD\u7559\u5DE5\u4F5C\u76EE\u5F55");
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
const pushed = await isBranchPushed(branchName, workDir, logger);
|
|
2084
|
+
if (!pushed) {
|
|
2085
|
+
logger.warn(`\u5206\u652F ${branchName} \u5C1A\u672A\u63A8\u9001\u5230\u8FDC\u7AEF\uFF0C\u5DF2\u4FDD\u7559 worktree`);
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
if (!prInfo) {
|
|
2089
|
+
logger.warn("\u672A\u68C0\u6D4B\u5230\u5173\u8054 PR\uFF0C\u5DF2\u4FDD\u7559 worktree");
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
await removeWorktree(workDir, repoRoot, logger);
|
|
2093
|
+
}
|
|
2094
|
+
async function runLoop(config) {
|
|
2095
|
+
const logger = new Logger({ verbose: config.verbose, logFile: config.logFile });
|
|
2096
|
+
const repoRoot = await getRepoRoot(config.cwd, logger);
|
|
2097
|
+
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
|
+
let branchName = config.git.branchName;
|
|
2110
|
+
let lastRound = 0;
|
|
2111
|
+
let runError = null;
|
|
2112
|
+
const notifyWebhook = async (event, iteration, stage) => {
|
|
2113
|
+
const payload = buildWebhookPayload({
|
|
2114
|
+
event,
|
|
2115
|
+
task: config.task,
|
|
2116
|
+
branch: branchName,
|
|
2117
|
+
iteration,
|
|
2118
|
+
stage
|
|
2119
|
+
});
|
|
2120
|
+
await sendWebhookNotifications(config.webhooks, payload, logger);
|
|
2121
|
+
};
|
|
2122
|
+
try {
|
|
2123
|
+
if (!branchName) {
|
|
2124
|
+
try {
|
|
2125
|
+
branchName = await getCurrentBranch(workDir, logger);
|
|
2126
|
+
} catch (error) {
|
|
2127
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2128
|
+
logger.warn(`\u8BFB\u53D6\u5206\u652F\u540D\u5931\u8D25\uFF0Cwebhook \u4E2D\u5C06\u7F3A\u5931\u5206\u652F\u4FE1\u606F\uFF1A${message}`);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
await notifyWebhook("task_start", 0, "\u4EFB\u52A1\u5F00\u59CB");
|
|
2132
|
+
if (config.skipInstall) {
|
|
2133
|
+
logger.info("\u5DF2\u8DF3\u8FC7\u4F9D\u8D56\u68C0\u67E5");
|
|
2134
|
+
} else {
|
|
2135
|
+
await ensureDependencies(workDir, logger);
|
|
2136
|
+
}
|
|
2137
|
+
const workflowFiles = reRootWorkflowFiles(config.workflowFiles, repoRoot, workDir);
|
|
2138
|
+
await ensureWorkflowFiles(workflowFiles);
|
|
2139
|
+
const planContent = await readFileSafe(workflowFiles.planFile);
|
|
2140
|
+
if (planContent.trim().length === 0) {
|
|
2141
|
+
logger.warn("plan \u6587\u4EF6\u4E3A\u7A7A\uFF0C\u5EFA\u8BAE AI \u9996\u8F6E\u751F\u6210\u8BA1\u5212");
|
|
2142
|
+
}
|
|
2143
|
+
const aiConfig = config.ai;
|
|
2144
|
+
let accumulatedUsage = null;
|
|
2145
|
+
let lastTestResults = null;
|
|
2146
|
+
let lastAiOutput = "";
|
|
2147
|
+
let prInfo = null;
|
|
2148
|
+
let prFailed = false;
|
|
2149
|
+
for (let i = 1; i <= config.iterations; i += 1) {
|
|
2150
|
+
await notifyWebhook("iteration_start", i, `\u5F00\u59CB\u7B2C ${i} \u8F6E\u8FED\u4EE3`);
|
|
2151
|
+
const workflowGuide = await readFileSafe(workflowFiles.workflowDoc);
|
|
2152
|
+
const plan = await readFileSafe(workflowFiles.planFile);
|
|
2153
|
+
const notes = await readFileSafe(workflowFiles.notesFile);
|
|
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);
|
|
2165
|
+
accumulatedUsage = mergeTokenUsage(accumulatedUsage, aiResult.usage);
|
|
2166
|
+
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
|
+
const record = formatIterationRecord({
|
|
2187
|
+
iteration: i,
|
|
2188
|
+
prompt,
|
|
2189
|
+
aiOutput: aiResult.output,
|
|
2190
|
+
timestamp: isoNow(),
|
|
2191
|
+
testResults
|
|
2192
|
+
});
|
|
2193
|
+
await appendSection(workflowFiles.notesFile, record);
|
|
2194
|
+
logger.success(`\u5DF2\u5C06\u7B2C ${i} \u8F6E\u8F93\u51FA\u5199\u5165 ${workflowFiles.notesFile}`);
|
|
2195
|
+
lastTestResults = testResults;
|
|
2196
|
+
lastRound = i;
|
|
2197
|
+
await runTracker?.update(i, accumulatedUsage?.totalTokens ?? 0);
|
|
2198
|
+
const hasTestFailure = testResults.some((result) => !result.success);
|
|
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;
|
|
2202
|
+
}
|
|
2203
|
+
if (hitStop && hasTestFailure) {
|
|
2204
|
+
logger.info(`\u68C0\u6D4B\u5230\u505C\u6B62\u6807\u8BB0 ${config.stopSignal}\uFF0C\u4F46\u6D4B\u8BD5\u5931\u8D25\uFF0C\u7EE7\u7EED\u8FDB\u5165\u4E0B\u4E00\u8F6E\u4FEE\u590D`);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
const lastTestFailed = lastTestResults?.some((result) => !result.success) ?? false;
|
|
2208
|
+
if (lastTestFailed) {
|
|
2209
|
+
logger.warn("\u5B58\u5728\u672A\u901A\u8FC7\u7684\u6D4B\u8BD5\uFF0C\u5DF2\u8DF3\u8FC7\u81EA\u52A8\u63D0\u4EA4/\u63A8\u9001/PR");
|
|
2210
|
+
}
|
|
2211
|
+
let deliverySummary = null;
|
|
2212
|
+
const shouldPrepareDelivery = !lastTestFailed && (config.autoCommit || config.pr.enable);
|
|
2213
|
+
if (shouldPrepareDelivery) {
|
|
2214
|
+
const [gitStatus, diffStat] = await Promise.all([
|
|
2215
|
+
safeCommandOutput("git", ["status", "--short"], workDir, logger, "git", "git status --short"),
|
|
2216
|
+
safeCommandOutput("git", ["diff", "--stat"], workDir, logger, "git", "git diff --stat")
|
|
2217
|
+
]);
|
|
2218
|
+
const summaryPrompt = buildSummaryPrompt({
|
|
2219
|
+
task: config.task,
|
|
2220
|
+
plan: await readFileSafe(workflowFiles.planFile),
|
|
2221
|
+
notes: await readFileSafe(workflowFiles.notesFile),
|
|
2222
|
+
lastAiOutput,
|
|
2223
|
+
testResults: lastTestResults,
|
|
2224
|
+
gitStatus,
|
|
2225
|
+
diffStat,
|
|
2226
|
+
branchName
|
|
2227
|
+
});
|
|
2228
|
+
try {
|
|
2229
|
+
const summaryResult = await runAi(summaryPrompt, aiConfig, logger, workDir);
|
|
2230
|
+
accumulatedUsage = mergeTokenUsage(accumulatedUsage, summaryResult.usage);
|
|
2231
|
+
deliverySummary = parseDeliverySummary(summaryResult.output);
|
|
2232
|
+
if (!deliverySummary) {
|
|
2233
|
+
logger.warn("AI \u603B\u7ED3\u8F93\u51FA\u89E3\u6790\u5931\u8D25\uFF0C\u4F7F\u7528\u515C\u5E95\u6587\u6848");
|
|
2234
|
+
}
|
|
2235
|
+
} catch (error) {
|
|
2236
|
+
logger.warn(`AI \u603B\u7ED3\u751F\u6210\u5931\u8D25: ${String(error)}`);
|
|
2237
|
+
}
|
|
2238
|
+
if (!deliverySummary) {
|
|
2239
|
+
deliverySummary = buildFallbackSummary({ task: config.task, testResults: lastTestResults });
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
await runTracker?.update(lastRound, accumulatedUsage?.totalTokens ?? 0);
|
|
2243
|
+
if (config.autoCommit && !lastTestFailed) {
|
|
2244
|
+
const summary = deliverySummary ?? buildFallbackSummary({ task: config.task, testResults: lastTestResults });
|
|
2245
|
+
const commitMessage = {
|
|
2246
|
+
title: summary.commitTitle,
|
|
2247
|
+
body: summary.commitBody
|
|
2248
|
+
};
|
|
2249
|
+
await commitAll(commitMessage, workDir, logger).catch((error) => {
|
|
2250
|
+
logger.warn(String(error));
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
if (config.autoPush && branchName && !lastTestFailed) {
|
|
2254
|
+
await pushBranch(branchName, workDir, logger).catch((error) => {
|
|
2255
|
+
logger.warn(String(error));
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
if (config.pr.enable && branchName && !lastTestFailed) {
|
|
2259
|
+
logger.info("\u5F00\u59CB\u521B\u5EFA PR...");
|
|
2260
|
+
const summary = deliverySummary ?? buildFallbackSummary({ task: config.task, testResults: lastTestResults });
|
|
2261
|
+
const prTitleCandidate = config.pr.title?.trim() || summary.prTitle;
|
|
2262
|
+
const prBodyContent = ensurePrBodySections(summary.prBody, {
|
|
2263
|
+
commitTitle: summary.commitTitle,
|
|
2264
|
+
commitBody: summary.commitBody,
|
|
2265
|
+
testResults: lastTestResults
|
|
2266
|
+
});
|
|
2267
|
+
const bodyFile = config.pr.bodyPath ?? buildBodyFile(workDir);
|
|
2268
|
+
await writePrBody(bodyFile, prBodyContent, Boolean(config.pr.bodyPath));
|
|
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
|
+
});
|
|
2278
|
+
}
|
|
2279
|
+
} else {
|
|
2280
|
+
prFailed = true;
|
|
2281
|
+
logger.error("PR \u521B\u5EFA\u5931\u8D25\uFF0C\u8BE6\u89C1\u4E0A\u65B9 gh \u8F93\u51FA");
|
|
2282
|
+
}
|
|
2283
|
+
} else if (branchName && !config.pr.enable) {
|
|
2284
|
+
logger.info("\u672A\u5F00\u542F PR \u521B\u5EFA\uFF08--pr \u672A\u4F20\uFF09\uFF0C\u5C1D\u8BD5\u67E5\u770B\u5DF2\u6709 PR");
|
|
2285
|
+
const existingPr = await viewPr(branchName, workDir, logger);
|
|
2286
|
+
prInfo = existingPr;
|
|
2287
|
+
if (existingPr) logger.info(`\u5DF2\u6709 PR: ${existingPr.url}`);
|
|
2288
|
+
}
|
|
2289
|
+
if (accumulatedUsage) {
|
|
2290
|
+
const input = accumulatedUsage.inputTokens ?? "-";
|
|
2291
|
+
const output = accumulatedUsage.outputTokens ?? "-";
|
|
2292
|
+
logger.info(`Token \u6D88\u8017\u6C47\u603B\uFF1A\u8F93\u5165 ${input}\uFF5C\u8F93\u51FA ${output}\uFF5C\u603B\u8BA1 ${accumulatedUsage.totalTokens}`);
|
|
2293
|
+
} else {
|
|
2294
|
+
logger.info("\u672A\u89E3\u6790\u5230 Token \u6D88\u8017\u4FE1\u606F\uFF0C\u53EF\u68C0\u67E5 AI CLI \u8F93\u51FA\u683C\u5F0F\u662F\u5426\u5305\u542B token \u63D0\u793A");
|
|
2295
|
+
}
|
|
2296
|
+
if (lastTestFailed || prFailed) {
|
|
2297
|
+
throw new Error("\u6D41\u7A0B\u5B58\u5728\u672A\u89E3\u51B3\u7684\u95EE\u9898\uFF08\u6D4B\u8BD5\u672A\u901A\u8FC7\u6216 PR \u521B\u5EFA\u5931\u8D25\uFF09");
|
|
2298
|
+
}
|
|
2299
|
+
if (config.git.useWorktree && workDir !== repoRoot) {
|
|
2300
|
+
await cleanupWorktreeIfSafe({
|
|
2301
|
+
repoRoot,
|
|
2302
|
+
workDir,
|
|
2303
|
+
branchName,
|
|
2304
|
+
prInfo,
|
|
2305
|
+
worktreeCreated,
|
|
2306
|
+
logger
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
logger.success(`wheel-ai \u8FED\u4EE3\u6D41\u7A0B\u7ED3\u675F\uFF5CToken \u603B\u8BA1 ${accumulatedUsage?.totalTokens ?? "\u672A\u77E5"}`);
|
|
2310
|
+
} catch (error) {
|
|
2311
|
+
runError = error instanceof Error ? error.message : String(error);
|
|
2312
|
+
throw error;
|
|
2313
|
+
} finally {
|
|
2314
|
+
const stage = runError ? "\u4EFB\u52A1\u7ED3\u675F\uFF08\u5931\u8D25\uFF09" : "\u4EFB\u52A1\u7ED3\u675F";
|
|
2315
|
+
await notifyWebhook("task_end", lastRound, stage);
|
|
2316
|
+
await runTracker?.finalize();
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
// src/monitor.ts
|
|
2321
|
+
var import_fs_extra8 = __toESM(require("fs-extra"));
|
|
2322
|
+
var import_node_path9 = __toESM(require("path"));
|
|
2323
|
+
var REFRESH_INTERVAL = 1e3;
|
|
2324
|
+
function getTerminalSize2() {
|
|
2325
|
+
const rows = process.stdout.rows ?? 24;
|
|
2326
|
+
const columns = process.stdout.columns ?? 80;
|
|
2327
|
+
return { rows, columns };
|
|
2328
|
+
}
|
|
2329
|
+
function truncateLine2(line, width) {
|
|
2330
|
+
if (width <= 0) return "";
|
|
2331
|
+
if (line.length <= width) return line;
|
|
2332
|
+
return line.slice(0, width);
|
|
2333
|
+
}
|
|
2334
|
+
async function readLogLines2(logFile) {
|
|
2335
|
+
try {
|
|
2336
|
+
const content = await import_fs_extra8.default.readFile(logFile, "utf8");
|
|
2337
|
+
const normalized = content.replace(/\r\n?/g, "\n");
|
|
2338
|
+
const lines = normalized.split("\n");
|
|
2339
|
+
return lines.length > 0 ? lines : [""];
|
|
2340
|
+
} catch (error) {
|
|
2341
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2342
|
+
return [`\uFF08\u65E0\u6CD5\u8BFB\u53D6\u65E5\u5FD7\u6587\u4EF6\uFF1A${message}\uFF09`];
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
async function loadTasks(logsDir) {
|
|
2346
|
+
const registry = await readCurrentRegistry();
|
|
2347
|
+
const entries = Object.entries(registry).sort(([a], [b]) => a.localeCompare(b));
|
|
2348
|
+
const tasks = await Promise.all(entries.map(async ([key, meta]) => {
|
|
2349
|
+
const logFile = meta.logFile ?? import_node_path9.default.join(logsDir, key);
|
|
2350
|
+
const lines = await readLogLines2(logFile);
|
|
2351
|
+
return {
|
|
2352
|
+
key,
|
|
2353
|
+
logFile,
|
|
2354
|
+
meta,
|
|
2355
|
+
lines
|
|
2356
|
+
};
|
|
2357
|
+
}));
|
|
2358
|
+
return tasks;
|
|
2359
|
+
}
|
|
2360
|
+
function buildHeader(state, columns) {
|
|
2361
|
+
if (state.tasks.length === 0) {
|
|
2362
|
+
return truncateLine2("\u6682\u65E0\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\uFF0C\u6309 q \u9000\u51FA", columns);
|
|
2363
|
+
}
|
|
2364
|
+
const current = state.tasks[state.selectedIndex];
|
|
2365
|
+
const total = state.tasks.length;
|
|
2366
|
+
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 truncateLine2(title, columns);
|
|
2369
|
+
}
|
|
2370
|
+
function buildStatus(task, page, columns, errorMessage) {
|
|
2371
|
+
const meta = task.meta;
|
|
2372
|
+
const status = `\u8F6E\u6B21 ${meta.round} \uFF5C Token ${meta.tokenUsed} \uFF5C \u9875 ${page.current}/${page.total} \uFF5C \u9879\u76EE ${meta.path}`;
|
|
2373
|
+
const suffix = errorMessage ? ` \uFF5C \u5237\u65B0\u5931\u8D25\uFF1A${errorMessage}` : "";
|
|
2374
|
+
return truncateLine2(`${status}${suffix}`, columns);
|
|
2375
|
+
}
|
|
2376
|
+
function getPageSize2(rows) {
|
|
2377
|
+
return Math.max(1, rows - 2);
|
|
2378
|
+
}
|
|
2379
|
+
function render2(state) {
|
|
2380
|
+
const { rows, columns } = getTerminalSize2();
|
|
2381
|
+
const pageSize = getPageSize2(rows);
|
|
2382
|
+
const header = buildHeader(state, columns);
|
|
2383
|
+
if (state.tasks.length === 0) {
|
|
2384
|
+
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 = truncateLine2(statusText, columns);
|
|
2387
|
+
const content2 = [header, ...filler, status2].join("\n");
|
|
2388
|
+
process.stdout.write(`\x1B[2J\x1B[H${content2}`);
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
const current = state.tasks[state.selectedIndex];
|
|
2392
|
+
const lines = current.lines;
|
|
2393
|
+
const maxOffset = Math.max(0, Math.ceil(lines.length / pageSize) - 1);
|
|
2394
|
+
const offset = state.pageOffsets.get(current.key) ?? maxOffset;
|
|
2395
|
+
const stick = state.stickToBottom.get(current.key) ?? true;
|
|
2396
|
+
const nextOffset = Math.min(Math.max(stick ? maxOffset : offset, 0), maxOffset);
|
|
2397
|
+
state.pageOffsets.set(current.key, nextOffset);
|
|
2398
|
+
state.stickToBottom.set(current.key, nextOffset === maxOffset);
|
|
2399
|
+
const start = nextOffset * pageSize;
|
|
2400
|
+
const pageLines = lines.slice(start, start + pageSize).map((line) => truncateLine2(line, columns));
|
|
2401
|
+
while (pageLines.length < pageSize) {
|
|
2402
|
+
pageLines.push("");
|
|
2403
|
+
}
|
|
2404
|
+
const status = buildStatus(
|
|
2405
|
+
current,
|
|
2406
|
+
{ current: nextOffset + 1, total: Math.max(1, maxOffset + 1) },
|
|
2407
|
+
columns,
|
|
2408
|
+
state.lastError
|
|
2409
|
+
);
|
|
2410
|
+
const content = [header, ...pageLines, status].join("\n");
|
|
2411
|
+
process.stdout.write(`\x1B[2J\x1B[H${content}`);
|
|
2412
|
+
}
|
|
2413
|
+
function updateSelection(state, tasks) {
|
|
2414
|
+
state.tasks = tasks;
|
|
2415
|
+
if (tasks.length === 0) {
|
|
2416
|
+
state.selectedIndex = 0;
|
|
2417
|
+
state.selectedKey = void 0;
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
if (state.selectedKey) {
|
|
2421
|
+
const index = tasks.findIndex((task) => task.key === state.selectedKey);
|
|
2422
|
+
if (index >= 0) {
|
|
2423
|
+
state.selectedIndex = index;
|
|
2424
|
+
} else {
|
|
2425
|
+
state.selectedIndex = 0;
|
|
2426
|
+
}
|
|
2427
|
+
} else {
|
|
2428
|
+
state.selectedIndex = 0;
|
|
2429
|
+
}
|
|
2430
|
+
state.selectedKey = tasks[state.selectedIndex]?.key;
|
|
2431
|
+
const existing = new Set(tasks.map((task) => task.key));
|
|
2432
|
+
for (const key of state.pageOffsets.keys()) {
|
|
2433
|
+
if (!existing.has(key)) {
|
|
2434
|
+
state.pageOffsets.delete(key);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
for (const key of state.stickToBottom.keys()) {
|
|
2438
|
+
if (!existing.has(key)) {
|
|
2439
|
+
state.stickToBottom.delete(key);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
function moveSelection(state, direction) {
|
|
2444
|
+
if (state.tasks.length === 0) return;
|
|
2445
|
+
const total = state.tasks.length;
|
|
2446
|
+
state.selectedIndex = (state.selectedIndex + direction + total) % total;
|
|
2447
|
+
state.selectedKey = state.tasks[state.selectedIndex]?.key;
|
|
2448
|
+
}
|
|
2449
|
+
function movePage(state, direction) {
|
|
2450
|
+
if (state.tasks.length === 0) return;
|
|
2451
|
+
const { rows } = getTerminalSize2();
|
|
2452
|
+
const pageSize = getPageSize2(rows);
|
|
2453
|
+
const current = state.tasks[state.selectedIndex];
|
|
2454
|
+
const lines = current.lines;
|
|
2455
|
+
const maxOffset = Math.max(0, Math.ceil(lines.length / pageSize) - 1);
|
|
2456
|
+
const offset = state.pageOffsets.get(current.key) ?? maxOffset;
|
|
2457
|
+
const nextOffset = Math.min(Math.max(offset + direction, 0), maxOffset);
|
|
2458
|
+
state.pageOffsets.set(current.key, nextOffset);
|
|
2459
|
+
state.stickToBottom.set(current.key, nextOffset === maxOffset);
|
|
2460
|
+
}
|
|
2461
|
+
function shouldExit2(input) {
|
|
2462
|
+
if (input === "") return true;
|
|
2463
|
+
if (input.toLowerCase() === "q") return true;
|
|
2464
|
+
return false;
|
|
2465
|
+
}
|
|
2466
|
+
function handleInput(state, input) {
|
|
2467
|
+
if (input.includes("\x1B[D")) {
|
|
2468
|
+
moveSelection(state, -1);
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
2471
|
+
if (input.includes("\x1B[C")) {
|
|
2472
|
+
moveSelection(state, 1);
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
if (input.includes("\x1B[A")) {
|
|
2476
|
+
movePage(state, -1);
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
if (input.includes("\x1B[B")) {
|
|
2480
|
+
movePage(state, 1);
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
function setupCleanup2(cleanup) {
|
|
2485
|
+
const exitHandler = () => {
|
|
2486
|
+
cleanup();
|
|
2487
|
+
};
|
|
2488
|
+
const signalHandler = () => {
|
|
2489
|
+
cleanup();
|
|
2490
|
+
process.exit(0);
|
|
2491
|
+
};
|
|
2492
|
+
process.on("SIGINT", signalHandler);
|
|
2493
|
+
process.on("SIGTERM", signalHandler);
|
|
2494
|
+
process.on("exit", exitHandler);
|
|
2495
|
+
}
|
|
2496
|
+
async function runMonitor() {
|
|
2497
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
2498
|
+
console.log("\u5F53\u524D\u7EC8\u7AEF\u4E0D\u652F\u6301\u4EA4\u4E92\u5F0F monitor\u3002");
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
const logsDir = getLogsDir();
|
|
2502
|
+
const state = {
|
|
2503
|
+
tasks: [],
|
|
2504
|
+
selectedIndex: 0,
|
|
2505
|
+
pageOffsets: /* @__PURE__ */ new Map(),
|
|
2506
|
+
stickToBottom: /* @__PURE__ */ new Map()
|
|
2507
|
+
};
|
|
2508
|
+
let cleaned = false;
|
|
2509
|
+
const cleanup = () => {
|
|
2510
|
+
if (cleaned) return;
|
|
2511
|
+
cleaned = true;
|
|
2512
|
+
if (process.stdin.isTTY) {
|
|
2513
|
+
process.stdin.setRawMode(false);
|
|
2514
|
+
process.stdin.pause();
|
|
2515
|
+
}
|
|
2516
|
+
process.stdout.write("\x1B[?25h");
|
|
2517
|
+
};
|
|
2518
|
+
setupCleanup2(cleanup);
|
|
2519
|
+
process.stdout.write("\x1B[?25l");
|
|
2520
|
+
process.stdin.setRawMode(true);
|
|
2521
|
+
process.stdin.resume();
|
|
2522
|
+
let refreshing = false;
|
|
2523
|
+
const refresh = async () => {
|
|
2524
|
+
if (refreshing) return;
|
|
2525
|
+
refreshing = true;
|
|
2526
|
+
try {
|
|
2527
|
+
const tasks = await loadTasks(logsDir);
|
|
2528
|
+
state.lastError = void 0;
|
|
2529
|
+
updateSelection(state, tasks);
|
|
2530
|
+
render2(state);
|
|
2531
|
+
} catch (error) {
|
|
2532
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2533
|
+
state.lastError = message;
|
|
2534
|
+
render2(state);
|
|
2535
|
+
} finally {
|
|
2536
|
+
refreshing = false;
|
|
2537
|
+
}
|
|
2538
|
+
};
|
|
2539
|
+
await refresh();
|
|
2540
|
+
const timer = setInterval(refresh, REFRESH_INTERVAL);
|
|
2541
|
+
process.stdin.on("data", (data) => {
|
|
2542
|
+
const input = data.toString("utf8");
|
|
2543
|
+
if (shouldExit2(input)) {
|
|
2544
|
+
clearInterval(timer);
|
|
2545
|
+
cleanup();
|
|
2546
|
+
process.exit(0);
|
|
2547
|
+
}
|
|
2548
|
+
handleInput(state, input);
|
|
2549
|
+
render2(state);
|
|
2550
|
+
});
|
|
2551
|
+
process.stdout.on("resize", () => {
|
|
2552
|
+
render2(state);
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
// src/cli.ts
|
|
2557
|
+
function parseInteger(value, defaultValue) {
|
|
2558
|
+
const parsed = Number.parseInt(value, 10);
|
|
2559
|
+
if (Number.isNaN(parsed)) return defaultValue;
|
|
2560
|
+
return parsed;
|
|
2561
|
+
}
|
|
2562
|
+
function collect(value, previous) {
|
|
2563
|
+
return [...previous, value];
|
|
2564
|
+
}
|
|
2565
|
+
function normalizeOptional(value) {
|
|
2566
|
+
if (typeof value !== "string") return void 0;
|
|
2567
|
+
const trimmed = value.trim();
|
|
2568
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
2569
|
+
}
|
|
2570
|
+
function hasOption(argv, option) {
|
|
2571
|
+
return argv.some((arg) => arg === option || arg.startsWith(`${option}=`));
|
|
2572
|
+
}
|
|
2573
|
+
function buildBackgroundArgs(argv, logFile, branchName, injectBranch = false) {
|
|
2574
|
+
const rawArgs = argv.slice(1);
|
|
2575
|
+
const filtered = rawArgs.filter((arg) => !(arg === "--background" || arg.startsWith("--background=")));
|
|
2576
|
+
if (!hasOption(filtered, "--log-file")) {
|
|
2577
|
+
filtered.push("--log-file", logFile);
|
|
2578
|
+
}
|
|
2579
|
+
if (injectBranch && branchName && !hasOption(filtered, "--branch")) {
|
|
2580
|
+
filtered.push("--branch", branchName);
|
|
2581
|
+
}
|
|
2582
|
+
return filtered;
|
|
2583
|
+
}
|
|
2584
|
+
async function runCli(argv) {
|
|
2585
|
+
const globalConfig = await loadGlobalConfig(defaultLogger);
|
|
2586
|
+
const effectiveArgv = applyShortcutArgv(argv, globalConfig);
|
|
2587
|
+
const program = new import_commander.Command();
|
|
2588
|
+
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").requiredOption("-t, --task <task>", "\u9700\u8981\u5B8C\u6210\u7684\u4EFB\u52A1\u63CF\u8FF0\uFF08\u4F1A\u8FDB\u5165 AI \u63D0\u793A\uFF09").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("--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("--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).action(async (options) => {
|
|
2590
|
+
const useWorktree = Boolean(options.worktree);
|
|
2591
|
+
const branchInput = normalizeOptional(options.branch);
|
|
2592
|
+
const logFileInput = normalizeOptional(options.logFile);
|
|
2593
|
+
const background = Boolean(options.background);
|
|
2594
|
+
let branchName = branchInput;
|
|
2595
|
+
if (useWorktree && !branchName) {
|
|
2596
|
+
branchName = generateBranchName();
|
|
2597
|
+
}
|
|
2598
|
+
let logFile = logFileInput;
|
|
2599
|
+
if (background && !logFile) {
|
|
2600
|
+
let branchForLog = branchName;
|
|
2601
|
+
if (!branchForLog) {
|
|
2602
|
+
try {
|
|
2603
|
+
const current = await getCurrentBranch(process.cwd(), defaultLogger);
|
|
2604
|
+
branchForLog = current || "detached";
|
|
2605
|
+
} catch {
|
|
2606
|
+
branchForLog = "unknown";
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
logFile = buildAutoLogFilePath(branchForLog);
|
|
2610
|
+
}
|
|
2611
|
+
if (background) {
|
|
2612
|
+
if (!logFile) {
|
|
2613
|
+
throw new Error("\u540E\u53F0\u8FD0\u884C\u9700\u8981\u6307\u5B9A\u65E5\u5FD7\u6587\u4EF6");
|
|
2614
|
+
}
|
|
2615
|
+
const args = buildBackgroundArgs(effectiveArgv, logFile, branchName, useWorktree && !branchInput);
|
|
2616
|
+
const child = (0, import_node_child_process.spawn)(process.execPath, [...process.execArgv, ...args], {
|
|
2617
|
+
detached: true,
|
|
2618
|
+
stdio: "ignore"
|
|
2619
|
+
});
|
|
2620
|
+
child.unref();
|
|
2621
|
+
const displayLogFile = resolvePath(process.cwd(), logFile);
|
|
2622
|
+
console.log(`\u5DF2\u5207\u5165\u540E\u53F0\u8FD0\u884C\uFF0C\u65E5\u5FD7\u8F93\u51FA\u81F3 ${displayLogFile}`);
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
const cliOptions = {
|
|
2626
|
+
task: options.task,
|
|
2627
|
+
iterations: options.iterations,
|
|
2628
|
+
aiCli: options.aiCli,
|
|
2629
|
+
aiArgs: options.aiArgs ?? [],
|
|
2630
|
+
aiPromptArg: options.aiPromptArg,
|
|
2631
|
+
notesFile: options.notesFile,
|
|
2632
|
+
planFile: options.planFile,
|
|
2633
|
+
workflowDoc: options.workflowDoc,
|
|
2634
|
+
useWorktree,
|
|
2635
|
+
branch: branchName,
|
|
2636
|
+
worktreePath: options.worktreePath,
|
|
2637
|
+
baseBranch: options.baseBranch,
|
|
2638
|
+
runTests: Boolean(options.runTests),
|
|
2639
|
+
runE2e: Boolean(options.runE2e),
|
|
2640
|
+
unitCommand: options.unitCommand,
|
|
2641
|
+
e2eCommand: options.e2eCommand,
|
|
2642
|
+
autoCommit: Boolean(options.autoCommit),
|
|
2643
|
+
autoPush: Boolean(options.autoPush),
|
|
2644
|
+
pr: Boolean(options.pr),
|
|
2645
|
+
prTitle: options.prTitle,
|
|
2646
|
+
prBody: options.prBody,
|
|
2647
|
+
draft: Boolean(options.draft),
|
|
2648
|
+
reviewers: options.reviewer ?? [],
|
|
2649
|
+
webhookUrls: options.webhook ?? [],
|
|
2650
|
+
webhookTimeout: options.webhookTimeout,
|
|
2651
|
+
stopSignal: options.stopSignal,
|
|
2652
|
+
logFile,
|
|
2653
|
+
verbose: Boolean(options.verbose),
|
|
2654
|
+
skipInstall: Boolean(options.skipInstall)
|
|
2655
|
+
};
|
|
2656
|
+
const config = buildLoopConfig(cliOptions, process.cwd());
|
|
2657
|
+
await runLoop(config);
|
|
2658
|
+
});
|
|
2659
|
+
program.command("monitor").description("\u67E5\u770B\u540E\u53F0\u8FD0\u884C\u65E5\u5FD7").action(async () => {
|
|
2660
|
+
await runMonitor();
|
|
2661
|
+
});
|
|
2662
|
+
program.command("logs").description("\u67E5\u770B\u5386\u53F2\u65E5\u5FD7").action(async () => {
|
|
2663
|
+
await runLogsViewer();
|
|
2664
|
+
});
|
|
2665
|
+
await program.parseAsync(effectiveArgv);
|
|
2666
|
+
}
|
|
2667
|
+
if (require.main === module) {
|
|
2668
|
+
runCli(process.argv).catch((error) => {
|
|
2669
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
2670
|
+
defaultLogger.error(message);
|
|
2671
|
+
process.exitCode = 1;
|
|
2672
|
+
});
|
|
2673
|
+
}
|
|
2674
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2675
|
+
0 && (module.exports = {
|
|
2676
|
+
runCli
|
|
2677
|
+
});
|
|
2678
|
+
//# sourceMappingURL=cli.js.map
|