@neat.is/core 0.3.7 → 0.3.8

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
@@ -16,6 +16,7 @@ import {
16
16
  addServiceNodes,
17
17
  attachGraphToEventBus,
18
18
  buildApi,
19
+ computeDivergences,
19
20
  discoverServices,
20
21
  emitNeatEvent,
21
22
  ensureCompatLoaded,
@@ -39,21 +40,152 @@ import {
39
40
  setStatus,
40
41
  startPersistLoop,
41
42
  startStalenessLoop
42
- } from "./chunk-ZU2RQRCN.js";
43
+ } from "./chunk-CZ3T6TE2.js";
43
44
  import {
44
45
  startOtelGrpcReceiver
45
- } from "./chunk-CY67YKNO.js";
46
+ } from "./chunk-V4TU7OKZ.js";
46
47
  import {
48
+ __require,
47
49
  buildOtelReceiver
48
- } from "./chunk-75IBA4UL.js";
50
+ } from "./chunk-7TYESDAI.js";
49
51
 
50
52
  // src/cli.ts
51
- import path4 from "path";
52
- import { promises as fs4 } from "fs";
53
+ import path7 from "path";
54
+ import { promises as fs7 } from "fs";
53
55
 
54
- // src/watch.ts
55
- import fs from "fs";
56
+ // src/gitignore.ts
57
+ import { promises as fs } from "fs";
56
58
  import path from "path";
59
+ var NEAT_OUT_LINE = "neat-out/";
60
+ var NEAT_HEADER = "# NEAT \u2014 machine-local snapshots and events";
61
+ function isNeatOutLine(line) {
62
+ const trimmed = line.trim();
63
+ return trimmed === "neat-out/" || trimmed === "neat-out";
64
+ }
65
+ async function ensureNeatOutIgnored(projectDir) {
66
+ const file = path.join(projectDir, ".gitignore");
67
+ let existing = null;
68
+ try {
69
+ existing = await fs.readFile(file, "utf8");
70
+ } catch (err) {
71
+ if (err.code !== "ENOENT") throw err;
72
+ }
73
+ if (existing === null) {
74
+ await fs.writeFile(file, `${NEAT_HEADER}
75
+ ${NEAT_OUT_LINE}
76
+ `, "utf8");
77
+ return { action: "created", file };
78
+ }
79
+ for (const line of existing.split(/\r?\n/)) {
80
+ if (isNeatOutLine(line)) return { action: "unchanged", file };
81
+ }
82
+ const needsLeadingNewline = existing.length > 0 && !existing.endsWith("\n");
83
+ const appended = `${needsLeadingNewline ? "\n" : ""}
84
+ ${NEAT_HEADER}
85
+ ${NEAT_OUT_LINE}
86
+ `;
87
+ await fs.writeFile(file, existing + appended, "utf8");
88
+ return { action: "added", file };
89
+ }
90
+
91
+ // src/summary.ts
92
+ import { NodeType, Provenance } from "@neat.is/types";
93
+ function renderOtelEnvBlock() {
94
+ return [
95
+ "for prod OTel routing, set these in your deploy platform's env:",
96
+ " OTEL_EXPORTER_OTLP_ENDPOINT=https://<your-neat-host>:4318",
97
+ " OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer <NEAT_AUTH_TOKEN>"
98
+ ].join("\n");
99
+ }
100
+ function findIncompatServices(nodes) {
101
+ return nodes.filter(
102
+ (n) => n.type === NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
103
+ );
104
+ }
105
+ function servicesWithoutObserved(nodes, edges) {
106
+ const seen = /* @__PURE__ */ new Set();
107
+ for (const e of edges) {
108
+ if (e.provenance === Provenance.OBSERVED) {
109
+ seen.add(e.source);
110
+ seen.add(e.target);
111
+ }
112
+ }
113
+ return nodes.filter(
114
+ (n) => n.type === NodeType.ServiceNode && !seen.has(n.id)
115
+ );
116
+ }
117
+ function formatDivergence(d) {
118
+ const conf = d.confidence.toFixed(2);
119
+ return ` [${conf}] ${d.type} ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
120
+ }
121
+ function renderValueForwardSummary(input) {
122
+ const { graph, divergences, verbose } = input;
123
+ const nodes = [];
124
+ graph.forEachNode((_id, attrs) => nodes.push(attrs));
125
+ const edges = [];
126
+ graph.forEachEdge((_id, attrs) => edges.push(attrs));
127
+ const lines = [];
128
+ lines.push("=== neat: findings ===");
129
+ lines.push("");
130
+ const incompatServices = findIncompatServices(nodes);
131
+ const totalIncompats = incompatServices.reduce(
132
+ (acc, s) => acc + (s.incompatibilities?.length ?? 0),
133
+ 0
134
+ );
135
+ lines.push(`compat violations: ${totalIncompats}`);
136
+ for (const svc of incompatServices) {
137
+ for (const inc of svc.incompatibilities ?? []) {
138
+ const detail = formatIncompat(inc);
139
+ lines.push(` ${svc.name}: ${detail}`);
140
+ }
141
+ }
142
+ lines.push("");
143
+ const top = [...divergences].sort((a, b) => b.confidence - a.confidence).slice(0, 3);
144
+ lines.push(`top divergences: ${divergences.length} total${top.length > 0 ? ", top 3:" : ""}`);
145
+ for (const d of top) lines.push(formatDivergence(d));
146
+ lines.push("");
147
+ const noObserved = servicesWithoutObserved(nodes, edges);
148
+ if (noObserved.length > 0) {
149
+ lines.push(`services without OBSERVED coverage: ${noObserved.length}`);
150
+ for (const svc of noObserved) lines.push(` ${svc.name}`);
151
+ lines.push(" \u2192 run your services with the generated otel-init to populate OBSERVED edges.");
152
+ lines.push("");
153
+ }
154
+ lines.push(renderOtelEnvBlock());
155
+ lines.push("");
156
+ if (verbose) {
157
+ const byNode = /* @__PURE__ */ new Map();
158
+ for (const n of nodes) byNode.set(n.type, (byNode.get(n.type) ?? 0) + 1);
159
+ const byEdge = /* @__PURE__ */ new Map();
160
+ for (const e of edges) byEdge.set(e.type, (byEdge.get(e.type) ?? 0) + 1);
161
+ lines.push("=== graph (verbose) ===");
162
+ lines.push(`total: ${graph.order} nodes, ${graph.size} edges`);
163
+ lines.push("nodes:");
164
+ for (const [t, c] of [...byNode.entries()].sort()) lines.push(` ${t}: ${c}`);
165
+ lines.push("edges:");
166
+ for (const [t, c] of [...byEdge.entries()].sort()) lines.push(` ${t}: ${c}`);
167
+ lines.push("");
168
+ }
169
+ return lines.join("\n");
170
+ }
171
+ function formatIncompat(inc) {
172
+ if (inc.kind === "node-engine") {
173
+ const range = inc.declaredNodeEngine ? ` (engines.node="${inc.declaredNodeEngine}")` : "";
174
+ return `${inc.package}@${inc.packageVersion ?? "?"} requires Node ${inc.requiredNodeVersion}${range} \u2014 ${inc.reason}`;
175
+ }
176
+ if (inc.kind === "package-conflict") {
177
+ const found = inc.foundVersion ? `@${inc.foundVersion}` : " (missing)";
178
+ return `${inc.package}@${inc.packageVersion ?? "?"} requires ${inc.requires.name}>=${inc.requires.minVersion}; found ${inc.requires.name}${found} \u2014 ${inc.reason}`;
179
+ }
180
+ if (inc.kind === "deprecated-api") {
181
+ return `${inc.package}@${inc.packageVersion ?? "?"} is deprecated \u2014 ${inc.reason}`;
182
+ }
183
+ return `${inc.driver}@${inc.driverVersion} vs ${inc.engine} ${inc.engineVersion} \u2014 ${inc.reason}`;
184
+ }
185
+
186
+ // src/watch.ts
187
+ import fs2 from "fs";
188
+ import path2 from "path";
57
189
  import chokidar from "chokidar";
58
190
  var ALL_PHASES = [
59
191
  "services",
@@ -65,8 +197,8 @@ var ALL_PHASES = [
65
197
  ];
66
198
  function classifyChange(relPath) {
67
199
  const phases = /* @__PURE__ */ new Set();
68
- const base = path.basename(relPath).toLowerCase();
69
- const segments = relPath.split(path.sep).map((s) => s.toLowerCase());
200
+ const base = path2.basename(relPath).toLowerCase();
201
+ const segments = relPath.split(path2.sep).map((s) => s.toLowerCase());
70
202
  if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
71
203
  phases.add("services");
72
204
  phases.add("aliases");
@@ -161,16 +293,16 @@ function countWatchableDirs(scanPath, limit) {
161
293
  if (count >= limit) return;
162
294
  let entries;
163
295
  try {
164
- entries = fs.readdirSync(dir, { withFileTypes: true });
296
+ entries = fs2.readdirSync(dir, { withFileTypes: true });
165
297
  } catch {
166
298
  return;
167
299
  }
168
300
  for (const e of entries) {
169
301
  if (count >= limit) return;
170
302
  if (!e.isDirectory()) continue;
171
- if (IGNORED_WATCH_PATHS.some((re) => re.test(path.join(dir, e.name) + path.sep))) continue;
303
+ if (IGNORED_WATCH_PATHS.some((re) => re.test(path2.join(dir, e.name) + path2.sep))) continue;
172
304
  count++;
173
- if (depth < 2) visit(path.join(dir, e.name), depth + 1);
305
+ if (depth < 2) visit(path2.join(dir, e.name), depth + 1);
174
306
  }
175
307
  };
176
308
  visit(scanPath, 0);
@@ -188,8 +320,8 @@ async function startWatch(graph, opts) {
188
320
  const projectName = opts.project ?? DEFAULT_PROJECT;
189
321
  await loadGraphFromDisk(graph, opts.outPath);
190
322
  const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
191
- const policyFilePath = path.join(opts.scanPath, "policy.json");
192
- const policyViolationsPath = path.join(path.dirname(opts.outPath), "policy-violations.ndjson");
323
+ const policyFilePath = path2.join(opts.scanPath, "policy.json");
324
+ const policyViolationsPath = path2.join(path2.dirname(opts.outPath), "policy-violations.ndjson");
193
325
  let policies = [];
194
326
  try {
195
327
  policies = await loadPolicyFile(policyFilePath);
@@ -238,7 +370,7 @@ async function startWatch(graph, opts) {
238
370
  const host = opts.host ?? "0.0.0.0";
239
371
  const port = opts.port ?? 8080;
240
372
  const otelPort = opts.otelPort ?? 4318;
241
- const cachePath = opts.embeddingsCachePath ?? path.join(path.dirname(opts.outPath), "embeddings.json");
373
+ const cachePath = opts.embeddingsCachePath ?? path2.join(path2.dirname(opts.outPath), "embeddings.json");
242
374
  let searchIndex;
243
375
  try {
244
376
  searchIndex = await buildSearchIndex(graph, { cachePath });
@@ -256,7 +388,7 @@ async function startWatch(graph, opts) {
256
388
  // Paths are derived from the explicit options the watch caller passes
257
389
  // — pathsForProject is only used to fill in the embeddings/snapshot
258
390
  // fields so the registry shape is complete.
259
- ...pathsForProject(projectName, path.dirname(opts.outPath)),
391
+ ...pathsForProject(projectName, path2.dirname(opts.outPath)),
260
392
  snapshotPath: opts.outPath,
261
393
  errorsPath: opts.errorsPath,
262
394
  staleEventsPath: opts.staleEventsPath
@@ -341,9 +473,9 @@ async function startWatch(graph, opts) {
341
473
  };
342
474
  const onPath = (absPath) => {
343
475
  if (shouldIgnore(absPath)) return;
344
- const rel = path.relative(opts.scanPath, absPath);
476
+ const rel = path2.relative(opts.scanPath, absPath);
345
477
  if (!rel || rel.startsWith("..")) return;
346
- pendingPaths.add(rel.split(path.sep).join("/"));
478
+ pendingPaths.add(rel.split(path2.sep).join("/"));
347
479
  const phases = classifyChange(rel);
348
480
  if (phases.size === 0) {
349
481
  for (const p of ALL_PHASES) pending.add(p);
@@ -395,9 +527,153 @@ async function startWatch(graph, opts) {
395
527
  return { api, stop };
396
528
  }
397
529
 
530
+ // src/deploy/detect.ts
531
+ import { promises as fs3 } from "fs";
532
+ import path3 from "path";
533
+ import { spawn } from "child_process";
534
+ import { randomBytes } from "crypto";
535
+ function generateToken() {
536
+ return randomBytes(32).toString("base64url");
537
+ }
538
+ async function probeBinary(binary, arg) {
539
+ return new Promise((resolve) => {
540
+ const child = spawn(binary, [arg], { stdio: "ignore" });
541
+ const timer = setTimeout(() => {
542
+ child.kill("SIGKILL");
543
+ resolve(false);
544
+ }, 2e3);
545
+ child.once("error", () => {
546
+ clearTimeout(timer);
547
+ resolve(false);
548
+ });
549
+ child.once("exit", (code) => {
550
+ clearTimeout(timer);
551
+ resolve(code === 0);
552
+ });
553
+ });
554
+ }
555
+ async function detectSubstrate(opts = {}) {
556
+ const hasDocker = opts.hasDocker ?? (() => probeBinary("docker", "version"));
557
+ const hasSystemd = opts.hasSystemd ?? (() => probeBinary("systemctl", "--version"));
558
+ if (await hasDocker()) return "docker-compose";
559
+ if (await hasSystemd()) return "systemd";
560
+ return "docker-run";
561
+ }
562
+ var IMAGE = "ghcr.io/neat-technologies/neat:latest";
563
+ function emitDockerCompose(cwd) {
564
+ return [
565
+ "services:",
566
+ " neat:",
567
+ ` image: ${IMAGE}`,
568
+ " restart: unless-stopped",
569
+ " environment:",
570
+ " NEAT_AUTH_TOKEN: ${NEAT_AUTH_TOKEN:?NEAT_AUTH_TOKEN must be set}",
571
+ " ports:",
572
+ ' - "8080:8080"',
573
+ ' - "4318:4318"',
574
+ ' - "6328:6328"',
575
+ " volumes:",
576
+ ` - ${cwd}:/workspace`,
577
+ " - ./neat-data:/neat-out",
578
+ ""
579
+ ].join("\n");
580
+ }
581
+ function emitSystemdUnit(cwd) {
582
+ return [
583
+ "# NEAT_AUTH_TOKEN is read from /etc/neat/neatd.env at startup.",
584
+ "# Put `NEAT_AUTH_TOKEN=<value>` in that file with mode 0640 root:root.",
585
+ "[Unit]",
586
+ "Description=NEAT daemon",
587
+ "After=network-online.target",
588
+ "Wants=network-online.target",
589
+ "",
590
+ "[Service]",
591
+ "Type=simple",
592
+ "ExecStart=/usr/local/bin/neatd start --foreground",
593
+ `WorkingDirectory=${cwd}`,
594
+ "EnvironmentFile=/etc/neat/neatd.env",
595
+ "Restart=always",
596
+ "RestartSec=5",
597
+ "",
598
+ "[Install]",
599
+ "WantedBy=multi-user.target",
600
+ ""
601
+ ].join("\n");
602
+ }
603
+ function emitDockerRunSnippet() {
604
+ return [
605
+ "#!/usr/bin/env bash",
606
+ "set -euo pipefail",
607
+ "",
608
+ "# Generate a fresh token once, then store it where your secrets live.",
609
+ ': "${NEAT_AUTH_TOKEN:?NEAT_AUTH_TOKEN must be set}"',
610
+ "",
611
+ `docker run -d --name neat \\`,
612
+ ' -e NEAT_AUTH_TOKEN="$NEAT_AUTH_TOKEN" \\',
613
+ " -p 8080:8080 -p 4318:4318 -p 6328:6328 \\",
614
+ ' -v "$PWD":/workspace -v /var/lib/neat:/neat-out \\',
615
+ ` ${IMAGE}`,
616
+ ""
617
+ ].join("\n");
618
+ }
619
+ function renderOtelEnvBlock2(token, host = "<host>") {
620
+ return [
621
+ `OTEL_EXPORTER_OTLP_ENDPOINT=https://${host}:4318`,
622
+ `OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${token}`,
623
+ "OTEL_SERVICE_NAME=<service>"
624
+ ].join("\n");
625
+ }
626
+ async function runDeploy(opts = {}) {
627
+ const cwd = opts.cwd ?? process.cwd();
628
+ const substrate = await detectSubstrate(opts);
629
+ const token = generateToken();
630
+ switch (substrate) {
631
+ case "docker-compose": {
632
+ const artifactPath = path3.join(cwd, "docker-compose.neat.yml");
633
+ const contents = emitDockerCompose(cwd);
634
+ await fs3.writeFile(artifactPath, contents, "utf8");
635
+ return {
636
+ substrate,
637
+ artifactPath,
638
+ token,
639
+ contents,
640
+ startCommand: `NEAT_AUTH_TOKEN=${token} docker compose -f ${path3.basename(artifactPath)} up -d`
641
+ };
642
+ }
643
+ case "systemd": {
644
+ const artifactPath = path3.join(cwd, "neat.service");
645
+ const contents = emitSystemdUnit(cwd);
646
+ await fs3.writeFile(artifactPath, contents, "utf8");
647
+ return {
648
+ substrate,
649
+ artifactPath,
650
+ token,
651
+ contents,
652
+ startCommand: [
653
+ `sudo install -m 0640 -o root -g root <(echo NEAT_AUTH_TOKEN=${token}) /etc/neat/neatd.env`,
654
+ `sudo install -m 0644 neat.service /etc/systemd/system/neat.service`,
655
+ "sudo systemctl daemon-reload && sudo systemctl enable --now neat"
656
+ ].join(" && ")
657
+ };
658
+ }
659
+ case "docker-run":
660
+ default: {
661
+ const contents = emitDockerRunSnippet();
662
+ return {
663
+ substrate: "docker-run",
664
+ token,
665
+ contents,
666
+ startCommand: `NEAT_AUTH_TOKEN=${token} bash <(cat <<'EOF'
667
+ ${contents}EOF
668
+ )`
669
+ };
670
+ }
671
+ }
672
+ }
673
+
398
674
  // src/installers/javascript.ts
399
- import { promises as fs2 } from "fs";
400
- import path2 from "path";
675
+ import { promises as fs4 } from "fs";
676
+ import path4 from "path";
401
677
 
402
678
  // src/installers/templates.ts
403
679
  var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
@@ -444,6 +720,57 @@ function renderEnvNeat(serviceName) {
444
720
  ""
445
721
  ].join("\n");
446
722
  }
723
+ var NEXT_INSTRUMENTATION_HEADER = "// Generated by `neat init --apply` (ADR-073). Next.js instrumentation hook.";
724
+ var NEXT_INSTRUMENTATION_TS = `${NEXT_INSTRUMENTATION_HEADER}
725
+ export async function register() {
726
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
727
+ await import('./instrumentation.node')
728
+ }
729
+ }
730
+ `;
731
+ var NEXT_INSTRUMENTATION_JS = `${NEXT_INSTRUMENTATION_HEADER}
732
+ export async function register() {
733
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
734
+ await import('./instrumentation.node')
735
+ }
736
+ }
737
+ `;
738
+ var NEXT_INSTRUMENTATION_NODE_TS = `${NEXT_INSTRUMENTATION_HEADER}
739
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
740
+ // the OTel SDK starts so the exporter target and service name are in scope.
741
+ import { fileURLToPath } from 'node:url'
742
+ import path from 'node:path'
743
+ import dotenv from 'dotenv'
744
+ import { NodeSDK } from '@opentelemetry/sdk-node'
745
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
746
+
747
+ const here = path.dirname(fileURLToPath(import.meta.url))
748
+ dotenv.config({ path: path.join(here, '.env.neat') })
749
+
750
+ const sdk = new NodeSDK({
751
+ serviceName: process.env.OTEL_SERVICE_NAME,
752
+ instrumentations: [getNodeAutoInstrumentations()],
753
+ })
754
+ sdk.start()
755
+ `;
756
+ var NEXT_INSTRUMENTATION_NODE_JS = `${NEXT_INSTRUMENTATION_HEADER}
757
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
758
+ // the OTel SDK starts so the exporter target and service name are in scope.
759
+ import { fileURLToPath } from 'node:url'
760
+ import path from 'node:path'
761
+ import dotenv from 'dotenv'
762
+ import { NodeSDK } from '@opentelemetry/sdk-node'
763
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
764
+
765
+ const here = path.dirname(fileURLToPath(import.meta.url))
766
+ dotenv.config({ path: path.join(here, '.env.neat') })
767
+
768
+ const sdk = new NodeSDK({
769
+ serviceName: process.env.OTEL_SERVICE_NAME,
770
+ instrumentations: [getNodeAutoInstrumentations()],
771
+ })
772
+ sdk.start()
773
+ `;
447
774
 
448
775
  // src/installers/javascript.ts
449
776
  var SDK_PACKAGES = [
@@ -466,7 +793,7 @@ var OTEL_ENV = {
466
793
  };
467
794
  async function readPackageJson(serviceDir) {
468
795
  try {
469
- const raw = await fs2.readFile(path2.join(serviceDir, "package.json"), "utf8");
796
+ const raw = await fs4.readFile(path4.join(serviceDir, "package.json"), "utf8");
470
797
  return JSON.parse(raw);
471
798
  } catch {
472
799
  return null;
@@ -474,7 +801,7 @@ async function readPackageJson(serviceDir) {
474
801
  }
475
802
  async function exists(p) {
476
803
  try {
477
- await fs2.stat(p);
804
+ await fs4.stat(p);
478
805
  return true;
479
806
  } catch {
480
807
  return false;
@@ -484,6 +811,28 @@ async function detect(serviceDir) {
484
811
  const pkg = await readPackageJson(serviceDir);
485
812
  return pkg !== null && typeof pkg.name === "string";
486
813
  }
814
+ var NEXT_CONFIG_CANDIDATES = ["next.config.js", "next.config.ts", "next.config.mjs"];
815
+ async function findNextConfig(serviceDir) {
816
+ for (const name of NEXT_CONFIG_CANDIDATES) {
817
+ const candidate = path4.join(serviceDir, name);
818
+ if (await exists(candidate)) return candidate;
819
+ }
820
+ return null;
821
+ }
822
+ function hasNextDependency(pkg) {
823
+ return pkg.dependencies?.next !== void 0 || pkg.devDependencies?.next !== void 0;
824
+ }
825
+ function parseNextMajor(range) {
826
+ if (!range) return null;
827
+ const cleaned = range.trim().replace(/^[\^~>=<\s]+/, "");
828
+ const match = cleaned.match(/^(\d+)/);
829
+ if (!match) return null;
830
+ const n = Number(match[1]);
831
+ return Number.isFinite(n) ? n : null;
832
+ }
833
+ async function isTypeScriptProject(serviceDir) {
834
+ return exists(path4.join(serviceDir, "tsconfig.json"));
835
+ }
487
836
  var INDEX_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".cjs"];
488
837
  var INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `index${ext}`);
489
838
  var SRC_INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `src/index${ext}`);
@@ -528,7 +877,7 @@ function entryFromScript(script) {
528
877
  }
529
878
  async function resolveEntry(serviceDir, pkg) {
530
879
  if (typeof pkg.main === "string" && pkg.main.length > 0) {
531
- const candidate = path2.resolve(serviceDir, pkg.main);
880
+ const candidate = path4.resolve(serviceDir, pkg.main);
532
881
  if (await exists(candidate)) return candidate;
533
882
  }
534
883
  if (pkg.bin) {
@@ -542,36 +891,36 @@ async function resolveEntry(serviceDir, pkg) {
542
891
  if (typeof first === "string") binEntry = first;
543
892
  }
544
893
  if (binEntry) {
545
- const candidate = path2.resolve(serviceDir, binEntry);
894
+ const candidate = path4.resolve(serviceDir, binEntry);
546
895
  if (await exists(candidate)) return candidate;
547
896
  }
548
897
  }
549
898
  const startEntry = entryFromScript(pkg.scripts?.start);
550
899
  if (startEntry) {
551
- const candidate = path2.resolve(serviceDir, startEntry);
900
+ const candidate = path4.resolve(serviceDir, startEntry);
552
901
  if (await exists(candidate)) return candidate;
553
902
  }
554
903
  const devEntry = entryFromScript(pkg.scripts?.dev);
555
904
  if (devEntry) {
556
- const candidate = path2.resolve(serviceDir, devEntry);
905
+ const candidate = path4.resolve(serviceDir, devEntry);
557
906
  if (await exists(candidate)) return candidate;
558
907
  }
559
908
  for (const rel of SRC_INDEX_CANDIDATES) {
560
- const candidate = path2.join(serviceDir, rel);
909
+ const candidate = path4.join(serviceDir, rel);
561
910
  if (await exists(candidate)) return candidate;
562
911
  }
563
912
  for (const rel of SRC_NAMED_CANDIDATES) {
564
- const candidate = path2.join(serviceDir, rel);
913
+ const candidate = path4.join(serviceDir, rel);
565
914
  if (await exists(candidate)) return candidate;
566
915
  }
567
916
  for (const name of INDEX_CANDIDATES) {
568
- const candidate = path2.join(serviceDir, name);
917
+ const candidate = path4.join(serviceDir, name);
569
918
  if (await exists(candidate)) return candidate;
570
919
  }
571
920
  return null;
572
921
  }
573
922
  function dispatchEntry(entryFile, pkg) {
574
- const ext = path2.extname(entryFile).toLowerCase();
923
+ const ext = path4.extname(entryFile).toLowerCase();
575
924
  if (ext === ".ts" || ext === ".tsx") return "ts";
576
925
  if (ext === ".mjs") return "esm";
577
926
  if (ext === ".cjs") return "cjs";
@@ -588,9 +937,9 @@ function otelInitContents(flavor) {
588
937
  return OTEL_INIT_CJS;
589
938
  }
590
939
  function injectionLine(flavor, entryFile, otelInitFile) {
591
- let rel = path2.relative(path2.dirname(entryFile), otelInitFile);
940
+ let rel = path4.relative(path4.dirname(entryFile), otelInitFile);
592
941
  if (!rel.startsWith(".")) rel = `./${rel}`;
593
- rel = rel.split(path2.sep).join("/");
942
+ rel = rel.split(path4.sep).join("/");
594
943
  if (flavor === "cjs") return `require('${rel}')`;
595
944
  if (flavor === "esm") return `import '${rel}'`;
596
945
  const tsRel = rel.replace(/\.ts$/, "");
@@ -601,9 +950,88 @@ function lineIsOtelInjection(line) {
601
950
  if (trimmed.length === 0) return false;
602
951
  return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
603
952
  }
953
+ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
954
+ const useTs = await isTypeScriptProject(serviceDir);
955
+ const instrumentationFile = path4.join(serviceDir, useTs ? "instrumentation.ts" : "instrumentation.js");
956
+ const instrumentationNodeFile = path4.join(
957
+ serviceDir,
958
+ useTs ? "instrumentation.node.ts" : "instrumentation.node.js"
959
+ );
960
+ const envNeatFile = path4.join(serviceDir, ".env.neat");
961
+ const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
962
+ const dependencyEdits = [];
963
+ for (const sdk of SDK_PACKAGES) {
964
+ if (sdk.name in existingDeps) continue;
965
+ dependencyEdits.push({
966
+ file: manifestPath,
967
+ kind: "add",
968
+ name: sdk.name,
969
+ version: sdk.version
970
+ });
971
+ }
972
+ const generatedFiles = [];
973
+ if (!await exists(instrumentationFile)) {
974
+ generatedFiles.push({
975
+ file: instrumentationFile,
976
+ contents: useTs ? NEXT_INSTRUMENTATION_TS : NEXT_INSTRUMENTATION_JS,
977
+ skipIfExists: true
978
+ });
979
+ }
980
+ if (!await exists(instrumentationNodeFile)) {
981
+ generatedFiles.push({
982
+ file: instrumentationNodeFile,
983
+ contents: useTs ? NEXT_INSTRUMENTATION_NODE_TS : NEXT_INSTRUMENTATION_NODE_JS,
984
+ skipIfExists: true
985
+ });
986
+ }
987
+ if (!await exists(envNeatFile)) {
988
+ generatedFiles.push({
989
+ file: envNeatFile,
990
+ contents: renderEnvNeat(pkg.name ?? path4.basename(serviceDir)),
991
+ skipIfExists: true
992
+ });
993
+ }
994
+ let nextConfigEdit;
995
+ const nextRange = pkg.dependencies?.next ?? pkg.devDependencies?.next;
996
+ const nextMajor = parseNextMajor(nextRange);
997
+ if (nextMajor !== null && nextMajor < 15) {
998
+ try {
999
+ const raw = await fs4.readFile(nextConfigPath, "utf8");
1000
+ if (!raw.includes("instrumentationHook")) {
1001
+ nextConfigEdit = {
1002
+ file: nextConfigPath,
1003
+ reason: `enable experimental.instrumentationHook (Next ${nextMajor} requires the opt-in flag)`
1004
+ };
1005
+ }
1006
+ } catch {
1007
+ }
1008
+ }
1009
+ const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && nextConfigEdit === void 0;
1010
+ if (empty) {
1011
+ return {
1012
+ language: "javascript",
1013
+ serviceDir,
1014
+ dependencyEdits: [],
1015
+ entrypointEdits: [],
1016
+ envEdits: [],
1017
+ generatedFiles: [],
1018
+ framework: "next"
1019
+ };
1020
+ }
1021
+ return {
1022
+ language: "javascript",
1023
+ serviceDir,
1024
+ dependencyEdits,
1025
+ entrypointEdits: [],
1026
+ envEdits: [OTEL_ENV],
1027
+ generatedFiles,
1028
+ framework: "next",
1029
+ ...nextConfigEdit ? { nextConfigEdit } : {}
1030
+ };
1031
+ }
604
1032
  async function plan(serviceDir) {
605
1033
  const pkg = await readPackageJson(serviceDir);
606
- const manifestPath = path2.join(serviceDir, "package.json");
1034
+ const manifestPath = path4.join(serviceDir, "package.json");
607
1035
  const empty = {
608
1036
  language: "javascript",
609
1037
  serviceDir,
@@ -613,13 +1041,19 @@ async function plan(serviceDir) {
613
1041
  generatedFiles: []
614
1042
  };
615
1043
  if (!pkg) return empty;
1044
+ if (hasNextDependency(pkg)) {
1045
+ const nextConfig = await findNextConfig(serviceDir);
1046
+ if (nextConfig) {
1047
+ return planNext(serviceDir, pkg, manifestPath, nextConfig);
1048
+ }
1049
+ }
616
1050
  const entryFile = await resolveEntry(serviceDir, pkg);
617
1051
  if (!entryFile) {
618
1052
  return { ...empty, libOnly: true };
619
1053
  }
620
1054
  const flavor = dispatchEntry(entryFile, pkg);
621
- const otelInitFile = path2.join(path2.dirname(entryFile), otelInitFilename(flavor));
622
- const envNeatFile = path2.join(serviceDir, ".env.neat");
1055
+ const otelInitFile = path4.join(path4.dirname(entryFile), otelInitFilename(flavor));
1056
+ const envNeatFile = path4.join(serviceDir, ".env.neat");
623
1057
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
624
1058
  const dependencyEdits = [];
625
1059
  for (const sdk of SDK_PACKAGES) {
@@ -633,7 +1067,7 @@ async function plan(serviceDir) {
633
1067
  }
634
1068
  const entrypointEdits = [];
635
1069
  try {
636
- const raw = await fs2.readFile(entryFile, "utf8");
1070
+ const raw = await fs4.readFile(entryFile, "utf8");
637
1071
  const lines = raw.split(/\r?\n/);
638
1072
  const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
639
1073
  if (!lineIsOtelInjection(firstReal)) {
@@ -658,7 +1092,7 @@ async function plan(serviceDir) {
658
1092
  if (!await exists(envNeatFile)) {
659
1093
  generatedFiles.push({
660
1094
  file: envNeatFile,
661
- contents: renderEnvNeat(pkg.name ?? path2.basename(serviceDir)),
1095
+ contents: renderEnvNeat(pkg.name ?? path4.basename(serviceDir)),
662
1096
  skipIfExists: true
663
1097
  });
664
1098
  }
@@ -677,18 +1111,20 @@ async function plan(serviceDir) {
677
1111
  };
678
1112
  }
679
1113
  function isAllowedWritePath(serviceDir, target) {
680
- const rel = path2.relative(serviceDir, target);
1114
+ const rel = path4.relative(serviceDir, target);
681
1115
  if (rel.startsWith("..")) return false;
682
- const base = path2.basename(target);
1116
+ const base = path4.basename(target);
683
1117
  if (base === "package.json") return true;
684
1118
  if (base === ".env.neat") return true;
685
1119
  if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
1120
+ if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
1121
+ if (/^next\.config\.(?:js|mjs|ts)$/.test(base)) return true;
686
1122
  return false;
687
1123
  }
688
1124
  async function writeAtomic(file, contents) {
689
1125
  const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
690
- await fs2.writeFile(tmp, contents, "utf8");
691
- await fs2.rename(tmp, file);
1126
+ await fs4.writeFile(tmp, contents, "utf8");
1127
+ await fs4.rename(tmp, file);
692
1128
  }
693
1129
  async function apply(installPlan) {
694
1130
  const { serviceDir } = installPlan;
@@ -700,7 +1136,7 @@ async function apply(installPlan) {
700
1136
  writtenFiles: []
701
1137
  };
702
1138
  }
703
- if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0) {
1139
+ if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0 && installPlan.nextConfigEdit === void 0) {
704
1140
  return {
705
1141
  serviceDir,
706
1142
  outcome: "already-instrumented",
@@ -711,6 +1147,7 @@ async function apply(installPlan) {
711
1147
  for (const d of installPlan.dependencyEdits) allTargets.add(d.file);
712
1148
  for (const e of installPlan.entrypointEdits) allTargets.add(e.file);
713
1149
  for (const g of installPlan.generatedFiles ?? []) allTargets.add(g.file);
1150
+ if (installPlan.nextConfigEdit) allTargets.add(installPlan.nextConfigEdit.file);
714
1151
  for (const target of allTargets) {
715
1152
  const isEntryEdit = installPlan.entrypointEdits.some((e) => e.file === target);
716
1153
  if (isEntryEdit) continue;
@@ -725,7 +1162,7 @@ async function apply(installPlan) {
725
1162
  for (const target of allTargets) {
726
1163
  if (await exists(target)) {
727
1164
  try {
728
- originals.set(target, await fs2.readFile(target, "utf8"));
1165
+ originals.set(target, await fs4.readFile(target, "utf8"));
729
1166
  } catch {
730
1167
  }
731
1168
  }
@@ -780,6 +1217,17 @@ async function apply(installPlan) {
780
1217
  await writeAtomic(ep.file, newRaw);
781
1218
  writtenFiles.push(ep.file);
782
1219
  }
1220
+ if (installPlan.nextConfigEdit) {
1221
+ const target = installPlan.nextConfigEdit.file;
1222
+ const raw = originals.get(target);
1223
+ if (raw !== void 0 && !raw.includes("instrumentationHook")) {
1224
+ const updated = injectInstrumentationHook(raw);
1225
+ if (updated !== null) {
1226
+ await writeAtomic(target, updated);
1227
+ writtenFiles.push(target);
1228
+ }
1229
+ }
1230
+ }
783
1231
  } catch (err) {
784
1232
  await rollback(installPlan, originals, createdFiles);
785
1233
  throw err;
@@ -795,14 +1243,14 @@ async function rollback(installPlan, originals, createdFiles) {
795
1243
  const removed = [];
796
1244
  for (const [file, raw] of originals.entries()) {
797
1245
  try {
798
- await fs2.writeFile(file, raw, "utf8");
1246
+ await fs4.writeFile(file, raw, "utf8");
799
1247
  restored.push(file);
800
1248
  } catch {
801
1249
  }
802
1250
  }
803
1251
  for (const file of createdFiles) {
804
1252
  try {
805
- await fs2.unlink(file);
1253
+ await fs4.unlink(file);
806
1254
  removed.push(file);
807
1255
  } catch {
808
1256
  }
@@ -817,8 +1265,26 @@ async function rollback(installPlan, originals, createdFiles) {
817
1265
  ...removed.map((f) => `removed: ${f}`),
818
1266
  ""
819
1267
  ];
820
- const rollbackPath = path2.join(installPlan.serviceDir, "neat-rollback.patch");
821
- await fs2.writeFile(rollbackPath, lines.join("\n"), "utf8");
1268
+ const rollbackPath = path4.join(installPlan.serviceDir, "neat-rollback.patch");
1269
+ await fs4.writeFile(rollbackPath, lines.join("\n"), "utf8");
1270
+ }
1271
+ function injectInstrumentationHook(raw) {
1272
+ if (raw.includes("instrumentationHook")) return raw;
1273
+ const anchors = [
1274
+ { pattern: /(module\.exports\s*=\s*\{)/, label: "cjs-default" },
1275
+ { pattern: /(export\s+default\s*\{)/, label: "esm-default" },
1276
+ { pattern: /(?:const|let|var)\s+\w+(?:\s*:\s*[^=]+)?\s*=\s*(\{)/, label: "named-config" }
1277
+ ];
1278
+ for (const { pattern } of anchors) {
1279
+ const match = pattern.exec(raw);
1280
+ if (!match) continue;
1281
+ const insertAfter = match.index + match[0].length;
1282
+ const before = raw.slice(0, insertAfter);
1283
+ const after = raw.slice(insertAfter);
1284
+ const injection = "\n experimental: { instrumentationHook: true },";
1285
+ return `${before}${injection}${after}`;
1286
+ }
1287
+ return null;
822
1288
  }
823
1289
  var javascriptInstaller = {
824
1290
  name: "javascript",
@@ -828,8 +1294,8 @@ var javascriptInstaller = {
828
1294
  };
829
1295
 
830
1296
  // src/installers/python.ts
831
- import { promises as fs3 } from "fs";
832
- import path3 from "path";
1297
+ import { promises as fs5 } from "fs";
1298
+ import path5 from "path";
833
1299
  var SDK_PACKAGES2 = [
834
1300
  { name: "opentelemetry-distro", version: ">=0.49b0" },
835
1301
  { name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
@@ -841,7 +1307,7 @@ var OTEL_ENV2 = {
841
1307
  };
842
1308
  async function exists2(p) {
843
1309
  try {
844
- await fs3.stat(p);
1310
+ await fs5.stat(p);
845
1311
  return true;
846
1312
  } catch {
847
1313
  return false;
@@ -850,7 +1316,7 @@ async function exists2(p) {
850
1316
  async function detect2(serviceDir) {
851
1317
  const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
852
1318
  for (const m of markers) {
853
- if (await exists2(path3.join(serviceDir, m))) return true;
1319
+ if (await exists2(path5.join(serviceDir, m))) return true;
854
1320
  }
855
1321
  return false;
856
1322
  }
@@ -860,9 +1326,9 @@ function reqPackageName(line) {
860
1326
  return head.replace(/[<>=!~].*$/, "").toLowerCase();
861
1327
  }
862
1328
  async function planRequirementsTxtEdits(serviceDir) {
863
- const file = path3.join(serviceDir, "requirements.txt");
1329
+ const file = path5.join(serviceDir, "requirements.txt");
864
1330
  if (!await exists2(file)) return null;
865
- const raw = await fs3.readFile(file, "utf8");
1331
+ const raw = await fs5.readFile(file, "utf8");
866
1332
  const presentNames = new Set(
867
1333
  raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
868
1334
  );
@@ -870,9 +1336,9 @@ async function planRequirementsTxtEdits(serviceDir) {
870
1336
  return { manifest: file, missing: [...missing] };
871
1337
  }
872
1338
  async function planProcfileEdits(serviceDir) {
873
- const procfile = path3.join(serviceDir, "Procfile");
1339
+ const procfile = path5.join(serviceDir, "Procfile");
874
1340
  if (!await exists2(procfile)) return [];
875
- const raw = await fs3.readFile(procfile, "utf8");
1341
+ const raw = await fs5.readFile(procfile, "utf8");
876
1342
  const edits = [];
877
1343
  for (const line of raw.split(/\r?\n/)) {
878
1344
  if (line.length === 0) continue;
@@ -924,8 +1390,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
924
1390
  const next = `${original}${trailing}${newlines.join("\n")}
925
1391
  `;
926
1392
  const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
927
- await fs3.writeFile(tmp, next, "utf8");
928
- await fs3.rename(tmp, manifest);
1393
+ await fs5.writeFile(tmp, next, "utf8");
1394
+ await fs5.rename(tmp, manifest);
929
1395
  }
930
1396
  async function applyProcfile(procfile, edits, original) {
931
1397
  let next = original;
@@ -934,8 +1400,8 @@ async function applyProcfile(procfile, edits, original) {
934
1400
  next = next.replace(e.before, e.after);
935
1401
  }
936
1402
  const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
937
- await fs3.writeFile(tmp, next, "utf8");
938
- await fs3.rename(tmp, procfile);
1403
+ await fs5.writeFile(tmp, next, "utf8");
1404
+ await fs5.rename(tmp, procfile);
939
1405
  }
940
1406
  async function apply2(installPlan) {
941
1407
  const { serviceDir } = installPlan;
@@ -948,7 +1414,7 @@ async function apply2(installPlan) {
948
1414
  const originals = /* @__PURE__ */ new Map();
949
1415
  for (const file of touched) {
950
1416
  try {
951
- originals.set(file, await fs3.readFile(file, "utf8"));
1417
+ originals.set(file, await fs5.readFile(file, "utf8"));
952
1418
  } catch {
953
1419
  }
954
1420
  }
@@ -959,7 +1425,7 @@ async function apply2(installPlan) {
959
1425
  if (raw === void 0) {
960
1426
  throw new Error(`python installer: cannot read ${file} during apply`);
961
1427
  }
962
- const base = path3.basename(file);
1428
+ const base = path5.basename(file);
963
1429
  if (base === "requirements.txt") {
964
1430
  const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
965
1431
  if (edits.length > 0) {
@@ -984,7 +1450,7 @@ async function rollback2(installPlan, originals) {
984
1450
  const restored = [];
985
1451
  for (const [file, raw] of originals.entries()) {
986
1452
  try {
987
- await fs3.writeFile(file, raw, "utf8");
1453
+ await fs5.writeFile(file, raw, "utf8");
988
1454
  restored.push(file);
989
1455
  } catch {
990
1456
  }
@@ -998,8 +1464,8 @@ async function rollback2(installPlan, originals) {
998
1464
  ...restored.map((f) => `restored: ${f}`),
999
1465
  ""
1000
1466
  ];
1001
- const rollbackPath = path3.join(installPlan.serviceDir, "neat-rollback.patch");
1002
- await fs3.writeFile(rollbackPath, lines.join("\n"), "utf8");
1467
+ const rollbackPath = path5.join(installPlan.serviceDir, "neat-rollback.patch");
1468
+ await fs5.writeFile(rollbackPath, lines.join("\n"), "utf8");
1003
1469
  }
1004
1470
  var pythonInstaller = {
1005
1471
  name: "python",
@@ -1010,7 +1476,7 @@ var pythonInstaller = {
1010
1476
 
1011
1477
  // src/installers/shared.ts
1012
1478
  function isEmptyPlan(plan3) {
1013
- return plan3.dependencyEdits.length === 0 && plan3.entrypointEdits.length === 0 && plan3.envEdits.length === 0 && (plan3.generatedFiles?.length ?? 0) === 0;
1479
+ return plan3.dependencyEdits.length === 0 && plan3.entrypointEdits.length === 0 && plan3.envEdits.length === 0 && (plan3.generatedFiles?.length ?? 0) === 0 && plan3.nextConfigEdit === void 0;
1014
1480
  }
1015
1481
 
1016
1482
  // src/installers/index.ts
@@ -1109,15 +1575,229 @@ function renderPatch(sections) {
1109
1575
  }
1110
1576
  lines.push("");
1111
1577
  }
1578
+ if (plan3.nextConfigEdit) {
1579
+ lines.push("### next.config (framework flag)");
1580
+ lines.push(`--- ${plan3.nextConfigEdit.file}`);
1581
+ lines.push(`+ experimental: { instrumentationHook: true }, // ${plan3.nextConfigEdit.reason}`);
1582
+ lines.push("");
1583
+ }
1112
1584
  }
1113
1585
  return lines.join("\n");
1114
1586
  }
1115
1587
 
1588
+ // src/orchestrator.ts
1589
+ import { promises as fs6 } from "fs";
1590
+ import http from "http";
1591
+ import path6 from "path";
1592
+ import { spawn as spawn2 } from "child_process";
1593
+ import readline from "readline";
1594
+ async function promptYesNo(question) {
1595
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1596
+ return new Promise((resolve) => {
1597
+ rl.question(`${question} [Y/n] `, (answer) => {
1598
+ rl.close();
1599
+ const trimmed = answer.trim().toLowerCase();
1600
+ resolve(trimmed === "" || trimmed === "y" || trimmed === "yes");
1601
+ });
1602
+ });
1603
+ }
1604
+ var DEFAULT_DAEMON_READY_TIMEOUT_MS = 15e3;
1605
+ async function checkDaemonHealth(restPort) {
1606
+ return new Promise((resolve) => {
1607
+ const req = http.get(`http://127.0.0.1:${restPort}/health`, (res) => {
1608
+ const ok = res.statusCode !== void 0 && res.statusCode >= 200 && res.statusCode < 300;
1609
+ res.resume();
1610
+ resolve(ok);
1611
+ });
1612
+ req.on("error", () => resolve(false));
1613
+ req.setTimeout(1e3, () => {
1614
+ req.destroy();
1615
+ resolve(false);
1616
+ });
1617
+ });
1618
+ }
1619
+ async function waitForDaemonReady(restPort, timeoutMs) {
1620
+ const deadline = Date.now() + timeoutMs;
1621
+ while (Date.now() < deadline) {
1622
+ if (await checkDaemonHealth(restPort)) return true;
1623
+ await new Promise((r) => setTimeout(r, 300));
1624
+ }
1625
+ return false;
1626
+ }
1627
+ function spawnDaemonDetached() {
1628
+ const here = path6.dirname(new URL(import.meta.url).pathname);
1629
+ const candidates = [
1630
+ path6.join(here, "neatd.cjs"),
1631
+ path6.join(here, "neatd.js")
1632
+ ];
1633
+ let entry2 = null;
1634
+ const fsSync = __require("fs");
1635
+ for (const c of candidates) {
1636
+ try {
1637
+ fsSync.accessSync(c);
1638
+ entry2 = c;
1639
+ break;
1640
+ } catch {
1641
+ }
1642
+ }
1643
+ if (!entry2) {
1644
+ throw new Error(`orchestrator: cannot locate neatd entry in ${here}`);
1645
+ }
1646
+ const child = spawn2(process.execPath, [entry2, "start"], {
1647
+ detached: true,
1648
+ stdio: "ignore",
1649
+ env: process.env
1650
+ });
1651
+ child.unref();
1652
+ }
1653
+ function openBrowser(url) {
1654
+ if (!process.stdout.isTTY) return "failed";
1655
+ const platform = process.platform;
1656
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
1657
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
1658
+ try {
1659
+ const child = spawn2(cmd, args, { detached: true, stdio: "ignore" });
1660
+ child.on("error", () => {
1661
+ });
1662
+ child.unref();
1663
+ return "opened";
1664
+ } catch {
1665
+ return "failed";
1666
+ }
1667
+ }
1668
+ async function runOrchestrator(opts) {
1669
+ const result = {
1670
+ exitCode: 0,
1671
+ steps: {
1672
+ discovery: { services: 0, languages: [] },
1673
+ extraction: { nodesAdded: 0, edgesAdded: 0 },
1674
+ gitignore: "unchanged",
1675
+ apply: { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, skipped: false },
1676
+ daemon: "skipped",
1677
+ browser: "skipped"
1678
+ }
1679
+ };
1680
+ const stat = await fs6.stat(opts.scanPath).catch(() => null);
1681
+ if (!stat || !stat.isDirectory()) {
1682
+ console.error(`neat: ${opts.scanPath} is not a directory`);
1683
+ result.exitCode = 2;
1684
+ return result;
1685
+ }
1686
+ console.log(`neat: ${opts.scanPath}`);
1687
+ console.log("");
1688
+ const services = await discoverServices(opts.scanPath);
1689
+ const languages = [...new Set(services.map((s) => s.node.language))].sort();
1690
+ result.steps.discovery = { services: services.length, languages };
1691
+ console.log(`discovered ${services.length} service(s) across ${languages.length} language(s)`);
1692
+ let runApply = !opts.noInstrument;
1693
+ if (runApply && !opts.yes && process.stdout.isTTY && process.stdin.isTTY) {
1694
+ runApply = await promptYesNo("instrument your services and open the dashboard?");
1695
+ }
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
+ result.steps.extraction = {
1704
+ nodesAdded: extraction.nodesAdded,
1705
+ edgesAdded: extraction.edgesAdded
1706
+ };
1707
+ const gi = await ensureNeatOutIgnored(opts.scanPath);
1708
+ result.steps.gitignore = gi.action;
1709
+ if (gi.action !== "unchanged") {
1710
+ console.log(`${gi.action} .gitignore (neat-out/)`);
1711
+ }
1712
+ try {
1713
+ await addProject({
1714
+ name: opts.project,
1715
+ path: opts.scanPath,
1716
+ languages,
1717
+ status: "active"
1718
+ });
1719
+ } catch (err) {
1720
+ if (!(err instanceof ProjectNameCollisionError)) throw err;
1721
+ console.error(`neat: ${err.message}`);
1722
+ console.error("pass --project <other-name> to register under a different name.");
1723
+ result.exitCode = 1;
1724
+ return result;
1725
+ }
1726
+ if (!runApply) {
1727
+ result.steps.apply.skipped = true;
1728
+ console.log("skipped instrumentation (--no-instrument)");
1729
+ } 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}`);
1748
+ }
1749
+ const restPort = Number(process.env.PORT ?? 8080);
1750
+ const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
1751
+ if (await checkDaemonHealth(restPort)) {
1752
+ result.steps.daemon = "already-running";
1753
+ } else {
1754
+ try {
1755
+ spawnDaemonDetached();
1756
+ } catch (err) {
1757
+ console.error(`neat: daemon spawn failed \u2014 ${err.message}`);
1758
+ result.exitCode = 1;
1759
+ return result;
1760
+ }
1761
+ const ready = await waitForDaemonReady(restPort, timeoutMs);
1762
+ result.steps.daemon = ready ? "spawned" : "timed-out";
1763
+ if (!ready) {
1764
+ console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
1765
+ result.exitCode = 1;
1766
+ return result;
1767
+ }
1768
+ }
1769
+ const dashboardUrl = opts.dashboardUrl ?? "http://localhost:6328";
1770
+ if (opts.noOpen || !process.stdout.isTTY) {
1771
+ result.steps.browser = "skipped";
1772
+ } else {
1773
+ result.steps.browser = openBrowser(dashboardUrl);
1774
+ }
1775
+ printSummary(result, graph, dashboardUrl);
1776
+ return result;
1777
+ }
1778
+ function printSummary(result, graph, dashboardUrl) {
1779
+ const nodes = [];
1780
+ graph.forEachNode((_id, attrs) => nodes.push(attrs));
1781
+ const edges = [];
1782
+ graph.forEachEdge((_id, attrs) => edges.push(attrs));
1783
+ const byNode = /* @__PURE__ */ new Map();
1784
+ for (const n of nodes) byNode.set(n.type, (byNode.get(n.type) ?? 0) + 1);
1785
+ const byEdge = /* @__PURE__ */ new Map();
1786
+ for (const e of edges) byEdge.set(e.type, (byEdge.get(e.type) ?? 0) + 1);
1787
+ console.log("");
1788
+ console.log("=== summary ===");
1789
+ console.log(`graph: ${graph.order} nodes, ${graph.size} edges`);
1790
+ for (const [t, c] of [...byNode.entries()].sort()) console.log(` ${t}: ${c}`);
1791
+ for (const [t, c] of [...byEdge.entries()].sort()) console.log(` ${t}: ${c}`);
1792
+ console.log("");
1793
+ console.log(`dashboard: ${dashboardUrl}`);
1794
+ }
1795
+
1116
1796
  // src/cli.ts
1117
1797
  import { DivergenceTypeSchema } from "@neat.is/types";
1118
1798
 
1119
1799
  // src/cli-client.ts
1120
- import { Provenance } from "@neat.is/types";
1800
+ import { Provenance as Provenance2 } from "@neat.is/types";
1121
1801
  var HttpError = class extends Error {
1122
1802
  constructor(status, message, responseBody = "") {
1123
1803
  super(message);
@@ -1137,10 +1817,10 @@ var TransportError = class extends Error {
1137
1817
  function createHttpClient(baseUrl) {
1138
1818
  const root = baseUrl.replace(/\/$/, "");
1139
1819
  return {
1140
- async get(path5) {
1820
+ async get(path8) {
1141
1821
  let res;
1142
1822
  try {
1143
- res = await fetch(`${root}${path5}`);
1823
+ res = await fetch(`${root}${path8}`);
1144
1824
  } catch (err) {
1145
1825
  throw new TransportError(
1146
1826
  `cannot reach neat-core at ${root}: ${err.message}`
@@ -1150,16 +1830,16 @@ function createHttpClient(baseUrl) {
1150
1830
  const body = await res.text().catch(() => "");
1151
1831
  throw new HttpError(
1152
1832
  res.status,
1153
- `${res.status} ${res.statusText} on GET ${path5}: ${body}`,
1833
+ `${res.status} ${res.statusText} on GET ${path8}: ${body}`,
1154
1834
  body
1155
1835
  );
1156
1836
  }
1157
1837
  return await res.json();
1158
1838
  },
1159
- async post(path5, body) {
1839
+ async post(path8, body) {
1160
1840
  let res;
1161
1841
  try {
1162
- res = await fetch(`${root}${path5}`, {
1842
+ res = await fetch(`${root}${path8}`, {
1163
1843
  method: "POST",
1164
1844
  headers: { "content-type": "application/json" },
1165
1845
  body: JSON.stringify(body)
@@ -1173,7 +1853,7 @@ function createHttpClient(baseUrl) {
1173
1853
  const text = await res.text().catch(() => "");
1174
1854
  throw new HttpError(
1175
1855
  res.status,
1176
- `${res.status} ${res.statusText} on POST ${path5}: ${text}`,
1856
+ `${res.status} ${res.statusText} on POST ${path8}: ${text}`,
1177
1857
  text
1178
1858
  );
1179
1859
  }
@@ -1187,12 +1867,12 @@ function projectPath(project, suffix) {
1187
1867
  }
1188
1868
  async function runRootCause(client, input) {
1189
1869
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
1190
- const path5 = projectPath(
1870
+ const path8 = projectPath(
1191
1871
  input.project,
1192
1872
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
1193
1873
  );
1194
1874
  try {
1195
- const result = await client.get(path5);
1875
+ const result = await client.get(path8);
1196
1876
  const arrowPath = result.traversalPath.join(" \u2190 ");
1197
1877
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
1198
1878
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -1218,12 +1898,12 @@ async function runRootCause(client, input) {
1218
1898
  }
1219
1899
  async function runBlastRadius(client, input) {
1220
1900
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
1221
- const path5 = projectPath(
1901
+ const path8 = projectPath(
1222
1902
  input.project,
1223
1903
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
1224
1904
  );
1225
1905
  try {
1226
- const result = await client.get(path5);
1906
+ const result = await client.get(path8);
1227
1907
  if (result.totalAffected === 0) {
1228
1908
  return {
1229
1909
  summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
@@ -1252,17 +1932,17 @@ async function runBlastRadius(client, input) {
1252
1932
  }
1253
1933
  }
1254
1934
  function formatBlastEntry(n) {
1255
- const tag = n.edgeProvenance === Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
1935
+ const tag = n.edgeProvenance === Provenance2.STALE ? " [STALE \u2014 last seen too long ago]" : "";
1256
1936
  return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
1257
1937
  }
1258
1938
  async function runDependencies(client, input) {
1259
1939
  const depth = input.depth ?? 3;
1260
- const path5 = projectPath(
1940
+ const path8 = projectPath(
1261
1941
  input.project,
1262
1942
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
1263
1943
  );
1264
1944
  try {
1265
- const result = await client.get(path5);
1945
+ const result = await client.get(path8);
1266
1946
  if (result.total === 0) {
1267
1947
  return {
1268
1948
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -1298,9 +1978,9 @@ async function runObservedDependencies(client, input) {
1298
1978
  const edges = await client.get(
1299
1979
  projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
1300
1980
  );
1301
- const observed = edges.outbound.filter((e) => e.provenance === Provenance.OBSERVED);
1981
+ const observed = edges.outbound.filter((e) => e.provenance === Provenance2.OBSERVED);
1302
1982
  if (observed.length === 0) {
1303
- const hasExtracted = edges.outbound.some((e) => e.provenance === Provenance.EXTRACTED);
1983
+ const hasExtracted = edges.outbound.some((e) => e.provenance === Provenance2.EXTRACTED);
1304
1984
  const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
1305
1985
  return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
1306
1986
  }
@@ -1308,7 +1988,7 @@ async function runObservedDependencies(client, input) {
1308
1988
  return {
1309
1989
  summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
1310
1990
  block: blockLines.join("\n"),
1311
- provenance: Provenance.OBSERVED
1991
+ provenance: Provenance2.OBSERVED
1312
1992
  };
1313
1993
  } catch (err) {
1314
1994
  if (err instanceof HttpError && err.status === 404) {
@@ -1343,9 +2023,9 @@ function formatDuration(ms) {
1343
2023
  return `${Math.round(h / 24)}d`;
1344
2024
  }
1345
2025
  async function runIncidents(client, input) {
1346
- const path5 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
2026
+ const path8 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
1347
2027
  try {
1348
- const body = await client.get(path5);
2028
+ const body = await client.get(path8);
1349
2029
  const events = body.events;
1350
2030
  if (events.length === 0) {
1351
2031
  return {
@@ -1362,7 +2042,7 @@ async function runIncidents(client, input) {
1362
2042
  return {
1363
2043
  summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
1364
2044
  block: blockLines.join("\n"),
1365
- provenance: Provenance.OBSERVED
2045
+ provenance: Provenance2.OBSERVED
1366
2046
  };
1367
2047
  } catch (err) {
1368
2048
  if (err instanceof HttpError && err.status === 404) {
@@ -1471,7 +2151,7 @@ async function runStaleEdges(client, input) {
1471
2151
  return {
1472
2152
  summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
1473
2153
  block: blockLines.join("\n"),
1474
- provenance: Provenance.STALE
2154
+ provenance: Provenance2.STALE
1475
2155
  };
1476
2156
  }
1477
2157
  async function runPolicies(client, input) {
@@ -1637,6 +2317,9 @@ function usage() {
1637
2317
  console.log(" Flags:");
1638
2318
  console.log(" --print-config print the JSON snippet to stdout");
1639
2319
  console.log(" --apply merge mcpServers.neat into ~/.claude.json");
2320
+ console.log(" deploy Detect the deploy substrate, generate NEAT_AUTH_TOKEN,");
2321
+ console.log(" emit a docker-compose / systemd / docker run artifact, and");
2322
+ console.log(" print the OTel env-vars block to paste into your platform.");
1640
2323
  console.log("");
1641
2324
  console.log("query commands (mirror the MCP tools, ADR-050):");
1642
2325
  console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
@@ -1699,6 +2382,10 @@ function parseArgs(rest) {
1699
2382
  apply: false,
1700
2383
  dryRun: false,
1701
2384
  noInstall: false,
2385
+ noInstrument: false,
2386
+ noOpen: false,
2387
+ yes: false,
2388
+ verbose: false,
1702
2389
  printConfig: false,
1703
2390
  json: false,
1704
2391
  depth: null,
@@ -1727,6 +2414,22 @@ function parseArgs(rest) {
1727
2414
  out.noInstall = true;
1728
2415
  continue;
1729
2416
  }
2417
+ if (arg === "--no-instrument") {
2418
+ out.noInstrument = true;
2419
+ continue;
2420
+ }
2421
+ if (arg === "--no-open") {
2422
+ out.noOpen = true;
2423
+ continue;
2424
+ }
2425
+ if (arg === "--yes" || arg === "-y") {
2426
+ out.yes = true;
2427
+ continue;
2428
+ }
2429
+ if (arg === "--verbose" || arg === "-v") {
2430
+ out.verbose = true;
2431
+ continue;
2432
+ }
1730
2433
  if (arg === "--print-config") {
1731
2434
  out.printConfig = true;
1732
2435
  continue;
@@ -1782,34 +2485,6 @@ function assignFlag(out, field, value) {
1782
2485
  ;
1783
2486
  out[field] = value;
1784
2487
  }
1785
- function summarise(nodes, edges) {
1786
- const byNode = /* @__PURE__ */ new Map();
1787
- for (const n of nodes) byNode.set(n.type, (byNode.get(n.type) ?? 0) + 1);
1788
- const byEdge = /* @__PURE__ */ new Map();
1789
- for (const e of edges) byEdge.set(e.type, (byEdge.get(e.type) ?? 0) + 1);
1790
- const nodeLines = [...byNode.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([t, c]) => ` ${t}: ${c}`);
1791
- const edgeLines = [...byEdge.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([t, c]) => ` ${t}: ${c}`);
1792
- return ["nodes:", ...nodeLines, "edges:", ...edgeLines].join("\n");
1793
- }
1794
- function formatIncompat(inc) {
1795
- if (inc.kind === "node-engine") {
1796
- const range = inc.declaredNodeEngine ? ` (engines.node="${inc.declaredNodeEngine}")` : "";
1797
- return `${inc.package}@${inc.packageVersion ?? "?"} requires Node ${inc.requiredNodeVersion}${range} \u2014 ${inc.reason}`;
1798
- }
1799
- if (inc.kind === "package-conflict") {
1800
- const found = inc.foundVersion ? `@${inc.foundVersion}` : " (missing)";
1801
- return `${inc.package}@${inc.packageVersion ?? "?"} requires ${inc.requires.name}>=${inc.requires.minVersion}; found ${inc.requires.name}${found} \u2014 ${inc.reason}`;
1802
- }
1803
- if (inc.kind === "deprecated-api") {
1804
- return `${inc.package}@${inc.packageVersion ?? "?"} is deprecated \u2014 ${inc.reason}`;
1805
- }
1806
- return `${inc.driver}@${inc.driverVersion} vs ${inc.engine} ${inc.engineVersion} \u2014 ${inc.reason}`;
1807
- }
1808
- function findIncompatibilities(nodes) {
1809
- return nodes.filter(
1810
- (n) => n.type === "ServiceNode" && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
1811
- );
1812
- }
1813
2488
  function printBanner() {
1814
2489
  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");
1815
2490
  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");
@@ -1860,7 +2535,7 @@ async function buildPatchSections(services) {
1860
2535
  }
1861
2536
  async function runInit(opts) {
1862
2537
  const written = [];
1863
- const stat = await fs4.stat(opts.scanPath).catch(() => null);
2538
+ const stat = await fs7.stat(opts.scanPath).catch(() => null);
1864
2539
  if (!stat || !stat.isDirectory()) {
1865
2540
  console.error(`neat init: ${opts.scanPath} is not a directory`);
1866
2541
  return { exitCode: 2, writtenFiles: written };
@@ -1869,11 +2544,15 @@ async function runInit(opts) {
1869
2544
  printDiscoveryReport(opts, services);
1870
2545
  const sections = opts.noInstall ? [] : await buildPatchSections(services);
1871
2546
  const patch = renderPatch(sections);
1872
- const patchPath = path4.join(opts.scanPath, "neat.patch");
2547
+ const patchPath = path7.join(opts.scanPath, "neat.patch");
1873
2548
  if (opts.dryRun) {
1874
- await fs4.writeFile(patchPath, patch, "utf8");
2549
+ await fs7.writeFile(patchPath, patch, "utf8");
1875
2550
  written.push(patchPath);
1876
2551
  console.log(`dry-run: patch written to ${patchPath}`);
2552
+ const gitignorePath = path7.join(opts.scanPath, ".gitignore");
2553
+ const gitignoreExists = await fs7.stat(gitignorePath).then(() => true).catch(() => false);
2554
+ const verb = gitignoreExists ? "append" : "create";
2555
+ console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
1877
2556
  console.log("rerun without --dry-run to register and snapshot.");
1878
2557
  return { exitCode: 0, writtenFiles: written };
1879
2558
  }
@@ -1882,12 +2561,16 @@ async function runInit(opts) {
1882
2561
  const graph = getGraph(graphKey);
1883
2562
  const projectPaths = pathsForProject(
1884
2563
  graphKey,
1885
- path4.join(opts.scanPath, "neat-out")
2564
+ path7.join(opts.scanPath, "neat-out")
1886
2565
  );
1887
- const errorsPath = path4.join(path4.dirname(opts.outPath), path4.basename(projectPaths.errorsPath));
2566
+ const errorsPath = path7.join(path7.dirname(opts.outPath), path7.basename(projectPaths.errorsPath));
1888
2567
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
1889
2568
  await saveGraphToDisk(graph, opts.outPath);
1890
2569
  written.push(opts.outPath);
2570
+ const gitignoreResult = await ensureNeatOutIgnored(opts.scanPath);
2571
+ if (gitignoreResult.action !== "unchanged") {
2572
+ written.push(gitignoreResult.file);
2573
+ }
1891
2574
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
1892
2575
  try {
1893
2576
  await addProject({
@@ -1930,35 +2613,27 @@ async function runInit(opts) {
1930
2613
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
1931
2614
  }
1932
2615
  } else {
1933
- await fs4.writeFile(patchPath, patch, "utf8");
2616
+ await fs7.writeFile(patchPath, patch, "utf8");
1934
2617
  written.push(patchPath);
1935
2618
  }
1936
2619
  }
1937
- const nodes = [];
1938
- graph.forEachNode((_id, attrs) => nodes.push(attrs));
1939
- const edges = [];
1940
- graph.forEachEdge((_id, attrs) => edges.push(attrs));
1941
2620
  console.log("");
1942
- console.log("=== neat init: summary ===");
1943
2621
  console.log(`snapshot: ${opts.outPath}`);
1944
2622
  console.log(`added: ${result.nodesAdded} nodes, ${result.edgesAdded} edges`);
1945
- console.log(`total: ${graph.order} nodes, ${graph.size} edges`);
1946
- console.log(summarise(nodes, edges));
2623
+ console.log("");
2624
+ const divergenceResult = computeDivergences(graph);
2625
+ console.log(
2626
+ renderValueForwardSummary({
2627
+ graph,
2628
+ divergences: divergenceResult.divergences,
2629
+ verbose: opts.verbose
2630
+ })
2631
+ );
1947
2632
  console.log(formatExtractionBanner(result.extractionErrors));
1948
2633
  if (result.extractionErrors > 0) {
1949
2634
  console.log(`errors: ${errorsPath}`);
1950
2635
  }
1951
2636
  console.log(formatPrecisionFloorBanner(result.extractedDropped));
1952
- const incompatibilities = findIncompatibilities(nodes);
1953
- if (incompatibilities.length > 0) {
1954
- console.log("");
1955
- console.log(`incompatibilities found in ${incompatibilities.length} service(s):`);
1956
- for (const svc of incompatibilities) {
1957
- for (const inc of svc.incompatibilities ?? []) {
1958
- console.log(` ${svc.name}: ${formatIncompat(inc)}`);
1959
- }
1960
- }
1961
- }
1962
2637
  if (result.extractionErrors > 0 && isStrictExtractionEnabled()) {
1963
2638
  return { exitCode: 4, writtenFiles: written };
1964
2639
  }
@@ -1978,9 +2653,9 @@ var CLAUDE_SKILL_CONFIG = {
1978
2653
  };
1979
2654
  function claudeConfigPath() {
1980
2655
  const override = process.env.NEAT_CLAUDE_CONFIG;
1981
- if (override && override.length > 0) return path4.resolve(override);
2656
+ if (override && override.length > 0) return path7.resolve(override);
1982
2657
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
1983
- return path4.join(home, ".claude.json");
2658
+ return path7.join(home, ".claude.json");
1984
2659
  }
1985
2660
  async function runSkill(opts) {
1986
2661
  const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -1992,7 +2667,7 @@ async function runSkill(opts) {
1992
2667
  const target = claudeConfigPath();
1993
2668
  let existing = {};
1994
2669
  try {
1995
- existing = JSON.parse(await fs4.readFile(target, "utf8"));
2670
+ existing = JSON.parse(await fs7.readFile(target, "utf8"));
1996
2671
  } catch (err) {
1997
2672
  if (err.code !== "ENOENT") {
1998
2673
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -2004,8 +2679,8 @@ async function runSkill(opts) {
2004
2679
  ...existing,
2005
2680
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
2006
2681
  };
2007
- await fs4.mkdir(path4.dirname(target), { recursive: true });
2008
- await fs4.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
2682
+ await fs7.mkdir(path7.dirname(target), { recursive: true });
2683
+ await fs7.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
2009
2684
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
2010
2685
  console.log("restart Claude Code to pick up the new MCP server.");
2011
2686
  return { exitCode: 0 };
@@ -2039,12 +2714,12 @@ async function main() {
2039
2714
  console.error("neat init: --apply and --dry-run are mutually exclusive");
2040
2715
  process.exit(2);
2041
2716
  }
2042
- const scanPath = path4.resolve(target);
2717
+ const scanPath = path7.resolve(target);
2043
2718
  const projectExplicit = parsed.project !== null;
2044
- const projectName = projectExplicit ? project : path4.basename(scanPath);
2719
+ const projectName = projectExplicit ? project : path7.basename(scanPath);
2045
2720
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
2046
- const fallback = pathsForProject(projectKey, path4.join(scanPath, "neat-out")).snapshotPath;
2047
- const outPath = path4.resolve(process.env.NEAT_OUT_PATH ?? fallback);
2721
+ const fallback = pathsForProject(projectKey, path7.join(scanPath, "neat-out")).snapshotPath;
2722
+ const outPath = path7.resolve(process.env.NEAT_OUT_PATH ?? fallback);
2048
2723
  const result = await runInit({
2049
2724
  scanPath,
2050
2725
  outPath,
@@ -2052,7 +2727,8 @@ async function main() {
2052
2727
  projectExplicit,
2053
2728
  apply: apply3,
2054
2729
  dryRun,
2055
- noInstall
2730
+ noInstall,
2731
+ verbose: parsed.verbose
2056
2732
  });
2057
2733
  if (result.exitCode !== 0) process.exit(result.exitCode);
2058
2734
  return;
@@ -2064,21 +2740,21 @@ async function main() {
2064
2740
  usage();
2065
2741
  process.exit(2);
2066
2742
  }
2067
- const scanPath = path4.resolve(target);
2068
- const stat = await fs4.stat(scanPath).catch(() => null);
2743
+ const scanPath = path7.resolve(target);
2744
+ const stat = await fs7.stat(scanPath).catch(() => null);
2069
2745
  if (!stat || !stat.isDirectory()) {
2070
2746
  console.error(`neat watch: ${scanPath} is not a directory`);
2071
2747
  process.exit(2);
2072
2748
  }
2073
- const projectPaths = pathsForProject(project, path4.join(scanPath, "neat-out"));
2074
- const outPath = path4.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
2075
- const errorsPath = path4.resolve(
2076
- process.env.NEAT_ERRORS_PATH ?? path4.join(path4.dirname(outPath), path4.basename(projectPaths.errorsPath))
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))
2077
2753
  );
2078
- const staleEventsPath = path4.resolve(
2079
- process.env.NEAT_STALE_EVENTS_PATH ?? path4.join(path4.dirname(outPath), path4.basename(projectPaths.staleEventsPath))
2754
+ const staleEventsPath = path7.resolve(
2755
+ process.env.NEAT_STALE_EVENTS_PATH ?? path7.join(path7.dirname(outPath), path7.basename(projectPaths.staleEventsPath))
2080
2756
  );
2081
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path4.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
2757
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path7.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
2082
2758
  const handle = await startWatch(getGraph(project), {
2083
2759
  scanPath,
2084
2760
  outPath,
@@ -2171,15 +2847,62 @@ async function main() {
2171
2847
  console.log("note: neat-out/, policy.json, and other files at the project path were left in place.");
2172
2848
  return;
2173
2849
  }
2850
+ if (cmd === "deploy") {
2851
+ const artifact = await runDeploy();
2852
+ const block = renderOtelEnvBlock2(artifact.token);
2853
+ console.log();
2854
+ console.log(`Substrate detected: ${artifact.substrate}`);
2855
+ if (artifact.artifactPath) {
2856
+ console.log(`Artifact written: ${artifact.artifactPath}`);
2857
+ } else {
2858
+ console.log("No on-disk artifact \u2014 copy the snippet below into your substrate.");
2859
+ console.log();
2860
+ console.log(artifact.contents);
2861
+ }
2862
+ console.log();
2863
+ console.log("NEAT_AUTH_TOKEN (store this \u2014 it will not be printed again):");
2864
+ console.log(` ${artifact.token}`);
2865
+ console.log();
2866
+ console.log("For your application's deploy platform, set these env vars:");
2867
+ console.log(block.split("\n").map((l) => ` ${l}`).join("\n"));
2868
+ console.log();
2869
+ console.log("Once NEAT is running, your dashboard will be at:");
2870
+ console.log(" https://<host>:6328");
2871
+ console.log();
2872
+ console.log("To start NEAT, run:");
2873
+ console.log(` ${artifact.startCommand}`);
2874
+ return;
2875
+ }
2174
2876
  if (QUERY_VERBS.has(cmd)) {
2175
2877
  const code = await runQueryVerb(cmd, parsed);
2176
2878
  if (code !== 0) process.exit(code);
2177
2879
  return;
2178
2880
  }
2881
+ const orchestratorCode = await tryOrchestrator(cmd, parsed);
2882
+ if (orchestratorCode !== null) {
2883
+ if (orchestratorCode !== 0) process.exit(orchestratorCode);
2884
+ return;
2885
+ }
2179
2886
  console.error(`neat: unknown command "${cmd}"`);
2180
2887
  usage();
2181
2888
  process.exit(1);
2182
2889
  }
2890
+ async function tryOrchestrator(cmd, parsed) {
2891
+ const scanPath = path7.resolve(cmd);
2892
+ const stat = await fs7.stat(scanPath).catch(() => null);
2893
+ if (!stat || !stat.isDirectory()) return null;
2894
+ const projectExplicit = parsed.project !== null;
2895
+ const projectName = projectExplicit ? parsed.project : path7.basename(scanPath);
2896
+ const result = await runOrchestrator({
2897
+ scanPath,
2898
+ project: projectName,
2899
+ projectExplicit,
2900
+ noInstrument: parsed.noInstrument,
2901
+ noOpen: parsed.noOpen,
2902
+ yes: parsed.yes
2903
+ });
2904
+ return result.exitCode;
2905
+ }
2183
2906
  var QUERY_VERBS = /* @__PURE__ */ new Set([
2184
2907
  "root-cause",
2185
2908
  "blast-radius",