What
On Windows, the command that npm link or npm i -g @axiomcode/code-graph installs does not run from PowerShell or cmd:
PS> axiomcode --help
& : The term 'bash.exe' is not recognized as the name of a cmdlet, function, script file, or operable program.
At C:\Users\<user>\AppData\Roaming\npm\axiomcode.ps1:24 char:7
+ & "bash$exe" "$basedir/node_modules/@axiomcode/code-graph/bin/axiomcode" ...
The same command works from Git Bash. Verified at f60e4d4f on Windows 11, Node 24.11.1, npm 11.6.2, with a default Git for Windows install.
Why
bin.axiomcode points at bin/axiomcode, a bash script. npm generates the Windows axiomcode.cmd and axiomcode.ps1 wrappers from the script's shebang, so both run a bare bash.exe looked up on PATH. A default Git for Windows install adds Git\cmd to PATH but not Git\bin, so no bash is found.
Adding Git\bin to PATH is not a durable workaround. Once WSL is installed, C:\Windows\System32\bash.exe is WSL's bash, and it sits on the machine PATH ahead of anything in the user PATH. The wrapper would then run this Windows install inside Linux, where neither the Windows node nor a C:/ path exists. That part is inferred and not reproduced, because WSL wasn't installed on the test machine.
It also affects every agent that starts the MCP server on Windows with npx -y @axiomcode/code-graph mcp, since npx goes through the same wrapper.
Generic repro
- On Windows, with Git for Windows installed with default options: clone, then
npm install && npm link.
- Open PowerShell or cmd and run
axiomcode --help.
Proposed fix (tested locally)
No separate cmd/ or Windows folder is needed, because npm writes the Windows wrappers itself. The fix is to point bin at a small Node launcher, bin/axiomcode.js. npm then generates wrappers that run node, and the launcher picks the bash:
$AXIOMCODE_BASH if set; a set-but-missing path is an error.
- On Windows: Git's own
bin\bash.exe, found from git --exec-path, then %ProgramFiles%, %ProgramFiles(x86)% and %LOCALAPPDATA%\Programs \Git\bin\bash.exe. PATH is never searched, so WSL's bash is never used. Git\bin\bash.exe is the wrapper that puts mingw64/bin and usr/bin on PATH; usr\bin\bash.exe alone does not.
- Elsewhere: plain
bash, as before.
- If none is found, exit 127 with a message naming Git for Windows and
AXIOMCODE_BASH.
- Pass the exit status and any signal through.
bin/axiomcode stays unchanged, since every test suite and doc calls it directly.
Two related changes were needed:
.gitignore:131: [Bb]in/ (added for the C# dotnet build output) also ignores the root bin/. A new file there is silently left out of a commit, which is presumably why bin/axiomcode had to be force-added. A !bin/axiomcode.js exception cannot work, because git won't re-include a file under an excluded directory, so the fix is !/bin/ directly after it. graph/test/csharp/**/bin/ stays ignored (checked with git check-ignore).
graph/test/tools/package-contents-test.sh: also assert that bin/axiomcode.js ships.
Results on the test machine with the patch applied and npm link re-run:
| where |
axiomcode --help |
exit code passthrough |
parse run (Java case 01) |
axiomcode mcp initialize + tools/list |
| PowerShell |
✅ exit 0 |
✅ bad arg → 2 |
✅ IR written, then the expected no engine for java@… (#454) |
✅ |
| cmd |
✅ exit 0 |
✅ |
— |
✅ |
| Git Bash |
✅ exit 0 |
✅ |
✅ |
✅ |
AXIOMCODE_BASH=C:\nope\bash.exe → ❌ AXIOMCODE_BASH is set to C:\nope\bash.exe, which does not exist., exit 127. npm pack --dry-run ships both bin/axiomcode and bin/axiomcode.js. package-contents-test passes, 23 of 23 checks (needs the separate test fix filed alongside this issue).
Not verified: macOS and Linux. The POSIX path is spawnSync('bash', [script, …]), and __dirname resolves through the .bin symlink, so #886's root walk should be unaffected, but that is untested here.
Patch (against f60e4d4)
diff --git a/.gitignore b/.gitignore
index d53e24ef..f70e4558 100644
--- a/.gitignore
+++ b/.gitignore
@@ -129,6 +129,9 @@ oracles/
# four binaries into the tree.
[Oo]bj/
[Bb]in/
+# ...except the repository's own bin/, which is the CLI and not build output: without this
+# every new file there, bin/axiomcode.js included, is silently left out of a commit.
+!/bin/
*.user
reports/
diff --git a/graph/test/tools/package-contents-test.sh b/graph/test/tools/package-contents-test.sh
index 68f18ac7..2937fa3e 100755
--- a/graph/test/tools/package-contents-test.sh
+++ b/graph/test/tools/package-contents-test.sh
@@ -56,12 +56,13 @@ try{
let files;
try{
files=JSON.parse(execFileSync("npm",["pack","--dry-run","--json","--ignore-scripts"],
- {cwd:root, encoding:"utf8", stdio:["ignore","pipe","ignore"], maxBuffer:64*1024*1024}))[0].files.map(f=>f.path);
+ {cwd:root, encoding:"utf8", stdio:["ignore","pipe","ignore"], maxBuffer:64*1024*1024, shell:process.platform==="win32"}))[0].files.map(f=>f.path);
}catch(e){ console.log(" FAIL npm pack --dry-run failed: "+e.message.split("\n")[0]); process.exit(1); }
const has=(p)=>files.includes(p);
const count=(re)=>files.filter(f=>re.test(f)).length;
has("bin/axiomcode") ? ok("ships bin/axiomcode") : bad("does NOT ship bin/axiomcode");
+has("bin/axiomcode.js") ? ok("ships bin/axiomcode.js") : bad("does NOT ship bin/axiomcode.js, the command npm links");
// The corpora are the bulk of the repository and run nothing for a consumer.
count(/^graph\/test\//)===0 ? ok("does not ship graph/test") : bad("ships "+count(/^graph\/test\//)+" files under graph/test");
count(/^parser\/src\/test-data\//)===0 ? ok("does not ship parser test-data") : bad("ships "+count(/^parser\/src\/test-data\//)+" files under parser/src/test-data");
diff --git a/package.json b/package.json
index bfb73b7a..0d59e6d3 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,7 @@
"@axiomcode/engine-win32-x64": "0.1.0"
},
"bin": {
- "axiomcode": "bin/axiomcode"
+ "axiomcode": "bin/axiomcode.js"
},
"files": [
"bin/",
diff --git a/bin/axiomcode.js b/bin/axiomcode.js
new file mode 100644
index 00000000..2ebd2358
--- /dev/null
+++ b/bin/axiomcode.js
@@ -0,0 +1,65 @@
+#!/usr/bin/env node
+// ─────────────────────────────────────────────────────────────────────────────
+// The installed `axiomcode` command. It only finds a bash and hands everything to
+// bin/axiomcode, which is the CLI.
+//
+// WHY A NODE LAUNCHER. npm writes the command's wrappers itself from the `bin` entry: a
+// symlink on POSIX, and on Windows an axiomcode.cmd and an axiomcode.ps1 that run whatever
+// the script's shebang names. For bin/axiomcode that is `bash`, so both wrappers run a bare
+// `bash.exe` from PATH -- and a default Git for Windows install puts Git\cmd on PATH, not
+// Git\bin, so PowerShell and cmd fail with "'bash.exe' is not recognized". Adding Git\bin to
+// PATH is not a fix either: once WSL is installed, C:\Windows\System32\bash.exe comes first on
+// the machine PATH, and it is WSL's bash, which runs this Windows install inside Linux where
+// neither the Windows node nor a C:/ path exists. A Node shebang makes npm's wrappers run
+// node, which is always there, and the choice of bash is made here instead of by PATH.
+//
+// On Windows, in order: $AXIOMCODE_BASH, Git's own bash located from `git --exec-path`, then
+// the default install locations. PATH is never searched for bash, so WSL's is never taken.
+// Everywhere else it is plain `bash`, as the shebang of bin/axiomcode already assumes.
+// ─────────────────────────────────────────────────────────────────────────────
+'use strict';
+const { spawnSync, execFileSync } = require('child_process');
+const fs = require('fs');
+const path = require('path');
+
+const script = path.join(__dirname, 'axiomcode');
+
+function gitBash() {
+ const candidates = [];
+ try {
+ // <git>/mingw64/libexec/git-core, or <git>/libexec/git-core on some layouts. Git\bin\bash.exe
+ // is the wrapper that puts mingw64/bin and usr/bin on PATH, which the pipeline needs;
+ // usr\bin\bash.exe alone does not.
+ const exec = execFileSync('git', ['--exec-path'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true }).trim();
+ for (let d = path.resolve(exec), i = 0; i < 4; i++, d = path.dirname(d)) candidates.push(path.join(d, 'bin', 'bash.exe'));
+ } catch { /* no git on PATH: fall through to the default locations */ }
+ for (const base of [process.env.ProgramFiles, process.env['ProgramFiles(x86)'], process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, 'Programs')]) {
+ if (base) candidates.push(path.join(base, 'Git', 'bin', 'bash.exe'));
+ }
+ return candidates.find((p) => fs.existsSync(p));
+}
+
+function resolveBash() {
+ const override = process.env.AXIOMCODE_BASH;
+ if (override) {
+ if (fs.existsSync(override)) return override;
+ fail(`AXIOMCODE_BASH is set to ${override}, which does not exist.`);
+ }
+ if (process.platform !== 'win32') return 'bash';
+ const found = gitBash();
+ if (found) return found;
+ fail('axiomcode needs the bash that comes with Git for Windows, and none was found.\n' +
+ ' • install Git for Windows (https://git-scm.com/download/win), or\n' +
+ ' • set AXIOMCODE_BASH to the full path of a Git Bash bash.exe (not C:\Windows\System32\bash.exe, which is WSL).');
+}
+
+function fail(msg) {
+ process.stderr.write(`❌ ${msg}\n`);
+ process.exit(127);
+}
+
+const r = spawnSync(resolveBash(), [script, ...process.argv.slice(2)], { stdio: 'inherit', windowsHide: false });
+if (r.error) fail(`could not start bash: ${r.error.message}`);
+// Die of the same signal the CLI died of, so a caller sees what really happened.
+if (r.signal) process.kill(process.pid, r.signal);
+process.exit(r.status ?? 1);
Related: #16 (Windows engine pipeline; the comment there that first reported this now points here), #886 (the bin entry and root walk this builds on), #454 / #904 (prebuilt engines).
What
On Windows, the command that
npm linkornpm i -g @axiomcode/code-graphinstalls does not run from PowerShell or cmd:The same command works from Git Bash. Verified at
f60e4d4fon Windows 11, Node 24.11.1, npm 11.6.2, with a default Git for Windows install.Why
bin.axiomcodepoints atbin/axiomcode, a bash script. npm generates the Windowsaxiomcode.cmdandaxiomcode.ps1wrappers from the script's shebang, so both run a barebash.exelooked up on PATH. A default Git for Windows install addsGit\cmdto PATH but notGit\bin, so no bash is found.Adding
Git\binto PATH is not a durable workaround. Once WSL is installed,C:\Windows\System32\bash.exeis WSL's bash, and it sits on the machine PATH ahead of anything in the user PATH. The wrapper would then run this Windows install inside Linux, where neither the Windowsnodenor aC:/path exists. That part is inferred and not reproduced, because WSL wasn't installed on the test machine.It also affects every agent that starts the MCP server on Windows with
npx -y @axiomcode/code-graph mcp, since npx goes through the same wrapper.Generic repro
npm install && npm link.axiomcode --help.Proposed fix (tested locally)
No separate cmd/ or Windows folder is needed, because npm writes the Windows wrappers itself. The fix is to point
binat a small Node launcher,bin/axiomcode.js. npm then generates wrappers that runnode, and the launcher picks the bash:$AXIOMCODE_BASHif set; a set-but-missing path is an error.bin\bash.exe, found fromgit --exec-path, then%ProgramFiles%,%ProgramFiles(x86)%and%LOCALAPPDATA%\Programs\Git\bin\bash.exe. PATH is never searched, so WSL's bash is never used.Git\bin\bash.exeis the wrapper that putsmingw64/binandusr/binon PATH;usr\bin\bash.exealone does not.bash, as before.AXIOMCODE_BASH.bin/axiomcodestays unchanged, since every test suite and doc calls it directly.Two related changes were needed:
.gitignore:131:[Bb]in/(added for the C#dotnet buildoutput) also ignores the rootbin/. A new file there is silently left out of a commit, which is presumably whybin/axiomcodehad to be force-added. A!bin/axiomcode.jsexception cannot work, because git won't re-include a file under an excluded directory, so the fix is!/bin/directly after it.graph/test/csharp/**/bin/stays ignored (checked withgit check-ignore).graph/test/tools/package-contents-test.sh: also assert thatbin/axiomcode.jsships.Results on the test machine with the patch applied and
npm linkre-run:axiomcode --helpaxiomcode mcpinitialize + tools/listno engine for java@…(#454)AXIOMCODE_BASH=C:\nope\bash.exe→❌ AXIOMCODE_BASH is set to C:\nope\bash.exe, which does not exist., exit 127.npm pack --dry-runships bothbin/axiomcodeandbin/axiomcode.js. package-contents-test passes, 23 of 23 checks (needs the separate test fix filed alongside this issue).Not verified: macOS and Linux. The POSIX path is
spawnSync('bash', [script, …]), and__dirnameresolves through the.binsymlink, so #886's root walk should be unaffected, but that is untested here.Patch (against f60e4d4)
Related: #16 (Windows engine pipeline; the comment there that first reported this now points here), #886 (the
binentry and root walk this builds on), #454 / #904 (prebuilt engines).