@neat.is/core 0.3.6 → 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-G3PDTGOW.js";
46
+ } from "./chunk-V4TU7OKZ.js";
46
47
  import {
48
+ __require,
47
49
  buildOtelReceiver
48
- } from "./chunk-4ASCXBZF.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,10 +811,73 @@ async function detect(serviceDir) {
484
811
  const pkg = await readPackageJson(serviceDir);
485
812
  return pkg !== null && typeof pkg.name === "string";
486
813
  }
487
- var INDEX_CANDIDATES = ["index.ts", "index.tsx", "index.js", "index.mjs", "index.cjs"];
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
+ }
836
+ var INDEX_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".cjs"];
837
+ var INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `index${ext}`);
838
+ var SRC_INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `src/index${ext}`);
839
+ var SRC_NAMED_CANDIDATES = ["server", "main", "app"].flatMap(
840
+ (name) => INDEX_EXTENSIONS.map((ext) => `src/${name}${ext}`)
841
+ );
842
+ var SCRIPT_LAUNCHERS = /* @__PURE__ */ new Set([
843
+ "node",
844
+ "ts-node",
845
+ "tsx",
846
+ "ts-node-dev",
847
+ "nodemon",
848
+ "npx",
849
+ "pnpm",
850
+ "yarn",
851
+ "npm",
852
+ "cross-env",
853
+ "dotenv",
854
+ "--"
855
+ ]);
856
+ function looksLikeEntryPath(token) {
857
+ if (token.length === 0) return false;
858
+ if (token.startsWith("-")) return false;
859
+ if (token.includes("=")) return false;
860
+ if (token.includes("/")) return true;
861
+ return /\.(?:m?[jt]sx?|c[jt]s)$/.test(token);
862
+ }
863
+ function scriptHasShellChain(script) {
864
+ return /(?:&&|\|\||;|\|(?!\|))/.test(script);
865
+ }
866
+ function entryFromScript(script) {
867
+ if (!script) return void 0;
868
+ if (scriptHasShellChain(script)) return void 0;
869
+ const tokens = script.split(/\s+/).filter((t) => t.length > 0);
870
+ for (const token of tokens) {
871
+ const lower = token.toLowerCase();
872
+ if (SCRIPT_LAUNCHERS.has(lower)) continue;
873
+ const cleaned = token.startsWith("./") ? token.slice(2) : token;
874
+ if (looksLikeEntryPath(cleaned)) return cleaned;
875
+ }
876
+ return void 0;
877
+ }
488
878
  async function resolveEntry(serviceDir, pkg) {
489
879
  if (typeof pkg.main === "string" && pkg.main.length > 0) {
490
- const candidate = path2.resolve(serviceDir, pkg.main);
880
+ const candidate = path4.resolve(serviceDir, pkg.main);
491
881
  if (await exists(candidate)) return candidate;
492
882
  }
493
883
  if (pkg.bin) {
@@ -501,18 +891,36 @@ async function resolveEntry(serviceDir, pkg) {
501
891
  if (typeof first === "string") binEntry = first;
502
892
  }
503
893
  if (binEntry) {
504
- const candidate = path2.resolve(serviceDir, binEntry);
894
+ const candidate = path4.resolve(serviceDir, binEntry);
505
895
  if (await exists(candidate)) return candidate;
506
896
  }
507
897
  }
898
+ const startEntry = entryFromScript(pkg.scripts?.start);
899
+ if (startEntry) {
900
+ const candidate = path4.resolve(serviceDir, startEntry);
901
+ if (await exists(candidate)) return candidate;
902
+ }
903
+ const devEntry = entryFromScript(pkg.scripts?.dev);
904
+ if (devEntry) {
905
+ const candidate = path4.resolve(serviceDir, devEntry);
906
+ if (await exists(candidate)) return candidate;
907
+ }
908
+ for (const rel of SRC_INDEX_CANDIDATES) {
909
+ const candidate = path4.join(serviceDir, rel);
910
+ if (await exists(candidate)) return candidate;
911
+ }
912
+ for (const rel of SRC_NAMED_CANDIDATES) {
913
+ const candidate = path4.join(serviceDir, rel);
914
+ if (await exists(candidate)) return candidate;
915
+ }
508
916
  for (const name of INDEX_CANDIDATES) {
509
- const candidate = path2.join(serviceDir, name);
917
+ const candidate = path4.join(serviceDir, name);
510
918
  if (await exists(candidate)) return candidate;
511
919
  }
512
920
  return null;
513
921
  }
514
922
  function dispatchEntry(entryFile, pkg) {
515
- const ext = path2.extname(entryFile).toLowerCase();
923
+ const ext = path4.extname(entryFile).toLowerCase();
516
924
  if (ext === ".ts" || ext === ".tsx") return "ts";
517
925
  if (ext === ".mjs") return "esm";
518
926
  if (ext === ".cjs") return "cjs";
@@ -529,9 +937,9 @@ function otelInitContents(flavor) {
529
937
  return OTEL_INIT_CJS;
530
938
  }
531
939
  function injectionLine(flavor, entryFile, otelInitFile) {
532
- let rel = path2.relative(path2.dirname(entryFile), otelInitFile);
940
+ let rel = path4.relative(path4.dirname(entryFile), otelInitFile);
533
941
  if (!rel.startsWith(".")) rel = `./${rel}`;
534
- rel = rel.split(path2.sep).join("/");
942
+ rel = rel.split(path4.sep).join("/");
535
943
  if (flavor === "cjs") return `require('${rel}')`;
536
944
  if (flavor === "esm") return `import '${rel}'`;
537
945
  const tsRel = rel.replace(/\.ts$/, "");
@@ -542,9 +950,88 @@ function lineIsOtelInjection(line) {
542
950
  if (trimmed.length === 0) return false;
543
951
  return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
544
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
+ }
545
1032
  async function plan(serviceDir) {
546
1033
  const pkg = await readPackageJson(serviceDir);
547
- const manifestPath = path2.join(serviceDir, "package.json");
1034
+ const manifestPath = path4.join(serviceDir, "package.json");
548
1035
  const empty = {
549
1036
  language: "javascript",
550
1037
  serviceDir,
@@ -554,13 +1041,19 @@ async function plan(serviceDir) {
554
1041
  generatedFiles: []
555
1042
  };
556
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
+ }
557
1050
  const entryFile = await resolveEntry(serviceDir, pkg);
558
1051
  if (!entryFile) {
559
1052
  return { ...empty, libOnly: true };
560
1053
  }
561
1054
  const flavor = dispatchEntry(entryFile, pkg);
562
- const otelInitFile = path2.join(path2.dirname(entryFile), otelInitFilename(flavor));
563
- 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");
564
1057
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
565
1058
  const dependencyEdits = [];
566
1059
  for (const sdk of SDK_PACKAGES) {
@@ -574,7 +1067,7 @@ async function plan(serviceDir) {
574
1067
  }
575
1068
  const entrypointEdits = [];
576
1069
  try {
577
- const raw = await fs2.readFile(entryFile, "utf8");
1070
+ const raw = await fs4.readFile(entryFile, "utf8");
578
1071
  const lines = raw.split(/\r?\n/);
579
1072
  const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
580
1073
  if (!lineIsOtelInjection(firstReal)) {
@@ -599,7 +1092,7 @@ async function plan(serviceDir) {
599
1092
  if (!await exists(envNeatFile)) {
600
1093
  generatedFiles.push({
601
1094
  file: envNeatFile,
602
- contents: renderEnvNeat(pkg.name ?? path2.basename(serviceDir)),
1095
+ contents: renderEnvNeat(pkg.name ?? path4.basename(serviceDir)),
603
1096
  skipIfExists: true
604
1097
  });
605
1098
  }
@@ -618,18 +1111,20 @@ async function plan(serviceDir) {
618
1111
  };
619
1112
  }
620
1113
  function isAllowedWritePath(serviceDir, target) {
621
- const rel = path2.relative(serviceDir, target);
1114
+ const rel = path4.relative(serviceDir, target);
622
1115
  if (rel.startsWith("..")) return false;
623
- const base = path2.basename(target);
1116
+ const base = path4.basename(target);
624
1117
  if (base === "package.json") return true;
625
1118
  if (base === ".env.neat") return true;
626
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;
627
1122
  return false;
628
1123
  }
629
1124
  async function writeAtomic(file, contents) {
630
1125
  const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
631
- await fs2.writeFile(tmp, contents, "utf8");
632
- await fs2.rename(tmp, file);
1126
+ await fs4.writeFile(tmp, contents, "utf8");
1127
+ await fs4.rename(tmp, file);
633
1128
  }
634
1129
  async function apply(installPlan) {
635
1130
  const { serviceDir } = installPlan;
@@ -641,7 +1136,7 @@ async function apply(installPlan) {
641
1136
  writtenFiles: []
642
1137
  };
643
1138
  }
644
- 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) {
645
1140
  return {
646
1141
  serviceDir,
647
1142
  outcome: "already-instrumented",
@@ -652,6 +1147,7 @@ async function apply(installPlan) {
652
1147
  for (const d of installPlan.dependencyEdits) allTargets.add(d.file);
653
1148
  for (const e of installPlan.entrypointEdits) allTargets.add(e.file);
654
1149
  for (const g of installPlan.generatedFiles ?? []) allTargets.add(g.file);
1150
+ if (installPlan.nextConfigEdit) allTargets.add(installPlan.nextConfigEdit.file);
655
1151
  for (const target of allTargets) {
656
1152
  const isEntryEdit = installPlan.entrypointEdits.some((e) => e.file === target);
657
1153
  if (isEntryEdit) continue;
@@ -666,7 +1162,7 @@ async function apply(installPlan) {
666
1162
  for (const target of allTargets) {
667
1163
  if (await exists(target)) {
668
1164
  try {
669
- originals.set(target, await fs2.readFile(target, "utf8"));
1165
+ originals.set(target, await fs4.readFile(target, "utf8"));
670
1166
  } catch {
671
1167
  }
672
1168
  }
@@ -721,6 +1217,17 @@ async function apply(installPlan) {
721
1217
  await writeAtomic(ep.file, newRaw);
722
1218
  writtenFiles.push(ep.file);
723
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
+ }
724
1231
  } catch (err) {
725
1232
  await rollback(installPlan, originals, createdFiles);
726
1233
  throw err;
@@ -736,14 +1243,14 @@ async function rollback(installPlan, originals, createdFiles) {
736
1243
  const removed = [];
737
1244
  for (const [file, raw] of originals.entries()) {
738
1245
  try {
739
- await fs2.writeFile(file, raw, "utf8");
1246
+ await fs4.writeFile(file, raw, "utf8");
740
1247
  restored.push(file);
741
1248
  } catch {
742
1249
  }
743
1250
  }
744
1251
  for (const file of createdFiles) {
745
1252
  try {
746
- await fs2.unlink(file);
1253
+ await fs4.unlink(file);
747
1254
  removed.push(file);
748
1255
  } catch {
749
1256
  }
@@ -758,8 +1265,26 @@ async function rollback(installPlan, originals, createdFiles) {
758
1265
  ...removed.map((f) => `removed: ${f}`),
759
1266
  ""
760
1267
  ];
761
- const rollbackPath = path2.join(installPlan.serviceDir, "neat-rollback.patch");
762
- 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;
763
1288
  }
764
1289
  var javascriptInstaller = {
765
1290
  name: "javascript",
@@ -769,8 +1294,8 @@ var javascriptInstaller = {
769
1294
  };
770
1295
 
771
1296
  // src/installers/python.ts
772
- import { promises as fs3 } from "fs";
773
- import path3 from "path";
1297
+ import { promises as fs5 } from "fs";
1298
+ import path5 from "path";
774
1299
  var SDK_PACKAGES2 = [
775
1300
  { name: "opentelemetry-distro", version: ">=0.49b0" },
776
1301
  { name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
@@ -782,7 +1307,7 @@ var OTEL_ENV2 = {
782
1307
  };
783
1308
  async function exists2(p) {
784
1309
  try {
785
- await fs3.stat(p);
1310
+ await fs5.stat(p);
786
1311
  return true;
787
1312
  } catch {
788
1313
  return false;
@@ -791,7 +1316,7 @@ async function exists2(p) {
791
1316
  async function detect2(serviceDir) {
792
1317
  const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
793
1318
  for (const m of markers) {
794
- if (await exists2(path3.join(serviceDir, m))) return true;
1319
+ if (await exists2(path5.join(serviceDir, m))) return true;
795
1320
  }
796
1321
  return false;
797
1322
  }
@@ -801,9 +1326,9 @@ function reqPackageName(line) {
801
1326
  return head.replace(/[<>=!~].*$/, "").toLowerCase();
802
1327
  }
803
1328
  async function planRequirementsTxtEdits(serviceDir) {
804
- const file = path3.join(serviceDir, "requirements.txt");
1329
+ const file = path5.join(serviceDir, "requirements.txt");
805
1330
  if (!await exists2(file)) return null;
806
- const raw = await fs3.readFile(file, "utf8");
1331
+ const raw = await fs5.readFile(file, "utf8");
807
1332
  const presentNames = new Set(
808
1333
  raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
809
1334
  );
@@ -811,9 +1336,9 @@ async function planRequirementsTxtEdits(serviceDir) {
811
1336
  return { manifest: file, missing: [...missing] };
812
1337
  }
813
1338
  async function planProcfileEdits(serviceDir) {
814
- const procfile = path3.join(serviceDir, "Procfile");
1339
+ const procfile = path5.join(serviceDir, "Procfile");
815
1340
  if (!await exists2(procfile)) return [];
816
- const raw = await fs3.readFile(procfile, "utf8");
1341
+ const raw = await fs5.readFile(procfile, "utf8");
817
1342
  const edits = [];
818
1343
  for (const line of raw.split(/\r?\n/)) {
819
1344
  if (line.length === 0) continue;
@@ -865,8 +1390,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
865
1390
  const next = `${original}${trailing}${newlines.join("\n")}
866
1391
  `;
867
1392
  const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
868
- await fs3.writeFile(tmp, next, "utf8");
869
- await fs3.rename(tmp, manifest);
1393
+ await fs5.writeFile(tmp, next, "utf8");
1394
+ await fs5.rename(tmp, manifest);
870
1395
  }
871
1396
  async function applyProcfile(procfile, edits, original) {
872
1397
  let next = original;
@@ -875,8 +1400,8 @@ async function applyProcfile(procfile, edits, original) {
875
1400
  next = next.replace(e.before, e.after);
876
1401
  }
877
1402
  const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
878
- await fs3.writeFile(tmp, next, "utf8");
879
- await fs3.rename(tmp, procfile);
1403
+ await fs5.writeFile(tmp, next, "utf8");
1404
+ await fs5.rename(tmp, procfile);
880
1405
  }
881
1406
  async function apply2(installPlan) {
882
1407
  const { serviceDir } = installPlan;
@@ -889,7 +1414,7 @@ async function apply2(installPlan) {
889
1414
  const originals = /* @__PURE__ */ new Map();
890
1415
  for (const file of touched) {
891
1416
  try {
892
- originals.set(file, await fs3.readFile(file, "utf8"));
1417
+ originals.set(file, await fs5.readFile(file, "utf8"));
893
1418
  } catch {
894
1419
  }
895
1420
  }
@@ -900,7 +1425,7 @@ async function apply2(installPlan) {
900
1425
  if (raw === void 0) {
901
1426
  throw new Error(`python installer: cannot read ${file} during apply`);
902
1427
  }
903
- const base = path3.basename(file);
1428
+ const base = path5.basename(file);
904
1429
  if (base === "requirements.txt") {
905
1430
  const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
906
1431
  if (edits.length > 0) {
@@ -925,7 +1450,7 @@ async function rollback2(installPlan, originals) {
925
1450
  const restored = [];
926
1451
  for (const [file, raw] of originals.entries()) {
927
1452
  try {
928
- await fs3.writeFile(file, raw, "utf8");
1453
+ await fs5.writeFile(file, raw, "utf8");
929
1454
  restored.push(file);
930
1455
  } catch {
931
1456
  }
@@ -939,8 +1464,8 @@ async function rollback2(installPlan, originals) {
939
1464
  ...restored.map((f) => `restored: ${f}`),
940
1465
  ""
941
1466
  ];
942
- const rollbackPath = path3.join(installPlan.serviceDir, "neat-rollback.patch");
943
- 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");
944
1469
  }
945
1470
  var pythonInstaller = {
946
1471
  name: "python",
@@ -951,7 +1476,7 @@ var pythonInstaller = {
951
1476
 
952
1477
  // src/installers/shared.ts
953
1478
  function isEmptyPlan(plan3) {
954
- 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;
955
1480
  }
956
1481
 
957
1482
  // src/installers/index.ts
@@ -1050,15 +1575,229 @@ function renderPatch(sections) {
1050
1575
  }
1051
1576
  lines.push("");
1052
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
+ }
1053
1584
  }
1054
1585
  return lines.join("\n");
1055
1586
  }
1056
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
+
1057
1796
  // src/cli.ts
1058
1797
  import { DivergenceTypeSchema } from "@neat.is/types";
1059
1798
 
1060
1799
  // src/cli-client.ts
1061
- import { Provenance } from "@neat.is/types";
1800
+ import { Provenance as Provenance2 } from "@neat.is/types";
1062
1801
  var HttpError = class extends Error {
1063
1802
  constructor(status, message, responseBody = "") {
1064
1803
  super(message);
@@ -1078,10 +1817,10 @@ var TransportError = class extends Error {
1078
1817
  function createHttpClient(baseUrl) {
1079
1818
  const root = baseUrl.replace(/\/$/, "");
1080
1819
  return {
1081
- async get(path5) {
1820
+ async get(path8) {
1082
1821
  let res;
1083
1822
  try {
1084
- res = await fetch(`${root}${path5}`);
1823
+ res = await fetch(`${root}${path8}`);
1085
1824
  } catch (err) {
1086
1825
  throw new TransportError(
1087
1826
  `cannot reach neat-core at ${root}: ${err.message}`
@@ -1091,16 +1830,16 @@ function createHttpClient(baseUrl) {
1091
1830
  const body = await res.text().catch(() => "");
1092
1831
  throw new HttpError(
1093
1832
  res.status,
1094
- `${res.status} ${res.statusText} on GET ${path5}: ${body}`,
1833
+ `${res.status} ${res.statusText} on GET ${path8}: ${body}`,
1095
1834
  body
1096
1835
  );
1097
1836
  }
1098
1837
  return await res.json();
1099
1838
  },
1100
- async post(path5, body) {
1839
+ async post(path8, body) {
1101
1840
  let res;
1102
1841
  try {
1103
- res = await fetch(`${root}${path5}`, {
1842
+ res = await fetch(`${root}${path8}`, {
1104
1843
  method: "POST",
1105
1844
  headers: { "content-type": "application/json" },
1106
1845
  body: JSON.stringify(body)
@@ -1114,7 +1853,7 @@ function createHttpClient(baseUrl) {
1114
1853
  const text = await res.text().catch(() => "");
1115
1854
  throw new HttpError(
1116
1855
  res.status,
1117
- `${res.status} ${res.statusText} on POST ${path5}: ${text}`,
1856
+ `${res.status} ${res.statusText} on POST ${path8}: ${text}`,
1118
1857
  text
1119
1858
  );
1120
1859
  }
@@ -1128,12 +1867,12 @@ function projectPath(project, suffix) {
1128
1867
  }
1129
1868
  async function runRootCause(client, input) {
1130
1869
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
1131
- const path5 = projectPath(
1870
+ const path8 = projectPath(
1132
1871
  input.project,
1133
1872
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
1134
1873
  );
1135
1874
  try {
1136
- const result = await client.get(path5);
1875
+ const result = await client.get(path8);
1137
1876
  const arrowPath = result.traversalPath.join(" \u2190 ");
1138
1877
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
1139
1878
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -1159,12 +1898,12 @@ async function runRootCause(client, input) {
1159
1898
  }
1160
1899
  async function runBlastRadius(client, input) {
1161
1900
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
1162
- const path5 = projectPath(
1901
+ const path8 = projectPath(
1163
1902
  input.project,
1164
1903
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
1165
1904
  );
1166
1905
  try {
1167
- const result = await client.get(path5);
1906
+ const result = await client.get(path8);
1168
1907
  if (result.totalAffected === 0) {
1169
1908
  return {
1170
1909
  summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
@@ -1193,17 +1932,17 @@ async function runBlastRadius(client, input) {
1193
1932
  }
1194
1933
  }
1195
1934
  function formatBlastEntry(n) {
1196
- 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]" : "";
1197
1936
  return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
1198
1937
  }
1199
1938
  async function runDependencies(client, input) {
1200
1939
  const depth = input.depth ?? 3;
1201
- const path5 = projectPath(
1940
+ const path8 = projectPath(
1202
1941
  input.project,
1203
1942
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
1204
1943
  );
1205
1944
  try {
1206
- const result = await client.get(path5);
1945
+ const result = await client.get(path8);
1207
1946
  if (result.total === 0) {
1208
1947
  return {
1209
1948
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -1239,9 +1978,9 @@ async function runObservedDependencies(client, input) {
1239
1978
  const edges = await client.get(
1240
1979
  projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
1241
1980
  );
1242
- const observed = edges.outbound.filter((e) => e.provenance === Provenance.OBSERVED);
1981
+ const observed = edges.outbound.filter((e) => e.provenance === Provenance2.OBSERVED);
1243
1982
  if (observed.length === 0) {
1244
- const hasExtracted = edges.outbound.some((e) => e.provenance === Provenance.EXTRACTED);
1983
+ const hasExtracted = edges.outbound.some((e) => e.provenance === Provenance2.EXTRACTED);
1245
1984
  const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
1246
1985
  return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
1247
1986
  }
@@ -1249,7 +1988,7 @@ async function runObservedDependencies(client, input) {
1249
1988
  return {
1250
1989
  summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
1251
1990
  block: blockLines.join("\n"),
1252
- provenance: Provenance.OBSERVED
1991
+ provenance: Provenance2.OBSERVED
1253
1992
  };
1254
1993
  } catch (err) {
1255
1994
  if (err instanceof HttpError && err.status === 404) {
@@ -1284,9 +2023,9 @@ function formatDuration(ms) {
1284
2023
  return `${Math.round(h / 24)}d`;
1285
2024
  }
1286
2025
  async function runIncidents(client, input) {
1287
- 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");
1288
2027
  try {
1289
- const body = await client.get(path5);
2028
+ const body = await client.get(path8);
1290
2029
  const events = body.events;
1291
2030
  if (events.length === 0) {
1292
2031
  return {
@@ -1303,7 +2042,7 @@ async function runIncidents(client, input) {
1303
2042
  return {
1304
2043
  summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
1305
2044
  block: blockLines.join("\n"),
1306
- provenance: Provenance.OBSERVED
2045
+ provenance: Provenance2.OBSERVED
1307
2046
  };
1308
2047
  } catch (err) {
1309
2048
  if (err instanceof HttpError && err.status === 404) {
@@ -1412,7 +2151,7 @@ async function runStaleEdges(client, input) {
1412
2151
  return {
1413
2152
  summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
1414
2153
  block: blockLines.join("\n"),
1415
- provenance: Provenance.STALE
2154
+ provenance: Provenance2.STALE
1416
2155
  };
1417
2156
  }
1418
2157
  async function runPolicies(client, input) {
@@ -1578,6 +2317,9 @@ function usage() {
1578
2317
  console.log(" Flags:");
1579
2318
  console.log(" --print-config print the JSON snippet to stdout");
1580
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.");
1581
2323
  console.log("");
1582
2324
  console.log("query commands (mirror the MCP tools, ADR-050):");
1583
2325
  console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
@@ -1640,6 +2382,10 @@ function parseArgs(rest) {
1640
2382
  apply: false,
1641
2383
  dryRun: false,
1642
2384
  noInstall: false,
2385
+ noInstrument: false,
2386
+ noOpen: false,
2387
+ yes: false,
2388
+ verbose: false,
1643
2389
  printConfig: false,
1644
2390
  json: false,
1645
2391
  depth: null,
@@ -1668,6 +2414,22 @@ function parseArgs(rest) {
1668
2414
  out.noInstall = true;
1669
2415
  continue;
1670
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
+ }
1671
2433
  if (arg === "--print-config") {
1672
2434
  out.printConfig = true;
1673
2435
  continue;
@@ -1723,34 +2485,6 @@ function assignFlag(out, field, value) {
1723
2485
  ;
1724
2486
  out[field] = value;
1725
2487
  }
1726
- function summarise(nodes, edges) {
1727
- const byNode = /* @__PURE__ */ new Map();
1728
- for (const n of nodes) byNode.set(n.type, (byNode.get(n.type) ?? 0) + 1);
1729
- const byEdge = /* @__PURE__ */ new Map();
1730
- for (const e of edges) byEdge.set(e.type, (byEdge.get(e.type) ?? 0) + 1);
1731
- const nodeLines = [...byNode.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([t, c]) => ` ${t}: ${c}`);
1732
- const edgeLines = [...byEdge.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([t, c]) => ` ${t}: ${c}`);
1733
- return ["nodes:", ...nodeLines, "edges:", ...edgeLines].join("\n");
1734
- }
1735
- function formatIncompat(inc) {
1736
- if (inc.kind === "node-engine") {
1737
- const range = inc.declaredNodeEngine ? ` (engines.node="${inc.declaredNodeEngine}")` : "";
1738
- return `${inc.package}@${inc.packageVersion ?? "?"} requires Node ${inc.requiredNodeVersion}${range} \u2014 ${inc.reason}`;
1739
- }
1740
- if (inc.kind === "package-conflict") {
1741
- const found = inc.foundVersion ? `@${inc.foundVersion}` : " (missing)";
1742
- return `${inc.package}@${inc.packageVersion ?? "?"} requires ${inc.requires.name}>=${inc.requires.minVersion}; found ${inc.requires.name}${found} \u2014 ${inc.reason}`;
1743
- }
1744
- if (inc.kind === "deprecated-api") {
1745
- return `${inc.package}@${inc.packageVersion ?? "?"} is deprecated \u2014 ${inc.reason}`;
1746
- }
1747
- return `${inc.driver}@${inc.driverVersion} vs ${inc.engine} ${inc.engineVersion} \u2014 ${inc.reason}`;
1748
- }
1749
- function findIncompatibilities(nodes) {
1750
- return nodes.filter(
1751
- (n) => n.type === "ServiceNode" && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
1752
- );
1753
- }
1754
2488
  function printBanner() {
1755
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");
1756
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");
@@ -1801,7 +2535,7 @@ async function buildPatchSections(services) {
1801
2535
  }
1802
2536
  async function runInit(opts) {
1803
2537
  const written = [];
1804
- const stat = await fs4.stat(opts.scanPath).catch(() => null);
2538
+ const stat = await fs7.stat(opts.scanPath).catch(() => null);
1805
2539
  if (!stat || !stat.isDirectory()) {
1806
2540
  console.error(`neat init: ${opts.scanPath} is not a directory`);
1807
2541
  return { exitCode: 2, writtenFiles: written };
@@ -1810,11 +2544,15 @@ async function runInit(opts) {
1810
2544
  printDiscoveryReport(opts, services);
1811
2545
  const sections = opts.noInstall ? [] : await buildPatchSections(services);
1812
2546
  const patch = renderPatch(sections);
1813
- const patchPath = path4.join(opts.scanPath, "neat.patch");
2547
+ const patchPath = path7.join(opts.scanPath, "neat.patch");
1814
2548
  if (opts.dryRun) {
1815
- await fs4.writeFile(patchPath, patch, "utf8");
2549
+ await fs7.writeFile(patchPath, patch, "utf8");
1816
2550
  written.push(patchPath);
1817
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/)`);
1818
2556
  console.log("rerun without --dry-run to register and snapshot.");
1819
2557
  return { exitCode: 0, writtenFiles: written };
1820
2558
  }
@@ -1823,12 +2561,16 @@ async function runInit(opts) {
1823
2561
  const graph = getGraph(graphKey);
1824
2562
  const projectPaths = pathsForProject(
1825
2563
  graphKey,
1826
- path4.join(opts.scanPath, "neat-out")
2564
+ path7.join(opts.scanPath, "neat-out")
1827
2565
  );
1828
- 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));
1829
2567
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
1830
2568
  await saveGraphToDisk(graph, opts.outPath);
1831
2569
  written.push(opts.outPath);
2570
+ const gitignoreResult = await ensureNeatOutIgnored(opts.scanPath);
2571
+ if (gitignoreResult.action !== "unchanged") {
2572
+ written.push(gitignoreResult.file);
2573
+ }
1832
2574
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
1833
2575
  try {
1834
2576
  await addProject({
@@ -1871,35 +2613,27 @@ async function runInit(opts) {
1871
2613
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
1872
2614
  }
1873
2615
  } else {
1874
- await fs4.writeFile(patchPath, patch, "utf8");
2616
+ await fs7.writeFile(patchPath, patch, "utf8");
1875
2617
  written.push(patchPath);
1876
2618
  }
1877
2619
  }
1878
- const nodes = [];
1879
- graph.forEachNode((_id, attrs) => nodes.push(attrs));
1880
- const edges = [];
1881
- graph.forEachEdge((_id, attrs) => edges.push(attrs));
1882
2620
  console.log("");
1883
- console.log("=== neat init: summary ===");
1884
2621
  console.log(`snapshot: ${opts.outPath}`);
1885
2622
  console.log(`added: ${result.nodesAdded} nodes, ${result.edgesAdded} edges`);
1886
- console.log(`total: ${graph.order} nodes, ${graph.size} edges`);
1887
- 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
+ );
1888
2632
  console.log(formatExtractionBanner(result.extractionErrors));
1889
2633
  if (result.extractionErrors > 0) {
1890
2634
  console.log(`errors: ${errorsPath}`);
1891
2635
  }
1892
2636
  console.log(formatPrecisionFloorBanner(result.extractedDropped));
1893
- const incompatibilities = findIncompatibilities(nodes);
1894
- if (incompatibilities.length > 0) {
1895
- console.log("");
1896
- console.log(`incompatibilities found in ${incompatibilities.length} service(s):`);
1897
- for (const svc of incompatibilities) {
1898
- for (const inc of svc.incompatibilities ?? []) {
1899
- console.log(` ${svc.name}: ${formatIncompat(inc)}`);
1900
- }
1901
- }
1902
- }
1903
2637
  if (result.extractionErrors > 0 && isStrictExtractionEnabled()) {
1904
2638
  return { exitCode: 4, writtenFiles: written };
1905
2639
  }
@@ -1919,9 +2653,9 @@ var CLAUDE_SKILL_CONFIG = {
1919
2653
  };
1920
2654
  function claudeConfigPath() {
1921
2655
  const override = process.env.NEAT_CLAUDE_CONFIG;
1922
- if (override && override.length > 0) return path4.resolve(override);
2656
+ if (override && override.length > 0) return path7.resolve(override);
1923
2657
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
1924
- return path4.join(home, ".claude.json");
2658
+ return path7.join(home, ".claude.json");
1925
2659
  }
1926
2660
  async function runSkill(opts) {
1927
2661
  const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -1933,7 +2667,7 @@ async function runSkill(opts) {
1933
2667
  const target = claudeConfigPath();
1934
2668
  let existing = {};
1935
2669
  try {
1936
- existing = JSON.parse(await fs4.readFile(target, "utf8"));
2670
+ existing = JSON.parse(await fs7.readFile(target, "utf8"));
1937
2671
  } catch (err) {
1938
2672
  if (err.code !== "ENOENT") {
1939
2673
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -1945,8 +2679,8 @@ async function runSkill(opts) {
1945
2679
  ...existing,
1946
2680
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
1947
2681
  };
1948
- await fs4.mkdir(path4.dirname(target), { recursive: true });
1949
- 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");
1950
2684
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
1951
2685
  console.log("restart Claude Code to pick up the new MCP server.");
1952
2686
  return { exitCode: 0 };
@@ -1980,12 +2714,12 @@ async function main() {
1980
2714
  console.error("neat init: --apply and --dry-run are mutually exclusive");
1981
2715
  process.exit(2);
1982
2716
  }
1983
- const scanPath = path4.resolve(target);
2717
+ const scanPath = path7.resolve(target);
1984
2718
  const projectExplicit = parsed.project !== null;
1985
- const projectName = projectExplicit ? project : path4.basename(scanPath);
2719
+ const projectName = projectExplicit ? project : path7.basename(scanPath);
1986
2720
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
1987
- const fallback = pathsForProject(projectKey, path4.join(scanPath, "neat-out")).snapshotPath;
1988
- 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);
1989
2723
  const result = await runInit({
1990
2724
  scanPath,
1991
2725
  outPath,
@@ -1993,7 +2727,8 @@ async function main() {
1993
2727
  projectExplicit,
1994
2728
  apply: apply3,
1995
2729
  dryRun,
1996
- noInstall
2730
+ noInstall,
2731
+ verbose: parsed.verbose
1997
2732
  });
1998
2733
  if (result.exitCode !== 0) process.exit(result.exitCode);
1999
2734
  return;
@@ -2005,21 +2740,21 @@ async function main() {
2005
2740
  usage();
2006
2741
  process.exit(2);
2007
2742
  }
2008
- const scanPath = path4.resolve(target);
2009
- const stat = await fs4.stat(scanPath).catch(() => null);
2743
+ const scanPath = path7.resolve(target);
2744
+ const stat = await fs7.stat(scanPath).catch(() => null);
2010
2745
  if (!stat || !stat.isDirectory()) {
2011
2746
  console.error(`neat watch: ${scanPath} is not a directory`);
2012
2747
  process.exit(2);
2013
2748
  }
2014
- const projectPaths = pathsForProject(project, path4.join(scanPath, "neat-out"));
2015
- const outPath = path4.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
2016
- const errorsPath = path4.resolve(
2017
- 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))
2018
2753
  );
2019
- const staleEventsPath = path4.resolve(
2020
- 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))
2021
2756
  );
2022
- 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;
2023
2758
  const handle = await startWatch(getGraph(project), {
2024
2759
  scanPath,
2025
2760
  outPath,
@@ -2112,15 +2847,62 @@ async function main() {
2112
2847
  console.log("note: neat-out/, policy.json, and other files at the project path were left in place.");
2113
2848
  return;
2114
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
+ }
2115
2876
  if (QUERY_VERBS.has(cmd)) {
2116
2877
  const code = await runQueryVerb(cmd, parsed);
2117
2878
  if (code !== 0) process.exit(code);
2118
2879
  return;
2119
2880
  }
2881
+ const orchestratorCode = await tryOrchestrator(cmd, parsed);
2882
+ if (orchestratorCode !== null) {
2883
+ if (orchestratorCode !== 0) process.exit(orchestratorCode);
2884
+ return;
2885
+ }
2120
2886
  console.error(`neat: unknown command "${cmd}"`);
2121
2887
  usage();
2122
2888
  process.exit(1);
2123
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
+ }
2124
2906
  var QUERY_VERBS = /* @__PURE__ */ new Set([
2125
2907
  "root-cause",
2126
2908
  "blast-radius",