2026-01-17 11:40:02 +00:00
import { confirm , isCancel , spinner } from "@clack/prompts" ;
import fs from "node:fs/promises" ;
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-10 20:32:15 +01:00
import { resolveClawdbotPackageRoot } from "../infra/clawdbot-root.js" ;
2026-01-20 14:05:55 +00:00
import {
checkUpdateStatus ,
compareSemverStrings ,
fetchNpmTagVersion ,
} 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 ,
type UpdateStepProgress ,
2026-01-10 18:18:10 +00:00
} from "../infra/update-runner.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-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-17 11:40:02 +00:00
import { 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" ;
import {
formatUpdateAvailableHint ,
formatUpdateOneLiner ,
resolveUpdateAvailability ,
} from "../commands/status.update.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-20 14:05:55 +00:00
export type UpdateStatusOptions = {
json? : boolean ;
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" ,
"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
} ;
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
function normalizeTag ( value? : string | null ) : string | null {
if ( ! value ) return null ;
const trimmed = value . trim ( ) ;
if ( ! trimmed ) return null ;
return trimmed . startsWith ( "clawdbot@" ) ? trimmed . slice ( "clawdbot@" . length ) : trimmed ;
}
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-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 =
( await resolveClawdbotPackageRoot ( {
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 ,
} ,
] ;
defaultRuntime . log ( theme . heading ( "Clawdbot update status" ) ) ;
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-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-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-17 11:40:02 +00:00
const configSnapshot = await readConfigFileSnapshot ( ) ;
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 ;
}
const gitCheckout = await isGitCheckout ( root ) ;
2026-01-20 13:33:31 +00:00
const defaultChannel = gitCheckout ? DEFAULT_GIT_CHANNEL : DEFAULT_PACKAGE_CHANNEL ;
const channel = requestedChannel ? ? storedChannel ? ? defaultChannel ;
const tag = normalizeTag ( opts . tag ) ? ? channelToNpmTag ( channel ) ;
2026-01-17 11:40:02 +00:00
if ( ! gitCheckout ) {
const currentVersion = await readPackageVersion ( root ) ;
const targetVersion = await resolveTargetVersion ( tag , timeoutMs ) ;
const cmp =
currentVersion && targetVersion ? compareSemverStrings ( currentVersion , targetVersion ) : null ;
const needsConfirm =
currentVersion != null && ( targetVersion == null || ( cmp != null && cmp > 0 ) ) ;
if ( needsConfirm ) {
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 ) ;
if ( ! opts . json ) {
defaultRuntime . log ( theme . muted ( ` Update channel set to ${ requestedChannel } . ` ) ) ;
}
}
const showProgress = ! opts . json && process . stdout . isTTY ;
if ( ! opts . json ) {
defaultRuntime . log ( theme . heading ( "Updating Clawdbot..." ) ) ;
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-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-20 13:33:31 +00:00
channel ,
2026-01-17 11:40:02 +00:00
tag ,
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-20 07:42:21 +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 \` ${ formatCliCommand ( "clawdbot doctor" ) } \` and \` ${ formatCliCommand ( "clawdbot daemon restart" ) } \` . ` ,
2026-01-10 20:32:15 +01:00
) ,
) ;
defaultRuntime . log (
2026-01-16 16:01:59 +00:00
theme . muted ( "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-20 07:42:21 +00:00
theme . muted (
` You may need to restart the daemon manually: ${ formatCliCommand ( "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 (
2026-01-20 07:42:21 +00:00
` Tip: Run \` ${ formatCliCommand ( "clawdbot doctor" ) } \` , then \` ${ formatCliCommand ( "clawdbot daemon restart" ) } \` 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 (
` Tip: Run \` ${ formatCliCommand ( "clawdbot daemon restart" ) } \` to apply updates to a running gateway. ` ,
) ,
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
}
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" )
. 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 )
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-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-20 13:33:31 +00:00
clawdbot update -- channel beta # Switch to beta channel ( git + npm )
clawdbot update -- channel dev # Switch to dev channel ( git + npm )
2026-01-17 11:40:02 +00:00
clawdbot update -- tag beta # One - off update to a dist - tag or version
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-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-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 ) ,
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 ,
} ) ;
} 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" ,
( ) = >
`
Examples :
clawdbot update status
clawdbot update status -- json
clawdbot update status -- timeout 10
Notes :
- Shows current update channel ( stable / beta / dev ) and source
- Includes git tag / branch / SHA for source checkouts
$ { theme . muted ( "Docs:" ) } $ { formatDocsLink ( "/cli/update" , "docs.clawd.bot/cli/update" ) } ` ,
)
. 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
}