mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 12:34:47 +00:00
feat(migrations): add plugin-owned Hermes import
* feat: add migration providers * feat: offer Hermes migration during onboarding * feat(hermes): map imported config surfaces * feat(onboard): require fresh migration imports * docs(cli): clarify Hermes import coverage * chore(migrations): rename Hermes importer package * chore(migrations): rewire Hermes importer id * fix(migrations): redact migration JSON details * fix(hermes): use provider runtime for config imports * test(hermes): cover missing source planning --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -81,6 +81,11 @@ const coreEntrySpecs: readonly CommandGroupDescriptorSpec<
|
||||
loadModule: () => import("./register.backup.js"),
|
||||
exportName: "registerBackupCommand",
|
||||
},
|
||||
{
|
||||
commandNames: ["migrate"],
|
||||
loadModule: () => import("./register.migrate.js"),
|
||||
exportName: "registerMigrateCommand",
|
||||
},
|
||||
{
|
||||
commandNames: ["doctor", "dashboard", "reset", "uninstall"],
|
||||
loadModule: () => import("./register.maintenance.js"),
|
||||
|
||||
@@ -35,6 +35,11 @@ const coreCliCommandCatalog = defineCommandDescriptorCatalog([
|
||||
description: "Create and verify local backup archives for OpenClaw state",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
{
|
||||
name: "migrate",
|
||||
description: "Import state from another agent system",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
{
|
||||
name: "doctor",
|
||||
description: "Health checks + quick fixes for the gateway and channels",
|
||||
|
||||
117
src/cli/program/register.migrate.ts
Normal file
117
src/cli/program/register.migrate.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import type { Command } from "commander";
|
||||
import {
|
||||
migrateApplyCommand,
|
||||
migrateDefaultCommand,
|
||||
migrateListCommand,
|
||||
migratePlanCommand,
|
||||
} from "../../commands/migrate.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
import { runCommandWithRuntime } from "../cli-utils.js";
|
||||
import { formatHelpExamples } from "../help-format.js";
|
||||
|
||||
function addMigrationOptions(command: Command): Command {
|
||||
return command
|
||||
.option("--from <path>", "Source directory to migrate from")
|
||||
.option("--include-secrets", "Import supported credentials and secrets", false)
|
||||
.option("--overwrite", "Overwrite conflicting target files after item-level backups", false)
|
||||
.option("--json", "Output JSON", false);
|
||||
}
|
||||
|
||||
export function registerMigrateCommand(program: Command) {
|
||||
const migrate = program
|
||||
.command("migrate")
|
||||
.description("Import state from another agent system")
|
||||
.argument("[provider]", "Migration provider id, for example hermes")
|
||||
.option("--from <path>", "Source directory to migrate from")
|
||||
.option("--include-secrets", "Import supported credentials and secrets", false)
|
||||
.option("--overwrite", "Overwrite conflicting target files after item-level backups", false)
|
||||
.option("--dry-run", "Preview only; do not apply changes", false)
|
||||
.option("--yes", "Apply without prompting after preview", false)
|
||||
.option("--backup-output <path>", "Pre-migration backup archive path or directory")
|
||||
.option("--no-backup", "Skip the pre-migration OpenClaw backup")
|
||||
.option("--force", "Allow dangerous options such as --no-backup", false)
|
||||
.option("--json", "Output JSON", false)
|
||||
.addHelpText(
|
||||
"after",
|
||||
() =>
|
||||
`\n${theme.heading("Examples:")}\n${formatHelpExamples([
|
||||
["openclaw migrate list", "Show available migration providers."],
|
||||
["openclaw migrate hermes", "Preview Hermes migration, then prompt before applying."],
|
||||
["openclaw migrate hermes --dry-run", "Preview Hermes migration only."],
|
||||
[
|
||||
"openclaw migrate apply hermes --yes",
|
||||
"Apply Hermes migration non-interactively after writing a verified backup.",
|
||||
],
|
||||
[
|
||||
"openclaw migrate apply hermes --include-secrets --yes",
|
||||
"Include supported credentials in the migration.",
|
||||
],
|
||||
])}`,
|
||||
)
|
||||
.action(async (provider, opts) => {
|
||||
await runCommandWithRuntime(defaultRuntime, async () => {
|
||||
await migrateDefaultCommand(defaultRuntime, {
|
||||
provider: provider as string | undefined,
|
||||
source: opts.from as string | undefined,
|
||||
includeSecrets: Boolean(opts.includeSecrets),
|
||||
overwrite: Boolean(opts.overwrite),
|
||||
dryRun: Boolean(opts.dryRun),
|
||||
yes: Boolean(opts.yes),
|
||||
backupOutput: opts.backupOutput as string | undefined,
|
||||
noBackup: opts.backup === false,
|
||||
force: Boolean(opts.force),
|
||||
json: Boolean(opts.json),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
migrate
|
||||
.command("list")
|
||||
.description("List migration providers")
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runCommandWithRuntime(defaultRuntime, async () => {
|
||||
await migrateListCommand(defaultRuntime, { json: Boolean(opts.json) });
|
||||
});
|
||||
});
|
||||
|
||||
addMigrationOptions(
|
||||
migrate
|
||||
.command("plan <provider>")
|
||||
.description("Preview a migration without changing OpenClaw state"),
|
||||
).action(async (provider, opts) => {
|
||||
await runCommandWithRuntime(defaultRuntime, async () => {
|
||||
await migratePlanCommand(defaultRuntime, {
|
||||
provider: provider as string,
|
||||
source: opts.from as string | undefined,
|
||||
includeSecrets: Boolean(opts.includeSecrets),
|
||||
overwrite: Boolean(opts.overwrite),
|
||||
json: Boolean(opts.json),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
addMigrationOptions(
|
||||
migrate.command("apply <provider>").description("Apply a migration after a verified backup"),
|
||||
)
|
||||
.option("--yes", "Apply without prompting", false)
|
||||
.option("--backup-output <path>", "Pre-migration backup archive path or directory")
|
||||
.option("--no-backup", "Skip the pre-migration OpenClaw backup")
|
||||
.option("--force", "Allow dangerous options such as --no-backup", false)
|
||||
.action(async (provider, opts) => {
|
||||
await runCommandWithRuntime(defaultRuntime, async () => {
|
||||
await migrateApplyCommand(defaultRuntime, {
|
||||
provider: provider as string,
|
||||
source: opts.from as string | undefined,
|
||||
includeSecrets: Boolean(opts.includeSecrets),
|
||||
overwrite: Boolean(opts.overwrite),
|
||||
yes: Boolean(opts.yes),
|
||||
backupOutput: opts.backupOutput as string | undefined,
|
||||
noBackup: opts.backup === false,
|
||||
force: Boolean(opts.force),
|
||||
json: Boolean(opts.json),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -181,6 +181,28 @@ describe("registerOnboardCommand", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards onboarding migration flags", async () => {
|
||||
await runCli([
|
||||
"onboard",
|
||||
"--flow",
|
||||
"import",
|
||||
"--import-from",
|
||||
"hermes",
|
||||
"--import-source",
|
||||
"/tmp/hermes",
|
||||
"--import-secrets",
|
||||
]);
|
||||
expect(setupWizardCommandMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
flow: "import",
|
||||
importFrom: "hermes",
|
||||
importSource: "/tmp/hermes",
|
||||
importSecrets: true,
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
});
|
||||
|
||||
it("reports errors via runtime on setup wizard command failures", async () => {
|
||||
setupWizardCommandMock.mockRejectedValueOnce(new Error("setup failed"));
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ export function registerOnboardCommand(program: Command) {
|
||||
"Acknowledge that agents are powerful and full system access is risky (required for --non-interactive)",
|
||||
false,
|
||||
)
|
||||
.option("--flow <flow>", "Onboard flow: quickstart|advanced|manual")
|
||||
.option("--flow <flow>", "Onboard flow: quickstart|advanced|manual|import")
|
||||
.option("--mode <mode>", "Onboard mode: local|remote")
|
||||
.option("--auth-choice <choice>", `Auth: ${AUTH_CHOICE_HELP}`)
|
||||
.option(
|
||||
@@ -168,6 +168,9 @@ export function registerOnboardCommand(program: Command) {
|
||||
.option("--skip-health", "Skip health check")
|
||||
.option("--skip-ui", "Skip Control UI/TUI prompts")
|
||||
.option("--node-manager <name>", "Node manager for skills: npm|pnpm|bun")
|
||||
.option("--import-from <provider>", "Migration provider to run during onboarding")
|
||||
.option("--import-source <path>", "Source agent home for --import-from")
|
||||
.option("--import-secrets", "Import supported secrets during onboarding migration", false)
|
||||
.option("--json", "Output JSON summary", false);
|
||||
|
||||
command.action(async (opts, commandRuntime) => {
|
||||
@@ -195,7 +198,7 @@ export function registerOnboardCommand(program: Command) {
|
||||
workspace: opts.workspace as string | undefined,
|
||||
nonInteractive: Boolean(opts.nonInteractive),
|
||||
acceptRisk: Boolean(opts.acceptRisk),
|
||||
flow: opts.flow as "quickstart" | "advanced" | "manual" | undefined,
|
||||
flow: opts.flow as "quickstart" | "advanced" | "manual" | "import" | undefined,
|
||||
mode: opts.mode as "local" | "remote" | undefined,
|
||||
authChoice: opts.authChoice as AuthChoice | undefined,
|
||||
tokenProvider: opts.tokenProvider as string | undefined,
|
||||
@@ -235,6 +238,9 @@ export function registerOnboardCommand(program: Command) {
|
||||
skipHealth: Boolean(opts.skipHealth),
|
||||
skipUi: Boolean(opts.skipUi),
|
||||
nodeManager: opts.nodeManager as NodeManagerChoice | undefined,
|
||||
importFrom: opts.importFrom as string | undefined,
|
||||
importSource: opts.importSource as string | undefined,
|
||||
importSecrets: Boolean(opts.importSecrets),
|
||||
json: Boolean(opts.json),
|
||||
},
|
||||
defaultRuntime,
|
||||
|
||||
@@ -79,6 +79,27 @@ describe("registerSetupCommand", () => {
|
||||
expect(setupCommandMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("runs setup wizard command for migration import flags", async () => {
|
||||
await runCli([
|
||||
"setup",
|
||||
"--import-from",
|
||||
"hermes",
|
||||
"--import-source",
|
||||
"/tmp/hermes",
|
||||
"--import-secrets",
|
||||
]);
|
||||
|
||||
expect(setupWizardCommandMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
importFrom: "hermes",
|
||||
importSource: "/tmp/hermes",
|
||||
importSecrets: true,
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
expect(setupCommandMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports setup errors through runtime", async () => {
|
||||
setupCommandMock.mockRejectedValueOnce(new Error("setup failed"));
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ export function registerSetupCommand(program: Command) {
|
||||
.option("--wizard", "Run interactive onboarding", false)
|
||||
.option("--non-interactive", "Run onboarding without prompts", false)
|
||||
.option("--mode <mode>", "Onboard mode: local|remote")
|
||||
.option("--import-from <provider>", "Migration provider to run during onboarding")
|
||||
.option("--import-source <path>", "Source agent home for --import-from")
|
||||
.option("--import-secrets", "Import supported secrets during onboarding migration", false)
|
||||
.option("--remote-url <url>", "Remote Gateway WebSocket URL")
|
||||
.option("--remote-token <token>", "Remote Gateway token (optional)")
|
||||
.action(async (opts, command) => {
|
||||
@@ -31,6 +34,9 @@ export function registerSetupCommand(program: Command) {
|
||||
"wizard",
|
||||
"nonInteractive",
|
||||
"mode",
|
||||
"importFrom",
|
||||
"importSource",
|
||||
"importSecrets",
|
||||
"remoteUrl",
|
||||
"remoteToken",
|
||||
]);
|
||||
@@ -40,6 +46,9 @@ export function registerSetupCommand(program: Command) {
|
||||
workspace: opts.workspace as string | undefined,
|
||||
nonInteractive: Boolean(opts.nonInteractive),
|
||||
mode: opts.mode as "local" | "remote" | undefined,
|
||||
importFrom: opts.importFrom as string | undefined,
|
||||
importSource: opts.importSource as string | undefined,
|
||||
importSecrets: Boolean(opts.importSecrets),
|
||||
remoteUrl: opts.remoteUrl as string | undefined,
|
||||
remoteToken: opts.remoteToken as string | undefined,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user