import type { Command } from "commander"; import { resolveClawdbotPackageRoot } from "../infra/clawdbot-root.js"; import { runGatewayUpdate, type UpdateRunResult, } from "../infra/update-runner.js"; import { defaultRuntime } from "../runtime.js"; import { formatDocsLink } from "../terminal/links.js"; import { theme } from "../terminal/theme.js"; import { runDaemonRestart } from "./daemon-cli.js"; export type UpdateCommandOptions = { json?: boolean; restart?: boolean; timeout?: string; }; function formatDuration(ms: number): string { if (ms < 1000) return `${ms}ms`; const seconds = (ms / 1000).toFixed(1); return `${seconds}s`; } function formatStepStatus(exitCode: number | null): string { if (exitCode === 0) return theme.success("\u2713"); if (exitCode === null) return theme.warn("?"); return theme.error("\u2717"); } function printResult(result: UpdateRunResult, opts: UpdateCommandOptions) { 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())}`, ); defaultRuntime.log(` Mode: ${theme.muted(result.mode)}`); 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 (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(`(${formatDuration(step.durationMs)})`); defaultRuntime.log(` ${status} ${step.name} ${duration}`); // Show stderr for failed steps 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(formatDuration(result.durationMs))}`, ); } export async function updateCommand(opts: UpdateCommandOptions): Promise { const timeoutMs = opts.timeout ? Number.parseInt(opts.timeout, 10) * 1000 : undefined; if (timeoutMs !== undefined && (Number.isNaN(timeoutMs) || timeoutMs <= 0)) { defaultRuntime.error("--timeout must be a positive integer (seconds)"); defaultRuntime.exit(1); return; } if (!opts.json) { defaultRuntime.log(theme.heading("Updating Clawdbot...")); defaultRuntime.log(""); } const root = (await resolveClawdbotPackageRoot({ moduleUrl: import.meta.url, argv1: process.argv[1], cwd: process.cwd(), })) ?? process.cwd(); const result = await runGatewayUpdate({ cwd: root, argv1: process.argv[1], timeoutMs, }); printResult(result, opts); if (result.status === "error") { defaultRuntime.exit(1); return; } if (result.status === "skipped") { if (result.reason === "dirty") { defaultRuntime.log( theme.warn( "Skipped: working directory has uncommitted changes. Commit or stash them first.", ), ); } if (result.reason === "not-git-install") { defaultRuntime.log( theme.warn( "Skipped: this Clawdbot install isn't a git checkout. Update via your package manager, then run `clawdbot doctor` and `clawdbot daemon restart`.", ), ); defaultRuntime.log( theme.muted( "Examples: `npm i -g clawdbot@latest` or `pnpm add -g clawdbot@latest`", ), ); } defaultRuntime.exit(0); return; } // Restart daemon if requested if (opts.restart) { if (!opts.json) { defaultRuntime.log(""); defaultRuntime.log(theme.heading("Restarting daemon...")); } try { await runDaemonRestart(); if (!opts.json) { defaultRuntime.log(theme.success("Daemon restarted successfully.")); } } catch (err) { if (!opts.json) { defaultRuntime.log(theme.warn(`Daemon restart failed: ${String(err)}`)); defaultRuntime.log( theme.muted( "You may need to restart the daemon manually: clawdbot daemon restart", ), ); } } } else if (!opts.json) { defaultRuntime.log(""); defaultRuntime.log( theme.muted( "Tip: Run `clawdbot daemon restart` to apply updates to a running gateway.", ), ); } } export function registerUpdateCli(program: Command) { program .command("update") .description("Update Clawdbot to the latest version") .option("--json", "Output result as JSON", false) .option( "--restart", "Restart the gateway daemon after a successful update", false, ) .option( "--timeout ", "Timeout for each update step in seconds (default: 1200)", ) .addHelpText( "after", () => ` Examples: clawdbot update # Update a source checkout (git) clawdbot update --restart # Update and restart the daemon clawdbot update --json # Output result as JSON clawdbot --update # Shorthand for clawdbot update Notes: - For git installs: fetches, rebases, installs deps, builds, and runs doctor - For npm installs: use npm/pnpm to reinstall (see docs/install/updating.md) - Skips update if the working directory has uncommitted changes ${theme.muted("Docs:")} ${formatDocsLink("/updating", "docs.clawd.bot/updating")}`, ) .action(async (opts) => { try { await updateCommand({ json: Boolean(opts.json), restart: Boolean(opts.restart), timeout: opts.timeout as string | undefined, }); } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); } }); }