import { cancel, confirm, intro, isCancel, multiselect, outro, } from "@clack/prompts"; import type { SetupAction, SetupConfirmationDecision, SetupConfirmationContext, SetupClientState, SetupProgressEvent, SetupResult, } from "../application/SetupTypes.js"; import { SUPPORTED_CLIENT_DEFINITIONS } from "../application/SupportedClients.js"; import { invokingPackageRunner } from "../application/ClientRegistrationIdentity.js"; const clientDisplayNames: ReadonlyMap = new Map( SUPPORTED_CLIENT_DEFINITIONS.map(({ name, displayName }) => [ name, displayName, ]), ); const promptStreams = { input: process.stdin, output: process.stderr, withGuide: true, } as const; /** Select agent targets, then review the resolved plan before consent. */ export const confirmInteractiveSetup = async ( actions: readonly SetupAction[], accessible: boolean, context?: SetupConfirmationContext, ): Promise => { if (context?.stage === "select" || context === undefined) { intro("REA setup", promptStreams); writeLine( "│ Understand local apps and binaries from your terminal or agent.", ); renderDetectedSummary(context?.clientStates ?? []); renderKeyboardHelp(accessible); } if (context?.stage === "select") { const selected = await selectSetupActions(actions, accessible, context); return selected === undefined ? cancelledDecision() : { approved: false, selectedActionIds: selected }; } if (actions.length === 0) return { approved: true, selectedActionIds: [] }; renderPreflight(actions); const approved = await confirm({ ...promptStreams, message: actions.length === 1 ? "Apply this change?" : `Apply these ${String(actions.length)} changes?`, initialValue: true, active: "Yes, apply", inactive: "No, cancel", vertical: accessible, }); if (isCancel(approved) || !approved) return cancelledDecision(); return { approved: true, selectedActionIds: actions.map(({ id }) => id) }; }; /** Render stable, append-only progress for real setup operations. */ export const renderSetupProgress = (event: SetupProgressEvent): void => { const symbol = event.state === "started" ? "◇" : event.state === "completed" ? "◆" : event.state === "warning" ? "!" : "✗"; const suffix = event.detail === undefined ? "" : ` · ${event.detail}`; writeLine(`${symbol} ${event.label}${suffix}`); }; /** Show the resolved dry-run plan to a human at an interactive terminal. */ export const renderSetupDryRun = (actions: readonly SetupAction[]): void => { intro("REA setup plan", promptStreams); renderPreflight(actions); outro("Dry run complete. No changes were made.", promptStreams); }; /** Finish an interactive setup journey without exposing the full doctor catalog. */ export const renderInteractiveSetupResult = ( result: SetupResult, environment: Readonly = process.env, ): void => { if (result.status === "ready") { const changedClients = Object.entries(result.clients) .filter(([, client]) => client.status === "configured") .map(([client]) => clientDisplayNames.get(client) ?? client); const readyClients = readyAgentClients(result, changedClients); renderReadyCapabilities(result, readyClients, environment); outro( changedClients.length === 0 ? "REA is ready for local app analysis." : `Restart ${changedClients.join(", ")} to load REA.`, promptStreams, ); return; } if (result.status === "planned" || result.status === "cancelled") return; writeLine(`! ${result.remediation ?? "Setup needs attention."}`); outro( `Run \`${cliInvocation(environment)} doctor\` for the remaining checks.`, promptStreams, ); }; const selectSetupActions = async ( actions: readonly SetupAction[], accessible: boolean, context: SetupConfirmationContext, ): Promise => { const clientStates = context.clientStates.filter( ({ client }) => client.format !== "unsupported", ); const initial = new Set(context.selectedClientIds); const selectedClientIds = accessible ? await selectClientsAccessibly(clientStates, initial) : await selectClients(clientStates, initial); if (selectedClientIds === undefined) return undefined; const selectedActionIds = selectedClientIds.map( (id) => `configure_client:${id}`, ); const skill = actions.find(({ kind }) => kind === "install_skill"); if (skill !== undefined) { if (selectedClientIds.length > 0) selectedActionIds.push(skill.id); else { const included = await confirm({ ...promptStreams, message: "Install the guided REA skill for CLI use?", initialValue: false, vertical: accessible, }); if (isCancel(included)) return undefined; if (included) selectedActionIds.push(skill.id); } } const hopper = actions.find(({ kind }) => kind === "install_hopper"); if (hopper !== undefined) { const included = await confirm({ ...promptStreams, message: "Install Hopper for deep binary analysis?", initialValue: false, vertical: accessible, }); if (isCancel(included)) return undefined; if (included) selectedActionIds.push(hopper.id); } return selectedActionIds; }; const clientHint = (state: SetupClientState): string => state.configured ? "REA already configured · selected from existing configuration" : state.status === "invalid" ? "Existing configuration needs repair before setup" : state.detected ? "Detected · choose to configure" : "Not detected · configuration is still available"; const selectClients = async ( states: readonly SetupClientState[], initial: ReadonlySet, ): Promise => { if (states.length === 0) return []; const selection = await multiselect({ ...promptStreams, message: "Which agents should use REA?", options: states.map((state) => ({ value: state.client.name, label: clientDisplayNames.get(state.client.name) ?? state.client.name, hint: clientHint(state), })), initialValues: states .filter(({ client }) => initial.has(client.name)) .map(({ client }) => client.name), required: false, maxItems: Math.max( 3, Math.min(states.length, (process.stderr.rows ?? 24) - 8), ), showInstructions: true, }); return isCancel(selection) ? undefined : selection; }; const selectClientsAccessibly = async ( states: readonly SetupClientState[], initial: ReadonlySet, ): Promise => { const selected: string[] = []; for (const state of states) { const included = await confirm({ ...promptStreams, message: `Configure ${clientDisplayNames.get(state.client.name) ?? state.client.name}? ${clientHint(state)}`, initialValue: initial.has(state.client.name), vertical: true, }); if (isCancel(included)) return undefined; if (included) selected.push(state.client.name); } return selected; }; const renderDetectedSummary = (states: readonly SetupClientState[]): void => { const detected = states.filter(({ detected }) => detected); writeLine("│"); writeLine( detected.length === 0 ? "◆ No agents detected. Choose any supported agent below." : `◆ Detected: ${humanList(detected.map(({ client }) => clientDisplayNames.get(client.name) ?? client.name))}`, ); }; const renderKeyboardHelp = (accessible: boolean): void => { writeLine("│"); writeLine( accessible ? "│ Keys: Enter answer · Ctrl-C cancel" : "│ Keys: ↑/↓ navigate · Space toggle · Enter confirm · Ctrl-C cancel", ); }; const cliInvocation = (environment: Readonly): string => { const runner = invokingPackageRunner(environment); return runner === undefined ? "rea" : `${runner} rea-agents`; }; const renderReadyCapabilities = ( result: SetupResult, readyClients: readonly string[], environment: Readonly, ): void => { writeLine("◆ What you can do now"); const providers = readyProviders(result); if (providers.length > 0) writeLine(`│ Deep analysis: ${humanList(providers)}`); if (readyClients.length > 0) writeLine(`│ Agent access: ${humanList(readyClients)}`); if (result.doctor.identity?.skill.state === "aligned") writeLine("│ Guided reverse-engineering workflows: installed"); writeLine( `│ CLI: ${cliInvocation(environment)} ${providers.length > 0 ? "analyze /path/to/app" : "capabilities"}`, ); const firstClient = readyClients[0]; if (firstClient !== undefined) writeLine( `│ Try in ${firstClient}: "Explain how a feature works in /path/to/app and show the evidence."`, ); }; const readyAgentClients = ( result: SetupResult, changedClients: readonly string[], ): readonly string[] => { const alignedClients = (result.doctor.identity?.registrations ?? []) .filter(({ state }) => state === "aligned") .map(({ client }) => clientDisplayNames.get(client) ?? client); return [...new Set([...alignedClients, ...changedClients])]; }; const readyProviders = (result: SetupResult): readonly string[] => result.doctor.availableProviders.map(displayProviderId); const displayProviderId = (id: string): string => id.length === 0 ? id : `${id[0]?.toUpperCase() ?? ""}${id.slice(1)}`; const humanList = (values: readonly string[]): string => { if (values.length < 2) return values[0] ?? ""; if (values.length === 2) return `${values[0]} and ${values[1]}`; return `${values.slice(0, -1).join(", ")}, and ${values.at(-1)}`; }; const renderPreflight = (actions: readonly SetupAction[]): void => { writeLine("│"); writeLine("◆ Ready to review"); for (const action of actions) { writeLine("│"); writeLine(`│ ${action.operation.toUpperCase()} ${action.label}`); writeLine(`│ ${action.target}`); if (action.backupPath !== undefined) writeLine(`│ backup: ${action.backupPath}`); for (const origin of action.networkOrigins ?? []) writeLine(`│ network: ${origin}`); if (action.integrity !== undefined) writeLine(`│ integrity: ${action.integrity}`); for (const command of action.commands ?? []) writeLine(`│ command: ${command}`); writeLine(`│ ${action.detail}`); if (action.external) writeLine("│ external software"); } writeLine("│"); writeLine( "│ REA will preserve unrelated configuration and will not install or upgrade Node.js, npm, Homebrew, Java, or Ghidra.", ); }; const cancelledDecision = (): SetupConfirmationDecision => { cancel("Setup cancelled. No changes were made.", promptStreams); return { approved: false, selectedActionIds: [], cancelled: true, }; }; const writeLine = (line: string): void => { process.stderr.write(`${line}\n`); };