@neat.is/core 0.10.5 → 0.10.6-dev.20261008

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/{chunk-DVCGDMTZ.js → chunk-6QMWJAWR.js} +1454 -1019
  2. package/dist/chunk-6QMWJAWR.js.map +1 -0
  3. package/dist/{chunk-Q2SL2SVH.js → chunk-EMFX3V77.js} +43 -224
  4. package/dist/chunk-EMFX3V77.js.map +1 -0
  5. package/dist/chunk-MR64ICXX.js +19 -0
  6. package/dist/chunk-MR64ICXX.js.map +1 -0
  7. package/dist/{chunk-EFBWQIKG.js → chunk-QPG2QXCW.js} +11 -27
  8. package/dist/chunk-QPG2QXCW.js.map +1 -0
  9. package/dist/chunk-RGYC5U2Q.js +511 -0
  10. package/dist/chunk-RGYC5U2Q.js.map +1 -0
  11. package/dist/{chunk-LXQCL4MP.js → chunk-TZXSWSHX.js} +2 -2
  12. package/dist/{chunk-BC53SCT7.js → chunk-VFZ6CXLI.js} +20 -6
  13. package/dist/chunk-VFZ6CXLI.js.map +1 -0
  14. package/dist/cli.cjs +2065 -1178
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.d.cts +2 -0
  17. package/dist/cli.d.ts +2 -0
  18. package/dist/cli.js +249 -76
  19. package/dist/cli.js.map +1 -1
  20. package/dist/index.cjs +1653 -952
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +45 -99
  23. package/dist/index.d.ts +45 -99
  24. package/dist/index.js +18 -15
  25. package/dist/neatd.cjs +1666 -965
  26. package/dist/neatd.cjs.map +1 -1
  27. package/dist/neatd.js +7 -4
  28. package/dist/neatd.js.map +1 -1
  29. package/dist/otel-grpc-CCCI772O.js +11 -0
  30. package/dist/registry.cjs +570 -0
  31. package/dist/registry.cjs.map +1 -0
  32. package/dist/registry.d.cts +220 -0
  33. package/dist/registry.d.ts +220 -0
  34. package/dist/registry.js +62 -0
  35. package/dist/registry.js.map +1 -0
  36. package/dist/server.cjs +1384 -657
  37. package/dist/server.cjs.map +1 -1
  38. package/dist/server.js +10 -6
  39. package/dist/server.js.map +1 -1
  40. package/package.json +9 -3
  41. package/dist/chunk-BC53SCT7.js.map +0 -1
  42. package/dist/chunk-DVCGDMTZ.js.map +0 -1
  43. package/dist/chunk-EFBWQIKG.js.map +0 -1
  44. package/dist/chunk-Q2SL2SVH.js.map +0 -1
  45. package/dist/otel-grpc-B7ABHLV7.js +0 -10
  46. /package/dist/{chunk-LXQCL4MP.js.map → chunk-TZXSWSHX.js.map} +0 -0
  47. /package/dist/{otel-grpc-B7ABHLV7.js.map → otel-grpc-CCCI772O.js.map} +0 -0
@@ -0,0 +1,511 @@
1
+ // src/registry.ts
2
+ import { promises as fs, readFileSync, readdirSync, realpathSync } from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import {
6
+ RegistryFileSchema
7
+ } from "@neat.is/types";
8
+ var LOCK_TIMEOUT_MS = 5e3;
9
+ var LOCK_RETRY_MS = 50;
10
+ function neatHome() {
11
+ const override = process.env.NEAT_HOME;
12
+ if (override && override.length > 0) return path.resolve(override);
13
+ return path.join(os.homedir(), ".neat");
14
+ }
15
+ function registryPath() {
16
+ return path.join(neatHome(), "projects.json");
17
+ }
18
+ function registryLockPath() {
19
+ return path.join(neatHome(), "projects.json.lock");
20
+ }
21
+ function daemonPidPath() {
22
+ return path.join(neatHome(), "neatd.pid");
23
+ }
24
+ function daemonsDir() {
25
+ return path.join(neatHome(), "daemons");
26
+ }
27
+ function isFiniteInt(v) {
28
+ return typeof v === "number" && Number.isFinite(v);
29
+ }
30
+ function parseDaemonRecord(raw) {
31
+ let obj;
32
+ try {
33
+ obj = JSON.parse(raw);
34
+ } catch {
35
+ return void 0;
36
+ }
37
+ if (typeof obj !== "object" || obj === null) return void 0;
38
+ const r = obj;
39
+ const ports = r.ports;
40
+ if (typeof r.project !== "string" || typeof r.projectPath !== "string" || !isFiniteInt(r.pid) || r.status !== "running" && r.status !== "stopped" || typeof r.startedAt !== "string" || typeof r.neatVersion !== "string" || !ports || !isFiniteInt(ports.rest) || !isFiniteInt(ports.otlp) || !isFiniteInt(ports.web)) {
41
+ return void 0;
42
+ }
43
+ return {
44
+ project: r.project,
45
+ projectPath: r.projectPath,
46
+ pid: r.pid,
47
+ status: r.status,
48
+ ports: { rest: ports.rest, otlp: ports.otlp, web: ports.web },
49
+ startedAt: r.startedAt,
50
+ neatVersion: r.neatVersion
51
+ };
52
+ }
53
+ function isPidAlive(pid) {
54
+ return isPidAliveDefault(pid);
55
+ }
56
+ var defaultDiscoveryProbe = { isPidAlive: isPidAliveDefault };
57
+ async function discoverDaemons(probe = defaultDiscoveryProbe) {
58
+ const dir = daemonsDir();
59
+ let names;
60
+ try {
61
+ names = await fs.readdir(dir);
62
+ } catch (err) {
63
+ if (err.code === "ENOENT") return [];
64
+ throw err;
65
+ }
66
+ const out = [];
67
+ for (const name of names) {
68
+ if (!name.endsWith(".json")) continue;
69
+ const file = path.join(dir, name);
70
+ let raw;
71
+ try {
72
+ raw = await fs.readFile(file, "utf8");
73
+ } catch {
74
+ continue;
75
+ }
76
+ const record = parseDaemonRecord(raw);
77
+ if (!record) continue;
78
+ const live = record.status === "running" && probe.isPidAlive(record.pid);
79
+ out.push({ record, live, source: file });
80
+ }
81
+ out.sort((a, b) => a.record.project.localeCompare(b.record.project));
82
+ return out;
83
+ }
84
+ async function removeDaemonRecord(source) {
85
+ await fs.unlink(source).catch(() => {
86
+ });
87
+ }
88
+ async function listMachineProjects(probe = defaultDiscoveryProbe) {
89
+ const discovered = await discoverDaemons(probe);
90
+ const byPath = /* @__PURE__ */ new Map();
91
+ for (const d of discovered) {
92
+ const key = await normalizeProjectPath(d.record.projectPath);
93
+ byPath.set(key, {
94
+ project: d.record.project,
95
+ projectPath: d.record.projectPath,
96
+ state: d.live ? "running" : "stopped",
97
+ ports: d.record.ports,
98
+ pid: d.record.pid
99
+ });
100
+ }
101
+ let legacy = [];
102
+ try {
103
+ legacy = (await readRegistry()).projects;
104
+ } catch {
105
+ legacy = [];
106
+ }
107
+ for (const entry of legacy) {
108
+ const key = await normalizeProjectPath(entry.path);
109
+ if (byPath.has(key)) continue;
110
+ byPath.set(key, {
111
+ project: entry.name,
112
+ projectPath: entry.path,
113
+ state: "registered",
114
+ registryStatus: entry.status
115
+ });
116
+ }
117
+ return [...byPath.values()].sort((a, b) => a.project.localeCompare(b.project));
118
+ }
119
+ async function findDaemonByProject(name, probe = defaultDiscoveryProbe) {
120
+ const discovered = await discoverDaemons(probe);
121
+ return discovered.find((d) => d.record.project === name);
122
+ }
123
+ function signalDaemonStop(pid) {
124
+ try {
125
+ process.kill(pid, "SIGTERM");
126
+ return true;
127
+ } catch (err) {
128
+ if (err.code === "ESRCH") return false;
129
+ throw err;
130
+ }
131
+ }
132
+ function isPidAliveDefault(pid) {
133
+ try {
134
+ process.kill(pid, 0);
135
+ return true;
136
+ } catch (err) {
137
+ return err.code === "EPERM";
138
+ }
139
+ }
140
+ async function readPidFile(file) {
141
+ try {
142
+ const raw = await fs.readFile(file, "utf8");
143
+ const pid = Number.parseInt(raw.trim(), 10);
144
+ return Number.isInteger(pid) && pid > 0 ? pid : void 0;
145
+ } catch {
146
+ return void 0;
147
+ }
148
+ }
149
+ var defaultLockHolderProbe = {
150
+ isPidAlive: isPidAliveDefault,
151
+ daemonPidFromFile: () => readPidFile(daemonPidPath()),
152
+ async daemonResponds() {
153
+ const base = process.env.NEAT_API_URL ?? "http://localhost:8080";
154
+ try {
155
+ await fetch(`${base}/health`, { signal: AbortSignal.timeout(750) });
156
+ return true;
157
+ } catch {
158
+ return false;
159
+ }
160
+ }
161
+ };
162
+ async function readLockPid(lockPath) {
163
+ return readPidFile(lockPath);
164
+ }
165
+ async function classifyLockHolder(lockPath, probe = defaultLockHolderProbe) {
166
+ const lockPid = await readLockPid(lockPath);
167
+ const daemonPid = await probe.daemonPidFromFile();
168
+ if (lockPid !== void 0 && lockPid !== process.pid && probe.isPidAlive(lockPid)) {
169
+ return { kind: lockPid === daemonPid ? "daemon" : "command", pid: lockPid };
170
+ }
171
+ if (daemonPid !== void 0 && daemonPid !== process.pid && probe.isPidAlive(daemonPid) && // Guards against a stale pidfile whose PID got reused.
172
+ await probe.daemonResponds()) {
173
+ return { kind: "daemon", pid: daemonPid };
174
+ }
175
+ return { kind: "stale" };
176
+ }
177
+ function lockHolderMessage(holder, lockPath, timeoutMs) {
178
+ switch (holder.kind) {
179
+ case "daemon":
180
+ return `The neat daemon (pid ${holder.pid}) is holding the registry lock. Register this project through the daemon, or stop neatd and re-run \`neat init\`.`;
181
+ case "command":
182
+ return `Another neat command (pid ${holder.pid}) is holding the registry lock. Wait for it to finish, or check \`ps\` if you're not sure what's running.`;
183
+ case "stale":
184
+ return `timed out after ${timeoutMs}ms waiting for ${lockPath}. Another neat process is holding the lock; if no such process exists, remove the file by hand.`;
185
+ }
186
+ }
187
+ async function normalizeProjectPath(input) {
188
+ const resolved = path.resolve(input);
189
+ try {
190
+ return await fs.realpath(resolved);
191
+ } catch {
192
+ return resolved;
193
+ }
194
+ }
195
+ async function writeAtomically(target, contents) {
196
+ await fs.mkdir(path.dirname(target), { recursive: true });
197
+ const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
198
+ const fd = await fs.open(tmp, "w");
199
+ try {
200
+ await fd.writeFile(contents, "utf8");
201
+ await fd.sync();
202
+ } finally {
203
+ await fd.close();
204
+ }
205
+ await fs.rename(tmp, target);
206
+ }
207
+ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
208
+ const deadline = Date.now() + timeoutMs;
209
+ await fs.mkdir(path.dirname(lockPath), { recursive: true });
210
+ let probedHolder = false;
211
+ while (true) {
212
+ try {
213
+ const fd = await fs.open(lockPath, "wx");
214
+ try {
215
+ await fd.writeFile(`${process.pid}
216
+ `, "utf8");
217
+ } finally {
218
+ await fd.close();
219
+ }
220
+ return;
221
+ } catch (err) {
222
+ const code = err.code;
223
+ if (code !== "EEXIST") throw err;
224
+ if (!probedHolder) {
225
+ probedHolder = true;
226
+ const holder = await classifyLockHolder(lockPath, probe);
227
+ if (holder.kind === "daemon") {
228
+ throw new RegistryLockError(lockHolderMessage(holder, lockPath, timeoutMs));
229
+ }
230
+ if (holder.kind === "stale" && await isReclaimableLock(lockPath)) {
231
+ await fs.unlink(lockPath).catch(() => {
232
+ });
233
+ continue;
234
+ }
235
+ }
236
+ if (Date.now() >= deadline) {
237
+ const holder = await classifyLockHolder(lockPath, probe);
238
+ throw new RegistryLockError(lockHolderMessage(holder, lockPath, timeoutMs));
239
+ }
240
+ await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
241
+ }
242
+ }
243
+ }
244
+ var LOCK_UNSTAMPED_GRACE_MS = 5e3;
245
+ async function isReclaimableLock(lockPath) {
246
+ const pid = await readLockPid(lockPath);
247
+ if (pid !== void 0) return true;
248
+ try {
249
+ const stat = await fs.stat(lockPath);
250
+ return Date.now() - stat.mtimeMs > LOCK_UNSTAMPED_GRACE_MS;
251
+ } catch {
252
+ return false;
253
+ }
254
+ }
255
+ async function releaseLock(lockPath) {
256
+ await fs.unlink(lockPath).catch(() => {
257
+ });
258
+ }
259
+ async function withLock(fn) {
260
+ const lock = registryLockPath();
261
+ await acquireLock(lock);
262
+ try {
263
+ return await fn();
264
+ } finally {
265
+ await releaseLock(lock);
266
+ }
267
+ }
268
+ async function readRegistry() {
269
+ const file = registryPath();
270
+ let raw;
271
+ try {
272
+ raw = await fs.readFile(file, "utf8");
273
+ } catch (err) {
274
+ if (err.code === "ENOENT") {
275
+ return { version: 1, projects: [] };
276
+ }
277
+ throw err;
278
+ }
279
+ const parsed = JSON.parse(raw);
280
+ return RegistryFileSchema.parse(parsed);
281
+ }
282
+ async function writeRegistry(reg) {
283
+ const validated = RegistryFileSchema.parse(reg);
284
+ await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
285
+ }
286
+ var RegistryError = class extends Error {
287
+ constructor(message) {
288
+ super(message);
289
+ this.name = "RegistryError";
290
+ }
291
+ };
292
+ var RegistryLockError = class extends RegistryError {
293
+ constructor(message) {
294
+ super(message);
295
+ this.name = "RegistryLockError";
296
+ }
297
+ };
298
+ var ProjectNameCollisionError = class extends RegistryError {
299
+ projectName;
300
+ constructor(name) {
301
+ super(`a project named "${name}" is already registered`);
302
+ this.name = "ProjectNameCollisionError";
303
+ this.projectName = name;
304
+ }
305
+ };
306
+ async function addProject(opts) {
307
+ const resolvedPath = await normalizeProjectPath(opts.path);
308
+ return withLock(async () => {
309
+ const reg = await readRegistry();
310
+ const byName = reg.projects.find((p) => p.name === opts.name);
311
+ const byPath = reg.projects.find((p) => p.path === resolvedPath);
312
+ if (byName && byName.path !== resolvedPath) {
313
+ throw new ProjectNameCollisionError(opts.name);
314
+ }
315
+ const now = (/* @__PURE__ */ new Date()).toISOString();
316
+ if (byName && byName.path === resolvedPath) {
317
+ byName.lastSeenAt = now;
318
+ if (opts.languages) byName.languages = opts.languages;
319
+ if (opts.status) byName.status = opts.status;
320
+ await writeRegistry(reg);
321
+ return byName;
322
+ }
323
+ if (byPath && byPath.name !== opts.name) {
324
+ throw new ProjectNameCollisionError(byPath.name);
325
+ }
326
+ const entry = {
327
+ name: opts.name,
328
+ path: resolvedPath,
329
+ registeredAt: now,
330
+ languages: opts.languages ?? [],
331
+ status: opts.status ?? "active"
332
+ };
333
+ reg.projects.push(entry);
334
+ await writeRegistry(reg);
335
+ return entry;
336
+ });
337
+ }
338
+ async function getProject(name) {
339
+ const reg = await readRegistry();
340
+ return reg.projects.find((p) => p.name === name);
341
+ }
342
+ async function findProjectByPath(dir) {
343
+ const here = await normalizeProjectPath(dir);
344
+ const { projects } = await readRegistry();
345
+ let best;
346
+ for (const entry of projects) {
347
+ const inside = here === entry.path || here.startsWith(entry.path + path.sep);
348
+ if (inside && (!best || entry.path.length > best.path.length)) best = entry;
349
+ }
350
+ return best;
351
+ }
352
+ function registeredDaemonForPathSync(dir) {
353
+ const normalize = (input) => {
354
+ const resolved = path.resolve(input);
355
+ try {
356
+ return realpathSync(resolved);
357
+ } catch {
358
+ return resolved;
359
+ }
360
+ };
361
+ let raw;
362
+ try {
363
+ raw = readFileSync(registryPath(), "utf8");
364
+ } catch (err) {
365
+ if (err.code === "ENOENT") return { registered: false };
366
+ throw err;
367
+ }
368
+ const projects = RegistryFileSchema.parse(JSON.parse(raw)).projects;
369
+ const here = normalize(dir);
370
+ let best;
371
+ for (const entry of projects) {
372
+ const entryPath = normalize(entry.path);
373
+ if ((here === entryPath || here.startsWith(entryPath + path.sep)) && (!best || entryPath.length > normalize(best.path).length))
374
+ best = entry;
375
+ }
376
+ if (!best) return { registered: false };
377
+ let files;
378
+ try {
379
+ files = readdirSync(daemonsDir());
380
+ } catch {
381
+ return { registered: true };
382
+ }
383
+ for (const file of files) {
384
+ if (!file.endsWith(".json")) continue;
385
+ try {
386
+ const record = parseDaemonRecord(readFileSync(path.join(daemonsDir(), file), "utf8"));
387
+ if (record?.project === best.name && record.status !== "stopped") {
388
+ return { registered: true, restPort: record.ports.rest };
389
+ }
390
+ } catch {
391
+ }
392
+ }
393
+ return { registered: true };
394
+ }
395
+ async function listProjects() {
396
+ const reg = await readRegistry();
397
+ return reg.projects;
398
+ }
399
+ async function setStatus(name, status) {
400
+ return withLock(async () => {
401
+ const reg = await readRegistry();
402
+ const entry = reg.projects.find((p) => p.name === name);
403
+ if (!entry) throw new RegistryError(`no project named "${name}"`);
404
+ entry.status = status;
405
+ await writeRegistry(reg);
406
+ return entry;
407
+ });
408
+ }
409
+ async function touchLastSeen(name, at = (/* @__PURE__ */ new Date()).toISOString()) {
410
+ await withLock(async () => {
411
+ const reg = await readRegistry();
412
+ const entry = reg.projects.find((p) => p.name === name);
413
+ if (!entry) return;
414
+ entry.lastSeenAt = at;
415
+ await writeRegistry(reg);
416
+ });
417
+ }
418
+ async function removeProject(name) {
419
+ return withLock(async () => {
420
+ const reg = await readRegistry();
421
+ const idx = reg.projects.findIndex((p) => p.name === name);
422
+ if (idx < 0) return void 0;
423
+ const [removed] = reg.projects.splice(idx, 1);
424
+ await writeRegistry(reg);
425
+ return removed;
426
+ });
427
+ }
428
+ var DAY_MS = 24 * 60 * 60 * 1e3;
429
+ var DEFAULT_PRUNE_TTL_MS = 7 * DAY_MS;
430
+ function pruneTtlMs() {
431
+ const raw = process.env.NEAT_REGISTRY_PRUNE_TTL_MS;
432
+ if (!raw) return DEFAULT_PRUNE_TTL_MS;
433
+ const n = Number.parseInt(raw, 10);
434
+ if (Number.isFinite(n) && n >= 0) return n;
435
+ console.warn(
436
+ `[neat] NEAT_REGISTRY_PRUNE_TTL_MS could not be parsed (${raw}); using default ${DEFAULT_PRUNE_TTL_MS}ms`
437
+ );
438
+ return DEFAULT_PRUNE_TTL_MS;
439
+ }
440
+ async function statPathStatus(p) {
441
+ try {
442
+ const stat = await fs.stat(p);
443
+ return stat.isDirectory() ? "present" : "unknown";
444
+ } catch (err) {
445
+ return err.code === "ENOENT" ? "gone" : "unknown";
446
+ }
447
+ }
448
+ async function pruneRegistry(opts = {}) {
449
+ const ttlMs = opts.ttlMs ?? pruneTtlMs();
450
+ const statPath = opts.statPath ?? statPathStatus;
451
+ const now = opts.now ?? Date.now;
452
+ return withLock(async () => {
453
+ const reg = await readRegistry();
454
+ const removed = [];
455
+ const kept = [];
456
+ for (const entry of reg.projects) {
457
+ const status = await statPath(entry.path);
458
+ if (status !== "gone") {
459
+ kept.push(entry);
460
+ continue;
461
+ }
462
+ if (ttlMs <= 0) {
463
+ removed.push(entry);
464
+ continue;
465
+ }
466
+ const lastSeen = Date.parse(entry.lastSeenAt ?? entry.registeredAt);
467
+ const age = now() - (Number.isFinite(lastSeen) ? lastSeen : 0);
468
+ if (age > ttlMs) {
469
+ removed.push(entry);
470
+ } else {
471
+ kept.push(entry);
472
+ }
473
+ }
474
+ if (removed.length === 0) return [];
475
+ reg.projects = kept;
476
+ await writeRegistry(reg);
477
+ return removed;
478
+ });
479
+ }
480
+
481
+ export {
482
+ registryPath,
483
+ registryLockPath,
484
+ daemonsDir,
485
+ isPidAlive,
486
+ discoverDaemons,
487
+ removeDaemonRecord,
488
+ listMachineProjects,
489
+ findDaemonByProject,
490
+ signalDaemonStop,
491
+ classifyLockHolder,
492
+ lockHolderMessage,
493
+ normalizeProjectPath,
494
+ writeAtomically,
495
+ readRegistry,
496
+ RegistryError,
497
+ RegistryLockError,
498
+ ProjectNameCollisionError,
499
+ addProject,
500
+ getProject,
501
+ findProjectByPath,
502
+ registeredDaemonForPathSync,
503
+ listProjects,
504
+ setStatus,
505
+ touchLastSeen,
506
+ removeProject,
507
+ DEFAULT_PRUNE_TTL_MS,
508
+ pruneTtlMs,
509
+ pruneRegistry
510
+ };
511
+ //# sourceMappingURL=chunk-RGYC5U2Q.js.map
@@ -0,0 +1 @@
1
+ {"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 * 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":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  parseOtlpRequest
3
- } from "./chunk-EFBWQIKG.js";
3
+ } from "./chunk-QPG2QXCW.js";
4
4
 
5
5
  // src/otel-grpc.ts
6
6
  import { fileURLToPath } from "url";
@@ -138,4 +138,4 @@ export {
138
138
  reshapeGrpcRequest,
139
139
  startOtelGrpcReceiver
140
140
  };
141
- //# sourceMappingURL=chunk-LXQCL4MP.js.map
141
+ //# sourceMappingURL=chunk-TZXSWSHX.js.map