mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-17 04:50:51 +00:00
feat: simplify provider plugin install input
This commit is contained in:
@@ -182,6 +182,72 @@ describe("setupSearch", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the updated configure-or-install action label", async () => {
|
||||
vi.stubEnv("BRAVE_API_KEY", "BSA-test-key");
|
||||
loadOpenClawPlugins.mockReturnValue({
|
||||
searchProviders: [
|
||||
{
|
||||
pluginId: "tavily-search",
|
||||
provider: {
|
||||
id: "tavily",
|
||||
name: "Tavily Search",
|
||||
description: "Plugin search",
|
||||
isAvailable: () => true,
|
||||
search: async () => ({ content: "ok" }),
|
||||
},
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
{
|
||||
id: "tavily-search",
|
||||
name: "Tavily Search",
|
||||
description: "External Tavily plugin",
|
||||
origin: "workspace",
|
||||
source: "/tmp/tavily-search",
|
||||
configJsonSchema: undefined,
|
||||
configUiHints: undefined,
|
||||
},
|
||||
],
|
||||
typedHooks: [],
|
||||
});
|
||||
const cfg: OpenClawConfig = {
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "brave",
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
"tavily-search": {
|
||||
enabled: true,
|
||||
config: { apiKey: "tvly-installed-key" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const { prompter } = createPrompter({
|
||||
actionValue: "__skip__",
|
||||
selectValue: "__skip__",
|
||||
});
|
||||
|
||||
await setupSearch(cfg, runtime, prompter);
|
||||
|
||||
expect(prompter.select).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: "Web search setup",
|
||||
options: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
value: "__configure_provider__",
|
||||
label: "Configure or install a provider",
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes workspaceDir when resolving plugin providers for setup", async () => {
|
||||
const cfg: OpenClawConfig = {};
|
||||
const { prompter } = createPrompter({ selectValue: "__skip__" });
|
||||
@@ -286,6 +352,76 @@ describe("setupSearch", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the install option visible in configure-provider flow even when Tavily is already loaded", async () => {
|
||||
vi.stubEnv("BRAVE_API_KEY", "BSA-test-key");
|
||||
loadOpenClawPlugins.mockReturnValue({
|
||||
searchProviders: [
|
||||
{
|
||||
pluginId: "tavily-search",
|
||||
provider: {
|
||||
id: "tavily",
|
||||
name: "Tavily Search",
|
||||
description: "Plugin search",
|
||||
isAvailable: () => true,
|
||||
search: async () => ({ content: "ok" }),
|
||||
},
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
{
|
||||
id: "tavily-search",
|
||||
name: "Tavily Search",
|
||||
description: "External Tavily plugin",
|
||||
origin: "workspace",
|
||||
source: "/tmp/tavily-search",
|
||||
configJsonSchema: undefined,
|
||||
configUiHints: undefined,
|
||||
},
|
||||
],
|
||||
typedHooks: [],
|
||||
});
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "brave",
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
"tavily-search": {
|
||||
enabled: true,
|
||||
config: { apiKey: "tvly-installed-key" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const { prompter } = createPrompter({
|
||||
actionValue: "__configure_provider__",
|
||||
selectValue: "__skip__",
|
||||
});
|
||||
|
||||
await setupSearch(cfg, runtime, prompter);
|
||||
|
||||
const configurePickerCall = (prompter.select as ReturnType<typeof vi.fn>).mock.calls.find(
|
||||
(call) => call[0]?.message === "Choose provider to configure",
|
||||
);
|
||||
expect(configurePickerCall?.[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
options: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
value: "__install_plugin__",
|
||||
label: "Install another provider plugin",
|
||||
hint: "Add another supported web search plugin",
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("sets provider and key for perplexity", async () => {
|
||||
const cfg: OpenClawConfig = {};
|
||||
const { prompter } = createPrompter({
|
||||
|
||||
@@ -133,9 +133,12 @@ export function resolveInstallableSearchProviderPlugins(
|
||||
const loadedPluginProviderIds = new Set(
|
||||
providerEntries.filter((entry) => entry.kind === "plugin").map((entry) => entry.value),
|
||||
);
|
||||
return SEARCH_PROVIDER_PLUGIN_INSTALL_CATALOG.filter(
|
||||
(entry) => !loadedPluginProviderIds.has(entry.providerId),
|
||||
);
|
||||
return SEARCH_PROVIDER_PLUGIN_INSTALL_CATALOG.map((entry) => ({
|
||||
...entry,
|
||||
description: loadedPluginProviderIds.has(entry.providerId)
|
||||
? `${entry.description} Already installed.`
|
||||
: entry.description,
|
||||
}));
|
||||
}
|
||||
|
||||
function normalizePluginConfigObject(value: unknown): Record<string, unknown> {
|
||||
@@ -935,7 +938,7 @@ export function buildSearchProviderPickerModel(
|
||||
{
|
||||
value: SEARCH_PROVIDER_INSTALL_SENTINEL as const,
|
||||
label: "Install another provider plugin",
|
||||
hint: "Add Tavily or another supported web search plugin",
|
||||
hint: "Add another supported web search plugin",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -1262,8 +1265,9 @@ export async function promptSearchProviderFlow(params: {
|
||||
configureValue: SEARCH_PROVIDER_CONFIGURE_SENTINEL,
|
||||
switchValue: SEARCH_PROVIDER_SWITCH_ACTIVE_SENTINEL,
|
||||
skipValue: SEARCH_PROVIDER_SKIP_SENTINEL,
|
||||
configureLabel: "Configure a provider",
|
||||
configureHint: "Update keys or plugin settings without changing the active provider",
|
||||
configureLabel: "Configure or install a provider",
|
||||
configureHint:
|
||||
"Update keys, plugin settings, or install a provider without changing the active provider",
|
||||
switchLabel: "Switch active provider",
|
||||
switchHint: "Change which provider web_search uses right now",
|
||||
skipHint: "Configure later with openclaw configure --section web",
|
||||
|
||||
@@ -95,10 +95,18 @@ function mockRepoLocalPathExists() {
|
||||
|
||||
async function runInitialValueForChannel(channel: "dev" | "beta") {
|
||||
const runtime = makeRuntime();
|
||||
const select = vi.fn((async <T extends string>() => "skip" as T) as WizardPrompter["select"]);
|
||||
const prompter = makePrompter({ select: select as unknown as WizardPrompter["select"] });
|
||||
const select = vi.fn((async <T extends string>() => "enter" as T) as WizardPrompter["select"]);
|
||||
const text = vi.fn(async ({ initialValue }: { initialValue?: string }) => initialValue ?? "");
|
||||
const prompter = makePrompter({
|
||||
select: select as unknown as WizardPrompter["select"],
|
||||
text: text as unknown as WizardPrompter["text"],
|
||||
});
|
||||
const cfg: OpenClawConfig = { update: { channel } };
|
||||
mockRepoLocalPathExists();
|
||||
installPluginFromNpmSpec.mockResolvedValue({
|
||||
ok: false,
|
||||
error: "nope",
|
||||
});
|
||||
|
||||
await ensureOnboardingPluginInstalled({
|
||||
cfg,
|
||||
@@ -107,7 +115,7 @@ async function runInitialValueForChannel(channel: "dev" | "beta") {
|
||||
runtime,
|
||||
});
|
||||
|
||||
const call = select.mock.calls[0];
|
||||
const call = text.mock.calls[0];
|
||||
return call?.[0]?.initialValue;
|
||||
}
|
||||
|
||||
@@ -123,7 +131,8 @@ describe("ensureOnboardingPluginInstalled", () => {
|
||||
it("installs from npm and enables the plugin", async () => {
|
||||
const runtime = makeRuntime();
|
||||
const prompter = makePrompter({
|
||||
select: vi.fn(async () => "npm") as WizardPrompter["select"],
|
||||
select: vi.fn(async () => "enter") as WizardPrompter["select"],
|
||||
text: vi.fn(async () => "@openclaw/zalo") as WizardPrompter["text"],
|
||||
});
|
||||
const cfg: OpenClawConfig = { plugins: { allow: ["other"] } };
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
@@ -154,8 +163,11 @@ describe("ensureOnboardingPluginInstalled", () => {
|
||||
|
||||
it("uses local path when selected", async () => {
|
||||
const runtime = makeRuntime();
|
||||
const note = vi.fn(async () => {});
|
||||
const prompter = makePrompter({
|
||||
select: vi.fn(async () => "local") as WizardPrompter["select"],
|
||||
select: vi.fn(async () => "enter") as WizardPrompter["select"],
|
||||
text: vi.fn(async () => "extensions/zalo") as WizardPrompter["text"],
|
||||
note,
|
||||
});
|
||||
const cfg: OpenClawConfig = {};
|
||||
mockRepoLocalPathExists();
|
||||
@@ -169,22 +181,35 @@ describe("ensureOnboardingPluginInstalled", () => {
|
||||
|
||||
expectPluginLoadedFromLocalPath(result);
|
||||
expect(result.cfg.plugins?.entries?.zalo?.enabled).toBe(true);
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
`Using existing local plugin at ${path.resolve(process.cwd(), "extensions/zalo")}.\nNo download needed.`,
|
||||
"Plugin install",
|
||||
);
|
||||
});
|
||||
|
||||
it("defaults to local on dev channel when local path exists", async () => {
|
||||
expect(await runInitialValueForChannel("dev")).toBe("local");
|
||||
expect(await runInitialValueForChannel("dev")).toBe(
|
||||
path.resolve(process.cwd(), "extensions/zalo"),
|
||||
);
|
||||
});
|
||||
|
||||
it("defaults to npm on beta channel even when local path exists", async () => {
|
||||
expect(await runInitialValueForChannel("beta")).toBe("npm");
|
||||
expect(await runInitialValueForChannel("beta")).toBe("@openclaw/zalo");
|
||||
});
|
||||
|
||||
it("defaults to bundled local path on beta channel when available", async () => {
|
||||
const runtime = makeRuntime();
|
||||
const select = vi.fn((async <T extends string>() => "skip" as T) as WizardPrompter["select"]);
|
||||
const prompter = makePrompter({ select: select as unknown as WizardPrompter["select"] });
|
||||
const select = vi.fn((async <T extends string>() => "enter" as T) as WizardPrompter["select"]);
|
||||
const text = vi.fn(async ({ initialValue }: { initialValue?: string }) => initialValue ?? "");
|
||||
const prompter = makePrompter({
|
||||
select: select as unknown as WizardPrompter["select"],
|
||||
text: text as unknown as WizardPrompter["text"],
|
||||
});
|
||||
const cfg: OpenClawConfig = { update: { channel: "beta" } };
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
vi.mocked(fs.existsSync).mockImplementation((value) => {
|
||||
const raw = String(value);
|
||||
return raw === "/opt/openclaw/extensions/zalo" || raw.endsWith(`${path.sep}.git`);
|
||||
});
|
||||
resolveBundledPluginSources.mockReturnValue(
|
||||
new Map([
|
||||
[
|
||||
@@ -205,15 +230,9 @@ describe("ensureOnboardingPluginInstalled", () => {
|
||||
runtime,
|
||||
});
|
||||
|
||||
expect(select).toHaveBeenCalledWith(
|
||||
expect(text).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
initialValue: "local",
|
||||
options: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
value: "local",
|
||||
hint: "/opt/openclaw/extensions/zalo",
|
||||
}),
|
||||
]),
|
||||
initialValue: "/opt/openclaw/extensions/zalo",
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -223,7 +242,8 @@ describe("ensureOnboardingPluginInstalled", () => {
|
||||
const note = vi.fn(async () => {});
|
||||
const confirm = vi.fn(async () => true);
|
||||
const prompter = makePrompter({
|
||||
select: vi.fn(async () => "npm") as WizardPrompter["select"],
|
||||
select: vi.fn(async () => "enter") as WizardPrompter["select"],
|
||||
text: vi.fn(async () => "@openclaw/zalo") as WizardPrompter["text"],
|
||||
note,
|
||||
confirm,
|
||||
});
|
||||
@@ -242,10 +262,72 @@ describe("ensureOnboardingPluginInstalled", () => {
|
||||
});
|
||||
|
||||
expectPluginLoadedFromLocalPath(result);
|
||||
expect(note).toHaveBeenCalled();
|
||||
expect(note).toHaveBeenCalledWith(`Failed to install @openclaw/zalo: nope`, "Plugin install");
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
`Using existing local plugin at ${path.resolve(process.cwd(), "extensions/zalo")}.\nNo download needed.`,
|
||||
"Plugin install",
|
||||
);
|
||||
expect(runtime.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("re-prompts when a path-like input does not exist", async () => {
|
||||
const runtime = makeRuntime();
|
||||
const note = vi.fn(async () => {});
|
||||
const text = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce("./missing-plugin")
|
||||
.mockResolvedValueOnce("@openclaw/zalo");
|
||||
const prompter = makePrompter({
|
||||
select: vi.fn(async () => "enter") as WizardPrompter["select"],
|
||||
text: text as unknown as WizardPrompter["text"],
|
||||
note,
|
||||
});
|
||||
const cfg: OpenClawConfig = {};
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
installPluginFromNpmSpec.mockResolvedValue({
|
||||
ok: true,
|
||||
pluginId: "zalo",
|
||||
targetDir: "/tmp/zalo",
|
||||
extensions: [],
|
||||
});
|
||||
|
||||
const result = await ensureOnboardingPluginInstalled({
|
||||
cfg,
|
||||
entry: baseEntry,
|
||||
prompter,
|
||||
runtime,
|
||||
});
|
||||
|
||||
expect(result.installed).toBe(true);
|
||||
expect(note).toHaveBeenCalledWith("Path not found: ./missing-plugin", "Plugin install");
|
||||
expect(text).toHaveBeenCalledTimes(2);
|
||||
expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ spec: "@openclaw/zalo" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns unchanged config when install is skipped", async () => {
|
||||
const runtime = makeRuntime();
|
||||
const text = vi.fn();
|
||||
const prompter = makePrompter({
|
||||
select: vi.fn(async () => "skip") as WizardPrompter["select"],
|
||||
text: text as unknown as WizardPrompter["text"],
|
||||
});
|
||||
const cfg: OpenClawConfig = {};
|
||||
|
||||
const result = await ensureOnboardingPluginInstalled({
|
||||
cfg,
|
||||
entry: baseEntry,
|
||||
prompter,
|
||||
runtime,
|
||||
});
|
||||
|
||||
expect(result.installed).toBe(false);
|
||||
expect(result.cfg).toBe(cfg);
|
||||
expect(text).not.toHaveBeenCalled();
|
||||
expect(installPluginFromNpmSpec).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears discovery cache before reloading the onboarding plugin registry", () => {
|
||||
const runtime = makeRuntime();
|
||||
const cfg: OpenClawConfig = {};
|
||||
|
||||
@@ -17,7 +17,7 @@ import { createPluginLoaderLogger } from "../../plugins/logger.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import type { WizardPrompter } from "../../wizard/prompts.js";
|
||||
|
||||
type InstallChoice = "npm" | "local" | "skip";
|
||||
type InstallChoice = "enter" | "skip";
|
||||
|
||||
export type InstallablePluginCatalogEntry = {
|
||||
id: string;
|
||||
@@ -75,6 +75,31 @@ function resolveLocalPath(
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveExistingPath(
|
||||
rawValue: string,
|
||||
workspaceDir: string | undefined,
|
||||
allowLocal: boolean,
|
||||
): string | null {
|
||||
if (!allowLocal) {
|
||||
return null;
|
||||
}
|
||||
const raw = rawValue.trim();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const candidates = new Set<string>();
|
||||
candidates.add(path.resolve(process.cwd(), raw));
|
||||
if (workspaceDir && workspaceDir !== process.cwd()) {
|
||||
candidates.add(path.resolve(workspaceDir, raw));
|
||||
}
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function addPluginLoadPath(cfg: OpenClawConfig, pluginPath: string): OpenClawConfig {
|
||||
const existing = cfg.plugins?.load?.paths ?? [];
|
||||
const merged = Array.from(new Set([...existing, pluginPath]));
|
||||
@@ -93,31 +118,84 @@ function addPluginLoadPath(cfg: OpenClawConfig, pluginPath: string): OpenClawCon
|
||||
async function promptInstallChoice(params: {
|
||||
entry: InstallablePluginCatalogEntry;
|
||||
localPath?: string | null;
|
||||
defaultChoice: InstallChoice;
|
||||
defaultSource: string;
|
||||
prompter: WizardPrompter;
|
||||
}): Promise<InstallChoice> {
|
||||
const { entry, localPath, prompter, defaultChoice } = params;
|
||||
const localOptions: Array<{ value: InstallChoice; label: string; hint?: string }> = localPath
|
||||
? [
|
||||
{
|
||||
value: "local",
|
||||
label: "Use local plugin path",
|
||||
hint: localPath,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
const options: Array<{ value: InstallChoice; label: string; hint?: string }> = [
|
||||
{ value: "npm", label: `Download from npm (${entry.install.npmSpec})` },
|
||||
...localOptions,
|
||||
{ value: "skip", label: "Skip for now" },
|
||||
];
|
||||
const initialValue: InstallChoice =
|
||||
defaultChoice === "local" && !localPath ? "npm" : defaultChoice;
|
||||
return await prompter.select<InstallChoice>({
|
||||
workspaceDir?: string;
|
||||
allowLocal: boolean;
|
||||
}): Promise<string | null> {
|
||||
const { entry, localPath, prompter, defaultSource, workspaceDir, allowLocal } = params;
|
||||
const action = await prompter.select<InstallChoice>({
|
||||
message: `Install ${entry.meta.label} plugin?`,
|
||||
options,
|
||||
initialValue,
|
||||
options: [
|
||||
{
|
||||
value: "enter",
|
||||
label: "Enter package or local path",
|
||||
hint: localPath
|
||||
? `${entry.install.npmSpec} or ${localPath}`
|
||||
: `${entry.install.npmSpec} or ./path/to/plugin`,
|
||||
},
|
||||
{ value: "skip", label: "Skip for now" },
|
||||
],
|
||||
initialValue: "enter",
|
||||
});
|
||||
|
||||
if (action === "skip") {
|
||||
return null;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const source = (
|
||||
await prompter.text({
|
||||
message: "Plugin package or local path",
|
||||
initialValue: defaultSource,
|
||||
placeholder: localPath
|
||||
? `${entry.install.npmSpec} or ${localPath}`
|
||||
: `${entry.install.npmSpec} or ./path/to/plugin`,
|
||||
validate: (value) =>
|
||||
value.trim().length > 0 ? undefined : "Enter a package or local path",
|
||||
})
|
||||
).trim();
|
||||
|
||||
const existingPath = resolveExistingPath(source, workspaceDir, allowLocal);
|
||||
if (existingPath) {
|
||||
return existingPath;
|
||||
}
|
||||
|
||||
const looksLikePath =
|
||||
source.startsWith(".") ||
|
||||
source.startsWith("/") ||
|
||||
source.startsWith("~") ||
|
||||
source.includes("/") ||
|
||||
source.includes("\\");
|
||||
if (looksLikePath) {
|
||||
await prompter.note(`Path not found: ${source}`, "Plugin install");
|
||||
continue;
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveInstallDefaultSource(params: {
|
||||
entry: InstallablePluginCatalogEntry;
|
||||
defaultChoice: "npm" | "local";
|
||||
localPath?: string | null;
|
||||
}): string {
|
||||
const { entry, defaultChoice, localPath } = params;
|
||||
if (defaultChoice === "local" && localPath) {
|
||||
return localPath;
|
||||
}
|
||||
return entry.install.npmSpec;
|
||||
}
|
||||
|
||||
function isLikelyLocalPath(source: string): boolean {
|
||||
return (
|
||||
source.startsWith(".") ||
|
||||
source.startsWith("/") ||
|
||||
source.startsWith("~") ||
|
||||
source.includes("/") ||
|
||||
source.includes("\\")
|
||||
);
|
||||
}
|
||||
|
||||
function resolveInstallDefaultChoice(params: {
|
||||
@@ -172,25 +250,31 @@ export async function ensureOnboardingPluginInstalled(params: {
|
||||
localPath,
|
||||
bundledLocalPath,
|
||||
});
|
||||
const choice = await promptInstallChoice({
|
||||
const source = await promptInstallChoice({
|
||||
entry,
|
||||
localPath,
|
||||
defaultChoice,
|
||||
defaultSource: resolveInstallDefaultSource({ entry, defaultChoice, localPath }),
|
||||
prompter,
|
||||
workspaceDir,
|
||||
allowLocal,
|
||||
});
|
||||
|
||||
if (choice === "skip") {
|
||||
if (!source) {
|
||||
return { cfg: next, installed: false };
|
||||
}
|
||||
|
||||
if (choice === "local" && localPath) {
|
||||
next = addPluginLoadPath(next, localPath);
|
||||
if (isLikelyLocalPath(source)) {
|
||||
await prompter.note(
|
||||
[`Using existing local plugin at ${source}.`, "No download needed."].join("\n"),
|
||||
"Plugin install",
|
||||
);
|
||||
next = addPluginLoadPath(next, source);
|
||||
next = enablePluginInConfig(next, entry.id).config;
|
||||
return { cfg: next, installed: true };
|
||||
}
|
||||
|
||||
const result = await installPluginFromNpmSpec({
|
||||
spec: entry.install.npmSpec,
|
||||
spec: source,
|
||||
logger: {
|
||||
info: (msg) => runtime.log?.(msg),
|
||||
warn: (msg) => runtime.log?.(msg),
|
||||
@@ -202,7 +286,7 @@ export async function ensureOnboardingPluginInstalled(params: {
|
||||
next = recordPluginInstall(next, {
|
||||
pluginId: result.pluginId,
|
||||
source: "npm",
|
||||
spec: entry.install.npmSpec,
|
||||
spec: source,
|
||||
installPath: result.targetDir,
|
||||
version: result.version,
|
||||
...buildNpmResolutionInstallFields(result.npmResolution),
|
||||
@@ -210,10 +294,7 @@ export async function ensureOnboardingPluginInstalled(params: {
|
||||
return { cfg: next, installed: true };
|
||||
}
|
||||
|
||||
await prompter.note(
|
||||
`Failed to install ${entry.install.npmSpec}: ${result.error}`,
|
||||
"Plugin install",
|
||||
);
|
||||
await prompter.note(`Failed to install ${source}: ${result.error}`, "Plugin install");
|
||||
|
||||
if (localPath) {
|
||||
const fallback = await prompter.confirm({
|
||||
@@ -221,6 +302,10 @@ export async function ensureOnboardingPluginInstalled(params: {
|
||||
initialValue: true,
|
||||
});
|
||||
if (fallback) {
|
||||
await prompter.note(
|
||||
[`Using existing local plugin at ${localPath}.`, "No download needed."].join("\n"),
|
||||
"Plugin install",
|
||||
);
|
||||
next = addPluginLoadPath(next, localPath);
|
||||
next = enablePluginInConfig(next, entry.id).config;
|
||||
return { cfg: next, installed: true };
|
||||
|
||||
Reference in New Issue
Block a user