helloloop 0.9.1 → 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.
Files changed (50) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +230 -506
  3. package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
  4. package/hosts/gemini/extension/gemini-extension.json +1 -1
  5. package/native/windows-hidden-shell-proxy/HelloLoopHiddenShellProxy.csproj +11 -0
  6. package/native/windows-hidden-shell-proxy/Program.cs +498 -0
  7. package/package.json +4 -2
  8. package/src/activity_projection.mjs +294 -0
  9. package/src/analyze_confirmation.mjs +3 -1
  10. package/src/analyzer.mjs +2 -1
  11. package/src/auto_execution_options.mjs +13 -0
  12. package/src/background_launch.mjs +73 -0
  13. package/src/cli.mjs +49 -1
  14. package/src/cli_analyze_command.mjs +9 -5
  15. package/src/cli_args.mjs +102 -37
  16. package/src/cli_command_handlers.mjs +44 -4
  17. package/src/cli_support.mjs +2 -0
  18. package/src/dashboard_command.mjs +371 -0
  19. package/src/dashboard_tui.mjs +289 -0
  20. package/src/dashboard_web.mjs +351 -0
  21. package/src/dashboard_web_client.mjs +167 -0
  22. package/src/dashboard_web_page.mjs +49 -0
  23. package/src/engine_event_parser_codex.mjs +167 -0
  24. package/src/engine_process_support.mjs +1 -0
  25. package/src/engine_selection.mjs +24 -0
  26. package/src/engine_selection_probe.mjs +10 -6
  27. package/src/engine_selection_settings.mjs +12 -19
  28. package/src/execution_interactivity.mjs +12 -0
  29. package/src/host_continuation.mjs +305 -0
  30. package/src/install_codex.mjs +20 -8
  31. package/src/install_shared.mjs +9 -0
  32. package/src/node_process_launch.mjs +28 -0
  33. package/src/process.mjs +2 -0
  34. package/src/runner_execute_task.mjs +4 -0
  35. package/src/runner_execution_support.mjs +69 -3
  36. package/src/runner_once.mjs +4 -0
  37. package/src/runner_status.mjs +63 -7
  38. package/src/runtime_engine_support.mjs +41 -4
  39. package/src/runtime_engine_task.mjs +7 -0
  40. package/src/runtime_settings.mjs +105 -0
  41. package/src/runtime_settings_loader.mjs +19 -0
  42. package/src/shell_invocation.mjs +227 -9
  43. package/src/supervisor_cli_support.mjs +3 -2
  44. package/src/supervisor_guardian.mjs +307 -0
  45. package/src/supervisor_runtime.mjs +138 -82
  46. package/src/supervisor_state.mjs +64 -0
  47. package/src/supervisor_watch.mjs +92 -48
  48. package/src/terminal_session_limits.mjs +1 -21
  49. package/src/windows_hidden_shell_proxy.mjs +405 -0
  50. package/src/workspace_registry.mjs +155 -0
@@ -0,0 +1,64 @@
1
+ import { nowIso, readJson, writeJson } from "./common.mjs";
2
+
3
+ export const ACTIVE_SUPERVISOR_STATUSES = new Set(["launching", "running"]);
4
+ export const FINAL_SUPERVISOR_STATUSES = new Set(["completed", "failed", "stopped"]);
5
+
6
+ export function readJsonIfExists(filePath) {
7
+ try {
8
+ return filePath ? readJson(filePath) : null;
9
+ } catch {
10
+ return null;
11
+ }
12
+ }
13
+
14
+ export function isTrackedPidAlive(pid) {
15
+ const numberPid = Number(pid || 0);
16
+ if (!Number.isFinite(numberPid) || numberPid <= 0) {
17
+ return false;
18
+ }
19
+ try {
20
+ process.kill(numberPid, 0);
21
+ return true;
22
+ } catch (error) {
23
+ return String(error?.code || "") === "EPERM";
24
+ }
25
+ }
26
+
27
+ function buildSupervisorState(context, patch = {}) {
28
+ const current = readSupervisorState(context) || {};
29
+ return {
30
+ ...current,
31
+ ...patch,
32
+ updatedAt: nowIso(),
33
+ };
34
+ }
35
+
36
+ export function readSupervisorState(context) {
37
+ return readJsonIfExists(context.supervisorStateFile);
38
+ }
39
+
40
+ export function hasActiveSupervisor(context) {
41
+ const state = readSupervisorState(context);
42
+ const activePid = Number(state?.guardianPid || state?.pid || 0);
43
+ return Boolean(
44
+ state?.status
45
+ && ACTIVE_SUPERVISOR_STATUSES.has(String(state.status))
46
+ && isTrackedPidAlive(activePid),
47
+ );
48
+ }
49
+
50
+ export function writeSupervisorState(context, patch) {
51
+ writeJson(context.supervisorStateFile, buildSupervisorState(context, patch));
52
+ }
53
+
54
+ export function writeActiveSupervisorState(context, patch = {}) {
55
+ const nextPatch = {
56
+ ...patch,
57
+ exitCode: null,
58
+ completedAt: "",
59
+ };
60
+ if (!Object.hasOwn(nextPatch, "message")) {
61
+ nextPatch.message = "";
62
+ }
63
+ writeSupervisorState(context, nextPatch);
64
+ }
@@ -1,22 +1,16 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
- import { fileExists, readJson, sleep } from "./common.mjs";
4
+ import {
5
+ readJsonIfExists,
6
+ selectLatestActivityFile,
7
+ selectLatestRuntimeFile,
8
+ } from "./activity_projection.mjs";
9
+ import { fileExists, sleep } from "./common.mjs";
5
10
  import { renderHostLeaseLabel } from "./host_lease.mjs";
6
-
7
- const FINAL_STATUSES = new Set(["completed", "failed", "stopped"]);
8
-
9
- function readJsonIfExists(filePath) {
10
- if (!filePath || !fileExists(filePath)) {
11
- return null;
12
- }
13
-
14
- try {
15
- return readJson(filePath);
16
- } catch {
17
- return null;
18
- }
19
- }
11
+ import { loadRuntimeSettings } from "./runtime_settings_loader.mjs";
12
+ import { hasRetryBudget, pickRetryDelaySeconds } from "./runtime_settings.mjs";
13
+ import { FINAL_SUPERVISOR_STATUSES } from "./supervisor_state.mjs";
20
14
 
21
15
  function writeLine(stream, message) {
22
16
  stream.write(`${message}\n`);
@@ -65,37 +59,6 @@ function formatTaskStatus(status) {
65
59
  return lines.join("\n");
66
60
  }
67
61
 
68
- function selectRuntimeFile(runDir) {
69
- if (!runDir || !fileExists(runDir)) {
70
- return "";
71
- }
72
-
73
- const candidates = [];
74
- for (const entry of fs.readdirSync(runDir, { withFileTypes: true })) {
75
- if (entry.isFile() && entry.name.endsWith("-runtime.json")) {
76
- candidates.push(path.join(runDir, entry.name));
77
- continue;
78
- }
79
- if (!entry.isDirectory()) {
80
- continue;
81
- }
82
- const nestedDir = path.join(runDir, entry.name);
83
- for (const nestedName of fs.readdirSync(nestedDir)) {
84
- if (nestedName.endsWith("-runtime.json")) {
85
- candidates.push(path.join(nestedDir, nestedName));
86
- }
87
- }
88
- }
89
-
90
- candidates.sort((left, right) => {
91
- const rightTime = fs.statSync(right).mtimeMs;
92
- const leftTime = fs.statSync(left).mtimeMs;
93
- return rightTime - leftTime;
94
- });
95
-
96
- return candidates[0] || "";
97
- }
98
-
99
62
  function formatRuntimeState(runtime, previousRuntime) {
100
63
  if (!runtime?.status) {
101
64
  return "";
@@ -161,6 +124,44 @@ function formatRuntimeState(runtime, previousRuntime) {
161
124
  return `[HelloLoop watch] ${label}${details ? ` | ${details}` : ""}`;
162
125
  }
163
126
 
127
+ function formatActivityState(activity, previousActivity) {
128
+ if (!activity?.current?.label && !activity?.todo?.total) {
129
+ return "";
130
+ }
131
+
132
+ const activeCommandLabel = Array.isArray(activity?.activeCommands) ? activity.activeCommands[0]?.label || "" : "";
133
+ const signature = [
134
+ activity.current?.status || "",
135
+ activity.current?.label || "",
136
+ activity.todo?.completed || 0,
137
+ activity.todo?.total || 0,
138
+ activeCommandLabel,
139
+ ].join("|");
140
+ const previousSignature = previousActivity
141
+ ? [
142
+ previousActivity.current?.status || "",
143
+ previousActivity.current?.label || "",
144
+ previousActivity.todo?.completed || 0,
145
+ previousActivity.todo?.total || 0,
146
+ Array.isArray(previousActivity?.activeCommands) ? previousActivity.activeCommands[0]?.label || "" : "",
147
+ ].join("|")
148
+ : "";
149
+
150
+ if (signature === previousSignature) {
151
+ return "";
152
+ }
153
+
154
+ const details = [];
155
+ if (activity.todo?.total) {
156
+ details.push(`todo=${activity.todo.completed}/${activity.todo.total}`);
157
+ }
158
+ if (activeCommandLabel) {
159
+ details.push(`cmd=${activeCommandLabel}`);
160
+ }
161
+
162
+ return `[HelloLoop watch] 当前动作:${activity.current?.label || "等待事件"}${details.length ? ` | ${details.join(" | ")}` : ""}`;
163
+ }
164
+
164
165
  function readTextDelta(filePath, offset) {
165
166
  if (!filePath || !fileExists(filePath)) {
166
167
  return { nextOffset: 0, text: "" };
@@ -244,6 +245,7 @@ export async function watchSupervisorSession(context, options = {}) {
244
245
  let lastSupervisorSignature = "";
245
246
  let lastTaskSignature = "";
246
247
  let previousRuntime = null;
248
+ let previousActivity = null;
247
249
  let missingPolls = 0;
248
250
 
249
251
  while (true) {
@@ -251,8 +253,12 @@ export async function watchSupervisorSession(context, options = {}) {
251
253
  const result = readJsonIfExists(context.supervisorResultFile);
252
254
  const activeSessionId = expectedSessionId || supervisor?.sessionId || result?.sessionId || "";
253
255
  const taskStatus = resolveStatusForSession(readJsonIfExists(context.statusFile), activeSessionId);
254
- const runtimeFile = taskStatus?.runDir ? selectRuntimeFile(taskStatus.runDir) : "";
256
+ const runtimeFile = taskStatus?.runDir ? selectLatestRuntimeFile(taskStatus.runDir) : "";
255
257
  const runtime = readJsonIfExists(runtimeFile);
258
+ const activityFile = runtime?.activityFile && fileExists(runtime.activityFile)
259
+ ? runtime.activityFile
260
+ : (taskStatus?.runDir ? selectLatestActivityFile(taskStatus.runDir, runtime?.attemptPrefix || "") : "");
261
+ const activity = readJsonIfExists(activityFile);
256
262
 
257
263
  if (!supervisor && !result) {
258
264
  missingPolls += 1;
@@ -296,6 +302,11 @@ export async function watchSupervisorSession(context, options = {}) {
296
302
  writeLine(stdoutStream, runtimeMessage);
297
303
  }
298
304
  previousRuntime = runtime || previousRuntime;
305
+ const activityMessage = formatActivityState(activity, previousActivity);
306
+ if (activityMessage) {
307
+ writeLine(stdoutStream, activityMessage);
308
+ }
309
+ previousActivity = activity || previousActivity;
299
310
 
300
311
  const activePrefix = runtime?.attemptPrefix || "";
301
312
  const runtimeDir = runtimeFile ? path.dirname(runtimeFile) : "";
@@ -309,7 +320,7 @@ export async function watchSupervisorSession(context, options = {}) {
309
320
  readAndWriteDelta(stdoutCursor, stdoutFile, stdoutStream);
310
321
  readAndWriteDelta(stderrCursor, stderrFile, stderrStream);
311
322
 
312
- if (supervisor?.status && FINAL_STATUSES.has(String(supervisor.status))) {
323
+ if (supervisor?.status && FINAL_SUPERVISOR_STATUSES.has(String(supervisor.status))) {
313
324
  readAndWriteDelta(stdoutCursor, stdoutFile, stdoutStream);
314
325
  readAndWriteDelta(stderrCursor, stderrFile, stderrStream);
315
326
  return buildWatchResult(supervisor, result);
@@ -318,3 +329,36 @@ export async function watchSupervisorSession(context, options = {}) {
318
329
  await sleep(pollMs);
319
330
  }
320
331
  }
332
+
333
+ function formatRetryAttachMessage(options, attemptNumber, delaySeconds) {
334
+ const sessionLabel = String(options.sessionId || "").trim();
335
+ const targetLabel = sessionLabel
336
+ ? `后台会话 ${sessionLabel}`
337
+ : "后台会话";
338
+ return `[HelloLoop watch] 暂未检测到可附着的${targetLabel},将在 ${delaySeconds} 秒后自动重试(第 ${attemptNumber} 次)。`;
339
+ }
340
+
341
+ export async function watchSupervisorSessionWithRecovery(context, options = {}) {
342
+ const observerRetry = loadRuntimeSettings({
343
+ globalConfigFile: options.globalConfigFile,
344
+ }).observerRetry;
345
+ const stdoutStream = options.stdoutStream || process.stdout;
346
+ let retryCount = 0;
347
+
348
+ while (true) {
349
+ const result = await watchSupervisorSession(context, options);
350
+ if (!result.empty) {
351
+ return result;
352
+ }
353
+
354
+ const nextAttempt = retryCount + 1;
355
+ if (!observerRetry.enabled || !hasRetryBudget(observerRetry.maxRetryCount, nextAttempt)) {
356
+ return result;
357
+ }
358
+
359
+ const delaySeconds = pickRetryDelaySeconds(observerRetry.retryDelaysSeconds, nextAttempt);
360
+ writeLine(stdoutStream, formatRetryAttachMessage(options, nextAttempt, delaySeconds));
361
+ retryCount = nextAttempt;
362
+ await sleep(delaySeconds * 1000);
363
+ }
364
+ }
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
 
4
4
  import { ensureDir, fileExists, nowIso, readJson, timestampForFile, writeJson } from "./common.mjs";
5
5
  import { loadGlobalConfig } from "./global_config.mjs";
6
+ import { normalizeTerminalConcurrencySettings } from "./runtime_settings.mjs";
6
7
 
7
8
  const SESSION_DIR_NAME = "terminal-sessions";
8
9
  const RUNTIME_DIR_NAME = "runtime";
@@ -33,27 +34,6 @@ function isPidAlive(pid) {
33
34
  }
34
35
  }
35
36
 
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
37
  function resolveTerminalRuntimeConfig(options = {}) {
58
38
  const globalConfig = loadGlobalConfig({
59
39
  globalConfigFile: options.globalConfigFile,
@@ -0,0 +1,405 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ import { ensureDir, fileExists, readText, writeJson } from "./common.mjs";
9
+
10
+ const REAL_PWSH_ENV = "HELLOLOOP_REAL_PWSH";
11
+ const REAL_POWERSHELL_ENV = "HELLOLOOP_REAL_POWERSHELL";
12
+ const ORIGINAL_PATH_ENV = "HELLOLOOP_ORIGINAL_PATH";
13
+ const PROXY_ENABLED_ENV = "HELLOLOOP_HIDDEN_SHELL_PROXY_ENABLED";
14
+ export const HIDDEN_PROCESS_PROXY_TARGET_ENV = "HELLOLOOP_PROXY_TARGET_EXE";
15
+ const PROXY_METADATA_FILE = "metadata.json";
16
+ const PROXY_ALIASES = ["pwsh.exe", "powershell.exe"];
17
+ const PROXY_EXECUTABLE = "HelloLoopHiddenShellProxy.exe";
18
+ const WINDOWS_SYSTEM_ROOT = process.env.SystemRoot || "C:\\Windows";
19
+ const WHERE_EXE = path.join(WINDOWS_SYSTEM_ROOT, "System32", "where.exe");
20
+
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = path.dirname(__filename);
23
+ const toolRoot = path.resolve(__dirname, "..");
24
+ const proxySourceRoot = path.join(toolRoot, "native", "windows-hidden-shell-proxy");
25
+ const proxyProjectFile = path.join(proxySourceRoot, "HelloLoopHiddenShellProxy.csproj");
26
+ const proxyProgramFile = path.join(proxySourceRoot, "Program.cs");
27
+
28
+ function trimOuterQuotes(value) {
29
+ return String(value || "").trim().replace(/^"(.*)"$/u, "$1");
30
+ }
31
+
32
+ function splitWindowsPath(value) {
33
+ return String(value || "")
34
+ .split(";")
35
+ .map((item) => trimOuterQuotes(item))
36
+ .filter(Boolean);
37
+ }
38
+
39
+ function uniqueNonEmpty(values) {
40
+ return [...new Set(values.map((item) => trimOuterQuotes(item)).filter(Boolean))];
41
+ }
42
+
43
+ function buildWindowsPath(frontDirs, basePath) {
44
+ return uniqueNonEmpty([
45
+ ...frontDirs,
46
+ ...splitWindowsPath(basePath),
47
+ ]).join(";");
48
+ }
49
+
50
+ function parseWhereOutput(output) {
51
+ return String(output || "")
52
+ .split(/\r?\n/)
53
+ .map((line) => trimOuterQuotes(line))
54
+ .filter(Boolean);
55
+ }
56
+
57
+ function sleepSync(ms) {
58
+ const shared = new SharedArrayBuffer(4);
59
+ const view = new Int32Array(shared);
60
+ Atomics.wait(view, 0, 0, Math.max(0, ms));
61
+ }
62
+
63
+ function isManagedProxyPath(candidate) {
64
+ return String(candidate || "")
65
+ .replaceAll("/", "\\")
66
+ .toLowerCase()
67
+ .includes("\\.helloloop\\runtime\\windows-hidden-shell\\");
68
+ }
69
+
70
+ function resolveShellFallbacks() {
71
+ const programFiles = process.env.ProgramW6432 || process.env.ProgramFiles || "";
72
+ const systemRoot = process.env.SystemRoot || "C:\\Windows";
73
+ return {
74
+ pwsh: [
75
+ path.join(programFiles, "PowerShell", "7", "pwsh.exe"),
76
+ path.join(programFiles, "PowerShell", "7-preview", "pwsh.exe"),
77
+ ],
78
+ powershell: [
79
+ path.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
80
+ ],
81
+ };
82
+ }
83
+
84
+ function findExecutableInWindowsPath(executableName, lookupPath) {
85
+ const result = spawnSync(WHERE_EXE, [executableName], {
86
+ encoding: "utf8",
87
+ shell: false,
88
+ windowsHide: true,
89
+ env: {
90
+ ...process.env,
91
+ PATH: lookupPath || process.env.PATH || "",
92
+ },
93
+ });
94
+ if (result.status !== 0) {
95
+ return "";
96
+ }
97
+ return parseWhereOutput(result.stdout).find((candidate) => !isManagedProxyPath(candidate) && fs.existsSync(candidate)) || "";
98
+ }
99
+
100
+ function resolveDotnetExecutable() {
101
+ const candidates = uniqueNonEmpty([
102
+ process.env.HELLOLOOP_DOTNET_EXECUTABLE,
103
+ process.env.DOTNET_ROOT ? path.join(process.env.DOTNET_ROOT, "dotnet.exe") : "",
104
+ process.env.ProgramFiles ? path.join(process.env.ProgramFiles, "dotnet", "dotnet.exe") : "",
105
+ process.env["ProgramFiles(x86)"] ? path.join(process.env["ProgramFiles(x86)"], "dotnet", "dotnet.exe") : "",
106
+ findExecutableInWindowsPath("dotnet.exe", process.env.PATH || ""),
107
+ ]);
108
+ return candidates.find((candidate) => fileExists(candidate)) || "";
109
+ }
110
+
111
+ function resolveRealWindowsShells(lookupPath) {
112
+ const fallbacks = resolveShellFallbacks();
113
+ const pwsh = findExecutableInWindowsPath("pwsh.exe", lookupPath)
114
+ || fallbacks.pwsh.find((candidate) => fileExists(candidate))
115
+ || "";
116
+ const powershell = findExecutableInWindowsPath("powershell.exe", lookupPath)
117
+ || fallbacks.powershell.find((candidate) => fileExists(candidate))
118
+ || "";
119
+
120
+ return {
121
+ pwsh,
122
+ powershell,
123
+ };
124
+ }
125
+
126
+ function resolveSourceHash() {
127
+ const hash = crypto.createHash("sha256");
128
+ hash.update(readText(path.join(toolRoot, "package.json")));
129
+ hash.update(readText(proxyProjectFile));
130
+ hash.update(readText(proxyProgramFile));
131
+ hash.update(process.arch);
132
+ return hash.digest("hex").slice(0, 16);
133
+ }
134
+
135
+ function resolveRuntimeRoot() {
136
+ return path.join(
137
+ os.homedir(),
138
+ ".helloloop",
139
+ "runtime",
140
+ "windows-hidden-shell",
141
+ `${process.arch}-${resolveSourceHash()}`,
142
+ );
143
+ }
144
+
145
+ function resolveProxyBinDir(runtimeRoot) {
146
+ return path.join(runtimeRoot, "bin");
147
+ }
148
+
149
+ function resolveProxyMetadataFile(runtimeRoot) {
150
+ return path.join(runtimeRoot, PROXY_METADATA_FILE);
151
+ }
152
+
153
+ function isProxyReady(runtimeRoot) {
154
+ const metadataFile = resolveProxyMetadataFile(runtimeRoot);
155
+ if (!fileExists(metadataFile)) {
156
+ return false;
157
+ }
158
+
159
+ try {
160
+ const metadata = JSON.parse(readText(metadataFile));
161
+ if (metadata.sourceHash !== resolveSourceHash()) {
162
+ return false;
163
+ }
164
+ } catch {
165
+ return false;
166
+ }
167
+
168
+ const binDir = resolveProxyBinDir(runtimeRoot);
169
+ return PROXY_ALIASES.every((fileName) => fileExists(path.join(binDir, fileName)));
170
+ }
171
+
172
+ function writeProxyMetadata(runtimeRoot, realShells, buildTool) {
173
+ writeJson(resolveProxyMetadataFile(runtimeRoot), {
174
+ schemaVersion: 1,
175
+ sourceHash: resolveSourceHash(),
176
+ builtAt: new Date().toISOString(),
177
+ buildTool,
178
+ realShells,
179
+ });
180
+ }
181
+
182
+ function finalizeBuiltProxy(runtimeRoot, realShells, buildTool) {
183
+ const binDir = resolveProxyBinDir(runtimeRoot);
184
+ const baseExe = path.join(binDir, PROXY_EXECUTABLE);
185
+ if (!fileExists(baseExe)) {
186
+ throw new Error(`未生成隐藏 shell 代理可执行文件:${baseExe}`);
187
+ }
188
+
189
+ for (const alias of PROXY_ALIASES) {
190
+ fs.copyFileSync(baseExe, path.join(binDir, alias));
191
+ }
192
+
193
+ writeProxyMetadata(runtimeRoot, realShells, buildTool);
194
+ }
195
+
196
+ function resolveDotnetSdkExecutable() {
197
+ const dotnetExecutable = resolveDotnetExecutable();
198
+ if (!dotnetExecutable) {
199
+ return "";
200
+ }
201
+ const result = spawnSync(dotnetExecutable, ["--list-sdks"], {
202
+ encoding: "utf8",
203
+ shell: false,
204
+ windowsHide: true,
205
+ });
206
+ if (result.status !== 0 || !String(result.stdout || "").trim()) {
207
+ return "";
208
+ }
209
+ return dotnetExecutable;
210
+ }
211
+
212
+ function buildProxyWithDotnet(runtimeRoot, realShells, dotnetExecutable) {
213
+ ensureDir(runtimeRoot);
214
+
215
+ const binDir = resolveProxyBinDir(runtimeRoot);
216
+ ensureDir(binDir);
217
+
218
+ const publish = spawnSync(dotnetExecutable, [
219
+ "publish",
220
+ proxyProjectFile,
221
+ "-nologo",
222
+ "-c",
223
+ "Release",
224
+ "-o",
225
+ binDir,
226
+ ], {
227
+ encoding: "utf8",
228
+ shell: false,
229
+ windowsHide: true,
230
+ cwd: proxySourceRoot,
231
+ });
232
+
233
+ if (publish.status !== 0) {
234
+ throw new Error([
235
+ "dotnet publish 执行失败。",
236
+ String(publish.stdout || "").trim(),
237
+ String(publish.stderr || "").trim(),
238
+ ].filter(Boolean).join("\n"));
239
+ }
240
+
241
+ finalizeBuiltProxy(runtimeRoot, realShells, "dotnet");
242
+ }
243
+
244
+ function buildProxyWithPowerShell(runtimeRoot, realShells) {
245
+ const compilerShell = realShells.powershell || realShells.pwsh;
246
+ if (!compilerShell) {
247
+ throw new Error("未找到可用于编译隐藏 shell 代理的 PowerShell。");
248
+ }
249
+
250
+ ensureDir(runtimeRoot);
251
+ const binDir = resolveProxyBinDir(runtimeRoot);
252
+ ensureDir(binDir);
253
+
254
+ const buildScriptFile = path.join(runtimeRoot, "build-hidden-shell-proxy.ps1");
255
+ const outputAssembly = path.join(binDir, "HelloLoopHiddenShellProxy.exe");
256
+ const scriptContent = [
257
+ "param(",
258
+ " [string]$SourcePath,",
259
+ " [string]$OutputAssembly",
260
+ ")",
261
+ "$ErrorActionPreference = 'Stop'",
262
+ "Add-Type -Path $SourcePath -OutputAssembly $OutputAssembly -OutputType WindowsApplication",
263
+ ].join("\n");
264
+ fs.writeFileSync(buildScriptFile, scriptContent, "utf8");
265
+
266
+ const compile = spawnSync(compilerShell, [
267
+ "-NoLogo",
268
+ "-NoProfile",
269
+ "-ExecutionPolicy",
270
+ "Bypass",
271
+ "-File",
272
+ buildScriptFile,
273
+ "-SourcePath",
274
+ proxyProgramFile,
275
+ "-OutputAssembly",
276
+ outputAssembly,
277
+ ], {
278
+ encoding: "utf8",
279
+ shell: false,
280
+ windowsHide: true,
281
+ cwd: runtimeRoot,
282
+ });
283
+
284
+ if (compile.status !== 0) {
285
+ throw new Error([
286
+ "PowerShell Add-Type 编译隐藏 shell 代理失败。",
287
+ String(compile.stdout || "").trim(),
288
+ String(compile.stderr || "").trim(),
289
+ ].filter(Boolean).join("\n"));
290
+ }
291
+
292
+ finalizeBuiltProxy(runtimeRoot, realShells, "powershell-add-type");
293
+ }
294
+
295
+ function buildProxy(runtimeRoot, realShells) {
296
+ const dotnetExecutable = resolveDotnetSdkExecutable();
297
+ if (dotnetExecutable) {
298
+ buildProxyWithDotnet(runtimeRoot, realShells, dotnetExecutable);
299
+ return;
300
+ }
301
+ buildProxyWithPowerShell(runtimeRoot, realShells);
302
+ }
303
+
304
+ function acquireBuildLock(runtimeRoot) {
305
+ const lockDir = path.join(runtimeRoot, ".build-lock");
306
+ ensureDir(runtimeRoot);
307
+ try {
308
+ fs.mkdirSync(lockDir);
309
+ return {
310
+ acquired: true,
311
+ lockDir,
312
+ };
313
+ } catch (error) {
314
+ if (String(error?.code || "") !== "EEXIST") {
315
+ throw error;
316
+ }
317
+ return {
318
+ acquired: false,
319
+ lockDir,
320
+ };
321
+ }
322
+ }
323
+
324
+ function waitForExistingBuild(runtimeRoot) {
325
+ const deadline = Date.now() + 180000;
326
+ while (Date.now() < deadline) {
327
+ if (isProxyReady(runtimeRoot)) {
328
+ return;
329
+ }
330
+ sleepSync(500);
331
+ }
332
+ throw new Error("等待已有 Windows 隐藏 shell 代理构建完成超时。");
333
+ }
334
+
335
+ function ensureProxyBuilt(realShells) {
336
+ const runtimeRoot = resolveRuntimeRoot();
337
+ if (isProxyReady(runtimeRoot)) {
338
+ return runtimeRoot;
339
+ }
340
+
341
+ const lock = acquireBuildLock(runtimeRoot);
342
+ if (!lock.acquired) {
343
+ waitForExistingBuild(runtimeRoot);
344
+ return runtimeRoot;
345
+ }
346
+
347
+ try {
348
+ if (!isProxyReady(runtimeRoot)) {
349
+ buildProxy(runtimeRoot, realShells);
350
+ }
351
+ return runtimeRoot;
352
+ } finally {
353
+ fs.rmSync(lock.lockDir, { recursive: true, force: true });
354
+ }
355
+ }
356
+
357
+ function ensureProxyRuntime(options = {}) {
358
+ const basePath = String(options.basePath || process.env.PATH || "").trim();
359
+ const realShells = resolveRealWindowsShells(basePath);
360
+ if (!realShells.pwsh && !realShells.powershell) {
361
+ throw new Error("未找到真实的 pwsh.exe 或 powershell.exe,无法建立隐藏 shell 代理。");
362
+ }
363
+
364
+ const runtimeRoot = ensureProxyBuilt(realShells);
365
+ const proxyBinDir = resolveProxyBinDir(runtimeRoot);
366
+ const proxyExecutable = path.join(proxyBinDir, PROXY_EXECUTABLE);
367
+ if (!fileExists(proxyExecutable)) {
368
+ throw new Error(`未找到隐藏 shell 代理可执行文件:${proxyExecutable}`);
369
+ }
370
+
371
+ return {
372
+ basePath,
373
+ runtimeRoot,
374
+ proxyBinDir,
375
+ proxyExecutable,
376
+ realShells,
377
+ };
378
+ }
379
+
380
+ export function resolveWindowsHiddenProcessProxyExecutable(options = {}) {
381
+ if ((options.platform || process.platform) !== "win32") {
382
+ return "";
383
+ }
384
+ return ensureProxyRuntime(options).proxyExecutable;
385
+ }
386
+
387
+ /**
388
+ * Resolve the Windows hidden-shell proxy environment patch used to suppress
389
+ * background pwsh/powershell console windows for Codex child processes.
390
+ */
391
+ export function resolveWindowsHiddenShellEnvPatch(options = {}) {
392
+ if ((options.platform || process.platform) !== "win32") {
393
+ return {};
394
+ }
395
+
396
+ const runtime = ensureProxyRuntime(options);
397
+
398
+ return {
399
+ [REAL_PWSH_ENV]: runtime.realShells.pwsh,
400
+ [REAL_POWERSHELL_ENV]: runtime.realShells.powershell,
401
+ [ORIGINAL_PATH_ENV]: runtime.basePath,
402
+ [PROXY_ENABLED_ENV]: "1",
403
+ PATH: buildWindowsPath([runtime.proxyBinDir], runtime.basePath),
404
+ };
405
+ }