@tonyclaw/llm-inspector 1.19.0 → 1.19.2
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/.output/cli.js +338 -102
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-DwayZPPO.js → CompareDrawer-BzTsEelr.js} +1 -1
- package/.output/public/assets/{ProxyViewerContainer-iv3LVMEW.js → ProxyViewerContainer-BHm-n-_W.js} +9 -9
- package/.output/public/assets/{ReplayDialog-CaV1elYO.js → ReplayDialog-Dxxo80xO.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-CSfnjK7j.js → RequestAnatomy-D-swiaii.js} +1 -1
- package/.output/public/assets/{ResponseView-YkOL__xm.js → ResponseView-DvdH2bGk.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-D_p6L-oB.js → StreamingChunkSequence-D_RzgyKq.js} +1 -1
- package/.output/public/assets/_sessionId-DdODJCYY.js +1 -0
- package/.output/public/assets/{index-DeJyypsp.css → index-Bqi9RAGS.css} +1 -1
- package/.output/public/assets/index-EvnsNPOK.js +1 -0
- package/.output/public/assets/{json-viewer-BB-9bqnP.js → json-viewer-DIHZbEId.js} +1 -1
- package/.output/public/assets/{main-COVN451W.js → main-Br2EjrqZ.js} +2 -2
- package/.output/server/{_sessionId-BJT5qIib.mjs → _sessionId-CPkCxTP8.mjs} +4 -3
- package/.output/server/_ssr/{CompareDrawer-DNGYdUXs.mjs → CompareDrawer-DKHgXC5-.mjs} +4 -4
- package/.output/server/_ssr/{ProxyViewerContainer-B-zDOLYE.mjs → ProxyViewerContainer-B41D-2Eo.mjs} +57 -9
- package/.output/server/_ssr/{ReplayDialog-DWeqMA4y.mjs → ReplayDialog-D2piRWb0.mjs} +5 -5
- package/.output/server/_ssr/{RequestAnatomy-TOsrMu9-.mjs → RequestAnatomy-Ce7QdQNP.mjs} +4 -3
- package/.output/server/_ssr/{ResponseView-BuqdPrzm.mjs → ResponseView-D50UPv-r.mjs} +5 -5
- package/.output/server/_ssr/{StreamingChunkSequence-DuzNZkqL.mjs → StreamingChunkSequence-CDlNFS3Z.mjs} +4 -4
- package/.output/server/_ssr/{index-1nCQUt3y.mjs → index-DhAQxjnZ.mjs} +4 -3
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-BL8xhHbi.mjs → json-viewer-BZRjG_f7.mjs} +4 -4
- package/.output/server/_ssr/{router-aCaUgVTW.mjs → router-yP98-Gq-.mjs} +126 -105
- package/.output/server/{_tanstack-start-manifest_v-cBRxvCjb.mjs → _tanstack-start-manifest_v-d4a4xlOi.mjs} +1 -1
- package/.output/server/index.mjs +64 -64
- package/README.md +22 -0
- package/package.json +3 -1
- package/src/cli/detect-tools.ts +1 -0
- package/src/cli/templates/skill-onboard.ts +204 -71
- package/src/cli.ts +164 -39
- package/src/components/ProxyViewerContainer.tsx +52 -0
- package/src/components/proxy-viewer/LogEntryHeader.tsx +1 -0
- package/src/proxy/logFinalizer.ts +7 -3
- package/src/proxy/sessionProcess.ts +14 -7
- package/src/proxy/sessionSupervisor.ts +3 -2
- package/src/proxy/socketTracker.ts +19 -7
- package/styles/globals.css +14 -7
- package/.output/public/assets/_sessionId-BgCVUC6R.js +0 -1
- package/.output/public/assets/index-CWA4S0FO.js +0 -1
package/src/cli.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn, execSync } from "node:child_process";
|
|
2
|
+
import { spawn, execSync, type ChildProcess } from "node:child_process";
|
|
3
|
+
import { createConnection } from "node:net";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { dirname, join } from "node:path";
|
|
5
6
|
import { existsSync } from "node:fs";
|
|
@@ -8,6 +9,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
8
9
|
const __dirname = dirname(__filename);
|
|
9
10
|
|
|
10
11
|
const DEFAULT_PORT = 25947;
|
|
12
|
+
const LOCAL_PROBE_TIMEOUT_MS = 2000;
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Subcommand router. The legacy one-liner UX (`llm-inspector` with no args,
|
|
@@ -23,18 +25,107 @@ if (subcommand === "onboard") {
|
|
|
23
25
|
process.exit(code);
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
runStart(process.argv.slice(2));
|
|
28
|
+
await runStart(process.argv.slice(2));
|
|
27
29
|
|
|
28
30
|
// -----------------------------------------------------------------------------
|
|
29
31
|
// Legacy `start` behavior — start the proxy on the configured port. Extracted
|
|
30
32
|
// into a function so the router above can keep the top-level flow readable.
|
|
31
33
|
// -----------------------------------------------------------------------------
|
|
32
|
-
function
|
|
34
|
+
async function isInspectorHealthy(port: number): Promise<boolean> {
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const timeout = setTimeout(() => controller.abort(), LOCAL_PROBE_TIMEOUT_MS);
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/health`, {
|
|
39
|
+
cache: "no-store",
|
|
40
|
+
signal: controller.signal,
|
|
41
|
+
});
|
|
42
|
+
return response.ok;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
} finally {
|
|
46
|
+
clearTimeout(timeout);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isPortAcceptingConnections(port: number): Promise<boolean> {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
const socket = createConnection({ host: "127.0.0.1", port });
|
|
53
|
+
const finish = (value: boolean): void => {
|
|
54
|
+
socket.removeAllListeners();
|
|
55
|
+
socket.destroy();
|
|
56
|
+
resolve(value);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
socket.setTimeout(LOCAL_PROBE_TIMEOUT_MS);
|
|
60
|
+
socket.once("connect", () => finish(true));
|
|
61
|
+
socket.once("timeout", () => finish(false));
|
|
62
|
+
socket.once("error", () => finish(false));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function sleep(ms: number): Promise<void> {
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
setTimeout(resolve, ms);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function waitForInspectorHealthy(port: number, timeoutMs: number): Promise<boolean> {
|
|
73
|
+
const start = Date.now();
|
|
74
|
+
while (Date.now() - start < timeoutMs) {
|
|
75
|
+
if (await isInspectorHealthy(port)) return true;
|
|
76
|
+
await sleep(250);
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function openBrowser(targetUrl: string): void {
|
|
82
|
+
let command: string[] | undefined;
|
|
83
|
+
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
|
|
84
|
+
switch (process.platform) {
|
|
85
|
+
case "darwin":
|
|
86
|
+
command = ["open", targetUrl];
|
|
87
|
+
break;
|
|
88
|
+
case "linux":
|
|
89
|
+
command = ["xdg-open", targetUrl];
|
|
90
|
+
break;
|
|
91
|
+
case "win32":
|
|
92
|
+
command = ["cmd", "/c", "start", "", targetUrl];
|
|
93
|
+
break;
|
|
94
|
+
default:
|
|
95
|
+
// Unsupported platform - do nothing
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
if (command === undefined) return;
|
|
99
|
+
const [bin, ...cmdArgs] = command;
|
|
100
|
+
if (bin === undefined) return;
|
|
101
|
+
const browserProcess = spawn(bin, cmdArgs, {
|
|
102
|
+
stdio: "ignore",
|
|
103
|
+
detached: true,
|
|
104
|
+
windowsHide: true,
|
|
105
|
+
});
|
|
106
|
+
browserProcess.unref();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function waitForProcessExit(child: ChildProcess): Promise<number> {
|
|
110
|
+
return new Promise((resolve) => {
|
|
111
|
+
child.once("exit", (code) => {
|
|
112
|
+
resolve(code ?? 1);
|
|
113
|
+
});
|
|
114
|
+
child.once("error", () => {
|
|
115
|
+
resolve(1);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function runStart(args: string[]): Promise<void> {
|
|
33
121
|
const envPort = process.env["PORT"];
|
|
34
122
|
const portDefault = envPort !== undefined ? Number(envPort) : DEFAULT_PORT;
|
|
35
123
|
|
|
36
124
|
let port = portDefault;
|
|
37
125
|
let open = true;
|
|
126
|
+
let openWasSpecified = false;
|
|
127
|
+
let background = false;
|
|
128
|
+
let forceRestart = false;
|
|
38
129
|
let configDir: string | undefined;
|
|
39
130
|
let providersJson: string | undefined;
|
|
40
131
|
|
|
@@ -48,9 +139,18 @@ function runStart(args: string[]): void {
|
|
|
48
139
|
break;
|
|
49
140
|
case "--no-open":
|
|
50
141
|
open = false;
|
|
142
|
+
openWasSpecified = true;
|
|
51
143
|
break;
|
|
52
144
|
case "--open":
|
|
53
145
|
open = true;
|
|
146
|
+
openWasSpecified = true;
|
|
147
|
+
break;
|
|
148
|
+
case "--force-restart":
|
|
149
|
+
case "--restart":
|
|
150
|
+
forceRestart = true;
|
|
151
|
+
break;
|
|
152
|
+
case "--background":
|
|
153
|
+
background = true;
|
|
54
154
|
break;
|
|
55
155
|
case "--config-dir":
|
|
56
156
|
configDir = args[i + 1];
|
|
@@ -65,6 +165,12 @@ function runStart(args: string[]): void {
|
|
|
65
165
|
}
|
|
66
166
|
}
|
|
67
167
|
|
|
168
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
169
|
+
console.error(`Invalid port: ${String(port)}. Use --port <1-65535>.`);
|
|
170
|
+
process.exitCode = 1;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
68
174
|
/**
|
|
69
175
|
* Check if a port is in use and kill the process using it
|
|
70
176
|
*/
|
|
@@ -79,6 +185,7 @@ function runStart(args: string[]): void {
|
|
|
79
185
|
const output = execSync(`netstat -ano | findstr :${targetPort}`, {
|
|
80
186
|
encoding: "utf8",
|
|
81
187
|
timeout: 5000,
|
|
188
|
+
windowsHide: true,
|
|
82
189
|
});
|
|
83
190
|
const lines = output.trim().split("\n");
|
|
84
191
|
for (const line of lines) {
|
|
@@ -99,8 +206,12 @@ function runStart(args: string[]): void {
|
|
|
99
206
|
|
|
100
207
|
for (const pid of pids) {
|
|
101
208
|
try {
|
|
102
|
-
console.log(`Killing process ${pid} on port ${
|
|
103
|
-
execSync(`taskkill /PID ${pid} /F`, {
|
|
209
|
+
console.log(`Killing process ${pid} on port ${targetPort}...`);
|
|
210
|
+
execSync(`taskkill /PID ${pid} /F`, {
|
|
211
|
+
encoding: "utf8",
|
|
212
|
+
timeout: 5000,
|
|
213
|
+
windowsHide: true,
|
|
214
|
+
});
|
|
104
215
|
} catch {
|
|
105
216
|
// Process may have already exited
|
|
106
217
|
}
|
|
@@ -120,7 +231,7 @@ function runStart(args: string[]): void {
|
|
|
120
231
|
|
|
121
232
|
for (const pid of pids) {
|
|
122
233
|
try {
|
|
123
|
-
console.log(`Killing process ${pid} on port ${
|
|
234
|
+
console.log(`Killing process ${pid} on port ${targetPort}...`);
|
|
124
235
|
execSync(`kill -9 ${pid}`, { encoding: "utf8", timeout: 5000 });
|
|
125
236
|
} catch {
|
|
126
237
|
// Process may have already exited
|
|
@@ -134,11 +245,28 @@ function runStart(args: string[]): void {
|
|
|
134
245
|
|
|
135
246
|
process.env["PORT"] = String(port);
|
|
136
247
|
|
|
137
|
-
// Kill any existing process on the port
|
|
138
|
-
killProcessOnPort(port);
|
|
139
|
-
|
|
140
248
|
const url = `http://localhost:${port}`;
|
|
141
249
|
|
|
250
|
+
if (!forceRestart && (await isInspectorHealthy(port))) {
|
|
251
|
+
console.log(`llm-inspector is already running at ${url}`);
|
|
252
|
+
console.log(`Use --force-restart to restart the existing instance.`);
|
|
253
|
+
if (open && openWasSpecified) {
|
|
254
|
+
openBrowser(url);
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!forceRestart && (await isPortAcceptingConnections(port))) {
|
|
260
|
+
console.error(`Port ${port} is already in use, but it is not a healthy llm-inspector.`);
|
|
261
|
+
console.error(`Stop that process, choose --port <n>, or re-run with --force-restart.`);
|
|
262
|
+
process.exitCode = 1;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (forceRestart) {
|
|
267
|
+
killProcessOnPort(port);
|
|
268
|
+
}
|
|
269
|
+
|
|
142
270
|
console.log(`Server running at ${url}`);
|
|
143
271
|
console.log(` Proxy: ${url}/proxy`);
|
|
144
272
|
console.log(``);
|
|
@@ -157,29 +285,6 @@ function runStart(args: string[]): void {
|
|
|
157
285
|
` Example: ROUTES='{"claude-":"https://api.anthropic.com","MiniMax":"https://api.minimaxi.com/anthropic"}'`,
|
|
158
286
|
);
|
|
159
287
|
|
|
160
|
-
const openBrowser = (targetUrl: string): void => {
|
|
161
|
-
let command: string[] | undefined;
|
|
162
|
-
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
|
|
163
|
-
switch (process.platform) {
|
|
164
|
-
case "darwin":
|
|
165
|
-
command = ["open", targetUrl];
|
|
166
|
-
break;
|
|
167
|
-
case "linux":
|
|
168
|
-
command = ["xdg-open", targetUrl];
|
|
169
|
-
break;
|
|
170
|
-
case "win32":
|
|
171
|
-
command = ["cmd", "/c", "start", targetUrl];
|
|
172
|
-
break;
|
|
173
|
-
default:
|
|
174
|
-
// Unsupported platform - do nothing
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
if (command === undefined) return;
|
|
178
|
-
const [bin, ...cmdArgs] = command;
|
|
179
|
-
if (bin === undefined) return;
|
|
180
|
-
spawn(bin, cmdArgs, { stdio: "ignore", detached: true });
|
|
181
|
-
};
|
|
182
|
-
|
|
183
288
|
if (open) {
|
|
184
289
|
openBrowser(url);
|
|
185
290
|
}
|
|
@@ -191,11 +296,19 @@ function runStart(args: string[]): void {
|
|
|
191
296
|
// Start server with node
|
|
192
297
|
const serverEnv = { ...process.env };
|
|
193
298
|
if (configDir !== undefined) {
|
|
194
|
-
//
|
|
299
|
+
// Normalize MSYS / Git Bash paths to Windows native form.
|
|
300
|
+
// On Windows, `path.join('/c/Users/foo', 'config.json')` becomes
|
|
301
|
+
// `\c\Users\foo\config.json` (leading slash converted to backslash).
|
|
302
|
+
// Child processes spawned by `spawn()` won't follow that style, so
|
|
303
|
+
// rewrite `\c\...` (or any `\x\...` drive) to `C:\...` before
|
|
304
|
+
// handing the path to the proxy server. No-op on already-native
|
|
305
|
+
// Windows paths and on non-Windows platforms.
|
|
195
306
|
let resolvedPath = join(configDir, "config.json");
|
|
196
|
-
|
|
197
|
-
if (
|
|
198
|
-
|
|
307
|
+
const msysMatch = /^\\([a-z])\\(.*)$/i.exec(resolvedPath);
|
|
308
|
+
if (msysMatch !== null) {
|
|
309
|
+
const drive = (msysMatch[1] ?? "").toUpperCase();
|
|
310
|
+
const rest = msysMatch[2] ?? "";
|
|
311
|
+
resolvedPath = `${drive}:\\${rest}`;
|
|
199
312
|
}
|
|
200
313
|
serverEnv["LLM_INSPECTOR_CONFIG_PATH"] = resolvedPath;
|
|
201
314
|
}
|
|
@@ -203,10 +316,22 @@ function runStart(args: string[]): void {
|
|
|
203
316
|
serverEnv["LLM_INSPECTOR_PROVIDERS_JSON"] = providersJson;
|
|
204
317
|
}
|
|
205
318
|
const serverProcess = spawn(process.execPath, [serverPath], {
|
|
206
|
-
stdio: ["ignore", "
|
|
207
|
-
detached:
|
|
319
|
+
stdio: background ? ["ignore", "ignore", "ignore"] : "inherit",
|
|
320
|
+
detached: background,
|
|
208
321
|
env: serverEnv,
|
|
322
|
+
windowsHide: background,
|
|
209
323
|
});
|
|
210
324
|
|
|
211
|
-
|
|
325
|
+
if (background) {
|
|
326
|
+
serverProcess.unref();
|
|
327
|
+
if (await waitForInspectorHealthy(port, 5000)) {
|
|
328
|
+
console.log(`llm-inspector background server is ready at ${url}`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
console.error(`llm-inspector background server did not become ready at ${url}.`);
|
|
332
|
+
process.exitCode = 1;
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
process.exitCode = await waitForProcessExit(serverProcess);
|
|
212
337
|
}
|
|
@@ -61,6 +61,8 @@ function filterLogs(
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
const DEBOUNCE_MS = 50;
|
|
64
|
+
const HASH_SCROLL_ATTEMPTS = 12;
|
|
65
|
+
const HASH_HIGHLIGHT_MS = 1800;
|
|
64
66
|
|
|
65
67
|
function buildLogsStreamUrl(sessionId: string | undefined): string {
|
|
66
68
|
if (sessionId === undefined) return "/api/logs/stream";
|
|
@@ -93,6 +95,7 @@ export function ProxyViewerContainer({
|
|
|
93
95
|
const [error, setError] = useState<string | null>(null);
|
|
94
96
|
const eventSourceRef = useRef<EventSource | null>(null);
|
|
95
97
|
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
98
|
+
const handledHashRef = useRef<string | null>(null);
|
|
96
99
|
|
|
97
100
|
// O(1) log lookup by id
|
|
98
101
|
const logIndexRef = useRef<Map<number, number>>(new Map());
|
|
@@ -214,6 +217,55 @@ export function ProxyViewerContainer({
|
|
|
214
217
|
};
|
|
215
218
|
}, [connectSSE]);
|
|
216
219
|
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
const hash = window.location.hash;
|
|
222
|
+
if (!hash.startsWith("#log-")) return;
|
|
223
|
+
if (handledHashRef.current === hash) return;
|
|
224
|
+
const targetId = hash.slice(1);
|
|
225
|
+
let cancelled = false;
|
|
226
|
+
let attempts = 0;
|
|
227
|
+
let highlightedTarget: HTMLElement | null = null;
|
|
228
|
+
let highlightTimer: number | null = null;
|
|
229
|
+
|
|
230
|
+
const tryScrollToLog = (): void => {
|
|
231
|
+
if (cancelled) return;
|
|
232
|
+
const target = document.getElementById(targetId);
|
|
233
|
+
if (target !== null) {
|
|
234
|
+
handledHashRef.current = hash;
|
|
235
|
+
target.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
236
|
+
if (target instanceof HTMLElement) {
|
|
237
|
+
highlightedTarget = target;
|
|
238
|
+
target.setAttribute("data-deep-link-highlight", "true");
|
|
239
|
+
highlightTimer = window.setTimeout(() => {
|
|
240
|
+
target.removeAttribute("data-deep-link-highlight");
|
|
241
|
+
}, HASH_HIGHLIGHT_MS);
|
|
242
|
+
target.focus({ preventScroll: true });
|
|
243
|
+
if (target.getAttribute("data-nav-action") === "expand") {
|
|
244
|
+
target.click();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
attempts += 1;
|
|
251
|
+
if (attempts < HASH_SCROLL_ATTEMPTS) {
|
|
252
|
+
window.setTimeout(tryScrollToLog, 100);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
tryScrollToLog();
|
|
257
|
+
|
|
258
|
+
return () => {
|
|
259
|
+
cancelled = true;
|
|
260
|
+
if (highlightTimer !== null) {
|
|
261
|
+
window.clearTimeout(highlightTimer);
|
|
262
|
+
}
|
|
263
|
+
if (highlightedTarget !== null) {
|
|
264
|
+
highlightedTarget.removeAttribute("data-deep-link-highlight");
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}, [logs.length]);
|
|
268
|
+
|
|
217
269
|
const handleClearAll = useCallback(() => {
|
|
218
270
|
if (initialSessionId !== undefined && allLogs.length === 0) return;
|
|
219
271
|
void (async () => {
|
|
@@ -3,7 +3,7 @@ import { writeChunks } from "./chunkStorage";
|
|
|
3
3
|
import { formatForPath } from "./formats";
|
|
4
4
|
import { appendLogEntry, logger } from "./logger";
|
|
5
5
|
import type { CapturedLog } from "./schemas";
|
|
6
|
-
import { getSessionProcess } from "./sessionProcess";
|
|
6
|
+
import { getSessionProcess, isSessionProcessAvailable } from "./sessionProcess";
|
|
7
7
|
import { finalizeLogUpdate } from "./store";
|
|
8
8
|
|
|
9
9
|
type BaseFinalizeLogJob = {
|
|
@@ -209,17 +209,21 @@ export function commitFinalizeLogResult(result: FinalizeLogResult): void {
|
|
|
209
209
|
|
|
210
210
|
// ── Routing ─────────────────────────────────────────────────────
|
|
211
211
|
// FINALIZER_RUNTIME selects the execution backend:
|
|
212
|
-
// "process" → per-session child process (
|
|
212
|
+
// "process" → per-session child process (max isolation)
|
|
213
213
|
// "worker" → shared Worker Thread pool
|
|
214
214
|
// "inline" → synchronous in-process (debug / fallback)
|
|
215
|
+
// The default is "process" only when its worker entry exists. Production
|
|
216
|
+
// bundles that do not emit the standalone worker entry fall back to "inline"
|
|
217
|
+
// instead of repeatedly spawning a failing child process.
|
|
215
218
|
// For backward compatibility, FINALIZER_USE_WORKER=0 forces "inline".
|
|
216
219
|
|
|
217
220
|
const RUNTIME: "process" | "worker" | "inline" = (() => {
|
|
218
221
|
if (process.env["FINALIZER_USE_WORKER"] === "0") return "inline";
|
|
219
222
|
const mode = process.env["FINALIZER_RUNTIME"];
|
|
223
|
+
if (mode === "process") return "process";
|
|
220
224
|
if (mode === "worker") return "worker";
|
|
221
225
|
if (mode === "inline") return "inline";
|
|
222
|
-
return "process";
|
|
226
|
+
return isSessionProcessAvailable() ? "process" : "inline";
|
|
223
227
|
})();
|
|
224
228
|
|
|
225
229
|
function executeBuildInSessionProcess(job: FinalizeLogJob): Promise<FinalizeLogResult> {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
2
4
|
import { logger } from "./logger";
|
|
3
5
|
import type { FinalizeLogJob, FinalizeLogResult } from "./logFinalizer";
|
|
4
6
|
|
|
@@ -12,6 +14,14 @@ type PendingJob = {
|
|
|
12
14
|
reject: (err: Error) => void;
|
|
13
15
|
};
|
|
14
16
|
|
|
17
|
+
function resolveSessionWorkerPath(): string {
|
|
18
|
+
return fileURLToPath(new URL("./sessionWorkerEntry.ts", import.meta.url));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isSessionProcessAvailable(): boolean {
|
|
22
|
+
return existsSync(resolveSessionWorkerPath());
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
export class SessionProcess {
|
|
16
26
|
private child: ChildProcess | null = null;
|
|
17
27
|
private pending = new Map<string, PendingJob>();
|
|
@@ -30,14 +40,11 @@ export class SessionProcess {
|
|
|
30
40
|
private ensureRunning(): ChildProcess {
|
|
31
41
|
if (this.child !== null && this.child.connected) return this.child;
|
|
32
42
|
|
|
33
|
-
const
|
|
34
|
-
// On Windows, the path from URL.pathname starts with "/" which needs to
|
|
35
|
-
// be stripped when it's a drive letter path (e.g. "/C:/..." → "C:/...")
|
|
36
|
-
const resolvedPath =
|
|
37
|
-
process.platform === "win32" && entryPath.startsWith("/") ? entryPath.slice(1) : entryPath;
|
|
43
|
+
const resolvedPath = resolveSessionWorkerPath();
|
|
38
44
|
|
|
39
|
-
this.child =
|
|
45
|
+
this.child = spawn(process.execPath, [...process.execArgv, resolvedPath], {
|
|
40
46
|
stdio: ["pipe", "pipe", "pipe", "ipc"],
|
|
47
|
+
windowsHide: true,
|
|
41
48
|
});
|
|
42
49
|
|
|
43
50
|
this.restartCount += 1;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CapturedLog } from "./schemas";
|
|
2
|
+
import { isSessionProcessAvailable } from "./sessionProcess";
|
|
2
3
|
|
|
3
4
|
export const PROVIDER_TEST_SESSION_ID = "provider-test";
|
|
4
5
|
|
|
@@ -21,10 +22,10 @@ export type SessionRuntimeMode = "in-process" | "worker-thread" | "child-process
|
|
|
21
22
|
function getRuntimeMode(): SessionRuntimeMode {
|
|
22
23
|
if (process.env["FINALIZER_USE_WORKER"] === "0") return "in-process";
|
|
23
24
|
const mode = process.env["FINALIZER_RUNTIME"];
|
|
25
|
+
if (mode === "process") return "child-process";
|
|
24
26
|
if (mode === "worker") return "worker-thread";
|
|
25
27
|
if (mode === "inline") return "in-process";
|
|
26
|
-
|
|
27
|
-
return "child-process";
|
|
28
|
+
return isSessionProcessAvailable() ? "child-process" : "in-process";
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export type SessionSnapshot = {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { exec } from "node:child_process";
|
|
1
|
+
import { exec, execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import { logger } from "./logger";
|
|
4
4
|
|
|
5
5
|
const execAsync = promisify(exec);
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
6
7
|
|
|
7
8
|
type ClientInfo = {
|
|
8
9
|
port: number | null;
|
|
@@ -78,9 +79,15 @@ async function lookupClientInfo(port: number): Promise<ClientInfo> {
|
|
|
78
79
|
` Write-Output "$pid|$cmd"`,
|
|
79
80
|
`}`,
|
|
80
81
|
].join("; ");
|
|
81
|
-
const { stdout } = await
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
const { stdout } = await execFileAsync(
|
|
83
|
+
"powershell.exe",
|
|
84
|
+
["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", psScript],
|
|
85
|
+
{
|
|
86
|
+
windowsHide: true,
|
|
87
|
+
timeout: 3000,
|
|
88
|
+
maxBuffer: 64 * 1024,
|
|
89
|
+
},
|
|
90
|
+
);
|
|
84
91
|
const trimmed = stdout.trim();
|
|
85
92
|
if (trimmed === "") {
|
|
86
93
|
return { port, pid: null, cwd: null, projectFolder: null };
|
|
@@ -164,9 +171,14 @@ async function lookupProcessInfo(
|
|
|
164
171
|
|
|
165
172
|
try {
|
|
166
173
|
if (platform === "win32") {
|
|
167
|
-
const { stdout } = await
|
|
168
|
-
|
|
169
|
-
{
|
|
174
|
+
const { stdout } = await execFileAsync(
|
|
175
|
+
"wmic.exe",
|
|
176
|
+
["process", "where", `processid=${pid}`, "get", "commandline", "/value"],
|
|
177
|
+
{
|
|
178
|
+
windowsHide: true,
|
|
179
|
+
timeout: 3000,
|
|
180
|
+
maxBuffer: 64 * 1024,
|
|
181
|
+
},
|
|
170
182
|
);
|
|
171
183
|
const lines = stdout.trim().split("\n").filter(Boolean);
|
|
172
184
|
for (const line of lines) {
|
package/styles/globals.css
CHANGED
|
@@ -165,13 +165,20 @@
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
@media (prefers-reduced-motion: reduce) {
|
|
169
|
-
.animate-crab-piano-pop {
|
|
170
|
-
animation: none !important;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
168
|
+
@media (prefers-reduced-motion: reduce) {
|
|
169
|
+
.animate-crab-piano-pop {
|
|
170
|
+
animation: none !important;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
[data-deep-link-highlight="true"] {
|
|
175
|
+
background: color-mix(in oklch, var(--chart-2) 18%, transparent);
|
|
176
|
+
box-shadow:
|
|
177
|
+
inset 0 0 0 1px color-mix(in oklch, var(--chart-2) 65%, transparent),
|
|
178
|
+
0 0 0 3px color-mix(in oklch, var(--chart-2) 20%, transparent);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@layer base {
|
|
175
182
|
* {
|
|
176
183
|
@apply border-border outline-ring/50;
|
|
177
184
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{R as s,j as e}from"./main-COVN451W.js";import{P as i}from"./ProxyViewerContainer-iv3LVMEW.js";function t(){const{sessionId:o}=s.useParams();return e.jsx(i,{initialSessionId:o},o)}export{t as component};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{P as o}from"./ProxyViewerContainer-iv3LVMEW.js";import"./main-COVN451W.js";const r=o;export{r as component};
|