Files
openclaw/src/cli/plugin-install-plan.ts
Jesse Merhi 00364ee777 improve: warn before non-ClawHub plugin installs (#102197)
Merged via squash.

Prepared head SHA: e08d9e737d
Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>
Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>
Reviewed-by: @jesse-merhi
2026-07-15 03:25:36 +10:00

115 lines
3.5 KiB
TypeScript

// Plugin install planning helpers for bundled, official external, and npm fallback paths.
import { parseRegistryNpmSpec } from "../infra/npm-registry-spec.js";
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 resolveBundledInstallPlanForCatalogEntry(params: {
pluginId: string;
npmSpec: string;
findBundledSource: BundledLookup;
}): { bundledSource: BundledPluginSource } | null {
const pluginId = params.pluginId.trim();
const npmSpec = params.npmSpec.trim();
if (!pluginId || !npmSpec) {
return null;
}
const bundledBySpec = params.findBundledSource({
kind: "npmSpec",
value: npmSpec,
});
if (bundledBySpec?.pluginId === pluginId) {
return { bundledSource: bundledBySpec };
}
const bundledById = params.findBundledSource({
kind: "pluginId",
value: pluginId,
});
if (bundledById?.pluginId !== pluginId) {
return null;
}
if (bundledById.npmSpec && bundledById.npmSpec !== npmSpec) {
return null;
}
return { bundledSource: bundledById };
}
export function resolveBundledInstallPlanBeforeNpm(params: {
rawSpec: string;
findBundledSource: BundledLookup;
}): { bundledSource: BundledPluginSource; warning: string } | null {
// Bundled plugin ids win before npm lookup so local official plugins do not hit the registry.
const rawSpec = params.rawSpec.trim();
if (!rawSpec) {
return null;
}
if (isBareNpmPackageName(rawSpec)) {
const bundledSource = params.findBundledSource({
kind: "pluginId",
value: rawSpec,
});
if (!bundledSource) {
return null;
}
return {
bundledSource,
warning: `Using bundled plugin "${bundledSource.pluginId}" from ${shortenHomePath(bundledSource.localPath)} for bare install spec "${rawSpec}". To install an npm package with the same name, use a scoped package name (for example @scope/${rawSpec}).`,
};
}
const parsedNpmSpec = parseRegistryNpmSpec(rawSpec);
if (!parsedNpmSpec) {
return null;
}
const bundledSource =
params.findBundledSource({
kind: "npmSpec",
value: rawSpec,
}) ??
params.findBundledSource({
kind: "npmSpec",
value: parsedNpmSpec.name,
});
if (!bundledSource) {
return null;
}
return {
bundledSource,
warning: `Using bundled plugin "${bundledSource.pluginId}" from ${shortenHomePath(bundledSource.localPath)} for npm install spec "${rawSpec}" because this plugin ships with the current OpenClaw build. To force an external npm override, use npm:${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)}.`,
};
}