refactor: resolve channel env vars from plugin manifests
This commit is contained in:
@@ -609,8 +609,9 @@ conversation, and it runs after core approval handling finishes.
|
||||
|
||||
Provider plugins now have two layers:
|
||||
|
||||
- manifest metadata: `providerAuthEnvVars` for cheap env-auth lookup before
|
||||
runtime load, plus `providerAuthChoices` for cheap onboarding/auth-choice
|
||||
- manifest metadata: `providerAuthEnvVars` for cheap provider env-auth lookup
|
||||
before runtime load, `channelEnvVars` for cheap channel env/setup lookup
|
||||
before runtime load, plus `providerAuthChoices` for cheap onboarding/auth-choice
|
||||
labels and CLI flag metadata before runtime load
|
||||
- config-time hooks: `catalog` / legacy `discovery` plus `applyConfigDefaults`
|
||||
- runtime hooks: `normalizeModelId`, `normalizeTransport`,
|
||||
@@ -645,6 +646,10 @@ one-flag auth wiring without loading provider runtime. Keep provider runtime
|
||||
`envVars` for operator-facing hints such as onboarding labels or OAuth
|
||||
client-id/client-secret setup vars.
|
||||
|
||||
Use manifest `channelEnvVars` when a channel has env-driven auth or setup that
|
||||
generic shell-env fallback, config/status checks, or setup prompts should see
|
||||
without loading channel runtime.
|
||||
|
||||
### Hook order and usage
|
||||
|
||||
For model/provider plugins, OpenClaw calls hooks in this rough order.
|
||||
|
||||
@@ -93,6 +93,9 @@ Those belong in your plugin code and `package.json`.
|
||||
"providerAuthEnvVars": {
|
||||
"openrouter": ["OPENROUTER_API_KEY"]
|
||||
},
|
||||
"channelEnvVars": {
|
||||
"openrouter-chatops": ["OPENROUTER_CHATOPS_TOKEN"]
|
||||
},
|
||||
"providerAuthChoices": [
|
||||
{
|
||||
"provider": "openrouter",
|
||||
@@ -142,6 +145,7 @@ Those belong in your plugin code and `package.json`.
|
||||
| `modelSupport` | No | `object` | Manifest-owned shorthand model-family metadata used to auto-load the plugin before runtime. |
|
||||
| `cliBackends` | No | `string[]` | CLI inference backend ids owned by this plugin. Used for startup auto-activation from explicit config refs. |
|
||||
| `providerAuthEnvVars` | No | `Record<string, string[]>` | Cheap provider-auth env metadata that OpenClaw can inspect without loading plugin code. |
|
||||
| `channelEnvVars` | No | `Record<string, string[]>` | Cheap channel env metadata that OpenClaw can inspect without loading plugin code. Use this for env-driven channel setup or auth surfaces that generic startup/config helpers should see. |
|
||||
| `providerAuthChoices` | No | `object[]` | Cheap auth-choice metadata for onboarding pickers, preferred-provider resolution, and simple CLI flag wiring. |
|
||||
| `contracts` | No | `object` | Static bundled capability snapshot for speech, realtime transcription, realtime voice, media-understanding, image-generation, music-generation, video-generation, web-fetch, web search, and tool ownership. |
|
||||
| `channelConfigs` | No | `Record<string, object>` | Manifest-owned channel config metadata merged into discovery and validation surfaces before runtime loads. |
|
||||
@@ -436,6 +440,9 @@ See [Configuration reference](/gateway/configuration) for the full `plugins.*` s
|
||||
- `providerAuthEnvVars` is the cheap metadata path for auth probes, env-marker
|
||||
validation, and similar provider-auth surfaces that should not boot plugin
|
||||
runtime just to inspect env names.
|
||||
- `channelEnvVars` is the cheap metadata path for shell-env fallback, setup
|
||||
prompts, and similar channel surfaces that should not boot plugin runtime
|
||||
just to inspect env names.
|
||||
- `providerAuthChoices` is the cheap metadata path for auth-choice pickers,
|
||||
`--auth-choice` resolution, preferred-provider mapping, and simple onboarding
|
||||
CLI flag registration before provider runtime loads. For runtime wizard
|
||||
|
||||
@@ -108,9 +108,15 @@ For setup specifically:
|
||||
- `openclaw/plugin-sdk/channel-setup` covers the optional-install setup
|
||||
builders plus a few setup-safe primitives:
|
||||
`createOptionalChannelSetupSurface`, `createOptionalChannelSetupAdapter`,
|
||||
`createOptionalChannelSetupWizard`, `DEFAULT_ACCOUNT_ID`,
|
||||
`createTopLevelChannelDmPolicy`, `setSetupChannelEnabled`, and
|
||||
`splitSetupEntries`
|
||||
|
||||
If your channel supports env-driven setup or auth and generic startup/config
|
||||
flows should know those env names before runtime loads, declare them in the
|
||||
plugin manifest with `channelEnvVars`. Keep channel runtime `envVars` or local
|
||||
constants for operator-facing copy only.
|
||||
`createOptionalChannelSetupWizard`, `DEFAULT_ACCOUNT_ID`,
|
||||
`createTopLevelChannelDmPolicy`, `setSetupChannelEnabled`, and
|
||||
`splitSetupEntries`
|
||||
|
||||
- use the broader `openclaw/plugin-sdk/setup` seam only when you also need the
|
||||
heavier shared setup/config helpers such as
|
||||
`moveSingleAccountChannelSectionToDefaultAccount(...)`
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "discord",
|
||||
"channels": ["discord"],
|
||||
"channelEnvVars": {
|
||||
"discord": ["DISCORD_BOT_TOKEN"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
{
|
||||
"id": "feishu",
|
||||
"channels": ["feishu"],
|
||||
"channelEnvVars": {
|
||||
"feishu": [
|
||||
"FEISHU_APP_ID",
|
||||
"FEISHU_APP_SECRET",
|
||||
"FEISHU_VERIFICATION_TOKEN",
|
||||
"FEISHU_ENCRYPT_KEY"
|
||||
]
|
||||
},
|
||||
"skills": ["./skills"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "googlechat",
|
||||
"channels": ["googlechat"],
|
||||
"channelEnvVars": {
|
||||
"googlechat": ["GOOGLE_CHAT_SERVICE_ACCOUNT", "GOOGLE_CHAT_SERVICE_ACCOUNT_FILE"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
{
|
||||
"id": "irc",
|
||||
"channels": ["irc"],
|
||||
"channelEnvVars": {
|
||||
"irc": [
|
||||
"IRC_HOST",
|
||||
"IRC_PORT",
|
||||
"IRC_TLS",
|
||||
"IRC_NICK",
|
||||
"IRC_USERNAME",
|
||||
"IRC_REALNAME",
|
||||
"IRC_PASSWORD",
|
||||
"IRC_CHANNELS",
|
||||
"IRC_NICKSERV_PASSWORD",
|
||||
"IRC_NICKSERV_REGISTER_EMAIL"
|
||||
]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "line",
|
||||
"channels": ["line"],
|
||||
"channelEnvVars": {
|
||||
"line": ["LINE_CHANNEL_ACCESS_TOKEN", "LINE_CHANNEL_SECRET"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
{
|
||||
"id": "matrix",
|
||||
"channels": ["matrix"],
|
||||
"channelEnvVars": {
|
||||
"matrix": [
|
||||
"MATRIX_HOMESERVER",
|
||||
"MATRIX_USER_ID",
|
||||
"MATRIX_ACCESS_TOKEN",
|
||||
"MATRIX_PASSWORD",
|
||||
"MATRIX_DEVICE_ID",
|
||||
"MATRIX_DEVICE_NAME",
|
||||
"MATRIX_OPS_HOMESERVER",
|
||||
"MATRIX_OPS_ACCESS_TOKEN",
|
||||
"MATRIX_OPS_DEVICE_ID",
|
||||
"MATRIX_OPS_DEVICE_NAME"
|
||||
]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "mattermost",
|
||||
"channels": ["mattermost"],
|
||||
"channelEnvVars": {
|
||||
"mattermost": ["MATTERMOST_BOT_TOKEN", "MATTERMOST_URL"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "msteams",
|
||||
"channels": ["msteams"],
|
||||
"channelEnvVars": {
|
||||
"msteams": ["MSTEAMS_APP_ID", "MSTEAMS_APP_PASSWORD", "MSTEAMS_TENANT_ID"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "nextcloud-talk",
|
||||
"channels": ["nextcloud-talk"],
|
||||
"channelEnvVars": {
|
||||
"nextcloud-talk": ["NEXTCLOUD_TALK_BOT_SECRET", "NEXTCLOUD_TALK_API_PASSWORD"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "nostr",
|
||||
"channels": ["nostr"],
|
||||
"channelEnvVars": {
|
||||
"nostr": ["NOSTR_PRIVATE_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "qqbot",
|
||||
"channels": ["qqbot"],
|
||||
"channelEnvVars": {
|
||||
"qqbot": ["QQBOT_APP_ID", "QQBOT_CLIENT_SECRET"]
|
||||
},
|
||||
"skills": ["./skills"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "slack",
|
||||
"channels": ["slack"],
|
||||
"channelEnvVars": {
|
||||
"slack": ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "SLACK_USER_TOKEN"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
{
|
||||
"id": "synology-chat",
|
||||
"channels": ["synology-chat"],
|
||||
"channelEnvVars": {
|
||||
"synology-chat": [
|
||||
"SYNOLOGY_CHAT_TOKEN",
|
||||
"SYNOLOGY_CHAT_INCOMING_URL",
|
||||
"SYNOLOGY_NAS_HOST",
|
||||
"SYNOLOGY_ALLOWED_USER_IDS",
|
||||
"SYNOLOGY_RATE_LIMIT",
|
||||
"OPENCLAW_BOT_NAME"
|
||||
]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "telegram",
|
||||
"channels": ["telegram"],
|
||||
"channelEnvVars": {
|
||||
"telegram": ["TELEGRAM_BOT_TOKEN"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "twitch",
|
||||
"channels": ["twitch"],
|
||||
"channelEnvVars": {
|
||||
"twitch": ["OPENCLAW_TWITCH_ACCESS_TOKEN"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
{
|
||||
"id": "voice-call",
|
||||
"channelEnvVars": {
|
||||
"voice-call": [
|
||||
"TELNYX_API_KEY",
|
||||
"TELNYX_CONNECTION_ID",
|
||||
"TELNYX_PUBLIC_KEY",
|
||||
"TWILIO_ACCOUNT_SID",
|
||||
"TWILIO_AUTH_TOKEN",
|
||||
"PLIVO_AUTH_ID",
|
||||
"PLIVO_AUTH_TOKEN",
|
||||
"NGROK_AUTHTOKEN",
|
||||
"NGROK_DOMAIN"
|
||||
]
|
||||
},
|
||||
"uiHints": {
|
||||
"provider": {
|
||||
"label": "Provider",
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "zalo",
|
||||
"channels": ["zalo"],
|
||||
"channelEnvVars": {
|
||||
"zalo": ["ZALO_BOT_TOKEN", "ZALO_WEBHOOK_SECRET"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "zalouser",
|
||||
"channels": ["zalouser"],
|
||||
"channelEnvVars": {
|
||||
"zalouser": ["ZALOUSER_PROFILE", "ZCA_PROFILE"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const listKnownChannelEnvVarNames = vi.hoisted(() => vi.fn(() => ["DISCORD_BOT_TOKEN"]));
|
||||
const listKnownProviderAuthEnvVarNames = vi.hoisted(() => vi.fn(() => ["OPENAI_API_KEY"]));
|
||||
|
||||
vi.mock("../secrets/channel-env-vars.js", () => ({
|
||||
listKnownChannelEnvVarNames,
|
||||
}));
|
||||
|
||||
vi.mock("../secrets/provider-env-vars.js", () => ({
|
||||
listKnownProviderAuthEnvVarNames,
|
||||
}));
|
||||
|
||||
describe("config io shell env expected keys", () => {
|
||||
it("includes provider auth env vars from manifest-driven provider metadata", async () => {
|
||||
it("includes provider and channel env vars from manifest-driven plugin metadata", async () => {
|
||||
listKnownProviderAuthEnvVarNames.mockReturnValueOnce([
|
||||
"OPENAI_API_KEY",
|
||||
"ARCEEAI_API_KEY",
|
||||
"FIREWORKS_ALT_API_KEY",
|
||||
]);
|
||||
listKnownChannelEnvVarNames.mockReturnValueOnce([
|
||||
"DISCORD_BOT_TOKEN",
|
||||
"SLACK_BOT_TOKEN",
|
||||
"SLACK_APP_TOKEN",
|
||||
]);
|
||||
|
||||
vi.resetModules();
|
||||
const { resolveShellEnvExpectedKeys } = await import("./shell-env-expected-keys.js");
|
||||
@@ -22,8 +32,9 @@ describe("config io shell env expected keys", () => {
|
||||
"OPENAI_API_KEY",
|
||||
"ARCEEAI_API_KEY",
|
||||
"FIREWORKS_ALT_API_KEY",
|
||||
"OPENCLAW_GATEWAY_TOKEN",
|
||||
"DISCORD_BOT_TOKEN",
|
||||
"SLACK_BOT_TOKEN",
|
||||
"OPENCLAW_GATEWAY_TOKEN",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { listKnownChannelEnvVarNames } from "../secrets/channel-env-vars.js";
|
||||
import { listKnownProviderAuthEnvVarNames } from "../secrets/provider-env-vars.js";
|
||||
|
||||
const CORE_SHELL_ENV_EXPECTED_KEYS = [
|
||||
"TELEGRAM_BOT_TOKEN",
|
||||
"DISCORD_BOT_TOKEN",
|
||||
"SLACK_BOT_TOKEN",
|
||||
"SLACK_APP_TOKEN",
|
||||
"OPENCLAW_GATEWAY_TOKEN",
|
||||
"OPENCLAW_GATEWAY_PASSWORD",
|
||||
];
|
||||
const CORE_SHELL_ENV_EXPECTED_KEYS = ["OPENCLAW_GATEWAY_TOKEN", "OPENCLAW_GATEWAY_PASSWORD"];
|
||||
|
||||
export function resolveShellEnvExpectedKeys(env: NodeJS.ProcessEnv): string[] {
|
||||
return [
|
||||
...new Set([...listKnownProviderAuthEnvVarNames({ env }), ...CORE_SHELL_ENV_EXPECTED_KEYS]),
|
||||
...new Set([
|
||||
...listKnownProviderAuthEnvVarNames({ env }),
|
||||
...listKnownChannelEnvVarNames({ env }),
|
||||
...CORE_SHELL_ENV_EXPECTED_KEYS,
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -417,6 +417,28 @@ describe("loadPluginManifestRegistry", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves channel env metadata from plugin manifests", () => {
|
||||
const dir = makeTempDir();
|
||||
writeManifest(dir, {
|
||||
id: "slack",
|
||||
channels: ["slack"],
|
||||
channelEnvVars: {
|
||||
slack: ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "SLACK_USER_TOKEN"],
|
||||
},
|
||||
configSchema: { type: "object" },
|
||||
});
|
||||
|
||||
const registry = loadSingleCandidateRegistry({
|
||||
idHint: "slack",
|
||||
rootDir: dir,
|
||||
origin: "bundled",
|
||||
});
|
||||
|
||||
expect(registry.plugins[0]?.channelEnvVars).toEqual({
|
||||
slack: ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "SLACK_USER_TOKEN"],
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves channel config metadata from plugin manifests", () => {
|
||||
const dir = makeTempDir();
|
||||
writeManifest(dir, {
|
||||
|
||||
@@ -73,6 +73,7 @@ export type PluginManifestRecord = {
|
||||
modelSupport?: PluginManifestModelSupport;
|
||||
cliBackends: string[];
|
||||
providerAuthEnvVars?: Record<string, string[]>;
|
||||
channelEnvVars?: Record<string, string[]>;
|
||||
providerAuthChoices?: PluginManifest["providerAuthChoices"];
|
||||
skills: string[];
|
||||
settingsFiles?: string[];
|
||||
@@ -292,6 +293,7 @@ function buildRecord(params: {
|
||||
modelSupport: params.manifest.modelSupport,
|
||||
cliBackends: params.manifest.cliBackends ?? [],
|
||||
providerAuthEnvVars: params.manifest.providerAuthEnvVars,
|
||||
channelEnvVars: params.manifest.channelEnvVars,
|
||||
providerAuthChoices: params.manifest.providerAuthChoices,
|
||||
skills: params.manifest.skills ?? [],
|
||||
settingsFiles: [],
|
||||
|
||||
@@ -89,6 +89,8 @@ export type PluginManifest = {
|
||||
cliBackends?: string[];
|
||||
/** Cheap provider-auth env lookup without booting plugin runtime. */
|
||||
providerAuthEnvVars?: Record<string, string[]>;
|
||||
/** Cheap channel env lookup without booting plugin runtime. */
|
||||
channelEnvVars?: Record<string, string[]>;
|
||||
/**
|
||||
* Cheap onboarding/auth-choice metadata used by config validation, CLI help,
|
||||
* and non-runtime auth-choice routing before provider runtime loads.
|
||||
@@ -500,6 +502,7 @@ export function loadPluginManifest(
|
||||
const modelSupport = normalizeManifestModelSupport(raw.modelSupport);
|
||||
const cliBackends = normalizeStringList(raw.cliBackends);
|
||||
const providerAuthEnvVars = normalizeStringListRecord(raw.providerAuthEnvVars);
|
||||
const channelEnvVars = normalizeStringListRecord(raw.channelEnvVars);
|
||||
const providerAuthChoices = normalizeProviderAuthChoices(raw.providerAuthChoices);
|
||||
const skills = normalizeStringList(raw.skills);
|
||||
const contracts = normalizeManifestContracts(raw.contracts);
|
||||
@@ -527,6 +530,7 @@ export function loadPluginManifest(
|
||||
modelSupport,
|
||||
cliBackends,
|
||||
providerAuthEnvVars,
|
||||
channelEnvVars,
|
||||
providerAuthChoices,
|
||||
skills,
|
||||
name,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
type MockManifestRegistry = {
|
||||
plugins: Array<{
|
||||
id: string;
|
||||
origin: string;
|
||||
channelEnvVars?: Record<string, string[]>;
|
||||
}>;
|
||||
diagnostics: unknown[];
|
||||
};
|
||||
|
||||
const loadPluginManifestRegistry = vi.hoisted(() =>
|
||||
vi.fn<() => MockManifestRegistry>(() => ({ plugins: [], diagnostics: [] })),
|
||||
);
|
||||
|
||||
vi.mock("../plugins/manifest-registry.js", () => ({
|
||||
loadPluginManifestRegistry,
|
||||
}));
|
||||
|
||||
describe("channel env vars dynamic manifest metadata", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
loadPluginManifestRegistry.mockReset();
|
||||
loadPluginManifestRegistry.mockReturnValue({ plugins: [], diagnostics: [] });
|
||||
});
|
||||
|
||||
it("includes later-installed plugin env vars without a bundled generated map", async () => {
|
||||
loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "external-mattermost",
|
||||
origin: "global",
|
||||
channelEnvVars: {
|
||||
mattermost: ["MATTERMOST_BOT_TOKEN", "MATTERMOST_URL"],
|
||||
},
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const mod = await import("./channel-env-vars.js");
|
||||
|
||||
expect(mod.getChannelEnvVars("mattermost")).toEqual(["MATTERMOST_BOT_TOKEN", "MATTERMOST_URL"]);
|
||||
expect(mod.listKnownChannelEnvVarNames()).toEqual(
|
||||
expect.arrayContaining(["MATTERMOST_BOT_TOKEN", "MATTERMOST_URL"]),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
|
||||
type ChannelEnvVarLookupParams = {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
};
|
||||
|
||||
function appendUniqueEnvVarCandidates(
|
||||
target: Record<string, string[]>,
|
||||
channelId: string,
|
||||
keys: readonly string[],
|
||||
) {
|
||||
const normalizedChannelId = channelId.trim();
|
||||
if (!normalizedChannelId || keys.length === 0) {
|
||||
return;
|
||||
}
|
||||
const bucket = (target[normalizedChannelId] ??= []);
|
||||
const seen = new Set(bucket);
|
||||
for (const key of keys) {
|
||||
const normalizedKey = key.trim();
|
||||
if (!normalizedKey || seen.has(normalizedKey)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(normalizedKey);
|
||||
bucket.push(normalizedKey);
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveChannelEnvVars(
|
||||
params?: ChannelEnvVarLookupParams,
|
||||
): Record<string, readonly string[]> {
|
||||
const registry = loadPluginManifestRegistry({
|
||||
config: params?.config,
|
||||
workspaceDir: params?.workspaceDir,
|
||||
env: params?.env,
|
||||
});
|
||||
const candidates: Record<string, string[]> = Object.create(null) as Record<string, string[]>;
|
||||
for (const plugin of registry.plugins) {
|
||||
if (!plugin.channelEnvVars) {
|
||||
continue;
|
||||
}
|
||||
for (const [channelId, keys] of Object.entries(plugin.channelEnvVars).toSorted(
|
||||
([left], [right]) => left.localeCompare(right),
|
||||
)) {
|
||||
appendUniqueEnvVarCandidates(candidates, channelId, keys);
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
export function getChannelEnvVars(channelId: string, params?: ChannelEnvVarLookupParams): string[] {
|
||||
const channelEnvVars = resolveChannelEnvVars(params);
|
||||
const envVars = Object.hasOwn(channelEnvVars, channelId) ? channelEnvVars[channelId] : undefined;
|
||||
return Array.isArray(envVars) ? [...envVars] : [];
|
||||
}
|
||||
|
||||
export function listKnownChannelEnvVarNames(params?: ChannelEnvVarLookupParams): string[] {
|
||||
return [...new Set(Object.values(resolveChannelEnvVars(params)).flatMap((keys) => keys))];
|
||||
}
|
||||
Reference in New Issue
Block a user