refactor(cli): extract plugin install plan helper
This commit is contained in:
67
src/cli/plugin-install-plan.test.ts
Normal file
67
src/cli/plugin-install-plan.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { PLUGIN_INSTALL_ERROR_CODE } from "../plugins/install.js";
|
||||
import {
|
||||
resolveBundledInstallPlanBeforeNpm,
|
||||
resolveBundledInstallPlanForNpmFailure,
|
||||
} from "./plugin-install-plan.js";
|
||||
|
||||
describe("plugin install plan helpers", () => {
|
||||
it("prefers bundled plugin for bare plugin-id specs", () => {
|
||||
const findBundledSource = vi.fn().mockReturnValue({
|
||||
pluginId: "voice-call",
|
||||
localPath: "/tmp/extensions/voice-call",
|
||||
npmSpec: "@openclaw/voice-call",
|
||||
});
|
||||
|
||||
const result = resolveBundledInstallPlanBeforeNpm({
|
||||
rawSpec: "voice-call",
|
||||
findBundledSource,
|
||||
});
|
||||
|
||||
expect(findBundledSource).toHaveBeenCalledWith({ kind: "pluginId", value: "voice-call" });
|
||||
expect(result?.bundledSource.pluginId).toBe("voice-call");
|
||||
expect(result?.warning).toContain('bare install spec "voice-call"');
|
||||
});
|
||||
|
||||
it("skips bundled pre-plan for scoped npm specs", () => {
|
||||
const findBundledSource = vi.fn();
|
||||
const result = resolveBundledInstallPlanBeforeNpm({
|
||||
rawSpec: "@openclaw/voice-call",
|
||||
findBundledSource,
|
||||
});
|
||||
|
||||
expect(findBundledSource).not.toHaveBeenCalled();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("uses npm-spec bundled fallback only for package-not-found", () => {
|
||||
const findBundledSource = vi.fn().mockReturnValue({
|
||||
pluginId: "voice-call",
|
||||
localPath: "/tmp/extensions/voice-call",
|
||||
npmSpec: "@openclaw/voice-call",
|
||||
});
|
||||
const result = resolveBundledInstallPlanForNpmFailure({
|
||||
rawSpec: "@openclaw/voice-call",
|
||||
code: PLUGIN_INSTALL_ERROR_CODE.NPM_PACKAGE_NOT_FOUND,
|
||||
findBundledSource,
|
||||
});
|
||||
|
||||
expect(findBundledSource).toHaveBeenCalledWith({
|
||||
kind: "npmSpec",
|
||||
value: "@openclaw/voice-call",
|
||||
});
|
||||
expect(result?.warning).toContain("npm package unavailable");
|
||||
});
|
||||
|
||||
it("skips fallback for non-not-found npm failures", () => {
|
||||
const findBundledSource = vi.fn();
|
||||
const result = resolveBundledInstallPlanForNpmFailure({
|
||||
rawSpec: "@openclaw/voice-call",
|
||||
code: "INSTALL_FAILED",
|
||||
findBundledSource,
|
||||
});
|
||||
|
||||
expect(findBundledSource).not.toHaveBeenCalled();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
54
src/cli/plugin-install-plan.ts
Normal file
54
src/cli/plugin-install-plan.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { BundledPluginSource } from "../plugins/bundled-sources.js";
|
||||
import { PLUGIN_INSTALL_ERROR_CODE } from "../plugins/install.js";
|
||||
import { shortenHomePath } from "../utils.js";
|
||||
|
||||
type BundledLookup = (params: {
|
||||
kind: "pluginId" | "npmSpec";
|
||||
value: string;
|
||||
}) => BundledPluginSource | undefined;
|
||||
|
||||
function isBareNpmPackageName(spec: string): boolean {
|
||||
const trimmed = spec.trim();
|
||||
return /^[a-z0-9][a-z0-9-._~]*$/.test(trimmed);
|
||||
}
|
||||
|
||||
export function resolveBundledInstallPlanBeforeNpm(params: {
|
||||
rawSpec: string;
|
||||
findBundledSource: BundledLookup;
|
||||
}): { bundledSource: BundledPluginSource; warning: string } | null {
|
||||
if (!isBareNpmPackageName(params.rawSpec)) {
|
||||
return null;
|
||||
}
|
||||
const bundledSource = params.findBundledSource({
|
||||
kind: "pluginId",
|
||||
value: params.rawSpec,
|
||||
});
|
||||
if (!bundledSource) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
bundledSource,
|
||||
warning: `Using bundled plugin "${bundledSource.pluginId}" from ${shortenHomePath(bundledSource.localPath)} for bare install spec "${params.rawSpec}". To install an npm package with the same name, use a scoped package name (for example @scope/${params.rawSpec}).`,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveBundledInstallPlanForNpmFailure(params: {
|
||||
rawSpec: string;
|
||||
code?: string;
|
||||
findBundledSource: BundledLookup;
|
||||
}): { bundledSource: BundledPluginSource; warning: string } | null {
|
||||
if (params.code !== PLUGIN_INSTALL_ERROR_CODE.NPM_PACKAGE_NOT_FOUND) {
|
||||
return null;
|
||||
}
|
||||
const bundledSource = params.findBundledSource({
|
||||
kind: "npmSpec",
|
||||
value: params.rawSpec,
|
||||
});
|
||||
if (!bundledSource) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
bundledSource,
|
||||
warning: `npm package unavailable for ${params.rawSpec}; using bundled plugin at ${shortenHomePath(bundledSource.localPath)}.`,
|
||||
};
|
||||
}
|
||||
@@ -8,11 +8,7 @@ import { resolveStateDir } from "../config/paths.js";
|
||||
import { resolveArchiveKind } from "../infra/archive.js";
|
||||
import { type BundledPluginSource, findBundledPluginSource } from "../plugins/bundled-sources.js";
|
||||
import { enablePluginInConfig } from "../plugins/enable.js";
|
||||
import {
|
||||
installPluginFromNpmSpec,
|
||||
installPluginFromPath,
|
||||
PLUGIN_INSTALL_ERROR_CODE,
|
||||
} from "../plugins/install.js";
|
||||
import { installPluginFromNpmSpec, installPluginFromPath } from "../plugins/install.js";
|
||||
import { recordPluginInstall } from "../plugins/installs.js";
|
||||
import { clearPluginManifestRegistryCache } from "../plugins/manifest-registry.js";
|
||||
import type { PluginRecord } from "../plugins/registry.js";
|
||||
@@ -28,6 +24,10 @@ import { theme } from "../terminal/theme.js";
|
||||
import { resolveUserPath, shortenHomeInString, shortenHomePath } from "../utils.js";
|
||||
import { looksLikeLocalInstallSpec } from "./install-spec.js";
|
||||
import { resolvePinnedNpmInstallRecordForCli } from "./npm-resolution.js";
|
||||
import {
|
||||
resolveBundledInstallPlanBeforeNpm,
|
||||
resolveBundledInstallPlanForNpmFailure,
|
||||
} from "./plugin-install-plan.js";
|
||||
import { setPluginEnabledInConfig } from "./plugins-config.js";
|
||||
import { promptYesNo } from "./prompt.js";
|
||||
|
||||
@@ -153,11 +153,6 @@ function logSlotWarnings(warnings: string[]) {
|
||||
}
|
||||
}
|
||||
|
||||
function isBareNpmPackageName(spec: string): boolean {
|
||||
const trimmed = spec.trim();
|
||||
return /^[a-z0-9][a-z0-9-._~]*$/.test(trimmed);
|
||||
}
|
||||
|
||||
async function installBundledPluginSource(params: {
|
||||
config: OpenClawConfig;
|
||||
rawSpec: string;
|
||||
@@ -305,17 +300,16 @@ async function runPluginInstallCommand(params: {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const bundledByPluginId = isBareNpmPackageName(raw)
|
||||
? findBundledPluginSource({
|
||||
lookup: { kind: "pluginId", value: raw },
|
||||
})
|
||||
: undefined;
|
||||
if (bundledByPluginId) {
|
||||
const bundledPreNpmPlan = resolveBundledInstallPlanBeforeNpm({
|
||||
rawSpec: raw,
|
||||
findBundledSource: (lookup) => findBundledPluginSource({ lookup }),
|
||||
});
|
||||
if (bundledPreNpmPlan) {
|
||||
await installBundledPluginSource({
|
||||
config: cfg,
|
||||
rawSpec: raw,
|
||||
bundledSource: bundledByPluginId,
|
||||
warning: `Using bundled plugin "${bundledByPluginId.pluginId}" from ${shortenHomePath(bundledByPluginId.localPath)} for bare install spec "${raw}". To install an npm package with the same name, use a scoped package name (for example @scope/${raw}).`,
|
||||
bundledSource: bundledPreNpmPlan.bundledSource,
|
||||
warning: bundledPreNpmPlan.warning,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -325,13 +319,12 @@ async function runPluginInstallCommand(params: {
|
||||
logger: createPluginInstallLogger(),
|
||||
});
|
||||
if (!result.ok) {
|
||||
const bundledFallback =
|
||||
result.code === PLUGIN_INSTALL_ERROR_CODE.NPM_PACKAGE_NOT_FOUND
|
||||
? findBundledPluginSource({
|
||||
lookup: { kind: "npmSpec", value: raw },
|
||||
})
|
||||
: undefined;
|
||||
if (!bundledFallback) {
|
||||
const bundledFallbackPlan = resolveBundledInstallPlanForNpmFailure({
|
||||
rawSpec: raw,
|
||||
code: result.code,
|
||||
findBundledSource: (lookup) => findBundledPluginSource({ lookup }),
|
||||
});
|
||||
if (!bundledFallbackPlan) {
|
||||
defaultRuntime.error(result.error);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -339,8 +332,8 @@ async function runPluginInstallCommand(params: {
|
||||
await installBundledPluginSource({
|
||||
config: cfg,
|
||||
rawSpec: raw,
|
||||
bundledSource: bundledFallback,
|
||||
warning: `npm package unavailable for ${raw}; using bundled plugin at ${shortenHomePath(bundledFallback.localPath)}.`,
|
||||
bundledSource: bundledFallbackPlan.bundledSource,
|
||||
warning: bundledFallbackPlan.warning,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user