@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/cli.cjs CHANGED
@@ -480,6 +480,7 @@ __export(cli_exports, {
480
480
  CLAUDE_SKILL_CONFIG: () => CLAUDE_SKILL_CONFIG,
481
481
  QUERY_VERBS: () => QUERY_VERBS,
482
482
  parseArgs: () => parseArgs,
483
+ readPackageVersion: () => readPackageVersion,
483
484
  runInit: () => runInit,
484
485
  runQueryVerb: () => runQueryVerb,
485
486
  runSkill: () => runSkill
@@ -488,6 +489,7 @@ module.exports = __toCommonJS(cli_exports);
488
489
  init_cjs_shims();
489
490
  var import_node_path44 = __toESM(require("path"), 1);
490
491
  var import_node_fs28 = require("fs");
492
+ var import_node_url3 = require("url");
491
493
 
492
494
  // src/graph.ts
493
495
  init_cjs_shims();
@@ -2093,8 +2095,36 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
2093
2095
  ".turbo",
2094
2096
  "dist",
2095
2097
  "build",
2096
- ".next"
2098
+ ".next",
2099
+ // Python virtualenv shapes (issue #344). Walking into a venv pulls in the
2100
+ // entire CPython stdlib + every installed package as if it were first-party
2101
+ // service code — 20k+ files, none of which the user wrote. The shape names
2102
+ // cover the three common venv tools (`venv` / `python -m venv`,
2103
+ // `virtualenv`) and the tox + PEP 582 conventions.
2104
+ ".venv",
2105
+ "venv",
2106
+ "__pypackages__",
2107
+ ".tox",
2108
+ // `site-packages` shows up nested inside venvs that don't carry one of the
2109
+ // outer names (system Python on macOS, in-place pyenv layouts). Listing it
2110
+ // here means we stop the walk at the boundary even when the wrapper dir
2111
+ // wasn't recognisable.
2112
+ "site-packages"
2097
2113
  ]);
2114
+ var PYVENV_MARKER_CACHE = /* @__PURE__ */ new Map();
2115
+ async function isPythonVenvDir(dir) {
2116
+ const cached = PYVENV_MARKER_CACHE.get(dir);
2117
+ if (cached !== void 0) return cached;
2118
+ try {
2119
+ const stat = await import_node_fs4.promises.stat(import_node_path4.default.join(dir, "pyvenv.cfg"));
2120
+ const ok = stat.isFile();
2121
+ PYVENV_MARKER_CACHE.set(dir, ok);
2122
+ return ok;
2123
+ } catch {
2124
+ PYVENV_MARKER_CACHE.set(dir, false);
2125
+ return false;
2126
+ }
2127
+ }
2098
2128
  function isConfigFile(name) {
2099
2129
  const ext = import_node_path4.default.extname(name);
2100
2130
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
@@ -2443,6 +2473,7 @@ async function walkDirs(start, scanPath, options, visit) {
2443
2473
  const rel = import_node_path8.default.relative(scanPath, child).split(import_node_path8.default.sep).join("/");
2444
2474
  if (rel && options.ig.ignores(rel + "/")) continue;
2445
2475
  }
2476
+ if (await isPythonVenvDir(child)) continue;
2446
2477
  await visit(child);
2447
2478
  await recurse(child, depth + 1);
2448
2479
  }
@@ -2721,7 +2752,9 @@ async function walkYamlFiles(start, depth = 0, max = 5) {
2721
2752
  for (const entry2 of entries) {
2722
2753
  if (entry2.isDirectory()) {
2723
2754
  if (IGNORED_DIRS.has(entry2.name)) continue;
2724
- out.push(...await walkYamlFiles(import_node_path9.default.join(start, entry2.name), depth + 1, max));
2755
+ const child = import_node_path9.default.join(start, entry2.name);
2756
+ if (await isPythonVenvDir(child)) continue;
2757
+ out.push(...await walkYamlFiles(child, depth + 1, max));
2725
2758
  } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path9.default.extname(entry2.name))) {
2726
2759
  out.push(import_node_path9.default.join(start, entry2.name));
2727
2760
  }
@@ -3403,7 +3436,9 @@ async function walkConfigFiles(dir) {
3403
3436
  for (const entry2 of entries) {
3404
3437
  const full = import_node_path18.default.join(current, entry2.name);
3405
3438
  if (entry2.isDirectory()) {
3406
- if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
3439
+ if (IGNORED_DIRS.has(entry2.name)) continue;
3440
+ if (await isPythonVenvDir(full)) continue;
3441
+ await walk(full);
3407
3442
  } else if (entry2.isFile() && isConfigFile(entry2.name).match) {
3408
3443
  out.push(full);
3409
3444
  }
@@ -3471,7 +3506,9 @@ async function walkSourceFiles(dir) {
3471
3506
  for (const entry2 of entries) {
3472
3507
  const full = import_node_path19.default.join(current, entry2.name);
3473
3508
  if (entry2.isDirectory()) {
3474
- if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
3509
+ if (IGNORED_DIRS.has(entry2.name)) continue;
3510
+ if (await isPythonVenvDir(full)) continue;
3511
+ await walk(full);
3475
3512
  } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry2.name))) {
3476
3513
  out.push(full);
3477
3514
  }
@@ -4107,7 +4144,9 @@ async function walkTfFiles(start, depth = 0, max = 5) {
4107
4144
  for (const entry2 of entries) {
4108
4145
  if (entry2.isDirectory()) {
4109
4146
  if (IGNORED_DIRS.has(entry2.name) || entry2.name === ".terraform") continue;
4110
- out.push(...await walkTfFiles(import_node_path27.default.join(start, entry2.name), depth + 1, max));
4147
+ const child = import_node_path27.default.join(start, entry2.name);
4148
+ if (await isPythonVenvDir(child)) continue;
4149
+ out.push(...await walkTfFiles(child, depth + 1, max));
4111
4150
  } else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
4112
4151
  out.push(import_node_path27.default.join(start, entry2.name));
4113
4152
  }
@@ -4155,7 +4194,9 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
4155
4194
  for (const entry2 of entries) {
4156
4195
  if (entry2.isDirectory()) {
4157
4196
  if (IGNORED_DIRS.has(entry2.name)) continue;
4158
- out.push(...await walkYamlFiles2(import_node_path28.default.join(start, entry2.name), depth + 1, max));
4197
+ const child = import_node_path28.default.join(start, entry2.name);
4198
+ if (await isPythonVenvDir(child)) continue;
4199
+ out.push(...await walkYamlFiles2(child, depth + 1, max));
4159
4200
  } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry2.name))) {
4160
4201
  out.push(import_node_path28.default.join(start, entry2.name));
4161
4202
  }
@@ -5155,10 +5196,19 @@ function projectFromReq(req) {
5155
5196
  const params = req.params;
5156
5197
  return params.project ?? DEFAULT_PROJECT;
5157
5198
  }
5158
- function resolveProject(registry, req, reply) {
5199
+ function resolveProject(registry, req, reply, bootstrap) {
5159
5200
  const name = projectFromReq(req);
5160
5201
  const ctx = registry.get(name);
5161
5202
  if (!ctx) {
5203
+ const phase = bootstrap?.status(name);
5204
+ if (phase === "bootstrapping") {
5205
+ void reply.code(503).send({ ready: false, project: name, status: "bootstrapping" });
5206
+ return null;
5207
+ }
5208
+ if (phase === "broken") {
5209
+ void reply.code(503).send({ ready: false, project: name, status: "broken" });
5210
+ return null;
5211
+ }
5162
5212
  void reply.code(404).send({ error: "project not found", project: name });
5163
5213
  return null;
5164
5214
  }
@@ -5188,36 +5238,38 @@ function buildLegacyRegistry(opts) {
5188
5238
  function registerRoutes(scope, ctx) {
5189
5239
  const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
5190
5240
  scope.get("/events", (req, reply) => {
5191
- const proj = resolveProject(registry, req, reply);
5241
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5192
5242
  if (!proj) return;
5193
5243
  handleSse(req, reply, { project: proj.name });
5194
5244
  });
5195
- scope.get("/health", async (req, reply) => {
5196
- const proj = resolveProject(registry, req, reply);
5197
- if (!proj) return;
5198
- const uptimeMs = Date.now() - startedAt;
5199
- return {
5200
- ok: true,
5201
- project: proj.name,
5202
- uptimeMs,
5203
- // Legacy fields kept additively. The web shell's StatusBar reads
5204
- // nodeCount / edgeCount; ADR-061's HealthResponseSchema validates
5205
- // the canonical triple and lets the extras pass through.
5206
- uptime: Math.floor(uptimeMs / 1e3),
5207
- nodeCount: proj.graph.order,
5208
- edgeCount: proj.graph.size,
5209
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
5210
- };
5211
- });
5245
+ if (ctx.scope === "project") {
5246
+ scope.get("/health", async (req, reply) => {
5247
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5248
+ if (!proj) return;
5249
+ const uptimeMs = Date.now() - startedAt;
5250
+ return {
5251
+ ok: true,
5252
+ project: proj.name,
5253
+ uptimeMs,
5254
+ // Legacy fields kept additively. The web shell's StatusBar reads
5255
+ // nodeCount / edgeCount; ADR-061's HealthResponseSchema validates
5256
+ // the canonical triple and lets the extras pass through.
5257
+ uptime: Math.floor(uptimeMs / 1e3),
5258
+ nodeCount: proj.graph.order,
5259
+ edgeCount: proj.graph.size,
5260
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
5261
+ };
5262
+ });
5263
+ }
5212
5264
  scope.get("/graph", async (req, reply) => {
5213
- const proj = resolveProject(registry, req, reply);
5265
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5214
5266
  if (!proj) return;
5215
5267
  return serializeGraph(proj.graph);
5216
5268
  });
5217
5269
  scope.get(
5218
5270
  "/graph/node/:id",
5219
5271
  async (req, reply) => {
5220
- const proj = resolveProject(registry, req, reply);
5272
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5221
5273
  if (!proj) return;
5222
5274
  const { id } = req.params;
5223
5275
  if (!proj.graph.hasNode(id)) {
@@ -5229,7 +5281,7 @@ function registerRoutes(scope, ctx) {
5229
5281
  scope.get(
5230
5282
  "/graph/edges/:id",
5231
5283
  async (req, reply) => {
5232
- const proj = resolveProject(registry, req, reply);
5284
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5233
5285
  if (!proj) return;
5234
5286
  const { id } = req.params;
5235
5287
  if (!proj.graph.hasNode(id)) {
@@ -5241,7 +5293,7 @@ function registerRoutes(scope, ctx) {
5241
5293
  }
5242
5294
  );
5243
5295
  scope.get("/graph/dependencies/:nodeId", async (req, reply) => {
5244
- const proj = resolveProject(registry, req, reply);
5296
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5245
5297
  if (!proj) return;
5246
5298
  const { nodeId } = req.params;
5247
5299
  if (!proj.graph.hasNode(nodeId)) {
@@ -5256,7 +5308,7 @@ function registerRoutes(scope, ctx) {
5256
5308
  return getTransitiveDependencies(proj.graph, nodeId, depth);
5257
5309
  });
5258
5310
  scope.get("/graph/divergences", async (req, reply) => {
5259
- const proj = resolveProject(registry, req, reply);
5311
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5260
5312
  if (!proj) return;
5261
5313
  let typeFilter;
5262
5314
  if (req.query.type) {
@@ -5291,7 +5343,7 @@ function registerRoutes(scope, ctx) {
5291
5343
  });
5292
5344
  });
5293
5345
  scope.get("/incidents", async (req, reply) => {
5294
- const proj = resolveProject(registry, req, reply);
5346
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5295
5347
  if (!proj) return;
5296
5348
  const epath = errorsPathFor(proj);
5297
5349
  if (!epath) return { count: 0, total: 0, events: [] };
@@ -5303,7 +5355,7 @@ function registerRoutes(scope, ctx) {
5303
5355
  return { count: sliced.length, total, events: sliced };
5304
5356
  });
5305
5357
  scope.get("/stale-events", async (req, reply) => {
5306
- const proj = resolveProject(registry, req, reply);
5358
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5307
5359
  if (!proj) return;
5308
5360
  const spath = staleEventsPathFor(proj);
5309
5361
  if (!spath) return { count: 0, total: 0, events: [] };
@@ -5318,7 +5370,7 @@ function registerRoutes(scope, ctx) {
5318
5370
  scope.get(
5319
5371
  "/incidents/:nodeId",
5320
5372
  async (req, reply) => {
5321
- const proj = resolveProject(registry, req, reply);
5373
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5322
5374
  if (!proj) return;
5323
5375
  const { nodeId } = req.params;
5324
5376
  if (!proj.graph.hasNode(nodeId)) {
@@ -5334,7 +5386,7 @@ function registerRoutes(scope, ctx) {
5334
5386
  }
5335
5387
  );
5336
5388
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
5337
- const proj = resolveProject(registry, req, reply);
5389
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5338
5390
  if (!proj) return;
5339
5391
  const { nodeId } = req.params;
5340
5392
  if (!proj.graph.hasNode(nodeId)) {
@@ -5354,7 +5406,7 @@ function registerRoutes(scope, ctx) {
5354
5406
  return result;
5355
5407
  });
5356
5408
  scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
5357
- const proj = resolveProject(registry, req, reply);
5409
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5358
5410
  if (!proj) return;
5359
5411
  const { nodeId } = req.params;
5360
5412
  if (!proj.graph.hasNode(nodeId)) {
@@ -5367,7 +5419,7 @@ function registerRoutes(scope, ctx) {
5367
5419
  return getBlastRadius(proj.graph, nodeId, depth);
5368
5420
  });
5369
5421
  scope.get("/search", async (req, reply) => {
5370
- const proj = resolveProject(registry, req, reply);
5422
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5371
5423
  if (!proj) return;
5372
5424
  const raw = (req.query.q ?? "").trim();
5373
5425
  if (!raw) return reply.code(400).send({ error: "query parameter `q` is required" });
@@ -5398,7 +5450,7 @@ function registerRoutes(scope, ctx) {
5398
5450
  scope.get(
5399
5451
  "/graph/diff",
5400
5452
  async (req, reply) => {
5401
- const proj = resolveProject(registry, req, reply);
5453
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5402
5454
  if (!proj) return;
5403
5455
  const against = req.query.against;
5404
5456
  if (!against) {
@@ -5413,7 +5465,7 @@ function registerRoutes(scope, ctx) {
5413
5465
  }
5414
5466
  );
5415
5467
  scope.post("/snapshot", async (req, reply) => {
5416
- const proj = resolveProject(registry, req, reply);
5468
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5417
5469
  if (!proj) return;
5418
5470
  const body = req.body;
5419
5471
  if (!body || typeof body !== "object" || !body.snapshot) {
@@ -5442,7 +5494,7 @@ function registerRoutes(scope, ctx) {
5442
5494
  }
5443
5495
  });
5444
5496
  scope.post("/graph/scan", async (req, reply) => {
5445
- const proj = resolveProject(registry, req, reply);
5497
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5446
5498
  if (!proj) return;
5447
5499
  if (!proj.scanPath) {
5448
5500
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -5458,7 +5510,7 @@ function registerRoutes(scope, ctx) {
5458
5510
  };
5459
5511
  });
5460
5512
  scope.get("/policies", async (req, reply) => {
5461
- const proj = resolveProject(registry, req, reply);
5513
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5462
5514
  if (!proj) return;
5463
5515
  const policyPath = ctx.policyFilePathFor(proj);
5464
5516
  if (!policyPath) {
@@ -5475,7 +5527,7 @@ function registerRoutes(scope, ctx) {
5475
5527
  }
5476
5528
  });
5477
5529
  scope.get("/policies/violations", async (req, reply) => {
5478
- const proj = resolveProject(registry, req, reply);
5530
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5479
5531
  if (!proj) return;
5480
5532
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
5481
5533
  let violations = await log.readAll();
@@ -5495,7 +5547,7 @@ function registerRoutes(scope, ctx) {
5495
5547
  return { violations };
5496
5548
  });
5497
5549
  scope.post("/policies/check", async (req, reply) => {
5498
- const proj = resolveProject(registry, req, reply);
5550
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5499
5551
  if (!proj) return;
5500
5552
  const parsed = import_types23.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5501
5553
  if (!parsed.success) {
@@ -5566,10 +5618,36 @@ async function buildApi(opts) {
5566
5618
  const routeCtx = {
5567
5619
  registry,
5568
5620
  startedAt,
5621
+ scope: "root",
5569
5622
  errorsPathFor,
5570
5623
  staleEventsPathFor,
5571
- policyFilePathFor
5624
+ policyFilePathFor,
5625
+ bootstrap: opts.bootstrap
5572
5626
  };
5627
+ app.get("/health", async () => {
5628
+ const uptimeMs = Date.now() - startedAt;
5629
+ const bootstrapList = opts.bootstrap?.list() ?? [];
5630
+ const byName = new Map(bootstrapList.map((p) => [p.name, p]));
5631
+ const names = /* @__PURE__ */ new Set([
5632
+ ...registry.list(),
5633
+ ...bootstrapList.map((p) => p.name)
5634
+ ]);
5635
+ const projects = [...names].sort().map((name) => {
5636
+ const proj = registry.get(name);
5637
+ const tracked = byName.get(name);
5638
+ return {
5639
+ name,
5640
+ nodeCount: proj?.graph.order ?? 0,
5641
+ edgeCount: proj?.graph.size ?? 0,
5642
+ ...tracked ? { status: tracked.status, elapsedMs: tracked.elapsedMs } : {}
5643
+ };
5644
+ });
5645
+ return {
5646
+ ok: true,
5647
+ uptimeMs,
5648
+ projects
5649
+ };
5650
+ });
5573
5651
  app.get("/projects", async (_req, reply) => {
5574
5652
  try {
5575
5653
  return await listProjects();
@@ -5597,7 +5675,7 @@ async function buildApi(opts) {
5597
5675
  registerRoutes(app, routeCtx);
5598
5676
  await app.register(
5599
5677
  async (scope) => {
5600
- registerRoutes(scope, routeCtx);
5678
+ registerRoutes(scope, { ...routeCtx, scope: "project" });
5601
5679
  },
5602
5680
  { prefix: "/projects/:project" }
5603
5681
  );
@@ -5980,6 +6058,14 @@ var IGNORED_WATCH_GLOBS = [
5980
6058
  "**/.turbo/**",
5981
6059
  "**/.next/**",
5982
6060
  "**/neat-out/**",
6061
+ // Python venv shapes (issue #344). chokidar opens one watch handle per
6062
+ // descended dir; a CPython venv carries 20k+ files and trivially blows
6063
+ // through the macOS kqueue cap before extraction even runs.
6064
+ "**/.venv/**",
6065
+ "**/venv/**",
6066
+ "**/__pypackages__/**",
6067
+ "**/.tox/**",
6068
+ "**/site-packages/**",
5983
6069
  "**/.DS_Store"
5984
6070
  ];
5985
6071
  var IGNORED_WATCH_PATHS = [
@@ -5990,6 +6076,11 @@ var IGNORED_WATCH_PATHS = [
5990
6076
  /(?:^|[\\/])\.turbo[\\/]/,
5991
6077
  /(?:^|[\\/])\.next[\\/]/,
5992
6078
  /(?:^|[\\/])neat-out[\\/]/,
6079
+ /(?:^|[\\/])\.venv[\\/]/,
6080
+ /(?:^|[\\/])venv[\\/]/,
6081
+ /(?:^|[\\/])__pypackages__[\\/]/,
6082
+ /(?:^|[\\/])\.tox[\\/]/,
6083
+ /(?:^|[\\/])site-packages[\\/]/,
5993
6084
  /[\\/]?\.DS_Store$/
5994
6085
  ];
5995
6086
  function shouldIgnore(absPath) {
@@ -6427,10 +6518,10 @@ dotenv.config({ path: path.join(here, '.env.neat') })
6427
6518
 
6428
6519
  await import('@opentelemetry/auto-instrumentations-node/register')
6429
6520
  `;
6430
- function renderEnvNeat(serviceName) {
6521
+ function renderEnvNeat(projectName) {
6431
6522
  return [
6432
6523
  "# Generated by `neat init --apply` (ADR-069).",
6433
- `OTEL_SERVICE_NAME=${serviceName}`,
6524
+ `OTEL_SERVICE_NAME=${projectName}`,
6434
6525
  "OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318",
6435
6526
  ""
6436
6527
  ].join("\n");
@@ -6600,6 +6691,10 @@ var OTEL_ENV = {
6600
6691
  key: "OTEL_EXPORTER_OTLP_ENDPOINT",
6601
6692
  value: "http://localhost:4318"
6602
6693
  };
6694
+ function envServiceName(pkg, serviceDir, project) {
6695
+ if (project && project.length > 0) return project;
6696
+ return pkg.name ?? import_node_path40.default.basename(serviceDir);
6697
+ }
6603
6698
  async function readPackageJson(serviceDir) {
6604
6699
  try {
6605
6700
  const raw = await import_node_fs25.promises.readFile(import_node_path40.default.join(serviceDir, "package.json"), "utf8");
@@ -6824,7 +6919,7 @@ function lineIsOtelInjection(line) {
6824
6919
  if (trimmed.length === 0) return false;
6825
6920
  return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
6826
6921
  }
6827
- async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
6922
+ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project) {
6828
6923
  const useTs = await isTypeScriptProject(serviceDir);
6829
6924
  const instrumentationFile = import_node_path40.default.join(serviceDir, useTs ? "instrumentation.ts" : "instrumentation.js");
6830
6925
  const instrumentationNodeFile = import_node_path40.default.join(
@@ -6861,7 +6956,7 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
6861
6956
  if (!await exists2(envNeatFile)) {
6862
6957
  generatedFiles.push({
6863
6958
  file: envNeatFile,
6864
- contents: renderEnvNeat(pkg.name ?? import_node_path40.default.basename(serviceDir)),
6959
+ contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
6865
6960
  skipIfExists: true
6866
6961
  });
6867
6962
  }
@@ -6917,12 +7012,12 @@ function buildDependencyEdits(pkg, manifestPath) {
6917
7012
  }
6918
7013
  return edits;
6919
7014
  }
6920
- async function queueEnvNeat(serviceDir, pkg, generatedFiles) {
7015
+ async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
6921
7016
  const envNeatFile = import_node_path40.default.join(serviceDir, ".env.neat");
6922
7017
  if (!await exists2(envNeatFile)) {
6923
7018
  generatedFiles.push({
6924
7019
  file: envNeatFile,
6925
- contents: renderEnvNeat(pkg.name ?? import_node_path40.default.basename(serviceDir)),
7020
+ contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
6926
7021
  skipIfExists: true
6927
7022
  });
6928
7023
  }
@@ -6941,7 +7036,7 @@ function fileImportsOtelHook(raw, specifiers) {
6941
7036
  }
6942
7037
  return false;
6943
7038
  }
6944
- async function planRemix(serviceDir, pkg, manifestPath, entryFile) {
7039
+ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
6945
7040
  const useTs = await isTypeScriptProject(serviceDir);
6946
7041
  const otelServerFile = import_node_path40.default.join(
6947
7042
  serviceDir,
@@ -6956,7 +7051,7 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile) {
6956
7051
  skipIfExists: true
6957
7052
  });
6958
7053
  }
6959
- await queueEnvNeat(serviceDir, pkg, generatedFiles);
7054
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
6960
7055
  const entrypointEdits = [];
6961
7056
  try {
6962
7057
  const raw = await import_node_fs25.promises.readFile(entryFile, "utf8");
@@ -6993,7 +7088,7 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile) {
6993
7088
  framework: "remix"
6994
7089
  };
6995
7090
  }
6996
- async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile) {
7091
+ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project) {
6997
7092
  const useTs = await isTypeScriptProject(serviceDir);
6998
7093
  const otelInitFile = import_node_path40.default.join(
6999
7094
  serviceDir,
@@ -7010,7 +7105,7 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile) {
7010
7105
  skipIfExists: true
7011
7106
  });
7012
7107
  }
7013
- await queueEnvNeat(serviceDir, pkg, generatedFiles);
7108
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
7014
7109
  if (hooksFile === null) {
7015
7110
  generatedFiles.push({
7016
7111
  file: resolvedHooksFile,
@@ -7054,7 +7149,7 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile) {
7054
7149
  framework: "sveltekit"
7055
7150
  };
7056
7151
  }
7057
- async function planNuxt(serviceDir, pkg, manifestPath) {
7152
+ async function planNuxt(serviceDir, pkg, manifestPath, project) {
7058
7153
  const useTs = await isTypeScriptProject(serviceDir);
7059
7154
  const otelPluginFile = import_node_path40.default.join(
7060
7155
  serviceDir,
@@ -7080,7 +7175,7 @@ async function planNuxt(serviceDir, pkg, manifestPath) {
7080
7175
  skipIfExists: true
7081
7176
  });
7082
7177
  }
7083
- await queueEnvNeat(serviceDir, pkg, generatedFiles);
7178
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
7084
7179
  const empty = dependencyEdits.length === 0 && generatedFiles.length === 0;
7085
7180
  if (empty) {
7086
7181
  return {
@@ -7111,7 +7206,7 @@ async function findAstroMiddleware(serviceDir) {
7111
7206
  }
7112
7207
  return null;
7113
7208
  }
7114
- async function planAstro(serviceDir, pkg, manifestPath) {
7209
+ async function planAstro(serviceDir, pkg, manifestPath, project) {
7115
7210
  const useTs = await isTypeScriptProject(serviceDir);
7116
7211
  const otelInitFile = import_node_path40.default.join(
7117
7212
  serviceDir,
@@ -7129,7 +7224,7 @@ async function planAstro(serviceDir, pkg, manifestPath) {
7129
7224
  skipIfExists: true
7130
7225
  });
7131
7226
  }
7132
- await queueEnvNeat(serviceDir, pkg, generatedFiles);
7227
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
7133
7228
  if (existingMiddleware === null) {
7134
7229
  generatedFiles.push({
7135
7230
  file: middlewareFile,
@@ -7173,9 +7268,10 @@ async function planAstro(serviceDir, pkg, manifestPath) {
7173
7268
  framework: "astro"
7174
7269
  };
7175
7270
  }
7176
- async function plan(serviceDir) {
7271
+ async function plan(serviceDir, opts) {
7177
7272
  const pkg = await readPackageJson(serviceDir);
7178
7273
  const manifestPath = import_node_path40.default.join(serviceDir, "package.json");
7274
+ const project = opts?.project;
7179
7275
  const empty = {
7180
7276
  language: "javascript",
7181
7277
  serviceDir,
@@ -7188,32 +7284,32 @@ async function plan(serviceDir) {
7188
7284
  if (hasNextDependency(pkg)) {
7189
7285
  const nextConfig = await findNextConfig(serviceDir);
7190
7286
  if (nextConfig) {
7191
- return planNext(serviceDir, pkg, manifestPath, nextConfig);
7287
+ return planNext(serviceDir, pkg, manifestPath, nextConfig, project);
7192
7288
  }
7193
7289
  }
7194
7290
  if (hasRemixDependency(pkg)) {
7195
7291
  const remixEntry = await findRemixEntry(serviceDir);
7196
7292
  if (remixEntry) {
7197
- return planRemix(serviceDir, pkg, manifestPath, remixEntry);
7293
+ return planRemix(serviceDir, pkg, manifestPath, remixEntry, project);
7198
7294
  }
7199
7295
  }
7200
7296
  if (hasSvelteKitDependency(pkg)) {
7201
7297
  const hooks = await findSvelteKitHooks(serviceDir);
7202
7298
  const config = await findSvelteKitConfig(serviceDir);
7203
7299
  if (hooks || config) {
7204
- return planSvelteKit(serviceDir, pkg, manifestPath, hooks);
7300
+ return planSvelteKit(serviceDir, pkg, manifestPath, hooks, project);
7205
7301
  }
7206
7302
  }
7207
7303
  if (hasNuxtDependency(pkg)) {
7208
7304
  const nuxtConfig = await findNuxtConfig(serviceDir);
7209
7305
  if (nuxtConfig) {
7210
- return planNuxt(serviceDir, pkg, manifestPath);
7306
+ return planNuxt(serviceDir, pkg, manifestPath, project);
7211
7307
  }
7212
7308
  }
7213
7309
  if (hasAstroDependency(pkg)) {
7214
7310
  const astroConfig = await findAstroConfig(serviceDir);
7215
7311
  if (astroConfig) {
7216
- return planAstro(serviceDir, pkg, manifestPath);
7312
+ return planAstro(serviceDir, pkg, manifestPath, project);
7217
7313
  }
7218
7314
  }
7219
7315
  const entryFile = await resolveEntry(serviceDir, pkg);
@@ -7261,7 +7357,7 @@ async function plan(serviceDir) {
7261
7357
  if (!await exists2(envNeatFile)) {
7262
7358
  generatedFiles.push({
7263
7359
  file: envNeatFile,
7264
- contents: renderEnvNeat(pkg.name ?? import_node_path40.default.basename(serviceDir)),
7360
+ contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
7265
7361
  skipIfExists: true
7266
7362
  });
7267
7363
  }
@@ -7796,14 +7892,14 @@ async function extractAndPersist(opts) {
7796
7892
  errorsPath: projectPaths.errorsPath
7797
7893
  };
7798
7894
  }
7799
- async function applyInstallersOver(services) {
7895
+ async function applyInstallersOver(services, project) {
7800
7896
  let instrumented = 0;
7801
7897
  let already = 0;
7802
7898
  let libOnly = 0;
7803
7899
  for (const svc of services) {
7804
7900
  const installer = await pickInstaller(svc.dir);
7805
7901
  if (!installer) continue;
7806
- const plan3 = await installer.plan(svc.dir);
7902
+ const plan3 = await installer.plan(svc.dir, { project });
7807
7903
  if (isEmptyPlan(plan3) && !plan3.libOnly) {
7808
7904
  already++;
7809
7905
  continue;
@@ -7825,28 +7921,106 @@ async function promptYesNo(question) {
7825
7921
  });
7826
7922
  });
7827
7923
  }
7828
- var DEFAULT_DAEMON_READY_TIMEOUT_MS = 15e3;
7829
- async function checkDaemonHealth(restPort) {
7924
+ var DEFAULT_DAEMON_READY_TIMEOUT_MS = 6e4;
7925
+ var PROBE_INTERVAL_MS = 500;
7926
+ async function fetchDaemonHealth(restPort) {
7830
7927
  return new Promise((resolve) => {
7831
7928
  const req = import_node_http.default.get(`http://127.0.0.1:${restPort}/health`, (res) => {
7832
7929
  const ok = res.statusCode !== void 0 && res.statusCode >= 200 && res.statusCode < 300;
7833
- res.resume();
7834
- resolve(ok);
7930
+ if (!ok) {
7931
+ res.resume();
7932
+ resolve(null);
7933
+ return;
7934
+ }
7935
+ let body = "";
7936
+ res.setEncoding("utf8");
7937
+ res.on("data", (chunk) => {
7938
+ body += chunk;
7939
+ });
7940
+ res.on("end", () => {
7941
+ try {
7942
+ resolve(JSON.parse(body));
7943
+ } catch {
7944
+ resolve({ ok: true });
7945
+ }
7946
+ });
7835
7947
  });
7836
- req.on("error", () => resolve(false));
7948
+ req.on("error", () => resolve(null));
7837
7949
  req.setTimeout(1e3, () => {
7838
7950
  req.destroy();
7839
- resolve(false);
7951
+ resolve(null);
7952
+ });
7953
+ });
7954
+ }
7955
+ async function checkDaemonHealth(restPort) {
7956
+ const body = await fetchDaemonHealth(restPort);
7957
+ return body !== null;
7958
+ }
7959
+ async function probeProjectHealth(restPort, name) {
7960
+ return new Promise((resolve) => {
7961
+ const req = import_node_http.default.get(
7962
+ `http://127.0.0.1:${restPort}/projects/${encodeURIComponent(name)}/health`,
7963
+ (res) => {
7964
+ const code = res.statusCode ?? 0;
7965
+ res.resume();
7966
+ if (code >= 200 && code < 300) resolve("active");
7967
+ else resolve("bootstrapping");
7968
+ }
7969
+ );
7970
+ req.on("error", () => resolve("bootstrapping"));
7971
+ req.setTimeout(1e3, () => {
7972
+ req.destroy();
7973
+ resolve("bootstrapping");
7840
7974
  });
7841
7975
  });
7842
7976
  }
7977
+ async function snapshotProjectStatus(restPort, body) {
7978
+ if (body.projects && body.projects.length > 0) {
7979
+ return body.projects.map((p) => ({
7980
+ name: p.name,
7981
+ status: p.status ?? "active"
7982
+ }));
7983
+ }
7984
+ const entries = await listProjects().catch(() => []);
7985
+ if (entries.length === 0) return [];
7986
+ return Promise.all(
7987
+ entries.map(async (entry2) => ({
7988
+ name: entry2.name,
7989
+ status: await probeProjectHealth(restPort, entry2.name)
7990
+ }))
7991
+ );
7992
+ }
7843
7993
  async function waitForDaemonReady(restPort, timeoutMs) {
7844
7994
  const deadline = Date.now() + timeoutMs;
7995
+ let lastBootstrapping = [];
7845
7996
  while (Date.now() < deadline) {
7846
- if (await checkDaemonHealth(restPort)) return true;
7847
- await new Promise((r) => setTimeout(r, 300));
7997
+ const body = await fetchDaemonHealth(restPort);
7998
+ if (body !== null) {
7999
+ const projects2 = await snapshotProjectStatus(restPort, body);
8000
+ const bootstrapping = projects2.filter((p) => p.status === "bootstrapping").map((p) => p.name);
8001
+ const broken = projects2.filter((p) => p.status === "broken").map((p) => p.name);
8002
+ if (bootstrapping.length === 0) {
8003
+ return { ready: true, brokenProjects: broken, stillBootstrapping: [] };
8004
+ }
8005
+ const key = bootstrapping.slice().sort().join(",");
8006
+ const prevKey = lastBootstrapping.slice().sort().join(",");
8007
+ if (key !== prevKey) {
8008
+ const plural = bootstrapping.length === 1 ? "" : "s";
8009
+ console.log(
8010
+ `neat: waiting on ${bootstrapping.length} project${plural}: ${bootstrapping.join(", ")}`
8011
+ );
8012
+ lastBootstrapping = bootstrapping;
8013
+ }
8014
+ }
8015
+ await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
7848
8016
  }
7849
- return false;
8017
+ const final = await fetchDaemonHealth(restPort);
8018
+ const projects = final ? await snapshotProjectStatus(restPort, final) : [];
8019
+ return {
8020
+ ready: false,
8021
+ brokenProjects: projects.filter((p) => p.status === "broken").map((p) => p.name),
8022
+ stillBootstrapping: projects.filter((p) => p.status === "bootstrapping").map((p) => p.name)
8023
+ };
7850
8024
  }
7851
8025
  function spawnDaemonDetached() {
7852
8026
  const here = import_node_path42.default.dirname(new URL(importMetaUrl).pathname);
@@ -7867,12 +8041,21 @@ function spawnDaemonDetached() {
7867
8041
  if (!entry2) {
7868
8042
  throw new Error(`orchestrator: cannot locate neatd entry in ${here}`);
7869
8043
  }
8044
+ const env = { ...process.env };
8045
+ const hasToken = typeof env.NEAT_AUTH_TOKEN === "string" && env.NEAT_AUTH_TOKEN.length > 0;
8046
+ if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
8047
+ env.HOST = "127.0.0.1";
8048
+ }
7870
8049
  const child = (0, import_node_child_process2.spawn)(process.execPath, [entry2, "start"], {
7871
8050
  detached: true,
7872
- stdio: "ignore",
7873
- env: process.env
8051
+ // stderr inherits the orchestrator's fd so the daemon's
8052
+ // `BindAuthorityError` message lands in front of the operator instead
8053
+ // of being swallowed (issue #341).
8054
+ stdio: ["ignore", "ignore", "inherit"],
8055
+ env
7874
8056
  });
7875
8057
  child.unref();
8058
+ return child;
7876
8059
  }
7877
8060
  function openBrowser(url) {
7878
8061
  if (!process.stdout.isTTY) return "failed";
@@ -7948,7 +8131,7 @@ async function runOrchestrator(opts) {
7948
8131
  result.steps.apply.skipped = true;
7949
8132
  console.log("skipped instrumentation (--no-instrument)");
7950
8133
  } else {
7951
- const tally = await applyInstallersOver(services);
8134
+ const tally = await applyInstallersOver(services, opts.project);
7952
8135
  result.steps.apply = { ...tally, skipped: false };
7953
8136
  console.log(
7954
8137
  `instrumented ${tally.instrumented}, already ${tally.alreadyInstrumented}, lib-only ${tally.libOnly}`
@@ -7967,12 +8150,25 @@ async function runOrchestrator(opts) {
7967
8150
  return result;
7968
8151
  }
7969
8152
  const ready = await waitForDaemonReady(restPort, timeoutMs);
7970
- result.steps.daemon = ready ? "spawned" : "timed-out";
7971
- if (!ready) {
8153
+ result.steps.daemon = ready.ready ? "spawned" : "timed-out";
8154
+ if (!ready.ready) {
7972
8155
  console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
8156
+ if (ready.stillBootstrapping.length > 0) {
8157
+ console.error(
8158
+ `neat: still bootstrapping: ${ready.stillBootstrapping.join(", ")}`
8159
+ );
8160
+ }
8161
+ if (ready.brokenProjects.length > 0) {
8162
+ console.error(`neat: broken projects: ${ready.brokenProjects.join(", ")}`);
8163
+ }
7973
8164
  result.exitCode = 1;
7974
8165
  return result;
7975
8166
  }
8167
+ if (ready.brokenProjects.length > 0) {
8168
+ console.warn(
8169
+ `neat: ${ready.brokenProjects.length} project(s) reported broken: ${ready.brokenProjects.join(", ")}`
8170
+ );
8171
+ }
7976
8172
  }
7977
8173
  const dashboardUrl = opts.dashboardUrl ?? "http://localhost:6328";
7978
8174
  if (opts.noOpen || !process.stdout.isTTY) {
@@ -8603,7 +8799,7 @@ async function runSync(opts) {
8603
8799
  snapshotPath = target;
8604
8800
  }
8605
8801
  const skipApply = opts.dryRun || opts.noInstrument;
8606
- const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services);
8802
+ const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services, entry2.name);
8607
8803
  const warnings = [];
8608
8804
  let daemonState = "skipped";
8609
8805
  let exitCode = 0;
@@ -8699,6 +8895,8 @@ function usage() {
8699
8895
  console.log(" uninstall <name>");
8700
8896
  console.log(" Remove a project from the registry. Does not touch");
8701
8897
  console.log(" neat-out/, policy.json, or any user file.");
8898
+ console.log(" version Print the installed @neat.is/core version and exit.");
8899
+ console.log(" Aliases: --version, -v.");
8702
8900
  console.log(" skill Install or print the Claude Code MCP drop-in.");
8703
8901
  console.log(" Flags:");
8704
8902
  console.log(" --print-config print the JSON snippet to stdout");
@@ -8825,7 +9023,7 @@ function parseArgs(rest) {
8825
9023
  out.yes = true;
8826
9024
  continue;
8827
9025
  }
8828
- if (arg === "--verbose" || arg === "-v") {
9026
+ if (arg === "--verbose") {
8829
9027
  out.verbose = true;
8830
9028
  continue;
8831
9029
  }
@@ -8884,6 +9082,28 @@ function assignFlag(out, field, value) {
8884
9082
  ;
8885
9083
  out[field] = value;
8886
9084
  }
9085
+ function readPackageVersion() {
9086
+ const here = typeof __dirname !== "undefined" ? __dirname : import_node_path44.default.dirname((0, import_node_url3.fileURLToPath)(importMetaUrl));
9087
+ const candidates = [
9088
+ import_node_path44.default.resolve(here, "../package.json"),
9089
+ import_node_path44.default.resolve(here, "../../package.json")
9090
+ ];
9091
+ for (const candidate of candidates) {
9092
+ try {
9093
+ const raw = (0, import_node_fs28.readFileSync)(candidate, "utf8");
9094
+ const parsed = JSON.parse(raw);
9095
+ if (parsed.name === "@neat.is/core" && typeof parsed.version === "string") {
9096
+ return parsed.version;
9097
+ }
9098
+ } catch {
9099
+ }
9100
+ }
9101
+ return "unknown";
9102
+ }
9103
+ function printVersion() {
9104
+ process.stdout.write(`${readPackageVersion()}
9105
+ `);
9106
+ }
8887
9107
  function printBanner() {
8888
9108
  console.log("\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557");
8889
9109
  console.log("\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D");
@@ -8893,7 +9113,7 @@ function printBanner() {
8893
9113
  console.log("\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D ");
8894
9114
  console.log("");
8895
9115
  console.log(" Network Expressive Architecting Tool");
8896
- console.log(" neat.is \xB7 v0.2.5 \xB7 BSL 1.1");
9116
+ console.log(" neat.is \xB7 v0.4.0 \xB7 Apache 2.0");
8897
9117
  console.log("");
8898
9118
  }
8899
9119
  function printDiscoveryReport(opts, services) {
@@ -8921,12 +9141,12 @@ function printDiscoveryReport(opts, services) {
8921
9141
  }
8922
9142
  console.log("");
8923
9143
  }
8924
- async function buildPatchSections(services) {
9144
+ async function buildPatchSections(services, project) {
8925
9145
  const sections = [];
8926
9146
  for (const svc of services) {
8927
9147
  const installer = await pickInstaller(svc.dir);
8928
9148
  if (!installer) continue;
8929
- const plan3 = await installer.plan(svc.dir);
9149
+ const plan3 = await installer.plan(svc.dir, { project });
8930
9150
  if (isEmptyPlan(plan3) && !plan3.libOnly) continue;
8931
9151
  sections.push({ installer: installer.name, plan: plan3 });
8932
9152
  }
@@ -8941,7 +9161,7 @@ async function runInit(opts) {
8941
9161
  }
8942
9162
  const services = await discoverServices(opts.scanPath);
8943
9163
  printDiscoveryReport(opts, services);
8944
- const sections = opts.noInstall ? [] : await buildPatchSections(services);
9164
+ const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
8945
9165
  const patch = renderPatch(sections);
8946
9166
  const patchPath = import_node_path44.default.join(opts.scanPath, "neat.patch");
8947
9167
  if (opts.dryRun) {
@@ -9099,6 +9319,10 @@ async function main() {
9099
9319
  usage();
9100
9320
  process.exit(0);
9101
9321
  }
9322
+ if (cmd === "--version" || cmd === "-v" || cmd === "version") {
9323
+ printVersion();
9324
+ process.exit(0);
9325
+ }
9102
9326
  const parsed = parseArgs(rest);
9103
9327
  const { positional, apply: apply3, dryRun, noInstall } = parsed;
9104
9328
  const project = parsed.project ?? DEFAULT_PROJECT;
@@ -9505,6 +9729,7 @@ if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsW
9505
9729
  CLAUDE_SKILL_CONFIG,
9506
9730
  QUERY_VERBS,
9507
9731
  parseArgs,
9732
+ readPackageVersion,
9508
9733
  runInit,
9509
9734
  runQueryVerb,
9510
9735
  runSkill