// Commander registration for onboard setup flags and lazy onboard runtime execution. import type { Command } from "commander"; import { formatDocsLink } from "../../../packages/terminal-core/src/links.js"; import { theme } from "../../../packages/terminal-core/src/theme.js"; import { formatAuthChoiceChoicesForCli } from "../../commands/auth-choice-options.js"; import type { GatewayDaemonRuntime } from "../../commands/daemon-runtime.js"; import { CORE_ONBOARD_AUTH_FLAGS } from "../../commands/onboard-core-auth-flags.js"; import type { AuthChoice, GatewayAuthChoice, GatewayBind, NodeManagerChoice, OnboardOptions, ResetScope, SecretInputMode, TailscaleMode, } from "../../commands/onboard-types.js"; import { resolveProviderOnboardAuthFlags } from "../../plugins/provider-auth-choices.js"; import { runCommandWithRuntime } from "../cli-utils.js"; import { formatCliCommand } from "../command-format.js"; import { parsePort } from "../shared/parse-port.js"; export function resolveInstallDaemonFlag(command: Command): boolean | undefined { // Commander doesn't support option conflicts natively; keep original behavior. // If --skip-daemon is explicitly passed, it wins. if (command.getOptionValueSource("skipDaemon") === "cli") { return false; } if (command.getOptionValueSource("installDaemon") === "cli") { return Boolean(command.getOptionValue("installDaemon")); } return undefined; } const MODERN_ONBOARD_OPTION_KEYS = new Set([ "modern", "workspace", "acceptRisk", "nonInteractive", "json", ]); function listUnsupportedModernOptions(command: Command): string[] { const optionsByKey = new Map(); for (const option of command.options) { const key = option.attributeName(); if (MODERN_ONBOARD_OPTION_KEYS.has(key) || command.getOptionValueSource(key) !== "cli") { continue; } const existing = optionsByKey.get(key); const valueIsNegated = command.getOptionValue(key) === false; if (!existing || option.negate === valueIsNegated) { // Positive and --no-* forms can share one Commander attribute. Report // only the spelling whose parsed value actually won. optionsByKey.set(key, option); } } return [...optionsByKey.values()] .map((option) => option.long ?? option.short ?? option.flags) .toSorted(); } const AUTH_CHOICE_HELP = formatAuthChoiceChoicesForCli({ includeLegacyAliases: true, includeSkip: true, }); type OnboardAuthFlag = { readonly cliOption: string; readonly description: string; readonly optionKey: string; }; function extractCliFlags(cliOption: string): string[] { return cliOption .split(/[ ,|]+/) .filter((part) => part.startsWith("-")) .map((part) => { const equalsIndex = part.indexOf("="); return equalsIndex === -1 ? part : part.slice(0, equalsIndex); }); } function resolveOnboardAuthFlags(): OnboardAuthFlag[] { // Provider manifests can add auth flags; keep duplicate CLI aliases out of Commander. const seenCliFlags = new Set(); const flags: OnboardAuthFlag[] = []; for (const flag of [...CORE_ONBOARD_AUTH_FLAGS, ...resolveProviderOnboardAuthFlags()]) { const cliFlags = extractCliFlags(flag.cliOption); if (cliFlags.some((cliFlag) => seenCliFlags.has(cliFlag))) { continue; } for (const cliFlag of cliFlags) { seenCliFlags.add(cliFlag); } flags.push(flag); } return flags; } const ONBOARD_AUTH_FLAGS = resolveOnboardAuthFlags(); function pickOnboardProviderAuthOptionValues( opts: Record, ): Partial> { return Object.fromEntries( ONBOARD_AUTH_FLAGS.map((flag) => [flag.optionKey, opts[flag.optionKey] as string | undefined]), ); } export function registerOnboardAuthOptions(command: Command): Command { command .option("--auth-choice ", `Auth: ${AUTH_CHOICE_HELP}`) .option( "--token-provider ", "Token provider id (non-interactive; used with --auth-choice token)", ) .option("--token ", "Token value (non-interactive; used with --auth-choice token)") .option( "--token-profile-id ", "Auth profile id (non-interactive; default: :manual)", ) .option("--token-expires-in ", "Optional token expiry duration (e.g. 365d, 12h)") .option( "--secret-input-mode ", "API key persistence mode: plaintext|ref (default: plaintext)", ) .option("--cloudflare-ai-gateway-account-id ", "Cloudflare Account ID") .option("--cloudflare-ai-gateway-gateway-id ", "Cloudflare AI Gateway ID"); for (const providerFlag of ONBOARD_AUTH_FLAGS) { command.option(providerFlag.cliOption, providerFlag.description); } return command .option("--custom-base-url ", "Custom provider base URL") .option("--custom-api-key ", "Custom provider API key (optional)") .option("--custom-model-id ", "Custom provider model ID") .option("--custom-provider-id ", "Custom provider ID (optional; auto-derived by default)") .option( "--custom-compatibility ", "Custom provider API compatibility: openai|openai-responses|anthropic (default: openai)", ) .option("--custom-image-input", "Mark the custom provider model as image-capable") .option("--custom-text-input", "Mark the custom provider model as text-only"); } export function pickOnboardAuthOptionValues( opts: Record, ): Partial { const customTextInput = opts.customTextInput === true; return { authChoice: opts.authChoice as AuthChoice | undefined, tokenProvider: opts.tokenProvider as string | undefined, token: opts.token as string | undefined, tokenProfileId: opts.tokenProfileId as string | undefined, tokenExpiresIn: opts.tokenExpiresIn as string | undefined, secretInputMode: opts.secretInputMode as SecretInputMode | undefined, ...pickOnboardProviderAuthOptionValues(opts), cloudflareAiGatewayAccountId: opts.cloudflareAiGatewayAccountId as string | undefined, cloudflareAiGatewayGatewayId: opts.cloudflareAiGatewayGatewayId as string | undefined, customBaseUrl: opts.customBaseUrl as string | undefined, customApiKey: opts.customApiKey as string | undefined, customModelId: opts.customModelId as string | undefined, customProviderId: opts.customProviderId as string | undefined, customCompatibility: opts.customCompatibility as | "openai" | "openai-responses" | "anthropic" | undefined, customImageInput: customTextInput ? false : opts.customImageInput === true ? true : undefined, }; } export function registerOnboardCommand(program: Command): void { const command = program .command("onboard") .description("Guided setup for auth, models, Gateway, workspace, channels, and skills") .addHelpText( "after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/onboard", "docs.openclaw.ai/cli/onboard")}\n`, ) .option( "--workspace ", "Workspace proposal for guided setup; persisted by classic/non-interactive setup", ) .option( "--reset", "Reset config + credentials + sessions before running onboard (workspace only with --reset-scope full)", ) .option("--reset-scope ", "Reset scope: config|config+creds+sessions|full") .option("--non-interactive", "Run without prompts", false) .option("--modern", "Open inference-gated OpenClaw (kept for compatibility)", false) .option("--classic", "Use the classic multi-step setup wizard", false) .option("--tui", "Use the terminal hatch instead of the browser handoff", false) .option( "--accept-risk", "Acknowledge that agents are powerful and full system access is risky (required for --non-interactive)", false, ) .option("--flow ", "Onboard flow: quickstart|advanced|manual|import") .option("--mode ", "Onboard mode: local|remote"); registerOnboardAuthOptions(command); command .option("--gateway-port ", "Gateway port") .option("--gateway-bind ", "Gateway bind: loopback|tailnet|lan|auto|custom") .option("--gateway-auth ", "Gateway auth: token|password") .option("--gateway-token ", "Gateway token (token auth)") .option( "--gateway-token-ref-env ", "Gateway token SecretRef env var name (token auth; e.g. OPENCLAW_GATEWAY_TOKEN)", ) .option("--gateway-password ", "Gateway password (password auth)") .option("--remote-url ", "Remote Gateway WebSocket URL") .option("--remote-token ", "Remote Gateway token (optional)") .option("--tailscale ", "Tailscale: off|serve|funnel") .option("--tailscale-reset-on-exit", "Reset tailscale serve/funnel on exit") .option("--install-daemon", "Install gateway service") .option("--no-install-daemon", "Skip gateway service install") .option("--skip-daemon", "Skip gateway service install") .option("--daemon-runtime ", "Daemon runtime: node") .option("--skip-channels", "Skip channel setup") .option("--skip-skills", "Skip skills setup") .option("--skip-bootstrap", "Skip creating default agent workspace files") .option("--skip-search", "Skip search provider setup") .option("--skip-health", "Skip health check") .option("--skip-ui", "Skip Control UI/TUI prompts") .option("--suppress-gateway-token-output", "Suppress token-bearing Gateway/UI output") .option("--skip-hooks", "Skip hook setup") .option("--node-manager ", "Node manager for skills: npm|pnpm|bun") .option("--import-from ", "Migration provider to run during onboarding") .option("--import-source ", "Source agent home for --import-from") .option("--import-secrets", "Import supported secrets during onboarding migration", false) .option("--json", "Output JSON summary", false); const recommendations = command .command("recommendations") .description("Read the app recommendations stored during onboarding") .option("--json", "Output stored recommendation matches as JSON", false) .action(async (opts, recommendationsCommand: Command) => { const { defaultRuntime } = await import("../../runtime.js"); await runCommandWithRuntime(defaultRuntime, async () => { const { onboardRecommendationsCommand } = await import("../../commands/onboard-recommendations.js"); onboardRecommendationsCommand( { json: Boolean(opts.json || recommendationsCommand.parent?.opts().json), }, defaultRuntime, ); }); }); recommendations .command("acknowledge") .description("Mark the stored onboarding recommendation offer as answered") .action(async () => { const { defaultRuntime } = await import("../../runtime.js"); await runCommandWithRuntime(defaultRuntime, async () => { const { acknowledgeOnboardRecommendationsCommand } = await import("../../commands/onboard-recommendations.js"); acknowledgeOnboardRecommendationsCommand(defaultRuntime); }); }); command.action(async (opts, commandRuntime: Command) => { const { defaultRuntime } = await import("../../runtime.js"); await runCommandWithRuntime(defaultRuntime, async () => { if (opts.modern) { const unsupportedOptions = listUnsupportedModernOptions(commandRuntime); if (unsupportedOptions.length > 0) { defaultRuntime.error( [ `--modern cannot be combined with: ${unsupportedOptions.join(", ")}.`, "Run those setup options without --modern, or remove them to open OpenClaw.", ].join("\n"), ); defaultRuntime.exit(1); return; } if (opts.nonInteractive && opts.acceptRisk !== true) { defaultRuntime.error( [ "Non-interactive setup requires explicit risk acknowledgement.", "Read: https://docs.openclaw.ai/security", `Re-run with: ${formatCliCommand("openclaw onboard --modern --non-interactive --accept-risk ...")}`, ].join("\n"), ); defaultRuntime.exit(1); return; } const { runSystemAgentWithInference } = await import("../../commands/system-agent-with-inference.js"); await runSystemAgentWithInference( { yes: false, json: Boolean(opts.json), interactive: !opts.nonInteractive, welcomeVariant: "onboarding", ...(opts.workspace ? { setupWorkspace: opts.workspace as string } : {}), }, defaultRuntime, { ...(opts.workspace ? { workspace: opts.workspace as string } : {}), ...(opts.acceptRisk ? { acceptRisk: true } : {}), }, ); return; } const installDaemon = resolveInstallDaemonFlag(commandRuntime); const gatewayPort = parsePort(opts.gatewayPort); const { setupWizardCommand } = await import("../../commands/onboard.js"); await setupWizardCommand( { workspace: opts.workspace as string | undefined, nonInteractive: Boolean(opts.nonInteractive), acceptRisk: Boolean(opts.acceptRisk), classic: Boolean(opts.classic), tui: Boolean(opts.tui), flow: opts.flow as "quickstart" | "advanced" | "manual" | "import" | undefined, mode: opts.mode as "local" | "remote" | undefined, ...pickOnboardAuthOptionValues(opts as Record), gatewayPort: gatewayPort ?? undefined, gatewayBind: opts.gatewayBind as GatewayBind | undefined, gatewayAuth: opts.gatewayAuth as GatewayAuthChoice | undefined, gatewayToken: opts.gatewayToken as string | undefined, gatewayTokenRefEnv: opts.gatewayTokenRefEnv as string | undefined, gatewayPassword: opts.gatewayPassword as string | undefined, remoteUrl: opts.remoteUrl as string | undefined, remoteToken: opts.remoteToken as string | undefined, tailscale: opts.tailscale as TailscaleMode | undefined, tailscaleResetOnExit: Boolean(opts.tailscaleResetOnExit), reset: Boolean(opts.reset), resetScope: opts.resetScope as ResetScope | undefined, installDaemon, daemonRuntime: opts.daemonRuntime as GatewayDaemonRuntime | undefined, skipChannels: Boolean(opts.skipChannels), skipSkills: Boolean(opts.skipSkills), skipBootstrap: Boolean(opts.skipBootstrap), skipSearch: Boolean(opts.skipSearch), skipHealth: Boolean(opts.skipHealth), skipUi: Boolean(opts.skipUi), suppressGatewayTokenOutput: Boolean(opts.suppressGatewayTokenOutput), skipHooks: Boolean(opts.skipHooks), nodeManager: opts.nodeManager as NodeManagerChoice | undefined, importFrom: opts.importFrom as string | undefined, importSource: opts.importSource as string | undefined, importSecrets: Boolean(opts.importSecrets), json: Boolean(opts.json), }, defaultRuntime, ); }); }); }