Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,21 @@ npm install -g mex-agent

### Windows

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.
**Recommended:** use one environment end-to-end — WSL, Git Bash, or the cross-platform npm installer:

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

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`.
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.

See [issue #10](https://github.com/theDakshJaitly/mex/issues/10) for context.
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.

If you already hit a module-not-found error, rebuild natively:

```powershell
.\.mex\setup.ps1
```

## How It Works

Expand Down
71 changes: 71 additions & 0 deletions setup.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Windows-native mex setup for git-clone installs.

.DESCRIPTION
Builds the mex CLI with native Windows Node, then runs interactive setup.
Use this instead of setup.sh when you are not in WSL/Git Bash.

Run from your project root:
.\.mex\setup.ps1

If you already ran setup.sh in WSL and see "Cannot find module" errors from
PowerShell, this script rebuilds the bundled CLI so it runs without
.mex\node_modules.
#>
param(
[switch]$DryRun
)

$ErrorActionPreference = "Stop"

$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$ProjectDir = (Get-Location).Path

if ($ScriptDir -eq $ProjectDir) {
Write-Error "Run this script from your project root, not from inside .mex."
}

if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
Write-Error "Node.js is required. Install Node 20+ and try again."
}

Write-Host ""
Write-Host "mex setup (Windows)" -ForegroundColor White
Write-Host ""

Write-Host "-> Building mex CLI with native Windows Node..."
Push-Location $ScriptDir
try {
npm install --silent 2>$null
if ($LASTEXITCODE -ne 0) {
throw "npm install failed in .mex"
}
npm run build --silent 2>$null
if ($LASTEXITCODE -ne 0) {
throw "npm run build failed in .mex"
}
} finally {
Pop-Location
}

Write-Host "OK CLI engine built" -ForegroundColor Green
Write-Host ""

$setupArgs = @("setup")
if ($DryRun) {
$setupArgs += "--dry-run"
}

& node (Join-Path $ScriptDir "dist\cli.js") @setupArgs
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}

Write-Host ""
Write-Host "OK Setup complete." -ForegroundColor Green
Write-Host ""
Write-Host "-> Verify: ask your AI tool to read .mex/ROUTER.md"
Write-Host "-> Run: node .mex/dist/cli.js check"
Write-Host "-> Or: npx mex-agent check"
9 changes: 9 additions & 0 deletions setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,14 @@ if [ "$DRY_RUN" -eq 1 ]; then
echo ""
fi

# Windows + WSL: warn when the project lives on a Windows-mounted drive.
if grep -qi microsoft /proc/version 2>/dev/null && [[ "$PROJECT_DIR" == /mnt/* ]]; then
info "Windows filesystem detected — bundled CLI runs from PowerShell after setup."
info "On Windows, prefer: npx mex-agent setup or .mex/setup.ps1"
info "Do not mix WSL npm install with native Windows Node in the same .mex folder."
echo ""
fi

# ─────────────────────────────────────────────────────────────
# Step 1 — Build CLI engine (if Node available)
# ─────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -148,6 +156,7 @@ elif command -v node &>/dev/null; then
if [ -f "$SCRIPT_DIR/dist/cli.js" ]; then
MEX_CMD="node $SCRIPT_DIR/dist/cli.js"
ok "CLI engine built — drift detection, pre-analysis, and targeted sync ready"
info "Bundled CLI runs without .mex/node_modules (safe to use from Windows terminals)"
fi
fi
else
Expand Down
9 changes: 2 additions & 7 deletions src/drift/checkers/path.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { existsSync, readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { resolve } from "node:path";
import { globSync } from "glob";
import YAML from "yaml";
Expand Down Expand Up @@ -136,13 +135,9 @@ function pathExists(
if (scopedMatch) {
const pkgName = `@${scopedMatch[1]}/${scopedMatch[2]}`;

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

// Check workspace names (handles package managers that don't symlink
Expand Down
119 changes: 119 additions & 0 deletions test/cli-bundle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { execSync } from "node:child_process";
import {
copyFileSync,
mkdirSync,
mkdtempSync,
readFileSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { beforeAll, describe, expect, it } from "vitest";

const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
const cliPath = join(repoRoot, "dist/cli.js");

/** Node built-ins that may appear as bare imports in the bundled CLI. */
const NODE_BUILTINS = new Set([
"assert",
"buffer",
"child_process",
"crypto",
"events",
"fs",
"fs/promises",
"module",
"os",
"path",
"process",
"readline",
"readline/promises",
"stream",
"string_decoder",
"tty",
"url",
"util",
]);

function writeMinimalScaffold(projectRoot: string, mexDir: string): void {
const frontmatter = (name: string) => `---
name: ${name}
description: test
triggers: []
edges: []
last_updated: 2026-06-06
---
content
`;

writeFileSync(join(mexDir, "ROUTER.md"), frontmatter("router"));
writeFileSync(join(mexDir, "AGENTS.md"), "# Agents\n[Project Name]\n");
for (const name of ["architecture", "stack", "conventions", "decisions", "setup"]) {
writeFileSync(join(mexDir, "context", `${name}.md`), frontmatter(name));
}
writeFileSync(
join(mexDir, "patterns", "INDEX.md"),
`${frontmatter("index")}\n| Pattern | Description |\n|---------|-------------|\n`,
);

execSync("git init -q", { cwd: projectRoot });
execSync("git add -A", { cwd: projectRoot });
execSync('git -c user.email=test@test.com -c user.name=test commit -q -m init', {
cwd: projectRoot,
});
}

describe("bundled CLI (Windows/WSL issue #10)", () => {
beforeAll(() => {
execSync("npm run build", { cwd: repoRoot, stdio: "pipe" });
}, 120_000);

it("does not leave npm package imports in dist/cli.js", () => {
const source = readFileSync(cliPath, "utf8");
const externalImports = [
...source.matchAll(/^import\s+.+\s+from\s+["']([^./][^"']*)["']/gm),
]
.map((match) => match[1])
.filter((name) => !name.startsWith("node:") && !NODE_BUILTINS.has(name));

expect(externalImports).toEqual([]);
});

it("runs --version without .mex/node_modules", () => {
const tmp = mkdtempSync(join(tmpdir(), "mex-bundle-"));
const mexDir = join(tmp, ".mex");
mkdirSync(join(mexDir, "dist"), { recursive: true });
copyFileSync(cliPath, join(mexDir, "dist/cli.js"));
copyFileSync(join(repoRoot, "package.json"), join(mexDir, "package.json"));

const out = execSync("node dist/cli.js --version", {
cwd: mexDir,
encoding: "utf8",
});

const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")) as {
version: string;
};
expect(out.trim()).toBe(pkg.version);
});

it("runs check --quiet on a minimal scaffold without node_modules", () => {
const tmp = mkdtempSync(join(tmpdir(), "mex-bundle-check-"));
const mexDir = join(tmp, ".mex");
mkdirSync(join(mexDir, "dist"), { recursive: true });
mkdirSync(join(mexDir, "context"), { recursive: true });
mkdirSync(join(mexDir, "patterns"), { recursive: true });

copyFileSync(cliPath, join(mexDir, "dist/cli.js"));
copyFileSync(join(repoRoot, "package.json"), join(mexDir, "package.json"));
writeMinimalScaffold(tmp, mexDir);

const out = execSync("node .mex/dist/cli.js check --quiet", {
cwd: tmp,
encoding: "utf8",
});

expect(out.trim()).toMatch(/^mex: drift score \d+\/100/);
});
});
40 changes: 38 additions & 2 deletions tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
import { defineConfig } from "tsup";

/** Stub optional ink devtools peer so the bundled CLI build succeeds without it. */
const stubReactDevtools = {
name: "stub-react-devtools-core",
setup(build: {
onResolve: (
args: { filter: RegExp },
callback: (
args: { path: string },
) => { path: string; namespace: string },
) => void;
onLoad: (
args: { filter: RegExp; namespace: string },
callback: () => { contents: string; loader: "js" },
) => void;
}) {
build.onResolve({ filter: /^react-devtools-core$/ }, (args) => ({
path: args.path,
namespace: "react-devtools-stub",
}));
build.onLoad({ filter: /.*/, namespace: "react-devtools-stub" }, () => ({
contents: "export default { initialize() {}, connectToDevTools() {} };",
loader: "js",
}));
},
};

/**
* Two-config build:
* - cli → dist/cli.js (shebang banner, no .d.ts; consumed by `bin`)
Expand All @@ -15,8 +41,18 @@ export default defineConfig([
splitting: false,
sourcemap: true,
dts: false,
banner: {
js: "#!/usr/bin/env node",
// Bundle all npm deps into dist/cli.js so `node .mex/dist/cli.js` works on
// Windows without a .mex/node_modules tree (fixes WSL build + Windows runtime).
noExternal: [/.*/],
esbuildPlugins: [stubReactDevtools],
esbuildOptions(options) {
options.banner = {
js: [
"#!/usr/bin/env node",
"import { createRequire } from 'node:module';",
"const require = createRequire(import.meta.url);",
].join("\n"),
};
},
},
{
Expand Down
2 changes: 2 additions & 0 deletions update.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ banner() {
# These are owned by mex, not the user's populated content
INFRA_FILES=(
"setup.sh"
"setup.ps1"
"update.sh"
"sync.sh"
"visualize.sh"
Expand Down Expand Up @@ -190,6 +191,7 @@ done

# Preserve executable permissions on scripts
chmod +x "$SCRIPT_DIR/setup.sh" 2>/dev/null || true
chmod +x "$SCRIPT_DIR/setup.ps1" 2>/dev/null || true
chmod +x "$SCRIPT_DIR/update.sh" 2>/dev/null || true
chmod +x "$SCRIPT_DIR/sync.sh" 2>/dev/null || true
chmod +x "$SCRIPT_DIR/visualize.sh" 2>/dev/null || true
Expand Down
Loading