Files
autofirmer-expanded/lib/updater-status.ts
T
Brandon LiandClaude Opus 5.5 ab3132182a Report each tick's outcome and the updater's state to the master
The master saw a scheduler's last run time and nothing of what it did. Each
tick now records, per lane, its outcome, a line on what happened (what
fired, or the spread, contract or hold cap that stopped it) and why the
lane's accounts sat it out, counted by reason with a few names each. It
rides the scheduler status as lastTicks.

Each report also carries the auto-updater's state from the files it keeps
and from git: its last check, a failed update waiting to retry, how far the
checkout is behind origin, and tracked files edited by hand, which stop the
fast-forward pull.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 18:31:21 -05:00

79 lines
2.9 KiB
TypeScript

/**
* The auto-updater's state on this machine (scripts/update-check.mjs), for
* the master: its last check, an update that failed and waits to retry, how
* far the checkout is behind origin, and tracked files edited by hand — which
* stop the fast-forward pull. Read from the files the updater keeps and from
* git, and cached: the reporter asks every 30 seconds.
*/
import { execFileSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import type { UpdaterStatus } from './master-protocol';
const CACHE_MS = 2 * 60_000;
const BRANCH = process.env.AUTOFIRMER_BRANCH ?? 'master';
// Per process: instrumentation.ts and the route handlers each load this module.
const g = globalThis as typeof globalThis & { __updaterStatus?: { at: number; status: UpdaterStatus } };
function readScript(file: string): string | null {
try {
return readFileSync(path.join(process.cwd(), 'scripts', file), 'utf8');
} catch {
return null;
}
}
function git(args: string[]): string | null {
try {
return execFileSync('git', args, {
cwd: process.cwd(),
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
timeout: 5_000,
}).trimEnd();
} catch {
return null;
}
}
function readFailure(): UpdaterStatus['failed'] {
try {
const f = JSON.parse(readScript('.update-failed') ?? 'null') as Record<string, unknown> | null;
if (!f || typeof f.target !== 'string') return null;
const at = Number(f.at);
return {
target: f.target.slice(0, 12),
step: String(f.step ?? '').slice(0, 40),
at: Number.isFinite(at) ? new Date(at).toISOString() : '',
attempts: Number(f.count) || 1,
};
} catch {
return null;
}
}
export function updaterStatus(now = Date.now()): UpdaterStatus {
const cached = g.__updaterStatus;
if (cached && now - cached.at < CACHE_MS) return cached.status;
// "<ISO time> <status>", rewritten on every check.
const last = /^(\S+)\s+(.*)$/.exec(readScript('.last-check')?.trim() ?? '');
const behind = git(['rev-list', '--count', `HEAD..origin/${BRANCH}`]);
const changed = git(['status', '--porcelain', '--untracked-files=no']);
const status: UpdaterStatus = {
lastCheckAt: last ? last[1] : null,
lastCheck: last ? last[2].slice(0, 200) : null,
failed: readFailure(),
builtCommit: readScript('.built-commit')?.trim().slice(0, 12) || null,
behind: behind !== null && /^\d+$/.test(behind) ? Number(behind) : null,
// "XY path" per line; the status column can start with a space.
localChanges: changed
? changed.split('\n').map((line) => line.slice(3).trim()).filter(Boolean).slice(0, 10)
: [],
};
g.__updaterStatus = { at: now, status };
return status;
}