fix(cli): accepted npm protocol plugin installs

Allowed npm:<package> install specs to validate against the resolved package name while still forwarding the original spec to Bun.

Added regression coverage for installing npm:pi-figma-remote-auth through PluginManager.

Fixes #4310
This commit is contained in:
roboomp
2026-07-02 12:08:01 +00:00
parent 0ea6ea630b
commit 805b994211
3 changed files with 50 additions and 5 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed `omp install npm:<package>` rejecting Pi package specs before Bun could resolve them. ([#4310](https://github.com/can1357/oh-my-pi/issues/4310))
## [16.3.1] - 2026-07-02
### Breaking Changes
@@ -86,20 +86,22 @@ export function formatPluginSpec(spec: ParsedPluginSpec): string {
}
/**
* Extract the base package name without version specifier.
* Extract the dependency key from an npm package specifier.
* Used for path lookups after npm install.
*
* @example
* extractPackageName("lodash@4.17.21") // "lodash"
* extractPackageName("@scope/pkg@1.0.0") // "@scope/pkg"
* extractPackageName("@scope/pkg") // "@scope/pkg"
* extractPackageName("npm:lodash") // "lodash"
*/
export function extractPackageName(specifier: string): string {
const npmSpecifier = specifier.replace(/^npm:/i, "");
// Handle scoped packages: @scope/name@version -> @scope/name
if (specifier.startsWith("@")) {
const match = specifier.match(/^(@[^/]+\/[^@]+)/);
return match ? match[1] : specifier;
if (npmSpecifier.startsWith("@")) {
const match = npmSpecifier.match(/^(@[^/]+\/[^@]+)/);
return match ? match[1] : npmSpecifier;
}
// Unscoped: name@version -> name
return specifier.replace(/@[^@]+$/, "");
return npmSpecifier.replace(/@[^@]+$/, "");
}
@@ -68,6 +68,45 @@ describe("PluginManager.install load validation", () => {
await removeWithRetries(tmpRoot);
});
test("installs npm protocol specs with the resolved package name", async () => {
vi.spyOn(Bun, "spawn").mockImplementation(((cmd: string[]) => {
expect(cmd).toEqual(["bun", "install", "npm:pi-figma-remote-auth"]);
const prepare = (async () => {
await Bun.write(
pluginsPkgJson,
JSON.stringify(
{
name: "omp-plugins",
private: true,
dependencies: { "pi-figma-remote-auth": "npm:pi-figma-remote-auth" },
},
null,
2,
),
);
await writePluginPackage(pluginsNodeModules, "pi-figma-remote-auth", {
version: "1.2.3",
source:
'export default function(pi) { pi.registerCommand("figma-auth", { handler: async () => {} }); }\n',
});
})();
return {
pid: 1,
stdout: emptyStream(),
stderr: emptyStream(),
exited: prepare.then(() => 0),
} as Subprocess;
}) as typeof Bun.spawn);
const result = await new PluginManager(tmpRoot).install("npm:pi-figma-remote-auth");
expect(result.name).toBe("pi-figma-remote-auth");
expect(result.version).toBe("1.2.3");
expect(result.path).toBe(path.join(pluginsNodeModules, "pi-figma-remote-auth"));
});
test("rejects an install whose extension entry cannot resolve its dependencies", async () => {
vi.spyOn(Bun, "spawn").mockImplementation(((cmd: string[]) => {
expect(cmd).toEqual(["bun", "install", "broken-plugin"]);