feat(updates): detect the newest release and self-apply it, including plugin installs
Update detection was a commit count against the default branch, which says nothing about whether a *version* shipped and is pure noise on the plugin cache (a depth-1 clone, where every distance is a graft artefact). It now reads the forge's own releases API and compares the published tag to this install's package.json version, falling back to the commit count only when no release can be read — with the reason in release_check, so "up to date" never silently means "we could not look". Two states a commit count cannot express: dev_build (ahead of the newest release, missing nothing from it) now reads "Dev build - N ahead" instead of a false "Up to date"; and on a shallow clone every commits_*_release field is null and dev_build is never asserted, because the numbers would be meaningless. POST /api/updates/apply gains a second mode, chosen by install kind and never by the request body. A checkout still fast-forwards onto the release tag, rebuilds and self-restarts. A ~/.claude/plugins/cache/ install is refused by applyUpdate (that directory is Claude Code's to replace) and handed instead to a detached chain: claude plugin marketplace update -> claude plugin update -y -> plugin-bootstrap.js --force from the NEW cache dir, which retires this server and starts its replacement. Both write the HTTP response before anything stops the process. The chain records every outcome to plugin-update.json in the runtime dir, outside the cache the update replaces, so the next server reports it as last_plugin_update: a failed chain is otherwise indistinguishable from a slow one, and the tab would poll a server that never returns. /reload-plugins is deliberately not in the chain - it is REPL-scoped to the session that runs it, so no background process can perform it, and plugin-bootstrap derives PLUGIN_ROOT from its own __dirname, which is what makes it unnecessary for the dashboard. Also rebrands the sidebar subtitle, splash and page title to SmartGift Solution, and corrects three ARCHITECTURE.md statements that claimed /apply did not exist. Verified: server 1683/1683, client 486/486, build, prettier, file-header audit, openapi.yaml regenerated.
This commit is contained in:
@@ -1,15 +1,26 @@
|
||||
/**
|
||||
* @file HTTP routes for dashboard upstream-update detection, plus the one
|
||||
* self-applying path: POST /apply fast-forwards, rebuilds, and restarts the
|
||||
* process on the user's explicit click (see `server/lib/update-check.js`'s
|
||||
* `applyUpdate` and `server/lib/self-restart.js`). Everywhere else, the
|
||||
* dashboard only ever prints a command for the user to run themselves.
|
||||
* @file HTTP routes for release-update detection plus the self-applying path.
|
||||
* `POST /apply` has two modes, chosen by how this copy was installed:
|
||||
*
|
||||
* - **checkout** — fast-forward onto the newest release tag, rebuild, and
|
||||
* restart in place (`update-check.js`'s `applyUpdate` + `self-restart.js`).
|
||||
* - **plugin** — `applyUpdate` cannot touch a `~/.claude/plugins/cache/`
|
||||
* directory, so it refuses with `plugin_managed`; this route catches that and
|
||||
* hands the job to the detached chain in `plugin-update.js` instead
|
||||
* (`claude plugin marketplace update` → `claude plugin update` →
|
||||
* `plugin-bootstrap.js --force`, which retires this server and starts the new
|
||||
* one). Answered 200 with `mode: "plugin"`, not 409.
|
||||
*
|
||||
* Both modes reply BEFORE anything stops the process, and the request is only
|
||||
* ever a trigger: the marketplace and plugin names come from the local cache
|
||||
* path, never from the body, so there is nothing in the payload to validate.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const { Router } = require("express");
|
||||
const { getUpdatesStatus, applyUpdate } = require("../lib/update-check");
|
||||
const { scheduleRestart } = require("../lib/self-restart");
|
||||
const { schedulePluginUpdate } = require("../lib/plugin-update");
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -44,6 +55,33 @@ router.post("/check", async (_req, res) => {
|
||||
router.post("/apply", async (_req, res) => {
|
||||
try {
|
||||
const result = await applyUpdate();
|
||||
|
||||
// A plugin-cache install is refused by applyUpdate on purpose (it must not
|
||||
// rewrite a directory Claude Code owns). Route it to the detached chain,
|
||||
// which pulls the new version and re-bootstraps from the NEW cache dir.
|
||||
// Only when there is genuinely something to pull — a `plugin_managed`
|
||||
// refusal with nothing newer published must stay a 409, not spawn a
|
||||
// process that would stop the server for no reason.
|
||||
if (
|
||||
!result.applied &&
|
||||
result.reason === "plugin_managed" &&
|
||||
result.update_available &&
|
||||
result.plugin_marketplace &&
|
||||
result.plugin_name &&
|
||||
result.repo_root
|
||||
) {
|
||||
const scheduled = schedulePluginUpdate({
|
||||
marketplace: result.plugin_marketplace,
|
||||
plugin: result.plugin_name,
|
||||
root: result.repo_root,
|
||||
});
|
||||
// `applied: true` because the update IS being applied — just by the
|
||||
// plugin chain rather than by a git fast-forward. `mode` tells the client
|
||||
// which one, so it can word the progress text honestly.
|
||||
res.json({ ...result, applied: true, mode: "plugin", ...scheduled });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.applied) {
|
||||
res.status(409).json({
|
||||
...result,
|
||||
@@ -52,7 +90,9 @@ router.post("/apply", async (_req, res) => {
|
||||
message:
|
||||
result.reason === "up_to_date"
|
||||
? "Already up to date."
|
||||
: "Current branch can't be fast-forwarded automatically.",
|
||||
: result.reason === "plugin_managed"
|
||||
? "This install runs from the Claude Code plugin cache and nothing newer is published; run /agenthub-update to force a refresh."
|
||||
: "Current branch can't be fast-forwarded automatically.",
|
||||
},
|
||||
});
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user