rea/src/cli.ts

494 lines
15 KiB
TypeScript

import { Cli, z } from "incur";
import { fileURLToPath } from "node:url";
import { createInterface } from "node:readline/promises";
import { runDoctor } from "./application/Doctor.js";
import {
runDirectAnalysis,
runProviderAnalysis,
runSessionStatus,
} from "./application/DirectAnalysis.js";
import { runSetup, type SetupAction } from "./application/Setup.js";
import { runUninstall } from "./application/Uninstall.js";
import { runUpgrade, systemUpgradeHost } from "./application/Upgrade.js";
import { PRODUCT_IDENTITY } from "./identity.js";
import { createLogger, parseLogLevel, type Logger } from "./logger.js";
import { logCliCommand } from "./cliLogging.js";
import { parseConfig } from "./config.js";
import { importReferenceSource } from "./application/ReferenceSourceImport.js";
import { registerEvidenceCommands } from "./cliEvidenceCommands.js";
import { registerProcessCommands } from "./cliProcessCommands.js";
/**
* Build the one-shot Incur CLI without starting Hopper at import time.
* Analysis commands acquire and close their own sessions; bare `mcp` and
* `--mcp` are intercepted by the executable dispatcher before this module loads.
*/
export const createCli = (): ReturnType<typeof Cli.create> => {
const logger = createLogger(
"cli",
process.env.REA_LOG_LEVEL === undefined
? "silent"
: parseLogLevel(process.env.REA_LOG_LEVEL),
);
const cli = Cli.create(PRODUCT_IDENTITY.cliBinary, {
version: process.env.REA_PACKAGE_VERSION ?? "0.0.0-development",
description:
"Reverse engineer anything from your terminal or coding agent.",
mcp: {
command: PRODUCT_IDENTITY.mcpCommand,
instructions:
"Ask what software, artifact, protocol, or behavior the user wants to understand, then choose the available investigation capabilities that can produce evidence.",
},
sync: {
cwd: fileURLToPath(new URL("..", import.meta.url)),
include: ["skills/*"],
suggestions: [
"understand how a software feature works",
"investigate an artifact or observed behavior",
"check my REA setup",
],
},
});
registerCoreCommands(cli, logger);
registerFunctionCommand(cli, logger);
registerSearchCommand(cli, logger);
registerXrefsCommand(cli, logger);
registerTraceCommand(cli, logger);
registerCapabilityCommands(cli, logger);
registerNativeCommands(cli, logger);
registerArtifactCommands(cli, logger);
registerEvidenceCommands(cli, logger);
registerReferenceSourceCommand(cli, logger);
registerProcessCommands(cli, logger);
return cli;
};
const registerCoreCommands = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
): void => {
const overviewOptions = z.object({
detail: z.enum(["concise", "detailed"]).default("concise"),
limit: z.number().int().min(1).max(50).default(10),
});
cli.command("setup", {
description: "Install requirements and configure coding agents",
options: z.object({
yes: z
.boolean()
.default(false)
.describe("Approve user-owned setup actions without prompting"),
installHopper: z
.boolean()
.default(false)
.describe("Also approve Hopper installation with --yes"),
}),
alias: { yes: "y", installHopper: "install-hopper" },
run: ({ options, formatExplicit }) =>
logCliCommand(logger, "setup", () =>
runSetup(
{
approved: options.yes,
installHopper: options.installHopper,
structured: formatExplicit,
},
undefined,
options.yes || formatExplicit || process.stdin.isTTY !== true
? undefined
: confirmSetup,
),
),
});
cli.command("doctor", {
description: "Check whether REA is ready",
options: z.object({
target: z.string().optional().describe("Optional app path to check"),
}),
run: ({ options }) =>
logCliCommand(logger, "doctor", () => runDoctor(options.target)),
});
cli.command("uninstall", {
description: "Remove REA-owned agent configuration and skill files",
options: z.object({
purgeData: z
.boolean()
.default(false)
.describe("Also remove REA caches and state"),
}),
alias: { purgeData: "purge-data" },
run: ({ options }) =>
logCliCommand(logger, "uninstall", () => runUninstall(options.purgeData)),
});
cli.command("upgrade", {
description: "Upgrade a global npm installation to the latest REA release",
run: ({ formatExplicit }) =>
logCliCommand(logger, "upgrade", () =>
runUpgrade(
process.env.REA_PACKAGE_VERSION ?? "0.0.0-development",
systemUpgradeHost(),
formatExplicit ? "structured" : "human",
),
),
});
cli.command("analyze", {
description: "Get an overview of an app",
args: z.object({
path: z.string().describe("App, program, or Hopper database path"),
}),
options: overviewOptions,
run: ({ args, options }) =>
logCliCommand(logger, "analyze", () =>
runDirectAnalysis(
args.path,
"binary_overview",
{ detail: options.detail, limit: options.limit },
logger,
),
),
});
cli.command("inspect", {
description: "Inspect an app overview with evidence",
args: z.object({
path: z.string().describe("App, program, or Hopper database path"),
}),
options: overviewOptions,
run: ({ args, options }) =>
logCliCommand(logger, "inspect", () =>
runDirectAnalysis(
args.path,
"binary_overview",
{ detail: options.detail, limit: options.limit },
logger,
),
),
});
cli.command("decompile", {
description: "Read one part of an app as code",
args: z.object({
path: z.string().describe("App or program path"),
address: z.string().describe("Procedure address"),
}),
run: ({ args }) =>
logCliCommand(logger, "decompile", () =>
runDirectAnalysis(
args.path,
"procedure_pseudo_code",
{ procedure: args.address },
logger,
),
),
});
};
const confirmSetup = async (
actions: readonly SetupAction[],
): Promise<boolean> => {
process.stdout.write("\nREA setup plan\n");
for (const action of actions)
process.stdout.write(
` - ${action.detail}\n ${action.target}${action.external ? " (external software)" : ""}\n`,
);
const prompt = createInterface({
input: process.stdin,
output: process.stdout,
});
try {
const answer = (await prompt.question("Continue? [Y/n] "))
.trim()
.toLowerCase();
return answer === "" || answer === "y" || answer === "yes";
} finally {
prompt.close();
}
};
const registerXrefsCommand = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
): void => {
cli.command("xrefs", {
description: "List bounded references to an analyzed address",
args: z.object({
path: z.string().describe("App or program path"),
address: z.string().describe("Hexadecimal address"),
}),
run: ({ args }) =>
logCliCommand(logger, "xrefs", () =>
runDirectAnalysis(
args.path,
"xrefs",
{ address: args.address },
logger,
),
),
});
};
const registerTraceCommand = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
): void => {
cli.command("trace", {
description: "Trace a bounded literal feature through analyzed references",
args: z.object({
path: z.string().describe("App or program path"),
query: z.string().min(1).describe("Literal feature query"),
}),
options: z.object({
caseSensitive: z.boolean().default(false),
limit: z.number().int().min(1).max(100).default(20),
maxOperations: z.number().int().min(1).max(100).default(20),
}),
alias: {
caseSensitive: "case-sensitive",
maxOperations: "max-operations",
},
run: ({ args, options }) =>
logCliCommand(logger, "trace", () =>
runDirectAnalysis(
args.path,
"trace_feature",
{
query: args.query,
case_sensitive: options.caseSensitive,
limit: options.limit,
max_operations: options.maxOperations,
},
logger,
),
),
});
};
const registerCapabilityCommands = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
): void => {
for (const command of ["capabilities", "providers"] as const) {
cli.command(command, {
description:
command === "capabilities"
? "List provider capabilities and side effects"
: "List configured analysis providers",
run: () => logCliCommand(logger, command, () => runSessionStatus(logger)),
});
}
};
const registerFunctionCommand = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
): void => {
cli.command("function", {
description: "Analyze one bounded function with evidence",
args: z.object({
path: z.string().describe("App or program path"),
address: z.string().describe("Procedure name or address"),
}),
options: z.object({
includeAssembly: z.boolean().default(false),
limit: z.number().int().min(1).max(500).default(100),
maxPseudocodeChars: z.number().int().min(1).max(100_000).default(20_000),
maxInstructions: z.number().int().min(1).max(5_000).default(500),
}),
alias: {
includeAssembly: "include-assembly",
maxPseudocodeChars: "max-pseudocode-chars",
maxInstructions: "max-instructions",
},
run: ({ args, options }) =>
logCliCommand(logger, "function", () =>
runDirectAnalysis(
args.path,
"analyze_function",
{
procedure: args.address,
include_assembly: options.includeAssembly,
limit: options.limit,
max_pseudocode_chars: options.maxPseudocodeChars,
max_instructions: options.maxInstructions,
},
logger,
),
),
});
};
const registerSearchCommand = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
): void => {
cli.command("search", {
description: "Search bounded analyzed strings or procedure names",
args: z.object({
path: z.string().describe("App or program path"),
pattern: z.string().min(1).describe("Literal text or regex pattern"),
}),
options: z.object({
kind: z.enum(["strings", "procedures"]).default("strings"),
mode: z.enum(["literal", "regex"]).default("literal"),
caseSensitive: z.boolean().default(false),
offset: z.number().int().min(0).default(0),
limit: z.number().int().min(1).max(100).default(100),
}),
alias: { caseSensitive: "case-sensitive" },
run: ({ args, options }) =>
logCliCommand(logger, "search", () =>
runDirectAnalysis(
args.path,
options.kind === "strings" ? "search_strings" : "search_procedures",
{
pattern: args.pattern,
mode: options.mode,
case_sensitive: options.caseSensitive,
offset: options.offset,
limit: options.limit,
},
logger,
),
),
});
};
const registerReferenceSourceCommand = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
): void => {
cli.command("import-reference-source", {
description: "Import a bounded source tree as historical reference only",
args: z.object({
root: z
.string()
.describe("Source root allowed by REA_REFERENCE_ROOTS_JSON"),
}),
run: ({ args }) =>
logCliCommand(logger, "import-reference-source", async () => {
const config = parseConfig(process.env);
if (!config.ok)
return { error: config.error._tag, message: config.error.message };
const imported = await importReferenceSource({
root: args.root,
caller: "rea-cli",
policy: config.value.referenceSourcePolicy,
importer: PRODUCT_IDENTITY.packageName,
importerVersion: null,
});
return imported.ok
? imported.value
: { error: imported.error.code, message: imported.error.message };
}),
});
};
const registerArtifactCommands = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
): void => {
cli.command("inventory-artifact", {
description: "Build a bounded deterministic artifact graph",
args: z.object({
path: z.string().describe("Application or package path"),
}),
options: z.object({
offset: z.number().int().min(0).default(0),
limit: z.number().int().min(1).max(500).default(100),
}),
run: ({ args, options }) =>
logCliCommand(logger, "inventory-artifact", () =>
runProviderAnalysis(
args.path,
"inventory_artifact",
{
node_offset: options.offset,
node_limit: options.limit,
occurrence_offset: options.offset,
occurrence_limit: options.limit,
edge_offset: options.offset,
edge_limit: options.limit,
},
logger,
),
),
});
cli.command("extract-artifact", {
description: "Extract explicitly selected artifact occurrences safely",
args: z.object({
path: z.string().describe("Application or package path"),
outputRoot: z.string().describe("Absent absolute output root"),
occurrenceIds: z
.array(z.string().regex(/^occ_[a-f0-9]{64}$/u))
.min(1)
.max(500),
}),
alias: { outputRoot: "output-root", occurrenceIds: "occurrence-ids" },
run: ({ args }) =>
logCliCommand(logger, "extract-artifact", () =>
runProviderAnalysis(
args.path,
"extract_artifact",
{
approved: true,
output_root: args.outputRoot,
occurrence_ids: args.occurrenceIds,
},
logger,
),
),
});
};
const registerNativeCommands = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
): void => {
for (const [command, tool] of [
["inspect-macho", "inspect_macho"],
["inspect-signature", "inspect_signature"],
["list-architectures", "list_architectures"],
] as const) {
cli.command(command, {
description: `Run ${tool} without launching Hopper`,
args: z.object({ path: z.string().describe("Mach-O or app path") }),
run: ({ args }) =>
logCliCommand(logger, command, () =>
runProviderAnalysis(args.path, tool, {}, logger),
),
});
}
cli.command("inspect-plist", {
description: "Parse app plist metadata without launching Hopper",
args: z.object({ path: z.string().describe("App or Mach-O path") }),
options: z.object({
relativePath: z.string().default("Contents/Info.plist"),
}),
alias: { relativePath: "relative-path" },
run: ({ args, options }) =>
logCliCommand(logger, "inspect-plist", () =>
runProviderAnalysis(
args.path,
"inspect_plist",
{ relative_path: options.relativePath },
logger,
),
),
});
cli.command("demangle-swift", {
description: "Demangle a bounded Swift symbol batch without Hopper",
args: z.object({
path: z.string().describe("Artifact path used for evidence identity"),
symbols: z.array(z.string().min(1)).min(1).max(500),
}),
run: ({ args }) =>
logCliCommand(logger, "demangle-swift", () =>
runProviderAnalysis(
args.path,
"demangle_swift",
{ symbols: args.symbols },
logger,
),
),
});
};