Polish import adapter defaults
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
1246ccf250
commit
06f5632d1a
2 changed files with 154 additions and 14 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import type { CompanyPortabilityPreviewResult } from "@paperclipai/shared";
|
import type { CompanyPortabilityPreviewResult } from "@paperclipai/shared";
|
||||||
import {
|
import {
|
||||||
|
buildDefaultImportAdapterOverrides,
|
||||||
buildDefaultImportSelectionState,
|
buildDefaultImportSelectionState,
|
||||||
buildImportSelectionCatalog,
|
buildImportSelectionCatalog,
|
||||||
buildSelectedFilesFromImportSelection,
|
buildSelectedFilesFromImportSelection,
|
||||||
|
|
@ -217,6 +218,7 @@ describe("renderCompanyImportPreview", () => {
|
||||||
const rendered = renderCompanyImportPreview(preview, {
|
const rendered = renderCompanyImportPreview(preview, {
|
||||||
sourceLabel: "GitHub: https://github.com/paperclipai/companies/demo",
|
sourceLabel: "GitHub: https://github.com/paperclipai/companies/demo",
|
||||||
targetLabel: "Imported Co (company-123)",
|
targetLabel: "Imported Co (company-123)",
|
||||||
|
infoMessages: ["Using claude-local adapter"],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(rendered).toContain("Include");
|
expect(rendered).toContain("Include");
|
||||||
|
|
@ -226,6 +228,7 @@ describe("renderCompanyImportPreview", () => {
|
||||||
expect(rendered).toContain("1 task total");
|
expect(rendered).toContain("1 task total");
|
||||||
expect(rendered).toContain("skills: 1 skill packaged");
|
expect(rendered).toContain("skills: 1 skill packaged");
|
||||||
expect(rendered).toContain("+1 more");
|
expect(rendered).toContain("+1 more");
|
||||||
|
expect(rendered).toContain("Using claude-local adapter");
|
||||||
expect(rendered).toContain("Warnings");
|
expect(rendered).toContain("Warnings");
|
||||||
expect(rendered).toContain("Errors");
|
expect(rendered).toContain("Errors");
|
||||||
});
|
});
|
||||||
|
|
@ -248,12 +251,16 @@ describe("renderCompanyImportResult", () => {
|
||||||
envInputs: [],
|
envInputs: [],
|
||||||
warnings: ["Review API keys"],
|
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("Company");
|
||||||
expect(rendered).toContain("3 agents total (1 created, 1 updated, 1 skipped)");
|
expect(rendered).toContain("3 agents total (1 created, 1 updated, 1 skipped)");
|
||||||
expect(rendered).toContain("Agent results");
|
expect(rendered).toContain("Agent results");
|
||||||
|
expect(rendered).toContain("Using claude-local adapter");
|
||||||
expect(rendered).toContain("Review API keys");
|
expect(rendered).toContain("Review API keys");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -421,3 +428,90 @@ describe("import selection catalog", () => {
|
||||||
expect(selectedFiles).not.toContain("projects/alpha/issues/kickoff/details.md");
|
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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ interface CompanyImportOptions extends BaseClientOptions {
|
||||||
agents?: string;
|
agents?: string;
|
||||||
collision?: CompanyCollisionMode;
|
collision?: CompanyCollisionMode;
|
||||||
ref?: string;
|
ref?: string;
|
||||||
|
paperclipUrl?: string;
|
||||||
yes?: boolean;
|
yes?: boolean;
|
||||||
dryRun?: boolean;
|
dryRun?: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -346,6 +347,37 @@ export function buildSelectedFilesFromImportSelection(
|
||||||
return Array.from(selected).sort((left, right) => left.localeCompare(right));
|
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[]> {
|
async function promptForImportSelection(preview: CompanyPortabilityPreviewResult): Promise<string[]> {
|
||||||
const catalog = buildImportSelectionCatalog(preview);
|
const catalog = buildImportSelectionCatalog(preview);
|
||||||
const state = buildDefaultImportSelectionState(catalog);
|
const state = buildDefaultImportSelectionState(catalog);
|
||||||
|
|
@ -419,7 +451,7 @@ async function promptForImportSelection(preview: CompanyPortabilityPreviewResult
|
||||||
}
|
}
|
||||||
|
|
||||||
const selection = await p.multiselect<string>({
|
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) => ({
|
options: groupItems.map((item) => ({
|
||||||
value: item.key,
|
value: item.key,
|
||||||
label: item.label,
|
label: item.label,
|
||||||
|
|
@ -544,6 +576,7 @@ export function renderCompanyImportPreview(
|
||||||
meta: {
|
meta: {
|
||||||
sourceLabel: string;
|
sourceLabel: string;
|
||||||
targetLabel: string;
|
targetLabel: string;
|
||||||
|
infoMessages?: string[];
|
||||||
},
|
},
|
||||||
): string {
|
): string {
|
||||||
const lines: 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.yellow("Warnings"), preview.warnings);
|
||||||
appendMessageBlock(lines, pc.red("Errors"), preview.errors);
|
appendMessageBlock(lines, pc.red("Errors"), preview.errors);
|
||||||
|
|
||||||
|
|
@ -611,7 +645,7 @@ export function renderCompanyImportPreview(
|
||||||
|
|
||||||
export function renderCompanyImportResult(
|
export function renderCompanyImportResult(
|
||||||
result: CompanyPortabilityImportResult,
|
result: CompanyPortabilityImportResult,
|
||||||
meta: { targetLabel: string },
|
meta: { targetLabel: string; infoMessages?: string[] },
|
||||||
): string {
|
): string {
|
||||||
const lines: string[] = [
|
const lines: string[] = [
|
||||||
`${pc.bold("Target")} ${meta.targetLabel}`,
|
`${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);
|
appendMessageBlock(lines, pc.yellow("Warnings"), result.warnings);
|
||||||
|
|
||||||
return lines.join("\n");
|
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("--agents <list>", "Comma-separated agent slugs to import, or all", "all")
|
||||||
.option("--collision <mode>", "Collision strategy: rename | skip | replace", "rename")
|
.option("--collision <mode>", "Collision strategy: rename | skip | replace", "rename")
|
||||||
.option("--ref <value>", "Git ref to use for GitHub imports (branch, tag, or commit)")
|
.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("--yes", "Accept the default import selection without opening the TUI", false)
|
||||||
.option("--dry-run", "Run preview only without applying", false)
|
.option("--dry-run", "Run preview only without applying", false)
|
||||||
.action(async (fromPathOrUrl: string, opts: CompanyImportOptions) => {
|
.action(async (fromPathOrUrl: string, opts: CompanyImportOptions) => {
|
||||||
try {
|
try {
|
||||||
|
if (!opts.apiBase?.trim() && opts.paperclipUrl?.trim()) {
|
||||||
|
opts.apiBase = opts.paperclipUrl.trim();
|
||||||
|
}
|
||||||
const ctx = resolveCommandContext(opts);
|
const ctx = resolveCommandContext(opts);
|
||||||
const interactiveView = isInteractiveTerminal() && !ctx.json;
|
const interactiveView = isInteractiveTerminal() && !ctx.json;
|
||||||
const from = fromPathOrUrl.trim();
|
const from = fromPathOrUrl.trim();
|
||||||
|
|
@ -1149,7 +1188,7 @@ export function registerCompanyCommands(program: Command): void {
|
||||||
selectedFiles = await promptForImportSelection(initialPreview);
|
selectedFiles = await promptForImportSelection(initialPreview);
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
const previewPayload = {
|
||||||
source: sourcePayload,
|
source: sourcePayload,
|
||||||
include,
|
include,
|
||||||
target: targetPayload,
|
target: targetPayload,
|
||||||
|
|
@ -1157,17 +1196,14 @@ export function registerCompanyCommands(program: Command): void {
|
||||||
collisionStrategy: collision,
|
collisionStrategy: collision,
|
||||||
selectedFiles,
|
selectedFiles,
|
||||||
};
|
};
|
||||||
const importApiPath = resolveCompanyImportApiPath({
|
const preview = await ctx.api.post<CompanyPortabilityPreviewResult>(previewApiPath, previewPayload);
|
||||||
dryRun: Boolean(opts.dryRun),
|
if (!preview) {
|
||||||
targetMode: targetPayload.mode,
|
throw new Error("Import preview returned no data.");
|
||||||
companyId: targetPayload.mode === "existing_company" ? targetPayload.companyId : null,
|
}
|
||||||
});
|
const adapterOverrides = buildDefaultImportAdapterOverrides(preview);
|
||||||
|
const adapterMessages = buildDefaultImportAdapterMessages(adapterOverrides);
|
||||||
|
|
||||||
if (opts.dryRun) {
|
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) {
|
if (ctx.json) {
|
||||||
printOutput(preview, { json: true });
|
printOutput(preview, { json: true });
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1176,6 +1212,7 @@ export function registerCompanyCommands(program: Command): void {
|
||||||
renderCompanyImportPreview(preview, {
|
renderCompanyImportPreview(preview, {
|
||||||
sourceLabel,
|
sourceLabel,
|
||||||
targetLabel: formatTargetLabel(targetPayload, preview),
|
targetLabel: formatTargetLabel(targetPayload, preview),
|
||||||
|
infoMessages: adapterMessages,
|
||||||
}),
|
}),
|
||||||
{ interactive: interactiveView },
|
{ interactive: interactiveView },
|
||||||
);
|
);
|
||||||
|
|
@ -1183,7 +1220,15 @@ export function registerCompanyCommands(program: Command): void {
|
||||||
return;
|
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) {
|
if (!imported) {
|
||||||
throw new Error("Import request returned no data.");
|
throw new Error("Import request returned no data.");
|
||||||
}
|
}
|
||||||
|
|
@ -1194,6 +1239,7 @@ export function registerCompanyCommands(program: Command): void {
|
||||||
"Import Result",
|
"Import Result",
|
||||||
renderCompanyImportResult(imported, {
|
renderCompanyImportResult(imported, {
|
||||||
targetLabel,
|
targetLabel,
|
||||||
|
infoMessages: adapterMessages,
|
||||||
}),
|
}),
|
||||||
{ interactive: interactiveView },
|
{ interactive: interactiveView },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue