import { theme } from "../../../packages/terminal-core/src/theme.js"; import type { UpdateChannel } from "../../infra/update-channels.js"; import type { UpdateFailureFact } from "../../infra/update-failure-facts.js"; import { canResolveRegistryVersionForPackageTarget } from "../../infra/update-global.js"; import { getUpdateRun } from "../../infra/update-run-ledger.js"; import type { UpdateRunRecord } from "../../infra/update-run-record.js"; import type { UpdateRunResult } from "../../infra/update-runner.js"; import { defaultRuntime } from "../../runtime.js"; import type { OpenClawDatabaseSchemaPreflight } from "../../state/openclaw-database-preflight.js"; import { printResult } from "./progress.js"; import { formatSchemaRefusalLines, hasSchemaRefusal } from "./schema-preflight.js"; import { UpdatePreMutationError, type UpdateCommandOptions } from "./shared.js"; import type { ManagedServiceRootRedirect } from "./update-command-service-plan.js"; export async function handleDryRunPreflightError( error: unknown, notes: string[], refuseUpdate: ( reason: string, message: string, failureFacts?: readonly UpdateFailureFact[], ) => Promise, ): Promise { if (!(error instanceof UpdatePreMutationError)) { throw error; } if ( error.reason === "database-schema-preflight" || error.reason === "target-metadata-preflight" || error.reason === "invalid-config" ) { // A best-effort preview reports incomplete admission; it never authorizes mutation. notes.push(error.message.replace(/^Update refused:/u, "Would refuse update:")); return { incompatible: [], indeterminate: [] }; } await refuseUpdate(error.reason, error.message, error.failureFacts); return { incompatible: [], indeterminate: [] }; } type UpdateDryRunPreview = { runId: string; run?: UpdateRunRecord; dryRun: true; root: string; installKind: "git" | "package" | "unknown"; mode: UpdateRunResult["mode"]; updateInstallKind: "git" | "package" | "unknown"; switchToGit: boolean; switchToPackage: boolean; restart: boolean; requestedChannel: UpdateChannel | null; storedChannel: UpdateChannel | null; effectiveChannel: UpdateChannel; tag: string; currentVersion: string | null; targetVersion: string | null; targetVersionReason?: string; downgradeRisk: boolean; actions: string[]; notes: string[]; }; function printDryRunPreview(preview: UpdateDryRunPreview, jsonMode: boolean): void { if (jsonMode) { defaultRuntime.writeJson(preview); return; } defaultRuntime.log(theme.heading("Update dry-run")); defaultRuntime.log(theme.muted("No changes were applied.")); defaultRuntime.log(""); defaultRuntime.log(` Root: ${theme.muted(preview.root)}`); defaultRuntime.log(` Install kind: ${theme.muted(preview.installKind)}`); defaultRuntime.log(` Mode: ${theme.muted(preview.mode)}`); defaultRuntime.log(` Channel: ${theme.muted(preview.effectiveChannel)}`); defaultRuntime.log(` Tag/spec: ${theme.muted(preview.tag)}`); if (preview.currentVersion) { defaultRuntime.log(` Current version: ${theme.muted(preview.currentVersion)}`); } if (preview.targetVersion) { defaultRuntime.log(` Target version: ${theme.muted(preview.targetVersion)}`); } else if (preview.targetVersionReason) { defaultRuntime.log(` Target version: unresolved (${preview.targetVersionReason})`); } if (preview.downgradeRisk) { defaultRuntime.log(theme.warn(" Downgrade confirmation would be required in a real run.")); } defaultRuntime.log(""); defaultRuntime.log(theme.heading("Planned actions:")); for (const action of preview.actions) { defaultRuntime.log(` - ${action}`); } if (preview.notes.length > 0) { defaultRuntime.log(""); defaultRuntime.log(theme.heading("Notes:")); for (const note of preview.notes) { defaultRuntime.log(` - ${theme.muted(note)}`); } } } export function printUpdateDryRun(params: { runId: string; root: string; installKind: "git" | "package" | "unknown"; updateInstallKind: "git" | "package" | "unknown"; mode: UpdateRunResult["mode"]; switchToGit: boolean; switchToPackage: boolean; shouldRestart: boolean; requestedChannel: UpdateChannel | null; storedChannel: UpdateChannel | null; channel: UpdateChannel; tag: string; packageInstallSpec: string | null; currentVersion: string | null; targetVersion: string | null; downgradeRisk: boolean; packageAlreadyCurrent: boolean; fallbackToLatest: boolean; managedServiceRootRedirect: ManagedServiceRootRedirect | null; explicitTag: string | null; packageSchemaPreflight: OpenClawDatabaseSchemaPreflight; preflightNotes?: readonly string[]; opts: Pick; }): void { const actions: string[] = []; if (params.requestedChannel && params.requestedChannel !== params.storedChannel) { actions.push(`Persist update.channel=${params.requestedChannel} in config`); } if (params.switchToGit) { actions.push("Switch install mode from package to git checkout (dev channel)"); } else if (params.switchToPackage) { actions.push(`Switch install mode from git to package manager (${params.mode})`); } else if (params.updateInstallKind === "git") { actions.push(`Run git update flow on channel ${params.channel} (fetch/rebase/build/doctor)`); } else if (params.packageAlreadyCurrent) { actions.push( `Refresh package install with spec ${params.packageInstallSpec ?? params.tag}; current version already matches ${params.targetVersion}`, ); } else { actions.push( `Run global package manager update with spec ${params.packageInstallSpec ?? params.tag}`, ); } actions.push("Run plugin update sync after core update"); actions.push("Refresh shell completion cache (if needed)"); actions.push( params.shouldRestart ? "Restart gateway service and run doctor checks" : "Skip restart (because --no-restart is set)", ); const notes: string[] = [...(params.preflightNotes ?? [])]; if (params.opts.tag && params.updateInstallKind === "git") { notes.push("--tag applies to npm installs only; git updates ignore it."); } if (params.fallbackToLatest) { notes.push("Beta channel resolves to latest for this run (fallback)."); } if (params.managedServiceRootRedirect) { notes.push( `Package update targets managed service root ${params.managedServiceRootRedirect.root} instead of invoking root ${params.managedServiceRootRedirect.previousRoot}.`, ); } if (params.explicitTag && !canResolveRegistryVersionForPackageTarget(params.tag)) { notes.push("Non-registry package specs skip npm version lookup and downgrade previews."); } if (hasSchemaRefusal(params.packageSchemaPreflight)) { notes.push(...formatSchemaRefusalLines(params.packageSchemaPreflight, true)); } if (params.updateInstallKind === "git") { notes.push( "Git preview does not execute target scripts or select a build-tested development fallback. The real update repeats database admission before executing each candidate.", ); } const run = getUpdateRun(params.runId, { env: params.opts.run?.env }); const targetVersionReason = params.targetVersion ? undefined : params.updateInstallKind === "git" ? "Git dry-runs do not select a build-tested target version." : canResolveRegistryVersionForPackageTarget(params.packageInstallSpec ?? params.tag) ? "The package target version could not be resolved." : "The package artifact is not staged during a dry-run."; printDryRunPreview( { runId: params.runId, run, dryRun: true, root: params.root, installKind: params.installKind, mode: params.mode, updateInstallKind: params.updateInstallKind, switchToGit: params.switchToGit, switchToPackage: params.switchToPackage, restart: params.shouldRestart, requestedChannel: params.requestedChannel, storedChannel: params.storedChannel, effectiveChannel: params.channel, tag: params.packageInstallSpec ?? params.tag, currentVersion: run?.before?.version ?? params.currentVersion, targetVersion: params.targetVersion, ...(targetVersionReason ? { targetVersionReason } : {}), downgradeRisk: params.downgradeRisk, actions, notes, }, Boolean(params.opts.json), ); if (!params.opts.json) { printResult( { runId: params.runId, status: "skipped", mode: params.mode, reason: "dry-run", steps: [], durationMs: 0, }, params.opts, ); } }