@neat.is/core 0.3.8 → 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.js CHANGED
@@ -31,6 +31,7 @@ import {
31
31
  loadPolicyFile,
32
32
  makeErrorSpanWriter,
33
33
  makeSpanHandler,
34
+ normalizeProjectPath,
34
35
  pathsForProject,
35
36
  promoteFrontierNodes,
36
37
  removeProject,
@@ -40,18 +41,20 @@ import {
40
41
  setStatus,
41
42
  startPersistLoop,
42
43
  startStalenessLoop
43
- } from "./chunk-CZ3T6TE2.js";
44
+ } from "./chunk-NTQHMXWE.js";
44
45
  import {
45
46
  startOtelGrpcReceiver
46
- } from "./chunk-V4TU7OKZ.js";
47
+ } from "./chunk-D5PIJFBE.js";
47
48
  import {
49
+ __dirname,
48
50
  __require,
49
51
  buildOtelReceiver
50
- } from "./chunk-7TYESDAI.js";
52
+ } from "./chunk-KYRIQIPG.js";
51
53
 
52
54
  // src/cli.ts
53
- import path7 from "path";
54
- import { promises as fs7 } from "fs";
55
+ import path8 from "path";
56
+ import { promises as fs7, readFileSync } from "fs";
57
+ import { fileURLToPath } from "url";
55
58
 
56
59
  // src/gitignore.ts
57
60
  import { promises as fs } from "fs";
@@ -271,6 +274,14 @@ var IGNORED_WATCH_GLOBS = [
271
274
  "**/.turbo/**",
272
275
  "**/.next/**",
273
276
  "**/neat-out/**",
277
+ // Python venv shapes (issue #344). chokidar opens one watch handle per
278
+ // descended dir; a CPython venv carries 20k+ files and trivially blows
279
+ // through the macOS kqueue cap before extraction even runs.
280
+ "**/.venv/**",
281
+ "**/venv/**",
282
+ "**/__pypackages__/**",
283
+ "**/.tox/**",
284
+ "**/site-packages/**",
274
285
  "**/.DS_Store"
275
286
  ];
276
287
  var IGNORED_WATCH_PATHS = [
@@ -281,6 +292,11 @@ var IGNORED_WATCH_PATHS = [
281
292
  /(?:^|[\\/])\.turbo[\\/]/,
282
293
  /(?:^|[\\/])\.next[\\/]/,
283
294
  /(?:^|[\\/])neat-out[\\/]/,
295
+ /(?:^|[\\/])\.venv[\\/]/,
296
+ /(?:^|[\\/])venv[\\/]/,
297
+ /(?:^|[\\/])__pypackages__[\\/]/,
298
+ /(?:^|[\\/])\.tox[\\/]/,
299
+ /(?:^|[\\/])site-packages[\\/]/,
284
300
  /[\\/]?\.DS_Store$/
285
301
  ];
286
302
  function shouldIgnore(absPath) {
@@ -712,10 +728,10 @@ dotenv.config({ path: path.join(here, '.env.neat') })
712
728
 
713
729
  await import('@opentelemetry/auto-instrumentations-node/register')
714
730
  `;
715
- function renderEnvNeat(serviceName) {
731
+ function renderEnvNeat(projectName) {
716
732
  return [
717
733
  "# Generated by `neat init --apply` (ADR-069).",
718
- `OTEL_SERVICE_NAME=${serviceName}`,
734
+ `OTEL_SERVICE_NAME=${projectName}`,
719
735
  "OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318",
720
736
  ""
721
737
  ].join("\n");
@@ -771,6 +787,100 @@ const sdk = new NodeSDK({
771
787
  })
772
788
  sdk.start()
773
789
  `;
790
+ var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
791
+ var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
792
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
793
+ // the OTel SDK starts so the exporter target and service name are in scope.
794
+ import { fileURLToPath } from 'node:url'
795
+ import path from 'node:path'
796
+ import dotenv from 'dotenv'
797
+ import { NodeSDK } from '@opentelemetry/sdk-node'
798
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
799
+
800
+ const here = path.dirname(fileURLToPath(import.meta.url))
801
+ dotenv.config({ path: path.join(here, '.env.neat') })
802
+
803
+ const sdk = new NodeSDK({
804
+ serviceName: process.env.OTEL_SERVICE_NAME,
805
+ instrumentations: [getNodeAutoInstrumentations()],
806
+ })
807
+ sdk.start()
808
+ `;
809
+ var FRAMEWORK_OTEL_INIT_JS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
810
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
811
+ // the OTel SDK starts so the exporter target and service name are in scope.
812
+ const path = require('node:path')
813
+ try {
814
+ require('dotenv').config({ path: path.join(__dirname, '.env.neat') })
815
+ } catch (err) {
816
+ // dotenv unavailable \u2014 fall through to process.env.
817
+ }
818
+ const { NodeSDK } = require('@opentelemetry/sdk-node')
819
+ const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
820
+
821
+ const sdk = new NodeSDK({
822
+ serviceName: process.env.OTEL_SERVICE_NAME,
823
+ instrumentations: [getNodeAutoInstrumentations()],
824
+ })
825
+ sdk.start()
826
+ `;
827
+ var REMIX_OTEL_SERVER_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
828
+ var REMIX_OTEL_SERVER_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
829
+ var SVELTEKIT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
830
+ var SVELTEKIT_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
831
+ var SVELTEKIT_HOOKS_SERVER_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
832
+ import './otel-init'
833
+
834
+ import type { Handle } from '@sveltejs/kit'
835
+
836
+ export const handle: Handle = async ({ event, resolve }) => {
837
+ return resolve(event)
838
+ }
839
+ `;
840
+ var SVELTEKIT_HOOKS_SERVER_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
841
+ import './otel-init'
842
+
843
+ /** @type {import('@sveltejs/kit').Handle} */
844
+ export const handle = async ({ event, resolve }) => {
845
+ return resolve(event)
846
+ }
847
+ `;
848
+ var NUXT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
849
+ var NUXT_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
850
+ var NUXT_OTEL_PLUGIN_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
851
+ import './otel-init'
852
+
853
+ export default defineNitroPlugin(() => {
854
+ // OTel SDK is initialised at module-load via the side-effect import above.
855
+ })
856
+ `;
857
+ var NUXT_OTEL_PLUGIN_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
858
+ require('./otel-init')
859
+
860
+ module.exports = defineNitroPlugin(() => {
861
+ // OTel SDK is initialised at module-load via the side-effect import above.
862
+ })
863
+ `;
864
+ var ASTRO_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
865
+ var ASTRO_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
866
+ var ASTRO_MIDDLEWARE_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
867
+ import './otel-init'
868
+
869
+ import { defineMiddleware } from 'astro:middleware'
870
+
871
+ export const onRequest = defineMiddleware(async (context, next) => {
872
+ return next()
873
+ })
874
+ `;
875
+ var ASTRO_MIDDLEWARE_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
876
+ import './otel-init'
877
+
878
+ import { defineMiddleware } from 'astro:middleware'
879
+
880
+ export const onRequest = defineMiddleware(async (context, next) => {
881
+ return next()
882
+ })
883
+ `;
774
884
 
775
885
  // src/installers/javascript.ts
776
886
  var SDK_PACKAGES = [
@@ -791,6 +901,10 @@ var OTEL_ENV = {
791
901
  key: "OTEL_EXPORTER_OTLP_ENDPOINT",
792
902
  value: "http://localhost:4318"
793
903
  };
904
+ function envServiceName(pkg, serviceDir, project) {
905
+ if (project && project.length > 0) return project;
906
+ return pkg.name ?? path4.basename(serviceDir);
907
+ }
794
908
  async function readPackageJson(serviceDir) {
795
909
  try {
796
910
  const raw = await fs4.readFile(path4.join(serviceDir, "package.json"), "utf8");
@@ -822,6 +936,71 @@ async function findNextConfig(serviceDir) {
822
936
  function hasNextDependency(pkg) {
823
937
  return pkg.dependencies?.next !== void 0 || pkg.devDependencies?.next !== void 0;
824
938
  }
939
+ function allDeps(pkg) {
940
+ return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
941
+ }
942
+ var REMIX_ENTRY_CANDIDATES = [
943
+ "app/entry.server.ts",
944
+ "app/entry.server.tsx",
945
+ "app/entry.server.js",
946
+ "app/entry.server.jsx"
947
+ ];
948
+ function hasRemixDependency(pkg) {
949
+ const deps = allDeps(pkg);
950
+ if ("remix" in deps) return true;
951
+ for (const name of Object.keys(deps)) {
952
+ if (name.startsWith("@remix-run/")) return true;
953
+ }
954
+ return false;
955
+ }
956
+ async function findRemixEntry(serviceDir) {
957
+ for (const rel of REMIX_ENTRY_CANDIDATES) {
958
+ const candidate = path4.join(serviceDir, rel);
959
+ if (await exists(candidate)) return candidate;
960
+ }
961
+ return null;
962
+ }
963
+ var SVELTEKIT_HOOKS_CANDIDATES = ["src/hooks.server.ts", "src/hooks.server.js"];
964
+ var SVELTEKIT_CONFIG_CANDIDATES = ["svelte.config.js", "svelte.config.ts"];
965
+ function hasSvelteKitDependency(pkg) {
966
+ return "@sveltejs/kit" in allDeps(pkg);
967
+ }
968
+ async function findSvelteKitHooks(serviceDir) {
969
+ for (const rel of SVELTEKIT_HOOKS_CANDIDATES) {
970
+ const candidate = path4.join(serviceDir, rel);
971
+ if (await exists(candidate)) return candidate;
972
+ }
973
+ return null;
974
+ }
975
+ async function findSvelteKitConfig(serviceDir) {
976
+ for (const rel of SVELTEKIT_CONFIG_CANDIDATES) {
977
+ const candidate = path4.join(serviceDir, rel);
978
+ if (await exists(candidate)) return candidate;
979
+ }
980
+ return null;
981
+ }
982
+ var NUXT_CONFIG_CANDIDATES = ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"];
983
+ function hasNuxtDependency(pkg) {
984
+ return "nuxt" in allDeps(pkg);
985
+ }
986
+ async function findNuxtConfig(serviceDir) {
987
+ for (const name of NUXT_CONFIG_CANDIDATES) {
988
+ const candidate = path4.join(serviceDir, name);
989
+ if (await exists(candidate)) return candidate;
990
+ }
991
+ return null;
992
+ }
993
+ var ASTRO_CONFIG_CANDIDATES = ["astro.config.mjs", "astro.config.ts", "astro.config.js"];
994
+ function hasAstroDependency(pkg) {
995
+ return "astro" in allDeps(pkg);
996
+ }
997
+ async function findAstroConfig(serviceDir) {
998
+ for (const name of ASTRO_CONFIG_CANDIDATES) {
999
+ const candidate = path4.join(serviceDir, name);
1000
+ if (await exists(candidate)) return candidate;
1001
+ }
1002
+ return null;
1003
+ }
825
1004
  function parseNextMajor(range) {
826
1005
  if (!range) return null;
827
1006
  const cleaned = range.trim().replace(/^[\^~>=<\s]+/, "");
@@ -950,7 +1129,7 @@ function lineIsOtelInjection(line) {
950
1129
  if (trimmed.length === 0) return false;
951
1130
  return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
952
1131
  }
953
- async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
1132
+ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project) {
954
1133
  const useTs = await isTypeScriptProject(serviceDir);
955
1134
  const instrumentationFile = path4.join(serviceDir, useTs ? "instrumentation.ts" : "instrumentation.js");
956
1135
  const instrumentationNodeFile = path4.join(
@@ -987,7 +1166,7 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
987
1166
  if (!await exists(envNeatFile)) {
988
1167
  generatedFiles.push({
989
1168
  file: envNeatFile,
990
- contents: renderEnvNeat(pkg.name ?? path4.basename(serviceDir)),
1169
+ contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
991
1170
  skipIfExists: true
992
1171
  });
993
1172
  }
@@ -1029,9 +1208,280 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
1029
1208
  ...nextConfigEdit ? { nextConfigEdit } : {}
1030
1209
  };
1031
1210
  }
1032
- async function plan(serviceDir) {
1211
+ function buildDependencyEdits(pkg, manifestPath) {
1212
+ const existingDeps = allDeps(pkg);
1213
+ const edits = [];
1214
+ for (const sdk of SDK_PACKAGES) {
1215
+ if (sdk.name in existingDeps) continue;
1216
+ edits.push({
1217
+ file: manifestPath,
1218
+ kind: "add",
1219
+ name: sdk.name,
1220
+ version: sdk.version
1221
+ });
1222
+ }
1223
+ return edits;
1224
+ }
1225
+ async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
1226
+ const envNeatFile = path4.join(serviceDir, ".env.neat");
1227
+ if (!await exists(envNeatFile)) {
1228
+ generatedFiles.push({
1229
+ file: envNeatFile,
1230
+ contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
1231
+ skipIfExists: true
1232
+ });
1233
+ }
1234
+ }
1235
+ function fileImportsOtelHook(raw, specifiers) {
1236
+ const lines = raw.split(/\r?\n/);
1237
+ for (const line of lines) {
1238
+ const trimmed = line.trim();
1239
+ for (const spec of specifiers) {
1240
+ const escaped = spec.replace(/\./g, "\\.");
1241
+ const pattern = new RegExp(
1242
+ `(?:import\\s+['"]${escaped}['"]|require\\(['"]${escaped}['"]\\))`
1243
+ );
1244
+ if (pattern.test(trimmed)) return true;
1245
+ }
1246
+ }
1247
+ return false;
1248
+ }
1249
+ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
1250
+ const useTs = await isTypeScriptProject(serviceDir);
1251
+ const otelServerFile = path4.join(
1252
+ serviceDir,
1253
+ useTs ? "app/otel.server.ts" : "app/otel.server.js"
1254
+ );
1255
+ const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
1256
+ const generatedFiles = [];
1257
+ if (!await exists(otelServerFile)) {
1258
+ generatedFiles.push({
1259
+ file: otelServerFile,
1260
+ contents: useTs ? REMIX_OTEL_SERVER_TS : REMIX_OTEL_SERVER_JS,
1261
+ skipIfExists: true
1262
+ });
1263
+ }
1264
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
1265
+ const entrypointEdits = [];
1266
+ try {
1267
+ const raw = await fs4.readFile(entryFile, "utf8");
1268
+ if (!fileImportsOtelHook(raw, ["./otel.server"])) {
1269
+ const lines = raw.split(/\r?\n/);
1270
+ const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
1271
+ entrypointEdits.push({
1272
+ file: entryFile,
1273
+ before: firstReal,
1274
+ after: "import './otel.server'"
1275
+ });
1276
+ }
1277
+ } catch {
1278
+ }
1279
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
1280
+ if (empty) {
1281
+ return {
1282
+ language: "javascript",
1283
+ serviceDir,
1284
+ dependencyEdits: [],
1285
+ entrypointEdits: [],
1286
+ envEdits: [],
1287
+ generatedFiles: [],
1288
+ framework: "remix"
1289
+ };
1290
+ }
1291
+ return {
1292
+ language: "javascript",
1293
+ serviceDir,
1294
+ dependencyEdits,
1295
+ entrypointEdits,
1296
+ envEdits: [OTEL_ENV],
1297
+ generatedFiles,
1298
+ framework: "remix"
1299
+ };
1300
+ }
1301
+ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project) {
1302
+ const useTs = await isTypeScriptProject(serviceDir);
1303
+ const otelInitFile = path4.join(
1304
+ serviceDir,
1305
+ useTs ? "src/otel-init.ts" : "src/otel-init.js"
1306
+ );
1307
+ const resolvedHooksFile = hooksFile ?? path4.join(serviceDir, useTs ? "src/hooks.server.ts" : "src/hooks.server.js");
1308
+ const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
1309
+ const generatedFiles = [];
1310
+ const entrypointEdits = [];
1311
+ if (!await exists(otelInitFile)) {
1312
+ generatedFiles.push({
1313
+ file: otelInitFile,
1314
+ contents: useTs ? SVELTEKIT_OTEL_INIT_TS : SVELTEKIT_OTEL_INIT_JS,
1315
+ skipIfExists: true
1316
+ });
1317
+ }
1318
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
1319
+ if (hooksFile === null) {
1320
+ generatedFiles.push({
1321
+ file: resolvedHooksFile,
1322
+ contents: useTs ? SVELTEKIT_HOOKS_SERVER_TS : SVELTEKIT_HOOKS_SERVER_JS,
1323
+ skipIfExists: true
1324
+ });
1325
+ } else {
1326
+ try {
1327
+ const raw = await fs4.readFile(hooksFile, "utf8");
1328
+ if (!fileImportsOtelHook(raw, ["./otel-init"])) {
1329
+ const lines = raw.split(/\r?\n/);
1330
+ const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
1331
+ entrypointEdits.push({
1332
+ file: hooksFile,
1333
+ before: firstReal,
1334
+ after: "import './otel-init'"
1335
+ });
1336
+ }
1337
+ } catch {
1338
+ }
1339
+ }
1340
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
1341
+ if (empty) {
1342
+ return {
1343
+ language: "javascript",
1344
+ serviceDir,
1345
+ dependencyEdits: [],
1346
+ entrypointEdits: [],
1347
+ envEdits: [],
1348
+ generatedFiles: [],
1349
+ framework: "sveltekit"
1350
+ };
1351
+ }
1352
+ return {
1353
+ language: "javascript",
1354
+ serviceDir,
1355
+ dependencyEdits,
1356
+ entrypointEdits,
1357
+ envEdits: [OTEL_ENV],
1358
+ generatedFiles,
1359
+ framework: "sveltekit"
1360
+ };
1361
+ }
1362
+ async function planNuxt(serviceDir, pkg, manifestPath, project) {
1363
+ const useTs = await isTypeScriptProject(serviceDir);
1364
+ const otelPluginFile = path4.join(
1365
+ serviceDir,
1366
+ useTs ? "server/plugins/otel.ts" : "server/plugins/otel.js"
1367
+ );
1368
+ const otelInitFile = path4.join(
1369
+ serviceDir,
1370
+ useTs ? "server/plugins/otel-init.ts" : "server/plugins/otel-init.js"
1371
+ );
1372
+ const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
1373
+ const generatedFiles = [];
1374
+ if (!await exists(otelInitFile)) {
1375
+ generatedFiles.push({
1376
+ file: otelInitFile,
1377
+ contents: useTs ? NUXT_OTEL_INIT_TS : NUXT_OTEL_INIT_JS,
1378
+ skipIfExists: true
1379
+ });
1380
+ }
1381
+ if (!await exists(otelPluginFile)) {
1382
+ generatedFiles.push({
1383
+ file: otelPluginFile,
1384
+ contents: useTs ? NUXT_OTEL_PLUGIN_TS : NUXT_OTEL_PLUGIN_JS,
1385
+ skipIfExists: true
1386
+ });
1387
+ }
1388
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
1389
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0;
1390
+ if (empty) {
1391
+ return {
1392
+ language: "javascript",
1393
+ serviceDir,
1394
+ dependencyEdits: [],
1395
+ entrypointEdits: [],
1396
+ envEdits: [],
1397
+ generatedFiles: [],
1398
+ framework: "nuxt"
1399
+ };
1400
+ }
1401
+ return {
1402
+ language: "javascript",
1403
+ serviceDir,
1404
+ dependencyEdits,
1405
+ entrypointEdits: [],
1406
+ envEdits: [OTEL_ENV],
1407
+ generatedFiles,
1408
+ framework: "nuxt"
1409
+ };
1410
+ }
1411
+ var ASTRO_MIDDLEWARE_CANDIDATES = ["src/middleware.ts", "src/middleware.js"];
1412
+ async function findAstroMiddleware(serviceDir) {
1413
+ for (const rel of ASTRO_MIDDLEWARE_CANDIDATES) {
1414
+ const candidate = path4.join(serviceDir, rel);
1415
+ if (await exists(candidate)) return candidate;
1416
+ }
1417
+ return null;
1418
+ }
1419
+ async function planAstro(serviceDir, pkg, manifestPath, project) {
1420
+ const useTs = await isTypeScriptProject(serviceDir);
1421
+ const otelInitFile = path4.join(
1422
+ serviceDir,
1423
+ useTs ? "src/otel-init.ts" : "src/otel-init.js"
1424
+ );
1425
+ const existingMiddleware = await findAstroMiddleware(serviceDir);
1426
+ const middlewareFile = existingMiddleware ?? path4.join(serviceDir, useTs ? "src/middleware.ts" : "src/middleware.js");
1427
+ const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
1428
+ const generatedFiles = [];
1429
+ const entrypointEdits = [];
1430
+ if (!await exists(otelInitFile)) {
1431
+ generatedFiles.push({
1432
+ file: otelInitFile,
1433
+ contents: useTs ? ASTRO_OTEL_INIT_TS : ASTRO_OTEL_INIT_JS,
1434
+ skipIfExists: true
1435
+ });
1436
+ }
1437
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
1438
+ if (existingMiddleware === null) {
1439
+ generatedFiles.push({
1440
+ file: middlewareFile,
1441
+ contents: useTs ? ASTRO_MIDDLEWARE_TS : ASTRO_MIDDLEWARE_JS,
1442
+ skipIfExists: true
1443
+ });
1444
+ } else {
1445
+ try {
1446
+ const raw = await fs4.readFile(existingMiddleware, "utf8");
1447
+ if (!fileImportsOtelHook(raw, ["./otel-init"])) {
1448
+ const lines = raw.split(/\r?\n/);
1449
+ const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
1450
+ entrypointEdits.push({
1451
+ file: existingMiddleware,
1452
+ before: firstReal,
1453
+ after: "import './otel-init'"
1454
+ });
1455
+ }
1456
+ } catch {
1457
+ }
1458
+ }
1459
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
1460
+ if (empty) {
1461
+ return {
1462
+ language: "javascript",
1463
+ serviceDir,
1464
+ dependencyEdits: [],
1465
+ entrypointEdits: [],
1466
+ envEdits: [],
1467
+ generatedFiles: [],
1468
+ framework: "astro"
1469
+ };
1470
+ }
1471
+ return {
1472
+ language: "javascript",
1473
+ serviceDir,
1474
+ dependencyEdits,
1475
+ entrypointEdits,
1476
+ envEdits: [OTEL_ENV],
1477
+ generatedFiles,
1478
+ framework: "astro"
1479
+ };
1480
+ }
1481
+ async function plan(serviceDir, opts) {
1033
1482
  const pkg = await readPackageJson(serviceDir);
1034
1483
  const manifestPath = path4.join(serviceDir, "package.json");
1484
+ const project = opts?.project;
1035
1485
  const empty = {
1036
1486
  language: "javascript",
1037
1487
  serviceDir,
@@ -1044,7 +1494,32 @@ async function plan(serviceDir) {
1044
1494
  if (hasNextDependency(pkg)) {
1045
1495
  const nextConfig = await findNextConfig(serviceDir);
1046
1496
  if (nextConfig) {
1047
- return planNext(serviceDir, pkg, manifestPath, nextConfig);
1497
+ return planNext(serviceDir, pkg, manifestPath, nextConfig, project);
1498
+ }
1499
+ }
1500
+ if (hasRemixDependency(pkg)) {
1501
+ const remixEntry = await findRemixEntry(serviceDir);
1502
+ if (remixEntry) {
1503
+ return planRemix(serviceDir, pkg, manifestPath, remixEntry, project);
1504
+ }
1505
+ }
1506
+ if (hasSvelteKitDependency(pkg)) {
1507
+ const hooks = await findSvelteKitHooks(serviceDir);
1508
+ const config = await findSvelteKitConfig(serviceDir);
1509
+ if (hooks || config) {
1510
+ return planSvelteKit(serviceDir, pkg, manifestPath, hooks, project);
1511
+ }
1512
+ }
1513
+ if (hasNuxtDependency(pkg)) {
1514
+ const nuxtConfig = await findNuxtConfig(serviceDir);
1515
+ if (nuxtConfig) {
1516
+ return planNuxt(serviceDir, pkg, manifestPath, project);
1517
+ }
1518
+ }
1519
+ if (hasAstroDependency(pkg)) {
1520
+ const astroConfig = await findAstroConfig(serviceDir);
1521
+ if (astroConfig) {
1522
+ return planAstro(serviceDir, pkg, manifestPath, project);
1048
1523
  }
1049
1524
  }
1050
1525
  const entryFile = await resolveEntry(serviceDir, pkg);
@@ -1092,7 +1567,7 @@ async function plan(serviceDir) {
1092
1567
  if (!await exists(envNeatFile)) {
1093
1568
  generatedFiles.push({
1094
1569
  file: envNeatFile,
1095
- contents: renderEnvNeat(pkg.name ?? path4.basename(serviceDir)),
1570
+ contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
1096
1571
  skipIfExists: true
1097
1572
  });
1098
1573
  }
@@ -1119,9 +1594,18 @@ function isAllowedWritePath(serviceDir, target) {
1119
1594
  if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
1120
1595
  if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
1121
1596
  if (/^next\.config\.(?:js|mjs|ts)$/.test(base)) return true;
1597
+ const relPosix = rel.split(path4.sep).join("/");
1598
+ if (relPosix === "app/otel.server.ts" || relPosix === "app/otel.server.js") return true;
1599
+ if (/^app\/entry\.server\.(?:tsx?|jsx?)$/.test(relPosix)) return true;
1600
+ if (relPosix === "src/otel-init.ts" || relPosix === "src/otel-init.js") return true;
1601
+ if (relPosix === "src/hooks.server.ts" || relPosix === "src/hooks.server.js") return true;
1602
+ if (relPosix === "server/plugins/otel.ts" || relPosix === "server/plugins/otel.js") return true;
1603
+ if (relPosix === "server/plugins/otel-init.ts" || relPosix === "server/plugins/otel-init.js") return true;
1604
+ if (relPosix === "src/middleware.ts" || relPosix === "src/middleware.js") return true;
1122
1605
  return false;
1123
1606
  }
1124
1607
  async function writeAtomic(file, contents) {
1608
+ await fs4.mkdir(path4.dirname(file), { recursive: true });
1125
1609
  const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
1126
1610
  await fs4.writeFile(tmp, contents, "utf8");
1127
1611
  await fs4.rename(tmp, file);
@@ -1591,6 +2075,49 @@ import http from "http";
1591
2075
  import path6 from "path";
1592
2076
  import { spawn as spawn2 } from "child_process";
1593
2077
  import readline from "readline";
2078
+ async function extractAndPersist(opts) {
2079
+ const services = await discoverServices(opts.scanPath);
2080
+ const languages = [...new Set(services.map((s) => s.node.language))].sort();
2081
+ const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
2082
+ resetGraph(graphKey);
2083
+ const graph = getGraph(graphKey);
2084
+ const projectPaths = pathsForProject(graphKey, path6.join(opts.scanPath, "neat-out"));
2085
+ const extraction = await extractFromDirectory(graph, opts.scanPath, {
2086
+ errorsPath: projectPaths.errorsPath
2087
+ });
2088
+ if (!opts.dryRun) {
2089
+ await saveGraphToDisk(graph, projectPaths.snapshotPath);
2090
+ }
2091
+ return {
2092
+ graph,
2093
+ graphKey,
2094
+ services,
2095
+ languages,
2096
+ nodesAdded: extraction.nodesAdded,
2097
+ edgesAdded: extraction.edgesAdded,
2098
+ snapshotPath: projectPaths.snapshotPath,
2099
+ errorsPath: projectPaths.errorsPath
2100
+ };
2101
+ }
2102
+ async function applyInstallersOver(services, project) {
2103
+ let instrumented = 0;
2104
+ let already = 0;
2105
+ let libOnly = 0;
2106
+ for (const svc of services) {
2107
+ const installer = await pickInstaller(svc.dir);
2108
+ if (!installer) continue;
2109
+ const plan3 = await installer.plan(svc.dir, { project });
2110
+ if (isEmptyPlan(plan3) && !plan3.libOnly) {
2111
+ already++;
2112
+ continue;
2113
+ }
2114
+ const outcome = await installer.apply(plan3);
2115
+ if (outcome.outcome === "instrumented") instrumented++;
2116
+ else if (outcome.outcome === "already-instrumented") already++;
2117
+ else if (outcome.outcome === "lib-only") libOnly++;
2118
+ }
2119
+ return { instrumented, alreadyInstrumented: already, libOnly };
2120
+ }
1594
2121
  async function promptYesNo(question) {
1595
2122
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1596
2123
  return new Promise((resolve) => {
@@ -1601,28 +2128,106 @@ async function promptYesNo(question) {
1601
2128
  });
1602
2129
  });
1603
2130
  }
1604
- var DEFAULT_DAEMON_READY_TIMEOUT_MS = 15e3;
1605
- async function checkDaemonHealth(restPort) {
2131
+ var DEFAULT_DAEMON_READY_TIMEOUT_MS = 6e4;
2132
+ var PROBE_INTERVAL_MS = 500;
2133
+ async function fetchDaemonHealth(restPort) {
1606
2134
  return new Promise((resolve) => {
1607
2135
  const req = http.get(`http://127.0.0.1:${restPort}/health`, (res) => {
1608
2136
  const ok = res.statusCode !== void 0 && res.statusCode >= 200 && res.statusCode < 300;
1609
- res.resume();
1610
- resolve(ok);
2137
+ if (!ok) {
2138
+ res.resume();
2139
+ resolve(null);
2140
+ return;
2141
+ }
2142
+ let body = "";
2143
+ res.setEncoding("utf8");
2144
+ res.on("data", (chunk) => {
2145
+ body += chunk;
2146
+ });
2147
+ res.on("end", () => {
2148
+ try {
2149
+ resolve(JSON.parse(body));
2150
+ } catch {
2151
+ resolve({ ok: true });
2152
+ }
2153
+ });
1611
2154
  });
1612
- req.on("error", () => resolve(false));
2155
+ req.on("error", () => resolve(null));
1613
2156
  req.setTimeout(1e3, () => {
1614
2157
  req.destroy();
1615
- resolve(false);
2158
+ resolve(null);
2159
+ });
2160
+ });
2161
+ }
2162
+ async function checkDaemonHealth(restPort) {
2163
+ const body = await fetchDaemonHealth(restPort);
2164
+ return body !== null;
2165
+ }
2166
+ async function probeProjectHealth(restPort, name) {
2167
+ return new Promise((resolve) => {
2168
+ const req = http.get(
2169
+ `http://127.0.0.1:${restPort}/projects/${encodeURIComponent(name)}/health`,
2170
+ (res) => {
2171
+ const code = res.statusCode ?? 0;
2172
+ res.resume();
2173
+ if (code >= 200 && code < 300) resolve("active");
2174
+ else resolve("bootstrapping");
2175
+ }
2176
+ );
2177
+ req.on("error", () => resolve("bootstrapping"));
2178
+ req.setTimeout(1e3, () => {
2179
+ req.destroy();
2180
+ resolve("bootstrapping");
1616
2181
  });
1617
2182
  });
1618
2183
  }
2184
+ async function snapshotProjectStatus(restPort, body) {
2185
+ if (body.projects && body.projects.length > 0) {
2186
+ return body.projects.map((p) => ({
2187
+ name: p.name,
2188
+ status: p.status ?? "active"
2189
+ }));
2190
+ }
2191
+ const entries = await listProjects().catch(() => []);
2192
+ if (entries.length === 0) return [];
2193
+ return Promise.all(
2194
+ entries.map(async (entry2) => ({
2195
+ name: entry2.name,
2196
+ status: await probeProjectHealth(restPort, entry2.name)
2197
+ }))
2198
+ );
2199
+ }
1619
2200
  async function waitForDaemonReady(restPort, timeoutMs) {
1620
2201
  const deadline = Date.now() + timeoutMs;
2202
+ let lastBootstrapping = [];
1621
2203
  while (Date.now() < deadline) {
1622
- if (await checkDaemonHealth(restPort)) return true;
1623
- await new Promise((r) => setTimeout(r, 300));
2204
+ const body = await fetchDaemonHealth(restPort);
2205
+ if (body !== null) {
2206
+ const projects2 = await snapshotProjectStatus(restPort, body);
2207
+ const bootstrapping = projects2.filter((p) => p.status === "bootstrapping").map((p) => p.name);
2208
+ const broken = projects2.filter((p) => p.status === "broken").map((p) => p.name);
2209
+ if (bootstrapping.length === 0) {
2210
+ return { ready: true, brokenProjects: broken, stillBootstrapping: [] };
2211
+ }
2212
+ const key = bootstrapping.slice().sort().join(",");
2213
+ const prevKey = lastBootstrapping.slice().sort().join(",");
2214
+ if (key !== prevKey) {
2215
+ const plural = bootstrapping.length === 1 ? "" : "s";
2216
+ console.log(
2217
+ `neat: waiting on ${bootstrapping.length} project${plural}: ${bootstrapping.join(", ")}`
2218
+ );
2219
+ lastBootstrapping = bootstrapping;
2220
+ }
2221
+ }
2222
+ await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
1624
2223
  }
1625
- return false;
2224
+ const final = await fetchDaemonHealth(restPort);
2225
+ const projects = final ? await snapshotProjectStatus(restPort, final) : [];
2226
+ return {
2227
+ ready: false,
2228
+ brokenProjects: projects.filter((p) => p.status === "broken").map((p) => p.name),
2229
+ stillBootstrapping: projects.filter((p) => p.status === "bootstrapping").map((p) => p.name)
2230
+ };
1626
2231
  }
1627
2232
  function spawnDaemonDetached() {
1628
2233
  const here = path6.dirname(new URL(import.meta.url).pathname);
@@ -1643,12 +2248,21 @@ function spawnDaemonDetached() {
1643
2248
  if (!entry2) {
1644
2249
  throw new Error(`orchestrator: cannot locate neatd entry in ${here}`);
1645
2250
  }
2251
+ const env = { ...process.env };
2252
+ const hasToken = typeof env.NEAT_AUTH_TOKEN === "string" && env.NEAT_AUTH_TOKEN.length > 0;
2253
+ if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
2254
+ env.HOST = "127.0.0.1";
2255
+ }
1646
2256
  const child = spawn2(process.execPath, [entry2, "start"], {
1647
2257
  detached: true,
1648
- stdio: "ignore",
1649
- env: process.env
2258
+ // stderr inherits the orchestrator's fd so the daemon's
2259
+ // `BindAuthorityError` message lands in front of the operator instead
2260
+ // of being swallowed (issue #341).
2261
+ stdio: ["ignore", "ignore", "inherit"],
2262
+ env
1650
2263
  });
1651
2264
  child.unref();
2265
+ return child;
1652
2266
  }
1653
2267
  function openBrowser(url) {
1654
2268
  if (!process.stdout.isTTY) return "failed";
@@ -1685,24 +2299,21 @@ async function runOrchestrator(opts) {
1685
2299
  }
1686
2300
  console.log(`neat: ${opts.scanPath}`);
1687
2301
  console.log("");
1688
- const services = await discoverServices(opts.scanPath);
1689
- const languages = [...new Set(services.map((s) => s.node.language))].sort();
2302
+ const persisted = await extractAndPersist({
2303
+ scanPath: opts.scanPath,
2304
+ project: opts.project,
2305
+ projectExplicit: opts.projectExplicit
2306
+ });
2307
+ const { graph, services, languages } = persisted;
1690
2308
  result.steps.discovery = { services: services.length, languages };
1691
2309
  console.log(`discovered ${services.length} service(s) across ${languages.length} language(s)`);
1692
2310
  let runApply = !opts.noInstrument;
1693
2311
  if (runApply && !opts.yes && process.stdout.isTTY && process.stdin.isTTY) {
1694
2312
  runApply = await promptYesNo("instrument your services and open the dashboard?");
1695
2313
  }
1696
- const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
1697
- resetGraph(graphKey);
1698
- const graph = getGraph(graphKey);
1699
- const projectPaths = pathsForProject(graphKey, path6.join(opts.scanPath, "neat-out"));
1700
- const errorsPath = projectPaths.errorsPath;
1701
- const extraction = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
1702
- await saveGraphToDisk(graph, projectPaths.snapshotPath);
1703
2314
  result.steps.extraction = {
1704
- nodesAdded: extraction.nodesAdded,
1705
- edgesAdded: extraction.edgesAdded
2315
+ nodesAdded: persisted.nodesAdded,
2316
+ edgesAdded: persisted.edgesAdded
1706
2317
  };
1707
2318
  const gi = await ensureNeatOutIgnored(opts.scanPath);
1708
2319
  result.steps.gitignore = gi.action;
@@ -1727,24 +2338,11 @@ async function runOrchestrator(opts) {
1727
2338
  result.steps.apply.skipped = true;
1728
2339
  console.log("skipped instrumentation (--no-instrument)");
1729
2340
  } else {
1730
- let instrumented = 0;
1731
- let already = 0;
1732
- let libOnly = 0;
1733
- for (const svc of services) {
1734
- const installer = await pickInstaller(svc.dir);
1735
- if (!installer) continue;
1736
- const plan3 = await installer.plan(svc.dir);
1737
- if (isEmptyPlan(plan3) && !plan3.libOnly) {
1738
- already++;
1739
- continue;
1740
- }
1741
- const outcome = await installer.apply(plan3);
1742
- if (outcome.outcome === "instrumented") instrumented++;
1743
- else if (outcome.outcome === "already-instrumented") already++;
1744
- else if (outcome.outcome === "lib-only") libOnly++;
1745
- }
1746
- result.steps.apply = { instrumented, alreadyInstrumented: already, libOnly, skipped: false };
1747
- console.log(`instrumented ${instrumented}, already ${already}, lib-only ${libOnly}`);
2341
+ const tally = await applyInstallersOver(services, opts.project);
2342
+ result.steps.apply = { ...tally, skipped: false };
2343
+ console.log(
2344
+ `instrumented ${tally.instrumented}, already ${tally.alreadyInstrumented}, lib-only ${tally.libOnly}`
2345
+ );
1748
2346
  }
1749
2347
  const restPort = Number(process.env.PORT ?? 8080);
1750
2348
  const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
@@ -1759,12 +2357,25 @@ async function runOrchestrator(opts) {
1759
2357
  return result;
1760
2358
  }
1761
2359
  const ready = await waitForDaemonReady(restPort, timeoutMs);
1762
- result.steps.daemon = ready ? "spawned" : "timed-out";
1763
- if (!ready) {
2360
+ result.steps.daemon = ready.ready ? "spawned" : "timed-out";
2361
+ if (!ready.ready) {
1764
2362
  console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
2363
+ if (ready.stillBootstrapping.length > 0) {
2364
+ console.error(
2365
+ `neat: still bootstrapping: ${ready.stillBootstrapping.join(", ")}`
2366
+ );
2367
+ }
2368
+ if (ready.brokenProjects.length > 0) {
2369
+ console.error(`neat: broken projects: ${ready.brokenProjects.join(", ")}`);
2370
+ }
1765
2371
  result.exitCode = 1;
1766
2372
  return result;
1767
2373
  }
2374
+ if (ready.brokenProjects.length > 0) {
2375
+ console.warn(
2376
+ `neat: ${ready.brokenProjects.length} project(s) reported broken: ${ready.brokenProjects.join(", ")}`
2377
+ );
2378
+ }
1768
2379
  }
1769
2380
  const dashboardUrl = opts.dashboardUrl ?? "http://localhost:6328";
1770
2381
  if (opts.noOpen || !process.stdout.isTTY) {
@@ -1793,8 +2404,8 @@ function printSummary(result, graph, dashboardUrl) {
1793
2404
  console.log(`dashboard: ${dashboardUrl}`);
1794
2405
  }
1795
2406
 
1796
- // src/cli.ts
1797
- import { DivergenceTypeSchema } from "@neat.is/types";
2407
+ // src/cli-verbs.ts
2408
+ import path7 from "path";
1798
2409
 
1799
2410
  // src/cli-client.ts
1800
2411
  import { Provenance as Provenance2 } from "@neat.is/types";
@@ -1814,13 +2425,16 @@ var TransportError = class extends Error {
1814
2425
  this.name = "TransportError";
1815
2426
  }
1816
2427
  };
1817
- function createHttpClient(baseUrl) {
2428
+ function createHttpClient(baseUrl, bearerToken) {
1818
2429
  const root = baseUrl.replace(/\/$/, "");
2430
+ const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
1819
2431
  return {
1820
- async get(path8) {
2432
+ async get(path9) {
1821
2433
  let res;
1822
2434
  try {
1823
- res = await fetch(`${root}${path8}`);
2435
+ res = await fetch(`${root}${path9}`, {
2436
+ headers: { ...authHeader }
2437
+ });
1824
2438
  } catch (err) {
1825
2439
  throw new TransportError(
1826
2440
  `cannot reach neat-core at ${root}: ${err.message}`
@@ -1830,18 +2444,18 @@ function createHttpClient(baseUrl) {
1830
2444
  const body = await res.text().catch(() => "");
1831
2445
  throw new HttpError(
1832
2446
  res.status,
1833
- `${res.status} ${res.statusText} on GET ${path8}: ${body}`,
2447
+ `${res.status} ${res.statusText} on GET ${path9}: ${body}`,
1834
2448
  body
1835
2449
  );
1836
2450
  }
1837
2451
  return await res.json();
1838
2452
  },
1839
- async post(path8, body) {
2453
+ async post(path9, body) {
1840
2454
  let res;
1841
2455
  try {
1842
- res = await fetch(`${root}${path8}`, {
2456
+ res = await fetch(`${root}${path9}`, {
1843
2457
  method: "POST",
1844
- headers: { "content-type": "application/json" },
2458
+ headers: { "content-type": "application/json", ...authHeader },
1845
2459
  body: JSON.stringify(body)
1846
2460
  });
1847
2461
  } catch (err) {
@@ -1853,7 +2467,7 @@ function createHttpClient(baseUrl) {
1853
2467
  const text = await res.text().catch(() => "");
1854
2468
  throw new HttpError(
1855
2469
  res.status,
1856
- `${res.status} ${res.statusText} on POST ${path8}: ${text}`,
2470
+ `${res.status} ${res.statusText} on POST ${path9}: ${text}`,
1857
2471
  text
1858
2472
  );
1859
2473
  }
@@ -1867,12 +2481,12 @@ function projectPath(project, suffix) {
1867
2481
  }
1868
2482
  async function runRootCause(client, input) {
1869
2483
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
1870
- const path8 = projectPath(
2484
+ const path9 = projectPath(
1871
2485
  input.project,
1872
2486
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
1873
2487
  );
1874
2488
  try {
1875
- const result = await client.get(path8);
2489
+ const result = await client.get(path9);
1876
2490
  const arrowPath = result.traversalPath.join(" \u2190 ");
1877
2491
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
1878
2492
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -1898,12 +2512,12 @@ async function runRootCause(client, input) {
1898
2512
  }
1899
2513
  async function runBlastRadius(client, input) {
1900
2514
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
1901
- const path8 = projectPath(
2515
+ const path9 = projectPath(
1902
2516
  input.project,
1903
2517
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
1904
2518
  );
1905
2519
  try {
1906
- const result = await client.get(path8);
2520
+ const result = await client.get(path9);
1907
2521
  if (result.totalAffected === 0) {
1908
2522
  return {
1909
2523
  summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
@@ -1937,12 +2551,12 @@ function formatBlastEntry(n) {
1937
2551
  }
1938
2552
  async function runDependencies(client, input) {
1939
2553
  const depth = input.depth ?? 3;
1940
- const path8 = projectPath(
2554
+ const path9 = projectPath(
1941
2555
  input.project,
1942
2556
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
1943
2557
  );
1944
2558
  try {
1945
- const result = await client.get(path8);
2559
+ const result = await client.get(path9);
1946
2560
  if (result.total === 0) {
1947
2561
  return {
1948
2562
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -2023,9 +2637,9 @@ function formatDuration(ms) {
2023
2637
  return `${Math.round(h / 24)}d`;
2024
2638
  }
2025
2639
  async function runIncidents(client, input) {
2026
- const path8 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
2640
+ const path9 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
2027
2641
  try {
2028
- const body = await client.get(path8);
2642
+ const body = await client.get(path9);
2029
2643
  const events = body.events;
2030
2644
  if (events.length === 0) {
2031
2645
  return {
@@ -2291,8 +2905,181 @@ function exitCodeForError(err) {
2291
2905
  if (err instanceof HttpError) return 1;
2292
2906
  return 1;
2293
2907
  }
2908
+ function createSnapshotPushClient(baseUrl, token) {
2909
+ return createHttpClient(baseUrl, token && token.length > 0 ? token : void 0);
2910
+ }
2911
+ async function pushSnapshotToRemote(input) {
2912
+ const client = createSnapshotPushClient(input.baseUrl, input.token);
2913
+ if (typeof client.post !== "function") {
2914
+ throw new Error("HttpClient does not support POST \u2014 required for snapshot push");
2915
+ }
2916
+ return client.post(
2917
+ `/projects/${encodeURIComponent(input.project)}/snapshot`,
2918
+ { snapshot: input.snapshot }
2919
+ );
2920
+ }
2921
+
2922
+ // src/cli-verbs.ts
2923
+ async function resolveProjectEntry(opts) {
2924
+ const entries = await listProjects();
2925
+ if (opts.project) {
2926
+ const match = entries.find((e) => e.name === opts.project);
2927
+ return match ?? null;
2928
+ }
2929
+ const cwd = opts.cwd ?? process.cwd();
2930
+ const resolvedCwd = await normalizeProjectPath(cwd);
2931
+ for (const entry2 of entries) {
2932
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path7.sep}`)) {
2933
+ return entry2;
2934
+ }
2935
+ }
2936
+ return null;
2937
+ }
2938
+ async function checkDaemonHealth2(baseUrl) {
2939
+ try {
2940
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/health`, {
2941
+ signal: AbortSignal.timeout(1500)
2942
+ });
2943
+ return res.ok;
2944
+ } catch {
2945
+ return false;
2946
+ }
2947
+ }
2948
+ function snapshotForGraph(persisted) {
2949
+ return {
2950
+ schemaVersion: 3,
2951
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
2952
+ graph: persisted.graph.export()
2953
+ };
2954
+ }
2955
+ function emitResult(result, json) {
2956
+ if (json) {
2957
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
2958
+ return;
2959
+ }
2960
+ const verb = result.mode === "dry-run" ? "dry-run" : result.mode === "remote" ? "pushed" : "synced";
2961
+ console.log(
2962
+ `${verb}: ${result.project} \u2014 ${result.nodesAdded} node(s) and ${result.edgesAdded} edge(s) added` + (result.snapshotPath ? `; snapshot at ${result.snapshotPath}` : "")
2963
+ );
2964
+ if (!result.apply.skipped) {
2965
+ const { instrumented, alreadyInstrumented, libOnly } = result.apply;
2966
+ console.log(
2967
+ `instrumented ${instrumented}, already ${alreadyInstrumented}, lib-only ${libOnly}`
2968
+ );
2969
+ } else {
2970
+ console.log("skipped instrumentation (--no-instrument)");
2971
+ }
2972
+ for (const warn of result.warnings) console.error(warn);
2973
+ }
2974
+ async function runSync(opts) {
2975
+ const entry2 = await resolveProjectEntry(opts);
2976
+ if (!entry2) {
2977
+ const target = opts.project ?? opts.cwd ?? process.cwd();
2978
+ console.error(
2979
+ `neat sync: no registered project ${opts.project ? `named "${opts.project}"` : `covers ${target}`}. Run \`neat <path>\` or \`neat init <path>\` first.`
2980
+ );
2981
+ return {
2982
+ exitCode: 1,
2983
+ project: opts.project ?? "",
2984
+ scanPath: target,
2985
+ nodesAdded: 0,
2986
+ edgesAdded: 0,
2987
+ snapshotPath: null,
2988
+ mode: opts.to ? "remote" : "local",
2989
+ daemon: "skipped",
2990
+ apply: { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, skipped: true },
2991
+ warnings: []
2992
+ };
2993
+ }
2994
+ const persisted = await extractAndPersist({
2995
+ scanPath: entry2.path,
2996
+ project: entry2.name,
2997
+ projectExplicit: true,
2998
+ dryRun: true
2999
+ });
3000
+ let snapshotPath = null;
3001
+ if (!opts.dryRun) {
3002
+ const target = persisted.snapshotPath;
3003
+ await saveGraphToDisk(persisted.graph, target);
3004
+ snapshotPath = target;
3005
+ }
3006
+ const skipApply = opts.dryRun || opts.noInstrument;
3007
+ const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services, entry2.name);
3008
+ const warnings = [];
3009
+ let daemonState = "skipped";
3010
+ let exitCode = 0;
3011
+ const mode = opts.dryRun ? "dry-run" : opts.to ? "remote" : "local";
3012
+ if (!opts.dryRun) {
3013
+ const snapshot = snapshotForGraph(persisted);
3014
+ if (opts.to) {
3015
+ const token = opts.token ?? process.env.NEAT_REMOTE_TOKEN;
3016
+ try {
3017
+ await pushSnapshotToRemote({
3018
+ baseUrl: opts.to,
3019
+ token,
3020
+ project: entry2.name,
3021
+ snapshot
3022
+ });
3023
+ daemonState = "remote-ok";
3024
+ } catch (err) {
3025
+ if (err instanceof HttpError) {
3026
+ console.error(`neat sync: ${err.message}`);
3027
+ exitCode = 1;
3028
+ } else if (err instanceof TransportError) {
3029
+ console.error(`neat sync: ${err.message}`);
3030
+ exitCode = 3;
3031
+ } else {
3032
+ console.error(`neat sync: ${err.message}`);
3033
+ exitCode = 1;
3034
+ }
3035
+ daemonState = "skipped";
3036
+ }
3037
+ } else {
3038
+ const daemonUrl = opts.daemonUrl ?? process.env.NEAT_API_URL ?? "http://localhost:8080";
3039
+ const healthy = await checkDaemonHealth2(daemonUrl);
3040
+ if (healthy) {
3041
+ try {
3042
+ await pushSnapshotToRemote({
3043
+ baseUrl: daemonUrl,
3044
+ token: process.env.NEAT_AUTH_TOKEN,
3045
+ project: entry2.name,
3046
+ snapshot
3047
+ });
3048
+ daemonState = "reloaded";
3049
+ } catch (err) {
3050
+ warnings.push(
3051
+ `neat sync: daemon merge failed \u2014 ${err.message}. Snapshot is on disk at ${snapshotPath}.`
3052
+ );
3053
+ daemonState = "down";
3054
+ exitCode = 2;
3055
+ }
3056
+ } else {
3057
+ warnings.push(
3058
+ "neat sync: daemon not running; snapshot updated, run `neatd start` to serve it"
3059
+ );
3060
+ daemonState = "down";
3061
+ exitCode = 2;
3062
+ }
3063
+ }
3064
+ }
3065
+ const result = {
3066
+ exitCode,
3067
+ project: entry2.name,
3068
+ scanPath: entry2.path,
3069
+ nodesAdded: persisted.nodesAdded,
3070
+ edgesAdded: persisted.edgesAdded,
3071
+ snapshotPath,
3072
+ mode,
3073
+ daemon: daemonState,
3074
+ apply: { ...applyTally, skipped: skipApply },
3075
+ warnings
3076
+ };
3077
+ emitResult(result, opts.json);
3078
+ return result;
3079
+ }
2294
3080
 
2295
3081
  // src/cli.ts
3082
+ import { DivergenceTypeSchema } from "@neat.is/types";
2296
3083
  function usage() {
2297
3084
  console.log("usage: neat <command> [args] [--project <name>]");
2298
3085
  console.log("");
@@ -2313,6 +3100,8 @@ function usage() {
2313
3100
  console.log(" uninstall <name>");
2314
3101
  console.log(" Remove a project from the registry. Does not touch");
2315
3102
  console.log(" neat-out/, policy.json, or any user file.");
3103
+ console.log(" version Print the installed @neat.is/core version and exit.");
3104
+ console.log(" Aliases: --version, -v.");
2316
3105
  console.log(" skill Install or print the Claude Code MCP drop-in.");
2317
3106
  console.log(" Flags:");
2318
3107
  console.log(" --print-config print the JSON snippet to stdout");
@@ -2320,6 +3109,15 @@ function usage() {
2320
3109
  console.log(" deploy Detect the deploy substrate, generate NEAT_AUTH_TOKEN,");
2321
3110
  console.log(" emit a docker-compose / systemd / docker run artifact, and");
2322
3111
  console.log(" print the OTel env-vars block to paste into your platform.");
3112
+ console.log(" sync Re-run discovery, extraction, and SDK apply against the");
3113
+ console.log(" registered project, then notify the running daemon.");
3114
+ console.log(" Flags:");
3115
+ console.log(" --project <name> target a registered project by name");
3116
+ console.log(" --to <url> push the snapshot to a remote daemon");
3117
+ console.log(" --token <token> bearer token for --to (or $NEAT_REMOTE_TOKEN)");
3118
+ console.log(" --dry-run run extraction in-memory; do not write");
3119
+ console.log(" --no-instrument skip the SDK install apply step");
3120
+ console.log(" --json emit the delta summary as JSON");
2323
3121
  console.log("");
2324
3122
  console.log("query commands (mirror the MCP tools, ADR-050):");
2325
3123
  console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
@@ -2373,7 +3171,9 @@ var STRING_FLAGS = [
2373
3171
  ["--error-id", "errorId"],
2374
3172
  ["--hypothetical-action", "hypotheticalAction"],
2375
3173
  ["--type", "type"],
2376
- ["--min-confidence", "minConfidence"]
3174
+ ["--min-confidence", "minConfidence"],
3175
+ ["--to", "to"],
3176
+ ["--token", "token"]
2377
3177
  ];
2378
3178
  function parseArgs(rest) {
2379
3179
  const positional = [];
@@ -2398,6 +3198,8 @@ function parseArgs(rest) {
2398
3198
  hypotheticalAction: null,
2399
3199
  type: null,
2400
3200
  minConfidence: null,
3201
+ to: null,
3202
+ token: null,
2401
3203
  positional: []
2402
3204
  };
2403
3205
  for (let i = 0; i < rest.length; i++) {
@@ -2426,7 +3228,7 @@ function parseArgs(rest) {
2426
3228
  out.yes = true;
2427
3229
  continue;
2428
3230
  }
2429
- if (arg === "--verbose" || arg === "-v") {
3231
+ if (arg === "--verbose") {
2430
3232
  out.verbose = true;
2431
3233
  continue;
2432
3234
  }
@@ -2485,6 +3287,28 @@ function assignFlag(out, field, value) {
2485
3287
  ;
2486
3288
  out[field] = value;
2487
3289
  }
3290
+ function readPackageVersion() {
3291
+ const here = typeof __dirname !== "undefined" ? __dirname : path8.dirname(fileURLToPath(import.meta.url));
3292
+ const candidates = [
3293
+ path8.resolve(here, "../package.json"),
3294
+ path8.resolve(here, "../../package.json")
3295
+ ];
3296
+ for (const candidate of candidates) {
3297
+ try {
3298
+ const raw = readFileSync(candidate, "utf8");
3299
+ const parsed = JSON.parse(raw);
3300
+ if (parsed.name === "@neat.is/core" && typeof parsed.version === "string") {
3301
+ return parsed.version;
3302
+ }
3303
+ } catch {
3304
+ }
3305
+ }
3306
+ return "unknown";
3307
+ }
3308
+ function printVersion() {
3309
+ process.stdout.write(`${readPackageVersion()}
3310
+ `);
3311
+ }
2488
3312
  function printBanner() {
2489
3313
  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");
2490
3314
  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");
@@ -2494,7 +3318,7 @@ function printBanner() {
2494
3318
  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 ");
2495
3319
  console.log("");
2496
3320
  console.log(" Network Expressive Architecting Tool");
2497
- console.log(" neat.is \xB7 v0.2.5 \xB7 BSL 1.1");
3321
+ console.log(" neat.is \xB7 v0.4.0 \xB7 Apache 2.0");
2498
3322
  console.log("");
2499
3323
  }
2500
3324
  function printDiscoveryReport(opts, services) {
@@ -2522,12 +3346,12 @@ function printDiscoveryReport(opts, services) {
2522
3346
  }
2523
3347
  console.log("");
2524
3348
  }
2525
- async function buildPatchSections(services) {
3349
+ async function buildPatchSections(services, project) {
2526
3350
  const sections = [];
2527
3351
  for (const svc of services) {
2528
3352
  const installer = await pickInstaller(svc.dir);
2529
3353
  if (!installer) continue;
2530
- const plan3 = await installer.plan(svc.dir);
3354
+ const plan3 = await installer.plan(svc.dir, { project });
2531
3355
  if (isEmptyPlan(plan3) && !plan3.libOnly) continue;
2532
3356
  sections.push({ installer: installer.name, plan: plan3 });
2533
3357
  }
@@ -2542,14 +3366,14 @@ async function runInit(opts) {
2542
3366
  }
2543
3367
  const services = await discoverServices(opts.scanPath);
2544
3368
  printDiscoveryReport(opts, services);
2545
- const sections = opts.noInstall ? [] : await buildPatchSections(services);
3369
+ const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
2546
3370
  const patch = renderPatch(sections);
2547
- const patchPath = path7.join(opts.scanPath, "neat.patch");
3371
+ const patchPath = path8.join(opts.scanPath, "neat.patch");
2548
3372
  if (opts.dryRun) {
2549
3373
  await fs7.writeFile(patchPath, patch, "utf8");
2550
3374
  written.push(patchPath);
2551
3375
  console.log(`dry-run: patch written to ${patchPath}`);
2552
- const gitignorePath = path7.join(opts.scanPath, ".gitignore");
3376
+ const gitignorePath = path8.join(opts.scanPath, ".gitignore");
2553
3377
  const gitignoreExists = await fs7.stat(gitignorePath).then(() => true).catch(() => false);
2554
3378
  const verb = gitignoreExists ? "append" : "create";
2555
3379
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
@@ -2561,9 +3385,9 @@ async function runInit(opts) {
2561
3385
  const graph = getGraph(graphKey);
2562
3386
  const projectPaths = pathsForProject(
2563
3387
  graphKey,
2564
- path7.join(opts.scanPath, "neat-out")
3388
+ path8.join(opts.scanPath, "neat-out")
2565
3389
  );
2566
- const errorsPath = path7.join(path7.dirname(opts.outPath), path7.basename(projectPaths.errorsPath));
3390
+ const errorsPath = path8.join(path8.dirname(opts.outPath), path8.basename(projectPaths.errorsPath));
2567
3391
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
2568
3392
  await saveGraphToDisk(graph, opts.outPath);
2569
3393
  written.push(opts.outPath);
@@ -2653,9 +3477,9 @@ var CLAUDE_SKILL_CONFIG = {
2653
3477
  };
2654
3478
  function claudeConfigPath() {
2655
3479
  const override = process.env.NEAT_CLAUDE_CONFIG;
2656
- if (override && override.length > 0) return path7.resolve(override);
3480
+ if (override && override.length > 0) return path8.resolve(override);
2657
3481
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
2658
- return path7.join(home, ".claude.json");
3482
+ return path8.join(home, ".claude.json");
2659
3483
  }
2660
3484
  async function runSkill(opts) {
2661
3485
  const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -2679,7 +3503,7 @@ async function runSkill(opts) {
2679
3503
  ...existing,
2680
3504
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
2681
3505
  };
2682
- await fs7.mkdir(path7.dirname(target), { recursive: true });
3506
+ await fs7.mkdir(path8.dirname(target), { recursive: true });
2683
3507
  await fs7.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
2684
3508
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
2685
3509
  console.log("restart Claude Code to pick up the new MCP server.");
@@ -2700,6 +3524,10 @@ async function main() {
2700
3524
  usage();
2701
3525
  process.exit(0);
2702
3526
  }
3527
+ if (cmd === "--version" || cmd === "-v" || cmd === "version") {
3528
+ printVersion();
3529
+ process.exit(0);
3530
+ }
2703
3531
  const parsed = parseArgs(rest);
2704
3532
  const { positional, apply: apply3, dryRun, noInstall } = parsed;
2705
3533
  const project = parsed.project ?? DEFAULT_PROJECT;
@@ -2714,12 +3542,12 @@ async function main() {
2714
3542
  console.error("neat init: --apply and --dry-run are mutually exclusive");
2715
3543
  process.exit(2);
2716
3544
  }
2717
- const scanPath = path7.resolve(target);
3545
+ const scanPath = path8.resolve(target);
2718
3546
  const projectExplicit = parsed.project !== null;
2719
- const projectName = projectExplicit ? project : path7.basename(scanPath);
3547
+ const projectName = projectExplicit ? project : path8.basename(scanPath);
2720
3548
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
2721
- const fallback = pathsForProject(projectKey, path7.join(scanPath, "neat-out")).snapshotPath;
2722
- const outPath = path7.resolve(process.env.NEAT_OUT_PATH ?? fallback);
3549
+ const fallback = pathsForProject(projectKey, path8.join(scanPath, "neat-out")).snapshotPath;
3550
+ const outPath = path8.resolve(process.env.NEAT_OUT_PATH ?? fallback);
2723
3551
  const result = await runInit({
2724
3552
  scanPath,
2725
3553
  outPath,
@@ -2740,21 +3568,21 @@ async function main() {
2740
3568
  usage();
2741
3569
  process.exit(2);
2742
3570
  }
2743
- const scanPath = path7.resolve(target);
3571
+ const scanPath = path8.resolve(target);
2744
3572
  const stat = await fs7.stat(scanPath).catch(() => null);
2745
3573
  if (!stat || !stat.isDirectory()) {
2746
3574
  console.error(`neat watch: ${scanPath} is not a directory`);
2747
3575
  process.exit(2);
2748
3576
  }
2749
- const projectPaths = pathsForProject(project, path7.join(scanPath, "neat-out"));
2750
- const outPath = path7.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
2751
- const errorsPath = path7.resolve(
2752
- process.env.NEAT_ERRORS_PATH ?? path7.join(path7.dirname(outPath), path7.basename(projectPaths.errorsPath))
3577
+ const projectPaths = pathsForProject(project, path8.join(scanPath, "neat-out"));
3578
+ const outPath = path8.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
3579
+ const errorsPath = path8.resolve(
3580
+ process.env.NEAT_ERRORS_PATH ?? path8.join(path8.dirname(outPath), path8.basename(projectPaths.errorsPath))
2753
3581
  );
2754
- const staleEventsPath = path7.resolve(
2755
- process.env.NEAT_STALE_EVENTS_PATH ?? path7.join(path7.dirname(outPath), path7.basename(projectPaths.staleEventsPath))
3582
+ const staleEventsPath = path8.resolve(
3583
+ process.env.NEAT_STALE_EVENTS_PATH ?? path8.join(path8.dirname(outPath), path8.basename(projectPaths.staleEventsPath))
2756
3584
  );
2757
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path7.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
3585
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path8.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
2758
3586
  const handle = await startWatch(getGraph(project), {
2759
3587
  scanPath,
2760
3588
  outPath,
@@ -2873,6 +3701,18 @@ async function main() {
2873
3701
  console.log(` ${artifact.startCommand}`);
2874
3702
  return;
2875
3703
  }
3704
+ if (cmd === "sync") {
3705
+ const result = await runSync({
3706
+ ...parsed.project ? { project: parsed.project } : {},
3707
+ ...parsed.to ? { to: parsed.to } : {},
3708
+ ...parsed.token ? { token: parsed.token } : {},
3709
+ dryRun: parsed.dryRun,
3710
+ noInstrument: parsed.noInstrument,
3711
+ json: parsed.json
3712
+ });
3713
+ if (result.exitCode !== 0) process.exit(result.exitCode);
3714
+ return;
3715
+ }
2876
3716
  if (QUERY_VERBS.has(cmd)) {
2877
3717
  const code = await runQueryVerb(cmd, parsed);
2878
3718
  if (code !== 0) process.exit(code);
@@ -2888,11 +3728,11 @@ async function main() {
2888
3728
  process.exit(1);
2889
3729
  }
2890
3730
  async function tryOrchestrator(cmd, parsed) {
2891
- const scanPath = path7.resolve(cmd);
3731
+ const scanPath = path8.resolve(cmd);
2892
3732
  const stat = await fs7.stat(scanPath).catch(() => null);
2893
3733
  if (!stat || !stat.isDirectory()) return null;
2894
3734
  const projectExplicit = parsed.project !== null;
2895
- const projectName = projectExplicit ? parsed.project : path7.basename(scanPath);
3735
+ const projectName = projectExplicit ? parsed.project : path8.basename(scanPath);
2896
3736
  const result = await runOrchestrator({
2897
3737
  scanPath,
2898
3738
  project: projectName,
@@ -3093,6 +3933,7 @@ export {
3093
3933
  CLAUDE_SKILL_CONFIG,
3094
3934
  QUERY_VERBS,
3095
3935
  parseArgs,
3936
+ readPackageVersion,
3096
3937
  runInit,
3097
3938
  runQueryVerb,
3098
3939
  runSkill