mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix(plugins): warn when npm install is shadowed
This commit is contained in:
committed by
Peter Steinberger
parent
0459bff556
commit
f73a614d66
@@ -78,6 +78,40 @@ describe("plugins cli list", () => {
|
||||
expect(runtimeLogs).toContain("No plugin issues detected.");
|
||||
});
|
||||
|
||||
it("reports config-selected plugin source shadowing in doctor output", async () => {
|
||||
buildPluginDiagnosticsReport.mockReturnValue({
|
||||
plugins: [
|
||||
createPluginRecord({
|
||||
id: "discord",
|
||||
origin: "config",
|
||||
source: "/tmp/openclaw-upstream/extensions/discord/index.ts",
|
||||
status: "error",
|
||||
error: "Cannot find module 'chalk'",
|
||||
}),
|
||||
],
|
||||
diagnostics: [
|
||||
{
|
||||
level: "warn",
|
||||
pluginId: "discord",
|
||||
source: "/tmp/openclaw/npm/node_modules/@openclaw/discord/index.ts",
|
||||
message:
|
||||
"duplicate plugin id resolved by explicit config-selected plugin; global plugin will be overridden by config plugin (/tmp/openclaw-upstream/extensions/discord/index.ts)",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await runPluginsCommand(["plugins", "doctor"]);
|
||||
|
||||
const output = runtimeLogs.join("\n");
|
||||
expect(output).toContain("Plugin source shadowing:");
|
||||
expect(output).toContain(
|
||||
"discord: duplicate plugin id resolved by explicit config-selected plugin",
|
||||
);
|
||||
expect(output).toContain("active: /tmp/openclaw-upstream/extensions/discord/index.ts");
|
||||
expect(output).toContain("shadowed: /tmp/openclaw/npm/node_modules/@openclaw/discord/index.ts");
|
||||
expect(output).toContain("openclaw plugins registry --refresh");
|
||||
});
|
||||
|
||||
it("reports persisted plugin registry state without refreshing", async () => {
|
||||
inspectPluginRegistry.mockResolvedValue({
|
||||
state: "stale",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { tracePluginLifecyclePhaseAsync } from "../plugins/plugin-lifecycle-trac
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { shortenHomeInString } from "../utils.js";
|
||||
import type { PluginInspectOptions } from "./plugins-inspect-command.js";
|
||||
import type { PluginsListOptions } from "./plugins-list-command.js";
|
||||
import { applyParentDefaultHelpAction } from "./program/parent-default-help.js";
|
||||
@@ -62,6 +63,14 @@ function matchesPluginId(plugin: { id: string }, id: string) {
|
||||
return plugin.id === id;
|
||||
}
|
||||
|
||||
function isConfigSelectedShadowDiagnostic(entry: { level?: string; message?: string }): boolean {
|
||||
return (
|
||||
entry.level === "warn" &&
|
||||
typeof entry.message === "string" &&
|
||||
entry.message.includes("duplicate plugin id resolved by explicit config-selected plugin")
|
||||
);
|
||||
}
|
||||
|
||||
export function registerPluginsCli(program: Command) {
|
||||
const plugins = program
|
||||
.command("plugins")
|
||||
@@ -336,9 +345,15 @@ export function registerPluginsCli(program: Command) {
|
||||
const report = buildPluginDiagnosticsReport({ effectiveOnly: true });
|
||||
const errors = report.plugins.filter((p) => p.status === "error");
|
||||
const diags = report.diagnostics.filter((d) => d.level === "error");
|
||||
const shadowed = report.diagnostics.filter(isConfigSelectedShadowDiagnostic);
|
||||
const compatibility = buildPluginCompatibilityNotices({ report });
|
||||
|
||||
if (errors.length === 0 && diags.length === 0 && compatibility.length === 0) {
|
||||
if (
|
||||
errors.length === 0 &&
|
||||
diags.length === 0 &&
|
||||
shadowed.length === 0 &&
|
||||
compatibility.length === 0
|
||||
) {
|
||||
defaultRuntime.log("No plugin issues detected.");
|
||||
return;
|
||||
}
|
||||
@@ -361,6 +376,31 @@ export function registerPluginsCli(program: Command) {
|
||||
lines.push(`- ${target}${diag.message}`);
|
||||
}
|
||||
}
|
||||
if (shadowed.length > 0) {
|
||||
if (lines.length > 0) {
|
||||
lines.push("");
|
||||
}
|
||||
lines.push(theme.warn("Plugin source shadowing:"));
|
||||
for (const diag of shadowed) {
|
||||
const active = report.plugins.find((plugin) => plugin.id === diag.pluginId);
|
||||
const target = diag.pluginId ? `${diag.pluginId}: ` : "";
|
||||
lines.push(`- ${target}${diag.message}`);
|
||||
if (active) {
|
||||
lines.push(` active: ${shortenHomeInString(active.source)} (${active.origin})`);
|
||||
if (active.status === "error") {
|
||||
lines.push(` active status: error${active.error ? `: ${active.error}` : ""}`);
|
||||
}
|
||||
}
|
||||
if (diag.source) {
|
||||
lines.push(` shadowed: ${shortenHomeInString(diag.source)}`);
|
||||
}
|
||||
lines.push(" repair:");
|
||||
lines.push(" openclaw plugins inspect " + (diag.pluginId ?? "<plugin-id>"));
|
||||
lines.push(" edit or remove the config-selected plugin source");
|
||||
lines.push(" openclaw plugins registry --refresh");
|
||||
lines.push(" openclaw gateway restart --force");
|
||||
}
|
||||
}
|
||||
if (compatibility.length > 0) {
|
||||
if (lines.length > 0) {
|
||||
lines.push("");
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
applyExclusiveSlotSelection,
|
||||
buildPluginDiagnosticsReport,
|
||||
buildPluginSnapshotReport,
|
||||
clearPluginRegistryLoadCache,
|
||||
enablePluginInConfig,
|
||||
loadPluginManifestRegistry,
|
||||
@@ -124,6 +125,64 @@ describe("persistPluginInstall", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("warns when an installed npm plugin remains shadowed by a config-selected source", async () => {
|
||||
const { persistPluginInstall } = await import("./plugins-install-persist.js");
|
||||
const baseConfig = {
|
||||
plugins: {
|
||||
entries: {},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const enabledConfig = {
|
||||
plugins: {
|
||||
entries: {
|
||||
discord: { enabled: true },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
enablePluginInConfig.mockReturnValue({ config: enabledConfig });
|
||||
buildPluginSnapshotReport.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "discord",
|
||||
origin: "config",
|
||||
source: "/tmp/openclaw-upstream/extensions/discord/index.ts",
|
||||
status: "error",
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const next = await persistPluginInstall({
|
||||
snapshot: {
|
||||
config: baseConfig,
|
||||
baseHash: "config-1",
|
||||
},
|
||||
pluginId: "discord",
|
||||
install: {
|
||||
source: "npm",
|
||||
spec: "@openclaw/discord",
|
||||
installPath: "/tmp/openclaw/npm/node_modules/@openclaw/discord/index.ts",
|
||||
},
|
||||
});
|
||||
|
||||
expect(next).toEqual(enabledConfig);
|
||||
expect(buildPluginSnapshotReport).toHaveBeenCalledWith({
|
||||
config: enabledConfig,
|
||||
effectiveOnly: true,
|
||||
onlyPluginIds: ["discord"],
|
||||
});
|
||||
expect(runtimeLogs.join("\n")).toContain(
|
||||
'Warning: installed plugin "discord" is not the active source',
|
||||
);
|
||||
expect(runtimeLogs.join("\n")).toContain(
|
||||
"active config source: /tmp/openclaw-upstream/extensions/discord/index.ts",
|
||||
);
|
||||
expect(runtimeLogs.join("\n")).toContain(
|
||||
"installed npm source: /tmp/openclaw/npm/node_modules/@openclaw/discord/index.ts",
|
||||
);
|
||||
expect(runtimeLogs.join("\n")).toContain("openclaw plugins doctor");
|
||||
});
|
||||
|
||||
it("invalidates runtime cache even when registry refresh fails", async () => {
|
||||
const { persistPluginInstall } = await import("./plugins-install-persist.js");
|
||||
const baseConfig = {
|
||||
|
||||
@@ -9,8 +9,10 @@ import {
|
||||
} from "../plugins/installed-plugin-index-records.js";
|
||||
import type { PluginInstallUpdate } from "../plugins/installs.js";
|
||||
import { tracePluginLifecyclePhaseAsync } from "../plugins/plugin-lifecycle-trace.js";
|
||||
import { buildPluginSnapshotReport } from "../plugins/status.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { shortenHomePath } from "../utils.js";
|
||||
import {
|
||||
applySlotSelectionForPlugin,
|
||||
enableInternalHookEntries,
|
||||
@@ -58,6 +60,41 @@ export type ConfigSnapshotForInstallPersist = {
|
||||
baseHash: string | undefined;
|
||||
};
|
||||
|
||||
function logShadowedNpmInstallWarning(params: {
|
||||
config: OpenClawConfig;
|
||||
pluginId: string;
|
||||
install: Omit<PluginInstallUpdate, "pluginId">;
|
||||
runtime: RuntimeEnv;
|
||||
}): void {
|
||||
if (params.install.source !== "npm") {
|
||||
return;
|
||||
}
|
||||
const installedSource = params.install.installPath ?? params.install.sourcePath;
|
||||
if (!installedSource) {
|
||||
return;
|
||||
}
|
||||
const report = buildPluginSnapshotReport({
|
||||
config: params.config,
|
||||
effectiveOnly: true,
|
||||
onlyPluginIds: [params.pluginId],
|
||||
});
|
||||
const active = report.plugins.find((plugin) => plugin.id === params.pluginId);
|
||||
if (!active || active.origin !== "config" || active.source === installedSource) {
|
||||
return;
|
||||
}
|
||||
|
||||
params.runtime.log(
|
||||
theme.warn(
|
||||
[
|
||||
`Warning: installed plugin "${params.pluginId}" is not the active source because a config-selected plugin with the same id is currently selected:`,
|
||||
` active config source: ${shortenHomePath(active.source)}`,
|
||||
` installed npm source: ${shortenHomePath(installedSource)}`,
|
||||
"Run `openclaw plugins doctor` for repair options.",
|
||||
].join("\n"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export async function persistPluginInstall(params: {
|
||||
snapshot: ConfigSnapshotForInstallPersist;
|
||||
pluginId: string;
|
||||
@@ -127,6 +164,12 @@ export async function persistPluginInstall(params: {
|
||||
runtime.log(theme.warn(params.warningMessage));
|
||||
}
|
||||
runtime.log(params.successMessage ?? `Installed plugin: ${params.pluginId}`);
|
||||
logShadowedNpmInstallWarning({
|
||||
config: next,
|
||||
pluginId: params.pluginId,
|
||||
install: params.install,
|
||||
runtime,
|
||||
});
|
||||
runtime.log("Restart the gateway to load plugins.");
|
||||
return next;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user