Merge pull request #244 from morluto/fix/source-checkout-startup

fix(cli): make clean source checkouts start reliably
This commit is contained in:
morluto
2026-07-15 22:46:42 +08:00
committed by GitHub
5 changed files with 88 additions and 10 deletions
+10 -5
View File
@@ -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
+2 -3
View File
@@ -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": {
+35 -1
View File
@@ -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"
);
}
+29
View File
@@ -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"),
});
}
});
});
+12 -1
View File
@@ -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();
});
});