From 6ba97656ca73e97ae1dbecc15a86a193978298b6 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 15 Jul 2026 14:44:04 +0000 Subject: [PATCH] fix(cli): make clean source checkouts start reliably --- CONTRIBUTING.md | 15 ++++++++++----- package.json | 5 ++--- scripts/rea.mjs | 36 +++++++++++++++++++++++++++++++++++- tests/dispatcher.test.ts | 29 +++++++++++++++++++++++++++++ tests/identity.test.ts | 13 ++++++++++++- 5 files changed, 88 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cb1ae8d8d..ded183066 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,10 +9,14 @@ REA development requires Node.js 24.18.x and npm 11.16.x. Real-Hopper verificati ```bash npm ci npm run rebuild:native # only when the packaged PTY binary is incompatible -npm run build npm test ``` +`npm ci` installs the exact dependencies, builds the compiled runtime through +the package `prepare` lifecycle, and prepares the Husky hook. Run +`npm run build` again after source changes when you need the standalone CLI or +MCP server without running the test prebuild. + Keep dependencies flowing inward through the existing domain, contracts, provider, application, server, and adapter layers. Parse unknown values at process and protocol boundaries, model expected failures with `Result`, and preserve the 33 direct, 10 enhanced, 5 native, 2 artifact, 8 browser, 2 Electron, and 18 session tools (78 total) unless a deliberate contract change updates every verifier and snapshot. Before submitting a pull request, run: @@ -23,10 +27,11 @@ npm run verify:package npm pack --dry-run ``` -`npm install` prepares the Husky pre-commit hook. Commits format and lint staged -files with lint-staged, then typecheck the complete project. The normal test and -CI coverage runs enforce the thresholds in `vitest.config.ts`; `npm run -lint:dead` rejects unused files, exports, and dependencies. +`npm install` builds the compiled runtime and prepares the Husky pre-commit +hook. Commits format and lint staged files with lint-staged, then typecheck the +complete project. The normal test and CI coverage runs enforce the thresholds +in `vitest.config.ts`; `npm run lint:dead` rejects unused files, exports, and +dependencies. Set `REA_LOG_LEVEL` to `trace`, `debug`, `info`, `warn`, `error`, `fatal`, or `silent` to control structured JSON diagnostics. MCP mode defaults to `info` and diff --git a/package.json b/package.json index 4281b9dc0..1209fa27c 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "lint:dead": "knip", "jscpd": "jscpd src/ tests/", "scan:todos": "node scripts/scan-todos.mjs", - "start": "node dist/main.js", + "start": "node scripts/rea.mjs mcp", "pretest": "npm run build", "postinstall": "node scripts/prepare-node-pty.mjs", "test": "vitest run", @@ -82,8 +82,7 @@ "docs:generate": "npm run build && node scripts/generate-product-catalog.mjs && typedoc && node scripts/generate-error-schema.mjs", "docs:check": "node scripts/generate-package-metadata.mjs --check && npm run build && node scripts/generate-product-catalog.mjs --check && node scripts/check-doc-facts.mjs && node scripts/check-typedoc.mjs && node scripts/generate-error-schema.mjs --check", "docs:error-schema": "npm run build && node scripts/generate-error-schema.mjs", - "prepack": "npm run build", - "prepare": "husky", + "prepare": "npm run build && husky", "rebuild:native": "node scripts/rebuild-native.mjs" }, "lint-staged": { diff --git a/scripts/rea.mjs b/scripts/rea.mjs index 2ad29c8e0..787cdab8e 100755 --- a/scripts/rea.mjs +++ b/scripts/rea.mjs @@ -1,5 +1,9 @@ #!/usr/bin/env node +import { access } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + // Route production MCP before importing Incur. Incur owns registration helpers // such as `mcp add`, while only dist/main.js may serve the 78-tool stdio server. const args = process.argv.slice(2); @@ -9,8 +13,17 @@ const { default: packageJson } = await import("../package.json", { process.env.REA_PACKAGE_VERSION = packageJson.version; const isMcpMode = args.length === 1 && (args[0] === "--mcp" || args[0] === "mcp"); +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const runtimeFiles = isMcpMode + ? ["dist/main.js"] + : ["dist/cli.js", "dist/cliOutput.js"]; -if (isMcpMode) { +if (!(await compiledRuntimeExists(runtimeFiles))) { + process.stderr.write( + `REA's compiled runtime is missing. Run \`npm ci\` in ${packageRoot} to install dependencies and build REA, then restart it. If this is an installed package, reinstall rea-agents.\n`, + ); + process.exitCode = 1; +} else if (isMcpMode) { const { runEntrypoint } = await import("../dist/main.js"); await runEntrypoint(); } else { @@ -20,3 +33,24 @@ if (isMcpMode) { stdout: (output) => process.stdout.write(sanitizeCliOutput(output)), }); } + +async function compiledRuntimeExists(paths) { + for (const path of paths) { + try { + await access(resolve(packageRoot, path)); + } catch (cause) { + if (isMissing(cause)) return false; + throw cause; + } + } + return true; +} + +function isMissing(cause) { + return ( + typeof cause === "object" && + cause !== null && + "code" in cause && + cause.code === "ENOENT" + ); +} diff --git a/tests/dispatcher.test.ts b/tests/dispatcher.test.ts index 2d082f989..ad38b66b7 100644 --- a/tests/dispatcher.test.ts +++ b/tests/dispatcher.test.ts @@ -61,4 +61,33 @@ describe("executable dispatcher", () => { args: ["--mcp", "extra"], }); }); + + it("explains how to restore a missing compiled runtime", async () => { + fixtureRoot = await mkdtemp(join(tmpdir(), "rea-dispatcher-")); + const scripts = join(fixtureRoot, "scripts"); + await mkdir(scripts); + await Promise.all([ + copyFile("scripts/rea.mjs", join(scripts, "rea.mjs")), + writeFile( + join(fixtureRoot, "package.json"), + JSON.stringify({ type: "module", version: "1.0.0" }), + ), + ]); + + for (const args of [["mcp"], ["--help"]]) { + const execution = execFileAsync(process.execPath, [ + join(scripts, "rea.mjs"), + ...args, + ]); + await expect(execution).rejects.toMatchObject({ + code: 1, + stderr: expect.stringContaining( + "REA's compiled runtime is missing. Run `npm ci` in", + ), + }); + await expect(execution).rejects.toMatchObject({ + stderr: expect.not.stringContaining("ERR_MODULE_NOT_FOUND"), + }); + } + }); }); diff --git a/tests/identity.test.ts b/tests/identity.test.ts index 01196cb01..98fca65bd 100644 --- a/tests/identity.test.ts +++ b/tests/identity.test.ts @@ -8,7 +8,15 @@ import { PRODUCT_IDENTITY } from "../src/identity.js"; describe("package identity", () => { it("keeps the manifest, executable, and floating MCP command aligned", async () => { const packageJson = z - .object({ name: z.string(), bin: z.record(z.string(), z.string()) }) + .object({ + name: z.string(), + bin: z.record(z.string(), z.string()), + scripts: z.object({ + start: z.string(), + prepare: z.string(), + prepack: z.string().optional(), + }), + }) .parse( JSON.parse( await readFile( @@ -20,5 +28,8 @@ describe("package identity", () => { expect(packageJson.name).toBe(PRODUCT_IDENTITY.packageName); expect(Object.keys(packageJson.bin)).toEqual([PRODUCT_IDENTITY.cliBinary]); expect(PRODUCT_IDENTITY.mcpCommand).toBe(`npx -y ${packageJson.name} mcp`); + expect(packageJson.scripts.start).toBe("node scripts/rea.mjs mcp"); + expect(packageJson.scripts.prepare).toBe("npm run build && husky"); + expect(packageJson.scripts.prepack).toBeUndefined(); }); });