update means something now: it depends on tags
"update" was `git pull` and re-install, unconditionally, for every plugin.
nothing could say whether there was anything to pull, so the button was always
there and usually did nothing — and "update all" was twenty-one no-ops with a
spinner on it. the button's own comment admitted it: gating on `outdated` would
hide it for ever, because nothing bumped a version.
versions are dated and tagged now, so the rule the owner asked for is possible:
· a checkout NOT at a tag is always updateable — it is between releases, no
claim is being made about it, pulling is always reasonable
· a checkout AT a tag is updateable only when a NEWER tag exists
the part worth defending is how "newer" is decided. the obvious version-string
comparison is wrong the first time a plugin is tagged `v2` or `1.0.0-rc1`, and
wrong SILENTLY — a plugin that never offers an update looks like one nobody has
released. so newness is asked of git: the newest tag reachable from the REMOTE
branch, compared against the tag at HEAD. that holds for any names anyone
chooses, and there is a test with a tag called `beta` that sorts before `v1` to
keep it honest.
it never fetches. every answer comes from refs already on disk, because this
runs once per plugin on a request that renders a list and twenty-one network
round trips would make the screen feel broken. tags arrive at clone and at the
fetch `pullPlugin` already does.
the VERB refuses too, not just the UI. a plugins list is fetched once and a tag
can be pushed a second later, so the copy someone clicks is routinely older than
the truth — and `updateAll` iterates a list rather than asking per plugin.
"up to date · <tag>" is said out loud rather than shown as an absent button. no
button and up-to-date look identical, and only one of them is information.
verified on the real installs: example pulled to its tag and flipped to
up-to-date, caldav on an older commit stayed updateable, and updatePlugin on
example answered "already at 2026.08.27.01, the newest release" without pulling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
@@ -523,14 +523,25 @@ export const PluginDetail = () => {
|
||||
{t('plugins.enable')}
|
||||
</Button>
|
||||
)}
|
||||
{/* Always offered, not gated on `outdated`. That flag compares the manifest's version to the
|
||||
recorded one, and nothing bumps a version today — so an artwork or code change lands with
|
||||
the number unmoved and the button would never appear. It also used to run `install`,
|
||||
which cannot fetch: the directory exists, so the clone is skipped and only the apply
|
||||
steps re-ran. `update` pulls first. */}
|
||||
<Button disabled={busy} onClick={() => run('update', plugin.appName)}>
|
||||
{plugin.outdated ? t('plugins.updateTo', { version: plugin.version }) : 'Update'}
|
||||
</Button>
|
||||
{/* Offered only when there is something to update TO — the tag rule, see plugins/release.ts.
|
||||
A checkout between releases always is; one sitting on a tag only when a newer tag exists.
|
||||
|
||||
This was unconditional, with a comment explaining that nothing bumped versions so gating
|
||||
it on `outdated` would hide it for ever. Versions are dated and tagged now, so the button
|
||||
can finally mean something — and an Update that lands on the version you already have is
|
||||
how you teach someone to stop pressing it. */}
|
||||
{plugin.updateable && (
|
||||
<Button disabled={busy} onClick={() => run('update', plugin.appName)}>
|
||||
{plugin.releaseReason === 'new-release' && plugin.newestTag
|
||||
? `Update to ${plugin.newestTag}`
|
||||
: 'Update'}
|
||||
</Button>
|
||||
)}
|
||||
{/* Said out loud rather than shown as an absent button. "No Update button" and "up to date"
|
||||
look identical, and only one of them is information. */}
|
||||
{!plugin.updateable && plugin.releaseReason === 'at-latest' && plugin.tagAtHead && (
|
||||
<span className="self-center text-xs text-duck-dark/40">Up to date · {plugin.tagAtHead}</span>
|
||||
)}
|
||||
{/* Absent while enabled — disable first. `DeletePlugin` decides that itself rather than being
|
||||
conditioned here, so the rule lives next to the confirmation that explains it. */}
|
||||
<DeletePlugin
|
||||
|
||||
@@ -74,6 +74,14 @@ export type PluginItem = {
|
||||
installedVersion: string | null;
|
||||
/** The code on disk moved after it was installed — normal while developing, and worth seeing. */
|
||||
outdated: boolean;
|
||||
/**
|
||||
* Whether there is anything to update TO. A checkout between releases always is; one sitting on a tag
|
||||
* only when a newer tag exists. See `plugins/release.ts` for why this is not a version comparison.
|
||||
*/
|
||||
updateable: boolean;
|
||||
releaseReason: 'untagged' | 'new-release' | 'at-latest' | 'not-a-checkout';
|
||||
tagAtHead: string | null;
|
||||
newestTag: string | null;
|
||||
/**
|
||||
* PM2's word for the sidecar, or null when the plugin has none. `online` while enabled is healthy;
|
||||
* anything else while enabled is the state worth rendering differently — the difference between a
|
||||
|
||||
Reference in New Issue
Block a user