Files
AgentHub/server/routes/updates.js
T
nntrivi2001 b3fe10a4f3 fix: v2.7.4 — a developer's System page carries no host or other-user path
- GET /api/settings/info for a developer: no DB path, no hooks file path,
  and no transcript-cache keys — those were EVERY user's transcript paths
  (their folders and session ids). GET /api/updates/status: no repo_root.
  Test: server-mode-leaks.
- Also in this release (earlier commits): a member's missing workspace
  folder is made as them at start; --allow-dangerously-skip-permissions only
  for an account that accepted the bypass warning or a bypass run.
Full suites: server 2889/2889, client 1250/1250, tsc -b.
2026-10-12 15:56:26 +07:00

125 lines
5.0 KiB
JavaScript

/**
* @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();
router.get("/status", async (req, res) => {
try {
const status = await getUpdatesStatus();
// The install folder is the host's; a developer sees versions only.
if (require("../lib/scope").memberId(req) != null) status.repo_root = null;
res.json(status);
} catch (err) {
res.status(500).json({
error: { code: "UPDATE_STATUS_FAILED", message: err.message || String(err) },
});
}
});
router.post("/check", async (_req, res) => {
try {
const status = await getUpdatesStatus();
try {
const { broadcast } = require("../websocket");
broadcast("update_status", status);
} catch {
// WS not initialized (e.g. in isolated tests) — safe to ignore.
}
res.json(status);
} catch (err) {
res.status(500).json({
error: { code: "UPDATE_CHECK_FAILED", message: err.message || String(err) },
});
}
});
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,
error: {
code: "UPDATE_NOT_APPLICABLE",
message:
result.reason === "up_to_date"
? "Already up to date."
: result.reason === "plugin_managed"
? // Do not claim "nothing newer is published" unless the release
// lookup actually succeeded — saying so after a failed check
// reports an unknown as a negative, which is how a broken
// check looks identical to an up-to-date install.
result.release_check === "ok"
? `This install runs from the Claude Code plugin cache and ${result.latest_release || "the published release"} is already the newest; run /agenthub-update to force a refresh.`
: `This install runs from the Claude Code plugin cache and the release check did not succeed (${result.release_check || "unknown"}), so there is nothing to apply; run /agenthub-update to update it by hand.`
: "Current branch can't be fast-forwarded automatically.",
},
});
return;
}
res.json(result);
try {
const { broadcast } = require("../websocket");
broadcast("update_status", { ...result, update_available: false });
} catch {
// WS not initialized (e.g. in isolated tests) — safe to ignore.
}
// After the response above, restart onto the freshly-built code.
setImmediate(() => scheduleRestart());
} catch (err) {
res.status(500).json({
error: { code: "UPDATE_APPLY_FAILED", message: err.message || String(err) },
});
}
});
module.exports = router;