From ba0e2a8aa9363ab879e63b7d902161eb18d3c36e Mon Sep 17 00:00:00 2001 From: yetone Date: Sat, 10 Oct 2026 10:18:13 +0800 Subject: [PATCH] plugin: Update all and `magpie plugin update` bring a plugin pinned to an older version to npm's newest, so the update prompt clears (ARNO on Discord) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ARNO: 非官方插件安装后就算更新到最新版,magpie依然提示有更新. A third-party plugin added as name@1.2.3 is pinned. The Plugins page still counts it under "Update all (n)", its row says "vX out" and the Plugins dot is on. Update all (and `magpie plugin update`) reinstalled the pinned version, reported "Plugins updated", and the prompt stayed. The row's own Update already installs the newest version and unpins the spec (name@latest). Update now does the same for a pinned plugin when npm has a newer version than the one installed: it installs that version through add, which keeps the plugin's options and the provider picked for it. A pinned plugin that is already at npm's newest keeps its pin. Git plugins, folders and plugins npm didn't answer for behave as before. The plugin docs (en, zh, ja) now say that updating a pinned plugin yourself unpins it. Reproduced in a sandbox HOME with the real opencode-gemini-auth from npm (magpie web on a spare port, China mirror switch on and off). Pinned at 1.4.15 with 2.0.1 out: before the fix, Update all and `magpie plugin update` left it at 1.4.15 and /api/plugins/updates still listed it. After the fix it is at 2.0.1 as opencode-gemini-auth@latest, the waiting list is empty and Check for updates reports "current". The row's Update (upgrade) path was checked too: unpinned opencode-gemini-auth, and scoped @gitlab/opencode-gitlab-auth 1.3.1 -> 1.3.3 with the mirror on. Both cleared the prompt before the fix as well. TestUpdateAllUpdatesAPinnedPlugin (real bun, fake registry) fails without the change: "Update all left opencode-someones-auth at 1.0.0, want 2.0.0". gofmt -l, go vet (darwin, GOOS=linux, GOOS=windows) on internal/plugin, and go test -tags nogui of internal/plugin, internal/gui and internal/provider under a temp HOME with pinned caches all pass. --- internal/plugin/store.go | 25 +++++++---- internal/plugin/update_held_test.go | 64 +++++++++++++++++++++++++++++ site/public/docs/ja/plugins.html | 2 +- site/public/docs/plugins.html | 2 +- site/public/docs/zh/plugins.html | 2 +- 5 files changed, 85 insertions(+), 10 deletions(-) diff --git a/internal/plugin/store.go b/internal/plugin/store.go index acfba5fc..65e952c3 100644 --- a/internal/plugin/store.go +++ b/internal/plugin/store.go @@ -476,22 +476,33 @@ func notPlugin(target string) error { return errors.New(why) } -// Update installs the version of each npm plugin its spec says now -// (latest, for the most part: npm's newest, asked now, installAt), and -// fetches each git one again. +// Update installs npm's newest version (asked now, installAt) of each npm +// plugin, and fetches each git one again. A plugin pinned to a version +// that npm has a newer one of is updated too, and unpinned, as its row's +// Update does: the page counts it in Update all, and the CLI's update +// says it installs the newest of each (ARNO on Discord: a plugin updated +// with Update all still said an update was out, pinned where it was). A +// pinned one already at npm's newest stays as its spec says. func Update(ctx context.Context) error { var errs []error for _, e := range Load().Plugins { if !IsPath(e.Spec) { v := "" - if !IsGit(e.Spec) && !Pinned(e.Spec) { + if !IsGit(e.Spec) { v = newestOf(ctx, Name(e.Spec)) } var err error - if v != "" { + switch { + case v != "" && Pinned(e.Spec): + if update.Newer(v, Installed(e.Spec)) { + _, err = add(ctx, Name(e.Spec), v) + } else { + err = reinstall(ctx, e.Spec) + } + case v != "": err = installAt(ctx, Name(e.Spec), v) - } else { - // pinned, a git one, or npm not answering: as its spec says + default: + // a git one, or npm not answering: as its spec says err = reinstall(ctx, e.Spec) } if err != nil { diff --git a/internal/plugin/update_held_test.go b/internal/plugin/update_held_test.go index e181c816..dcc046f3 100644 --- a/internal/plugin/update_held_test.go +++ b/internal/plugin/update_held_test.go @@ -163,3 +163,67 @@ func TestAgeText(t *testing.T) { } } } + +// Update all (and magpie plugin update) brings a plugin pinned to an older +// version to npm's newest, unpinned, as its row's Update does: the page +// counts it among the updates, and before, Update all installed the pinned +// version again, said "Plugins updated", and the row still said the newer +// one was out (ARNO on Discord: 非官方插件…更新到最新版,magpie依然提示有更新). +// A pinned one already at npm's newest stays pinned. +func TestUpdateAllUpdatesAPinnedPlugin(t *testing.T) { + sandbox(t) + const pkg = "opencode-someones-auth" + heldRegistry(t, pkg, map[string]time.Time{"1.0.0": time.Now().Add(-72 * time.Hour), "2.0.0": time.Now().Add(-48 * time.Hour)}, "2.0.0") + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + + if _, err := Add(ctx, pkg+"@1.0.0"); err != nil { + t.Fatal(err) + } + if !Pinned(Load().Plugins[0].Spec) || Installed(pkg) != "1.0.0" { + t.Fatalf("added %+v at %s, want it pinned at 1.0.0", Load().Plugins, Installed(pkg)) + } + if err := Prefer(pkg, "someone"); err != nil { + t.Fatal(err) + } + if _, err := CheckUpdates(ctx); err != nil { + t.Fatal(err) + } + if w := PendingUpdates().Waiting; len(w) != 1 || w[0].Latest != "2.0.0" { + t.Fatalf("waiting before Update all = %+v, want 2.0.0", w) + } + + if err := Update(ctx); err != nil { + t.Fatal(err) + } + if v := Installed(pkg); v != "2.0.0" { + t.Fatalf("Update all left %s at %s, want 2.0.0", pkg, v) + } + l := Load() + if len(l.Plugins) != 1 || l.Plugins[0].Spec != pkg+"@latest" { + t.Fatalf("plugins after Update all = %+v, want %s@latest", l.Plugins, pkg) + } + if l.Prefer["someone"] != pkg+"@latest" { + t.Fatalf("the provider picked for it = %q, want it kept on %s@latest", l.Prefer["someone"], pkg) + } + if w := PendingUpdates().Waiting; len(w) != 0 { + t.Fatalf("still waiting after Update all: %+v", w) + } + if _, err := CheckUpdates(ctx); err != nil { + t.Fatal(err) + } + if w := PendingUpdates().Waiting; len(w) != 0 { + t.Fatalf("the next look found an update again: %+v", w) + } + + // pinned at the newest: Update all leaves its pin + if _, err := Add(ctx, pkg+"@2.0.0"); err != nil { + t.Fatal(err) + } + if err := Update(ctx); err != nil { + t.Fatal(err) + } + if l := Load().Plugins; len(l) != 1 || l[0].Spec != pkg+"@2.0.0" || Installed(pkg) != "2.0.0" { + t.Fatalf("plugins = %+v at %s, want %s@2.0.0 kept", l, Installed(pkg), pkg) + } +} diff --git a/site/public/docs/ja/plugins.html b/site/public/docs/ja/plugins.html index 33ff9334..67679ec8 100644 --- a/site/public/docs/ja/plugins.html +++ b/site/public/docs/ja/plugins.html @@ -213,7 +213,7 @@ magpie quota # acme · Pro · 5h 37%

magpie が設定フォルダ(~/.config/magpie または $XDG_CONFIG_HOME/magpie)に保存するもの:

diff --git a/site/public/docs/plugins.html b/site/public/docs/plugins.html index 5f671e1c..82bd014f 100644 --- a/site/public/docs/plugins.html +++ b/site/public/docs/plugins.html @@ -213,7 +213,7 @@ magpie quota # acme · Pro · 5h 37%
  • The community’s packages (@magpie-community/*) update by themselves a little after magpie starts, then every six hours.
  • Any other package with a newer version shows a dot on Plugins, and the user updates it with a click or magpie plugin update.
  • -
  • A spec pinned to a version (name@1.2.3) stays on it.
  • +
  • A spec pinned to a version (name@1.2.3) stays on it until the user updates it: Update to …, Update all and magpie plugin update install the newest version and unpin it.

What magpie keeps, in its config folder (~/.config/magpie, or $XDG_CONFIG_HOME/magpie):

diff --git a/site/public/docs/zh/plugins.html b/site/public/docs/zh/plugins.html index 17b59b90..e6fdc99c 100644 --- a/site/public/docs/zh/plugins.html +++ b/site/public/docs/zh/plugins.html @@ -213,7 +213,7 @@ magpie quota # acme · Pro · 5h 37%
  • 社区的包(@magpie-community/*)在 magpie 启动后不久自动更新,之后每六小时一次。
  • 其他包有新版本时,插件上会出现一个小圆点,用户点一下或运行 magpie plugin update 更新。
  • -
  • 固定了版本的写法(name@1.2.3)会停在那个版本。
  • +
  • 固定了版本的写法(name@1.2.3)会停在那个版本,直到用户自己更新:更新到 …、全部更新和 magpie plugin update 会装上最新版并取消固定。

magpie 在它的配置目录(~/.config/magpie 或 $XDG_CONFIG_HOME/magpie)里保存这些: