mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 18:54:48 +00:00
ux(codex-migrate): polish preview/result output
Restructure the migrate codex CLI output: - Split into separate Before (preview) and After (result) messages so each can be tuned independently. Both render through clack's log.message so they pick up the standard '|' gutter. - Group items by kind (Skills, Plugins, Memory, Secrets, Archive, Manual review, Other) instead of one flat list. Hide config items from display and exclude them from the summary count. - Drop the internal kind/action tag (e.g. 'manual/manual'), strip '<kind>:' id prefixes and trailing ':N' disambiguators, and use '•' for bullets. - Mute parenthetical action text. - In result mode: replace status text with emoji (✅ migrated, ❌ error, ⏭️ skipped, ⚠️ conflict), show '(Migrated)' on success, show humanized failure reasons for known codes (plugin_missing, marketplace_missing, etc.), say '(Skipped)' for user-deselected skill/plugin items but keep the real message on manual-review skips. Drop warnings from the result message. - In preview mode: omit the 'Next' section and move warnings to the bottom. Use generic action descriptions ('Copy Codex skill into OpenClaw', 'Install Codex plugin into OpenClaw'). - Drop the redundant 'Codex cached plugin bundles remain manual-review only.' warning — covered by the source-installed warning above it.
This commit is contained in:
@@ -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.`,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<string, MigrationItem[]>();
|
||||
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<string, string> = {
|
||||
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<string, string> = {
|
||||
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) {
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user