Files
reverse-engineering-anything/src/cli/electronCommands.ts
T
morluto 93c620c3e7 fix(browser): own transport cleanup through finalization
Own CDP transports from admission through observed close, retain failed
owners for retry, and keep active peers separate from pending cleanup.
Preserve primary errors and completed facts through final cancellation.
Forward passive Electron CLI cancellation and dispose browser, runtime and
scenario owners from CLI and MCP shutdown.

Refs #4255, #4256, #4257
2026-10-16 06:46:26 +08:00

323 lines
11 KiB
TypeScript

import { isAbsolute, resolve, sep } from "node:path";
import { Cli, z } from "incur";
import { logCliCommand } from "../cliLogging.js";
import {
inspectElectronPageInputSchema,
listElectronTargetsInputSchema,
} from "../domain/javascript/electronObservation.js";
import { electronActiveObservationInputSchema } from "../domain/javascript/electronActiveObservation.js";
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { projectInputIssues } from "../domain/inputIssueProjection.js";
import type { JsonValue } from "../domain/jsonValue.js";
import type { Logger } from "pino";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { createElectronFuseService } from "../composition/electronFuses.js";
import { parseCliJsonInput, resolveCliJsonPaths } from "../cliJsonInput.js";
import {
electronPageInspectionOptions,
javascriptApplicationOptions,
} from "../cliObservationOptions.js";
import { runCliJavaScriptApplicationAnalysis } from "./javascriptApplicationAnalysis.js";
import { withCommandCancellation } from "./commandCancellation.js";
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
/** Register CLI equivalents of the Electron MCP tools. */
export const registerElectronCommands = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
environment: Readonly<Record<string, string | undefined>>,
): void => {
registerElectronObservationCommands(cli, logger);
registerElectronFuseCommand(cli, logger);
registerElectronActiveCommand(cli, logger, environment);
registerJavaScriptApplicationCommand(cli, logger);
registerJavaScriptRuntimeReconciliationCommand(cli, logger);
};
const registerElectronFuseCommand = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
): void => {
const service = createElectronFuseService();
cli.command(CLI_COMMANDS.inspectElectronFuses, {
description:
"Inspect packaged Electron fuse bytes without launching the application",
args: z.object({
path: z
.string()
.describe(
"Local Electron .app, Electron Framework binary, PE executable, or ELF executable",
),
}),
run: ({ args }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, CLI_COMMANDS.inspectElectronFuses, async () => {
const result = await service.inspect(
{ path: absoluteElectronFusePath(args.path) },
{ signal },
);
return result.ok ? result.value : projectAnalysisError(result.error);
}),
),
});
};
/** Anchor relative input without changing which file symlink/.. selects. */
const absoluteElectronFusePath = (path: string): string => {
if (path === "") return path;
// Win32 resolves dot components before directory links, including drive-relative paths.
if (sep === "\\") return resolve(path);
if (isAbsolute(path)) return path;
const base = process.cwd();
return `${base}${base.endsWith(sep) ? "" : sep}${path}`;
};
const registerElectronActiveCommand = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
environment: Readonly<Record<string, string | undefined>>,
): void => {
cli.command(CLI_COMMANDS.captureElectronScenario, {
description: "Run one owned Electron scenario with cancellable actions",
args: z.object({
inputJson: z
.string()
.describe(
"Inline JSON or a JSON file matching capture_electron_scenario",
),
}),
run: ({ args }) =>
withCommandCancellation((signal) =>
logCliCommand(
logger,
CLI_COMMANDS.captureElectronScenario,
async () => {
const input = await parseCliJsonInput(
args.inputJson,
"capture_electron_scenario",
);
if (!input.ok) return input.error;
const parsed = electronActiveObservationInputSchema.safeParse(
resolveCliJsonPaths(input.value, [
["executable_path"],
["application_path"],
["application_root"],
]),
);
if (!parsed.success)
return inputError(
"capture_electron_scenario",
parsed.error.issues,
input.value,
);
const { captureElectronScenario } =
await import("../application/javascript/ElectronActiveObservationService.js");
const { createElectronScenarioProvider } =
await import("../composition/electronScenario.js");
const result = await captureElectronScenario(
createElectronScenarioProvider(environment),
parsed.data,
{ signal },
);
return result.ok ? result.value : cliError(result.error);
},
),
),
});
};
const registerJavaScriptRuntimeReconciliationCommand = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
): void => {
cli.command(CLI_COMMANDS.reconcileJavaScriptRuntime, {
description:
"Reconcile static application and passive runtime Evidence JSON",
args: z.object({
inputJson: z
.string()
.describe(
"Inline JSON or a JSON file path matching reconcile_javascript_runtime",
),
}),
run: ({ args }) =>
logCliCommand(
logger,
CLI_COMMANDS.reconcileJavaScriptRuntime,
async () => {
const input = await parseCliJsonInput(
args.inputJson,
"reconcile_javascript_runtime",
);
if (!input.ok) return input.error;
const { reconcileJavaScriptRuntimeEvidence } =
await import("../application/javascript/JavaScriptRuntimeReconciliationService.js");
const result = reconcileJavaScriptRuntimeEvidence(input.value);
return result.ok ? result.value : cliError(result.error);
},
),
});
};
const registerElectronObservationCommands = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
): void => {
registerElectronTargetList(cli, logger);
registerElectronPageInspection(cli, logger);
};
const registerElectronTargetList = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
): void => {
cli.command(CLI_COMMANDS.listElectronTargets, {
description:
"List local file pages exposed by the selected Electron CDP endpoint",
args: z.object({
endpoint: z.string().describe("Literal-loopback Electron CDP endpoint"),
}),
run: ({ args }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, "list-electron-targets", async () => {
const context = await electronObservationContext();
const parsed = listElectronTargetsInputSchema.safeParse({
cdp_endpoint: args.endpoint,
});
if (!parsed.success) return inputError("list_electron_targets");
const { listElectronTargets } =
await import("../application/javascript/ElectronObservationService.js");
return await finishAnalysisCommand(
() =>
listElectronTargets(context.provider, parsed.data, { signal }),
{
close: async () => {
await context.provider.close?.();
},
},
"rea-cdp-electron",
"list_electron_targets",
undefined,
signal,
);
}),
),
});
};
const registerElectronPageInspection = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
): void => {
cli.command(CLI_COMMANDS.inspectElectronPage, {
description: "Passively inspect one Electron file page",
args: z.object({
endpoint: z.string().describe("Literal-loopback Electron CDP endpoint"),
targetId: z.string().describe("Target ID from list-electron-targets"),
}),
options: electronPageInspectionOptions,
run: ({ args, options }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, "inspect-electron-page", async () => {
const context = await electronObservationContext();
const parsed = inspectElectronPageInputSchema.safeParse({
cdp_endpoint: args.endpoint,
target_id: args.targetId,
observation_ms: options.observationMs,
include_script_sources: options.includeScriptSources,
});
if (!parsed.success) return inputError("inspect_electron_page");
const { inspectElectronPage } =
await import("../application/javascript/ElectronObservationService.js");
return await finishAnalysisCommand(
() =>
inspectElectronPage(context.provider, parsed.data, { signal }),
{
close: async () => {
await context.provider.close?.();
},
},
"rea-cdp-electron",
"inspect_electron_page",
undefined,
signal,
);
}),
),
});
};
const registerJavaScriptApplicationCommand = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
): void => {
cli.command(CLI_COMMANDS.analyzeJavaScriptApplication, {
description:
"Statically reconstruct a local JavaScript/Electron application",
args: z.object({
path: z.string().describe("ASAR or extracted application path"),
}),
options: javascriptApplicationOptions,
run: ({ args, options }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, CLI_COMMANDS.analyzeJavaScriptApplication, () =>
runCliJavaScriptApplicationAnalysis(
{
input_path: args.path,
format: options.artifactFormat,
integrity_policy: options.integrityPolicy,
...(options.maxSemanticNodes === undefined
? {}
: { max_semantic_nodes: options.maxSemanticNodes }),
...(options.maxSemanticNodesPerFile === undefined
? {}
: {
max_semantic_nodes_per_file:
options.maxSemanticNodesPerFile,
}),
...(options.sourcePath === undefined
? {}
: { source_paths: options.sourcePath }),
...(options.maxHeapMb === undefined
? {}
: { max_heap_mb: options.maxHeapMb }),
...(options.analysisTimeoutMs === undefined
? {}
: { analysis_timeout_ms: options.analysisTimeoutMs }),
},
signal,
),
),
),
});
};
const electronObservationContext = async () => {
const { createElectronObservationProvider } =
await import("../composition/electronObservation.js");
return { provider: createElectronObservationProvider() };
};
const inputError = (
operation: string,
issues?: Parameters<typeof projectInputIssues>[0],
input?: unknown,
): JsonValue =>
cliError(
new AnalysisInputError(
operation,
undefined,
issues === undefined ? [] : projectInputIssues(issues, input),
),
);
const cliError = (
error: Parameters<typeof projectAnalysisError>[0],
): JsonValue => ({
error: "Electron analysis failed",
...projectAnalysisError(error),
});