diff --git a/src/cli/interactiveSetup.ts b/src/cli/interactiveSetup.ts index d06b9da163..bc7953459b 100644 --- a/src/cli/interactiveSetup.ts +++ b/src/cli/interactiveSetup.ts @@ -94,9 +94,17 @@ export const renderSetupProgress = (event: SetupProgressEvent): void => { }; /** Show the resolved dry-run plan to a human at an interactive terminal. */ -export const renderSetupDryRun = (actions: readonly SetupAction[]): void => { +export const renderSetupDryRun = ( + actions: readonly SetupAction[], + remediation?: string, +): void => { intro("REA setup plan", promptStreams); renderPreflight(actions); + // A known manual step belongs to the plan, as it does at approval. + if (remediation !== undefined) { + writeLine("│"); + writeLine(`│ ${remediation}`); + } outro("Dry run complete. No changes were made.", promptStreams); }; diff --git a/src/cli/setupCommands.ts b/src/cli/setupCommands.ts index 7aef3d4007..0f9a5c9a7c 100644 --- a/src/cli/setupCommands.ts +++ b/src/cli/setupCommands.ts @@ -282,7 +282,8 @@ const runSetupCommand = async ( !input.formatExplicit && hasInteractiveTerminal() ) { - if (result.status === "planned") renderSetupDryRun(result.plannedActions); + if (result.status === "planned") + renderSetupDryRun(result.plannedActions, result.remediation); else renderInteractiveSetupResult(result, environment); } return result; diff --git a/tests/boundary/cli/cliSetup.test.ts b/tests/boundary/cli/cliSetup.test.ts index 10e693cf9a..4eec82b174 100644 --- a/tests/boundary/cli/cliSetup.test.ts +++ b/tests/boundary/cli/cliSetup.test.ts @@ -277,38 +277,47 @@ const runSetupInTty = async ( return { exitCode, output }; }; +const manualConnectorFixture = async (): Promise<{ + readonly home: string; + readonly environment: NodeJS.ProcessEnv; +}> => { + const home = await createTestTempDirectory("rea-cli-setup-manual-"); + const bin = join(home, "bin"); + await mkdir(bin); + const hopper = join(bin, "hopper"); + await writeFile(hopper, "#!/bin/sh\necho 'Hopper Disassembler v6.0.0'\n", { + mode: 0o755, + }); + // Linux's executable probe runs ldd before accepting this inert launcher. + await writeFile(join(bin, "ldd"), "#!/bin/sh\nexit 0\n", { mode: 0o755 }); + for (const runner of ["npx", "npm", "bun"]) + await writeFile(join(bin, runner), "#!/bin/sh\nexit 99\n", { + mode: 0o755, + }); + return { + home, + environment: { + PATH: `${bin}:${process.env.PATH ?? ""}`, + HOPPER_LAUNCHER_PATH: hopper, + DSH_HOME: join(home, ".dsh"), + DSH_AGENTS_HOME: join(home, ".dsh-agents"), + XDG_DATA_HOME: join(home, ".local/share"), + XDG_CACHE_HOME: join(home, ".cache"), + XDG_STATE_HOME: join(home, ".local/state"), + }, + }; +}; + describe("manual connector approval", () => { it.skipIf(process.platform === "win32").each([true, false])( "discloses the manual step before approval (apply: %s)", async (approved) => { - const home = await createTestTempDirectory("rea-cli-setup-manual-"); - const bin = join(home, "bin"); - await mkdir(bin); - const hopper = join(bin, "hopper"); - await writeFile( - hopper, - "#!/bin/sh\necho 'Hopper Disassembler v6.0.0'\n", - { mode: 0o755 }, - ); - // Linux's executable probe runs ldd before accepting this inert launcher. - await writeFile(join(bin, "ldd"), "#!/bin/sh\nexit 0\n", { mode: 0o755 }); - for (const runner of ["npx", "npm", "bun"]) - await writeFile(join(bin, runner), "#!/bin/sh\nexit 99\n", { - mode: 0o755, - }); + const { home, environment } = await manualConnectorFixture(); const { output, exitCode } = await runSetupInTty( home, ["--client", "grok_bot"], approved, - { - PATH: `${bin}:${process.env.PATH ?? ""}`, - HOPPER_LAUNCHER_PATH: hopper, - DSH_HOME: join(home, ".dsh"), - DSH_AGENTS_HOME: join(home, ".dsh-agents"), - XDG_DATA_HOME: join(home, ".local/share"), - XDG_CACHE_HOME: join(home, ".cache"), - XDG_STATE_HOME: join(home, ".local/state"), - }, + environment, ); const prompt = output.indexOf("Apply this change?"); const remediation = manualRegistrationRemediation("grok_bot"); @@ -328,6 +337,24 @@ describe("manual connector approval", () => { } }, ); + + it.skipIf(process.platform === "win32")( + "shows the manual step in a TTY dry run", + async () => { + const { home, environment } = await manualConnectorFixture(); + const { output, exitCode } = await runSetupInTty( + home, + ["--dry-run", "--client", "grok_bot"], + undefined, + environment, + ); + const remediation = manualRegistrationRemediation("grok_bot"); + expect(remediation).toBeDefined(); + expect(exitCode, output).toBe(0); + expect(output).toContain(remediation); + expect(output).toContain("Dry run complete. No changes were made."); + }, + ); }); describe("TTY setup dry run", () => {