feat(onboarding): wire plugin-backed auth choices
This commit is contained in:
@@ -13,6 +13,7 @@ export type AuthChoiceGroupId =
|
|||||||
| "openai"
|
| "openai"
|
||||||
| "anthropic"
|
| "anthropic"
|
||||||
| "google"
|
| "google"
|
||||||
|
| "copilot"
|
||||||
| "openrouter"
|
| "openrouter"
|
||||||
| "ai-gateway"
|
| "ai-gateway"
|
||||||
| "moonshot"
|
| "moonshot"
|
||||||
@@ -68,8 +69,14 @@ const AUTH_CHOICE_GROUP_DEFS: {
|
|||||||
{
|
{
|
||||||
value: "google",
|
value: "google",
|
||||||
label: "Google",
|
label: "Google",
|
||||||
hint: "Gemini API key",
|
hint: "Gemini API key + OAuth",
|
||||||
choices: ["gemini-api-key"],
|
choices: ["gemini-api-key", "google-antigravity", "google-gemini-cli"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "copilot",
|
||||||
|
label: "Copilot",
|
||||||
|
hint: "GitHub + local proxy",
|
||||||
|
choices: ["github-copilot", "copilot-proxy"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "openrouter",
|
value: "openrouter",
|
||||||
@@ -195,8 +202,23 @@ export function buildAuthChoiceOptions(params: {
|
|||||||
hint: "Uses GitHub device flow",
|
hint: "Uses GitHub device flow",
|
||||||
});
|
});
|
||||||
options.push({ value: "gemini-api-key", label: "Google Gemini API key" });
|
options.push({ value: "gemini-api-key", label: "Google Gemini API key" });
|
||||||
|
options.push({
|
||||||
|
value: "google-antigravity",
|
||||||
|
label: "Google Antigravity OAuth",
|
||||||
|
hint: "Uses the bundled Antigravity auth plugin",
|
||||||
|
});
|
||||||
|
options.push({
|
||||||
|
value: "google-gemini-cli",
|
||||||
|
label: "Google Gemini CLI OAuth",
|
||||||
|
hint: "Uses the bundled Gemini CLI auth plugin",
|
||||||
|
});
|
||||||
options.push({ value: "zai-api-key", label: "Z.AI (GLM 4.7) API key" });
|
options.push({ value: "zai-api-key", label: "Z.AI (GLM 4.7) API key" });
|
||||||
options.push({ value: "qwen-portal", label: "Qwen OAuth" });
|
options.push({ value: "qwen-portal", label: "Qwen OAuth" });
|
||||||
|
options.push({
|
||||||
|
value: "copilot-proxy",
|
||||||
|
label: "Copilot Proxy (local)",
|
||||||
|
hint: "Local proxy for VS Code Copilot models",
|
||||||
|
});
|
||||||
options.push({ value: "apiKey", label: "Anthropic API key" });
|
options.push({ value: "apiKey", label: "Anthropic API key" });
|
||||||
// Token flow is currently Anthropic-only; use CLI for advanced providers.
|
// Token flow is currently Anthropic-only; use CLI for advanced providers.
|
||||||
options.push({
|
options.push({
|
||||||
|
|||||||
14
src/commands/auth-choice.apply.copilot-proxy.ts
Normal file
14
src/commands/auth-choice.apply.copilot-proxy.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||||
|
import { applyAuthChoicePluginProvider } from "./auth-choice.apply.plugin-provider.js";
|
||||||
|
|
||||||
|
export async function applyAuthChoiceCopilotProxy(
|
||||||
|
params: ApplyAuthChoiceParams,
|
||||||
|
): Promise<ApplyAuthChoiceResult | null> {
|
||||||
|
return await applyAuthChoicePluginProvider(params, {
|
||||||
|
authChoice: "copilot-proxy",
|
||||||
|
pluginId: "copilot-proxy",
|
||||||
|
providerId: "copilot-proxy",
|
||||||
|
methodId: "local",
|
||||||
|
label: "Copilot Proxy",
|
||||||
|
});
|
||||||
|
}
|
||||||
14
src/commands/auth-choice.apply.google-antigravity.ts
Normal file
14
src/commands/auth-choice.apply.google-antigravity.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||||
|
import { applyAuthChoicePluginProvider } from "./auth-choice.apply.plugin-provider.js";
|
||||||
|
|
||||||
|
export async function applyAuthChoiceGoogleAntigravity(
|
||||||
|
params: ApplyAuthChoiceParams,
|
||||||
|
): Promise<ApplyAuthChoiceResult | null> {
|
||||||
|
return await applyAuthChoicePluginProvider(params, {
|
||||||
|
authChoice: "google-antigravity",
|
||||||
|
pluginId: "google-antigravity-auth",
|
||||||
|
providerId: "google-antigravity",
|
||||||
|
methodId: "oauth",
|
||||||
|
label: "Google Antigravity",
|
||||||
|
});
|
||||||
|
}
|
||||||
14
src/commands/auth-choice.apply.google-gemini-cli.ts
Normal file
14
src/commands/auth-choice.apply.google-gemini-cli.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||||
|
import { applyAuthChoicePluginProvider } from "./auth-choice.apply.plugin-provider.js";
|
||||||
|
|
||||||
|
export async function applyAuthChoiceGoogleGeminiCli(
|
||||||
|
params: ApplyAuthChoiceParams,
|
||||||
|
): Promise<ApplyAuthChoiceResult | null> {
|
||||||
|
return await applyAuthChoicePluginProvider(params, {
|
||||||
|
authChoice: "google-gemini-cli",
|
||||||
|
pluginId: "google-gemini-cli-auth",
|
||||||
|
providerId: "google-gemini-cli",
|
||||||
|
methodId: "oauth",
|
||||||
|
label: "Google Gemini CLI",
|
||||||
|
});
|
||||||
|
}
|
||||||
197
src/commands/auth-choice.apply.plugin-provider.ts
Normal file
197
src/commands/auth-choice.apply.plugin-provider.ts
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import { resolveClawdbotAgentDir } from "../agents/agent-paths.js";
|
||||||
|
import {
|
||||||
|
resolveDefaultAgentId,
|
||||||
|
resolveAgentDir,
|
||||||
|
resolveAgentWorkspaceDir,
|
||||||
|
} from "../agents/agent-scope.js";
|
||||||
|
import { upsertAuthProfile } from "../agents/auth-profiles.js";
|
||||||
|
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||||
|
import { resolveDefaultAgentWorkspaceDir } from "../agents/workspace.js";
|
||||||
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
|
import { enablePluginInConfig } from "../plugins/enable.js";
|
||||||
|
import { resolvePluginProviders } from "../plugins/providers.js";
|
||||||
|
import type { ProviderAuthMethod, ProviderPlugin } from "../plugins/types.js";
|
||||||
|
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||||
|
import { applyAuthProfileConfig } from "./onboard-auth.js";
|
||||||
|
import { openUrl } from "./onboard-helpers.js";
|
||||||
|
import { createVpsAwareOAuthHandlers } from "./oauth-flow.js";
|
||||||
|
import { isRemoteEnvironment } from "./oauth-env.js";
|
||||||
|
|
||||||
|
export type PluginProviderAuthChoiceOptions = {
|
||||||
|
authChoice: string;
|
||||||
|
pluginId: string;
|
||||||
|
providerId: string;
|
||||||
|
methodId?: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveProviderMatch(
|
||||||
|
providers: ProviderPlugin[],
|
||||||
|
rawProvider: string,
|
||||||
|
): ProviderPlugin | null {
|
||||||
|
const normalized = normalizeProviderId(rawProvider);
|
||||||
|
return (
|
||||||
|
providers.find((provider) => normalizeProviderId(provider.id) === normalized) ??
|
||||||
|
providers.find(
|
||||||
|
(provider) =>
|
||||||
|
provider.aliases?.some((alias) => normalizeProviderId(alias) === normalized) ?? false,
|
||||||
|
) ??
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickAuthMethod(provider: ProviderPlugin, rawMethod?: string): ProviderAuthMethod | null {
|
||||||
|
const raw = rawMethod?.trim();
|
||||||
|
if (!raw) return null;
|
||||||
|
const normalized = raw.toLowerCase();
|
||||||
|
return (
|
||||||
|
provider.auth.find((method) => method.id.toLowerCase() === normalized) ??
|
||||||
|
provider.auth.find((method) => method.label.toLowerCase() === normalized) ??
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeConfigPatch<T>(base: T, patch: unknown): T {
|
||||||
|
if (!isPlainRecord(base) || !isPlainRecord(patch)) {
|
||||||
|
return patch as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next: Record<string, unknown> = { ...base };
|
||||||
|
for (const [key, value] of Object.entries(patch)) {
|
||||||
|
const existing = next[key];
|
||||||
|
if (isPlainRecord(existing) && isPlainRecord(value)) {
|
||||||
|
next[key] = mergeConfigPatch(existing, value);
|
||||||
|
} else {
|
||||||
|
next[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDefaultModel(cfg: ClawdbotConfig, model: string): ClawdbotConfig {
|
||||||
|
const models = { ...cfg.agents?.defaults?.models };
|
||||||
|
models[model] = models[model] ?? {};
|
||||||
|
|
||||||
|
const existingModel = cfg.agents?.defaults?.model;
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
agents: {
|
||||||
|
...cfg.agents,
|
||||||
|
defaults: {
|
||||||
|
...cfg.agents?.defaults,
|
||||||
|
models,
|
||||||
|
model: {
|
||||||
|
...(existingModel && typeof existingModel === "object" && "fallbacks" in existingModel
|
||||||
|
? { fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks }
|
||||||
|
: undefined),
|
||||||
|
primary: model,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applyAuthChoicePluginProvider(
|
||||||
|
params: ApplyAuthChoiceParams,
|
||||||
|
options: PluginProviderAuthChoiceOptions,
|
||||||
|
): Promise<ApplyAuthChoiceResult | null> {
|
||||||
|
if (params.authChoice !== options.authChoice) return null;
|
||||||
|
|
||||||
|
const enableResult = enablePluginInConfig(params.config, options.pluginId);
|
||||||
|
let nextConfig = enableResult.config;
|
||||||
|
if (!enableResult.enabled) {
|
||||||
|
await params.prompter.note(
|
||||||
|
`${options.label} plugin is disabled (${enableResult.reason ?? "blocked"}).`,
|
||||||
|
options.label,
|
||||||
|
);
|
||||||
|
return { config: nextConfig };
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentId = params.agentId ?? resolveDefaultAgentId(nextConfig);
|
||||||
|
const defaultAgentId = resolveDefaultAgentId(nextConfig);
|
||||||
|
const agentDir =
|
||||||
|
params.agentDir ??
|
||||||
|
(agentId === defaultAgentId ? resolveClawdbotAgentDir() : resolveAgentDir(nextConfig, agentId));
|
||||||
|
const workspaceDir =
|
||||||
|
resolveAgentWorkspaceDir(nextConfig, agentId) ?? resolveDefaultAgentWorkspaceDir();
|
||||||
|
|
||||||
|
const providers = resolvePluginProviders({ config: nextConfig, workspaceDir });
|
||||||
|
const provider = resolveProviderMatch(providers, options.providerId);
|
||||||
|
if (!provider) {
|
||||||
|
await params.prompter.note(
|
||||||
|
`${options.label} auth plugin is not available. Enable it and re-run the wizard.`,
|
||||||
|
options.label,
|
||||||
|
);
|
||||||
|
return { config: nextConfig };
|
||||||
|
}
|
||||||
|
|
||||||
|
const method = pickAuthMethod(provider, options.methodId) ?? provider.auth[0];
|
||||||
|
if (!method) {
|
||||||
|
await params.prompter.note(`${options.label} auth method missing.`, options.label);
|
||||||
|
return { config: nextConfig };
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRemote = isRemoteEnvironment();
|
||||||
|
const result = await method.run({
|
||||||
|
config: nextConfig,
|
||||||
|
agentDir,
|
||||||
|
workspaceDir,
|
||||||
|
prompter: params.prompter,
|
||||||
|
runtime: params.runtime,
|
||||||
|
isRemote,
|
||||||
|
openUrl: async (url) => {
|
||||||
|
await openUrl(url);
|
||||||
|
},
|
||||||
|
oauth: {
|
||||||
|
createVpsAwareHandlers: (opts) => createVpsAwareOAuthHandlers(opts),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.configPatch) {
|
||||||
|
nextConfig = mergeConfigPatch(nextConfig, result.configPatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const profile of result.profiles) {
|
||||||
|
upsertAuthProfile({
|
||||||
|
profileId: profile.profileId,
|
||||||
|
credential: profile.credential,
|
||||||
|
agentDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||||
|
profileId: profile.profileId,
|
||||||
|
provider: profile.credential.provider,
|
||||||
|
mode: profile.credential.type === "token" ? "token" : profile.credential.type,
|
||||||
|
...("email" in profile.credential && profile.credential.email
|
||||||
|
? { email: profile.credential.email }
|
||||||
|
: {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let agentModelOverride: string | undefined;
|
||||||
|
if (result.defaultModel) {
|
||||||
|
if (params.setDefaultModel) {
|
||||||
|
nextConfig = applyDefaultModel(nextConfig, result.defaultModel);
|
||||||
|
await params.prompter.note(
|
||||||
|
`Default model set to ${result.defaultModel}`,
|
||||||
|
"Model configured",
|
||||||
|
);
|
||||||
|
} else if (params.agentId) {
|
||||||
|
agentModelOverride = result.defaultModel;
|
||||||
|
await params.prompter.note(
|
||||||
|
`Default model set to ${result.defaultModel} for agent "${params.agentId}".`,
|
||||||
|
"Model configured",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.notes && result.notes.length > 0) {
|
||||||
|
await params.prompter.note(result.notes.join("\n"), "Provider notes");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { config: nextConfig, agentModelOverride };
|
||||||
|
}
|
||||||
@@ -1,195 +1,14 @@
|
|||||||
import { resolveClawdbotAgentDir } from "../agents/agent-paths.js";
|
|
||||||
import {
|
|
||||||
resolveDefaultAgentId,
|
|
||||||
resolveAgentDir,
|
|
||||||
resolveAgentWorkspaceDir,
|
|
||||||
} from "../agents/agent-scope.js";
|
|
||||||
import { upsertAuthProfile } from "../agents/auth-profiles.js";
|
|
||||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
|
||||||
import { resolveDefaultAgentWorkspaceDir } from "../agents/workspace.js";
|
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
|
||||||
import { resolvePluginProviders } from "../plugins/providers.js";
|
|
||||||
import type { ProviderAuthMethod, ProviderPlugin } from "../plugins/types.js";
|
|
||||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||||
import { applyAuthProfileConfig } from "./onboard-auth.js";
|
import { applyAuthChoicePluginProvider } from "./auth-choice.apply.plugin-provider.js";
|
||||||
import { openUrl } from "./onboard-helpers.js";
|
|
||||||
import { createVpsAwareOAuthHandlers } from "./oauth-flow.js";
|
|
||||||
import { isRemoteEnvironment } from "./oauth-env.js";
|
|
||||||
|
|
||||||
const PLUGIN_ID = "qwen-portal-auth";
|
|
||||||
const PROVIDER_ID = "qwen-portal";
|
|
||||||
|
|
||||||
function enableBundledPlugin(cfg: ClawdbotConfig): ClawdbotConfig {
|
|
||||||
const existingEntry = cfg.plugins?.entries?.[PLUGIN_ID];
|
|
||||||
return {
|
|
||||||
...cfg,
|
|
||||||
plugins: {
|
|
||||||
...cfg.plugins,
|
|
||||||
entries: {
|
|
||||||
...cfg.plugins?.entries,
|
|
||||||
[PLUGIN_ID]: {
|
|
||||||
...existingEntry,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveProviderMatch(
|
|
||||||
providers: ProviderPlugin[],
|
|
||||||
rawProvider: string,
|
|
||||||
): ProviderPlugin | null {
|
|
||||||
const normalized = normalizeProviderId(rawProvider);
|
|
||||||
return (
|
|
||||||
providers.find((provider) => normalizeProviderId(provider.id) === normalized) ??
|
|
||||||
providers.find(
|
|
||||||
(provider) =>
|
|
||||||
provider.aliases?.some((alias) => normalizeProviderId(alias) === normalized) ?? false,
|
|
||||||
) ??
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function pickAuthMethod(provider: ProviderPlugin, rawMethod?: string): ProviderAuthMethod | null {
|
|
||||||
const raw = rawMethod?.trim();
|
|
||||||
if (!raw) return null;
|
|
||||||
const normalized = raw.toLowerCase();
|
|
||||||
return (
|
|
||||||
provider.auth.find((method) => method.id.toLowerCase() === normalized) ??
|
|
||||||
provider.auth.find((method) => method.label.toLowerCase() === normalized) ??
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
||||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeConfigPatch<T>(base: T, patch: unknown): T {
|
|
||||||
if (!isPlainRecord(base) || !isPlainRecord(patch)) {
|
|
||||||
return patch as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
const next: Record<string, unknown> = { ...base };
|
|
||||||
for (const [key, value] of Object.entries(patch)) {
|
|
||||||
const existing = next[key];
|
|
||||||
if (isPlainRecord(existing) && isPlainRecord(value)) {
|
|
||||||
next[key] = mergeConfigPatch(existing, value);
|
|
||||||
} else {
|
|
||||||
next[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return next as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyDefaultModel(cfg: ClawdbotConfig, model: string): ClawdbotConfig {
|
|
||||||
const models = { ...cfg.agents?.defaults?.models };
|
|
||||||
models[model] = models[model] ?? {};
|
|
||||||
|
|
||||||
const existingModel = cfg.agents?.defaults?.model;
|
|
||||||
return {
|
|
||||||
...cfg,
|
|
||||||
agents: {
|
|
||||||
...cfg.agents,
|
|
||||||
defaults: {
|
|
||||||
...cfg.agents?.defaults,
|
|
||||||
models,
|
|
||||||
model: {
|
|
||||||
...(existingModel && typeof existingModel === "object" && "fallbacks" in existingModel
|
|
||||||
? { fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks }
|
|
||||||
: undefined),
|
|
||||||
primary: model,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function applyAuthChoiceQwenPortal(
|
export async function applyAuthChoiceQwenPortal(
|
||||||
params: ApplyAuthChoiceParams,
|
params: ApplyAuthChoiceParams,
|
||||||
): Promise<ApplyAuthChoiceResult | null> {
|
): Promise<ApplyAuthChoiceResult | null> {
|
||||||
if (params.authChoice !== "qwen-portal") return null;
|
return await applyAuthChoicePluginProvider(params, {
|
||||||
|
authChoice: "qwen-portal",
|
||||||
let nextConfig = enableBundledPlugin(params.config);
|
pluginId: "qwen-portal-auth",
|
||||||
const agentId = params.agentId ?? resolveDefaultAgentId(nextConfig);
|
providerId: "qwen-portal",
|
||||||
const defaultAgentId = resolveDefaultAgentId(nextConfig);
|
methodId: "device",
|
||||||
const agentDir =
|
label: "Qwen",
|
||||||
params.agentDir ??
|
|
||||||
(agentId === defaultAgentId ? resolveClawdbotAgentDir() : resolveAgentDir(nextConfig, agentId));
|
|
||||||
const workspaceDir =
|
|
||||||
resolveAgentWorkspaceDir(nextConfig, agentId) ?? resolveDefaultAgentWorkspaceDir();
|
|
||||||
|
|
||||||
const providers = resolvePluginProviders({ config: nextConfig, workspaceDir });
|
|
||||||
const provider = resolveProviderMatch(providers, PROVIDER_ID);
|
|
||||||
if (!provider) {
|
|
||||||
await params.prompter.note(
|
|
||||||
"Qwen auth plugin is not available. Run `clawdbot plugins enable qwen-portal-auth` and re-run the wizard.",
|
|
||||||
"Qwen",
|
|
||||||
);
|
|
||||||
return { config: nextConfig };
|
|
||||||
}
|
|
||||||
|
|
||||||
const method = pickAuthMethod(provider, "device") ?? provider.auth[0];
|
|
||||||
if (!method) {
|
|
||||||
await params.prompter.note("Qwen auth method missing.", "Qwen");
|
|
||||||
return { config: nextConfig };
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRemote = isRemoteEnvironment();
|
|
||||||
const result = await method.run({
|
|
||||||
config: nextConfig,
|
|
||||||
agentDir,
|
|
||||||
workspaceDir,
|
|
||||||
prompter: params.prompter,
|
|
||||||
runtime: params.runtime,
|
|
||||||
isRemote,
|
|
||||||
openUrl: async (url) => {
|
|
||||||
await openUrl(url);
|
|
||||||
},
|
|
||||||
oauth: {
|
|
||||||
createVpsAwareHandlers: (opts) => createVpsAwareOAuthHandlers(opts),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.configPatch) {
|
|
||||||
nextConfig = mergeConfigPatch(nextConfig, result.configPatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const profile of result.profiles) {
|
|
||||||
upsertAuthProfile({
|
|
||||||
profileId: profile.profileId,
|
|
||||||
credential: profile.credential,
|
|
||||||
agentDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
|
||||||
profileId: profile.profileId,
|
|
||||||
provider: profile.credential.provider,
|
|
||||||
mode: profile.credential.type === "token" ? "token" : profile.credential.type,
|
|
||||||
...("email" in profile.credential && profile.credential.email
|
|
||||||
? { email: profile.credential.email }
|
|
||||||
: {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let agentModelOverride: string | undefined;
|
|
||||||
if (result.defaultModel) {
|
|
||||||
if (params.setDefaultModel) {
|
|
||||||
nextConfig = applyDefaultModel(nextConfig, result.defaultModel);
|
|
||||||
await params.prompter.note(`Default model set to ${result.defaultModel}`, "Model configured");
|
|
||||||
} else if (params.agentId) {
|
|
||||||
agentModelOverride = result.defaultModel;
|
|
||||||
await params.prompter.note(
|
|
||||||
`Default model set to ${result.defaultModel} for agent "${params.agentId}".`,
|
|
||||||
"Model configured",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.notes && result.notes.length > 0) {
|
|
||||||
await params.prompter.note(result.notes.join("\n"), "Provider notes");
|
|
||||||
}
|
|
||||||
|
|
||||||
return { config: nextConfig, agentModelOverride };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import type { RuntimeEnv } from "../runtime.js";
|
|||||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||||
import { applyAuthChoiceAnthropic } from "./auth-choice.apply.anthropic.js";
|
import { applyAuthChoiceAnthropic } from "./auth-choice.apply.anthropic.js";
|
||||||
import { applyAuthChoiceApiProviders } from "./auth-choice.apply.api-providers.js";
|
import { applyAuthChoiceApiProviders } from "./auth-choice.apply.api-providers.js";
|
||||||
|
import { applyAuthChoiceCopilotProxy } from "./auth-choice.apply.copilot-proxy.js";
|
||||||
import { applyAuthChoiceGitHubCopilot } from "./auth-choice.apply.github-copilot.js";
|
import { applyAuthChoiceGitHubCopilot } from "./auth-choice.apply.github-copilot.js";
|
||||||
|
import { applyAuthChoiceGoogleAntigravity } from "./auth-choice.apply.google-antigravity.js";
|
||||||
|
import { applyAuthChoiceGoogleGeminiCli } from "./auth-choice.apply.google-gemini-cli.js";
|
||||||
import { applyAuthChoiceMiniMax } from "./auth-choice.apply.minimax.js";
|
import { applyAuthChoiceMiniMax } from "./auth-choice.apply.minimax.js";
|
||||||
import { applyAuthChoiceOAuth } from "./auth-choice.apply.oauth.js";
|
import { applyAuthChoiceOAuth } from "./auth-choice.apply.oauth.js";
|
||||||
import { applyAuthChoiceOpenAI } from "./auth-choice.apply.openai.js";
|
import { applyAuthChoiceOpenAI } from "./auth-choice.apply.openai.js";
|
||||||
@@ -35,6 +38,9 @@ export async function applyAuthChoice(
|
|||||||
applyAuthChoiceApiProviders,
|
applyAuthChoiceApiProviders,
|
||||||
applyAuthChoiceMiniMax,
|
applyAuthChoiceMiniMax,
|
||||||
applyAuthChoiceGitHubCopilot,
|
applyAuthChoiceGitHubCopilot,
|
||||||
|
applyAuthChoiceGoogleAntigravity,
|
||||||
|
applyAuthChoiceGoogleGeminiCli,
|
||||||
|
applyAuthChoiceCopilotProxy,
|
||||||
applyAuthChoiceQwenPortal,
|
applyAuthChoiceQwenPortal,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,12 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
|
|||||||
"moonshot-api-key": "moonshot",
|
"moonshot-api-key": "moonshot",
|
||||||
"kimi-code-api-key": "kimi-code",
|
"kimi-code-api-key": "kimi-code",
|
||||||
"gemini-api-key": "google",
|
"gemini-api-key": "google",
|
||||||
|
"google-antigravity": "google-antigravity",
|
||||||
|
"google-gemini-cli": "google-gemini-cli",
|
||||||
"zai-api-key": "zai",
|
"zai-api-key": "zai",
|
||||||
"synthetic-api-key": "synthetic",
|
"synthetic-api-key": "synthetic",
|
||||||
"github-copilot": "github-copilot",
|
"github-copilot": "github-copilot",
|
||||||
|
"copilot-proxy": "copilot-proxy",
|
||||||
"minimax-cloud": "minimax",
|
"minimax-cloud": "minimax",
|
||||||
"minimax-api": "minimax",
|
"minimax-api": "minimax",
|
||||||
"minimax-api-lightning": "minimax",
|
"minimax-api-lightning": "minimax",
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||||
import { listChannelPluginCatalogEntries } from "../channels/plugins/catalog.js";
|
import { listChannelPluginCatalogEntries } from "../channels/plugins/catalog.js";
|
||||||
import { listChannelPlugins, getChannelPlugin } from "../channels/plugins/index.js";
|
import { listChannelPlugins, getChannelPlugin } from "../channels/plugins/index.js";
|
||||||
import { formatChannelPrimerLine, formatChannelSelectionLine } from "../channels/registry.js";
|
import type { ChannelMeta } from "../channels/plugins/types.js";
|
||||||
|
import {
|
||||||
|
formatChannelPrimerLine,
|
||||||
|
formatChannelSelectionLine,
|
||||||
|
listChatChannels,
|
||||||
|
} from "../channels/registry.js";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
|
import { isChannelConfigured } from "../config/plugin-auto-enable.js";
|
||||||
import type { DmPolicy } from "../config/types.js";
|
import type { DmPolicy } from "../config/types.js";
|
||||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
||||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { formatDocsLink } from "../terminal/links.js";
|
import { formatDocsLink } from "../terminal/links.js";
|
||||||
|
import { enablePluginInConfig } from "../plugins/enable.js";
|
||||||
import type { WizardPrompter, WizardSelectOption } from "../wizard/prompts.js";
|
import type { WizardPrompter, WizardSelectOption } from "../wizard/prompts.js";
|
||||||
import type { ChannelChoice } from "./onboard-types.js";
|
import type { ChannelChoice } from "./onboard-types.js";
|
||||||
import {
|
import {
|
||||||
@@ -115,6 +122,20 @@ async function collectChannelStatus(params: {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
const statusByChannel = new Map(statusEntries.map((entry) => [entry.channel, entry]));
|
||||||
|
const fallbackStatuses = listChatChannels()
|
||||||
|
.filter((meta) => !statusByChannel.has(meta.id))
|
||||||
|
.map((meta) => {
|
||||||
|
const configured = isChannelConfigured(params.cfg, meta.id);
|
||||||
|
const statusLabel = configured ? "configured (plugin disabled)" : "not configured";
|
||||||
|
return {
|
||||||
|
channel: meta.id,
|
||||||
|
configured,
|
||||||
|
statusLines: [`${meta.label}: ${statusLabel}`],
|
||||||
|
selectionHint: configured ? "configured · plugin disabled" : "not configured",
|
||||||
|
quickstartScore: 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
const catalogStatuses = catalogEntries.map((entry) => ({
|
const catalogStatuses = catalogEntries.map((entry) => ({
|
||||||
channel: entry.id,
|
channel: entry.id,
|
||||||
configured: false,
|
configured: false,
|
||||||
@@ -122,13 +143,13 @@ async function collectChannelStatus(params: {
|
|||||||
selectionHint: "plugin · install",
|
selectionHint: "plugin · install",
|
||||||
quickstartScore: 0,
|
quickstartScore: 0,
|
||||||
}));
|
}));
|
||||||
const combinedStatuses = [...statusEntries, ...catalogStatuses];
|
const combinedStatuses = [...statusEntries, ...fallbackStatuses, ...catalogStatuses];
|
||||||
const statusByChannel = new Map(combinedStatuses.map((entry) => [entry.channel, entry]));
|
const mergedStatusByChannel = new Map(combinedStatuses.map((entry) => [entry.channel, entry]));
|
||||||
const statusLines = combinedStatuses.flatMap((entry) => entry.statusLines);
|
const statusLines = combinedStatuses.flatMap((entry) => entry.statusLines);
|
||||||
return {
|
return {
|
||||||
installedPlugins,
|
installedPlugins,
|
||||||
catalogEntries,
|
catalogEntries,
|
||||||
statusByChannel,
|
statusByChannel: mergedStatusByChannel,
|
||||||
statusLines,
|
statusLines,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -270,17 +291,28 @@ export async function setupChannels(
|
|||||||
});
|
});
|
||||||
if (!shouldConfigure) return cfg;
|
if (!shouldConfigure) return cfg;
|
||||||
|
|
||||||
|
const corePrimer = listChatChannels().map((meta) => ({
|
||||||
|
id: meta.id as ChannelChoice,
|
||||||
|
label: meta.label,
|
||||||
|
blurb: meta.blurb,
|
||||||
|
}));
|
||||||
|
const coreIds = new Set(corePrimer.map((entry) => entry.id));
|
||||||
const primerChannels = [
|
const primerChannels = [
|
||||||
...installedPlugins.map((plugin) => ({
|
...corePrimer,
|
||||||
id: plugin.id as ChannelChoice,
|
...installedPlugins
|
||||||
label: plugin.meta.label,
|
.filter((plugin) => !coreIds.has(plugin.id as ChannelChoice))
|
||||||
blurb: plugin.meta.blurb,
|
.map((plugin) => ({
|
||||||
})),
|
id: plugin.id as ChannelChoice,
|
||||||
...catalogEntries.map((entry) => ({
|
label: plugin.meta.label,
|
||||||
id: entry.id as ChannelChoice,
|
blurb: plugin.meta.blurb,
|
||||||
label: entry.meta.label,
|
})),
|
||||||
blurb: entry.meta.blurb,
|
...catalogEntries
|
||||||
})),
|
.filter((entry) => !coreIds.has(entry.id as ChannelChoice))
|
||||||
|
.map((entry) => ({
|
||||||
|
id: entry.id as ChannelChoice,
|
||||||
|
label: entry.meta.label,
|
||||||
|
blurb: entry.meta.blurb,
|
||||||
|
})),
|
||||||
];
|
];
|
||||||
await noteChannelPrimer(prompter, primerChannels);
|
await noteChannelPrimer(prompter, primerChannels);
|
||||||
|
|
||||||
@@ -301,7 +333,11 @@ export async function setupChannels(
|
|||||||
|
|
||||||
const resolveDisabledHint = (channel: ChannelChoice): string | undefined => {
|
const resolveDisabledHint = (channel: ChannelChoice): string | undefined => {
|
||||||
const plugin = getChannelPlugin(channel);
|
const plugin = getChannelPlugin(channel);
|
||||||
if (!plugin) return undefined;
|
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 });
|
const accountId = resolveChannelDefaultAccountId({ plugin, cfg: next });
|
||||||
const account = plugin.config.resolveAccount(next, accountId);
|
const account = plugin.config.resolveAccount(next, accountId);
|
||||||
let enabled: boolean | undefined;
|
let enabled: boolean | undefined;
|
||||||
@@ -336,21 +372,28 @@ export async function setupChannels(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getChannelEntries = () => {
|
const getChannelEntries = () => {
|
||||||
|
const core = listChatChannels();
|
||||||
const installed = listChannelPlugins();
|
const installed = listChannelPlugins();
|
||||||
const installedIds = new Set(installed.map((plugin) => plugin.id));
|
const installedIds = new Set(installed.map((plugin) => plugin.id));
|
||||||
const catalog = listChannelPluginCatalogEntries().filter(
|
const catalog = listChannelPluginCatalogEntries().filter(
|
||||||
(entry) => !installedIds.has(entry.id),
|
(entry) => !installedIds.has(entry.id),
|
||||||
);
|
);
|
||||||
const entries = [
|
const metaById = new Map<string, ChannelMeta>();
|
||||||
...installed.map((plugin) => ({
|
for (const meta of core) {
|
||||||
id: plugin.id as ChannelChoice,
|
metaById.set(meta.id, meta);
|
||||||
meta: plugin.meta,
|
}
|
||||||
})),
|
for (const plugin of installed) {
|
||||||
...catalog.map((entry) => ({
|
metaById.set(plugin.id, plugin.meta);
|
||||||
id: entry.id as ChannelChoice,
|
}
|
||||||
meta: entry.meta,
|
for (const entry of catalog) {
|
||||||
})),
|
if (!metaById.has(entry.id)) {
|
||||||
];
|
metaById.set(entry.id, entry.meta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const entries = Array.from(metaById, ([id, meta]) => ({
|
||||||
|
id: id as ChannelChoice,
|
||||||
|
meta,
|
||||||
|
}));
|
||||||
return {
|
return {
|
||||||
entries,
|
entries,
|
||||||
catalog,
|
catalog,
|
||||||
@@ -365,6 +408,31 @@ export async function setupChannels(
|
|||||||
statusByChannel.set(channel, status);
|
statusByChannel.set(channel, status);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ensureBundledPluginEnabled = async (channel: ChannelChoice): Promise<boolean> => {
|
||||||
|
if (getChannelPlugin(channel)) return true;
|
||||||
|
const result = enablePluginInConfig(next, channel);
|
||||||
|
next = result.config;
|
||||||
|
if (!result.enabled) {
|
||||||
|
await prompter.note(
|
||||||
|
`Cannot enable ${channel}: ${result.reason ?? "plugin disabled"}.`,
|
||||||
|
"Channel setup",
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const workspaceDir = resolveAgentWorkspaceDir(next, resolveDefaultAgentId(next));
|
||||||
|
reloadOnboardingPluginRegistry({
|
||||||
|
cfg: next,
|
||||||
|
runtime,
|
||||||
|
workspaceDir,
|
||||||
|
});
|
||||||
|
if (!getChannelPlugin(channel)) {
|
||||||
|
await prompter.note(`${channel} plugin not available.`, "Channel setup");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await refreshStatus(channel);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const configureChannel = async (channel: ChannelChoice) => {
|
const configureChannel = async (channel: ChannelChoice) => {
|
||||||
const adapter = getChannelOnboardingAdapter(channel);
|
const adapter = getChannelOnboardingAdapter(channel);
|
||||||
if (!adapter) {
|
if (!adapter) {
|
||||||
@@ -476,6 +544,9 @@ export async function setupChannels(
|
|||||||
workspaceDir,
|
workspaceDir,
|
||||||
});
|
});
|
||||||
await refreshStatus(channel);
|
await refreshStatus(channel);
|
||||||
|
} else {
|
||||||
|
const enabled = await ensureBundledPluginEnabled(channel);
|
||||||
|
if (!enabled) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const plugin = getChannelPlugin(channel);
|
const plugin = getChannelPlugin(channel);
|
||||||
@@ -531,10 +602,8 @@ export async function setupChannels(
|
|||||||
options?.onSelection?.(selection);
|
options?.onSelection?.(selection);
|
||||||
|
|
||||||
const selectionNotes = new Map<string, string>();
|
const selectionNotes = new Map<string, string>();
|
||||||
for (const plugin of listChannelPlugins()) {
|
const { entries: selectionEntries } = getChannelEntries();
|
||||||
selectionNotes.set(plugin.id, formatChannelSelectionLine(plugin.meta, formatDocsLink));
|
for (const entry of selectionEntries) {
|
||||||
}
|
|
||||||
for (const entry of listChannelPluginCatalogEntries()) {
|
|
||||||
selectionNotes.set(entry.id, formatChannelSelectionLine(entry.meta, formatDocsLink));
|
selectionNotes.set(entry.id, formatChannelSelectionLine(entry.meta, formatDocsLink));
|
||||||
}
|
}
|
||||||
const selectedLines = selection
|
const selectedLines = selection
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export type AuthChoice =
|
|||||||
| "codex-cli"
|
| "codex-cli"
|
||||||
| "apiKey"
|
| "apiKey"
|
||||||
| "gemini-api-key"
|
| "gemini-api-key"
|
||||||
|
| "google-antigravity"
|
||||||
|
| "google-gemini-cli"
|
||||||
| "zai-api-key"
|
| "zai-api-key"
|
||||||
| "minimax-cloud"
|
| "minimax-cloud"
|
||||||
| "minimax"
|
| "minimax"
|
||||||
@@ -26,6 +28,7 @@ export type AuthChoice =
|
|||||||
| "minimax-api-lightning"
|
| "minimax-api-lightning"
|
||||||
| "opencode-zen"
|
| "opencode-zen"
|
||||||
| "github-copilot"
|
| "github-copilot"
|
||||||
|
| "copilot-proxy"
|
||||||
| "qwen-portal"
|
| "qwen-portal"
|
||||||
| "skip";
|
| "skip";
|
||||||
export type GatewayAuthChoice = "off" | "token" | "password";
|
export type GatewayAuthChoice = "off" | "token" | "password";
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { ChannelPluginCatalogEntry } from "../../channels/plugins/catalog.j
|
|||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import { createSubsystemLogger } from "../../logging.js";
|
import { createSubsystemLogger } from "../../logging.js";
|
||||||
import { recordPluginInstall } from "../../plugins/installs.js";
|
import { recordPluginInstall } from "../../plugins/installs.js";
|
||||||
|
import { enablePluginInConfig } from "../../plugins/enable.js";
|
||||||
import { loadClawdbotPlugins } from "../../plugins/loader.js";
|
import { loadClawdbotPlugins } from "../../plugins/loader.js";
|
||||||
import { installPluginFromNpmSpec } from "../../plugins/install.js";
|
import { installPluginFromNpmSpec } from "../../plugins/install.js";
|
||||||
import type { RuntimeEnv } from "../../runtime.js";
|
import type { RuntimeEnv } from "../../runtime.js";
|
||||||
@@ -48,37 +49,6 @@ function resolveLocalPath(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensurePluginEnabled(cfg: ClawdbotConfig, pluginId: string): ClawdbotConfig {
|
|
||||||
const entries = {
|
|
||||||
...cfg.plugins?.entries,
|
|
||||||
[pluginId]: {
|
|
||||||
...(cfg.plugins?.entries?.[pluginId] as Record<string, unknown> | undefined),
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const next: ClawdbotConfig = {
|
|
||||||
...cfg,
|
|
||||||
plugins: {
|
|
||||||
...cfg.plugins,
|
|
||||||
...(cfg.plugins?.enabled === false ? { enabled: true } : {}),
|
|
||||||
entries,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return ensurePluginAllowlist(next, pluginId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensurePluginAllowlist(cfg: ClawdbotConfig, pluginId: string): ClawdbotConfig {
|
|
||||||
const allow = cfg.plugins?.allow;
|
|
||||||
if (!allow || allow.includes(pluginId)) return cfg;
|
|
||||||
return {
|
|
||||||
...cfg,
|
|
||||||
plugins: {
|
|
||||||
...cfg.plugins,
|
|
||||||
allow: [...allow, pluginId],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function addPluginLoadPath(cfg: ClawdbotConfig, pluginPath: string): ClawdbotConfig {
|
function addPluginLoadPath(cfg: ClawdbotConfig, pluginPath: string): ClawdbotConfig {
|
||||||
const existing = cfg.plugins?.load?.paths ?? [];
|
const existing = cfg.plugins?.load?.paths ?? [];
|
||||||
const merged = Array.from(new Set([...existing, pluginPath]));
|
const merged = Array.from(new Set([...existing, pluginPath]));
|
||||||
@@ -145,7 +115,7 @@ export async function ensureOnboardingPluginInstalled(params: {
|
|||||||
|
|
||||||
if (choice === "local" && localPath) {
|
if (choice === "local" && localPath) {
|
||||||
next = addPluginLoadPath(next, localPath);
|
next = addPluginLoadPath(next, localPath);
|
||||||
next = ensurePluginEnabled(next, entry.id);
|
next = enablePluginInConfig(next, entry.id).config;
|
||||||
return { cfg: next, installed: true };
|
return { cfg: next, installed: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +128,7 @@ export async function ensureOnboardingPluginInstalled(params: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
next = ensurePluginEnabled(next, result.pluginId);
|
next = enablePluginInConfig(next, result.pluginId).config;
|
||||||
next = recordPluginInstall(next, {
|
next = recordPluginInstall(next, {
|
||||||
pluginId: result.pluginId,
|
pluginId: result.pluginId,
|
||||||
source: "npm",
|
source: "npm",
|
||||||
@@ -181,7 +151,7 @@ export async function ensureOnboardingPluginInstalled(params: {
|
|||||||
});
|
});
|
||||||
if (fallback) {
|
if (fallback) {
|
||||||
next = addPluginLoadPath(next, localPath);
|
next = addPluginLoadPath(next, localPath);
|
||||||
next = ensurePluginEnabled(next, entry.id);
|
next = enablePluginInConfig(next, entry.id).config;
|
||||||
return { cfg: next, installed: true };
|
return { cfg: next, installed: true };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user