import { cancel, confirm, intro, isCancel, multiselect, outro, } from "@clack/prompts"; import type { SetupAction, SetupConfirmationDecision, SetupProgressEvent, SetupResult, } from "./application/Setup.js"; import { projectDoctorReport } from "./application/DoctorProjection.js"; import { SUPPORTED_CLIENT_DEFINITIONS } from "./application/SupportedClients.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; const agentIntegrationCapabilityId = "agent_integration"; interface SetupCapability { readonly id: string; readonly label: string; readonly hint: string; } /** Run the inline setup picker, exact preflight, and default-No consent. */ export const confirmInteractiveSetup = async ( actions: readonly SetupAction[], accessible: boolean, ): Promise => { intro("REA setup", promptStreams); renderValueIntroduction(); renderDetectedSummary(actions); renderKeyboardHelp(accessible); const selectedActionIds = await selectSetupActions(actions, accessible); if (selectedActionIds === undefined) return cancelledDecision(); if (selectedActionIds.length === 0) { cancel("Nothing selected. No changes were made.", promptStreams); return { approved: false, selectedActionIds }; } const selected = new Set(selectedActionIds); const selectedActions = actions.filter(({ id }) => selected.has(id)); renderPreflight(selectedActions); const approved = await confirm({ ...promptStreams, message: selectedActions.length === 1 ? "Apply this change?" : `Apply these ${String(selectedActions.length)} changes?`, initialValue: false, active: "Yes, apply", inactive: "No, cancel", }); if (isCancel(approved) || !approved) { cancel("Setup cancelled. No changes were made.", promptStreams); return { approved: false, selectedActionIds }; } return { approved: true, selectedActionIds }; }; /** 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}`); }; /** Finish an interactive setup journey without exposing the full doctor catalog. */ export const renderInteractiveSetupResult = (result: SetupResult): 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); outro( changedClients.length === 0 ? "REA is ready for local app analysis." : `Restart ${changedClients.join(", ")} to load REA.`, promptStreams, ); return; } if (result.status === "planned") return; writeLine(`! ${result.remediation ?? "Setup needs attention."}`); outro("Run `rea doctor` for the remaining checks.", promptStreams); }; /** Keep default non-TTY output actionable without emitting the full catalog. */ export const conciseSetupResult = (result: SetupResult) => ({ status: result.status, plannedActions: result.plannedActions, appliedActions: result.appliedActions, clients: result.clients, doctor: projectDoctorReport(result.doctor, "summary"), ...(result.code === undefined ? {} : { code: result.code }), ...(result.remediation === undefined ? {} : { remediation: result.remediation }), }); const selectSetupActions = async ( actions: readonly SetupAction[], accessible: boolean, ): Promise => { const clientActions = actions.filter( ({ kind }) => kind === "configure_client", ); const componentActions = actions.filter( ({ kind }) => kind !== "configure_client", ); const capabilities = setupCapabilities(clientActions, componentActions); const selectedCapabilities = accessible ? await selectCapabilitiesAccessibly(capabilities) : await selectCapabilities(capabilities); if (selectedCapabilities === undefined) return undefined; const selectedCapabilityIds = new Set(selectedCapabilities); const bundledSkillAction = componentActions.find( ({ kind }) => kind === "install_skill", ); const selectedActionIds = componentActions .filter( ({ id }) => id !== bundledSkillAction?.id && selectedCapabilityIds.has(id), ) .map(({ id }) => id); if (!selectedCapabilityIds.has(agentIntegrationCapabilityId)) return selectedActionIds; if (clientActions.length === 0) return [ ...(bundledSkillAction === undefined ? [] : [bundledSkillAction.id]), ...selectedActionIds, ]; const selectedClients = accessible ? await selectClientsAccessibly(clientActions) : await selectClients(clientActions); if (selectedClients === undefined) return undefined; if (selectedClients.length === 0) return selectedActionIds; return [ ...selectedClients, ...(bundledSkillAction === undefined ? [] : [bundledSkillAction.id]), ...selectedActionIds, ]; }; const setupCapabilities = ( clientActions: readonly SetupAction[], componentActions: readonly SetupAction[], ): readonly SetupCapability[] => [ ...(clientActions.length === 0 && !componentActions.some(({ kind }) => kind === "install_skill") ? [] : [ { id: agentIntegrationCapabilityId, label: "Agent integration (MCP + guided workflow)", hint: clientActions.length === 0 ? "MCP registrations are aligned; installs the matching bundled skill" : `${String(clientActions.length)} detected ${clientActions.length === 1 ? "agent" : "agents"}; installs the bundled skill when needed`, }, ]), ...componentActions .filter(({ kind }) => kind !== "install_skill") .map((action) => ({ id: action.id, label: `${action.label} (${actionModality(action)})`, hint: `${action.operation} · ${action.target}`, })), ]; const selectCapabilities = async ( capabilities: readonly SetupCapability[], ): Promise => { const selection = await multiselect({ ...promptStreams, message: "What should REA set up?", options: capabilities.map((capability) => ({ value: capability.id, label: capability.label, hint: capability.hint, })), required: false, maxItems: Math.max( 3, Math.min(capabilities.length, (process.stderr.rows ?? 24) - 8), ), showInstructions: true, }); return isCancel(selection) ? undefined : selection; }; const selectCapabilitiesAccessibly = async ( capabilities: readonly SetupCapability[], ): Promise => { const selected: string[] = []; for (const capability of capabilities) { const included = await confirm({ ...promptStreams, message: `Set up ${capability.label}? ${capability.hint}`, initialValue: false, vertical: true, }); if (isCancel(included)) return undefined; if (included) selected.push(capability.id); } return selected; }; const selectClients = async ( actions: readonly SetupAction[], ): Promise => { const selection = await multiselect({ ...promptStreams, message: "Which agents should use REA?", options: actions.map((action) => ({ value: action.id, label: `${action.label} (detected)`, hint: `${action.operation} · ${action.target}`, })), required: false, maxItems: Math.max( 3, Math.min(actions.length, (process.stderr.rows ?? 24) - 8), ), showInstructions: true, }); return isCancel(selection) ? undefined : selection; }; const selectClientsAccessibly = async ( actions: readonly SetupAction[], ): Promise => { const selected: string[] = []; for (const action of actions) { const included = await confirm({ ...promptStreams, message: `Configure ${action.label}? ${action.target}`, initialValue: false, vertical: true, }); if (isCancel(included)) return undefined; if (included) selected.push(action.id); } return selected; }; const renderValueIntroduction = (): void => { writeLine("│ Understand local apps and binaries from your coding agent."); writeLine("│"); writeLine("│ • Trace how a feature works with local evidence."); writeLine( "│ • Use the same analysis capabilities from REA's CLI and supported agents.", ); writeLine( "│ • Keep analyzed targets on this machine instead of uploading them to a hosted service.", ); }; const renderDetectedSummary = (actions: readonly SetupAction[]): void => { const clients = actions.filter(({ kind }) => kind === "configure_client"); writeLine("│"); if (clients.length === 0) { writeLine("◆ No agent integrations need configuration"); } else { writeLine( `◆ Found ${String(clients.length)} supported ${clients.length === 1 ? "agent" : "agents"}`, ); writeLine(`│ ${humanList(clients.map(({ label }) => label))}`); } }; const renderKeyboardHelp = (accessible: boolean): void => { writeLine("│"); writeLine( accessible ? "│ Keys: Enter answer · Ctrl-C cancel" : "│ Keys: ↑/↓ navigate · Space toggle · Enter confirm · Ctrl-C cancel", ); }; const renderReadyCapabilities = ( result: SetupResult, readyClients: readonly string[], ): 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"); if (providers.length > 0) writeLine("│ CLI: rea analyze /path/to/app"); 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[] => { const providers = [ ...(result.doctor.hopperPath === undefined ? [] : ["Hopper"]), ...(result.doctor.providerInspections ?? []) .filter(({ available }) => available) .map(({ id }) => displayProviderId(id)), ]; return [...new Set(providers)]; }; const displayProviderId = (id: string): string => id.length === 0 ? id : `${id[0]?.toUpperCase() ?? ""}${id.slice(1)}`; const actionModality = (action: SetupAction): string => { if (action.kind === "configure_client") return "MCP"; if (action.kind === "install_hopper") return "provider"; return "skill"; }; 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: [], }; }; const writeLine = (line: string): void => { process.stderr.write(`${line}\n`); };