openclaw / src /cli /update-cli /update-command-dry-run.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
eb3f11e verified
Raw
History Blame Contribute Delete
8.7 kB
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<void>,
): Promise<OpenClawDatabaseSchemaPreflight> {
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<UpdateCommandOptions, "tag" | "json" | "run">;
}): 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,
);
}
}