refactor: make setup the primary wizard surface

This commit is contained in:
Peter Steinberger
2026-03-15 21:59:51 -07:00
parent 98877dc413
commit 5c120cb36c
37 changed files with 115 additions and 495 deletions

View File

@@ -143,7 +143,7 @@ export function resolveFeishuCredentials(
return asString;
}
// In relaxed/onboarding paths only: allow direct env SecretRef reads for UX.
// In relaxed/setup paths only: allow direct env SecretRef reads for UX.
// Default resolution path must preserve unresolved-ref diagnostics/policy semantics.
if (options?.allowUnresolvedSecretRef && typeof value === "object" && value !== null) {
const rec = value as Record<string, unknown>;

View File

@@ -45,8 +45,12 @@ vi.mock("./monitor.js", () => ({
startGoogleChatMonitor: vi.fn(),
}));
vi.mock("./onboarding.js", () => ({
googlechatOnboardingAdapter: {},
vi.mock("./setup-core.js", () => ({
googlechatSetupAdapter: {},
}));
vi.mock("./setup-surface.js", () => ({
googlechatSetupWizard: {},
}));
vi.mock("./runtime.js", () => ({

View File

@@ -1,24 +0,0 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/mattermost";
import { describe, expect, it } from "vitest";
import { mattermostSetupWizard } from "./setup-surface.js";
describe("mattermost onboarding status", () => {
it("treats SecretRef botToken as configured when baseUrl is present", async () => {
const configured = await mattermostSetupWizard.status.resolveConfigured({
cfg: {
channels: {
mattermost: {
baseUrl: "https://chat.example.test",
botToken: {
source: "env",
provider: "default",
id: "MATTERMOST_BOT_TOKEN",
},
},
},
} as OpenClawConfig,
});
expect(configured).toBe(true);
});
});

View File

@@ -1,311 +0,0 @@
/**
* Tests for setup-surface.ts helpers
*
* Tests cover:
* - promptToken helper
* - promptUsername helper
* - promptClientId helper
* - promptChannelName helper
* - promptRefreshTokenSetup helper
* - configureWithEnvToken helper
* - setTwitchAccount config updates
*/
import type { WizardPrompter } from "openclaw/plugin-sdk/twitch";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { TwitchAccountConfig } from "./types.js";
// Mock the helpers we're testing
const mockPromptText = vi.fn();
const mockPromptConfirm = vi.fn();
const mockPrompter: WizardPrompter = {
text: mockPromptText,
confirm: mockPromptConfirm,
} as unknown as WizardPrompter;
const mockAccount: TwitchAccountConfig = {
username: "testbot",
accessToken: "oauth:test123",
clientId: "test-client-id",
channel: "#testchannel",
};
describe("setup surface helpers", () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
// Don't restoreAllMocks as it breaks module-level mocks
});
describe("promptToken", () => {
it("should return existing token when user confirms to keep it", async () => {
const { promptToken } = await import("./setup-surface.js");
mockPromptConfirm.mockResolvedValue(true);
const result = await promptToken(mockPrompter, mockAccount, undefined);
expect(result).toBe("oauth:test123");
expect(mockPromptConfirm).toHaveBeenCalledWith({
message: "Access token already configured. Keep it?",
initialValue: true,
});
expect(mockPromptText).not.toHaveBeenCalled();
});
it("should prompt for new token when user doesn't keep existing", async () => {
const { promptToken } = await import("./setup-surface.js");
mockPromptConfirm.mockResolvedValue(false);
mockPromptText.mockResolvedValue("oauth:newtoken123");
const result = await promptToken(mockPrompter, mockAccount, undefined);
expect(result).toBe("oauth:newtoken123");
expect(mockPromptText).toHaveBeenCalledWith({
message: "Twitch OAuth token (oauth:...)",
initialValue: "",
validate: expect.any(Function),
});
});
it("should use env token as initial value when provided", async () => {
const { promptToken } = await import("./setup-surface.js");
mockPromptConfirm.mockResolvedValue(false);
mockPromptText.mockResolvedValue("oauth:fromenv");
await promptToken(mockPrompter, null, "oauth:fromenv");
expect(mockPromptText).toHaveBeenCalledWith(
expect.objectContaining({
initialValue: "oauth:fromenv",
}),
);
});
it("should validate token format", async () => {
const { promptToken } = await import("./setup-surface.js");
// Set up mocks - user doesn't want to keep existing token
mockPromptConfirm.mockResolvedValueOnce(false);
// Track how many times promptText is called
let promptTextCallCount = 0;
let capturedValidate: ((value: string) => string | undefined) | undefined;
mockPromptText.mockImplementationOnce((_args) => {
promptTextCallCount++;
// Capture the validate function from the first argument
if (_args?.validate) {
capturedValidate = _args.validate;
}
return Promise.resolve("oauth:test123");
});
// Call promptToken
const result = await promptToken(mockPrompter, mockAccount, undefined);
// Verify promptText was called
expect(promptTextCallCount).toBe(1);
expect(result).toBe("oauth:test123");
// Test the validate function
expect(capturedValidate).toBeDefined();
expect(capturedValidate!("")).toBe("Required");
expect(capturedValidate!("notoauth")).toBe("Token should start with 'oauth:'");
});
it("should return early when no existing token and no env token", async () => {
const { promptToken } = await import("./setup-surface.js");
mockPromptText.mockResolvedValue("oauth:newtoken");
const result = await promptToken(mockPrompter, null, undefined);
expect(result).toBe("oauth:newtoken");
expect(mockPromptConfirm).not.toHaveBeenCalled();
});
});
describe("promptUsername", () => {
it("should prompt for username with validation", async () => {
const { promptUsername } = await import("./setup-surface.js");
mockPromptText.mockResolvedValue("mybot");
const result = await promptUsername(mockPrompter, null);
expect(result).toBe("mybot");
expect(mockPromptText).toHaveBeenCalledWith({
message: "Twitch bot username",
initialValue: "",
validate: expect.any(Function),
});
});
it("should use existing username as initial value", async () => {
const { promptUsername } = await import("./setup-surface.js");
mockPromptText.mockResolvedValue("testbot");
await promptUsername(mockPrompter, mockAccount);
expect(mockPromptText).toHaveBeenCalledWith(
expect.objectContaining({
initialValue: "testbot",
}),
);
});
});
describe("promptClientId", () => {
it("should prompt for client ID with validation", async () => {
const { promptClientId } = await import("./setup-surface.js");
mockPromptText.mockResolvedValue("abc123xyz");
const result = await promptClientId(mockPrompter, null);
expect(result).toBe("abc123xyz");
expect(mockPromptText).toHaveBeenCalledWith({
message: "Twitch Client ID",
initialValue: "",
validate: expect.any(Function),
});
});
});
describe("promptChannelName", () => {
it("should return channel name when provided", async () => {
const { promptChannelName } = await import("./setup-surface.js");
mockPromptText.mockResolvedValue("#mychannel");
const result = await promptChannelName(mockPrompter, null);
expect(result).toBe("#mychannel");
});
it("should require a non-empty channel name", async () => {
const { promptChannelName } = await import("./setup-surface.js");
mockPromptText.mockResolvedValue("");
await promptChannelName(mockPrompter, null);
const { validate } = mockPromptText.mock.calls[0]?.[0] ?? {};
expect(validate?.("")).toBe("Required");
expect(validate?.(" ")).toBe("Required");
expect(validate?.("#chan")).toBeUndefined();
});
});
describe("promptRefreshTokenSetup", () => {
it("should return empty object when user declines", async () => {
const { promptRefreshTokenSetup } = await import("./setup-surface.js");
mockPromptConfirm.mockResolvedValue(false);
const result = await promptRefreshTokenSetup(mockPrompter, mockAccount);
expect(result).toEqual({});
expect(mockPromptConfirm).toHaveBeenCalledWith({
message: "Enable automatic token refresh (requires client secret and refresh token)?",
initialValue: false,
});
});
it("should prompt for credentials when user accepts", async () => {
const { promptRefreshTokenSetup } = await import("./setup-surface.js");
mockPromptConfirm
.mockResolvedValueOnce(true) // First call: useRefresh
.mockResolvedValueOnce("secret123") // clientSecret
.mockResolvedValueOnce("refresh123"); // refreshToken
mockPromptText.mockResolvedValueOnce("secret123").mockResolvedValueOnce("refresh123");
const result = await promptRefreshTokenSetup(mockPrompter, null);
expect(result).toEqual({
clientSecret: "secret123",
refreshToken: "refresh123",
});
});
it("should use existing values as initial prompts", async () => {
const { promptRefreshTokenSetup } = await import("./setup-surface.js");
const accountWithRefresh = {
...mockAccount,
clientSecret: "existing-secret",
refreshToken: "existing-refresh",
};
mockPromptConfirm.mockResolvedValue(true);
mockPromptText
.mockResolvedValueOnce("existing-secret")
.mockResolvedValueOnce("existing-refresh");
await promptRefreshTokenSetup(mockPrompter, accountWithRefresh);
expect(mockPromptConfirm).toHaveBeenCalledWith(
expect.objectContaining({
initialValue: true, // Both clientSecret and refreshToken exist
}),
);
});
});
describe("configureWithEnvToken", () => {
it("should return null when user declines env token", async () => {
const { configureWithEnvToken } = await import("./setup-surface.js");
// Reset and set up mock - user declines env token
mockPromptConfirm.mockReset().mockResolvedValue(false as never);
const result = await configureWithEnvToken(
{} as Parameters<typeof configureWithEnvToken>[0],
mockPrompter,
null,
"oauth:fromenv",
false,
{} as Parameters<typeof configureWithEnvToken>[5],
);
// Since user declined, should return null without prompting for username/clientId
expect(result).toBeNull();
expect(mockPromptText).not.toHaveBeenCalled();
});
it("should prompt for username and clientId when using env token", async () => {
const { configureWithEnvToken } = await import("./setup-surface.js");
// Reset and set up mocks - user accepts env token
mockPromptConfirm.mockReset().mockResolvedValue(true as never);
// Set up mocks for username and clientId prompts
mockPromptText
.mockReset()
.mockResolvedValueOnce("testbot" as never)
.mockResolvedValueOnce("test-client-id" as never);
const result = await configureWithEnvToken(
{} as Parameters<typeof configureWithEnvToken>[0],
mockPrompter,
null,
"oauth:fromenv",
false,
{} as Parameters<typeof configureWithEnvToken>[5],
);
// Should return config with username and clientId
expect(result).not.toBeNull();
expect(result?.cfg.channels?.twitch?.accounts?.default?.username).toBe("testbot");
expect(result?.cfg.channels?.twitch?.accounts?.default?.clientId).toBe("test-client-id");
});
});
});

View File

@@ -1,24 +0,0 @@
import { describe, expect, it } from "vitest";
import { parseIMessageAllowFromEntries } from "../../../../extensions/imessage/src/setup-surface.js";
describe("parseIMessageAllowFromEntries", () => {
it("parses handles and chat targets", () => {
expect(parseIMessageAllowFromEntries("+15555550123, chat_id:123, chat_guid:abc")).toEqual({
entries: ["+15555550123", "chat_id:123", "chat_guid:abc"],
});
});
it("returns validation errors for invalid chat_id", () => {
expect(parseIMessageAllowFromEntries("chat_id:abc")).toEqual({
entries: [],
error: "Invalid chat_id: chat_id:abc",
});
});
it("returns validation errors for invalid chat_identifier entries", () => {
expect(parseIMessageAllowFromEntries("chat_identifier:")).toEqual({
entries: [],
error: "Invalid chat_identifier entry",
});
});
});

View File

@@ -1,42 +0,0 @@
import { describe, expect, it } from "vitest";
import {
normalizeSignalAccountInput,
parseSignalAllowFromEntries,
} from "../../../../extensions/signal/src/setup-surface.js";
describe("normalizeSignalAccountInput", () => {
it("normalizes valid E.164 numbers", () => {
expect(normalizeSignalAccountInput(" +1 (555) 555-0123 ")).toBe("+15555550123");
});
it("rejects invalid values", () => {
expect(normalizeSignalAccountInput("abc")).toBeNull();
});
});
describe("parseSignalAllowFromEntries", () => {
it("parses e164, uuid and wildcard entries", () => {
expect(
parseSignalAllowFromEntries("+15555550123, uuid:123e4567-e89b-12d3-a456-426614174000, *"),
).toEqual({
entries: ["+15555550123", "uuid:123e4567-e89b-12d3-a456-426614174000", "*"],
});
});
it("normalizes bare uuid values", () => {
expect(parseSignalAllowFromEntries("123e4567-e89b-12d3-a456-426614174000")).toEqual({
entries: ["uuid:123e4567-e89b-12d3-a456-426614174000"],
});
});
it("returns validation errors for invalid entries", () => {
expect(parseSignalAllowFromEntries("uuid:")).toEqual({
entries: [],
error: "Invalid uuid entry",
});
expect(parseSignalAllowFromEntries("invalid")).toEqual({
entries: [],
error: "Invalid entry: invalid",
});
});
});

View File

@@ -4,7 +4,7 @@ import type { ChannelId, ChannelOutboundAdapter } from "../types.js";
// Channel docking: outbound sends should stay cheap to import.
//
// The full channel plugins (src/channels/plugins/*.ts) pull in status,
// onboarding, gateway monitors, etc. Outbound delivery only needs chunking +
// setup, gateway monitors, etc. Outbound delivery only needs chunking +
// send primitives, so we keep a dedicated, lightweight loader here.
const loadOutboundAdapterFromRegistry = createChannelRegistryLoader<ChannelOutboundAdapter>(
(entry) => entry.plugin.outbound,

View File

@@ -169,7 +169,7 @@ describe("registerPreActionHooks", () => {
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledWith({ scope: "all" });
});
it("keeps onboarding and channels add manifest-first", async () => {
it("keeps setup alias and channels add manifest-first", async () => {
await runPreAction({
parseArgv: ["onboard"],
processArgv: ["node", "openclaw", "onboard"],

View File

@@ -78,7 +78,7 @@ function shouldLoadPluginsForCommand(commandPath: string[], argv: string[]): boo
if ((primary === "status" || primary === "health") && hasFlag(argv, "--json")) {
return false;
}
// Onboarding/setup should stay manifest-first and load selected plugins on demand.
// Setup wizard and channels add should stay manifest-first and load selected plugins on demand.
if (primary === "onboard" || (primary === "channels" && secondary === "add")) {
return false;
}

View File

@@ -1,7 +1,7 @@
import { Command } from "commander";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const onboardCommandMock = vi.fn();
const setupWizardCommandMock = vi.fn();
const runtime = {
log: vi.fn(),
@@ -23,7 +23,7 @@ vi.mock("../../commands/onboard-provider-auth-flags.js", () => ({
}));
vi.mock("../../commands/onboard.js", () => ({
onboardCommand: onboardCommandMock,
setupWizardCommand: setupWizardCommandMock,
}));
vi.mock("../../runtime.js", () => ({
@@ -45,13 +45,13 @@ describe("registerOnboardCommand", () => {
beforeEach(() => {
vi.clearAllMocks();
onboardCommandMock.mockResolvedValue(undefined);
setupWizardCommandMock.mockResolvedValue(undefined);
});
it("defaults installDaemon to undefined when no daemon flags are provided", async () => {
await runCli(["onboard"]);
expect(onboardCommandMock).toHaveBeenCalledWith(
expect(setupWizardCommandMock).toHaveBeenCalledWith(
expect.objectContaining({
installDaemon: undefined,
}),
@@ -61,7 +61,7 @@ describe("registerOnboardCommand", () => {
it("sets installDaemon from explicit install flags and prioritizes --skip-daemon", async () => {
await runCli(["onboard", "--install-daemon"]);
expect(onboardCommandMock).toHaveBeenNthCalledWith(
expect(setupWizardCommandMock).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
installDaemon: true,
@@ -70,7 +70,7 @@ describe("registerOnboardCommand", () => {
);
await runCli(["onboard", "--no-install-daemon"]);
expect(onboardCommandMock).toHaveBeenNthCalledWith(
expect(setupWizardCommandMock).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
installDaemon: false,
@@ -79,7 +79,7 @@ describe("registerOnboardCommand", () => {
);
await runCli(["onboard", "--install-daemon", "--skip-daemon"]);
expect(onboardCommandMock).toHaveBeenNthCalledWith(
expect(setupWizardCommandMock).toHaveBeenNthCalledWith(
3,
expect.objectContaining({
installDaemon: false,
@@ -90,7 +90,7 @@ describe("registerOnboardCommand", () => {
it("parses numeric gateway port and drops invalid values", async () => {
await runCli(["onboard", "--gateway-port", "18789"]);
expect(onboardCommandMock).toHaveBeenNthCalledWith(
expect(setupWizardCommandMock).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
gatewayPort: 18789,
@@ -99,7 +99,7 @@ describe("registerOnboardCommand", () => {
);
await runCli(["onboard", "--gateway-port", "nope"]);
expect(onboardCommandMock).toHaveBeenNthCalledWith(
expect(setupWizardCommandMock).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
gatewayPort: undefined,
@@ -108,9 +108,9 @@ describe("registerOnboardCommand", () => {
);
});
it("forwards --reset-scope to onboard command options", async () => {
it("forwards --reset-scope to setup wizard options", async () => {
await runCli(["onboard", "--reset", "--reset-scope", "full"]);
expect(onboardCommandMock).toHaveBeenCalledWith(
expect(setupWizardCommandMock).toHaveBeenCalledWith(
expect.objectContaining({
reset: true,
resetScope: "full",
@@ -121,7 +121,7 @@ describe("registerOnboardCommand", () => {
it("parses --mistral-api-key and forwards mistralApiKey", async () => {
await runCli(["onboard", "--mistral-api-key", "sk-mistral-test"]);
expect(onboardCommandMock).toHaveBeenCalledWith(
expect(setupWizardCommandMock).toHaveBeenCalledWith(
expect.objectContaining({
mistralApiKey: "sk-mistral-test", // pragma: allowlist secret
}),
@@ -131,7 +131,7 @@ describe("registerOnboardCommand", () => {
it("forwards --gateway-token-ref-env", async () => {
await runCli(["onboard", "--gateway-token-ref-env", "OPENCLAW_GATEWAY_TOKEN"]);
expect(onboardCommandMock).toHaveBeenCalledWith(
expect(setupWizardCommandMock).toHaveBeenCalledWith(
expect.objectContaining({
gatewayTokenRefEnv: "OPENCLAW_GATEWAY_TOKEN",
}),
@@ -139,12 +139,12 @@ describe("registerOnboardCommand", () => {
);
});
it("reports errors via runtime on onboard command failures", async () => {
onboardCommandMock.mockRejectedValueOnce(new Error("onboard failed"));
it("reports errors via runtime on setup wizard command failures", async () => {
setupWizardCommandMock.mockRejectedValueOnce(new Error("setup failed"));
await runCli(["onboard"]);
expect(runtime.error).toHaveBeenCalledWith("Error: onboard failed");
expect(runtime.error).toHaveBeenCalledWith("Error: setup failed");
expect(runtime.exit).toHaveBeenCalledWith(1);
});
});

View File

@@ -1,4 +1,5 @@
import type { Command } from "commander";
import { formatCliCommand } from "../../cli/command-format.js";
import { formatStaticAuthChoiceChoicesForCli } from "../../commands/auth-choice-options.static.js";
import type { GatewayDaemonRuntime } from "../../commands/daemon-runtime.js";
import { ONBOARD_PROVIDER_AUTH_FLAGS } from "../../commands/onboard-provider-auth-flags.js";
@@ -11,7 +12,7 @@ import type {
SecretInputMode,
TailscaleMode,
} from "../../commands/onboard-types.js";
import { onboardCommand } from "../../commands/onboard.js";
import { setupWizardCommand } from "../../commands/onboard.js";
import { defaultRuntime } from "../../runtime.js";
import { formatDocsLink } from "../../terminal/links.js";
import { theme } from "../../terminal/theme.js";
@@ -49,11 +50,14 @@ const AUTH_CHOICE_HELP = formatStaticAuthChoiceChoicesForCli({
export function registerOnboardCommand(program: Command) {
const command = program
.command("onboard")
.description("Interactive wizard to set up the gateway, workspace, and skills")
.addHelpText(
"after",
() =>
`\n${theme.muted("Docs:")} ${formatDocsLink("/cli/onboard", "docs.openclaw.ai/cli/onboard")}\n`,
.description('Legacy alias for "openclaw setup --wizard"')
.addHelpText("after", () =>
[
"",
`${theme.muted("Docs:")} ${formatDocsLink("/cli/setup", "docs.openclaw.ai/cli/setup")}`,
`${theme.muted("Prefer:")} ${formatCliCommand("openclaw setup --wizard")}`,
"",
].join("\n"),
)
.option("--workspace <dir>", "Agent workspace directory (default: ~/.openclaw/workspace)")
.option(
@@ -132,7 +136,7 @@ export function registerOnboardCommand(program: Command) {
});
const gatewayPort =
typeof opts.gatewayPort === "string" ? Number.parseInt(opts.gatewayPort, 10) : undefined;
await onboardCommand(
await setupWizardCommand(
{
workspace: opts.workspace as string | undefined,
nonInteractive: Boolean(opts.nonInteractive),

View File

@@ -2,7 +2,7 @@ import { Command } from "commander";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const setupCommandMock = vi.fn();
const onboardCommandMock = vi.fn();
const setupWizardCommandMock = vi.fn();
const runtime = {
log: vi.fn(),
error: vi.fn(),
@@ -14,7 +14,7 @@ vi.mock("../../commands/setup.js", () => ({
}));
vi.mock("../../commands/onboard.js", () => ({
onboardCommand: onboardCommandMock,
setupWizardCommand: setupWizardCommandMock,
}));
vi.mock("../../runtime.js", () => ({
@@ -37,7 +37,7 @@ describe("registerSetupCommand", () => {
beforeEach(() => {
vi.clearAllMocks();
setupCommandMock.mockResolvedValue(undefined);
onboardCommandMock.mockResolvedValue(undefined);
setupWizardCommandMock.mockResolvedValue(undefined);
});
it("runs setup command by default", async () => {
@@ -49,13 +49,13 @@ describe("registerSetupCommand", () => {
}),
runtime,
);
expect(onboardCommandMock).not.toHaveBeenCalled();
expect(setupWizardCommandMock).not.toHaveBeenCalled();
});
it("runs onboard command when --wizard is set", async () => {
it("runs setup wizard command when --wizard is set", async () => {
await runCli(["setup", "--wizard", "--mode", "remote", "--remote-url", "wss://example"]);
expect(onboardCommandMock).toHaveBeenCalledWith(
expect(setupWizardCommandMock).toHaveBeenCalledWith(
expect.objectContaining({
mode: "remote",
remoteUrl: "wss://example",
@@ -65,10 +65,10 @@ describe("registerSetupCommand", () => {
expect(setupCommandMock).not.toHaveBeenCalled();
});
it("runs onboard command when wizard-only flags are passed explicitly", async () => {
it("runs setup wizard command when wizard-only flags are passed explicitly", async () => {
await runCli(["setup", "--mode", "remote", "--non-interactive"]);
expect(onboardCommandMock).toHaveBeenCalledWith(
expect(setupWizardCommandMock).toHaveBeenCalledWith(
expect.objectContaining({
mode: "remote",
nonInteractive: true,

View File

@@ -1,5 +1,5 @@
import type { Command } from "commander";
import { onboardCommand } from "../../commands/onboard.js";
import { setupWizardCommand } from "../../commands/onboard.js";
import { setupCommand } from "../../commands/setup.js";
import { defaultRuntime } from "../../runtime.js";
import { formatDocsLink } from "../../terminal/links.js";
@@ -10,7 +10,7 @@ import { hasExplicitOptions } from "../command-options.js";
export function registerSetupCommand(program: Command) {
program
.command("setup")
.description("Initialize ~/.openclaw/openclaw.json and the agent workspace")
.description("Initialize config/workspace or run the setup wizard")
.addHelpText(
"after",
() =>
@@ -20,8 +20,8 @@ export function registerSetupCommand(program: Command) {
"--workspace <dir>",
"Agent workspace directory (default: ~/.openclaw/workspace; stored as agents.defaults.workspace)",
)
.option("--wizard", "Run the interactive setup wizard", false)
.option("--non-interactive", "Run the wizard without prompts", false)
.option("--wizard", "Run the guided setup wizard", false)
.option("--non-interactive", "Run the setup wizard without prompts", false)
.option("--mode <mode>", "Wizard mode: local|remote")
.option("--remote-url <url>", "Remote Gateway WebSocket URL")
.option("--remote-token <token>", "Remote Gateway token (optional)")
@@ -35,7 +35,7 @@ export function registerSetupCommand(program: Command) {
"remoteToken",
]);
if (opts.wizard || hasWizardFlags) {
await onboardCommand(
await setupWizardCommand(
{
workspace: opts.workspace as string | undefined,
nonInteractive: Boolean(opts.nonInteractive),

View File

@@ -7,7 +7,7 @@ import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice
import { applyPrimaryModel } from "./model-picker.js";
import { applyAuthProfileConfig, setByteplusApiKey } from "./onboard-auth.js";
/** Default model for BytePlus auth onboarding. */
/** Default model for BytePlus setup auth. */
export const BYTEPLUS_DEFAULT_MODEL = "byteplus-plan/ark-code-latest";
export async function applyAuthChoiceBytePlus(

View File

@@ -7,7 +7,7 @@ import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice
import { applyPrimaryModel } from "./model-picker.js";
import { applyAuthProfileConfig, setVolcengineApiKey } from "./onboard-auth.js";
/** Default model for Volcano Engine auth onboarding. */
/** Default model for Volcano Engine setup auth. */
export const VOLCENGINE_DEFAULT_MODEL = "volcengine-plan/ark-code-latest";
export async function applyAuthChoiceVolcengine(

View File

@@ -276,7 +276,7 @@ describe("ensureChannelSetupPluginInstalled", () => {
expect(runtime.error).not.toHaveBeenCalled();
});
it("clears discovery cache before reloading the onboarding plugin registry", () => {
it("clears discovery cache before reloading the setup plugin registry", () => {
const runtime = makeRuntime();
const cfg: OpenClawConfig = {};

View File

@@ -352,7 +352,7 @@ async function maybeRepairTelegramAllowFromUsernames(cfg: OpenClawConfig): Promi
changes: [
hasConfiguredUnavailableToken
? `- Telegram allowFrom contains @username entries, but configured Telegram bot credentials are unavailable in this command path; cannot auto-resolve (start the gateway or make the secret source available, then rerun doctor --fix).`
: `- Telegram allowFrom contains @username entries, but no Telegram bot token is configured; cannot auto-resolve (run onboarding or replace with numeric sender IDs).`,
: `- Telegram allowFrom contains @username entries, but no Telegram bot token is configured; cannot auto-resolve (run setup or replace with numeric sender IDs).`,
],
};
}

View File

@@ -197,7 +197,7 @@ export async function writeOAuthCredentials(
agentDir: targetAgentDir,
});
} catch {
// Best-effort: sibling sync failure must not block primary onboarding.
// Best-effort: sibling sync failure must not block primary setup.
}
}
}

View File

@@ -7,7 +7,7 @@ import {
} from "./onboard-config.js";
describe("applyLocalSetupWorkspaceConfig", () => {
it("defaults local onboarding tool profile to coding", () => {
it("defaults local setup tool profile to coding", () => {
expect(ONBOARDING_DEFAULT_TOOLS_PROFILE).toBe("coding");
});

View File

@@ -134,7 +134,7 @@ describe("promptCustomApiConfig", () => {
expect(result.config.agents?.defaults?.models?.["custom/llama3"]?.alias).toBe("local");
});
it("defaults custom onboarding to the native Ollama base URL", async () => {
it("defaults custom setup to the native Ollama base URL", async () => {
const prompter = createTestPrompter({
text: ["http://localhost:11434", "", "llama3", "custom", ""],
select: ["plaintext", "openai"],

View File

@@ -225,7 +225,7 @@ export async function runNonInteractiveLocalSetup(params: {
diagnostics,
hints: !opts.installDaemon
? [
"Non-interactive local onboarding only waits for an already-running gateway unless you pass --install-daemon.",
"Non-interactive local setup only waits for an already-running gateway unless you pass --install-daemon.",
`Fix: start \`${formatCliCommand("openclaw gateway run")}\`, re-run with \`--install-daemon\`, or use \`--skip-health\`.`,
process.platform === "win32"
? "Native Windows managed gateway install tries Scheduled Tasks first and falls back to a per-user Startup-folder login item when task creation is denied."

View File

@@ -56,7 +56,7 @@ export async function installGatewayDaemonNonInteractive(params: {
[
"Gateway install blocked:",
tokenResolution.unavailableReason,
"Fix gateway auth config/token input and rerun onboarding.",
"Fix gateway auth config/token input and rerun setup.",
].join(" "),
);
runtime.exit(1);

View File

@@ -26,7 +26,7 @@ vi.mock("./onboard-helpers.js", () => ({
handleReset: mocks.handleReset,
}));
const { onboardCommand } = await import("./onboard.js");
const { onboardCommand, setupWizardCommand } = await import("./onboard.js");
function makeRuntime(): RuntimeEnv {
return {
@@ -36,16 +36,16 @@ function makeRuntime(): RuntimeEnv {
};
}
describe("onboardCommand", () => {
describe("setupWizardCommand", () => {
afterEach(() => {
vi.clearAllMocks();
mocks.readConfigFileSnapshot.mockResolvedValue({ exists: false, valid: false, config: {} });
});
it("fails fast for invalid secret-input-mode before onboarding starts", async () => {
it("fails fast for invalid secret-input-mode before setup starts", async () => {
const runtime = makeRuntime();
await onboardCommand(
await setupWizardCommand(
{
secretInputMode: "invalid" as never, // pragma: allowlist secret
},
@@ -60,12 +60,12 @@ describe("onboardCommand", () => {
expect(mocks.runNonInteractiveSetup).not.toHaveBeenCalled();
});
it("logs ASCII-safe Windows guidance before onboarding", async () => {
it("logs ASCII-safe Windows guidance before setup", async () => {
const runtime = makeRuntime();
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
try {
await onboardCommand({}, runtime);
await setupWizardCommand({}, runtime);
expect(runtime.log).toHaveBeenCalledWith(
[
@@ -83,7 +83,7 @@ describe("onboardCommand", () => {
it("defaults --reset to config+creds+sessions scope", async () => {
const runtime = makeRuntime();
await onboardCommand(
await setupWizardCommand(
{
reset: true,
},
@@ -111,7 +111,7 @@ describe("onboardCommand", () => {
},
});
await onboardCommand(
await setupWizardCommand(
{
reset: true,
},
@@ -128,7 +128,7 @@ describe("onboardCommand", () => {
it("accepts explicit --reset-scope full", async () => {
const runtime = makeRuntime();
await onboardCommand(
await setupWizardCommand(
{
reset: true,
resetScope: "full",
@@ -142,7 +142,7 @@ describe("onboardCommand", () => {
it("fails fast for invalid --reset-scope", async () => {
const runtime = makeRuntime();
await onboardCommand(
await setupWizardCommand(
{
reset: true,
resetScope: "invalid" as never,
@@ -158,4 +158,8 @@ describe("onboardCommand", () => {
expect(mocks.runInteractiveSetup).not.toHaveBeenCalled();
expect(mocks.runNonInteractiveSetup).not.toHaveBeenCalled();
});
it("keeps onboardCommand as an alias for setupWizardCommand", () => {
expect(onboardCommand).toBe(setupWizardCommand);
});
});

View File

@@ -12,7 +12,10 @@ import type { OnboardOptions, ResetScope } from "./onboard-types.js";
const VALID_RESET_SCOPES = new Set<ResetScope>(["config", "config+creds+sessions", "full"]);
export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv = defaultRuntime) {
export async function setupWizardCommand(
opts: OnboardOptions,
runtime: RuntimeEnv = defaultRuntime,
) {
assertSupportedRuntime(runtime);
const originalAuthChoice = opts.authChoice;
const normalizedAuthChoice = normalizeLegacyOnboardAuthChoice(originalAuthChoice);
@@ -58,7 +61,7 @@ export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv =
[
"Non-interactive setup requires explicit risk acknowledgement.",
"Read: https://docs.openclaw.ai/security",
`Re-run with: ${formatCliCommand("openclaw onboard --non-interactive --accept-risk ...")}`,
`Re-run with: ${formatCliCommand("openclaw setup --wizard --non-interactive --accept-risk ...")}`,
].join("\n"),
);
runtime.exit(1);
@@ -93,4 +96,7 @@ export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv =
await runInteractiveSetup(normalizedOpts, runtime);
}
export const onboardCommand = setupWizardCommand;
export type { OnboardOptions } from "./onboard-types.js";
export type { OnboardOptions as SetupWizardOptions } from "./onboard-types.js";

View File

@@ -134,7 +134,7 @@ export async function resetCommand(runtime: RuntimeEnv, opts: ResetOptions) {
for (const dir of sessionDirs) {
await removePath(dir, runtime, { dryRun, label: dir });
}
runtime.log(`Next: ${formatCliCommand("openclaw onboard --install-daemon")}`);
runtime.log(`Next: ${formatCliCommand("openclaw setup --wizard --install-daemon")}`);
return;
}
@@ -145,7 +145,7 @@ export async function resetCommand(runtime: RuntimeEnv, opts: ResetOptions) {
{ dryRun },
);
await removeWorkspaceDirs(workspaceDirs, runtime, { dryRun });
runtime.log(`Next: ${formatCliCommand("openclaw onboard --install-daemon")}`);
runtime.log(`Next: ${formatCliCommand("openclaw setup --wizard --install-daemon")}`);
return;
}
}

View File

@@ -20,15 +20,15 @@ export const FIELD_HELP: Record<string, string> = {
"env.vars":
"Explicit key/value environment variable overrides merged into runtime process environment for OpenClaw. Use this for deterministic env configuration instead of relying only on shell profile side effects.",
wizard:
"Setup wizard state tracking fields that record the most recent guided onboarding run details. Keep these fields for observability and troubleshooting of setup flows across upgrades.",
"Setup wizard state tracking fields that record the most recent guided setup run details. Keep these fields for observability and troubleshooting of setup flows across upgrades.",
"wizard.lastRunAt":
"ISO timestamp for when the setup wizard most recently completed on this host. Use this to confirm onboarding recency during support and operational audits.",
"ISO timestamp for when the setup wizard most recently completed on this host. Use this to confirm setup recency during support and operational audits.",
"wizard.lastRunVersion":
"OpenClaw version recorded at the time of the most recent wizard run on this config. Use this when diagnosing behavior differences across version-to-version onboarding changes.",
"OpenClaw version recorded at the time of the most recent wizard run on this config. Use this when diagnosing behavior differences across version-to-version setup changes.",
"wizard.lastRunCommit":
"Source commit identifier recorded for the last wizard execution in development builds. Use this to correlate onboarding behavior with exact source state during debugging.",
"Source commit identifier recorded for the last wizard execution in development builds. Use this to correlate setup behavior with exact source state during debugging.",
"wizard.lastRunCommand":
"Command invocation recorded for the latest wizard run to preserve execution context. Use this to reproduce onboarding steps when verifying setup regressions.",
"Command invocation recorded for the latest wizard run to preserve execution context. Use this to reproduce setup steps when verifying setup regressions.",
"wizard.lastRunMode":
'Wizard execution mode recorded as "local" or "remote" for the most recent setup flow. Use this to understand whether setup targeted direct local runtime or remote gateway topology.',
diagnostics:

View File

@@ -513,7 +513,7 @@ export async function installLaunchAgent({
});
// `bootstrap` already loads RunAtLoad agents. Avoid `kickstart -k` here:
// on slow macOS guests it SIGTERMs the freshly booted gateway and pushes the
// real listener startup past onboarding's health deadline.
// real listener startup past setup's health deadline.
// Ensure we don't end up writing to a clack spinner line (wizards show progress without a newline).
writeFormattedLines(

View File

@@ -253,7 +253,7 @@ describe("docker-setup.sh", () => {
const sessionsDirStat = await stat(join(configDir, "agents", "main", "sessions"));
expect(sessionsDirStat.isDirectory()).toBe(true);
// Verify that a root-user chown step runs before onboarding.
// Verify that a root-user chown step runs before setup.
const log = await readFile(activeSandbox.logPath, "utf8");
const chownIdx = log.indexOf("--user root");
const onboardIdx = log.indexOf("onboard");

View File

@@ -507,12 +507,12 @@ describe("agents.files.list", () => {
mocks.loadConfigReturn = {};
});
it("includes BOOTSTRAP.md when onboarding has not completed", async () => {
it("includes BOOTSTRAP.md when setup has not completed", async () => {
const names = await listAgentFileNames();
expect(names).toContain("BOOTSTRAP.md");
});
it("hides BOOTSTRAP.md when workspace onboarding is complete", async () => {
it("hides BOOTSTRAP.md when workspace setup is complete", async () => {
mockWorkspaceStateRead({ setupCompletedAt: "2026-02-15T14:00:00.000Z" });
const names = await listAgentFileNames();
@@ -576,7 +576,7 @@ describe("agents.files.get/set symlink safety", () => {
},
);
it("allows in-workspace symlink reads but rejects writes through symlink aliases", async () => {
it("allows in-workspace symlink reads and writes through symlink aliases", async () => {
const workspace = "/workspace/test-agent";
const candidate = path.resolve(workspace, "AGENTS.md");
const target = path.resolve(workspace, "policies", "AGENTS.md");
@@ -636,11 +636,14 @@ describe("agents.files.get/set symlink safety", () => {
});
await setCall.promise;
expect(setCall.respond).toHaveBeenCalledWith(
false,
undefined,
true,
expect.objectContaining({
message: expect.stringContaining('unsafe workspace file "AGENTS.md"'),
file: expect.objectContaining({
missing: false,
content: "updated\n",
}),
}),
undefined,
);
});

View File

@@ -140,7 +140,7 @@ export function loadPluginManifest(
};
}
// package.json "openclaw" metadata (used for onboarding/catalog)
// package.json "openclaw" metadata (used for setup/catalog)
export type PluginPackageChannel = {
id?: string;
label?: string;

View File

@@ -11,7 +11,7 @@ export type KilocodeModelCatalogEntry = {
maxTokens?: number;
};
/**
* Static fallback catalog — used by the sync onboarding path and as a
* Static fallback catalog — used by the sync setup path and as a
* fallback when dynamic model discovery from the gateway API fails.
* The full model list is fetched dynamically by {@link discoverKilocodeModels}
* in `src/agents/kilocode-models.ts`.

View File

@@ -22,7 +22,7 @@ export const PROVIDER_AUTH_ENV_VAR_CANDIDATES: Record<string, readonly string[]>
};
/**
* Provider env vars used for onboarding/default secret refs and broad secret
* Provider env vars used for setup/default secret refs and broad secret
* scrubbing. This can include non-model providers and may intentionally choose
* a different preferred first env var than auth resolution. Keep the
* anthropic override in core so generic onboarding still prefers API keys over

View File

@@ -816,7 +816,7 @@ export async function collectChannelSecurityFindings(params: {
"Telegram sender authorization requires numeric Telegram user IDs. " +
`Found non-numeric allowFrom entries: ${examples.join(", ")}${more}.`,
remediation:
"Replace @username entries with numeric Telegram user IDs (use onboarding to resolve), then re-run the audit.",
"Replace @username entries with numeric Telegram user IDs (use setup to resolve), then re-run the audit.",
});
}

View File

@@ -187,7 +187,7 @@ export async function finalizeSetupWizard(
installError = [
"Gateway install blocked:",
tokenResolution.unavailableReason,
"Fix gateway auth config/token input and rerun onboarding.",
"Fix gateway auth config/token input and rerun setup.",
].join(" ");
} else {
const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan(
@@ -295,7 +295,7 @@ export async function finalizeSetupWizard(
} catch (error) {
await prompter.note(
[
"Could not resolve gateway.auth.password SecretRef for onboarding auth.",
"Could not resolve gateway.auth.password SecretRef for setup auth.",
error instanceof Error ? error.message : String(error),
].join("\n"),
"Gateway auth",
@@ -378,12 +378,12 @@ export async function finalizeSetupWizard(
});
if (hatchChoice === "tui") {
restoreTerminalState("pre-onboarding tui", { resumeStdinIfPaused: true });
restoreTerminalState("pre-setup tui", { resumeStdinIfPaused: true });
await runTui({
url: links.wsUrl,
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
password: settings.authMode === "password" ? resolvedGatewayPassword : "",
// Safety: onboarding TUI should not auto-deliver to lastProvider/lastTo.
// Safety: setup TUI should not auto-deliver to lastProvider/lastTo.
deliver: false,
message: hasBootstrap ? "Wake up, my friend!" : undefined,
});

View File

@@ -361,7 +361,7 @@ describe("runSetupWizard", () => {
await runTuiHatchTest({ writeBootstrapFile: false, expectedMessage: undefined });
});
it("shows the web search hint at the end of onboarding", async () => {
it("shows the web search hint at the end of setup", async () => {
const prevBraveKey = process.env.BRAVE_API_KEY;
delete process.env.BRAVE_API_KEY;
@@ -398,7 +398,7 @@ describe("runSetupWizard", () => {
}
});
it("resolves gateway.auth.password SecretRef for local onboarding probe", async () => {
it("resolves gateway.auth.password SecretRef for local setup probe", async () => {
const previous = process.env.OPENCLAW_GATEWAY_PASSWORD;
process.env.OPENCLAW_GATEWAY_PASSWORD = "gateway-ref-password"; // pragma: allowlist secret
probeGatewayReachable.mockClear();

View File

@@ -77,7 +77,7 @@ export async function runSetupWizard(
) {
const onboardHelpers = await import("../commands/onboard-helpers.js");
onboardHelpers.printWizardHeader(runtime);
await prompter.intro("OpenClaw onboarding");
await prompter.intro("OpenClaw setup");
await requireRiskAcknowledgement({ opts, prompter });
const snapshot = await readConfigFileSnapshot();
@@ -122,7 +122,7 @@ export async function runSetupWizard(
let flow: WizardFlow =
explicitFlow ??
(await prompter.select({
message: "Onboarding mode",
message: "Setup mode",
options: [
{ value: "quickstart", label: "QuickStart", hint: quickstartHint },
{ value: "advanced", label: "Manual", hint: manualHint },
@@ -295,7 +295,7 @@ export async function runSetupWizard(
} catch (error) {
await prompter.note(
[
"Could not resolve gateway.auth.token SecretRef for onboarding probe.",
"Could not resolve gateway.auth.token SecretRef for setup probe.",
error instanceof Error ? error.message : String(error),
].join("\n"),
"Gateway auth",
@@ -316,7 +316,7 @@ export async function runSetupWizard(
} catch (error) {
await prompter.note(
[
"Could not resolve gateway.auth.password SecretRef for onboarding probe.",
"Could not resolve gateway.auth.password SecretRef for setup probe.",
error instanceof Error ? error.message : String(error),
].join("\n"),
"Gateway auth",
@@ -343,7 +343,7 @@ export async function runSetupWizard(
} catch (error) {
await prompter.note(
[
"Could not resolve gateway.remote.token SecretRef for onboarding probe.",
"Could not resolve gateway.remote.token SecretRef for setup probe.",
error instanceof Error ? error.message : String(error),
].join("\n"),
"Gateway auth",

View File

@@ -166,7 +166,7 @@ export const en: TranslationMap = {
hideCronSessions: "Hide cron sessions",
showCronSessions: "Show cron sessions",
showCronSessionsHidden: "Show cron sessions ({count} hidden)",
onboardingDisabled: "Disabled during onboarding",
onboardingDisabled: "Disabled during setup",
},
languages: {
en: "English",