@neat.is/core 0.3.8 → 0.4.0

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,17 +41,17 @@ import {
40
41
  setStatus,
41
42
  startPersistLoop,
42
43
  startStalenessLoop
43
- } from "./chunk-CZ3T6TE2.js";
44
+ } from "./chunk-UPW4CMOH.js";
44
45
  import {
45
46
  startOtelGrpcReceiver
46
- } from "./chunk-V4TU7OKZ.js";
47
+ } from "./chunk-IXIFJKMM.js";
47
48
  import {
48
49
  __require,
49
50
  buildOtelReceiver
50
- } from "./chunk-7TYESDAI.js";
51
+ } from "./chunk-4V23KYOP.js";
51
52
 
52
53
  // src/cli.ts
53
- import path7 from "path";
54
+ import path8 from "path";
54
55
  import { promises as fs7 } from "fs";
55
56
 
56
57
  // src/gitignore.ts
@@ -771,6 +772,100 @@ const sdk = new NodeSDK({
771
772
  })
772
773
  sdk.start()
773
774
  `;
775
+ var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
776
+ var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
777
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
778
+ // the OTel SDK starts so the exporter target and service name are in scope.
779
+ import { fileURLToPath } from 'node:url'
780
+ import path from 'node:path'
781
+ import dotenv from 'dotenv'
782
+ import { NodeSDK } from '@opentelemetry/sdk-node'
783
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
784
+
785
+ const here = path.dirname(fileURLToPath(import.meta.url))
786
+ dotenv.config({ path: path.join(here, '.env.neat') })
787
+
788
+ const sdk = new NodeSDK({
789
+ serviceName: process.env.OTEL_SERVICE_NAME,
790
+ instrumentations: [getNodeAutoInstrumentations()],
791
+ })
792
+ sdk.start()
793
+ `;
794
+ var FRAMEWORK_OTEL_INIT_JS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
795
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
796
+ // the OTel SDK starts so the exporter target and service name are in scope.
797
+ const path = require('node:path')
798
+ try {
799
+ require('dotenv').config({ path: path.join(__dirname, '.env.neat') })
800
+ } catch (err) {
801
+ // dotenv unavailable \u2014 fall through to process.env.
802
+ }
803
+ const { NodeSDK } = require('@opentelemetry/sdk-node')
804
+ const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
805
+
806
+ const sdk = new NodeSDK({
807
+ serviceName: process.env.OTEL_SERVICE_NAME,
808
+ instrumentations: [getNodeAutoInstrumentations()],
809
+ })
810
+ sdk.start()
811
+ `;
812
+ var REMIX_OTEL_SERVER_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
813
+ var REMIX_OTEL_SERVER_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
814
+ var SVELTEKIT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
815
+ var SVELTEKIT_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
816
+ var SVELTEKIT_HOOKS_SERVER_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
817
+ import './otel-init'
818
+
819
+ import type { Handle } from '@sveltejs/kit'
820
+
821
+ export const handle: Handle = async ({ event, resolve }) => {
822
+ return resolve(event)
823
+ }
824
+ `;
825
+ var SVELTEKIT_HOOKS_SERVER_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
826
+ import './otel-init'
827
+
828
+ /** @type {import('@sveltejs/kit').Handle} */
829
+ export const handle = async ({ event, resolve }) => {
830
+ return resolve(event)
831
+ }
832
+ `;
833
+ var NUXT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
834
+ var NUXT_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
835
+ var NUXT_OTEL_PLUGIN_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
836
+ import './otel-init'
837
+
838
+ export default defineNitroPlugin(() => {
839
+ // OTel SDK is initialised at module-load via the side-effect import above.
840
+ })
841
+ `;
842
+ var NUXT_OTEL_PLUGIN_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
843
+ require('./otel-init')
844
+
845
+ module.exports = defineNitroPlugin(() => {
846
+ // OTel SDK is initialised at module-load via the side-effect import above.
847
+ })
848
+ `;
849
+ var ASTRO_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
850
+ var ASTRO_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
851
+ var ASTRO_MIDDLEWARE_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
852
+ import './otel-init'
853
+
854
+ import { defineMiddleware } from 'astro:middleware'
855
+
856
+ export const onRequest = defineMiddleware(async (context, next) => {
857
+ return next()
858
+ })
859
+ `;
860
+ var ASTRO_MIDDLEWARE_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
861
+ import './otel-init'
862
+
863
+ import { defineMiddleware } from 'astro:middleware'
864
+
865
+ export const onRequest = defineMiddleware(async (context, next) => {
866
+ return next()
867
+ })
868
+ `;
774
869
 
775
870
  // src/installers/javascript.ts
776
871
  var SDK_PACKAGES = [
@@ -822,6 +917,71 @@ async function findNextConfig(serviceDir) {
822
917
  function hasNextDependency(pkg) {
823
918
  return pkg.dependencies?.next !== void 0 || pkg.devDependencies?.next !== void 0;
824
919
  }
920
+ function allDeps(pkg) {
921
+ return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
922
+ }
923
+ var REMIX_ENTRY_CANDIDATES = [
924
+ "app/entry.server.ts",
925
+ "app/entry.server.tsx",
926
+ "app/entry.server.js",
927
+ "app/entry.server.jsx"
928
+ ];
929
+ function hasRemixDependency(pkg) {
930
+ const deps = allDeps(pkg);
931
+ if ("remix" in deps) return true;
932
+ for (const name of Object.keys(deps)) {
933
+ if (name.startsWith("@remix-run/")) return true;
934
+ }
935
+ return false;
936
+ }
937
+ async function findRemixEntry(serviceDir) {
938
+ for (const rel of REMIX_ENTRY_CANDIDATES) {
939
+ const candidate = path4.join(serviceDir, rel);
940
+ if (await exists(candidate)) return candidate;
941
+ }
942
+ return null;
943
+ }
944
+ var SVELTEKIT_HOOKS_CANDIDATES = ["src/hooks.server.ts", "src/hooks.server.js"];
945
+ var SVELTEKIT_CONFIG_CANDIDATES = ["svelte.config.js", "svelte.config.ts"];
946
+ function hasSvelteKitDependency(pkg) {
947
+ return "@sveltejs/kit" in allDeps(pkg);
948
+ }
949
+ async function findSvelteKitHooks(serviceDir) {
950
+ for (const rel of SVELTEKIT_HOOKS_CANDIDATES) {
951
+ const candidate = path4.join(serviceDir, rel);
952
+ if (await exists(candidate)) return candidate;
953
+ }
954
+ return null;
955
+ }
956
+ async function findSvelteKitConfig(serviceDir) {
957
+ for (const rel of SVELTEKIT_CONFIG_CANDIDATES) {
958
+ const candidate = path4.join(serviceDir, rel);
959
+ if (await exists(candidate)) return candidate;
960
+ }
961
+ return null;
962
+ }
963
+ var NUXT_CONFIG_CANDIDATES = ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"];
964
+ function hasNuxtDependency(pkg) {
965
+ return "nuxt" in allDeps(pkg);
966
+ }
967
+ async function findNuxtConfig(serviceDir) {
968
+ for (const name of NUXT_CONFIG_CANDIDATES) {
969
+ const candidate = path4.join(serviceDir, name);
970
+ if (await exists(candidate)) return candidate;
971
+ }
972
+ return null;
973
+ }
974
+ var ASTRO_CONFIG_CANDIDATES = ["astro.config.mjs", "astro.config.ts", "astro.config.js"];
975
+ function hasAstroDependency(pkg) {
976
+ return "astro" in allDeps(pkg);
977
+ }
978
+ async function findAstroConfig(serviceDir) {
979
+ for (const name of ASTRO_CONFIG_CANDIDATES) {
980
+ const candidate = path4.join(serviceDir, name);
981
+ if (await exists(candidate)) return candidate;
982
+ }
983
+ return null;
984
+ }
825
985
  function parseNextMajor(range) {
826
986
  if (!range) return null;
827
987
  const cleaned = range.trim().replace(/^[\^~>=<\s]+/, "");
@@ -1029,6 +1189,276 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
1029
1189
  ...nextConfigEdit ? { nextConfigEdit } : {}
1030
1190
  };
1031
1191
  }
1192
+ function buildDependencyEdits(pkg, manifestPath) {
1193
+ const existingDeps = allDeps(pkg);
1194
+ const edits = [];
1195
+ for (const sdk of SDK_PACKAGES) {
1196
+ if (sdk.name in existingDeps) continue;
1197
+ edits.push({
1198
+ file: manifestPath,
1199
+ kind: "add",
1200
+ name: sdk.name,
1201
+ version: sdk.version
1202
+ });
1203
+ }
1204
+ return edits;
1205
+ }
1206
+ async function queueEnvNeat(serviceDir, pkg, generatedFiles) {
1207
+ const envNeatFile = path4.join(serviceDir, ".env.neat");
1208
+ if (!await exists(envNeatFile)) {
1209
+ generatedFiles.push({
1210
+ file: envNeatFile,
1211
+ contents: renderEnvNeat(pkg.name ?? path4.basename(serviceDir)),
1212
+ skipIfExists: true
1213
+ });
1214
+ }
1215
+ }
1216
+ function fileImportsOtelHook(raw, specifiers) {
1217
+ const lines = raw.split(/\r?\n/);
1218
+ for (const line of lines) {
1219
+ const trimmed = line.trim();
1220
+ for (const spec of specifiers) {
1221
+ const escaped = spec.replace(/\./g, "\\.");
1222
+ const pattern = new RegExp(
1223
+ `(?:import\\s+['"]${escaped}['"]|require\\(['"]${escaped}['"]\\))`
1224
+ );
1225
+ if (pattern.test(trimmed)) return true;
1226
+ }
1227
+ }
1228
+ return false;
1229
+ }
1230
+ async function planRemix(serviceDir, pkg, manifestPath, entryFile) {
1231
+ const useTs = await isTypeScriptProject(serviceDir);
1232
+ const otelServerFile = path4.join(
1233
+ serviceDir,
1234
+ useTs ? "app/otel.server.ts" : "app/otel.server.js"
1235
+ );
1236
+ const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
1237
+ const generatedFiles = [];
1238
+ if (!await exists(otelServerFile)) {
1239
+ generatedFiles.push({
1240
+ file: otelServerFile,
1241
+ contents: useTs ? REMIX_OTEL_SERVER_TS : REMIX_OTEL_SERVER_JS,
1242
+ skipIfExists: true
1243
+ });
1244
+ }
1245
+ await queueEnvNeat(serviceDir, pkg, generatedFiles);
1246
+ const entrypointEdits = [];
1247
+ try {
1248
+ const raw = await fs4.readFile(entryFile, "utf8");
1249
+ if (!fileImportsOtelHook(raw, ["./otel.server"])) {
1250
+ const lines = raw.split(/\r?\n/);
1251
+ const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
1252
+ entrypointEdits.push({
1253
+ file: entryFile,
1254
+ before: firstReal,
1255
+ after: "import './otel.server'"
1256
+ });
1257
+ }
1258
+ } catch {
1259
+ }
1260
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
1261
+ if (empty) {
1262
+ return {
1263
+ language: "javascript",
1264
+ serviceDir,
1265
+ dependencyEdits: [],
1266
+ entrypointEdits: [],
1267
+ envEdits: [],
1268
+ generatedFiles: [],
1269
+ framework: "remix"
1270
+ };
1271
+ }
1272
+ return {
1273
+ language: "javascript",
1274
+ serviceDir,
1275
+ dependencyEdits,
1276
+ entrypointEdits,
1277
+ envEdits: [OTEL_ENV],
1278
+ generatedFiles,
1279
+ framework: "remix"
1280
+ };
1281
+ }
1282
+ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile) {
1283
+ const useTs = await isTypeScriptProject(serviceDir);
1284
+ const otelInitFile = path4.join(
1285
+ serviceDir,
1286
+ useTs ? "src/otel-init.ts" : "src/otel-init.js"
1287
+ );
1288
+ const resolvedHooksFile = hooksFile ?? path4.join(serviceDir, useTs ? "src/hooks.server.ts" : "src/hooks.server.js");
1289
+ const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
1290
+ const generatedFiles = [];
1291
+ const entrypointEdits = [];
1292
+ if (!await exists(otelInitFile)) {
1293
+ generatedFiles.push({
1294
+ file: otelInitFile,
1295
+ contents: useTs ? SVELTEKIT_OTEL_INIT_TS : SVELTEKIT_OTEL_INIT_JS,
1296
+ skipIfExists: true
1297
+ });
1298
+ }
1299
+ await queueEnvNeat(serviceDir, pkg, generatedFiles);
1300
+ if (hooksFile === null) {
1301
+ generatedFiles.push({
1302
+ file: resolvedHooksFile,
1303
+ contents: useTs ? SVELTEKIT_HOOKS_SERVER_TS : SVELTEKIT_HOOKS_SERVER_JS,
1304
+ skipIfExists: true
1305
+ });
1306
+ } else {
1307
+ try {
1308
+ const raw = await fs4.readFile(hooksFile, "utf8");
1309
+ if (!fileImportsOtelHook(raw, ["./otel-init"])) {
1310
+ const lines = raw.split(/\r?\n/);
1311
+ const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
1312
+ entrypointEdits.push({
1313
+ file: hooksFile,
1314
+ before: firstReal,
1315
+ after: "import './otel-init'"
1316
+ });
1317
+ }
1318
+ } catch {
1319
+ }
1320
+ }
1321
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
1322
+ if (empty) {
1323
+ return {
1324
+ language: "javascript",
1325
+ serviceDir,
1326
+ dependencyEdits: [],
1327
+ entrypointEdits: [],
1328
+ envEdits: [],
1329
+ generatedFiles: [],
1330
+ framework: "sveltekit"
1331
+ };
1332
+ }
1333
+ return {
1334
+ language: "javascript",
1335
+ serviceDir,
1336
+ dependencyEdits,
1337
+ entrypointEdits,
1338
+ envEdits: [OTEL_ENV],
1339
+ generatedFiles,
1340
+ framework: "sveltekit"
1341
+ };
1342
+ }
1343
+ async function planNuxt(serviceDir, pkg, manifestPath) {
1344
+ const useTs = await isTypeScriptProject(serviceDir);
1345
+ const otelPluginFile = path4.join(
1346
+ serviceDir,
1347
+ useTs ? "server/plugins/otel.ts" : "server/plugins/otel.js"
1348
+ );
1349
+ const otelInitFile = path4.join(
1350
+ serviceDir,
1351
+ useTs ? "server/plugins/otel-init.ts" : "server/plugins/otel-init.js"
1352
+ );
1353
+ const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
1354
+ const generatedFiles = [];
1355
+ if (!await exists(otelInitFile)) {
1356
+ generatedFiles.push({
1357
+ file: otelInitFile,
1358
+ contents: useTs ? NUXT_OTEL_INIT_TS : NUXT_OTEL_INIT_JS,
1359
+ skipIfExists: true
1360
+ });
1361
+ }
1362
+ if (!await exists(otelPluginFile)) {
1363
+ generatedFiles.push({
1364
+ file: otelPluginFile,
1365
+ contents: useTs ? NUXT_OTEL_PLUGIN_TS : NUXT_OTEL_PLUGIN_JS,
1366
+ skipIfExists: true
1367
+ });
1368
+ }
1369
+ await queueEnvNeat(serviceDir, pkg, generatedFiles);
1370
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0;
1371
+ if (empty) {
1372
+ return {
1373
+ language: "javascript",
1374
+ serviceDir,
1375
+ dependencyEdits: [],
1376
+ entrypointEdits: [],
1377
+ envEdits: [],
1378
+ generatedFiles: [],
1379
+ framework: "nuxt"
1380
+ };
1381
+ }
1382
+ return {
1383
+ language: "javascript",
1384
+ serviceDir,
1385
+ dependencyEdits,
1386
+ entrypointEdits: [],
1387
+ envEdits: [OTEL_ENV],
1388
+ generatedFiles,
1389
+ framework: "nuxt"
1390
+ };
1391
+ }
1392
+ var ASTRO_MIDDLEWARE_CANDIDATES = ["src/middleware.ts", "src/middleware.js"];
1393
+ async function findAstroMiddleware(serviceDir) {
1394
+ for (const rel of ASTRO_MIDDLEWARE_CANDIDATES) {
1395
+ const candidate = path4.join(serviceDir, rel);
1396
+ if (await exists(candidate)) return candidate;
1397
+ }
1398
+ return null;
1399
+ }
1400
+ async function planAstro(serviceDir, pkg, manifestPath) {
1401
+ const useTs = await isTypeScriptProject(serviceDir);
1402
+ const otelInitFile = path4.join(
1403
+ serviceDir,
1404
+ useTs ? "src/otel-init.ts" : "src/otel-init.js"
1405
+ );
1406
+ const existingMiddleware = await findAstroMiddleware(serviceDir);
1407
+ const middlewareFile = existingMiddleware ?? path4.join(serviceDir, useTs ? "src/middleware.ts" : "src/middleware.js");
1408
+ const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
1409
+ const generatedFiles = [];
1410
+ const entrypointEdits = [];
1411
+ if (!await exists(otelInitFile)) {
1412
+ generatedFiles.push({
1413
+ file: otelInitFile,
1414
+ contents: useTs ? ASTRO_OTEL_INIT_TS : ASTRO_OTEL_INIT_JS,
1415
+ skipIfExists: true
1416
+ });
1417
+ }
1418
+ await queueEnvNeat(serviceDir, pkg, generatedFiles);
1419
+ if (existingMiddleware === null) {
1420
+ generatedFiles.push({
1421
+ file: middlewareFile,
1422
+ contents: useTs ? ASTRO_MIDDLEWARE_TS : ASTRO_MIDDLEWARE_JS,
1423
+ skipIfExists: true
1424
+ });
1425
+ } else {
1426
+ try {
1427
+ const raw = await fs4.readFile(existingMiddleware, "utf8");
1428
+ if (!fileImportsOtelHook(raw, ["./otel-init"])) {
1429
+ const lines = raw.split(/\r?\n/);
1430
+ const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
1431
+ entrypointEdits.push({
1432
+ file: existingMiddleware,
1433
+ before: firstReal,
1434
+ after: "import './otel-init'"
1435
+ });
1436
+ }
1437
+ } catch {
1438
+ }
1439
+ }
1440
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
1441
+ if (empty) {
1442
+ return {
1443
+ language: "javascript",
1444
+ serviceDir,
1445
+ dependencyEdits: [],
1446
+ entrypointEdits: [],
1447
+ envEdits: [],
1448
+ generatedFiles: [],
1449
+ framework: "astro"
1450
+ };
1451
+ }
1452
+ return {
1453
+ language: "javascript",
1454
+ serviceDir,
1455
+ dependencyEdits,
1456
+ entrypointEdits,
1457
+ envEdits: [OTEL_ENV],
1458
+ generatedFiles,
1459
+ framework: "astro"
1460
+ };
1461
+ }
1032
1462
  async function plan(serviceDir) {
1033
1463
  const pkg = await readPackageJson(serviceDir);
1034
1464
  const manifestPath = path4.join(serviceDir, "package.json");
@@ -1047,6 +1477,31 @@ async function plan(serviceDir) {
1047
1477
  return planNext(serviceDir, pkg, manifestPath, nextConfig);
1048
1478
  }
1049
1479
  }
1480
+ if (hasRemixDependency(pkg)) {
1481
+ const remixEntry = await findRemixEntry(serviceDir);
1482
+ if (remixEntry) {
1483
+ return planRemix(serviceDir, pkg, manifestPath, remixEntry);
1484
+ }
1485
+ }
1486
+ if (hasSvelteKitDependency(pkg)) {
1487
+ const hooks = await findSvelteKitHooks(serviceDir);
1488
+ const config = await findSvelteKitConfig(serviceDir);
1489
+ if (hooks || config) {
1490
+ return planSvelteKit(serviceDir, pkg, manifestPath, hooks);
1491
+ }
1492
+ }
1493
+ if (hasNuxtDependency(pkg)) {
1494
+ const nuxtConfig = await findNuxtConfig(serviceDir);
1495
+ if (nuxtConfig) {
1496
+ return planNuxt(serviceDir, pkg, manifestPath);
1497
+ }
1498
+ }
1499
+ if (hasAstroDependency(pkg)) {
1500
+ const astroConfig = await findAstroConfig(serviceDir);
1501
+ if (astroConfig) {
1502
+ return planAstro(serviceDir, pkg, manifestPath);
1503
+ }
1504
+ }
1050
1505
  const entryFile = await resolveEntry(serviceDir, pkg);
1051
1506
  if (!entryFile) {
1052
1507
  return { ...empty, libOnly: true };
@@ -1119,9 +1574,18 @@ function isAllowedWritePath(serviceDir, target) {
1119
1574
  if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
1120
1575
  if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
1121
1576
  if (/^next\.config\.(?:js|mjs|ts)$/.test(base)) return true;
1577
+ const relPosix = rel.split(path4.sep).join("/");
1578
+ if (relPosix === "app/otel.server.ts" || relPosix === "app/otel.server.js") return true;
1579
+ if (/^app\/entry\.server\.(?:tsx?|jsx?)$/.test(relPosix)) return true;
1580
+ if (relPosix === "src/otel-init.ts" || relPosix === "src/otel-init.js") return true;
1581
+ if (relPosix === "src/hooks.server.ts" || relPosix === "src/hooks.server.js") return true;
1582
+ if (relPosix === "server/plugins/otel.ts" || relPosix === "server/plugins/otel.js") return true;
1583
+ if (relPosix === "server/plugins/otel-init.ts" || relPosix === "server/plugins/otel-init.js") return true;
1584
+ if (relPosix === "src/middleware.ts" || relPosix === "src/middleware.js") return true;
1122
1585
  return false;
1123
1586
  }
1124
1587
  async function writeAtomic(file, contents) {
1588
+ await fs4.mkdir(path4.dirname(file), { recursive: true });
1125
1589
  const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
1126
1590
  await fs4.writeFile(tmp, contents, "utf8");
1127
1591
  await fs4.rename(tmp, file);
@@ -1591,6 +2055,49 @@ import http from "http";
1591
2055
  import path6 from "path";
1592
2056
  import { spawn as spawn2 } from "child_process";
1593
2057
  import readline from "readline";
2058
+ async function extractAndPersist(opts) {
2059
+ const services = await discoverServices(opts.scanPath);
2060
+ const languages = [...new Set(services.map((s) => s.node.language))].sort();
2061
+ const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
2062
+ resetGraph(graphKey);
2063
+ const graph = getGraph(graphKey);
2064
+ const projectPaths = pathsForProject(graphKey, path6.join(opts.scanPath, "neat-out"));
2065
+ const extraction = await extractFromDirectory(graph, opts.scanPath, {
2066
+ errorsPath: projectPaths.errorsPath
2067
+ });
2068
+ if (!opts.dryRun) {
2069
+ await saveGraphToDisk(graph, projectPaths.snapshotPath);
2070
+ }
2071
+ return {
2072
+ graph,
2073
+ graphKey,
2074
+ services,
2075
+ languages,
2076
+ nodesAdded: extraction.nodesAdded,
2077
+ edgesAdded: extraction.edgesAdded,
2078
+ snapshotPath: projectPaths.snapshotPath,
2079
+ errorsPath: projectPaths.errorsPath
2080
+ };
2081
+ }
2082
+ async function applyInstallersOver(services) {
2083
+ let instrumented = 0;
2084
+ let already = 0;
2085
+ let libOnly = 0;
2086
+ for (const svc of services) {
2087
+ const installer = await pickInstaller(svc.dir);
2088
+ if (!installer) continue;
2089
+ const plan3 = await installer.plan(svc.dir);
2090
+ if (isEmptyPlan(plan3) && !plan3.libOnly) {
2091
+ already++;
2092
+ continue;
2093
+ }
2094
+ const outcome = await installer.apply(plan3);
2095
+ if (outcome.outcome === "instrumented") instrumented++;
2096
+ else if (outcome.outcome === "already-instrumented") already++;
2097
+ else if (outcome.outcome === "lib-only") libOnly++;
2098
+ }
2099
+ return { instrumented, alreadyInstrumented: already, libOnly };
2100
+ }
1594
2101
  async function promptYesNo(question) {
1595
2102
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1596
2103
  return new Promise((resolve) => {
@@ -1685,24 +2192,21 @@ async function runOrchestrator(opts) {
1685
2192
  }
1686
2193
  console.log(`neat: ${opts.scanPath}`);
1687
2194
  console.log("");
1688
- const services = await discoverServices(opts.scanPath);
1689
- const languages = [...new Set(services.map((s) => s.node.language))].sort();
2195
+ const persisted = await extractAndPersist({
2196
+ scanPath: opts.scanPath,
2197
+ project: opts.project,
2198
+ projectExplicit: opts.projectExplicit
2199
+ });
2200
+ const { graph, services, languages } = persisted;
1690
2201
  result.steps.discovery = { services: services.length, languages };
1691
2202
  console.log(`discovered ${services.length} service(s) across ${languages.length} language(s)`);
1692
2203
  let runApply = !opts.noInstrument;
1693
2204
  if (runApply && !opts.yes && process.stdout.isTTY && process.stdin.isTTY) {
1694
2205
  runApply = await promptYesNo("instrument your services and open the dashboard?");
1695
2206
  }
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
2207
  result.steps.extraction = {
1704
- nodesAdded: extraction.nodesAdded,
1705
- edgesAdded: extraction.edgesAdded
2208
+ nodesAdded: persisted.nodesAdded,
2209
+ edgesAdded: persisted.edgesAdded
1706
2210
  };
1707
2211
  const gi = await ensureNeatOutIgnored(opts.scanPath);
1708
2212
  result.steps.gitignore = gi.action;
@@ -1727,24 +2231,11 @@ async function runOrchestrator(opts) {
1727
2231
  result.steps.apply.skipped = true;
1728
2232
  console.log("skipped instrumentation (--no-instrument)");
1729
2233
  } 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}`);
2234
+ const tally = await applyInstallersOver(services);
2235
+ result.steps.apply = { ...tally, skipped: false };
2236
+ console.log(
2237
+ `instrumented ${tally.instrumented}, already ${tally.alreadyInstrumented}, lib-only ${tally.libOnly}`
2238
+ );
1748
2239
  }
1749
2240
  const restPort = Number(process.env.PORT ?? 8080);
1750
2241
  const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
@@ -1793,8 +2284,8 @@ function printSummary(result, graph, dashboardUrl) {
1793
2284
  console.log(`dashboard: ${dashboardUrl}`);
1794
2285
  }
1795
2286
 
1796
- // src/cli.ts
1797
- import { DivergenceTypeSchema } from "@neat.is/types";
2287
+ // src/cli-verbs.ts
2288
+ import path7 from "path";
1798
2289
 
1799
2290
  // src/cli-client.ts
1800
2291
  import { Provenance as Provenance2 } from "@neat.is/types";
@@ -1814,13 +2305,16 @@ var TransportError = class extends Error {
1814
2305
  this.name = "TransportError";
1815
2306
  }
1816
2307
  };
1817
- function createHttpClient(baseUrl) {
2308
+ function createHttpClient(baseUrl, bearerToken) {
1818
2309
  const root = baseUrl.replace(/\/$/, "");
2310
+ const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
1819
2311
  return {
1820
- async get(path8) {
2312
+ async get(path9) {
1821
2313
  let res;
1822
2314
  try {
1823
- res = await fetch(`${root}${path8}`);
2315
+ res = await fetch(`${root}${path9}`, {
2316
+ headers: { ...authHeader }
2317
+ });
1824
2318
  } catch (err) {
1825
2319
  throw new TransportError(
1826
2320
  `cannot reach neat-core at ${root}: ${err.message}`
@@ -1830,18 +2324,18 @@ function createHttpClient(baseUrl) {
1830
2324
  const body = await res.text().catch(() => "");
1831
2325
  throw new HttpError(
1832
2326
  res.status,
1833
- `${res.status} ${res.statusText} on GET ${path8}: ${body}`,
2327
+ `${res.status} ${res.statusText} on GET ${path9}: ${body}`,
1834
2328
  body
1835
2329
  );
1836
2330
  }
1837
2331
  return await res.json();
1838
2332
  },
1839
- async post(path8, body) {
2333
+ async post(path9, body) {
1840
2334
  let res;
1841
2335
  try {
1842
- res = await fetch(`${root}${path8}`, {
2336
+ res = await fetch(`${root}${path9}`, {
1843
2337
  method: "POST",
1844
- headers: { "content-type": "application/json" },
2338
+ headers: { "content-type": "application/json", ...authHeader },
1845
2339
  body: JSON.stringify(body)
1846
2340
  });
1847
2341
  } catch (err) {
@@ -1853,7 +2347,7 @@ function createHttpClient(baseUrl) {
1853
2347
  const text = await res.text().catch(() => "");
1854
2348
  throw new HttpError(
1855
2349
  res.status,
1856
- `${res.status} ${res.statusText} on POST ${path8}: ${text}`,
2350
+ `${res.status} ${res.statusText} on POST ${path9}: ${text}`,
1857
2351
  text
1858
2352
  );
1859
2353
  }
@@ -1867,12 +2361,12 @@ function projectPath(project, suffix) {
1867
2361
  }
1868
2362
  async function runRootCause(client, input) {
1869
2363
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
1870
- const path8 = projectPath(
2364
+ const path9 = projectPath(
1871
2365
  input.project,
1872
2366
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
1873
2367
  );
1874
2368
  try {
1875
- const result = await client.get(path8);
2369
+ const result = await client.get(path9);
1876
2370
  const arrowPath = result.traversalPath.join(" \u2190 ");
1877
2371
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
1878
2372
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -1898,12 +2392,12 @@ async function runRootCause(client, input) {
1898
2392
  }
1899
2393
  async function runBlastRadius(client, input) {
1900
2394
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
1901
- const path8 = projectPath(
2395
+ const path9 = projectPath(
1902
2396
  input.project,
1903
2397
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
1904
2398
  );
1905
2399
  try {
1906
- const result = await client.get(path8);
2400
+ const result = await client.get(path9);
1907
2401
  if (result.totalAffected === 0) {
1908
2402
  return {
1909
2403
  summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
@@ -1937,12 +2431,12 @@ function formatBlastEntry(n) {
1937
2431
  }
1938
2432
  async function runDependencies(client, input) {
1939
2433
  const depth = input.depth ?? 3;
1940
- const path8 = projectPath(
2434
+ const path9 = projectPath(
1941
2435
  input.project,
1942
2436
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
1943
2437
  );
1944
2438
  try {
1945
- const result = await client.get(path8);
2439
+ const result = await client.get(path9);
1946
2440
  if (result.total === 0) {
1947
2441
  return {
1948
2442
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -2023,9 +2517,9 @@ function formatDuration(ms) {
2023
2517
  return `${Math.round(h / 24)}d`;
2024
2518
  }
2025
2519
  async function runIncidents(client, input) {
2026
- const path8 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
2520
+ const path9 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
2027
2521
  try {
2028
- const body = await client.get(path8);
2522
+ const body = await client.get(path9);
2029
2523
  const events = body.events;
2030
2524
  if (events.length === 0) {
2031
2525
  return {
@@ -2291,8 +2785,181 @@ function exitCodeForError(err) {
2291
2785
  if (err instanceof HttpError) return 1;
2292
2786
  return 1;
2293
2787
  }
2788
+ function createSnapshotPushClient(baseUrl, token) {
2789
+ return createHttpClient(baseUrl, token && token.length > 0 ? token : void 0);
2790
+ }
2791
+ async function pushSnapshotToRemote(input) {
2792
+ const client = createSnapshotPushClient(input.baseUrl, input.token);
2793
+ if (typeof client.post !== "function") {
2794
+ throw new Error("HttpClient does not support POST \u2014 required for snapshot push");
2795
+ }
2796
+ return client.post(
2797
+ `/projects/${encodeURIComponent(input.project)}/snapshot`,
2798
+ { snapshot: input.snapshot }
2799
+ );
2800
+ }
2801
+
2802
+ // src/cli-verbs.ts
2803
+ async function resolveProjectEntry(opts) {
2804
+ const entries = await listProjects();
2805
+ if (opts.project) {
2806
+ const match = entries.find((e) => e.name === opts.project);
2807
+ return match ?? null;
2808
+ }
2809
+ const cwd = opts.cwd ?? process.cwd();
2810
+ const resolvedCwd = await normalizeProjectPath(cwd);
2811
+ for (const entry2 of entries) {
2812
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path7.sep}`)) {
2813
+ return entry2;
2814
+ }
2815
+ }
2816
+ return null;
2817
+ }
2818
+ async function checkDaemonHealth2(baseUrl) {
2819
+ try {
2820
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/health`, {
2821
+ signal: AbortSignal.timeout(1500)
2822
+ });
2823
+ return res.ok;
2824
+ } catch {
2825
+ return false;
2826
+ }
2827
+ }
2828
+ function snapshotForGraph(persisted) {
2829
+ return {
2830
+ schemaVersion: 3,
2831
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
2832
+ graph: persisted.graph.export()
2833
+ };
2834
+ }
2835
+ function emitResult(result, json) {
2836
+ if (json) {
2837
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
2838
+ return;
2839
+ }
2840
+ const verb = result.mode === "dry-run" ? "dry-run" : result.mode === "remote" ? "pushed" : "synced";
2841
+ console.log(
2842
+ `${verb}: ${result.project} \u2014 ${result.nodesAdded} node(s) and ${result.edgesAdded} edge(s) added` + (result.snapshotPath ? `; snapshot at ${result.snapshotPath}` : "")
2843
+ );
2844
+ if (!result.apply.skipped) {
2845
+ const { instrumented, alreadyInstrumented, libOnly } = result.apply;
2846
+ console.log(
2847
+ `instrumented ${instrumented}, already ${alreadyInstrumented}, lib-only ${libOnly}`
2848
+ );
2849
+ } else {
2850
+ console.log("skipped instrumentation (--no-instrument)");
2851
+ }
2852
+ for (const warn of result.warnings) console.error(warn);
2853
+ }
2854
+ async function runSync(opts) {
2855
+ const entry2 = await resolveProjectEntry(opts);
2856
+ if (!entry2) {
2857
+ const target = opts.project ?? opts.cwd ?? process.cwd();
2858
+ console.error(
2859
+ `neat sync: no registered project ${opts.project ? `named "${opts.project}"` : `covers ${target}`}. Run \`neat <path>\` or \`neat init <path>\` first.`
2860
+ );
2861
+ return {
2862
+ exitCode: 1,
2863
+ project: opts.project ?? "",
2864
+ scanPath: target,
2865
+ nodesAdded: 0,
2866
+ edgesAdded: 0,
2867
+ snapshotPath: null,
2868
+ mode: opts.to ? "remote" : "local",
2869
+ daemon: "skipped",
2870
+ apply: { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, skipped: true },
2871
+ warnings: []
2872
+ };
2873
+ }
2874
+ const persisted = await extractAndPersist({
2875
+ scanPath: entry2.path,
2876
+ project: entry2.name,
2877
+ projectExplicit: true,
2878
+ dryRun: true
2879
+ });
2880
+ let snapshotPath = null;
2881
+ if (!opts.dryRun) {
2882
+ const target = persisted.snapshotPath;
2883
+ await saveGraphToDisk(persisted.graph, target);
2884
+ snapshotPath = target;
2885
+ }
2886
+ const skipApply = opts.dryRun || opts.noInstrument;
2887
+ const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services);
2888
+ const warnings = [];
2889
+ let daemonState = "skipped";
2890
+ let exitCode = 0;
2891
+ const mode = opts.dryRun ? "dry-run" : opts.to ? "remote" : "local";
2892
+ if (!opts.dryRun) {
2893
+ const snapshot = snapshotForGraph(persisted);
2894
+ if (opts.to) {
2895
+ const token = opts.token ?? process.env.NEAT_REMOTE_TOKEN;
2896
+ try {
2897
+ await pushSnapshotToRemote({
2898
+ baseUrl: opts.to,
2899
+ token,
2900
+ project: entry2.name,
2901
+ snapshot
2902
+ });
2903
+ daemonState = "remote-ok";
2904
+ } catch (err) {
2905
+ if (err instanceof HttpError) {
2906
+ console.error(`neat sync: ${err.message}`);
2907
+ exitCode = 1;
2908
+ } else if (err instanceof TransportError) {
2909
+ console.error(`neat sync: ${err.message}`);
2910
+ exitCode = 3;
2911
+ } else {
2912
+ console.error(`neat sync: ${err.message}`);
2913
+ exitCode = 1;
2914
+ }
2915
+ daemonState = "skipped";
2916
+ }
2917
+ } else {
2918
+ const daemonUrl = opts.daemonUrl ?? process.env.NEAT_API_URL ?? "http://localhost:8080";
2919
+ const healthy = await checkDaemonHealth2(daemonUrl);
2920
+ if (healthy) {
2921
+ try {
2922
+ await pushSnapshotToRemote({
2923
+ baseUrl: daemonUrl,
2924
+ token: process.env.NEAT_AUTH_TOKEN,
2925
+ project: entry2.name,
2926
+ snapshot
2927
+ });
2928
+ daemonState = "reloaded";
2929
+ } catch (err) {
2930
+ warnings.push(
2931
+ `neat sync: daemon merge failed \u2014 ${err.message}. Snapshot is on disk at ${snapshotPath}.`
2932
+ );
2933
+ daemonState = "down";
2934
+ exitCode = 2;
2935
+ }
2936
+ } else {
2937
+ warnings.push(
2938
+ "neat sync: daemon not running; snapshot updated, run `neatd start` to serve it"
2939
+ );
2940
+ daemonState = "down";
2941
+ exitCode = 2;
2942
+ }
2943
+ }
2944
+ }
2945
+ const result = {
2946
+ exitCode,
2947
+ project: entry2.name,
2948
+ scanPath: entry2.path,
2949
+ nodesAdded: persisted.nodesAdded,
2950
+ edgesAdded: persisted.edgesAdded,
2951
+ snapshotPath,
2952
+ mode,
2953
+ daemon: daemonState,
2954
+ apply: { ...applyTally, skipped: skipApply },
2955
+ warnings
2956
+ };
2957
+ emitResult(result, opts.json);
2958
+ return result;
2959
+ }
2294
2960
 
2295
2961
  // src/cli.ts
2962
+ import { DivergenceTypeSchema } from "@neat.is/types";
2296
2963
  function usage() {
2297
2964
  console.log("usage: neat <command> [args] [--project <name>]");
2298
2965
  console.log("");
@@ -2320,6 +2987,15 @@ function usage() {
2320
2987
  console.log(" deploy Detect the deploy substrate, generate NEAT_AUTH_TOKEN,");
2321
2988
  console.log(" emit a docker-compose / systemd / docker run artifact, and");
2322
2989
  console.log(" print the OTel env-vars block to paste into your platform.");
2990
+ console.log(" sync Re-run discovery, extraction, and SDK apply against the");
2991
+ console.log(" registered project, then notify the running daemon.");
2992
+ console.log(" Flags:");
2993
+ console.log(" --project <name> target a registered project by name");
2994
+ console.log(" --to <url> push the snapshot to a remote daemon");
2995
+ console.log(" --token <token> bearer token for --to (or $NEAT_REMOTE_TOKEN)");
2996
+ console.log(" --dry-run run extraction in-memory; do not write");
2997
+ console.log(" --no-instrument skip the SDK install apply step");
2998
+ console.log(" --json emit the delta summary as JSON");
2323
2999
  console.log("");
2324
3000
  console.log("query commands (mirror the MCP tools, ADR-050):");
2325
3001
  console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
@@ -2373,7 +3049,9 @@ var STRING_FLAGS = [
2373
3049
  ["--error-id", "errorId"],
2374
3050
  ["--hypothetical-action", "hypotheticalAction"],
2375
3051
  ["--type", "type"],
2376
- ["--min-confidence", "minConfidence"]
3052
+ ["--min-confidence", "minConfidence"],
3053
+ ["--to", "to"],
3054
+ ["--token", "token"]
2377
3055
  ];
2378
3056
  function parseArgs(rest) {
2379
3057
  const positional = [];
@@ -2398,6 +3076,8 @@ function parseArgs(rest) {
2398
3076
  hypotheticalAction: null,
2399
3077
  type: null,
2400
3078
  minConfidence: null,
3079
+ to: null,
3080
+ token: null,
2401
3081
  positional: []
2402
3082
  };
2403
3083
  for (let i = 0; i < rest.length; i++) {
@@ -2544,12 +3224,12 @@ async function runInit(opts) {
2544
3224
  printDiscoveryReport(opts, services);
2545
3225
  const sections = opts.noInstall ? [] : await buildPatchSections(services);
2546
3226
  const patch = renderPatch(sections);
2547
- const patchPath = path7.join(opts.scanPath, "neat.patch");
3227
+ const patchPath = path8.join(opts.scanPath, "neat.patch");
2548
3228
  if (opts.dryRun) {
2549
3229
  await fs7.writeFile(patchPath, patch, "utf8");
2550
3230
  written.push(patchPath);
2551
3231
  console.log(`dry-run: patch written to ${patchPath}`);
2552
- const gitignorePath = path7.join(opts.scanPath, ".gitignore");
3232
+ const gitignorePath = path8.join(opts.scanPath, ".gitignore");
2553
3233
  const gitignoreExists = await fs7.stat(gitignorePath).then(() => true).catch(() => false);
2554
3234
  const verb = gitignoreExists ? "append" : "create";
2555
3235
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
@@ -2561,9 +3241,9 @@ async function runInit(opts) {
2561
3241
  const graph = getGraph(graphKey);
2562
3242
  const projectPaths = pathsForProject(
2563
3243
  graphKey,
2564
- path7.join(opts.scanPath, "neat-out")
3244
+ path8.join(opts.scanPath, "neat-out")
2565
3245
  );
2566
- const errorsPath = path7.join(path7.dirname(opts.outPath), path7.basename(projectPaths.errorsPath));
3246
+ const errorsPath = path8.join(path8.dirname(opts.outPath), path8.basename(projectPaths.errorsPath));
2567
3247
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
2568
3248
  await saveGraphToDisk(graph, opts.outPath);
2569
3249
  written.push(opts.outPath);
@@ -2653,9 +3333,9 @@ var CLAUDE_SKILL_CONFIG = {
2653
3333
  };
2654
3334
  function claudeConfigPath() {
2655
3335
  const override = process.env.NEAT_CLAUDE_CONFIG;
2656
- if (override && override.length > 0) return path7.resolve(override);
3336
+ if (override && override.length > 0) return path8.resolve(override);
2657
3337
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
2658
- return path7.join(home, ".claude.json");
3338
+ return path8.join(home, ".claude.json");
2659
3339
  }
2660
3340
  async function runSkill(opts) {
2661
3341
  const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -2679,7 +3359,7 @@ async function runSkill(opts) {
2679
3359
  ...existing,
2680
3360
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
2681
3361
  };
2682
- await fs7.mkdir(path7.dirname(target), { recursive: true });
3362
+ await fs7.mkdir(path8.dirname(target), { recursive: true });
2683
3363
  await fs7.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
2684
3364
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
2685
3365
  console.log("restart Claude Code to pick up the new MCP server.");
@@ -2714,12 +3394,12 @@ async function main() {
2714
3394
  console.error("neat init: --apply and --dry-run are mutually exclusive");
2715
3395
  process.exit(2);
2716
3396
  }
2717
- const scanPath = path7.resolve(target);
3397
+ const scanPath = path8.resolve(target);
2718
3398
  const projectExplicit = parsed.project !== null;
2719
- const projectName = projectExplicit ? project : path7.basename(scanPath);
3399
+ const projectName = projectExplicit ? project : path8.basename(scanPath);
2720
3400
  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);
3401
+ const fallback = pathsForProject(projectKey, path8.join(scanPath, "neat-out")).snapshotPath;
3402
+ const outPath = path8.resolve(process.env.NEAT_OUT_PATH ?? fallback);
2723
3403
  const result = await runInit({
2724
3404
  scanPath,
2725
3405
  outPath,
@@ -2740,21 +3420,21 @@ async function main() {
2740
3420
  usage();
2741
3421
  process.exit(2);
2742
3422
  }
2743
- const scanPath = path7.resolve(target);
3423
+ const scanPath = path8.resolve(target);
2744
3424
  const stat = await fs7.stat(scanPath).catch(() => null);
2745
3425
  if (!stat || !stat.isDirectory()) {
2746
3426
  console.error(`neat watch: ${scanPath} is not a directory`);
2747
3427
  process.exit(2);
2748
3428
  }
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))
3429
+ const projectPaths = pathsForProject(project, path8.join(scanPath, "neat-out"));
3430
+ const outPath = path8.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
3431
+ const errorsPath = path8.resolve(
3432
+ process.env.NEAT_ERRORS_PATH ?? path8.join(path8.dirname(outPath), path8.basename(projectPaths.errorsPath))
2753
3433
  );
2754
- const staleEventsPath = path7.resolve(
2755
- process.env.NEAT_STALE_EVENTS_PATH ?? path7.join(path7.dirname(outPath), path7.basename(projectPaths.staleEventsPath))
3434
+ const staleEventsPath = path8.resolve(
3435
+ process.env.NEAT_STALE_EVENTS_PATH ?? path8.join(path8.dirname(outPath), path8.basename(projectPaths.staleEventsPath))
2756
3436
  );
2757
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path7.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
3437
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path8.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
2758
3438
  const handle = await startWatch(getGraph(project), {
2759
3439
  scanPath,
2760
3440
  outPath,
@@ -2873,6 +3553,18 @@ async function main() {
2873
3553
  console.log(` ${artifact.startCommand}`);
2874
3554
  return;
2875
3555
  }
3556
+ if (cmd === "sync") {
3557
+ const result = await runSync({
3558
+ ...parsed.project ? { project: parsed.project } : {},
3559
+ ...parsed.to ? { to: parsed.to } : {},
3560
+ ...parsed.token ? { token: parsed.token } : {},
3561
+ dryRun: parsed.dryRun,
3562
+ noInstrument: parsed.noInstrument,
3563
+ json: parsed.json
3564
+ });
3565
+ if (result.exitCode !== 0) process.exit(result.exitCode);
3566
+ return;
3567
+ }
2876
3568
  if (QUERY_VERBS.has(cmd)) {
2877
3569
  const code = await runQueryVerb(cmd, parsed);
2878
3570
  if (code !== 0) process.exit(code);
@@ -2888,11 +3580,11 @@ async function main() {
2888
3580
  process.exit(1);
2889
3581
  }
2890
3582
  async function tryOrchestrator(cmd, parsed) {
2891
- const scanPath = path7.resolve(cmd);
3583
+ const scanPath = path8.resolve(cmd);
2892
3584
  const stat = await fs7.stat(scanPath).catch(() => null);
2893
3585
  if (!stat || !stat.isDirectory()) return null;
2894
3586
  const projectExplicit = parsed.project !== null;
2895
- const projectName = projectExplicit ? parsed.project : path7.basename(scanPath);
3587
+ const projectName = projectExplicit ? parsed.project : path8.basename(scanPath);
2896
3588
  const result = await runOrchestrator({
2897
3589
  scanPath,
2898
3590
  project: projectName,