From ce3005bf958979dac1a1e86776ab5adc4daa9427 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 6 Jun 2026 21:14:29 +0000 Subject: [PATCH] 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. --- README.md | 16 +++-- setup.ps1 | 71 ++++++++++++++++++++++ setup.sh | 9 +++ src/drift/checkers/path.ts | 9 +-- test/cli-bundle.test.ts | 119 +++++++++++++++++++++++++++++++++++++ tsup.config.ts | 40 ++++++++++++- update.sh | 2 + 7 files changed, 253 insertions(+), 13 deletions(-) create mode 100644 setup.ps1 create mode 100644 test/cli-bundle.test.ts diff --git a/README.md b/README.md index 1306002..e5c7390 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/setup.ps1 b/setup.ps1 new file mode 100644 index 0000000..cc15b76 --- /dev/null +++ b/setup.ps1 @@ -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" diff --git a/setup.sh b/setup.sh index 18005df..7222a5d 100755 --- a/setup.sh +++ b/setup.sh @@ -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) # ───────────────────────────────────────────────────────────── @@ -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 diff --git a/src/drift/checkers/path.ts b/src/drift/checkers/path.ts index 892136e..6418ca4 100644 --- a/src/drift/checkers/path.ts +++ b/src/drift/checkers/path.ts @@ -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"; @@ -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 diff --git a/test/cli-bundle.test.ts b/test/cli-bundle.test.ts new file mode 100644 index 0000000..9e9ee24 --- /dev/null +++ b/test/cli-bundle.test.ts @@ -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/); + }); +}); diff --git a/tsup.config.ts b/tsup.config.ts index 059c703..e8a5969 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -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`) @@ -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"), + }; }, }, { diff --git a/update.sh b/update.sh index 3bc7765..21d9438 100755 --- a/update.sh +++ b/update.sh @@ -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" @@ -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