From c02dc73d3c1372d03da72e28dfeb0d26dcc2773a Mon Sep 17 00:00:00 2001 From: dotta Date: Mon, 23 Mar 2026 13:47:22 -0500 Subject: [PATCH] Confirm company imports after preview Co-Authored-By: Paperclip --- cli/src/__tests__/company.test.ts | 43 +++++++++++++++++++++++++ cli/src/commands/client/company.ts | 51 +++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/cli/src/__tests__/company.test.ts b/cli/src/__tests__/company.test.ts index 75174dfa2..bcc3137c3 100644 --- a/cli/src/__tests__/company.test.ts +++ b/cli/src/__tests__/company.test.ts @@ -7,6 +7,7 @@ import { buildSelectedFilesFromImportSelection, renderCompanyImportPreview, renderCompanyImportResult, + resolveCompanyImportApplyConfirmationMode, resolveCompanyImportApiPath, } from "../commands/client/company.js"; @@ -58,6 +59,48 @@ describe("resolveCompanyImportApiPath", () => { }); }); +describe("resolveCompanyImportApplyConfirmationMode", () => { + it("skips confirmation when --yes is set", () => { + expect( + resolveCompanyImportApplyConfirmationMode({ + yes: true, + interactive: false, + json: false, + }), + ).toBe("skip"); + }); + + it("prompts in interactive text mode when --yes is not set", () => { + expect( + resolveCompanyImportApplyConfirmationMode({ + yes: false, + interactive: true, + json: false, + }), + ).toBe("prompt"); + }); + + it("requires --yes for non-interactive apply", () => { + expect(() => + resolveCompanyImportApplyConfirmationMode({ + yes: false, + interactive: false, + json: false, + }) + ).toThrow(/non-interactive terminal requires --yes/i); + }); + + it("requires --yes for json apply", () => { + expect(() => + resolveCompanyImportApplyConfirmationMode({ + yes: false, + interactive: false, + json: true, + }) + ).toThrow(/with --json requires --yes/i); + }); +}); + describe("renderCompanyImportPreview", () => { it("summarizes the preview with counts, selection info, and truncated examples", () => { const preview: CompanyPortabilityPreviewResult = { diff --git a/cli/src/commands/client/company.ts b/cli/src/commands/client/company.ts index 242dc70ad..dbf52890a 100644 --- a/cli/src/commands/client/company.ts +++ b/cli/src/commands/client/company.ts @@ -704,6 +704,27 @@ export function resolveCompanyImportApiPath(input: { return input.dryRun ? "/api/companies/import/preview" : "/api/companies/import"; } +export function resolveCompanyImportApplyConfirmationMode(input: { + yes?: boolean; + interactive: boolean; + json: boolean; +}): "skip" | "prompt" { + if (input.yes) { + return "skip"; + } + if (input.json) { + throw new Error( + "Applying a company import with --json requires --yes. Use --dry-run first to inspect the preview.", + ); + } + if (!input.interactive) { + throw new Error( + "Applying a company import from a non-interactive terminal requires --yes. Use --dry-run first to inspect the preview.", + ); + } + return "prompt"; +} + export function isHttpUrl(input: string): boolean { return /^https?:\/\//i.test(input.trim()); } @@ -1095,7 +1116,7 @@ export function registerCompanyCommands(program: Command): void { .option("--collision ", "Collision strategy: rename | skip | replace", "rename") .option("--ref ", "Git ref to use for GitHub imports (branch, tag, or commit)") .option("--paperclip-url ", "Alias for --api-base on this command") - .option("--yes", "Accept the default import selection without opening the TUI", false) + .option("--yes", "Accept default selection and skip the pre-import confirmation prompt", false) .option("--dry-run", "Run preview only without applying", false) .action(async (fromPathOrUrl: string, opts: CompanyImportOptions) => { try { @@ -1220,6 +1241,34 @@ export function registerCompanyCommands(program: Command): void { return; } + if (!ctx.json) { + printCompanyImportView( + "Import Preview", + renderCompanyImportPreview(preview, { + sourceLabel, + targetLabel: formatTargetLabel(targetPayload, preview), + infoMessages: adapterMessages, + }), + { interactive: interactiveView }, + ); + } + + const confirmationMode = resolveCompanyImportApplyConfirmationMode({ + yes: opts.yes, + interactive: interactiveView, + json: ctx.json, + }); + if (confirmationMode === "prompt") { + const confirmed = await p.confirm({ + message: "Apply this import? (y/N)", + initialValue: false, + }); + if (p.isCancel(confirmed) || !confirmed) { + p.log.warn("Import cancelled."); + return; + } + } + const importApiPath = resolveCompanyImportApiPath({ dryRun: false, targetMode: targetPayload.mode,