import { z } from "incur"; import type { Logger } from "pino"; import { CutterBridgeClient } from "../cutter/CutterBridgeClient.js"; import { CutterBridgeService } from "../cutter/CutterBridgeService.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; import { logCliCommand } from "../cliLogging.js"; import { finishAnalysisCommand } from "./finishAnalysisCommand.js"; import { err, type Result } from "../domain/result.js"; import type { CutterBridgeDiscovery } from "../cutter/CutterBridgeClient.js"; import { AnalysisError } from "../domain/analysisErrorBase.js"; import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; import { ProviderAdapterError } from "../domain/providerAdapterError.js"; import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import { withCommandCancellation } from "./commandCancellation.js"; import type { CliInstance } from "./types.js"; import { cliIntegerArgument } from "./options.js"; /** Register commands that control an already-running Cutter instance. */ export const registerCutterCommands = ( cli: CliInstance, logger: Logger, environment: Readonly, ): void => { const createCutter = () => new CutterBridgeService(new CutterBridgeClient(environment)); cli.command(CLI_COMMANDS.listCutterSessions, { description: "List live Cutter instances connected through the REA Python plugin bridge", args: z.object({}), run: () => withCommandCancellation((signal) => logCliCommand(logger, CLI_COMMANDS.listCutterSessions, async () => { const cutter = createCutter(); let result: Result; try { result = await cutter.listSessions({ signal }); } catch (cause: unknown) { result = err( cause instanceof AnalysisError ? cause : new ProviderAdapterError("cutter", "list_cutter_sessions", { cause, }), ); } try { await cutter.close(); } catch (cause: unknown) { const cleanup = cause instanceof AnalysisError ? cause : new ProviderAdapterError("cutter", "list_cutter_sessions", { cause, }); result = err( result.ok ? cleanup : analysisErrorWithCleanupFailure( result.error, cleanup, "list_cutter_sessions", ), ); } if (result.ok && signal.aborted) result = err( new AnalysisCancelledError("list_cutter_sessions", { cause: signal.reason, }), ); return result.ok ? result.value : projectAnalysisError(result.error); }), ), }); cli.command(CLI_COMMANDS.cutterCommand, { description: "Execute one Rizin command in a selected live Cutter instance", args: z.object({ sessionId: z .string() .uuid() .describe("Session ID copied from list-cutter-sessions."), expectedGeneration: cliIntegerArgument(0).describe( "Session generation returned by list-cutter-sessions.", ), command: z .string() .min(1) .describe("One unrestricted Rizin command to run in Cutter.") .refine((value) => !/[\r\n]/u.test(value)), }), options: z.object({ jsonOutput: z .boolean() .optional() .describe( "Return command-specific JSON output when available (independent of CLI output formatting).", ), json: z .boolean() .optional() .describe( "Deprecated compatibility input: use --json-output; pass --json=true or --json=false (bare --json formats CLI output).", ), }), run: ({ args, options }) => withCommandCancellation((signal) => logCliCommand(logger, CLI_COMMANDS.cutterCommand, async () => { const cutter = createCutter(); return finishAnalysisCommand( () => cutter.execute( { session_id: args.sessionId, expected_generation: args.expectedGeneration, command: args.command, json: options.jsonOutput ?? options.json ?? false, }, { signal }, ), cutter, "cutter", "cutter_command", signal, ); }), ), }); };