fix(plugin-loader): preserve scoped npm package names in plugin key parsing

Scoped packages like @scope/pkg were truncated to just 'pkg' because
basename() strips the scope prefix. Fix:
- Detect scoped packages (starting with @) and find version separator
  after the scope slash, not at the leading @
- Return full scoped name (@scope/pkg) instead of calling basename
- Add regression test for scoped package name preservation
This commit is contained in:
MoerAI
2026-03-25 17:10:07 +09:00
parent 23a30e86f2
commit d7a1945b27
2 changed files with 47 additions and 1 deletions
@@ -32,6 +32,41 @@ describe("discoverInstalledPlugins", () => {
}
})
it("preserves scoped package name from npm plugin keys", () => {
//#given
const pluginsHome = process.env.CLAUDE_PLUGINS_HOME as string
const installPath = join(createTemporaryDirectory("omo-plugin-install-"), "@myorg", "my-plugin")
mkdirSync(installPath, { recursive: true })
const databasePath = join(pluginsHome, "installed_plugins.json")
writeFileSync(
databasePath,
JSON.stringify({
version: 2,
plugins: {
"@myorg/my-plugin@1.0.0": [
{
scope: "user",
installPath,
version: "1.0.0",
installedAt: "2026-03-25T00:00:00Z",
lastUpdated: "2026-03-25T00:00:00Z",
},
],
},
}),
"utf-8",
)
//#when
const discovered = discoverInstalledPlugins()
//#then
expect(discovered.errors).toHaveLength(0)
expect(discovered.plugins).toHaveLength(1)
expect(discovered.plugins[0]?.name).toBe("@myorg/my-plugin")
})
it("derives package name from file URL plugin keys", () => {
//#given
const pluginsHome = process.env.CLAUDE_PLUGINS_HOME as string
@@ -81,7 +81,14 @@ function loadPluginManifest(installPath: string): PluginManifest | null {
function derivePluginNameFromKey(pluginKey: string): string {
const keyWithoutSource = pluginKey.startsWith("npm:") ? pluginKey.slice(4) : pluginKey
const versionSeparator = keyWithoutSource.lastIndexOf("@")
let versionSeparator: number
if (keyWithoutSource.startsWith("@")) {
const scopeEnd = keyWithoutSource.indexOf("/")
versionSeparator = scopeEnd > 0 ? keyWithoutSource.indexOf("@", scopeEnd) : -1
} else {
versionSeparator = keyWithoutSource.lastIndexOf("@")
}
const keyWithoutVersion = versionSeparator > 0 ? keyWithoutSource.slice(0, versionSeparator) : keyWithoutSource
if (keyWithoutVersion.startsWith("file://")) {
@@ -92,6 +99,10 @@ function derivePluginNameFromKey(pluginKey: string): string {
}
}
if (keyWithoutVersion.startsWith("@") && keyWithoutVersion.includes("/")) {
return keyWithoutVersion
}
if (keyWithoutVersion.includes("/") || keyWithoutVersion.includes("\\")) {
return basename(keyWithoutVersion)
}