@neat.is/core 0.4.0 → 0.4.3

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");
@@ -6451,41 +6542,26 @@ export async function register() {
6451
6542
  }
6452
6543
  `;
6453
6544
  var NEXT_INSTRUMENTATION_NODE_TS = `${NEXT_INSTRUMENTATION_HEADER}
6454
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
6455
- // the OTel SDK starts so the exporter target and service name are in scope.
6456
- import { fileURLToPath } from 'node:url'
6457
- import path from 'node:path'
6458
- import dotenv from 'dotenv'
6545
+ process.env.OTEL_SERVICE_NAME ||= '__PROJECT_NAME__'
6546
+ process.env.OTEL_EXPORTER_OTLP_ENDPOINT ||= 'http://localhost:4318'
6547
+
6459
6548
  import { NodeSDK } from '@opentelemetry/sdk-node'
6460
6549
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6461
6550
 
6462
- const here = path.dirname(fileURLToPath(import.meta.url))
6463
- dotenv.config({ path: path.join(here, '.env.neat') })
6464
-
6465
- const sdk = new NodeSDK({
6466
- serviceName: process.env.OTEL_SERVICE_NAME,
6467
- instrumentations: [getNodeAutoInstrumentations()],
6468
- })
6469
- sdk.start()
6551
+ new NodeSDK({ instrumentations: [getNodeAutoInstrumentations()] }).start()
6470
6552
  `;
6471
6553
  var NEXT_INSTRUMENTATION_NODE_JS = `${NEXT_INSTRUMENTATION_HEADER}
6472
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
6473
- // the OTel SDK starts so the exporter target and service name are in scope.
6474
- import { fileURLToPath } from 'node:url'
6475
- import path from 'node:path'
6476
- import dotenv from 'dotenv'
6477
- import { NodeSDK } from '@opentelemetry/sdk-node'
6478
- import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6554
+ process.env.OTEL_SERVICE_NAME ||= '__PROJECT_NAME__'
6555
+ process.env.OTEL_EXPORTER_OTLP_ENDPOINT ||= 'http://localhost:4318'
6479
6556
 
6480
- const here = path.dirname(fileURLToPath(import.meta.url))
6481
- dotenv.config({ path: path.join(here, '.env.neat') })
6557
+ const { NodeSDK } = require('@opentelemetry/sdk-node')
6558
+ const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
6482
6559
 
6483
- const sdk = new NodeSDK({
6484
- serviceName: process.env.OTEL_SERVICE_NAME,
6485
- instrumentations: [getNodeAutoInstrumentations()],
6486
- })
6487
- sdk.start()
6560
+ new NodeSDK({ instrumentations: [getNodeAutoInstrumentations()] }).start()
6488
6561
  `;
6562
+ function renderNextInstrumentationNode(template, projectName) {
6563
+ return template.replace(/__PROJECT_NAME__/g, projectName);
6564
+ }
6489
6565
  var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
6490
6566
  var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
6491
6567
  // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
@@ -6600,6 +6676,10 @@ var OTEL_ENV = {
6600
6676
  key: "OTEL_EXPORTER_OTLP_ENDPOINT",
6601
6677
  value: "http://localhost:4318"
6602
6678
  };
6679
+ function envServiceName(pkg, serviceDir, project) {
6680
+ if (project && project.length > 0) return project;
6681
+ return pkg.name ?? import_node_path40.default.basename(serviceDir);
6682
+ }
6603
6683
  async function readPackageJson(serviceDir) {
6604
6684
  try {
6605
6685
  const raw = await import_node_fs25.promises.readFile(import_node_path40.default.join(serviceDir, "package.json"), "utf8");
@@ -6824,17 +6904,29 @@ function lineIsOtelInjection(line) {
6824
6904
  if (trimmed.length === 0) return false;
6825
6905
  return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
6826
6906
  }
6827
- async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
6907
+ async function detectsSrcLayout(serviceDir) {
6908
+ const [hasSrcApp, hasSrcPages, hasRootApp, hasRootPages] = await Promise.all([
6909
+ exists2(import_node_path40.default.join(serviceDir, "src", "app")),
6910
+ exists2(import_node_path40.default.join(serviceDir, "src", "pages")),
6911
+ exists2(import_node_path40.default.join(serviceDir, "app")),
6912
+ exists2(import_node_path40.default.join(serviceDir, "pages"))
6913
+ ]);
6914
+ return (hasSrcApp || hasSrcPages) && !hasRootApp && !hasRootPages;
6915
+ }
6916
+ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project) {
6828
6917
  const useTs = await isTypeScriptProject(serviceDir);
6829
- const instrumentationFile = import_node_path40.default.join(serviceDir, useTs ? "instrumentation.ts" : "instrumentation.js");
6918
+ const srcLayout = await detectsSrcLayout(serviceDir);
6919
+ const baseDir = srcLayout ? import_node_path40.default.join(serviceDir, "src") : serviceDir;
6920
+ const instrumentationFile = import_node_path40.default.join(baseDir, useTs ? "instrumentation.ts" : "instrumentation.js");
6830
6921
  const instrumentationNodeFile = import_node_path40.default.join(
6831
- serviceDir,
6922
+ baseDir,
6832
6923
  useTs ? "instrumentation.node.ts" : "instrumentation.node.js"
6833
6924
  );
6834
- const envNeatFile = import_node_path40.default.join(serviceDir, ".env.neat");
6925
+ const envNeatFile = import_node_path40.default.join(baseDir, ".env.neat");
6835
6926
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
6836
6927
  const dependencyEdits = [];
6837
6928
  for (const sdk of SDK_PACKAGES) {
6929
+ if (sdk.name === "dotenv") continue;
6838
6930
  if (sdk.name in existingDeps) continue;
6839
6931
  dependencyEdits.push({
6840
6932
  file: manifestPath,
@@ -6843,6 +6935,7 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
6843
6935
  version: sdk.version
6844
6936
  });
6845
6937
  }
6938
+ const projectName = envServiceName(pkg, serviceDir, project);
6846
6939
  const generatedFiles = [];
6847
6940
  if (!await exists2(instrumentationFile)) {
6848
6941
  generatedFiles.push({
@@ -6854,14 +6947,17 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
6854
6947
  if (!await exists2(instrumentationNodeFile)) {
6855
6948
  generatedFiles.push({
6856
6949
  file: instrumentationNodeFile,
6857
- contents: useTs ? NEXT_INSTRUMENTATION_NODE_TS : NEXT_INSTRUMENTATION_NODE_JS,
6950
+ contents: renderNextInstrumentationNode(
6951
+ useTs ? NEXT_INSTRUMENTATION_NODE_TS : NEXT_INSTRUMENTATION_NODE_JS,
6952
+ projectName
6953
+ ),
6858
6954
  skipIfExists: true
6859
6955
  });
6860
6956
  }
6861
6957
  if (!await exists2(envNeatFile)) {
6862
6958
  generatedFiles.push({
6863
6959
  file: envNeatFile,
6864
- contents: renderEnvNeat(pkg.name ?? import_node_path40.default.basename(serviceDir)),
6960
+ contents: renderEnvNeat(projectName),
6865
6961
  skipIfExists: true
6866
6962
  });
6867
6963
  }
@@ -6917,12 +7013,12 @@ function buildDependencyEdits(pkg, manifestPath) {
6917
7013
  }
6918
7014
  return edits;
6919
7015
  }
6920
- async function queueEnvNeat(serviceDir, pkg, generatedFiles) {
7016
+ async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
6921
7017
  const envNeatFile = import_node_path40.default.join(serviceDir, ".env.neat");
6922
7018
  if (!await exists2(envNeatFile)) {
6923
7019
  generatedFiles.push({
6924
7020
  file: envNeatFile,
6925
- contents: renderEnvNeat(pkg.name ?? import_node_path40.default.basename(serviceDir)),
7021
+ contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
6926
7022
  skipIfExists: true
6927
7023
  });
6928
7024
  }
@@ -6941,7 +7037,7 @@ function fileImportsOtelHook(raw, specifiers) {
6941
7037
  }
6942
7038
  return false;
6943
7039
  }
6944
- async function planRemix(serviceDir, pkg, manifestPath, entryFile) {
7040
+ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
6945
7041
  const useTs = await isTypeScriptProject(serviceDir);
6946
7042
  const otelServerFile = import_node_path40.default.join(
6947
7043
  serviceDir,
@@ -6956,7 +7052,7 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile) {
6956
7052
  skipIfExists: true
6957
7053
  });
6958
7054
  }
6959
- await queueEnvNeat(serviceDir, pkg, generatedFiles);
7055
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
6960
7056
  const entrypointEdits = [];
6961
7057
  try {
6962
7058
  const raw = await import_node_fs25.promises.readFile(entryFile, "utf8");
@@ -6993,7 +7089,7 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile) {
6993
7089
  framework: "remix"
6994
7090
  };
6995
7091
  }
6996
- async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile) {
7092
+ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project) {
6997
7093
  const useTs = await isTypeScriptProject(serviceDir);
6998
7094
  const otelInitFile = import_node_path40.default.join(
6999
7095
  serviceDir,
@@ -7010,7 +7106,7 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile) {
7010
7106
  skipIfExists: true
7011
7107
  });
7012
7108
  }
7013
- await queueEnvNeat(serviceDir, pkg, generatedFiles);
7109
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
7014
7110
  if (hooksFile === null) {
7015
7111
  generatedFiles.push({
7016
7112
  file: resolvedHooksFile,
@@ -7054,7 +7150,7 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile) {
7054
7150
  framework: "sveltekit"
7055
7151
  };
7056
7152
  }
7057
- async function planNuxt(serviceDir, pkg, manifestPath) {
7153
+ async function planNuxt(serviceDir, pkg, manifestPath, project) {
7058
7154
  const useTs = await isTypeScriptProject(serviceDir);
7059
7155
  const otelPluginFile = import_node_path40.default.join(
7060
7156
  serviceDir,
@@ -7080,7 +7176,7 @@ async function planNuxt(serviceDir, pkg, manifestPath) {
7080
7176
  skipIfExists: true
7081
7177
  });
7082
7178
  }
7083
- await queueEnvNeat(serviceDir, pkg, generatedFiles);
7179
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
7084
7180
  const empty = dependencyEdits.length === 0 && generatedFiles.length === 0;
7085
7181
  if (empty) {
7086
7182
  return {
@@ -7111,7 +7207,7 @@ async function findAstroMiddleware(serviceDir) {
7111
7207
  }
7112
7208
  return null;
7113
7209
  }
7114
- async function planAstro(serviceDir, pkg, manifestPath) {
7210
+ async function planAstro(serviceDir, pkg, manifestPath, project) {
7115
7211
  const useTs = await isTypeScriptProject(serviceDir);
7116
7212
  const otelInitFile = import_node_path40.default.join(
7117
7213
  serviceDir,
@@ -7129,7 +7225,7 @@ async function planAstro(serviceDir, pkg, manifestPath) {
7129
7225
  skipIfExists: true
7130
7226
  });
7131
7227
  }
7132
- await queueEnvNeat(serviceDir, pkg, generatedFiles);
7228
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
7133
7229
  if (existingMiddleware === null) {
7134
7230
  generatedFiles.push({
7135
7231
  file: middlewareFile,
@@ -7173,9 +7269,10 @@ async function planAstro(serviceDir, pkg, manifestPath) {
7173
7269
  framework: "astro"
7174
7270
  };
7175
7271
  }
7176
- async function plan(serviceDir) {
7272
+ async function plan(serviceDir, opts) {
7177
7273
  const pkg = await readPackageJson(serviceDir);
7178
7274
  const manifestPath = import_node_path40.default.join(serviceDir, "package.json");
7275
+ const project = opts?.project;
7179
7276
  const empty = {
7180
7277
  language: "javascript",
7181
7278
  serviceDir,
@@ -7188,32 +7285,32 @@ async function plan(serviceDir) {
7188
7285
  if (hasNextDependency(pkg)) {
7189
7286
  const nextConfig = await findNextConfig(serviceDir);
7190
7287
  if (nextConfig) {
7191
- return planNext(serviceDir, pkg, manifestPath, nextConfig);
7288
+ return planNext(serviceDir, pkg, manifestPath, nextConfig, project);
7192
7289
  }
7193
7290
  }
7194
7291
  if (hasRemixDependency(pkg)) {
7195
7292
  const remixEntry = await findRemixEntry(serviceDir);
7196
7293
  if (remixEntry) {
7197
- return planRemix(serviceDir, pkg, manifestPath, remixEntry);
7294
+ return planRemix(serviceDir, pkg, manifestPath, remixEntry, project);
7198
7295
  }
7199
7296
  }
7200
7297
  if (hasSvelteKitDependency(pkg)) {
7201
7298
  const hooks = await findSvelteKitHooks(serviceDir);
7202
7299
  const config = await findSvelteKitConfig(serviceDir);
7203
7300
  if (hooks || config) {
7204
- return planSvelteKit(serviceDir, pkg, manifestPath, hooks);
7301
+ return planSvelteKit(serviceDir, pkg, manifestPath, hooks, project);
7205
7302
  }
7206
7303
  }
7207
7304
  if (hasNuxtDependency(pkg)) {
7208
7305
  const nuxtConfig = await findNuxtConfig(serviceDir);
7209
7306
  if (nuxtConfig) {
7210
- return planNuxt(serviceDir, pkg, manifestPath);
7307
+ return planNuxt(serviceDir, pkg, manifestPath, project);
7211
7308
  }
7212
7309
  }
7213
7310
  if (hasAstroDependency(pkg)) {
7214
7311
  const astroConfig = await findAstroConfig(serviceDir);
7215
7312
  if (astroConfig) {
7216
- return planAstro(serviceDir, pkg, manifestPath);
7313
+ return planAstro(serviceDir, pkg, manifestPath, project);
7217
7314
  }
7218
7315
  }
7219
7316
  const entryFile = await resolveEntry(serviceDir, pkg);
@@ -7261,7 +7358,7 @@ async function plan(serviceDir) {
7261
7358
  if (!await exists2(envNeatFile)) {
7262
7359
  generatedFiles.push({
7263
7360
  file: envNeatFile,
7264
- contents: renderEnvNeat(pkg.name ?? import_node_path40.default.basename(serviceDir)),
7361
+ contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
7265
7362
  skipIfExists: true
7266
7363
  });
7267
7364
  }
@@ -7286,9 +7383,13 @@ function isAllowedWritePath(serviceDir, target) {
7286
7383
  if (base === "package.json") return true;
7287
7384
  if (base === ".env.neat") return true;
7288
7385
  if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
7289
- if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
7290
- if (/^next\.config\.(?:js|mjs|ts)$/.test(base)) return true;
7291
7386
  const relPosix = rel.split(import_node_path40.default.sep).join("/");
7387
+ if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) {
7388
+ if (relPosix === base) return true;
7389
+ if (relPosix === `src/${base}`) return true;
7390
+ return false;
7391
+ }
7392
+ if (/^next\.config\.(?:js|mjs|ts)$/.test(base)) return true;
7292
7393
  if (relPosix === "app/otel.server.ts" || relPosix === "app/otel.server.js") return true;
7293
7394
  if (/^app\/entry\.server\.(?:tsx?|jsx?)$/.test(relPosix)) return true;
7294
7395
  if (relPosix === "src/otel-init.ts" || relPosix === "src/otel-init.js") return true;
@@ -7796,14 +7897,14 @@ async function extractAndPersist(opts) {
7796
7897
  errorsPath: projectPaths.errorsPath
7797
7898
  };
7798
7899
  }
7799
- async function applyInstallersOver(services) {
7900
+ async function applyInstallersOver(services, project) {
7800
7901
  let instrumented = 0;
7801
7902
  let already = 0;
7802
7903
  let libOnly = 0;
7803
7904
  for (const svc of services) {
7804
7905
  const installer = await pickInstaller(svc.dir);
7805
7906
  if (!installer) continue;
7806
- const plan3 = await installer.plan(svc.dir);
7907
+ const plan3 = await installer.plan(svc.dir, { project });
7807
7908
  if (isEmptyPlan(plan3) && !plan3.libOnly) {
7808
7909
  already++;
7809
7910
  continue;
@@ -7825,28 +7926,106 @@ async function promptYesNo(question) {
7825
7926
  });
7826
7927
  });
7827
7928
  }
7828
- var DEFAULT_DAEMON_READY_TIMEOUT_MS = 15e3;
7829
- async function checkDaemonHealth(restPort) {
7929
+ var DEFAULT_DAEMON_READY_TIMEOUT_MS = 6e4;
7930
+ var PROBE_INTERVAL_MS = 500;
7931
+ async function fetchDaemonHealth(restPort) {
7830
7932
  return new Promise((resolve) => {
7831
7933
  const req = import_node_http.default.get(`http://127.0.0.1:${restPort}/health`, (res) => {
7832
7934
  const ok = res.statusCode !== void 0 && res.statusCode >= 200 && res.statusCode < 300;
7833
- res.resume();
7834
- resolve(ok);
7935
+ if (!ok) {
7936
+ res.resume();
7937
+ resolve(null);
7938
+ return;
7939
+ }
7940
+ let body = "";
7941
+ res.setEncoding("utf8");
7942
+ res.on("data", (chunk) => {
7943
+ body += chunk;
7944
+ });
7945
+ res.on("end", () => {
7946
+ try {
7947
+ resolve(JSON.parse(body));
7948
+ } catch {
7949
+ resolve({ ok: true });
7950
+ }
7951
+ });
7835
7952
  });
7836
- req.on("error", () => resolve(false));
7953
+ req.on("error", () => resolve(null));
7837
7954
  req.setTimeout(1e3, () => {
7838
7955
  req.destroy();
7839
- resolve(false);
7956
+ resolve(null);
7957
+ });
7958
+ });
7959
+ }
7960
+ async function checkDaemonHealth(restPort) {
7961
+ const body = await fetchDaemonHealth(restPort);
7962
+ return body !== null;
7963
+ }
7964
+ async function probeProjectHealth(restPort, name) {
7965
+ return new Promise((resolve) => {
7966
+ const req = import_node_http.default.get(
7967
+ `http://127.0.0.1:${restPort}/projects/${encodeURIComponent(name)}/health`,
7968
+ (res) => {
7969
+ const code = res.statusCode ?? 0;
7970
+ res.resume();
7971
+ if (code >= 200 && code < 300) resolve("active");
7972
+ else resolve("bootstrapping");
7973
+ }
7974
+ );
7975
+ req.on("error", () => resolve("bootstrapping"));
7976
+ req.setTimeout(1e3, () => {
7977
+ req.destroy();
7978
+ resolve("bootstrapping");
7840
7979
  });
7841
7980
  });
7842
7981
  }
7982
+ async function snapshotProjectStatus(restPort, body) {
7983
+ if (body.projects && body.projects.length > 0) {
7984
+ return body.projects.map((p) => ({
7985
+ name: p.name,
7986
+ status: p.status ?? "active"
7987
+ }));
7988
+ }
7989
+ const entries = await listProjects().catch(() => []);
7990
+ if (entries.length === 0) return [];
7991
+ return Promise.all(
7992
+ entries.map(async (entry2) => ({
7993
+ name: entry2.name,
7994
+ status: await probeProjectHealth(restPort, entry2.name)
7995
+ }))
7996
+ );
7997
+ }
7843
7998
  async function waitForDaemonReady(restPort, timeoutMs) {
7844
7999
  const deadline = Date.now() + timeoutMs;
8000
+ let lastBootstrapping = [];
7845
8001
  while (Date.now() < deadline) {
7846
- if (await checkDaemonHealth(restPort)) return true;
7847
- await new Promise((r) => setTimeout(r, 300));
8002
+ const body = await fetchDaemonHealth(restPort);
8003
+ if (body !== null) {
8004
+ const projects2 = await snapshotProjectStatus(restPort, body);
8005
+ const bootstrapping = projects2.filter((p) => p.status === "bootstrapping").map((p) => p.name);
8006
+ const broken = projects2.filter((p) => p.status === "broken").map((p) => p.name);
8007
+ if (bootstrapping.length === 0) {
8008
+ return { ready: true, brokenProjects: broken, stillBootstrapping: [] };
8009
+ }
8010
+ const key = bootstrapping.slice().sort().join(",");
8011
+ const prevKey = lastBootstrapping.slice().sort().join(",");
8012
+ if (key !== prevKey) {
8013
+ const plural = bootstrapping.length === 1 ? "" : "s";
8014
+ console.log(
8015
+ `neat: waiting on ${bootstrapping.length} project${plural}: ${bootstrapping.join(", ")}`
8016
+ );
8017
+ lastBootstrapping = bootstrapping;
8018
+ }
8019
+ }
8020
+ await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
7848
8021
  }
7849
- return false;
8022
+ const final = await fetchDaemonHealth(restPort);
8023
+ const projects = final ? await snapshotProjectStatus(restPort, final) : [];
8024
+ return {
8025
+ ready: false,
8026
+ brokenProjects: projects.filter((p) => p.status === "broken").map((p) => p.name),
8027
+ stillBootstrapping: projects.filter((p) => p.status === "bootstrapping").map((p) => p.name)
8028
+ };
7850
8029
  }
7851
8030
  function spawnDaemonDetached() {
7852
8031
  const here = import_node_path42.default.dirname(new URL(importMetaUrl).pathname);
@@ -7867,12 +8046,21 @@ function spawnDaemonDetached() {
7867
8046
  if (!entry2) {
7868
8047
  throw new Error(`orchestrator: cannot locate neatd entry in ${here}`);
7869
8048
  }
8049
+ const env = { ...process.env };
8050
+ const hasToken = typeof env.NEAT_AUTH_TOKEN === "string" && env.NEAT_AUTH_TOKEN.length > 0;
8051
+ if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
8052
+ env.HOST = "127.0.0.1";
8053
+ }
7870
8054
  const child = (0, import_node_child_process2.spawn)(process.execPath, [entry2, "start"], {
7871
8055
  detached: true,
7872
- stdio: "ignore",
7873
- env: process.env
8056
+ // stderr inherits the orchestrator's fd so the daemon's
8057
+ // `BindAuthorityError` message lands in front of the operator instead
8058
+ // of being swallowed (issue #341).
8059
+ stdio: ["ignore", "ignore", "inherit"],
8060
+ env
7874
8061
  });
7875
8062
  child.unref();
8063
+ return child;
7876
8064
  }
7877
8065
  function openBrowser(url) {
7878
8066
  if (!process.stdout.isTTY) return "failed";
@@ -7948,7 +8136,7 @@ async function runOrchestrator(opts) {
7948
8136
  result.steps.apply.skipped = true;
7949
8137
  console.log("skipped instrumentation (--no-instrument)");
7950
8138
  } else {
7951
- const tally = await applyInstallersOver(services);
8139
+ const tally = await applyInstallersOver(services, opts.project);
7952
8140
  result.steps.apply = { ...tally, skipped: false };
7953
8141
  console.log(
7954
8142
  `instrumented ${tally.instrumented}, already ${tally.alreadyInstrumented}, lib-only ${tally.libOnly}`
@@ -7967,12 +8155,25 @@ async function runOrchestrator(opts) {
7967
8155
  return result;
7968
8156
  }
7969
8157
  const ready = await waitForDaemonReady(restPort, timeoutMs);
7970
- result.steps.daemon = ready ? "spawned" : "timed-out";
7971
- if (!ready) {
8158
+ result.steps.daemon = ready.ready ? "spawned" : "timed-out";
8159
+ if (!ready.ready) {
7972
8160
  console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
8161
+ if (ready.stillBootstrapping.length > 0) {
8162
+ console.error(
8163
+ `neat: still bootstrapping: ${ready.stillBootstrapping.join(", ")}`
8164
+ );
8165
+ }
8166
+ if (ready.brokenProjects.length > 0) {
8167
+ console.error(`neat: broken projects: ${ready.brokenProjects.join(", ")}`);
8168
+ }
7973
8169
  result.exitCode = 1;
7974
8170
  return result;
7975
8171
  }
8172
+ if (ready.brokenProjects.length > 0) {
8173
+ console.warn(
8174
+ `neat: ${ready.brokenProjects.length} project(s) reported broken: ${ready.brokenProjects.join(", ")}`
8175
+ );
8176
+ }
7976
8177
  }
7977
8178
  const dashboardUrl = opts.dashboardUrl ?? "http://localhost:6328";
7978
8179
  if (opts.noOpen || !process.stdout.isTTY) {
@@ -8603,7 +8804,7 @@ async function runSync(opts) {
8603
8804
  snapshotPath = target;
8604
8805
  }
8605
8806
  const skipApply = opts.dryRun || opts.noInstrument;
8606
- const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services);
8807
+ const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services, entry2.name);
8607
8808
  const warnings = [];
8608
8809
  let daemonState = "skipped";
8609
8810
  let exitCode = 0;
@@ -8699,6 +8900,8 @@ function usage() {
8699
8900
  console.log(" uninstall <name>");
8700
8901
  console.log(" Remove a project from the registry. Does not touch");
8701
8902
  console.log(" neat-out/, policy.json, or any user file.");
8903
+ console.log(" version Print the installed @neat.is/core version and exit.");
8904
+ console.log(" Aliases: --version, -v.");
8702
8905
  console.log(" skill Install or print the Claude Code MCP drop-in.");
8703
8906
  console.log(" Flags:");
8704
8907
  console.log(" --print-config print the JSON snippet to stdout");
@@ -8825,7 +9028,7 @@ function parseArgs(rest) {
8825
9028
  out.yes = true;
8826
9029
  continue;
8827
9030
  }
8828
- if (arg === "--verbose" || arg === "-v") {
9031
+ if (arg === "--verbose") {
8829
9032
  out.verbose = true;
8830
9033
  continue;
8831
9034
  }
@@ -8884,6 +9087,28 @@ function assignFlag(out, field, value) {
8884
9087
  ;
8885
9088
  out[field] = value;
8886
9089
  }
9090
+ function readPackageVersion() {
9091
+ const here = typeof __dirname !== "undefined" ? __dirname : import_node_path44.default.dirname((0, import_node_url3.fileURLToPath)(importMetaUrl));
9092
+ const candidates = [
9093
+ import_node_path44.default.resolve(here, "../package.json"),
9094
+ import_node_path44.default.resolve(here, "../../package.json")
9095
+ ];
9096
+ for (const candidate of candidates) {
9097
+ try {
9098
+ const raw = (0, import_node_fs28.readFileSync)(candidate, "utf8");
9099
+ const parsed = JSON.parse(raw);
9100
+ if (parsed.name === "@neat.is/core" && typeof parsed.version === "string") {
9101
+ return parsed.version;
9102
+ }
9103
+ } catch {
9104
+ }
9105
+ }
9106
+ return "unknown";
9107
+ }
9108
+ function printVersion() {
9109
+ process.stdout.write(`${readPackageVersion()}
9110
+ `);
9111
+ }
8887
9112
  function printBanner() {
8888
9113
  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
9114
  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 +9118,7 @@ function printBanner() {
8893
9118
  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
9119
  console.log("");
8895
9120
  console.log(" Network Expressive Architecting Tool");
8896
- console.log(" neat.is \xB7 v0.2.5 \xB7 BSL 1.1");
9121
+ console.log(" neat.is \xB7 v0.4.0 \xB7 Apache 2.0");
8897
9122
  console.log("");
8898
9123
  }
8899
9124
  function printDiscoveryReport(opts, services) {
@@ -8921,12 +9146,12 @@ function printDiscoveryReport(opts, services) {
8921
9146
  }
8922
9147
  console.log("");
8923
9148
  }
8924
- async function buildPatchSections(services) {
9149
+ async function buildPatchSections(services, project) {
8925
9150
  const sections = [];
8926
9151
  for (const svc of services) {
8927
9152
  const installer = await pickInstaller(svc.dir);
8928
9153
  if (!installer) continue;
8929
- const plan3 = await installer.plan(svc.dir);
9154
+ const plan3 = await installer.plan(svc.dir, { project });
8930
9155
  if (isEmptyPlan(plan3) && !plan3.libOnly) continue;
8931
9156
  sections.push({ installer: installer.name, plan: plan3 });
8932
9157
  }
@@ -8941,7 +9166,7 @@ async function runInit(opts) {
8941
9166
  }
8942
9167
  const services = await discoverServices(opts.scanPath);
8943
9168
  printDiscoveryReport(opts, services);
8944
- const sections = opts.noInstall ? [] : await buildPatchSections(services);
9169
+ const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
8945
9170
  const patch = renderPatch(sections);
8946
9171
  const patchPath = import_node_path44.default.join(opts.scanPath, "neat.patch");
8947
9172
  if (opts.dryRun) {
@@ -9099,6 +9324,10 @@ async function main() {
9099
9324
  usage();
9100
9325
  process.exit(0);
9101
9326
  }
9327
+ if (cmd === "--version" || cmd === "-v" || cmd === "version") {
9328
+ printVersion();
9329
+ process.exit(0);
9330
+ }
9102
9331
  const parsed = parseArgs(rest);
9103
9332
  const { positional, apply: apply3, dryRun, noInstall } = parsed;
9104
9333
  const project = parsed.project ?? DEFAULT_PROJECT;
@@ -9494,7 +9723,7 @@ async function runQueryVerb(cmd, parsed) {
9494
9723
  }
9495
9724
  }
9496
9725
  var entry = process.argv[1] ?? "";
9497
- if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsWith("/neat")) {
9726
+ if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsWith("/neat") || entry.endsWith("/neat.is")) {
9498
9727
  main().catch((err) => {
9499
9728
  console.error(err);
9500
9729
  process.exit(1);
@@ -9505,6 +9734,7 @@ if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsW
9505
9734
  CLAUDE_SKILL_CONFIG,
9506
9735
  QUERY_VERBS,
9507
9736
  parseArgs,
9737
+ readPackageVersion,
9508
9738
  runInit,
9509
9739
  runQueryVerb,
9510
9740
  runSkill