helloloop 0.8.4 → 0.8.6
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +66 -1
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
- package/hosts/gemini/extension/gemini-extension.json +1 -1
- package/package.json +2 -2
- package/src/analyzer.mjs +20 -230
- package/src/analyzer_support.mjs +232 -0
- package/src/cli.mjs +56 -32
- package/src/cli_analyze_command.mjs +10 -7
- package/src/cli_args.mjs +5 -0
- package/src/cli_command_handlers.mjs +31 -0
- package/src/completion_review.mjs +2 -0
- package/src/context.mjs +6 -0
- package/src/engine_process_support.mjs +32 -0
- package/src/engine_selection_settings.mjs +12 -0
- package/src/host_lease.mjs +204 -0
- package/src/process.mjs +7 -654
- package/src/runner_execute_task.mjs +55 -1
- package/src/runner_execution_support.mjs +14 -0
- package/src/runner_status.mjs +12 -1
- package/src/runtime_engine_support.mjs +342 -0
- package/src/runtime_engine_task.mjs +395 -0
- package/src/supervisor_runtime.mjs +314 -0
- package/src/terminal_session_limits.mjs +394 -0
- package/src/verify_runner.mjs +84 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { ensureDir, fileExists, nowIso, readJson, timestampForFile, writeJson } from "./common.mjs";
|
|
5
|
+
import { loadGlobalConfig } from "./global_config.mjs";
|
|
6
|
+
|
|
7
|
+
const SESSION_DIR_NAME = "terminal-sessions";
|
|
8
|
+
const RUNTIME_DIR_NAME = "runtime";
|
|
9
|
+
const LOCK_FILE_NAME = ".lock";
|
|
10
|
+
const LOCK_RETRY_DELAYS_MS = [0, 20, 50, 100, 200, 300, 500];
|
|
11
|
+
const STALE_PREPARED_SESSION_MS = 60_000;
|
|
12
|
+
const TRACKED_VISIBLE_COMMANDS = new Set(["analyze", "next", "run-loop", "run-once"]);
|
|
13
|
+
|
|
14
|
+
let currentTerminalSession = null;
|
|
15
|
+
let cleanupRegistered = false;
|
|
16
|
+
|
|
17
|
+
function sleepSync(ms) {
|
|
18
|
+
const shared = new SharedArrayBuffer(4);
|
|
19
|
+
const view = new Int32Array(shared);
|
|
20
|
+
Atomics.wait(view, 0, 0, Math.max(0, ms));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isPidAlive(pid) {
|
|
24
|
+
const value = Number(pid || 0);
|
|
25
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
process.kill(value, 0);
|
|
30
|
+
return true;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return String(error?.code || "") === "EPERM";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeNonNegativeInteger(value, fallbackValue) {
|
|
37
|
+
if (value === null || value === undefined || value === "") {
|
|
38
|
+
return fallbackValue;
|
|
39
|
+
}
|
|
40
|
+
const parsed = Number(value);
|
|
41
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
42
|
+
return fallbackValue;
|
|
43
|
+
}
|
|
44
|
+
return Math.floor(parsed);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function normalizeTerminalConcurrencySettings(settings = {}) {
|
|
48
|
+
const source = settings && typeof settings === "object" ? settings : {};
|
|
49
|
+
return {
|
|
50
|
+
enabled: source.enabled !== false,
|
|
51
|
+
visibleMax: normalizeNonNegativeInteger(source.visibleMax, 8),
|
|
52
|
+
backgroundMax: normalizeNonNegativeInteger(source.backgroundMax, 8),
|
|
53
|
+
totalMax: normalizeNonNegativeInteger(source.totalMax, 8),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resolveTerminalRuntimeConfig(options = {}) {
|
|
58
|
+
const globalConfig = loadGlobalConfig({
|
|
59
|
+
globalConfigFile: options.globalConfigFile,
|
|
60
|
+
});
|
|
61
|
+
const settingsFile = globalConfig?._meta?.configFile || "";
|
|
62
|
+
const settingsHome = settingsFile ? path.dirname(settingsFile) : process.cwd();
|
|
63
|
+
return {
|
|
64
|
+
settingsFile,
|
|
65
|
+
registryRoot: path.join(settingsHome, RUNTIME_DIR_NAME, SESSION_DIR_NAME),
|
|
66
|
+
limits: normalizeTerminalConcurrencySettings(globalConfig?.runtime?.terminalConcurrency || {}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function withRegistryLock(registryRoot, callback) {
|
|
71
|
+
ensureDir(registryRoot);
|
|
72
|
+
const lockFile = path.join(registryRoot, LOCK_FILE_NAME);
|
|
73
|
+
let lastError = null;
|
|
74
|
+
|
|
75
|
+
for (const delayMs of LOCK_RETRY_DELAYS_MS) {
|
|
76
|
+
if (delayMs > 0) {
|
|
77
|
+
sleepSync(delayMs);
|
|
78
|
+
}
|
|
79
|
+
let lockFd = null;
|
|
80
|
+
try {
|
|
81
|
+
lockFd = fs.openSync(lockFile, "wx");
|
|
82
|
+
const result = callback();
|
|
83
|
+
fs.closeSync(lockFd);
|
|
84
|
+
fs.rmSync(lockFile, { force: true });
|
|
85
|
+
return result;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
lastError = error;
|
|
88
|
+
if (lockFd !== null) {
|
|
89
|
+
try {
|
|
90
|
+
fs.closeSync(lockFd);
|
|
91
|
+
} catch {
|
|
92
|
+
// ignore lock close failure
|
|
93
|
+
}
|
|
94
|
+
fs.rmSync(lockFile, { force: true });
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
if (String(error?.code || "").toUpperCase() !== "EEXIST") {
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
throw lastError || new Error(`无法获取 HelloLoop 终端会话锁:${registryRoot}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function listSessionFiles(registryRoot) {
|
|
107
|
+
if (!fileExists(registryRoot)) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
return fs.readdirSync(registryRoot)
|
|
111
|
+
.filter((item) => item.endsWith(".json"))
|
|
112
|
+
.map((item) => path.join(registryRoot, item));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readSessionRecord(sessionFile) {
|
|
116
|
+
try {
|
|
117
|
+
return readJson(sessionFile);
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isPreparedSessionExpired(record) {
|
|
124
|
+
const updatedAt = Date.parse(String(record?.updatedAt || record?.createdAt || ""));
|
|
125
|
+
if (!Number.isFinite(updatedAt)) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return Date.now() - updatedAt > STALE_PREPARED_SESSION_MS;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isStaleSession(record) {
|
|
132
|
+
const kind = String(record?.kind || "");
|
|
133
|
+
if (!["visible", "background"].includes(kind)) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
if (Number(record?.pid || 0) > 0) {
|
|
137
|
+
return !isPidAlive(record.pid);
|
|
138
|
+
}
|
|
139
|
+
if (Number(record?.ownerPid || 0) > 0 && !isPidAlive(record.ownerPid)) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
return isPreparedSessionExpired(record);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function cleanupStaleSessions(registryRoot) {
|
|
146
|
+
const activeSessions = [];
|
|
147
|
+
for (const sessionFile of listSessionFiles(registryRoot)) {
|
|
148
|
+
const record = readSessionRecord(sessionFile);
|
|
149
|
+
if (!record || isStaleSession(record)) {
|
|
150
|
+
fs.rmSync(sessionFile, { force: true });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
activeSessions.push({
|
|
154
|
+
...record,
|
|
155
|
+
file: sessionFile,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return activeSessions;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function countSessions(sessions, excludingId = "") {
|
|
162
|
+
return sessions
|
|
163
|
+
.filter((session) => session.id !== excludingId)
|
|
164
|
+
.reduce((counts, session) => {
|
|
165
|
+
if (session.kind === "visible") {
|
|
166
|
+
counts.visible += 1;
|
|
167
|
+
}
|
|
168
|
+
if (session.kind === "background") {
|
|
169
|
+
counts.background += 1;
|
|
170
|
+
}
|
|
171
|
+
counts.total += 1;
|
|
172
|
+
return counts;
|
|
173
|
+
}, { visible: 0, background: 0, total: 0 });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function throwSessionLimitError(kind, counts, limits, settingsFile, reason) {
|
|
177
|
+
const kindLabel = kind === "background" ? "背景终端" : "显示终端";
|
|
178
|
+
const scope = [
|
|
179
|
+
`显示终端 ${counts.visible}/${limits.visibleMax}`,
|
|
180
|
+
`背景终端 ${counts.background}/${limits.backgroundMax}`,
|
|
181
|
+
`总并发 ${counts.total}/${limits.totalMax}`,
|
|
182
|
+
].join(",");
|
|
183
|
+
throw new Error(
|
|
184
|
+
`${kindLabel}${reason},当前 ${scope}。`
|
|
185
|
+
+ ` 如需调整,请修改 ${settingsFile} 中的 runtime.terminalConcurrency.visibleMax / backgroundMax / totalMax。`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function assertSessionLimit(kind, sessions, limits, settingsFile, excludingId = "") {
|
|
190
|
+
if (!limits.enabled) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const counts = countSessions(sessions, excludingId);
|
|
195
|
+
const nextCounts = {
|
|
196
|
+
visible: counts.visible + (kind === "visible" ? 1 : 0),
|
|
197
|
+
background: counts.background + (kind === "background" ? 1 : 0),
|
|
198
|
+
total: counts.total + 1,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
if (kind === "visible" && nextCounts.visible > limits.visibleMax) {
|
|
202
|
+
throwSessionLimitError(kind, nextCounts, limits, settingsFile, "并发已达上限");
|
|
203
|
+
}
|
|
204
|
+
if (kind === "background" && nextCounts.background > limits.backgroundMax) {
|
|
205
|
+
throwSessionLimitError(kind, nextCounts, limits, settingsFile, "并发已达上限");
|
|
206
|
+
}
|
|
207
|
+
if (nextCounts.total > limits.totalMax) {
|
|
208
|
+
throwSessionLimitError(kind, nextCounts, limits, settingsFile, "启动被阻止:显示终端与背景终端合计并发已达上限");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildSessionRecord(kind, options = {}) {
|
|
213
|
+
return {
|
|
214
|
+
id: options.id || `${timestampForFile()}-${kind}-${process.pid}`,
|
|
215
|
+
kind,
|
|
216
|
+
pid: Number(options.pid || process.pid),
|
|
217
|
+
ownerPid: Number(options.ownerPid || process.pid),
|
|
218
|
+
command: String(options.command || "").trim(),
|
|
219
|
+
sessionId: String(options.sessionId || "").trim(),
|
|
220
|
+
repoRoot: String(options.repoRoot || "").trim(),
|
|
221
|
+
createdAt: options.createdAt || nowIso(),
|
|
222
|
+
updatedAt: nowIso(),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function writeSessionRecord(sessionFile, record) {
|
|
227
|
+
writeJson(sessionFile, record);
|
|
228
|
+
return {
|
|
229
|
+
...record,
|
|
230
|
+
file: sessionFile,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function ensureCleanupRegistration() {
|
|
235
|
+
if (cleanupRegistered) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
cleanupRegistered = true;
|
|
239
|
+
process.on("exit", () => {
|
|
240
|
+
try {
|
|
241
|
+
releaseCurrentTerminalSession();
|
|
242
|
+
} catch {
|
|
243
|
+
// ignore exit cleanup failure
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function setCurrentTerminalSession(record) {
|
|
249
|
+
if (!record) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
currentTerminalSession = {
|
|
253
|
+
...record,
|
|
254
|
+
preparedForBackground: false,
|
|
255
|
+
handedOff: false,
|
|
256
|
+
};
|
|
257
|
+
ensureCleanupRegistration();
|
|
258
|
+
return currentTerminalSession;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function shouldTrackVisibleTerminalCommand(command) {
|
|
262
|
+
return TRACKED_VISIBLE_COMMANDS.has(String(command || "").trim());
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function acquireVisibleTerminalSession(options = {}) {
|
|
266
|
+
if (currentTerminalSession) {
|
|
267
|
+
return currentTerminalSession;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const runtime = resolveTerminalRuntimeConfig(options);
|
|
271
|
+
if (!runtime.limits.enabled) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return withRegistryLock(runtime.registryRoot, () => {
|
|
276
|
+
const sessions = cleanupStaleSessions(runtime.registryRoot);
|
|
277
|
+
assertSessionLimit("visible", sessions, runtime.limits, runtime.settingsFile);
|
|
278
|
+
const record = buildSessionRecord("visible", options);
|
|
279
|
+
const sessionFile = path.join(runtime.registryRoot, `${record.id}.json`);
|
|
280
|
+
return setCurrentTerminalSession(writeSessionRecord(sessionFile, record));
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function prepareCurrentTerminalSessionForBackground(options = {}) {
|
|
285
|
+
const runtime = resolveTerminalRuntimeConfig(options);
|
|
286
|
+
if (!runtime.limits.enabled) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return withRegistryLock(runtime.registryRoot, () => {
|
|
291
|
+
const sessions = cleanupStaleSessions(runtime.registryRoot);
|
|
292
|
+
|
|
293
|
+
if (!currentTerminalSession) {
|
|
294
|
+
assertSessionLimit("background", sessions, runtime.limits, runtime.settingsFile);
|
|
295
|
+
const record = buildSessionRecord("background", {
|
|
296
|
+
...options,
|
|
297
|
+
pid: 0,
|
|
298
|
+
});
|
|
299
|
+
const sessionFile = path.join(runtime.registryRoot, `${record.id}.json`);
|
|
300
|
+
const next = writeSessionRecord(sessionFile, record);
|
|
301
|
+
currentTerminalSession = {
|
|
302
|
+
...next,
|
|
303
|
+
preparedForBackground: true,
|
|
304
|
+
handedOff: false,
|
|
305
|
+
};
|
|
306
|
+
ensureCleanupRegistration();
|
|
307
|
+
return currentTerminalSession;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
assertSessionLimit("background", sessions, runtime.limits, runtime.settingsFile, currentTerminalSession.id);
|
|
311
|
+
const record = buildSessionRecord("background", {
|
|
312
|
+
...currentTerminalSession,
|
|
313
|
+
...options,
|
|
314
|
+
pid: 0,
|
|
315
|
+
ownerPid: process.pid,
|
|
316
|
+
});
|
|
317
|
+
const next = writeSessionRecord(currentTerminalSession.file, record);
|
|
318
|
+
currentTerminalSession = {
|
|
319
|
+
...next,
|
|
320
|
+
preparedForBackground: true,
|
|
321
|
+
handedOff: false,
|
|
322
|
+
};
|
|
323
|
+
return currentTerminalSession;
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function finalizePreparedTerminalSessionBackground(pid, options = {}) {
|
|
328
|
+
if (!currentTerminalSession?.file) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const runtime = resolveTerminalRuntimeConfig(options);
|
|
333
|
+
return withRegistryLock(runtime.registryRoot, () => {
|
|
334
|
+
const record = buildSessionRecord("background", {
|
|
335
|
+
...currentTerminalSession,
|
|
336
|
+
...options,
|
|
337
|
+
pid,
|
|
338
|
+
ownerPid: pid,
|
|
339
|
+
createdAt: currentTerminalSession.createdAt,
|
|
340
|
+
});
|
|
341
|
+
const next = writeSessionRecord(currentTerminalSession.file, record);
|
|
342
|
+
currentTerminalSession = {
|
|
343
|
+
...next,
|
|
344
|
+
preparedForBackground: false,
|
|
345
|
+
handedOff: true,
|
|
346
|
+
};
|
|
347
|
+
return currentTerminalSession;
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function cancelPreparedTerminalSessionBackground(options = {}) {
|
|
352
|
+
if (!currentTerminalSession?.file || !currentTerminalSession.preparedForBackground) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const runtime = resolveTerminalRuntimeConfig(options);
|
|
357
|
+
return withRegistryLock(runtime.registryRoot, () => {
|
|
358
|
+
fs.rmSync(currentTerminalSession.file, { force: true });
|
|
359
|
+
currentTerminalSession = null;
|
|
360
|
+
return true;
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function bindBackgroundTerminalSession(sessionFile, options = {}) {
|
|
365
|
+
if (!sessionFile) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const registryRoot = path.dirname(sessionFile);
|
|
370
|
+
return withRegistryLock(registryRoot, () => {
|
|
371
|
+
const existing = fileExists(sessionFile) ? readSessionRecord(sessionFile) : null;
|
|
372
|
+
const record = buildSessionRecord("background", {
|
|
373
|
+
...existing,
|
|
374
|
+
...options,
|
|
375
|
+
pid: process.pid,
|
|
376
|
+
ownerPid: process.pid,
|
|
377
|
+
});
|
|
378
|
+
return setCurrentTerminalSession(writeSessionRecord(sessionFile, record));
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function releaseCurrentTerminalSession() {
|
|
383
|
+
if (!currentTerminalSession?.file || currentTerminalSession.handedOff) {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const sessionFile = currentTerminalSession.file;
|
|
388
|
+
const registryRoot = path.dirname(sessionFile);
|
|
389
|
+
withRegistryLock(registryRoot, () => {
|
|
390
|
+
fs.rmSync(sessionFile, { force: true });
|
|
391
|
+
});
|
|
392
|
+
currentTerminalSession = null;
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { tailText, writeText } from "./common.mjs";
|
|
4
|
+
import { resolveVerifyInvocation, runChild } from "./engine_process_support.mjs";
|
|
5
|
+
import { isHostLeaseAlive } from "./host_lease.mjs";
|
|
6
|
+
import { buildHostLeaseStoppedResult } from "./runtime_engine_support.mjs";
|
|
7
|
+
|
|
8
|
+
export async function runShellCommand(context, commandLine, runDir, index, options = {}) {
|
|
9
|
+
if (!isHostLeaseAlive(options.hostLease)) {
|
|
10
|
+
const stopped = buildHostLeaseStoppedResult("检测到宿主窗口已关闭,HelloLoop 已停止当前验证命令。");
|
|
11
|
+
const prefix = String(index + 1).padStart(2, "0");
|
|
12
|
+
writeText(path.join(runDir, `${prefix}-verify-command.txt`), commandLine);
|
|
13
|
+
writeText(path.join(runDir, `${prefix}-verify-stdout.log`), stopped.stdout);
|
|
14
|
+
writeText(path.join(runDir, `${prefix}-verify-stderr.log`), stopped.stderr);
|
|
15
|
+
return { command: commandLine, ...stopped };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const shellInvocation = resolveVerifyInvocation();
|
|
19
|
+
if (shellInvocation.error) {
|
|
20
|
+
const result = {
|
|
21
|
+
command: commandLine,
|
|
22
|
+
ok: false,
|
|
23
|
+
code: 1,
|
|
24
|
+
stdout: "",
|
|
25
|
+
stderr: shellInvocation.error,
|
|
26
|
+
};
|
|
27
|
+
const prefix = String(index + 1).padStart(2, "0");
|
|
28
|
+
writeText(path.join(runDir, `${prefix}-verify-command.txt`), commandLine);
|
|
29
|
+
writeText(path.join(runDir, `${prefix}-verify-stdout.log`), result.stdout);
|
|
30
|
+
writeText(path.join(runDir, `${prefix}-verify-stderr.log`), result.stderr);
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const result = await runChild(shellInvocation.command, [
|
|
35
|
+
...shellInvocation.argsPrefix,
|
|
36
|
+
commandLine,
|
|
37
|
+
], {
|
|
38
|
+
cwd: context.repoRoot,
|
|
39
|
+
shell: shellInvocation.shell,
|
|
40
|
+
shouldKeepRunning() {
|
|
41
|
+
return isHostLeaseAlive(options.hostLease);
|
|
42
|
+
},
|
|
43
|
+
leaseStopReason: "检测到宿主窗口已关闭,HelloLoop 已停止当前验证命令。",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const prefix = String(index + 1).padStart(2, "0");
|
|
47
|
+
writeText(path.join(runDir, `${prefix}-verify-command.txt`), commandLine);
|
|
48
|
+
writeText(path.join(runDir, `${prefix}-verify-stdout.log`), result.stdout);
|
|
49
|
+
writeText(path.join(runDir, `${prefix}-verify-stderr.log`), result.stderr);
|
|
50
|
+
|
|
51
|
+
return { command: commandLine, ...result };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function runVerifyCommands(context, commands, runDir, options = {}) {
|
|
55
|
+
const results = [];
|
|
56
|
+
|
|
57
|
+
for (const [index, command] of commands.entries()) {
|
|
58
|
+
const result = await runShellCommand(context, command, runDir, index, options);
|
|
59
|
+
results.push(result);
|
|
60
|
+
if (!result.ok) {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
results,
|
|
64
|
+
failed: result,
|
|
65
|
+
summary: [
|
|
66
|
+
`验证失败:${result.command}`,
|
|
67
|
+
"",
|
|
68
|
+
"stdout 尾部:",
|
|
69
|
+
tailText(result.stdout, 40),
|
|
70
|
+
"",
|
|
71
|
+
"stderr 尾部:",
|
|
72
|
+
tailText(result.stderr, 40),
|
|
73
|
+
].join("\n").trim(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
ok: true,
|
|
80
|
+
results,
|
|
81
|
+
failed: null,
|
|
82
|
+
summary: "全部验证命令通过。",
|
|
83
|
+
};
|
|
84
|
+
}
|