From b5fde733b0814c032c55548081f7022a42cbf0c4 Mon Sep 17 00:00:00 2001 From: dotta Date: Mon, 23 Mar 2026 16:41:52 -0500 Subject: [PATCH] Open imported company after import Co-Authored-By: Paperclip --- cli/src/__tests__/company.test.ts | 21 +++++++++++++++ cli/src/client/board-auth.ts | 2 +- cli/src/commands/client/company.ts | 42 +++++++++++++++++++++++++++++- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/cli/src/__tests__/company.test.ts b/cli/src/__tests__/company.test.ts index bcc3137c..2345fb7d 100644 --- a/cli/src/__tests__/company.test.ts +++ b/cli/src/__tests__/company.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import type { CompanyPortabilityPreviewResult } from "@paperclipai/shared"; import { + buildCompanyDashboardUrl, buildDefaultImportAdapterOverrides, buildDefaultImportSelectionState, buildImportSelectionCatalog, @@ -101,6 +102,14 @@ describe("resolveCompanyImportApplyConfirmationMode", () => { }); }); +describe("buildCompanyDashboardUrl", () => { + it("preserves the configured base path when building a dashboard URL", () => { + expect(buildCompanyDashboardUrl("https://paperclip.example/app/", "PAP")).toBe( + "https://paperclip.example/app/PAP/dashboard", + ); + }); +}); + describe("renderCompanyImportPreview", () => { it("summarizes the preview with counts, selection info, and truncated examples", () => { const preview: CompanyPortabilityPreviewResult = { @@ -155,6 +164,10 @@ describe("renderCompanyImportPreview", () => { logoPath: null, requireBoardApprovalForNewAgents: false, }, + sidebar: { + agents: ["ceo"], + projects: ["alpha"], + }, agents: [ { slug: "ceo", @@ -291,16 +304,19 @@ describe("renderCompanyImportResult", () => { { slug: "cto", id: "agent-2", action: "updated", name: "CTO", reason: "replace strategy" }, { slug: "ops", id: null, action: "skipped", name: "Ops", reason: "skip strategy" }, ], + projects: [], envInputs: [], warnings: ["Review API keys"], }, { targetLabel: "Imported Co (company-123)", + companyUrl: "https://paperclip.example/PAP/dashboard", infoMessages: ["Using claude-local adapter"], }, ); expect(rendered).toContain("Company"); + expect(rendered).toContain("https://paperclip.example/PAP/dashboard"); expect(rendered).toContain("3 agents total (1 created, 1 updated, 1 skipped)"); expect(rendered).toContain("Agent results"); expect(rendered).toContain("Using claude-local adapter"); @@ -350,6 +366,10 @@ describe("import selection catalog", () => { logoPath: "images/company-logo.png", requireBoardApprovalForNewAgents: false, }, + sidebar: { + agents: ["ceo"], + projects: ["alpha"], + }, agents: [ { slug: "ceo", @@ -504,6 +524,7 @@ describe("default adapter overrides", () => { skills: false, }, company: null, + sidebar: null, agents: [ { slug: "legacy-agent", diff --git a/cli/src/client/board-auth.ts b/cli/src/client/board-auth.ts index 5ed7cd7a..7c1121ec 100644 --- a/cli/src/client/board-auth.ts +++ b/cli/src/client/board-auth.ts @@ -169,7 +169,7 @@ async function requestJson(url: string, init?: RequestInit): Promise { return response.json() as Promise; } -function openUrl(url: string): boolean { +export function openUrl(url: string): boolean { const platform = process.platform; try { if (platform === "darwin") { diff --git a/cli/src/commands/client/company.ts b/cli/src/commands/client/company.ts index 013f8e56..ca0c3b92 100644 --- a/cli/src/commands/client/company.ts +++ b/cli/src/commands/client/company.ts @@ -12,6 +12,7 @@ import type { CompanyPortabilityImportResult, } from "@paperclipai/shared"; import { ApiRequestError } from "../../client/http.js"; +import { openUrl } from "../../client/board-auth.js"; import { readZipArchive } from "./zip.js"; import { addCommonClientOptions, @@ -654,7 +655,7 @@ export function renderCompanyImportPreview( export function renderCompanyImportResult( result: CompanyPortabilityImportResult, - meta: { targetLabel: string; infoMessages?: string[] }, + meta: { targetLabel: string; companyUrl?: string; infoMessages?: string[] }, ): string { const lines: string[] = [ `${pc.bold("Target")} ${meta.targetLabel}`, @@ -662,6 +663,10 @@ export function renderCompanyImportResult( `${pc.bold("Agents")} ${summarizeImportAgentResults(result.agents)}`, ]; + if (meta.companyUrl) { + lines.splice(1, 0, `${pc.bold("URL")} ${meta.companyUrl}`); + } + appendPreviewExamples( lines, "Agent results", @@ -713,6 +718,15 @@ export function resolveCompanyImportApiPath(input: { return input.dryRun ? "/api/companies/import/preview" : "/api/companies/import"; } +export function buildCompanyDashboardUrl(apiBase: string, issuePrefix: string): string { + const url = new URL(apiBase); + const normalizedPrefix = issuePrefix.trim().replace(/^\/+|\/+$/g, ""); + url.pathname = `${url.pathname.replace(/\/+$/, "")}/${normalizedPrefix}/dashboard`; + url.search = ""; + url.hash = ""; + return url.toString(); +} + export function resolveCompanyImportApplyConfirmationMode(input: { yes?: boolean; interactive: boolean; @@ -1298,6 +1312,18 @@ export function registerCompanyCommands(program: Command): void { if (!imported) { throw new Error("Import request returned no data."); } + let companyUrl: string | undefined; + if (!ctx.json) { + try { + const importedCompany = await ctx.api.get(`/api/companies/${imported.company.id}`); + const issuePrefix = importedCompany?.issuePrefix?.trim(); + if (issuePrefix) { + companyUrl = buildCompanyDashboardUrl(ctx.api.apiBase, issuePrefix); + } + } catch { + companyUrl = undefined; + } + } if (ctx.json) { printOutput(imported, { json: true }); } else { @@ -1305,10 +1331,24 @@ export function registerCompanyCommands(program: Command): void { "Import Result", renderCompanyImportResult(imported, { targetLabel, + companyUrl, infoMessages: adapterMessages, }), { interactive: interactiveView }, ); + if (interactiveView && companyUrl) { + const openImportedCompany = await p.confirm({ + message: "Open the imported company in your browser?", + initialValue: true, + }); + if (!p.isCancel(openImportedCompany) && openImportedCompany) { + if (openUrl(companyUrl)) { + p.log.info(`Opened ${companyUrl}`); + } else { + p.log.warn(`Could not open your browser automatically. Open this URL manually:\n${companyUrl}`); + } + } + } } } catch (err) { handleCommandError(err);