import { spinner } from "@clack/prompts"; import { formatDurationPrecise } from "../../infra/format-time/format-duration.ts"; import type { UpdateRunResult, UpdateStepInfo, UpdateStepProgress, } from "../../infra/update-runner.js"; import { defaultRuntime } from "../../runtime.js"; import { theme } from "../../terminal/theme.js"; import type { UpdateCommandOptions } from "./shared.js"; const STEP_LABELS: Record = { "clean check": "Working directory is clean", "upstream check": "Upstream branch exists", "git fetch": "Fetching latest changes", "git rebase": "Rebasing onto target commit", "git rev-parse @{upstream}": "Resolving upstream commit", "git rev-list": "Enumerating candidate commits", "git clone": "Cloning git checkout", "preflight worktree": "Preparing preflight worktree", "preflight cleanup": "Cleaning preflight worktree", "deps install": "Installing dependencies", build: "Building", "ui:build": "Building UI assets", "ui:build (post-doctor repair)": "Restoring missing UI assets", "ui assets verify": "Validating UI assets", "openclaw doctor entry": "Checking doctor entrypoint", "openclaw doctor": "Running doctor checks", "git rev-parse HEAD (after)": "Verifying update", "global update": "Updating via package manager", "global install": "Installing global package", }; function getStepLabel(step: UpdateStepInfo): string { return STEP_LABELS[step.name] ?? step.name; } export type ProgressController = { progress: UpdateStepProgress; stop: () => void; }; export function createUpdateProgress(enabled: boolean): ProgressController { if (!enabled) { return { progress: {}, stop: () => {}, }; } let currentSpinner: ReturnType | null = null; const progress: UpdateStepProgress = { onStepStart: (step) => { currentSpinner = spinner(); currentSpinner.start(theme.accent(getStepLabel(step))); }, onStepComplete: (step) => { if (!currentSpinner) { return; } const label = getStepLabel(step); const duration = theme.muted(`(${formatDurationPrecise(step.durationMs)})`); const icon = step.exitCode === 0 ? theme.success("\u2713") : theme.error("\u2717"); currentSpinner.stop(`${icon} ${label} ${duration}`); currentSpinner = null; if (step.exitCode !== 0 && step.stderrTail) { const lines = step.stderrTail.split("\n").slice(-10); for (const line of lines) { if (line.trim()) { defaultRuntime.log(` ${theme.error(line)}`); } } } }, }; return { progress, stop: () => { if (currentSpinner) { currentSpinner.stop(); currentSpinner = null; } }, }; } function formatStepStatus(exitCode: number | null): string { if (exitCode === 0) { return theme.success("\u2713"); } if (exitCode === null) { return theme.warn("?"); } return theme.error("\u2717"); } type PrintResultOptions = UpdateCommandOptions & { hideSteps?: boolean; }; export function printResult(result: UpdateRunResult, opts: PrintResultOptions): void { if (opts.json) { defaultRuntime.log(JSON.stringify(result, null, 2)); return; } const statusColor = result.status === "ok" ? theme.success : result.status === "skipped" ? theme.warn : theme.error; defaultRuntime.log(""); defaultRuntime.log( `${theme.heading("Update Result:")} ${statusColor(result.status.toUpperCase())}`, ); if (result.root) { defaultRuntime.log(` Root: ${theme.muted(result.root)}`); } if (result.reason) { defaultRuntime.log(` Reason: ${theme.muted(result.reason)}`); } if (result.before?.version || result.before?.sha) { const before = result.before.version ?? result.before.sha?.slice(0, 8) ?? ""; defaultRuntime.log(` Before: ${theme.muted(before)}`); } if (result.after?.version || result.after?.sha) { const after = result.after.version ?? result.after.sha?.slice(0, 8) ?? ""; defaultRuntime.log(` After: ${theme.muted(after)}`); } if (!opts.hideSteps && result.steps.length > 0) { defaultRuntime.log(""); defaultRuntime.log(theme.heading("Steps:")); for (const step of result.steps) { const status = formatStepStatus(step.exitCode); const duration = theme.muted(`(${formatDurationPrecise(step.durationMs)})`); defaultRuntime.log(` ${status} ${step.name} ${duration}`); if (step.exitCode !== 0 && step.stderrTail) { const lines = step.stderrTail.split("\n").slice(0, 5); for (const line of lines) { if (line.trim()) { defaultRuntime.log(` ${theme.error(line)}`); } } } } } defaultRuntime.log(""); defaultRuntime.log(`Total time: ${theme.muted(formatDurationPrecise(result.durationMs))}`); }