Skip to content

Commit ce3005b

Browse files
author
root
committed
fix: bundle CLI for Windows/WSL cross-environment use (closes #10)
Bundle all npm dependencies into dist/cli.js so node .mex/dist/cli.js works from native Windows terminals after WSL setup, without relying on .mex/node_modules symlinks. Add setup.ps1 for native Windows rebuilds, WSL warnings in setup.sh, and regression tests for the bundled CLI.
1 parent 36fb538 commit ce3005b

7 files changed

Lines changed: 253 additions & 13 deletions

File tree

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,21 @@ npm install -g mex-agent
8888

8989
### Windows
9090

91-
The recommended `npx mex-agent setup` flow runs in any terminal (Command Prompt, PowerShell, or WSL) and does not need bash, so most Windows users do not have to think about this section.
91+
**Recommended:** use one environment end-to-end — WSL, Git Bash, or the cross-platform npm installer:
9292

93-
> **Windows users (legacy `setup.sh` flow):** Run all commands inside WSL or Git Bash. Do not mix environments.
93+
```bash
94+
npx mex-agent setup
95+
```
9496

95-
If you previously installed via the legacy `setup.sh` script, building inside WSL and then running the CLI from a native Windows terminal causes "module not found" errors because `node_modules` and path resolution differ between the two filesystems. Run install, build, and CLI commands inside the same environment: either entirely in WSL / Git Bash, or entirely in native Windows via `npx mex-agent`.
97+
For git-clone installs, run **either** `.mex/setup.sh` (WSL/Git Bash) **or** `.mex/setup.ps1` (PowerShell). Do not mix WSL `npm install` with native Windows Node in the same `.mex` folder; symlinks in `node_modules` break across environments.
9698

97-
See [issue #10](https://github.com/theDakshJaitly/mex/issues/10) for context.
99+
After setup, the CLI is bundled into `.mex/dist/cli.js` and does not need `.mex/node_modules` at runtime, so commands like `node .mex/dist/cli.js check` work from PowerShell/CMD even when the scaffold was built in WSL.
100+
101+
If you already hit a module-not-found error, rebuild natively:
102+
103+
```powershell
104+
.\.mex\setup.ps1
105+
```
98106

99107
## How It Works
100108

setup.ps1

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#Requires -Version 5.1
2+
<#
3+
.SYNOPSIS
4+
Windows-native mex setup for git-clone installs.
5+
6+
.DESCRIPTION
7+
Builds the mex CLI with native Windows Node, then runs interactive setup.
8+
Use this instead of setup.sh when you are not in WSL/Git Bash.
9+
10+
Run from your project root:
11+
.\.mex\setup.ps1
12+
13+
If you already ran setup.sh in WSL and see "Cannot find module" errors from
14+
PowerShell, this script rebuilds the bundled CLI so it runs without
15+
.mex\node_modules.
16+
#>
17+
param(
18+
[switch]$DryRun
19+
)
20+
21+
$ErrorActionPreference = "Stop"
22+
23+
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
24+
$ProjectDir = (Get-Location).Path
25+
26+
if ($ScriptDir -eq $ProjectDir) {
27+
Write-Error "Run this script from your project root, not from inside .mex."
28+
}
29+
30+
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
31+
Write-Error "Node.js is required. Install Node 20+ and try again."
32+
}
33+
34+
Write-Host ""
35+
Write-Host "mex setup (Windows)" -ForegroundColor White
36+
Write-Host ""
37+
38+
Write-Host "-> Building mex CLI with native Windows Node..."
39+
Push-Location $ScriptDir
40+
try {
41+
npm install --silent 2>$null
42+
if ($LASTEXITCODE -ne 0) {
43+
throw "npm install failed in .mex"
44+
}
45+
npm run build --silent 2>$null
46+
if ($LASTEXITCODE -ne 0) {
47+
throw "npm run build failed in .mex"
48+
}
49+
} finally {
50+
Pop-Location
51+
}
52+
53+
Write-Host "OK CLI engine built" -ForegroundColor Green
54+
Write-Host ""
55+
56+
$setupArgs = @("setup")
57+
if ($DryRun) {
58+
$setupArgs += "--dry-run"
59+
}
60+
61+
& node (Join-Path $ScriptDir "dist\cli.js") @setupArgs
62+
if ($LASTEXITCODE -ne 0) {
63+
exit $LASTEXITCODE
64+
}
65+
66+
Write-Host ""
67+
Write-Host "OK Setup complete." -ForegroundColor Green
68+
Write-Host ""
69+
Write-Host "-> Verify: ask your AI tool to read .mex/ROUTER.md"
70+
Write-Host "-> Run: node .mex/dist/cli.js check"
71+
Write-Host "-> Or: npx mex-agent check"

setup.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,14 @@ if [ "$DRY_RUN" -eq 1 ]; then
119119
echo ""
120120
fi
121121

122+
# Windows + WSL: warn when the project lives on a Windows-mounted drive.
123+
if grep -qi microsoft /proc/version 2>/dev/null && [[ "$PROJECT_DIR" == /mnt/* ]]; then
124+
info "Windows filesystem detected — bundled CLI runs from PowerShell after setup."
125+
info "On Windows, prefer: npx mex-agent setup or .mex/setup.ps1"
126+
info "Do not mix WSL npm install with native Windows Node in the same .mex folder."
127+
echo ""
128+
fi
129+
122130
# ─────────────────────────────────────────────────────────────
123131
# Step 1 — Build CLI engine (if Node available)
124132
# ─────────────────────────────────────────────────────────────
@@ -148,6 +156,7 @@ elif command -v node &>/dev/null; then
148156
if [ -f "$SCRIPT_DIR/dist/cli.js" ]; then
149157
MEX_CMD="node $SCRIPT_DIR/dist/cli.js"
150158
ok "CLI engine built — drift detection, pre-analysis, and targeted sync ready"
159+
info "Bundled CLI runs without .mex/node_modules (safe to use from Windows terminals)"
151160
fi
152161
fi
153162
else

src/drift/checkers/path.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { existsSync, readFileSync } from "node:fs";
2-
import { createRequire } from "node:module";
32
import { resolve } from "node:path";
43
import { globSync } from "glob";
54
import YAML from "yaml";
@@ -136,13 +135,9 @@ function pathExists(
136135
if (scopedMatch) {
137136
const pkgName = `@${scopedMatch[1]}/${scopedMatch[2]}`;
138137

139-
// Try Node's module resolution first (works for installed npm packages)
140-
try {
141-
const req = createRequire(resolve(projectRoot, "noop.js"));
142-
req.resolve(`${pkgName}/package.json`);
138+
// Check node_modules (works for installed npm packages and most workspace layouts)
139+
if (existsSync(resolve(projectRoot, "node_modules", pkgName, "package.json"))) {
143140
return true;
144-
} catch {
145-
// Fall through to workspace check
146141
}
147142

148143
// Check workspace names (handles package managers that don't symlink

test/cli-bundle.test.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { execSync } from "node:child_process";
2+
import {
3+
copyFileSync,
4+
mkdirSync,
5+
mkdtempSync,
6+
readFileSync,
7+
writeFileSync,
8+
} from "node:fs";
9+
import { tmpdir } from "node:os";
10+
import { dirname, join } from "node:path";
11+
import { fileURLToPath } from "node:url";
12+
import { beforeAll, describe, expect, it } from "vitest";
13+
14+
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
15+
const cliPath = join(repoRoot, "dist/cli.js");
16+
17+
/** Node built-ins that may appear as bare imports in the bundled CLI. */
18+
const NODE_BUILTINS = new Set([
19+
"assert",
20+
"buffer",
21+
"child_process",
22+
"crypto",
23+
"events",
24+
"fs",
25+
"fs/promises",
26+
"module",
27+
"os",
28+
"path",
29+
"process",
30+
"readline",
31+
"readline/promises",
32+
"stream",
33+
"string_decoder",
34+
"tty",
35+
"url",
36+
"util",
37+
]);
38+
39+
function writeMinimalScaffold(projectRoot: string, mexDir: string): void {
40+
const frontmatter = (name: string) => `---
41+
name: ${name}
42+
description: test
43+
triggers: []
44+
edges: []
45+
last_updated: 2026-06-06
46+
---
47+
content
48+
`;
49+
50+
writeFileSync(join(mexDir, "ROUTER.md"), frontmatter("router"));
51+
writeFileSync(join(mexDir, "AGENTS.md"), "# Agents\n[Project Name]\n");
52+
for (const name of ["architecture", "stack", "conventions", "decisions", "setup"]) {
53+
writeFileSync(join(mexDir, "context", `${name}.md`), frontmatter(name));
54+
}
55+
writeFileSync(
56+
join(mexDir, "patterns", "INDEX.md"),
57+
`${frontmatter("index")}\n| Pattern | Description |\n|---------|-------------|\n`,
58+
);
59+
60+
execSync("git init -q", { cwd: projectRoot });
61+
execSync("git add -A", { cwd: projectRoot });
62+
execSync('git -c user.email=test@test.com -c user.name=test commit -q -m init', {
63+
cwd: projectRoot,
64+
});
65+
}
66+
67+
describe("bundled CLI (Windows/WSL issue #10)", () => {
68+
beforeAll(() => {
69+
execSync("npm run build", { cwd: repoRoot, stdio: "pipe" });
70+
}, 120_000);
71+
72+
it("does not leave npm package imports in dist/cli.js", () => {
73+
const source = readFileSync(cliPath, "utf8");
74+
const externalImports = [
75+
...source.matchAll(/^import\s+.+\s+from\s+["']([^./][^"']*)["']/gm),
76+
]
77+
.map((match) => match[1])
78+
.filter((name) => !name.startsWith("node:") && !NODE_BUILTINS.has(name));
79+
80+
expect(externalImports).toEqual([]);
81+
});
82+
83+
it("runs --version without .mex/node_modules", () => {
84+
const tmp = mkdtempSync(join(tmpdir(), "mex-bundle-"));
85+
const mexDir = join(tmp, ".mex");
86+
mkdirSync(join(mexDir, "dist"), { recursive: true });
87+
copyFileSync(cliPath, join(mexDir, "dist/cli.js"));
88+
copyFileSync(join(repoRoot, "package.json"), join(mexDir, "package.json"));
89+
90+
const out = execSync("node dist/cli.js --version", {
91+
cwd: mexDir,
92+
encoding: "utf8",
93+
});
94+
95+
const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")) as {
96+
version: string;
97+
};
98+
expect(out.trim()).toBe(pkg.version);
99+
});
100+
101+
it("runs check --quiet on a minimal scaffold without node_modules", () => {
102+
const tmp = mkdtempSync(join(tmpdir(), "mex-bundle-check-"));
103+
const mexDir = join(tmp, ".mex");
104+
mkdirSync(join(mexDir, "dist"), { recursive: true });
105+
mkdirSync(join(mexDir, "context"), { recursive: true });
106+
mkdirSync(join(mexDir, "patterns"), { recursive: true });
107+
108+
copyFileSync(cliPath, join(mexDir, "dist/cli.js"));
109+
copyFileSync(join(repoRoot, "package.json"), join(mexDir, "package.json"));
110+
writeMinimalScaffold(tmp, mexDir);
111+
112+
const out = execSync("node .mex/dist/cli.js check --quiet", {
113+
cwd: tmp,
114+
encoding: "utf8",
115+
});
116+
117+
expect(out.trim()).toMatch(/^mex: drift score \d+\/100/);
118+
});
119+
});

tsup.config.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
11
import { defineConfig } from "tsup";
22

3+
/** Stub optional ink devtools peer so the bundled CLI build succeeds without it. */
4+
const stubReactDevtools = {
5+
name: "stub-react-devtools-core",
6+
setup(build: {
7+
onResolve: (
8+
args: { filter: RegExp },
9+
callback: (
10+
args: { path: string },
11+
) => { path: string; namespace: string },
12+
) => void;
13+
onLoad: (
14+
args: { filter: RegExp; namespace: string },
15+
callback: () => { contents: string; loader: "js" },
16+
) => void;
17+
}) {
18+
build.onResolve({ filter: /^react-devtools-core$/ }, (args) => ({
19+
path: args.path,
20+
namespace: "react-devtools-stub",
21+
}));
22+
build.onLoad({ filter: /.*/, namespace: "react-devtools-stub" }, () => ({
23+
contents: "export default { initialize() {}, connectToDevTools() {} };",
24+
loader: "js",
25+
}));
26+
},
27+
};
28+
329
/**
430
* Two-config build:
531
* - cli → dist/cli.js (shebang banner, no .d.ts; consumed by `bin`)
@@ -15,8 +41,18 @@ export default defineConfig([
1541
splitting: false,
1642
sourcemap: true,
1743
dts: false,
18-
banner: {
19-
js: "#!/usr/bin/env node",
44+
// Bundle all npm deps into dist/cli.js so `node .mex/dist/cli.js` works on
45+
// Windows without a .mex/node_modules tree (fixes WSL build + Windows runtime).
46+
noExternal: [/.*/],
47+
esbuildPlugins: [stubReactDevtools],
48+
esbuildOptions(options) {
49+
options.banner = {
50+
js: [
51+
"#!/usr/bin/env node",
52+
"import { createRequire } from 'node:module';",
53+
"const require = createRequire(import.meta.url);",
54+
].join("\n"),
55+
};
2056
},
2157
},
2258
{

update.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ banner() {
6868
# These are owned by mex, not the user's populated content
6969
INFRA_FILES=(
7070
"setup.sh"
71+
"setup.ps1"
7172
"update.sh"
7273
"sync.sh"
7374
"visualize.sh"
@@ -190,6 +191,7 @@ done
190191

191192
# Preserve executable permissions on scripts
192193
chmod +x "$SCRIPT_DIR/setup.sh" 2>/dev/null || true
194+
chmod +x "$SCRIPT_DIR/setup.ps1" 2>/dev/null || true
193195
chmod +x "$SCRIPT_DIR/update.sh" 2>/dev/null || true
194196
chmod +x "$SCRIPT_DIR/sync.sh" 2>/dev/null || true
195197
chmod +x "$SCRIPT_DIR/visualize.sh" 2>/dev/null || true

0 commit comments

Comments
 (0)