fix(cli): cancel routed JavaScript analysis in rea analyze (#1156)

`rea analyze PATH` routes a JavaScript directory or ASAR to the same
static analysis as `rea analyze-javascript-application`, but it passed
no signal and was not wrapped in withCommandCancellation. On SIGINT the
process was killed with no output (signal SIGINT, empty stdout).
The dedicated command returns a typed `cancelled` result and exits 130.
The #1098 review raised this route.

Decide the route first. A JavaScript target now runs exactly like the
dedicated command: cancellation wraps logging, so the cancelled exit
code is not replaced by the logged failure status. The binary route is
unchanged and keeps its own SIGINT handling in runDirectAnalysis.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822andClaude Opus 5.5 authored and GitHub committed 2026-10-09 09:48:10 +08:00
1 parent 72201668ed
commit 089bbf58f3
2 files changed
+47 -43

No files matched your search

+39 -39
View File
@@ -14,10 +14,8 @@ import {
} from "./options.js";
import type { CliInstance } from "./types.js";
import { runCliJavaScriptApplicationAnalysis } from "./javascriptApplicationAnalysis.js";
import type {
CliCommandOutput,
CliResultOutput,
} from "./streamedJsonOutput.js";
import { withCommandCancellation } from "./commandCancellation.js";
import type { CliResultOutput } from "./streamedJsonOutput.js";
/** Register provider-neutral binary overview and procedure CLI commands. */
export const registerCoreBinaryCommands = (
@@ -54,17 +52,38 @@ const registerOverviewCommands = (
path: z.string().describe("App, program, or analysis database path"),
}),
options: overviewOptions,
run: ({ args, options, format }) =>
logCliCommand(logger, "analyze", () =>
runRoutedOverview(
run: async ({ args, options, format }) => {
const output =
resultOutput === undefined
? undefined
: { output: resultOutput, command: CLI_COMMANDS.analyze, format };
// Route JavaScript targets exactly like analyze-javascript-application,
// including typed cancellation; cancellation wraps logging so its exit
// code is not replaced by the logged failure status.
if (await routesToJavaScriptAnalysis(args.path, options))
return withCommandCancellation((signal) =>
logCliCommand(logger, "analyze", () =>
runCliJavaScriptApplicationAnalysis(
{ input_path: resolve(args.path) },
output,
signal,
),
),
);
return logCliCommand(logger, "analyze", () =>
runDirectAnalysis(
args.path,
options,
logger,
resultOutput === undefined
? undefined
: { output: resultOutput, command: CLI_COMMANDS.analyze, format },
"binary_overview",
{},
directAnalysisOptions(
logger,
options.snapshot,
options.provider,
options["target-format"],
),
),
),
);
},
});
cli.command(CLI_COMMANDS.inspect, {
description: "Inspect an app overview with evidence",
@@ -140,38 +159,19 @@ const registerDecompileCommand = (cli: CliInstance, logger: Logger): void => {
});
};
const runRoutedOverview = async (
/** Select static JavaScript analysis only when no binary-analysis option applies. */
const routesToJavaScriptAnalysis = async (
path: string,
options: {
readonly snapshot?: string | undefined;
readonly "target-format"?: "dos-com" | undefined;
readonly provider?: string | undefined;
},
logger: Logger,
output?: CliCommandOutput,
) => {
if (
options.provider === undefined &&
options.snapshot === undefined &&
options["target-format"] === undefined &&
(await isJavaScriptApplicationPath(path))
)
return runCliJavaScriptApplicationAnalysis(
{ input_path: resolve(path) },
output,
);
return runDirectAnalysis(
path,
"binary_overview",
{},
directAnalysisOptions(
logger,
options.snapshot,
options.provider,
options["target-format"],
),
);
};
): Promise<boolean> =>
options.provider === undefined &&
options.snapshot === undefined &&
options["target-format"] === undefined &&
(await isJavaScriptApplicationPath(path));
const isJavaScriptApplicationPath = async (path: string): Promise<boolean> => {
const lower = path.toLowerCase();
@@ -34,14 +34,18 @@ it("returns a nonzero exit for a streamed JSON analysis failure", async () => {
});
});
it.skipIf(process.platform === "win32")(
"returns a typed cancelled CLI result and exit 130 after SIGINT",
async () => {
// `analyze` routes a JavaScript directory to the same workflow and must keep
// its typed cancellation contract.
it
.skipIf(process.platform === "win32")
.each(["analyze-javascript-application", "analyze"])(
"returns a typed cancelled CLI result and exit 130 after SIGINT through %s",
async (command) => {
const root = await createTestTempDirectory("rea-cli-js-cancellation-");
await writeFile(join(root, "main.js"), "export const observed = 1;\n");
const child = spawn(
process.execPath,
["scripts/rea.mjs", "analyze-javascript-application", root, "--json"],
["scripts/rea.mjs", command, root, "--json"],
{
stdio: ["ignore", "pipe", "pipe"],
},