2026-01-11 00:55:04 +01:00
import { spinner } from "@clack/prompts" ;
2026-01-10 18:18:10 +00:00
import type { Command } from "commander" ;
2026-01-10 20:32:15 +01:00
import { resolveClawdbotPackageRoot } from "../infra/clawdbot-root.js" ;
2026-01-10 18:18:10 +00:00
import {
runGatewayUpdate ,
type UpdateRunResult ,
2026-01-11 00:48:39 +01:00
type UpdateStepInfo ,
type UpdateStepProgress ,
2026-01-10 18:18:10 +00:00
} from "../infra/update-runner.js" ;
import { defaultRuntime } from "../runtime.js" ;
2026-01-10 20:50:17 +01:00
import { formatDocsLink } from "../terminal/links.js" ;
2026-01-10 18:18:10 +00:00
import { theme } from "../terminal/theme.js" ;
export type UpdateCommandOptions = {
json? : boolean ;
restart? : boolean ;
timeout? : string ;
} ;
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" ,
"git rebase" : "Rebasing onto upstream" ,
"deps install" : "Installing dependencies" ,
build : "Building" ,
"ui:build" : "Building UI" ,
"clawdbot doctor" : "Running doctor checks" ,
"git rev-parse HEAD (after)" : "Verifying update" ,
2026-01-16 11:45:37 +00:00
"global update" : "Updating via package manager" ,
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-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-14 14:31:43 +00:00
const timeoutMs = opts . timeout ? Number . parseInt ( opts . timeout , 10 ) * 1000 : undefined ;
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-11 03:03:09 +01:00
const showProgress = ! opts . json && process . stdout . isTTY ;
2026-01-11 00:48:39 +01:00
2026-01-10 18:18:10 +00:00
if ( ! opts . json ) {
defaultRuntime . log ( theme . heading ( "Updating Clawdbot..." ) ) ;
defaultRuntime . log ( "" ) ;
}
2026-01-10 20:32:15 +01:00
const root =
( await resolveClawdbotPackageRoot ( {
moduleUrl : import.meta.url ,
argv1 : process.argv [ 1 ] ,
cwd : process.cwd ( ) ,
} ) ) ? ? process . cwd ( ) ;
2026-01-11 00:55:04 +01:00
const { progress , stop } = createUpdateProgress ( showProgress ) ;
2026-01-11 00:48:39 +01:00
2026-01-10 18:18:10 +00:00
const result = await runGatewayUpdate ( {
2026-01-10 20:32:15 +01:00
cwd : root ,
2026-01-10 18:18:10 +00:00
argv1 : process.argv [ 1 ] ,
timeoutMs ,
2026-01-11 00:48:39 +01:00
progress ,
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-16 11:45:37 +00:00
"Skipped: this Clawdbot install isn't a git checkout, and the package manager couldn't be detected. Update via your package manager, then run `clawdbot doctor` and `clawdbot daemon restart`." ,
2026-01-10 20:32:15 +01:00
) ,
) ;
defaultRuntime . log (
theme . muted (
2026-01-16 11:45:37 +00:00
"Examples: `npm i -g clawdbot@latest` or `pnpm add -g clawdbot@latest`" ,
2026-01-10 20:32:15 +01:00
) ,
) ;
}
2026-01-10 18:18:10 +00:00
defaultRuntime . exit ( 0 ) ;
return ;
}
// Restart daemon if requested
if ( opts . restart ) {
if ( ! opts . json ) {
defaultRuntime . log ( "" ) ;
defaultRuntime . log ( theme . heading ( "Restarting daemon..." ) ) ;
}
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 ( "" ) ;
process . env . CLAWDBOT_UPDATE_IN_PROGRESS = "1" ;
try {
2026-01-10 23:39:30 +01:00
const { doctorCommand } = await import ( "../commands/doctor.js" ) ;
2026-01-10 23:14:55 +01:00
await doctorCommand ( defaultRuntime , { nonInteractive : true } ) ;
} 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 {
delete process . env . CLAWDBOT_UPDATE_IN_PROGRESS ;
}
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-14 14:31:43 +00:00
theme . muted ( "You may need to restart the daemon manually: clawdbot daemon restart" ) ,
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 (
"Tip: Run `clawdbot doctor`, then `clawdbot daemon restart` to apply updates to a running gateway." ,
) ,
) ;
} else {
defaultRuntime . log (
theme . muted ( "Tip: Run `clawdbot daemon restart` to apply updates to a running gateway." ) ,
) ;
}
2026-01-10 18:18:10 +00:00
}
}
export function registerUpdateCli ( program : Command ) {
program
. command ( "update" )
. description ( "Update Clawdbot to the latest version" )
. option ( "--json" , "Output result as JSON" , false )
2026-01-14 14:31:43 +00:00
. option ( "--restart" , "Restart the gateway daemon after a successful update" , false )
. option ( "--timeout <seconds>" , "Timeout for each update step in seconds (default: 1200)" )
2026-01-10 18:18:10 +00:00
. addHelpText (
"after" ,
2026-01-10 20:50:17 +01:00
( ) = >
`
2026-01-10 18:18:10 +00:00
Examples :
2026-01-10 20:32:15 +01:00
clawdbot update # Update a source checkout ( git )
2026-01-10 18:18:10 +00:00
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
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-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-15 06:12:54 +00:00
$ { theme . muted ( "Docs:" ) } $ { formatDocsLink ( "/cli/update" , "docs.clawd.bot/cli/update" ) } ` ,
2026-01-10 18:18:10 +00:00
)
. 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 ) ;
}
} ) ;
}