chore: v2.8.21 — update progress steps, clone in background, extra SSH keys inline, today's spend

- Plugin update reports its running step (plugin-update-progress.json →
  plugin_update_progress); a hidden update or clone leaves a progress chip.
- Clone here can run in the background.
- Git identity: extra SSH keys are added inline per host (no More keys tab).
- Users: spend today (viewer's local day, ?tz_offset=).
- Prettier on the files touched since v2.8.20.
This commit is contained in:
2026-10-23 10:44:11 +07:00
parent 62b4520507
commit 9dd97a369c
33 changed files with 906 additions and 393 deletions
+38 -1
View File
@@ -28,6 +28,9 @@ const path = require("path");
const { spawn } = require("child_process");
const { getDataDir } = require("./claude-home");
/** Longer than any one step's timeout in the helper (bootstrap: 15 min). */
const PROGRESS_STALE_MS = 20 * 60 * 1000;
/** Runtime directory the helper writes its verdict into. Outside the plugin
* cache on purpose — that directory is replaced by the update itself. */
function runtimeDir() {
@@ -53,6 +56,34 @@ function readLastPluginUpdate(rt = runtimeDir()) {
}
}
/**
* The step a running chain is on (`plugin-update-progress.json`, written by the
* helper as each step starts and removed with its final verdict), or null.
* Surfaced as `plugin_update_progress` so the dashboard can show how far an
* update got. A file older than the chain's longest run is left over from a
* helper that died, and reads as nothing.
*
* @returns {{step: string, index: number, total: number, started_at: string} | null}
*/
function readPluginUpdateProgress(rt = runtimeDir()) {
try {
const parsed = JSON.parse(
fs.readFileSync(path.join(rt, "plugin-update-progress.json"), "utf8")
);
if (!parsed || typeof parsed.step !== "string") return null;
const started = Date.parse(parsed.started_at);
if (!Number.isFinite(started) || Date.now() - started > PROGRESS_STALE_MS) return null;
return {
step: parsed.step,
index: Number(parsed.index) || 0,
total: Number(parsed.total) || 0,
started_at: parsed.started_at,
};
} catch {
return null;
}
}
/**
* Spawn the detached update chain. Returns immediately; the caller must have
* already written its HTTP response, because this process is about to be
@@ -69,6 +100,7 @@ function schedulePluginUpdate({ marketplace, plugin, root }) {
try {
fs.mkdirSync(rt, { recursive: true });
fs.rmSync(path.join(rt, "plugin-update.json"), { force: true });
fs.rmSync(path.join(rt, "plugin-update-progress.json"), { force: true });
} catch {
/* best effort — a stale verdict is better than failing the update */
}
@@ -86,4 +118,9 @@ function schedulePluginUpdate({ marketplace, plugin, root }) {
return { spawned: true, runtime_dir: rt };
}
module.exports = { schedulePluginUpdate, readLastPluginUpdate, runtimeDir };
module.exports = {
schedulePluginUpdate,
readLastPluginUpdate,
readPluginUpdateProgress,
runtimeDir,
};