refactor(tlon): share setup wizard base

This commit is contained in:
Peter Steinberger 2026-03-17 04:18:35 +00:00
parent d20363bcc9
commit dd85ff4da7
3 changed files with 144 additions and 175 deletions

View File

@ -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<ChannelPlugin["setupWizard"]>;
}) satisfies NonNullable<ChannelPlugin["setupWizard"]>;
export const tlonPlugin: ChannelPlugin = {
id: TLON_CHANNEL_ID,

View File

@ -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<boolean>;
resolveStatusLines?: (params: {
cfg: OpenClawConfig;
configured: boolean;
}) => string[] | Promise<string[]>;
finalize: (params: {
cfg: OpenClawConfig;
accountId: string;
prompter: WizardPrompter;
options?: Record<string, unknown>;
}) => 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<boolean> {
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<string[]> {
const configured = await resolveTlonSetupConfigured(cfg);
return [`Tlon: ${configured ? "configured" : "needs setup"}`];
}
export function applyTlonSetupConfig(params: {
cfg: OpenClawConfig;
accountId: string;

View File

@ -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 };
},
};
});