From dd85ff4da75a5f047a2fa85ff8ac4b1178a42fe0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 17 Mar 2026 04:18:35 +0000 Subject: [PATCH] refactor(tlon): share setup wizard base --- extensions/tlon/src/channel.ts | 100 ++++------------------- extensions/tlon/src/setup-core.ts | 116 ++++++++++++++++++++++++++- extensions/tlon/src/setup-surface.ts | 103 +++--------------------- 3 files changed, 144 insertions(+), 175 deletions(-) diff --git a/extensions/tlon/src/channel.ts b/extensions/tlon/src/channel.ts index 4442279a727..fa7c702354d 100644 --- a/extensions/tlon/src/channel.ts +++ b/extensions/tlon/src/channel.ts @@ -1,7 +1,11 @@ import type { ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk/tlon"; import { tlonChannelConfigSchema } from "./config-schema.js"; -import { tlonSetupAdapter } from "./setup-core.js"; -import { applyTlonSetupConfig } from "./setup-core.js"; +import { + applyTlonSetupConfig, + createTlonSetupWizardBase, + resolveTlonSetupConfigured, + tlonSetupAdapter, +} from "./setup-core.js"; import { formatTargetHint, normalizeShip, parseTlonTarget } from "./targets.js"; import { resolveTlonAccount, listTlonAccountIds } from "./types.js"; import { validateUrbitBaseUrl } from "./urbit/base-url.js"; @@ -15,91 +19,21 @@ async function loadTlonChannelRuntime() { return tlonChannelRuntimePromise; } -const tlonSetupWizardProxy = { - channel: "tlon", - status: { - configuredLabel: "configured", - unconfiguredLabel: "needs setup", - configuredHint: "configured", - unconfiguredHint: "urbit messenger", - configuredScore: 1, - unconfiguredScore: 4, - resolveConfigured: async ({ cfg }) => - await (await loadTlonChannelRuntime()).tlonSetupWizard.status.resolveConfigured({ cfg }), - resolveStatusLines: async ({ cfg, configured }) => - (await ( - await loadTlonChannelRuntime() - ).tlonSetupWizard.status.resolveStatusLines?.({ - cfg, - configured, - })) ?? [], - }, - introNote: { - title: "Tlon setup", - lines: [ - "You need your Urbit ship URL and login code.", - "Example URL: https://your-ship-host", - "Example ship: ~sampel-palnet", - "If your ship URL is on a private network (LAN/localhost), you must explicitly allow it during setup.", - "Docs: https://docs.openclaw.ai/channels/tlon", - ], - }, - credentials: [], - textInputs: [ - { - inputKey: "ship", - message: "Ship name", - placeholder: "~sampel-palnet", - currentValue: ({ cfg, accountId }) => resolveTlonAccount(cfg, accountId).ship ?? undefined, - validate: ({ value }) => (String(value ?? "").trim() ? undefined : "Required"), - normalizeValue: ({ value }) => normalizeShip(String(value).trim()), - applySet: async ({ cfg, accountId, value }) => - applyTlonSetupConfig({ - cfg, - accountId, - input: { ship: value }, - }), - }, - { - inputKey: "url", - message: "Ship URL", - placeholder: "https://your-ship-host", - currentValue: ({ cfg, accountId }) => resolveTlonAccount(cfg, accountId).url ?? undefined, - validate: ({ value }) => { - const next = validateUrbitBaseUrl(String(value ?? "")); - if (!next.ok) { - return next.error; - } - return undefined; - }, - normalizeValue: ({ value }) => String(value).trim(), - applySet: async ({ cfg, accountId, value }) => - applyTlonSetupConfig({ - cfg, - accountId, - input: { url: value }, - }), - }, - { - inputKey: "code", - message: "Login code", - placeholder: "lidlut-tabwed-pillex-ridrup", - currentValue: ({ cfg, accountId }) => resolveTlonAccount(cfg, accountId).code ?? undefined, - validate: ({ value }) => (String(value ?? "").trim() ? undefined : "Required"), - normalizeValue: ({ value }) => String(value).trim(), - applySet: async ({ cfg, accountId, value }) => - applyTlonSetupConfig({ - cfg, - accountId, - input: { code: value }, - }), - }, - ], +const tlonSetupWizardProxy = createTlonSetupWizardBase({ + resolveConfigured: async ({ cfg }) => + await (await loadTlonChannelRuntime()).tlonSetupWizard.status.resolveConfigured({ cfg }), + resolveStatusLines: async ({ cfg, configured }) => + (await ( + await loadTlonChannelRuntime() + ).tlonSetupWizard.status.resolveStatusLines?.({ + cfg, + configured, + })) ?? [], finalize: async (params) => await ( await loadTlonChannelRuntime() ).tlonSetupWizard.finalize!(params), -} satisfies NonNullable; +}) satisfies NonNullable; export const tlonPlugin: ChannelPlugin = { id: TLON_CHANNEL_ID, diff --git a/extensions/tlon/src/setup-core.ts b/extensions/tlon/src/setup-core.ts index 846af4f08a3..8d54e37444a 100644 --- a/extensions/tlon/src/setup-core.ts +++ b/extensions/tlon/src/setup-core.ts @@ -1,14 +1,19 @@ import { DEFAULT_ACCOUNT_ID, + formatDocsLink, normalizeAccountId, patchScopedAccountConfig, prepareScopedSetupConfig, type ChannelSetupAdapter, type ChannelSetupInput, + type ChannelSetupWizard, type OpenClawConfig, + type WizardPrompter, } from "openclaw/plugin-sdk/setup"; import { buildTlonAccountFields } from "./account-fields.js"; -import { resolveTlonAccount } from "./types.js"; +import { normalizeShip } from "./targets.js"; +import { listTlonAccountIds, resolveTlonAccount, type TlonResolvedAccount } from "./types.js"; +import { validateUrbitBaseUrl } from "./urbit/base-url.js"; const channel = "tlon" as const; @@ -23,6 +28,115 @@ export type TlonSetupInput = ChannelSetupInput & { ownerShip?: string; }; +function isConfigured(account: TlonResolvedAccount): boolean { + return Boolean(account.ship && account.url && account.code); +} + +type TlonSetupWizardBaseParams = { + resolveConfigured: (params: { cfg: OpenClawConfig }) => boolean | Promise; + resolveStatusLines?: (params: { + cfg: OpenClawConfig; + configured: boolean; + }) => string[] | Promise; + finalize: (params: { + cfg: OpenClawConfig; + accountId: string; + prompter: WizardPrompter; + options?: Record; + }) => Promise<{ cfg: OpenClawConfig }>; +}; + +export function createTlonSetupWizardBase(params: TlonSetupWizardBaseParams): ChannelSetupWizard { + return { + channel, + status: { + configuredLabel: "configured", + unconfiguredLabel: "needs setup", + configuredHint: "configured", + unconfiguredHint: "urbit messenger", + configuredScore: 1, + unconfiguredScore: 4, + resolveConfigured: ({ cfg }) => params.resolveConfigured({ cfg }), + resolveStatusLines: ({ cfg, configured }) => + params.resolveStatusLines?.({ cfg, configured }) ?? [], + }, + introNote: { + title: "Tlon setup", + lines: [ + "You need your Urbit ship URL and login code.", + "Example URL: https://your-ship-host", + "Example ship: ~sampel-palnet", + "If your ship URL is on a private network (LAN/localhost), you must explicitly allow it during setup.", + `Docs: ${formatDocsLink("/channels/tlon", "channels/tlon")}`, + ], + }, + credentials: [], + textInputs: [ + { + inputKey: "ship", + message: "Ship name", + placeholder: "~sampel-palnet", + currentValue: ({ cfg, accountId }) => resolveTlonAccount(cfg, accountId).ship ?? undefined, + validate: ({ value }) => (String(value ?? "").trim() ? undefined : "Required"), + normalizeValue: ({ value }) => normalizeShip(String(value).trim()), + applySet: async ({ cfg, accountId, value }) => + applyTlonSetupConfig({ + cfg, + accountId, + input: { ship: value }, + }), + }, + { + inputKey: "url", + message: "Ship URL", + placeholder: "https://your-ship-host", + currentValue: ({ cfg, accountId }) => resolveTlonAccount(cfg, accountId).url ?? undefined, + validate: ({ value }) => { + const next = validateUrbitBaseUrl(String(value ?? "")); + if (!next.ok) { + return next.error; + } + return undefined; + }, + normalizeValue: ({ value }) => String(value).trim(), + applySet: async ({ cfg, accountId, value }) => + applyTlonSetupConfig({ + cfg, + accountId, + input: { url: value }, + }), + }, + { + inputKey: "code", + message: "Login code", + placeholder: "lidlut-tabwed-pillex-ridrup", + currentValue: ({ cfg, accountId }) => resolveTlonAccount(cfg, accountId).code ?? undefined, + validate: ({ value }) => (String(value ?? "").trim() ? undefined : "Required"), + normalizeValue: ({ value }) => String(value).trim(), + applySet: async ({ cfg, accountId, value }) => + applyTlonSetupConfig({ + cfg, + accountId, + input: { code: value }, + }), + }, + ], + finalize: params.finalize, + }; +} + +export async function resolveTlonSetupConfigured(cfg: OpenClawConfig): Promise { + const accountIds = listTlonAccountIds(cfg); + return accountIds.length > 0 + ? accountIds.some((accountId) => isConfigured(resolveTlonAccount(cfg, accountId))) + : isConfigured(resolveTlonAccount(cfg, DEFAULT_ACCOUNT_ID)); +} + +export async function resolveTlonSetupStatusLines(cfg: OpenClawConfig): Promise { + const configured = await resolveTlonSetupConfigured(cfg); + return [`Tlon: ${configured ? "configured" : "needs setup"}`]; +} + export function applyTlonSetupConfig(params: { cfg: OpenClawConfig; accountId: string; diff --git a/extensions/tlon/src/setup-surface.ts b/extensions/tlon/src/setup-surface.ts index e3c1b43f0c1..bf4ce6fbf2e 100644 --- a/extensions/tlon/src/setup-surface.ts +++ b/extensions/tlon/src/setup-surface.ts @@ -1,9 +1,12 @@ +import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup"; import { - DEFAULT_ACCOUNT_ID, - formatDocsLink, - type ChannelSetupWizard, -} from "openclaw/plugin-sdk/setup"; -import { applyTlonSetupConfig, type TlonSetupInput, tlonSetupAdapter } from "./setup-core.js"; + applyTlonSetupConfig, + createTlonSetupWizardBase, + resolveTlonSetupConfigured, + resolveTlonSetupStatusLines, + type TlonSetupInput, + tlonSetupAdapter, +} from "./setup-core.js"; import { normalizeShip } from "./targets.js"; import { listTlonAccountIds, resolveTlonAccount, type TlonResolvedAccount } from "./types.js"; import { isBlockedUrbitHostname, validateUrbitBaseUrl } from "./urbit/base-url.js"; @@ -23,91 +26,9 @@ function parseList(value: string): string[] { export { tlonSetupAdapter } from "./setup-core.js"; -export const tlonSetupWizard: ChannelSetupWizard = { - channel, - status: { - configuredLabel: "configured", - unconfiguredLabel: "needs setup", - configuredHint: "configured", - unconfiguredHint: "urbit messenger", - configuredScore: 1, - unconfiguredScore: 4, - resolveConfigured: ({ cfg }) => { - const accountIds = listTlonAccountIds(cfg); - return accountIds.length > 0 - ? accountIds.some((accountId) => isConfigured(resolveTlonAccount(cfg, accountId))) - : isConfigured(resolveTlonAccount(cfg, DEFAULT_ACCOUNT_ID)); - }, - resolveStatusLines: ({ cfg }) => { - const accountIds = listTlonAccountIds(cfg); - const configured = - accountIds.length > 0 - ? accountIds.some((accountId) => isConfigured(resolveTlonAccount(cfg, accountId))) - : isConfigured(resolveTlonAccount(cfg, DEFAULT_ACCOUNT_ID)); - return [`Tlon: ${configured ? "configured" : "needs setup"}`]; - }, - }, - introNote: { - title: "Tlon setup", - lines: [ - "You need your Urbit ship URL and login code.", - "Example URL: https://your-ship-host", - "Example ship: ~sampel-palnet", - "If your ship URL is on a private network (LAN/localhost), you must explicitly allow it during setup.", - `Docs: ${formatDocsLink("/channels/tlon", "channels/tlon")}`, - ], - }, - credentials: [], - textInputs: [ - { - inputKey: "ship", - message: "Ship name", - placeholder: "~sampel-palnet", - currentValue: ({ cfg, accountId }) => resolveTlonAccount(cfg, accountId).ship ?? undefined, - validate: ({ value }) => (String(value ?? "").trim() ? undefined : "Required"), - normalizeValue: ({ value }) => normalizeShip(String(value).trim()), - applySet: async ({ cfg, accountId, value }) => - applyTlonSetupConfig({ - cfg, - accountId, - input: { ship: value }, - }), - }, - { - inputKey: "url", - message: "Ship URL", - placeholder: "https://your-ship-host", - currentValue: ({ cfg, accountId }) => resolveTlonAccount(cfg, accountId).url ?? undefined, - validate: ({ value }) => { - const next = validateUrbitBaseUrl(String(value ?? "")); - if (!next.ok) { - return next.error; - } - return undefined; - }, - normalizeValue: ({ value }) => String(value).trim(), - applySet: async ({ cfg, accountId, value }) => - applyTlonSetupConfig({ - cfg, - accountId, - input: { url: value }, - }), - }, - { - inputKey: "code", - message: "Login code", - placeholder: "lidlut-tabwed-pillex-ridrup", - currentValue: ({ cfg, accountId }) => resolveTlonAccount(cfg, accountId).code ?? undefined, - validate: ({ value }) => (String(value ?? "").trim() ? undefined : "Required"), - normalizeValue: ({ value }) => String(value).trim(), - applySet: async ({ cfg, accountId, value }) => - applyTlonSetupConfig({ - cfg, - accountId, - input: { code: value }, - }), - }, - ], +export const tlonSetupWizard = createTlonSetupWizardBase({ + resolveConfigured: async ({ cfg }) => await resolveTlonSetupConfigured(cfg), + resolveStatusLines: async ({ cfg }) => await resolveTlonSetupStatusLines(cfg), finalize: async ({ cfg, accountId, prompter }) => { let next = cfg; const resolved = resolveTlonAccount(next, accountId); @@ -183,4 +104,4 @@ export const tlonSetupWizard: ChannelSetupWizard = { return { cfg: next }; }, -}; +});