@neat.is/core 0.2.7 → 0.2.8

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.
package/dist/index.cjs CHANGED
@@ -100,8 +100,8 @@ function reshapeGrpcRequest(req) {
100
100
  };
101
101
  }
102
102
  function resolveProtoRoot() {
103
- const here = import_node_path29.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
104
- return import_node_path29.default.resolve(here, "..", "proto");
103
+ const here = import_node_path30.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
104
+ return import_node_path30.default.resolve(here, "..", "proto");
105
105
  }
106
106
  function loadTraceService() {
107
107
  const protoRoot = resolveProtoRoot();
@@ -156,13 +156,13 @@ async function startOtelGrpcReceiver(opts) {
156
156
  })
157
157
  };
158
158
  }
159
- var import_node_url, import_node_path29, grpc, protoLoader;
159
+ var import_node_url, import_node_path30, grpc, protoLoader;
160
160
  var init_otel_grpc = __esm({
161
161
  "src/otel-grpc.ts"() {
162
162
  "use strict";
163
163
  init_cjs_shims();
164
164
  import_node_url = require("url");
165
- import_node_path29 = __toESM(require("path"), 1);
165
+ import_node_path30 = __toESM(require("path"), 1);
166
166
  grpc = __toESM(require("@grpc/grpc-js"), 1);
167
167
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
168
168
  init_otel();
@@ -259,10 +259,10 @@ function parseOtlpRequest(body) {
259
259
  }
260
260
  function loadProtobufDecoder() {
261
261
  if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
262
- const here = import_node_path30.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
263
- const protoRoot = import_node_path30.default.resolve(here, "..", "proto");
262
+ const here = import_node_path31.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
263
+ const protoRoot = import_node_path31.default.resolve(here, "..", "proto");
264
264
  const root = new import_protobufjs.default.Root();
265
- root.resolvePath = (_origin, target) => import_node_path30.default.resolve(protoRoot, target);
265
+ root.resolvePath = (_origin, target) => import_node_path31.default.resolve(protoRoot, target);
266
266
  root.loadSync(
267
267
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
268
268
  { keepCase: true }
@@ -362,12 +362,12 @@ function logSpanHandler(span) {
362
362
  `otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
363
363
  );
364
364
  }
365
- var import_node_path30, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
365
+ var import_node_path31, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
366
366
  var init_otel = __esm({
367
367
  "src/otel.ts"() {
368
368
  "use strict";
369
369
  init_cjs_shims();
370
- import_node_path30 = __toESM(require("path"), 1);
370
+ import_node_path31 = __toESM(require("path"), 1);
371
371
  import_node_url2 = require("url");
372
372
  import_fastify2 = __toESM(require("fastify"), 1);
373
373
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -777,6 +777,18 @@ function deprecatedApis() {
777
777
  return currentMatrix().deprecatedApis ?? [];
778
778
  }
779
779
 
780
+ // src/events.ts
781
+ init_cjs_shims();
782
+ var import_node_events = require("events");
783
+ var EVENT_BUS_CHANNEL = "event";
784
+ var NeatEventBus = class extends import_node_events.EventEmitter {
785
+ };
786
+ var eventBus = new NeatEventBus();
787
+ eventBus.setMaxListeners(0);
788
+ function emitNeatEvent(envelope) {
789
+ eventBus.emit(EVENT_BUS_CHANNEL, envelope);
790
+ }
791
+
780
792
  // src/traverse.ts
781
793
  init_cjs_shims();
782
794
  var import_types = require("@neat.is/types");
@@ -1340,9 +1352,11 @@ async function loadPolicyFile(policyPath) {
1340
1352
  }
1341
1353
  var PolicyViolationsLog = class {
1342
1354
  path;
1355
+ project;
1343
1356
  seen = null;
1344
- constructor(logPath) {
1357
+ constructor(logPath, project = DEFAULT_PROJECT) {
1345
1358
  this.path = logPath;
1359
+ this.project = project;
1346
1360
  }
1347
1361
  async append(v) {
1348
1362
  if (!this.seen) await this.hydrate();
@@ -1350,6 +1364,11 @@ var PolicyViolationsLog = class {
1350
1364
  this.seen.add(v.id);
1351
1365
  await import_node_fs2.promises.mkdir(import_node_path2.default.dirname(this.path), { recursive: true });
1352
1366
  await import_node_fs2.promises.appendFile(this.path, JSON.stringify(v) + "\n", "utf8");
1367
+ emitNeatEvent({
1368
+ type: "policy-violation",
1369
+ project: this.project,
1370
+ payload: { violation: v }
1371
+ });
1353
1372
  return true;
1354
1373
  }
1355
1374
  async readAll() {
@@ -1796,6 +1815,7 @@ async function markStaleEdges(graph, options = {}) {
1796
1815
  const thresholds = options.thresholds ?? loadStaleThresholdsFromEnv();
1797
1816
  const now = options.now ?? Date.now();
1798
1817
  const events = [];
1818
+ const project = options.project ?? DEFAULT_PROJECT;
1799
1819
  graph.forEachEdge((id, attrs) => {
1800
1820
  const e = attrs;
1801
1821
  if (e.provenance !== import_types3.Provenance.OBSERVED) return;
@@ -1815,6 +1835,15 @@ async function markStaleEdges(graph, options = {}) {
1815
1835
  lastObserved: e.lastObserved,
1816
1836
  transitionedAt: new Date(now).toISOString()
1817
1837
  });
1838
+ emitNeatEvent({
1839
+ type: "stale-transition",
1840
+ project,
1841
+ payload: {
1842
+ edgeId: id,
1843
+ from: import_types3.Provenance.OBSERVED,
1844
+ to: import_types3.Provenance.STALE
1845
+ }
1846
+ });
1818
1847
  }
1819
1848
  });
1820
1849
  if (options.staleEventsPath && events.length > 0) {
@@ -1845,7 +1874,8 @@ function startStalenessLoop(graph, options = {}) {
1845
1874
  try {
1846
1875
  await markStaleEdges(graph, {
1847
1876
  thresholds: options.thresholds,
1848
- staleEventsPath: options.staleEventsPath
1877
+ staleEventsPath: options.staleEventsPath,
1878
+ project: options.project
1849
1879
  });
1850
1880
  if (options.onPolicyTrigger) await options.onPolicyTrigger(graph);
1851
1881
  } catch (err) {
@@ -3075,7 +3105,15 @@ async function addHttpCallEdges(graph, services) {
3075
3105
  const seenTargets = /* @__PURE__ */ new Map();
3076
3106
  for (const file of files) {
3077
3107
  const parser = import_node_path18.default.extname(file.path) === ".py" ? pyParser : jsParser;
3078
- const targets = callsFromSource(file.content, parser, knownHosts);
3108
+ let targets;
3109
+ try {
3110
+ targets = callsFromSource(file.content, parser, knownHosts);
3111
+ } catch (err) {
3112
+ console.warn(
3113
+ `[neat] http call extraction skipped ${file.path}: ${err.message}`
3114
+ );
3115
+ continue;
3116
+ }
3079
3117
  for (const t of targets) {
3080
3118
  const targetId = hostToNodeId.get(t);
3081
3119
  if (!targetId || targetId === service.node.id) continue;
@@ -3604,11 +3642,22 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3604
3642
  const phase5 = await addInfra(graph, scanPath, services);
3605
3643
  const frontiersPromoted = promoteFrontierNodes(graph);
3606
3644
  if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
3607
- return {
3645
+ const result = {
3608
3646
  nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
3609
3647
  edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
3610
3648
  frontiersPromoted
3611
3649
  };
3650
+ emitNeatEvent({
3651
+ type: "extraction-complete",
3652
+ project: opts.project ?? DEFAULT_PROJECT,
3653
+ payload: {
3654
+ project: opts.project ?? DEFAULT_PROJECT,
3655
+ fileCount: services.length,
3656
+ nodesAdded: result.nodesAdded,
3657
+ edgesAdded: result.edgesAdded
3658
+ }
3659
+ });
3660
+ return result;
3612
3661
  }
3613
3662
 
3614
3663
  // src/persist.ts
@@ -3699,7 +3748,7 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
3699
3748
  init_cjs_shims();
3700
3749
  var import_fastify = __toESM(require("fastify"), 1);
3701
3750
  var import_cors = __toESM(require("@fastify/cors"), 1);
3702
- var import_types18 = require("@neat.is/types");
3751
+ var import_types19 = require("@neat.is/types");
3703
3752
 
3704
3753
  // src/diff.ts
3705
3754
  init_cjs_shims();
@@ -3830,6 +3879,229 @@ var Projects = class {
3830
3879
  }
3831
3880
  };
3832
3881
 
3882
+ // src/registry.ts
3883
+ init_cjs_shims();
3884
+ var import_node_fs17 = require("fs");
3885
+ var import_node_os2 = __toESM(require("os"), 1);
3886
+ var import_node_path29 = __toESM(require("path"), 1);
3887
+ var import_types18 = require("@neat.is/types");
3888
+ var LOCK_TIMEOUT_MS = 5e3;
3889
+ var LOCK_RETRY_MS = 50;
3890
+ function neatHome() {
3891
+ const override = process.env.NEAT_HOME;
3892
+ if (override && override.length > 0) return import_node_path29.default.resolve(override);
3893
+ return import_node_path29.default.join(import_node_os2.default.homedir(), ".neat");
3894
+ }
3895
+ function registryPath() {
3896
+ return import_node_path29.default.join(neatHome(), "projects.json");
3897
+ }
3898
+ function registryLockPath() {
3899
+ return import_node_path29.default.join(neatHome(), "projects.json.lock");
3900
+ }
3901
+ async function normalizeProjectPath(input) {
3902
+ const resolved = import_node_path29.default.resolve(input);
3903
+ try {
3904
+ return await import_node_fs17.promises.realpath(resolved);
3905
+ } catch {
3906
+ return resolved;
3907
+ }
3908
+ }
3909
+ async function writeAtomically(target, contents) {
3910
+ await import_node_fs17.promises.mkdir(import_node_path29.default.dirname(target), { recursive: true });
3911
+ const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
3912
+ const fd = await import_node_fs17.promises.open(tmp, "w");
3913
+ try {
3914
+ await fd.writeFile(contents, "utf8");
3915
+ await fd.sync();
3916
+ } finally {
3917
+ await fd.close();
3918
+ }
3919
+ await import_node_fs17.promises.rename(tmp, target);
3920
+ }
3921
+ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
3922
+ const deadline = Date.now() + timeoutMs;
3923
+ await import_node_fs17.promises.mkdir(import_node_path29.default.dirname(lockPath), { recursive: true });
3924
+ while (true) {
3925
+ try {
3926
+ const fd = await import_node_fs17.promises.open(lockPath, "wx");
3927
+ await fd.close();
3928
+ return;
3929
+ } catch (err) {
3930
+ const code = err.code;
3931
+ if (code !== "EEXIST") throw err;
3932
+ if (Date.now() >= deadline) {
3933
+ throw new Error(
3934
+ `neat registry: 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.`
3935
+ );
3936
+ }
3937
+ await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
3938
+ }
3939
+ }
3940
+ }
3941
+ async function releaseLock(lockPath) {
3942
+ await import_node_fs17.promises.unlink(lockPath).catch(() => {
3943
+ });
3944
+ }
3945
+ async function withLock(fn) {
3946
+ const lock = registryLockPath();
3947
+ await acquireLock(lock);
3948
+ try {
3949
+ return await fn();
3950
+ } finally {
3951
+ await releaseLock(lock);
3952
+ }
3953
+ }
3954
+ async function readRegistry() {
3955
+ const file = registryPath();
3956
+ let raw;
3957
+ try {
3958
+ raw = await import_node_fs17.promises.readFile(file, "utf8");
3959
+ } catch (err) {
3960
+ if (err.code === "ENOENT") {
3961
+ return { version: 1, projects: [] };
3962
+ }
3963
+ throw err;
3964
+ }
3965
+ const parsed = JSON.parse(raw);
3966
+ return import_types18.RegistryFileSchema.parse(parsed);
3967
+ }
3968
+ async function writeRegistry(reg) {
3969
+ const validated = import_types18.RegistryFileSchema.parse(reg);
3970
+ await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
3971
+ }
3972
+ var ProjectNameCollisionError = class extends Error {
3973
+ projectName;
3974
+ constructor(name) {
3975
+ super(`neat registry: a project named "${name}" is already registered`);
3976
+ this.name = "ProjectNameCollisionError";
3977
+ this.projectName = name;
3978
+ }
3979
+ };
3980
+ async function addProject(opts) {
3981
+ const resolvedPath = await normalizeProjectPath(opts.path);
3982
+ return withLock(async () => {
3983
+ const reg = await readRegistry();
3984
+ const byName = reg.projects.find((p) => p.name === opts.name);
3985
+ const byPath = reg.projects.find((p) => p.path === resolvedPath);
3986
+ if (byName && byName.path !== resolvedPath) {
3987
+ throw new ProjectNameCollisionError(opts.name);
3988
+ }
3989
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3990
+ if (byName && byName.path === resolvedPath) {
3991
+ byName.lastSeenAt = now;
3992
+ if (opts.languages) byName.languages = opts.languages;
3993
+ if (opts.status) byName.status = opts.status;
3994
+ await writeRegistry(reg);
3995
+ return byName;
3996
+ }
3997
+ if (byPath && byPath.name !== opts.name) {
3998
+ throw new ProjectNameCollisionError(byPath.name);
3999
+ }
4000
+ const entry = {
4001
+ name: opts.name,
4002
+ path: resolvedPath,
4003
+ registeredAt: now,
4004
+ languages: opts.languages ?? [],
4005
+ status: opts.status ?? "active"
4006
+ };
4007
+ reg.projects.push(entry);
4008
+ await writeRegistry(reg);
4009
+ return entry;
4010
+ });
4011
+ }
4012
+ async function getProject(name) {
4013
+ const reg = await readRegistry();
4014
+ return reg.projects.find((p) => p.name === name);
4015
+ }
4016
+ async function listProjects() {
4017
+ const reg = await readRegistry();
4018
+ return reg.projects;
4019
+ }
4020
+ async function setStatus(name, status2) {
4021
+ return withLock(async () => {
4022
+ const reg = await readRegistry();
4023
+ const entry = reg.projects.find((p) => p.name === name);
4024
+ if (!entry) throw new Error(`neat registry: no project named "${name}"`);
4025
+ entry.status = status2;
4026
+ await writeRegistry(reg);
4027
+ return entry;
4028
+ });
4029
+ }
4030
+ async function touchLastSeen(name, at = (/* @__PURE__ */ new Date()).toISOString()) {
4031
+ await withLock(async () => {
4032
+ const reg = await readRegistry();
4033
+ const entry = reg.projects.find((p) => p.name === name);
4034
+ if (!entry) return;
4035
+ entry.lastSeenAt = at;
4036
+ await writeRegistry(reg);
4037
+ });
4038
+ }
4039
+ async function removeProject(name) {
4040
+ return withLock(async () => {
4041
+ const reg = await readRegistry();
4042
+ const idx = reg.projects.findIndex((p) => p.name === name);
4043
+ if (idx < 0) return void 0;
4044
+ const [removed] = reg.projects.splice(idx, 1);
4045
+ await writeRegistry(reg);
4046
+ return removed;
4047
+ });
4048
+ }
4049
+
4050
+ // src/streaming.ts
4051
+ init_cjs_shims();
4052
+ var SSE_HEARTBEAT_MS = 3e4;
4053
+ var SSE_BACKPRESSURE_CAP = 1e3;
4054
+ function handleSse(req, reply, opts) {
4055
+ const heartbeatMs = opts.heartbeatMs ?? SSE_HEARTBEAT_MS;
4056
+ const backpressureCap = opts.backpressureCap ?? SSE_BACKPRESSURE_CAP;
4057
+ reply.raw.setHeader("Content-Type", "text/event-stream");
4058
+ reply.raw.setHeader("Cache-Control", "no-cache, no-transform");
4059
+ reply.raw.setHeader("Connection", "keep-alive");
4060
+ reply.raw.setHeader("X-Accel-Buffering", "no");
4061
+ reply.raw.flushHeaders?.();
4062
+ let pending = 0;
4063
+ let dropped = false;
4064
+ const closeConnection = () => {
4065
+ if (dropped) return;
4066
+ dropped = true;
4067
+ eventBus.off(EVENT_BUS_CHANNEL, listener);
4068
+ clearInterval(heartbeat);
4069
+ if (!reply.raw.writableEnded) reply.raw.end();
4070
+ };
4071
+ const writeFrame = (frame) => {
4072
+ if (dropped) return;
4073
+ if (pending >= backpressureCap) {
4074
+ const errFrame = `event: error
4075
+ data: ${JSON.stringify({ reason: "backpressure" })}
4076
+
4077
+ `;
4078
+ reply.raw.write(errFrame);
4079
+ closeConnection();
4080
+ return;
4081
+ }
4082
+ pending++;
4083
+ reply.raw.write(frame, () => {
4084
+ pending = Math.max(0, pending - 1);
4085
+ });
4086
+ };
4087
+ const listener = (envelope) => {
4088
+ if (envelope.project !== opts.project) return;
4089
+ writeFrame(`event: ${envelope.type}
4090
+ data: ${JSON.stringify(envelope.payload)}
4091
+
4092
+ `);
4093
+ };
4094
+ eventBus.on(EVENT_BUS_CHANNEL, listener);
4095
+ const heartbeat = setInterval(() => {
4096
+ if (dropped) return;
4097
+ reply.raw.write(":heartbeat\n\n");
4098
+ }, heartbeatMs);
4099
+ if (typeof heartbeat.unref === "function") heartbeat.unref();
4100
+ req.raw.on("close", closeConnection);
4101
+ reply.raw.on("close", closeConnection);
4102
+ reply.raw.on("error", closeConnection);
4103
+ }
4104
+
3833
4105
  // src/api.ts
3834
4106
  function serializeGraph(graph) {
3835
4107
  const nodes = [];
@@ -3878,6 +4150,11 @@ function buildLegacyRegistry(opts) {
3878
4150
  }
3879
4151
  function registerRoutes(scope, ctx) {
3880
4152
  const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
4153
+ scope.get("/events", (req, reply) => {
4154
+ const proj = resolveProject(registry, req, reply);
4155
+ if (!proj) return;
4156
+ handleSse(req, reply, { project: proj.name });
4157
+ });
3881
4158
  scope.get("/health", async (req, reply) => {
3882
4159
  const proj = resolveProject(registry, req, reply);
3883
4160
  if (!proj) return;
@@ -4088,7 +4365,7 @@ function registerRoutes(scope, ctx) {
4088
4365
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
4089
4366
  let violations = await log.readAll();
4090
4367
  if (req.query.severity) {
4091
- const sev = import_types18.PolicySeveritySchema.safeParse(req.query.severity);
4368
+ const sev = import_types19.PolicySeveritySchema.safeParse(req.query.severity);
4092
4369
  if (!sev.success) {
4093
4370
  return reply.code(400).send({
4094
4371
  error: "invalid severity",
@@ -4105,7 +4382,7 @@ function registerRoutes(scope, ctx) {
4105
4382
  scope.post("/policies/check", async (req, reply) => {
4106
4383
  const proj = resolveProject(registry, req, reply);
4107
4384
  if (!proj) return;
4108
- const parsed = import_types18.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4385
+ const parsed = import_types19.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4109
4386
  if (!parsed.success) {
4110
4387
  return reply.code(400).send({
4111
4388
  error: "invalid /policies/check body",
@@ -4169,17 +4446,16 @@ async function buildApi(opts) {
4169
4446
  staleEventsPathFor,
4170
4447
  policyFilePathFor
4171
4448
  };
4172
- app.get("/projects", async () => ({
4173
- projects: registry.list().map((name) => {
4174
- const proj = registry.get(name);
4175
- return {
4176
- name,
4177
- nodeCount: proj.graph.order,
4178
- edgeCount: proj.graph.size,
4179
- scanPath: proj.scanPath
4180
- };
4181
- })
4182
- }));
4449
+ app.get("/projects", async (_req, reply) => {
4450
+ try {
4451
+ return await listProjects();
4452
+ } catch (err) {
4453
+ return reply.code(500).send({
4454
+ error: "failed to read project registry",
4455
+ details: err.message
4456
+ });
4457
+ }
4458
+ });
4183
4459
  registerRoutes(app, routeCtx);
4184
4460
  await app.register(
4185
4461
  async (scope) => {
@@ -4198,176 +4474,6 @@ init_otel_grpc();
4198
4474
  init_cjs_shims();
4199
4475
  var import_node_fs18 = require("fs");
4200
4476
  var import_node_path32 = __toESM(require("path"), 1);
4201
-
4202
- // src/registry.ts
4203
- init_cjs_shims();
4204
- var import_node_fs17 = require("fs");
4205
- var import_node_os2 = __toESM(require("os"), 1);
4206
- var import_node_path31 = __toESM(require("path"), 1);
4207
- var import_types19 = require("@neat.is/types");
4208
- var LOCK_TIMEOUT_MS = 5e3;
4209
- var LOCK_RETRY_MS = 50;
4210
- function neatHome() {
4211
- const override = process.env.NEAT_HOME;
4212
- if (override && override.length > 0) return import_node_path31.default.resolve(override);
4213
- return import_node_path31.default.join(import_node_os2.default.homedir(), ".neat");
4214
- }
4215
- function registryPath() {
4216
- return import_node_path31.default.join(neatHome(), "projects.json");
4217
- }
4218
- function registryLockPath() {
4219
- return import_node_path31.default.join(neatHome(), "projects.json.lock");
4220
- }
4221
- async function normalizeProjectPath(input) {
4222
- const resolved = import_node_path31.default.resolve(input);
4223
- try {
4224
- return await import_node_fs17.promises.realpath(resolved);
4225
- } catch {
4226
- return resolved;
4227
- }
4228
- }
4229
- async function writeAtomically(target, contents) {
4230
- await import_node_fs17.promises.mkdir(import_node_path31.default.dirname(target), { recursive: true });
4231
- const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
4232
- const fd = await import_node_fs17.promises.open(tmp, "w");
4233
- try {
4234
- await fd.writeFile(contents, "utf8");
4235
- await fd.sync();
4236
- } finally {
4237
- await fd.close();
4238
- }
4239
- await import_node_fs17.promises.rename(tmp, target);
4240
- }
4241
- async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4242
- const deadline = Date.now() + timeoutMs;
4243
- await import_node_fs17.promises.mkdir(import_node_path31.default.dirname(lockPath), { recursive: true });
4244
- while (true) {
4245
- try {
4246
- const fd = await import_node_fs17.promises.open(lockPath, "wx");
4247
- await fd.close();
4248
- return;
4249
- } catch (err) {
4250
- const code = err.code;
4251
- if (code !== "EEXIST") throw err;
4252
- if (Date.now() >= deadline) {
4253
- throw new Error(
4254
- `neat registry: 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.`
4255
- );
4256
- }
4257
- await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
4258
- }
4259
- }
4260
- }
4261
- async function releaseLock(lockPath) {
4262
- await import_node_fs17.promises.unlink(lockPath).catch(() => {
4263
- });
4264
- }
4265
- async function withLock(fn) {
4266
- const lock = registryLockPath();
4267
- await acquireLock(lock);
4268
- try {
4269
- return await fn();
4270
- } finally {
4271
- await releaseLock(lock);
4272
- }
4273
- }
4274
- async function readRegistry() {
4275
- const file = registryPath();
4276
- let raw;
4277
- try {
4278
- raw = await import_node_fs17.promises.readFile(file, "utf8");
4279
- } catch (err) {
4280
- if (err.code === "ENOENT") {
4281
- return { version: 1, projects: [] };
4282
- }
4283
- throw err;
4284
- }
4285
- const parsed = JSON.parse(raw);
4286
- return import_types19.RegistryFileSchema.parse(parsed);
4287
- }
4288
- async function writeRegistry(reg) {
4289
- const validated = import_types19.RegistryFileSchema.parse(reg);
4290
- await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
4291
- }
4292
- var ProjectNameCollisionError = class extends Error {
4293
- projectName;
4294
- constructor(name) {
4295
- super(`neat registry: a project named "${name}" is already registered`);
4296
- this.name = "ProjectNameCollisionError";
4297
- this.projectName = name;
4298
- }
4299
- };
4300
- async function addProject(opts) {
4301
- const resolvedPath = await normalizeProjectPath(opts.path);
4302
- return withLock(async () => {
4303
- const reg = await readRegistry();
4304
- const byName = reg.projects.find((p) => p.name === opts.name);
4305
- const byPath = reg.projects.find((p) => p.path === resolvedPath);
4306
- if (byName && byName.path !== resolvedPath) {
4307
- throw new ProjectNameCollisionError(opts.name);
4308
- }
4309
- const now = (/* @__PURE__ */ new Date()).toISOString();
4310
- if (byName && byName.path === resolvedPath) {
4311
- byName.lastSeenAt = now;
4312
- if (opts.languages) byName.languages = opts.languages;
4313
- if (opts.status) byName.status = opts.status;
4314
- await writeRegistry(reg);
4315
- return byName;
4316
- }
4317
- if (byPath && byPath.name !== opts.name) {
4318
- throw new ProjectNameCollisionError(byPath.name);
4319
- }
4320
- const entry = {
4321
- name: opts.name,
4322
- path: resolvedPath,
4323
- registeredAt: now,
4324
- languages: opts.languages ?? [],
4325
- status: opts.status ?? "active"
4326
- };
4327
- reg.projects.push(entry);
4328
- await writeRegistry(reg);
4329
- return entry;
4330
- });
4331
- }
4332
- async function getProject(name) {
4333
- const reg = await readRegistry();
4334
- return reg.projects.find((p) => p.name === name);
4335
- }
4336
- async function listProjects() {
4337
- const reg = await readRegistry();
4338
- return reg.projects;
4339
- }
4340
- async function setStatus(name, status2) {
4341
- return withLock(async () => {
4342
- const reg = await readRegistry();
4343
- const entry = reg.projects.find((p) => p.name === name);
4344
- if (!entry) throw new Error(`neat registry: no project named "${name}"`);
4345
- entry.status = status2;
4346
- await writeRegistry(reg);
4347
- return entry;
4348
- });
4349
- }
4350
- async function touchLastSeen(name, at = (/* @__PURE__ */ new Date()).toISOString()) {
4351
- await withLock(async () => {
4352
- const reg = await readRegistry();
4353
- const entry = reg.projects.find((p) => p.name === name);
4354
- if (!entry) return;
4355
- entry.lastSeenAt = at;
4356
- await writeRegistry(reg);
4357
- });
4358
- }
4359
- async function removeProject(name) {
4360
- return withLock(async () => {
4361
- const reg = await readRegistry();
4362
- const idx = reg.projects.findIndex((p) => p.name === name);
4363
- if (idx < 0) return void 0;
4364
- const [removed] = reg.projects.splice(idx, 1);
4365
- await writeRegistry(reg);
4366
- return removed;
4367
- });
4368
- }
4369
-
4370
- // src/daemon.ts
4371
4477
  function neatHomeFor(opts) {
4372
4478
  if (opts.neatHome && opts.neatHome.length > 0) return import_node_path32.default.resolve(opts.neatHome);
4373
4479
  const env = process.env.NEAT_HOME;