{"version":3,"file":"process-registry.mjs","names":[],"sources":["../src/process-registry.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { access } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { Effect } from \"effect\";\n\nexport type ProcessRole = \"standalone\" | \"workspace-parent\" | \"workspace-child\";\n\nexport interface PidEntry {\n  pid: number;\n  configDir: string;\n  parentPid?: number;\n  role: ProcessRole;\n  ports: Record<string, number>;\n  budget?: { min: number; max: number };\n  startedAt: number;\n  description: string;\n}\n\nfunction getRegistryDir(): string {\n  return join(homedir(), \".cache\", \"everything-dev\");\n}\n\nfunction ensureRegistryDir(): void {\n  const path = getRegistryPath();\n  const dir = join(path, \"..\");\n  if (!existsSync(dir)) {\n    mkdirSync(dir, { recursive: true });\n  }\n}\n\nexport function readRegistry(): PidEntry[] {\n  const path = getRegistryPath();\n  if (!existsSync(path)) return [];\n  try {\n    const raw = JSON.parse(readFileSync(path, \"utf-8\"));\n    if (!Array.isArray(raw)) return [];\n    return raw.filter(\n      (entry): entry is PidEntry =>\n        entry &&\n        typeof entry === \"object\" &&\n        typeof entry.pid === \"number\" &&\n        typeof entry.configDir === \"string\" &&\n        typeof entry.role === \"string\",\n    );\n  } catch {\n    return [];\n  }\n}\n\nexport function isPidAlive(pid: number): boolean {\n  try {\n    process.kill(pid, 0);\n    return true;\n  } catch (err) {\n    return (err as NodeJS.ErrnoException).code !== \"ESRCH\";\n  }\n}\n\nexport function pruneDead(entries: PidEntry[]): PidEntry[] {\n  return entries.filter(\n    (entry) => entry.pid > 1 && existsSync(entry.configDir) && isPidAlive(entry.pid),\n  );\n}\n\nexport function pruneDeadEffect(entries: PidEntry[]): Effect.Effect<PidEntry[]> {\n  return Effect.forEach(\n    entries,\n    (entry) => {\n      if (entry.pid <= 1) return Effect.succeed(null);\n      return Effect.gen(function* () {\n        const dirExists = yield* Effect.promise(() =>\n          access(entry.configDir)\n            .then(() => true)\n            .catch(() => false),\n        );\n        if (!dirExists) return null;\n        if (!isPidAlive(entry.pid)) return null;\n        return entry;\n      });\n    },\n    { concurrency: \"unbounded\" },\n  ).pipe(Effect.map((results) => results.filter((e): e is PidEntry => e !== null)));\n}\n\nexport function writeRegistry(entries: PidEntry[]): void {\n  ensureRegistryDir();\n  const path = getRegistryPath();\n  const tmpPath = `${path}.tmp`;\n  writeFileSync(tmpPath, `${JSON.stringify(entries, null, 2)}\\n`);\n  renameSync(tmpPath, path);\n}\n\nexport function registerStandalone(entry: Omit<PidEntry, \"role\">): PidEntry {\n  const live = pruneDead(readRegistry());\n  const full: PidEntry = { ...entry, role: \"standalone\" };\n  const withoutSelf = live.filter((existing) => existing.pid !== entry.pid);\n  withoutSelf.push(full);\n  writeRegistry(withoutSelf);\n  return full;\n}\n\nexport function registerEntry(entry: PidEntry): void {\n  const live = pruneDead(readRegistry());\n  const withoutSelf = live.filter((existing) => existing.pid !== entry.pid);\n  withoutSelf.push(entry);\n  writeRegistry(withoutSelf);\n}\n\nexport function unregisterPid(pid: number): void {\n  const live = pruneDead(readRegistry());\n  const next = live.filter((entry) => entry.pid !== pid);\n  if (next.length === live.length) return;\n  writeRegistry(next);\n}\n\nexport function removeRegistryFile(): void {\n  const path = getRegistryPath();\n  if (existsSync(path)) {\n    rmSync(path, { force: true });\n  }\n}\n\nexport function getRegistryPath(): string {\n  return process.env.BO_PID_REGISTRY_PATH ?? join(getRegistryDir(), \"pids.json\");\n}\n\nexport function claimedPorts(): Set<number> {\n  const live = pruneDead(readRegistry());\n  const out = new Set<number>();\n  for (const entry of live) {\n    for (const port of Object.values(entry.ports)) {\n      if (typeof port === \"number\") out.add(port);\n    }\n  }\n  return out;\n}\n"],"mappings":";;;;;;;AAmBA,SAAS,iBAAyB;AAChC,QAAO,KAAK,SAAS,EAAE,UAAU,iBAAiB;;AAGpD,SAAS,oBAA0B;CAEjC,MAAM,MAAM,KADC,iBACQ,EAAE,KAAK;AAC5B,KAAI,CAAC,WAAW,IAAI,CAClB,WAAU,KAAK,EAAE,WAAW,MAAM,CAAC;;AAIvC,SAAgB,eAA2B;CACzC,MAAM,OAAO,iBAAiB;AAC9B,KAAI,CAAC,WAAW,KAAK,CAAE,QAAO,EAAE;AAChC,KAAI;EACF,MAAM,MAAM,KAAK,MAAM,aAAa,MAAM,QAAQ,CAAC;AACnD,MAAI,CAAC,MAAM,QAAQ,IAAI,CAAE,QAAO,EAAE;AAClC,SAAO,IAAI,QACR,UACC,SACA,OAAO,UAAU,YACjB,OAAO,MAAM,QAAQ,YACrB,OAAO,MAAM,cAAc,YAC3B,OAAO,MAAM,SAAS,SACzB;SACK;AACN,SAAO,EAAE;;;AAIb,SAAgB,WAAW,KAAsB;AAC/C,KAAI;AACF,UAAQ,KAAK,KAAK,EAAE;AACpB,SAAO;UACA,KAAK;AACZ,SAAQ,IAA8B,SAAS;;;AAInD,SAAgB,UAAU,SAAiC;AACzD,QAAO,QAAQ,QACZ,UAAU,MAAM,MAAM,KAAK,WAAW,MAAM,UAAU,IAAI,WAAW,MAAM,IAAI,CACjF;;AAGH,SAAgB,gBAAgB,SAAgD;AAC9E,QAAO,OAAO,QACZ,UACC,UAAU;AACT,MAAI,MAAM,OAAO,EAAG,QAAO,OAAO,QAAQ,KAAK;AAC/C,SAAO,OAAO,IAAI,aAAa;AAM7B,OAAI,EAAC,OALoB,OAAO,cAC9B,OAAO,MAAM,UAAU,CACpB,WAAW,KAAK,CAChB,YAAY,MAAM,CACtB,EACe,QAAO;AACvB,OAAI,CAAC,WAAW,MAAM,IAAI,CAAE,QAAO;AACnC,UAAO;IACP;IAEJ,EAAE,aAAa,aAAa,CAC7B,CAAC,KAAK,OAAO,KAAK,YAAY,QAAQ,QAAQ,MAAqB,MAAM,KAAK,CAAC,CAAC;;AAGnF,SAAgB,cAAc,SAA2B;AACvD,oBAAmB;CACnB,MAAM,OAAO,iBAAiB;CAC9B,MAAM,UAAU,GAAG,KAAK;AACxB,eAAc,SAAS,GAAG,KAAK,UAAU,SAAS,MAAM,EAAE,CAAC,IAAI;AAC/D,YAAW,SAAS,KAAK;;AAG3B,SAAgB,mBAAmB,OAAyC;CAC1E,MAAM,OAAO,UAAU,cAAc,CAAC;CACtC,MAAM,OAAiB;EAAE,GAAG;EAAO,MAAM;EAAc;CACvD,MAAM,cAAc,KAAK,QAAQ,aAAa,SAAS,QAAQ,MAAM,IAAI;AACzE,aAAY,KAAK,KAAK;AACtB,eAAc,YAAY;AAC1B,QAAO;;AAUT,SAAgB,cAAc,KAAmB;CAC/C,MAAM,OAAO,UAAU,cAAc,CAAC;CACtC,MAAM,OAAO,KAAK,QAAQ,UAAU,MAAM,QAAQ,IAAI;AACtD,KAAI,KAAK,WAAW,KAAK,OAAQ;AACjC,eAAc,KAAK;;AAUrB,SAAgB,kBAA0B;AACxC,QAAO,QAAQ,IAAI,wBAAwB,KAAK,gBAAgB,EAAE,YAAY;;AAGhF,SAAgB,eAA4B;CAC1C,MAAM,OAAO,UAAU,cAAc,CAAC;CACtC,MAAM,sBAAM,IAAI,KAAa;AAC7B,MAAK,MAAM,SAAS,KAClB,MAAK,MAAM,QAAQ,OAAO,OAAO,MAAM,MAAM,CAC3C,KAAI,OAAO,SAAS,SAAU,KAAI,IAAI,KAAK;AAG/C,QAAO"}