{"version":3,"sources":["../src/registry.ts"],"sourcesContent":["/**\n * Machine-level project registry + machine-wide daemon discovery.\n *\n * This module owns two surfaces under `~/.neat/`:\n *\n *  1. The legacy project registry at `~/.neat/projects.json` (ADR-048). One\n *     file, per-user, machine-local, not synced. Under the project-daemon\n *     contract (ADR-096) it is no longer the coordination point — it is read\n *     once for migration and otherwise left to the additive writes the daemon\n *     and orchestrator still perform. The read-modify-write helpers below keep\n *     their atomic-write + exclusive-lock machinery for that legacy surface.\n *\n *  2. The machine-wide daemon discovery directory at `~/.neat/daemons/`\n *     (ADR-096 §6). One file per running daemon — `<project>.json` — each owned\n *     solely by the daemon that wrote it (on start) and removes it (on graceful\n *     stop). Discovery is **append-only and lock-free**: a reader scans the\n *     directory and reconciles liveness; it never acquires a shared lock, so it\n *     can never deadlock against a daemon (#506). Losing or rebuilding the\n *     directory costs discovery convenience, not correctness — each project's\n *     own `neat-out/daemon.json` stays authoritative.\n *\n * `neat ps` / `neat list` and the per-daemon `pause` / `resume` / `uninstall`\n * verbs read discovery, falling back to the legacy registry where no daemon\n * file is present yet (the migration window before every daemon self-describes).\n *\n * The legacy lock is a file we exclusively-create (`O_EXCL`), hold while we\n * mutate, and unlink on the way out. Crude but cross-platform; matches what\n * `proper-lockfile` does internally without pulling the dep in. It never sits\n * on the discovery path.\n */\n\nimport { promises as fs, readFileSync, readdirSync, realpathSync } from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\nimport {\n  RegistryFileSchema,\n  type RegistryEntry,\n  type RegistryFile,\n  type RegistryStatus,\n} from '@neat.is/types'\n\nconst LOCK_TIMEOUT_MS = 5_000\nconst LOCK_RETRY_MS = 50\n\n// Resolve `~/.neat/` per call so tests can override `HOME` / `NEAT_HOME`\n// before each run without module-load order mattering.\nfunction neatHome(): string {\n  const override = process.env.NEAT_HOME\n  if (override && override.length > 0) return path.resolve(override)\n  return path.join(os.homedir(), '.neat')\n}\n\nexport function registryPath(): string {\n  return path.join(neatHome(), 'projects.json')\n}\n\nexport function registryLockPath(): string {\n  return path.join(neatHome(), 'projects.json.lock')\n}\n\n// The daemon writes its PID here on startup (daemon.ts). It's the authoritative\n// \"is a neat daemon running\" signal — more reliable than matching the lock's\n// own PID, since the daemon holds the registry lock only for brief read-modify-\n// write windows and an init that contends usually catches an orphaned lock\n// rather than the daemon mid-write.\nfunction daemonPidPath(): string {\n  return path.join(neatHome(), 'neatd.pid')\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// #366 / ADR-096 §6 — machine-wide daemon discovery, lock-free.\n//\n// Each running daemon drops a `<project>.json` file into `~/.neat/daemons/`\n// on start and removes it on graceful stop. The file is a copy of that\n// project's authoritative `neat-out/daemon.json` record. A daemon owns only\n// its own file — there is no shared file and no shared lock. Discovery reads\n// the directory and reconciles liveness; it never acquires a lock, so it can\n// never deadlock against a daemon mid-write the way the registry lock did\n// (#506). The shape mirrors what the daemon writes (keystone #508); we read it\n// as plain JSON rather than importing the daemon's writer so the two stay\n// decoupled, and validate defensively so a malformed file is skipped, not\n// fatal.\n// ─────────────────────────────────────────────────────────────────────────\n\nexport interface DaemonPorts {\n  rest: number\n  otlp: number\n  web: number\n}\n\nexport interface DaemonRecord {\n  project: string\n  // Project root whose `neat-out/` holds the authoritative record.\n  projectPath: string\n  pid: number\n  status: 'running' | 'stopped'\n  ports: DaemonPorts\n  // ISO8601.\n  startedAt: string\n  neatVersion: string\n}\n\n// A discovered daemon plus the liveness verdict the reader reconciled. `live`\n// is true only when the record claims `running` AND its pid is actually alive\n// — a daemon that crashed without clearing its file reads as `running` but\n// `live: false`, so `neat ps` never reports a ghost as up.\nexport interface DiscoveredDaemon {\n  record: DaemonRecord\n  live: boolean\n  // Where the discovery copy was read from. Useful for diagnostics and for the\n  // per-daemon verbs that clean a stale file.\n  source: string\n}\n\nexport function daemonsDir(): string {\n  return path.join(neatHome(), 'daemons')\n}\n\nfunction isFiniteInt(v: unknown): v is number {\n  return typeof v === 'number' && Number.isFinite(v)\n}\n\n// Parse one discovery file's contents into a DaemonRecord, or undefined when\n// the shape doesn't hold. Defensive on purpose: discovery is convenience, so a\n// half-written or hand-edited file is skipped rather than crashing `neat ps`.\nfunction parseDaemonRecord(raw: string): DaemonRecord | undefined {\n  let obj: unknown\n  try {\n    obj = JSON.parse(raw)\n  } catch {\n    return undefined\n  }\n  if (typeof obj !== 'object' || obj === null) return undefined\n  const r = obj as Record<string, unknown>\n  const ports = r.ports as Record<string, unknown> | undefined\n  if (\n    typeof r.project !== 'string' ||\n    typeof r.projectPath !== 'string' ||\n    !isFiniteInt(r.pid) ||\n    (r.status !== 'running' && r.status !== 'stopped') ||\n    typeof r.startedAt !== 'string' ||\n    typeof r.neatVersion !== 'string' ||\n    !ports ||\n    !isFiniteInt(ports.rest) ||\n    !isFiniteInt(ports.otlp) ||\n    !isFiniteInt(ports.web)\n  ) {\n    return undefined\n  }\n  return {\n    project: r.project,\n    projectPath: r.projectPath,\n    pid: r.pid,\n    status: r.status,\n    ports: { rest: ports.rest, otlp: ports.otlp, web: ports.web },\n    startedAt: r.startedAt,\n    neatVersion: r.neatVersion,\n  }\n}\n\n// Liveness for a discovered daemon. Exported (via the default probe) so the\n// verbs and tests reconcile the same way: `running` claim AND pid alive.\nexport function isPidAlive(pid: number): boolean {\n  return isPidAliveDefault(pid)\n}\n\nexport interface DiscoveryProbe {\n  isPidAlive(pid: number): boolean\n}\n\nconst defaultDiscoveryProbe: DiscoveryProbe = { isPidAlive: isPidAliveDefault }\n\n/**\n * Scan `~/.neat/daemons/` and return every well-formed discovery record with\n * its reconciled liveness. Lock-free: a plain directory read, no rendezvous.\n *\n * A missing directory (no daemon has ever started under this model) yields an\n * empty list — first run, nothing to discover. Malformed or unreadable files\n * are skipped silently; discovery degrades to \"fewer entries,\" never an error.\n */\nexport async function discoverDaemons(\n  probe: DiscoveryProbe = defaultDiscoveryProbe,\n): Promise<DiscoveredDaemon[]> {\n  const dir = daemonsDir()\n  let names: string[]\n  try {\n    names = await fs.readdir(dir)\n  } catch (err) {\n    if ((err as NodeJS.ErrnoException).code === 'ENOENT') return []\n    throw err\n  }\n  const out: DiscoveredDaemon[] = []\n  for (const name of names) {\n    if (!name.endsWith('.json')) continue\n    const file = path.join(dir, name)\n    let raw: string\n    try {\n      raw = await fs.readFile(file, 'utf8')\n    } catch {\n      continue\n    }\n    const record = parseDaemonRecord(raw)\n    if (!record) continue\n    const live = record.status === 'running' && probe.isPidAlive(record.pid)\n    out.push({ record, live, source: file })\n  }\n  out.sort((a, b) => a.record.project.localeCompare(b.record.project))\n  return out\n}\n\n/**\n * `discoverDaemons`, synchronously, for a caller that resolves its target before\n * it can await anything: the MCP server picks its daemon at module load\n * (ADR-259). Same directory, parser, liveness rule and never-throws degrading.\n */\nexport function discoverDaemonsSync(probe: DiscoveryProbe = defaultDiscoveryProbe): DiscoveredDaemon[] {\n  const dir = daemonsDir()\n  let names: string[]\n  try {\n    names = readdirSync(dir)\n  } catch {\n    return []\n  }\n  const out: DiscoveredDaemon[] = []\n  for (const name of names) {\n    if (!name.endsWith('.json')) continue\n    const file = path.join(dir, name)\n    let raw: string\n    try {\n      raw = readFileSync(file, 'utf8')\n    } catch {\n      continue\n    }\n    const record = parseDaemonRecord(raw)\n    if (!record) continue\n    const live = record.status === 'running' && probe.isPidAlive(record.pid)\n    out.push({ record, live, source: file })\n  }\n  out.sort((a, b) => a.record.project.localeCompare(b.record.project))\n  return out\n}\n\n/**\n * Remove a daemon's discovery file. A daemon owns its own file, so this is for\n * the per-daemon verbs reconciling a record whose daemon is gone (a crashed\n * daemon that never cleared its own file) — never a rendezvous another process\n * coordinates through. Best-effort: an already-absent file is success.\n */\nexport async function removeDaemonRecord(source: string): Promise<void> {\n  await fs.unlink(source).catch(() => {})\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// #366 / ADR-096 §8 — migration off the global registry.\n//\n// Installs carrying `~/.neat/projects.json` map their registered projects onto\n// per-project daemons on first run under this model. The discovery directory is\n// authoritative for \"what's running\"; the registry is read once to surface\n// projects that were registered before any daemon self-described, so the\n// machine-wide verbs still see them during the migration window. The registry\n// is no longer the coordination surface — nothing here writes it back.\n// ─────────────────────────────────────────────────────────────────────────\n\n// A unified view for the machine-wide verbs: a discovered daemon when one is\n// present, otherwise a legacy registry entry projected into the same shape so\n// `neat ps` / `neat list` render one table regardless of which surface the\n// project lives on yet.\nexport interface MachineProject {\n  project: string\n  projectPath: string\n  // 'running' / 'stopped' come from a live discovery record; 'registered'\n  // marks a legacy registry entry with no daemon file yet (migration window).\n  state: 'running' | 'stopped' | 'registered'\n  // Present only when a discovery record backs this row.\n  ports?: DaemonPorts\n  // Legacy registry status when the row came from the registry; undefined for\n  // a discovery-backed row.\n  registryStatus?: RegistryStatus\n  pid?: number\n}\n\n/**\n * The machine-wide project view the CLI verbs render. Discovery wins: every\n * `~/.neat/daemons/` record becomes a row (running or stopped per liveness).\n * Legacy registry entries whose path isn't already covered by a discovery\n * record are folded in as `registered` rows so a pre-#508 install still lists\n * its projects. Keyed on resolved path so a registry entry and its daemon\n * record collapse to one row.\n *\n * Read-only on both surfaces. The registry is read once for migration; nothing\n * here writes it back, in keeping with ADR-096 §8.\n */\nexport async function listMachineProjects(\n  probe: DiscoveryProbe = defaultDiscoveryProbe,\n): Promise<MachineProject[]> {\n  const discovered = await discoverDaemons(probe)\n  const byPath = new Map<string, MachineProject>()\n  for (const d of discovered) {\n    const key = await normalizeProjectPath(d.record.projectPath)\n    byPath.set(key, {\n      project: d.record.project,\n      projectPath: d.record.projectPath,\n      state: d.live ? 'running' : 'stopped',\n      ports: d.record.ports,\n      pid: d.record.pid,\n    })\n  }\n\n  // Migration read: legacy registry entries not already covered by a daemon\n  // record. Read once, never written back.\n  let legacy: RegistryEntry[] = []\n  try {\n    legacy = (await readRegistry()).projects\n  } catch {\n    // A corrupt legacy file shouldn't sink discovery — the daemons we found are\n    // still valid. Skip the legacy fold.\n    legacy = []\n  }\n  for (const entry of legacy) {\n    const key = await normalizeProjectPath(entry.path)\n    if (byPath.has(key)) continue\n    byPath.set(key, {\n      project: entry.name,\n      projectPath: entry.path,\n      state: 'registered',\n      registryStatus: entry.status,\n    })\n  }\n\n  return [...byPath.values()].sort((a, b) => a.project.localeCompare(b.project))\n}\n\n// Find a discovery record by project name, with its reconciled liveness. The\n// per-daemon verbs key on the project name the operator types; discovery is the\n// source of truth for whether a daemon is running and which pid to signal.\nexport async function findDaemonByProject(\n  name: string,\n  probe: DiscoveryProbe = defaultDiscoveryProbe,\n): Promise<DiscoveredDaemon | undefined> {\n  const discovered = await discoverDaemons(probe)\n  return discovered.find((d) => d.record.project === name)\n}\n\n// Signal a running daemon to shut down via its discovery-recorded pid. SIGTERM\n// is the graceful-stop signal every daemon already wires (neatd.ts) — the\n// daemon clears its own discovery file on the way out. Returns true when the\n// signal was delivered, false when the pid was already gone (nothing to stop).\nexport function signalDaemonStop(pid: number): boolean {\n  try {\n    process.kill(pid, 'SIGTERM')\n    return true\n  } catch (err) {\n    // ESRCH → already gone; treat as a no-op success of intent. EPERM and the\n    // rest surface as \"couldn't signal\" so the verb can report honestly.\n    if ((err as NodeJS.ErrnoException).code === 'ESRCH') return false\n    throw err\n  }\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// #432 — distinguish a live daemon from a genuinely stale lock.\n//\n// `neat init` used to hang the full timeout on any contended lock and then\n// print one remediation: \"remove the file by hand.\" That advice is actively\n// dangerous when a neat daemon is alive — the daemon grabs the lock for its\n// own registry writes, so hand-removing it races the daemon and corrupts the\n// registry for the live process. The lock now carries the holder PID, and the\n// timeout (or first contention with a live daemon) resolves to a message that\n// names who holds it and what's safe to do.\n// ─────────────────────────────────────────────────────────────────────────\n\nexport type LockHolder =\n  | { kind: 'daemon'; pid: number }\n  | { kind: 'command'; pid: number }\n  | { kind: 'stale' }\n\n// Probes the holder-resolution logic depends on, broken out so tests can drive\n// each branch without a real daemon or live PIDs.\nexport interface LockHolderProbe {\n  // POSIX liveness via `process.kill(pid, 0)`: true if the process exists\n  // (including EPERM — alive but owned by another user), false if it's gone.\n  isPidAlive(pid: number): boolean\n  // PID recorded in `~/.neat/neatd.pid`, or undefined if there's no pidfile.\n  daemonPidFromFile(): Promise<number | undefined>\n  // Whether the daemon's always-open `/health` endpoint answers. Confirms a\n  // live neatd.pid is actually our daemon and not a reused PID.\n  daemonResponds(): Promise<boolean>\n}\n\nfunction isPidAliveDefault(pid: number): boolean {\n  try {\n    process.kill(pid, 0)\n    return true\n  } catch (err) {\n    // ESRCH → no such process (dead). EPERM → exists but owned by another user\n    // (alive). Treat anything else as not-alive: we'd rather under-claim a live\n    // holder than wrongly block on a lock we can't verify.\n    return (err as NodeJS.ErrnoException).code === 'EPERM'\n  }\n}\n\nasync function readPidFile(file: string): Promise<number | undefined> {\n  try {\n    const raw = await fs.readFile(file, 'utf8')\n    const pid = Number.parseInt(raw.trim(), 10)\n    return Number.isInteger(pid) && pid > 0 ? pid : undefined\n  } catch {\n    return undefined\n  }\n}\n\nconst defaultLockHolderProbe: LockHolderProbe = {\n  isPidAlive: isPidAliveDefault,\n  daemonPidFromFile: () => readPidFile(daemonPidPath()),\n  async daemonResponds(): Promise<boolean> {\n    const base = process.env.NEAT_API_URL ?? 'http://localhost:8080'\n    try {\n      // `/health` stays unauthenticated in every mode (auth.ts), so a reachable\n      // daemon answers it even with a bearer token set. Any HTTP response — not\n      // just 2xx — means a daemon owns the port. A short timeout keeps the\n      // fail-fast path well under a second.\n      await fetch(`${base}/health`, { signal: AbortSignal.timeout(750) })\n      return true\n    } catch {\n      return false\n    }\n  },\n}\n\n// Read the PID a lock holder recorded when it created the lock. Undefined for a\n// legacy empty lock, an unreadable file, or a lock unlinked out from under us.\nasync function readLockPid(lockPath: string): Promise<number | undefined> {\n  return readPidFile(lockPath)\n}\n\n// Decide who holds (or orphaned) the lock.\n//\n// The lock's own PID is the evidence, so it is read first: a live process named\n// in the lock is the holder, and it is \"the daemon\" only when the daemon pidfile\n// names that same process. Each project runs its own daemon, so a live pidfile\n// and an answering port say a daemon exists somewhere on the machine — not that\n// it is the one holding this lock (#1268).\n//\n// A lock with no live PID in it can't name anyone. There a live daemon still\n// dominates: while neatd is alive, hand-removing the lock is never safe, so we\n// surface the daemon message even when the lock itself is an empty orphan. We\n// never classify our own process as the blocker — a daemon serializing two of\n// its own registry writes contends with itself briefly and should just retry.\nexport async function classifyLockHolder(\n  lockPath: string,\n  probe: LockHolderProbe = defaultLockHolderProbe,\n): Promise<LockHolder> {\n  const lockPid = await readLockPid(lockPath)\n  const daemonPid = await probe.daemonPidFromFile()\n  if (lockPid !== undefined && lockPid !== process.pid && probe.isPidAlive(lockPid)) {\n    return { kind: lockPid === daemonPid ? 'daemon' : 'command', pid: lockPid }\n  }\n  if (\n    daemonPid !== undefined &&\n    daemonPid !== process.pid &&\n    probe.isPidAlive(daemonPid) &&\n    // Guards against a stale pidfile whose PID got reused.\n    (await probe.daemonResponds())\n  ) {\n    return { kind: 'daemon', pid: daemonPid }\n  }\n  return { kind: 'stale' }\n}\n\nexport function lockHolderMessage(holder: LockHolder, lockPath: string, timeoutMs: number): string {\n  switch (holder.kind) {\n    case 'daemon':\n      return (\n        `The neat daemon (pid ${holder.pid}) is holding the registry lock. ` +\n        'Register this project through the daemon, or stop neatd and re-run `neat init`.'\n      )\n    case 'command':\n      return (\n        `Another neat command (pid ${holder.pid}) is holding the registry lock. ` +\n        \"Wait for it to finish, or check `ps` if you're not sure what's running.\"\n      )\n    case 'stale':\n      return (\n        `timed out after ${timeoutMs}ms waiting for ${lockPath}. ` +\n        'Another neat process is holding the lock; if no such process exists, remove the file by hand.'\n      )\n  }\n}\n\n/**\n * Path normalisation per ADR-048 #7. Two `init` calls from different relative\n * paths to the same dir must collapse to one entry. `path.resolve` handles\n * relative-to-cwd; we pass it through `fs.realpath` when the dir exists so\n * symlinked paths land on the same canonical entry too.\n */\nexport async function normalizeProjectPath(input: string): Promise<string> {\n  const resolved = path.resolve(input)\n  try {\n    return await fs.realpath(resolved)\n  } catch {\n    return resolved\n  }\n}\n\n/**\n * tmp + fsync + rename. The fsync on the data fd guarantees the bytes are on\n * disk before rename swaps the inode; rename itself is atomic on POSIX.\n *\n * Exported so the init flow and test harnesses can use the same helper.\n */\nexport async function writeAtomically(target: string, contents: string): Promise<void> {\n  await fs.mkdir(path.dirname(target), { recursive: true })\n  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`\n  const fd = await fs.open(tmp, 'w')\n  try {\n    await fd.writeFile(contents, 'utf8')\n    await fd.sync()\n  } finally {\n    await fd.close()\n  }\n  await fs.rename(tmp, target)\n}\n\nasync function acquireLock(\n  lockPath: string,\n  timeoutMs: number = LOCK_TIMEOUT_MS,\n  probe: LockHolderProbe = defaultLockHolderProbe,\n): Promise<void> {\n  const deadline = Date.now() + timeoutMs\n  await fs.mkdir(path.dirname(lockPath), { recursive: true })\n  let probedHolder = false\n  while (true) {\n    try {\n      const fd = await fs.open(lockPath, 'wx')\n      try {\n        // Stamp the lock with our PID so a contender can name who holds it and\n        // tell a live holder apart from a stale file. Best-effort: an empty\n        // lock still excludes correctly, it just can't be diagnosed.\n        await fd.writeFile(`${process.pid}\\n`, 'utf8')\n      } finally {\n        await fd.close()\n      }\n      return\n    } catch (err) {\n      const code = (err as NodeJS.ErrnoException).code\n      if (code !== 'EEXIST') throw err\n      // A live daemon holds the registry continuously enough that spinning the\n      // full timeout is pointless — surface the routed remediation on the first\n      // contention. A peer command clears on its own, so that one falls through\n      // to the retry.\n      if (!probedHolder) {\n        probedHolder = true\n        const holder = await classifyLockHolder(lockPath, probe)\n        if (holder.kind === 'daemon') {\n          throw new RegistryLockError(lockHolderMessage(holder, lockPath, timeoutMs))\n        }\n        // Nobody holds this one. A run killed mid-registration leaves the file\n        // behind, and it then blocked every project on the machine until someone\n        // deleted a file they had never heard of (#1241). Reclaim it instead.\n        //\n        // The race is benign: two contenders may both judge it stale and both\n        // unlink, but only one can win the exclusive create that follows and the\n        // loser sees EEXIST and retries. A lock carrying no readable PID waits\n        // out the write window first, so one being stamped right now is never\n        // taken from a live holder.\n        if (holder.kind === 'stale' && (await isReclaimableLock(lockPath))) {\n          await fs.unlink(lockPath).catch(() => {})\n          continue\n        }\n      }\n      if (Date.now() >= deadline) {\n        const holder = await classifyLockHolder(lockPath, probe)\n        throw new RegistryLockError(lockHolderMessage(holder, lockPath, timeoutMs))\n      }\n      await new Promise((r) => setTimeout(r, LOCK_RETRY_MS))\n    }\n  }\n}\n\n// How long a lock with no readable PID has to sit before it counts as\n// abandoned. The PID stamp is best-effort — a holder is briefly EEXIST-visible\n// before it has written — so a pid-less lock is only reclaimed once it has\n// outlived any plausible window between create and stamp.\nconst LOCK_UNSTAMPED_GRACE_MS = 5_000\n\n/**\n * Is this lock safe to take? Only called once the holder has been classified\n * stale — a dead PID, or none at all.\n *\n * A dead PID is unambiguous and reclaims immediately. No PID is not: it might be\n * a holder that has created the file and not yet stamped it, so that case waits\n * out the grace window and reclaims only if the file is genuinely old.\n */\nasync function isReclaimableLock(lockPath: string): Promise<boolean> {\n  const pid = await readLockPid(lockPath)\n  if (pid !== undefined) return true // classified stale with a PID = that PID is dead\n  try {\n    const stat = await fs.stat(lockPath)\n    return Date.now() - stat.mtimeMs > LOCK_UNSTAMPED_GRACE_MS\n  } catch {\n    // Gone between the EEXIST and here — the retry will simply take it.\n    return false\n  }\n}\n\nasync function releaseLock(lockPath: string): Promise<void> {\n  await fs.unlink(lockPath).catch(() => {})\n}\n\nasync function withLock<T>(fn: () => Promise<T>): Promise<T> {\n  const lock = registryLockPath()\n  await acquireLock(lock)\n  try {\n    return await fn()\n  } finally {\n    await releaseLock(lock)\n  }\n}\n\n/**\n * Read the registry from disk. Returns an empty registry if the file does\n * not exist yet — first run, never registered anything.\n *\n * Throws on parse / schema errors. The contract is single-source-of-truth;\n * a corrupt file is louder than a silent reset.\n */\nexport async function readRegistry(): Promise<RegistryFile> {\n  const file = registryPath()\n  let raw: string\n  try {\n    raw = await fs.readFile(file, 'utf8')\n  } catch (err) {\n    if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n      return { version: 1, projects: [] }\n    }\n    throw err\n  }\n  const parsed = JSON.parse(raw)\n  return RegistryFileSchema.parse(parsed)\n}\n\nasync function writeRegistry(reg: RegistryFile): Promise<void> {\n  // Re-parse before writing to surface schema drift introduced by callers\n  // mutating the in-memory object directly.\n  const validated = RegistryFileSchema.parse(reg)\n  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + '\\n')\n}\n\nexport interface AddProjectOptions {\n  name: string\n  path: string\n  languages?: string[]\n  status?: RegistryStatus\n}\n\n/**\n * Anything the registry refuses to do.\n *\n * The identity lives on the class, not in the message text. Every caller that\n * prints one already prefixes its own name — the CLI writes `neat: ` — so a\n * message carrying `neat registry:` came out as `neat: neat registry: …`\n * (#1240). A non-CLI caller that wants to know where an error came from reads\n * `err.name`, which is more reliable than a string prefix anyway.\n */\nexport class RegistryError extends Error {\n  constructor(message: string) {\n    super(message)\n    this.name = 'RegistryError'\n  }\n}\n\n/** The registry lock could not be taken, and says who holds it. */\nexport class RegistryLockError extends RegistryError {\n  constructor(message: string) {\n    super(message)\n    this.name = 'RegistryLockError'\n  }\n}\n\nexport class ProjectNameCollisionError extends RegistryError {\n  readonly projectName: string\n  constructor(name: string) {\n    super(`a project named \"${name}\" is already registered`)\n    this.name = 'ProjectNameCollisionError'\n    this.projectName = name\n  }\n}\n\n/**\n * Register a project, or update its `lastSeenAt` if the same path is already\n * registered under the same name (idempotent re-init).\n *\n * Hard error on name collision against a different path — ADR-046 #7. The\n * caller can recover by passing `--project <new-name>`.\n */\nexport async function addProject(opts: AddProjectOptions): Promise<RegistryEntry> {\n  const resolvedPath = await normalizeProjectPath(opts.path)\n  return withLock(async () => {\n    const reg = await readRegistry()\n    const byName = reg.projects.find((p) => p.name === opts.name)\n    const byPath = reg.projects.find((p) => p.path === resolvedPath)\n\n    if (byName && byName.path !== resolvedPath) {\n      throw new ProjectNameCollisionError(opts.name)\n    }\n\n    const now = new Date().toISOString()\n\n    if (byName && byName.path === resolvedPath) {\n      // Idempotent re-register: same name, same path. Refresh languages /\n      // status if the caller passed new ones.\n      byName.lastSeenAt = now\n      if (opts.languages) byName.languages = opts.languages\n      if (opts.status) byName.status = opts.status\n      await writeRegistry(reg)\n      return byName\n    }\n\n    if (byPath && byPath.name !== opts.name) {\n      // Same dir already registered under a different name. Treat as a\n      // collision so the user is forced to decide which name wins.\n      throw new ProjectNameCollisionError(byPath.name)\n    }\n\n    const entry: RegistryEntry = {\n      name: opts.name,\n      path: resolvedPath,\n      registeredAt: now,\n      languages: opts.languages ?? [],\n      status: opts.status ?? 'active',\n    }\n    reg.projects.push(entry)\n    await writeRegistry(reg)\n    return entry\n  })\n}\n\nexport async function getProject(name: string): Promise<RegistryEntry | undefined> {\n  const reg = await readRegistry()\n  return reg.projects.find((p) => p.name === name)\n}\n\n/**\n * The registered project a directory belongs to: the entry whose path is `dir` itself,\n * or failing that its nearest registered ancestor — so a verb run from `services/api`\n * inside a registered repo still finds the repo. Paths are compared after the same\n * normalisation registration applies (`normalizeProjectPath`), so a symlinked spelling\n * of the directory matches the realpath the registry stored.\n */\nexport async function findProjectByPath(dir: string): Promise<RegistryEntry | undefined> {\n  const here = await normalizeProjectPath(dir)\n  const { projects } = await readRegistry()\n  let best: RegistryEntry | undefined\n  for (const entry of projects) {\n    const inside = here === entry.path || here.startsWith(entry.path + path.sep)\n    if (inside && (!best || entry.path.length > best.path.length)) best = entry\n  }\n  return best\n}\n\n/**\n * Synchronous startup lookup for stdio clients such as MCP. This keeps the\n * registry file owned by this module while applying the same nearest-ancestor\n * rule as findProjectByPath and the same discovery parser as the CLI.\n */\nexport function registeredDaemonForPathSync(dir: string): {\n  registered: boolean\n  restPort?: number\n} {\n  const normalize = (input: string): string => {\n    const resolved = path.resolve(input)\n    try {\n      return realpathSync(resolved)\n    } catch {\n      return resolved\n    }\n  }\n  let raw: string\n  try {\n    raw = readFileSync(registryPath(), 'utf8')\n  } catch (err) {\n    if ((err as NodeJS.ErrnoException).code === 'ENOENT') return { registered: false }\n    throw err\n  }\n  const projects = RegistryFileSchema.parse(JSON.parse(raw)).projects\n  const here = normalize(dir)\n  let best: RegistryEntry | undefined\n  for (const entry of projects) {\n    const entryPath = normalize(entry.path)\n    if (\n      (here === entryPath || here.startsWith(entryPath + path.sep)) &&\n      (!best || entryPath.length > normalize(best.path).length)\n    )\n      best = entry\n  }\n  if (!best) return { registered: false }\n  let files: string[]\n  try {\n    files = readdirSync(daemonsDir())\n  } catch {\n    return { registered: true }\n  }\n  for (const file of files) {\n    if (!file.endsWith('.json')) continue\n    try {\n      const record = parseDaemonRecord(readFileSync(path.join(daemonsDir(), file), 'utf8'))\n      if (record?.project === best.name && record.status !== 'stopped') {\n        return { registered: true, restPort: record.ports.rest }\n      }\n    } catch {\n      /* skip an unreadable discovery copy */\n    }\n  }\n  return { registered: true }\n}\n\nexport async function listProjects(): Promise<RegistryEntry[]> {\n  const reg = await readRegistry()\n  return reg.projects\n}\n\nexport async function setStatus(name: string, status: RegistryStatus): Promise<RegistryEntry> {\n  return withLock(async () => {\n    const reg = await readRegistry()\n    const entry = reg.projects.find((p) => p.name === name)\n    if (!entry) throw new RegistryError(`no project named \"${name}\"`)\n    entry.status = status\n    await writeRegistry(reg)\n    return entry\n  })\n}\n\nexport async function touchLastSeen(\n  name: string,\n  at: string = new Date().toISOString(),\n): Promise<void> {\n  await withLock(async () => {\n    const reg = await readRegistry()\n    const entry = reg.projects.find((p) => p.name === name)\n    if (!entry) return\n    entry.lastSeenAt = at\n    await writeRegistry(reg)\n  })\n}\n\n/**\n * Remove the registry entry for `name`. Per ADR-048 #6: this only removes the\n * registry row. It does **not** touch `neat-out/`, `policy.json`, or any user\n * file in the project directory. SDK-install rollback is a separate flow\n * (`neat-rollback.patch`) that the caller opts in to.\n */\nexport async function removeProject(name: string): Promise<RegistryEntry | undefined> {\n  return withLock(async () => {\n    const reg = await readRegistry()\n    const idx = reg.projects.findIndex((p) => p.name === name)\n    if (idx < 0) return undefined\n    const [removed] = reg.projects.splice(idx, 1)\n    await writeRegistry(reg)\n    return removed\n  })\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// #463 — prune dead-path entries so the registry doesn't accumulate zombies.\n//\n// Ephemeral tmp-dir projects (smoke runs, throwaway demos) leave entries whose\n// `path` is gone from disk. Without pruning the daemon logs an ENOENT warning\n// for each on every startup forever and `/api/health` lists them as permanent\n// `broken` rows. Pruning removes user-facing state, so it's deliberately\n// conservative: we only ever drop an entry on a *definite* ENOENT (the path is\n// genuinely gone). A transient stat failure — EACCES, EBUSY, an unmounted\n// drive — leaves the entry untouched; it isn't dead, just unreachable.\n//\n// Two modes:\n//  - Auto-prune (daemon bootstrap / reload) gates removal on a staleness TTL.\n//    An ENOENT entry is only dropped if its `lastSeenAt` is older than the TTL,\n//    so a recently-active project whose path is temporarily unavailable stays.\n//  - `neat prune` is explicit user intent, so it drops any ENOENT entry\n//    immediately regardless of age.\n// ─────────────────────────────────────────────────────────────────────────\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n// Default 7 days, mirroring NEAT_STALE_THRESHOLDS' env-override shape.\nexport const DEFAULT_PRUNE_TTL_MS = 7 * DAY_MS\n\nexport function pruneTtlMs(): number {\n  const raw = process.env.NEAT_REGISTRY_PRUNE_TTL_MS\n  if (!raw) return DEFAULT_PRUNE_TTL_MS\n  const n = Number.parseInt(raw, 10)\n  if (Number.isFinite(n) && n >= 0) return n\n  console.warn(\n    `[neat] NEAT_REGISTRY_PRUNE_TTL_MS could not be parsed (${raw}); using default ${DEFAULT_PRUNE_TTL_MS}ms`,\n  )\n  return DEFAULT_PRUNE_TTL_MS\n}\n\n// Classify a single path: 'gone' (definite ENOENT), 'present' (stat succeeded,\n// directory exists), or 'unknown' (a transient/ambiguous error — never prune).\n// Exported so the daemon and tests can probe the same logic.\nexport type PathStatus = 'gone' | 'present' | 'unknown'\n\nasync function statPathStatus(p: string): Promise<PathStatus> {\n  try {\n    const stat = await fs.stat(p)\n    return stat.isDirectory() ? 'present' : 'unknown'\n  } catch (err) {\n    return (err as NodeJS.ErrnoException).code === 'ENOENT' ? 'gone' : 'unknown'\n  }\n}\n\nexport interface PruneOptions {\n  // Override the staleness gate. Auto-prune passes a TTL; `neat prune` passes\n  // `ttlMs: 0` to drop any ENOENT entry immediately. Default is the env-driven\n  // TTL.\n  ttlMs?: number\n  // Override the path probe (tests drive ENOENT / EACCES / present branches\n  // without a real filesystem). Defaults to a real stat.\n  statPath?: (p: string) => Promise<PathStatus>\n  // Clock injection for the staleness comparison; defaults to Date.now().\n  now?: () => number\n}\n\n/**\n * Remove registry entries whose `path` is definitely gone (ENOENT). Goes\n * through the same lock + atomic-write path as every other mutation — never a\n * raw rewrite — so the contract's atomicity invariant holds.\n *\n * Staleness gate: an ENOENT entry is dropped only when `ttlMs` is 0 (explicit\n * `neat prune`) or its `lastSeenAt` (falling back to `registeredAt`) is older\n * than `ttlMs`. A fresh ENOENT entry under a non-zero TTL is left in place — the\n * daemon still marks it `broken`, the TTL is the safety margin before removal.\n *\n * Returns the entries that were removed.\n */\nexport async function pruneRegistry(opts: PruneOptions = {}): Promise<RegistryEntry[]> {\n  const ttlMs = opts.ttlMs ?? pruneTtlMs()\n  const statPath = opts.statPath ?? statPathStatus\n  const now = opts.now ?? Date.now\n\n  return withLock(async () => {\n    const reg = await readRegistry()\n    const removed: RegistryEntry[] = []\n    const kept: RegistryEntry[] = []\n    for (const entry of reg.projects) {\n      const status = await statPath(entry.path)\n      if (status !== 'gone') {\n        // Present, or a transient/ambiguous error — keep it. We only prune on a\n        // definite ENOENT.\n        kept.push(entry)\n        continue\n      }\n      // Path is gone. Drop it immediately when there's no TTL gate, otherwise\n      // only once it's been quiet longer than the TTL.\n      if (ttlMs <= 0) {\n        removed.push(entry)\n        continue\n      }\n      const lastSeen = Date.parse(entry.lastSeenAt ?? entry.registeredAt)\n      const age = now() - (Number.isFinite(lastSeen) ? lastSeen : 0)\n      if (age > ttlMs) {\n        removed.push(entry)\n      } else {\n        kept.push(entry)\n      }\n    }\n    if (removed.length === 0) return []\n    reg.projects = kept\n    await writeRegistry(reg)\n    return removed\n  })\n}\n"],"mappings":"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","names":["path","os","fs"]}