mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-16 12:30:49 +00:00
refactor: make setup the primary wizard surface
This commit is contained in:
@@ -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>;
|
||||
|
||||
@@ -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", () => ({
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
|
||||
@@ -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).`,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user