refactor: dedupe cli config cron and install flows

This commit is contained in:
Peter Steinberger
2026-03-02 19:48:38 +00:00
parent 9d30159fcd
commit b1c30f0ba9
80 changed files with 1379 additions and 2027 deletions

View File

@@ -26,8 +26,8 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
return withTempHomeBase(fn, { prefix: "openclaw-agent-acp-" });
}
function mockConfig(home: string, storePath: string) {
loadConfigSpy.mockReturnValue({
function createAcpEnabledConfig(home: string, storePath: string): OpenClawConfig {
return {
acp: {
enabled: true,
backend: "acpx",
@@ -42,7 +42,11 @@ function mockConfig(home: string, storePath: string) {
},
},
session: { store: storePath, mainKey: "main" },
} satisfies OpenClawConfig);
};
}
function mockConfig(home: string, storePath: string) {
loadConfigSpy.mockReturnValue(createAcpEnabledConfig(home, storePath));
}
function mockConfigWithAcpOverrides(
@@ -50,23 +54,12 @@ function mockConfigWithAcpOverrides(
storePath: string,
acpOverrides: Partial<NonNullable<OpenClawConfig["acp"]>>,
) {
loadConfigSpy.mockReturnValue({
acp: {
enabled: true,
backend: "acpx",
allowedAgents: ["codex"],
dispatch: { enabled: true },
...acpOverrides,
},
agents: {
defaults: {
model: { primary: "openai/gpt-5.3-codex" },
models: { "openai/gpt-5.3-codex": {} },
workspace: path.join(home, "openclaw"),
},
},
session: { store: storePath, mainKey: "main" },
} satisfies OpenClawConfig);
const cfg = createAcpEnabledConfig(home, storePath);
cfg.acp = {
...cfg.acp,
...acpOverrides,
};
loadConfigSpy.mockReturnValue(cfg);
}
function writeAcpSessionStore(storePath: string) {

View File

@@ -304,6 +304,24 @@ export function createAuthChoiceDefaultModelApplier(
};
}
export function createAuthChoiceDefaultModelApplierForMutableState(
params: ApplyAuthChoiceParams,
getConfig: () => ApplyAuthChoiceParams["config"],
setConfig: (config: ApplyAuthChoiceParams["config"]) => void,
getAgentModelOverride: () => string | undefined,
setAgentModelOverride: (model: string | undefined) => void,
): ReturnType<typeof createAuthChoiceDefaultModelApplier> {
return createAuthChoiceDefaultModelApplier(
params,
createAuthChoiceModelStateBridge({
getConfig,
setConfig,
getAgentModelOverride,
setAgentModelOverride,
}),
);
}
export function normalizeTokenProviderInput(
tokenProvider: string | null | undefined,
): string | undefined {

View File

@@ -4,8 +4,7 @@ import { normalizeApiKeyInput, validateApiKeyInput } from "./auth-choice.api-key
import {
normalizeSecretInputModeInput,
createAuthChoiceAgentModelNoter,
createAuthChoiceDefaultModelApplier,
createAuthChoiceModelStateBridge,
createAuthChoiceDefaultModelApplierForMutableState,
ensureApiKeyFromOptionEnvOrPrompt,
normalizeTokenProviderInput,
} from "./auth-choice.apply-helpers.js";
@@ -317,14 +316,12 @@ export async function applyAuthChoiceApiProviders(
let nextConfig = params.config;
let agentModelOverride: string | undefined;
const noteAgentModel = createAuthChoiceAgentModelNoter(params);
const applyProviderDefaultModel = createAuthChoiceDefaultModelApplier(
const applyProviderDefaultModel = createAuthChoiceDefaultModelApplierForMutableState(
params,
createAuthChoiceModelStateBridge({
getConfig: () => nextConfig,
setConfig: (config) => (nextConfig = config),
getAgentModelOverride: () => agentModelOverride,
setAgentModelOverride: (model) => (agentModelOverride = model),
}),
() => nextConfig,
(config) => (nextConfig = config),
() => agentModelOverride,
(model) => (agentModelOverride = model),
);
let authChoice = params.authChoice;

View File

@@ -1,7 +1,6 @@
import { normalizeApiKeyInput, validateApiKeyInput } from "./auth-choice.api-key.js";
import {
createAuthChoiceDefaultModelApplier,
createAuthChoiceModelStateBridge,
createAuthChoiceDefaultModelApplierForMutableState,
ensureApiKeyFromOptionEnvOrPrompt,
normalizeSecretInputModeInput,
} from "./auth-choice.apply-helpers.js";
@@ -23,14 +22,12 @@ export async function applyAuthChoiceMiniMax(
): Promise<ApplyAuthChoiceResult | null> {
let nextConfig = params.config;
let agentModelOverride: string | undefined;
const applyProviderDefaultModel = createAuthChoiceDefaultModelApplier(
const applyProviderDefaultModel = createAuthChoiceDefaultModelApplierForMutableState(
params,
createAuthChoiceModelStateBridge({
getConfig: () => nextConfig,
setConfig: (config) => (nextConfig = config),
getAgentModelOverride: () => agentModelOverride,
setAgentModelOverride: (model) => (agentModelOverride = model),
}),
() => nextConfig,
(config) => (nextConfig = config),
() => agentModelOverride,
(model) => (agentModelOverride = model),
);
const requestedSecretInputMode = normalizeSecretInputModeInput(params.opts?.secretInputMode);
const ensureMinimaxApiKey = async (opts: {

View File

@@ -22,6 +22,8 @@ vi.mock("../terminal/note.js", () => ({
note,
}));
const { maybeRepairSandboxImages } = await import("./doctor-sandbox.js");
describe("maybeRepairSandboxImages", () => {
const mockRuntime: RuntimeEnv = {
log: vi.fn(),
@@ -37,22 +39,32 @@ describe("maybeRepairSandboxImages", () => {
vi.clearAllMocks();
});
it("warns when sandbox mode is enabled but Docker is not available", async () => {
// Simulate Docker not available (command fails)
runExec.mockRejectedValue(new Error("Docker not installed"));
const config: OpenClawConfig = {
function createSandboxConfig(mode: "off" | "all" | "non-main"): OpenClawConfig {
return {
agents: {
defaults: {
sandbox: {
mode: "non-main",
mode,
},
},
},
};
}
const { maybeRepairSandboxImages } = await import("./doctor-sandbox.js");
await maybeRepairSandboxImages(config, mockRuntime, mockPrompter);
async function runSandboxRepair(params: {
mode: "off" | "all" | "non-main";
dockerAvailable: boolean;
}) {
if (params.dockerAvailable) {
runExec.mockResolvedValue({ stdout: "24.0.0", stderr: "" });
} else {
runExec.mockRejectedValue(new Error("Docker not installed"));
}
await maybeRepairSandboxImages(createSandboxConfig(params.mode), mockRuntime, mockPrompter);
}
it("warns when sandbox mode is enabled but Docker is not available", async () => {
await runSandboxRepair({ mode: "non-main", dockerAvailable: false });
// The warning should clearly indicate sandbox is enabled but won't work
expect(note).toHaveBeenCalled();
@@ -66,20 +78,7 @@ describe("maybeRepairSandboxImages", () => {
});
it("warns when sandbox mode is 'all' but Docker is not available", async () => {
runExec.mockRejectedValue(new Error("Docker not installed"));
const config: OpenClawConfig = {
agents: {
defaults: {
sandbox: {
mode: "all",
},
},
},
};
const { maybeRepairSandboxImages } = await import("./doctor-sandbox.js");
await maybeRepairSandboxImages(config, mockRuntime, mockPrompter);
await runSandboxRepair({ mode: "all", dockerAvailable: false });
expect(note).toHaveBeenCalled();
const noteCall = note.mock.calls[0];
@@ -90,41 +89,14 @@ describe("maybeRepairSandboxImages", () => {
});
it("does not warn when sandbox mode is off", async () => {
runExec.mockRejectedValue(new Error("Docker not installed"));
const config: OpenClawConfig = {
agents: {
defaults: {
sandbox: {
mode: "off",
},
},
},
};
const { maybeRepairSandboxImages } = await import("./doctor-sandbox.js");
await maybeRepairSandboxImages(config, mockRuntime, mockPrompter);
await runSandboxRepair({ mode: "off", dockerAvailable: false });
// No warning needed when sandbox is off
expect(note).not.toHaveBeenCalled();
});
it("does not warn when Docker is available", async () => {
// Simulate Docker available
runExec.mockResolvedValue({ stdout: "24.0.0", stderr: "" });
const config: OpenClawConfig = {
agents: {
defaults: {
sandbox: {
mode: "non-main",
},
},
},
};
const { maybeRepairSandboxImages } = await import("./doctor-sandbox.js");
await maybeRepairSandboxImages(config, mockRuntime, mockPrompter);
await runSandboxRepair({ mode: "non-main", dockerAvailable: true });
// May have other notes about images, but not the Docker unavailable warning
const dockerUnavailableWarning = note.mock.calls.find(

View File

@@ -95,6 +95,73 @@ function patchTelegramAdapter(overrides: Parameters<typeof patchChannelOnboardin
});
}
function createUnexpectedConfigureCall(message: string) {
return vi.fn(async () => {
throw new Error(message);
});
}
async function runConfiguredTelegramSetup(params: {
strictUnexpected?: boolean;
configureWhenConfigured: NonNullable<
Parameters<typeof patchTelegramAdapter>[0]["configureWhenConfigured"]
>;
configureErrorMessage: string;
}) {
const select = createQuickstartTelegramSelect({ strictUnexpected: params.strictUnexpected });
const selection = vi.fn();
const onAccountId = vi.fn();
const configure = createUnexpectedConfigureCall(params.configureErrorMessage);
const restore = patchTelegramAdapter({
configureInteractive: undefined,
configureWhenConfigured: params.configureWhenConfigured,
configure,
});
const { prompter } = createUnexpectedQuickstartPrompter(
select as unknown as WizardPrompter["select"],
);
try {
const cfg = await runSetupChannels(createTelegramCfg("old-token"), prompter, {
quickstartDefaults: true,
onSelection: selection,
onAccountId,
});
return { cfg, selection, onAccountId, configure };
} finally {
restore();
}
}
async function runQuickstartTelegramSetupWithInteractive(params: {
configureInteractive: NonNullable<
Parameters<typeof patchTelegramAdapter>[0]["configureInteractive"]
>;
configure?: NonNullable<Parameters<typeof patchTelegramAdapter>[0]["configure"]>;
}) {
const select = createQuickstartTelegramSelect();
const selection = vi.fn();
const onAccountId = vi.fn();
const restore = patchTelegramAdapter({
configureInteractive: params.configureInteractive,
...(params.configure ? { configure: params.configure } : {}),
});
const { prompter } = createUnexpectedQuickstartPrompter(
select as unknown as WizardPrompter["select"],
);
try {
const cfg = await runSetupChannels({} as OpenClawConfig, prompter, {
quickstartDefaults: true,
onSelection: selection,
onAccountId,
});
return { cfg, selection, onAccountId };
} finally {
restore();
}
}
vi.mock("node:fs/promises", () => ({
default: {
access: vi.fn(async () => {
@@ -269,39 +336,20 @@ describe("setupChannels", () => {
});
it("uses configureInteractive skip without mutating selection/account state", async () => {
const select = createQuickstartTelegramSelect();
const selection = vi.fn();
const onAccountId = vi.fn();
const configureInteractive = vi.fn(async () => "skip" as const);
const restore = patchTelegramAdapter({
const { cfg, selection, onAccountId } = await runQuickstartTelegramSetupWithInteractive({
configureInteractive,
});
const { prompter } = createUnexpectedQuickstartPrompter(
select as unknown as WizardPrompter["select"],
expect(configureInteractive).toHaveBeenCalledWith(
expect.objectContaining({ configured: false, label: expect.any(String) }),
);
try {
const cfg = await runSetupChannels({} as OpenClawConfig, prompter, {
quickstartDefaults: true,
onSelection: selection,
onAccountId,
});
expect(configureInteractive).toHaveBeenCalledWith(
expect.objectContaining({ configured: false, label: expect.any(String) }),
);
expect(selection).toHaveBeenCalledWith([]);
expect(onAccountId).not.toHaveBeenCalled();
expect(cfg.channels?.telegram?.botToken).toBeUndefined();
} finally {
restore();
}
expect(selection).toHaveBeenCalledWith([]);
expect(onAccountId).not.toHaveBeenCalled();
expect(cfg.channels?.telegram?.botToken).toBeUndefined();
});
it("applies configureInteractive result cfg/account updates", async () => {
const select = createQuickstartTelegramSelect();
const selection = vi.fn();
const onAccountId = vi.fn();
const configureInteractive = vi.fn(async ({ cfg }: { cfg: OpenClawConfig }) => ({
cfg: {
...cfg,
@@ -312,38 +360,22 @@ describe("setupChannels", () => {
} as OpenClawConfig,
accountId: "acct-1",
}));
const configure = vi.fn(async () => {
throw new Error("configure should not be called when configureInteractive is present");
});
const restore = patchTelegramAdapter({
const configure = createUnexpectedConfigureCall(
"configure should not be called when configureInteractive is present",
);
const { cfg, selection, onAccountId } = await runQuickstartTelegramSetupWithInteractive({
configureInteractive,
configure,
});
const { prompter } = createUnexpectedQuickstartPrompter(
select as unknown as WizardPrompter["select"],
);
try {
const cfg = await runSetupChannels({} as OpenClawConfig, prompter, {
quickstartDefaults: true,
onSelection: selection,
onAccountId,
});
expect(configureInteractive).toHaveBeenCalledTimes(1);
expect(configure).not.toHaveBeenCalled();
expect(selection).toHaveBeenCalledWith(["telegram"]);
expect(onAccountId).toHaveBeenCalledWith("telegram", "acct-1");
expect(cfg.channels?.telegram?.botToken).toBe("new-token");
} finally {
restore();
}
expect(configureInteractive).toHaveBeenCalledTimes(1);
expect(configure).not.toHaveBeenCalled();
expect(selection).toHaveBeenCalledWith(["telegram"]);
expect(onAccountId).toHaveBeenCalledWith("telegram", "acct-1");
expect(cfg.channels?.telegram?.botToken).toBe("new-token");
});
it("uses configureWhenConfigured when channel is already configured", async () => {
const select = createQuickstartTelegramSelect();
const selection = vi.fn();
const onAccountId = vi.fn();
const configureWhenConfigured = vi.fn(async ({ cfg }: { cfg: OpenClawConfig }) => ({
cfg: {
...cfg,
@@ -354,74 +386,37 @@ describe("setupChannels", () => {
} as OpenClawConfig,
accountId: "acct-2",
}));
const configure = vi.fn(async () => {
throw new Error(
"configure should not be called when configureWhenConfigured handles updates",
);
});
const restore = patchTelegramAdapter({
configureInteractive: undefined,
const { cfg, selection, onAccountId, configure } = await runConfiguredTelegramSetup({
configureWhenConfigured,
configure,
configureErrorMessage:
"configure should not be called when configureWhenConfigured handles updates",
});
const { prompter } = createUnexpectedQuickstartPrompter(
select as unknown as WizardPrompter["select"],
expect(configureWhenConfigured).toHaveBeenCalledTimes(1);
expect(configureWhenConfigured).toHaveBeenCalledWith(
expect.objectContaining({ configured: true, label: expect.any(String) }),
);
try {
const cfg = await runSetupChannels(createTelegramCfg("old-token"), prompter, {
quickstartDefaults: true,
onSelection: selection,
onAccountId,
});
expect(configureWhenConfigured).toHaveBeenCalledTimes(1);
expect(configureWhenConfigured).toHaveBeenCalledWith(
expect.objectContaining({ configured: true, label: expect.any(String) }),
);
expect(configure).not.toHaveBeenCalled();
expect(selection).toHaveBeenCalledWith(["telegram"]);
expect(onAccountId).toHaveBeenCalledWith("telegram", "acct-2");
expect(cfg.channels?.telegram?.botToken).toBe("updated-token");
} finally {
restore();
}
expect(configure).not.toHaveBeenCalled();
expect(selection).toHaveBeenCalledWith(["telegram"]);
expect(onAccountId).toHaveBeenCalledWith("telegram", "acct-2");
expect(cfg.channels?.telegram?.botToken).toBe("updated-token");
});
it("respects configureWhenConfigured skip without mutating selection or account state", async () => {
const select = createQuickstartTelegramSelect({ strictUnexpected: true });
const selection = vi.fn();
const onAccountId = vi.fn();
const configureWhenConfigured = vi.fn(async () => "skip" as const);
const configure = vi.fn(async () => {
throw new Error("configure should not run when configureWhenConfigured handles skip");
});
const restore = patchTelegramAdapter({
configureInteractive: undefined,
const { cfg, selection, onAccountId, configure } = await runConfiguredTelegramSetup({
strictUnexpected: true,
configureWhenConfigured,
configure,
configureErrorMessage: "configure should not run when configureWhenConfigured handles skip",
});
const { prompter } = createUnexpectedQuickstartPrompter(
select as unknown as WizardPrompter["select"],
expect(configureWhenConfigured).toHaveBeenCalledWith(
expect.objectContaining({ configured: true, label: expect.any(String) }),
);
try {
const cfg = await runSetupChannels(createTelegramCfg("old-token"), prompter, {
quickstartDefaults: true,
onSelection: selection,
onAccountId,
});
expect(configureWhenConfigured).toHaveBeenCalledWith(
expect.objectContaining({ configured: true, label: expect.any(String) }),
);
expect(configure).not.toHaveBeenCalled();
expect(selection).toHaveBeenCalledWith([]);
expect(onAccountId).not.toHaveBeenCalled();
expect(cfg.channels?.telegram?.botToken).toBe("old-token");
} finally {
restore();
}
expect(configure).not.toHaveBeenCalled();
expect(selection).toHaveBeenCalledWith([]);
expect(onAccountId).not.toHaveBeenCalled();
expect(cfg.channels?.telegram?.botToken).toBe("old-token");
});
it("prefers configureInteractive over configureWhenConfigured when both hooks exist", async () => {

View File

@@ -42,6 +42,21 @@ function createSelectPrompter(
describe("promptRemoteGatewayConfig", () => {
const envSnapshot = captureEnv(["OPENCLAW_ALLOW_INSECURE_PRIVATE_WS"]);
async function runRemotePrompt(params: {
text: WizardPrompter["text"];
selectResponses: Partial<Record<string, string>>;
confirm: boolean;
}) {
const cfg = {} as OpenClawConfig;
const prompter = createPrompter({
confirm: vi.fn(async () => params.confirm),
select: createSelectPrompter(params.selectResponses),
text: params.text,
});
const next = await promptRemoteGatewayConfig(cfg, prompter);
return { next, prompter };
}
beforeEach(() => {
vi.clearAllMocks();
envSnapshot.restore();
@@ -61,12 +76,6 @@ describe("promptRemoteGatewayConfig", () => {
},
]);
const select = createSelectPrompter({
"Select gateway": "0",
"Connection method": "direct",
"Gateway auth": "token",
});
const text: WizardPrompter["text"] = vi.fn(async (params) => {
if (params.message === "Gateway WebSocket URL") {
expect(params.initialValue).toBe("wss://gateway.tailnet.ts.net:18789");
@@ -79,15 +88,16 @@ describe("promptRemoteGatewayConfig", () => {
return "";
}) as WizardPrompter["text"];
const cfg = {} as OpenClawConfig;
const prompter = createPrompter({
confirm: vi.fn(async () => true),
select,
const { next, prompter } = await runRemotePrompt({
text,
confirm: true,
selectResponses: {
"Select gateway": "0",
"Connection method": "direct",
"Gateway auth": "token",
},
});
const next = await promptRemoteGatewayConfig(cfg, prompter);
expect(next.gateway?.mode).toBe("remote");
expect(next.gateway?.remote?.url).toBe("wss://gateway.tailnet.ts.net:18789");
expect(next.gateway?.remote?.token).toBe("token-123");
@@ -111,17 +121,12 @@ describe("promptRemoteGatewayConfig", () => {
return "";
}) as WizardPrompter["text"];
const select = createSelectPrompter({ "Gateway auth": "off" });
const cfg = {} as OpenClawConfig;
const prompter = createPrompter({
confirm: vi.fn(async () => false),
select,
const { next } = await runRemotePrompt({
text,
confirm: false,
selectResponses: { "Gateway auth": "off" },
});
const next = await promptRemoteGatewayConfig(cfg, prompter);
expect(next.gateway?.mode).toBe("remote");
expect(next.gateway?.remote?.url).toBe("wss://remote.example.com:18789");
expect(next.gateway?.remote?.token).toBeUndefined();
@@ -138,17 +143,12 @@ describe("promptRemoteGatewayConfig", () => {
return "";
}) as WizardPrompter["text"];
const select = createSelectPrompter({ "Gateway auth": "off" });
const cfg = {} as OpenClawConfig;
const prompter = createPrompter({
confirm: vi.fn(async () => false),
select,
const { next } = await runRemotePrompt({
text,
confirm: false,
selectResponses: { "Gateway auth": "off" },
});
const next = await promptRemoteGatewayConfig(cfg, prompter);
expect(next.gateway?.remote?.url).toBe("ws://10.0.0.8:18789");
});
});

View File

@@ -0,0 +1,15 @@
export function groupChannelIssuesByChannel<T extends { channel: string }>(
issues: readonly T[],
): Map<string, T[]> {
const byChannel = new Map<string, T[]>();
for (const issue of issues) {
const key = issue.channel;
const list = byChannel.get(key);
if (list) {
list.push(issue);
} else {
byChannel.set(key, [issue]);
}
}
return byChannel;
}

View File

@@ -2,6 +2,8 @@ import fs from "node:fs";
import {
buildChannelAccountSnapshot,
formatChannelAllowFrom,
resolveChannelAccountConfigured,
resolveChannelAccountEnabled,
} from "../../channels/account-summary.js";
import { resolveChannelDefaultAccountId } from "../../channels/plugins/helpers.js";
import { listChannelPlugins } from "../../channels/plugins/index.js";
@@ -85,30 +87,6 @@ const formatAccountLabel = (params: { accountId: string; name?: string }) => {
return base;
};
const resolveAccountEnabled = (
plugin: ChannelPlugin,
account: unknown,
cfg: OpenClawConfig,
): boolean => {
if (plugin.config.isEnabled) {
return plugin.config.isEnabled(account, cfg);
}
const enabled = asRecord(account).enabled;
return enabled !== false;
};
const resolveAccountConfigured = async (
plugin: ChannelPlugin,
account: unknown,
cfg: OpenClawConfig,
): Promise<boolean> => {
if (plugin.config.isConfigured) {
return await plugin.config.isConfigured(account, cfg);
}
const configured = asRecord(account).configured;
return configured !== false;
};
const buildAccountNotes = (params: {
plugin: ChannelPlugin;
cfg: OpenClawConfig;
@@ -343,8 +321,13 @@ export async function buildChannelsTable(
const accounts: ChannelAccountRow[] = [];
for (const accountId of resolvedAccountIds) {
const account = plugin.config.resolveAccount(cfg, accountId);
const enabled = resolveAccountEnabled(plugin, account, cfg);
const configured = await resolveAccountConfigured(plugin, account, cfg);
const enabled = resolveChannelAccountEnabled({ plugin, account, cfg });
const configured = await resolveChannelAccountConfigured({
plugin,
account,
cfg,
readAccountConfiguredField: true,
});
const snapshot = buildChannelAccountSnapshot({
plugin,
cfg,

View File

@@ -1,6 +1,7 @@
import type { ProgressReporter } from "../../cli/progress.js";
import { renderTable } from "../../terminal/table.js";
import { isRich, theme } from "../../terminal/theme.js";
import { groupChannelIssuesByChannel } from "./channel-issues.js";
import { appendStatusAllDiagnosis } from "./diagnosis.js";
import { formatTimeAgo } from "./format.js";
@@ -81,19 +82,7 @@ export async function buildStatusAllReportLines(params: {
: theme.accentDim("SETUP"),
Detail: row.detail,
}));
const channelIssuesByChannel = (() => {
const map = new Map<string, ChannelIssueLike[]>();
for (const issue of params.channelIssues) {
const key = issue.channel;
const list = map.get(key);
if (list) {
list.push(issue);
} else {
map.set(key, [issue]);
}
}
return map;
})();
const channelIssuesByChannel = groupChannelIssuesByChannel(params.channelIssues);
const channelRowsWithIssues = channelRows.map((row) => {
const issues = channelIssuesByChannel.get(row.channelId) ?? [];
if (issues.length === 0) {

View File

@@ -21,6 +21,7 @@ import { theme } from "../terminal/theme.js";
import { formatHealthChannelLines, type HealthSummary } from "./health.js";
import { resolveControlUiLinks } from "./onboard-helpers.js";
import { statusAllCommand } from "./status-all.js";
import { groupChannelIssuesByChannel } from "./status-all/channel-issues.js";
import { formatGatewayAuthUsed } from "./status-all/format.js";
import { getDaemonStatusSummary, getNodeDaemonStatusSummary } from "./status.daemon.js";
import {
@@ -500,19 +501,7 @@ export async function statusCommand(
runtime.log("");
runtime.log(theme.heading("Channels"));
const channelIssuesByChannel = (() => {
const map = new Map<string, typeof channelIssues>();
for (const issue of channelIssues) {
const key = issue.channel;
const list = map.get(key);
if (list) {
list.push(issue);
} else {
map.set(key, [issue]);
}
}
return map;
})();
const channelIssuesByChannel = groupChannelIssuesByChannel(channelIssues);
runtime.log(
renderTable({
width: tableWidth,