diff --git a/extensions/codex/src/migration/plan.ts b/extensions/codex/src/migration/plan.ts index 139ae8eb6f5..5d2412f13c8 100644 --- a/extensions/codex/src/migration/plan.ts +++ b/extensions/codex/src/migration/plan.ts @@ -441,9 +441,6 @@ export async function buildCodexMigrationPlan( "Codex app-backed plugins were planned without source app accessibility verification. Re-run with --verify-plugin-apps to force a fresh source app/list check before planning native plugin activation.", ] : []), - ...(source.plugins.some((plugin) => plugin.sourceKind === "cache") - ? ["Codex cached plugin bundles remain manual-review only."] - : []), ...(source.pluginDiscoveryError ? [ `Codex app-server plugin inventory discovery failed: ${source.pluginDiscoveryError}. Cached plugin bundles, if any, are advisory only.`, diff --git a/src/commands/migrate.ts b/src/commands/migrate.ts index c1cd6f1f6cf..91b27025f9d 100644 --- a/src/commands/migrate.ts +++ b/src/commands/migrate.ts @@ -1,4 +1,4 @@ -import { cancel, isCancel } from "@clack/prompts"; +import { cancel, isCancel, log } from "@clack/prompts"; import { formatCliCommand } from "../cli/command-format.js"; import { withProgress } from "../cli/progress.js"; import { promptYesNo } from "../cli/prompt.js"; @@ -13,7 +13,7 @@ import type { RuntimeEnv } from "../runtime.js"; import { writeRuntimeJson } from "../runtime.js"; import { stylePromptHint, stylePromptMessage, stylePromptTitle } from "../terminal/prompt-style.js"; import { runMigrationApply } from "./migrate/apply.js"; -import { formatMigrationPlan } from "./migrate/output.js"; +import { formatMigrationPreview } from "./migrate/output.js"; import { createMigrationPlan, resolveMigrationProvider } from "./migrate/providers.js"; import { applyMigrationPluginSelection, @@ -331,7 +331,7 @@ export async function migratePlanCommand( if (opts.json) { writeRuntimeJson(runtime, redactMigrationPlan(plan)); } else if (opts.suppressPlanLog !== true) { - runtime.log(formatMigrationPlan(plan).join("\n")); + log.message(formatMigrationPreview(plan).join("\n")); } return plan; } diff --git a/src/commands/migrate/output.ts b/src/commands/migrate/output.ts index 358b87fb145..8798d9d7722 100644 --- a/src/commands/migrate/output.ts +++ b/src/commands/migrate/output.ts @@ -1,76 +1,202 @@ +import { log } from "@clack/prompts"; import { redactMigrationPlan } from "../../plugin-sdk/migration.js"; import type { MigrationApplyResult, MigrationItem, MigrationPlan } from "../../plugins/types.js"; import { writeRuntimeJson } from "../../runtime.js"; import type { RuntimeEnv } from "../../runtime.js"; import { theme } from "../../terminal/theme.js"; -import { - formatMigrationPluginSelectionLabel, - getSelectableMigrationPluginItems, -} from "./selection.js"; import type { MigrateApplyOptions } from "./types.js"; function formatCount(value: number, label: string): string { return `${value} ${label}${value === 1 ? "" : "s"}`; } -export function formatMigrationPlan(plan: MigrationPlan): string[] { - const lines = [ - `${theme.heading("Migration plan:")} ${plan.providerId}`, - `Source: ${plan.source}`, - ]; +function formatPlanHeader(plan: MigrationPlan, heading: string): string[] { + const lines = [`${theme.heading(heading)} ${plan.providerId}`, `Source: ${plan.source}`]; if (plan.target) { lines.push(`Target: ${plan.target}`); } + const visible = plan.items.filter((item) => !HIDDEN_KINDS.has(item.kind)); + const visibleConflicts = visible.filter((item) => item.status === "conflict").length; + const visibleSensitive = visible.filter((item) => item.sensitive === true).length; lines.push( [ - formatCount(plan.summary.total, "item"), - formatCount(plan.summary.conflicts, "conflict"), - formatCount(plan.summary.sensitive, "sensitive item"), + formatCount(visible.length, "item"), + formatCount(visibleConflicts, "conflict"), + formatCount(visibleSensitive, "sensitive item"), ].join(", "), ); - if (plan.warnings && plan.warnings.length > 0) { - lines.push(""); - lines.push(theme.warn("Warnings:")); - for (const warning of plan.warnings) { - lines.push(`- ${warning}`); + return lines; +} + +type ItemGroup = { + kind: string; + heading: string; +}; + +const ITEM_GROUPS: ItemGroup[] = [ + { kind: "skill", heading: "Skills:" }, + { kind: "plugin", heading: "Plugins:" }, + { kind: "memory", heading: "Memory:" }, + { kind: "secret", heading: "Secrets:" }, + { kind: "archive", heading: "Archive:" }, + { kind: "manual", heading: "Manual review:" }, +]; + +const HIDDEN_KINDS = new Set(["config"]); +const KNOWN_KINDS = new Set(ITEM_GROUPS.map((group) => group.kind)); + +type FormatMode = "preview" | "result"; + +function formatPlanItems(plan: MigrationPlan, mode: FormatMode): string[] { + const lines: string[] = []; + const buckets = new Map(); + const other: MigrationItem[] = []; + for (const item of plan.items) { + if (HIDDEN_KINDS.has(item.kind)) { + continue; + } + if (KNOWN_KINDS.has(item.kind)) { + const list = buckets.get(item.kind) ?? []; + list.push(item); + buckets.set(item.kind, list); + } else { + other.push(item); } } - const visibleItems = plan.items.slice(0, 25); - const visibleItemIds = new Set(visibleItems.map((item) => item.id)); - const pluginItems = getSelectableMigrationPluginItems(plan); - const hasPluginHiddenByTruncation = pluginItems.some((item) => !visibleItemIds.has(item.id)); - if (plan.providerId === "codex" && hasPluginHiddenByTruncation) { + for (const group of ITEM_GROUPS) { + const items = buckets.get(group.kind); + if (!items || items.length === 0) { + continue; + } lines.push(""); - lines.push(theme.heading("Native Codex plugins:")); - for (const item of pluginItems) { - lines.push(`- ${formatMigrationPluginSelectionLabel(item)}`); + lines.push(theme.heading(group.heading)); + for (const item of items) { + lines.push(formatMigrationItem(item, mode)); } } - if (visibleItems.length > 0) { + if (other.length > 0) { lines.push(""); - lines.push(theme.heading("Items:")); - for (const item of visibleItems) { - lines.push(formatMigrationItem(item)); - } - if (plan.items.length > visibleItems.length) { - lines.push(`- ... ${plan.items.length - visibleItems.length} more`); - } - } - if (plan.nextSteps && plan.nextSteps.length > 0) { - lines.push(""); - lines.push(theme.heading("Next:")); - for (const step of plan.nextSteps) { - lines.push(`- ${step}`); + lines.push(theme.heading("Other:")); + for (const item of other) { + lines.push(formatMigrationItem(item, mode)); } } return lines; } -function formatMigrationItem(item: MigrationItem): string { - const target = item.target ? ` -> ${item.target}` : ""; - const message = item.message ? ` (${item.message})` : item.reason ? ` (${item.reason})` : ""; +function formatPlanWarnings(plan: MigrationPlan): string[] { + if (!plan.warnings || plan.warnings.length === 0) { + return []; + } + const lines = ["", theme.warn("Warnings:")]; + for (const warning of plan.warnings) { + lines.push(`• ${warning}`); + } + return lines; +} + +export function formatMigrationPreview(plan: MigrationPlan): string[] { + return [ + ...formatPlanHeader(plan, "Migration preview:"), + ...formatPlanItems(plan, "preview"), + ...formatPlanWarnings(plan), + ]; +} + +export function formatMigrationResult(plan: MigrationPlan): string[] { + const lines = [...formatPlanHeader(plan, "Migration plan:"), ...formatPlanItems(plan, "result")]; + if (plan.nextSteps && plan.nextSteps.length > 0) { + lines.push(""); + lines.push(theme.heading("Next:")); + for (const step of plan.nextSteps) { + lines.push(`• ${step}`); + } + } + return lines; +} + +function formatItemDisplayName(item: MigrationItem): string { + const colonIndex = item.id.indexOf(":"); + const withoutPrefix = colonIndex >= 0 ? item.id.slice(colonIndex + 1) : item.id; + return withoutPrefix.replace(/:\d+$/, ""); +} + +const REASON_CODE_MESSAGES: Record = { + plugin_missing: "Plugin not found in the Codex marketplace.", + marketplace_missing: "Codex marketplace is unavailable.", + disabled: "Plugin is disabled in Codex.", + refresh_failed: "Failed to refresh the Codex plugin marketplace.", + auth_required: "Plugin requires additional authentication.", + already_active: "Plugin is already active in OpenClaw.", + installed: "Plugin is already installed in OpenClaw.", + plugin_install_failed: "Plugin installation failed.", + codex_subscription_required: "Plugin requires an active Codex subscription.", + "not selected for migration": "Skipped because it was not selected for migration.", +}; + +function humanizeReason(reason: string | undefined): string | undefined { + if (!reason) { + return undefined; + } + return REASON_CODE_MESSAGES[reason] ?? reason; +} + +function formatItemMessage(item: MigrationItem, mode: FormatMode): string | undefined { + if (mode === "preview") { + if (item.kind === "skill" && item.action === "copy") { + return "Copy Codex skill into OpenClaw"; + } + if (item.kind === "plugin" && item.action === "install") { + return "Install Codex plugin into OpenClaw"; + } + return item.message ?? humanizeReason(item.reason); + } + if ( + (item.kind === "skill" && item.action === "copy") || + (item.kind === "plugin" && item.action === "install") + ) { + if (item.status === "migrated") { + return "Migrated"; + } + if (item.status === "skipped") { + return "Skipped"; + } + if (item.status === "error" || item.status === "conflict") { + return humanizeReason(item.reason) ?? item.message; + } + return undefined; + } + if (item.status === "error" || item.status === "conflict") { + return humanizeReason(item.reason) ?? item.message; + } + return item.message ?? humanizeReason(item.reason); +} + +const RESULT_STATUS_GLYPHS: Record = { + migrated: "✅", + error: "❌", + skipped: "⏭️ ", + conflict: "⚠️ ", +}; + +function formatItemPrefix(item: MigrationItem, mode: FormatMode): string { + if (mode === "result") { + const glyph = RESULT_STATUS_GLYPHS[item.status]; + if (glyph) { + return `${glyph} `; + } + return "• "; + } + return item.status === "planned" ? "• " : `• ${item.status}: `; +} + +function formatMigrationItem(item: MigrationItem, mode: FormatMode): string { + const name = formatItemDisplayName(item); + const message = formatItemMessage(item, mode); + const messageSuffix = message ? ` ${theme.muted(`(${message})`)}` : ""; const sensitive = item.sensitive ? " [sensitive]" : ""; - return `- ${item.status}: ${item.kind}/${item.action} ${item.id}${target}${sensitive}${message}`; + const prefix = formatItemPrefix(item, mode); + return `${prefix}${name}${sensitive}${messageSuffix}`; } export function assertConflictFreePlan(plan: MigrationPlan, providerId: string): void { @@ -90,7 +216,7 @@ export function writeApplyResult( writeRuntimeJson(runtime, redactMigrationPlan(result)); return; } - runtime.log(formatMigrationPlan(result).join("\n")); + log.message(formatMigrationResult(result).join("\n")); if (result.backupPath) { runtime.log(`Backup: ${result.backupPath}`); } else if (!opts.noBackup) { diff --git a/src/wizard/setup.migration-import.ts b/src/wizard/setup.migration-import.ts index f955234c17e..62abc0df037 100644 --- a/src/wizard/setup.migration-import.ts +++ b/src/wizard/setup.migration-import.ts @@ -212,7 +212,7 @@ export async function runSetupMigrationImport(params: { { applyLocalSetupWorkspaceConfig, applySkipBootstrapConfig }, { createMigrationLogger, buildMigrationReportDir }, { createPreMigrationBackup }, - { assertApplySucceeded, assertConflictFreePlan, formatMigrationPlan }, + { assertApplySucceeded, assertConflictFreePlan, formatMigrationPreview, formatMigrationResult }, { resolveStateDir }, onboardHelpers, ] = await Promise.all([ @@ -273,7 +273,7 @@ export async function runSetupMigrationImport(params: { logger: createMigrationLogger(params.runtime), }; const plan = await provider.plan(ctx); - await params.prompter.note(formatMigrationPlan(plan).join("\n"), "Migration preview"); + await params.prompter.note(formatMigrationPreview(plan).join("\n"), "Migration preview"); assertConflictFreePlan(plan, providerId); const confirmed = @@ -307,6 +307,6 @@ export async function runSetupMigrationImport(params: { reportDir: result.reportDir ?? reportDir, }; assertApplySucceeded(withReport); - await params.prompter.note(formatMigrationPlan(withReport).join("\n"), "Migration applied"); + await params.prompter.note(formatMigrationResult(withReport).join("\n"), "Migration applied"); await params.prompter.outro("Migration complete. Run `openclaw doctor` next."); }