mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-25 13:01:15 +00:00
* fix(wizard): defer config commit until after migration apply succeeds Onboarding migration import commits the config file to disk before the migration provider's apply() runs. If apply() legitimately reports an error on any item, runSetupMigrationImport() throws, but the config is already committed with wizard metadata stamped on it. On the very next onboarding attempt, the freshness gate sees that stamped config and refuses to run the import, telling the user a fresh setup is required. Move the commitConfigFile call to after assertApplySucceeded(), so a failed apply never leaves wizard metadata on disk that blocks the next retry behind the fresh-setup gate. The in-memory config still has wizard metadata applied before the apply context is constructed (so the apply function can reference a concrete target), but the disk write only happens after apply succeeds. Fixes #103262 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(lint): remove useless assignment in migration config commit The `targetConfig = await params.commitConfigFile(targetConfig)` line assigns to `targetConfig` but the value is never read afterwards. Remove the assignment, keeping the side-effect call. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(wizard): preserve config patches from provider apply after commit Restore the targetConfig assignment from commitConfigFile so that config patches written by the provider during apply (e.g. auth profile selections) are carried forward. Without this, the pre-apply snapshot silently replaces the post-apply config, losing provider-authored patches. Fixes #103262 * fix: remove useless assignment to satisfy lint commitConfigFile persists the config to disk; the returned config with patches is already on disk, making the in-memory assignment unnecessary. * fix(wizard): make Hermes import retries recoverable * chore: keep release notes release-owned * fix(wizard): keep retry identity stable across acknowledgement * fix(wizard): satisfy Hermes retry quality gates * test(wizard): use managed migration temp dirs --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
608 lines
20 KiB
TypeScript
608 lines
20 KiB
TypeScript
import type { OnboardOptions } from "../commands/onboard-types.js";
|
|
import {
|
|
ensureOnboardingPluginInstalled,
|
|
type OnboardingPluginInstallEntry,
|
|
} from "../commands/onboarding-plugin-install.js";
|
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import { formatErrorMessage } from "../infra/errors.js";
|
|
import {
|
|
listAvailableManifestContractPlugins,
|
|
loadManifestContractSnapshot,
|
|
} from "../plugins/manifest-contract-eligibility.js";
|
|
import {
|
|
getOfficialExternalPluginCatalogManifest,
|
|
listOfficialExternalPluginCatalogEntries,
|
|
resolveOfficialExternalPluginId,
|
|
resolveOfficialExternalPluginInstall,
|
|
resolveOfficialExternalPluginLabel,
|
|
} from "../plugins/official-external-plugin-catalog.js";
|
|
import type {
|
|
MigrationPlan,
|
|
MigrationProviderContext,
|
|
MigrationProviderPlugin,
|
|
} from "../plugins/types.js";
|
|
import type { RuntimeEnv } from "../runtime.js";
|
|
import { createLazyRuntimeModule } from "../shared/lazy-runtime.js";
|
|
import { resolveUserPath } from "../utils.js";
|
|
import { t } from "./i18n/index.js";
|
|
import { WizardCancelledError, type WizardPrompter } from "./prompts.js";
|
|
import {
|
|
createSetupMigrationAttempt,
|
|
prepareSetupMigrationRetryPlan,
|
|
resolveSetupMigrationRecovery,
|
|
runSetupMigrationAttempt,
|
|
setupMigrationAttemptMatchesSource,
|
|
setupMigrationProviderSupportsRecovery,
|
|
} from "./setup.migration-recovery.js";
|
|
import {
|
|
assertFreshSetupMigrationTarget,
|
|
buildSetupMigrationPlanSourceSnapshot,
|
|
buildSetupMigrationTargetSnapshot,
|
|
inspectSetupMigrationFreshness,
|
|
preserveSetupMigrationSecurityAcknowledgement,
|
|
prepareSetupMigrationAttemptBoundary,
|
|
withSetupMigrationTargetLock,
|
|
} from "./setup.migration-snapshot.js";
|
|
|
|
// Onboarding migration import: detect, preview, back up, and apply into a fresh setup.
|
|
type SetupMigrationDetection = {
|
|
providerId: string;
|
|
label: string;
|
|
source?: string;
|
|
message?: string;
|
|
};
|
|
type SetupMigrationOption = {
|
|
providerId: string;
|
|
label: string;
|
|
hint?: string;
|
|
};
|
|
type InstallableSetupMigrationProvider = {
|
|
providerId: string;
|
|
entry: OnboardingPluginInstallEntry;
|
|
description?: string;
|
|
};
|
|
type ManifestSetupMigrationProvider = {
|
|
providerId: string;
|
|
label: string;
|
|
description?: string;
|
|
};
|
|
const loadMigrationProviderRuntimeModule = createLazyRuntimeModule(
|
|
() => import("../plugins/migration-provider-runtime.js"),
|
|
);
|
|
|
|
const loadMigrationContextModule = createLazyRuntimeModule(
|
|
() => import("../commands/migrate/context.js"),
|
|
);
|
|
|
|
const loadConfigPathsModule = createLazyRuntimeModule(() => import("../config/paths.js"));
|
|
|
|
export async function detectSetupMigrationSources(params: {
|
|
config: OpenClawConfig;
|
|
runtime: RuntimeEnv;
|
|
}): Promise<SetupMigrationDetection[]> {
|
|
const [
|
|
{ ensureStandaloneMigrationProviderRegistryLoaded, resolvePluginMigrationProviders },
|
|
{ createMigrationLogger },
|
|
{ resolveStateDir },
|
|
] = await Promise.all([
|
|
loadMigrationProviderRuntimeModule(),
|
|
loadMigrationContextModule(),
|
|
loadConfigPathsModule(),
|
|
]);
|
|
ensureStandaloneMigrationProviderRegistryLoaded({ cfg: params.config });
|
|
const stateDir = resolveStateDir();
|
|
const logger = createMigrationLogger(params.runtime);
|
|
const detections: SetupMigrationDetection[] = [];
|
|
for (const provider of resolvePluginMigrationProviders({ cfg: params.config })) {
|
|
if (!provider.detect) {
|
|
continue;
|
|
}
|
|
try {
|
|
const detection = await provider.detect({
|
|
config: params.config,
|
|
stateDir,
|
|
logger,
|
|
});
|
|
if (detection.found) {
|
|
detections.push({
|
|
providerId: provider.id,
|
|
label: detection.label ?? provider.label,
|
|
...(detection.source ? { source: detection.source } : {}),
|
|
...(detection.message ? { message: detection.message } : {}),
|
|
});
|
|
}
|
|
} catch (error) {
|
|
// Detection is advisory; one failing provider must not prevent onboarding
|
|
// from offering other migration sources.
|
|
logger.debug?.(
|
|
`Migration provider ${provider.id} detection failed: ${formatErrorMessage(error)}`,
|
|
);
|
|
}
|
|
}
|
|
return detections;
|
|
}
|
|
|
|
function resolveImportSourceDefault(params: {
|
|
providerId: string;
|
|
detections: readonly SetupMigrationDetection[];
|
|
}): string {
|
|
const detected = params.detections.find(
|
|
(detection) => detection.providerId === params.providerId,
|
|
);
|
|
if (detected?.source) {
|
|
return detected.source;
|
|
}
|
|
return params.providerId === "hermes" ? "~/.hermes" : "";
|
|
}
|
|
|
|
function resolveInstallableSetupMigrationProviders(): InstallableSetupMigrationProvider[] {
|
|
const providers: InstallableSetupMigrationProvider[] = [];
|
|
for (const catalogEntry of listOfficialExternalPluginCatalogEntries()) {
|
|
const manifest = getOfficialExternalPluginCatalogManifest(catalogEntry);
|
|
const pluginId = resolveOfficialExternalPluginId(catalogEntry);
|
|
const install = resolveOfficialExternalPluginInstall(catalogEntry);
|
|
if (!pluginId || !install) {
|
|
continue;
|
|
}
|
|
for (const providerId of manifest?.contracts?.migrationProviders ?? []) {
|
|
providers.push({
|
|
providerId,
|
|
entry: {
|
|
pluginId,
|
|
label: resolveOfficialExternalPluginLabel(catalogEntry),
|
|
install,
|
|
trustedSourceLinkedOfficialInstall: true,
|
|
},
|
|
...(catalogEntry.description ? { description: catalogEntry.description } : {}),
|
|
});
|
|
}
|
|
}
|
|
return providers;
|
|
}
|
|
|
|
function formatMigrationProviderId(providerId: string): string {
|
|
return providerId
|
|
.split(/[-_]+/)
|
|
.filter(Boolean)
|
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
.join(" ");
|
|
}
|
|
|
|
function resolveManifestMigrationProviderLabel(params: {
|
|
providerId: string;
|
|
pluginName?: string;
|
|
}): string {
|
|
const pluginName = params.pluginName?.trim().replace(/\s+Migration$/i, "");
|
|
return pluginName || formatMigrationProviderId(params.providerId) || params.providerId;
|
|
}
|
|
|
|
function resolveManifestSetupMigrationProviders(
|
|
baseConfig: OpenClawConfig,
|
|
): ManifestSetupMigrationProvider[] {
|
|
const snapshot = loadManifestContractSnapshot({ config: baseConfig });
|
|
return listAvailableManifestContractPlugins({
|
|
snapshot,
|
|
contract: "migrationProviders",
|
|
config: baseConfig,
|
|
}).flatMap((plugin) =>
|
|
(plugin.contracts?.migrationProviders ?? []).map((providerId) => {
|
|
const provider: ManifestSetupMigrationProvider = {
|
|
providerId,
|
|
label: resolveManifestMigrationProviderLabel({ providerId, pluginName: plugin.name }),
|
|
};
|
|
if (plugin.description) {
|
|
provider.description = plugin.description;
|
|
}
|
|
return provider;
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function listSetupMigrationOptions(params: {
|
|
baseConfig: OpenClawConfig;
|
|
detections: readonly SetupMigrationDetection[];
|
|
}): Promise<SetupMigrationOption[]> {
|
|
const { resolvePluginMigrationProviders } = await loadMigrationProviderRuntimeModule();
|
|
const providers = resolvePluginMigrationProviders({ cfg: params.baseConfig });
|
|
const options: SetupMigrationOption[] = [];
|
|
const providerIds = new Set<string>();
|
|
const addOption = (option: SetupMigrationOption) => {
|
|
if (providerIds.has(option.providerId)) {
|
|
return;
|
|
}
|
|
providerIds.add(option.providerId);
|
|
options.push(option);
|
|
};
|
|
|
|
for (const detection of params.detections) {
|
|
addOption({
|
|
providerId: detection.providerId,
|
|
label: detection.label,
|
|
...(detection.source || detection.message
|
|
? { hint: detection.source ?? detection.message }
|
|
: {}),
|
|
});
|
|
}
|
|
for (const provider of providers) {
|
|
addOption({
|
|
providerId: provider.id,
|
|
label: provider.label,
|
|
hint: provider.description ?? t("wizard.migration.sourcePathHint"),
|
|
});
|
|
}
|
|
for (const provider of resolveManifestSetupMigrationProviders(params.baseConfig)) {
|
|
addOption({
|
|
providerId: provider.providerId,
|
|
label: provider.label,
|
|
hint: provider.description ?? t("wizard.migration.sourcePathHint"),
|
|
});
|
|
}
|
|
for (const provider of resolveInstallableSetupMigrationProviders()) {
|
|
addOption({
|
|
providerId: provider.providerId,
|
|
label: provider.entry.label,
|
|
hint: provider.description ?? t("wizard.migration.sourcePathHint"),
|
|
});
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
async function selectSetupMigrationProvider(params: {
|
|
opts: OnboardOptions;
|
|
baseConfig: OpenClawConfig;
|
|
detections: readonly SetupMigrationDetection[];
|
|
prompter: WizardPrompter;
|
|
}): Promise<string> {
|
|
const options = await listSetupMigrationOptions({
|
|
baseConfig: params.baseConfig,
|
|
detections: params.detections,
|
|
});
|
|
if (options.length === 0) {
|
|
throw new Error("No migration providers found.");
|
|
}
|
|
const providerId =
|
|
params.opts.importFrom?.trim() ||
|
|
(await params.prompter.select({
|
|
message: t("wizard.migration.source"),
|
|
options: options.map((option) => ({
|
|
value: option.providerId,
|
|
label: option.label,
|
|
...(option.hint ? { hint: option.hint } : {}),
|
|
})),
|
|
initialValue: params.detections[0]?.providerId ?? options[0]?.providerId,
|
|
}));
|
|
if (!options.some((option) => option.providerId === providerId)) {
|
|
throw new Error(`Unknown migration provider "${providerId}".`);
|
|
}
|
|
return providerId;
|
|
}
|
|
|
|
async function resolveSetupMigrationProvider(params: {
|
|
providerId: string;
|
|
baseConfig: OpenClawConfig;
|
|
prompter: WizardPrompter;
|
|
runtime: RuntimeEnv;
|
|
workspaceDir: string;
|
|
}): Promise<{ provider: MigrationProviderPlugin; baseConfig: OpenClawConfig }> {
|
|
const { ensureStandaloneMigrationProviderRegistryLoaded, resolvePluginMigrationProvider } =
|
|
await loadMigrationProviderRuntimeModule();
|
|
ensureStandaloneMigrationProviderRegistryLoaded({
|
|
cfg: params.baseConfig,
|
|
providerId: params.providerId,
|
|
});
|
|
const existing = resolvePluginMigrationProvider({
|
|
providerId: params.providerId,
|
|
cfg: params.baseConfig,
|
|
});
|
|
if (existing) {
|
|
return { provider: existing, baseConfig: params.baseConfig };
|
|
}
|
|
const installable = resolveInstallableSetupMigrationProviders().find(
|
|
(provider) => provider.providerId === params.providerId,
|
|
);
|
|
if (!installable) {
|
|
throw new Error(`Unknown migration provider "${params.providerId}".`);
|
|
}
|
|
const result = await ensureOnboardingPluginInstalled({
|
|
cfg: params.baseConfig,
|
|
entry: installable.entry,
|
|
prompter: params.prompter,
|
|
runtime: params.runtime,
|
|
workspaceDir: params.workspaceDir,
|
|
promptInstall: false,
|
|
});
|
|
if (!result.installed) {
|
|
throw new Error(`Could not install migration provider "${params.providerId}".`);
|
|
}
|
|
ensureStandaloneMigrationProviderRegistryLoaded({
|
|
cfg: result.cfg,
|
|
providerId: params.providerId,
|
|
});
|
|
const provider = resolvePluginMigrationProvider({
|
|
providerId: params.providerId,
|
|
cfg: result.cfg,
|
|
});
|
|
if (!provider) {
|
|
throw new Error(`Installed plugin did not register migration provider "${params.providerId}".`);
|
|
}
|
|
return { provider, baseConfig: result.cfg };
|
|
}
|
|
|
|
function hasCredentialCandidate(plan: MigrationPlan): boolean {
|
|
return plan.items.some(
|
|
(item) => item.kind === "auth" || item.kind === "secret" || item.sensitive === true,
|
|
);
|
|
}
|
|
|
|
async function createSetupMigrationPlan(params: {
|
|
provider: MigrationProviderPlugin;
|
|
ctx: MigrationProviderContext;
|
|
importSecrets: boolean;
|
|
nonInteractive: boolean;
|
|
prompter: WizardPrompter;
|
|
}): Promise<{ ctx: MigrationProviderContext; plan: MigrationPlan }> {
|
|
let ctx = { ...params.ctx, includeSecrets: params.importSecrets };
|
|
let plan = await params.provider.plan(ctx);
|
|
if (params.nonInteractive || params.importSecrets || !hasCredentialCandidate(plan)) {
|
|
return { ctx, plan };
|
|
}
|
|
const includeSecrets = await params.prompter.confirm({
|
|
message: t("wizard.migration.includeCredentials"),
|
|
initialValue: true,
|
|
});
|
|
if (!includeSecrets) {
|
|
return { ctx, plan };
|
|
}
|
|
ctx = { ...ctx, includeSecrets: true };
|
|
plan = await params.provider.plan(ctx);
|
|
return { ctx, plan };
|
|
}
|
|
|
|
export async function runSetupMigrationImport(params: {
|
|
opts: OnboardOptions;
|
|
baseConfig: OpenClawConfig;
|
|
detections: readonly SetupMigrationDetection[];
|
|
prompter: WizardPrompter;
|
|
runtime: RuntimeEnv;
|
|
readConfigFile: () => Promise<OpenClawConfig>;
|
|
commitConfigFile: (config: OpenClawConfig) => Promise<OpenClawConfig>;
|
|
continueOnboarding?: boolean;
|
|
}): Promise<void> {
|
|
const [
|
|
{ applyLocalSetupWorkspaceConfig, applySkipBootstrapConfig },
|
|
{ createMigrationLogger, buildMigrationReportDir },
|
|
{ createPreMigrationBackup },
|
|
{ assertApplySucceeded, assertConflictFreePlan, formatMigrationPreview, formatMigrationResult },
|
|
{ resolveStateDir },
|
|
onboardHelpers,
|
|
] = await Promise.all([
|
|
import("../commands/onboard-config.js"),
|
|
loadMigrationContextModule(),
|
|
import("../commands/migrate/apply.js"),
|
|
import("../commands/migrate/output.js"),
|
|
loadConfigPathsModule(),
|
|
import("../commands/onboard-helpers.js"),
|
|
]);
|
|
const providerId = await selectSetupMigrationProvider({
|
|
opts: params.opts,
|
|
baseConfig: params.baseConfig,
|
|
detections: params.detections,
|
|
prompter: params.prompter,
|
|
});
|
|
const workspaceInput =
|
|
params.opts.workspace ??
|
|
(params.opts.nonInteractive
|
|
? (params.baseConfig.agents?.defaults?.workspace ?? onboardHelpers.DEFAULT_WORKSPACE)
|
|
: await params.prompter.text({
|
|
message: t("wizard.migration.targetWorkspace"),
|
|
initialValue:
|
|
params.baseConfig.agents?.defaults?.workspace ?? onboardHelpers.DEFAULT_WORKSPACE,
|
|
}));
|
|
const workspaceDir = resolveUserPath(workspaceInput.trim() || onboardHelpers.DEFAULT_WORKSPACE);
|
|
const stateDir = resolveStateDir();
|
|
await withSetupMigrationTargetLock(stateDir, async () => {
|
|
const lockedBaseConfig = preserveSetupMigrationSecurityAcknowledgement(
|
|
await params.readConfigFile(),
|
|
params.baseConfig,
|
|
);
|
|
const initialTargetSnapshotHash = await buildSetupMigrationTargetSnapshot({
|
|
config: lockedBaseConfig,
|
|
stateDir,
|
|
workspaceDir,
|
|
});
|
|
const freshness = await inspectSetupMigrationFreshness({
|
|
baseConfig: lockedBaseConfig,
|
|
stateDir,
|
|
workspaceDir,
|
|
});
|
|
const recoveryState =
|
|
!setupMigrationProviderSupportsRecovery(providerId) ||
|
|
process.env.OPENCLAW_MIGRATION_EXISTING_IMPORT === "1"
|
|
? ({ kind: "none" } as const)
|
|
: await resolveSetupMigrationRecovery({
|
|
stateDir,
|
|
providerId,
|
|
workspaceDir,
|
|
targetSnapshotHash: initialTargetSnapshotHash,
|
|
});
|
|
const recoveryAttempt =
|
|
!freshness.fresh && recoveryState.kind === "recoverable" ? recoveryState.attempt : undefined;
|
|
if (!recoveryAttempt) {
|
|
assertFreshSetupMigrationTarget(freshness);
|
|
}
|
|
const resolvedProvider = await resolveSetupMigrationProvider({
|
|
providerId,
|
|
baseConfig: lockedBaseConfig,
|
|
prompter: params.prompter,
|
|
runtime: params.runtime,
|
|
workspaceDir,
|
|
});
|
|
const planningBaseConfig = await params.readConfigFile();
|
|
const planningTargetSnapshotHash = await buildSetupMigrationTargetSnapshot({
|
|
config: planningBaseConfig,
|
|
stateDir,
|
|
workspaceDir,
|
|
});
|
|
const migrationLogger = createMigrationLogger(params.runtime);
|
|
const selectedDetections = [...params.detections];
|
|
if (
|
|
resolvedProvider.provider.detect &&
|
|
!selectedDetections.some((detection) => detection.providerId === providerId)
|
|
) {
|
|
try {
|
|
const detection = await resolvedProvider.provider.detect({
|
|
config: resolvedProvider.baseConfig,
|
|
stateDir,
|
|
logger: migrationLogger,
|
|
});
|
|
if (detection.found) {
|
|
selectedDetections.push({
|
|
providerId,
|
|
label: detection.label ?? resolvedProvider.provider.label,
|
|
...(detection.source ? { source: detection.source } : {}),
|
|
...(detection.message ? { message: detection.message } : {}),
|
|
});
|
|
}
|
|
} catch (error) {
|
|
migrationLogger.debug?.(
|
|
`Migration provider ${providerId} detection failed: ${formatErrorMessage(error)}`,
|
|
);
|
|
}
|
|
}
|
|
const sourceDefault = resolveImportSourceDefault({
|
|
providerId,
|
|
detections: selectedDetections,
|
|
});
|
|
const sourceDir =
|
|
params.opts.importSource?.trim() ||
|
|
sourceDefault ||
|
|
(params.opts.nonInteractive
|
|
? (() => {
|
|
throw new Error("--import-source is required for non-interactive migration import.");
|
|
})()
|
|
: await params.prompter.text({
|
|
message: t("wizard.migration.sourceAgentHome"),
|
|
initialValue: providerId === "hermes" ? "~/.hermes" : undefined,
|
|
}));
|
|
const retryingFailedAttempt =
|
|
recoveryAttempt !== undefined &&
|
|
setupMigrationAttemptMatchesSource(recoveryAttempt, sourceDir);
|
|
if (!retryingFailedAttempt) {
|
|
assertFreshSetupMigrationTarget(freshness);
|
|
} else if (planningTargetSnapshotHash !== initialTargetSnapshotHash) {
|
|
throw new Error("Migration target changed while preparing the retry. Review it and retry.");
|
|
}
|
|
let targetConfig = applyLocalSetupWorkspaceConfig(resolvedProvider.baseConfig, workspaceDir);
|
|
if (params.opts.skipBootstrap) {
|
|
targetConfig = applySkipBootstrapConfig(targetConfig);
|
|
}
|
|
const initialCtx = {
|
|
config: targetConfig,
|
|
stateDir,
|
|
source: sourceDir,
|
|
overwrite: false,
|
|
logger: migrationLogger,
|
|
};
|
|
const planned = await createSetupMigrationPlan({
|
|
provider: resolvedProvider.provider,
|
|
ctx: initialCtx,
|
|
importSecrets: Boolean(params.opts.importSecrets),
|
|
nonInteractive: Boolean(params.opts.nonInteractive),
|
|
prompter: params.prompter,
|
|
});
|
|
const plannedSourceSnapshotHash = await buildSetupMigrationPlanSourceSnapshot(planned.plan);
|
|
const ctx = planned.ctx;
|
|
const plan =
|
|
retryingFailedAttempt && recoveryAttempt
|
|
? prepareSetupMigrationRetryPlan(planned.plan, recoveryAttempt, plannedSourceSnapshotHash)
|
|
: planned.plan;
|
|
await params.prompter.note(
|
|
formatMigrationPreview(plan).join("\n"),
|
|
t("wizard.migration.previewTitle"),
|
|
);
|
|
assertConflictFreePlan(plan, providerId);
|
|
|
|
const confirmed =
|
|
params.opts.nonInteractive === true
|
|
? true
|
|
: await params.prompter.confirm({
|
|
message: t("wizard.migration.apply"),
|
|
initialValue: true,
|
|
});
|
|
if (!confirmed) {
|
|
throw new WizardCancelledError(t("wizard.migration.cancelled"));
|
|
}
|
|
|
|
const reportDir = buildMigrationReportDir(providerId, stateDir);
|
|
const backupPath = await createPreMigrationBackup({});
|
|
targetConfig = onboardHelpers.applyWizardMetadata(targetConfig, {
|
|
command: "onboard",
|
|
mode: "local",
|
|
});
|
|
const boundary = await prepareSetupMigrationAttemptBoundary({
|
|
currentConfig: await params.readConfigFile(),
|
|
targetConfig,
|
|
stateDir,
|
|
workspaceDir,
|
|
plan: planned.plan,
|
|
expectedTargetSnapshotHash: planningTargetSnapshotHash,
|
|
expectedSourceSnapshotHash: plannedSourceSnapshotHash,
|
|
});
|
|
const attempt = createSetupMigrationAttempt({
|
|
providerId,
|
|
source: sourceDir,
|
|
workspaceDir,
|
|
plan,
|
|
sourceSnapshotHash: boundary.sourceSnapshotHash,
|
|
preparedTargetSnapshotHash: boundary.preparedTargetSnapshotHash,
|
|
targetSnapshotHash: boundary.targetSnapshotHash,
|
|
...(recoveryAttempt ? { previousAttempt: recoveryAttempt } : {}),
|
|
});
|
|
const withReport = await runSetupMigrationAttempt({
|
|
reportDir,
|
|
attempt,
|
|
assertSucceeded: assertApplySucceeded,
|
|
async readTargetSnapshot() {
|
|
return await buildSetupMigrationTargetSnapshot({
|
|
config: await params.readConfigFile(),
|
|
stateDir,
|
|
workspaceDir,
|
|
});
|
|
},
|
|
async apply() {
|
|
targetConfig = await params.commitConfigFile(targetConfig);
|
|
// Provider config mutations persist; recommitting targetConfig would overwrite them.
|
|
const result = await resolvedProvider.provider.apply(
|
|
{
|
|
...ctx,
|
|
config: targetConfig,
|
|
...(backupPath ? { backupPath } : {}),
|
|
reportDir,
|
|
},
|
|
plan,
|
|
);
|
|
return {
|
|
...result,
|
|
...((result.backupPath ?? backupPath)
|
|
? { backupPath: result.backupPath ?? backupPath }
|
|
: {}),
|
|
reportDir: result.reportDir ?? reportDir,
|
|
};
|
|
},
|
|
});
|
|
await params.prompter.note(
|
|
formatMigrationResult(withReport).join("\n"),
|
|
t("wizard.migration.appliedTitle"),
|
|
);
|
|
if (params.continueOnboarding) {
|
|
await params.prompter.note(
|
|
t("wizard.migration.continuing"),
|
|
t("wizard.migration.appliedTitle"),
|
|
);
|
|
} else {
|
|
await params.prompter.outro(t("wizard.migration.complete"));
|
|
}
|
|
});
|
|
}
|