@tonyclaw/agent-inspector 3.0.7 → 3.0.8
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 +245 -171
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-C0Kbsm-Q.js → CompareDrawer-Bth_Ywyt.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-C73EU9Jq.js +106 -0
- package/.output/public/assets/ReplayDialog-C2YYAuTe.js +1 -0
- package/.output/public/assets/{RequestAnatomy-fa0JXy5p.js → RequestAnatomy-8k9dAX2j.js} +1 -1
- package/.output/public/assets/ResponseView-DEL6JcXm.js +3 -0
- package/.output/public/assets/StreamingChunkSequence-DngmoR9j.js +1 -0
- package/.output/public/assets/{_sessionId-D5fYH2C0.js → _sessionId-4vk9RMZ5.js} +1 -1
- package/.output/public/assets/index-DBEB1rHe.css +1 -0
- package/.output/public/assets/{index-BEsqvywM.js → index-DINLvnm9.js} +1 -1
- package/.output/public/assets/index-LPIGiUMP.js +1 -0
- package/.output/public/assets/{json-viewer-B0QRbMSf.js → json-viewer-CH5dvS2Y.js} +1 -1
- package/.output/public/assets/{main-CuKZD7n0.js → main-GWkZn4xy.js} +2 -2
- package/.output/server/{_sessionId-DdBTK3qm.mjs → _sessionId-EgsjdkN7.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-m82cVZ73.mjs → CompareDrawer-CaM8FYuD.mjs} +2 -2
- package/.output/server/_ssr/{ProxyViewerContainer-7geuf78f.mjs → ProxyViewerContainer-EJIogcGd.mjs} +545 -309
- package/.output/server/_ssr/{ReplayDialog-B8RlWLLH.mjs → ReplayDialog-BPpJ2-q8.mjs} +58 -23
- package/.output/server/_ssr/{RequestAnatomy-BE7pmwpv.mjs → RequestAnatomy-DeQAWnZx.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-D-sDsdQr.mjs → ResponseView-Dshm8iQ7.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-DU_hThJr.mjs → StreamingChunkSequence-32yIU2hn.mjs} +30 -20
- package/.output/server/_ssr/{index-D0sV8UbE.mjs → index-XRcx9blU.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-CaDh1zrw.mjs → json-viewer-C0aX5rqC.mjs} +2 -2
- package/.output/server/_ssr/{router-Zh2iZ_u0.mjs → router-BI-Mvxz9.mjs} +441 -172
- package/.output/server/_tanstack-start-manifest_v-jKScklL_.mjs +4 -0
- package/.output/server/index.mjs +71 -71
- package/package.json +1 -1
- package/src/assets/IDE/opencode.svg +1 -1
- package/src/assets/IDE/xiaomimimo.svg +1 -1
- package/src/cli/detect-tools.ts +2 -27
- package/src/cli/templates/codex-skill-onboard.ts +1 -1
- package/src/cli/templates/skill-onboard.ts +2 -3
- package/src/cli.ts +9 -86
- package/src/components/ProxyViewer.tsx +55 -8
- package/src/components/ProxyViewerContainer.tsx +30 -19
- package/src/components/clients/ClientLogo.tsx +22 -2
- package/src/components/ecosystem/AgentLabDialog.tsx +204 -3
- package/src/components/groups/GroupsDialog.tsx +28 -40
- package/src/components/providers/ImportWizardDialog.tsx +51 -101
- package/src/components/providers/ProvidersPanel.tsx +82 -68
- package/src/components/providers/SettingsDialog.tsx +11 -12
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +15 -23
- package/src/components/proxy-viewer/ReplayDialog.tsx +65 -22
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +33 -20
- package/src/lib/apiClient.ts +43 -10
- package/src/lib/ecosystemContract.ts +32 -0
- package/src/lib/export-logs.ts +37 -3
- package/src/proxy/ecosystemRegistry.ts +244 -0
- package/src/proxy/ecosystemTasks.ts +153 -0
- package/src/proxy/platformCommands.ts +212 -0
- package/src/proxy/socketTracker.ts +84 -143
- package/src/routes/api/ecosystem.packages.$packageId.help.ts +16 -0
- package/src/routes/api/ecosystem.packages.$packageId.install.ts +16 -0
- package/src/routes/api/ecosystem.packages.$packageId.runner-presets.ts +19 -0
- package/src/routes/api/ecosystem.packages.$packageId.upgrade.ts +16 -0
- package/src/routes/api/ecosystem.packages.ts +1 -248
- package/src/routes/api/ecosystem.tasks.$taskId.ts +16 -0
- package/src/routes/api/ecosystem.tasks.ts +12 -0
- package/.output/public/assets/ProxyViewerContainer-p-rpYyiF.js +0 -106
- package/.output/public/assets/ReplayDialog-CM3dhP8v.js +0 -1
- package/.output/public/assets/ResponseView-Bq6sC7Ay.js +0 -3
- package/.output/public/assets/StreamingChunkSequence-CGOun3Df.js +0 -1
- package/.output/public/assets/index-C29Olfzt.js +0 -1
- package/.output/public/assets/index-DgANE0r3.css +0 -1
- package/.output/server/_tanstack-start-manifest_v-LCE-2AaF.mjs +0 -4
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
2
3
|
import { promisify } from "node:util";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { logger } from "./logger";
|
|
5
6
|
|
|
6
|
-
const execAsync = promisify(exec);
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
8
|
|
|
9
9
|
type ClientInfo = {
|
|
@@ -102,7 +102,7 @@ function setCache(port: number, info: ClientInfo): void {
|
|
|
102
102
|
* 2. Bun-style: `request.socket` accessor.
|
|
103
103
|
* 3. Direct `socket` / `connection` properties on the request (legacy Node
|
|
104
104
|
* IncomingMessage-style wrapping).
|
|
105
|
-
* Returns `null` when nothing matches
|
|
105
|
+
* Returns `null` when nothing matches 鈥?the caller will then leave `pid`
|
|
106
106
|
* (and downstream `clientPid`) as `null`.
|
|
107
107
|
*/
|
|
108
108
|
export function extractRemotePort(request: Request): number | null {
|
|
@@ -154,90 +154,6 @@ function pick(value: unknown, key: string): unknown {
|
|
|
154
154
|
return value[key];
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
/**
|
|
158
|
-
* Combined lookup: PID from port, then process info from PID.
|
|
159
|
-
* On Windows, uses a single PowerShell call to get PID + command line.
|
|
160
|
-
*/
|
|
161
|
-
async function lookupClientInfo(port: number): Promise<ClientInfo> {
|
|
162
|
-
const platform = process.platform;
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
if (platform === "win32") {
|
|
166
|
-
// Single PowerShell call: get PID and command line in one shot
|
|
167
|
-
const psScript = [
|
|
168
|
-
`$conn = Get-NetTCPConnection -LocalPort ${port} -State Established -ErrorAction SilentlyContinue | Select-Object -First 1`,
|
|
169
|
-
`if ($conn) {`,
|
|
170
|
-
` $pid = $conn.OwningProcess`,
|
|
171
|
-
` $proc = Get-CimInstance Win32_Process -Filter "ProcessId=$pid" -ErrorAction SilentlyContinue`,
|
|
172
|
-
` $cmd = if ($proc) { $proc.CommandLine } else { "" }`,
|
|
173
|
-
` Write-Output "$pid|$cmd"`,
|
|
174
|
-
`}`,
|
|
175
|
-
].join("; ");
|
|
176
|
-
const { stdout } = await execFileAsync(
|
|
177
|
-
"powershell.exe",
|
|
178
|
-
["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", psScript],
|
|
179
|
-
{
|
|
180
|
-
windowsHide: true,
|
|
181
|
-
timeout: 3000,
|
|
182
|
-
maxBuffer: 64 * 1024,
|
|
183
|
-
},
|
|
184
|
-
);
|
|
185
|
-
const trimmed = stdout.trim();
|
|
186
|
-
if (trimmed === "") {
|
|
187
|
-
return { port, pid: null, cwd: null, projectFolder: null };
|
|
188
|
-
}
|
|
189
|
-
const [pidStr, ...cmdParts] = trimmed.split("|");
|
|
190
|
-
const pid = parseInt(pidStr ?? "", 10);
|
|
191
|
-
if (isNaN(pid) || pid <= 0) {
|
|
192
|
-
return { port, pid: null, cwd: null, projectFolder: null };
|
|
193
|
-
}
|
|
194
|
-
const commandLine = cmdParts.join("|").trim();
|
|
195
|
-
const { cwd, projectFolder } = parseCommandLine(commandLine, "\\");
|
|
196
|
-
return { port, pid, cwd, projectFolder };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Unix: two-step lookup (PID from port, then process info from PID)
|
|
200
|
-
let pid: number | null = null;
|
|
201
|
-
|
|
202
|
-
if (platform === "darwin") {
|
|
203
|
-
const { stdout } = await execAsync(`lsof -i :${port} -sTCP:ESTABLISHED -t 2>/dev/null`, {
|
|
204
|
-
shell: "/bin/sh",
|
|
205
|
-
});
|
|
206
|
-
const parsed = parseInt(stdout.trim(), 10);
|
|
207
|
-
if (!isNaN(parsed) && parsed > 0) pid = parsed;
|
|
208
|
-
} else {
|
|
209
|
-
try {
|
|
210
|
-
const { stdout } = await execAsync(
|
|
211
|
-
`ss -tlnp 'sport = :${port}' 2>/dev/null | grep ESTAB | awk '{print $6}' | grep -o 'pid=[0-9]*' | cut -d= -f2`,
|
|
212
|
-
{ shell: "/bin/sh" },
|
|
213
|
-
);
|
|
214
|
-
const parsed = parseInt(stdout.trim(), 10);
|
|
215
|
-
if (!isNaN(parsed) && parsed > 0) pid = parsed;
|
|
216
|
-
} catch {
|
|
217
|
-
// Fall back to netstat
|
|
218
|
-
}
|
|
219
|
-
if (pid === null) {
|
|
220
|
-
const { stdout } = await execAsync(
|
|
221
|
-
`netstat -tan 2>/dev/null | grep ':${port}' | grep ESTABLISHED | awk '{print $7}' | cut -d/ -f1`,
|
|
222
|
-
{ shell: "/bin/sh" },
|
|
223
|
-
);
|
|
224
|
-
const parsed = parseInt(stdout.trim(), 10);
|
|
225
|
-
if (!isNaN(parsed) && parsed > 0) pid = parsed;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (pid === null) {
|
|
230
|
-
return { port, pid: null, cwd: null, projectFolder: null };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const { cwd, projectFolder } = await lookupProcessInfo(pid);
|
|
234
|
-
return { port, pid, cwd, projectFolder };
|
|
235
|
-
} catch (err) {
|
|
236
|
-
logger.debug(`[socketTracker] Failed to lookup info for port ${port}:`, String(err));
|
|
237
|
-
return { port, pid: null, cwd: null, projectFolder: null };
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
157
|
function parseCommandLine(
|
|
242
158
|
commandLine: string,
|
|
243
159
|
sep: string,
|
|
@@ -265,30 +181,25 @@ async function lookupProcessInfo(
|
|
|
265
181
|
|
|
266
182
|
try {
|
|
267
183
|
if (platform === "win32") {
|
|
184
|
+
const psScript = [
|
|
185
|
+
`$proc = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}" -ErrorAction SilentlyContinue`,
|
|
186
|
+
`if ($proc) { Write-Output $proc.CommandLine }`,
|
|
187
|
+
].join("; ");
|
|
268
188
|
const { stdout } = await execFileAsync(
|
|
269
|
-
"
|
|
270
|
-
["
|
|
189
|
+
"powershell.exe",
|
|
190
|
+
["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", psScript],
|
|
271
191
|
{
|
|
272
192
|
windowsHide: true,
|
|
273
193
|
timeout: 3000,
|
|
274
194
|
maxBuffer: 64 * 1024,
|
|
275
195
|
},
|
|
276
196
|
);
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
if (line.includes("=")) {
|
|
280
|
-
const commandLine = (line.split("=")[1] ?? "").trim();
|
|
281
|
-
if (commandLine) {
|
|
282
|
-
return parseCommandLine(commandLine, "\\");
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
197
|
+
const commandLine = stdout.trim();
|
|
198
|
+
if (commandLine.length > 0) return parseCommandLine(commandLine, "\\");
|
|
286
199
|
} else {
|
|
287
200
|
try {
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
});
|
|
291
|
-
const commandLine = stdout.trim();
|
|
201
|
+
const raw = await readFile(`/proc/${String(pid)}/cmdline`, "utf8");
|
|
202
|
+
const commandLine = raw.replace(/\0/g, " ").trim();
|
|
292
203
|
if (commandLine) {
|
|
293
204
|
return parseCommandLine(commandLine, "/");
|
|
294
205
|
}
|
|
@@ -297,8 +208,9 @@ async function lookupProcessInfo(
|
|
|
297
208
|
}
|
|
298
209
|
|
|
299
210
|
if (platform === "darwin") {
|
|
300
|
-
const { stdout } = await
|
|
301
|
-
|
|
211
|
+
const { stdout } = await execFileAsync("ps", ["-p", String(pid), "-o", "comm="], {
|
|
212
|
+
timeout: 3000,
|
|
213
|
+
maxBuffer: 64 * 1024,
|
|
302
214
|
});
|
|
303
215
|
const exePath = stdout.trim();
|
|
304
216
|
if (exePath) {
|
|
@@ -313,7 +225,7 @@ async function lookupProcessInfo(
|
|
|
313
225
|
}
|
|
314
226
|
|
|
315
227
|
/**
|
|
316
|
-
* Scan the OS connection table for the freshest client
|
|
228
|
+
* Scan the OS connection table for the freshest client鈫抪roxy connection and
|
|
317
229
|
* return the matching PID plus its cwd/projectFolder.
|
|
318
230
|
*/
|
|
319
231
|
async function lookupRecentClient(): Promise<ClientInfo> {
|
|
@@ -346,16 +258,9 @@ async function lookupRecentClient(): Promise<ClientInfo> {
|
|
|
346
258
|
// Unix: netstat with -p prints `pid/program` next to each entry. Pick
|
|
347
259
|
// the row matching our proxy port on the remote side (i.e. the client's
|
|
348
260
|
// ephemeral port + the proxy's listen port). When multiple clients are
|
|
349
|
-
// connected we take the highest ephemeral port
|
|
261
|
+
// connected we take the highest ephemeral port 鈥?Linux/macOS assign
|
|
350
262
|
// ports in ascending order, so the largest one is the freshest.
|
|
351
|
-
const
|
|
352
|
-
`netstat -tanp 2>/dev/null | grep ':${port}' | grep ESTAB || ss -tanp 2>/dev/null | grep ':${port}' | grep ESTAB`,
|
|
353
|
-
{ shell: "/bin/sh" },
|
|
354
|
-
);
|
|
355
|
-
const lines = stdout
|
|
356
|
-
.trim()
|
|
357
|
-
.split("\n")
|
|
358
|
-
.filter((line) => line.includes("ESTAB"));
|
|
263
|
+
const lines = await readUnixEstablishedConnectionLines(port);
|
|
359
264
|
const clientPid = pickNewestClientPidFromNetstat(lines, port);
|
|
360
265
|
if (clientPid === null) {
|
|
361
266
|
return { port: null, pid: null, cwd: null, projectFolder: null };
|
|
@@ -378,14 +283,14 @@ function pickNewestClientPidFromNetstat(lines: string[], proxyPort: number): num
|
|
|
378
283
|
for (const line of lines) {
|
|
379
284
|
order++;
|
|
380
285
|
const cols = line.trim().split(/\s+/);
|
|
381
|
-
if (cols.length <
|
|
286
|
+
if (cols.length < 5) continue;
|
|
382
287
|
const localAddr = cols[3] ?? "";
|
|
383
288
|
const remoteAddr = cols[4] ?? "";
|
|
384
289
|
const pidField = cols[6] ?? "";
|
|
385
290
|
// Client-side rows: `RemoteAddress` (5th col) is the proxy's listen port;
|
|
386
291
|
// `LocalAddress` (4th col) is the client's ephemeral port.
|
|
387
292
|
if (!remoteAddr.endsWith(proxyToken)) continue;
|
|
388
|
-
const pidMatch = pidField.match(/^(\d+)/);
|
|
293
|
+
const pidMatch = pidField.match(/^(\d+)/) ?? line.match(/pid=(\d+)/);
|
|
389
294
|
if (pidMatch === null) continue;
|
|
390
295
|
const pid = parseInt(pidMatch[1] ?? "", 10);
|
|
391
296
|
if (!Number.isInteger(pid) || pid <= 0) continue;
|
|
@@ -398,6 +303,30 @@ function pickNewestClientPidFromNetstat(lines: string[], proxyPort: number): num
|
|
|
398
303
|
return candidates[0]?.pid ?? null;
|
|
399
304
|
}
|
|
400
305
|
|
|
306
|
+
async function readUnixEstablishedConnectionLines(port: number): Promise<string[]> {
|
|
307
|
+
const commands = [
|
|
308
|
+
{ command: "netstat", args: ["-tanp"] },
|
|
309
|
+
{ command: "ss", args: ["-tanp"] },
|
|
310
|
+
];
|
|
311
|
+
for (const spec of commands) {
|
|
312
|
+
try {
|
|
313
|
+
const { stdout } = await execFileAsync(spec.command, spec.args, {
|
|
314
|
+
timeout: 2000,
|
|
315
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
316
|
+
});
|
|
317
|
+
const portToken = `:${port}`;
|
|
318
|
+
const lines = stdout
|
|
319
|
+
.trim()
|
|
320
|
+
.split("\n")
|
|
321
|
+
.filter((line) => line.includes(portToken) && line.includes("ESTAB"));
|
|
322
|
+
if (lines.length > 0) return lines;
|
|
323
|
+
} catch {
|
|
324
|
+
// Try the next socket table command.
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
|
|
401
330
|
function parseAddressPort(address: string): number | null {
|
|
402
331
|
const match = address.match(/:(\d+)$/);
|
|
403
332
|
if (match === null) return null;
|
|
@@ -448,6 +377,13 @@ export function _pickNewestClientFromWindowsNetstatForTests(
|
|
|
448
377
|
return pickNewestClientFromWindowsNetstat(lines, proxyPort);
|
|
449
378
|
}
|
|
450
379
|
|
|
380
|
+
export function _pickNewestClientPidFromNetstatForTests(
|
|
381
|
+
lines: string[],
|
|
382
|
+
proxyPort: number,
|
|
383
|
+
): number | null {
|
|
384
|
+
return pickNewestClientPidFromNetstat(lines, proxyPort);
|
|
385
|
+
}
|
|
386
|
+
|
|
451
387
|
/** Custom request headers the client (or a wrapper script) may set so we
|
|
452
388
|
* can identify the originating process without scanning the OS connection
|
|
453
389
|
* table. Recognized names:
|
|
@@ -479,7 +415,7 @@ function readIntHeader(headers: Headers, name: string): number | null {
|
|
|
479
415
|
}
|
|
480
416
|
|
|
481
417
|
/** Read client-supplied headers. Returns `null` when no PID header is
|
|
482
|
-
* present
|
|
418
|
+
* present 鈥?caller falls back to OS scanning. */
|
|
483
419
|
function readClientHeaders(request: Request): ClientInfo | null {
|
|
484
420
|
const pid = readIntHeader(request.headers, CLIENT_HEADERS.pid);
|
|
485
421
|
if (pid === null || pid === 0) return null;
|
|
@@ -502,7 +438,7 @@ function readClientHeaders(request: Request): ClientInfo | null {
|
|
|
502
438
|
*
|
|
503
439
|
* Strategy: enumerate the OS-level TCP connections to our proxy's listen
|
|
504
440
|
* port and pick the most recently established client-side connection. This
|
|
505
|
-
* is essentially "who is currently talking to us"
|
|
441
|
+
* is essentially "who is currently talking to us" 鈥?for a session with many
|
|
506
442
|
* rapid-fire requests the same client holds a long-lived connection, so
|
|
507
443
|
* consecutive lookups within the cache TTL converge on the same PID.
|
|
508
444
|
*
|
|
@@ -513,8 +449,8 @@ function readClientHeaders(request: Request): ClientInfo | null {
|
|
|
513
449
|
export async function getClientInfo(request: Request): Promise<ClientInfo> {
|
|
514
450
|
// Preferred path: explicit client identity in headers. AI coding tools
|
|
515
451
|
// (or the wrapper scripts that launch them) attach
|
|
516
|
-
// `X-Agent-Inspector-Client-Pid` (and optionally
|
|
517
|
-
//
|
|
452
|
+
// `X-Agent-Inspector-Client-Pid` (and optionally 鈥?Cwd /
|
|
453
|
+
// 鈥?Project-Folder / 鈥?Port) so we never have to guess.
|
|
518
454
|
const fromHeaders = readClientHeaders(request);
|
|
519
455
|
if (fromHeaders !== null) return fromHeaders;
|
|
520
456
|
|
|
@@ -550,7 +486,7 @@ export async function getClientInfo(request: Request): Promise<ClientInfo> {
|
|
|
550
486
|
}
|
|
551
487
|
|
|
552
488
|
// Fallback: scan the OS connection table for the most recently opened
|
|
553
|
-
// client
|
|
489
|
+
// client鈫抪roxy connection. Best-effort 鈥?in environments with many
|
|
554
490
|
// long-lived keep-alive connections (browsers, background processes) the
|
|
555
491
|
// picked connection may not belong to the current request.
|
|
556
492
|
const cached = getRecentClientFromCache();
|
|
@@ -582,7 +518,7 @@ export async function getClientInfo(request: Request): Promise<ClientInfo> {
|
|
|
582
518
|
* `req.socket.remotePort` (the client's ephemeral port) and needs to know
|
|
583
519
|
* which process opened the connection.
|
|
584
520
|
*
|
|
585
|
-
* Results are cached by `clientPort` for `PORT_LOOKUP_TTL_MS`
|
|
521
|
+
* Results are cached by `clientPort` for `PORT_LOOKUP_TTL_MS` 鈥?a single
|
|
586
522
|
* keep-alive connection typically reuses the same client port for many
|
|
587
523
|
* requests, so we don't want to spawn a subprocess per request.
|
|
588
524
|
*/
|
|
@@ -626,27 +562,24 @@ async function lookupPidByLocalPort(clientPort: number): Promise<number | null>
|
|
|
626
562
|
const platform = process.platform;
|
|
627
563
|
try {
|
|
628
564
|
if (platform === "win32") {
|
|
629
|
-
//
|
|
630
|
-
// `
|
|
631
|
-
//
|
|
632
|
-
// busy machine takes 10+ seconds — longer than our per-request
|
|
633
|
-
// timeout. `netstat -ano` streams its output and `findstr` filters
|
|
634
|
-
// line by line, completing in ~0.5s.
|
|
565
|
+
// `Get-NetTCPConnection -LocalPort` can enumerate the entire socket table
|
|
566
|
+
// before filtering on busy machines. `netstat.exe` streams quickly and
|
|
567
|
+
// avoids PowerShell process startup for this hot lookup path.
|
|
635
568
|
//
|
|
636
|
-
// `netstat -ano` lists every TCP connection twice
|
|
637
|
-
// endpoint. The format is:
|
|
569
|
+
// `netstat -ano` lists every TCP connection twice: once for each endpoint.
|
|
638
570
|
//
|
|
639
571
|
// Proto LocalAddress RemoteAddress State PID
|
|
640
|
-
// TCP [::1]:25949 [::1]:55193 ESTABLISHED 10936
|
|
641
|
-
// TCP [::1]:55193 [::1]:25949 ESTABLISHED 29356
|
|
572
|
+
// TCP [::1]:25949 [::1]:55193 ESTABLISHED 10936 鈫?server-side row (proxy owns the listener)
|
|
573
|
+
// TCP [::1]:55193 [::1]:25949 ESTABLISHED 29356 鈫?client-side row (curl owns the ephemeral port)
|
|
642
574
|
//
|
|
643
575
|
// For `clientPort` (the client's ephemeral port) we want the row
|
|
644
|
-
// whose LOCAL column ends with `:<clientPort>`
|
|
576
|
+
// whose LOCAL column ends with `:<clientPort>` 鈥?that's the row
|
|
645
577
|
// attributed to the client process.
|
|
646
|
-
const { stdout } = await
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
578
|
+
const { stdout } = await execFileAsync("netstat.exe", ["-ano", "-p", "TCP"], {
|
|
579
|
+
windowsHide: true,
|
|
580
|
+
timeout: 2000,
|
|
581
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
582
|
+
});
|
|
650
583
|
const clientToken = `:${clientPort}`;
|
|
651
584
|
for (const rawLine of stdout.split("\n")) {
|
|
652
585
|
const line = rawLine.trim();
|
|
@@ -654,6 +587,8 @@ async function lookupPidByLocalPort(clientPort: number): Promise<number | null>
|
|
|
654
587
|
const cols = line.split(/\s+/);
|
|
655
588
|
if (cols.length < 5) continue;
|
|
656
589
|
const localAddr = cols[1] ?? "";
|
|
590
|
+
const state = cols[3] ?? "";
|
|
591
|
+
if (state !== "ESTABLISHED") continue;
|
|
657
592
|
if (!localAddr.endsWith(clientToken)) continue;
|
|
658
593
|
const pidStr = cols[cols.length - 1] ?? "";
|
|
659
594
|
const parsed = parseInt(pidStr, 10);
|
|
@@ -665,9 +600,13 @@ async function lookupPidByLocalPort(clientPort: number): Promise<number | null>
|
|
|
665
600
|
// CAP_NET_ADMIN to see PIDs of processes owned by other users; when
|
|
666
601
|
// permission is denied the command exits non-zero and we return null.
|
|
667
602
|
try {
|
|
668
|
-
const { stdout } = await
|
|
669
|
-
|
|
670
|
-
|
|
603
|
+
const { stdout } = await execFileAsync(
|
|
604
|
+
"ss",
|
|
605
|
+
["-tanp", "state", "established", "sport", `= :${clientPort}`],
|
|
606
|
+
{
|
|
607
|
+
timeout: 2000,
|
|
608
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
609
|
+
},
|
|
671
610
|
);
|
|
672
611
|
const match = stdout.match(/pid=(\d+)/);
|
|
673
612
|
if (match !== null) {
|
|
@@ -678,10 +617,12 @@ async function lookupPidByLocalPort(clientPort: number): Promise<number | null>
|
|
|
678
617
|
// fall through
|
|
679
618
|
}
|
|
680
619
|
try {
|
|
681
|
-
const { stdout } = await
|
|
682
|
-
|
|
620
|
+
const { stdout } = await execFileAsync(
|
|
621
|
+
"lsof",
|
|
622
|
+
["-ti", `tcp:${clientPort}`, "-sTCP:ESTABLISHED"],
|
|
683
623
|
{
|
|
684
|
-
|
|
624
|
+
timeout: 2000,
|
|
625
|
+
maxBuffer: 64 * 1024,
|
|
685
626
|
},
|
|
686
627
|
);
|
|
687
628
|
const trimmed = stdout.trim().split("\n")[0] ?? "";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { startEcosystemTask } from "../../proxy/ecosystemTasks";
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute("/api/ecosystem/packages/$packageId/help")({
|
|
5
|
+
server: {
|
|
6
|
+
handlers: {
|
|
7
|
+
POST: ({ params }: { params: { packageId: string } }) => {
|
|
8
|
+
const task = startEcosystemTask(params.packageId, "help");
|
|
9
|
+
if (task === null) {
|
|
10
|
+
return Response.json({ error: "Unknown ecosystem package" }, { status: 404 });
|
|
11
|
+
}
|
|
12
|
+
return Response.json({ task });
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { startEcosystemTask } from "../../proxy/ecosystemTasks";
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute("/api/ecosystem/packages/$packageId/install")({
|
|
5
|
+
server: {
|
|
6
|
+
handlers: {
|
|
7
|
+
POST: ({ params }: { params: { packageId: string } }) => {
|
|
8
|
+
const task = startEcosystemTask(params.packageId, "install");
|
|
9
|
+
if (task === null) {
|
|
10
|
+
return Response.json({ error: "Unknown ecosystem package" }, { status: 404 });
|
|
11
|
+
}
|
|
12
|
+
return Response.json({ task });
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { startEcosystemTask } from "../../proxy/ecosystemTasks";
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute("/api/ecosystem/packages/$packageId/runner-presets")({
|
|
5
|
+
server: {
|
|
6
|
+
handlers: {
|
|
7
|
+
POST: ({ params }: { params: { packageId: string } }) => {
|
|
8
|
+
const task = startEcosystemTask(params.packageId, "runner-presets");
|
|
9
|
+
if (task === null) {
|
|
10
|
+
return Response.json(
|
|
11
|
+
{ error: "Runner presets are only available for Eval Harness" },
|
|
12
|
+
{ status: 404 },
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return Response.json({ task });
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { startEcosystemTask } from "../../proxy/ecosystemTasks";
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute("/api/ecosystem/packages/$packageId/upgrade")({
|
|
5
|
+
server: {
|
|
6
|
+
handlers: {
|
|
7
|
+
POST: ({ params }: { params: { packageId: string } }) => {
|
|
8
|
+
const task = startEcosystemTask(params.packageId, "upgrade");
|
|
9
|
+
if (task === null) {
|
|
10
|
+
return Response.json({ error: "Unknown ecosystem package" }, { status: 404 });
|
|
11
|
+
}
|
|
12
|
+
return Response.json({ task });
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|