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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +230 -506
- 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 +49 -1
- package/src/cli_analyze_command.mjs +9 -5
- package/src/cli_args.mjs +102 -37
- package/src/cli_command_handlers.mjs +44 -4
- package/src/cli_support.mjs +2 -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 +1 -0
- package/src/engine_selection.mjs +24 -0
- package/src/engine_selection_probe.mjs +10 -6
- package/src/engine_selection_settings.mjs +12 -19
- package/src/execution_interactivity.mjs +12 -0
- package/src/host_continuation.mjs +305 -0
- package/src/install_codex.mjs +20 -8
- 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 +4 -0
- package/src/runner_execution_support.mjs +69 -3
- package/src/runner_once.mjs +4 -0
- package/src/runner_status.mjs +63 -7
- package/src/runtime_engine_support.mjs +41 -4
- 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 +3 -2
- package/src/supervisor_guardian.mjs +307 -0
- package/src/supervisor_runtime.mjs +138 -82
- package/src/supervisor_state.mjs +64 -0
- package/src/supervisor_watch.mjs +92 -48
- package/src/terminal_session_limits.mjs +1 -21
- package/src/windows_hidden_shell_proxy.mjs +405 -0
- 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
|
+
}
|
package/src/supervisor_watch.mjs
CHANGED
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import {
|
|
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
|
-
|
|
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 ?
|
|
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 &&
|
|
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
|
+
}
|