helloloop 0.8.6 → 0.10.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +230 -498
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
- package/hosts/gemini/extension/gemini-extension.json +1 -1
- package/native/windows-hidden-shell-proxy/HelloLoopHiddenShellProxy.csproj +11 -0
- package/native/windows-hidden-shell-proxy/Program.cs +498 -0
- package/package.json +4 -2
- package/src/activity_projection.mjs +294 -0
- package/src/analyze_confirmation.mjs +3 -1
- package/src/analyzer.mjs +2 -1
- package/src/auto_execution_options.mjs +13 -0
- package/src/background_launch.mjs +73 -0
- package/src/cli.mjs +51 -1
- package/src/cli_analyze_command.mjs +12 -14
- package/src/cli_args.mjs +106 -32
- package/src/cli_command_handlers.mjs +73 -25
- package/src/cli_support.mjs +2 -0
- package/src/common.mjs +11 -0
- package/src/dashboard_command.mjs +371 -0
- package/src/dashboard_tui.mjs +289 -0
- package/src/dashboard_web.mjs +351 -0
- package/src/dashboard_web_client.mjs +167 -0
- package/src/dashboard_web_page.mjs +49 -0
- package/src/engine_event_parser_codex.mjs +167 -0
- package/src/engine_process_support.mjs +7 -2
- package/src/engine_selection.mjs +24 -0
- package/src/engine_selection_probe.mjs +10 -6
- package/src/engine_selection_settings.mjs +53 -44
- package/src/execution_interactivity.mjs +12 -0
- package/src/host_continuation.mjs +305 -0
- package/src/install_codex.mjs +20 -30
- package/src/install_shared.mjs +9 -0
- package/src/node_process_launch.mjs +28 -0
- package/src/process.mjs +2 -0
- package/src/runner_execute_task.mjs +15 -1
- package/src/runner_execution_support.mjs +69 -3
- package/src/runner_once.mjs +5 -0
- package/src/runner_status.mjs +72 -4
- package/src/runtime_engine_support.mjs +52 -5
- package/src/runtime_engine_task.mjs +7 -0
- package/src/runtime_settings.mjs +105 -0
- package/src/runtime_settings_loader.mjs +19 -0
- package/src/shell_invocation.mjs +227 -9
- package/src/supervisor_cli_support.mjs +49 -0
- package/src/supervisor_guardian.mjs +307 -0
- package/src/supervisor_runtime.mjs +142 -83
- package/src/supervisor_state.mjs +64 -0
- package/src/supervisor_watch.mjs +364 -0
- package/src/terminal_session_limits.mjs +1 -21
- package/src/windows_hidden_shell_proxy.mjs +405 -0
- package/src/workspace_registry.mjs +155 -0
package/src/runner_status.mjs
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
readJsonIfExists,
|
|
5
|
+
selectLatestActivityFile,
|
|
6
|
+
selectLatestRuntimeFile,
|
|
7
|
+
} from "./activity_projection.mjs";
|
|
3
8
|
import { fileExists, readJson, sanitizeId, tailText, timestampForFile } from "./common.mjs";
|
|
4
9
|
import { renderTaskSummary, selectNextTask, summarizeBacklog } from "./backlog.mjs";
|
|
5
10
|
import { loadBacklog } from "./config.mjs";
|
|
@@ -83,13 +88,42 @@ export function renderStatusMarkdown(context, { summary, currentTask, lastResult
|
|
|
83
88
|
].join("\n");
|
|
84
89
|
}
|
|
85
90
|
|
|
86
|
-
export function
|
|
91
|
+
export function collectRepoStatusSnapshot(context, options = {}) {
|
|
87
92
|
const backlog = loadBacklog(context);
|
|
88
93
|
const summary = summarizeBacklog(backlog);
|
|
89
94
|
const nextTask = selectNextTask(backlog, options);
|
|
90
|
-
const supervisor = fileExists(context.supervisorStateFile)
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
const supervisor = fileExists(context.supervisorStateFile) ? readJson(context.supervisorStateFile) : null;
|
|
96
|
+
const latestStatus = fileExists(context.statusFile) ? readJson(context.statusFile) : null;
|
|
97
|
+
const runtimeFile = latestStatus?.runDir ? selectLatestRuntimeFile(latestStatus.runDir) : "";
|
|
98
|
+
const runtime = readJsonIfExists(runtimeFile);
|
|
99
|
+
const activityFile = runtime?.activityFile && fileExists(runtime.activityFile)
|
|
100
|
+
? runtime.activityFile
|
|
101
|
+
: (latestStatus?.runDir ? selectLatestActivityFile(latestStatus.runDir, runtime?.attemptPrefix || "") : "");
|
|
102
|
+
const activity = readJsonIfExists(activityFile);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
summary,
|
|
106
|
+
nextTask,
|
|
107
|
+
supervisor,
|
|
108
|
+
latestStatus,
|
|
109
|
+
runtimeFile,
|
|
110
|
+
runtime,
|
|
111
|
+
activityFile,
|
|
112
|
+
activity,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function renderStatusText(context, options = {}) {
|
|
117
|
+
const snapshot = collectRepoStatusSnapshot(context, options);
|
|
118
|
+
const {
|
|
119
|
+
summary,
|
|
120
|
+
nextTask,
|
|
121
|
+
supervisor,
|
|
122
|
+
latestStatus,
|
|
123
|
+
runtime,
|
|
124
|
+
activity,
|
|
125
|
+
} = snapshot;
|
|
126
|
+
const hostResume = options.hostResume || null;
|
|
93
127
|
|
|
94
128
|
return [
|
|
95
129
|
"HelloLoop 状态",
|
|
@@ -106,10 +140,44 @@ export function renderStatusText(context, options = {}) {
|
|
|
106
140
|
`后台会话:${supervisor.status}`,
|
|
107
141
|
`后台会话 ID:${supervisor.sessionId || "unknown"}`,
|
|
108
142
|
`后台租约:${renderHostLeaseLabel(supervisor.lease)}`,
|
|
143
|
+
...(Number.isFinite(Number(supervisor.guardianRestartCount)) && Number(supervisor.guardianRestartCount) > 0
|
|
144
|
+
? [`守护重拉起次数:${supervisor.guardianRestartCount}`]
|
|
145
|
+
: []),
|
|
146
|
+
]
|
|
147
|
+
: []),
|
|
148
|
+
...(latestStatus?.taskTitle
|
|
149
|
+
? [
|
|
150
|
+
`当前运行任务:${latestStatus.taskTitle}`,
|
|
151
|
+
`当前运行目录:${latestStatus.runDir || "unknown"}`,
|
|
152
|
+
`当前运行阶段:${latestStatus.stage || "unknown"}`,
|
|
109
153
|
]
|
|
110
154
|
: []),
|
|
155
|
+
...(runtime?.status
|
|
156
|
+
? [
|
|
157
|
+
`当前引擎状态:${runtime.status}`,
|
|
158
|
+
...(Number.isFinite(Number(runtime.recoveryCount))
|
|
159
|
+
? [`自动恢复次数:${runtime.recoveryCount}`]
|
|
160
|
+
: []),
|
|
161
|
+
]
|
|
162
|
+
: []),
|
|
163
|
+
...(activity?.current?.label
|
|
164
|
+
? [`当前动作:${activity.current.label}`]
|
|
165
|
+
: []),
|
|
166
|
+
...(activity?.todo?.total
|
|
167
|
+
? [`当前待办:${activity.todo.completed}/${activity.todo.total}`]
|
|
168
|
+
: []),
|
|
169
|
+
...(Array.isArray(activity?.activeCommands) && activity.activeCommands[0]?.label
|
|
170
|
+
? [`活动命令:${activity.activeCommands[0].label}`]
|
|
171
|
+
: []),
|
|
172
|
+
...(hostResume?.issue?.label
|
|
173
|
+
? [`宿主续跑:${hostResume.issue.label}`]
|
|
174
|
+
: (hostResume?.supervisorActive ? ["宿主续跑:后台仍在运行,可直接接续观察"] : [])),
|
|
111
175
|
"",
|
|
112
176
|
nextTask ? "下一任务:" : "下一任务:无",
|
|
113
177
|
nextTask ? renderTaskSummary(nextTask) : "",
|
|
178
|
+
"",
|
|
179
|
+
"聚合看板:helloloop dashboard",
|
|
180
|
+
"续跑提示:helloloop resume-host",
|
|
181
|
+
"实时观察:helloloop watch",
|
|
114
182
|
].filter(Boolean).join("\n");
|
|
115
183
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { createActivityProjector } from "./activity_projection.mjs";
|
|
5
|
+
import { appendText, nowIso, writeText } from "./common.mjs";
|
|
5
6
|
import { getEngineDisplayName } from "./engine_metadata.mjs";
|
|
6
7
|
import {
|
|
7
8
|
buildClaudeArgs,
|
|
@@ -53,14 +54,16 @@ export function buildHostLeaseStoppedResult(reason) {
|
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
export function createRuntimeStatusWriter(runtimeStatusFile, baseState) {
|
|
57
|
+
export function createRuntimeStatusWriter(runtimeStatusFile, baseState, onUpdate) {
|
|
57
58
|
return function writeRuntimeStatus(status, extra = {}) {
|
|
58
|
-
|
|
59
|
+
const payload = {
|
|
59
60
|
...baseState,
|
|
60
61
|
...extra,
|
|
61
62
|
status,
|
|
62
63
|
updatedAt: nowIso(),
|
|
63
|
-
}
|
|
64
|
+
};
|
|
65
|
+
writeJson(runtimeStatusFile, payload);
|
|
66
|
+
onUpdate?.(payload);
|
|
64
67
|
};
|
|
65
68
|
}
|
|
66
69
|
|
|
@@ -165,6 +168,16 @@ export async function runEngineAttempt({
|
|
|
165
168
|
}) {
|
|
166
169
|
const attemptPromptFile = path.join(runDir, `${attemptPrefix}-prompt.md`);
|
|
167
170
|
const attemptLastMessageFile = path.join(runDir, `${attemptPrefix}-last-message.txt`);
|
|
171
|
+
const attemptStdoutFile = path.join(runDir, `${attemptPrefix}-stdout.log`);
|
|
172
|
+
const attemptStderrFile = path.join(runDir, `${attemptPrefix}-stderr.log`);
|
|
173
|
+
const activityProjector = createActivityProjector({
|
|
174
|
+
engine,
|
|
175
|
+
phase: executionMode,
|
|
176
|
+
repoRoot: context.repoRoot,
|
|
177
|
+
runDir,
|
|
178
|
+
outputPrefix: attemptPrefix,
|
|
179
|
+
attemptPrefix,
|
|
180
|
+
});
|
|
168
181
|
|
|
169
182
|
if (invocation.error) {
|
|
170
183
|
const result = {
|
|
@@ -181,6 +194,17 @@ export async function runEngineAttempt({
|
|
|
181
194
|
};
|
|
182
195
|
writeText(attemptPromptFile, prompt);
|
|
183
196
|
writeEngineRunArtifacts(runDir, attemptPrefix, result, "");
|
|
197
|
+
activityProjector.onRuntimeStatus({
|
|
198
|
+
status: "failed",
|
|
199
|
+
attemptPrefix,
|
|
200
|
+
activityFile: activityProjector.activityFile,
|
|
201
|
+
activityEventsFile: activityProjector.activityEventsFile,
|
|
202
|
+
});
|
|
203
|
+
activityProjector.finalize({
|
|
204
|
+
status: "failed",
|
|
205
|
+
result,
|
|
206
|
+
finalMessage: "",
|
|
207
|
+
});
|
|
184
208
|
return {
|
|
185
209
|
result,
|
|
186
210
|
finalMessage: "",
|
|
@@ -208,11 +232,16 @@ export async function runEngineAttempt({
|
|
|
208
232
|
recoveryCount,
|
|
209
233
|
recoveryHistory,
|
|
210
234
|
});
|
|
235
|
+
writeText(attemptStdoutFile, "");
|
|
236
|
+
writeText(attemptStderrFile, "");
|
|
211
237
|
|
|
212
238
|
const result = await runChild(invocation.command, finalArgs, {
|
|
213
239
|
cwd: context.repoRoot,
|
|
214
240
|
stdin: prompt,
|
|
215
|
-
env
|
|
241
|
+
env: {
|
|
242
|
+
...(invocation.env || {}),
|
|
243
|
+
...(env || {}),
|
|
244
|
+
},
|
|
216
245
|
shell: invocation.shell,
|
|
217
246
|
heartbeatIntervalMs: recoveryPolicy.heartbeatIntervalSeconds * 1000,
|
|
218
247
|
stallWarningMs: recoveryPolicy.stallWarningSeconds * 1000,
|
|
@@ -224,8 +253,22 @@ export async function runEngineAttempt({
|
|
|
224
253
|
recoveryCount,
|
|
225
254
|
recoveryHistory,
|
|
226
255
|
heartbeat: payload,
|
|
256
|
+
activityFile: activityProjector.activityFile,
|
|
257
|
+
activityEventsFile: activityProjector.activityEventsFile,
|
|
258
|
+
});
|
|
259
|
+
activityProjector.onRuntimeStatus({
|
|
260
|
+
...payload,
|
|
261
|
+
attemptPrefix,
|
|
262
|
+
recoveryCount,
|
|
227
263
|
});
|
|
228
264
|
},
|
|
265
|
+
onStdout(text) {
|
|
266
|
+
appendText(attemptStdoutFile, text);
|
|
267
|
+
activityProjector.onStdoutChunk(text);
|
|
268
|
+
},
|
|
269
|
+
onStderr(text) {
|
|
270
|
+
appendText(attemptStderrFile, text);
|
|
271
|
+
},
|
|
229
272
|
shouldKeepRunning() {
|
|
230
273
|
return isHostLeaseAlive(hostLease);
|
|
231
274
|
},
|
|
@@ -235,6 +278,10 @@ export async function runEngineAttempt({
|
|
|
235
278
|
|
|
236
279
|
writeText(attemptPromptFile, prompt);
|
|
237
280
|
writeEngineRunArtifacts(runDir, attemptPrefix, result, finalMessage);
|
|
281
|
+
activityProjector.finalize({
|
|
282
|
+
result,
|
|
283
|
+
finalMessage,
|
|
284
|
+
});
|
|
238
285
|
|
|
239
286
|
return {
|
|
240
287
|
result,
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { ensureDir, nowIso, tailText, writeJson, writeText } from "./common.mjs";
|
|
4
4
|
import { getEngineDisplayName, normalizeEngineName } from "./engine_metadata.mjs";
|
|
5
5
|
import { resolveEngineInvocation } from "./engine_process_support.mjs";
|
|
6
|
+
import { refreshHostContinuationArtifacts } from "./host_continuation.mjs";
|
|
6
7
|
import { isHostLeaseAlive } from "./host_lease.mjs";
|
|
7
8
|
import {
|
|
8
9
|
buildRuntimeRecoveryPrompt,
|
|
@@ -52,6 +53,12 @@ export async function runEngineTask({
|
|
|
52
53
|
outputPrefix: prefix,
|
|
53
54
|
hardRetryBudget: recoveryPolicy.hardRetryDelaysSeconds.length,
|
|
54
55
|
softRetryBudget: recoveryPolicy.softRetryDelaysSeconds.length,
|
|
56
|
+
}, () => {
|
|
57
|
+
try {
|
|
58
|
+
refreshHostContinuationArtifacts(context);
|
|
59
|
+
} catch {
|
|
60
|
+
// ignore continuation snapshot refresh failures during heartbeat writes
|
|
61
|
+
}
|
|
55
62
|
});
|
|
56
63
|
|
|
57
64
|
const recoveryHistory = [];
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
function normalizeBoolean(value, fallback = false) {
|
|
2
|
+
return typeof value === "boolean" ? value : fallback;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function normalizeNonNegativeInteger(value, fallbackValue) {
|
|
6
|
+
if (value === null || value === undefined || value === "") {
|
|
7
|
+
return fallbackValue;
|
|
8
|
+
}
|
|
9
|
+
const parsed = Number(value);
|
|
10
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
11
|
+
return fallbackValue;
|
|
12
|
+
}
|
|
13
|
+
return Math.floor(parsed);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizePositiveInteger(value, fallbackValue, minimum = 1) {
|
|
17
|
+
const parsed = normalizeNonNegativeInteger(value, fallbackValue);
|
|
18
|
+
if (!Number.isFinite(parsed) || parsed < minimum) {
|
|
19
|
+
return fallbackValue;
|
|
20
|
+
}
|
|
21
|
+
return parsed;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeSecondsList(values, fallbackValues) {
|
|
25
|
+
if (!Array.isArray(values) || !values.length) {
|
|
26
|
+
return [...fallbackValues];
|
|
27
|
+
}
|
|
28
|
+
const normalized = values
|
|
29
|
+
.map((item) => normalizePositiveInteger(item, 0))
|
|
30
|
+
.filter((item) => item > 0);
|
|
31
|
+
return normalized.length ? normalized : [...fallbackValues];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function defaultTerminalConcurrencySettings() {
|
|
35
|
+
return {
|
|
36
|
+
enabled: true,
|
|
37
|
+
visibleMax: 8,
|
|
38
|
+
backgroundMax: 8,
|
|
39
|
+
totalMax: 8,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function defaultObserverRetrySettings() {
|
|
44
|
+
return {
|
|
45
|
+
enabled: true,
|
|
46
|
+
missingPollsBeforeRetry: 3,
|
|
47
|
+
retryDelaysSeconds: [2, 5, 10, 15, 30, 60],
|
|
48
|
+
maxRetryCount: 0,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function defaultSupervisorKeepAliveSettings() {
|
|
53
|
+
return {
|
|
54
|
+
enabled: true,
|
|
55
|
+
restartDelaysSeconds: [2, 5, 10, 15, 30, 60],
|
|
56
|
+
maxRestartCount: 0,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function normalizeTerminalConcurrencySettings(settings = {}) {
|
|
61
|
+
const defaults = defaultTerminalConcurrencySettings();
|
|
62
|
+
return {
|
|
63
|
+
enabled: normalizeBoolean(settings?.enabled, defaults.enabled),
|
|
64
|
+
visibleMax: normalizeNonNegativeInteger(settings?.visibleMax, defaults.visibleMax),
|
|
65
|
+
backgroundMax: normalizeNonNegativeInteger(settings?.backgroundMax, defaults.backgroundMax),
|
|
66
|
+
totalMax: normalizeNonNegativeInteger(settings?.totalMax, defaults.totalMax),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function normalizeObserverRetrySettings(settings = {}) {
|
|
71
|
+
const defaults = defaultObserverRetrySettings();
|
|
72
|
+
return {
|
|
73
|
+
enabled: normalizeBoolean(settings?.enabled, defaults.enabled),
|
|
74
|
+
missingPollsBeforeRetry: normalizePositiveInteger(
|
|
75
|
+
settings?.missingPollsBeforeRetry,
|
|
76
|
+
defaults.missingPollsBeforeRetry,
|
|
77
|
+
),
|
|
78
|
+
retryDelaysSeconds: normalizeSecondsList(settings?.retryDelaysSeconds, defaults.retryDelaysSeconds),
|
|
79
|
+
maxRetryCount: normalizeNonNegativeInteger(settings?.maxRetryCount, defaults.maxRetryCount),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function normalizeSupervisorKeepAliveSettings(settings = {}) {
|
|
84
|
+
const defaults = defaultSupervisorKeepAliveSettings();
|
|
85
|
+
return {
|
|
86
|
+
enabled: normalizeBoolean(settings?.enabled, defaults.enabled),
|
|
87
|
+
restartDelaysSeconds: normalizeSecondsList(settings?.restartDelaysSeconds, defaults.restartDelaysSeconds),
|
|
88
|
+
maxRestartCount: normalizeNonNegativeInteger(settings?.maxRestartCount, defaults.maxRestartCount),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function pickRetryDelaySeconds(delays, attemptNumber) {
|
|
93
|
+
const values = Array.isArray(delays) && delays.length
|
|
94
|
+
? delays.map((item) => normalizePositiveInteger(item, 0)).filter((item) => item > 0)
|
|
95
|
+
: [];
|
|
96
|
+
if (!values.length) {
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
const index = Math.max(0, Math.min(Number(attemptNumber || 1) - 1, values.length - 1));
|
|
100
|
+
return values[index];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function hasRetryBudget(maxRetryCount, nextAttemptNumber) {
|
|
104
|
+
return Number(maxRetryCount || 0) <= 0 || Number(nextAttemptNumber || 0) <= Number(maxRetryCount || 0);
|
|
105
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { loadGlobalConfig } from "./global_config.mjs";
|
|
2
|
+
import {
|
|
3
|
+
normalizeObserverRetrySettings,
|
|
4
|
+
normalizeSupervisorKeepAliveSettings,
|
|
5
|
+
normalizeTerminalConcurrencySettings,
|
|
6
|
+
} from "./runtime_settings.mjs";
|
|
7
|
+
|
|
8
|
+
export function loadRuntimeSettings(options = {}) {
|
|
9
|
+
const globalConfig = loadGlobalConfig({
|
|
10
|
+
globalConfigFile: options.globalConfigFile,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
terminalConcurrency: normalizeTerminalConcurrencySettings(globalConfig?.runtime?.terminalConcurrency || {}),
|
|
15
|
+
observerRetry: normalizeObserverRetrySettings(globalConfig?.runtime?.observerRetry || {}),
|
|
16
|
+
supervisorKeepAlive: normalizeSupervisorKeepAliveSettings(globalConfig?.runtime?.supervisorKeepAlive || {}),
|
|
17
|
+
_meta: globalConfig?._meta || {},
|
|
18
|
+
};
|
|
19
|
+
}
|
package/src/shell_invocation.mjs
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { spawnSync } from "node:child_process";
|
|
2
4
|
|
|
5
|
+
import { resolveWindowsHiddenShellEnvPatch } from "./windows_hidden_shell_proxy.mjs";
|
|
6
|
+
|
|
7
|
+
function buildSyncSpawnOptions(platform = process.platform, extra = {}) {
|
|
8
|
+
return {
|
|
9
|
+
...extra,
|
|
10
|
+
...(platform === "win32" ? { windowsHide: true } : {}),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
3
14
|
function createUnavailableInvocation(message) {
|
|
4
15
|
return {
|
|
5
16
|
command: "",
|
|
@@ -19,15 +30,19 @@ function parseWindowsCommandMatches(output) {
|
|
|
19
30
|
function hasCommand(command, platform = process.platform) {
|
|
20
31
|
if (platform === "win32") {
|
|
21
32
|
const result = spawnSync("where.exe", [command], {
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
...buildSyncSpawnOptions(platform, {
|
|
34
|
+
encoding: "utf8",
|
|
35
|
+
shell: false,
|
|
36
|
+
}),
|
|
24
37
|
});
|
|
25
38
|
return result.status === 0;
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
const result = spawnSync("sh", ["-lc", `command -v ${command}`], {
|
|
29
|
-
|
|
30
|
-
|
|
42
|
+
...buildSyncSpawnOptions(platform, {
|
|
43
|
+
encoding: "utf8",
|
|
44
|
+
shell: false,
|
|
45
|
+
}),
|
|
31
46
|
});
|
|
32
47
|
return result.status === 0;
|
|
33
48
|
}
|
|
@@ -35,8 +50,10 @@ function hasCommand(command, platform = process.platform) {
|
|
|
35
50
|
function findWindowsCommandPaths(command, resolver) {
|
|
36
51
|
const lookup = resolver || ((name) => {
|
|
37
52
|
const result = spawnSync("where.exe", [name], {
|
|
38
|
-
|
|
39
|
-
|
|
53
|
+
...buildSyncSpawnOptions("win32", {
|
|
54
|
+
encoding: "utf8",
|
|
55
|
+
shell: false,
|
|
56
|
+
}),
|
|
40
57
|
});
|
|
41
58
|
return result.status === 0 ? parseWindowsCommandMatches(result.stdout) : [];
|
|
42
59
|
});
|
|
@@ -125,6 +142,14 @@ function isCmdLikeExecutable(executable) {
|
|
|
125
142
|
return /\.(cmd|bat)$/i.test(String(executable || ""));
|
|
126
143
|
}
|
|
127
144
|
|
|
145
|
+
function trimOuterQuotes(value) {
|
|
146
|
+
return String(value || "").trim().replace(/^"(.*)"$/u, "$1");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function uniqueNonEmpty(values) {
|
|
150
|
+
return [...new Set(values.map((item) => trimOuterQuotes(item)).filter(Boolean))];
|
|
151
|
+
}
|
|
152
|
+
|
|
128
153
|
function resolveWindowsNamedExecutable(toolName, options = {}) {
|
|
129
154
|
const explicitExecutable = String(options.explicitExecutable || "").trim();
|
|
130
155
|
const findCommandPaths = options.findCommandPaths || ((command) => findWindowsCommandPaths(command));
|
|
@@ -133,7 +158,7 @@ function resolveWindowsNamedExecutable(toolName, options = {}) {
|
|
|
133
158
|
return explicitExecutable;
|
|
134
159
|
}
|
|
135
160
|
|
|
136
|
-
const searchOrder = [`${toolName}.
|
|
161
|
+
const searchOrder = [`${toolName}.exe`, `${toolName}.ps1`, toolName];
|
|
137
162
|
for (const query of searchOrder) {
|
|
138
163
|
const safeMatch = findCommandPaths(query).find((candidate) => !isCmdLikeExecutable(candidate));
|
|
139
164
|
if (safeMatch) {
|
|
@@ -144,6 +169,189 @@ function resolveWindowsNamedExecutable(toolName, options = {}) {
|
|
|
144
169
|
return "";
|
|
145
170
|
}
|
|
146
171
|
|
|
172
|
+
function resolveNodeHostForWrapper(wrapperPath) {
|
|
173
|
+
const wrapperDir = path.dirname(wrapperPath);
|
|
174
|
+
const bundledNode = path.join(wrapperDir, "node.exe");
|
|
175
|
+
if (fs.existsSync(bundledNode)) {
|
|
176
|
+
return bundledNode;
|
|
177
|
+
}
|
|
178
|
+
return process.execPath || "node";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function getUpdatedWindowsPath(newDirs, basePath = process.env.PATH || "") {
|
|
182
|
+
const existing = String(basePath || "")
|
|
183
|
+
.split(";")
|
|
184
|
+
.map((item) => trimOuterQuotes(item))
|
|
185
|
+
.filter(Boolean);
|
|
186
|
+
return uniqueNonEmpty([...newDirs, ...existing]).join(";");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function resolveCodexWindowsTargetTriple() {
|
|
190
|
+
if (process.arch === "arm64") {
|
|
191
|
+
return {
|
|
192
|
+
packageName: "@openai/codex-win32-arm64",
|
|
193
|
+
targetTriple: "aarch64-pc-windows-msvc",
|
|
194
|
+
managedEnvKey: "CODEX_MANAGED_BY_NPM",
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (process.arch === "x64") {
|
|
199
|
+
return {
|
|
200
|
+
packageName: "@openai/codex-win32-x64",
|
|
201
|
+
targetTriple: "x86_64-pc-windows-msvc",
|
|
202
|
+
managedEnvKey: "CODEX_MANAGED_BY_NPM",
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function resolveWindowsNativeCodexInvocation(options = {}) {
|
|
210
|
+
const platform = options.platform || process.platform;
|
|
211
|
+
if (platform !== "win32") {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const target = resolveCodexWindowsTargetTriple();
|
|
216
|
+
if (!target) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const explicitExecutable = trimOuterQuotes(options.explicitExecutable || "");
|
|
221
|
+
if (explicitExecutable && /\.exe$/iu.test(explicitExecutable) && fs.existsSync(explicitExecutable)) {
|
|
222
|
+
return {
|
|
223
|
+
command: explicitExecutable,
|
|
224
|
+
argsPrefix: [],
|
|
225
|
+
shell: false,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const findCommandPaths = options.findCommandPaths || ((command) => findWindowsCommandPaths(command));
|
|
230
|
+
const wrapperCandidates = [];
|
|
231
|
+
|
|
232
|
+
if (explicitExecutable) {
|
|
233
|
+
if (fs.existsSync(explicitExecutable)) {
|
|
234
|
+
wrapperCandidates.push(explicitExecutable);
|
|
235
|
+
} else {
|
|
236
|
+
wrapperCandidates.push(...findCommandPaths(explicitExecutable));
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
wrapperCandidates.push(...findCommandPaths("codex.ps1"));
|
|
240
|
+
wrapperCandidates.push(...findCommandPaths("codex"));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
for (const wrapperPath of uniqueNonEmpty(wrapperCandidates)) {
|
|
244
|
+
const wrapperDir = path.dirname(wrapperPath);
|
|
245
|
+
const codexPackageRoot = path.join(wrapperDir, "node_modules", "@openai", "codex");
|
|
246
|
+
const vendorRoot = path.join(
|
|
247
|
+
codexPackageRoot,
|
|
248
|
+
"node_modules",
|
|
249
|
+
target.packageName,
|
|
250
|
+
"vendor",
|
|
251
|
+
target.targetTriple,
|
|
252
|
+
);
|
|
253
|
+
const binaryPath = path.join(vendorRoot, "codex", "codex.exe");
|
|
254
|
+
if (!fs.existsSync(binaryPath)) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const rgPathDir = path.join(vendorRoot, "path");
|
|
258
|
+
const envPatch = {
|
|
259
|
+
[target.managedEnvKey]: "1",
|
|
260
|
+
};
|
|
261
|
+
if (fs.existsSync(rgPathDir)) {
|
|
262
|
+
envPatch.PATH = getUpdatedWindowsPath([rgPathDir], process.env.PATH || "");
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
command: binaryPath,
|
|
266
|
+
argsPrefix: [],
|
|
267
|
+
shell: false,
|
|
268
|
+
env: envPatch,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function attachWindowsHiddenShellProxy(invocation, options = {}) {
|
|
276
|
+
if (!invocation || (options.platform || process.platform) !== "win32") {
|
|
277
|
+
return invocation;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const envPatch = resolveWindowsHiddenShellEnvPatch({
|
|
282
|
+
basePath: invocation.env?.PATH || process.env.PATH || "",
|
|
283
|
+
});
|
|
284
|
+
if (!envPatch || !Object.keys(envPatch).length) {
|
|
285
|
+
return invocation;
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
...invocation,
|
|
289
|
+
env: {
|
|
290
|
+
...(invocation.env || {}),
|
|
291
|
+
...envPatch,
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
} catch (error) {
|
|
295
|
+
return {
|
|
296
|
+
...invocation,
|
|
297
|
+
error: [
|
|
298
|
+
String(invocation.error || "").trim(),
|
|
299
|
+
`HelloLoop 无法准备 Windows 隐藏 shell 代理:${String(error?.message || error || "未知错误")}`,
|
|
300
|
+
].filter(Boolean).join("\n"),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function resolveWindowsNodePackageInvocation(toolName, packageScriptSegments, options = {}) {
|
|
306
|
+
const platform = options.platform || process.platform;
|
|
307
|
+
if (platform !== "win32") {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const explicitExecutable = trimOuterQuotes(options.explicitExecutable || "");
|
|
312
|
+
if (explicitExecutable && /\.m?js$/iu.test(explicitExecutable) && fs.existsSync(explicitExecutable)) {
|
|
313
|
+
return {
|
|
314
|
+
command: process.execPath || "node",
|
|
315
|
+
argsPrefix: [explicitExecutable],
|
|
316
|
+
shell: false,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const findCommandPaths = options.findCommandPaths || ((command) => findWindowsCommandPaths(command));
|
|
321
|
+
const wrapperCandidates = [];
|
|
322
|
+
|
|
323
|
+
if (explicitExecutable) {
|
|
324
|
+
if (fs.existsSync(explicitExecutable)) {
|
|
325
|
+
wrapperCandidates.push(explicitExecutable);
|
|
326
|
+
} else {
|
|
327
|
+
wrapperCandidates.push(...findCommandPaths(explicitExecutable));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!wrapperCandidates.length) {
|
|
332
|
+
wrapperCandidates.push(...findCommandPaths(`${toolName}.ps1`));
|
|
333
|
+
wrapperCandidates.push(...findCommandPaths(`${toolName}.exe`));
|
|
334
|
+
wrapperCandidates.push(...findCommandPaths(toolName));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
for (const wrapperPath of uniqueNonEmpty(wrapperCandidates)) {
|
|
338
|
+
if (isCmdLikeExecutable(wrapperPath) || !fs.existsSync(wrapperPath)) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const packageScript = path.join(path.dirname(wrapperPath), "node_modules", ...packageScriptSegments);
|
|
342
|
+
if (!fs.existsSync(packageScript)) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
command: resolveNodeHostForWrapper(wrapperPath),
|
|
347
|
+
argsPrefix: [packageScript],
|
|
348
|
+
shell: false,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
147
355
|
export function resolveVerifyShellInvocation(options = {}) {
|
|
148
356
|
const platform = options.platform || process.platform;
|
|
149
357
|
|
|
@@ -217,11 +425,21 @@ export function resolveCliInvocation(options = {}) {
|
|
|
217
425
|
}
|
|
218
426
|
|
|
219
427
|
export function resolveCodexInvocation(options = {}) {
|
|
220
|
-
|
|
428
|
+
const nativeCodexInvocation = resolveWindowsNativeCodexInvocation(options);
|
|
429
|
+
if (nativeCodexInvocation) {
|
|
430
|
+
return attachWindowsHiddenShellProxy(nativeCodexInvocation, options);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const nodePackageInvocation = resolveWindowsNodePackageInvocation("codex", ["@openai", "codex", "bin", "codex.js"], options);
|
|
434
|
+
if (nodePackageInvocation) {
|
|
435
|
+
return attachWindowsHiddenShellProxy(nodePackageInvocation, options);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return attachWindowsHiddenShellProxy(resolveCliInvocation({
|
|
221
439
|
...options,
|
|
222
440
|
commandName: "codex",
|
|
223
441
|
toolDisplayName: "Codex",
|
|
224
|
-
});
|
|
442
|
+
}), options);
|
|
225
443
|
}
|
|
226
444
|
|
|
227
445
|
export function resolveClaudeInvocation(options = {}) {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { launchSupervisedCommand, renderSupervisorLaunchSummary } from "./supervisor_runtime.mjs";
|
|
2
|
+
import { watchSupervisorSessionWithRecovery } from "./supervisor_watch.mjs";
|
|
3
|
+
|
|
4
|
+
export function shouldUseSupervisor(options = {}) {
|
|
5
|
+
return !options.dryRun
|
|
6
|
+
&& process.env.HELLOLOOP_SUPERVISOR_ACTIVE !== "1";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function shouldAutoWatchSupervisor(options = {}) {
|
|
10
|
+
if (options.detach) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
if (options.watch === true) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
if (options.watch === false) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return Boolean(process.stdout.isTTY);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function launchAndMaybeWatchSupervisedCommand(context, command, options = {}) {
|
|
23
|
+
const session = launchSupervisedCommand(context, command, options);
|
|
24
|
+
console.log(renderSupervisorLaunchSummary(session));
|
|
25
|
+
|
|
26
|
+
if (!shouldAutoWatchSupervisor(options)) {
|
|
27
|
+
console.log("- 已切换为后台执行;可稍后运行 `helloloop watch` 或 `helloloop status` 查看进度。");
|
|
28
|
+
return {
|
|
29
|
+
detached: true,
|
|
30
|
+
exitCode: 0,
|
|
31
|
+
ok: true,
|
|
32
|
+
session,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log("- 已进入附着观察模式;按 Ctrl+C 仅退出观察,不会停止后台任务。");
|
|
37
|
+
const watchResult = await watchSupervisorSessionWithRecovery(context, {
|
|
38
|
+
sessionId: session.sessionId,
|
|
39
|
+
pollMs: options.watchPollMs,
|
|
40
|
+
globalConfigFile: options.globalConfigFile,
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
detached: false,
|
|
44
|
+
exitCode: watchResult.exitCode,
|
|
45
|
+
ok: watchResult.ok,
|
|
46
|
+
session,
|
|
47
|
+
watchResult,
|
|
48
|
+
};
|
|
49
|
+
}
|