refactor: centralize provider helpers

This commit is contained in:
Peter Steinberger
2026-01-08 07:20:02 +01:00
parent 6a81652ebf
commit d14e05ac45
6 changed files with 446 additions and 389 deletions

View File

@@ -19,6 +19,7 @@ If you change the CLI code, update this doc.
## Output styling ## Output styling
- ANSI colors and progress indicators only render in TTY sessions. - ANSI colors and progress indicators only render in TTY sessions.
- OSC-8 hyperlinks render as clickable links in supported terminals; otherwise we fall back to plain URLs.
- `--json` (and `--plain` where supported) disables styling for clean output. - `--json` (and `--plain` where supported) disables styling for clean output.
- Long-running commands show a progress indicator (OSC 9;4 when supported). - Long-running commands show a progress indicator (OSC 9;4 when supported).
@@ -50,6 +51,10 @@ clawdbot [--dev] [--profile <name>] <command>
status status
add add
remove remove
skills
list
info
check
send send
poll poll
agent agent
@@ -237,6 +242,21 @@ clawdbot providers status --probe
clawdbot status --deep clawdbot status --deep
``` ```
### `skills`
List and inspect available skills plus readiness info.
Subcommands:
- `skills list`: list skills (default when no subcommand).
- `skills info <name>`: show details for one skill.
- `skills check`: summary of ready vs missing requirements.
Options:
- `--eligible`: show only ready skills.
- `--json`: output JSON (no styling).
- `-v`, `--verbose`: include missing requirements detail.
Tip: use `npx clawdhub` to search, install, and sync skills.
### `pairing` ### `pairing`
Approve DM pairing requests across providers. Approve DM pairing requests across providers.

View File

@@ -0,0 +1,344 @@
import type { ClawdbotConfig } from "../../config/config.js";
import type { ChatProviderId } from "../../providers/registry.js";
import {
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
} from "../../routing/session-key.js";
type ChatProvider = ChatProviderId;
function providerHasAccounts(cfg: ClawdbotConfig, provider: ChatProvider) {
if (provider === "whatsapp") return true;
const base = (cfg as Record<string, unknown>)[provider] as
| { accounts?: Record<string, unknown> }
| undefined;
return Boolean(base?.accounts && Object.keys(base.accounts).length > 0);
}
function shouldStoreNameInAccounts(
cfg: ClawdbotConfig,
provider: ChatProvider,
accountId: string,
): boolean {
if (provider === "whatsapp") return true;
if (accountId !== DEFAULT_ACCOUNT_ID) return true;
return providerHasAccounts(cfg, provider);
}
function migrateBaseNameToDefaultAccount(
cfg: ClawdbotConfig,
provider: ChatProvider,
): ClawdbotConfig {
if (provider === "whatsapp") return cfg;
const base = (cfg as Record<string, unknown>)[provider] as
| { name?: string; accounts?: Record<string, Record<string, unknown>> }
| undefined;
const baseName = base?.name?.trim();
if (!baseName) return cfg;
const accounts: Record<string, Record<string, unknown>> = {
...base?.accounts,
};
const defaultAccount = accounts[DEFAULT_ACCOUNT_ID] ?? {};
if (!defaultAccount.name) {
accounts[DEFAULT_ACCOUNT_ID] = { ...defaultAccount, name: baseName };
}
const { name: _ignored, ...rest } = base ?? {};
return {
...cfg,
[provider]: {
...rest,
accounts,
},
} as ClawdbotConfig;
}
export function applyAccountName(params: {
cfg: ClawdbotConfig;
provider: ChatProvider;
accountId: string;
name?: string;
}): ClawdbotConfig {
const trimmed = params.name?.trim();
if (!trimmed) return params.cfg;
const accountId = normalizeAccountId(params.accountId);
if (params.provider === "whatsapp") {
return {
...params.cfg,
whatsapp: {
...params.cfg.whatsapp,
accounts: {
...params.cfg.whatsapp?.accounts,
[accountId]: {
...params.cfg.whatsapp?.accounts?.[accountId],
name: trimmed,
},
},
},
};
}
const key = params.provider;
const useAccounts = shouldStoreNameInAccounts(params.cfg, key, accountId);
if (!useAccounts && accountId === DEFAULT_ACCOUNT_ID) {
const baseConfig = (params.cfg as Record<string, unknown>)[key];
const safeBase =
typeof baseConfig === "object" && baseConfig
? (baseConfig as Record<string, unknown>)
: {};
return {
...params.cfg,
[key]: {
...safeBase,
name: trimmed,
},
} as ClawdbotConfig;
}
const base = (params.cfg as Record<string, unknown>)[key] as
| { name?: string; accounts?: Record<string, Record<string, unknown>> }
| undefined;
const baseAccounts: Record<
string,
Record<string, unknown>
> = base?.accounts ?? {};
const existingAccount = baseAccounts[accountId] ?? {};
const baseWithoutName =
accountId === DEFAULT_ACCOUNT_ID
? (({ name: _ignored, ...rest }) => rest)(base ?? {})
: (base ?? {});
return {
...params.cfg,
[key]: {
...baseWithoutName,
accounts: {
...baseAccounts,
[accountId]: {
...existingAccount,
name: trimmed,
},
},
},
} as ClawdbotConfig;
}
export function applyProviderAccountConfig(params: {
cfg: ClawdbotConfig;
provider: ChatProvider;
accountId: string;
name?: string;
token?: string;
tokenFile?: string;
botToken?: string;
appToken?: string;
signalNumber?: string;
cliPath?: string;
dbPath?: string;
service?: "imessage" | "sms" | "auto";
region?: string;
authDir?: string;
httpUrl?: string;
httpHost?: string;
httpPort?: string;
useEnv?: boolean;
}): ClawdbotConfig {
const accountId = normalizeAccountId(params.accountId);
const name = params.name?.trim() || undefined;
const namedConfig = applyAccountName({
cfg: params.cfg,
provider: params.provider,
accountId,
name,
});
const next =
accountId !== DEFAULT_ACCOUNT_ID
? migrateBaseNameToDefaultAccount(namedConfig, params.provider)
: namedConfig;
if (params.provider === "whatsapp") {
const entry = {
...next.whatsapp?.accounts?.[accountId],
...(params.authDir ? { authDir: params.authDir } : {}),
enabled: true,
};
return {
...next,
whatsapp: {
...next.whatsapp,
accounts: {
...next.whatsapp?.accounts,
[accountId]: entry,
},
},
};
}
if (params.provider === "telegram") {
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...next,
telegram: {
...next.telegram,
enabled: true,
...(params.useEnv
? {}
: params.tokenFile
? { tokenFile: params.tokenFile }
: params.token
? { botToken: params.token }
: {}),
},
};
}
return {
...next,
telegram: {
...next.telegram,
enabled: true,
accounts: {
...next.telegram?.accounts,
[accountId]: {
...next.telegram?.accounts?.[accountId],
enabled: true,
...(params.tokenFile
? { tokenFile: params.tokenFile }
: params.token
? { botToken: params.token }
: {}),
},
},
},
};
}
if (params.provider === "discord") {
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...next,
discord: {
...next.discord,
enabled: true,
...(params.useEnv ? {} : params.token ? { token: params.token } : {}),
},
};
}
return {
...next,
discord: {
...next.discord,
enabled: true,
accounts: {
...next.discord?.accounts,
[accountId]: {
...next.discord?.accounts?.[accountId],
enabled: true,
...(params.token ? { token: params.token } : {}),
},
},
},
};
}
if (params.provider === "slack") {
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...next,
slack: {
...next.slack,
enabled: true,
...(params.useEnv
? {}
: {
...(params.botToken ? { botToken: params.botToken } : {}),
...(params.appToken ? { appToken: params.appToken } : {}),
}),
},
};
}
return {
...next,
slack: {
...next.slack,
enabled: true,
accounts: {
...next.slack?.accounts,
[accountId]: {
...next.slack?.accounts?.[accountId],
enabled: true,
...(params.botToken ? { botToken: params.botToken } : {}),
...(params.appToken ? { appToken: params.appToken } : {}),
},
},
},
};
}
if (params.provider === "signal") {
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...next,
signal: {
...next.signal,
enabled: true,
...(params.signalNumber ? { account: params.signalNumber } : {}),
...(params.cliPath ? { cliPath: params.cliPath } : {}),
...(params.httpUrl ? { httpUrl: params.httpUrl } : {}),
...(params.httpHost ? { httpHost: params.httpHost } : {}),
...(params.httpPort ? { httpPort: Number(params.httpPort) } : {}),
},
};
}
return {
...next,
signal: {
...next.signal,
enabled: true,
accounts: {
...next.signal?.accounts,
[accountId]: {
...next.signal?.accounts?.[accountId],
enabled: true,
...(params.signalNumber ? { account: params.signalNumber } : {}),
...(params.cliPath ? { cliPath: params.cliPath } : {}),
...(params.httpUrl ? { httpUrl: params.httpUrl } : {}),
...(params.httpHost ? { httpHost: params.httpHost } : {}),
...(params.httpPort ? { httpPort: Number(params.httpPort) } : {}),
},
},
},
};
}
if (params.provider === "imessage") {
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...next,
imessage: {
...next.imessage,
enabled: true,
...(params.cliPath ? { cliPath: params.cliPath } : {}),
...(params.dbPath ? { dbPath: params.dbPath } : {}),
...(params.service ? { service: params.service } : {}),
...(params.region ? { region: params.region } : {}),
},
};
}
return {
...next,
imessage: {
...next.imessage,
enabled: true,
accounts: {
...next.imessage?.accounts,
[accountId]: {
...next.imessage?.accounts?.[accountId],
enabled: true,
...(params.cliPath ? { cliPath: params.cliPath } : {}),
...(params.dbPath ? { dbPath: params.dbPath } : {}),
...(params.service ? { service: params.service } : {}),
...(params.region ? { region: params.region } : {}),
},
},
},
};
}
return next;
}

View File

@@ -1,8 +1,5 @@
import { type ClawdbotConfig, writeConfigFile } from "../../config/config.js"; import { writeConfigFile } from "../../config/config.js";
import { import { normalizeChatProviderId } from "../../providers/registry.js";
type ChatProviderId,
normalizeChatProviderId,
} from "../../providers/registry.js";
import { import {
DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID,
normalizeAccountId, normalizeAccountId,
@@ -11,14 +8,16 @@ import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
import { createClackPrompter } from "../../wizard/clack-prompter.js"; import { createClackPrompter } from "../../wizard/clack-prompter.js";
import { setupProviders } from "../onboard-providers.js"; import { setupProviders } from "../onboard-providers.js";
import type { ProviderChoice } from "../onboard-types.js"; import type { ProviderChoice } from "../onboard-types.js";
import {
applyAccountName,
applyProviderAccountConfig,
} from "./add-mutators.js";
import { import {
providerLabel, providerLabel,
requireValidConfig, requireValidConfig,
shouldUseWizard, shouldUseWizard,
} from "./shared.js"; } from "./shared.js";
type ChatProvider = ChatProviderId;
export type ProvidersAddOptions = { export type ProvidersAddOptions = {
provider?: string; provider?: string;
account?: string; account?: string;
@@ -39,342 +38,6 @@ export type ProvidersAddOptions = {
useEnv?: boolean; useEnv?: boolean;
}; };
function providerHasAccounts(cfg: ClawdbotConfig, provider: ChatProvider) {
if (provider === "whatsapp") return true;
const base = (cfg as Record<string, unknown>)[provider] as
| { accounts?: Record<string, unknown> }
| undefined;
return Boolean(base?.accounts && Object.keys(base.accounts).length > 0);
}
function shouldStoreNameInAccounts(
cfg: ClawdbotConfig,
provider: ChatProvider,
accountId: string,
): boolean {
if (provider === "whatsapp") return true;
if (accountId !== DEFAULT_ACCOUNT_ID) return true;
return providerHasAccounts(cfg, provider);
}
function migrateBaseNameToDefaultAccount(
cfg: ClawdbotConfig,
provider: ChatProvider,
): ClawdbotConfig {
if (provider === "whatsapp") return cfg;
const base = (cfg as Record<string, unknown>)[provider] as
| { name?: string; accounts?: Record<string, Record<string, unknown>> }
| undefined;
const baseName = base?.name?.trim();
if (!baseName) return cfg;
const accounts: Record<string, Record<string, unknown>> = {
...base?.accounts,
};
const defaultAccount = accounts[DEFAULT_ACCOUNT_ID] ?? {};
if (!defaultAccount.name) {
accounts[DEFAULT_ACCOUNT_ID] = { ...defaultAccount, name: baseName };
}
const { name: _ignored, ...rest } = base ?? {};
return {
...cfg,
[provider]: {
...rest,
accounts,
},
} as ClawdbotConfig;
}
function applyAccountName(params: {
cfg: ClawdbotConfig;
provider: ChatProvider;
accountId: string;
name?: string;
}): ClawdbotConfig {
const trimmed = params.name?.trim();
if (!trimmed) return params.cfg;
const accountId = normalizeAccountId(params.accountId);
if (params.provider === "whatsapp") {
return {
...params.cfg,
whatsapp: {
...params.cfg.whatsapp,
accounts: {
...params.cfg.whatsapp?.accounts,
[accountId]: {
...params.cfg.whatsapp?.accounts?.[accountId],
name: trimmed,
},
},
},
};
}
const key = params.provider;
const useAccounts = shouldStoreNameInAccounts(params.cfg, key, accountId);
if (!useAccounts && accountId === DEFAULT_ACCOUNT_ID) {
const baseConfig = (params.cfg as Record<string, unknown>)[key];
const safeBase =
typeof baseConfig === "object" && baseConfig
? (baseConfig as Record<string, unknown>)
: {};
return {
...params.cfg,
[key]: {
...safeBase,
name: trimmed,
},
} as ClawdbotConfig;
}
const base = (params.cfg as Record<string, unknown>)[key] as
| { name?: string; accounts?: Record<string, Record<string, unknown>> }
| undefined;
const baseAccounts: Record<
string,
Record<string, unknown>
> = base?.accounts ?? {};
const existingAccount = baseAccounts[accountId] ?? {};
const baseWithoutName =
accountId === DEFAULT_ACCOUNT_ID
? (({ name: _ignored, ...rest }) => rest)(base ?? {})
: (base ?? {});
return {
...params.cfg,
[key]: {
...baseWithoutName,
accounts: {
...baseAccounts,
[accountId]: {
...existingAccount,
name: trimmed,
},
},
},
} as ClawdbotConfig;
}
function applyProviderAccountConfig(params: {
cfg: ClawdbotConfig;
provider: ChatProvider;
accountId: string;
name?: string;
token?: string;
tokenFile?: string;
botToken?: string;
appToken?: string;
signalNumber?: string;
cliPath?: string;
dbPath?: string;
service?: "imessage" | "sms" | "auto";
region?: string;
authDir?: string;
httpUrl?: string;
httpHost?: string;
httpPort?: string;
useEnv?: boolean;
}): ClawdbotConfig {
const accountId = normalizeAccountId(params.accountId);
const name = params.name?.trim() || undefined;
const namedConfig = applyAccountName({
cfg: params.cfg,
provider: params.provider,
accountId,
name,
});
const next =
accountId !== DEFAULT_ACCOUNT_ID
? migrateBaseNameToDefaultAccount(namedConfig, params.provider)
: namedConfig;
if (params.provider === "whatsapp") {
const entry = {
...next.whatsapp?.accounts?.[accountId],
...(params.authDir ? { authDir: params.authDir } : {}),
enabled: true,
};
return {
...next,
whatsapp: {
...next.whatsapp,
accounts: {
...next.whatsapp?.accounts,
[accountId]: entry,
},
},
};
}
if (params.provider === "telegram") {
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...next,
telegram: {
...next.telegram,
enabled: true,
...(params.useEnv
? {}
: params.tokenFile
? { tokenFile: params.tokenFile }
: params.token
? { botToken: params.token }
: {}),
},
};
}
return {
...next,
telegram: {
...next.telegram,
enabled: true,
accounts: {
...next.telegram?.accounts,
[accountId]: {
...next.telegram?.accounts?.[accountId],
enabled: true,
...(params.tokenFile
? { tokenFile: params.tokenFile }
: params.token
? { botToken: params.token }
: {}),
},
},
},
};
}
if (params.provider === "discord") {
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...next,
discord: {
...next.discord,
enabled: true,
...(params.useEnv ? {} : params.token ? { token: params.token } : {}),
},
};
}
return {
...next,
discord: {
...next.discord,
enabled: true,
accounts: {
...next.discord?.accounts,
[accountId]: {
...next.discord?.accounts?.[accountId],
enabled: true,
...(params.token ? { token: params.token } : {}),
},
},
},
};
}
if (params.provider === "slack") {
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...next,
slack: {
...next.slack,
enabled: true,
...(params.useEnv
? {}
: {
...(params.botToken ? { botToken: params.botToken } : {}),
...(params.appToken ? { appToken: params.appToken } : {}),
}),
},
};
}
return {
...next,
slack: {
...next.slack,
enabled: true,
accounts: {
...next.slack?.accounts,
[accountId]: {
...next.slack?.accounts?.[accountId],
enabled: true,
...(params.botToken ? { botToken: params.botToken } : {}),
...(params.appToken ? { appToken: params.appToken } : {}),
},
},
},
};
}
if (params.provider === "signal") {
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...next,
signal: {
...next.signal,
enabled: true,
...(params.signalNumber ? { account: params.signalNumber } : {}),
...(params.cliPath ? { cliPath: params.cliPath } : {}),
...(params.httpUrl ? { httpUrl: params.httpUrl } : {}),
...(params.httpHost ? { httpHost: params.httpHost } : {}),
...(params.httpPort ? { httpPort: Number(params.httpPort) } : {}),
},
};
}
return {
...next,
signal: {
...next.signal,
enabled: true,
accounts: {
...next.signal?.accounts,
[accountId]: {
...next.signal?.accounts?.[accountId],
enabled: true,
...(params.signalNumber ? { account: params.signalNumber } : {}),
...(params.cliPath ? { cliPath: params.cliPath } : {}),
...(params.httpUrl ? { httpUrl: params.httpUrl } : {}),
...(params.httpHost ? { httpHost: params.httpHost } : {}),
...(params.httpPort ? { httpPort: Number(params.httpPort) } : {}),
},
},
},
};
}
if (params.provider === "imessage") {
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...next,
imessage: {
...next.imessage,
enabled: true,
...(params.cliPath ? { cliPath: params.cliPath } : {}),
...(params.dbPath ? { dbPath: params.dbPath } : {}),
...(params.service ? { service: params.service } : {}),
...(params.region ? { region: params.region } : {}),
},
};
}
return {
...next,
imessage: {
...next.imessage,
enabled: true,
accounts: {
...next.imessage?.accounts,
[accountId]: {
...next.imessage?.accounts?.[accountId],
enabled: true,
...(params.cliPath ? { cliPath: params.cliPath } : {}),
...(params.dbPath ? { dbPath: params.dbPath } : {}),
...(params.service ? { service: params.service } : {}),
...(params.region ? { region: params.region } : {}),
},
},
},
};
}
return next;
}
export async function providersAddCommand( export async function providersAddCommand(
opts: ProvidersAddOptions, opts: ProvidersAddOptions,
runtime: RuntimeEnv = defaultRuntime, runtime: RuntimeEnv = defaultRuntime,

View File

@@ -40,11 +40,7 @@ import {
resolveWhatsAppAuthDir, resolveWhatsAppAuthDir,
} from "../../web/accounts.js"; } from "../../web/accounts.js";
import { webAuthExists } from "../../web/session.js"; import { webAuthExists } from "../../web/session.js";
import { import { formatProviderAccountLabel, requireValidConfig } from "./shared.js";
formatAccountLabel,
providerLabel,
requireValidConfig,
} from "./shared.js";
export type ProvidersListOptions = { export type ProvidersListOptions = {
json?: boolean; json?: boolean;
@@ -116,12 +112,14 @@ export async function providersListCommand(
> = { > = {
telegram: async (accountId) => { telegram: async (accountId) => {
const account = resolveTelegramAccount({ cfg, accountId }); const account = resolveTelegramAccount({ cfg, accountId });
return `- ${theme.accent(providerLabel("telegram"))} ${theme.heading( const label = formatProviderAccountLabel({
formatAccountLabel({ provider: "telegram",
accountId, accountId,
name: account.name, name: account.name,
}), providerStyle: theme.accent,
)}: ${formatConfigured(Boolean(account.token))}, ${formatTokenSource( accountStyle: theme.heading,
});
return `- ${label}: ${formatConfigured(Boolean(account.token))}, ${formatTokenSource(
account.tokenSource, account.tokenSource,
)}, ${formatEnabled(account.enabled)}`; )}, ${formatEnabled(account.enabled)}`;
}, },
@@ -129,12 +127,14 @@ export async function providersListCommand(
const { authDir } = resolveWhatsAppAuthDir({ cfg, accountId }); const { authDir } = resolveWhatsAppAuthDir({ cfg, accountId });
const linked = await webAuthExists(authDir); const linked = await webAuthExists(authDir);
const name = cfg.whatsapp?.accounts?.[accountId]?.name; const name = cfg.whatsapp?.accounts?.[accountId]?.name;
return `- ${theme.accent(providerLabel("whatsapp"))} ${theme.heading( const label = formatProviderAccountLabel({
formatAccountLabel({ provider: "whatsapp",
accountId, accountId,
name, name,
}), providerStyle: theme.accent,
)}: ${formatLinked(linked)}, ${formatEnabled( accountStyle: theme.heading,
});
return `- ${label}: ${formatLinked(linked)}, ${formatEnabled(
cfg.whatsapp?.accounts?.[accountId]?.enabled ?? cfg.whatsapp?.accounts?.[accountId]?.enabled ??
cfg.web?.enabled ?? cfg.web?.enabled ??
true, true,
@@ -142,24 +142,28 @@ export async function providersListCommand(
}, },
discord: async (accountId) => { discord: async (accountId) => {
const account = resolveDiscordAccount({ cfg, accountId }); const account = resolveDiscordAccount({ cfg, accountId });
return `- ${theme.accent(providerLabel("discord"))} ${theme.heading( const label = formatProviderAccountLabel({
formatAccountLabel({ provider: "discord",
accountId, accountId,
name: account.name, name: account.name,
}), providerStyle: theme.accent,
)}: ${formatConfigured(Boolean(account.token))}, ${formatTokenSource( accountStyle: theme.heading,
});
return `- ${label}: ${formatConfigured(Boolean(account.token))}, ${formatTokenSource(
account.tokenSource, account.tokenSource,
)}, ${formatEnabled(account.enabled)}`; )}, ${formatEnabled(account.enabled)}`;
}, },
slack: async (accountId) => { slack: async (accountId) => {
const account = resolveSlackAccount({ cfg, accountId }); const account = resolveSlackAccount({ cfg, accountId });
const configured = Boolean(account.botToken && account.appToken); const configured = Boolean(account.botToken && account.appToken);
return `- ${theme.accent(providerLabel("slack"))} ${theme.heading( const label = formatProviderAccountLabel({
formatAccountLabel({ provider: "slack",
accountId, accountId,
name: account.name, name: account.name,
}), providerStyle: theme.accent,
)}: ${formatConfigured(configured)}, ${formatSource( accountStyle: theme.heading,
});
return `- ${label}: ${formatConfigured(configured)}, ${formatSource(
"bot", "bot",
account.botTokenSource, account.botTokenSource,
)}, ${formatSource("app", account.appTokenSource)}, ${formatEnabled( )}, ${formatSource("app", account.appTokenSource)}, ${formatEnabled(
@@ -168,23 +172,27 @@ export async function providersListCommand(
}, },
signal: async (accountId) => { signal: async (accountId) => {
const account = resolveSignalAccount({ cfg, accountId }); const account = resolveSignalAccount({ cfg, accountId });
return `- ${theme.accent(providerLabel("signal"))} ${theme.heading( const label = formatProviderAccountLabel({
formatAccountLabel({ provider: "signal",
accountId, accountId,
name: account.name, name: account.name,
}), providerStyle: theme.accent,
)}: ${formatConfigured(account.configured)}, base=${theme.muted( accountStyle: theme.heading,
});
return `- ${label}: ${formatConfigured(account.configured)}, base=${theme.muted(
account.baseUrl, account.baseUrl,
)}, ${formatEnabled(account.enabled)}`; )}, ${formatEnabled(account.enabled)}`;
}, },
imessage: async (accountId) => { imessage: async (accountId) => {
const account = resolveIMessageAccount({ cfg, accountId }); const account = resolveIMessageAccount({ cfg, accountId });
return `- ${theme.accent(providerLabel("imessage"))} ${theme.heading( const label = formatProviderAccountLabel({
formatAccountLabel({ provider: "imessage",
accountId, accountId,
name: account.name, name: account.name,
}), providerStyle: theme.accent,
)}: ${formatEnabled(account.enabled)}`; accountStyle: theme.heading,
});
return `- ${label}: ${formatEnabled(account.enabled)}`;
}, },
}; };

View File

@@ -42,6 +42,27 @@ export function formatAccountLabel(params: {
export const providerLabel = (provider: ChatProvider) => export const providerLabel = (provider: ChatProvider) =>
getChatProviderMeta(provider).label; getChatProviderMeta(provider).label;
export function formatProviderAccountLabel(params: {
provider: ChatProvider;
accountId: string;
name?: string;
providerStyle?: (value: string) => string;
accountStyle?: (value: string) => string;
}): string {
const providerText = providerLabel(params.provider);
const accountText = formatAccountLabel({
accountId: params.accountId,
name: params.name,
});
const styledProvider = params.providerStyle
? params.providerStyle(providerText)
: providerText;
const styledAccount = params.accountStyle
? params.accountStyle(accountText)
: accountText;
return `${styledProvider} ${styledAccount}`;
}
export function shouldUseWizard(params?: { hasFlags?: boolean }) { export function shouldUseWizard(params?: { hasFlags?: boolean }) {
return params?.hasFlags === false; return params?.hasFlags === false;
} }

View File

@@ -4,7 +4,7 @@ import { listChatProviders } from "../../providers/registry.js";
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js"; import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
import { formatDocsLink } from "../../terminal/links.js"; import { formatDocsLink } from "../../terminal/links.js";
import { theme } from "../../terminal/theme.js"; import { theme } from "../../terminal/theme.js";
import { type ChatProvider, formatAccountLabel } from "./shared.js"; import { type ChatProvider, formatProviderAccountLabel } from "./shared.js";
export type ProvidersStatusOptions = { export type ProvidersStatusOptions = {
json?: boolean; json?: boolean;
@@ -18,7 +18,7 @@ export function formatGatewayProvidersStatusLines(
const lines: string[] = []; const lines: string[] = [];
lines.push(theme.success("Gateway reachable.")); lines.push(theme.success("Gateway reachable."));
const accountLines = ( const accountLines = (
label: string, provider: ChatProvider,
accounts: Array<Record<string, unknown>>, accounts: Array<Record<string, unknown>>,
) => ) =>
accounts.map((account) => { accounts.map((account) => {
@@ -42,10 +42,11 @@ export function formatGatewayProvidersStatusLines(
const accountId = const accountId =
typeof account.accountId === "string" ? account.accountId : "default"; typeof account.accountId === "string" ? account.accountId : "default";
const name = typeof account.name === "string" ? account.name.trim() : ""; const name = typeof account.name === "string" ? account.name.trim() : "";
const labelText = `${label} ${formatAccountLabel({ const labelText = formatProviderAccountLabel({
provider,
accountId, accountId,
name: name || undefined, name: name || undefined,
})}`; });
return `- ${labelText}: ${bits.join(", ")}`; return `- ${labelText}: ${bits.join(", ")}`;
}); });
@@ -75,7 +76,7 @@ export function formatGatewayProvidersStatusLines(
for (const meta of listChatProviders()) { for (const meta of listChatProviders()) {
const accounts = accountPayloads[meta.id]; const accounts = accountPayloads[meta.id];
if (accounts && accounts.length > 0) { if (accounts && accounts.length > 0) {
lines.push(...accountLines(meta.label, accounts)); lines.push(...accountLines(meta.id, accounts));
} }
} }