b3fe10a4f3
- 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.
125 lines
5.0 KiB
JavaScript
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;
|