Files
yetoneandClaude cdf325f178 plugins: a package that is only a command (an MCP server like magpie-x-search) isn't listed under Unreviewed, isn't added as a plugin, and one added before says what it is instead of a host stack (#1327, congee949)
A repository tagged magpie-plugin whose npm copy (from that repository)
names no file to load (no main, no exports) was listed and installed from
npm without the entry check the GitHub route has; it then failed every
host start with "has no entry" and a stack. Now:

- the Unreviewed list checks npm's copy the same way: one that names no
  file needs the index.js the repository's own route needs (askTagged);
- `magpie plugin add` / the Plugins page refuse a package that names no
  file, has no index file and is neither pi's nor a middleware, take the
  package just installed for it out again, and say it isn't an OpenCode
  or magpie plugin, and, for one with a bin, to add an MCP server under
  Library → MCP servers (notPlugin);
- the host says the same, without a stack, for one already on the list.

Run: TestCommandOnlyPackageIsNoPlugin (magpie-x-search 1.2.0's real
package.json) fails without the change (Add = <nil>; with only host.js
reverted, the stack message), TestTaggedRepos with the real GitHub
package.json and npm latest of cyberElar/magpie-x-search fails without it
(listed), both pass with it; TestNotPluginKeepsPlugins (index.mjs, main,
exports, middleware, pi, a file stay plugins). go test -tags nogui
./internal/plugin under a temp HOME ok; go vet -tags nogui for darwin,
linux, windows. The built binary in a sandbox HOME: `magpie plugin add
magpie-x-search@latest` from real npm prints the new error, and
plugins/package.json no longer lists it.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-08 23:09:25 +08:00

75 lines
2.5 KiB
Go

package plugin
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// A package that is only a command — magpie-x-search 1.2.0's own
// package.json, an MCP stdio server with a bin and no main, exports or
// index file (#1327) — isn't added as a plugin: Add says what it is and
// where it goes instead. One on the list already (added before) says so
// when the host loads it, without a stack.
func TestCommandOnlyPackageIsNoPlugin(t *testing.T) {
sandbox(t)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
abs, _ := filepath.Abs("testdata/notplugin/magpie-x-search")
_, err := Add(ctx, abs)
if err == nil || !strings.Contains(err.Error(), "magpie-x-search isn't an OpenCode or magpie plugin") || !strings.Contains(err.Error(), "Library → MCP servers") {
t.Fatalf("Add = %v", err)
}
if l := Load().Plugins; len(l) != 0 {
t.Fatalf("listed: %+v", l)
}
// added by an older magpie: the host says the same, plainly
if err := save(List{Plugins: []Entry{{Spec: abs}}}); err != nil {
t.Fatal(err)
}
Restart()
ps, err := Plugins(ctx)
if err != nil {
t.Fatal(err)
}
if len(ps) != 1 || !strings.Contains(ps[0].Error, "magpie-x-search isn't an OpenCode or magpie plugin") || !strings.Contains(ps[0].Error, "MCP servers") || strings.Contains(ps[0].Error, " at ") {
t.Fatalf("loaded: %+v", ps)
}
}
// What a package that names no file still loads stays a plugin: an index
// file, main or exports; so do pi's packages and middleware, and a file.
func TestNotPluginKeepsPlugins(t *testing.T) {
sandbox(t)
pkg := func(json string, files ...string) string {
d := t.TempDir()
if err := os.WriteFile(filepath.Join(d, "package.json"), []byte(json), 0o644); err != nil {
t.Fatal(err)
}
for _, f := range files {
if err := os.WriteFile(filepath.Join(d, f), []byte("export default {}\n"), 0o644); err != nil {
t.Fatal(err)
}
}
return d
}
for name, d := range map[string]string{
"index.mjs": pkg(`{"name":"a","bin":{"a":"cli.js"}}`, "index.mjs"),
"main": pkg(`{"name":"b","main":"dist/x.js"}`),
"exports": pkg(`{"name":"c","exports":{"./server":"./s.js"}}`),
"middleware": pkg(`{"name":"d","magpie":{"middleware":"d.middleware.js"}}`),
"pi": pkg(`{"name":"e","pi":{"extensions":["./x.ts"]},"peerDependencies":{"@earendil-works/pi-coding-agent":"*"}}`),
} {
if err := notPlugin(d); err != nil {
t.Errorf("%s: %v", name, err)
}
}
if abs, _ := filepath.Abs("testdata/fake/index.js"); notPlugin(abs) != nil {
t.Error("a file")
}
}