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 { absoluteCallerPath } from "../filesystem/callerPath.js"; import { finishAnalysisCommand } from "./finishAnalysisCommand.js"; import type { ElectronActiveObservationPort } from "../application/javascript/ElectronActiveObservationPort.js"; /** Register CLI equivalents of the Electron MCP tools. */ export const registerElectronCommands = ( cli: ReturnType, logger: Logger, environment: Readonly>, options: { readonly createActiveProvider?: ( environment: Readonly>, ) => ElectronActiveObservationPort; } = {}, ): void => { registerElectronObservationCommands(cli, logger); registerElectronFuseCommand(cli, logger); registerElectronActiveCommand(cli, logger, environment, options); registerJavaScriptApplicationCommand(cli, logger); registerJavaScriptRuntimeReconciliationCommand(cli, logger); }; const registerElectronFuseCommand = ( cli: ReturnType, logger: Logger, ): void => { 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 service = createElectronFuseService(); return finishAnalysisCommand( () => service.inspect( { path: absoluteElectronFusePath(args.path) }, { signal }, ), service, service.provider.identity.id, "inspect_electron_fuses", signal, ); }), ), }); }; /** 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, logger: Logger, environment: Readonly>, options: { readonly createActiveProvider?: ( environment: Readonly>, ) => ElectronActiveObservationPort; }, ): 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 provider = options.createActiveProvider === undefined ? await import("../composition/electronScenario.js").then( ({ createElectronScenarioProvider }) => createElectronScenarioProvider(environment), ) : options.createActiveProvider(environment); const result = await finishAnalysisCommand( () => captureElectronScenario(provider, parsed.data, { signal }), { close: async () => { await provider.close?.(); }, }, provider.identity().id, "capture_electron_scenario", signal, ); return "code" in result ? { error: "Electron analysis failed", ...result } : result; }, ), ), }); }; const registerJavaScriptRuntimeReconciliationCommand = ( cli: ReturnType, 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, logger: Logger, ): void => { registerElectronTargetList(cli, logger); registerElectronPageInspection(cli, logger); }; const registerElectronTargetList = ( cli: ReturnType, 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", signal, ); }), ), }); }; const registerElectronPageInspection = ( cli: ReturnType, 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", signal, ); }), ), }); }; const registerJavaScriptApplicationCommand = ( cli: ReturnType, 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: absoluteCallerPath(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.maxSemanticUnknowns === undefined ? {} : { max_semantic_unknowns: options.maxSemanticUnknowns }), ...(options.maxSemanticUnknownsPerFile === undefined ? {} : { max_semantic_unknowns_per_file: options.maxSemanticUnknownsPerFile, }), ...(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[0], input?: unknown, ): JsonValue => cliError( new AnalysisInputError( operation, undefined, issues === undefined ? [] : projectInputIssues(issues, input), ), ); const cliError = ( error: Parameters[0], ): JsonValue => ({ error: "Electron analysis failed", ...projectAnalysisError(error), });