Skip to content

fix(setup): prefer .cmd/.bat wrapper from Windows where output#1299

Merged
magyargergo merged 6 commits into
mainfrom
copilot/fix-gitnexus-command-path-windows
May 4, 2026
Merged

fix(setup): prefer .cmd/.bat wrapper from Windows where output#1299
magyargergo merged 6 commits into
mainfrom
copilot/fix-gitnexus-command-path-windows

Conversation

Copilot AI commented May 3, 2026

Copy link
Copy Markdown
Contributor

On Windows, resolveGitnexusBin() picks the first line from where gitnexus, which is the POSIX shell script. Node.js child_process.spawn() cannot execute it directly, causing spawn ENOENT when the MCP health check runs.

Fix:

  • Scan all lines from where output and prefer the entry matching /\.(cmd|bat)$/i
  • Falls back to first line if no wrapper found (safety for unusual installs)
// Before: always took first line (shell script on Windows)
.split('\n')[0].trim();

// After: on Windows, find the .cmd/.bat wrapper
const lines = output.split('\n').map((l) => l.trim()).filter(Boolean);
if (isWin) {
  const cmdLine = lines.find((l) => /\.(cmd|bat)$/i.test(l));
  return cmdLine || lines[0] || null;
}

Tests added:

  • Verifies .cmd is selected when where returns both shell script and .cmd
  • Verifies fallback to first line when no wrapper exists
  • Verifies CRLF line endings (as produced by Windows where) are handled correctly
  • Verifies .bat wrapper selection
  • Verifies case-insensitive matching (uppercase .CMD)

Spawn compatibility note: The .cmd wrapper is the standard Windows npm convention for CLI binaries. Claude Code's MCP launcher is expected to handle .cmd files, as this is how npm exposes all global CLI tools on Windows. The previous behavior (ENOENT from attempting to spawn the extensionless POSIX script) was strictly worse — the fix changes a guaranteed failure into the correct conventional path.

@vercel

vercel Bot commented May 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gitnexus Ready Ready Preview, Comment May 3, 2026 1:09pm

Request Review

On Windows, `where gitnexus` returns multiple entries including the
POSIX shell script and the .cmd wrapper. The code previously took the
first line (shell script), which cannot be spawned directly by Node.js
child_process on Windows. Now we prefer the .cmd entry when available.

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/e6b54037-87fb-4195-b157-4cfcafce5f5d

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/e6b54037-87fb-4195-b157-4cfcafce5f5d

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix command path for gitnexus setup on Windows fix(setup): prefer .cmd/.bat wrapper from Windows where output May 3, 2026
Copilot AI requested a review from magyargergo May 3, 2026 10:41
@github-actions

github-actions Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor

Claude finished @magyargergo's task in 3m 11s —— View job


PR #1299 Review — fix(setup): prefer .cmd/.bat wrapper from Windows where output

Tasks:

  • Gather repo context (AGENTS.md, GUARDRAILS.md, DoD.md, CONTRIBUTING.md)
  • Inspect changed files (setup.ts, setup.test.ts, package-lock.json)
  • Analyze resolveGitnexusBin() implementation and spawn compatibility
  • Review test coverage and quality
  • Assess package-lock.json churn
  • Check CI/draft status
  • Write final review

Review bar for this PR

  • resolveGitnexusBin() prefers .cmd/.bat wrapper paths from where gitnexus on Windows
  • On Windows, generated Claude config uses the executable wrapper path with args: ['mcp']
  • Unix/macOS behavior using which gitnexus remains unchanged
  • Empty, whitespace, CRLF, and multi-line output handled safely
  • Fallback to npx -y gitnexus@latest mcp (or cmd /c npx ... on Windows) when no local binary found
  • Tests cover wrapper preference and fallback
  • package-lock.json contains no unrelated platform dependency churn
  • PR is not in draft and CI/Docker/typecheck/focused tests are green

Issue and PR context considered

The problem: On Windows, where gitnexus returns both the extensionless POSIX shell script and the .cmd wrapper. Node.js spawn() (and Claude Code's MCP spawner) cannot execute the extensionless POSIX script on Windows — CreateProcess cannot find or run it without a known extension — causing spawn ENOENT during MCP health check.

Changed files:

  • gitnexus/src/cli/setup.tsresolveGitnexusBin() rewritten to scan all where output lines and prefer .cmd/.bat
  • gitnexus/test/unit/setup.test.ts — two new tests for the Windows selection logic
  • gitnexus/package-lock.jsonunrelated: removes @ladybugdb/core-darwin-x64 optional dep entry

Draft status: PR is marked draft.

CI status: As of review time, CI (in_progress) and Docker (in_progress) have not yet completed. PR Description Check and Conventional Labeler are success. The earlier action_required states appear to have resolved on push.

Package-lock concern: The diff removes an optional: true entry for @ladybugdb/core-darwin-x64 from the nested lockfile section — unrelated to the Windows fix.

Main risks:

  • .cmd resolves ENOENT but whether Claude Code's MCP spawner can execute .cmd without shell: true is not verified in this repo
  • package-lock churn unexplained
  • PR still draft; CI not yet finished
  • Missing CRLF / .bat test variants

Understanding of the change

When npm installs a package globally on Windows, it creates two entries in %APPDATA%\Roaming\npm\:

  1. The POSIX shell script (no extension) — executable on Unix/macOS, unusable by CreateProcess on Windows
  2. A .cmd wrapper (e.g. gitnexus.cmd) — a batch file that invokes node path\to\script.js

where gitnexus returns both paths. Before this PR, resolveGitnexusBin() took .split('\n')[0].trim() — always the POSIX script on Windows, which cannot be spawned or found by the Windows process creation API, producing spawn ENOENT.

The new logic:

const lines = output.split('\n').map((l) => l.trim()).filter(Boolean);
if (isWin) {
  const cmdLine = lines.find((l) => /\.(cmd|bat)$/i.test(l));
  return cmdLine || lines[0] || null;
}
return lines[0] || null;

Scans all lines, prefers the .cmd/.bat wrapper, falls back to first line if no wrapper found, and returns null on empty output. Unix/macOS path is unchanged.


Findings

[high] package-lock.json removes unrelated macOS optional dependency

  • Category: Supply chain / package integrity
  • Files: gitnexus/package-lock.json
  • Issue: The diff removes "node_modules/@ladybugdb/core/node_modules/@ladybugdb/core-darwin-x64": { "optional": true }. This entry has no connection to Windows .cmd resolution. It indicates npm install was run in a non-macOS environment (e.g. Linux CI) and the lockfile was regenerated with macOS-optional entries dropped.
  • Why it matters here: GUARDRAILS.md §Publishing & supply chain: "run tests and CI after lockfile updates." LadybugDB is GitNexus's embedded graph database — its native bindings are critical. If macOS x64 users install from a lockfile missing this entry, the optional darwin-x64 native binding may not install correctly during npm install, potentially falling back to a slower path or emitting install warnings that mask real errors.
  • Recommended fix: Revert package-lock.json to the base main state. This PR requires zero dependency changes — no npm install should have been needed.
  • Blocks merge: Yes. Unexplained lockfile churn against GUARDRAILS.md supply-chain rule. Fix this →

[high] .cmd resolves ENOENT but downstream spawn compatibility not verified in-repo

  • Category: Windows spawn / MCP health-check compatibility
  • Files: gitnexus/src/cli/setup.ts
  • Issue: Selecting .cmd is necessary but not sufficient. On Windows, child_process.spawn(cmdPath, args, { shell: false }) cannot execute .cmd or .bat files — CreateProcess does not invoke cmd.exe for batch scripts unless shell: true is set. The path to resolution: resolveGitnexusBin()getMcpEntry() → written to .claude.json → spawned by Claude Code's internal MCP launcher. Claude Code's MCP spawning is not in this repo, so we cannot directly verify it handles .cmd with shell: true or equivalent.
  • Why it matters here: If Claude Code's MCP spawner uses shell: false without CMD shim awareness, switching from the extensionless file (ENOENT) to .cmd may change the error from ENOENT to ERR_UNKNOWN_FILE_EXTENSION or a failed exec — still broken, just differently. The tests confirm .claude.json is written correctly but do not exercise the actual spawn path.
  • Recommended fix: Check Claude Code's Windows MCP launch behavior (or link to relevant Claude Code issue/docs confirming .cmd is supported). Alternatively, add a note to the PR description documenting the spawn-path assumption. This is partially mitigated by the fact that .cmd is the standard Windows npm wrapper convention and most modern MCP host implementations are aware of it.
  • Blocks merge: Not independently blocking (the original ENOENT was clearly worse), but warrants explicit documentation in the PR description before merge.

[medium] PR is draft; CI not yet completed

  • Category: Validation / release readiness
  • Files: N/A
  • Issue: The PR is marked draft. CI (test suite) and Docker Build are in_progress at review time — no pass/fail yet. DoD §4 requires cd gitnexus && npx tsc --noEmit and cd gitnexus && npm test to be green before merge.
  • Recommended fix: Mark ready for review only after CI is green. Ensure npm test includes the new setup tests and they pass.
  • Blocks merge: Yes per draft state. Conditionally on CI result.

[low] Missing CRLF, .bat, and .CMD uppercase test variants

  • Category: Test coverage
  • Files: gitnexus/test/unit/setup.test.ts
  • Issue: The regex is /\.(cmd|bat)$/i (case-insensitive, covers .bat), but:
    • No test with \r\n (CRLF) line endings, which Windows where actually produces
    • No .bat test (regex claims support but it's untested)
    • No .CMD uppercase test (regex claims case-insensitive but it's untested)
    • CRLF is correctly handled by .trim() in the implementation, but a test would make the invariant explicit
  • Recommended fix: Add one CRLF-output variant test and one .bat test. These are low-effort and would catch a regression if the trim() call were removed.
  • Blocks merge: No, but CRLF test is strongly recommended given the Windows-specific nature of the fix.

Definition of Done check

DoD item Status
resolveGitnexusBin() prefers .cmd/.bat from where output on Windows satisfied
Generated Claude config uses wrapper path + args: ['mcp'] satisfied
Unix/macOS which behavior unchanged satisfied
Empty, whitespace, CRLF output handled safely satisfied (CRLF via trim(), but no test)
Fallback to npx/cmd /c npx when no binary found satisfied
MCP health-check can spawn selected command on Windows partially satisfied.cmd is correct convention; spawn compatibility with Claude Code's launcher is assumed, not verified
Tests cover wrapper preference and fallback partially satisfied.cmd and fallback covered; CRLF/.bat missing
package-lock.json contains no unrelated churn not satisfied
PR not draft; CI/Docker/typecheck green not satisfied

Windows setup assessment

Aspect Assessment
where parsing Correct. split('\n').map(trim).filter(Boolean) handles multi-line output and CRLF (trim removes \r)
.cmd preference Correct. lines.find(l => /\.(cmd|bat)$/i.test(l)) — case-insensitive, scans all lines
.bat support Claimed in regex, not tested
CRLF output Handled correctly via trim(), no explicit test
Fallback behavior Falls back to lines[0] if no wrapper found — means POSIX script path on Windows, which is still broken, but this is an unusual install edge case
Generated .claude.json Correct: command = absolute .cmd path, args: ['mcp'], no embedded quotes
MCP health-check spawn compatibility Not verified in-repo; assumes Claude Code's MCP launcher handles .cmd on Windows

Note on fallback: when where returns only the extensionless POSIX script (no .cmd), the fallback returns that path, which will still fail on Windows. However, this scenario should not occur for standard npm global installs and is documented as an edge case. The comment in the code describes this correctly.


Cross-platform assessment

Aspect Assessment
Unix/macOS which behavior Unchanged — which path still returns lines[0] || null
npx fallback (Unix) Unchanged — npx -y gitnexus@latest mcp
npx fallback (Windows) Unchanged — cmd /c npx -y gitnexus@latest mcp
package-lock optional dependency Concern: @ladybugdb/core-darwin-x64 optional entry removed — risk to macOS x64 installs
macOS install risk Present: if macOS CI was not re-run against the modified lockfile, native binding availability is unvalidated

Test assessment

Aspect Assessment
New test: .cmd wrapper selected from multi-line where output Present, correct — mocks execFileSyncMock, sets win32 platform, asserts .claude.json content
New test: fallback to first line when no .cmd found Present, correct
setPlatform / module cache reset Correctly uses vi.resetModules() in beforeEach and setPlatform
execFileSyncMock reset Correctly reset via vi.clearAllMocks() in beforeEach
Temp home cleanup Correct — fs.rm(tempHome, { recursive: true, force: true }) in afterEach
Tests inspect actual .claude.json Yes — JSON.parse(fs.readFile(...))
Tests would fail before this PR Yes — the .cmd test would have selected the first line (POSIX script) instead
CRLF test Missing
.bat test Missing
.CMD uppercase test Missing
Existing Unix/macOS and npx fallback tests Present and unaffected

Validation assessment

Check Status
Draft PR Blocks merge
CI (gitnexus/ test suite) in_progress at review time
Docker Build in_progress at review time
PR Description Check success
Conventional Labeler success
npx tsc --noEmit Not verified in this review
npm test (setup tests) Not verified — CI not complete
Windows CI / actual Windows spawn validation No Windows CI runner visible; spawn behavior not integration-tested
Vercel preview Not relevant — CLI/setup path not validated by web preview

Final verdict

not production-ready

Three blockers prevent merge: (1) the PR is in draft, (2) CI and Docker are not yet complete — the test suite result is unknown, and (3) the package-lock.json removes the @ladybugdb/core-darwin-x64 optional native binding entry with no dependency justification, violating the GUARDRAILS.md supply-chain rule and risking macOS x64 install regressions unrelated to this Windows fix. The core logic change in resolveGitnexusBin() is correct and minimal — the regex, CRLF handling, and fallback are all sound. Once the lockfile is reverted to main, the PR is marked ready for review, and CI passes green, the remaining gaps (CRLF/.bat tests, spawn-compatibility note) are polish that can be addressed as minor follow-ups.
· Branch

- Revert package-lock.json to match base (no dependency changes needed)
- Add CRLF line ending test (Windows `where` produces \r\n)
- Add .bat wrapper test
- Add uppercase .CMD extension test (case-insensitive regex)

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/7ed71368-b3e8-44de-9f13-85af4effaf25

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
@github-actions

github-actions Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor

CI Report

All checks passed

Pipeline Status

Stage Status Details
✅ Typecheck success tsc --noEmit
✅ Tests success unit tests, 3 platforms
✅ E2E success gitnexus-web changes only

Test Results

Tests Passed Failed Skipped Duration
7848 7847 0 1 355s

✅ All 7847 tests passed

1 test(s) skipped — expand for details
  • buildTypeEnv > known limitations (documented skip tests) > Ruby block parameter: users.each { |user| } — closure param inference, different feature

Code Coverage

Tests

Metric Coverage Covered Base Delta Status
Statements 77.27% 23102/29894 77.02% 📈 +0.2 🟢 ███████████████░░░░░
Branches 66.16% 14813/22389 65.97% 📈 +0.2 🟢 █████████████░░░░░░░
Functions 82.32% 2273/2761 81.86% 📈 +0.5 🟢 ████████████████░░░░
Lines 80.16% 20846/26004 79.89% 📈 +0.3 🟢 ████████████████░░░░

📋 View full run · Generated by CI

@magyargergo magyargergo marked this pull request as ready for review May 4, 2026 07:11
@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Claude finished @magyargergo's task in 4m 24s —— View job


PR #1299 Adversarial Review — fix(setup): prefer .cmd/.bat wrapper from Windows where output

Tasks:

  • Post initial comment
  • Read DoD.md, AGENTS.md, GUARDRAILS.md
  • Read changed files: setup.ts, setup.test.ts
  • Run git diff to inspect exact changes
  • Check Windows process compatibility (spawn/.cmd/.bat)
  • Evaluate test coverage vs DoD requirements
  • Check lockfile / dependency churn
  • Write final review

APPROVE WITH NITS


Summary

resolveGitnexusBin() previously took split('\n')[0].trim() — always the extensionless POSIX shell script on Windows — causing spawn ENOENT when Claude Code's MCP health check tried to execute it. This PR scans all where output lines and prefers the .cmd/.bat wrapper. The change is minimal, correct, and well-tested. The previous blocking concerns (lockfile churn, draft status, incomplete CI) have all been resolved by the PR author.


Production-readiness verdict

Safe to merge. All DoD.md gates are satisfied:

DoD gate Status
resolveGitnexusBin() handles all Windows where scenarios ✅ Satisfied
Unix/macOS behavior unchanged ✅ Satisfied
Fallback (npx/cmd /c npx) unchanged ✅ Satisfied
Tests exercise real setup code and inspect real .claude.json ✅ Satisfied
Tests cover .cmd, CRLF, .bat, uppercase .CMD, fallback, npx ✅ Satisfied
package-lock.json — net diff vs main ✅ Zero (mid-branch churn reverted in 86c2b1e)
CI / typecheck ✅ 7847/7847 tests pass
Draft / scope ✅ Clean; only setup.ts + setup.test.ts in net diff
Security / injection ✅ Paths from where/which only; no string interpolation

Must-fix issues

None that block merge.


Non-blocking improvements

1. Paths with spaces not tested

C:\Users\John Doe\AppData\Roaming\npm\gitnexus.cmd is a real and common Windows path. The implementation handles it correctly (returns the raw string; no interpolation), but there's no test proving this. Add one:

it('handles paths with spaces on Windows', async () => {
  setPlatform('win32');
  execFileSyncMock.mockReturnValueOnce(
    'C:\\Users\\John Doe\\AppData\\Roaming\\npm\\gitnexus\nC:\\Users\\John Doe\\AppData\\Roaming\\npm\\gitnexus.cmd\n',
  );
  const { setupCommand } = await import('../../src/cli/setup.js');
  await setupCommand();
  const config = JSON.parse(
    await fs.readFile(path.join(tempHome, '.claude.json'), 'utf-8'),
  );
  expect(config.mcpServers.gitnexus).toEqual({
    command: 'C:\\Users\\John Doe\\AppData\\Roaming\\npm\\gitnexus.cmd',
    args: ['mcp'],
  });
});

This is low-effort and closes the most realistic Windows failure scenario that isn't covered.

2. OpenCode / Codex config not covered by the new tests

The new tests only assert .claude.json. getOpenCodeMcpEntry() and the Codex TOML path both call resolveGitnexusBin() too — they're implicitly covered by the unit path, but explicit assertions on those outputs for Windows would strengthen confidence. Not a blocker.


Windows compatibility review

The spawn compatibility question is an acceptable evidence gap, not a blocker.

The concern: child_process.spawn('foo.cmd', args, { shell: false }) does not invoke cmd.exe directly — .cmd files require the shell interpreter. Whether Claude Code's MCP spawner uses shell: true on Windows is not visible in this repo.

However:

  1. The previous behavior (ENOENT from the extensionless POSIX script) was definitively broken.
  2. .cmd is the standard npm convention for all global CLI tools on Windows. Every npm-installed CLI (including npx, tsc, eslint) follows this pattern.
  3. Claude Code itself is distributed as a .cmd wrapper on Windows — it would be architecturally inconsistent for Claude Code's MCP spawner to be unable to handle the same pattern it uses for its own distribution.
  4. Codex's local-binary path uses shell: process.platform === 'win32' explicitly (line 482), which handles .cmd correctly.

The PR description documents this assumption. That is sufficient. Changing from a guaranteed failure to the standard Windows npm convention is unambiguously correct.

One narrow case to be aware of (not introduced by this PR): if a user's install path contains spaces and Claude Code's MCP spawner builds a shell string without quoting, .cmd files with space-containing paths could fail. This is a Claude Code MCP launcher issue, not a GitNexus issue, and the old behavior couldn't even reach that point.


Test review

Tests exercise real production behavior, not duplicates of helper logic.

Each test:

  1. Mocks execFileSync at the module boundary (correct — child_process is mocked at the top level with vi.mock)
  2. Uses vi.resetModules() + dynamic import() to get a fresh module per test (correct — ensures process.platform reads are picked up)
  3. Asserts the actual generated .claude.json on disk (correct — not just checking what resolveGitnexusBin returns in isolation)
  4. Cleans up temp dirs in afterEach (correct)

Tests added (commit 86c2b1e):

  • .cmd preference from multi-line output ✅
  • CRLF (\r\n) line endings ✅
  • .bat wrapper ✅
  • Uppercase .CMD (case-insensitive) ✅
  • Fallback to first line when no wrapper ✅

Still missing: paths with spaces (flagged above as non-blocking nit).

Mock isolation: vi.clearAllMocks() correctly resets call history but not implementation queue. mockReturnValueOnce at the top of each test is consumed before clearAllMocks in the next beforeEach, so there's no cross-test leak in practice. The pattern is correct.


Supply-chain / lockfile review

Clean. The net diff of gitnexus/package-lock.json between the branch base (7f8b01d) and HEAD is zero — confirmed with git diff 7f8b01d HEAD -- gitnexus/package-lock.json (no output). The previous review's concern about @ladybugdb/core-darwin-x64 was valid at the time, but the agent correctly reverted the lockfile in commit 86c2b1e. No dependency changes exist in this PR.


Suggested code changes

The implementation diff is clean as-is. If you want to add the spaces test, the patch above is ready to paste. No changes to setup.ts are needed.


Commands run

Command Result
git diff 7f8b01d HEAD -- gitnexus/src/cli/setup.ts +18/-6, only resolveGitnexusBin() changed
git diff 7f8b01d HEAD -- gitnexus/package-lock.json No output (zero net diff)
git show HEAD:gitnexus/package-lock.json | grep -c ladybugdb 18 (same as branch base)
git log --oneline --name-status HEAD~3..HEAD Confirmed 86c2b1e reverted lockfile + added test variants
Read setup.ts, setup.test.ts, DoD.md, GUARDRAILS.md, AGENTS.md Context fully loaded
Windows runtime validation (gitnexus setup, MCP health check) Not possible — Linux CI runner; no Windows environment available

CI confirmation: 7847 tests passed per CI report (github-actions comment, 2026-05-03T13:27:42Z). TypeCheck (tsc --noEmit) passed. Coverage improved on all metrics.
· Branch

@magyargergo magyargergo merged commit 1272774 into main May 4, 2026
20 checks passed
@magyargergo magyargergo deleted the copilot/fix-gitnexus-command-path-windows branch May 4, 2026 07:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows: gitnexus setup writes wrong command path for Claude Code MCP config

2 participants