diff --git a/src/infra/install-safe-path.test.ts b/src/infra/install-safe-path.test.ts index 7cd9498e5d1..1227de8d516 100644 --- a/src/infra/install-safe-path.test.ts +++ b/src/infra/install-safe-path.test.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; import { assertCanonicalPathWithinBase, + packageNameMatchesId, resolveSafeInstallDir, safeDirName, safePathSegmentHashed, @@ -20,6 +21,18 @@ describe("unscopedPackageName", () => { }); }); +describe("packageNameMatchesId", () => { + it.each([ + { packageName: "@openclaw/matrix", id: "matrix", expected: true }, + { packageName: "@openclaw/matrix", id: "@openclaw/matrix", expected: true }, + { packageName: "@openclaw/matrix", id: "signal", expected: false }, + { packageName: " ", id: "matrix", expected: false }, + { packageName: "@openclaw/matrix", id: " ", expected: false }, + ])("matches ids for %j", ({ packageName, id, expected }) => { + expect(packageNameMatchesId(packageName, id)).toBe(expected); + }); +}); + describe("safeDirName", () => { it.each([ { value: " matrix ", expected: "matrix" }, diff --git a/src/infra/install-safe-path.ts b/src/infra/install-safe-path.ts index a2f012e70fb..e2a9a19f187 100644 --- a/src/infra/install-safe-path.ts +++ b/src/infra/install-safe-path.ts @@ -11,6 +11,20 @@ export function unscopedPackageName(name: string): string { return trimmed.includes("/") ? (trimmed.split("/").pop() ?? trimmed) : trimmed; } +export function packageNameMatchesId(packageName: string, id: string): boolean { + const trimmedId = id.trim(); + if (!trimmedId) { + return false; + } + + const trimmedPackageName = packageName.trim(); + if (!trimmedPackageName) { + return false; + } + + return trimmedId === trimmedPackageName || trimmedId === unscopedPackageName(trimmedPackageName); +} + export function safeDirName(input: string): string { const trimmed = input.trim(); if (!trimmed) { diff --git a/src/plugins/install.test.ts b/src/plugins/install.test.ts index 0a9120d6d81..32685e9f5de 100644 --- a/src/plugins/install.test.ts +++ b/src/plugins/install.test.ts @@ -264,12 +264,20 @@ async function installFromFileWithWarnings(params: { extensionsDir: string; file return { result, warnings }; } -function setupManifestInstallFixture(params: { manifestId: string }) { +function setupManifestInstallFixture(params: { manifestId: string; packageName?: string }) { const caseDir = makeTempDir(); const stateDir = path.join(caseDir, "state"); const pluginDir = path.join(caseDir, "plugin-src"); fs.mkdirSync(stateDir, { recursive: true }); fs.cpSync(manifestInstallTemplateDir, pluginDir, { recursive: true }); + if (params.packageName) { + const packageJsonPath = path.join(pluginDir, "package.json"); + const manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as { + name?: string; + }; + manifest.name = params.packageName; + fs.writeFileSync(packageJsonPath, JSON.stringify(manifest), "utf-8"); + } fs.writeFileSync( path.join(pluginDir, "openclaw.plugin.json"), JSON.stringify({ @@ -1080,6 +1088,23 @@ describe("installPluginFromDir", () => { ).toBe(true); }); + it("does not warn when a scoped npm package name matches the manifest id", async () => { + const { pluginDir, extensionsDir } = setupManifestInstallFixture({ + manifestId: "matrix", + packageName: "@openclaw/matrix", + }); + + const infoMessages: string[] = []; + const res = await installPluginFromDir({ + dirPath: pluginDir, + extensionsDir, + logger: { info: (msg: string) => infoMessages.push(msg), warn: () => {} }, + }); + + expectInstalledWithPluginId(res, extensionsDir, "matrix"); + expect(infoMessages.some((msg) => msg.includes("differs from npm package name"))).toBe(false); + }); + it.each([ { name: "manifest id wins for scoped plugin ids", diff --git a/src/plugins/install.ts b/src/plugins/install.ts index 34d4093d15d..69c30dc5a8d 100644 --- a/src/plugins/install.ts +++ b/src/plugins/install.ts @@ -1,6 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { + packageNameMatchesId, resolveSafeInstallDir, safeDirName, safePathSegmentHashed, @@ -546,7 +547,7 @@ async function installPluginFromPackageDir( }; } - if (manifestPluginId && manifestPluginId !== npmPluginId) { + if (manifestPluginId && !packageNameMatchesId(npmPluginId, manifestPluginId)) { logger.info?.( `Plugin manifest id "${manifestPluginId}" differs from npm package name "${npmPluginId}"; using manifest id as the config key.`, );