From 1a121da22ca7cd70439751be8302c58342ef39ad Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 22 Jul 2026 06:47:57 -0700 Subject: [PATCH] fix(onboarding): honor classic gateway options (#112396) * fix(onboarding): honor classic gateway options * test(wizard): type gateway setup mock * docs(onboarding): document quickstart gateway flags * chore(release): defer changelog entry --- docs/cli/onboard.md | 8 +- src/commands/onboard.test.ts | 47 +++++++++++ src/commands/onboard.ts | 22 ++++++ src/wizard/setup.gateway-config.test.ts | 75 ++++++++++++++++++ src/wizard/setup.shared.test.ts | 100 +++++++++++++++++++++++- src/wizard/setup.shared.ts | 61 +++++++++++++-- src/wizard/setup.test.ts | 96 ++++++++++++++++++++++- src/wizard/setup.ts | 60 +++++++------- 8 files changed, 426 insertions(+), 43 deletions(-) diff --git a/docs/cli/onboard.md b/docs/cli/onboard.md index 8cc722eba6e4..83d12ee66684 100644 --- a/docs/cli/onboard.md +++ b/docs/cli/onboard.md @@ -77,8 +77,12 @@ not overwrite the existing skill. - `--classic`: opens the full step-by-step wizard. It cannot be combined with `--non-interactive`; omit `--classic` for automated setup. -- `--flow quickstart`: opens the classic wizard with minimal prompts and - auto-generates a gateway token. +- `--flow quickstart`: opens the classic wizard with minimal prompts, uses + token auth by default, and generates a token when no stored or explicit + credential applies. Explicit local Gateway flags such as + `--gateway-port`, `--gateway-bind`, `--gateway-auth`, and `--tailscale` + override the corresponding stored or default quickstart values; omitted + options keep their current values. - `--flow manual` (alias `advanced`): opens the classic wizard with full prompts for port, bind, and auth. - `--flow import`: runs a detected migration provider (for example Hermes via `--import-from hermes`), previews the plan, then applies after confirmation. When an interactive import supplies a default model, onboarding requires that route to pass a live completion before it skips provider setup; a failed imported route returns to provider configuration. Import only runs against a fresh OpenClaw setup - reset config, credentials, sessions, and workspace state first if any exist. Use [`openclaw migrate`](/cli/migrate) for dry-run plans, overwrite mode, reports, and exact mappings. diff --git a/src/commands/onboard.test.ts b/src/commands/onboard.test.ts index 9d8fe9afcb0d..784989d118c3 100644 --- a/src/commands/onboard.test.ts +++ b/src/commands/onboard.test.ts @@ -458,6 +458,53 @@ describe("setupWizardCommand", () => { expect(mocks.runNonInteractiveSetup).not.toHaveBeenCalled(); }); + it("validates gateway token env refs before reset", async () => { + const runtime = makeRuntime(); + + await setupWizardCommand( + { + reset: true, + gatewayTokenRefEnv: "MISSING_GATEWAY_TOKEN_ENV", + }, + runtime, + ); + + expect(runtime.error).toHaveBeenCalledWith( + expect.stringContaining('Environment variable "MISSING_GATEWAY_TOKEN_ENV" is missing'), + ); + expect(mocks.handleReset).not.toHaveBeenCalled(); + expect(mocks.runInteractiveSetup).not.toHaveBeenCalled(); + }); + + it("rejects conflicting gateway token inputs before reset", async () => { + const previous = process.env.OPENCLAW_GATEWAY_TOKEN; + process.env.OPENCLAW_GATEWAY_TOKEN = "env-token"; + const runtime = makeRuntime(); + + try { + await setupWizardCommand( + { + reset: true, + gatewayToken: "plaintext-token", + gatewayTokenRefEnv: "OPENCLAW_GATEWAY_TOKEN", + }, + runtime, + ); + } finally { + if (previous === undefined) { + delete process.env.OPENCLAW_GATEWAY_TOKEN; + } else { + process.env.OPENCLAW_GATEWAY_TOKEN = previous; + } + } + + expect(runtime.error).toHaveBeenCalledWith( + expect.stringContaining("Use either --gateway-token or --gateway-token-ref-env"), + ); + expect(mocks.handleReset).not.toHaveBeenCalled(); + expect(mocks.runInteractiveSetup).not.toHaveBeenCalled(); + }); + it("validates dependent auth-choice options before reset", async () => { const runtime = makeRuntime(); diff --git a/src/commands/onboard.ts b/src/commands/onboard.ts index b2423870931f..ed735a46c4ca 100644 --- a/src/commands/onboard.ts +++ b/src/commands/onboard.ts @@ -8,6 +8,7 @@ import { formatCliCommand } from "../cli/command-format.js"; import { formatInvalidPortOption } from "../cli/error-format.js"; import { readConfigFileSnapshot, resolveGatewayPort } from "../config/config.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { isValidEnvSecretRefId } from "../config/types.secrets.js"; import { formatErrorMessage } from "../infra/errors.js"; import { assertSupportedRuntime } from "../infra/runtime-guard.js"; import { resolveProviderMatch } from "../plugins/provider-auth-choice-helpers.js"; @@ -102,6 +103,27 @@ function validatePreflightOptions(opts: OnboardOptions, runtime: RuntimeEnv): bo ) { return rejectOption(runtime, formatInvalidPortOption("--gateway-port")); } + if (opts.gatewayTokenRefEnv !== undefined) { + const gatewayTokenRefEnv = opts.gatewayTokenRefEnv.trim(); + if (!isValidEnvSecretRefId(gatewayTokenRefEnv)) { + return rejectOption( + runtime, + "Invalid --gateway-token-ref-env. Use an environment variable name like OPENCLAW_GATEWAY_TOKEN.", + ); + } + if (opts.gatewayToken !== undefined) { + return rejectOption( + runtime, + "Use either --gateway-token or --gateway-token-ref-env, not both. Prefer --gateway-token-ref-env to avoid writing plaintext tokens.", + ); + } + if (!process.env[gatewayTokenRefEnv]?.trim()) { + return rejectOption( + runtime, + `Environment variable "${gatewayTokenRefEnv}" is missing or empty. Export it first, then rerun ${formatCliCommand("openclaw onboard")}.`, + ); + } + } if (opts.nonInteractive && opts.mode === "remote" && !opts.remoteUrl?.trim()) { return rejectOption( runtime, diff --git a/src/wizard/setup.gateway-config.test.ts b/src/wizard/setup.gateway-config.test.ts index a8451f6a3032..4f92dc970dfc 100644 --- a/src/wizard/setup.gateway-config.test.ts +++ b/src/wizard/setup.gateway-config.test.ts @@ -25,6 +25,7 @@ vi.mock("../infra/tailscale.js", () => ({ })); import { configureGatewayForSetup } from "./setup.gateway-config.js"; +import { resolveQuickstartGatewayDefaults } from "./setup.shared.js"; describe("configureGatewayForSetup", () => { function createPrompter(params: { selectQueue: string[]; textQueue: Array }) { @@ -341,4 +342,78 @@ describe("configureGatewayForSetup", () => { expect(result.nextConfig.gateway?.auth?.token).toEqual(quickstartGateway.token); expect(result.settings.gatewayToken).toBe("token-from-exec"); }); + + it("persists an explicit classic quickstart env token ref", async () => { + const previous = process.env.OPENCLAW_GATEWAY_TOKEN; + process.env.OPENCLAW_GATEWAY_TOKEN = "token-from-env-ref"; + try { + const quickstartGateway = resolveQuickstartGatewayDefaults( + {}, + { gatewayTokenRefEnv: "OPENCLAW_GATEWAY_TOKEN" }, + ); + const result = await configureGatewayForSetup({ + flow: "quickstart", + baseConfig: {}, + nextConfig: {}, + localPort: 18789, + quickstartGateway, + prompter: createPrompter({ selectQueue: [], textQueue: [] }), + runtime: createRuntime(), + }); + + expect(result.nextConfig.gateway?.auth).toEqual({ + mode: "token", + token: { + source: "env", + provider: "default", + id: "OPENCLAW_GATEWAY_TOKEN", + }, + }); + expect(result.settings.gatewayToken).toBe("token-from-env-ref"); + } finally { + if (previous === undefined) { + delete process.env.OPENCLAW_GATEWAY_TOKEN; + } else { + process.env.OPENCLAW_GATEWAY_TOKEN = previous; + } + } + }); + + it("persists classic quickstart overrides through gateway safety normalization", async () => { + const password = ["classic", "gateway", "placeholder"].join("-"); + mocks.getTailnetHostname.mockResolvedValue("test-tailnet.ts.net"); + const note = vi.fn(async () => {}); + const prompter = buildWizardPrompter({ note }); + const quickstartGateway = resolveQuickstartGatewayDefaults( + {}, + { + gatewayPort: 19001, + gatewayBind: "lan", + gatewayAuth: "token", + gatewayToken: "unused-token", + gatewayPassword: password, + tailscale: "funnel", + tailscaleResetOnExit: true, + }, + ); + + const result = await configureGatewayForSetup({ + flow: "quickstart", + baseConfig: {}, + nextConfig: {}, + localPort: 18789, + quickstartGateway, + prompter, + runtime: createRuntime(), + }); + + expect(result.nextConfig.gateway).toMatchObject({ + port: 19001, + bind: "loopback", + auth: { mode: "password", password }, + tailscale: { mode: "funnel", resetOnExit: true }, + }); + expect(result.nextConfig.gateway?.auth?.token).toBeUndefined(); + expect(JSON.stringify(note.mock.calls)).not.toContain(password); + }); }); diff --git a/src/wizard/setup.shared.test.ts b/src/wizard/setup.shared.test.ts index 6c0f12a43e2a..ae4ac792faaf 100644 --- a/src/wizard/setup.shared.test.ts +++ b/src/wizard/setup.shared.test.ts @@ -17,7 +17,105 @@ vi.mock("../plugins/install-record-commit.js", async (importOriginal) => ({ commitConfigWriteWithPendingPluginInstalls: mocks.commitConfigWriteWithPendingPluginInstalls, })); -import { writeWizardConfigFile } from "./setup.shared.js"; +import { resolveQuickstartGatewayDefaults, writeWizardConfigFile } from "./setup.shared.js"; + +describe("resolveQuickstartGatewayDefaults", () => { + const storedConfig: OpenClawConfig = { + gateway: { + port: 19111, + bind: "custom", + customBindHost: "192.0.2.10", + auth: { + mode: "token", + token: "stored-token", + password: "stored-password", + }, + tailscale: { + mode: "serve", + resetOnExit: true, + }, + }, + }; + + it("overlays every explicitly supplied classic quickstart gateway option", () => { + const result = resolveQuickstartGatewayDefaults(storedConfig, { + gatewayPort: 19001, + gatewayBind: "lan", + gatewayAuth: "password", + gatewayToken: "explicit-token", + gatewayPassword: "explicit-password", + tailscale: "off", + tailscaleResetOnExit: false, + }); + + expect(result).toEqual({ + hasExisting: true, + port: 19001, + bind: "lan", + authMode: "password", + tailscaleMode: "off", + token: "explicit-token", + password: "explicit-password", + customBindHost: "192.0.2.10", + tailscaleResetOnExit: false, + }); + }); + + it("preserves stored quickstart defaults when no override is defined", () => { + expect(resolveQuickstartGatewayDefaults(storedConfig)).toEqual({ + hasExisting: true, + port: 19111, + bind: "custom", + authMode: "token", + tailscaleMode: "serve", + token: "stored-token", + password: "stored-password", + customBindHost: "192.0.2.10", + tailscaleResetOnExit: true, + }); + }); + + it("aligns credential-only overrides while keeping an explicit auth mode authoritative", () => { + expect( + resolveQuickstartGatewayDefaults(storedConfig, { + gatewayPassword: "explicit-password", + }).authMode, + ).toBe("password"); + expect( + resolveQuickstartGatewayDefaults( + { gateway: { auth: { mode: "password", password: "stored-password" } } }, + { gatewayToken: "explicit-token" }, + ).authMode, + ).toBe("token"); + expect( + resolveQuickstartGatewayDefaults(storedConfig, { + gatewayAuth: "password", + gatewayToken: "explicit-token", + }).authMode, + ).toBe("password"); + expect( + resolveQuickstartGatewayDefaults(storedConfig, { + gatewayAuth: "token", + gatewayPassword: "explicit-password", + }).authMode, + ).toBe("token"); + }); + + it("maps an explicit env-backed token to the canonical SecretRef", () => { + expect( + resolveQuickstartGatewayDefaults(storedConfig, { + gatewayTokenRefEnv: " OPENCLAW_GATEWAY_TOKEN ", + }), + ).toMatchObject({ + authMode: "token", + token: { + source: "env", + provider: "default", + id: "OPENCLAW_GATEWAY_TOKEN", + }, + }); + }); +}); describe("writeWizardConfigFile pending install ownership", () => { beforeEach(() => { diff --git a/src/wizard/setup.shared.ts b/src/wizard/setup.shared.ts index 86ebdb83d594..845841135077 100644 --- a/src/wizard/setup.shared.ts +++ b/src/wizard/setup.shared.ts @@ -9,6 +9,7 @@ import { stripPendingPluginInstallRecords, unchangedPendingPluginInstallRecordIds, } from "../plugins/install-record-commit.js"; +import { resolveDefaultSecretProviderAlias } from "../secrets/ref-contract.js"; import { isPlainObject } from "../utils.js"; import { t } from "./i18n/index.js"; import { WizardCancelledError, type WizardPrompter } from "./prompts.js"; @@ -19,6 +20,33 @@ import { } from "./setup.security-note.js"; import type { QuickstartGatewayDefaults } from "./setup.types.js"; +type QuickstartGatewayOptionOverrides = Pick< + OnboardOptions, + | "gatewayPort" + | "gatewayBind" + | "gatewayAuth" + | "gatewayToken" + | "gatewayTokenRefEnv" + | "gatewayPassword" + | "tailscale" + | "tailscaleResetOnExit" +>; + +export function hasQuickstartGatewayOverrides( + overrides: QuickstartGatewayOptionOverrides, +): boolean { + return ( + overrides.gatewayPort !== undefined || + overrides.gatewayBind !== undefined || + overrides.gatewayAuth !== undefined || + overrides.gatewayToken !== undefined || + overrides.gatewayTokenRefEnv !== undefined || + overrides.gatewayPassword !== undefined || + overrides.tailscale !== undefined || + overrides.tailscaleResetOnExit !== undefined + ); +} + function mergeWizardConfigValueOntoLatest(current: unknown, base: unknown, next: unknown): unknown { if (isDeepStrictEqual(next, base)) { return current; @@ -171,6 +199,7 @@ function applySecurityAcknowledgement(config: OpenClawConfig): OpenClawConfig { /** Derive quickstart gateway defaults, preserving any existing gateway settings. */ export function resolveQuickstartGatewayDefaults( baseConfig: OpenClawConfig, + overrides: QuickstartGatewayOptionOverrides = {}, ): QuickstartGatewayDefaults { const hasExisting = typeof baseConfig.gateway?.port === "number" || @@ -206,15 +235,33 @@ export function resolveQuickstartGatewayDefaults( ? tailscaleRaw : "off"; + const explicitAuthMode = + overrides.gatewayAuth ?? + (overrides.gatewayToken !== undefined || overrides.gatewayTokenRefEnv !== undefined + ? "token" + : overrides.gatewayPassword !== undefined + ? "password" + : undefined); + return { hasExisting, - port: resolveGatewayPort(baseConfig), - bind, - authMode, - tailscaleMode, - token: baseConfig.gateway?.auth?.token, - password: baseConfig.gateway?.auth?.password, + port: overrides.gatewayPort ?? resolveGatewayPort(baseConfig), + bind: overrides.gatewayBind ?? bind, + authMode: explicitAuthMode ?? authMode, + tailscaleMode: overrides.tailscale ?? tailscaleMode, + token: + overrides.gatewayTokenRefEnv !== undefined + ? { + source: "env", + provider: resolveDefaultSecretProviderAlias(baseConfig, "env", { + preferFirstProviderForSource: true, + }), + id: overrides.gatewayTokenRefEnv.trim(), + } + : (overrides.gatewayToken ?? baseConfig.gateway?.auth?.token), + password: overrides.gatewayPassword ?? baseConfig.gateway?.auth?.password, customBindHost: baseConfig.gateway?.customBindHost, - tailscaleResetOnExit: baseConfig.gateway?.tailscale?.resetOnExit ?? false, + tailscaleResetOnExit: + overrides.tailscaleResetOnExit ?? baseConfig.gateway?.tailscale?.resetOnExit ?? false, }; } diff --git a/src/wizard/setup.test.ts b/src/wizard/setup.test.ts index a4c872ccfcc6..d1539b573452 100644 --- a/src/wizard/setup.test.ts +++ b/src/wizard/setup.test.ts @@ -31,6 +31,7 @@ type ApplyAuthChoice = typeof import("../commands/auth-choice.js").applyAuthChoi type PrepareAuthChoice = typeof import("../commands/auth-choice.js").prepareAuthChoice; type VerifySetupInferenceConfig = typeof import("../system-agent/setup-inference.js").verifySetupInferenceConfig; +type ConfigureGatewayForSetup = typeof import("./setup.gateway-config.js").configureGatewayForSetup; const ensureAuthProfileStore = vi.hoisted(() => vi.fn(() => ({ profiles: {} }))); const keepCurrentAuthChoice = vi.hoisted(() => "__keep-current" as const); @@ -63,7 +64,7 @@ const applyPrimaryModel = vi.hoisted(() => vi.fn((cfg) => cfg)); const promptDefaultModel = vi.hoisted(() => vi.fn(async () => ({}))); const promptCustomApiConfig = vi.hoisted(() => vi.fn(async (args) => ({ config: args.config }))); const configureGatewayForSetup = vi.hoisted(() => - vi.fn(async (args) => ({ + vi.fn(async (args) => ({ nextConfig: args.nextConfig, settings: { port: args.localPort ?? 18789, @@ -2104,6 +2105,99 @@ describe("runSetupWizard", () => { ); }); + it("persists explicit classic quickstart gateway options without printing the password", async () => { + const password = ["classic", "password", "placeholder"].join("-"); + const note: WizardPrompter["note"] = vi.fn(async () => {}); + const prompter = buildWizardPrompter({ note }); + const runtime = createRuntime(); + readConfigFileSnapshot.mockResolvedValueOnce( + configSnapshot({ + gateway: { + port: 19111, + bind: "loopback", + auth: { mode: "token", token: "stored-token" }, + tailscale: { mode: "off", resetOnExit: false }, + }, + }), + ); + replaceConfigFile.mockClear(); + configureGatewayForSetup.mockImplementationOnce(async (args) => ({ + nextConfig: { + ...args.nextConfig, + gateway: { + ...args.nextConfig.gateway, + port: args.quickstartGateway.port, + bind: args.quickstartGateway.bind, + auth: { + ...args.nextConfig.gateway?.auth, + mode: args.quickstartGateway.authMode, + password: args.quickstartGateway.password, + }, + tailscale: { + ...args.nextConfig.gateway?.tailscale, + mode: args.quickstartGateway.tailscaleMode, + resetOnExit: args.quickstartGateway.tailscaleResetOnExit, + }, + }, + }, + settings: { + port: args.quickstartGateway.port, + bind: args.quickstartGateway.bind, + authMode: args.quickstartGateway.authMode, + gatewayToken: undefined, + tailscaleMode: args.quickstartGateway.tailscaleMode, + tailscaleResetOnExit: args.quickstartGateway.tailscaleResetOnExit, + }, + })); + + await runSetupWizard( + { + acceptRisk: true, + flow: "quickstart", + mode: "local", + authChoice: "skip", + gatewayPort: 19001, + gatewayBind: "lan", + gatewayAuth: "password", + gatewayPassword: password, + installDaemon: false, + skipChannels: true, + skipSkills: true, + skipSearch: true, + skipHealth: true, + skipUi: true, + }, + runtime, + prompter, + ); + + const gatewaySetup = getMockCallArg(configureGatewayForSetup, 0, 0, "gateway setup"); + expect(requireRecord(gatewaySetup, "gateway setup").quickstartGateway).toMatchObject({ + port: 19001, + bind: "lan", + authMode: "password", + password, + }); + expect( + persistedWizardConfigs().some( + (config) => + config.gateway?.port === 19001 && + config.gateway.bind === "lan" && + config.gateway.auth?.mode === "password" && + config.gateway.auth.password === password, + ), + ).toBe(true); + + const visibleOutput = [ + ...getWizardNoteCalls(note).flat(), + ...((runtime.log as unknown as ReturnType).mock.calls.flat() as unknown[]), + ...((runtime.error as unknown as ReturnType).mock.calls.flat() as unknown[]), + ].join("\n"); + expect(visibleOutput).toContain("19001"); + expect(visibleOutput).not.toContain("Keeping your current gateway settings:"); + expect(visibleOutput).not.toContain(password); + }); + it("shows the resolved gateway port in quickstart for fresh envs", async () => { const previousPort = process.env.OPENCLAW_GATEWAY_PORT; process.env.OPENCLAW_GATEWAY_PORT = "18791"; diff --git a/src/wizard/setup.ts b/src/wizard/setup.ts index 505c1b0c71ad..c8d8c76d08a1 100644 --- a/src/wizard/setup.ts +++ b/src/wizard/setup.ts @@ -26,6 +26,7 @@ import { import { runSetupModelAuthStep, type SetupModelAuthCandidate } from "./setup.model-auth.js"; import { resolveSetupSecretInputString } from "./setup.secret-input.js"; import { + hasQuickstartGatewayOverrides, readSetupConfigFileSnapshot, readValidSetupConfigFile, requireRiskAcknowledgement, @@ -343,8 +344,13 @@ async function runSetupWizardOnce( flow = "quickstart"; } const wizardFlow: WizardFlow = flow === "advanced" ? "advanced" : "quickstart"; + const hasExplicitQuickstartGatewayOverrides = + wizardFlow === "quickstart" && hasQuickstartGatewayOverrides(opts); - const quickstartGateway: QuickstartGatewayDefaults = resolveQuickstartGatewayDefaults(baseConfig); + const quickstartGateway: QuickstartGatewayDefaults = resolveQuickstartGatewayDefaults( + baseConfig, + wizardFlow === "quickstart" ? opts : undefined, + ); if (flow === "quickstart") { const formatBind = (value: "loopback" | "lan" | "auto" | "custom" | "tailnet") => { @@ -371,37 +377,27 @@ async function runSetupWizardOnce( const formatTailscale = (value: "off" | "serve" | "funnel") => { return t(`wizard.gatewayTailscale.${value}`); }; - const quickstartLines = quickstartGateway.hasExisting - ? [ - t("wizard.setup.quickstartKeepSettings"), - t("wizard.setup.quickstartGatewayPort", { port: quickstartGateway.port }), - t("wizard.setup.quickstartGatewayBind", { bind: formatBind(quickstartGateway.bind) }), - ...(quickstartGateway.bind === "custom" && quickstartGateway.customBindHost - ? [ - t("wizard.setup.quickstartGatewayCustomIp", { - host: quickstartGateway.customBindHost, - }), - ] - : []), - t("wizard.setup.quickstartGatewayAuth", { - auth: formatAuth(quickstartGateway.authMode), - }), - t("wizard.setup.quickstartTailscaleExposure", { - exposure: formatTailscale(quickstartGateway.tailscaleMode), - }), - t("wizard.setup.quickstartDirectChannels"), - ] - : [ - t("wizard.setup.quickstartGatewayPort", { port: quickstartGateway.port }), - t("wizard.setup.quickstartGatewayBind", { bind: t("wizard.gateway.bindLoopback") }), - t("wizard.setup.quickstartGatewayAuth", { - auth: t("wizard.setup.quickstartAuthTokenDefault"), - }), - t("wizard.setup.quickstartTailscaleExposure", { - exposure: t("wizard.gatewayTailscale.off"), - }), - t("wizard.setup.quickstartDirectChannels"), - ]; + const quickstartLines = [ + ...(quickstartGateway.hasExisting && !hasExplicitQuickstartGatewayOverrides + ? [t("wizard.setup.quickstartKeepSettings")] + : []), + t("wizard.setup.quickstartGatewayPort", { port: quickstartGateway.port }), + t("wizard.setup.quickstartGatewayBind", { bind: formatBind(quickstartGateway.bind) }), + ...(quickstartGateway.bind === "custom" && quickstartGateway.customBindHost + ? [ + t("wizard.setup.quickstartGatewayCustomIp", { + host: quickstartGateway.customBindHost, + }), + ] + : []), + t("wizard.setup.quickstartGatewayAuth", { + auth: formatAuth(quickstartGateway.authMode), + }), + t("wizard.setup.quickstartTailscaleExposure", { + exposure: formatTailscale(quickstartGateway.tailscaleMode), + }), + t("wizard.setup.quickstartDirectChannels"), + ]; await prompter.note(quickstartLines.join("\n"), "QuickStart"); }