@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/{chunk-4ASCXBZF.js → chunk-7TYESDAI.js} +101 -5
- 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-YHQYHFI3.js → chunk-LQ3JFBTX.js} +99 -17
- package/dist/chunk-LQ3JFBTX.js.map +1 -0
- package/dist/{chunk-G3PDTGOW.js → chunk-V4TU7OKZ.js} +16 -2
- package/dist/chunk-V4TU7OKZ.js.map +1 -0
- package/dist/cli.cjs +1264 -396
- 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 +935 -153
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +215 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +228 -19
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +14 -4
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-J4O2SIBZ.js → otel-grpc-S3AENOZ6.js} +3 -3
- package/dist/server.cjs +143 -10
- 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-4ASCXBZF.js.map +0 -1
- package/dist/chunk-G3PDTGOW.js.map +0 -1
- package/dist/chunk-YHQYHFI3.js.map +0 -1
- /package/dist/{otel-grpc-J4O2SIBZ.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,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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
940
|
+
let rel = path4.relative(path4.dirname(entryFile), otelInitFile);
|
|
533
941
|
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
534
|
-
rel = rel.split(
|
|
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 =
|
|
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 =
|
|
563
|
-
const envNeatFile =
|
|
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
|
|
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 ??
|
|
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 =
|
|
1114
|
+
const rel = path4.relative(serviceDir, target);
|
|
622
1115
|
if (rel.startsWith("..")) return false;
|
|
623
|
-
const base =
|
|
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
|
|
632
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
762
|
-
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;
|
|
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
|
|
773
|
-
import
|
|
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
|
|
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(
|
|
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 =
|
|
1329
|
+
const file = path5.join(serviceDir, "requirements.txt");
|
|
805
1330
|
if (!await exists2(file)) return null;
|
|
806
|
-
const raw = await
|
|
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 =
|
|
1339
|
+
const procfile = path5.join(serviceDir, "Procfile");
|
|
815
1340
|
if (!await exists2(procfile)) return [];
|
|
816
|
-
const raw = await
|
|
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
|
|
869
|
-
await
|
|
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
|
|
879
|
-
await
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
943
|
-
await
|
|
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(
|
|
1820
|
+
async get(path8) {
|
|
1082
1821
|
let res;
|
|
1083
1822
|
try {
|
|
1084
|
-
res = await fetch(`${root}${
|
|
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 ${
|
|
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(
|
|
1839
|
+
async post(path8, body) {
|
|
1101
1840
|
let res;
|
|
1102
1841
|
try {
|
|
1103
|
-
res = await fetch(`${root}${
|
|
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 ${
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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 ===
|
|
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
|
|
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(
|
|
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 ===
|
|
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 ===
|
|
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:
|
|
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
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
|
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 =
|
|
2547
|
+
const patchPath = path7.join(opts.scanPath, "neat.patch");
|
|
1814
2548
|
if (opts.dryRun) {
|
|
1815
|
-
await
|
|
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
|
-
|
|
2564
|
+
path7.join(opts.scanPath, "neat-out")
|
|
1827
2565
|
);
|
|
1828
|
-
const 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
|
|
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(
|
|
1887
|
-
|
|
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
|
|
2656
|
+
if (override && override.length > 0) return path7.resolve(override);
|
|
1923
2657
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
1924
|
-
return
|
|
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
|
|
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
|
|
1949
|
-
await
|
|
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 =
|
|
2717
|
+
const scanPath = path7.resolve(target);
|
|
1984
2718
|
const projectExplicit = parsed.project !== null;
|
|
1985
|
-
const projectName = projectExplicit ? project :
|
|
2719
|
+
const projectName = projectExplicit ? project : path7.basename(scanPath);
|
|
1986
2720
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
1987
|
-
const fallback = pathsForProject(projectKey,
|
|
1988
|
-
const outPath =
|
|
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 =
|
|
2009
|
-
const stat = await
|
|
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,
|
|
2015
|
-
const outPath =
|
|
2016
|
-
const errorsPath =
|
|
2017
|
-
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))
|
|
2018
2753
|
);
|
|
2019
|
-
const staleEventsPath =
|
|
2020
|
-
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))
|
|
2021
2756
|
);
|
|
2022
|
-
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;
|
|
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",
|