diff --git a/README.md b/README.md index 6db4595f8..7a7081a47 100644 --- a/README.md +++ b/README.md @@ -85,26 +85,29 @@ REA shows how it reached its conclusions. It does not claim to recover original ## Quick start -### Install the CLI — recommended +### Run setup — recommended ```bash -npm install --global rea-agents -rea setup +npx rea-agents setup ``` -Installing the CLI does not update Homebrew, Node.js, npm, Hopper, or agent -configuration. `rea setup` opens with the work it enables: investigate local -apps from an agent, recover evidence through a deep-analysis provider, and -reuse REA's guided workflow. It summarizes the detected agents, then offers -**Set up all available capabilities (recommended)**, **Customize**, or -**No thanks**. Customize shows every concrete target with an `MCP`, `provider`, -or `skill` label. +The npm package-runner prompt, when shown, approves downloading REA for this +invocation; it does not approve any setup changes. The REA wizard separately +shows its complete plan and asks before applying it. Setup does not update +Homebrew, Node.js, or npm. `npx rea-agents setup` opens with the work it +enables: investigate local apps from an agent, recover evidence through a +deep-analysis provider, and reuse REA's guided workflow. It summarizes the +detected agents, then asks which independent capabilities to set up: coding-agent +access through MCP, the shared investigation skill, and—when needed—the Hopper +provider. Nothing is preselected. Choosing MCP access opens a second empty +checklist for the specific detected agents that should receive a registration. -REA keeps the journey inline so its history remains in the terminal. Before -anything changes, it validates existing configuration, prints exact paths and -external effects, and asks for final approval with **No** as the default. The -screen keeps the available keys visible while you choose; Ctrl-C and declining -leave the system unchanged. +REA keeps the journey inline so its history remains in the terminal. Selecting +a capability does not select every detected target or authorize a change. +Before anything changes, REA validates existing configuration, prints exact +paths and external effects, and asks for final approval with **No** as the +default. The screen keeps the available keys visible while you choose; Ctrl-C +and declining leave the system unchanged. REA detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, and Devin. It configures the first six when detected; Devin is reported but left unchanged because it has no documented local MCP configuration boundary. Registrations are additive, backup-first, and read back after writing. You can safely rerun setup. @@ -138,7 +141,7 @@ Review the setup plan, approve it if appropriate, then describe the app or featu ### From Terminal — no installation ```bash -npx -y rea-agents@latest setup +npx rea-agents setup npx -y rea-agents@latest doctor npx -y rea-agents@latest analyze /Applications/Notes.app ``` diff --git a/README_ar.md b/README_ar.md index 7da1b5f8e..d8de3ce0b 100644 --- a/README_ar.md +++ b/README_ar.md @@ -90,7 +90,7 @@ npx skills add morluto/rea ```bash curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash -npx -y rea-agents@latest setup +npx rea-agents setup ``` إذا طلب macOS أو برنامج التثبيت تأكيدًا، فأكمل الخطوة ثم شغّل الأمر نفسه مرة أخرى. diff --git a/README_ja.md b/README_ja.md index ef7ea6772..c1bf4ea24 100644 --- a/README_ja.md +++ b/README_ja.md @@ -91,7 +91,7 @@ Ubuntu 24.04+、Fedora 41+、64 ビット Arch Linux では、REA が公式の D ```bash # 1. REA をセットアップ curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash -npx -y rea-agents@latest setup +npx rea-agents setup ``` macOS やインストーラーから確認を求められた場合は、その操作を完了してから同じコマンドをもう一度実行してください。 diff --git a/README_ko.md b/README_ko.md index 94f0fe530..feaee7f13 100644 --- a/README_ko.md +++ b/README_ko.md @@ -91,7 +91,7 @@ Ubuntu 24.04+, Fedora 41+, 64비트 Arch Linux에서 REA는 공식 DEB, RPM 또 ```bash # 1. REA 설정 curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash -npx -y rea-agents@latest setup +npx rea-agents setup ``` macOS나 설치 프로그램이 확인을 요청하면 해당 절차를 완료하고 같은 명령을 다시 실행하세요. diff --git a/README_zh.md b/README_zh.md index f7dfe1544..8d9ed066e 100644 --- a/README_zh.md +++ b/README_zh.md @@ -91,7 +91,7 @@ npx skills add morluto/rea ```bash # 1. 安装和配置 REA curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash -npx -y rea-agents@latest setup +npx rea-agents setup ``` 如果 macOS 或安装程序要求确认,请完成提示,然后再次运行同一命令。 diff --git a/docs/installation.md b/docs/installation.md index c7e398d43..4d8822501 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -2,21 +2,26 @@ REA separates installing its CLI from configuring external software and agents. -## Install the CLI +## Start setup -The recommended installation is: +The recommended setup entrypoint is: ```bash -npm install --global rea-agents -rea setup +npx rea-agents setup ``` +If npm asks to download and run the package, that approval applies only to the +current package-runner invocation. REA still prints its own setup plan and asks +for separate approval before changing agent configuration or installing a +product-owned component. + REA supports Node.js 22.19+ and 24.11+ (including newer releases). It uses the npm already paired with that runtime and never upgrades Node.js, npm, or Homebrew. Running `npm install rea-agents` without `--global` installs the executable only in the current project's `node_modules/.bin`; it does not make `rea` available -on the shell `PATH`. Use `npx -y rea-agents@latest` for one-off commands or the -global installation above for a shell-visible `rea` command. +on the shell `PATH`. Use `npx rea-agents setup` for the guided setup journey, +`npx -y rea-agents@latest` for unattended one-off commands, or install globally +with `npm install --global rea-agents` for a shell-visible `rea` command. The optional curl wrapper installs only the global npm package: @@ -45,24 +50,23 @@ mechanics: - recover evidence through an available deep-analysis provider; - use the bundled skill for a repeatable investigation workflow. -REA then summarizes the detected clients and presents one gateway: +REA then summarizes the detected clients and asks which independent +capabilities to set up. The capability picker starts empty and labels each +choice by its role: `MCP` for coding-agent access, `provider` for Hopper, and +`skill` for the shared REA investigation workflow. Selecting MCP access opens a +second empty checklist for the exact detected agents that should receive a +registration. Detection provides context; it does not preselect or authorize a +configuration write. -- **Set up all available capabilities (recommended)** selects the complete - resolved plan; -- **Customize** opens the concrete action picker; -- **No thanks** exits without making changes. +Choosing no capabilities exits without changes. Selecting the skill or provider +does not implicitly select an agent, and selecting agent access does not include +every detected agent. The picker keeps its navigation, selection, confirmation, +and cancellation keys visible instead of relying on a transient hint. -The custom picker labels each target by its role: `MCP` for an agent -registration, `provider` for Hopper or a validated bring-your-own provider, and -`skill` for the REA investigation skill. These labels describe different setup -modalities rather than suggesting that every target is an MCP registration. -The picker keeps its navigation, selection, confirmation, and cancellation keys -visible instead of relying on a transient hint. - -Both recommended and custom paths converge on the same exact preflight. REA -validates the current state, prints the proposed effects, and asks for final -approval with **No** as the default. Selection alone never authorizes a -mutation. The plan identifies: +Every selected path converges on the same exact preflight. REA validates the +current state, prints the proposed effects, and asks for final approval with +**No** as the default. Selection alone never authorizes a mutation. The plan +identifies: - an existing Hopper installation, a validated bring-your-own Ghidra environment, or the official Hopper package it proposes to install; - each detected agent configuration path; diff --git a/package-lock.json b/package-lock.json index 2c4a8e99a..e1193d251 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,8 @@ "zod": "4.4.3" }, "bin": { - "rea": "scripts/rea.mjs" + "rea": "scripts/rea.mjs", + "rea-agents": "scripts/rea.mjs" }, "devDependencies": { "@fast-check/vitest": "0.4.1", diff --git a/package.json b/package.json index 63c06de04..61da67acd 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ }, "type": "module", "bin": { - "rea": "scripts/rea.mjs" + "rea": "scripts/rea.mjs", + "rea-agents": "scripts/rea.mjs" }, "engines": { "node": "^22.19.0 || >=24.11.0" diff --git a/scripts/verify-package.mjs b/scripts/verify-package.mjs index adfb70514..584cb8831 100644 --- a/scripts/verify-package.mjs +++ b/scripts/verify-package.mjs @@ -14,6 +14,7 @@ import { join } from "node:path"; import { pathToFileURL } from "node:url"; import { promisify } from "node:util"; +import { spawn } from "@lydell/node-pty"; import { Client } from "@modelcontextprotocol/client"; import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js"; @@ -48,6 +49,11 @@ try { packedManifest.dependencies?.["@lydell/node-pty"] !== "1.1.0" ) throw new Error("package retained a lifecycle-dependent PTY installation"); + if ( + packedManifest.bin?.rea !== "scripts/rea.mjs" || + packedManifest.bin?.["rea-agents"] !== "scripts/rea.mjs" + ) + throw new Error("package did not expose both rea command entry points"); if ( packedFiles.some( (path) => path.includes("__pycache__") || path.endsWith(".pyc"), @@ -143,6 +149,7 @@ try { { env: environment }, ); const cli = join(prefix, "bin", "rea"); + const packageRunnerCli = join(prefix, "bin", "rea-agents"); const processCaptureCapabilityUrl = pathToFileURL( join( prefix, @@ -216,6 +223,12 @@ try { `packaged CLI did not degrade without the optional PTY binary: ${JSON.stringify(noOptionalCapability)}`, ); const help = await run(cli, ["--help"], environment); + const packageRunnerHelp = await run( + packageRunnerCli, + ["--help"], + environment, + ); + await verifyInteractiveSetup(packageRunnerCli, environment); const llms = await run(cli, ["--llms"], environment); const doctorExecution = await runWithStatus( cli, @@ -228,6 +241,7 @@ try { const expectedDoctorHealth = doctor.checks?.every(({ ok }) => ok) === true; if ( !help.includes("setup") || + !packageRunnerHelp.includes("setup") || !help.includes("upgrade") || !help.includes("inventory-artifact") || !help.includes("extract-artifact") || @@ -1157,6 +1171,53 @@ async function runWithStatus(command, args, env) { throw cause; } } +async function verifyInteractiveSetup(command, env) { + await new Promise((resolvePromise, reject) => { + let output = ""; + let cancelled = false; + let settled = false; + const terminal = spawn(command, ["setup"], { + cwd: root, + env: { ...env, NO_COLOR: "1", TERM: "xterm-256color" }, + name: "xterm-256color", + cols: 120, + rows: 40, + }); + const timeout = setTimeout(() => { + terminal.kill(); + finish( + new Error(`packaged setup wizard timed out after output: ${output}`), + ); + }, 20_000); + + terminal.onData((data) => { + output += data; + if (!cancelled && output.includes("What should REA set up?")) { + cancelled = true; + terminal.write("\u0003"); + } + }); + terminal.onExit(() => { + if (!cancelled || !output.includes("No changes were made.")) { + finish( + new Error( + `packaged rea-agents setup did not open and cancel the wizard: ${output}`, + ), + ); + return; + } + finish(); + }); + + function finish(error) { + if (settled) return; + settled = true; + clearTimeout(timeout); + if (error === undefined) resolvePromise(); + else reject(error); + } + }); +} function json(text) { return JSON.parse(text); } diff --git a/src/cliSetup.ts b/src/cliSetup.ts index 94ebe6b3b..0be6b5631 100644 --- a/src/cliSetup.ts +++ b/src/cliSetup.ts @@ -5,7 +5,6 @@ import { isCancel, multiselect, outro, - select, } from "@clack/prompts"; import type { @@ -29,7 +28,13 @@ const promptStreams = { withGuide: true, } as const; -type SetupMode = "recommended" | "custom" | "skip"; +const agentAccessCapabilityId = "agent_access"; + +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 ( @@ -40,22 +45,8 @@ export const confirmInteractiveSetup = async ( renderValueIntroduction(); renderDetectedSummary(actions); renderKeyboardHelp(accessible); - const mode = await selectSetupMode(actions, accessible); - if (mode === undefined) return cancelledDecision(actions); - if (mode === "skip") { - cancel("No changes were made.", promptStreams); - return { - approved: false, - selectedActionIds: actions.map(({ id }) => id), - }; - } - const selectedActionIds = - mode === "recommended" - ? actions.map(({ id }) => id) - : accessible - ? await selectActionsAccessibly(actions) - : await selectActions(actions); - if (selectedActionIds === undefined) return cancelledDecision(actions); + 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 }; @@ -131,18 +122,104 @@ export const conciseSetupResult = (result: SetupResult) => ({ : { remediation: result.remediation }), }); -const selectActions = async ( +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 selectedActionIds = componentActions + .filter(({ id }) => selectedCapabilityIds.has(id)) + .map(({ id }) => id); + if (!selectedCapabilityIds.has(agentAccessCapabilityId)) + return selectedActionIds; + const selectedClients = accessible + ? await selectClientsAccessibly(clientActions) + : await selectClients(clientActions); + return selectedClients === undefined + ? undefined + : [...selectedClients, ...selectedActionIds]; +}; + +const setupCapabilities = ( + clientActions: readonly SetupAction[], + componentActions: readonly SetupAction[], +): readonly SetupCapability[] => [ + ...(clientActions.length === 0 + ? [] + : [ + { + id: agentAccessCapabilityId, + label: "Coding-agent access (MCP)", + hint: `${String(clientActions.length)} detected ${clientActions.length === 1 ? "agent" : "agents"}`, + }, + ]), + ...componentActions.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: "Choose what REA should configure", + message: "Which agents should use REA?", options: actions.map((action) => ({ value: action.id, - label: `${action.label} (${actionModality(action)})`, + label: `${action.label} (detected)`, hint: `${action.operation} · ${action.target}`, })), - initialValues: actions.map(({ id }) => id), required: false, maxItems: Math.max( 3, @@ -153,15 +230,15 @@ const selectActions = async ( return isCancel(selection) ? undefined : selection; }; -const selectActionsAccessibly = async ( +const selectClientsAccessibly = async ( actions: readonly SetupAction[], ): Promise => { const selected: string[] = []; for (const action of actions) { const included = await confirm({ ...promptStreams, - message: `Include ${action.label} (${actionModality(action)})? ${action.target}`, - initialValue: true, + message: `Configure ${action.label}? ${action.target}`, + initialValue: false, vertical: true, }); if (isCancel(included)) return undefined; @@ -170,55 +247,6 @@ const selectActionsAccessibly = async ( return selected; }; -const selectSetupMode = async ( - actions: readonly SetupAction[], - accessible: boolean, -): Promise => { - if (accessible) return selectSetupModeAccessibly(); - const mode = await select({ - ...promptStreams, - message: "Choose a setup", - options: [ - { - value: "recommended", - label: "Set up all available capabilities", - hint: `recommended · ${actionCount(actions.length)}`, - }, - { - value: "custom", - label: "Customize setup", - hint: "choose integrations and components", - }, - { - value: "skip", - label: "No thanks", - hint: "make no changes", - }, - ], - initialValue: "recommended", - }); - return isCancel(mode) ? undefined : mode; -}; - -const selectSetupModeAccessibly = async (): Promise => { - const recommended = await confirm({ - ...promptStreams, - message: "Use the recommended complete setup?", - initialValue: true, - vertical: true, - }); - if (isCancel(recommended)) return undefined; - if (recommended) return "recommended"; - const customize = await confirm({ - ...promptStreams, - message: "Customize integrations and components?", - initialValue: true, - vertical: true, - }); - if (isCancel(customize)) return undefined; - return customize ? "custom" : "skip"; -}; - const renderValueIntroduction = (): void => { writeLine("│ Understand local apps and binaries from your coding agent."); writeLine("│"); @@ -237,20 +265,10 @@ const renderDetectedSummary = (actions: readonly SetupAction[]): void => { if (clients.length === 0) { writeLine("◆ No agent integrations need configuration"); } else { - writeLine(`◆ Detected ${humanList(clients.map(({ label }) => label))}`); - for (const client of clients) { - writeLine( - `│ ${client.label} · ${actionModality(client)} · ${client.operation}`, - ); - writeLine(`│ ${client.target}`); - } - } - const components = actions.filter(({ kind }) => kind !== "configure_client"); - if (components.length > 0) { - writeLine("│"); writeLine( - `│ Also available: ${components.map((action) => `${action.label} (${actionModality(action)})`).join(", ")}`, + `◆ Found ${String(clients.length)} supported ${clients.length === 1 ? "agent" : "agents"}`, ); + writeLine(`│ ${humanList(clients.map(({ label }) => label))}`); } }; @@ -312,9 +330,6 @@ const actionModality = (action: SetupAction): string => { return "skill"; }; -const actionCount = (count: number): string => - `${String(count)} ${count === 1 ? "change" : "changes"}`; - const humanList = (values: readonly string[]): string => { if (values.length < 2) return values[0] ?? ""; if (values.length === 2) return `${values[0]} and ${values[1]}`; @@ -345,13 +360,11 @@ const renderPreflight = (actions: readonly SetupAction[]): void => { ); }; -const cancelledDecision = ( - actions: readonly SetupAction[], -): SetupConfirmationDecision => { +const cancelledDecision = (): SetupConfirmationDecision => { cancel("Setup cancelled. No changes were made.", promptStreams); return { approved: false, - selectedActionIds: actions.map(({ id }) => id), + selectedActionIds: [], }; }; diff --git a/tests/cliSetup.test.ts b/tests/cliSetup.test.ts index d3f61b73a..1dc12a450 100644 --- a/tests/cliSetup.test.ts +++ b/tests/cliSetup.test.ts @@ -41,8 +41,10 @@ const actions = [ ] as const; describe("interactive setup journey", () => { - it("leads with value, detected targets, modality labels, and durable key help", async () => { - const result = await runJourney([step("Choose a setup", "\u0003")]); + it("leads with value, a compact detected-target summary, and durable key help", async () => { + const result = await runJourney([ + step("What should REA set up?", "\u0003"), + ]); expect(result.output).toContain( "Understand local apps and binaries from your coding agent.", @@ -50,8 +52,8 @@ describe("interactive setup journey", () => { expect(result.output).toContain( "Trace how a feature works with local evidence.", ); - expect(result.output).toContain("Detected Codex"); - expect(result.output).toContain("Codex · MCP · create"); + expect(result.output).toContain("Found 1 supported agent"); + expect(result.output).toContain("Codex"); expect(result.output).toContain("Hopper deep-analysis provider (provider)"); expect(result.output).toContain("REA reverse-engineering skill (skill)"); expect(result.output).toContain( @@ -60,49 +62,122 @@ describe("interactive setup journey", () => { expect(result.decision.approved).toBe(false); }); - it("offers recommended, custom, and no-thanks intent before choosing targets", async () => { - const result = await runJourney([ - step("Choose a setup", "\u001b[B\u001b[B\r"), - ]); + it("does not turn available capabilities into selected setup actions", async () => { + const result = await runJourney([step("What should REA set up?", "\r")]); - expect(result.output).toContain("Set up all available capabilities"); - expect(result.output).toContain("recommended · 3 changes"); - expect(result.output).toContain("Customize setup"); - expect(result.output).toContain("No thanks"); + expect(result.output).toContain("Coding-agent access (MCP)"); + expect(result.output).toContain("REA reverse-engineering skill (skill)"); + expect(result.output).toContain("Hopper deep-analysis provider (provider)"); expect(result.output).not.toContain("Ready to review"); - expect(result.decision).toMatchObject({ approved: false }); + expect(result.output).toContain("Nothing selected. No changes were made."); + expect(result.decision).toEqual({ + approved: false, + selectedActionIds: [], + }); }); - it("lets custom mode reach the modality-labelled target picker and cancel safely", async () => { + it("omits MCP access when no agent integration needs configuration", async () => { + const result = await runJourney( + [step("What should REA set up?", "\r")], + false, + actions.filter(({ kind }) => kind !== "configure_client"), + ); + + expect(result.output).toContain("No agent integrations need configuration"); + expect(result.output).not.toContain("Coding-agent access (MCP)"); + expect(result.decision.selectedActionIds).toEqual([]); + }); + + it("shows multiple detected agents without preselecting either one", async () => { + const result = await runJourney( + [ + step("What should REA set up?", " \r"), + step("Which agents should use REA?", "\u0003"), + ], + false, + [ + actions[0], + { + ...actions[0], + id: "configure_client:cursor", + label: "Cursor", + target: "/isolated/.cursor/mcp.json", + }, + ...actions.slice(1), + ], + ); + + expect(result.output).toContain("Codex (detected)"); + expect(result.output).toContain("Cursor (detected)"); + expect(result.decision.approved).toBe(false); + }); + + it("asks for agent targets only after MCP access is selected", async () => { const result = await runJourney([ - step("Choose a setup", "\u001b[B\r"), - step("Choose what REA should configure", "\u0003"), + step("What should REA set up?", " \r"), + step("Which agents should use REA?", "\u0003"), ]); - expect(result.output).toContain("Choose what REA should configure"); - expect(result.output).toContain("Codex (MCP)"); - expect(result.output).toContain("Hopper deep-analysis provider (provider)"); + expect(result.output).toContain("Codex (detected)"); expect(result.output).toContain("Setup cancelled. No changes were made."); expect(result.decision.approved).toBe(false); }); - it("keeps the recommended path subject to a default-No exact preflight", async () => { + it("reviews only explicitly selected actions with default-No consent", async () => { const result = await runJourney([ - step("Choose a setup", "\r"), - step("Apply these 3 changes?", "\r"), + step("What should REA set up?", " \r"), + step("Which agents should use REA?", " \r"), + step("Apply this change?", "\r"), ]); expect(result.output).toContain("Ready to review"); expect(result.output).toContain("CREATE Codex"); expect(result.output).toContain("/isolated/.codex/config.toml"); + expect(result.output).not.toContain( + "INSTALL REA reverse-engineering skill", + ); + expect(result.output).not.toContain( + "INSTALL Hopper deep-analysis provider", + ); expect(result.output).toContain("No, cancel"); expect(result.output).toContain("Setup cancelled. No changes were made."); expect(result.decision).toEqual({ approved: false, - selectedActionIds: actions.map(({ id }) => id), + selectedActionIds: ["configure_client:codex"], }); }); + it("can select the shared skill without selecting an agent integration", async () => { + const result = await runJourney([ + step("What should REA set up?", "\u001b[B \r"), + step("Apply this change?", "\r"), + ]); + + expect(result.output).toContain("INSTALL REA reverse-engineering skill"); + expect(result.output).not.toContain("CREATE Codex"); + expect(result.output).not.toContain( + "INSTALL Hopper deep-analysis provider", + ); + expect(result.decision).toEqual({ + approved: false, + selectedActionIds: ["install_skill"], + }); + }); + + it("keeps every accessible capability prompt defaulted to skip", async () => { + const result = await runJourney( + [ + step("Set up Coding-agent access (MCP)?", "\r"), + step("Set up REA reverse-engineering skill (skill)?", "\r"), + step("Set up Hopper deep-analysis provider (provider)?", "\r"), + ], + true, + ); + + expect(result.output).not.toContain("Ready to review"); + expect(result.decision.selectedActionIds).toEqual([]); + }); + it("frames completion around the capabilities that are now ready", async () => { const { stderr } = await execute( process.execPath, @@ -163,12 +238,14 @@ const step = (prompt: string, input: string): JourneyStep => ({ const runJourney = async ( steps: readonly JourneyStep[], + accessible = false, + journeyActions: readonly object[] = actions, ): Promise => { const isolatedHome = await mkdtemp(join(tmpdir(), "rea-cli-setup-test-")); const script = [ 'import { confirmInteractiveSetup } from "./dist/cliSetup.js";', - `const actions = ${JSON.stringify(actions)};`, - "const decision = await confirmInteractiveSetup(actions, false);", + `const actions = ${JSON.stringify(journeyActions)};`, + `const decision = await confirmInteractiveSetup(actions, ${JSON.stringify(accessible)});`, `process.stdout.write(${JSON.stringify(`\n${decisionMarker}`)} + JSON.stringify(decision) + "\\n");`, ].join("\n"); diff --git a/tests/identity.test.ts b/tests/identity.test.ts index 22642a26b..9d815dbec 100644 --- a/tests/identity.test.ts +++ b/tests/identity.test.ts @@ -27,7 +27,10 @@ describe("package identity", () => { ), ); expect(packageJson.name).toBe(PRODUCT_IDENTITY.packageName); - expect(Object.keys(packageJson.bin)).toEqual([PRODUCT_IDENTITY.cliBinary]); + expect(packageJson.bin).toEqual({ + [PRODUCT_IDENTITY.cliBinary]: "scripts/rea.mjs", + [PRODUCT_IDENTITY.packageName]: "scripts/rea.mjs", + }); expect(PRODUCT_IDENTITY.packageSpecifier).toBe( `${packageJson.name}@latest`, ); diff --git a/tests/readmeConsistency.test.ts b/tests/readmeConsistency.test.ts index b83aaf66c..6f31a3fe8 100644 --- a/tests/readmeConsistency.test.ts +++ b/tests/readmeConsistency.test.ts @@ -61,7 +61,7 @@ describe("localized README product facts", () => { expect(content).toContain( "curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash", ); - expect(content).toContain("npx -y rea-agents@latest setup"); + expect(content).toContain("npx rea-agents setup"); expect(content).toContain("npx -y rea-agents@latest doctor"); expect(content).toContain("rea uninstall"); expect(content).toContain('"args": ["-y", "rea-agents@latest", "mcp"]');