/** * 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 | 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; // " ", 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; }