@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/{chunk-75IBA4UL.js → chunk-7TYESDAI.js} +69 -2
- package/dist/chunk-7TYESDAI.js.map +1 -0
- package/dist/{chunk-ZU2RQRCN.js → chunk-CZ3T6TE2.js} +251 -245
- package/dist/{chunk-ZU2RQRCN.js.map → chunk-CZ3T6TE2.js.map} +1 -1
- package/dist/{chunk-EDHOCFOG.js → chunk-LQ3JFBTX.js} +15 -5
- package/dist/chunk-LQ3JFBTX.js.map +1 -0
- package/dist/{chunk-CY67YKNO.js → chunk-V4TU7OKZ.js} +16 -2
- package/dist/chunk-V4TU7OKZ.js.map +1 -0
- package/dist/cli.cjs +1185 -405
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +5 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +879 -156
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +98 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +111 -3
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +14 -4
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-PM4SWPZE.js → otel-grpc-S3AENOZ6.js} +3 -3
- package/dist/server.cjs +110 -6
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +25 -7
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-75IBA4UL.js.map +0 -1
- package/dist/chunk-CY67YKNO.js.map +0 -1
- package/dist/chunk-EDHOCFOG.js.map +0 -1
- /package/dist/{otel-grpc-PM4SWPZE.js.map → otel-grpc-S3AENOZ6.js.map} +0 -0
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-
|
|
43
|
+
} from "./chunk-CZ3T6TE2.js";
|
|
43
44
|
import {
|
|
44
45
|
startOtelGrpcReceiver
|
|
45
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-V4TU7OKZ.js";
|
|
46
47
|
import {
|
|
48
|
+
__require,
|
|
47
49
|
buildOtelReceiver
|
|
48
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-7TYESDAI.js";
|
|
49
51
|
|
|
50
52
|
// src/cli.ts
|
|
51
|
-
import
|
|
52
|
-
import { promises as
|
|
53
|
+
import path7 from "path";
|
|
54
|
+
import { promises as fs7 } from "fs";
|
|
53
55
|
|
|
54
|
-
// src/
|
|
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 =
|
|
69
|
-
const segments = relPath.split(
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
192
|
-
const policyViolationsPath =
|
|
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 ??
|
|
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,
|
|
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 =
|
|
476
|
+
const rel = path2.relative(opts.scanPath, absPath);
|
|
345
477
|
if (!rel || rel.startsWith("..")) return;
|
|
346
|
-
pendingPaths.add(rel.split(
|
|
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
|
|
400
|
-
import
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
940
|
+
let rel = path4.relative(path4.dirname(entryFile), otelInitFile);
|
|
592
941
|
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
593
|
-
rel = rel.split(
|
|
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 =
|
|
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 =
|
|
622
|
-
const envNeatFile =
|
|
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
|
|
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 ??
|
|
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 =
|
|
1114
|
+
const rel = path4.relative(serviceDir, target);
|
|
681
1115
|
if (rel.startsWith("..")) return false;
|
|
682
|
-
const base =
|
|
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
|
|
691
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
821
|
-
await
|
|
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
|
|
832
|
-
import
|
|
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
|
|
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(
|
|
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 =
|
|
1329
|
+
const file = path5.join(serviceDir, "requirements.txt");
|
|
864
1330
|
if (!await exists2(file)) return null;
|
|
865
|
-
const raw = await
|
|
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 =
|
|
1339
|
+
const procfile = path5.join(serviceDir, "Procfile");
|
|
874
1340
|
if (!await exists2(procfile)) return [];
|
|
875
|
-
const raw = await
|
|
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
|
|
928
|
-
await
|
|
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
|
|
938
|
-
await
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
1002
|
-
await
|
|
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(
|
|
1820
|
+
async get(path8) {
|
|
1141
1821
|
let res;
|
|
1142
1822
|
try {
|
|
1143
|
-
res = await fetch(`${root}${
|
|
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 ${
|
|
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(
|
|
1839
|
+
async post(path8, body) {
|
|
1160
1840
|
let res;
|
|
1161
1841
|
try {
|
|
1162
|
-
res = await fetch(`${root}${
|
|
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 ${
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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 ===
|
|
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
|
|
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(
|
|
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 ===
|
|
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 ===
|
|
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:
|
|
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
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
|
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 =
|
|
2547
|
+
const patchPath = path7.join(opts.scanPath, "neat.patch");
|
|
1873
2548
|
if (opts.dryRun) {
|
|
1874
|
-
await
|
|
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
|
-
|
|
2564
|
+
path7.join(opts.scanPath, "neat-out")
|
|
1886
2565
|
);
|
|
1887
|
-
const 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
|
|
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(
|
|
1946
|
-
|
|
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
|
|
2656
|
+
if (override && override.length > 0) return path7.resolve(override);
|
|
1982
2657
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
1983
|
-
return
|
|
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
|
|
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
|
|
2008
|
-
await
|
|
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 =
|
|
2717
|
+
const scanPath = path7.resolve(target);
|
|
2043
2718
|
const projectExplicit = parsed.project !== null;
|
|
2044
|
-
const projectName = projectExplicit ? project :
|
|
2719
|
+
const projectName = projectExplicit ? project : path7.basename(scanPath);
|
|
2045
2720
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
2046
|
-
const fallback = pathsForProject(projectKey,
|
|
2047
|
-
const outPath =
|
|
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 =
|
|
2068
|
-
const stat = await
|
|
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,
|
|
2074
|
-
const outPath =
|
|
2075
|
-
const errorsPath =
|
|
2076
|
-
process.env.NEAT_ERRORS_PATH ??
|
|
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 =
|
|
2079
|
-
process.env.NEAT_STALE_EVENTS_PATH ??
|
|
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 ?
|
|
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",
|