genexus-mcp 3.8.0 → 3.9.1

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.
@@ -0,0 +1,645 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const crypto = require('crypto');
7
+ const { execFileSync } = require('child_process');
8
+
9
+ function resolveDefaultRuntimeRoot() {
10
+ if (process.env.GENEXUS_MCP_RUNTIME_DIR) {
11
+ return process.env.GENEXUS_MCP_RUNTIME_DIR;
12
+ }
13
+ const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
14
+ return path.join(localAppData, 'GenexusMCP', 'runtime');
15
+ }
16
+
17
+ function isCheckoutMode(packageRoot) {
18
+ try {
19
+ const gitMarker = fs.statSync(path.join(packageRoot, '.git'));
20
+ return gitMarker.isDirectory() || gitMarker.isFile();
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ function shouldStage(packageRoot = path.resolve(__dirname, '..', '..')) {
27
+ if (process.env.GENEXUS_MCP_GATEWAY_EXE) {
28
+ return false;
29
+ }
30
+ if (process.env.GENEXUS_MCP_NO_STAGING === '1') {
31
+ return false;
32
+ }
33
+ if (process.env.GENEXUS_MCP_FORCE_STAGING === '1') {
34
+ return true;
35
+ }
36
+ return !isCheckoutMode(packageRoot);
37
+ }
38
+
39
+ function computeFileSha256(filePath) {
40
+ const hash = crypto.createHash('sha256');
41
+ const buffer = fs.readFileSync(filePath);
42
+ hash.update(buffer);
43
+ return hash.digest('hex');
44
+ }
45
+
46
+ function computePublishFingerprint(publishDir) {
47
+ const manifestPath = path.join(publishDir, 'gxmcp-manifest.json');
48
+ if (fs.existsSync(manifestPath)) {
49
+ try {
50
+ return computeFileSha256(manifestPath).slice(0, 8);
51
+ } catch {
52
+ // fallback
53
+ }
54
+ }
55
+ const gatewayExe = path.join(publishDir, 'GxMcp.Gateway.exe');
56
+ if (fs.existsSync(gatewayExe)) {
57
+ try {
58
+ return computeFileSha256(gatewayExe).slice(0, 8);
59
+ } catch {
60
+ // fallback
61
+ }
62
+ }
63
+ return '00000000';
64
+ }
65
+
66
+ function verifyManifest(stagedDir, manifestPath) {
67
+ if (!fs.existsSync(manifestPath)) {
68
+ return;
69
+ }
70
+ let manifest;
71
+ try {
72
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
73
+ } catch (err) {
74
+ throw new Error(`Failed to parse manifest at ${manifestPath}: ${err.message}`);
75
+ }
76
+
77
+ const artifacts = Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
78
+ for (const artifact of artifacts) {
79
+ if (!artifact || !artifact.path) continue;
80
+ const fullPath = path.join(stagedDir, artifact.path);
81
+ if (!fs.existsSync(fullPath)) {
82
+ throw new Error(`Manifest verification failed: missing artifact '${artifact.path}'`);
83
+ }
84
+ if (typeof artifact.size === 'number') {
85
+ const stat = fs.statSync(fullPath);
86
+ if (stat.size !== artifact.size) {
87
+ throw new Error(`Manifest verification failed: size mismatch for '${artifact.path}' (expected ${artifact.size}, got ${stat.size})`);
88
+ }
89
+ }
90
+ if (artifact.sha256) {
91
+ const actualSha = computeFileSha256(fullPath);
92
+ if (actualSha.toLowerCase() !== artifact.sha256.toLowerCase()) {
93
+ throw new Error(`Manifest verification failed: sha256 mismatch for '${artifact.path}'`);
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ function isPathWithin(candidatePath, rootPath) {
100
+ if (!candidatePath || !rootPath) return false;
101
+ const relative = path.relative(path.resolve(rootPath), path.resolve(candidatePath));
102
+ return relative === '' || (
103
+ relative !== '..'
104
+ && !relative.startsWith(`..${path.sep}`)
105
+ && !path.isAbsolute(relative)
106
+ );
107
+ }
108
+
109
+ function getRuntimeProcessUsage(runtimeRoot, processes, runtimeDirectories = null) {
110
+ const runtimes = Array.isArray(runtimeDirectories)
111
+ ? runtimeDirectories
112
+ : listStagedRuntimes(runtimeRoot);
113
+ const usage = new Map();
114
+
115
+ for (const runtime of runtimes) {
116
+ const pids = (Array.isArray(processes) ? processes : [])
117
+ .filter((proc) => proc && isPathWithin(proc.exePath, runtime.path))
118
+ .map((proc) => Number(proc.pid))
119
+ .filter((pid) => Number.isInteger(pid) && pid > 0)
120
+ .sort((a, b) => a - b);
121
+ if (pids.length > 0) {
122
+ usage.set(path.resolve(runtime.path).toLowerCase(), {
123
+ name: runtime.name,
124
+ path: runtime.path,
125
+ pids
126
+ });
127
+ }
128
+ }
129
+
130
+ return Array.from(usage.values()).sort((a, b) => a.name.localeCompare(b.name));
131
+ }
132
+
133
+ function createRuntimeCleanupResult() {
134
+ return {
135
+ deleted: [],
136
+ skipped: [],
137
+ failed: [],
138
+ processProbeAvailable: true
139
+ };
140
+ }
141
+
142
+ function readRuntimeProcessSnapshot(processProvider) {
143
+ let snapshot = processProvider();
144
+ if (snapshot && !Array.isArray(snapshot) && Array.isArray(snapshot.processes)) {
145
+ if (snapshot.ok === false) throw new Error(snapshot.error || 'runtime process probe failed');
146
+ snapshot = snapshot.processes;
147
+ }
148
+ if (!Array.isArray(snapshot)) throw new Error('runtime process probe returned no process list');
149
+ if (snapshot.some((proc) => !proc || !proc.exePath)) {
150
+ throw new Error('runtime process probe returned a process without an executable path');
151
+ }
152
+ return snapshot;
153
+ }
154
+
155
+ function processPidsUnder(directory, processes) {
156
+ return (Array.isArray(processes) ? processes : [])
157
+ .filter((proc) => proc && isPathWithin(proc.exePath, directory))
158
+ .map((proc) => Number(proc.pid))
159
+ .filter((pid) => Number.isInteger(pid) && pid > 0)
160
+ .sort((a, b) => a - b);
161
+ }
162
+
163
+ function processPidsUnderAny(directories, processes) {
164
+ const paths = Array.isArray(directories) ? directories.filter(Boolean) : [directories].filter(Boolean);
165
+ return [...new Set(paths.flatMap((directory) => processPidsUnder(directory, processes)))]
166
+ .sort((a, b) => a - b);
167
+ }
168
+
169
+ function originalPathForTombstone(tombstonePath) {
170
+ if (typeof tombstonePath !== 'string') return null;
171
+ const marker = tombstonePath.lastIndexOf('.deleting-');
172
+ return marker > 0 ? tombstonePath.slice(0, marker) : null;
173
+ }
174
+
175
+ function cleanupRuntimeDirectory(directory, options = {}) {
176
+ const result = createRuntimeCleanupResult();
177
+ if (!directory || !fs.existsSync(directory)) return result;
178
+
179
+ const processProvider = typeof options.getRunningProcesses === 'function'
180
+ ? options.getRunningProcesses
181
+ : probeRunningGxMcpProcesses;
182
+ const rmSync = typeof options.rmSync === 'function' ? options.rmSync : fs.rmSync;
183
+ const removeOptions = { recursive: true, force: true, maxRetries: 3, retryDelay: 100 };
184
+ const probePaths = [
185
+ directory,
186
+ ...(Array.isArray(options.additionalProbePaths) ? options.additionalProbePaths : [])
187
+ ].filter(Boolean);
188
+
189
+ let initialSnapshot;
190
+ try {
191
+ initialSnapshot = readRuntimeProcessSnapshot(processProvider);
192
+ } catch (err) {
193
+ result.processProbeAvailable = false;
194
+ result.skipped.push({
195
+ path: directory,
196
+ reason: 'process-probe-unavailable',
197
+ pids: [],
198
+ error: err && err.message ? err.message : String(err)
199
+ });
200
+ return result;
201
+ }
202
+
203
+ const initialPids = processPidsUnderAny(probePaths, initialSnapshot);
204
+ if (initialPids.length > 0) {
205
+ result.skipped.push({ path: directory, reason: 'in-use', pids: initialPids });
206
+ return result;
207
+ }
208
+
209
+ let finalSnapshot;
210
+ try {
211
+ finalSnapshot = readRuntimeProcessSnapshot(processProvider);
212
+ } catch (err) {
213
+ result.processProbeAvailable = false;
214
+ result.skipped.push({
215
+ path: directory,
216
+ reason: 'process-probe-unavailable-before-delete',
217
+ pids: [],
218
+ error: err && err.message ? err.message : String(err)
219
+ });
220
+ return result;
221
+ }
222
+
223
+ const finalPids = processPidsUnderAny(probePaths, finalSnapshot);
224
+ if (finalPids.length > 0) {
225
+ result.skipped.push({ path: directory, reason: 'in-use-before-delete', pids: finalPids });
226
+ return result;
227
+ }
228
+
229
+ try {
230
+ rmSync(directory, removeOptions);
231
+ result.deleted.push(directory);
232
+ } catch (err) {
233
+ result.failed.push({ path: directory, error: err && err.message ? err.message : String(err) });
234
+ }
235
+ return result;
236
+ }
237
+
238
+ function mergeRuntimeCleanupResults(target, additional) {
239
+ if (!additional) return target;
240
+ target.deleted.push(...additional.deleted);
241
+ target.skipped.push(...additional.skipped);
242
+ target.failed.push(...additional.failed);
243
+ target.processProbeAvailable = target.processProbeAvailable && additional.processProbeAvailable;
244
+ return target;
245
+ }
246
+
247
+ function cleanOldRuntimes(runtimeRoot, currentTargetDir, keepCount = 2, options = {}) {
248
+ const result = createRuntimeCleanupResult();
249
+
250
+ const processProvider = typeof options.getRunningProcesses === 'function'
251
+ ? options.getRunningProcesses
252
+ : probeRunningGxMcpProcesses;
253
+ const renameSync = typeof options.renameSync === 'function' ? options.renameSync : fs.renameSync;
254
+ const rmSync = typeof options.rmSync === 'function' ? options.rmSync : fs.rmSync;
255
+ const removeOptions = { recursive: true, force: true, maxRetries: 3, retryDelay: 100 };
256
+
257
+ const readProcessSnapshot = () => readRuntimeProcessSnapshot(processProvider);
258
+
259
+ const probeImmediatelyBeforeDelete = (directory) => {
260
+ try {
261
+ const snapshot = readProcessSnapshot();
262
+ return { ok: true, pids: processPidsUnder(directory, snapshot) };
263
+ } catch (err) {
264
+ return { ok: false, pids: [], error: err && err.message ? err.message : String(err) };
265
+ }
266
+ };
267
+
268
+ try {
269
+ if (!fs.existsSync(runtimeRoot)) return result;
270
+ const entries = fs.readdirSync(runtimeRoot, { withFileTypes: true });
271
+ const now = Date.now();
272
+ const versionDirs = [];
273
+ const tmpDirs = [];
274
+ const tombstoneDirs = [];
275
+
276
+ for (const entry of entries) {
277
+ if (!entry.isDirectory()) continue;
278
+ const fullPath = path.join(runtimeRoot, entry.name);
279
+ if (entry.name.includes('.tmp-')) {
280
+ tmpDirs.push(fullPath);
281
+ continue;
282
+ }
283
+ if (entry.name.includes('.deleting-')) {
284
+ tombstoneDirs.push(fullPath);
285
+ continue;
286
+ }
287
+ try {
288
+ const stat = fs.statSync(fullPath);
289
+ versionDirs.push({ name: entry.name, path: fullPath, mtimeMs: stat.mtimeMs });
290
+ } catch {
291
+ // Ignore an entry that disappeared during enumeration.
292
+ }
293
+ }
294
+
295
+ versionDirs.sort((a, b) => b.mtimeMs - a.mtimeMs);
296
+ const normalizedCurrent = path.resolve(currentTargetDir).toLowerCase();
297
+ const keep = Number.isFinite(Number(keepCount)) ? Math.max(0, Number(keepCount)) : 2;
298
+ let kept = 0;
299
+ const toDelete = [];
300
+ for (const vdir of versionDirs) {
301
+ if (path.resolve(vdir.path).toLowerCase() === normalizedCurrent) continue;
302
+ if (kept < keep) kept++;
303
+ else toDelete.push(vdir.path);
304
+ }
305
+
306
+ const staleTmpDirs = tmpDirs.filter((dir) => {
307
+ try { return now - fs.statSync(dir).mtimeMs > 10 * 60 * 1000; } catch { return false; }
308
+ });
309
+ if (toDelete.length === 0 && staleTmpDirs.length === 0 && tombstoneDirs.length === 0) {
310
+ return result;
311
+ }
312
+
313
+ // Finish tombstones and stale staging directories with the same
314
+ // fail-closed process protection used by immediate staging cleanup.
315
+ for (const tombstone of tombstoneDirs) {
316
+ const originalPath = originalPathForTombstone(tombstone);
317
+ mergeRuntimeCleanupResults(result, cleanupRuntimeDirectory(tombstone, {
318
+ ...options,
319
+ additionalProbePaths: originalPath ? [originalPath] : []
320
+ }));
321
+ }
322
+ for (const tmpDir of staleTmpDirs) {
323
+ mergeRuntimeCleanupResults(result, cleanupRuntimeDirectory(tmpDir, options));
324
+ }
325
+
326
+ if (toDelete.length === 0) return result;
327
+ let processSnapshot;
328
+ try {
329
+ processSnapshot = readProcessSnapshot();
330
+ } catch (err) {
331
+ result.processProbeAvailable = false;
332
+ const error = err && err.message ? err.message : String(err);
333
+ for (const dir of toDelete) {
334
+ result.skipped.push({ path: dir, reason: 'process-probe-unavailable', pids: [], error });
335
+ }
336
+ return result;
337
+ }
338
+
339
+ const initialUsage = new Map(
340
+ getRuntimeProcessUsage(runtimeRoot, processSnapshot, versionDirs)
341
+ .map((entry) => [path.resolve(entry.path).toLowerCase(), entry])
342
+ );
343
+
344
+ for (const dirToDelete of toDelete) {
345
+ let freshSnapshot;
346
+ try {
347
+ freshSnapshot = readProcessSnapshot();
348
+ } catch (err) {
349
+ result.skipped.push({
350
+ path: dirToDelete,
351
+ reason: 'process-probe-unavailable',
352
+ pids: [],
353
+ error: err && err.message ? err.message : String(err)
354
+ });
355
+ continue;
356
+ }
357
+
358
+ const freshUsage = getRuntimeProcessUsage(runtimeRoot, freshSnapshot, versionDirs)
359
+ .find((entry) => path.resolve(entry.path).toLowerCase() === path.resolve(dirToDelete).toLowerCase());
360
+ if (freshUsage || initialUsage.has(path.resolve(dirToDelete).toLowerCase())) {
361
+ const usage = freshUsage || initialUsage.get(path.resolve(dirToDelete).toLowerCase());
362
+ result.skipped.push({ path: dirToDelete, reason: 'in-use', pids: usage.pids });
363
+ continue;
364
+ }
365
+
366
+ // Rename first. The post-rename probe below closes the normal
367
+ // TOCTOU window; a process that appears while retiring is restored
368
+ // to its original name before any recursive removal is attempted.
369
+ const deletingPath = `${dirToDelete}.deleting-${process.pid}-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
370
+ try {
371
+ renameSync(dirToDelete, deletingPath);
372
+ } catch (err) {
373
+ result.failed.push({ path: dirToDelete, error: err && err.message ? err.message : String(err) });
374
+ continue;
375
+ }
376
+
377
+ let postSnapshot;
378
+ try {
379
+ postSnapshot = readProcessSnapshot();
380
+ } catch (err) {
381
+ try { renameSync(deletingPath, dirToDelete); } catch { }
382
+ result.skipped.push({
383
+ path: dirToDelete,
384
+ reason: 'process-probe-unavailable-after-rename',
385
+ pids: [],
386
+ error: err && err.message ? err.message : String(err)
387
+ });
388
+ continue;
389
+ }
390
+ const postPids = [
391
+ ...processPidsUnder(dirToDelete, postSnapshot),
392
+ ...processPidsUnder(deletingPath, postSnapshot)
393
+ ].filter((pid, index, all) => all.indexOf(pid) === index).sort((a, b) => a - b);
394
+ if (postPids.length > 0) {
395
+ let restored = true;
396
+ try { renameSync(deletingPath, dirToDelete); } catch { restored = false; }
397
+ result.skipped.push({
398
+ path: dirToDelete,
399
+ reason: 'in-use-after-rename',
400
+ pids: postPids,
401
+ restored
402
+ });
403
+ continue;
404
+ }
405
+ const finalDeletingProbe = probeImmediatelyBeforeDelete(deletingPath);
406
+ if (!finalDeletingProbe.ok) {
407
+ let restored = true;
408
+ try { renameSync(deletingPath, dirToDelete); } catch { restored = false; }
409
+ result.skipped.push({
410
+ path: dirToDelete,
411
+ reason: 'process-probe-unavailable-before-delete',
412
+ pids: [],
413
+ error: finalDeletingProbe.error,
414
+ restored
415
+ });
416
+ continue;
417
+ }
418
+ if (finalDeletingProbe.pids.length > 0) {
419
+ let restored = true;
420
+ try { renameSync(deletingPath, dirToDelete); } catch { restored = false; }
421
+ result.skipped.push({
422
+ path: dirToDelete,
423
+ reason: 'in-use-before-delete',
424
+ pids: finalDeletingProbe.pids,
425
+ restored
426
+ });
427
+ continue;
428
+ }
429
+
430
+ try {
431
+ rmSync(deletingPath, removeOptions);
432
+ result.deleted.push(dirToDelete);
433
+ } catch (err) {
434
+ result.failed.push({
435
+ path: dirToDelete,
436
+ deletingPath,
437
+ error: err && err.message ? err.message : String(err)
438
+ });
439
+ }
440
+ }
441
+ } catch (err) {
442
+ result.failed.push({ path: runtimeRoot, error: err && err.message ? err.message : String(err) });
443
+ }
444
+
445
+ return result;
446
+ }
447
+
448
+ function ensureStagedGateway(options = {}) {
449
+ const packageRoot = options.packageRoot || path.resolve(__dirname, '..', '..');
450
+ const publishDir = options.publishDir || path.join(packageRoot, 'publish');
451
+ const defaultGatewayExe = options.defaultGatewayExe || path.join(publishDir, 'GxMcp.Gateway.exe');
452
+
453
+ if (!options.forceStaging && !shouldStage(packageRoot)) {
454
+ return {
455
+ staged: false,
456
+ gatewayExePath: process.env.GENEXUS_MCP_GATEWAY_EXE || defaultGatewayExe,
457
+ runtimeDir: path.dirname(process.env.GENEXUS_MCP_GATEWAY_EXE || defaultGatewayExe),
458
+ reason: process.env.GENEXUS_MCP_GATEWAY_EXE ? 'GENEXUS_MCP_GATEWAY_EXE' : (process.env.GENEXUS_MCP_NO_STAGING === '1' ? 'NO_STAGING' : 'checkout'),
459
+ cleanup: null
460
+ };
461
+ }
462
+
463
+ let version = '0.0.0';
464
+ try {
465
+ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
466
+ if (pkg.version) version = pkg.version;
467
+ } catch {
468
+ // Fallback
469
+ }
470
+
471
+ const sha8 = computePublishFingerprint(publishDir);
472
+ const runtimeRoot = options.runtimeRoot || resolveDefaultRuntimeRoot();
473
+ const targetDir = path.join(runtimeRoot, `${version}-${sha8}`);
474
+ const stagedGatewayExe = path.join(targetDir, 'GxMcp.Gateway.exe');
475
+
476
+ if (fs.existsSync(stagedGatewayExe)) {
477
+ const cleanup = cleanOldRuntimes(runtimeRoot, targetDir, 2, options);
478
+ return {
479
+ staged: true,
480
+ gatewayExePath: stagedGatewayExe,
481
+ runtimeDir: targetDir,
482
+ fresh: false,
483
+ cleanup
484
+ };
485
+ }
486
+
487
+ if (!fs.existsSync(publishDir)) {
488
+ throw new Error(`Cannot stage runtime: publish directory not found at ${publishDir}`);
489
+ }
490
+
491
+ fs.mkdirSync(runtimeRoot, { recursive: true });
492
+ const tmpDir = `${targetDir}.tmp-${process.pid}-${Date.now()}`;
493
+ fs.mkdirSync(tmpDir, { recursive: true });
494
+ let temporaryCleanup = null;
495
+
496
+ try {
497
+ fs.cpSync(publishDir, tmpDir, { recursive: true });
498
+ const manifestPath = path.join(tmpDir, 'gxmcp-manifest.json');
499
+ verifyManifest(tmpDir, manifestPath);
500
+
501
+ try {
502
+ fs.renameSync(tmpDir, targetDir);
503
+ } catch (renameErr) {
504
+ // Concurrent launcher might have completed the rename first
505
+ if (fs.existsSync(stagedGatewayExe)) {
506
+ temporaryCleanup = cleanupRuntimeDirectory(tmpDir, options);
507
+ } else {
508
+ throw renameErr;
509
+ }
510
+ }
511
+ } catch (err) {
512
+ const failure = err instanceof Error ? err : new Error(String(err));
513
+ const cleanup = cleanupRuntimeDirectory(tmpDir, options);
514
+ failure.runtimeCleanup = cleanup;
515
+ const notes = [];
516
+ if (cleanup.skipped.length > 0) {
517
+ const reasons = cleanup.skipped
518
+ .map((entry) => `${entry.reason}${entry.pids?.length ? ` (PIDs: ${entry.pids.join(', ')})` : ''}`)
519
+ .join(', ');
520
+ notes.push(`skipped ${cleanup.skipped.length} path(s): ${reasons}`);
521
+ }
522
+ if (cleanup.failed.length > 0) {
523
+ notes.push(`failed ${cleanup.failed.length} path(s)`);
524
+ }
525
+ if (!cleanup.processProbeAvailable) {
526
+ notes.push('process probe unavailable');
527
+ }
528
+ if (notes.length > 0) {
529
+ failure.message = `${failure.message} Runtime cleanup ${notes.join('; ')}.`;
530
+ }
531
+ throw failure;
532
+ }
533
+
534
+ const cleanup = mergeRuntimeCleanupResults(
535
+ cleanOldRuntimes(runtimeRoot, targetDir, 2, options),
536
+ temporaryCleanup
537
+ );
538
+
539
+ return {
540
+ staged: true,
541
+ gatewayExePath: stagedGatewayExe,
542
+ runtimeDir: targetDir,
543
+ fresh: true,
544
+ cleanup
545
+ };
546
+ }
547
+
548
+ function listStagedRuntimes(runtimeRoot = resolveDefaultRuntimeRoot()) {
549
+ if (!fs.existsSync(runtimeRoot)) return [];
550
+ try {
551
+ const entries = fs.readdirSync(runtimeRoot, { withFileTypes: true });
552
+ return entries
553
+ .filter((e) => e.isDirectory() && !e.name.includes('.tmp-') && !e.name.includes('.deleting-'))
554
+ .map((e) => {
555
+ const dirPath = path.join(runtimeRoot, e.name);
556
+ let stat = null;
557
+ try {
558
+ stat = fs.statSync(dirPath);
559
+ } catch {
560
+ // Ignore
561
+ }
562
+ return {
563
+ name: e.name,
564
+ path: dirPath,
565
+ hasGateway: fs.existsSync(path.join(dirPath, 'GxMcp.Gateway.exe')),
566
+ mtime: stat ? stat.mtime : null
567
+ };
568
+ });
569
+ } catch {
570
+ return [];
571
+ }
572
+ }
573
+
574
+ function probeRunningGxMcpProcesses() {
575
+ if (process.platform !== 'win32') {
576
+ return { ok: true, processes: [] };
577
+ }
578
+ try {
579
+ // Query CIM Win32_Process for GxMcp processes. Keep the probe result
580
+ // distinguishable from a successful empty result: GC must fail closed
581
+ // when Windows denies the query.
582
+ const output = execFileSync('powershell.exe', [
583
+ '-NoProfile',
584
+ '-NonInteractive',
585
+ '-Command',
586
+ `Get-CimInstance Win32_Process -Filter "name like 'GxMcp%'" | Select-Object ProcessId, ExecutablePath, CommandLine | ConvertTo-Json -Compress`
587
+ ], { encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
588
+
589
+ if (!output) return { ok: true, processes: [] };
590
+ let data;
591
+ try {
592
+ data = JSON.parse(output);
593
+ } catch (err) {
594
+ return { ok: false, processes: [], error: `invalid process probe JSON: ${err.message}` };
595
+ }
596
+ const rows = Array.isArray(data) ? data : [data];
597
+ const processes = rows.filter(Boolean).map((r) => ({
598
+ pid: r.ProcessId,
599
+ exePath: r.ExecutablePath || '',
600
+ commandLine: r.CommandLine || ''
601
+ }));
602
+ const unresolved = processes.filter((proc) => !proc.exePath);
603
+ if (unresolved.length > 0) {
604
+ return {
605
+ ok: false,
606
+ processes,
607
+ error: `${unresolved.length} GxMcp process record(s) had no executable path; runtime GC is fail-closed.`
608
+ };
609
+ }
610
+ return { ok: true, processes };
611
+ } catch (err) {
612
+ return { ok: false, processes: [], error: err && err.message ? err.message : String(err) };
613
+ }
614
+ }
615
+
616
+ function getRunningGxMcpProcesses() {
617
+ return probeRunningGxMcpProcesses().processes;
618
+ }
619
+
620
+ function classifyProcessRuntime(exePath, runtimeRoot = resolveDefaultRuntimeRoot()) {
621
+ if (!exePath) return 'unknown';
622
+ if (isPathWithin(exePath, runtimeRoot)) {
623
+ return 'staged';
624
+ }
625
+ const norm = path.resolve(exePath).toLowerCase();
626
+ if (norm.includes('npm-cache') || norm.includes('_npx') || norm.includes('node_modules')) {
627
+ return 'npx-cache';
628
+ }
629
+ return 'other';
630
+ }
631
+
632
+ module.exports = {
633
+ resolveDefaultRuntimeRoot,
634
+ isCheckoutMode,
635
+ shouldStage,
636
+ computePublishFingerprint,
637
+ verifyManifest,
638
+ cleanOldRuntimes,
639
+ ensureStagedGateway,
640
+ listStagedRuntimes,
641
+ getRunningGxMcpProcesses,
642
+ probeRunningGxMcpProcesses,
643
+ getRuntimeProcessUsage,
644
+ classifyProcessRuntime
645
+ };