zz_

app/cli/lib/processRegistry.ts

zzmq.dev@gmail.com/zz / app / cli / lib / processRegistry.ts @ 4a679371 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];
 }