


























@@ -1,76 +1,202 @@
1+import { log } from "@clack/prompts";
12import { redactMigrationPlan } from "../../plugin-sdk/migration.js";
23import type { MigrationApplyResult, MigrationItem, MigrationPlan } from "../../plugins/types.js";
34import { writeRuntimeJson } from "../../runtime.js";
45import type { RuntimeEnv } from "../../runtime.js";
56import { theme } from "../../terminal/theme.js";
6-import {
7-formatMigrationPluginSelectionLabel,
8-getSelectableMigrationPluginItems,
9-} from "./selection.js";
107import type { MigrateApplyOptions } from "./types.js";
118129function formatCount(value: number, label: string): string {
1310return `${value} ${label}${value === 1 ? "" : "s"}`;
1411}
151216-export function formatMigrationPlan(plan: MigrationPlan): string[] {
17-const lines = [
18-`${theme.heading("Migration plan:")} ${plan.providerId}`,
19-`Source: ${plan.source}`,
20-];
13+function formatPlanHeader(plan: MigrationPlan, heading: string): string[] {
14+const lines = [`${theme.heading(heading)} ${plan.providerId}`, `Source: ${plan.source}`];
2115if (plan.target) {
2216lines.push(`Target: ${plan.target}`);
2317}
18+const visible = plan.items.filter((item) => !HIDDEN_KINDS.has(item.kind));
19+const visibleConflicts = visible.filter((item) => item.status === "conflict").length;
20+const visibleSensitive = visible.filter((item) => item.sensitive === true).length;
2421lines.push(
2522[
26-formatCount(plan.summary.total, "item"),
27-formatCount(plan.summary.conflicts, "conflict"),
28-formatCount(plan.summary.sensitive, "sensitive item"),
23+formatCount(visible.length, "item"),
24+formatCount(visibleConflicts, "conflict"),
25+formatCount(visibleSensitive, "sensitive item"),
2926].join(", "),
3027);
31-if (plan.warnings && plan.warnings.length > 0) {
32-lines.push("");
33-lines.push(theme.warn("Warnings:"));
34-for (const warning of plan.warnings) {
35-lines.push(`- ${warning}`);
28+return lines;
29+}
30+31+type ItemGroup = {
32+kind: string;
33+heading: string;
34+};
35+36+const ITEM_GROUPS: ItemGroup[] = [
37+{ kind: "skill", heading: "Skills:" },
38+{ kind: "plugin", heading: "Plugins:" },
39+{ kind: "memory", heading: "Memory:" },
40+{ kind: "secret", heading: "Secrets:" },
41+{ kind: "archive", heading: "Archive:" },
42+{ kind: "manual", heading: "Manual review:" },
43+];
44+45+const HIDDEN_KINDS = new Set(["config"]);
46+const KNOWN_KINDS = new Set(ITEM_GROUPS.map((group) => group.kind));
47+48+type FormatMode = "preview" | "result";
49+50+function formatPlanItems(plan: MigrationPlan, mode: FormatMode): string[] {
51+const lines: string[] = [];
52+const buckets = new Map<string, MigrationItem[]>();
53+const other: MigrationItem[] = [];
54+for (const item of plan.items) {
55+if (HIDDEN_KINDS.has(item.kind)) {
56+continue;
57+}
58+if (KNOWN_KINDS.has(item.kind)) {
59+const list = buckets.get(item.kind) ?? [];
60+list.push(item);
61+buckets.set(item.kind, list);
62+} else {
63+other.push(item);
3664}
3765}
38-const visibleItems = plan.items.slice(0, 25);
39-const visibleItemIds = new Set(visibleItems.map((item) => item.id));
40-const pluginItems = getSelectableMigrationPluginItems(plan);
41-const hasPluginHiddenByTruncation = pluginItems.some((item) => !visibleItemIds.has(item.id));
42-if (plan.providerId === "codex" && hasPluginHiddenByTruncation) {
66+for (const group of ITEM_GROUPS) {
67+ const items = buckets.get(group.kind);
68+ if (!items || items.length === 0) {
69+ continue;
70+ }
4371lines.push("");
44-lines.push(theme.heading("Native Codex plugins:"));
45-for (const item of pluginItems) {
46-lines.push(`- ${formatMigrationPluginSelectionLabel(item)}`);
72+lines.push(theme.heading(group.heading));
73+for (const item of items) {
74+lines.push(formatMigrationItem(item, mode));
4775}
4876}
49-if (visibleItems.length > 0) {
77+if (other.length > 0) {
5078lines.push("");
51-lines.push(theme.heading("Items:"));
52-for (const item of visibleItems) {
53-lines.push(formatMigrationItem(item));
54-}
55-if (plan.items.length > visibleItems.length) {
56-lines.push(`- ... ${plan.items.length - visibleItems.length} more`);
79+lines.push(theme.heading("Other:"));
80+for (const item of other) {
81+lines.push(formatMigrationItem(item, mode));
5782}
5883}
84+return lines;
85+}
86+87+function formatPlanWarnings(plan: MigrationPlan): string[] {
88+if (!plan.warnings || plan.warnings.length === 0) {
89+return [];
90+}
91+const lines = ["", theme.warn("Warnings:")];
92+for (const warning of plan.warnings) {
93+lines.push(`• ${warning}`);
94+}
95+return lines;
96+}
97+98+export function formatMigrationPreview(plan: MigrationPlan): string[] {
99+return [
100+ ...formatPlanHeader(plan, "Migration preview:"),
101+ ...formatPlanItems(plan, "preview"),
102+ ...formatPlanWarnings(plan),
103+];
104+}
105+106+export function formatMigrationResult(plan: MigrationPlan): string[] {
107+const lines = [...formatPlanHeader(plan, "Migration plan:"), ...formatPlanItems(plan, "result")];
59108if (plan.nextSteps && plan.nextSteps.length > 0) {
60109lines.push("");
61110lines.push(theme.heading("Next:"));
62111for (const step of plan.nextSteps) {
63-lines.push(`- ${step}`);
112+lines.push(`• ${step}`);
64113}
65114}
66115return lines;
67116}
6811769-function formatMigrationItem(item: MigrationItem): string {
70-const target = item.target ? ` -> ${item.target}` : "";
71-const message = item.message ? ` (${item.message})` : item.reason ? ` (${item.reason})` : "";
118+function formatItemDisplayName(item: MigrationItem): string {
119+const colonIndex = item.id.indexOf(":");
120+const withoutPrefix = colonIndex >= 0 ? item.id.slice(colonIndex + 1) : item.id;
121+return withoutPrefix.replace(/:\d+$/, "");
122+}
123+124+const REASON_CODE_MESSAGES: Record<string, string> = {
125+plugin_missing: "Plugin not found in the Codex marketplace.",
126+marketplace_missing: "Codex marketplace is unavailable.",
127+disabled: "Plugin is disabled in Codex.",
128+refresh_failed: "Failed to refresh the Codex plugin marketplace.",
129+auth_required: "Plugin requires additional authentication.",
130+already_active: "Plugin is already active in OpenClaw.",
131+installed: "Plugin is already installed in OpenClaw.",
132+plugin_install_failed: "Plugin installation failed.",
133+codex_subscription_required: "Plugin requires an active Codex subscription.",
134+"not selected for migration": "Skipped because it was not selected for migration.",
135+};
136+137+function humanizeReason(reason: string | undefined): string | undefined {
138+if (!reason) {
139+return undefined;
140+}
141+return REASON_CODE_MESSAGES[reason] ?? reason;
142+}
143+144+function formatItemMessage(item: MigrationItem, mode: FormatMode): string | undefined {
145+if (mode === "preview") {
146+if (item.kind === "skill" && item.action === "copy") {
147+return "Copy Codex skill into OpenClaw";
148+}
149+if (item.kind === "plugin" && item.action === "install") {
150+return "Install Codex plugin into OpenClaw";
151+}
152+return item.message ?? humanizeReason(item.reason);
153+}
154+if (
155+(item.kind === "skill" && item.action === "copy") ||
156+(item.kind === "plugin" && item.action === "install")
157+) {
158+if (item.status === "migrated") {
159+return "Migrated";
160+}
161+if (item.status === "skipped") {
162+return "Skipped";
163+}
164+if (item.status === "error" || item.status === "conflict") {
165+return humanizeReason(item.reason) ?? item.message;
166+}
167+return undefined;
168+}
169+if (item.status === "error" || item.status === "conflict") {
170+return humanizeReason(item.reason) ?? item.message;
171+}
172+return item.message ?? humanizeReason(item.reason);
173+}
174+175+const RESULT_STATUS_GLYPHS: Record<string, string> = {
176+migrated: "✅",
177+error: "❌",
178+skipped: "⏭️ ",
179+conflict: "⚠️ ",
180+};
181+182+function formatItemPrefix(item: MigrationItem, mode: FormatMode): string {
183+if (mode === "result") {
184+const glyph = RESULT_STATUS_GLYPHS[item.status];
185+if (glyph) {
186+return `${glyph} `;
187+}
188+return "• ";
189+}
190+return item.status === "planned" ? "• " : `• ${item.status}: `;
191+}
192+193+function formatMigrationItem(item: MigrationItem, mode: FormatMode): string {
194+const name = formatItemDisplayName(item);
195+const message = formatItemMessage(item, mode);
196+const messageSuffix = message ? ` ${theme.muted(`(${message})`)}` : "";
72197const sensitive = item.sensitive ? " [sensitive]" : "";
73-return `- ${item.status}: ${item.kind}/${item.action} ${item.id}${target}${sensitive}${message}`;
198+const prefix = formatItemPrefix(item, mode);
199+return `${prefix}${name}${sensitive}${messageSuffix}`;
74200}
7520176202export function assertConflictFreePlan(plan: MigrationPlan, providerId: string): void {
@@ -90,7 +216,7 @@ export function writeApplyResult(
90216writeRuntimeJson(runtime, redactMigrationPlan(result));
91217return;
92218}
93-runtime.log(formatMigrationPlan(result).join("\n"));
219+log.message(formatMigrationResult(result).join("\n"));
94220if (result.backupPath) {
95221runtime.log(`Backup: ${result.backupPath}`);
96222} else if (!opts.noBackup) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。