feat(system): report repository status for git-clone installs

`GET /api/system/update/check` answers "is there a newer published release
tag?", which is the right question for an npm install but not for a git clone
that tracks a branch. Such an install can be many commits behind its own remote
while the latest tag says it is current, and nothing surfaces that.

Adds `GET /api/system/repo-status`: an informational companion that reports what
this CHECKOUT looks like against its own remotes — current branch and commit,
ahead/behind counts per remote, the remote's role (tracking / upstream / other),
and a bounded list of incoming commits.

Read-only and defensive: every git invocation is `execFileSync` with an argv
array and a timeout, a non-git or remote-less install reports a structured
`error` rather than throwing, and nothing here mutates the working tree or
touches the updater's own state.

Tests: 24 cases in test/repo-status.test.ts.
This commit is contained in:
Aamer Akhter
2026-08-20 12:39:28 -04:00
parent 07b9c7fd7b
commit 02e7d3fcba
4 changed files with 511 additions and 0 deletions
+6
View File
@@ -48,6 +48,7 @@ import {
} from '../route-helpers.js';
import { SseEvent } from '../sse-events.js';
import { getInstallInfo, checkForUpdate, startUpdate, getUpdateStatusForApi } from '../self-update.js';
import { getRepositoryStatus } from '../repo-status.js';
import type { SessionPort, EventPort, ConfigPort, InfraPort, AuthPort } from '../ports/index.js';
import { AUTH_COOKIE_NAME } from '../middleware/auth.js';
import { QR_AUTH_FAILURE_MAX } from '../../config/tunnel-config.js';
@@ -354,6 +355,11 @@ export function registerSystemRoutes(
// Poll target for update progress — survives the restart the update triggers.
app.get('/api/system/update/status', async () => getUpdateStatusForApi());
// Informational companion to the release-tag updater above: what this CHECKOUT
// looks like against its own remotes (ahead/behind, incoming commits), which a
// release tag cannot answer for a git install tracking a branch.
app.get('/api/system/repo-status', async () => getRepositoryStatus());
// Kick off a detached update to the latest release. Returns immediately; the
// browser then polls /api/system/update/status across the service restart.
app.post('/api/system/update', async (_req, reply) => {