fix(cli): refuse silent direct command-registry execution (#11204)

Fail visibly with a dispatcher hint and exit status 1 when the compiled command registry is executed directly. Reuse the existing argv/module identity owner for both entry modules and preserve normal imports and supported dispatch.

Fixes #10762.
Builds on #10766.

Co-authored-by: 2SinceJuly <202121119+2SinceJuly@users.noreply.github.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-11-15 22:23:49 +08:00
1 parent 664fab7470
commit 6136c6f3ba
5 files changed
+116 -16

No files matched your search

+13
View File
@@ -1,4 +1,8 @@
import type { FridaInstrumentationPort } from "./application/frida/FridaInstrumentationPort.js";
import {
CURRENT_ENTRYPOINT_PATH,
isDirectEntrypoint,
} from "./process/CurrentEntrypointPath.js";
import {
CLI_COMMAND_REGISTRATIONS,
CLI_REGISTRATION_ORDER,
@@ -141,3 +145,12 @@ export const createCli = (
module: CLI_COMMAND_MODULES[id],
})),
});
// Direct execution needs a visible failure pointing to the dispatcher. Imports
// only expose the registry. A normal exit lets piped stderr drain completely.
if (isDirectEntrypoint(CURRENT_ENTRYPOINT_PATH, import.meta.url)) {
process.stderr.write(
"dist/cli.js is the command registry, not the CLI entry point. Run `rea` (scripts/rea.mjs) instead.\n",
);
process.exitCode = 1;
}
+4 -16
View File
@@ -2,9 +2,10 @@
import { snapshotEnvironment } from "./process/snapshotEnvironment.js";
import { HOST_PLATFORM } from "./platform/HostPlatform.js";
import { CURRENT_ENTRYPOINT_PATH } from "./process/CurrentEntrypointPath.js";
import { pathToFileURL } from "node:url";
import { realpathSync } from "node:fs";
import {
CURRENT_ENTRYPOINT_PATH,
isDirectEntrypoint,
} from "./process/CurrentEntrypointPath.js";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
@@ -206,19 +207,6 @@ export const runMcpHttpEntrypoint = async (
}
};
/** Return false when the runtime's argv path is synthetic or no longer exists. */
const isDirectEntrypoint = (
entryPath: string | undefined,
moduleUrl: string,
): boolean => {
if (entryPath === undefined) return false;
try {
return pathToFileURL(realpathSync(entryPath)).href === moduleUrl;
} catch {
return false;
}
};
if (isDirectEntrypoint(CURRENT_ENTRYPOINT_PATH, import.meta.url)) {
void runEntrypoint();
}
+37
View File
@@ -0,0 +1,37 @@
import { symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { isDirectEntrypoint } from "./CurrentEntrypointPath.js";
const temporaryScript = async (): Promise<{
path: string;
url: string;
}> => {
const directory = await createTestTempDirectory("rea-entrypoint-");
const path = join(directory, "module.js");
await writeFile(path, "export {};\n");
return { path, url: pathToFileURL(path).href };
};
it("matches a symlinked argv path by its real location", async () => {
const { path, url } = await temporaryScript();
const directory = await createTestTempDirectory("rea-entrypoint-link-");
const link = join(directory, "link.js");
await symlink(path, link);
expect(isDirectEntrypoint(link, url)).toBe(true);
});
it("reports no direct run when no argv path was supplied", async () => {
const { url } = await temporaryScript();
expect(isDirectEntrypoint(undefined, url)).toBe(false);
});
it("reports no direct run when the argv path no longer exists", async () => {
const { url } = await temporaryScript();
const directory = await createTestTempDirectory("rea-entrypoint-gone-");
expect(isDirectEntrypoint(join(directory, "missing.js"), url)).toBe(false);
});
+17
View File
@@ -1,4 +1,6 @@
import { realpathSync } from "node:fs";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
import {
javaScriptRuntime,
type JavaScriptRuntime,
@@ -19,3 +21,18 @@ export const normalizeEntrypointPath = (
: resolve(entrypoint);
export const CURRENT_ENTRYPOINT_PATH = normalizeEntrypointPath(process.argv[1]);
/** True when the runtime executed `moduleUrl` directly rather than importing it.
* A synthetic argv path that no longer resolves is not a direct run.
*/
export const isDirectEntrypoint = (
entryPath: string | undefined,
moduleUrl: string,
): boolean => {
if (entryPath === undefined) return false;
try {
return pathToFileURL(realpathSync(entryPath)).href === moduleUrl;
} catch {
return false;
}
};
@@ -0,0 +1,45 @@
import { expect } from "vitest";
import { processTest } from "../../support/process/processFixture.js";
// Exercise the compiled registry and supported dispatcher; test:focused builds them.
processTest.for([[], ["--version"]] as const)(
"refuses a direct run of the compiled command registry with %j",
async (arguments_, { processes }) => {
const direct = await processes.run(process.execPath, [
"dist/cli.js",
...arguments_,
]);
expect(direct.exitCode).toBe(1);
expect(direct.stdout).toBe("");
expect(direct.stderr).toContain("dist/cli.js is the command registry");
expect(direct.stderr).toContain("scripts/rea.mjs");
},
);
processTest(
"still serves --version through the dispatcher",
async ({ processes }) => {
const dispatched = await processes.run(process.execPath, [
"scripts/rea.mjs",
"--version",
]);
expect(dispatched.exitCode).toBe(0);
expect(dispatched.stderr).toBe("");
expect(dispatched.stdout.trim()).not.toBe("");
},
);
processTest(
"imports the registry without serving or replacing the caller exit status",
async ({ processes }) => {
const imported = await processes.run(process.execPath, [
"--input-type=module",
"--eval",
'process.exitCode = 23; const registry = await import("./dist/cli.js"); process.stdout.write(typeof registry.createCli);',
]);
expect(imported.exitCode).toBe(23);
expect(imported.stdout).toBe("function");
expect(imported.stderr).toBe("");
},
);