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
+49
View File
@@ -98,3 +98,52 @@ export interface UpdateCheckResult {
source: 'github-api' | 'git-ls-remote' | 'none';
error?: string;
}
/**
* Role of a remote in the repository-status view.
* - `tracking`: the current branch's `@{upstream}` remote (where `git pull` goes).
* - `upstream`: the canonical project (a remote named `origin`/`upstream` that is
* not the tracking remote).
* - `other`: anything else explicitly requested via `CODEMAN_UPDATE_REMOTES`.
*/
export type RepoRemoteRole = 'tracking' | 'upstream' | 'other';
/** A single incoming commit — present on the remote ref but not in local HEAD. */
export interface RepoIncomingCommit {
/** Abbreviated SHA. */
sha: string;
/** Commit subject (first line). */
subject: string;
}
/** Ahead/behind + incoming summary for local HEAD vs one remote's compare ref. */
export interface RepoRemoteStatus {
/** Remote name, e.g. `origin`, `bitbucket`. */
name: string;
/** Remote URL (best-effort; empty if unresolved). */
url: string;
role: RepoRemoteRole;
/** Ref HEAD is compared against, e.g. `origin/master`, `bitbucket/local`. */
compareRef: string;
/** Commits in local HEAD not on the remote ref (local-only / unpushed). */
ahead: number;
/** Commits on the remote ref not in local HEAD (incoming). */
behind: number;
/** Up to N most recent incoming commits (newest first). */
incoming: RepoIncomingCommit[];
/** Set when this remote could not be fetched/compared. */
error?: string;
}
/** Result of the repository-status check across the configured remotes. */
export interface RepositoryStatusResult {
/** epoch ms of the check. */
checkedAt: number;
/** False when this is not a git install (then `remotes` is empty + `error` set). */
isGit: boolean;
/** Current running version, for display. */
currentVersion: string;
remotes: RepoRemoteStatus[];
/** Top-level error (e.g. not a git install, or no remotes resolved). */
error?: string;
}