mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-07 07:58:36 +00:00
fix(channels): list channel catalog status
This commit is contained in:
@@ -100,7 +100,7 @@ export async function registerChannelsCli(
|
||||
"after",
|
||||
() =>
|
||||
`\n${theme.heading("Examples:")}\n${formatHelpExamples([
|
||||
["openclaw channels list", "List configured channels and auth profiles."],
|
||||
["openclaw channels list", "List available and configured channels."],
|
||||
["openclaw channels status --probe", "Run channel status checks and probes."],
|
||||
[
|
||||
"openclaw channels add --channel telegram --token <token>",
|
||||
|
||||
@@ -10,10 +10,13 @@ const mocks = vi.hoisted(() => ({
|
||||
effectiveConfig: config,
|
||||
diagnostics: [],
|
||||
})),
|
||||
loadAuthProfileStoreWithoutExternalProfiles: vi.fn(),
|
||||
listChatChannels: vi.fn(() => [
|
||||
{ id: "discord", label: "Discord", order: 10 },
|
||||
{ id: "telegram", label: "Telegram", order: 20 },
|
||||
]),
|
||||
listReadOnlyChannelPluginsForConfig: vi.fn<() => ChannelPlugin[]>(() => []),
|
||||
listTrustedChannelPluginCatalogEntries: vi.fn<() => any[]>(() => []),
|
||||
buildChannelAccountSnapshot: vi.fn(),
|
||||
loadProviderUsageSummary: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
@@ -28,8 +31,8 @@ vi.mock("../cli/command-secret-targets.js", () => ({
|
||||
getChannelsCommandSecretTargetIds: () => new Set<string>(),
|
||||
}));
|
||||
|
||||
vi.mock("../agents/auth-profiles.js", () => ({
|
||||
loadAuthProfileStoreWithoutExternalProfiles: mocks.loadAuthProfileStoreWithoutExternalProfiles,
|
||||
vi.mock("../channels/chat-meta.js", () => ({
|
||||
listChatChannels: mocks.listChatChannels,
|
||||
}));
|
||||
|
||||
vi.mock("../channels/plugins/read-only.js", () => ({
|
||||
@@ -40,103 +43,108 @@ vi.mock("../channels/plugins/status.js", () => ({
|
||||
buildChannelAccountSnapshot: mocks.buildChannelAccountSnapshot,
|
||||
}));
|
||||
|
||||
vi.mock("../infra/provider-usage.js", () => ({
|
||||
formatUsageReportLines: () => [],
|
||||
loadProviderUsageSummary: mocks.loadProviderUsageSummary,
|
||||
vi.mock("./channel-setup/trusted-catalog.js", () => ({
|
||||
listTrustedChannelPluginCatalogEntries: mocks.listTrustedChannelPluginCatalogEntries,
|
||||
}));
|
||||
|
||||
import { channelsListCommand } from "./channels/list.js";
|
||||
|
||||
function createMockChannelPlugin(accountIds: string[]): ChannelPlugin {
|
||||
function createMockChannelPlugin(params: {
|
||||
id?: string;
|
||||
label?: string;
|
||||
accountIds?: string[];
|
||||
order?: number;
|
||||
}): ChannelPlugin {
|
||||
const id = params.id ?? "telegram";
|
||||
return {
|
||||
id: "telegram",
|
||||
id,
|
||||
meta: {
|
||||
id: "telegram",
|
||||
label: "Telegram",
|
||||
selectionLabel: "Telegram",
|
||||
docsPath: "/channels/telegram",
|
||||
blurb: "Telegram",
|
||||
id,
|
||||
label: params.label ?? id,
|
||||
selectionLabel: params.label ?? id,
|
||||
docsPath: `/channels/${id}`,
|
||||
blurb: params.label ?? id,
|
||||
order: params.order,
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => accountIds,
|
||||
listAccountIds: () => params.accountIds ?? [],
|
||||
resolveAccount: () => ({}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("channels list auth profiles", () => {
|
||||
describe("channels list", () => {
|
||||
beforeEach(() => {
|
||||
mocks.readConfigFileSnapshot.mockReset();
|
||||
mocks.resolveCommandConfigWithSecrets.mockClear();
|
||||
mocks.loadAuthProfileStoreWithoutExternalProfiles.mockReset();
|
||||
mocks.loadProviderUsageSummary.mockReset();
|
||||
mocks.listChatChannels.mockReset();
|
||||
mocks.listChatChannels.mockReturnValue([
|
||||
{ id: "discord", label: "Discord", order: 10 },
|
||||
{ id: "telegram", label: "Telegram", order: 20 },
|
||||
]);
|
||||
mocks.listTrustedChannelPluginCatalogEntries.mockReset();
|
||||
mocks.listTrustedChannelPluginCatalogEntries.mockReturnValue([]);
|
||||
mocks.listReadOnlyChannelPluginsForConfig.mockReset();
|
||||
mocks.listReadOnlyChannelPluginsForConfig.mockReturnValue([]);
|
||||
mocks.buildChannelAccountSnapshot.mockReset();
|
||||
});
|
||||
|
||||
it("includes local auth profiles in JSON output without loading external profiles", async () => {
|
||||
it("lists only channels in JSON output", async () => {
|
||||
const runtime = createTestRuntime();
|
||||
mocks.readConfigFileSnapshot.mockResolvedValue({
|
||||
...baseConfigSnapshot,
|
||||
config: {},
|
||||
});
|
||||
mocks.loadAuthProfileStoreWithoutExternalProfiles.mockReturnValue({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:default": {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "token",
|
||||
refresh: "refresh",
|
||||
expires: 0,
|
||||
created: 0,
|
||||
},
|
||||
"openai-codex:default": {
|
||||
type: "oauth",
|
||||
provider: "openai",
|
||||
access: "token",
|
||||
refresh: "refresh",
|
||||
expires: 0,
|
||||
created: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await channelsListCommand({ json: true, usage: false }, runtime);
|
||||
|
||||
expect(mocks.resolveCommandConfigWithSecrets).toHaveBeenCalledTimes(1);
|
||||
const payload = JSON.parse(runtime.log.mock.calls[0]?.[0] as string) as {
|
||||
auth?: Array<{ id: string }>;
|
||||
channels?: Array<{ id: string; configured: boolean; enabled: boolean; installed: boolean }>;
|
||||
auth?: unknown;
|
||||
usage?: unknown;
|
||||
};
|
||||
const ids = payload.auth?.map((entry) => entry.id) ?? [];
|
||||
expect(ids).toContain("anthropic:default");
|
||||
expect(ids).toContain("openai-codex:default");
|
||||
expect(payload.auth).toBeUndefined();
|
||||
expect(payload.usage).toBeUndefined();
|
||||
expect(payload.channels?.map((entry) => entry.id)).toEqual(["discord", "telegram"]);
|
||||
expect(payload.channels?.[0]).toMatchObject({
|
||||
id: "discord",
|
||||
configured: false,
|
||||
enabled: true,
|
||||
installed: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("includes configured chat channel accounts in JSON output", async () => {
|
||||
it("includes bundled/catalog/configured channels with status flags", async () => {
|
||||
const runtime = createTestRuntime();
|
||||
mocks.listReadOnlyChannelPluginsForConfig.mockReturnValue([
|
||||
createMockChannelPlugin(["alerts", "default"]),
|
||||
createMockChannelPlugin({
|
||||
id: "telegram",
|
||||
label: "Telegram",
|
||||
accountIds: ["alerts"],
|
||||
order: 20,
|
||||
}),
|
||||
]);
|
||||
mocks.listTrustedChannelPluginCatalogEntries.mockReturnValue([
|
||||
{
|
||||
id: "slack",
|
||||
meta: { id: "slack", label: "Slack", selectionLabel: "Slack", order: 30 },
|
||||
install: { npmSpec: "@openclaw/slack" },
|
||||
},
|
||||
]);
|
||||
mocks.readConfigFileSnapshot.mockResolvedValue({
|
||||
...baseConfigSnapshot,
|
||||
config: {
|
||||
channels: {
|
||||
telegram: {
|
||||
accounts: {
|
||||
default: { botToken: "123:abc" },
|
||||
alerts: { botToken: "456:def" },
|
||||
},
|
||||
accounts: { alerts: { botToken: "456:def" } },
|
||||
},
|
||||
slack: { enabled: false },
|
||||
custom: { webhookUrl: "https://example.invalid/hook" },
|
||||
},
|
||||
},
|
||||
});
|
||||
mocks.loadAuthProfileStoreWithoutExternalProfiles.mockReturnValue({
|
||||
version: 1,
|
||||
profiles: {},
|
||||
});
|
||||
|
||||
await channelsListCommand({ json: true, usage: false }, runtime);
|
||||
|
||||
@@ -145,36 +153,50 @@ describe("channels list auth profiles", () => {
|
||||
expect.objectContaining({ includeSetupFallbackPlugins: true }),
|
||||
);
|
||||
const payload = JSON.parse(runtime.log.mock.calls[0]?.[0] as string) as {
|
||||
chat?: Record<string, string[]>;
|
||||
channels: Array<{
|
||||
id: string;
|
||||
configured: boolean;
|
||||
enabled: boolean;
|
||||
installed: boolean;
|
||||
accounts: string[];
|
||||
}>;
|
||||
};
|
||||
expect(payload.chat?.telegram).toEqual(["alerts", "default"]);
|
||||
expect(payload.channels).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: "telegram",
|
||||
configured: true,
|
||||
enabled: true,
|
||||
installed: true,
|
||||
accounts: ["alerts"],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "slack",
|
||||
configured: false,
|
||||
enabled: false,
|
||||
installed: false,
|
||||
accounts: [],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "custom",
|
||||
configured: true,
|
||||
enabled: true,
|
||||
installed: false,
|
||||
accounts: [],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps JSON output valid when usage loading fails", async () => {
|
||||
const runtime = createTestRuntime();
|
||||
mocks.readConfigFileSnapshot.mockResolvedValue({
|
||||
...baseConfigSnapshot,
|
||||
config: {},
|
||||
});
|
||||
mocks.loadAuthProfileStoreWithoutExternalProfiles.mockReturnValue({
|
||||
version: 1,
|
||||
profiles: {},
|
||||
});
|
||||
mocks.loadProviderUsageSummary.mockRejectedValue(new Error("fetch failed"));
|
||||
|
||||
await channelsListCommand({ json: true }, runtime);
|
||||
|
||||
const payload = JSON.parse(runtime.log.mock.calls[0]?.[0] as string) as {
|
||||
usage?: unknown;
|
||||
};
|
||||
expect(payload.usage).toBeUndefined();
|
||||
expect(runtime.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prints configured chat channel accounts before auth providers", async () => {
|
||||
it("prints channel rows and account rows without auth providers", async () => {
|
||||
const runtime = createTestRuntime();
|
||||
mocks.listReadOnlyChannelPluginsForConfig.mockReturnValue([
|
||||
createMockChannelPlugin(["default"]),
|
||||
createMockChannelPlugin({
|
||||
id: "telegram",
|
||||
label: "Telegram",
|
||||
accountIds: ["default"],
|
||||
order: 20,
|
||||
}),
|
||||
]);
|
||||
mocks.buildChannelAccountSnapshot.mockResolvedValue({
|
||||
accountId: "default",
|
||||
@@ -194,21 +216,14 @@ describe("channels list auth profiles", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
mocks.loadAuthProfileStoreWithoutExternalProfiles.mockReturnValue({
|
||||
version: 1,
|
||||
profiles: {},
|
||||
});
|
||||
|
||||
await channelsListCommand({ usage: false }, runtime);
|
||||
|
||||
expect(mocks.listReadOnlyChannelPluginsForConfig).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ includeSetupFallbackPlugins: true }),
|
||||
);
|
||||
const output = stripAnsi(runtime.log.mock.calls[0]?.[0] as string);
|
||||
expect(output).toContain("Chat channels:");
|
||||
expect(output).toContain("Telegram default:");
|
||||
expect(output).toContain("configured");
|
||||
expect(output.indexOf("Telegram default:")).toBeLessThan(output.indexOf("Auth providers"));
|
||||
expect(output).toContain("Discord: not configured, enabled, not installed");
|
||||
expect(output).toContain("Telegram: configured, enabled, installed");
|
||||
expect(output).toContain("Telegram default: configured, token=config, enabled");
|
||||
expect(output).not.toContain("Auth providers");
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,15 @@
|
||||
import { loadAuthProfileStoreWithoutExternalProfiles } from "../../agents/auth-profiles.js";
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
||||
import { listChatChannels } from "../../channels/chat-meta.js";
|
||||
import { isChannelVisibleInConfiguredLists } from "../../channels/plugins/exposure.js";
|
||||
import { listReadOnlyChannelPluginsForConfig } from "../../channels/plugins/read-only.js";
|
||||
import { buildChannelAccountSnapshot } from "../../channels/plugins/status.js";
|
||||
import type { ChannelPlugin } from "../../channels/plugins/types.plugin.js";
|
||||
import type { ChannelAccountSnapshot } from "../../channels/plugins/types.public.js";
|
||||
import { withProgress } from "../../cli/progress.js";
|
||||
import { formatUsageReportLines, loadProviderUsageSummary } from "../../infra/provider-usage.js";
|
||||
import { isStaticallyChannelConfigured } from "../../config/channel-configured-shared.js";
|
||||
import { defaultRuntime, type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
|
||||
import { formatDocsLink } from "../../terminal/links.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
import { listTrustedChannelPluginCatalogEntries } from "../channel-setup/trusted-catalog.js";
|
||||
import { formatChannelAccountLabel, requireValidConfig } from "./shared.js";
|
||||
|
||||
export type ChannelsListOptions = {
|
||||
@@ -89,21 +90,51 @@ function formatAccountLine(params: {
|
||||
}
|
||||
return `- ${label}: ${bits.join(", ")}`;
|
||||
}
|
||||
async function loadUsageWithProgress(
|
||||
runtime: RuntimeEnv,
|
||||
progress = true,
|
||||
): Promise<Awaited<ReturnType<typeof loadProviderUsageSummary>> | null> {
|
||||
try {
|
||||
return await withProgress(
|
||||
{ label: "Fetching usage snapshot…", indeterminate: true, enabled: progress },
|
||||
async () => await loadProviderUsageSummary({ skipPluginAuthWithoutCredentialSource: true }),
|
||||
);
|
||||
} catch (err) {
|
||||
if (progress) {
|
||||
runtime.error(String(err));
|
||||
type ChannelListEntry = {
|
||||
id: string;
|
||||
label: string;
|
||||
order: number;
|
||||
configured: boolean;
|
||||
enabled: boolean;
|
||||
installed: boolean;
|
||||
accounts: string[];
|
||||
source: "bundled" | "catalog" | "configured";
|
||||
};
|
||||
|
||||
const NON_CHANNEL_CONFIG_KEYS = new Set(["defaults"]);
|
||||
|
||||
function resolveChannelEnabled(cfg: Record<string, unknown>, channelId: string): boolean {
|
||||
const channels = cfg.channels;
|
||||
if (channels && typeof channels === "object" && !Array.isArray(channels)) {
|
||||
const channelConfig = (channels as Record<string, unknown>)[channelId];
|
||||
if (channelConfig && typeof channelConfig === "object" && !Array.isArray(channelConfig)) {
|
||||
if ((channelConfig as Record<string, unknown>).enabled === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function formatInstalled(value: boolean): string {
|
||||
return value ? theme.success("installed") : theme.warn("not installed");
|
||||
}
|
||||
|
||||
function formatChannelSummaryLine(entry: ChannelListEntry): string {
|
||||
const bits = [
|
||||
formatConfigured(entry.configured),
|
||||
formatEnabled(entry.enabled),
|
||||
formatInstalled(entry.installed),
|
||||
];
|
||||
return `- ${theme.accent(entry.label)}: ${bits.join(", ")}`;
|
||||
}
|
||||
|
||||
function configuredChannelIdsFromConfig(cfg: Record<string, unknown>): string[] {
|
||||
const channels = cfg.channels;
|
||||
if (!channels || typeof channels !== "object" || Array.isArray(channels)) {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(channels).filter((id) => !NON_CHANNEL_CONFIG_KEYS.has(id));
|
||||
}
|
||||
|
||||
export async function channelsListCommand(
|
||||
@@ -114,78 +145,129 @@ export async function channelsListCommand(
|
||||
if (!cfg) {
|
||||
return;
|
||||
}
|
||||
const includeUsage = opts.usage !== false;
|
||||
void opts.usage;
|
||||
|
||||
const plugins = listReadOnlyChannelPluginsForConfig(cfg, {
|
||||
includeSetupFallbackPlugins: true,
|
||||
});
|
||||
const pluginById = new Map(plugins.map((plugin) => [plugin.id, plugin]));
|
||||
const workspaceDir = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg));
|
||||
const catalogEntries = listTrustedChannelPluginCatalogEntries({ cfg, workspaceDir });
|
||||
const entries = new Map<string, ChannelListEntry>();
|
||||
|
||||
const authStore = loadAuthProfileStoreWithoutExternalProfiles();
|
||||
const authProfiles = Object.entries(authStore.profiles).map(([profileId, profile]) => ({
|
||||
id: profileId,
|
||||
provider: profile.provider,
|
||||
type: profile.type,
|
||||
isExternal: false,
|
||||
}));
|
||||
if (opts.json) {
|
||||
const usage = includeUsage ? await loadUsageWithProgress(runtime, false) : undefined;
|
||||
const chat: Record<string, string[]> = {};
|
||||
for (const plugin of plugins) {
|
||||
chat[plugin.id] = plugin.config.listAccountIds(cfg);
|
||||
const upsert = (entry: ChannelListEntry) => {
|
||||
const existing = entries.get(entry.id);
|
||||
entries.set(entry.id, {
|
||||
...entry,
|
||||
...existing,
|
||||
configured: Boolean(existing?.configured || entry.configured),
|
||||
enabled: existing?.enabled === false || entry.enabled === false ? false : true,
|
||||
installed: Boolean(existing?.installed || entry.installed),
|
||||
accounts: existing?.accounts.length ? existing.accounts : entry.accounts,
|
||||
source: existing?.source ?? entry.source,
|
||||
});
|
||||
};
|
||||
|
||||
for (const meta of listChatChannels()) {
|
||||
const plugin = pluginById.get(meta.id);
|
||||
upsert({
|
||||
id: meta.id,
|
||||
label: meta.label ?? meta.id,
|
||||
order: meta.order ?? Number.MAX_SAFE_INTEGER,
|
||||
configured: isStaticallyChannelConfigured(cfg, meta.id),
|
||||
enabled: resolveChannelEnabled(cfg, meta.id),
|
||||
installed: Boolean(plugin),
|
||||
accounts: plugin?.config.listAccountIds(cfg) ?? [],
|
||||
source: "bundled",
|
||||
});
|
||||
}
|
||||
|
||||
for (const plugin of plugins) {
|
||||
const accounts = plugin.config.listAccountIds(cfg);
|
||||
upsert({
|
||||
id: plugin.id,
|
||||
label: plugin.meta.label ?? plugin.id,
|
||||
order: plugin.meta.order ?? Number.MAX_SAFE_INTEGER,
|
||||
configured: accounts.length > 0 || isStaticallyChannelConfigured(cfg, plugin.id),
|
||||
enabled: resolveChannelEnabled(cfg, plugin.id),
|
||||
installed: true,
|
||||
accounts,
|
||||
source: "bundled",
|
||||
});
|
||||
}
|
||||
|
||||
for (const entry of catalogEntries) {
|
||||
const plugin = pluginById.get(entry.id);
|
||||
const accounts = plugin?.config.listAccountIds(cfg) ?? [];
|
||||
upsert({
|
||||
id: entry.id,
|
||||
label: entry.meta.label ?? entry.id,
|
||||
order: entry.meta.order ?? Number.MAX_SAFE_INTEGER,
|
||||
configured: accounts.length > 0 || isStaticallyChannelConfigured(cfg, entry.id),
|
||||
enabled: resolveChannelEnabled(cfg, entry.id),
|
||||
installed: Boolean(plugin),
|
||||
accounts,
|
||||
source: "catalog",
|
||||
});
|
||||
}
|
||||
|
||||
for (const channelId of configuredChannelIdsFromConfig(cfg)) {
|
||||
const plugin = pluginById.get(channelId);
|
||||
const accounts = plugin?.config.listAccountIds(cfg) ?? [];
|
||||
upsert({
|
||||
id: channelId,
|
||||
label: plugin?.meta.label ?? channelId,
|
||||
order: plugin?.meta.order ?? Number.MAX_SAFE_INTEGER,
|
||||
configured: accounts.length > 0 || isStaticallyChannelConfigured(cfg, channelId),
|
||||
enabled: resolveChannelEnabled(cfg, channelId),
|
||||
installed: Boolean(plugin),
|
||||
accounts,
|
||||
source: "configured",
|
||||
});
|
||||
}
|
||||
|
||||
const channels = [...entries.values()].toSorted((left, right) => {
|
||||
if (left.order !== right.order) {
|
||||
return left.order - right.order;
|
||||
}
|
||||
const payload = { chat, auth: authProfiles, ...(usage ? { usage } : {}) };
|
||||
writeRuntimeJson(runtime, payload);
|
||||
return left.label.localeCompare(right.label);
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
writeRuntimeJson(runtime, { channels });
|
||||
return;
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(theme.heading("Chat channels:"));
|
||||
|
||||
for (const plugin of plugins) {
|
||||
const accounts = plugin.config.listAccountIds(cfg);
|
||||
if (!accounts || accounts.length === 0) {
|
||||
if (channels.length === 0) {
|
||||
lines.push(theme.muted("- none"));
|
||||
}
|
||||
|
||||
for (const entry of channels) {
|
||||
const plugin = pluginById.get(entry.id);
|
||||
lines.push(formatChannelSummaryLine(entry));
|
||||
if (!plugin || entry.accounts.length === 0) {
|
||||
continue;
|
||||
}
|
||||
for (const accountId of accounts) {
|
||||
for (const accountId of entry.accounts) {
|
||||
const snapshot = await buildChannelAccountSnapshot({
|
||||
plugin,
|
||||
cfg,
|
||||
accountId,
|
||||
});
|
||||
lines.push(
|
||||
formatAccountLine({
|
||||
` ${formatAccountLine({
|
||||
channel: plugin,
|
||||
snapshot,
|
||||
}),
|
||||
}).slice(2)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push(theme.heading("Auth providers (OAuth + API keys):"));
|
||||
if (authProfiles.length === 0) {
|
||||
lines.push(theme.muted("- none"));
|
||||
} else {
|
||||
for (const profile of authProfiles) {
|
||||
const external = profile.isExternal ? theme.muted(" (synced)") : "";
|
||||
lines.push(`- ${theme.accent(profile.id)} (${theme.success(profile.type)}${external})`);
|
||||
}
|
||||
}
|
||||
|
||||
runtime.log(lines.join("\n"));
|
||||
|
||||
if (includeUsage) {
|
||||
runtime.log("");
|
||||
const usage = await loadUsageWithProgress(runtime);
|
||||
if (usage) {
|
||||
const usageLines = formatUsageReportLines(usage);
|
||||
if (usageLines.length > 0) {
|
||||
usageLines[0] = theme.accent(usageLines[0]);
|
||||
runtime.log(usageLines.join("\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runtime.log("");
|
||||
runtime.log(`Docs: ${formatDocsLink("/gateway/configuration", "gateway/configuration")}`);
|
||||
runtime.log(`Docs: ${formatDocsLink("/channels", "channels")}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user