mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:10:45 +00:00
Configure: defer channel status until selection
This commit is contained in:
@@ -293,6 +293,7 @@ export type SetupChannelsOptions = {
|
||||
onResolvedPlugin?: (channel: ChannelId, plugin: ChannelSetupPlugin) => void;
|
||||
promptAccountIds?: boolean;
|
||||
forceAllowFromChannels?: ChannelId[];
|
||||
deferStatusUntilSelection?: boolean;
|
||||
skipStatusNote?: boolean;
|
||||
skipDmPolicyPrompt?: boolean;
|
||||
skipConfirm?: boolean;
|
||||
|
||||
82
src/commands/configure.channels.test.ts
Normal file
82
src/commands/configure.channels.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const select = vi.hoisted(() => vi.fn());
|
||||
const confirm = vi.hoisted(() => vi.fn());
|
||||
const note = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../channels/chat-meta.js", () => ({
|
||||
listChatChannels: () => [
|
||||
{ id: "telegram", label: "Telegram" },
|
||||
{ id: "twitch", label: "Twitch" },
|
||||
],
|
||||
}));
|
||||
|
||||
vi.mock("../terminal/note.js", () => ({
|
||||
note: (...args: unknown[]) => note(...args),
|
||||
}));
|
||||
|
||||
vi.mock("./configure.shared.js", () => ({
|
||||
select: (params: unknown) => select(params),
|
||||
confirm: (params: unknown) => confirm(params),
|
||||
}));
|
||||
|
||||
import { removeChannelConfigWizard } from "./configure.channels.js";
|
||||
|
||||
describe("removeChannelConfigWizard", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
confirm.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it("lists configured channels from openclaw.json even when no plugins are loaded", async () => {
|
||||
select.mockResolvedValue("done");
|
||||
|
||||
await removeChannelConfigWizard(
|
||||
{
|
||||
channels: {
|
||||
twitch: {},
|
||||
unknown: {},
|
||||
telegram: {},
|
||||
},
|
||||
} as never,
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(select).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: "Remove which channel config?",
|
||||
options: [
|
||||
expect.objectContaining({ value: "telegram", label: "Telegram" }),
|
||||
expect.objectContaining({ value: "twitch", label: "Twitch" }),
|
||||
expect.objectContaining({ value: "unknown", label: "unknown" }),
|
||||
{ value: "done", label: "Done" },
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("deletes the selected channel block from openclaw.json", async () => {
|
||||
select.mockResolvedValueOnce("telegram").mockResolvedValueOnce("done");
|
||||
|
||||
const next = await removeChannelConfigWizard(
|
||||
{
|
||||
channels: {
|
||||
telegram: { token: "secret" },
|
||||
twitch: { token: "secret" },
|
||||
},
|
||||
} as never,
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(confirm).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: "Delete Telegram configuration from ~/.openclaw/openclaw.json?",
|
||||
}),
|
||||
);
|
||||
expect(next.channels).toEqual({ twitch: { token: "secret" } });
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
"Telegram removed from config.\nNote: credentials/sessions on disk are unchanged.",
|
||||
"Channel removed",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,28 +1,52 @@
|
||||
import { getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import { listChatChannels } from "../channels/chat-meta.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import { CONFIG_PATH } from "../config/config.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
import { shortenHomePath } from "../utils.js";
|
||||
import { shouldShowChannelInSetup } from "./channel-setup/discovery.js";
|
||||
import { confirm, select } from "./configure.shared.js";
|
||||
import { guardCancel } from "./onboard-helpers.js";
|
||||
|
||||
type ConfiguredChannelRemovalChoice = {
|
||||
id: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
function listConfiguredChannelRemovalChoices(
|
||||
cfg: OpenClawConfig,
|
||||
): ConfiguredChannelRemovalChoice[] {
|
||||
const channels = cfg.channels;
|
||||
if (!channels) {
|
||||
return [];
|
||||
}
|
||||
const labelsById = new Map(listChatChannels().map((meta) => [meta.id, meta.label]));
|
||||
return Object.keys(channels)
|
||||
.map((id) => ({
|
||||
id,
|
||||
label: labelsById.get(id) ?? id,
|
||||
}))
|
||||
.toSorted(compareChannelRemovalChoices);
|
||||
}
|
||||
|
||||
function compareChannelRemovalChoices(
|
||||
left: ConfiguredChannelRemovalChoice,
|
||||
right: ConfiguredChannelRemovalChoice,
|
||||
): number {
|
||||
return (
|
||||
left.label.localeCompare(right.label, undefined, { numeric: true, sensitivity: "base" }) ||
|
||||
left.id.localeCompare(right.id, undefined, { numeric: true, sensitivity: "base" })
|
||||
);
|
||||
}
|
||||
|
||||
export async function removeChannelConfigWizard(
|
||||
cfg: OpenClawConfig,
|
||||
runtime: RuntimeEnv,
|
||||
): Promise<OpenClawConfig> {
|
||||
let next = { ...cfg };
|
||||
|
||||
const listConfiguredChannels = () =>
|
||||
listChannelPlugins()
|
||||
.map((plugin) => plugin.meta)
|
||||
.filter((meta) => shouldShowChannelInSetup(meta))
|
||||
.filter((meta) => next.channels?.[meta.id] !== undefined);
|
||||
|
||||
while (true) {
|
||||
const configured = listConfiguredChannels();
|
||||
const configured = listConfiguredChannelRemovalChoices(next);
|
||||
if (configured.length === 0) {
|
||||
note(
|
||||
[
|
||||
@@ -53,7 +77,7 @@ export async function removeChannelConfigWizard(
|
||||
return next;
|
||||
}
|
||||
|
||||
const label = getChannelPlugin(channel)?.meta.label ?? channel;
|
||||
const label = configured.find((entry) => entry.id === channel)?.label ?? channel;
|
||||
const confirmed = guardCancel(
|
||||
await confirm({
|
||||
message: `Delete ${label} configuration from ${shortenHomePath(CONFIG_PATH)}?`,
|
||||
|
||||
@@ -28,6 +28,7 @@ const mocks = vi.hoisted(() => {
|
||||
isCodexNativeWebSearchRelevant: vi.fn(({ config }: { config: OpenClawConfig }) =>
|
||||
Boolean(config.auth?.profiles?.["openai-codex:default"]),
|
||||
),
|
||||
setupChannels: vi.fn(async (cfg: OpenClawConfig) => cfg),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -104,7 +105,7 @@ vi.mock("./onboard-skills.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./onboard-channels.js", () => ({
|
||||
setupChannels: vi.fn(),
|
||||
setupChannels: mocks.setupChannels,
|
||||
}));
|
||||
|
||||
vi.mock("./onboard-search.js", () => ({
|
||||
@@ -327,6 +328,28 @@ describe("runConfigureWizard", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("defers channel status checks until a channel is selected", async () => {
|
||||
setupBaseWizardState();
|
||||
queueWizardPrompts({
|
||||
select: ["local", "configure"],
|
||||
confirm: [],
|
||||
});
|
||||
|
||||
await runConfigureWizard({ command: "configure", sections: ["channels"] }, createRuntime());
|
||||
|
||||
expect(mocks.setupChannels).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
gateway: expect.objectContaining({ mode: "local" }),
|
||||
}),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
deferStatusUntilSelection: true,
|
||||
skipStatusNote: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("still supports keyless web search providers through the shared setup flow", async () => {
|
||||
setupBaseWizardState();
|
||||
mocks.resolveSearchProviderOptions.mockReturnValue([
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
} from "./configure.shared.js";
|
||||
import { formatHealthCheckFailure } from "./health-format.js";
|
||||
import { healthCommand } from "./health.js";
|
||||
import { noteChannelStatus, setupChannels } from "./onboard-channels.js";
|
||||
import { setupChannels } from "./onboard-channels.js";
|
||||
import {
|
||||
applyWizardMetadata,
|
||||
DEFAULT_WORKSPACE,
|
||||
@@ -561,12 +561,12 @@ export async function runConfigureWizard(
|
||||
};
|
||||
|
||||
const configureChannelsSection = async () => {
|
||||
await noteChannelStatus({ cfg: nextConfig, prompter });
|
||||
const channelMode = await promptChannelMode(runtime);
|
||||
if (channelMode === "configure") {
|
||||
nextConfig = await setupChannels(nextConfig, runtime, prompter, {
|
||||
allowDisable: true,
|
||||
allowSignalInstall: true,
|
||||
deferStatusUntilSelection: true,
|
||||
skipConfirm: true,
|
||||
skipStatusNote: true,
|
||||
});
|
||||
|
||||
116
src/flows/channel-setup.status.test.ts
Normal file
116
src/flows/channel-setup.status.test.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const listChatChannels = vi.hoisted(() => vi.fn(() => [{ id: "discord" }, { id: "bluebubbles" }]));
|
||||
|
||||
vi.mock("../channels/chat-meta.js", () => ({
|
||||
listChatChannels: () => listChatChannels(),
|
||||
}));
|
||||
|
||||
vi.mock("../channels/registry.js", () => ({
|
||||
formatChannelPrimerLine: vi.fn(() => ""),
|
||||
formatChannelSelectionLine: vi.fn(() => ""),
|
||||
}));
|
||||
|
||||
vi.mock("../commands/channel-setup/discovery.js", () => ({
|
||||
resolveChannelSetupEntries: vi.fn(() => ({
|
||||
entries: [],
|
||||
installedCatalogEntries: [],
|
||||
installableCatalogEntries: [],
|
||||
installedCatalogById: new Map(),
|
||||
installableCatalogById: new Map(),
|
||||
})),
|
||||
shouldShowChannelInSetup: (meta: { exposure?: { setup?: boolean }; showInSetup?: boolean }) =>
|
||||
meta.showInSetup !== false && meta.exposure?.setup !== false,
|
||||
}));
|
||||
|
||||
import { resolveChannelSetupSelectionContributions } from "./channel-setup.status.js";
|
||||
|
||||
describe("resolveChannelSetupSelectionContributions", () => {
|
||||
it("sorts channels alphabetically by picker label", () => {
|
||||
const contributions = resolveChannelSetupSelectionContributions({
|
||||
entries: [
|
||||
{
|
||||
id: "zalo",
|
||||
meta: {
|
||||
id: "zalo",
|
||||
label: "Zalo",
|
||||
selectionLabel: "Zalo (Bot API)",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "discord",
|
||||
meta: {
|
||||
id: "discord",
|
||||
label: "Discord",
|
||||
selectionLabel: "Discord (Bot API)",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "bluebubbles",
|
||||
meta: {
|
||||
id: "bluebubbles",
|
||||
label: "BlueBubbles",
|
||||
selectionLabel: "BlueBubbles (macOS app)",
|
||||
},
|
||||
},
|
||||
] as never,
|
||||
statusByChannel: new Map(),
|
||||
resolveDisabledHint: () => undefined,
|
||||
});
|
||||
|
||||
expect(contributions.map((contribution) => contribution.option.label)).toEqual([
|
||||
"BlueBubbles (macOS app)",
|
||||
"Discord (Bot API)",
|
||||
"Zalo (Bot API)",
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not invent hints before status has been collected", () => {
|
||||
const contributions = resolveChannelSetupSelectionContributions({
|
||||
entries: [
|
||||
{
|
||||
id: "zalo",
|
||||
meta: {
|
||||
id: "zalo",
|
||||
label: "Zalo",
|
||||
selectionLabel: "Zalo (Bot API)",
|
||||
quickstartAllowFrom: true,
|
||||
},
|
||||
},
|
||||
] as never,
|
||||
statusByChannel: new Map(),
|
||||
resolveDisabledHint: () => undefined,
|
||||
});
|
||||
|
||||
expect(contributions.map((contribution) => contribution.option)).toEqual([
|
||||
{
|
||||
value: "zalo",
|
||||
label: "Zalo (Bot API)",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("combines real status and disabled hints when available", () => {
|
||||
const contributions = resolveChannelSetupSelectionContributions({
|
||||
entries: [
|
||||
{
|
||||
id: "zalo",
|
||||
meta: {
|
||||
id: "zalo",
|
||||
label: "Zalo",
|
||||
selectionLabel: "Zalo (Bot API)",
|
||||
quickstartAllowFrom: true,
|
||||
},
|
||||
},
|
||||
] as never,
|
||||
statusByChannel: new Map([["zalo", { selectionHint: "configured" }]]),
|
||||
resolveDisabledHint: () => "disabled",
|
||||
});
|
||||
|
||||
expect(contributions[0]?.option).toEqual({
|
||||
value: "zalo",
|
||||
label: "Zalo (Bot API)",
|
||||
hint: "configured · disabled",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -35,6 +35,18 @@ export type ChannelSetupSelectionContribution = FlowContribution & {
|
||||
source: "catalog" | "core" | "plugin";
|
||||
};
|
||||
|
||||
type ChannelSetupSelectionEntry = {
|
||||
id: ChannelChoice;
|
||||
meta: {
|
||||
id: string;
|
||||
label: string;
|
||||
selectionLabel?: string;
|
||||
exposure?: { setup?: boolean };
|
||||
showConfigured?: boolean;
|
||||
showInSetup?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
function buildChannelSetupSelectionContribution(params: {
|
||||
channel: ChannelChoice;
|
||||
label: string;
|
||||
@@ -235,33 +247,35 @@ export function resolveChannelSelectionNoteLines(params: {
|
||||
}
|
||||
|
||||
export function resolveChannelSetupSelectionContributions(params: {
|
||||
entries: Array<{
|
||||
id: ChannelChoice;
|
||||
meta: {
|
||||
id: string;
|
||||
label: string;
|
||||
selectionLabel?: string;
|
||||
exposure?: { setup?: boolean };
|
||||
showConfigured?: boolean;
|
||||
showInSetup?: boolean;
|
||||
};
|
||||
}>;
|
||||
entries: ChannelSetupSelectionEntry[];
|
||||
statusByChannel: Map<ChannelChoice, { selectionHint?: string }>;
|
||||
resolveDisabledHint: (channel: ChannelChoice) => string | undefined;
|
||||
}): ChannelSetupSelectionContribution[] {
|
||||
const bundledChannelIds = new Set(listChatChannels().map((channel) => channel.id));
|
||||
return params.entries
|
||||
.filter((entry) => shouldShowChannelInSetup(entry.meta))
|
||||
.toSorted((left, right) => compareChannelSetupSelectionEntries(left, right))
|
||||
.map((entry) => {
|
||||
const disabledHint = params.resolveDisabledHint(entry.id);
|
||||
const hint =
|
||||
[params.statusByChannel.get(entry.id)?.selectionHint, disabledHint]
|
||||
.filter(Boolean)
|
||||
.join(" · ") || undefined;
|
||||
const statusHint = params.statusByChannel.get(entry.id)?.selectionHint;
|
||||
const hint = [statusHint, disabledHint].filter(Boolean).join(" · ") || undefined;
|
||||
return buildChannelSetupSelectionContribution({
|
||||
channel: entry.id,
|
||||
label: entry.meta.selectionLabel ?? entry.meta.label,
|
||||
hint,
|
||||
source: listChatChannels().some((channel) => channel.id === entry.id) ? "core" : "plugin",
|
||||
source: bundledChannelIds.has(entry.id) ? "core" : "plugin",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function compareChannelSetupSelectionEntries(
|
||||
left: ChannelSetupSelectionEntry,
|
||||
right: ChannelSetupSelectionEntry,
|
||||
): number {
|
||||
const leftLabel = left.meta.selectionLabel ?? left.meta.label;
|
||||
const rightLabel = right.meta.selectionLabel ?? right.meta.label;
|
||||
return (
|
||||
leftLabel.localeCompare(rightLabel, undefined, { numeric: true, sensitivity: "base" }) ||
|
||||
left.id.localeCompare(right.id, undefined, { numeric: true, sensitivity: "base" })
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,25 @@ const listChannelSetupPlugins = vi.hoisted(() => vi.fn((): unknown[] => []));
|
||||
const loadChannelSetupPluginRegistrySnapshotForChannel = vi.hoisted(() =>
|
||||
vi.fn((_params?: unknown) => ({ channels: [], channelSetups: [] })),
|
||||
);
|
||||
const resolveChannelSetupEntries = vi.hoisted(() =>
|
||||
vi.fn(
|
||||
(
|
||||
_params?: unknown,
|
||||
): {
|
||||
entries: unknown[];
|
||||
installedCatalogEntries: unknown[];
|
||||
installableCatalogEntries: unknown[];
|
||||
installedCatalogById: Map<unknown, unknown>;
|
||||
installableCatalogById: Map<unknown, unknown>;
|
||||
} => ({
|
||||
entries: [],
|
||||
installedCatalogEntries: [],
|
||||
installableCatalogEntries: [],
|
||||
installedCatalogById: new Map(),
|
||||
installableCatalogById: new Map(),
|
||||
}),
|
||||
),
|
||||
);
|
||||
const collectChannelStatus = vi.hoisted(() =>
|
||||
vi.fn(async (_params?: unknown) => ({
|
||||
installedPlugins: [],
|
||||
@@ -42,7 +61,7 @@ vi.mock("../channels/registry.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../commands/channel-setup/discovery.js", () => ({
|
||||
resolveChannelSetupEntries: vi.fn(),
|
||||
resolveChannelSetupEntries: (params?: unknown) => resolveChannelSetupEntries(params),
|
||||
shouldShowChannelInSetup: () => true,
|
||||
}));
|
||||
|
||||
@@ -101,6 +120,13 @@ describe("setupChannels workspace shadow exclusion", () => {
|
||||
channels: [],
|
||||
channelSetups: [],
|
||||
});
|
||||
resolveChannelSetupEntries.mockReturnValue({
|
||||
entries: [],
|
||||
installedCatalogEntries: [],
|
||||
installableCatalogEntries: [],
|
||||
installedCatalogById: new Map(),
|
||||
installableCatalogById: new Map(),
|
||||
});
|
||||
collectChannelStatus.mockResolvedValue({
|
||||
installedPlugins: [],
|
||||
catalogEntries: [],
|
||||
@@ -163,4 +189,41 @@ describe("setupChannels workspace shadow exclusion", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("defers status and setup-plugin loads until a channel is selected", async () => {
|
||||
resolveChannelSetupEntries.mockReturnValue({
|
||||
entries: [
|
||||
{
|
||||
id: "telegram",
|
||||
meta: { id: "telegram", label: "Telegram", blurb: "" },
|
||||
},
|
||||
],
|
||||
installedCatalogEntries: [],
|
||||
installableCatalogEntries: [],
|
||||
installedCatalogById: new Map(),
|
||||
installableCatalogById: new Map(),
|
||||
});
|
||||
const select = vi.fn(async () => "__done__");
|
||||
|
||||
await setupChannels(
|
||||
{} as never,
|
||||
{} as never,
|
||||
{
|
||||
confirm: vi.fn(async () => true),
|
||||
note: vi.fn(async () => undefined),
|
||||
select,
|
||||
} as never,
|
||||
{
|
||||
deferStatusUntilSelection: true,
|
||||
skipConfirm: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(select).toHaveBeenCalledWith(expect.objectContaining({ message: "Select a channel" }));
|
||||
expect(collectChannelStatus).not.toHaveBeenCalled();
|
||||
expect(listTrustedChannelPluginCatalogEntries).not.toHaveBeenCalled();
|
||||
expect(listChannelSetupPlugins).not.toHaveBeenCalled();
|
||||
expect(getChannelSetupPlugin).not.toHaveBeenCalled();
|
||||
expect(loadChannelSetupPluginRegistrySnapshotForChannel).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ import { listTrustedChannelPluginCatalogEntries } from "../commands/channel-setu
|
||||
import type {
|
||||
ChannelSetupConfiguredResult,
|
||||
ChannelSetupResult,
|
||||
ChannelSetupStatus,
|
||||
ChannelOnboardingPostWriteHook,
|
||||
SetupChannelsOptions,
|
||||
} from "../commands/channel-setup/types.js";
|
||||
@@ -110,6 +111,7 @@ export async function setupChannels(
|
||||
options?: SetupChannelsOptions,
|
||||
): Promise<OpenClawConfig> {
|
||||
let next = cfg;
|
||||
const deferStatusUntilSelection = options?.deferStatusUntilSelection === true;
|
||||
const forceAllowFromChannels = new Set(options?.forceAllowFromChannels ?? []);
|
||||
const accountOverrides: Partial<Record<ChannelChoice, string>> = {
|
||||
...options?.accountIds,
|
||||
@@ -122,12 +124,18 @@ export async function setupChannels(
|
||||
options?.onResolvedPlugin?.(channel, plugin);
|
||||
};
|
||||
const getVisibleChannelPlugin = (channel: ChannelChoice): ChannelSetupPlugin | undefined =>
|
||||
scopedPluginsById.get(channel) ?? getChannelSetupPlugin(channel);
|
||||
const listVisibleInstalledPlugins = (): ChannelSetupPlugin[] => {
|
||||
scopedPluginsById.get(channel) ??
|
||||
(deferStatusUntilSelection ? undefined : getChannelSetupPlugin(channel));
|
||||
const listVisibleInstalledPlugins = (params?: {
|
||||
includeRegistry?: boolean;
|
||||
}): ChannelSetupPlugin[] => {
|
||||
const includeRegistry = params?.includeRegistry ?? !deferStatusUntilSelection;
|
||||
const merged = new Map<string, ChannelSetupPlugin>();
|
||||
for (const plugin of listChannelSetupPlugins()) {
|
||||
if (shouldShowChannelInSetup(plugin.meta)) {
|
||||
merged.set(plugin.id, plugin);
|
||||
if (includeRegistry) {
|
||||
for (const plugin of listChannelSetupPlugins()) {
|
||||
if (shouldShowChannelInSetup(plugin.meta)) {
|
||||
merged.set(plugin.id, plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const plugin of scopedPluginsById.values()) {
|
||||
@@ -137,10 +145,10 @@ export async function setupChannels(
|
||||
}
|
||||
return Array.from(merged.values());
|
||||
};
|
||||
const resolveVisibleChannelEntries = () =>
|
||||
const resolveVisibleChannelEntries = (params?: { includeRegistry?: boolean }) =>
|
||||
resolveChannelSetupEntries({
|
||||
cfg: next,
|
||||
installedPlugins: listVisibleInstalledPlugins(),
|
||||
installedPlugins: listVisibleInstalledPlugins(params),
|
||||
workspaceDir: resolveWorkspaceDir(),
|
||||
});
|
||||
const loadScopedChannelPlugin = async (
|
||||
@@ -172,7 +180,7 @@ export async function setupChannels(
|
||||
if (scopedPlugin) {
|
||||
return resolveChannelSetupWizardAdapterForPlugin(scopedPlugin);
|
||||
}
|
||||
return resolveChannelSetupWizardAdapterForPlugin(getChannelSetupPlugin(channel));
|
||||
return resolveChannelSetupWizardAdapterForPlugin(getVisibleChannelPlugin(channel));
|
||||
};
|
||||
const preloadConfiguredExternalPlugins = async () => {
|
||||
// Keep setup memory bounded by snapshot-loading only configured external plugins.
|
||||
@@ -194,15 +202,20 @@ export async function setupChannels(
|
||||
}
|
||||
await Promise.all(preloadTasks);
|
||||
};
|
||||
await preloadConfiguredExternalPlugins();
|
||||
if (!deferStatusUntilSelection) {
|
||||
await preloadConfiguredExternalPlugins();
|
||||
}
|
||||
|
||||
const { statusByChannel, statusLines } = await collectChannelStatus({
|
||||
cfg: next,
|
||||
options,
|
||||
accountOverrides,
|
||||
installedPlugins: listVisibleInstalledPlugins(),
|
||||
resolveAdapter: getVisibleSetupFlowAdapter,
|
||||
});
|
||||
const statusSummary = deferStatusUntilSelection
|
||||
? { statusByChannel: new Map<ChannelChoice, ChannelSetupStatus>(), statusLines: [] }
|
||||
: await collectChannelStatus({
|
||||
cfg: next,
|
||||
options,
|
||||
accountOverrides,
|
||||
installedPlugins: listVisibleInstalledPlugins(),
|
||||
resolveAdapter: getVisibleSetupFlowAdapter,
|
||||
});
|
||||
const { statusByChannel, statusLines } = statusSummary;
|
||||
if (!options?.skipStatusNote && statusLines.length > 0) {
|
||||
await prompter.note(statusLines.join("\n"), "Channel status");
|
||||
}
|
||||
@@ -217,7 +230,9 @@ export async function setupChannels(
|
||||
return cfg;
|
||||
}
|
||||
|
||||
const primerChannels = resolveVisibleChannelEntries().entries.map((entry) => ({
|
||||
const primerChannels = resolveVisibleChannelEntries({
|
||||
includeRegistry: !deferStatusUntilSelection,
|
||||
}).entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
label: entry.meta.label,
|
||||
blurb: entry.meta.blurb,
|
||||
@@ -225,7 +240,8 @@ export async function setupChannels(
|
||||
await noteChannelPrimer(prompter, primerChannels);
|
||||
|
||||
const quickstartDefault =
|
||||
options?.initialSelection?.[0] ?? resolveQuickstartDefault(statusByChannel);
|
||||
options?.initialSelection?.[0] ??
|
||||
(deferStatusUntilSelection ? undefined : resolveQuickstartDefault(statusByChannel));
|
||||
|
||||
const shouldPromptAccountIds = options?.promptAccountIds === true;
|
||||
const accountIdsByChannel = new Map<ChannelChoice, string>();
|
||||
@@ -243,7 +259,7 @@ export async function setupChannels(
|
||||
}
|
||||
};
|
||||
|
||||
const resolveDisabledHint = (channel: ChannelChoice): string | undefined => {
|
||||
const resolveConfigDisabledHint = (channel: ChannelChoice): string | undefined => {
|
||||
if (
|
||||
typeof (next.channels as Record<string, { enabled?: boolean }> | undefined)?.[channel]
|
||||
?.enabled === "boolean"
|
||||
@@ -252,14 +268,22 @@ export async function setupChannels(
|
||||
? "disabled"
|
||||
: undefined;
|
||||
}
|
||||
if (next.plugins?.entries?.[channel]?.enabled === false) {
|
||||
return "plugin disabled";
|
||||
}
|
||||
if (next.plugins?.enabled === false) {
|
||||
return "plugins disabled";
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const resolveDisabledHint = (channel: ChannelChoice): string | undefined => {
|
||||
const configDisabledHint = resolveConfigDisabledHint(channel);
|
||||
if (configDisabledHint || deferStatusUntilSelection) {
|
||||
return configDisabledHint;
|
||||
}
|
||||
const plugin = getVisibleChannelPlugin(channel);
|
||||
if (!plugin) {
|
||||
if (next.plugins?.entries?.[channel]?.enabled === false) {
|
||||
return "plugin disabled";
|
||||
}
|
||||
if (next.plugins?.enabled === false) {
|
||||
return "plugins disabled";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
const accountId = resolveChannelDefaultAccountId({ plugin, cfg: next });
|
||||
@@ -274,7 +298,9 @@ export async function setupChannels(
|
||||
};
|
||||
|
||||
const getChannelEntries = () => {
|
||||
const resolved = resolveVisibleChannelEntries();
|
||||
const resolved = resolveVisibleChannelEntries({
|
||||
includeRegistry: !deferStatusUntilSelection,
|
||||
});
|
||||
return {
|
||||
entries: resolved.entries,
|
||||
catalogById: resolved.installableCatalogById,
|
||||
@@ -485,7 +511,7 @@ export async function setupChannels(
|
||||
}
|
||||
await loadScopedChannelPlugin(channel, result.pluginId ?? catalogEntry.pluginId);
|
||||
await refreshStatus(channel);
|
||||
} else if (installedCatalogEntry) {
|
||||
} else if (installedCatalogEntry && installedCatalogEntry.origin !== "bundled") {
|
||||
const plugin = await loadScopedChannelPlugin(channel, installedCatalogEntry.pluginId);
|
||||
if (!plugin) {
|
||||
await prompter.note(`${channel} plugin not available.`, "Channel setup");
|
||||
@@ -581,7 +607,9 @@ export async function setupChannels(
|
||||
|
||||
const selectedLines = resolveChannelSelectionNoteLines({
|
||||
cfg: next,
|
||||
installedPlugins: listVisibleInstalledPlugins(),
|
||||
installedPlugins: listVisibleInstalledPlugins({
|
||||
includeRegistry: !deferStatusUntilSelection,
|
||||
}),
|
||||
selection,
|
||||
});
|
||||
if (selectedLines.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user