mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-16 04:20:44 +00:00
199 lines
6.1 KiB
TypeScript
199 lines
6.1 KiB
TypeScript
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<string, string> = {
|
|
"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 update (omit optional)": "Retrying update without optional deps",
|
|
"global install": "Installing global package",
|
|
};
|
|
|
|
function getStepLabel(step: UpdateStepInfo): string {
|
|
return STEP_LABELS[step.name] ?? step.name;
|
|
}
|
|
|
|
export function inferUpdateFailureHints(result: UpdateRunResult): string[] {
|
|
if (result.status !== "error" || result.mode !== "npm") {
|
|
return [];
|
|
}
|
|
const failedStep = [...result.steps].toReversed().find((step) => step.exitCode !== 0);
|
|
if (!failedStep) {
|
|
return [];
|
|
}
|
|
|
|
const stderr = (failedStep.stderrTail ?? "").toLowerCase();
|
|
const hints: string[] = [];
|
|
|
|
if (failedStep.name.startsWith("global update") && stderr.includes("eacces")) {
|
|
hints.push(
|
|
"Detected permission failure (EACCES). Re-run with a writable global prefix or sudo (for system-managed Node installs).",
|
|
);
|
|
hints.push("Example: npm config set prefix ~/.local && npm i -g openclaw@latest");
|
|
}
|
|
|
|
if (
|
|
failedStep.name.startsWith("global update") &&
|
|
(stderr.includes("node-gyp") || stderr.includes("prebuild"))
|
|
) {
|
|
hints.push(
|
|
"Detected native optional dependency build failure. The updater retries with --omit=optional automatically.",
|
|
);
|
|
hints.push("If it still fails: npm i -g openclaw@latest --omit=optional");
|
|
}
|
|
|
|
return hints;
|
|
}
|
|
|
|
export type ProgressController = {
|
|
progress: UpdateStepProgress;
|
|
stop: () => void;
|
|
};
|
|
|
|
export function createUpdateProgress(enabled: boolean): ProgressController {
|
|
if (!enabled) {
|
|
return {
|
|
progress: {},
|
|
stop: () => {},
|
|
};
|
|
}
|
|
|
|
let currentSpinner: ReturnType<typeof spinner> | 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)}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const hints = inferUpdateFailureHints(result);
|
|
if (hints.length > 0) {
|
|
defaultRuntime.log("");
|
|
defaultRuntime.log(theme.heading("Recovery hints:"));
|
|
for (const hint of hints) {
|
|
defaultRuntime.log(` - ${theme.warn(hint)}`);
|
|
}
|
|
}
|
|
|
|
defaultRuntime.log("");
|
|
defaultRuntime.log(`Total time: ${theme.muted(formatDurationPrecise(result.durationMs))}`);
|
|
}
|