app/cli/lib/processRegistry.ts
zzmq.dev@gmail.com/zz / app / cli / lib / processRegistry.ts
@ 6ddce32f
Download
Ladder Summary
- file The module implements a tiny on‑disk registry that tracks which local **zz** processes are currently running and which repository slugs each of them is watching. It writes a JSON file keyed by the process ID when `registerRunningProcess` is called, removes it with `unregisterRunningProcess`, and `listRunningRepos` scans the directory, filters out entries whose PID is no longer alive, and returns a deduplicated list of all repos still being watched. Exported symbols: `registerRunningProcess`, `unregisterRunningProcess`, `listRunningRepos`.
// A lightweight "who else is running zz right now, and on what" --
// the local-mode equivalent of remote mode's `/api/me/repos`: `zz top
// --local` with no config/`--repos` has no server to ask, so it asks
// the filesystem instead. One file per process (keyed by pid, so a
// second process never collides with or overwrites the first's),
// written on start and removed on clean exit; `zz top` unions
// whatever's left, dropping any entry whose pid is no longer running
// (a crash/kill -9 leaves its file behind -- this is the self-healing
// part, not a separate cleanup pass someone has to remember to run).
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
interface RunningEntry {
pid: number;
repos: string[];
startedAt: string;
}
function registryDir(): string {
return join(homedir(), ".zz", "running");
}
function entryPath(pid: number): string {
return join(registryDir(), `${pid}.json`);
}
/** Called once at startup (zz work/zz review, local mode only -- remote mode has the server's own /api/me/repos for this) and again any time the watched set grows (a task shard), so a later `zz top --local` sees the full picture, not just the repo(s) the process started with. */
export function registerRunningProcess(repos: string[]): void {
mkdirSync(registryDir(), { recursive: true });
const entry: RunningEntry = { pid: process.pid, repos, startedAt: new Date().toISOString() };
writeFileSync(entryPath(process.pid), JSON.stringify(entry), "utf8");
}
/** Idempotent, best-effort -- safe to call from a SIGINT/SIGTERM handler and a normal finally without erroring on the second call. */
export function unregisterRunningProcess(): void {
try {
rmSync(entryPath(process.pid), { force: true });
} catch {
// best-effort only
}
}
function isAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
/** Every repo slug any other (still-alive) local zz process is currently watching, deduped. Stale entries (dead pid) are skipped, not deleted here -- cheap enough to re-check every call, and deleting on someone else's behalf risks a race against that process's own exit cleanup. */
export function listRunningRepos(): string[] {
const dir = registryDir();
if (!existsSync(dir)) return [];
const slugs = new Set<string>();
for (const file of readdirSync(dir)) {
if (!file.endsWith(".json")) continue;
try {
const entry = JSON.parse(readFileSync(join(dir, file), "utf8")) as RunningEntry;
if (entry.pid !== process.pid && !isAlive(entry.pid)) continue;
for (const slug of entry.repos) slugs.add(slug);
} catch {
// a half-written or corrupt entry -- skip rather than fail the whole listing
}
}
return [...slugs];
}