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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { ensureDir, nowIso, readJson, writeJson } from "./common.mjs";
|
|
6
|
+
|
|
7
|
+
const FINAL_SUPERVISOR_STATUSES = new Set(["completed", "failed", "stopped"]);
|
|
8
|
+
|
|
9
|
+
function activeSessionsRoot() {
|
|
10
|
+
return path.join(os.homedir(), ".helloloop", "runtime", "active-sessions");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function knownWorkspacesRoot() {
|
|
14
|
+
return path.join(os.homedir(), ".helloloop", "runtime", "known-workspaces");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readJsonIfExists(filePath) {
|
|
18
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return readJson(filePath);
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function registryFileFor(sessionId) {
|
|
29
|
+
const safeId = String(sessionId || "session")
|
|
30
|
+
.trim()
|
|
31
|
+
.replace(/[^\w.-]+/gu, "_");
|
|
32
|
+
return path.join(activeSessionsRoot(), `${safeId}.json`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function workspaceFileFor(repoRoot, configDirName = "") {
|
|
36
|
+
const key = `${String(repoRoot || "").trim()}|${String(configDirName || "").trim()}`;
|
|
37
|
+
const safeId = Buffer.from(key).toString("base64url");
|
|
38
|
+
return path.join(knownWorkspacesRoot(), `${safeId}.json`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isPidAlive(pid) {
|
|
42
|
+
const numericPid = Number(pid || 0);
|
|
43
|
+
if (!Number.isFinite(numericPid) || numericPid <= 0) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
process.kill(numericPid, 0);
|
|
48
|
+
return true;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return String(error?.code || "") === "EPERM";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isStaleEntry(entry) {
|
|
55
|
+
if (!entry?.sessionId) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
const supervisor = readJsonIfExists(entry.supervisorStateFile);
|
|
59
|
+
if (!supervisor) {
|
|
60
|
+
return !isPidAlive(entry.pid);
|
|
61
|
+
}
|
|
62
|
+
if (supervisor.sessionId && supervisor.sessionId !== entry.sessionId) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (FINAL_SUPERVISOR_STATUSES.has(String(supervisor.status || "").trim())) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
return !isPidAlive(supervisor.pid || entry.pid);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function registerActiveSession(entry = {}) {
|
|
72
|
+
ensureDir(activeSessionsRoot());
|
|
73
|
+
const filePath = registryFileFor(entry.sessionId);
|
|
74
|
+
const current = readJsonIfExists(filePath) || {};
|
|
75
|
+
writeJson(filePath, {
|
|
76
|
+
schemaVersion: 1,
|
|
77
|
+
...current,
|
|
78
|
+
...entry,
|
|
79
|
+
updatedAt: nowIso(),
|
|
80
|
+
});
|
|
81
|
+
registerKnownWorkspace(entry);
|
|
82
|
+
return filePath;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function unregisterActiveSession(sessionId) {
|
|
86
|
+
const filePath = registryFileFor(sessionId);
|
|
87
|
+
try {
|
|
88
|
+
fs.rmSync(filePath, { force: true });
|
|
89
|
+
} catch {
|
|
90
|
+
// ignore cleanup failures
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function listActiveSessionEntries() {
|
|
95
|
+
ensureDir(activeSessionsRoot());
|
|
96
|
+
const entries = [];
|
|
97
|
+
|
|
98
|
+
for (const item of fs.readdirSync(activeSessionsRoot(), { withFileTypes: true })) {
|
|
99
|
+
if (!item.isFile() || !item.name.endsWith(".json")) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const filePath = path.join(activeSessionsRoot(), item.name);
|
|
103
|
+
const entry = readJsonIfExists(filePath);
|
|
104
|
+
if (!entry) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (isStaleEntry(entry)) {
|
|
108
|
+
unregisterActiveSession(entry.sessionId);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
entries.push(entry);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return entries.sort((left, right) => String(right.updatedAt || "").localeCompare(String(left.updatedAt || "")));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function registerKnownWorkspace(entry = {}) {
|
|
118
|
+
const repoRoot = String(entry.repoRoot || "").trim();
|
|
119
|
+
if (!repoRoot) {
|
|
120
|
+
return "";
|
|
121
|
+
}
|
|
122
|
+
ensureDir(knownWorkspacesRoot());
|
|
123
|
+
const filePath = workspaceFileFor(repoRoot, entry.configDirName);
|
|
124
|
+
const current = readJsonIfExists(filePath) || {};
|
|
125
|
+
writeJson(filePath, {
|
|
126
|
+
schemaVersion: 1,
|
|
127
|
+
...current,
|
|
128
|
+
repoRoot,
|
|
129
|
+
configDirName: String(entry.configDirName || "").trim(),
|
|
130
|
+
sessionId: String(entry.sessionId || current.sessionId || "").trim(),
|
|
131
|
+
command: String(entry.command || current.command || "").trim(),
|
|
132
|
+
lease: entry.lease || current.lease || null,
|
|
133
|
+
updatedAt: nowIso(),
|
|
134
|
+
});
|
|
135
|
+
return filePath;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function listKnownWorkspaceEntries() {
|
|
139
|
+
ensureDir(knownWorkspacesRoot());
|
|
140
|
+
const entries = [];
|
|
141
|
+
|
|
142
|
+
for (const item of fs.readdirSync(knownWorkspacesRoot(), { withFileTypes: true })) {
|
|
143
|
+
if (!item.isFile() || !item.name.endsWith(".json")) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const filePath = path.join(knownWorkspacesRoot(), item.name);
|
|
147
|
+
const entry = readJsonIfExists(filePath);
|
|
148
|
+
if (!entry?.repoRoot) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
entries.push(entry);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return entries.sort((left, right) => String(right.updatedAt || "").localeCompare(String(left.updatedAt || "")));
|
|
155
|
+
}
|