Surface Windows MCP launcher drift in doctor

Windows npm updates can leave older GitNexus MCP Node processes running from npx caches or stale package roots, which makes the next install/update look broken when native files stay locked. The doctor command now reports Windows MCP process sources so users can restart editors/agents or stop current MCP servers before retrying the update.

Constraint: Windows package updates can leave node-based MCP servers holding native addon files open

Rejected: Auto-stop GitNexus MCP processes from doctor | too side-effectful for a diagnostic command

Confidence: medium

Scope-risk: narrow

Directive: Keep the Windows process scan best-effort and non-fatal; doctor must still work if PowerShell or CIM process lookup is unavailable

Tested: npx vitest run test/unit/doctor-format.test.ts; npx tsc --noEmit; npm run build; npx eslint gitnexus/src/cli/doctor.ts gitnexus/test/unit/doctor-format.test.ts gitnexus/scripts/cross-platform-tests.ts; git diff --check; GitNexus detect_changes

Not-tested: Full npm test passes on this Windows shell; broader unit and cross-platform suites have unrelated Windows/env failures noted in the PR
This commit is contained in:
abundantbeing 2026-06-19 05:31:09 +02:00
parent fff01189b1
commit e61b3e3278
3 changed files with 180 additions and 1 deletions

View file

@ -30,6 +30,7 @@ const PLATFORM_LOGIC = [
'test/unit/setup-jsonc.test.ts',
'test/unit/setup-codex.test.ts',
'test/unit/setup-antigravity.test.ts',
'test/unit/doctor-format.test.ts',
'test/unit/resolve-invocation.test.ts',
'test/unit/platform-capabilities.test.ts',
'test/unit/worker-pool-windows-quarantine.test.ts',

View file

@ -1,3 +1,6 @@
import { execFile } from 'node:child_process';
import path from 'node:path';
import { promisify } from 'node:util';
import { getRuntimeCapabilities, getRuntimeFingerprint } from '../core/platform/capabilities.js';
import { resolveEmbeddingConfig } from '../core/embeddings/config.js';
import { isHttpMode } from '../core/embeddings/http-client.js';
@ -6,6 +9,8 @@ import { checkLbugNative } from '../core/lbug/native-check.js';
import { getExtensionInstallPolicy } from '../core/lbug/extension-loader.js';
import { t } from './i18n/index.js';
const execFileAsync = promisify(execFile);
function isCombiningMark(codePoint: number): boolean {
return (
(codePoint >= 0x0300 && codePoint <= 0x036f) ||
@ -78,6 +83,114 @@ export function localEmbeddingDoctorStatus(opts: {
return { status: '✓ local embeddings supported', detail: null };
}
export type WindowsGitNexusMcpProcess = {
pid: number;
commandLine: string;
};
export type WindowsGitNexusMcpStatus = {
status: string;
detail: string | null;
};
function normalizePathForMatch(value: string): string {
return value.replace(/\\/g, '/').toLowerCase();
}
function currentPackageRootFromArgv(argv1: string | undefined): string | null {
if (!argv1) return null;
const normalized = path.resolve(argv1);
const parts = normalized.split(path.sep);
const packageIndex = parts.map((part) => part.toLowerCase()).lastIndexOf('gitnexus');
if (packageIndex < 0) return null;
return parts.slice(0, packageIndex + 1).join(path.sep);
}
function parseWindowsProcessJson(stdout: string): WindowsGitNexusMcpProcess[] {
const trimmed = stdout.trim();
if (!trimmed) return [];
const parsed = JSON.parse(trimmed) as
| { pid?: number; ProcessId?: number; commandLine?: string; CommandLine?: string }
| Array<{ pid?: number; ProcessId?: number; commandLine?: string; CommandLine?: string }>;
const rows = Array.isArray(parsed) ? parsed : [parsed];
return rows
.map((row) => ({
pid: Number(row.pid ?? row.ProcessId),
commandLine: String(row.commandLine ?? row.CommandLine ?? ''),
}))
.filter((row) => Number.isFinite(row.pid) && row.commandLine.length > 0);
}
export async function listWindowsGitNexusMcpProcesses(
opts: {
platform?: NodeJS.Platform;
} = {},
): Promise<WindowsGitNexusMcpProcess[]> {
if ((opts.platform ?? process.platform) !== 'win32') return [];
const script = [
"$ErrorActionPreference = 'Stop'",
'Get-CimInstance Win32_Process -Filter "name = \'node.exe\'"',
" | Where-Object { $_.CommandLine -match 'gitnexus' -and $_.CommandLine -match '\\bmcp\\b' }",
' | Select-Object @{Name="pid";Expression={$_.ProcessId}}, @{Name="commandLine";Expression={$_.CommandLine}}',
' | ConvertTo-Json -Compress',
].join('\n');
try {
const { stdout } = await execFileAsync(
'powershell.exe',
['-NoProfile', '-NonInteractive', '-Command', script],
{ timeout: 1500, windowsHide: true, maxBuffer: 1024 * 1024 },
);
return parseWindowsProcessJson(stdout);
} catch {
return [];
}
}
export function windowsGitNexusMcpDoctorStatus(opts: {
processes: WindowsGitNexusMcpProcess[];
currentPackageRoot?: string | null;
}): WindowsGitNexusMcpStatus {
if (opts.processes.length === 0) {
return { status: 'none running', detail: null };
}
const currentRoot = opts.currentPackageRoot
? normalizePathForMatch(opts.currentPackageRoot)
: null;
let currentInstall = 0;
let npxCache = 0;
let otherInstall = 0;
for (const proc of opts.processes) {
const command = normalizePathForMatch(proc.commandLine);
if (currentRoot && command.includes(currentRoot)) {
currentInstall += 1;
} else if (command.includes('/_npx/') || command.includes('/npx-cli.js')) {
npxCache += 1;
} else {
otherInstall += 1;
}
}
const launchers = [
currentInstall > 0 ? `${currentInstall} current` : null,
npxCache > 0 ? `${npxCache} npx-cache` : null,
otherInstall > 0 ? `${otherInstall} other` : null,
].filter(Boolean);
const detail =
npxCache > 0 || otherInstall > 0
? 'Restart editors/agents after updating GitNexus so MCP servers reload the same package version.'
: 'Stop running MCP servers before npm updates if cleanup reports locked native files.';
return {
status: `${opts.processes.length} running (${launchers.join(', ')})`,
detail,
};
}
export const doctorCommand = async () => {
const fingerprint = getRuntimeFingerprint();
const capabilities = getRuntimeCapabilities();
@ -139,4 +252,17 @@ export const doctorCommand = async () => {
if (support.detail) {
process.stderr.write(`\n${support.detail.replace(/^/gm, ' ')}\n\n`);
}
if (process.platform === 'win32') {
const mcpProcesses = await listWindowsGitNexusMcpProcesses();
const mcpStatus = windowsGitNexusMcpDoctorStatus({
processes: mcpProcesses,
currentPackageRoot: currentPackageRootFromArgv(process.argv[1]),
});
console.log('');
console.log('Windows MCP');
console.log(` ${padDisplayEnd('Processes:', 18)}${mcpStatus.status}`);
if (mcpStatus.detail) {
console.log(` ${padDisplayEnd('Note:', 18)}${mcpStatus.detail}`);
}
}
};

View file

@ -1,5 +1,10 @@
import { describe, expect, it } from 'vitest';
import { displayWidth, localEmbeddingDoctorStatus, padDisplayEnd } from '../../src/cli/doctor.js';
import {
displayWidth,
localEmbeddingDoctorStatus,
padDisplayEnd,
windowsGitNexusMcpDoctorStatus,
} from '../../src/cli/doctor.js';
describe('doctor output formatting', () => {
it('keeps ASCII padding equivalent to String.padEnd', () => {
@ -55,3 +60,50 @@ describe('doctor embedding-runtime support status', () => {
expect(detail).toBeNull();
});
});
describe('doctor Windows GitNexus MCP process status', () => {
it('reports no running processes without extra guidance', () => {
const status = windowsGitNexusMcpDoctorStatus({ processes: [] });
expect(status).toEqual({ status: 'none running', detail: null });
});
it('recognizes current-install MCP processes and warns about update locks', () => {
const status = windowsGitNexusMcpDoctorStatus({
currentPackageRoot: 'D:\\home\\jonkomet\\npm-global\\node_modules\\gitnexus',
processes: [
{
pid: 1234,
commandLine:
'"C:\\Program Files\\nodejs\\node.exe" D:\\home\\jonkomet\\npm-global\\node_modules\\gitnexus\\dist\\cli\\index.js mcp',
},
],
});
expect(status.status).toBe('1 running (1 current)');
expect(status.detail).toMatch(/Stop running MCP servers/);
expect(status.detail).toMatch(/locked native files/);
});
it('flags mixed current and npx-cache launchers after updates', () => {
const status = windowsGitNexusMcpDoctorStatus({
currentPackageRoot: 'D:\\home\\jonkomet\\npm-global\\node_modules\\gitnexus',
processes: [
{
pid: 1234,
commandLine:
'"C:\\Program Files\\nodejs\\node.exe" D:\\home\\jonkomet\\npm-global\\node_modules\\gitnexus\\dist\\cli\\index.js mcp',
},
{
pid: 5678,
commandLine:
'"node" "C:\\Users\\Jaybo\\AppData\\Local\\npm-cache\\_npx\\abc\\node_modules\\.bin\\..\\gitnexus\\dist\\cli\\index.js" mcp',
},
],
});
expect(status.status).toBe('2 running (1 current, 1 npx-cache)');
expect(status.detail).toMatch(/Restart editors\/agents/);
expect(status.detail).toMatch(/same package version/);
});
});