@neat.is/core 0.4.0 → 0.4.2

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/neatd.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startDaemon
4
- } from "./chunk-N6RPINEJ.js";
4
+ } from "./chunk-CB2UK4EH.js";
5
5
  import {
6
6
  listProjects,
7
7
  registryPath
8
- } from "./chunk-UPW4CMOH.js";
8
+ } from "./chunk-NTQHMXWE.js";
9
9
  import {
10
10
  BindAuthorityError,
11
11
  __require
12
- } from "./chunk-4V23KYOP.js";
12
+ } from "./chunk-KYRIQIPG.js";
13
13
 
14
14
  // src/neatd.ts
15
15
  import { promises as fs } from "fs";
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  reshapeGrpcRequest,
3
3
  startOtelGrpcReceiver
4
- } from "./chunk-IXIFJKMM.js";
5
- import "./chunk-4V23KYOP.js";
4
+ } from "./chunk-D5PIJFBE.js";
5
+ import "./chunk-KYRIQIPG.js";
6
6
  export {
7
7
  reshapeGrpcRequest,
8
8
  startOtelGrpcReceiver
9
9
  };
10
- //# sourceMappingURL=otel-grpc-GGZHR7VM.js.map
10
+ //# sourceMappingURL=otel-grpc-QTX2YQJZ.js.map
package/dist/server.cjs CHANGED
@@ -2278,8 +2278,36 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
2278
2278
  ".turbo",
2279
2279
  "dist",
2280
2280
  "build",
2281
- ".next"
2281
+ ".next",
2282
+ // Python virtualenv shapes (issue #344). Walking into a venv pulls in the
2283
+ // entire CPython stdlib + every installed package as if it were first-party
2284
+ // service code — 20k+ files, none of which the user wrote. The shape names
2285
+ // cover the three common venv tools (`venv` / `python -m venv`,
2286
+ // `virtualenv`) and the tox + PEP 582 conventions.
2287
+ ".venv",
2288
+ "venv",
2289
+ "__pypackages__",
2290
+ ".tox",
2291
+ // `site-packages` shows up nested inside venvs that don't carry one of the
2292
+ // outer names (system Python on macOS, in-place pyenv layouts). Listing it
2293
+ // here means we stop the walk at the boundary even when the wrapper dir
2294
+ // wasn't recognisable.
2295
+ "site-packages"
2282
2296
  ]);
2297
+ var PYVENV_MARKER_CACHE = /* @__PURE__ */ new Map();
2298
+ async function isPythonVenvDir(dir) {
2299
+ const cached = PYVENV_MARKER_CACHE.get(dir);
2300
+ if (cached !== void 0) return cached;
2301
+ try {
2302
+ const stat = await import_node_fs4.promises.stat(import_node_path4.default.join(dir, "pyvenv.cfg"));
2303
+ const ok = stat.isFile();
2304
+ PYVENV_MARKER_CACHE.set(dir, ok);
2305
+ return ok;
2306
+ } catch {
2307
+ PYVENV_MARKER_CACHE.set(dir, false);
2308
+ return false;
2309
+ }
2310
+ }
2283
2311
  function isConfigFile(name) {
2284
2312
  const ext = import_node_path4.default.extname(name);
2285
2313
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
@@ -2616,6 +2644,7 @@ async function walkDirs(start, scanPath, options, visit) {
2616
2644
  const rel = import_node_path8.default.relative(scanPath, child).split(import_node_path8.default.sep).join("/");
2617
2645
  if (rel && options.ig.ignores(rel + "/")) continue;
2618
2646
  }
2647
+ if (await isPythonVenvDir(child)) continue;
2619
2648
  await visit(child);
2620
2649
  await recurse(child, depth + 1);
2621
2650
  }
@@ -2894,7 +2923,9 @@ async function walkYamlFiles(start, depth = 0, max = 5) {
2894
2923
  for (const entry of entries) {
2895
2924
  if (entry.isDirectory()) {
2896
2925
  if (IGNORED_DIRS.has(entry.name)) continue;
2897
- out.push(...await walkYamlFiles(import_node_path9.default.join(start, entry.name), depth + 1, max));
2926
+ const child = import_node_path9.default.join(start, entry.name);
2927
+ if (await isPythonVenvDir(child)) continue;
2928
+ out.push(...await walkYamlFiles(child, depth + 1, max));
2898
2929
  } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path9.default.extname(entry.name))) {
2899
2930
  out.push(import_node_path9.default.join(start, entry.name));
2900
2931
  }
@@ -3576,7 +3607,9 @@ async function walkConfigFiles(dir) {
3576
3607
  for (const entry of entries) {
3577
3608
  const full = import_node_path18.default.join(current, entry.name);
3578
3609
  if (entry.isDirectory()) {
3579
- if (!IGNORED_DIRS.has(entry.name)) await walk(full);
3610
+ if (IGNORED_DIRS.has(entry.name)) continue;
3611
+ if (await isPythonVenvDir(full)) continue;
3612
+ await walk(full);
3580
3613
  } else if (entry.isFile() && isConfigFile(entry.name).match) {
3581
3614
  out.push(full);
3582
3615
  }
@@ -3644,7 +3677,9 @@ async function walkSourceFiles(dir) {
3644
3677
  for (const entry of entries) {
3645
3678
  const full = import_node_path19.default.join(current, entry.name);
3646
3679
  if (entry.isDirectory()) {
3647
- if (!IGNORED_DIRS.has(entry.name)) await walk(full);
3680
+ if (IGNORED_DIRS.has(entry.name)) continue;
3681
+ if (await isPythonVenvDir(full)) continue;
3682
+ await walk(full);
3648
3683
  } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry.name))) {
3649
3684
  out.push(full);
3650
3685
  }
@@ -4280,7 +4315,9 @@ async function walkTfFiles(start, depth = 0, max = 5) {
4280
4315
  for (const entry of entries) {
4281
4316
  if (entry.isDirectory()) {
4282
4317
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
4283
- out.push(...await walkTfFiles(import_node_path27.default.join(start, entry.name), depth + 1, max));
4318
+ const child = import_node_path27.default.join(start, entry.name);
4319
+ if (await isPythonVenvDir(child)) continue;
4320
+ out.push(...await walkTfFiles(child, depth + 1, max));
4284
4321
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
4285
4322
  out.push(import_node_path27.default.join(start, entry.name));
4286
4323
  }
@@ -4328,7 +4365,9 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
4328
4365
  for (const entry of entries) {
4329
4366
  if (entry.isDirectory()) {
4330
4367
  if (IGNORED_DIRS.has(entry.name)) continue;
4331
- out.push(...await walkYamlFiles2(import_node_path28.default.join(start, entry.name), depth + 1, max));
4368
+ const child = import_node_path28.default.join(start, entry.name);
4369
+ if (await isPythonVenvDir(child)) continue;
4370
+ out.push(...await walkYamlFiles2(child, depth + 1, max));
4332
4371
  } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry.name))) {
4333
4372
  out.push(import_node_path28.default.join(start, entry.name));
4334
4373
  }
@@ -4817,10 +4856,19 @@ function projectFromReq(req) {
4817
4856
  const params = req.params;
4818
4857
  return params.project ?? DEFAULT_PROJECT;
4819
4858
  }
4820
- function resolveProject(registry, req, reply) {
4859
+ function resolveProject(registry, req, reply, bootstrap) {
4821
4860
  const name = projectFromReq(req);
4822
4861
  const ctx = registry.get(name);
4823
4862
  if (!ctx) {
4863
+ const phase = bootstrap?.status(name);
4864
+ if (phase === "bootstrapping") {
4865
+ void reply.code(503).send({ ready: false, project: name, status: "bootstrapping" });
4866
+ return null;
4867
+ }
4868
+ if (phase === "broken") {
4869
+ void reply.code(503).send({ ready: false, project: name, status: "broken" });
4870
+ return null;
4871
+ }
4824
4872
  void reply.code(404).send({ error: "project not found", project: name });
4825
4873
  return null;
4826
4874
  }
@@ -4850,36 +4898,38 @@ function buildLegacyRegistry(opts) {
4850
4898
  function registerRoutes(scope, ctx) {
4851
4899
  const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
4852
4900
  scope.get("/events", (req, reply) => {
4853
- const proj = resolveProject(registry, req, reply);
4901
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4854
4902
  if (!proj) return;
4855
4903
  handleSse(req, reply, { project: proj.name });
4856
4904
  });
4857
- scope.get("/health", async (req, reply) => {
4858
- const proj = resolveProject(registry, req, reply);
4859
- if (!proj) return;
4860
- const uptimeMs = Date.now() - startedAt;
4861
- return {
4862
- ok: true,
4863
- project: proj.name,
4864
- uptimeMs,
4865
- // Legacy fields kept additively. The web shell's StatusBar reads
4866
- // nodeCount / edgeCount; ADR-061's HealthResponseSchema validates
4867
- // the canonical triple and lets the extras pass through.
4868
- uptime: Math.floor(uptimeMs / 1e3),
4869
- nodeCount: proj.graph.order,
4870
- edgeCount: proj.graph.size,
4871
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
4872
- };
4873
- });
4905
+ if (ctx.scope === "project") {
4906
+ scope.get("/health", async (req, reply) => {
4907
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4908
+ if (!proj) return;
4909
+ const uptimeMs = Date.now() - startedAt;
4910
+ return {
4911
+ ok: true,
4912
+ project: proj.name,
4913
+ uptimeMs,
4914
+ // Legacy fields kept additively. The web shell's StatusBar reads
4915
+ // nodeCount / edgeCount; ADR-061's HealthResponseSchema validates
4916
+ // the canonical triple and lets the extras pass through.
4917
+ uptime: Math.floor(uptimeMs / 1e3),
4918
+ nodeCount: proj.graph.order,
4919
+ edgeCount: proj.graph.size,
4920
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
4921
+ };
4922
+ });
4923
+ }
4874
4924
  scope.get("/graph", async (req, reply) => {
4875
- const proj = resolveProject(registry, req, reply);
4925
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4876
4926
  if (!proj) return;
4877
4927
  return serializeGraph(proj.graph);
4878
4928
  });
4879
4929
  scope.get(
4880
4930
  "/graph/node/:id",
4881
4931
  async (req, reply) => {
4882
- const proj = resolveProject(registry, req, reply);
4932
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4883
4933
  if (!proj) return;
4884
4934
  const { id } = req.params;
4885
4935
  if (!proj.graph.hasNode(id)) {
@@ -4891,7 +4941,7 @@ function registerRoutes(scope, ctx) {
4891
4941
  scope.get(
4892
4942
  "/graph/edges/:id",
4893
4943
  async (req, reply) => {
4894
- const proj = resolveProject(registry, req, reply);
4944
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4895
4945
  if (!proj) return;
4896
4946
  const { id } = req.params;
4897
4947
  if (!proj.graph.hasNode(id)) {
@@ -4903,7 +4953,7 @@ function registerRoutes(scope, ctx) {
4903
4953
  }
4904
4954
  );
4905
4955
  scope.get("/graph/dependencies/:nodeId", async (req, reply) => {
4906
- const proj = resolveProject(registry, req, reply);
4956
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4907
4957
  if (!proj) return;
4908
4958
  const { nodeId } = req.params;
4909
4959
  if (!proj.graph.hasNode(nodeId)) {
@@ -4918,7 +4968,7 @@ function registerRoutes(scope, ctx) {
4918
4968
  return getTransitiveDependencies(proj.graph, nodeId, depth);
4919
4969
  });
4920
4970
  scope.get("/graph/divergences", async (req, reply) => {
4921
- const proj = resolveProject(registry, req, reply);
4971
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4922
4972
  if (!proj) return;
4923
4973
  let typeFilter;
4924
4974
  if (req.query.type) {
@@ -4953,7 +5003,7 @@ function registerRoutes(scope, ctx) {
4953
5003
  });
4954
5004
  });
4955
5005
  scope.get("/incidents", async (req, reply) => {
4956
- const proj = resolveProject(registry, req, reply);
5006
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4957
5007
  if (!proj) return;
4958
5008
  const epath = errorsPathFor(proj);
4959
5009
  if (!epath) return { count: 0, total: 0, events: [] };
@@ -4965,7 +5015,7 @@ function registerRoutes(scope, ctx) {
4965
5015
  return { count: sliced.length, total, events: sliced };
4966
5016
  });
4967
5017
  scope.get("/stale-events", async (req, reply) => {
4968
- const proj = resolveProject(registry, req, reply);
5018
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4969
5019
  if (!proj) return;
4970
5020
  const spath = staleEventsPathFor(proj);
4971
5021
  if (!spath) return { count: 0, total: 0, events: [] };
@@ -4980,7 +5030,7 @@ function registerRoutes(scope, ctx) {
4980
5030
  scope.get(
4981
5031
  "/incidents/:nodeId",
4982
5032
  async (req, reply) => {
4983
- const proj = resolveProject(registry, req, reply);
5033
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4984
5034
  if (!proj) return;
4985
5035
  const { nodeId } = req.params;
4986
5036
  if (!proj.graph.hasNode(nodeId)) {
@@ -4996,7 +5046,7 @@ function registerRoutes(scope, ctx) {
4996
5046
  }
4997
5047
  );
4998
5048
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
4999
- const proj = resolveProject(registry, req, reply);
5049
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5000
5050
  if (!proj) return;
5001
5051
  const { nodeId } = req.params;
5002
5052
  if (!proj.graph.hasNode(nodeId)) {
@@ -5016,7 +5066,7 @@ function registerRoutes(scope, ctx) {
5016
5066
  return result;
5017
5067
  });
5018
5068
  scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
5019
- const proj = resolveProject(registry, req, reply);
5069
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5020
5070
  if (!proj) return;
5021
5071
  const { nodeId } = req.params;
5022
5072
  if (!proj.graph.hasNode(nodeId)) {
@@ -5029,7 +5079,7 @@ function registerRoutes(scope, ctx) {
5029
5079
  return getBlastRadius(proj.graph, nodeId, depth);
5030
5080
  });
5031
5081
  scope.get("/search", async (req, reply) => {
5032
- const proj = resolveProject(registry, req, reply);
5082
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5033
5083
  if (!proj) return;
5034
5084
  const raw = (req.query.q ?? "").trim();
5035
5085
  if (!raw) return reply.code(400).send({ error: "query parameter `q` is required" });
@@ -5060,7 +5110,7 @@ function registerRoutes(scope, ctx) {
5060
5110
  scope.get(
5061
5111
  "/graph/diff",
5062
5112
  async (req, reply) => {
5063
- const proj = resolveProject(registry, req, reply);
5113
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5064
5114
  if (!proj) return;
5065
5115
  const against = req.query.against;
5066
5116
  if (!against) {
@@ -5075,7 +5125,7 @@ function registerRoutes(scope, ctx) {
5075
5125
  }
5076
5126
  );
5077
5127
  scope.post("/snapshot", async (req, reply) => {
5078
- const proj = resolveProject(registry, req, reply);
5128
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5079
5129
  if (!proj) return;
5080
5130
  const body = req.body;
5081
5131
  if (!body || typeof body !== "object" || !body.snapshot) {
@@ -5104,7 +5154,7 @@ function registerRoutes(scope, ctx) {
5104
5154
  }
5105
5155
  });
5106
5156
  scope.post("/graph/scan", async (req, reply) => {
5107
- const proj = resolveProject(registry, req, reply);
5157
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5108
5158
  if (!proj) return;
5109
5159
  if (!proj.scanPath) {
5110
5160
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -5120,7 +5170,7 @@ function registerRoutes(scope, ctx) {
5120
5170
  };
5121
5171
  });
5122
5172
  scope.get("/policies", async (req, reply) => {
5123
- const proj = resolveProject(registry, req, reply);
5173
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5124
5174
  if (!proj) return;
5125
5175
  const policyPath = ctx.policyFilePathFor(proj);
5126
5176
  if (!policyPath) {
@@ -5137,7 +5187,7 @@ function registerRoutes(scope, ctx) {
5137
5187
  }
5138
5188
  });
5139
5189
  scope.get("/policies/violations", async (req, reply) => {
5140
- const proj = resolveProject(registry, req, reply);
5190
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5141
5191
  if (!proj) return;
5142
5192
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
5143
5193
  let violations = await log.readAll();
@@ -5157,7 +5207,7 @@ function registerRoutes(scope, ctx) {
5157
5207
  return { violations };
5158
5208
  });
5159
5209
  scope.post("/policies/check", async (req, reply) => {
5160
- const proj = resolveProject(registry, req, reply);
5210
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5161
5211
  if (!proj) return;
5162
5212
  const parsed = import_types22.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5163
5213
  if (!parsed.success) {
@@ -5228,10 +5278,36 @@ async function buildApi(opts) {
5228
5278
  const routeCtx = {
5229
5279
  registry,
5230
5280
  startedAt,
5281
+ scope: "root",
5231
5282
  errorsPathFor,
5232
5283
  staleEventsPathFor,
5233
- policyFilePathFor
5284
+ policyFilePathFor,
5285
+ bootstrap: opts.bootstrap
5234
5286
  };
5287
+ app.get("/health", async () => {
5288
+ const uptimeMs = Date.now() - startedAt;
5289
+ const bootstrapList = opts.bootstrap?.list() ?? [];
5290
+ const byName = new Map(bootstrapList.map((p) => [p.name, p]));
5291
+ const names = /* @__PURE__ */ new Set([
5292
+ ...registry.list(),
5293
+ ...bootstrapList.map((p) => p.name)
5294
+ ]);
5295
+ const projects = [...names].sort().map((name) => {
5296
+ const proj = registry.get(name);
5297
+ const tracked = byName.get(name);
5298
+ return {
5299
+ name,
5300
+ nodeCount: proj?.graph.order ?? 0,
5301
+ edgeCount: proj?.graph.size ?? 0,
5302
+ ...tracked ? { status: tracked.status, elapsedMs: tracked.elapsedMs } : {}
5303
+ };
5304
+ });
5305
+ return {
5306
+ ok: true,
5307
+ uptimeMs,
5308
+ projects
5309
+ };
5310
+ });
5235
5311
  app.get("/projects", async (_req, reply) => {
5236
5312
  try {
5237
5313
  return await listProjects();
@@ -5259,7 +5335,7 @@ async function buildApi(opts) {
5259
5335
  registerRoutes(app, routeCtx);
5260
5336
  await app.register(
5261
5337
  async (scope) => {
5262
- registerRoutes(scope, routeCtx);
5338
+ registerRoutes(scope, { ...routeCtx, scope: "project" });
5263
5339
  },
5264
5340
  { prefix: "/projects/:project" }
5265
5341
  );