mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-28 02:21:15 +00:00
feat: add ClickClack setup wizard
This commit is contained in:
10
extensions/clickclack/setup-entry.ts
Normal file
10
extensions/clickclack/setup-entry.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// ClickClack plugin module implements its setup-only bundled entry.
|
||||
import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entry-contract";
|
||||
|
||||
export default defineBundledChannelSetupEntry({
|
||||
importMetaUrl: import.meta.url,
|
||||
plugin: {
|
||||
specifier: "./setup-plugin-api.js",
|
||||
exportName: "clickClackSetupPlugin",
|
||||
},
|
||||
});
|
||||
2
extensions/clickclack/setup-plugin-api.ts
Normal file
2
extensions/clickclack/setup-plugin-api.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// Keep ClickClack setup loads isolated from the full channel runtime surface.
|
||||
export { clickClackSetupPlugin } from "./src/channel.setup.js";
|
||||
27
extensions/clickclack/src/channel-config.ts
Normal file
27
extensions/clickclack/src/channel-config.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// ClickClack plugin module shares channel metadata and account config behavior.
|
||||
import type { ChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
||||
import { getChatChannelMeta } from "openclaw/plugin-sdk/channel-plugin-common";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
listClickClackAccountIds,
|
||||
resolveClickClackAccount,
|
||||
resolveDefaultClickClackAccountId,
|
||||
} from "./accounts.js";
|
||||
import type { CoreConfig, ResolvedClickClackAccount } from "./types.js";
|
||||
|
||||
export const CLICKCLACK_CHANNEL_ID = "clickclack" as const;
|
||||
export const clickClackMeta = { ...getChatChannelMeta(CLICKCLACK_CHANNEL_ID) };
|
||||
|
||||
export const clickClackConfigAdapter = {
|
||||
listAccountIds: (cfg) => listClickClackAccountIds(cfg as CoreConfig),
|
||||
resolveAccount: (cfg, accountId) =>
|
||||
resolveClickClackAccount({ cfg: cfg as CoreConfig, accountId }),
|
||||
defaultAccountId: (cfg) => resolveDefaultClickClackAccountId(cfg as CoreConfig),
|
||||
isConfigured: (account) => account.configured,
|
||||
resolveAllowFrom: ({ cfg, accountId }) =>
|
||||
resolveClickClackAccount({ cfg: cfg as CoreConfig, accountId }).allowFrom,
|
||||
resolveDefaultTo: ({ cfg, accountId }) =>
|
||||
resolveClickClackAccount({ cfg: cfg as CoreConfig, accountId }).defaultTo,
|
||||
} satisfies ChannelPlugin<ResolvedClickClackAccount>["config"];
|
||||
|
||||
export { DEFAULT_ACCOUNT_ID };
|
||||
22
extensions/clickclack/src/channel.setup.ts
Normal file
22
extensions/clickclack/src/channel.setup.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// ClickClack plugin module exposes a setup-only channel surface.
|
||||
import type { ChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
||||
import { clickClackConfigAdapter, clickClackMeta } from "./channel-config.js";
|
||||
import { clickClackConfigSchema } from "./config-schema.js";
|
||||
import { clickClackSetupAdapter } from "./setup-core.js";
|
||||
import { clickClackSetupWizard } from "./setup-surface.js";
|
||||
import type { ResolvedClickClackAccount } from "./types.js";
|
||||
|
||||
export const clickClackSetupPlugin: ChannelPlugin<ResolvedClickClackAccount> = {
|
||||
id: "clickclack",
|
||||
meta: clickClackMeta,
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group"],
|
||||
threads: true,
|
||||
blockStreaming: true,
|
||||
},
|
||||
reload: { configPrefixes: ["channels.clickclack"] },
|
||||
configSchema: clickClackConfigSchema,
|
||||
config: clickClackConfigAdapter,
|
||||
setup: clickClackSetupAdapter,
|
||||
setupWizard: clickClackSetupWizard,
|
||||
};
|
||||
@@ -12,17 +12,17 @@ import {
|
||||
createMessageReceiptFromOutboundResults,
|
||||
defineChannelMessageAdapter,
|
||||
} from "openclaw/plugin-sdk/channel-outbound";
|
||||
import { getChatChannelMeta } from "openclaw/plugin-sdk/channel-plugin-common";
|
||||
import {
|
||||
createComputedAccountStatusAdapter,
|
||||
createDefaultChannelRuntimeState,
|
||||
} from "openclaw/plugin-sdk/status-helpers";
|
||||
import { resolveClickClackAccount } from "./accounts.js";
|
||||
import {
|
||||
CLICKCLACK_CHANNEL_ID,
|
||||
clickClackConfigAdapter,
|
||||
clickClackMeta,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
listClickClackAccountIds,
|
||||
resolveClickClackAccount,
|
||||
resolveDefaultClickClackAccountId,
|
||||
} from "./accounts.js";
|
||||
} from "./channel-config.js";
|
||||
import { clickClackConfigSchema } from "./config-schema.js";
|
||||
import { startClickClackGatewayAccount } from "./gateway.js";
|
||||
import {
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
sendClickClackMedia,
|
||||
sendClickClackText,
|
||||
} from "./outbound.js";
|
||||
import { clickClackSetupAdapter } from "./setup-core.js";
|
||||
import { clickClackSetupWizard } from "./setup-surface.js";
|
||||
import {
|
||||
buildClickClackTarget,
|
||||
looksLikeClickClackTarget,
|
||||
@@ -38,8 +40,7 @@ import {
|
||||
} from "./target.js";
|
||||
import type { CoreConfig, ResolvedClickClackAccount } from "./types.js";
|
||||
|
||||
const CHANNEL_ID = "clickclack" as const;
|
||||
const meta = { ...getChatChannelMeta(CHANNEL_ID) };
|
||||
const CHANNEL_ID = CLICKCLACK_CHANNEL_ID;
|
||||
|
||||
const clickClackMessageAdapter = defineChannelMessageAdapter({
|
||||
id: CHANNEL_ID,
|
||||
@@ -117,7 +118,7 @@ const clickClackMessageAdapter = defineChannelMessageAdapter({
|
||||
export const clickClackPlugin: ChannelPlugin<ResolvedClickClackAccount> = createChatChannelPlugin({
|
||||
base: {
|
||||
id: CHANNEL_ID,
|
||||
meta,
|
||||
meta: clickClackMeta,
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group"],
|
||||
threads: true,
|
||||
@@ -125,17 +126,9 @@ export const clickClackPlugin: ChannelPlugin<ResolvedClickClackAccount> = create
|
||||
},
|
||||
reload: { configPrefixes: ["channels.clickclack"] },
|
||||
configSchema: clickClackConfigSchema,
|
||||
config: {
|
||||
listAccountIds: (cfg) => listClickClackAccountIds(cfg as CoreConfig),
|
||||
resolveAccount: (cfg, accountId) =>
|
||||
resolveClickClackAccount({ cfg: cfg as CoreConfig, accountId }),
|
||||
defaultAccountId: (cfg) => resolveDefaultClickClackAccountId(cfg as CoreConfig),
|
||||
isConfigured: (account) => account.configured,
|
||||
resolveAllowFrom: ({ cfg, accountId }) =>
|
||||
resolveClickClackAccount({ cfg: cfg as CoreConfig, accountId }).allowFrom,
|
||||
resolveDefaultTo: ({ cfg, accountId }) =>
|
||||
resolveClickClackAccount({ cfg: cfg as CoreConfig, accountId }).defaultTo,
|
||||
},
|
||||
config: clickClackConfigAdapter,
|
||||
setup: clickClackSetupAdapter,
|
||||
setupWizard: clickClackSetupWizard,
|
||||
messaging: {
|
||||
targetPrefixes: ["clickclack", "cc"],
|
||||
normalizeTarget: normalizeClickClackTarget,
|
||||
|
||||
256
extensions/clickclack/src/setup-surface.test.ts
Normal file
256
extensions/clickclack/src/setup-surface.test.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
// ClickClack tests cover guided setup prompts and nonfatal live validation.
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
createPluginSetupWizardConfigure,
|
||||
createQueuedWizardPrompter,
|
||||
createTestWizardPrompter,
|
||||
runSetupWizardConfigure,
|
||||
runSetupWizardFinalize,
|
||||
} from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
me: vi.fn(),
|
||||
resolveWorkspaceId: vi.fn(),
|
||||
workspaces: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./http-client.js", () => ({
|
||||
createClickClackClient: () => ({
|
||||
me: mocks.me,
|
||||
workspaces: mocks.workspaces,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("./resolve.js", () => ({
|
||||
resolveWorkspaceId: mocks.resolveWorkspaceId,
|
||||
}));
|
||||
|
||||
import { clickClackSetupPlugin } from "./channel.setup.js";
|
||||
import { clickClackSetupWizard } from "./setup-surface.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
const configuredAccount = {
|
||||
channels: {
|
||||
clickclack: {
|
||||
baseUrl: "https://clickclack.example",
|
||||
token: "ccb_test",
|
||||
workspace: "default",
|
||||
},
|
||||
},
|
||||
} satisfies CoreConfig;
|
||||
|
||||
function tokenCredential() {
|
||||
const credential = clickClackSetupWizard.credentials[0];
|
||||
if (!credential) {
|
||||
throw new Error("expected ClickClack token credential");
|
||||
}
|
||||
return credential;
|
||||
}
|
||||
|
||||
describe("ClickClack setup wizard", () => {
|
||||
beforeEach(() => {
|
||||
mocks.me.mockReset();
|
||||
mocks.resolveWorkspaceId.mockReset();
|
||||
mocks.workspaces.mockReset();
|
||||
mocks.me.mockResolvedValue({ id: "usr_bot", handle: "openclaw" });
|
||||
mocks.resolveWorkspaceId.mockResolvedValue("wsp_default");
|
||||
mocks.workspaces.mockResolvedValue([
|
||||
{ id: "wsp_default", name: "Default", slug: "default", created_at: "2026-01-01" },
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("detects configured accounts and inspects plain, env, and file credentials", async () => {
|
||||
expect(await clickClackSetupWizard.status.resolveConfigured({ cfg: configuredAccount })).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
const credential = tokenCredential();
|
||||
expect(
|
||||
credential.inspect({
|
||||
cfg: configuredAccount,
|
||||
accountId: "default",
|
||||
}),
|
||||
).toMatchObject({
|
||||
accountConfigured: true,
|
||||
hasConfiguredValue: true,
|
||||
resolvedValue: "ccb_test",
|
||||
});
|
||||
|
||||
vi.stubEnv("CLICKCLACK_BOT_TOKEN", "ccb_env");
|
||||
expect(
|
||||
credential.inspect({
|
||||
cfg: {
|
||||
channels: {
|
||||
clickclack: {
|
||||
baseUrl: "https://clickclack.example",
|
||||
workspace: "default",
|
||||
},
|
||||
},
|
||||
} as CoreConfig,
|
||||
accountId: "default",
|
||||
}),
|
||||
).toMatchObject({
|
||||
accountConfigured: true,
|
||||
hasConfiguredValue: false,
|
||||
resolvedValue: "ccb_env",
|
||||
envValue: "ccb_env",
|
||||
});
|
||||
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "clickclack-setup-token-"));
|
||||
const tokenFile = path.join(tempDir, "token");
|
||||
fs.writeFileSync(tokenFile, "ccb_file\n", "utf8");
|
||||
try {
|
||||
expect(
|
||||
credential.inspect({
|
||||
cfg: {
|
||||
channels: {
|
||||
clickclack: {
|
||||
baseUrl: "https://clickclack.example",
|
||||
tokenFile,
|
||||
workspace: "default",
|
||||
},
|
||||
},
|
||||
} as CoreConfig,
|
||||
accountId: "default",
|
||||
}),
|
||||
).toMatchObject({
|
||||
accountConfigured: true,
|
||||
hasConfiguredValue: true,
|
||||
resolvedValue: "ccb_file",
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("switches the default account to env auth before URL and workspace prompts", async () => {
|
||||
const credential = tokenCredential();
|
||||
const next = await credential.applyUseEnv?.({
|
||||
cfg: {
|
||||
channels: {
|
||||
clickclack: {
|
||||
baseUrl: "https://clickclack.example",
|
||||
token: "ccb_stale",
|
||||
tokenFile: "/run/secrets/stale",
|
||||
workspace: "default",
|
||||
},
|
||||
},
|
||||
} as CoreConfig,
|
||||
accountId: "default",
|
||||
});
|
||||
|
||||
expect(next?.channels?.clickclack).not.toHaveProperty("token");
|
||||
expect(next?.channels?.clickclack).not.toHaveProperty("tokenFile");
|
||||
expect(next?.channels?.clickclack).toMatchObject({
|
||||
baseUrl: "https://clickclack.example",
|
||||
workspace: "default",
|
||||
});
|
||||
});
|
||||
|
||||
it("saves URL, token, and workspace through the guided flow", async () => {
|
||||
const queued = createQueuedWizardPrompter({
|
||||
textValues: ["ccb_guided", "https://clickclack.example/", "default"],
|
||||
});
|
||||
|
||||
const result = await runSetupWizardConfigure({
|
||||
configure: createPluginSetupWizardConfigure(clickClackSetupPlugin),
|
||||
cfg: {} as CoreConfig,
|
||||
prompter: queued.prompter,
|
||||
options: { secretInputMode: "plaintext" as const },
|
||||
});
|
||||
|
||||
expect(result.cfg.channels?.clickclack).toMatchObject({
|
||||
enabled: true,
|
||||
token: "ccb_guided",
|
||||
baseUrl: "https://clickclack.example",
|
||||
workspace: "default",
|
||||
});
|
||||
expect(mocks.me).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.resolveWorkspaceId).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("reports the resolved bot and workspace after live validation", async () => {
|
||||
const note = vi.fn(async () => undefined);
|
||||
|
||||
await runSetupWizardFinalize({
|
||||
finalize: clickClackSetupWizard.finalize,
|
||||
cfg: configuredAccount,
|
||||
prompter: createTestWizardPrompter({ note }),
|
||||
});
|
||||
|
||||
expect(mocks.me.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
mocks.resolveWorkspaceId.mock.invocationCallOrder[0] ?? Number.POSITIVE_INFINITY,
|
||||
);
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
"Connected as @openclaw — workspace Default resolved.",
|
||||
"ClickClack connection",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps setup saved when the token is rejected", async () => {
|
||||
mocks.me.mockRejectedValue({ status: 401 });
|
||||
const note = vi.fn(async () => undefined);
|
||||
|
||||
await expect(
|
||||
runSetupWizardFinalize({
|
||||
finalize: clickClackSetupWizard.finalize,
|
||||
cfg: configuredAccount,
|
||||
prompter: createTestWizardPrompter({ note }),
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
"ClickClack rejected the bot token (401). Copy a current token and rerun setup.",
|
||||
"ClickClack connection check",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps setup saved when workspace resolution fails", async () => {
|
||||
mocks.resolveWorkspaceId.mockRejectedValue(
|
||||
new Error("ClickClack workspace not found: missing"),
|
||||
);
|
||||
const note = vi.fn(async () => undefined);
|
||||
|
||||
await expect(
|
||||
runSetupWizardFinalize({
|
||||
finalize: clickClackSetupWizard.finalize,
|
||||
cfg: {
|
||||
channels: {
|
||||
clickclack: {
|
||||
...configuredAccount.channels.clickclack,
|
||||
workspace: "missing",
|
||||
},
|
||||
},
|
||||
},
|
||||
prompter: createTestWizardPrompter({ note }),
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
'Workspace "missing" was not found. Check the id, slug, or name, list available workspaces, and rerun setup.',
|
||||
"ClickClack connection check",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps setup saved when the server is unreachable", async () => {
|
||||
mocks.me.mockRejectedValue(new Error("network unavailable"));
|
||||
const note = vi.fn(async () => undefined);
|
||||
|
||||
await expect(
|
||||
runSetupWizardFinalize({
|
||||
finalize: clickClackSetupWizard.finalize,
|
||||
cfg: configuredAccount,
|
||||
prompter: createTestWizardPrompter({ note }),
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
"Connection check failed: network unavailable. Setup was saved; fix the connection and rerun setup.",
|
||||
"ClickClack connection check",
|
||||
);
|
||||
});
|
||||
});
|
||||
184
extensions/clickclack/src/setup-surface.ts
Normal file
184
extensions/clickclack/src/setup-surface.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
// ClickClack plugin module implements guided setup behavior.
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import {
|
||||
createStandardChannelSetupStatus,
|
||||
createSetupTranslator,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
hasConfiguredSecretInput,
|
||||
setSetupChannelEnabled,
|
||||
type ChannelSetupWizard,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import { listClickClackAccountIds, resolveClickClackAccount } from "./accounts.js";
|
||||
import { createClickClackClient } from "./http-client.js";
|
||||
import { resolveWorkspaceId } from "./resolve.js";
|
||||
import {
|
||||
applyClickClackCredentialConfig,
|
||||
applyClickClackSetupConfigPatch,
|
||||
normalizeClickClackBaseUrl,
|
||||
} from "./setup-core.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
const t = createSetupTranslator();
|
||||
const channel = "clickclack" as const;
|
||||
|
||||
function isHttpStatus(error: unknown, status: number): boolean {
|
||||
return (
|
||||
typeof error === "object" &&
|
||||
error !== null &&
|
||||
"status" in error &&
|
||||
(error as { status?: unknown }).status === status
|
||||
);
|
||||
}
|
||||
|
||||
function isWorkspaceNotFound(error: unknown): boolean {
|
||||
return error instanceof Error && error.message.startsWith("ClickClack workspace not found:");
|
||||
}
|
||||
|
||||
export const clickClackSetupWizard: ChannelSetupWizard = {
|
||||
channel,
|
||||
status: createStandardChannelSetupStatus({
|
||||
channelLabel: "ClickClack",
|
||||
configuredLabel: t("wizard.channels.statusConfigured"),
|
||||
unconfiguredLabel: t("wizard.channels.statusNeedsSetup"),
|
||||
configuredHint: t("wizard.channels.statusSelfHostedChat"),
|
||||
unconfiguredHint: t("wizard.channels.statusNeedsSetup"),
|
||||
configuredScore: 2,
|
||||
unconfiguredScore: 1,
|
||||
resolveConfigured: ({ cfg, accountId }) =>
|
||||
(accountId ? [accountId] : listClickClackAccountIds(cfg as CoreConfig)).some(
|
||||
(resolvedAccountId) =>
|
||||
resolveClickClackAccount({
|
||||
cfg: cfg as CoreConfig,
|
||||
accountId: resolvedAccountId,
|
||||
}).configured,
|
||||
),
|
||||
}),
|
||||
introNote: {
|
||||
title: t("wizard.clickclack.botTokenTitle"),
|
||||
lines: [
|
||||
t("wizard.clickclack.helpCreateToken"),
|
||||
t("wizard.channels.docs", {
|
||||
link: formatDocsLink("/channels/clickclack", "clickclack"),
|
||||
}),
|
||||
],
|
||||
shouldShow: ({ cfg, accountId }) =>
|
||||
!resolveClickClackAccount({
|
||||
cfg: cfg as CoreConfig,
|
||||
accountId,
|
||||
}).configured,
|
||||
},
|
||||
credentials: [
|
||||
{
|
||||
inputKey: "token",
|
||||
providerHint: channel,
|
||||
credentialLabel: t("wizard.clickclack.botToken"),
|
||||
preferredEnvVar: "CLICKCLACK_BOT_TOKEN",
|
||||
envPrompt: t("wizard.clickclack.envPrompt"),
|
||||
keepPrompt: t("wizard.clickclack.botTokenKeep"),
|
||||
inputPrompt: t("wizard.clickclack.botTokenInput"),
|
||||
allowEnv: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID,
|
||||
inspect: ({ cfg, accountId }) => {
|
||||
const resolved = resolveClickClackAccount({
|
||||
cfg: cfg as CoreConfig,
|
||||
accountId,
|
||||
});
|
||||
return {
|
||||
accountConfigured: resolved.configured,
|
||||
hasConfiguredValue:
|
||||
hasConfiguredSecretInput(resolved.config.token) ||
|
||||
Boolean(resolved.config.tokenFile?.trim()),
|
||||
resolvedValue: resolved.token || undefined,
|
||||
envValue:
|
||||
accountId === DEFAULT_ACCOUNT_ID
|
||||
? process.env.CLICKCLACK_BOT_TOKEN?.trim() || undefined
|
||||
: undefined,
|
||||
};
|
||||
},
|
||||
applyUseEnv: async ({ cfg, accountId }) =>
|
||||
applyClickClackCredentialConfig({
|
||||
cfg,
|
||||
accountId,
|
||||
useEnv: true,
|
||||
}),
|
||||
applySet: async ({ cfg, accountId, value }) =>
|
||||
applyClickClackCredentialConfig({
|
||||
cfg,
|
||||
accountId,
|
||||
token: value,
|
||||
}),
|
||||
},
|
||||
],
|
||||
textInputs: [
|
||||
{
|
||||
inputKey: "baseUrl",
|
||||
message: t("wizard.clickclack.baseUrlPrompt"),
|
||||
currentValue: ({ cfg, accountId }) =>
|
||||
resolveClickClackAccount({ cfg: cfg as CoreConfig, accountId }).baseUrl || undefined,
|
||||
initialValue: ({ cfg, accountId }) =>
|
||||
resolveClickClackAccount({ cfg: cfg as CoreConfig, accountId }).baseUrl || undefined,
|
||||
validate: ({ value }) =>
|
||||
normalizeClickClackBaseUrl(value)
|
||||
? undefined
|
||||
: "ClickClack server URL must be a valid http(s) URL.",
|
||||
normalizeValue: ({ value }) => normalizeClickClackBaseUrl(value) ?? value.trim(),
|
||||
applySet: async ({ cfg, accountId, value }) =>
|
||||
applyClickClackSetupConfigPatch({
|
||||
cfg,
|
||||
accountId,
|
||||
patch: { baseUrl: value },
|
||||
}),
|
||||
},
|
||||
{
|
||||
inputKey: "workspace",
|
||||
message: t("wizard.clickclack.workspacePrompt"),
|
||||
helpTitle: t("wizard.clickclack.workspacePrompt"),
|
||||
helpLines: [t("wizard.clickclack.workspaceHelp")],
|
||||
currentValue: ({ cfg, accountId }) =>
|
||||
resolveClickClackAccount({ cfg: cfg as CoreConfig, accountId }).workspace || undefined,
|
||||
initialValue: ({ cfg, accountId }) =>
|
||||
resolveClickClackAccount({ cfg: cfg as CoreConfig, accountId }).workspace || undefined,
|
||||
validate: ({ value }) => (value.trim() ? undefined : "Required"),
|
||||
normalizeValue: ({ value }) => value.trim(),
|
||||
applySet: async ({ cfg, accountId, value }) =>
|
||||
applyClickClackSetupConfigPatch({
|
||||
cfg,
|
||||
accountId,
|
||||
patch: { workspace: value },
|
||||
}),
|
||||
},
|
||||
],
|
||||
finalize: async ({ cfg, accountId, prompter }) => {
|
||||
const account = resolveClickClackAccount({
|
||||
cfg: cfg as CoreConfig,
|
||||
accountId,
|
||||
});
|
||||
try {
|
||||
const client = createClickClackClient({
|
||||
baseUrl: account.baseUrl,
|
||||
token: account.token,
|
||||
});
|
||||
const me = await client.me();
|
||||
const workspaceId = await resolveWorkspaceId(client, account.workspace);
|
||||
const workspaces = await client.workspaces();
|
||||
const workspace = workspaces.find((candidate) => candidate.id === workspaceId);
|
||||
await prompter.note(
|
||||
t("wizard.clickclack.connected", {
|
||||
handle: me.handle,
|
||||
workspace: workspace?.name ?? workspaceId,
|
||||
}),
|
||||
t("wizard.clickclack.connectionTitle"),
|
||||
);
|
||||
} catch (error) {
|
||||
const message = isHttpStatus(error, 401)
|
||||
? t("wizard.clickclack.invalidToken")
|
||||
: isWorkspaceNotFound(error)
|
||||
? t("wizard.clickclack.workspaceNotFound", { workspace: account.workspace })
|
||||
: t("wizard.clickclack.connectionFailed", {
|
||||
error: formatErrorMessage(error),
|
||||
});
|
||||
await prompter.note(message, t("wizard.clickclack.validationWarningTitle"));
|
||||
}
|
||||
},
|
||||
disable: (cfg) => setSetupChannelEnabled(cfg, channel, false),
|
||||
};
|
||||
Reference in New Issue
Block a user