openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
417 lines (416 loc) • 19.8 kB
JavaScript
import { a as writeRuntimeJson } from "./runtime-CF2WjnNZ.js";
import { n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js";
import { t as formatCliCommand } from "./command-format-C7YfyMTd.js";
import "./config-Cs0XXL3x.js";
import { n as stylePromptMessage, r as stylePromptTitle, t as stylePromptHint } from "./prompt-style-BQVvtDcR.js";
import { r as withProgress } from "./progress-DSeqxbrQ.js";
import { r as promptYesNo } from "./prompt-BvBir9Yc.js";
import { g as redactMigrationPlan } from "./migration-q6QzXKht.js";
import { r as resolvePluginMigrationProviders, t as ensureStandaloneMigrationProviderRegistryLoaded } from "./migration-provider-runtime-oYLrxrg5.js";
import { S as applyMigrationItemSelection, _ as reconcileInteractiveMigrationShortcutValues, a as applyMigrationPluginSelection, b as createMigrationPlan, c as applyMigrationSkillSelection, d as formatMigrationSkillSelectionHint, f as formatMigrationSkillSelectionLabel, g as reconcileInteractiveMigrationEnterValues, h as getSelectableMigrationSkillItems, i as MIGRATION_SELECTION_TOGGLE_ALL_ON, l as formatMigrationPluginSelectionHint, m as getSelectableMigrationPluginItems, n as MIGRATION_SELECTION_ACCEPT, o as applyMigrationSelectedPluginItemIds, p as getDefaultMigrationSelectionValues, r as MIGRATION_SELECTION_TOGGLE_ALL_OFF, s as applyMigrationSelectedSkillItemIds, t as runMigrationApply, u as formatMigrationPluginSelectionLabel, v as reconcileInteractiveMigrationSkillToggleValues, x as resolveMigrationProvider, y as resolveInteractiveMigrationSelection } from "./apply-BrRAbEDO.js";
import { i as formatMigrationPreview } from "./output-Dz-6-IZf.js";
import { styleText } from "node:util";
import { S_BAR, S_BAR_END, S_CHECKBOX_ACTIVE, S_CHECKBOX_INACTIVE, S_CHECKBOX_SELECTED, cancel, confirm, isCancel, limitOptions, log, symbol, symbolBar } from "@clack/prompts";
import { MultiSelectPrompt, settings as settings$1, wrapTextWithPrefix } from "@clack/core";
//#region src/commands/migrate/skill-selection-prompt.ts
/** Custom Clack multi-select prompt for Codex migration skill/plugin choices. */
function formatOption(option, state) {
const label = option.label ?? option.value;
const withHint = option.hint ? `${label} ${styleText("dim", `(${option.hint})`)}` : label;
switch (state) {
case "active": return `${styleText("cyan", S_CHECKBOX_ACTIVE)} ${withHint}`;
case "active-selected": return `${styleText("green", S_CHECKBOX_SELECTED)} ${withHint}`;
case "cancelled": return styleText(["strikethrough", "dim"], label);
case "disabled": return `${styleText("gray", S_CHECKBOX_INACTIVE)} ${styleText(["strikethrough", "gray"], label)}${option.hint ? ` ${styleText("dim", `(${option.hint})`)}` : ""}`;
case "selected": return `${styleText("green", S_CHECKBOX_SELECTED)} ${styleText("dim", withHint)}`;
case "submitted": return styleText("dim", label);
case "inactive": return `${styleText("dim", S_CHECKBOX_INACTIVE)} ${styleText("dim", withHint)}`;
}
return withHint;
}
/** Prompts for migration selection values and reconciles all/none/recommended shortcuts. */
function promptMigrationSkillSelectionValues(opts) {
const prompt = new MultiSelectPrompt({
options: opts.options,
signal: opts.signal,
input: opts.input,
output: opts.output,
initialValues: opts.initialValues,
cursorAt: opts.cursorAt,
render() {
const withGuide = opts.withGuide ?? settings$1.withGuide;
const message = wrapTextWithPrefix(opts.output, opts.message, withGuide ? `${symbolBar(this.state)} ` : "", `${symbol(this.state)} `);
const header = `${withGuide ? `${styleText("gray", S_BAR)}\n` : ""}${message}\n`;
const value = this.value ?? [];
const optionState = (option, active) => {
if (option.disabled) return formatOption(option, "disabled");
const selected = value.includes(option.value);
if (active && selected) return formatOption(option, "active-selected");
if (selected) return formatOption(option, "selected");
return formatOption(option, active ? "active" : "inactive");
};
switch (this.state) {
case "submit": {
const label = this.options.filter((option) => value.includes(option.value)).map((option) => formatOption(option, "submitted")).join(styleText("dim", ", ")) || styleText("dim", "none");
return `${header}${wrapTextWithPrefix(opts.output, label, withGuide ? `${styleText("gray", S_BAR)} ` : "")}`;
}
case "cancel": {
const selected = this.options.filter((option) => value.includes(option.value)).map((option) => formatOption(option, "cancelled")).join(styleText("dim", ", "));
if (selected.trim() === "") return `${header}${styleText("gray", S_BAR)}`;
return `${header}${wrapTextWithPrefix(opts.output, selected, withGuide ? `${styleText("gray", S_BAR)} ` : "")}${withGuide ? `\n${styleText("gray", S_BAR)}` : ""}`;
}
case "error": {
const prefix = withGuide ? `${styleText("yellow", S_BAR)} ` : "";
return `${header}${prefix}${limitOptions({
output: opts.output,
options: this.options,
cursor: this.cursor,
maxItems: opts.maxItems,
columnPadding: prefix.length,
rowPadding: header.split("\n").length + this.error.split("\n").length + 1,
style: optionState
}).join(`\n${prefix}`)}\n${this.error.split("\n").map((line, index) => index === 0 ? `${withGuide ? `${styleText("yellow", S_BAR_END)} ` : ""}${styleText("yellow", line)}` : ` ${line}`).join("\n")}\n`;
}
default: {
const prefix = withGuide ? `${styleText("cyan", S_BAR)} ` : "";
return `${header}${prefix}${limitOptions({
output: opts.output,
options: this.options,
cursor: this.cursor,
maxItems: opts.maxItems,
columnPadding: prefix.length,
rowPadding: header.split("\n").length + (withGuide ? 2 : 1),
style: optionState
}).join(`\n${prefix}`)}\n${withGuide ? styleText("cyan", S_BAR_END) : ""}\n`;
}
}
}
});
let lastSelectedValues = [...prompt.value ?? []];
let lastSpaceDeselectedValue;
prompt.on("cursor", (key) => {
if (key !== "space") {
lastSpaceDeselectedValue = void 0;
return;
}
const activatedValue = prompt.options[prompt.cursor]?.value;
if (activatedValue === "__openclaw_migrate_accept_recommended__") {
prompt.value = [...opts.initialValues ?? []];
lastSpaceDeselectedValue = void 0;
lastSelectedValues = [...prompt.value ?? []];
return;
}
const previousValues = lastSelectedValues;
const selectedValuesAfterClack = prompt.value ?? [];
prompt.value = reconcileInteractiveMigrationSkillToggleValues(selectedValuesAfterClack, activatedValue, opts.selectableValues);
lastSpaceDeselectedValue = activatedValue !== void 0 && opts.selectableValues.includes(activatedValue) && previousValues.includes(activatedValue) && !(prompt.value ?? []).includes(activatedValue) ? activatedValue : void 0;
lastSelectedValues = [...prompt.value ?? []];
});
prompt.on("key", (key, info) => {
if (info.name === "return") {
const activatedOption = prompt.options[prompt.cursor];
const activatedValue = activatedOption?.disabled ? void 0 : activatedOption?.value;
if (activatedValue === "__openclaw_migrate_accept_recommended__") {
prompt.value = [...opts.initialValues ?? []];
lastSpaceDeselectedValue = void 0;
lastSelectedValues = [...prompt.value ?? []];
return;
}
prompt.value = reconcileInteractiveMigrationEnterValues(prompt.value ?? [], activatedValue, opts.selectableValues, { preserveDeselectedActivatedValue: activatedValue !== void 0 && activatedValue === lastSpaceDeselectedValue && !(prompt.value ?? []).includes(activatedValue) });
lastSpaceDeselectedValue = void 0;
lastSelectedValues = [...prompt.value ?? []];
return;
}
if (key !== "a" && key !== "i") return;
prompt.value = reconcileInteractiveMigrationShortcutValues(lastSelectedValues, prompt.value ?? [], opts.selectableValues, key);
lastSpaceDeselectedValue = void 0;
lastSelectedValues = [...prompt.value ?? []];
});
return prompt.prompt();
}
//#endregion
//#region src/commands/migrate.ts
/** CLI command orchestration for migration list, plan, and apply flows. */
function selectMigrationItems(plan, opts) {
return applyMigrationItemSelection(applyMigrationPluginSelection(applyMigrationSkillSelection(plan, opts.skills), opts.plugins), opts.itemIds);
}
function hasAuthCredentialCandidate(plan) {
return plan.items.some((item) => item.kind === "auth" || item.kind === "secret" || item.sensitive === true);
}
function hasPlannedAuthCredentialItem(plan) {
return plan.items.some((item) => item.status === "planned" && (item.kind === "auth" || item.kind === "secret" || item.sensitive === true));
}
function resolveDefaultIncludeSecrets(opts) {
if (opts.authCredentials === false) return {
...opts,
includeSecrets: false
};
if (opts.includeSecrets !== void 0) return opts;
return opts;
}
function shouldPromptForAuthCredentials(opts) {
return opts.includeSecrets === void 0 && opts.authCredentials !== false && !opts.yes && !opts.json && process.stdin.isTTY;
}
async function createMigrationPlanWithProgress(runtime, opts) {
const createPlan = async () => await createMigrationPlan(runtime, opts);
if (opts.json) return selectMigrationItems(await createPlan(), opts);
return selectMigrationItems(await withProgress({
label: `Scanning ${opts.provider} migration…`,
indeterminate: true
}, async (progress) => {
progress.setLabel("Reading migration source…");
const planLocal = await createPlan();
progress.tick();
return planLocal;
}), opts);
}
async function createInteractiveMigrationPlanWithAuthPrompt(runtime, opts) {
if (!shouldPromptForAuthCredentials(opts)) return await migratePlanCommand(runtime, resolveDefaultIncludeSecrets(opts));
const initialPlan = await migratePlanCommand(runtime, {
...opts,
includeSecrets: false,
suppressPlanLog: true
});
if (!hasAuthCredentialCandidate(initialPlan)) {
if (!opts.suppressPlanLog) log.message(formatMigrationPreview(initialPlan).join("\n"));
return initialPlan;
}
const includeSecrets = await confirm({
message: stylePromptMessage("Do you want to migrate your auth credentials as well?"),
initialValue: true
});
if (isCancel(includeSecrets)) {
cancel(stylePromptTitle("Migration cancelled.") ?? "Migration cancelled.");
runtime.exit(0);
throw new Error("unreachable");
}
const finalPlan = includeSecrets ? await migratePlanCommand(runtime, {
...opts,
includeSecrets: true,
suppressPlanLog: true
}) : initialPlan;
if (!opts.suppressPlanLog) log.message(formatMigrationPreview(finalPlan).join("\n"));
return finalPlan;
}
function assertVerifyPluginAppsProvider(providerId, opts) {
if (opts.verifyPluginApps && providerId !== "codex") throw new Error("--verify-plugin-apps is only supported for Codex migrations.");
}
async function promptCodexMigrationSelection(runtime, plan, opts, kind) {
if (plan.providerId !== "codex" || opts.yes || opts.json || opts[kind] !== void 0 || !process.stdin.isTTY) return plan;
const skillSelection = kind === "skills";
const items = skillSelection ? getSelectableMigrationSkillItems(plan) : getSelectableMigrationPluginItems(plan);
if (items.length === 0) return plan;
const selected = await promptMigrationSkillSelectionValues({
message: stylePromptMessage(skillSelection ? "Select Codex skills to migrate into this agent" : "Select native Codex plugins to activate in this agent"),
options: [
{
value: MIGRATION_SELECTION_ACCEPT,
label: "Accept recommended",
hint: skillSelection ? "Migrate every recommended skill" : "Migrate every recommended plugin"
},
...items.map((item) => {
const hint = skillSelection ? formatMigrationSkillSelectionHint(item) : formatMigrationPluginSelectionHint(item);
return {
value: item.id,
label: skillSelection ? formatMigrationSkillSelectionLabel(item) : formatMigrationPluginSelectionLabel(item),
hint: hint === void 0 ? void 0 : stylePromptHint(hint)
};
}),
{
value: MIGRATION_SELECTION_TOGGLE_ALL_ON,
label: "Toggle all on"
},
{
value: MIGRATION_SELECTION_TOGGLE_ALL_OFF,
label: "Toggle all off"
}
],
initialValues: getDefaultMigrationSelectionValues(items),
selectableValues: items.map((item) => item.id),
cursorAt: MIGRATION_SELECTION_ACCEPT
});
if (isCancel(selected)) {
cancel(stylePromptTitle("Migration cancelled.") ?? "Migration cancelled.");
runtime.log("Migration cancelled.");
return null;
}
const selection = resolveInteractiveMigrationSelection(items, selected ?? []);
const selectedPlan = skillSelection ? applyMigrationSelectedSkillItemIds(plan, selection.selectedItemIds) : applyMigrationSelectedPluginItemIds(plan, selection.selectedItemIds);
const purpose = skillSelection ? "Codex skills for migration" : "native Codex plugins for activation";
runtime.log(`Selected ${selection.selectedItemIds.size} of ${items.length} ${purpose}.`);
return selectedPlan;
}
async function promptCodexMigrationSelections(runtime, plan, opts) {
const skillSelectedPlan = await promptCodexMigrationSelection(runtime, plan, opts, "skills");
if (!skillSelectedPlan) return null;
return await promptCodexMigrationSelection(runtime, skillSelectedPlan, opts, "plugins");
}
function hasSelectedCodexMigrationWork(plan) {
return plan.items.some((item) => item.status === "planned" && (item.kind === "auth" || item.kind === "secret" || item.kind === "skill" && item.action === "copy" || item.kind === "plugin" && item.action === "install"));
}
function shouldSkipCodexApplyAfterInteractiveSelection(plan) {
return plan.providerId === "codex" && !hasSelectedCodexMigrationWork(plan);
}
function hasCodexSubscriptionRequiredPlugin(plan) {
if (plan.providerId !== "codex") return false;
return plan.items.some((item) => item.reason === "codex_subscription_required");
}
function readCodexSubscriptionWarning(plan) {
return plan.warnings?.find((warning) => warning.includes("Codex app-backed plugin migration requires"));
}
function logNoCodexSelection(runtime, plan) {
if (hasCodexSubscriptionRequiredPlugin(plan)) {
const warning = readCodexSubscriptionWarning(plan);
if (warning) runtime.log(warning);
runtime.log("No Codex skills selected; native Codex plugins are not eligible for migration in this run.");
return;
}
runtime.log("No Codex skills or native Codex plugins selected for migration.");
}
/** Lists available migration providers as JSON or terse terminal rows. */
async function migrateListCommand(runtime, opts = {}) {
const cfg = getRuntimeConfig();
ensureStandaloneMigrationProviderRegistryLoaded({ cfg });
const providers = resolvePluginMigrationProviders({ cfg }).map((provider) => ({
id: provider.id,
label: provider.label,
description: provider.description
}));
if (opts.json) {
writeRuntimeJson(runtime, { providers });
return;
}
if (providers.length === 0) {
runtime.log(`No migration providers found. Run ${formatCliCommand("openclaw plugins list")} to verify provider plugins are installed and enabled.`);
return;
}
runtime.log(providers.map((provider) => provider.description ? `${provider.id}\t${provider.label} - ${provider.description}` : `${provider.id}\t${provider.label}`).join("\n"));
}
/** Creates and prints a migration plan without applying it. */
async function migratePlanCommand(runtime, opts) {
const providerId = opts.provider?.trim();
if (!providerId) throw new Error(`Migration provider is required. Run ${formatCliCommand("openclaw migrate list")} to choose one.`);
const resolvedOpts = resolveDefaultIncludeSecrets(opts);
assertVerifyPluginAppsProvider(providerId, resolvedOpts);
const plan = await createMigrationPlanWithProgress(runtime, {
...resolvedOpts,
provider: providerId
});
if (resolvedOpts.json) writeRuntimeJson(runtime, redactMigrationPlan(plan));
else if (resolvedOpts.suppressPlanLog !== true) log.message(formatMigrationPreview(plan).join("\n"));
return plan;
}
async function migrateApplyCommand(runtime, opts) {
const providerId = opts.provider?.trim();
if (!providerId) throw new Error(`Migration provider is required. Run ${formatCliCommand("openclaw migrate list")} to choose one.`);
assertVerifyPluginAppsProvider(providerId, opts);
if (opts.noBackup && !opts.force) throw new Error("--no-backup requires --force because it skips the automatic rollback copy.");
if (!opts.yes && !process.stdin.isTTY) throw new Error(`openclaw migrate apply requires --yes in non-interactive mode. Preview first with ${formatCliCommand(`openclaw migrate plan '${providerId.replaceAll("'", "'\\''")}'`)}.`);
const provider = resolveMigrationProvider(providerId, opts.configOverride);
if (!opts.yes) {
const plan = await createInteractiveMigrationPlanWithAuthPrompt(runtime, {
...opts,
provider: providerId,
json: opts.json
});
if (opts.json) return plan;
const selectedPlan = await promptCodexMigrationSelections(runtime, plan, opts);
if (!selectedPlan) return plan;
if (shouldSkipCodexApplyAfterInteractiveSelection(selectedPlan)) {
logNoCodexSelection(runtime, selectedPlan);
return selectedPlan;
}
if (!await promptYesNo("Apply this migration now?", false)) {
runtime.log("Migration cancelled.");
return selectedPlan;
}
return await runMigrationApply({
runtime,
opts: {
...opts,
provider: providerId,
yes: true,
includeSecrets: opts.includeSecrets ?? hasPlannedAuthCredentialItem(selectedPlan),
preflightPlan: selectedPlan
},
providerId,
provider
});
}
return await runMigrationApply({
runtime,
opts: resolveDefaultIncludeSecrets(opts),
providerId,
provider
});
}
/** Default migrate command: list providers, plan, dry-run, or apply based on flags. */
async function migrateDefaultCommand(runtime, opts) {
const providerId = opts.provider?.trim();
if (!providerId) {
await migrateListCommand(runtime, { json: opts.json });
return {
providerId: "list",
source: "",
summary: {
total: 0,
planned: 0,
migrated: 0,
skipped: 0,
conflicts: 0,
errors: 0,
sensitive: 0
},
items: []
};
}
assertVerifyPluginAppsProvider(providerId, opts);
const resolvedOpts = resolveDefaultIncludeSecrets(opts);
const plan = opts.json && opts.yes && !opts.dryRun ? selectMigrationItems(await createMigrationPlan(runtime, {
...resolvedOpts,
provider: providerId
}), resolvedOpts) : !opts.yes && process.stdin.isTTY ? await createInteractiveMigrationPlanWithAuthPrompt(runtime, {
...opts,
provider: providerId,
json: opts.json && (opts.dryRun || !opts.yes)
}) : await migratePlanCommand(runtime, {
...resolvedOpts,
provider: providerId,
json: opts.json && (opts.dryRun || !opts.yes)
});
if (opts.dryRun) return plan;
if (opts.json && !opts.yes) return plan;
if (!opts.yes) {
if (!process.stdin.isTTY) {
runtime.log("Re-run with --yes to apply this migration non-interactively.");
return plan;
}
const selectedPlan = await promptCodexMigrationSelections(runtime, plan, opts);
if (!selectedPlan) return plan;
if (shouldSkipCodexApplyAfterInteractiveSelection(selectedPlan)) {
logNoCodexSelection(runtime, selectedPlan);
return selectedPlan;
}
if (!await promptYesNo("Apply this migration now?", false)) {
runtime.log("Migration cancelled.");
return selectedPlan;
}
return await migrateApplyCommand(runtime, {
...opts,
provider: providerId,
yes: true,
includeSecrets: opts.includeSecrets ?? hasPlannedAuthCredentialItem(selectedPlan),
json: opts.json,
preflightPlan: selectedPlan
});
}
return await migrateApplyCommand(runtime, {
...resolvedOpts,
provider: providerId,
yes: true,
json: opts.json,
preflightPlan: plan
});
}
//#endregion
export { migratePlanCommand as i, migrateDefaultCommand as n, migrateListCommand as r, migrateApplyCommand as t };