Polish import adapter defaults

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta
2026-03-23 13:21:10 -05:00
parent 1246ccf250
commit 06f5632d1a
2 changed files with 154 additions and 14 deletions

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import type { CompanyPortabilityPreviewResult } from "@paperclipai/shared";
import {
buildDefaultImportAdapterOverrides,
buildDefaultImportSelectionState,
buildImportSelectionCatalog,
buildSelectedFilesFromImportSelection,
@@ -217,6 +218,7 @@ describe("renderCompanyImportPreview", () => {
const rendered = renderCompanyImportPreview(preview, {
sourceLabel: "GitHub: https://github.com/paperclipai/companies/demo",
targetLabel: "Imported Co (company-123)",
infoMessages: ["Using claude-local adapter"],
});
expect(rendered).toContain("Include");
@@ -226,6 +228,7 @@ describe("renderCompanyImportPreview", () => {
expect(rendered).toContain("1 task total");
expect(rendered).toContain("skills: 1 skill packaged");
expect(rendered).toContain("+1 more");
expect(rendered).toContain("Using claude-local adapter");
expect(rendered).toContain("Warnings");
expect(rendered).toContain("Errors");
});
@@ -248,12 +251,16 @@ describe("renderCompanyImportResult", () => {
envInputs: [],
warnings: ["Review API keys"],
},
{ targetLabel: "Imported Co (company-123)" },
{
targetLabel: "Imported Co (company-123)",
infoMessages: ["Using claude-local adapter"],
},
);
expect(rendered).toContain("Company");
expect(rendered).toContain("3 agents total (1 created, 1 updated, 1 skipped)");
expect(rendered).toContain("Agent results");
expect(rendered).toContain("Using claude-local adapter");
expect(rendered).toContain("Review API keys");
});
});
@@ -421,3 +428,90 @@ describe("import selection catalog", () => {
expect(selectedFiles).not.toContain("projects/alpha/issues/kickoff/details.md");
});
});
describe("default adapter overrides", () => {
it("maps process-only imported agents to claude_local", () => {
const preview: CompanyPortabilityPreviewResult = {
include: {
company: false,
agents: true,
projects: false,
issues: false,
skills: false,
},
targetCompanyId: null,
targetCompanyName: null,
collisionStrategy: "rename",
selectedAgentSlugs: ["legacy-agent", "explicit-agent"],
plan: {
companyAction: "none",
agentPlans: [],
projectPlans: [],
issuePlans: [],
},
manifest: {
schemaVersion: 1,
generatedAt: "2026-03-23T18:20:00.000Z",
source: null,
includes: {
company: false,
agents: true,
projects: false,
issues: false,
skills: false,
},
company: null,
agents: [
{
slug: "legacy-agent",
name: "Legacy Agent",
path: "agents/legacy-agent/AGENT.md",
skills: [],
role: "agent",
title: null,
icon: null,
capabilities: null,
reportsToSlug: null,
adapterType: "process",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
budgetMonthlyCents: 0,
metadata: null,
},
{
slug: "explicit-agent",
name: "Explicit Agent",
path: "agents/explicit-agent/AGENT.md",
skills: [],
role: "agent",
title: null,
icon: null,
capabilities: null,
reportsToSlug: null,
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
budgetMonthlyCents: 0,
metadata: null,
},
],
skills: [],
projects: [],
issues: [],
envInputs: [],
},
files: {},
envInputs: [],
warnings: [],
errors: [],
};
expect(buildDefaultImportAdapterOverrides(preview)).toEqual({
"legacy-agent": {
adapterType: "claude_local",
},
});
});
});

View File

@@ -50,6 +50,7 @@ interface CompanyImportOptions extends BaseClientOptions {
agents?: string;
collision?: CompanyCollisionMode;
ref?: string;
paperclipUrl?: string;
yes?: boolean;
dryRun?: boolean;
}
@@ -346,6 +347,37 @@ export function buildSelectedFilesFromImportSelection(
return Array.from(selected).sort((left, right) => left.localeCompare(right));
}
export function buildDefaultImportAdapterOverrides(
preview: Pick<CompanyPortabilityPreviewResult, "manifest" | "selectedAgentSlugs">,
): Record<string, { adapterType: string }> | undefined {
const selectedAgentSlugs = new Set(preview.selectedAgentSlugs);
const overrides = Object.fromEntries(
preview.manifest.agents
.filter((agent) => selectedAgentSlugs.size === 0 || selectedAgentSlugs.has(agent.slug))
.filter((agent) => agent.adapterType === "process")
.map((agent) => [
agent.slug,
{
// TODO: replace this temporary claude_local fallback with adapter selection in the import TUI.
adapterType: "claude_local",
},
]),
);
return Object.keys(overrides).length > 0 ? overrides : undefined;
}
function buildDefaultImportAdapterMessages(
overrides: Record<string, { adapterType: string }> | undefined,
): string[] {
if (!overrides) return [];
const adapterTypes = Array.from(new Set(Object.values(overrides).map((override) => override.adapterType)))
.map((adapterType) => adapterType.replace(/_/g, "-"));
const agentCount = Object.keys(overrides).length;
return [
`Using ${adapterTypes.join(", ")} adapter${adapterTypes.length === 1 ? "" : "s"} for ${agentCount} imported ${pluralize(agentCount, "agent")} without an explicit adapter.`,
];
}
async function promptForImportSelection(preview: CompanyPortabilityPreviewResult): Promise<string[]> {
const catalog = buildImportSelectionCatalog(preview);
const state = buildDefaultImportSelectionState(catalog);
@@ -419,7 +451,7 @@ async function promptForImportSelection(preview: CompanyPortabilityPreviewResult
}
const selection = await p.multiselect<string>({
message: `${getGroupLabel(group)} to import. Press enter to go back.`,
message: `${getGroupLabel(group)} to import. Space toggles, enter returns to the main menu.`,
options: groupItems.map((item) => ({
value: item.key,
label: item.label,
@@ -544,6 +576,7 @@ export function renderCompanyImportPreview(
meta: {
sourceLabel: string;
targetLabel: string;
infoMessages?: string[];
},
): string {
const lines: string[] = [
@@ -603,6 +636,7 @@ export function renderCompanyImportPreview(
})),
);
appendMessageBlock(lines, pc.cyan("Info"), meta.infoMessages ?? []);
appendMessageBlock(lines, pc.yellow("Warnings"), preview.warnings);
appendMessageBlock(lines, pc.red("Errors"), preview.errors);
@@ -611,7 +645,7 @@ export function renderCompanyImportPreview(
export function renderCompanyImportResult(
result: CompanyPortabilityImportResult,
meta: { targetLabel: string },
meta: { targetLabel: string; infoMessages?: string[] },
): string {
const lines: string[] = [
`${pc.bold("Target")} ${meta.targetLabel}`,
@@ -637,6 +671,7 @@ export function renderCompanyImportResult(
);
}
appendMessageBlock(lines, pc.cyan("Info"), meta.infoMessages ?? []);
appendMessageBlock(lines, pc.yellow("Warnings"), result.warnings);
return lines.join("\n");
@@ -1059,10 +1094,14 @@ export function registerCompanyCommands(program: Command): void {
.option("--agents <list>", "Comma-separated agent slugs to import, or all", "all")
.option("--collision <mode>", "Collision strategy: rename | skip | replace", "rename")
.option("--ref <value>", "Git ref to use for GitHub imports (branch, tag, or commit)")
.option("--paperclip-url <url>", "Alias for --api-base on this command")
.option("--yes", "Accept the default import selection without opening the TUI", false)
.option("--dry-run", "Run preview only without applying", false)
.action(async (fromPathOrUrl: string, opts: CompanyImportOptions) => {
try {
if (!opts.apiBase?.trim() && opts.paperclipUrl?.trim()) {
opts.apiBase = opts.paperclipUrl.trim();
}
const ctx = resolveCommandContext(opts);
const interactiveView = isInteractiveTerminal() && !ctx.json;
const from = fromPathOrUrl.trim();
@@ -1149,7 +1188,7 @@ export function registerCompanyCommands(program: Command): void {
selectedFiles = await promptForImportSelection(initialPreview);
}
const payload = {
const previewPayload = {
source: sourcePayload,
include,
target: targetPayload,
@@ -1157,17 +1196,14 @@ export function registerCompanyCommands(program: Command): void {
collisionStrategy: collision,
selectedFiles,
};
const importApiPath = resolveCompanyImportApiPath({
dryRun: Boolean(opts.dryRun),
targetMode: targetPayload.mode,
companyId: targetPayload.mode === "existing_company" ? targetPayload.companyId : null,
});
const preview = await ctx.api.post<CompanyPortabilityPreviewResult>(previewApiPath, previewPayload);
if (!preview) {
throw new Error("Import preview returned no data.");
}
const adapterOverrides = buildDefaultImportAdapterOverrides(preview);
const adapterMessages = buildDefaultImportAdapterMessages(adapterOverrides);
if (opts.dryRun) {
const preview = await ctx.api.post<CompanyPortabilityPreviewResult>(importApiPath, payload);
if (!preview) {
throw new Error("Import preview returned no data.");
}
if (ctx.json) {
printOutput(preview, { json: true });
} else {
@@ -1176,6 +1212,7 @@ export function registerCompanyCommands(program: Command): void {
renderCompanyImportPreview(preview, {
sourceLabel,
targetLabel: formatTargetLabel(targetPayload, preview),
infoMessages: adapterMessages,
}),
{ interactive: interactiveView },
);
@@ -1183,7 +1220,15 @@ export function registerCompanyCommands(program: Command): void {
return;
}
const imported = await ctx.api.post<CompanyPortabilityImportResult>(importApiPath, payload);
const importApiPath = resolveCompanyImportApiPath({
dryRun: false,
targetMode: targetPayload.mode,
companyId: targetPayload.mode === "existing_company" ? targetPayload.companyId : null,
});
const imported = await ctx.api.post<CompanyPortabilityImportResult>(importApiPath, {
...previewPayload,
adapterOverrides,
});
if (!imported) {
throw new Error("Import request returned no data.");
}
@@ -1194,6 +1239,7 @@ export function registerCompanyCommands(program: Command): void {
"Import Result",
renderCompanyImportResult(imported, {
targetLabel,
infoMessages: adapterMessages,
}),
{ interactive: interactiveView },
);