2026-03-03 12:24:43 -08:00
import { spawn , type StdioOptions } from "node:child_process" ;
2026-03-15 04:16:01 -07:00
import {
cpSync ,
existsSync ,
mkdirSync ,
readFileSync ,
readdirSync ,
realpathSync ,
rmSync ,
writeFileSync ,
} from "node:fs" ;
2026-03-01 16:11:40 -08:00
import path from "node:path" ;
import process from "node:process" ;
2026-03-15 04:16:01 -07:00
import { confirm , isCancel , select , spinner , text } from "@clack/prompts" ;
import json5 from "json5" ;
2026-03-01 16:11:40 -08:00
import { isTruthyEnvValue } from "../infra/env.js" ;
import { defaultRuntime , type RuntimeEnv } from "../runtime.js" ;
2026-03-04 17:33:27 -08:00
import { readTelemetryConfig , markNoticeShown } from "../telemetry/config.js" ;
import { track } from "../telemetry/telemetry.js" ;
2026-03-01 16:11:40 -08:00
import { stylePromptMessage } from "../terminal/prompt-style.js" ;
import { theme } from "../terminal/theme.js" ;
2026-03-04 16:32:58 -08:00
import { VERSION } from "../version.js" ;
2026-03-15 04:16:01 -07:00
import {
buildDenchCloudConfigPatch ,
DEFAULT_DENCH_CLOUD_GATEWAY_URL ,
fetchDenchCloudCatalog ,
formatDenchCloudModelHint ,
normalizeDenchGatewayUrl ,
readConfiguredDenchCloudSettings ,
RECOMMENDED_DENCH_CLOUD_MODEL_ID ,
resolveDenchCloudModel ,
validateDenchCloudApiKey ,
type DenchCloudCatalogLoadResult ,
type DenchCloudCatalogModel ,
} from "./dench-cloud.js" ;
2026-03-01 16:11:40 -08:00
import { applyCliProfileEnv } from "./profile.js" ;
2026-03-04 16:32:58 -08:00
import {
DEFAULT_WEB_APP_PORT ,
ensureManagedWebRuntime ,
resolveCliPackageRoot ,
resolveProfileStateDir ,
} from "./web-runtime.js" ;
2026-03-02 18:31:15 -08:00
import { seedWorkspaceFromAssets , type WorkspaceSeedResult } from "./workspace-seed.js" ;
2026-03-01 16:11:40 -08:00
2026-03-04 13:18:39 -08:00
const DEFAULT_DENCHCLAW_PROFILE = "dench" ;
const DENCHCLAW_GATEWAY_PORT_START = 19001 ;
2026-03-03 12:24:43 -08:00
const MAX_PORT_SCAN_ATTEMPTS = 100 ;
2026-03-01 16:11:40 -08:00
const DEFAULT_BOOTSTRAP_ROLLOUT_STAGE = "default" ;
const DEFAULT_GATEWAY_LAUNCH_AGENT_LABEL = "ai.openclaw.gateway" ;
2026-03-04 13:18:39 -08:00
const REQUIRED_TOOLS_PROFILE = "full" ;
2026-03-04 16:32:58 -08:00
const OPENCLAW_CLI_CHECK_CACHE_TTL_MS = 5 * 60 _000 ;
const OPENCLAW_UPDATE_PROMPT_SUPPRESS_AFTER_INSTALL_MS = 5 * 60 _000 ;
const OPENCLAW_CLI_CHECK_CACHE_FILE = "openclaw-cli-check.json" ;
const OPENCLAW_SETUP_PROGRESS_BAR_WIDTH = 16 ;
2026-03-01 16:11:40 -08:00
type BootstrapRolloutStage = "internal" | "beta" | "default" ;
type BootstrapCheckStatus = "pass" | "warn" | "fail" ;
export type BootstrapCheck = {
id :
| "openclaw-cli"
| "profile"
| "gateway"
2026-03-02 18:31:15 -08:00
| "agent-auth"
2026-03-01 16:11:40 -08:00
| "web-ui"
| "state-isolation"
| "daemon-label"
| "rollout-stage"
2026-03-05 10:46:03 -08:00
| "cutover-gates"
| "posthog-analytics" ;
2026-03-01 16:11:40 -08:00
status : BootstrapCheckStatus ;
detail : string ;
remediation? : string ;
} ;
export type BootstrapDiagnostics = {
rolloutStage : BootstrapRolloutStage ;
legacyFallbackEnabled : boolean ;
checks : BootstrapCheck [ ] ;
hasFailures : boolean ;
} ;
export type BootstrapOptions = {
2026-03-02 22:10:46 -08:00
profile? : string ;
2026-03-01 16:11:40 -08:00
yes? : boolean ;
nonInteractive? : boolean ;
forceOnboard? : boolean ;
skipUpdate? : boolean ;
updateNow? : boolean ;
noOpen? : boolean ;
json? : boolean ;
gatewayPort? : string | number ;
webPort? : string | number ;
2026-03-15 04:16:01 -07:00
denchCloud? : boolean ;
denchCloudApiKey? : string ;
denchCloudModel? : string ;
denchGatewayUrl? : string ;
2026-03-01 16:11:40 -08:00
} ;
type BootstrapSummary = {
profile : string ;
onboarded : boolean ;
installedOpenClawCli : boolean ;
openClawCliAvailable : boolean ;
openClawVersion? : string ;
gatewayUrl : string ;
gatewayReachable : boolean ;
2026-03-02 18:31:15 -08:00
gatewayAutoFix ? : {
attempted : boolean ;
recovered : boolean ;
steps : GatewayAutoFixStep [ ] ;
failureSummary? : string ;
logExcerpts : GatewayLogExcerpt [ ] ;
} ;
workspaceSeed? : WorkspaceSeedResult ;
2026-03-01 16:11:40 -08:00
webUrl : string ;
webReachable : boolean ;
webOpened : boolean ;
diagnostics : BootstrapDiagnostics ;
} ;
type SpawnResult = {
stdout : string ;
stderr : string ;
code : number ;
} ;
2026-03-02 18:31:15 -08:00
type OpenClawCliAvailability = {
available : boolean ;
installed : boolean ;
2026-03-04 16:32:58 -08:00
installedAt? : number ;
2026-03-02 18:31:15 -08:00
version? : string ;
command : string ;
globalBinDir? : string ;
shellCommandPath? : string ;
} ;
2026-03-04 16:32:58 -08:00
type OutputLineHandler = ( line : string , stream : "stdout" | "stderr" ) = > void ;
type OpenClawCliCheckCache = {
checkedAt : number ;
pathEnv : string ;
available : boolean ;
command : string ;
version? : string ;
globalBinDir? : string ;
shellCommandPath? : string ;
installedAt? : number ;
} ;
type OpenClawSetupProgress = {
startStage : ( label : string ) = > void ;
output : ( line : string ) = > void ;
completeStage : ( suffix? : string ) = > void ;
finish : ( message : string ) = > void ;
fail : ( message : string ) = > void ;
} ;
2026-03-02 18:31:15 -08:00
type GatewayAutoFixStep = {
name : string ;
ok : boolean ;
detail? : string ;
} ;
type GatewayLogExcerpt = {
path : string ;
excerpt : string ;
} ;
type GatewayAutoFixResult = {
attempted : boolean ;
recovered : boolean ;
steps : GatewayAutoFixStep [ ] ;
finalProbe : { ok : boolean ; detail? : string } ;
failureSummary? : string ;
logExcerpts : GatewayLogExcerpt [ ] ;
} ;
2026-03-15 04:16:01 -07:00
type BundledPluginSpec = {
pluginId : string ;
sourceDirName : string ;
enabled? : boolean ;
config? : Record < string , string | boolean > ;
} ;
type BundledPluginSyncResult = {
installedPluginIds : string [ ] ;
migratedLegacyDenchPlugin : boolean ;
} ;
type DenchCloudBootstrapSelection = {
enabled : boolean ;
apiKey? : string ;
gatewayUrl? : string ;
selectedModel? : string ;
catalog? : DenchCloudCatalogLoadResult ;
} ;
2026-03-01 16:11:40 -08:00
function resolveCommandForPlatform ( command : string ) : string {
if ( process . platform !== "win32" ) {
return command ;
}
if ( path . extname ( command ) ) {
return command ;
}
const normalized = path . basename ( command ) . toLowerCase ( ) ;
if (
normalized === "npm" ||
normalized === "pnpm" ||
normalized === "npx" ||
normalized === "yarn"
) {
return ` ${ command } .cmd ` ;
}
return command ;
}
async function runCommandWithTimeout (
argv : string [ ] ,
2026-03-02 18:31:15 -08:00
options : {
timeoutMs : number ;
cwd? : string ;
env? : NodeJS.ProcessEnv ;
ioMode ? : "capture" | "inherit" ;
2026-03-04 16:32:58 -08:00
onOutputLine? : OutputLineHandler ;
2026-03-02 18:31:15 -08:00
} ,
2026-03-01 16:11:40 -08:00
) : Promise < SpawnResult > {
const [ command , . . . args ] = argv ;
if ( ! command ) {
return { code : 1 , stdout : "" , stderr : "missing command" } ;
}
2026-03-03 12:24:43 -08:00
const stdio : StdioOptions = options . ioMode === "inherit" ? "inherit" : [ "ignore" , "pipe" , "pipe" ] ;
2026-03-01 16:11:40 -08:00
return await new Promise < SpawnResult > ( ( resolve , reject ) = > {
const child = spawn ( resolveCommandForPlatform ( command ) , args , {
cwd : options.cwd ,
2026-03-10 14:08:37 -07:00
env : options.env ? ? process . env ,
2026-03-02 18:31:15 -08:00
stdio ,
2026-03-01 16:11:40 -08:00
} ) ;
let stdout = "" ;
let stderr = "" ;
let settled = false ;
const timer = setTimeout ( ( ) = > {
if ( settled ) {
return ;
}
child . kill ( "SIGKILL" ) ;
} , options . timeoutMs ) ;
2026-03-03 12:24:43 -08:00
child . stdout ? . on ( "data" , ( chunk : Buffer | string ) = > {
2026-03-04 16:32:58 -08:00
const text = String ( chunk ) ;
stdout += text ;
if ( options . onOutputLine ) {
for ( const segment of text . split ( /\r?\n/ ) ) {
const line = segment . trim ( ) ;
if ( line . length > 0 ) {
options . onOutputLine ( line , "stdout" ) ;
}
}
}
2026-03-01 16:11:40 -08:00
} ) ;
2026-03-03 12:24:43 -08:00
child . stderr ? . on ( "data" , ( chunk : Buffer | string ) = > {
2026-03-04 16:32:58 -08:00
const text = String ( chunk ) ;
stderr += text ;
if ( options . onOutputLine ) {
for ( const segment of text . split ( /\r?\n/ ) ) {
const line = segment . trim ( ) ;
if ( line . length > 0 ) {
options . onOutputLine ( line , "stderr" ) ;
}
}
}
2026-03-01 16:11:40 -08:00
} ) ;
2026-03-03 12:24:43 -08:00
child . once ( "error" , ( error : Error ) = > {
2026-03-01 16:11:40 -08:00
if ( settled ) {
return ;
}
settled = true ;
clearTimeout ( timer ) ;
reject ( error ) ;
} ) ;
2026-03-03 12:24:43 -08:00
child . once ( "close" , ( code : number | null ) = > {
2026-03-01 16:11:40 -08:00
if ( settled ) {
return ;
}
settled = true ;
clearTimeout ( timer ) ;
resolve ( {
code : typeof code === "number" ? code : 1 ,
stdout ,
stderr ,
} ) ;
} ) ;
} ) ;
}
function parseOptionalPort ( value : string | number | undefined ) : number | undefined {
if ( value === undefined ) {
return undefined ;
}
const raw = typeof value === "number" ? value : Number.parseInt ( String ( value ) , 10 ) ;
if ( ! Number . isFinite ( raw ) || raw <= 0 ) {
return undefined ;
}
return raw ;
}
async function sleep ( ms : number ) {
await new Promise ( ( resolve ) = > setTimeout ( resolve , ms ) ) ;
}
2026-03-03 12:24:43 -08:00
import { createConnection } from "node:net" ;
function isPortAvailable ( port : number ) : Promise < boolean > {
return new Promise ( ( resolve ) = > {
const server = createConnection ( { port , host : "127.0.0.1" } , ( ) = > {
// Connection succeeded, port is in use
server . end ( ) ;
resolve ( false ) ;
} ) ;
server . on ( "error" , ( err : NodeJS.ErrnoException ) = > {
if ( err . code === "ECONNREFUSED" ) {
// Port is available (nothing listening)
resolve ( true ) ;
} else if ( err . code === "EADDRNOTAVAIL" ) {
// Address not available
resolve ( false ) ;
} else {
// Other errors, assume port is not available
resolve ( false ) ;
}
} ) ;
server . setTimeout ( 1000 , ( ) = > {
server . destroy ( ) ;
resolve ( false ) ;
} ) ;
} ) ;
}
async function findAvailablePort (
startPort : number ,
maxAttempts : number ,
) : Promise < number | undefined > {
for ( let i = 0 ; i < maxAttempts ; i ++ ) {
const port = startPort + i ;
if ( await isPortAvailable ( port ) ) {
return port ;
}
}
return undefined ;
}
2026-03-05 10:46:03 -08:00
/ * *
* Port 18789 belongs to the host OpenClaw installation . A persisted config
* that drifted to that value ( e . g . bootstrap ran while OpenClaw was down )
* must be rejected to prevent service hijack on launchd restart .
* /
export function isPersistedPortAcceptable ( port : number | undefined ) : port is number {
return typeof port === "number" && port > 0 && port !== 18789 ;
}
export function readExistingGatewayPort ( stateDir : string ) : number | undefined {
for ( const name of [ "openclaw.json" , "config.json" ] ) {
try {
2026-03-15 04:16:01 -07:00
const raw = json5 . parse ( readFileSync ( path . join ( stateDir , name ) , "utf-8" ) ) as {
2026-03-05 10:46:03 -08:00
gateway ? : { port? : unknown } ;
} ;
const port =
typeof raw . gateway ? . port === "number"
? raw . gateway . port
: typeof raw . gateway ? . port === "string"
? Number . parseInt ( raw . gateway . port , 10 )
: undefined ;
if ( typeof port === "number" && Number . isFinite ( port ) && port > 0 ) {
return port ;
}
} catch {
// Config file missing or malformed — try next candidate.
}
}
return undefined ;
}
2026-03-01 16:11:40 -08:00
function normalizeBootstrapRolloutStage ( raw : string | undefined ) : BootstrapRolloutStage {
const normalized = raw ? . trim ( ) . toLowerCase ( ) ;
if ( normalized === "internal" || normalized === "beta" || normalized === "default" ) {
return normalized ;
}
return DEFAULT_BOOTSTRAP_ROLLOUT_STAGE ;
}
export function resolveBootstrapRolloutStage (
env : NodeJS.ProcessEnv = process . env ,
) : BootstrapRolloutStage {
return normalizeBootstrapRolloutStage (
2026-03-04 13:18:39 -08:00
env . DENCHCLAW_BOOTSTRAP_ROLLOUT ? ? env . OPENCLAW_BOOTSTRAP_ROLLOUT ,
2026-03-01 16:11:40 -08:00
) ;
}
export function isLegacyFallbackEnabled ( env : NodeJS.ProcessEnv = process . env ) : boolean {
return (
2026-03-04 13:18:39 -08:00
isTruthyEnvValue ( env . DENCHCLAW_BOOTSTRAP_LEGACY_FALLBACK ) ||
2026-03-01 16:11:40 -08:00
isTruthyEnvValue ( env . OPENCLAW_BOOTSTRAP_LEGACY_FALLBACK )
) ;
}
function normalizeVersionOutput ( raw : string | undefined ) : string | undefined {
const first = raw
? . split ( /\r?\n/ )
. map ( ( line ) = > line . trim ( ) )
. find ( Boolean ) ;
return first && first . length > 0 ? first : undefined ;
}
function firstNonEmptyLine ( . . . values : Array < string | undefined > ) : string | undefined {
for ( const value of values ) {
const first = value
? . split ( /\r?\n/ )
. map ( ( line ) = > line . trim ( ) )
. find ( Boolean ) ;
if ( first ) {
return first ;
}
}
return undefined ;
}
function resolveGatewayLaunchAgentLabel ( profile : string ) : string {
const normalized = profile . trim ( ) . toLowerCase ( ) ;
if ( ! normalized || normalized === "default" ) {
return DEFAULT_GATEWAY_LAUNCH_AGENT_LABEL ;
}
return ` ai.openclaw. ${ normalized } ` ;
}
2026-03-15 04:16:01 -07:00
function uniqueStrings ( values : string [ ] ) : string [ ] {
return [ . . . new Set ( values . map ( ( value ) = > value . trim ( ) ) . filter ( Boolean ) ) ] ;
}
function asRecord ( value : unknown ) : Record < string , unknown > | undefined {
return value && typeof value === "object" && ! Array . isArray ( value )
? ( value as Record < string , unknown > )
: undefined ;
}
function normalizeFilesystemPath ( value : string ) : string {
try {
return realpathSync . native ( value ) ;
} catch {
return path . resolve ( value ) ;
}
}
function readBundledPluginVersion ( pluginDir : string ) : string | undefined {
const packageJsonPath = path . join ( pluginDir , "package.json" ) ;
if ( ! existsSync ( packageJsonPath ) ) {
return undefined ;
}
try {
const raw = JSON . parse ( readFileSync ( packageJsonPath , "utf-8" ) ) as {
version? : unknown ;
} ;
return typeof raw . version === "string" && raw . version . trim ( ) . length > 0
? raw . version . trim ( )
: undefined ;
} catch {
return undefined ;
}
}
function readConfiguredPluginAllowlist ( stateDir : string ) : string [ ] {
const raw = readBootstrapConfig ( stateDir ) as {
plugins ? : {
allow? : unknown ;
} ;
} | undefined ;
return Array . isArray ( raw ? . plugins ? . allow )
? raw . plugins . allow . filter ( ( value ) : value is string = > typeof value === "string" )
: [ ] ;
}
function readConfiguredPluginLoadPaths ( stateDir : string ) : string [ ] {
const raw = readBootstrapConfig ( stateDir ) as {
plugins ? : {
load ? : {
paths? : unknown ;
} ;
} ;
} | undefined ;
return Array . isArray ( raw ? . plugins ? . load ? . paths )
? raw . plugins . load . paths . filter ( ( value ) : value is string = > typeof value === "string" )
: [ ] ;
}
function isLegacyDenchCloudPluginPath ( value : string ) : boolean {
return value . replaceAll ( "\\" , "/" ) . includes ( "/dench-cloud-provider" ) ;
}
async function setOpenClawConfigJson ( params : {
openclawCommand : string ;
profile : string ;
key : string ;
value : unknown ;
errorMessage : string ;
} ) : Promise < void > {
await runOpenClawOrThrow ( {
openclawCommand : params.openclawCommand ,
args : [
"--profile" ,
params . profile ,
"config" ,
"set" ,
params . key ,
JSON . stringify ( params . value ) ,
] ,
timeoutMs : 30_000 ,
errorMessage : params.errorMessage ,
} ) ;
}
async function syncBundledPlugins ( params : {
2026-03-05 10:46:03 -08:00
openclawCommand : string ;
profile : string ;
stateDir : string ;
2026-03-15 04:16:01 -07:00
plugins : BundledPluginSpec [ ] ;
restartGateway? : boolean ;
} ) : Promise < BundledPluginSyncResult > {
2026-03-05 10:46:03 -08:00
try {
2026-03-15 04:16:01 -07:00
const packageRoot = resolveCliPackageRoot ( ) ;
const installedPluginIds : string [ ] = [ ] ;
const rawConfig = readBootstrapConfig ( params . stateDir ) ? ? { } ;
const nextConfig = {
. . . rawConfig ,
} ;
const pluginsConfig = {
. . . asRecord ( nextConfig . plugins ) ,
} ;
const loadConfig = {
. . . asRecord ( pluginsConfig . load ) ,
} ;
const installs = {
. . . asRecord ( pluginsConfig . installs ) ,
} ;
const entries = {
. . . asRecord ( pluginsConfig . entries ) ,
} ;
const currentAllow = readConfiguredPluginAllowlist ( params . stateDir ) ;
const currentLoadPaths = readConfiguredPluginLoadPaths ( params . stateDir ) ;
const nextAllow = currentAllow . filter (
( value ) = > value !== "dench-cloud-provider" ,
) ;
const nextLoadPaths = currentLoadPaths . filter (
( value ) = > ! isLegacyDenchCloudPluginPath ( value ) ,
) ;
const legacyPluginDir = path . join ( params . stateDir , "extensions" , "dench-cloud-provider" ) ;
const hadLegacyEntry = entries [ "dench-cloud-provider" ] !== undefined ;
const hadLegacyInstall = installs [ "dench-cloud-provider" ] !== undefined ;
delete entries [ "dench-cloud-provider" ] ;
delete installs [ "dench-cloud-provider" ] ;
const migratedLegacyDenchPlugin =
nextAllow . length !== currentAllow . length ||
nextLoadPaths . length !== currentLoadPaths . length ||
hadLegacyEntry ||
hadLegacyInstall ||
existsSync ( legacyPluginDir ) ;
for ( const plugin of params . plugins ) {
const pluginSrc = path . join ( packageRoot , "extensions" , plugin . sourceDirName ) ;
if ( ! existsSync ( pluginSrc ) ) {
continue ;
}
2026-03-05 10:46:03 -08:00
2026-03-15 04:16:01 -07:00
const pluginDest = path . join ( params . stateDir , "extensions" , plugin . sourceDirName ) ;
mkdirSync ( path . dirname ( pluginDest ) , { recursive : true } ) ;
cpSync ( pluginSrc , pluginDest , { recursive : true , force : true } ) ;
const normalizedPluginSrc = normalizeFilesystemPath ( pluginSrc ) ;
const normalizedPluginDest = normalizeFilesystemPath ( pluginDest ) ;
nextAllow . push ( plugin . pluginId ) ;
nextLoadPaths . push ( normalizedPluginDest ) ;
installedPluginIds . push ( plugin . pluginId ) ;
const existingEntry = {
. . . asRecord ( entries [ plugin . pluginId ] ) ,
} ;
if ( plugin . enabled !== undefined ) {
existingEntry . enabled = plugin . enabled ;
}
if ( plugin . config && Object . keys ( plugin . config ) . length > 0 ) {
existingEntry . config = {
. . . asRecord ( existingEntry . config ) ,
. . . plugin . config ,
} ;
}
if ( Object . keys ( existingEntry ) . length > 0 ) {
entries [ plugin . pluginId ] = existingEntry ;
}
2026-03-05 10:46:03 -08:00
2026-03-15 04:16:01 -07:00
const installRecord : Record < string , unknown > = {
source : "path" ,
sourcePath : normalizedPluginSrc ,
installPath : normalizedPluginDest ,
installedAt : new Date ( ) . toISOString ( ) ,
} ;
const version = readBundledPluginVersion ( pluginSrc ) ;
if ( version ) {
installRecord . version = version ;
}
installs [ plugin . pluginId ] = installRecord ;
}
2026-03-05 12:28:08 -08:00
2026-03-15 04:16:01 -07:00
pluginsConfig . allow = uniqueStrings ( nextAllow ) ;
loadConfig . paths = uniqueStrings ( nextLoadPaths ) ;
pluginsConfig . load = loadConfig ;
pluginsConfig . entries = entries ;
pluginsConfig . installs = installs ;
nextConfig . plugins = pluginsConfig ;
writeFileSync (
path . join ( params . stateDir , "openclaw.json" ) ,
` ${ JSON . stringify ( nextConfig , null , 2 ) } \ n ` ,
) ;
2026-03-05 13:57:38 -08:00
2026-03-15 04:16:01 -07:00
if ( migratedLegacyDenchPlugin ) {
rmSync ( legacyPluginDir , { recursive : true , force : true } ) ;
2026-03-05 10:46:03 -08:00
}
2026-03-05 15:35:46 -08:00
2026-03-15 04:16:01 -07:00
if ( params . restartGateway ) {
try {
await runOpenClawOrThrow ( {
openclawCommand : params.openclawCommand ,
args : [ "--profile" , params . profile , "gateway" , "restart" ] ,
timeoutMs : 60_000 ,
errorMessage : "Failed to restart gateway after plugin install." ,
} ) ;
} catch {
// Gateway may not be running yet (first bootstrap) — ignore.
}
2026-03-05 15:35:46 -08:00
}
2026-03-15 04:16:01 -07:00
return {
installedPluginIds ,
migratedLegacyDenchPlugin ,
} ;
2026-03-05 10:46:03 -08:00
} catch {
2026-03-15 04:16:01 -07:00
return {
installedPluginIds : [ ] ,
migratedLegacyDenchPlugin : false ,
} ;
2026-03-05 10:46:03 -08:00
}
}
2026-03-02 18:31:15 -08:00
async function ensureGatewayModeLocal ( openclawCommand : string , profile : string ) : Promise < void > {
2026-03-01 16:11:40 -08:00
const result = await runOpenClaw (
2026-03-02 18:31:15 -08:00
openclawCommand ,
[ "--profile" , profile , "config" , "get" , "gateway.mode" ] ,
2026-03-01 16:11:40 -08:00
10 _000 ,
) ;
const currentMode = result . stdout . trim ( ) ;
if ( currentMode === "local" ) {
return ;
}
await runOpenClawOrThrow ( {
2026-03-02 18:31:15 -08:00
openclawCommand ,
args : [ "--profile" , profile , "config" , "set" , "gateway.mode" , "local" ] ,
2026-03-01 16:11:40 -08:00
timeoutMs : 10_000 ,
errorMessage : "Failed to set gateway.mode=local." ,
} ) ;
}
2026-03-03 12:24:43 -08:00
async function ensureGatewayPort (
openclawCommand : string ,
profile : string ,
gatewayPort : number ,
) : Promise < void > {
await runOpenClawOrThrow ( {
openclawCommand ,
args : [ "--profile" , profile , "config" , "set" , "gateway.port" , String ( gatewayPort ) ] ,
timeoutMs : 10_000 ,
errorMessage : ` Failed to set gateway.port= ${ gatewayPort } . ` ,
} ) ;
}
2026-03-03 13:47:23 -08:00
async function ensureDefaultWorkspacePath (
openclawCommand : string ,
profile : string ,
workspaceDir : string ,
) : Promise < void > {
await runOpenClawOrThrow ( {
openclawCommand ,
args : [ "--profile" , profile , "config" , "set" , "agents.defaults.workspace" , workspaceDir ] ,
timeoutMs : 10_000 ,
errorMessage : ` Failed to set agents.defaults.workspace= ${ workspaceDir } . ` ,
} ) ;
}
2026-03-03 16:34:03 -08:00
async function ensureSubagentDefaults ( openclawCommand : string , profile : string ) : Promise < void > {
const settings : Array < [ string , string ] > = [
[ "agents.defaults.subagents.maxConcurrent" , "8" ] ,
[ "agents.defaults.subagents.maxSpawnDepth" , "2" ] ,
[ "agents.defaults.subagents.maxChildrenPerAgent" , "10" ] ,
[ "agents.defaults.subagents.archiveAfterMinutes" , "180" ] ,
[ "agents.defaults.subagents.runTimeoutSeconds" , "0" ] ,
[ "tools.subagents.tools.deny" , "[]" ] ,
] ;
for ( const [ key , value ] of settings ) {
await runOpenClawOrThrow ( {
openclawCommand ,
args : [ "--profile" , profile , "config" , "set" , key , value ] ,
timeoutMs : 10_000 ,
errorMessage : ` Failed to set ${ key } = ${ value } . ` ,
} ) ;
}
}
2026-03-04 13:18:39 -08:00
async function ensureToolsProfile ( openclawCommand : string , profile : string ) : Promise < void > {
await runOpenClawOrThrow ( {
openclawCommand ,
args : [ "--profile" , profile , "config" , "set" , "tools.profile" , REQUIRED_TOOLS_PROFILE ] ,
timeoutMs : 10_000 ,
errorMessage : ` Failed to set tools.profile= ${ REQUIRED_TOOLS_PROFILE } . ` ,
} ) ;
}
2026-03-02 18:31:15 -08:00
async function runOpenClaw (
openclawCommand : string ,
args : string [ ] ,
timeoutMs : number ,
ioMode : "capture" | "inherit" = "capture" ,
2026-03-03 12:24:43 -08:00
env? : NodeJS.ProcessEnv ,
2026-03-04 16:32:58 -08:00
onOutputLine? : OutputLineHandler ,
2026-03-02 18:31:15 -08:00
) : Promise < SpawnResult > {
2026-03-04 16:32:58 -08:00
return await runCommandWithTimeout ( [ openclawCommand , . . . args ] , {
timeoutMs ,
ioMode ,
env ,
onOutputLine ,
} ) ;
2026-03-01 16:11:40 -08:00
}
async function runOpenClawOrThrow ( params : {
2026-03-02 18:31:15 -08:00
openclawCommand : string ;
args : string [ ] ;
2026-03-01 16:11:40 -08:00
timeoutMs : number ;
errorMessage : string ;
} ) : Promise < SpawnResult > {
2026-03-02 18:31:15 -08:00
const result = await runOpenClaw ( params . openclawCommand , params . args , params . timeoutMs ) ;
if ( result . code === 0 ) {
return result ;
}
const detail = firstNonEmptyLine ( result . stderr , result . stdout ) ;
throw new Error ( detail ? ` ${ params . errorMessage } \ n ${ detail } ` : params . errorMessage ) ;
}
/ * *
* Runs an OpenClaw command attached to the current terminal .
* Use this for interactive flows like ` openclaw onboard ` .
* /
async function runOpenClawInteractiveOrThrow ( params : {
openclawCommand : string ;
args : string [ ] ;
timeoutMs : number ;
errorMessage : string ;
} ) : Promise < SpawnResult > {
const result = await runOpenClaw (
params . openclawCommand ,
params . args ,
params . timeoutMs ,
"inherit" ,
) ;
2026-03-01 16:11:40 -08:00
if ( result . code === 0 ) {
return result ;
}
const detail = firstNonEmptyLine ( result . stderr , result . stdout ) ;
throw new Error ( detail ? ` ${ params . errorMessage } \ n ${ detail } ` : params . errorMessage ) ;
}
/ * *
* Runs an openclaw sub - command with a visible spinner that streams progress
* from the subprocess stdout / stderr into the spinner message .
* /
async function runOpenClawWithProgress ( params : {
2026-03-02 18:31:15 -08:00
openclawCommand : string ;
args : string [ ] ;
2026-03-01 16:11:40 -08:00
timeoutMs : number ;
startMessage : string ;
successMessage : string ;
errorMessage : string ;
} ) : Promise < SpawnResult > {
const s = spinner ( ) ;
s . start ( params . startMessage ) ;
const result = await new Promise < SpawnResult > ( ( resolve , reject ) = > {
2026-03-02 18:31:15 -08:00
const child = spawn ( resolveCommandForPlatform ( params . openclawCommand ) , params . args , {
2026-03-01 16:11:40 -08:00
stdio : [ "ignore" , "pipe" , "pipe" ] ,
} ) ;
let stdout = "" ;
let stderr = "" ;
let settled = false ;
const timer = setTimeout ( ( ) = > {
if ( ! settled ) {
child . kill ( "SIGKILL" ) ;
}
} , params . timeoutMs ) ;
const updateSpinner = ( chunk : string ) = > {
const line = chunk
. split ( /\r?\n/ )
. map ( ( l ) = > l . trim ( ) )
. filter ( Boolean )
. pop ( ) ;
if ( line ) {
s . message ( line . length > 72 ? ` ${ line . slice ( 0 , 69 ) } ... ` : line ) ;
}
} ;
child . stdout ? . on ( "data" , ( chunk ) = > {
const text = String ( chunk ) ;
stdout += text ;
updateSpinner ( text ) ;
} ) ;
child . stderr ? . on ( "data" , ( chunk ) = > {
const text = String ( chunk ) ;
stderr += text ;
updateSpinner ( text ) ;
} ) ;
child . once ( "error" , ( error ) = > {
if ( settled ) {
return ;
}
settled = true ;
clearTimeout ( timer ) ;
reject ( error ) ;
} ) ;
child . once ( "close" , ( code ) = > {
if ( settled ) {
return ;
}
settled = true ;
clearTimeout ( timer ) ;
resolve ( { code : typeof code === "number" ? code : 1 , stdout , stderr } ) ;
} ) ;
} ) ;
if ( result . code === 0 ) {
s . stop ( params . successMessage ) ;
return result ;
}
const detail = firstNonEmptyLine ( result . stderr , result . stdout ) ;
2026-03-03 12:24:43 -08:00
const stopMessage = detail ? ` ${ params . errorMessage } : ${ detail } ` : params . errorMessage ;
s . stop ( stopMessage ) ;
2026-03-01 16:11:40 -08:00
throw new Error ( detail ? ` ${ params . errorMessage } \ n ${ detail } ` : params . errorMessage ) ;
}
2026-03-02 18:31:15 -08:00
function parseJsonPayload ( raw : string | undefined ) : Record < string , unknown > | undefined {
if ( ! raw ) {
return undefined ;
}
const trimmed = raw . trim ( ) ;
if ( ! trimmed ) {
return undefined ;
}
try {
const parsed = JSON . parse ( trimmed ) ;
return parsed && typeof parsed === "object" ? ( parsed as Record < string , unknown > ) : undefined ;
} catch {
const start = trimmed . indexOf ( "{" ) ;
const end = trimmed . lastIndexOf ( "}" ) ;
if ( start === - 1 || end <= start ) {
return undefined ;
}
try {
const parsed = JSON . parse ( trimmed . slice ( start , end + 1 ) ) ;
return parsed && typeof parsed === "object" ? ( parsed as Record < string , unknown > ) : undefined ;
} catch {
return undefined ;
}
}
}
2026-03-04 16:32:58 -08:00
function resolveOpenClawCliCheckCachePath ( stateDir : string ) : string {
return path . join ( stateDir , "cache" , OPENCLAW_CLI_CHECK_CACHE_FILE ) ;
}
function readOpenClawCliCheckCache ( stateDir : string ) : OpenClawCliCheckCache | undefined {
const cachePath = resolveOpenClawCliCheckCachePath ( stateDir ) ;
if ( ! existsSync ( cachePath ) ) {
return undefined ;
}
try {
const parsed = JSON . parse ( readFileSync ( cachePath , "utf-8" ) ) as Partial < OpenClawCliCheckCache > ;
if (
typeof parsed . checkedAt !== "number" ||
! Number . isFinite ( parsed . checkedAt ) ||
typeof parsed . pathEnv !== "string" ||
parsed . pathEnv !== ( process . env . PATH ? ? "" ) ||
typeof parsed . available !== "boolean" ||
! parsed . available ||
typeof parsed . command !== "string" ||
parsed . command . length === 0
) {
return undefined ;
}
const ageMs = Date . now ( ) - parsed . checkedAt ;
if ( ageMs < 0 || ageMs > OPENCLAW_CLI_CHECK_CACHE_TTL_MS ) {
return undefined ;
}
const looksLikePath =
parsed . command . includes ( path . sep ) ||
parsed . command . includes ( "/" ) ||
parsed . command . includes ( "\\" ) ;
if ( looksLikePath && ! existsSync ( parsed . command ) ) {
return undefined ;
}
return {
checkedAt : parsed.checkedAt ,
pathEnv : parsed.pathEnv ,
available : parsed.available ,
command : parsed.command ,
version : typeof parsed . version === "string" ? parsed.version : undefined ,
globalBinDir : typeof parsed . globalBinDir === "string" ? parsed.globalBinDir : undefined ,
shellCommandPath :
typeof parsed . shellCommandPath === "string" ? parsed.shellCommandPath : undefined ,
installedAt : typeof parsed . installedAt === "number" ? parsed.installedAt : undefined ,
} ;
} catch {
return undefined ;
}
}
function writeOpenClawCliCheckCache (
stateDir : string ,
cache : Omit < OpenClawCliCheckCache , "checkedAt" | "pathEnv" > ,
) : void {
try {
const cachePath = resolveOpenClawCliCheckCachePath ( stateDir ) ;
mkdirSync ( path . dirname ( cachePath ) , { recursive : true } ) ;
const payload : OpenClawCliCheckCache = {
. . . cache ,
checkedAt : Date.now ( ) ,
pathEnv : process.env.PATH ? ? "" ,
} ;
writeFileSync ( cachePath , JSON . stringify ( payload , null , 2 ) , "utf-8" ) ;
} catch {
// Cache write failures should never block bootstrap.
}
}
function createOpenClawSetupProgress ( params : {
enabled : boolean ;
totalStages : number ;
} ) : OpenClawSetupProgress {
if ( ! params . enabled || params . totalStages <= 0 || ! process . stdout . isTTY ) {
const noop = ( ) = > undefined ;
return {
startStage : noop ,
output : noop ,
completeStage : noop ,
finish : noop ,
fail : noop ,
} ;
}
const s = spinner ( ) ;
let completedStages = 0 ;
let activeLabel = "" ;
const renderBar = ( ) = > {
const ratio = completedStages / params . totalStages ;
const filled = Math . max (
0 ,
Math . min (
OPENCLAW_SETUP_PROGRESS_BAR_WIDTH ,
Math . round ( ratio * OPENCLAW_SETUP_PROGRESS_BAR_WIDTH ) ,
) ,
) ;
const bar = ` ${ "#" . repeat ( filled ) } ${ "-" . repeat ( OPENCLAW_SETUP_PROGRESS_BAR_WIDTH - filled ) } ` ;
return ` [ ${ bar } ] ${ completedStages } / ${ params . totalStages } ` ;
} ;
const truncate = ( value : string , max = 84 ) = >
value . length > max ? ` ${ value . slice ( 0 , max - 3 ) } ... ` : value ;
const renderStageLine = ( detail? : string ) = > {
const base = ` ${ renderBar ( ) } ${ activeLabel } ` . trim ( ) ;
if ( ! detail ) {
return base ;
}
return truncate ( ` ${ base } -> ${ detail } ` ) ;
} ;
return {
startStage : ( label : string ) = > {
activeLabel = label ;
s . start ( renderStageLine ( ) ) ;
} ,
output : ( line : string ) = > {
if ( ! line ) {
return ;
}
s . message ( renderStageLine ( line ) ) ;
} ,
completeStage : ( suffix? : string ) = > {
completedStages = Math . min ( params . totalStages , completedStages + 1 ) ;
s . stop ( renderStageLine ( suffix ? ? "done" ) ) ;
} ,
finish : ( message : string ) = > {
completedStages = params . totalStages ;
s . stop ( ` ${ renderBar ( ) } ${ truncate ( message ) } ` . trim ( ) ) ;
} ,
fail : ( message : string ) = > {
s . stop ( ` ${ renderBar ( ) } ${ truncate ( message ) } ` . trim ( ) ) ;
} ,
} ;
}
2026-03-10 14:08:37 -07:00
/ * *
* Returns a copy of ` process.env ` with ` npm_config_* ` , ` npm_package_* ` , and
* npm lifecycle variables stripped . When denchclaw is launched via ` npx ` , npm
* injects environment variables ( most critically ` npm_config_prefix ` ) that
* redirect ` npm install -g ` and ` npm ls -g ` to a temporary npx - managed
* prefix instead of the user ' s real global npm directory . Stripping these
* ensures child npm processes use the user ' s actual configuration .
* /
function cleanNpmGlobalEnv ( ) : NodeJS . ProcessEnv {
const cleaned : NodeJS.ProcessEnv = { } ;
for ( const [ key , value ] of Object . entries ( process . env ) ) {
if (
key . startsWith ( "npm_config_" ) ||
key . startsWith ( "npm_package_" ) ||
key === "npm_lifecycle_event" ||
key === "npm_lifecycle_script"
) {
continue ;
}
cleaned [ key ] = value ;
}
return cleaned ;
}
2026-03-04 16:32:58 -08:00
async function detectGlobalOpenClawInstall (
onOutputLine? : OutputLineHandler ,
) : Promise < { installed : boolean ; version? : string } > {
2026-03-02 18:31:15 -08:00
const result = await runCommandWithTimeout (
[ "npm" , "ls" , "-g" , "openclaw" , "--depth=0" , "--json" , "--silent" ] ,
{
timeoutMs : 15_000 ,
2026-03-04 16:32:58 -08:00
onOutputLine ,
2026-03-10 14:08:37 -07:00
env : cleanNpmGlobalEnv ( ) ,
2026-03-02 18:31:15 -08:00
} ,
) . catch ( ( ) = > null ) ;
const parsed = parseJsonPayload ( result ? . stdout ? ? result ? . stderr ) ;
const dependencies = parsed ? . dependencies as
| Record < string , { version ? : string } | undefined >
| undefined ;
const installedVersion = dependencies ? . openclaw ? . version ;
if ( typeof installedVersion === "string" && installedVersion . length > 0 ) {
return { installed : true , version : installedVersion } ;
}
return { installed : false } ;
}
2026-03-04 16:32:58 -08:00
async function resolveNpmGlobalBinDir (
onOutputLine? : OutputLineHandler ,
) : Promise < string | undefined > {
2026-03-02 18:31:15 -08:00
const result = await runCommandWithTimeout ( [ "npm" , "prefix" , "-g" ] , {
timeoutMs : 8_000 ,
2026-03-10 14:08:37 -07:00
env : cleanNpmGlobalEnv ( ) ,
2026-03-04 16:32:58 -08:00
onOutputLine ,
2026-03-02 18:31:15 -08:00
} ) . catch ( ( ) = > null ) ;
if ( ! result || result . code !== 0 ) {
return undefined ;
}
const prefix = firstNonEmptyLine ( result . stdout ) ;
if ( ! prefix ) {
return undefined ;
2026-03-01 16:11:40 -08:00
}
2026-03-02 18:31:15 -08:00
return process . platform === "win32" ? prefix : path.join ( prefix , "bin" ) ;
}
2026-03-01 16:11:40 -08:00
2026-03-02 18:31:15 -08:00
function resolveGlobalOpenClawCommand ( globalBinDir : string | undefined ) : string | undefined {
if ( ! globalBinDir ) {
return undefined ;
}
const candidates =
process . platform === "win32"
? [ path . join ( globalBinDir , "openclaw.cmd" ) , path . join ( globalBinDir , "openclaw.exe" ) ]
: [ path . join ( globalBinDir , "openclaw" ) ] ;
return candidates . find ( ( candidate ) = > existsSync ( candidate ) ) ;
}
2026-03-04 16:32:58 -08:00
async function resolveShellOpenClawPath (
onOutputLine? : OutputLineHandler ,
) : Promise < string | undefined > {
2026-03-02 18:31:15 -08:00
const locator = process . platform === "win32" ? "where" : "which" ;
const result = await runCommandWithTimeout ( [ locator , "openclaw" ] , {
timeoutMs : 4_000 ,
2026-03-04 16:32:58 -08:00
onOutputLine ,
2026-03-01 16:11:40 -08:00
} ) . catch ( ( ) = > null ) ;
2026-03-02 18:31:15 -08:00
if ( ! result || result . code !== 0 ) {
return undefined ;
2026-03-01 16:11:40 -08:00
}
2026-03-02 18:31:15 -08:00
return firstNonEmptyLine ( result . stdout ) ;
}
2026-03-01 16:11:40 -08:00
2026-03-02 18:31:15 -08:00
function isProjectLocalOpenClawPath ( commandPath : string | undefined ) : boolean {
if ( ! commandPath ) {
return false ;
}
const normalized = commandPath . replaceAll ( "\\" , "/" ) ;
return normalized . includes ( "/node_modules/.bin/openclaw" ) ;
}
2026-03-04 16:32:58 -08:00
async function ensureOpenClawCliAvailable ( params : {
stateDir : string ;
showProgress : boolean ;
} ) : Promise < OpenClawCliAvailability > {
const cached = readOpenClawCliCheckCache ( params . stateDir ) ;
if ( cached ) {
const ageSeconds = Math . max ( 0 , Math . floor ( ( Date . now ( ) - cached . checkedAt ) / 1000 ) ) ;
const progress = createOpenClawSetupProgress ( {
enabled : params.showProgress ,
totalStages : 1 ,
} ) ;
progress . startStage ( "Reusing cached OpenClaw install check" ) ;
progress . completeStage ( ` cache hit ( ${ ageSeconds } s old) ` ) ;
return {
available : true ,
installed : false ,
installedAt : cached.installedAt ,
version : cached.version ,
command : cached.command ,
globalBinDir : cached.globalBinDir ,
shellCommandPath : cached.shellCommandPath ,
} ;
}
const progress = createOpenClawSetupProgress ( {
enabled : params.showProgress ,
totalStages : 5 ,
} ) ;
progress . startStage ( "Checking global OpenClaw install" ) ;
const globalBefore = await detectGlobalOpenClawInstall ( ( line ) = > {
progress . output ( ` npm ls: ${ line } ` ) ;
} ) ;
progress . completeStage (
globalBefore . installed ? ` found ${ globalBefore . version ? ? "installed" } ` : "missing" ,
) ;
2026-03-02 18:31:15 -08:00
let installed = false ;
2026-03-04 16:32:58 -08:00
let installedAt : number | undefined ;
progress . startStage ( "Ensuring openclaw@latest is installed globally" ) ;
2026-03-02 18:31:15 -08:00
if ( ! globalBefore . installed ) {
const install = await runCommandWithTimeout ( [ "npm" , "install" , "-g" , "openclaw@latest" ] , {
timeoutMs : 10 * 60 _000 ,
2026-03-10 14:08:37 -07:00
env : cleanNpmGlobalEnv ( ) ,
2026-03-04 16:32:58 -08:00
onOutputLine : ( line ) = > {
progress . output ( ` npm install: ${ line } ` ) ;
} ,
2026-03-02 18:31:15 -08:00
} ) . catch ( ( ) = > null ) ;
if ( ! install || install . code !== 0 ) {
2026-03-04 16:32:58 -08:00
progress . fail ( "OpenClaw global install failed." ) ;
2026-03-02 18:31:15 -08:00
return {
available : false ,
installed : false ,
version : undefined ,
command : "openclaw" ,
} ;
}
installed = true ;
2026-03-04 16:32:58 -08:00
installedAt = Date . now ( ) ;
progress . completeStage ( "installed openclaw@latest" ) ;
} else {
progress . completeStage ( "already installed; skipping install" ) ;
2026-03-02 18:31:15 -08:00
}
2026-03-04 16:32:58 -08:00
progress . startStage ( "Resolving global and shell OpenClaw paths" ) ;
const [ globalBinDir , shellCommandPath ] = await Promise . all ( [
resolveNpmGlobalBinDir ( ( line ) = > {
progress . output ( ` npm prefix: ${ line } ` ) ;
} ) ,
resolveShellOpenClawPath ( ( line ) = > {
progress . output ( ` ${ process . platform === "win32" ? "where" : "which" } : ${ line } ` ) ;
} ) ,
] ) ;
progress . completeStage ( "path discovery complete" ) ;
const globalAfter = installed ? { installed : true , version : globalBefore.version } : globalBefore ;
2026-03-02 18:31:15 -08:00
const globalCommand = resolveGlobalOpenClawCommand ( globalBinDir ) ;
const command = globalCommand ? ? "openclaw" ;
2026-03-04 16:32:58 -08:00
progress . startStage ( "Verifying OpenClaw CLI responsiveness" ) ;
const check = await runOpenClaw ( command , [ "--version" ] , 4 _000 , "capture" , undefined , ( line ) = > {
progress . output ( ` openclaw --version: ${ line } ` ) ;
} ) . catch ( ( ) = > null ) ;
progress . completeStage (
check ? . code === 0 ? "OpenClaw responded" : "OpenClaw version probe failed" ,
) ;
2026-03-02 18:31:15 -08:00
const version = normalizeVersionOutput ( check ? . stdout || check ? . stderr || globalAfter . version ) ;
const available = Boolean ( globalAfter . installed && check && check . code === 0 ) ;
2026-03-04 16:32:58 -08:00
progress . startStage ( "Caching OpenClaw check result" ) ;
if ( available ) {
writeOpenClawCliCheckCache ( params . stateDir , {
available ,
command ,
version ,
globalBinDir ,
shellCommandPath ,
installedAt ,
} ) ;
progress . completeStage ( ` saved ( ${ Math . floor ( OPENCLAW_CLI_CHECK_CACHE_TTL_MS / 60 _000 ) } m TTL) ` ) ;
} else {
progress . fail ( "OpenClaw CLI check failed (cache not written)." ) ;
}
2026-03-01 16:11:40 -08:00
return {
2026-03-02 18:31:15 -08:00
available ,
installed ,
2026-03-04 16:32:58 -08:00
installedAt ,
2026-03-02 18:31:15 -08:00
version ,
command ,
globalBinDir ,
shellCommandPath ,
2026-03-01 16:11:40 -08:00
} ;
}
2026-03-02 18:31:15 -08:00
async function probeGateway (
openclawCommand : string ,
profile : string ,
2026-03-03 12:24:43 -08:00
gatewayPort? : number ,
2026-03-02 18:31:15 -08:00
) : Promise < { ok : boolean ; detail? : string } > {
2026-03-10 14:08:37 -07:00
const env = gatewayPort
? { . . . process . env , OPENCLAW_GATEWAY_PORT : String ( gatewayPort ) }
: undefined ;
2026-03-01 16:11:40 -08:00
const result = await runOpenClaw (
2026-03-02 18:31:15 -08:00
openclawCommand ,
[ "--profile" , profile , "health" , "--json" ] ,
2026-03-01 16:11:40 -08:00
12 _000 ,
2026-03-03 12:24:43 -08:00
"capture" ,
env ,
2026-03-01 16:11:40 -08:00
) . catch ( ( error ) = > {
const message = error instanceof Error ? error.message : String ( error ) ;
return {
code : 1 ,
stdout : "" ,
stderr : message ,
} as SpawnResult ;
} ) ;
if ( result . code === 0 ) {
return { ok : true } ;
}
return {
ok : false ,
detail : firstNonEmptyLine ( result . stderr , result . stdout ) ,
} ;
}
2026-03-02 18:31:15 -08:00
function readLogTail ( logPath : string , maxLines = 16 ) : string | undefined {
if ( ! existsSync ( logPath ) ) {
return undefined ;
}
try {
const lines = readFileSync ( logPath , "utf-8" )
. split ( /\r?\n/ )
. map ( ( line ) = > line . trimEnd ( ) )
. filter ( ( line ) = > line . length > 0 ) ;
if ( lines . length === 0 ) {
return undefined ;
}
return lines . slice ( - maxLines ) . join ( "\n" ) ;
} catch {
return undefined ;
}
}
function resolveLatestRuntimeLogPath ( ) : string | undefined {
const runtimeLogDir = "/tmp/openclaw" ;
if ( ! existsSync ( runtimeLogDir ) ) {
return undefined ;
}
try {
const files = readdirSync ( runtimeLogDir )
. filter ( ( name ) = > / ^ o p e n c l a w - . * \ . l o g $ / u . t e s t ( n a m e ) )
. toSorted ( ( a , b ) = > b . localeCompare ( a ) ) ;
if ( files . length === 0 ) {
return undefined ;
}
return path . join ( runtimeLogDir , files [ 0 ] ) ;
} catch {
return undefined ;
}
}
function collectGatewayLogExcerpts ( stateDir : string ) : GatewayLogExcerpt [ ] {
const candidates = [
path . join ( stateDir , "logs" , "gateway.err.log" ) ,
path . join ( stateDir , "logs" , "gateway.log" ) ,
resolveLatestRuntimeLogPath ( ) ,
] . filter ( ( candidate ) : candidate is string = > Boolean ( candidate ) ) ;
const excerpts : GatewayLogExcerpt [ ] = [ ] ;
for ( const candidate of candidates ) {
const excerpt = readLogTail ( candidate ) ;
if ( ! excerpt ) {
continue ;
}
excerpts . push ( { path : candidate , excerpt } ) ;
}
return excerpts ;
}
function deriveGatewayFailureSummary (
probeDetail : string | undefined ,
excerpts : GatewayLogExcerpt [ ] ,
) : string | undefined {
const combinedLines = excerpts . flatMap ( ( entry ) = > entry . excerpt . split ( /\r?\n/ ) ) ;
const signalRegex =
2026-03-02 22:10:46 -08:00
/ ( c a n n o t f i n d m o d u l e | p l u g i n n o t f o u n d | i n v a l i d c o n f i g | u n a u t h o r i z e d | t o k e n m i s m a t c h | d e v i c e t o k e n m i s m a t c h | d e v i c e s i g n a t u r e i n v a l i d | d e v i c e s i g n a t u r e e x p i r e d | d e v i c e - s i g n a t u r e | e a d d r i n u s e | a d d r e s s a l r e a d y i n u s e | e r r o r : | f a i l e d t o | f a i l o v e r e r r o r ) / i u ;
2026-03-02 18:31:15 -08:00
const likely = [ . . . combinedLines ] . toReversed ( ) . find ( ( line ) = > signalRegex . test ( line ) ) ;
if ( likely ) {
return likely . length > 220 ? ` ${ likely . slice ( 0 , 217 ) } ... ` : likely ;
}
return probeDetail ;
}
async function attemptGatewayAutoFix ( params : {
openclawCommand : string ;
profile : string ;
stateDir : string ;
2026-03-03 12:24:43 -08:00
gatewayPort : number ;
2026-03-02 18:31:15 -08:00
} ) : Promise < GatewayAutoFixResult > {
const steps : GatewayAutoFixStep [ ] = [ ] ;
const commands : Array < {
name : string ;
args : string [ ] ;
timeoutMs : number ;
} > = [
2026-03-02 22:10:46 -08:00
{
name : "openclaw gateway stop" ,
args : [ "--profile" , params . profile , "gateway" , "stop" ] ,
timeoutMs : 90_000 ,
} ,
2026-03-02 18:31:15 -08:00
{
name : "openclaw doctor --fix" ,
args : [ "--profile" , params . profile , "doctor" , "--fix" ] ,
timeoutMs : 2 * 60 _000 ,
} ,
{
2026-03-02 22:10:46 -08:00
name : "openclaw gateway install --force" ,
2026-03-03 12:24:43 -08:00
args : [
"--profile" ,
params . profile ,
"gateway" ,
"install" ,
"--force" ,
"--port" ,
String ( params . gatewayPort ) ,
] ,
2026-03-02 18:31:15 -08:00
timeoutMs : 2 * 60 _000 ,
} ,
{
name : "openclaw gateway start" ,
2026-03-03 12:24:43 -08:00
args : [ "--profile" , params . profile , "gateway" , "start" , "--port" , String ( params . gatewayPort ) ] ,
2026-03-02 18:31:15 -08:00
timeoutMs : 2 * 60 _000 ,
} ,
] ;
for ( const command of commands ) {
const result = await runOpenClaw ( params . openclawCommand , command . args , command . timeoutMs ) . catch (
( error ) = > {
const message = error instanceof Error ? error.message : String ( error ) ;
return {
code : 1 ,
stdout : "" ,
stderr : message ,
} as SpawnResult ;
} ,
) ;
steps . push ( {
name : command.name ,
ok : result.code === 0 ,
detail : result.code === 0 ? undefined : firstNonEmptyLine ( result . stderr , result . stdout ) ,
} ) ;
}
2026-03-03 12:24:43 -08:00
let finalProbe = await probeGateway ( params . openclawCommand , params . profile , params . gatewayPort ) ;
2026-03-02 18:31:15 -08:00
for ( let attempt = 0 ; attempt < 2 && ! finalProbe . ok ; attempt += 1 ) {
await sleep ( 1 _200 ) ;
2026-03-03 12:24:43 -08:00
finalProbe = await probeGateway ( params . openclawCommand , params . profile , params . gatewayPort ) ;
2026-03-02 18:31:15 -08:00
}
const logExcerpts = finalProbe . ok ? [ ] : collectGatewayLogExcerpts ( params . stateDir ) ;
const failureSummary = finalProbe . ok
? undefined
: deriveGatewayFailureSummary ( finalProbe . detail , logExcerpts ) ;
return {
attempted : true ,
recovered : finalProbe.ok ,
steps ,
finalProbe ,
failureSummary ,
logExcerpts ,
} ;
}
2026-03-01 16:11:40 -08:00
async function openUrl ( url : string ) : Promise < boolean > {
const argv =
process . platform === "darwin"
? [ "open" , url ]
: process . platform === "win32"
? [ "cmd" , "/c" , "start" , "" , url ]
: [ "xdg-open" , url ] ;
const result = await runCommandWithTimeout ( argv , { timeoutMs : 5_000 } ) . catch ( ( ) = > null ) ;
return Boolean ( result && result . code === 0 ) ;
}
2026-03-02 22:10:46 -08:00
function remediationForGatewayFailure (
detail : string | undefined ,
port : number ,
profile : string ,
) : string {
2026-03-01 16:11:40 -08:00
const normalized = detail ? . toLowerCase ( ) ? ? "" ;
2026-03-02 22:10:46 -08:00
const isDeviceAuthMismatch =
normalized . includes ( "device token mismatch" ) ||
normalized . includes ( "device signature invalid" ) ||
normalized . includes ( "device signature expired" ) ||
normalized . includes ( "device-signature" ) ;
if ( isDeviceAuthMismatch ) {
return [
` Gateway device-auth mismatch detected. Re-run \` openclaw --profile ${ profile } onboard --install-daemon --reset \` . ` ,
` Last resort (security downgrade): \` openclaw --profile ${ profile } config set gateway.controlUi.dangerouslyDisableDeviceAuth true \` . Revert after recovery: \` openclaw --profile ${ profile } config set gateway.controlUi.dangerouslyDisableDeviceAuth false \` . ` ,
] . join ( " " ) ;
2026-03-01 16:11:40 -08:00
}
2026-03-15 04:16:01 -07:00
if ( normalized . includes ( "missing scope" ) ) {
return [
` Gateway scope check failed ( ${ detail } ). ` ,
` Re-run \` openclaw --profile ${ profile } onboard --install-daemon --reset \` to re-pair with full operator scopes. ` ,
` If the problem persists, set OPENCLAW_GATEWAY_PASSWORD and restart the web runtime. ` ,
] . join ( " " ) ;
}
2026-03-01 16:11:40 -08:00
if (
normalized . includes ( "unauthorized" ) ||
normalized . includes ( "token" ) ||
normalized . includes ( "password" )
) {
2026-03-02 22:10:46 -08:00
return ` Gateway auth mismatch detected. Re-run \` openclaw --profile ${ profile } onboard --install-daemon --reset \` . ` ;
2026-03-01 16:11:40 -08:00
}
if ( normalized . includes ( "address already in use" ) || normalized . includes ( "eaddrinuse" ) ) {
2026-03-03 12:24:43 -08:00
return ` Port ${ port } is busy. The bootstrap will auto-assign an available port, or you can explicitly specify one with \` --gateway-port <port> \` . ` ;
2026-03-01 16:11:40 -08:00
}
2026-03-04 18:33:17 -08:00
return ` Run \` openclaw --profile ${ profile } doctor --fix \` and retry \` npx denchclaw bootstrap \` . ` ;
2026-03-01 16:11:40 -08:00
}
function remediationForWebUiFailure ( port : number ) : string {
2026-03-04 16:32:58 -08:00
return [
` Web UI did not respond on ${ port } . ` ,
2026-03-04 18:33:17 -08:00
` Run \` npx denchclaw update --web-port ${ port } \` to refresh the managed web runtime. ` ,
` If the port is stuck, run \` npx denchclaw stop --web-port ${ port } \` first. ` ,
2026-03-04 16:32:58 -08:00
] . join ( " " ) ;
2026-03-01 16:11:40 -08:00
}
2026-03-02 18:31:15 -08:00
function describeWorkspaceSeedResult ( result : WorkspaceSeedResult ) : string {
if ( result . seeded ) {
return ` seeded ${ result . dbPath } ` ;
}
if ( result . reason === "already-exists" ) {
return ` skipped; existing database found at ${ result . dbPath } ` ;
}
if ( result . reason === "seed-asset-missing" ) {
return ` skipped; seed asset missing at ${ result . seedDbPath } ` ;
}
if ( result . reason === "copy-failed" ) {
return ` failed to copy seed database: ${ result . error ? ? "unknown error" } ` ;
}
return ` skipped; reason= ${ result . reason } ` ;
}
2026-03-01 16:11:40 -08:00
function createCheck (
id : BootstrapCheck [ "id" ] ,
status : BootstrapCheckStatus ,
detail : string ,
remediation? : string ,
) : BootstrapCheck {
return { id , status , detail , remediation } ;
}
2026-03-02 18:31:15 -08:00
/ * *
* Load OpenClaw profile config from state dir .
* Supports both openclaw . json ( current ) and config . json ( legacy ) .
* /
function readBootstrapConfig ( stateDir : string ) : Record < string , unknown > | undefined {
for ( const name of [ "openclaw.json" , "config.json" ] ) {
const configPath = path . join ( stateDir , name ) ;
if ( ! existsSync ( configPath ) ) {
continue ;
}
try {
2026-03-15 04:16:01 -07:00
const raw = json5 . parse ( readFileSync ( configPath , "utf-8" ) ) ;
2026-03-02 18:31:15 -08:00
if ( raw && typeof raw === "object" ) {
return raw as Record < string , unknown > ;
}
} catch {
// Config unreadable; skip.
}
}
return undefined ;
}
2026-03-03 12:24:43 -08:00
function resolveBootstrapWorkspaceDir ( stateDir : string ) : string {
2026-03-03 13:47:23 -08:00
return path . join ( stateDir , "workspace" ) ;
2026-03-02 18:31:15 -08:00
}
/ * *
* Resolve the model provider prefix from the config ' s primary model string .
* e . g . "vercel-ai-gateway/anthropic/claude-opus-4.6" → "vercel-ai-gateway"
* /
function resolveModelProvider ( stateDir : string ) : string | undefined {
const raw = readBootstrapConfig ( stateDir ) ;
const model = ( raw as { agents ? : { defaults ? : { model ? : { primary? : string } | string } } } )
? . agents ? . defaults ? . model ;
const modelName = typeof model === "string" ? model : model?.primary ;
if ( typeof modelName === "string" && modelName . includes ( "/" ) ) {
return modelName . split ( "/" ) [ 0 ] ;
}
return undefined ;
}
/ * *
* Check if the agent auth store has at least one key for the given provider .
* /
export function checkAgentAuth (
stateDir : string ,
provider : string | undefined ,
) : { ok : boolean ; provider? : string ; detail : string } {
if ( ! provider ) {
return { ok : false , detail : "No model provider configured." } ;
}
2026-03-15 04:16:01 -07:00
const rawConfig = readBootstrapConfig ( stateDir ) as {
models ? : {
providers? : Record < string , unknown > ;
} ;
} | undefined ;
const customProvider = rawConfig ? . models ? . providers ? . [ provider ] ;
if ( customProvider && typeof customProvider === "object" ) {
const apiKey = ( customProvider as Record < string , unknown > ) . apiKey ;
if (
( typeof apiKey === "string" && apiKey . trim ( ) . length > 0 ) ||
( apiKey && typeof apiKey === "object" )
) {
return {
ok : true ,
provider ,
detail : ` Custom provider credentials configured for ${ provider } . ` ,
} ;
}
}
2026-03-02 18:31:15 -08:00
const authPath = path . join ( stateDir , "agents" , "main" , "agent" , "auth-profiles.json" ) ;
if ( ! existsSync ( authPath ) ) {
return {
ok : false ,
provider ,
detail : ` No auth-profiles.json found for agent (expected at ${ authPath } ). ` ,
} ;
}
try {
2026-03-15 04:16:01 -07:00
const raw = json5 . parse ( readFileSync ( authPath , "utf-8" ) ) ;
2026-03-02 18:31:15 -08:00
const profiles = raw ? . profiles ;
if ( ! profiles || typeof profiles !== "object" ) {
return { ok : false , provider , detail : ` auth-profiles.json has no profiles configured. ` } ;
}
const hasKey = Object . values ( profiles ) . some (
( p : unknown ) = >
p &&
typeof p === "object" &&
( p as Record < string , unknown > ) . provider === provider &&
typeof ( p as Record < string , unknown > ) . key === "string" &&
( ( p as Record < string , unknown > ) . key as string ) . length > 0 ,
) ;
if ( ! hasKey ) {
return {
ok : false ,
provider ,
detail : ` No API key for provider " ${ provider } " in agent auth store. ` ,
} ;
}
return { ok : true , provider , detail : ` API key configured for ${ provider } . ` } ;
} catch {
return { ok : false , provider , detail : ` Failed to read auth-profiles.json. ` } ;
}
}
2026-03-01 16:11:40 -08:00
export function buildBootstrapDiagnostics ( params : {
profile : string ;
openClawCliAvailable : boolean ;
openClawVersion? : string ;
gatewayPort : number ;
gatewayUrl : string ;
gatewayProbe : { ok : boolean ; detail? : string } ;
webPort : number ;
webReachable : boolean ;
rolloutStage : BootstrapRolloutStage ;
legacyFallbackEnabled : boolean ;
2026-03-02 18:31:15 -08:00
stateDir? : string ;
2026-03-01 16:11:40 -08:00
env? : NodeJS.ProcessEnv ;
2026-03-05 10:46:03 -08:00
posthogPluginInstalled? : boolean ;
2026-03-01 16:11:40 -08:00
} ) : BootstrapDiagnostics {
const env = params . env ? ? process . env ;
const checks : BootstrapCheck [ ] = [ ] ;
if ( params . openClawCliAvailable ) {
checks . push (
createCheck (
"openclaw-cli" ,
"pass" ,
` OpenClaw CLI detected ${ params . openClawVersion ? ` ( ${ params . openClawVersion } ) ` : "" } . ` ,
) ,
) ;
} else {
checks . push (
createCheck (
"openclaw-cli" ,
"fail" ,
"OpenClaw CLI is missing." ,
"Install OpenClaw globally once: `npm install -g openclaw`." ,
) ,
) ;
}
2026-03-04 13:18:39 -08:00
if ( params . profile === DEFAULT_DENCHCLAW_PROFILE ) {
2026-03-03 12:24:43 -08:00
checks . push ( createCheck ( "profile" , "pass" , ` Profile pinned: ${ params . profile } . ` ) ) ;
2026-03-01 16:11:40 -08:00
} else {
checks . push (
createCheck (
"profile" ,
2026-03-03 12:24:43 -08:00
"fail" ,
2026-03-04 13:18:39 -08:00
` DenchClaw profile drift detected ( ${ params . profile } ). ` ,
` DenchClaw requires \` --profile ${ DEFAULT_DENCHCLAW_PROFILE } \` . Re-run bootstrap to repair environment defaults. ` ,
2026-03-01 16:11:40 -08:00
) ,
) ;
}
if ( params . gatewayProbe . ok ) {
checks . push ( createCheck ( "gateway" , "pass" , ` Gateway reachable at ${ params . gatewayUrl } . ` ) ) ;
} else {
checks . push (
createCheck (
"gateway" ,
"fail" ,
` Gateway probe failed at ${ params . gatewayUrl } ${ params . gatewayProbe . detail ? ` ( ${ params . gatewayProbe . detail } ) ` : "" } . ` ,
2026-03-02 22:10:46 -08:00
remediationForGatewayFailure (
params . gatewayProbe . detail ,
params . gatewayPort ,
params . profile ,
) ,
2026-03-01 16:11:40 -08:00
) ,
) ;
}
2026-03-02 18:31:15 -08:00
const stateDir = params . stateDir ? ? resolveProfileStateDir ( params . profile , env ) ;
const modelProvider = resolveModelProvider ( stateDir ) ;
const authCheck = checkAgentAuth ( stateDir , modelProvider ) ;
if ( authCheck . ok ) {
checks . push ( createCheck ( "agent-auth" , "pass" , authCheck . detail ) ) ;
} else {
checks . push (
createCheck (
"agent-auth" ,
"fail" ,
authCheck . detail ,
2026-03-04 13:18:39 -08:00
` Run \` openclaw --profile ${ DEFAULT_DENCHCLAW_PROFILE } onboard --install-daemon \` to configure API keys. ` ,
2026-03-02 18:31:15 -08:00
) ,
) ;
}
2026-03-01 16:11:40 -08:00
if ( params . webReachable ) {
checks . push ( createCheck ( "web-ui" , "pass" , ` Web UI reachable on port ${ params . webPort } . ` ) ) ;
} else {
checks . push (
createCheck (
"web-ui" ,
"fail" ,
` Web UI is not reachable on port ${ params . webPort } . ` ,
remediationForWebUiFailure ( params . webPort ) ,
) ,
) ;
}
2026-03-04 13:18:39 -08:00
const expectedStateDir = resolveProfileStateDir ( DEFAULT_DENCHCLAW_PROFILE , env ) ;
2026-03-03 12:24:43 -08:00
const usesPinnedStateDir = path . resolve ( stateDir ) === path . resolve ( expectedStateDir ) ;
if ( usesPinnedStateDir ) {
checks . push ( createCheck ( "state-isolation" , "pass" , ` State dir pinned: ${ stateDir } . ` ) ) ;
2026-03-01 16:11:40 -08:00
} else {
checks . push (
createCheck (
"state-isolation" ,
"fail" ,
2026-03-03 12:24:43 -08:00
` Unexpected state dir: ${ stateDir } . ` ,
2026-03-04 13:18:39 -08:00
` DenchClaw requires \` ${ expectedStateDir } \` . Re-run bootstrap to restore pinned defaults. ` ,
2026-03-01 16:11:40 -08:00
) ,
) ;
}
const launchAgentLabel = resolveGatewayLaunchAgentLabel ( params . profile ) ;
2026-03-04 13:18:39 -08:00
const expectedLaunchAgentLabel = resolveGatewayLaunchAgentLabel ( DEFAULT_DENCHCLAW_PROFILE ) ;
2026-03-03 12:24:43 -08:00
if ( launchAgentLabel === expectedLaunchAgentLabel ) {
2026-03-01 16:11:40 -08:00
checks . push ( createCheck ( "daemon-label" , "pass" , ` Gateway service label: ${ launchAgentLabel } . ` ) ) ;
} else {
checks . push (
createCheck (
"daemon-label" ,
"fail" ,
2026-03-03 12:24:43 -08:00
` Gateway service label mismatch ( ${ launchAgentLabel } ). ` ,
2026-03-04 13:18:39 -08:00
` DenchClaw requires launch agent label ${ expectedLaunchAgentLabel } . ` ,
2026-03-01 16:11:40 -08:00
) ,
) ;
}
checks . push (
createCheck (
"rollout-stage" ,
params . rolloutStage === "default" ? "pass" : "warn" ,
` Bootstrap rollout stage: ${ params . rolloutStage } ${ params . legacyFallbackEnabled ? " (legacy fallback enabled)" : "" } . ` ,
params . rolloutStage === "beta"
2026-03-04 13:18:39 -08:00
? "Enable beta cutover by setting DENCHCLAW_BOOTSTRAP_BETA_OPT_IN=1."
2026-03-01 16:11:40 -08:00
: undefined ,
) ,
) ;
2026-03-04 13:18:39 -08:00
const migrationSuiteOk = isTruthyEnvValue ( env . DENCHCLAW_BOOTSTRAP_MIGRATION_SUITE_OK ) ;
const onboardingE2EOk = isTruthyEnvValue ( env . DENCHCLAW_BOOTSTRAP_ONBOARDING_E2E_OK ) ;
const enforceCutoverGates = isTruthyEnvValue ( env . DENCHCLAW_BOOTSTRAP_ENFORCE_SAFETY_GATES ) ;
2026-03-01 16:11:40 -08:00
const cutoverGatePassed = migrationSuiteOk && onboardingE2EOk ;
checks . push (
createCheck (
"cutover-gates" ,
cutoverGatePassed ? "pass" : enforceCutoverGates ? "fail" : "warn" ,
` Cutover gate: migrationSuite= ${ migrationSuiteOk ? "pass" : "missing" } , onboardingE2E= ${ onboardingE2EOk ? "pass" : "missing" } . ` ,
cutoverGatePassed
? undefined
2026-03-04 13:18:39 -08:00
: "Run migration contracts + onboarding E2E and set DENCHCLAW_BOOTSTRAP_MIGRATION_SUITE_OK=1 and DENCHCLAW_BOOTSTRAP_ONBOARDING_E2E_OK=1 before full cutover." ,
2026-03-01 16:11:40 -08:00
) ,
) ;
2026-03-05 10:46:03 -08:00
if ( params . posthogPluginInstalled != null ) {
checks . push (
createCheck (
"posthog-analytics" ,
params . posthogPluginInstalled ? "pass" : "warn" ,
params . posthogPluginInstalled
? "PostHog analytics plugin installed."
: "PostHog analytics plugin not installed (POSTHOG_KEY missing or extension not bundled)." ,
) ,
) ;
}
2026-03-01 16:11:40 -08:00
return {
rolloutStage : params.rolloutStage ,
legacyFallbackEnabled : params.legacyFallbackEnabled ,
checks ,
hasFailures : checks.some ( ( check ) = > check . status === "fail" ) ,
} ;
}
function formatCheckStatus ( status : BootstrapCheckStatus ) : string {
if ( status === "pass" ) {
return theme . success ( "[ok]" ) ;
}
if ( status === "warn" ) {
return theme . warn ( "[warn]" ) ;
}
return theme . error ( "[fail]" ) ;
}
function logBootstrapChecklist ( diagnostics : BootstrapDiagnostics , runtime : RuntimeEnv ) {
runtime . log ( "" ) ;
runtime . log ( theme . heading ( "Bootstrap checklist" ) ) ;
for ( const check of diagnostics . checks ) {
runtime . log ( ` ${ formatCheckStatus ( check . status ) } ${ check . detail } ` ) ;
if ( check . status !== "pass" && check . remediation ) {
runtime . log ( theme . muted ( ` remediation: ${ check . remediation } ` ) ) ;
}
}
}
2026-03-15 04:16:01 -07:00
function isExplicitDenchCloudRequest ( opts : BootstrapOptions ) : boolean {
return Boolean (
opts . denchCloud ||
opts . denchCloudApiKey ? . trim ( ) ||
opts . denchCloudModel ? . trim ( ) ||
opts . denchGatewayUrl ? . trim ( ) ,
) ;
}
function resolveDenchCloudApiKeyCandidate ( params : {
opts : BootstrapOptions ;
existingApiKey? : string ;
} ) : string | undefined {
return (
params . opts . denchCloudApiKey ? . trim ( ) ||
process . env . DENCH_CLOUD_API_KEY ? . trim ( ) ||
process . env . DENCH_API_KEY ? . trim ( ) ||
params . existingApiKey ? . trim ( )
) ;
}
async function promptForDenchCloudApiKey ( initialValue? : string ) : Promise < string | undefined > {
const value = await text ( {
message : stylePromptMessage (
"Enter your Dench Cloud API key (sign up at dench.com and get it at dench.com/settings)" ,
) ,
. . . ( initialValue ? { initialValue } : { } ) ,
validate : ( input ) = > ( input ? . trim ( ) . length ? undefined : "API key is required." ) ,
} ) ;
if ( isCancel ( value ) ) {
return undefined ;
}
return String ( value ) . trim ( ) ;
}
async function promptForDenchCloudModel ( params : {
models : DenchCloudCatalogModel [ ] ;
initialStableId? : string ;
} ) : Promise < string | undefined > {
const sorted = [ . . . params . models ] . sort ( ( a , b ) = > {
const aRec = a . id === RECOMMENDED_DENCH_CLOUD_MODEL_ID ? 0 : 1 ;
const bRec = b . id === RECOMMENDED_DENCH_CLOUD_MODEL_ID ? 0 : 1 ;
return aRec - bRec ;
} ) ;
const selection = await select ( {
message : stylePromptMessage ( "Choose your default Dench Cloud model" ) ,
options : sorted.map ( ( model ) = > ( {
value : model.stableId ,
label : model.displayName ,
hint : formatDenchCloudModelHint ( model ) ,
} ) ) ,
. . . ( params . initialStableId ? { initialValue : params.initialStableId } : { } ) ,
} ) ;
if ( isCancel ( selection ) ) {
return undefined ;
}
return String ( selection ) ;
}
async function applyDenchCloudBootstrapConfig ( params : {
openclawCommand : string ;
profile : string ;
stateDir : string ;
gatewayUrl : string ;
apiKey : string ;
catalog : DenchCloudCatalogLoadResult ;
selectedModel : string ;
} ) : Promise < void > {
const raw = readBootstrapConfig ( params . stateDir ) as {
agents ? : {
defaults ? : {
models? : unknown ;
} ;
} ;
} | undefined ;
const existingAgentModels =
raw ? . agents ? . defaults ? . models && typeof raw . agents . defaults . models === "object"
? ( raw . agents . defaults . models as Record < string , unknown > )
: { } ;
const configPatch = buildDenchCloudConfigPatch ( {
gatewayUrl : params.gatewayUrl ,
apiKey : params.apiKey ,
models : params.catalog.models ,
} ) ;
const nextAgentModels = {
. . . existingAgentModels ,
. . . ( ( configPatch . agents ? . defaults ? . models as Record < string , unknown > | undefined ) ? ? { } ) ,
} ;
await runOpenClawOrThrow ( {
openclawCommand : params.openclawCommand ,
args : [ "--profile" , params . profile , "config" , "set" , "models.mode" , "merge" ] ,
timeoutMs : 30_000 ,
errorMessage : "Failed to set models.mode=merge for Dench Cloud." ,
} ) ;
await setOpenClawConfigJson ( {
openclawCommand : params.openclawCommand ,
profile : params.profile ,
key : "models.providers.dench-cloud" ,
value : configPatch.models.providers [ "dench-cloud" ] ,
errorMessage : "Failed to configure models.providers.dench-cloud." ,
} ) ;
await runOpenClawOrThrow ( {
openclawCommand : params.openclawCommand ,
args : [
"--profile" ,
params . profile ,
"config" ,
"set" ,
"agents.defaults.model.primary" ,
` dench-cloud/ ${ params . selectedModel } ` ,
] ,
timeoutMs : 30_000 ,
errorMessage : "Failed to set the default Dench Cloud model." ,
} ) ;
await setOpenClawConfigJson ( {
openclawCommand : params.openclawCommand ,
profile : params.profile ,
key : "agents.defaults.models" ,
value : nextAgentModels ,
errorMessage : "Failed to update agents.defaults.models for Dench Cloud." ,
} ) ;
}
async function resolveDenchCloudBootstrapSelection ( params : {
opts : BootstrapOptions ;
nonInteractive : boolean ;
stateDir : string ;
runtime : RuntimeEnv ;
} ) : Promise < DenchCloudBootstrapSelection > {
const rawConfig = readBootstrapConfig ( params . stateDir ) ;
const existing = readConfiguredDenchCloudSettings ( rawConfig ) ;
const explicitRequest = isExplicitDenchCloudRequest ( params . opts ) ;
const currentProvider = resolveModelProvider ( params . stateDir ) ;
const existingDenchConfigured = currentProvider === "dench-cloud" && Boolean ( existing . apiKey ) ;
const gatewayUrl = normalizeDenchGatewayUrl (
params . opts . denchGatewayUrl ? . trim ( ) ||
process . env . DENCH_GATEWAY_URL ? . trim ( ) ||
existing . gatewayUrl ||
DEFAULT_DENCH_CLOUD_GATEWAY_URL ,
) ;
if ( params . nonInteractive ) {
if ( ! explicitRequest && ! existingDenchConfigured ) {
return { enabled : false } ;
}
const apiKey = resolveDenchCloudApiKeyCandidate ( {
opts : params.opts ,
existingApiKey : existing.apiKey ,
} ) ;
if ( ! apiKey ) {
throw new Error (
"Dench Cloud bootstrap requires --dench-cloud-api-key or DENCH_CLOUD_API_KEY in non-interactive mode." ,
) ;
}
await validateDenchCloudApiKey ( gatewayUrl , apiKey ) ;
const catalog = await fetchDenchCloudCatalog ( gatewayUrl ) ;
const selected = resolveDenchCloudModel (
catalog . models ,
params . opts . denchCloudModel ? . trim ( ) ||
process . env . DENCH_CLOUD_MODEL ? . trim ( ) ||
existing . selectedModel ,
) ;
if ( ! selected ) {
throw new Error ( "Configured Dench Cloud model is not available." ) ;
}
return {
enabled : true ,
apiKey ,
gatewayUrl ,
selectedModel : selected.stableId ,
catalog ,
} ;
}
const wantsDenchCloud = explicitRequest
? true
: await confirm ( {
message : stylePromptMessage (
"Use Dench API Key for inference? Sign up on dench.com and get your API key at dench.com/settings." ,
) ,
initialValue : existingDenchConfigured || ! currentProvider ,
} ) ;
if ( isCancel ( wantsDenchCloud ) || ! wantsDenchCloud ) {
return { enabled : false } ;
}
let apiKey = resolveDenchCloudApiKeyCandidate ( {
opts : params.opts ,
existingApiKey : existing.apiKey ,
} ) ;
const showSpinners = ! params . opts . json ;
while ( true ) {
apiKey = await promptForDenchCloudApiKey ( apiKey ) ;
if ( ! apiKey ) {
throw new Error ( "Dench Cloud setup cancelled before an API key was provided." ) ;
}
const keySpinner = showSpinners ? spinner ( ) : null ;
keySpinner ? . start ( "Validating API key…" ) ;
try {
await validateDenchCloudApiKey ( gatewayUrl , apiKey ) ;
keySpinner ? . stop ( "API key is valid." ) ;
} catch ( error ) {
keySpinner ? . stop ( "API key validation failed." ) ;
params . runtime . log ( theme . warn ( error instanceof Error ? error.message : String ( error ) ) ) ;
const retry = await confirm ( {
message : stylePromptMessage ( "Try another Dench Cloud API key?" ) ,
initialValue : true ,
} ) ;
if ( isCancel ( retry ) || ! retry ) {
throw error instanceof Error ? error : new Error ( String ( error ) ) ;
}
continue ;
}
const catalogSpinner = showSpinners ? spinner ( ) : null ;
catalogSpinner ? . start ( "Fetching available models…" ) ;
const catalog = await fetchDenchCloudCatalog ( gatewayUrl ) ;
if ( catalog . source === "fallback" ) {
catalogSpinner ? . stop (
` Model catalog fallback active ( ${ catalog . detail ? ? "public catalog unavailable" } ). ` ,
) ;
} else {
catalogSpinner ? . stop ( "Models loaded." ) ;
}
const explicitModel = params . opts . denchCloudModel ? . trim ( ) || process . env . DENCH_CLOUD_MODEL ? . trim ( ) ;
const preselected = resolveDenchCloudModel ( catalog . models , explicitModel || existing . selectedModel ) ;
if ( ! preselected && explicitModel ) {
params . runtime . log ( theme . warn ( ` Configured Dench Cloud model " ${ explicitModel } " is unavailable. ` ) ) ;
}
const selection = await promptForDenchCloudModel ( {
models : catalog.models ,
initialStableId : preselected?.stableId || existing . selectedModel ,
} ) ;
if ( ! selection ) {
throw new Error ( "Dench Cloud setup cancelled during model selection." ) ;
}
const selected = resolveDenchCloudModel ( catalog . models , selection ) ;
if ( ! selected ) {
throw new Error ( "No Dench Cloud model could be selected." ) ;
}
const verifySpinner = showSpinners ? spinner ( ) : null ;
verifySpinner ? . start ( "Verifying Dench Cloud configuration…" ) ;
try {
await validateDenchCloudApiKey ( gatewayUrl , apiKey ) ;
verifySpinner ? . stop ( "Dench Cloud ready." ) ;
} catch ( error ) {
verifySpinner ? . stop ( "Verification failed." ) ;
params . runtime . log (
theme . warn ( error instanceof Error ? error.message : String ( error ) ) ,
) ;
const retry = await confirm ( {
message : stylePromptMessage ( "Re-enter your Dench Cloud API key?" ) ,
initialValue : true ,
} ) ;
if ( isCancel ( retry ) || ! retry ) {
throw error instanceof Error ? error : new Error ( String ( error ) ) ;
}
continue ;
}
return {
enabled : true ,
apiKey ,
gatewayUrl ,
selectedModel : selected.stableId ,
catalog ,
} ;
}
}
2026-03-01 16:11:40 -08:00
async function shouldRunUpdate ( params : {
opts : BootstrapOptions ;
runtime : RuntimeEnv ;
2026-03-04 16:32:58 -08:00
installResult : OpenClawCliAvailability ;
2026-03-01 16:11:40 -08:00
} ) : Promise < boolean > {
if ( params . opts . updateNow ) {
return true ;
}
if (
params . opts . skipUpdate ||
params . opts . nonInteractive ||
params . opts . json ||
! process . stdin . isTTY
) {
return false ;
}
2026-03-04 16:32:58 -08:00
const installedRecently =
params . installResult . installed ||
( typeof params . installResult . installedAt === "number" &&
Date . now ( ) - params . installResult . installedAt <=
OPENCLAW_UPDATE_PROMPT_SUPPRESS_AFTER_INSTALL_MS ) ;
if ( installedRecently ) {
params . runtime . log (
theme . muted ( "Skipping update prompt because OpenClaw was installed moments ago." ) ,
) ;
return false ;
}
2026-03-01 16:11:40 -08:00
const decision = await confirm ( {
message : stylePromptMessage ( "Check and install OpenClaw updates now?" ) ,
initialValue : false ,
} ) ;
if ( isCancel ( decision ) ) {
params . runtime . log ( theme . muted ( "Update check skipped." ) ) ;
return false ;
}
return Boolean ( decision ) ;
}
export async function bootstrapCommand (
opts : BootstrapOptions ,
runtime : RuntimeEnv = defaultRuntime ,
) : Promise < BootstrapSummary > {
const nonInteractive = Boolean ( opts . nonInteractive || opts . json ) ;
const rolloutStage = resolveBootstrapRolloutStage ( ) ;
const legacyFallbackEnabled = isLegacyFallbackEnabled ( ) ;
2026-03-03 12:24:43 -08:00
const appliedProfile = applyCliProfileEnv ( { profile : opts.profile } ) ;
const profile = appliedProfile . effectiveProfile ;
2026-03-04 16:32:58 -08:00
const stateDir = resolveProfileStateDir ( profile ) ;
const workspaceDir = resolveBootstrapWorkspaceDir ( stateDir ) ;
2026-03-03 12:24:43 -08:00
if ( appliedProfile . warning && ! opts . json ) {
runtime . log ( theme . warn ( appliedProfile . warning ) ) ;
}
2026-03-01 16:11:40 -08:00
2026-03-04 17:33:27 -08:00
const bootstrapStartTime = Date . now ( ) ;
if ( ! opts . json ) {
const telemetryCfg = readTelemetryConfig ( ) ;
if ( ! telemetryCfg . noticeShown ) {
runtime . log (
theme . muted (
2026-03-04 18:33:17 -08:00
"Dench collects anonymous telemetry to improve the product.\n" +
2026-03-04 17:33:27 -08:00
"No personal data is ever collected. Disable anytime:\n" +
2026-03-04 18:33:17 -08:00
" npx denchclaw telemetry disable\n" +
2026-03-04 17:33:27 -08:00
" DENCHCLAW_TELEMETRY_DISABLED=1\n" +
" DO_NOT_TRACK=1\n" +
2026-03-04 18:33:17 -08:00
"Learn more: https://github.com/DenchHQ/DenchClaw/blob/main/TELEMETRY.md\n" ,
2026-03-04 17:33:27 -08:00
) ,
) ;
markNoticeShown ( ) ;
}
}
2026-03-04 18:33:17 -08:00
track ( "cli_bootstrap_started" , { version : VERSION } ) ;
2026-03-04 16:32:58 -08:00
const installResult = await ensureOpenClawCliAvailable ( {
stateDir ,
showProgress : ! opts . json ,
} ) ;
2026-03-01 16:11:40 -08:00
if ( ! installResult . available ) {
throw new Error (
[
"OpenClaw CLI is required but unavailable." ,
"Install it with: npm install -g openclaw" ,
2026-03-02 18:31:15 -08:00
installResult . globalBinDir
? ` Expected global binary directory: ${ installResult . globalBinDir } `
: "" ,
]
. filter ( ( line ) = > line . length > 0 )
. join ( "\n" ) ,
2026-03-01 16:11:40 -08:00
) ;
}
2026-03-02 18:31:15 -08:00
const openclawCommand = installResult . command ;
2026-03-01 16:11:40 -08:00
2026-03-04 16:32:58 -08:00
if ( await shouldRunUpdate ( { opts , runtime , installResult } ) ) {
2026-03-02 22:10:46 -08:00
await runOpenClawWithProgress ( {
openclawCommand ,
args : [ "update" , "--yes" ] ,
timeoutMs : 8 * 60 _000 ,
startMessage : "Checking for OpenClaw updates..." ,
successMessage : "OpenClaw is up to date." ,
errorMessage : "OpenClaw update failed" ,
} ) ;
}
2026-03-05 10:46:03 -08:00
// Determine gateway port: use explicit override, honour previously persisted
// port, or find an available one in the DenchClaw range (19001+).
// NEVER claim OpenClaw's default port (18789) — that belongs to the host
// OpenClaw installation and sharing it causes port-hijack on restart.
2026-03-15 04:16:01 -07:00
//
// When a persisted port exists, trust it unconditionally — the process
// occupying it is almost certainly our own gateway from a previous run.
// The onboard step will stop/replace the existing daemon on the same profile.
// Only scan for a free port on first run (no persisted port) when 19001 is
// occupied by something external.
const preCloudSpinner = ! opts . json ? spinner ( ) : null ;
preCloudSpinner ? . start ( "Preparing gateway configuration…" ) ;
2026-03-03 12:24:43 -08:00
const explicitPort = parseOptionalPort ( opts . gatewayPort ) ;
let gatewayPort : number ;
let portAutoAssigned = false ;
if ( explicitPort ) {
gatewayPort = explicitPort ;
} else {
2026-03-05 10:46:03 -08:00
const existingPort = readExistingGatewayPort ( stateDir ) ;
2026-03-15 04:16:01 -07:00
if ( isPersistedPortAcceptable ( existingPort ) ) {
2026-03-05 10:46:03 -08:00
gatewayPort = existingPort ;
} else if ( await isPortAvailable ( DENCHCLAW_GATEWAY_PORT_START ) ) {
gatewayPort = DENCHCLAW_GATEWAY_PORT_START ;
} else {
2026-03-15 04:16:01 -07:00
preCloudSpinner ? . message ( "Scanning for available port…" ) ;
2026-03-05 10:46:03 -08:00
const availablePort = await findAvailablePort (
DENCHCLAW_GATEWAY_PORT_START + 1 ,
MAX_PORT_SCAN_ATTEMPTS ,
2026-03-03 12:24:43 -08:00
) ;
2026-03-05 10:46:03 -08:00
if ( ! availablePort ) {
2026-03-15 04:16:01 -07:00
preCloudSpinner ? . stop ( "Port scan failed." ) ;
2026-03-05 10:46:03 -08:00
throw new Error (
` Could not find an available gateway port between ${ DENCHCLAW_GATEWAY_PORT_START } and ${ DENCHCLAW_GATEWAY_PORT_START + MAX_PORT_SCAN_ATTEMPTS } . ` +
` Please specify a port explicitly with --gateway-port. ` ,
) ;
}
gatewayPort = availablePort ;
portAutoAssigned = true ;
2026-03-03 12:24:43 -08:00
}
}
if ( portAutoAssigned && ! opts . json ) {
runtime . log (
theme . muted (
2026-03-05 10:46:03 -08:00
` Default gateway port ${ DENCHCLAW_GATEWAY_PORT_START } is in use. Using auto-assigned port ${ gatewayPort } . ` ,
2026-03-03 12:24:43 -08:00
) ,
) ;
}
2026-03-03 13:47:23 -08:00
// Pin OpenClaw to the managed default workspace before onboarding so bootstrap
// never drifts into creating/using legacy workspace-* paths.
2026-03-15 04:16:01 -07:00
preCloudSpinner ? . message ( "Configuring default workspace…" ) ;
2026-03-03 13:47:23 -08:00
await ensureDefaultWorkspacePath ( openclawCommand , profile , workspaceDir ) ;
2026-03-15 04:16:01 -07:00
preCloudSpinner ? . stop ( "Gateway ready." ) ;
const denchCloudSelection = await resolveDenchCloudBootstrapSelection ( {
opts ,
nonInteractive ,
stateDir ,
runtime ,
} ) ;
2026-03-05 12:28:08 -08:00
const packageRoot = resolveCliPackageRoot ( ) ;
2026-03-15 04:16:01 -07:00
const managedBundledPlugins : BundledPluginSpec [ ] = [
{
pluginId : "posthog-analytics" ,
sourceDirName : "posthog-analytics" ,
. . . ( process . env . POSTHOG_KEY
? {
enabled : true ,
config : {
apiKey : process.env.POSTHOG_KEY ,
} ,
}
: { } ) ,
} ,
{
pluginId : "dench-ai-gateway" ,
sourceDirName : "dench-ai-gateway" ,
enabled : true ,
config : {
gatewayUrl :
denchCloudSelection . gatewayUrl ||
opts . denchGatewayUrl ? . trim ( ) ||
process . env . DENCH_GATEWAY_URL ? . trim ( ) ||
DEFAULT_DENCH_CLOUD_GATEWAY_URL ,
} ,
} ,
] ;
2026-03-05 12:28:08 -08:00
2026-03-15 04:16:01 -07:00
// Trust managed bundled plugins BEFORE onboard so the gateway daemon never
// starts with transient "untracked local plugin" warnings for DenchClaw-owned
// extensions.
const preOnboardSpinner = ! opts . json ? spinner ( ) : null ;
preOnboardSpinner ? . start ( "Syncing bundled plugins…" ) ;
const preOnboardPlugins = await syncBundledPlugins ( {
2026-03-05 12:28:08 -08:00
openclawCommand ,
profile ,
stateDir ,
2026-03-15 04:16:01 -07:00
plugins : managedBundledPlugins ,
restartGateway : true ,
2026-03-05 12:28:08 -08:00
} ) ;
2026-03-15 04:16:01 -07:00
const posthogPluginInstalled = preOnboardPlugins . installedPluginIds . includes ( "posthog-analytics" ) ;
2026-03-05 12:28:08 -08:00
2026-03-10 14:37:24 -07:00
// Ensure gateway.mode=local BEFORE onboard so the daemon starts successfully.
// Previously this ran post-onboard, but onboard --install-daemon starts the
// gateway immediately — if gateway.mode is unset at that point the daemon
// blocks with "set gateway.mode=local" and enters a crash loop.
2026-03-15 04:16:01 -07:00
preOnboardSpinner ? . message ( "Configuring gateway…" ) ;
2026-03-10 14:37:24 -07:00
await ensureGatewayModeLocal ( openclawCommand , profile ) ;
// Persist the assigned port so the daemon binds to the correct port on first
// start rather than falling back to the default.
await ensureGatewayPort ( openclawCommand , profile , gatewayPort ) ;
2026-03-15 04:16:01 -07:00
// Push plugin trust through the CLI as the LAST config step before onboard.
// syncBundledPlugins writes plugins.allow / plugins.load.paths to the raw
// JSON file, but subsequent `openclaw config set` calls may clobber them.
// Re-applying via the CLI ensures OpenClaw's own config resolution sees them.
if ( preOnboardPlugins . installedPluginIds . length > 0 ) {
preOnboardSpinner ? . message ( "Trusting managed plugins…" ) ;
await setOpenClawConfigJson ( {
openclawCommand ,
profile ,
key : "plugins.allow" ,
value : preOnboardPlugins.installedPluginIds ,
errorMessage : "Failed to set plugins.allow for managed plugins." ,
} ) ;
const pluginLoadPaths = managedBundledPlugins . map ( ( plugin ) = >
normalizeFilesystemPath ( path . join ( stateDir , "extensions" , plugin . sourceDirName ) ) ,
) ;
await setOpenClawConfigJson ( {
openclawCommand ,
profile ,
key : "plugins.load.paths" ,
value : pluginLoadPaths ,
errorMessage : "Failed to set plugins.load.paths for managed plugins." ,
} ) ;
}
preOnboardSpinner ? . stop ( "Ready to onboard." ) ;
2026-03-02 18:31:15 -08:00
const onboardArgv = [
"--profile" ,
profile ,
"onboard" ,
"--install-daemon" ,
"--gateway-bind" ,
"loopback" ,
"--gateway-port" ,
2026-03-03 12:24:43 -08:00
String ( gatewayPort ) ,
2026-03-02 18:31:15 -08:00
] ;
2026-03-02 22:10:46 -08:00
if ( opts . forceOnboard ) {
onboardArgv . push ( "--reset" ) ;
}
2026-03-02 18:31:15 -08:00
if ( nonInteractive ) {
2026-03-04 18:33:17 -08:00
onboardArgv . push ( "--non-interactive" ) ;
2026-03-02 18:31:15 -08:00
}
2026-03-15 04:16:01 -07:00
if ( denchCloudSelection . enabled ) {
onboardArgv . push ( "--auth-choice" , "skip" ) ;
}
2026-03-04 18:33:17 -08:00
onboardArgv . push ( "--accept-risk" , "--skip-ui" ) ;
2026-03-02 18:31:15 -08:00
if ( nonInteractive ) {
2026-03-01 16:11:40 -08:00
await runOpenClawOrThrow ( {
2026-03-02 18:31:15 -08:00
openclawCommand ,
args : onboardArgv ,
timeoutMs : 12 * 60 _000 ,
errorMessage : "OpenClaw onboarding failed." ,
} ) ;
} else {
await runOpenClawInteractiveOrThrow ( {
openclawCommand ,
args : onboardArgv ,
2026-03-01 16:11:40 -08:00
timeoutMs : 12 * 60 _000 ,
errorMessage : "OpenClaw onboarding failed." ,
} ) ;
}
2026-03-02 18:31:15 -08:00
const workspaceSeed = seedWorkspaceFromAssets ( {
2026-03-03 12:24:43 -08:00
workspaceDir ,
2026-03-04 16:32:58 -08:00
packageRoot ,
2026-03-02 18:31:15 -08:00
} ) ;
2026-03-01 16:11:40 -08:00
2026-03-04 18:33:17 -08:00
const postOnboardSpinner = ! opts . json ? spinner ( ) : null ;
postOnboardSpinner ? . start ( "Finalizing configuration…" ) ;
2026-03-15 01:05:43 -07:00
// Re-apply gateway settings after onboard so interactive/wizard flows cannot
// drift DenchClaw away from its required local gateway and selected port.
2026-03-02 18:31:15 -08:00
await ensureGatewayModeLocal ( openclawCommand , profile ) ;
2026-03-04 18:33:17 -08:00
postOnboardSpinner ? . message ( "Configuring gateway port…" ) ;
2026-03-03 12:24:43 -08:00
await ensureGatewayPort ( openclawCommand , profile , gatewayPort ) ;
2026-03-04 18:33:17 -08:00
postOnboardSpinner ? . message ( "Setting tools profile…" ) ;
2026-03-04 13:18:39 -08:00
// DenchClaw requires the full tool profile; onboarding defaults can drift to
// messaging-only, so enforce this on every bootstrap run.
await ensureToolsProfile ( openclawCommand , profile ) ;
2026-03-01 16:11:40 -08:00
2026-03-15 04:16:01 -07:00
if (
denchCloudSelection . enabled &&
denchCloudSelection . apiKey &&
denchCloudSelection . gatewayUrl &&
denchCloudSelection . selectedModel &&
denchCloudSelection . catalog
) {
postOnboardSpinner ? . message ( "Applying Dench Cloud model config…" ) ;
await applyDenchCloudBootstrapConfig ( {
openclawCommand ,
profile ,
stateDir ,
gatewayUrl : denchCloudSelection.gatewayUrl ,
apiKey : denchCloudSelection.apiKey ,
catalog : denchCloudSelection.catalog ,
selectedModel : denchCloudSelection.selectedModel ,
} ) ;
}
postOnboardSpinner ? . message ( "Refreshing managed plugin config…" ) ;
await syncBundledPlugins ( {
openclawCommand ,
profile ,
stateDir ,
plugins : managedBundledPlugins ,
restartGateway : true ,
} ) ;
2026-03-04 18:33:17 -08:00
postOnboardSpinner ? . message ( "Configuring subagent defaults…" ) ;
2026-03-03 16:34:03 -08:00
await ensureSubagentDefaults ( openclawCommand , profile ) ;
2026-03-04 18:33:17 -08:00
postOnboardSpinner ? . message ( "Probing gateway health…" ) ;
2026-03-03 12:24:43 -08:00
let gatewayProbe = await probeGateway ( openclawCommand , profile , gatewayPort ) ;
2026-03-02 18:31:15 -08:00
let gatewayAutoFix : GatewayAutoFixResult | undefined ;
if ( ! gatewayProbe . ok ) {
2026-03-04 18:33:17 -08:00
postOnboardSpinner ? . message ( "Gateway unreachable, attempting auto-fix…" ) ;
2026-03-02 18:31:15 -08:00
gatewayAutoFix = await attemptGatewayAutoFix ( {
openclawCommand ,
profile ,
stateDir ,
2026-03-03 12:24:43 -08:00
gatewayPort ,
2026-03-02 18:31:15 -08:00
} ) ;
gatewayProbe = gatewayAutoFix . finalProbe ;
if ( ! gatewayProbe . ok && gatewayAutoFix . failureSummary ) {
gatewayProbe = {
. . . gatewayProbe ,
detail : [ gatewayProbe . detail , gatewayAutoFix . failureSummary ]
. filter ( ( value , index , self ) = > value && self . indexOf ( value ) === index )
. join ( " | " ) ,
} ;
}
}
2026-03-03 12:24:43 -08:00
const gatewayUrl = ` ws://127.0.0.1: ${ gatewayPort } ` ;
2026-03-01 16:11:40 -08:00
const preferredWebPort = parseOptionalPort ( opts . webPort ) ? ? DEFAULT_WEB_APP_PORT ;
2026-03-04 18:33:17 -08:00
postOnboardSpinner ? . message ( ` Starting web runtime on port ${ preferredWebPort } … ` ) ;
2026-03-04 16:32:58 -08:00
const webRuntimeStatus = await ensureManagedWebRuntime ( {
stateDir ,
packageRoot ,
denchVersion : VERSION ,
port : preferredWebPort ,
gatewayPort ,
} ) ;
2026-03-04 18:33:17 -08:00
postOnboardSpinner ? . stop (
webRuntimeStatus . ready
? "Post-onboard setup complete."
: "Post-onboard setup complete (web runtime unhealthy)." ,
) ;
2026-03-04 16:32:58 -08:00
const webReachable = webRuntimeStatus . ready ;
2026-03-02 22:10:46 -08:00
const webUrl = ` http://localhost: ${ preferredWebPort } ` ;
2026-03-01 16:11:40 -08:00
const diagnostics = buildBootstrapDiagnostics ( {
profile ,
openClawCliAvailable : installResult.available ,
openClawVersion : installResult.version ,
2026-03-03 12:24:43 -08:00
gatewayPort ,
2026-03-01 16:11:40 -08:00
gatewayUrl ,
gatewayProbe ,
2026-03-02 22:10:46 -08:00
webPort : preferredWebPort ,
2026-03-01 16:11:40 -08:00
webReachable ,
rolloutStage ,
legacyFallbackEnabled ,
2026-03-02 18:31:15 -08:00
stateDir ,
2026-03-05 10:46:03 -08:00
posthogPluginInstalled ,
2026-03-01 16:11:40 -08:00
} ) ;
2026-03-05 12:28:08 -08:00
let opened = false ;
let openAttempted = false ;
if ( ! opts . noOpen && ! opts . json && webReachable ) {
if ( nonInteractive ) {
openAttempted = true ;
opened = await openUrl ( webUrl ) ;
} else {
const wantOpen = await confirm ( {
message : stylePromptMessage ( ` Open ${ webUrl } in your browser? ` ) ,
initialValue : true ,
} ) ;
if ( ! isCancel ( wantOpen ) && wantOpen ) {
openAttempted = true ;
opened = await openUrl ( webUrl ) ;
}
}
}
2026-03-01 16:11:40 -08:00
if ( ! opts . json ) {
2026-03-04 16:32:58 -08:00
if ( ! webRuntimeStatus . ready ) {
runtime . log ( theme . warn ( ` Managed web runtime check failed: ${ webRuntimeStatus . reason } ` ) ) ;
}
2026-03-02 18:31:15 -08:00
if ( installResult . installed ) {
runtime . log ( theme . muted ( "Installed global OpenClaw CLI via npm." ) ) ;
}
if ( isProjectLocalOpenClawPath ( installResult . shellCommandPath ) ) {
runtime . log (
theme . warn (
` \` openclaw \` currently resolves to a project-local binary ( ${ installResult . shellCommandPath } ). ` ,
) ,
) ;
runtime . log (
theme . muted (
` Bootstrap now uses the global binary ( ${ openclawCommand } ) to avoid repo-local drift. ` ,
) ,
) ;
} else if ( ! installResult . shellCommandPath && installResult . globalBinDir ) {
runtime . log (
theme . warn ( "Global OpenClaw was installed, but `openclaw` is not on shell PATH." ) ,
) ;
runtime . log (
theme . muted (
` Add this to your shell profile, then open a new terminal: export PATH=" ${ installResult . globalBinDir } : $ PATH" ` ,
) ,
) ;
}
runtime . log ( theme . muted ( ` Workspace seed: ${ describeWorkspaceSeedResult ( workspaceSeed ) } ` ) ) ;
if ( gatewayAutoFix ? . attempted ) {
runtime . log (
theme . muted (
` Gateway auto-fix ${ gatewayAutoFix . recovered ? "recovered connectivity" : "ran but gateway is still unhealthy" } . ` ,
) ,
) ;
for ( const step of gatewayAutoFix . steps ) {
runtime . log (
theme . muted (
` ${ step . ok ? "[ok]" : "[fail]" } ${ step . name } ${ step . detail ? ` ( ${ step . detail } ) ` : "" } ` ,
) ,
) ;
}
if ( ! gatewayAutoFix . recovered && gatewayAutoFix . failureSummary ) {
runtime . log ( theme . error ( ` Likely gateway cause: ${ gatewayAutoFix . failureSummary } ` ) ) ;
}
if ( ! gatewayAutoFix . recovered && gatewayAutoFix . logExcerpts . length > 0 ) {
runtime . log ( theme . muted ( "Recent gateway logs:" ) ) ;
for ( const excerpt of gatewayAutoFix . logExcerpts ) {
runtime . log ( theme . muted ( ` ${ excerpt . path } ` ) ) ;
for ( const line of excerpt . excerpt . split ( /\r?\n/ ) ) {
runtime . log ( theme . muted ( ` ${ line } ` ) ) ;
}
}
}
}
2026-03-01 16:11:40 -08:00
logBootstrapChecklist ( diagnostics , runtime ) ;
runtime . log ( "" ) ;
2026-03-04 13:18:39 -08:00
runtime . log ( theme . heading ( "DenchClaw ready" ) ) ;
2026-03-01 16:11:40 -08:00
runtime . log ( ` Profile: ${ profile } ` ) ;
runtime . log ( ` OpenClaw CLI: ${ installResult . version ? ? "detected" } ` ) ;
runtime . log ( ` Gateway: ${ gatewayProbe . ok ? "reachable" : "check failed" } ` ) ;
runtime . log ( ` Web UI: ${ webUrl } ` ) ;
runtime . log (
` Rollout stage: ${ rolloutStage } ${ legacyFallbackEnabled ? " (legacy fallback enabled)" : "" } ` ,
) ;
2026-03-05 12:28:08 -08:00
if ( ! opened && openAttempted ) {
2026-03-01 16:11:40 -08:00
runtime . log ( theme . muted ( "Browser open failed; copy/paste the URL above." ) ) ;
}
if ( diagnostics . hasFailures ) {
runtime . log (
theme . warn (
"Bootstrap completed with failing checks. Address remediation items above before full cutover." ,
) ,
) ;
}
}
const summary : BootstrapSummary = {
profile ,
2026-03-02 18:31:15 -08:00
onboarded : true ,
2026-03-01 16:11:40 -08:00
installedOpenClawCli : installResult.installed ,
openClawCliAvailable : installResult.available ,
openClawVersion : installResult.version ,
gatewayUrl ,
gatewayReachable : gatewayProbe.ok ,
2026-03-02 18:31:15 -08:00
gatewayAutoFix : gatewayAutoFix
? {
attempted : gatewayAutoFix.attempted ,
recovered : gatewayAutoFix.recovered ,
steps : gatewayAutoFix.steps ,
failureSummary : gatewayAutoFix.failureSummary ,
logExcerpts : gatewayAutoFix.logExcerpts ,
}
: undefined ,
workspaceSeed ,
2026-03-01 16:11:40 -08:00
webUrl ,
webReachable ,
webOpened : opened ,
diagnostics ,
} ;
2026-03-04 17:33:27 -08:00
track ( "cli_bootstrap_completed" , {
duration_ms : Date.now ( ) - bootstrapStartTime ,
workspace_created : Boolean ( workspaceSeed ) ,
gateway_reachable : gatewayProbe.ok ,
web_reachable : webReachable ,
version : VERSION ,
} ) ;
2026-03-01 16:11:40 -08:00
if ( opts . json ) {
runtime . log ( JSON . stringify ( summary , null , 2 ) ) ;
}
return summary ;
}