2026-01-22 07:05:00 +00:00
import { confirm , isCancel , select , spinner } from "@clack/prompts" ;
2026-01-17 11:40:02 +00:00
import fs from "node:fs/promises" ;
2026-01-21 06:00:50 +00:00
import os from "node:os" ;
2026-01-17 11:40:02 +00:00
import path from "node:path" ;
2026-01-10 18:18:10 +00:00
import type { Command } from "commander" ;
2026-01-17 11:40:02 +00:00
import { readConfigFileSnapshot , writeConfigFile } from "../config/config.js" ;
2026-01-30 03:15:10 +01:00
import { resolveOpenClawPackageRoot } from "../infra/openclaw-root.js" ;
2026-01-20 14:05:55 +00:00
import {
checkUpdateStatus ,
compareSemverStrings ,
fetchNpmTagVersion ,
2026-01-20 16:28:25 +00:00
resolveNpmChannelTag ,
2026-01-20 14:05:55 +00:00
} from "../infra/update-check.js" ;
2026-01-17 11:40:02 +00:00
import { parseSemver } from "../infra/runtime-guard.js" ;
2026-01-10 18:18:10 +00:00
import {
runGatewayUpdate ,
type UpdateRunResult ,
2026-01-11 00:48:39 +01:00
type UpdateStepInfo ,
2026-01-21 06:28:22 +00:00
type UpdateStepResult ,
2026-01-11 00:48:39 +01:00
type UpdateStepProgress ,
2026-01-10 18:18:10 +00:00
} from "../infra/update-runner.js" ;
2026-01-21 06:00:50 +00:00
import {
detectGlobalInstallManagerByPresence ,
detectGlobalInstallManagerForRoot ,
globalInstallArgs ,
resolveGlobalPackageRoot ,
type GlobalInstallManager ,
} from "../infra/update-global.js" ;
2026-01-20 13:33:31 +00:00
import {
channelToNpmTag ,
DEFAULT_GIT_CHANNEL ,
DEFAULT_PACKAGE_CHANNEL ,
2026-01-20 14:05:55 +00:00
formatUpdateChannelLabel ,
2026-01-20 13:33:31 +00:00
normalizeUpdateChannel ,
2026-01-20 14:05:55 +00:00
resolveEffectiveUpdateChannel ,
2026-01-20 13:33:31 +00:00
} from "../infra/update-channels.js" ;
2026-01-21 06:00:50 +00:00
import { trimLogTail } from "../infra/restart-sentinel.js" ;
2026-01-10 18:18:10 +00:00
import { defaultRuntime } from "../runtime.js" ;
2026-01-10 20:50:17 +01:00
import { formatDocsLink } from "../terminal/links.js" ;
2026-01-20 07:42:21 +00:00
import { formatCliCommand } from "./command-format.js" ;
2026-01-27 11:27:41 +00:00
import { replaceCliName , resolveCliName } from "./cli-name.js" ;
2026-01-22 07:05:00 +00:00
import { stylePromptHint , stylePromptMessage } from "../terminal/prompt-style.js" ;
2026-01-10 18:18:10 +00:00
import { theme } from "../terminal/theme.js" ;
2026-01-20 14:05:55 +00:00
import { renderTable } from "../terminal/table.js" ;
2026-01-21 04:46:15 +00:00
import { formatHelpExamples } from "./help-format.js" ;
2026-01-20 14:05:55 +00:00
import {
formatUpdateAvailableHint ,
formatUpdateOneLiner ,
resolveUpdateAvailability ,
} from "../commands/status.update.js" ;
2026-01-20 15:56:48 +00:00
import { syncPluginsForUpdateChannel , updateNpmInstalledPlugins } from "../plugins/update.js" ;
2026-01-21 06:00:50 +00:00
import { runCommandWithTimeout } from "../process/exec.js" ;
2026-01-10 18:18:10 +00:00
export type UpdateCommandOptions = {
json? : boolean ;
restart? : boolean ;
2026-01-17 11:40:02 +00:00
channel? : string ;
tag? : string ;
2026-01-10 18:18:10 +00:00
timeout? : string ;
2026-01-21 03:39:39 +00:00
yes? : boolean ;
2026-01-10 18:18:10 +00:00
} ;
2026-01-20 14:05:55 +00:00
export type UpdateStatusOptions = {
json? : boolean ;
timeout? : string ;
} ;
2026-01-22 07:05:00 +00:00
export type UpdateWizardOptions = {
timeout? : string ;
} ;
2026-01-10 18:18:10 +00:00
2026-01-11 00:48:39 +01:00
const STEP_LABELS : Record < string , string > = {
2026-01-11 01:14:59 +01:00
"clean check" : "Working directory is clean" ,
"upstream check" : "Upstream branch exists" ,
2026-01-11 00:48:39 +01:00
"git fetch" : "Fetching latest changes" ,
2026-01-22 04:19:29 +00:00
"git rebase" : "Rebasing onto target commit" ,
"git rev-parse @{upstream}" : "Resolving upstream commit" ,
"git rev-list" : "Enumerating candidate commits" ,
2026-01-21 06:00:50 +00:00
"git clone" : "Cloning git checkout" ,
2026-01-22 04:19:29 +00:00
"preflight worktree" : "Preparing preflight worktree" ,
"preflight cleanup" : "Cleaning preflight worktree" ,
2026-01-11 00:48:39 +01:00
"deps install" : "Installing dependencies" ,
build : "Building" ,
"ui:build" : "Building UI" ,
2026-01-30 03:15:10 +01:00
"openclaw doctor" : "Running doctor checks" ,
2026-01-11 00:48:39 +01:00
"git rev-parse HEAD (after)" : "Verifying update" ,
2026-01-16 11:45:37 +00:00
"global update" : "Updating via package manager" ,
2026-01-21 06:00:50 +00:00
"global install" : "Installing global package" ,
2026-01-11 00:48:39 +01:00
} ;
2026-01-17 20:12:23 +00:00
const UPDATE_QUIPS = [
"Leveled up! New skills unlocked. You're welcome." ,
"Fresh code, same lobster. Miss me?" ,
"Back and better. Did you even notice I was gone?" ,
"Update complete. I learned some new tricks while I was out." ,
"Upgraded! Now with 23% more sass." ,
"I've evolved. Try to keep up." ,
"New version, who dis? Oh right, still me but shinier." ,
"Patched, polished, and ready to pinch. Let's go." ,
"The lobster has molted. Harder shell, sharper claws." ,
"Update done! Check the changelog or just trust me, it's good." ,
"Reborn from the boiling waters of npm. Stronger now." ,
"I went away and came back smarter. You should try it sometime." ,
"Update complete. The bugs feared me, so they left." ,
"New version installed. Old version sends its regards." ,
"Firmware fresh. Brain wrinkles: increased." ,
"I've seen things you wouldn't believe. Anyway, I'm updated." ,
"Back online. The changelog is long but our friendship is longer." ,
"Upgraded! Peter fixed stuff. Blame him if it breaks." ,
"Molting complete. Please don't look at my soft shell phase." ,
"Version bump! Same chaos energy, fewer crashes (probably)." ,
] ;
2026-01-17 11:40:02 +00:00
2026-01-21 06:00:50 +00:00
const MAX_LOG_CHARS = 8000 ;
2026-01-30 03:15:10 +01:00
const DEFAULT_PACKAGE_NAME = "openclaw" ;
const CORE_PACKAGE_NAMES = new Set ( [ DEFAULT_PACKAGE_NAME ] ) ;
2026-01-27 11:27:41 +00:00
const CLI_NAME = resolveCliName ( ) ;
2026-01-30 03:15:10 +01:00
const OPENCLAW_REPO_URL = "https://github.com/openclaw/openclaw.git" ;
const DEFAULT_GIT_DIR = path . join ( os . homedir ( ) , ".openclaw" ) ;
2026-01-21 06:00:50 +00:00
2026-01-17 11:40:02 +00:00
function normalizeTag ( value? : string | null ) : string | null {
if ( ! value ) return null ;
const trimmed = value . trim ( ) ;
if ( ! trimmed ) return null ;
2026-01-30 03:15:10 +01:00
if ( trimmed . startsWith ( "openclaw@" ) ) return trimmed . slice ( "openclaw@" . length ) ;
2026-01-27 11:27:41 +00:00
if ( trimmed . startsWith ( ` ${ DEFAULT_PACKAGE_NAME } @ ` ) ) {
return trimmed . slice ( ` ${ DEFAULT_PACKAGE_NAME } @ ` . length ) ;
}
return trimmed ;
2026-01-17 11:40:02 +00:00
}
2026-01-17 20:12:23 +00:00
function pickUpdateQuip ( ) : string {
return UPDATE_QUIPS [ Math . floor ( Math . random ( ) * UPDATE_QUIPS . length ) ] ? ? "Update complete." ;
}
2026-01-17 11:40:02 +00:00
function normalizeVersionTag ( tag : string ) : string | null {
const trimmed = tag . trim ( ) ;
if ( ! trimmed ) return null ;
const cleaned = trimmed . startsWith ( "v" ) ? trimmed . slice ( 1 ) : trimmed ;
return parseSemver ( cleaned ) ? cleaned : null ;
}
async function readPackageVersion ( root : string ) : Promise < string | null > {
try {
const raw = await fs . readFile ( path . join ( root , "package.json" ) , "utf-8" ) ;
const parsed = JSON . parse ( raw ) as { version? : string } ;
return typeof parsed . version === "string" ? parsed.version : null ;
} catch {
return null ;
}
}
async function resolveTargetVersion ( tag : string , timeoutMs? : number ) : Promise < string | null > {
const direct = normalizeVersionTag ( tag ) ;
if ( direct ) return direct ;
const res = await fetchNpmTagVersion ( { tag , timeoutMs } ) ;
return res . version ? ? null ;
}
async function isGitCheckout ( root : string ) : Promise < boolean > {
try {
await fs . stat ( path . join ( root , ".git" ) ) ;
return true ;
} catch {
return false ;
}
}
2026-01-27 11:27:41 +00:00
async function readPackageName ( root : string ) : Promise < string | null > {
2026-01-21 06:00:50 +00:00
try {
const raw = await fs . readFile ( path . join ( root , "package.json" ) , "utf-8" ) ;
const parsed = JSON . parse ( raw ) as { name? : string } ;
2026-01-27 11:27:41 +00:00
const name = parsed ? . name ? . trim ( ) ;
return name ? name : null ;
2026-01-21 06:00:50 +00:00
} catch {
2026-01-27 11:27:41 +00:00
return null ;
2026-01-21 06:00:50 +00:00
}
}
2026-01-27 11:27:41 +00:00
async function isCorePackage ( root : string ) : Promise < boolean > {
const name = await readPackageName ( root ) ;
return Boolean ( name && CORE_PACKAGE_NAMES . has ( name ) ) ;
}
2026-01-21 06:00:50 +00:00
async function pathExists ( targetPath : string ) : Promise < boolean > {
try {
await fs . stat ( targetPath ) ;
return true ;
} catch {
return false ;
}
}
async function isEmptyDir ( targetPath : string ) : Promise < boolean > {
try {
const entries = await fs . readdir ( targetPath ) ;
return entries . length === 0 ;
} catch {
return false ;
}
}
function resolveGitInstallDir ( ) : string {
2026-01-30 03:15:10 +01:00
const override = process . env . OPENCLAW_GIT_DIR ? . trim ( ) ;
2026-01-21 06:00:50 +00:00
if ( override ) return path . resolve ( override ) ;
2026-01-30 03:15:10 +01:00
return resolveDefaultGitDir ( ) ;
}
function resolveDefaultGitDir ( ) : string {
2026-01-21 06:00:50 +00:00
return DEFAULT_GIT_DIR ;
}
function resolveNodeRunner ( ) : string {
const base = path . basename ( process . execPath ) . toLowerCase ( ) ;
if ( base === "node" || base === "node.exe" ) return process . execPath ;
return "node" ;
}
async function runUpdateStep ( params : {
name : string ;
argv : string [ ] ;
cwd? : string ;
timeoutMs : number ;
progress? : UpdateStepProgress ;
} ) : Promise < UpdateStepResult > {
const command = params . argv . join ( " " ) ;
params . progress ? . onStepStart ? . ( {
name : params.name ,
command ,
index : 0 ,
total : 0 ,
} ) ;
const started = Date . now ( ) ;
const res = await runCommandWithTimeout ( params . argv , {
cwd : params.cwd ,
timeoutMs : params.timeoutMs ,
} ) ;
const durationMs = Date . now ( ) - started ;
const stderrTail = trimLogTail ( res . stderr , MAX_LOG_CHARS ) ;
params . progress ? . onStepComplete ? . ( {
name : params.name ,
command ,
index : 0 ,
total : 0 ,
durationMs ,
exitCode : res.code ,
stderrTail ,
} ) ;
return {
name : params.name ,
command ,
cwd : params.cwd ? ? process . cwd ( ) ,
durationMs ,
exitCode : res.code ,
stdoutTail : trimLogTail ( res . stdout , MAX_LOG_CHARS ) ,
stderrTail ,
} ;
}
async function ensureGitCheckout ( params : {
dir : string ;
timeoutMs : number ;
progress? : UpdateStepProgress ;
} ) : Promise < UpdateStepResult | null > {
const dirExists = await pathExists ( params . dir ) ;
if ( ! dirExists ) {
return await runUpdateStep ( {
name : "git clone" ,
2026-01-30 03:15:10 +01:00
argv : [ "git" , "clone" , OPENCLAW_REPO_URL , params . dir ] ,
2026-01-21 06:00:50 +00:00
timeoutMs : params.timeoutMs ,
progress : params.progress ,
} ) ;
}
if ( ! ( await isGitCheckout ( params . dir ) ) ) {
const empty = await isEmptyDir ( params . dir ) ;
if ( ! empty ) {
throw new Error (
2026-01-30 03:15:10 +01:00
` OPENCLAW_GIT_DIR points at a non-git directory: ${ params . dir } . Set OPENCLAW_GIT_DIR to an empty folder or an openclaw checkout. ` ,
2026-01-21 06:00:50 +00:00
) ;
}
return await runUpdateStep ( {
name : "git clone" ,
2026-01-30 03:15:10 +01:00
argv : [ "git" , "clone" , OPENCLAW_REPO_URL , params . dir ] ,
2026-01-21 06:00:50 +00:00
cwd : params.dir ,
timeoutMs : params.timeoutMs ,
progress : params.progress ,
} ) ;
}
2026-01-27 11:27:41 +00:00
if ( ! ( await isCorePackage ( params . dir ) ) ) {
2026-01-30 03:15:10 +01:00
throw new Error ( ` OPENCLAW_GIT_DIR does not look like a core checkout: ${ params . dir } . ` ) ;
2026-01-21 06:00:50 +00:00
}
return null ;
}
async function resolveGlobalManager ( params : {
root : string ;
installKind : "git" | "package" | "unknown" ;
timeoutMs : number ;
} ) : Promise < GlobalInstallManager > {
const runCommand = async ( argv : string [ ] , options : { timeoutMs : number } ) = > {
const res = await runCommandWithTimeout ( argv , options ) ;
return { stdout : res.stdout , stderr : res.stderr , code : res.code } ;
} ;
if ( params . installKind === "package" ) {
const detected = await detectGlobalInstallManagerForRoot (
runCommand ,
params . root ,
params . timeoutMs ,
) ;
if ( detected ) return detected ;
}
const byPresence = await detectGlobalInstallManagerByPresence ( runCommand , params . timeoutMs ) ;
return byPresence ? ? "npm" ;
}
2026-01-20 14:05:55 +00:00
function formatGitStatusLine ( params : {
branch : string | null ;
tag : string | null ;
sha : string | null ;
} ) : string {
const shortSha = params . sha ? params . sha . slice ( 0 , 8 ) : null ;
const branch = params . branch && params . branch !== "HEAD" ? params.branch : null ;
const tag = params . tag ;
const parts = [
branch ? ? ( tag ? "detached" : "git" ) ,
tag ? ` tag ${ tag } ` : null ,
shortSha ? ` @ ${ shortSha } ` : null ,
] . filter ( Boolean ) ;
return parts . join ( " · " ) ;
}
export async function updateStatusCommand ( opts : UpdateStatusOptions ) : Promise < void > {
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 ;
}
const root =
2026-01-30 03:15:10 +01:00
( await resolveOpenClawPackageRoot ( {
2026-01-20 14:05:55 +00:00
moduleUrl : import.meta.url ,
argv1 : process.argv [ 1 ] ,
cwd : process.cwd ( ) ,
} ) ) ? ? process . cwd ( ) ;
const configSnapshot = await readConfigFileSnapshot ( ) ;
const configChannel = configSnapshot . valid
? normalizeUpdateChannel ( configSnapshot . config . update ? . channel )
: null ;
const update = await checkUpdateStatus ( {
root ,
timeoutMs : timeoutMs ? ? 3500 ,
fetchGit : true ,
includeRegistry : true ,
} ) ;
const channelInfo = resolveEffectiveUpdateChannel ( {
configChannel ,
installKind : update.installKind ,
git : update.git ? { tag : update.git.tag , branch : update.git.branch } : undefined ,
} ) ;
const channelLabel = formatUpdateChannelLabel ( {
channel : channelInfo.channel ,
source : channelInfo.source ,
gitTag : update.git?.tag ? ? null ,
gitBranch : update.git?.branch ? ? null ,
} ) ;
const gitLabel =
update . installKind === "git"
? formatGitStatusLine ( {
branch : update.git?.branch ? ? null ,
tag : update.git?.tag ? ? null ,
sha : update.git?.sha ? ? null ,
} )
: null ;
const updateAvailability = resolveUpdateAvailability ( update ) ;
const updateLine = formatUpdateOneLiner ( update ) . replace ( /^Update:\s*/i , "" ) ;
if ( opts . json ) {
defaultRuntime . log (
JSON . stringify (
{
update ,
channel : {
value : channelInfo.channel ,
source : channelInfo.source ,
label : channelLabel ,
config : configChannel ,
} ,
availability : updateAvailability ,
} ,
null ,
2 ,
) ,
) ;
return ;
}
const tableWidth = Math . max ( 60 , ( process . stdout . columns ? ? 120 ) - 1 ) ;
const installLabel =
update . installKind === "git"
? ` git ( ${ update . root ? ? "unknown" } ) `
: update . installKind === "package"
? update . packageManager
: "unknown" ;
const rows = [
{ Item : "Install" , Value : installLabel } ,
{ Item : "Channel" , Value : channelLabel } ,
. . . ( gitLabel ? [ { Item : "Git" , Value : gitLabel } ] : [ ] ) ,
{
Item : "Update" ,
Value : updateAvailability.available ? theme . warn ( ` available · ${ updateLine } ` ) : updateLine ,
} ,
] ;
2026-01-30 03:15:10 +01:00
defaultRuntime . log ( theme . heading ( "OpenClaw update status" ) ) ;
2026-01-20 14:05:55 +00:00
defaultRuntime . log ( "" ) ;
defaultRuntime . log (
renderTable ( {
width : tableWidth ,
columns : [
{ key : "Item" , header : "Item" , minWidth : 10 } ,
{ key : "Value" , header : "Value" , flex : true , minWidth : 24 } ,
] ,
rows ,
} ) . trimEnd ( ) ,
) ;
defaultRuntime . log ( "" ) ;
const updateHint = formatUpdateAvailableHint ( update ) ;
if ( updateHint ) {
defaultRuntime . log ( theme . warn ( updateHint ) ) ;
}
}
2026-01-11 00:48:39 +01:00
function getStepLabel ( step : UpdateStepInfo ) : string {
2026-01-11 01:21:39 +01:00
return STEP_LABELS [ step . name ] ? ? step . name ;
2026-01-11 00:48:39 +01:00
}
2026-01-11 00:55:04 +01:00
type ProgressController = {
2026-01-11 00:48:39 +01:00
progress : UpdateStepProgress ;
2026-01-11 00:55:04 +01:00
stop : ( ) = > void ;
} ;
function createUpdateProgress ( enabled : boolean ) : ProgressController {
if ( ! enabled ) {
return {
progress : { } ,
stop : ( ) = > { } ,
} ;
}
let currentSpinner : ReturnType < typeof spinner > | null = null ;
2026-01-11 00:48:39 +01:00
const progress : UpdateStepProgress = {
onStepStart : ( step ) = > {
2026-01-11 00:55:04 +01:00
currentSpinner = spinner ( ) ;
currentSpinner . start ( theme . accent ( getStepLabel ( step ) ) ) ;
} ,
onStepComplete : ( step ) = > {
if ( ! currentSpinner ) return ;
const label = getStepLabel ( step ) ;
const duration = theme . muted ( ` ( ${ formatDuration ( step . durationMs ) } ) ` ) ;
2026-01-14 14:31:43 +00:00
const icon = step . exitCode === 0 ? theme . success ( "\u2713" ) : theme . error ( "\u2717" ) ;
2026-01-11 01:06:34 +01:00
currentSpinner . stop ( ` ${ icon } ${ label } ${ duration } ` ) ;
2026-01-11 00:55:04 +01:00
currentSpinner = null ;
2026-01-11 01:26:14 +01:00
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 ) } ` ) ;
}
}
}
2026-01-11 00:48:39 +01:00
} ,
} ;
2026-01-11 00:55:04 +01:00
return {
progress ,
stop : ( ) = > {
if ( currentSpinner ) {
currentSpinner . stop ( ) ;
currentSpinner = null ;
}
} ,
} ;
2026-01-11 00:48:39 +01:00
}
2026-01-10 18:18:10 +00:00
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" ) ;
}
2026-01-22 07:05:00 +00:00
const selectStyled = < T > ( params : Parameters < typeof select < T > > [ 0 ] ) = >
select ( {
. . . params ,
message : stylePromptMessage ( params . message ) ,
options : params.options.map ( ( opt ) = >
opt . hint === undefined ? opt : { . . . opt , hint : stylePromptHint ( opt . hint ) } ,
) ,
} ) ;
2026-01-11 00:58:01 +01:00
type PrintResultOptions = UpdateCommandOptions & {
hideSteps? : boolean ;
} ;
function printResult ( result : UpdateRunResult , opts : PrintResultOptions ) {
2026-01-10 18:18:10 +00:00
if ( opts . json ) {
defaultRuntime . log ( JSON . stringify ( result , null , 2 ) ) ;
return ;
}
const statusColor =
2026-01-14 14:31:43 +00:00
result . status === "ok" ? theme.success : result.status === "skipped" ? theme.warn : theme.error ;
2026-01-10 18:18:10 +00:00
defaultRuntime . log ( "" ) ;
defaultRuntime . log (
` ${ theme . heading ( "Update Result:" ) } ${ statusColor ( result . status . toUpperCase ( ) ) } ` ,
) ;
if ( result . root ) {
defaultRuntime . log ( ` Root: ${ theme . muted ( result . root ) } ` ) ;
}
2026-01-11 00:59:17 +01:00
if ( result . reason ) {
defaultRuntime . log ( ` Reason: ${ theme . muted ( result . reason ) } ` ) ;
}
2026-01-10 18:18:10 +00:00
if ( result . before ? . version || result . before ? . sha ) {
2026-01-14 14:31:43 +00:00
const before = result . before . version ? ? result . before . sha ? . slice ( 0 , 8 ) ? ? "" ;
2026-01-10 18:18:10 +00:00
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 ) } ` ) ;
}
2026-01-11 00:58:01 +01:00
if ( ! opts . hideSteps && result . steps . length > 0 ) {
2026-01-10 18:18:10 +00:00
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 } ` ) ;
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 ( "" ) ;
2026-01-14 14:31:43 +00:00
defaultRuntime . log ( ` Total time: ${ theme . muted ( formatDuration ( result . durationMs ) ) } ` ) ;
2026-01-10 18:18:10 +00:00
}
export async function updateCommand ( opts : UpdateCommandOptions ) : Promise < void > {
2026-01-21 03:40:26 +00:00
process . noDeprecation = true ;
process . env . NODE_NO_WARNINGS = "1" ;
2026-01-14 14:31:43 +00:00
const timeoutMs = opts . timeout ? Number . parseInt ( opts . timeout , 10 ) * 1000 : undefined ;
2026-01-23 11:49:59 +00:00
const shouldRestart = opts . restart !== false ;
2026-01-10 18:18:10 +00:00
if ( timeoutMs !== undefined && ( Number . isNaN ( timeoutMs ) || timeoutMs <= 0 ) ) {
defaultRuntime . error ( "--timeout must be a positive integer (seconds)" ) ;
defaultRuntime . exit ( 1 ) ;
return ;
}
2026-01-10 20:32:15 +01:00
const root =
2026-01-30 03:15:10 +01:00
( await resolveOpenClawPackageRoot ( {
2026-01-10 20:32:15 +01:00
moduleUrl : import.meta.url ,
argv1 : process.argv [ 1 ] ,
cwd : process.cwd ( ) ,
} ) ) ? ? process . cwd ( ) ;
2026-01-21 06:00:50 +00:00
const updateStatus = await checkUpdateStatus ( {
root ,
timeoutMs : timeoutMs ? ? 3500 ,
fetchGit : false ,
includeRegistry : false ,
} ) ;
2026-01-17 11:40:02 +00:00
const configSnapshot = await readConfigFileSnapshot ( ) ;
2026-01-20 15:56:48 +00:00
let activeConfig = configSnapshot . valid ? configSnapshot.config : null ;
2026-01-17 11:40:02 +00:00
const storedChannel = configSnapshot . valid
2026-01-20 13:33:31 +00:00
? normalizeUpdateChannel ( configSnapshot . config . update ? . channel )
2026-01-17 11:40:02 +00:00
: null ;
2026-01-20 13:33:31 +00:00
const requestedChannel = normalizeUpdateChannel ( opts . channel ) ;
2026-01-17 11:40:02 +00:00
if ( opts . channel && ! requestedChannel ) {
2026-01-20 13:33:31 +00:00
defaultRuntime . error ( ` --channel must be "stable", "beta", or "dev" (got " ${ opts . channel } ") ` ) ;
2026-01-17 11:40:02 +00:00
defaultRuntime . exit ( 1 ) ;
return ;
}
if ( opts . channel && ! configSnapshot . valid ) {
const issues = configSnapshot . issues . map ( ( issue ) = > ` - ${ issue . path } : ${ issue . message } ` ) ;
2026-01-17 12:51:08 +00:00
defaultRuntime . error ( [ "Config is invalid; cannot set update channel." , . . . issues ] . join ( "\n" ) ) ;
2026-01-17 11:40:02 +00:00
defaultRuntime . exit ( 1 ) ;
return ;
}
2026-01-21 06:00:50 +00:00
const installKind = updateStatus . installKind ;
const switchToGit = requestedChannel === "dev" && installKind !== "git" ;
const switchToPackage =
requestedChannel !== null && requestedChannel !== "dev" && installKind === "git" ;
const updateInstallKind = switchToGit ? "git" : switchToPackage ? "package" : installKind ;
const defaultChannel =
updateInstallKind === "git" ? DEFAULT_GIT_CHANNEL : DEFAULT_PACKAGE_CHANNEL ;
2026-01-20 13:33:31 +00:00
const channel = requestedChannel ? ? storedChannel ? ? defaultChannel ;
2026-01-20 16:28:25 +00:00
const explicitTag = normalizeTag ( opts . tag ) ;
let tag = explicitTag ? ? channelToNpmTag ( channel ) ;
2026-01-21 06:00:50 +00:00
if ( updateInstallKind !== "git" ) {
const currentVersion = switchToPackage ? null : await readPackageVersion ( root ) ;
2026-01-22 03:14:25 +00:00
let fallbackToLatest = false ;
2026-01-20 16:28:25 +00:00
const targetVersion = explicitTag
? await resolveTargetVersion ( tag , timeoutMs )
: await resolveNpmChannelTag ( { channel , timeoutMs } ) . then ( ( resolved ) = > {
tag = resolved . tag ;
2026-01-22 03:14:25 +00:00
fallbackToLatest = channel === "beta" && resolved . tag === "latest" ;
2026-01-20 16:28:25 +00:00
return resolved . version ;
} ) ;
2026-01-17 11:40:02 +00:00
const cmp =
currentVersion && targetVersion ? compareSemverStrings ( currentVersion , targetVersion ) : null ;
const needsConfirm =
2026-01-22 03:14:25 +00:00
! fallbackToLatest &&
currentVersion != null &&
( targetVersion == null || ( cmp != null && cmp > 0 ) ) ;
2026-01-17 11:40:02 +00:00
2026-01-21 03:39:39 +00:00
if ( needsConfirm && ! opts . yes ) {
2026-01-17 11:40:02 +00:00
if ( ! process . stdin . isTTY || opts . json ) {
defaultRuntime . error (
[
"Downgrade confirmation required." ,
"Downgrading can break configuration. Re-run in a TTY to confirm." ,
] . join ( "\n" ) ,
) ;
defaultRuntime . exit ( 1 ) ;
return ;
}
const targetLabel = targetVersion ? ? ` ${ tag } (unknown) ` ;
const message = ` Downgrading from ${ currentVersion } to ${ targetLabel } can break configuration. Continue? ` ;
const ok = await confirm ( {
message : stylePromptMessage ( message ) ,
initialValue : false ,
} ) ;
if ( isCancel ( ok ) || ok === false ) {
if ( ! opts . json ) {
defaultRuntime . log ( theme . muted ( "Update cancelled." ) ) ;
}
defaultRuntime . exit ( 0 ) ;
return ;
}
}
2026-01-20 13:33:31 +00:00
} else if ( opts . tag && ! opts . json ) {
2026-01-17 11:40:02 +00:00
defaultRuntime . log (
2026-01-20 13:33:31 +00:00
theme . muted ( "Note: --tag applies to npm installs only; git updates ignore it." ) ,
2026-01-17 11:40:02 +00:00
) ;
}
if ( requestedChannel && configSnapshot . valid ) {
const next = {
. . . configSnapshot . config ,
update : {
. . . configSnapshot . config . update ,
channel : requestedChannel ,
} ,
} ;
await writeConfigFile ( next ) ;
2026-01-20 15:56:48 +00:00
activeConfig = next ;
2026-01-17 11:40:02 +00:00
if ( ! opts . json ) {
defaultRuntime . log ( theme . muted ( ` Update channel set to ${ requestedChannel } . ` ) ) ;
}
}
const showProgress = ! opts . json && process . stdout . isTTY ;
if ( ! opts . json ) {
2026-01-30 03:15:10 +01:00
defaultRuntime . log ( theme . heading ( "Updating OpenClaw..." ) ) ;
2026-01-17 11:40:02 +00:00
defaultRuntime . log ( "" ) ;
}
2026-01-11 00:55:04 +01:00
const { progress , stop } = createUpdateProgress ( showProgress ) ;
2026-01-11 00:48:39 +01:00
2026-01-21 06:00:50 +00:00
const startedAt = Date . now ( ) ;
let result : UpdateRunResult ;
if ( switchToPackage ) {
const manager = await resolveGlobalManager ( {
root ,
installKind ,
timeoutMs : timeoutMs ? ? 20 * 60 _000 ,
} ) ;
const runCommand = async ( argv : string [ ] , options : { timeoutMs : number } ) = > {
const res = await runCommandWithTimeout ( argv , options ) ;
return { stdout : res.stdout , stderr : res.stderr , code : res.code } ;
} ;
const pkgRoot = await resolveGlobalPackageRoot ( manager , runCommand , timeoutMs ? ? 20 * 60 _000 ) ;
2026-01-27 11:27:41 +00:00
const packageName =
( pkgRoot ? await readPackageName ( pkgRoot ) : await readPackageName ( root ) ) ? ?
DEFAULT_PACKAGE_NAME ;
2026-01-21 06:00:50 +00:00
const beforeVersion = pkgRoot ? await readPackageVersion ( pkgRoot ) : null ;
const updateStep = await runUpdateStep ( {
name : "global update" ,
2026-01-27 11:27:41 +00:00
argv : globalInstallArgs ( manager , ` ${ packageName } @ ${ tag } ` ) ,
2026-01-21 06:00:50 +00:00
timeoutMs : timeoutMs ? ? 20 * 60 _000 ,
progress ,
} ) ;
const steps = [ updateStep ] ;
let afterVersion = beforeVersion ;
if ( pkgRoot ) {
afterVersion = await readPackageVersion ( pkgRoot ) ;
const entryPath = path . join ( pkgRoot , "dist" , "entry.js" ) ;
if ( await pathExists ( entryPath ) ) {
const doctorStep = await runUpdateStep ( {
2026-01-27 11:27:41 +00:00
name : ` ${ CLI_NAME } doctor ` ,
2026-01-21 06:00:50 +00:00
argv : [ resolveNodeRunner ( ) , entryPath , "doctor" , "--non-interactive" ] ,
timeoutMs : timeoutMs ? ? 20 * 60 _000 ,
progress ,
} ) ;
steps . push ( doctorStep ) ;
}
}
const failedStep = steps . find ( ( step ) = > step . exitCode !== 0 ) ;
result = {
status : failedStep ? "error" : "ok" ,
mode : manager ,
root : pkgRoot ? ? root ,
reason : failedStep ? failedStep.name : undefined ,
before : { version : beforeVersion } ,
after : { version : afterVersion } ,
steps ,
durationMs : Date.now ( ) - startedAt ,
} ;
} else {
const updateRoot = switchToGit ? resolveGitInstallDir ( ) : root ;
const cloneStep = switchToGit
? await ensureGitCheckout ( {
dir : updateRoot ,
timeoutMs : timeoutMs ? ? 20 * 60 _000 ,
progress ,
} )
: null ;
if ( cloneStep && cloneStep . exitCode !== 0 ) {
result = {
status : "error" ,
mode : "git" ,
root : updateRoot ,
reason : cloneStep.name ,
steps : [ cloneStep ] ,
durationMs : Date.now ( ) - startedAt ,
} ;
stop ( ) ;
printResult ( result , { . . . opts , hideSteps : showProgress } ) ;
defaultRuntime . exit ( 1 ) ;
return ;
}
const updateResult = await runGatewayUpdate ( {
cwd : updateRoot ,
argv1 : switchToGit ? undefined : process . argv [ 1 ] ,
timeoutMs ,
progress ,
channel ,
tag ,
} ) ;
const steps = [ . . . ( cloneStep ? [ cloneStep ] : [ ] ) , . . . updateResult . steps ] ;
if ( switchToGit && updateResult . status === "ok" ) {
const manager = await resolveGlobalManager ( {
root ,
installKind ,
timeoutMs : timeoutMs ? ? 20 * 60 _000 ,
} ) ;
const installStep = await runUpdateStep ( {
name : "global install" ,
argv : globalInstallArgs ( manager , updateRoot ) ,
cwd : updateRoot ,
timeoutMs : timeoutMs ? ? 20 * 60 _000 ,
progress ,
} ) ;
steps . push ( installStep ) ;
const failedStep = [ installStep ] . find ( ( step ) = > step . exitCode !== 0 ) ;
result = {
. . . updateResult ,
status : updateResult.status === "ok" && ! failedStep ? "ok" : "error" ,
steps ,
durationMs : Date.now ( ) - startedAt ,
} ;
} else {
result = {
. . . updateResult ,
steps ,
durationMs : Date.now ( ) - startedAt ,
} ;
}
}
2026-01-10 18:18:10 +00:00
2026-01-11 00:55:04 +01:00
stop ( ) ;
2026-01-11 00:48:39 +01:00
2026-01-11 00:58:01 +01:00
printResult ( result , { . . . opts , hideSteps : showProgress } ) ;
2026-01-10 18:18:10 +00:00
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." ,
) ,
) ;
}
2026-01-10 20:32:15 +01:00
if ( result . reason === "not-git-install" ) {
defaultRuntime . log (
theme . warn (
2026-01-30 03:15:10 +01:00
` Skipped: this OpenClaw install isn't a git checkout, and the package manager couldn't be detected. Update via your package manager, then run \` ${ replaceCliName ( formatCliCommand ( "openclaw doctor" ) , CLI_NAME ) } \` and \` ${ replaceCliName ( formatCliCommand ( "openclaw gateway restart" ) , CLI_NAME ) } \` . ` ,
2026-01-10 20:32:15 +01:00
) ,
) ;
defaultRuntime . log (
2026-01-27 11:27:41 +00:00
theme . muted (
2026-01-30 03:15:10 +01:00
` Examples: \` ${ replaceCliName ( "npm i -g openclaw@latest" , CLI_NAME ) } \` or \` ${ replaceCliName ( "pnpm add -g openclaw@latest" , CLI_NAME ) } \` ` ,
2026-01-27 11:27:41 +00:00
) ,
2026-01-10 20:32:15 +01:00
) ;
}
2026-01-10 18:18:10 +00:00
defaultRuntime . exit ( 0 ) ;
return ;
}
2026-01-20 15:56:48 +00:00
if ( activeConfig ) {
const pluginLogger = opts . json
? { }
: {
info : ( msg : string ) = > defaultRuntime . log ( msg ) ,
warn : ( msg : string ) = > defaultRuntime . log ( theme . warn ( msg ) ) ,
error : ( msg : string ) = > defaultRuntime . log ( theme . error ( msg ) ) ,
} ;
if ( ! opts . json ) {
defaultRuntime . log ( "" ) ;
defaultRuntime . log ( theme . heading ( "Updating plugins..." ) ) ;
}
const syncResult = await syncPluginsForUpdateChannel ( {
config : activeConfig ,
channel ,
workspaceDir : root ,
logger : pluginLogger ,
} ) ;
let pluginConfig = syncResult . config ;
const npmResult = await updateNpmInstalledPlugins ( {
config : pluginConfig ,
skipIds : new Set ( syncResult . summary . switchedToNpm ) ,
logger : pluginLogger ,
} ) ;
pluginConfig = npmResult . config ;
if ( syncResult . changed || npmResult . changed ) {
await writeConfigFile ( pluginConfig ) ;
}
if ( ! opts . json ) {
const summarizeList = ( list : string [ ] ) = > {
if ( list . length <= 6 ) return list . join ( ", " ) ;
return ` ${ list . slice ( 0 , 6 ) . join ( ", " ) } + ${ list . length - 6 } more ` ;
} ;
if ( syncResult . summary . switchedToBundled . length > 0 ) {
defaultRuntime . log (
theme . muted (
` Switched to bundled plugins: ${ summarizeList ( syncResult . summary . switchedToBundled ) } . ` ,
) ,
) ;
}
if ( syncResult . summary . switchedToNpm . length > 0 ) {
defaultRuntime . log (
theme . muted ( ` Restored npm plugins: ${ summarizeList ( syncResult . summary . switchedToNpm ) } . ` ) ,
) ;
}
for ( const warning of syncResult . summary . warnings ) {
defaultRuntime . log ( theme . warn ( warning ) ) ;
}
for ( const error of syncResult . summary . errors ) {
defaultRuntime . log ( theme . error ( error ) ) ;
}
const updated = npmResult . outcomes . filter ( ( entry ) = > entry . status === "updated" ) . length ;
const unchanged = npmResult . outcomes . filter ( ( entry ) = > entry . status === "unchanged" ) . length ;
const failed = npmResult . outcomes . filter ( ( entry ) = > entry . status === "error" ) . length ;
const skipped = npmResult . outcomes . filter ( ( entry ) = > entry . status === "skipped" ) . length ;
if ( npmResult . outcomes . length === 0 ) {
defaultRuntime . log ( theme . muted ( "No plugin updates needed." ) ) ;
} else {
const parts = [ ` ${ updated } updated ` , ` ${ unchanged } unchanged ` ] ;
if ( failed > 0 ) parts . push ( ` ${ failed } failed ` ) ;
if ( skipped > 0 ) parts . push ( ` ${ skipped } skipped ` ) ;
defaultRuntime . log ( theme . muted ( ` npm plugins: ${ parts . join ( ", " ) } . ` ) ) ;
}
for ( const outcome of npmResult . outcomes ) {
if ( outcome . status !== "error" ) continue ;
defaultRuntime . log ( theme . error ( outcome . message ) ) ;
}
}
} else if ( ! opts . json ) {
defaultRuntime . log ( theme . warn ( "Skipping plugin updates: config is invalid." ) ) ;
}
2026-01-21 17:45:06 +00:00
// Restart service if requested
2026-01-23 11:49:59 +00:00
if ( shouldRestart ) {
2026-01-10 18:18:10 +00:00
if ( ! opts . json ) {
defaultRuntime . log ( "" ) ;
2026-01-21 17:45:06 +00:00
defaultRuntime . log ( theme . heading ( "Restarting service..." ) ) ;
2026-01-10 18:18:10 +00:00
}
try {
2026-01-10 23:39:30 +01:00
const { runDaemonRestart } = await import ( "./daemon-cli.js" ) ;
2026-01-10 22:38:01 +01:00
const restarted = await runDaemonRestart ( ) ;
if ( ! opts . json && restarted ) {
2026-01-10 18:18:10 +00:00
defaultRuntime . log ( theme . success ( "Daemon restarted successfully." ) ) ;
2026-01-10 23:14:55 +01:00
defaultRuntime . log ( "" ) ;
2026-01-30 03:15:10 +01:00
process . env . OPENCLAW_UPDATE_IN_PROGRESS = "1" ;
2026-01-10 23:14:55 +01:00
try {
2026-01-10 23:39:30 +01:00
const { doctorCommand } = await import ( "../commands/doctor.js" ) ;
2026-01-21 05:23:22 +00:00
const interactiveDoctor = Boolean ( process . stdin . isTTY ) && ! opts . json && opts . yes !== true ;
await doctorCommand ( defaultRuntime , { nonInteractive : ! interactiveDoctor } ) ;
2026-01-10 23:14:55 +01:00
} catch ( err ) {
2026-01-10 23:23:23 +01:00
defaultRuntime . log ( theme . warn ( ` Doctor failed: ${ String ( err ) } ` ) ) ;
2026-01-10 23:14:55 +01:00
} finally {
2026-01-30 03:15:10 +01:00
delete process . env . OPENCLAW_UPDATE_IN_PROGRESS ;
2026-01-10 23:14:55 +01:00
}
2026-01-10 18:18:10 +00:00
}
} catch ( err ) {
if ( ! opts . json ) {
2026-01-10 20:32:15 +01:00
defaultRuntime . log ( theme . warn ( ` Daemon restart failed: ${ String ( err ) } ` ) ) ;
2026-01-10 18:18:10 +00:00
defaultRuntime . log (
2026-01-20 07:42:21 +00:00
theme . muted (
2026-01-30 03:15:10 +01:00
` You may need to restart the service manually: ${ replaceCliName ( formatCliCommand ( "openclaw gateway restart" ) , CLI_NAME ) } ` ,
2026-01-20 07:42:21 +00:00
) ,
2026-01-10 18:18:10 +00:00
) ;
}
}
} else if ( ! opts . json ) {
defaultRuntime . log ( "" ) ;
2026-01-16 11:45:37 +00:00
if ( result . mode === "npm" || result . mode === "pnpm" ) {
defaultRuntime . log (
theme . muted (
2026-01-30 03:15:10 +01:00
` Tip: Run \` ${ replaceCliName ( formatCliCommand ( "openclaw doctor" ) , CLI_NAME ) } \` , then \` ${ replaceCliName ( formatCliCommand ( "openclaw gateway restart" ) , CLI_NAME ) } \` to apply updates to a running gateway. ` ,
2026-01-16 11:45:37 +00:00
) ,
) ;
} else {
defaultRuntime . log (
2026-01-20 07:42:21 +00:00
theme . muted (
2026-01-30 03:15:10 +01:00
` Tip: Run \` ${ replaceCliName ( formatCliCommand ( "openclaw gateway restart" ) , CLI_NAME ) } \` to apply updates to a running gateway. ` ,
2026-01-20 07:42:21 +00:00
) ,
2026-01-16 11:45:37 +00:00
) ;
}
2026-01-10 18:18:10 +00:00
}
2026-01-17 20:12:23 +00:00
if ( ! opts . json ) {
defaultRuntime . log ( theme . muted ( pickUpdateQuip ( ) ) ) ;
}
2026-01-10 18:18:10 +00:00
}
2026-01-22 07:05:00 +00:00
export async function updateWizardCommand ( opts : UpdateWizardOptions = { } ) : Promise < void > {
if ( ! process . stdin . isTTY ) {
defaultRuntime . error (
2026-01-30 03:15:10 +01:00
"Update wizard requires a TTY. Use `openclaw update --channel <stable|beta|dev>` instead." ,
2026-01-22 07:05:00 +00:00
) ;
defaultRuntime . exit ( 1 ) ;
return ;
}
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 ;
}
const root =
2026-01-30 03:15:10 +01:00
( await resolveOpenClawPackageRoot ( {
2026-01-22 07:05:00 +00:00
moduleUrl : import.meta.url ,
argv1 : process.argv [ 1 ] ,
cwd : process.cwd ( ) ,
} ) ) ? ? process . cwd ( ) ;
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 (
2026-01-30 03:15:10 +01:00
` OPENCLAW_GIT_DIR points at a non-git directory: ${ gitDir } . Set OPENCLAW_GIT_DIR to an empty folder or an openclaw checkout. ` ,
2026-01-22 07:05:00 +00:00
) ;
defaultRuntime . exit ( 1 ) ;
return ;
}
}
const ok = await confirm ( {
message : stylePromptMessage (
2026-01-30 03:15:10 +01:00
` Create a git checkout at ${ gitDir } ? (override via OPENCLAW_GIT_DIR) ` ,
2026-01-22 07:05:00 +00:00
) ,
initialValue : true ,
} ) ;
if ( isCancel ( ok ) || ok === false ) {
defaultRuntime . log ( theme . muted ( "Update cancelled." ) ) ;
defaultRuntime . exit ( 0 ) ;
return ;
}
}
}
const restart = await confirm ( {
message : stylePromptMessage ( "Restart the gateway service after update?" ) ,
2026-01-23 11:49:59 +00:00
initialValue : true ,
2026-01-22 07:05:00 +00:00
} ) ;
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 ) ;
}
}
2026-01-10 18:18:10 +00:00
export function registerUpdateCli ( program : Command ) {
2026-01-20 14:05:55 +00:00
const update = program
2026-01-10 18:18:10 +00:00
. command ( "update" )
2026-01-30 03:15:10 +01:00
. description ( "Update OpenClaw to the latest version" )
2026-01-10 18:18:10 +00:00
. option ( "--json" , "Output result as JSON" , false )
2026-01-23 11:49:59 +00:00
. option ( "--no-restart" , "Skip restarting the gateway service after a successful update" )
2026-01-20 13:33:31 +00:00
. option ( "--channel <stable|beta|dev>" , "Persist update channel (git + npm)" )
2026-01-17 11:40:02 +00:00
. option ( "--tag <dist-tag|version>" , "Override npm dist-tag or version for this update" )
2026-01-14 14:31:43 +00:00
. option ( "--timeout <seconds>" , "Timeout for each update step in seconds (default: 1200)" )
2026-01-21 03:39:39 +00:00
. option ( "--yes" , "Skip confirmation prompts (non-interactive)" , false )
2026-01-21 04:08:46 +00:00
. addHelpText ( "after" , ( ) = > {
const examples = [
2026-01-30 03:15:10 +01:00
[ "openclaw update" , "Update a source checkout (git)" ] ,
[ "openclaw update --channel beta" , "Switch to beta channel (git + npm)" ] ,
[ "openclaw update --channel dev" , "Switch to dev channel (git + npm)" ] ,
[ "openclaw update --tag beta" , "One-off update to a dist-tag or version" ] ,
[ "openclaw update --no-restart" , "Update without restarting the service" ] ,
[ "openclaw update --json" , "Output result as JSON" ] ,
[ "openclaw update --yes" , "Non-interactive (accept downgrade prompts)" ] ,
[ "openclaw update wizard" , "Interactive update wizard" ] ,
[ "openclaw --update" , "Shorthand for openclaw update" ] ,
2026-01-21 04:08:46 +00:00
] as const ;
const fmtExamples = examples
. map ( ( [ cmd , desc ] ) = > ` ${ theme . command ( cmd ) } ${ theme . muted ( ` # ${ desc } ` ) } ` )
. join ( "\n" ) ;
return `
$ { theme . heading ( "What this does:" ) }
2026-01-21 03:39:39 +00:00
- Git checkouts : fetches , rebases , installs deps , builds , and runs doctor
- npm installs : updates via detected package manager
2026-01-21 04:08:46 +00:00
$ { theme . heading ( "Switch channels:" ) }
2026-01-21 03:49:13 +00:00
- Use -- channel stable | beta | dev to persist the update channel in config
2026-01-30 03:15:10 +01:00
- Run openclaw update status to see the active channel and source
2026-01-21 03:49:13 +00:00
- Use -- tag < dist - tag | version > for a one - off npm update without persisting
2026-01-21 04:08:46 +00:00
$ { theme . heading ( "Non-interactive:" ) }
2026-01-21 03:49:13 +00:00
- Use -- yes to accept downgrade prompts
- Combine with -- channel / -- tag / -- restart / -- json / -- timeout as needed
2026-01-21 04:08:46 +00:00
$ { theme . heading ( "Examples:" ) }
$ { fmtExamples }
$ { theme . heading ( "Notes:" ) }
2026-01-21 03:39:39 +00:00
- Switch channels with -- channel stable | beta | dev
2026-01-16 11:45:37 +00:00
- For global installs : auto - updates via detected package manager when possible ( see docs / install / updating . md )
2026-01-17 11:40:02 +00:00
- Downgrades require confirmation ( can break configuration )
2026-01-10 18:18:10 +00:00
- Skips update if the working directory has uncommitted changes
2026-01-10 20:50:17 +01:00
2026-01-30 03:15:10 +01:00
$ { theme . muted ( "Docs:" ) } $ { formatDocsLink ( "/cli/update" , "docs.openclaw.ai/cli/update" ) } ` ;
2026-01-21 04:08:46 +00:00
} )
2026-01-10 18:18:10 +00:00
. action ( async ( opts ) = > {
try {
await updateCommand ( {
json : Boolean ( opts . json ) ,
restart : Boolean ( opts . restart ) ,
2026-01-17 11:40:02 +00:00
channel : opts.channel as string | undefined ,
tag : opts.tag as string | undefined ,
2026-01-10 18:18:10 +00:00
timeout : opts.timeout as string | undefined ,
2026-01-21 03:39:39 +00:00
yes : Boolean ( opts . yes ) ,
2026-01-10 18:18:10 +00:00
} ) ;
} catch ( err ) {
defaultRuntime . error ( String ( err ) ) ;
defaultRuntime . exit ( 1 ) ;
}
} ) ;
2026-01-20 14:05:55 +00:00
2026-01-22 07:05:00 +00:00
update
. command ( "wizard" )
. description ( "Interactive update wizard" )
. option ( "--timeout <seconds>" , "Timeout for each update step in seconds (default: 1200)" )
. addHelpText (
"after" ,
2026-01-30 03:15:10 +01:00
` \ n ${ theme . muted ( "Docs:" ) } ${ formatDocsLink ( "/cli/update" , "docs.openclaw.ai/cli/update" ) } \ n ` ,
2026-01-22 07:05:00 +00:00
)
. action ( async ( opts ) = > {
try {
await updateWizardCommand ( { timeout : opts.timeout as string | undefined } ) ;
} catch ( err ) {
defaultRuntime . error ( String ( err ) ) ;
defaultRuntime . exit ( 1 ) ;
}
} ) ;
2026-01-20 14:05:55 +00:00
update
. command ( "status" )
. description ( "Show update channel and version status" )
. option ( "--json" , "Output result as JSON" , false )
. option ( "--timeout <seconds>" , "Timeout for update checks in seconds (default: 3)" )
. addHelpText (
"after" ,
( ) = >
2026-01-21 04:46:15 +00:00
` \ n ${ theme . heading ( "Examples:" ) } \ n ${ formatHelpExamples ( [
2026-01-30 03:15:10 +01:00
[ "openclaw update status" , "Show channel + version status." ] ,
[ "openclaw update status --json" , "JSON output." ] ,
[ "openclaw update status --timeout 10" , "Custom timeout." ] ,
2026-01-21 04:46:15 +00:00
] ) } \ n \ n $ { theme . heading ( "Notes:" ) } \ n $ { theme . muted (
"- Shows current update channel (stable/beta/dev) and source" ,
) } \ n $ { theme . muted ( "- Includes git tag/branch/SHA for source checkouts" ) } \ n \ n $ { theme . muted (
"Docs:" ,
2026-01-30 03:15:10 +01:00
) } $ { formatDocsLink ( "/cli/update" , "docs.openclaw.ai/cli/update" ) } ` ,
2026-01-20 14:05:55 +00:00
)
. action ( async ( opts ) = > {
try {
await updateStatusCommand ( {
json : Boolean ( opts . json ) ,
timeout : opts.timeout as string | undefined ,
} ) ;
} catch ( err ) {
defaultRuntime . error ( String ( err ) ) ;
defaultRuntime . exit ( 1 ) ;
}
} ) ;
2026-01-10 18:18:10 +00:00
}