import { confirm, isCancel } from "@clack/prompts"; import { readConfigFileSnapshot } from "../../config/config.js"; import { formatUpdateChannelLabel, normalizeUpdateChannel, resolveEffectiveUpdateChannel, } from "../../infra/update-channels.js"; import { checkUpdateStatus } from "../../infra/update-check.js"; import { defaultRuntime } from "../../runtime.js"; import { selectStyled } from "../../terminal/prompt-select-styled.js"; import { stylePromptMessage } from "../../terminal/prompt-style.js"; import { theme } from "../../terminal/theme.js"; import { pathExists } from "../../utils.js"; import { isEmptyDir, isGitCheckout, parseTimeoutMsOrExit, resolveGitInstallDir, resolveUpdateRoot, type UpdateWizardOptions, } from "./shared.js"; import { updateCommand } from "./update-command.js"; export async function updateWizardCommand(opts: UpdateWizardOptions = {}): Promise { if (!process.stdin.isTTY) { defaultRuntime.error( "Update wizard requires a TTY. Use `openclaw update --channel ` instead.", ); defaultRuntime.exit(1); return; } const timeoutMs = parseTimeoutMsOrExit(opts.timeout); if (timeoutMs === null) { return; } const root = await resolveUpdateRoot(); const [updateStatus, configSnapshot] = await Promise.all([ checkUpdateStatus({ root, timeoutMs: timeoutMs ?? 3500, fetchGit: false, includeRegistry: false, }), readConfigFileSnapshot(), ]); const configChannel = configSnapshot.valid ? normalizeUpdateChannel(configSnapshot.config.update?.channel) : null; const channelInfo = resolveEffectiveUpdateChannel({ configChannel, installKind: updateStatus.installKind, git: updateStatus.git ? { tag: updateStatus.git.tag, branch: updateStatus.git.branch } : undefined, }); const channelLabel = formatUpdateChannelLabel({ channel: channelInfo.channel, source: channelInfo.source, gitTag: updateStatus.git?.tag ?? null, gitBranch: updateStatus.git?.branch ?? null, }); const pickedChannel = await selectStyled({ message: "Update channel", options: [ { value: "keep", label: `Keep current (${channelInfo.channel})`, hint: channelLabel, }, { value: "stable", label: "Stable", hint: "Tagged releases (npm latest)", }, { value: "beta", label: "Beta", hint: "Prereleases (npm beta)", }, { value: "dev", label: "Dev", hint: "Git main", }, ], initialValue: "keep", }); if (isCancel(pickedChannel)) { defaultRuntime.log(theme.muted("Update cancelled.")); defaultRuntime.exit(0); return; } const requestedChannel = pickedChannel === "keep" ? null : pickedChannel; if (requestedChannel === "dev" && updateStatus.installKind !== "git") { const gitDir = resolveGitInstallDir(); const hasGit = await isGitCheckout(gitDir); if (!hasGit) { const dirExists = await pathExists(gitDir); if (dirExists) { const empty = await isEmptyDir(gitDir); if (!empty) { defaultRuntime.error( `OPENCLAW_GIT_DIR points at a non-git directory: ${gitDir}. Set OPENCLAW_GIT_DIR to an empty folder or an openclaw checkout.`, ); defaultRuntime.exit(1); return; } } const ok = await confirm({ message: stylePromptMessage( `Create a git checkout at ${gitDir}? (override via OPENCLAW_GIT_DIR)`, ), initialValue: true, }); if (isCancel(ok) || !ok) { defaultRuntime.log(theme.muted("Update cancelled.")); defaultRuntime.exit(0); return; } } } const restart = await confirm({ message: stylePromptMessage("Restart the gateway service after update?"), initialValue: true, }); if (isCancel(restart)) { defaultRuntime.log(theme.muted("Update cancelled.")); defaultRuntime.exit(0); return; } try { await updateCommand({ channel: requestedChannel ?? undefined, restart: Boolean(restart), timeout: opts.timeout, }); } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); } }