import { formatCliCommand } from "../cli/command-format.js"; import { commitConfigWriteWithPendingPluginInstalls } from "../cli/plugins-install-record-commit.js"; import type { AuthChoice, GatewayAuthChoice, OnboardMode, OnboardOptions, ResetScope, } from "../commands/onboard-types.js"; import { createConfigIO, resolveGatewayPort, writeConfigFile } from "../config/config.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { normalizeSecretInputString } from "../config/types.secrets.js"; import { formatErrorMessage } from "../infra/errors.js"; import { buildPluginCompatibilitySnapshotNotices, formatPluginCompatibilityNotice, } from "../plugins/status.js"; import type { RuntimeEnv } from "../runtime.js"; import { defaultRuntime } from "../runtime.js"; import { resolveUserPath } from "../utils.js"; import { WizardCancelledError, type WizardPrompter } from "./prompts.js"; import { resolveSetupSecretInputString } from "./setup.secret-input.js"; import { SECURITY_CONFIRM_MESSAGE, SECURITY_NOTE_MESSAGE, SECURITY_NOTE_TITLE, } from "./setup.security-note.js"; import type { QuickstartGatewayDefaults, WizardFlow } from "./setup.types.js"; type AuthChoiceModule = typeof import("../commands/auth-choice.js"); type ConfigLoggingModule = typeof import("../config/logging.js"); type ModelPickerModule = typeof import("../commands/model-picker.js"); let authChoiceModulePromise: Promise | undefined; let configLoggingModulePromise: Promise | undefined; let modelPickerModulePromise: Promise | undefined; function loadAuthChoiceModule(): Promise { authChoiceModulePromise ??= import("../commands/auth-choice.js"); return authChoiceModulePromise; } function loadConfigLoggingModule(): Promise { configLoggingModulePromise ??= import("../config/logging.js"); return configLoggingModulePromise; } function loadModelPickerModule(): Promise { modelPickerModulePromise ??= import("../commands/model-picker.js"); return modelPickerModulePromise; } async function writeWizardConfigFile(config: OpenClawConfig): Promise { const committed = await commitConfigWriteWithPendingPluginInstalls({ nextConfig: config, commit: async (nextConfig, writeOptions) => writeOptions ? await writeConfigFile(nextConfig, writeOptions) : await writeConfigFile(nextConfig), }); return committed.config; } async function readSetupConfigFileSnapshot() { return await createConfigIO({ pluginValidation: "skip" }).readConfigFileSnapshot(); } async function resolveAuthChoiceModelSelectionPolicy(params: { authChoice: string; config: OpenClawConfig; workspaceDir?: string; env?: NodeJS.ProcessEnv; resolvePreferredProviderForAuthChoice: (params: { choice: string; config?: OpenClawConfig; workspaceDir?: string; env?: NodeJS.ProcessEnv; }) => Promise; }): Promise<{ preferredProvider?: string; promptWhenAuthChoiceProvided: boolean; allowKeepCurrent: boolean; }> { const preferredProvider = await params.resolvePreferredProviderForAuthChoice({ choice: params.authChoice, config: params.config, workspaceDir: params.workspaceDir, env: params.env, }); const { resolvePluginProviders, resolveProviderPluginChoice } = await import("../plugins/provider-auth-choice.runtime.js"); const providers = resolvePluginProviders({ config: params.config, workspaceDir: params.workspaceDir, env: params.env, mode: "setup", }); const resolvedChoice = resolveProviderPluginChoice({ providers, choice: params.authChoice, }); const matchedProvider = resolvedChoice?.provider ?? (() => { const preferredId = preferredProvider?.trim(); if (!preferredId) { return undefined; } return providers.find( (provider) => typeof provider.id === "string" && provider.id.trim() === preferredId, ); })(); const setupPolicy = resolvedChoice?.wizard?.modelSelection ?? matchedProvider?.wizard?.setup?.modelSelection; return { preferredProvider, promptWhenAuthChoiceProvided: setupPolicy?.promptWhenAuthChoiceProvided === true, allowKeepCurrent: setupPolicy?.allowKeepCurrent ?? true, }; } async function requireRiskAcknowledgement(params: { opts: OnboardOptions; prompter: WizardPrompter; }) { if (params.opts.acceptRisk === true) { return; } await params.prompter.note(SECURITY_NOTE_MESSAGE, SECURITY_NOTE_TITLE); const ok = await params.prompter.confirm({ message: SECURITY_CONFIRM_MESSAGE, initialValue: false, }); if (!ok) { throw new WizardCancelledError("risk not accepted"); } } export async function runSetupWizard( opts: OnboardOptions, runtime: RuntimeEnv = defaultRuntime, prompter: WizardPrompter, ) { const onboardHelpers = await import("../commands/onboard-helpers.js"); onboardHelpers.printWizardHeader(runtime); await prompter.intro("OpenClaw setup"); await requireRiskAcknowledgement({ opts, prompter }); const snapshot = await readSetupConfigFileSnapshot(); let baseConfig: OpenClawConfig = snapshot.valid ? snapshot.exists ? (snapshot.sourceConfig ?? snapshot.config) : {} : {}; if (snapshot.exists && !snapshot.valid) { await prompter.note(onboardHelpers.summarizeExistingConfig(baseConfig), "Invalid config"); if (snapshot.issues.length > 0) { await prompter.note( [ ...snapshot.issues.map((iss) => `- ${iss.path}: ${iss.message}`), "", "Docs: https://docs.openclaw.ai/gateway/configuration", ].join("\n"), "Config issues", ); } await prompter.outro( `Config invalid. Run \`${formatCliCommand("openclaw doctor")}\` to repair it, then re-run setup.`, ); runtime.exit(1); return; } const compatibilityNotices = snapshot.valid ? buildPluginCompatibilitySnapshotNotices({ config: baseConfig }) : []; if (compatibilityNotices.length > 0) { await prompter.note( [ `Detected ${compatibilityNotices.length} plugin compatibility notice${compatibilityNotices.length === 1 ? "" : "s"} in the current config.`, ...compatibilityNotices .slice(0, 4) .map((notice) => `- ${formatPluginCompatibilityNotice(notice)}`), ...(compatibilityNotices.length > 4 ? [`- ... +${compatibilityNotices.length - 4} more`] : []), "", `Review: ${formatCliCommand("openclaw doctor")}`, `Inspect: ${formatCliCommand("openclaw plugins inspect --all")}`, ].join("\n"), "Plugin compatibility", ); } const quickstartHint = `Configure details later via ${formatCliCommand("openclaw configure")}.`; const manualHint = "Configure port, network, Tailscale, and auth options."; const explicitFlowRaw = opts.flow?.trim(); const normalizedExplicitFlow = explicitFlowRaw === "manual" ? "advanced" : explicitFlowRaw; if ( normalizedExplicitFlow && normalizedExplicitFlow !== "quickstart" && normalizedExplicitFlow !== "advanced" ) { runtime.error("Invalid --flow (use quickstart, manual, or advanced)."); runtime.exit(1); return; } const explicitFlow: WizardFlow | undefined = normalizedExplicitFlow === "quickstart" || normalizedExplicitFlow === "advanced" ? normalizedExplicitFlow : undefined; let flow: WizardFlow = explicitFlow ?? (await prompter.select({ message: "Setup mode", options: [ { value: "quickstart", label: "QuickStart", hint: quickstartHint }, { value: "advanced", label: "Manual", hint: manualHint }, ], initialValue: "quickstart", })); if (opts.mode === "remote" && flow === "quickstart") { await prompter.note( "QuickStart only supports local gateways. Switching to Manual mode.", "QuickStart", ); flow = "advanced"; } if (snapshot.exists) { await prompter.note( onboardHelpers.summarizeExistingConfig(baseConfig), "Existing config detected", ); const action = await prompter.select({ message: "Config handling", options: [ { value: "keep", label: "Use existing values" }, { value: "modify", label: "Update values" }, { value: "reset", label: "Reset" }, ], }); if (action === "reset") { const workspaceDefault = baseConfig.agents?.defaults?.workspace ?? onboardHelpers.DEFAULT_WORKSPACE; const resetScope = (await prompter.select({ message: "Reset scope", options: [ { value: "config", label: "Config only" }, { value: "config+creds+sessions", label: "Config + creds + sessions", }, { value: "full", label: "Full reset (config + creds + sessions + workspace)", }, ], })) as ResetScope; await onboardHelpers.handleReset(resetScope, resolveUserPath(workspaceDefault), runtime); baseConfig = {}; } } const quickstartGateway: QuickstartGatewayDefaults = (() => { const hasExisting = typeof baseConfig.gateway?.port === "number" || baseConfig.gateway?.bind !== undefined || baseConfig.gateway?.auth?.mode !== undefined || baseConfig.gateway?.auth?.token !== undefined || baseConfig.gateway?.auth?.password !== undefined || baseConfig.gateway?.customBindHost !== undefined || baseConfig.gateway?.tailscale?.mode !== undefined; const bindRaw = baseConfig.gateway?.bind; const bind = bindRaw === "loopback" || bindRaw === "lan" || bindRaw === "auto" || bindRaw === "custom" || bindRaw === "tailnet" ? bindRaw : "loopback"; let authMode: GatewayAuthChoice = "token"; if ( baseConfig.gateway?.auth?.mode === "token" || baseConfig.gateway?.auth?.mode === "password" ) { authMode = baseConfig.gateway.auth.mode; } else if (baseConfig.gateway?.auth?.token) { authMode = "token"; } else if (baseConfig.gateway?.auth?.password) { authMode = "password"; } const tailscaleRaw = baseConfig.gateway?.tailscale?.mode; const tailscaleMode = tailscaleRaw === "off" || tailscaleRaw === "serve" || tailscaleRaw === "funnel" ? tailscaleRaw : "off"; return { hasExisting, port: resolveGatewayPort(baseConfig), bind, authMode, tailscaleMode, token: baseConfig.gateway?.auth?.token, password: baseConfig.gateway?.auth?.password, customBindHost: baseConfig.gateway?.customBindHost, tailscaleResetOnExit: baseConfig.gateway?.tailscale?.resetOnExit ?? false, }; })(); if (flow === "quickstart") { const formatBind = (value: "loopback" | "lan" | "auto" | "custom" | "tailnet") => { if (value === "loopback") { return "Loopback (127.0.0.1)"; } if (value === "lan") { return "LAN"; } if (value === "custom") { return "Custom IP"; } if (value === "tailnet") { return "Tailnet (Tailscale IP)"; } return "Auto"; }; const formatAuth = (value: GatewayAuthChoice) => { if (value === "token") { return "Token (default)"; } return "Password"; }; const formatTailscale = (value: "off" | "serve" | "funnel") => { if (value === "off") { return "Off"; } if (value === "serve") { return "Serve"; } return "Funnel"; }; const quickstartLines = quickstartGateway.hasExisting ? [ "Keeping your current gateway settings:", `Gateway port: ${quickstartGateway.port}`, `Gateway bind: ${formatBind(quickstartGateway.bind)}`, ...(quickstartGateway.bind === "custom" && quickstartGateway.customBindHost ? [`Gateway custom IP: ${quickstartGateway.customBindHost}`] : []), `Gateway auth: ${formatAuth(quickstartGateway.authMode)}`, `Tailscale exposure: ${formatTailscale(quickstartGateway.tailscaleMode)}`, "Direct to chat channels.", ] : [ `Gateway port: ${quickstartGateway.port}`, "Gateway bind: Loopback (127.0.0.1)", "Gateway auth: Token (default)", "Tailscale exposure: Off", "Direct to chat channels.", ]; await prompter.note(quickstartLines.join("\n"), "QuickStart"); } const localPort = resolveGatewayPort(baseConfig); const localUrl = `ws://127.0.0.1:${localPort}`; let localGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN; try { const resolvedGatewayToken = await resolveSetupSecretInputString({ config: baseConfig, value: baseConfig.gateway?.auth?.token, path: "gateway.auth.token", env: process.env, }); if (resolvedGatewayToken) { localGatewayToken = resolvedGatewayToken; } } catch (error) { await prompter.note( [ "Could not resolve gateway.auth.token SecretRef for setup probe.", formatErrorMessage(error), ].join("\n"), "Gateway auth", ); } let localGatewayPassword = process.env.OPENCLAW_GATEWAY_PASSWORD; try { const resolvedGatewayPassword = await resolveSetupSecretInputString({ config: baseConfig, value: baseConfig.gateway?.auth?.password, path: "gateway.auth.password", env: process.env, }); if (resolvedGatewayPassword) { localGatewayPassword = resolvedGatewayPassword; } } catch (error) { await prompter.note( [ "Could not resolve gateway.auth.password SecretRef for setup probe.", formatErrorMessage(error), ].join("\n"), "Gateway auth", ); } const localProbe = await onboardHelpers.probeGatewayReachable({ url: localUrl, token: localGatewayToken, password: localGatewayPassword, }); const remoteUrl = baseConfig.gateway?.remote?.url?.trim() ?? ""; let remoteGatewayToken = normalizeSecretInputString(baseConfig.gateway?.remote?.token); try { const resolvedRemoteGatewayToken = await resolveSetupSecretInputString({ config: baseConfig, value: baseConfig.gateway?.remote?.token, path: "gateway.remote.token", env: process.env, }); if (resolvedRemoteGatewayToken) { remoteGatewayToken = resolvedRemoteGatewayToken; } } catch (error) { await prompter.note( [ "Could not resolve gateway.remote.token SecretRef for setup probe.", formatErrorMessage(error), ].join("\n"), "Gateway auth", ); } const remoteProbe = remoteUrl ? await onboardHelpers.probeGatewayReachable({ url: remoteUrl, token: remoteGatewayToken, }) : null; const mode = opts.mode ?? (flow === "quickstart" ? "local" : ((await prompter.select({ message: "What do you want to set up?", options: [ { value: "local", label: "Local gateway (this machine)", hint: localProbe.ok ? `Gateway reachable (${localUrl})` : `No gateway detected (${localUrl})`, }, { value: "remote", label: "Remote gateway (info-only)", hint: !remoteUrl ? "No remote URL configured yet" : remoteProbe?.ok ? `Gateway reachable (${remoteUrl})` : `Configured but unreachable (${remoteUrl})`, }, ], })) as OnboardMode)); if (mode === "remote") { const { promptRemoteGatewayConfig } = await import("../commands/onboard-remote.js"); const { applySkipBootstrapConfig } = await import("../commands/onboard-config.js"); const { logConfigUpdated } = await loadConfigLoggingModule(); let nextConfig = await promptRemoteGatewayConfig(baseConfig, prompter, { secretInputMode: opts.secretInputMode, }); if (opts.skipBootstrap) { nextConfig = applySkipBootstrapConfig(nextConfig); } nextConfig = onboardHelpers.applyWizardMetadata(nextConfig, { command: "onboard", mode }); nextConfig = await writeWizardConfigFile(nextConfig); logConfigUpdated(runtime); await prompter.outro("Remote gateway configured."); return; } const workspaceInput = opts.workspace ?? (flow === "quickstart" ? (baseConfig.agents?.defaults?.workspace ?? onboardHelpers.DEFAULT_WORKSPACE) : await prompter.text({ message: "Workspace directory", initialValue: baseConfig.agents?.defaults?.workspace ?? onboardHelpers.DEFAULT_WORKSPACE, })); const workspaceDir = resolveUserPath(workspaceInput.trim() || onboardHelpers.DEFAULT_WORKSPACE); const { applyLocalSetupWorkspaceConfig, applySkipBootstrapConfig } = await import("../commands/onboard-config.js"); let nextConfig: OpenClawConfig = applyLocalSetupWorkspaceConfig(baseConfig, workspaceDir); if (opts.skipBootstrap) { nextConfig = applySkipBootstrapConfig(nextConfig); } const authChoiceFromPrompt = opts.authChoice === undefined; let authChoice: AuthChoice | undefined = opts.authChoice; let authStore: | ReturnType<(typeof import("../agents/auth-profiles.runtime.js"))["ensureAuthProfileStore"]> | undefined; let promptAuthChoiceGrouped: | (typeof import("../commands/auth-choice-prompt.js"))["promptAuthChoiceGrouped"] | undefined; if (authChoiceFromPrompt) { const { ensureAuthProfileStore } = await import("../agents/auth-profiles.runtime.js"); ({ promptAuthChoiceGrouped } = await import("../commands/auth-choice-prompt.js")); authStore = ensureAuthProfileStore(undefined, { allowKeychainPrompt: false, }); } while (true) { if (authChoiceFromPrompt) { authChoice = await promptAuthChoiceGrouped!({ prompter, store: authStore!, includeSkip: true, config: nextConfig, workspaceDir, }); } if (authChoice === undefined) { throw new WizardCancelledError("auth choice is required"); } if (authChoice === "custom-api-key") { const { promptCustomApiConfig } = await import("../commands/onboard-custom.js"); const customResult = await promptCustomApiConfig({ prompter, runtime, config: nextConfig, secretInputMode: opts.secretInputMode, }); nextConfig = customResult.config; break; } if (authChoice === "skip") { // Explicit skip should stay cold: do not bootstrap auth/profile machinery // or run model/auth checks when the caller already chose to skip setup. if (authChoiceFromPrompt) { const { applyPrimaryModel, promptDefaultModel } = await loadModelPickerModule(); const modelSelection = await promptDefaultModel({ config: nextConfig, prompter, allowKeep: true, ignoreAllowlist: true, includeProviderPluginSetups: false, loadCatalog: false, workspaceDir, runtime, }); if (modelSelection.config) { nextConfig = modelSelection.config; } if (modelSelection.model) { nextConfig = applyPrimaryModel(nextConfig, modelSelection.model); } const { warnIfModelConfigLooksOff } = await loadAuthChoiceModule(); await warnIfModelConfigLooksOff(nextConfig, prompter, { validateCatalog: false }); } break; } const [ { applyAuthChoice, resolvePreferredProviderForAuthChoice, warnIfModelConfigLooksOff }, { applyPrimaryModel, promptDefaultModel }, ] = await Promise.all([loadAuthChoiceModule(), loadModelPickerModule()]); const authResult = await applyAuthChoice({ authChoice, config: nextConfig, prompter, runtime, setDefaultModel: true, opts: { tokenProvider: opts.tokenProvider, token: opts.authChoice === "apiKey" && opts.token ? opts.token : undefined, }, }); nextConfig = authResult.config; if (authResult.retrySelection) { if (authChoiceFromPrompt) { continue; } break; } if (authResult.agentModelOverride) { nextConfig = applyPrimaryModel(nextConfig, authResult.agentModelOverride); } const authChoiceModelSelectionPolicy = await resolveAuthChoiceModelSelectionPolicy({ authChoice, config: nextConfig, workspaceDir, resolvePreferredProviderForAuthChoice, }); const shouldPromptModelSelection = authChoiceFromPrompt || authChoiceModelSelectionPolicy?.promptWhenAuthChoiceProvided; if (shouldPromptModelSelection) { const modelSelection = await promptDefaultModel({ config: nextConfig, prompter, allowKeep: authChoiceModelSelectionPolicy?.allowKeepCurrent ?? true, ignoreAllowlist: true, includeProviderPluginSetups: true, preferredProvider: authChoiceModelSelectionPolicy?.preferredProvider, browseCatalogOnDemand: true, workspaceDir, runtime, }); if (modelSelection.config) { nextConfig = modelSelection.config; } if (modelSelection.model) { nextConfig = applyPrimaryModel(nextConfig, modelSelection.model); } } await warnIfModelConfigLooksOff(nextConfig, prompter, { validateCatalog: false }); break; } const { configureGatewayForSetup } = await import("./setup.gateway-config.js"); const gateway = await configureGatewayForSetup({ flow, baseConfig, nextConfig, localPort, quickstartGateway, secretInputMode: opts.secretInputMode, prompter, runtime, }); nextConfig = gateway.nextConfig; const settings = gateway.settings; if (opts.skipChannels ?? opts.skipProviders) { await prompter.note("Skipping channel setup.", "Channels"); } else { const { listChannelPlugins } = await import("../channels/plugins/index.js"); const { setupChannels } = await import("../commands/onboard-channels.js"); const quickstartAllowFromChannels = flow === "quickstart" ? listChannelPlugins() .filter((plugin) => plugin.meta.quickstartAllowFrom) .map((plugin) => plugin.id) : []; nextConfig = await setupChannels(nextConfig, runtime, prompter, { allowSignalInstall: true, deferStatusUntilSelection: flow === "quickstart", forceAllowFromChannels: quickstartAllowFromChannels, skipDmPolicyPrompt: flow === "quickstart", skipConfirm: flow === "quickstart", quickstartDefaults: flow === "quickstart", secretInputMode: opts.secretInputMode, }); } nextConfig = await writeWizardConfigFile(nextConfig); const { logConfigUpdated } = await loadConfigLoggingModule(); logConfigUpdated(runtime); await onboardHelpers.ensureWorkspaceAndSessions(workspaceDir, runtime, { skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap), }); if (opts.skipSearch) { await prompter.note("Skipping search setup.", "Search"); } else { const { setupSearch } = await import("../commands/onboard-search.js"); nextConfig = await setupSearch(nextConfig, runtime, prompter, { quickstartDefaults: flow === "quickstart", secretInputMode: opts.secretInputMode, }); } if (opts.skipSkills) { await prompter.note("Skipping skills setup.", "Skills"); } else { const { setupSkills } = await import("../commands/onboard-skills.js"); nextConfig = await setupSkills(nextConfig, workspaceDir, runtime, prompter); } // Plugin configuration (sandbox backends, tool plugins, etc.) if (flow !== "quickstart") { const { setupPluginConfig } = await import("./setup.plugin-config.js"); nextConfig = await setupPluginConfig({ config: nextConfig, prompter, workspaceDir, }); } // Setup hooks (session memory on /new) const { setupInternalHooks } = await import("../commands/onboard-hooks.js"); nextConfig = await setupInternalHooks(nextConfig, runtime, prompter); nextConfig = onboardHelpers.applyWizardMetadata(nextConfig, { command: "onboard", mode }); nextConfig = await writeWizardConfigFile(nextConfig); const { finalizeSetupWizard } = await import("./setup.finalize.js"); const { launchedTui } = await finalizeSetupWizard({ flow, opts, baseConfig, nextConfig, workspaceDir, settings, prompter, runtime, }); if (launchedTui) { return; } }