@neat.is/core 0.3.7 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-75IBA4UL.js → chunk-4V23KYOP.js} +98 -2
- package/dist/chunk-4V23KYOP.js.map +1 -0
- package/dist/{chunk-CY67YKNO.js → chunk-IXIFJKMM.js} +16 -2
- package/dist/chunk-IXIFJKMM.js.map +1 -0
- package/dist/{chunk-EDHOCFOG.js → chunk-N6RPINEJ.js} +16 -5
- package/dist/chunk-N6RPINEJ.js.map +1 -0
- package/dist/{chunk-ZU2RQRCN.js → chunk-UPW4CMOH.js} +368 -270
- package/dist/chunk-UPW4CMOH.js.map +1 -0
- package/dist/cli.cjs +2009 -421
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +7 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +1607 -192
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +246 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +259 -29
- 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-GGZHR7VM.js} +3 -3
- package/dist/server.cjs +387 -161
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +26 -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/chunk-ZU2RQRCN.js.map +0 -1
- /package/dist/{otel-grpc-PM4SWPZE.js.map → otel-grpc-GGZHR7VM.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,
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
loadPolicyFile,
|
|
31
32
|
makeErrorSpanWriter,
|
|
32
33
|
makeSpanHandler,
|
|
34
|
+
normalizeProjectPath,
|
|
33
35
|
pathsForProject,
|
|
34
36
|
promoteFrontierNodes,
|
|
35
37
|
removeProject,
|
|
@@ -39,21 +41,152 @@ import {
|
|
|
39
41
|
setStatus,
|
|
40
42
|
startPersistLoop,
|
|
41
43
|
startStalenessLoop
|
|
42
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-UPW4CMOH.js";
|
|
43
45
|
import {
|
|
44
46
|
startOtelGrpcReceiver
|
|
45
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-IXIFJKMM.js";
|
|
46
48
|
import {
|
|
49
|
+
__require,
|
|
47
50
|
buildOtelReceiver
|
|
48
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-4V23KYOP.js";
|
|
49
52
|
|
|
50
53
|
// src/cli.ts
|
|
51
|
-
import
|
|
52
|
-
import { promises as
|
|
54
|
+
import path8 from "path";
|
|
55
|
+
import { promises as fs7 } from "fs";
|
|
53
56
|
|
|
54
|
-
// src/
|
|
55
|
-
import fs from "fs";
|
|
57
|
+
// src/gitignore.ts
|
|
58
|
+
import { promises as fs } from "fs";
|
|
56
59
|
import path from "path";
|
|
60
|
+
var NEAT_OUT_LINE = "neat-out/";
|
|
61
|
+
var NEAT_HEADER = "# NEAT \u2014 machine-local snapshots and events";
|
|
62
|
+
function isNeatOutLine(line) {
|
|
63
|
+
const trimmed = line.trim();
|
|
64
|
+
return trimmed === "neat-out/" || trimmed === "neat-out";
|
|
65
|
+
}
|
|
66
|
+
async function ensureNeatOutIgnored(projectDir) {
|
|
67
|
+
const file = path.join(projectDir, ".gitignore");
|
|
68
|
+
let existing = null;
|
|
69
|
+
try {
|
|
70
|
+
existing = await fs.readFile(file, "utf8");
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (err.code !== "ENOENT") throw err;
|
|
73
|
+
}
|
|
74
|
+
if (existing === null) {
|
|
75
|
+
await fs.writeFile(file, `${NEAT_HEADER}
|
|
76
|
+
${NEAT_OUT_LINE}
|
|
77
|
+
`, "utf8");
|
|
78
|
+
return { action: "created", file };
|
|
79
|
+
}
|
|
80
|
+
for (const line of existing.split(/\r?\n/)) {
|
|
81
|
+
if (isNeatOutLine(line)) return { action: "unchanged", file };
|
|
82
|
+
}
|
|
83
|
+
const needsLeadingNewline = existing.length > 0 && !existing.endsWith("\n");
|
|
84
|
+
const appended = `${needsLeadingNewline ? "\n" : ""}
|
|
85
|
+
${NEAT_HEADER}
|
|
86
|
+
${NEAT_OUT_LINE}
|
|
87
|
+
`;
|
|
88
|
+
await fs.writeFile(file, existing + appended, "utf8");
|
|
89
|
+
return { action: "added", file };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/summary.ts
|
|
93
|
+
import { NodeType, Provenance } from "@neat.is/types";
|
|
94
|
+
function renderOtelEnvBlock() {
|
|
95
|
+
return [
|
|
96
|
+
"for prod OTel routing, set these in your deploy platform's env:",
|
|
97
|
+
" OTEL_EXPORTER_OTLP_ENDPOINT=https://<your-neat-host>:4318",
|
|
98
|
+
" OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer <NEAT_AUTH_TOKEN>"
|
|
99
|
+
].join("\n");
|
|
100
|
+
}
|
|
101
|
+
function findIncompatServices(nodes) {
|
|
102
|
+
return nodes.filter(
|
|
103
|
+
(n) => n.type === NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
function servicesWithoutObserved(nodes, edges) {
|
|
107
|
+
const seen = /* @__PURE__ */ new Set();
|
|
108
|
+
for (const e of edges) {
|
|
109
|
+
if (e.provenance === Provenance.OBSERVED) {
|
|
110
|
+
seen.add(e.source);
|
|
111
|
+
seen.add(e.target);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return nodes.filter(
|
|
115
|
+
(n) => n.type === NodeType.ServiceNode && !seen.has(n.id)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
function formatDivergence(d) {
|
|
119
|
+
const conf = d.confidence.toFixed(2);
|
|
120
|
+
return ` [${conf}] ${d.type} ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
|
|
121
|
+
}
|
|
122
|
+
function renderValueForwardSummary(input) {
|
|
123
|
+
const { graph, divergences, verbose } = input;
|
|
124
|
+
const nodes = [];
|
|
125
|
+
graph.forEachNode((_id, attrs) => nodes.push(attrs));
|
|
126
|
+
const edges = [];
|
|
127
|
+
graph.forEachEdge((_id, attrs) => edges.push(attrs));
|
|
128
|
+
const lines = [];
|
|
129
|
+
lines.push("=== neat: findings ===");
|
|
130
|
+
lines.push("");
|
|
131
|
+
const incompatServices = findIncompatServices(nodes);
|
|
132
|
+
const totalIncompats = incompatServices.reduce(
|
|
133
|
+
(acc, s) => acc + (s.incompatibilities?.length ?? 0),
|
|
134
|
+
0
|
|
135
|
+
);
|
|
136
|
+
lines.push(`compat violations: ${totalIncompats}`);
|
|
137
|
+
for (const svc of incompatServices) {
|
|
138
|
+
for (const inc of svc.incompatibilities ?? []) {
|
|
139
|
+
const detail = formatIncompat(inc);
|
|
140
|
+
lines.push(` ${svc.name}: ${detail}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
lines.push("");
|
|
144
|
+
const top = [...divergences].sort((a, b) => b.confidence - a.confidence).slice(0, 3);
|
|
145
|
+
lines.push(`top divergences: ${divergences.length} total${top.length > 0 ? ", top 3:" : ""}`);
|
|
146
|
+
for (const d of top) lines.push(formatDivergence(d));
|
|
147
|
+
lines.push("");
|
|
148
|
+
const noObserved = servicesWithoutObserved(nodes, edges);
|
|
149
|
+
if (noObserved.length > 0) {
|
|
150
|
+
lines.push(`services without OBSERVED coverage: ${noObserved.length}`);
|
|
151
|
+
for (const svc of noObserved) lines.push(` ${svc.name}`);
|
|
152
|
+
lines.push(" \u2192 run your services with the generated otel-init to populate OBSERVED edges.");
|
|
153
|
+
lines.push("");
|
|
154
|
+
}
|
|
155
|
+
lines.push(renderOtelEnvBlock());
|
|
156
|
+
lines.push("");
|
|
157
|
+
if (verbose) {
|
|
158
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
159
|
+
for (const n of nodes) byNode.set(n.type, (byNode.get(n.type) ?? 0) + 1);
|
|
160
|
+
const byEdge = /* @__PURE__ */ new Map();
|
|
161
|
+
for (const e of edges) byEdge.set(e.type, (byEdge.get(e.type) ?? 0) + 1);
|
|
162
|
+
lines.push("=== graph (verbose) ===");
|
|
163
|
+
lines.push(`total: ${graph.order} nodes, ${graph.size} edges`);
|
|
164
|
+
lines.push("nodes:");
|
|
165
|
+
for (const [t, c] of [...byNode.entries()].sort()) lines.push(` ${t}: ${c}`);
|
|
166
|
+
lines.push("edges:");
|
|
167
|
+
for (const [t, c] of [...byEdge.entries()].sort()) lines.push(` ${t}: ${c}`);
|
|
168
|
+
lines.push("");
|
|
169
|
+
}
|
|
170
|
+
return lines.join("\n");
|
|
171
|
+
}
|
|
172
|
+
function formatIncompat(inc) {
|
|
173
|
+
if (inc.kind === "node-engine") {
|
|
174
|
+
const range = inc.declaredNodeEngine ? ` (engines.node="${inc.declaredNodeEngine}")` : "";
|
|
175
|
+
return `${inc.package}@${inc.packageVersion ?? "?"} requires Node ${inc.requiredNodeVersion}${range} \u2014 ${inc.reason}`;
|
|
176
|
+
}
|
|
177
|
+
if (inc.kind === "package-conflict") {
|
|
178
|
+
const found = inc.foundVersion ? `@${inc.foundVersion}` : " (missing)";
|
|
179
|
+
return `${inc.package}@${inc.packageVersion ?? "?"} requires ${inc.requires.name}>=${inc.requires.minVersion}; found ${inc.requires.name}${found} \u2014 ${inc.reason}`;
|
|
180
|
+
}
|
|
181
|
+
if (inc.kind === "deprecated-api") {
|
|
182
|
+
return `${inc.package}@${inc.packageVersion ?? "?"} is deprecated \u2014 ${inc.reason}`;
|
|
183
|
+
}
|
|
184
|
+
return `${inc.driver}@${inc.driverVersion} vs ${inc.engine} ${inc.engineVersion} \u2014 ${inc.reason}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/watch.ts
|
|
188
|
+
import fs2 from "fs";
|
|
189
|
+
import path2 from "path";
|
|
57
190
|
import chokidar from "chokidar";
|
|
58
191
|
var ALL_PHASES = [
|
|
59
192
|
"services",
|
|
@@ -65,8 +198,8 @@ var ALL_PHASES = [
|
|
|
65
198
|
];
|
|
66
199
|
function classifyChange(relPath) {
|
|
67
200
|
const phases = /* @__PURE__ */ new Set();
|
|
68
|
-
const base =
|
|
69
|
-
const segments = relPath.split(
|
|
201
|
+
const base = path2.basename(relPath).toLowerCase();
|
|
202
|
+
const segments = relPath.split(path2.sep).map((s) => s.toLowerCase());
|
|
70
203
|
if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
|
|
71
204
|
phases.add("services");
|
|
72
205
|
phases.add("aliases");
|
|
@@ -161,16 +294,16 @@ function countWatchableDirs(scanPath, limit) {
|
|
|
161
294
|
if (count >= limit) return;
|
|
162
295
|
let entries;
|
|
163
296
|
try {
|
|
164
|
-
entries =
|
|
297
|
+
entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
165
298
|
} catch {
|
|
166
299
|
return;
|
|
167
300
|
}
|
|
168
301
|
for (const e of entries) {
|
|
169
302
|
if (count >= limit) return;
|
|
170
303
|
if (!e.isDirectory()) continue;
|
|
171
|
-
if (IGNORED_WATCH_PATHS.some((re) => re.test(
|
|
304
|
+
if (IGNORED_WATCH_PATHS.some((re) => re.test(path2.join(dir, e.name) + path2.sep))) continue;
|
|
172
305
|
count++;
|
|
173
|
-
if (depth < 2) visit(
|
|
306
|
+
if (depth < 2) visit(path2.join(dir, e.name), depth + 1);
|
|
174
307
|
}
|
|
175
308
|
};
|
|
176
309
|
visit(scanPath, 0);
|
|
@@ -188,8 +321,8 @@ async function startWatch(graph, opts) {
|
|
|
188
321
|
const projectName = opts.project ?? DEFAULT_PROJECT;
|
|
189
322
|
await loadGraphFromDisk(graph, opts.outPath);
|
|
190
323
|
const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
|
|
191
|
-
const policyFilePath =
|
|
192
|
-
const policyViolationsPath =
|
|
324
|
+
const policyFilePath = path2.join(opts.scanPath, "policy.json");
|
|
325
|
+
const policyViolationsPath = path2.join(path2.dirname(opts.outPath), "policy-violations.ndjson");
|
|
193
326
|
let policies = [];
|
|
194
327
|
try {
|
|
195
328
|
policies = await loadPolicyFile(policyFilePath);
|
|
@@ -238,7 +371,7 @@ async function startWatch(graph, opts) {
|
|
|
238
371
|
const host = opts.host ?? "0.0.0.0";
|
|
239
372
|
const port = opts.port ?? 8080;
|
|
240
373
|
const otelPort = opts.otelPort ?? 4318;
|
|
241
|
-
const cachePath = opts.embeddingsCachePath ??
|
|
374
|
+
const cachePath = opts.embeddingsCachePath ?? path2.join(path2.dirname(opts.outPath), "embeddings.json");
|
|
242
375
|
let searchIndex;
|
|
243
376
|
try {
|
|
244
377
|
searchIndex = await buildSearchIndex(graph, { cachePath });
|
|
@@ -256,7 +389,7 @@ async function startWatch(graph, opts) {
|
|
|
256
389
|
// Paths are derived from the explicit options the watch caller passes
|
|
257
390
|
// — pathsForProject is only used to fill in the embeddings/snapshot
|
|
258
391
|
// fields so the registry shape is complete.
|
|
259
|
-
...pathsForProject(projectName,
|
|
392
|
+
...pathsForProject(projectName, path2.dirname(opts.outPath)),
|
|
260
393
|
snapshotPath: opts.outPath,
|
|
261
394
|
errorsPath: opts.errorsPath,
|
|
262
395
|
staleEventsPath: opts.staleEventsPath
|
|
@@ -341,9 +474,9 @@ async function startWatch(graph, opts) {
|
|
|
341
474
|
};
|
|
342
475
|
const onPath = (absPath) => {
|
|
343
476
|
if (shouldIgnore(absPath)) return;
|
|
344
|
-
const rel =
|
|
477
|
+
const rel = path2.relative(opts.scanPath, absPath);
|
|
345
478
|
if (!rel || rel.startsWith("..")) return;
|
|
346
|
-
pendingPaths.add(rel.split(
|
|
479
|
+
pendingPaths.add(rel.split(path2.sep).join("/"));
|
|
347
480
|
const phases = classifyChange(rel);
|
|
348
481
|
if (phases.size === 0) {
|
|
349
482
|
for (const p of ALL_PHASES) pending.add(p);
|
|
@@ -395,9 +528,153 @@ async function startWatch(graph, opts) {
|
|
|
395
528
|
return { api, stop };
|
|
396
529
|
}
|
|
397
530
|
|
|
531
|
+
// src/deploy/detect.ts
|
|
532
|
+
import { promises as fs3 } from "fs";
|
|
533
|
+
import path3 from "path";
|
|
534
|
+
import { spawn } from "child_process";
|
|
535
|
+
import { randomBytes } from "crypto";
|
|
536
|
+
function generateToken() {
|
|
537
|
+
return randomBytes(32).toString("base64url");
|
|
538
|
+
}
|
|
539
|
+
async function probeBinary(binary, arg) {
|
|
540
|
+
return new Promise((resolve) => {
|
|
541
|
+
const child = spawn(binary, [arg], { stdio: "ignore" });
|
|
542
|
+
const timer = setTimeout(() => {
|
|
543
|
+
child.kill("SIGKILL");
|
|
544
|
+
resolve(false);
|
|
545
|
+
}, 2e3);
|
|
546
|
+
child.once("error", () => {
|
|
547
|
+
clearTimeout(timer);
|
|
548
|
+
resolve(false);
|
|
549
|
+
});
|
|
550
|
+
child.once("exit", (code) => {
|
|
551
|
+
clearTimeout(timer);
|
|
552
|
+
resolve(code === 0);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
async function detectSubstrate(opts = {}) {
|
|
557
|
+
const hasDocker = opts.hasDocker ?? (() => probeBinary("docker", "version"));
|
|
558
|
+
const hasSystemd = opts.hasSystemd ?? (() => probeBinary("systemctl", "--version"));
|
|
559
|
+
if (await hasDocker()) return "docker-compose";
|
|
560
|
+
if (await hasSystemd()) return "systemd";
|
|
561
|
+
return "docker-run";
|
|
562
|
+
}
|
|
563
|
+
var IMAGE = "ghcr.io/neat-technologies/neat:latest";
|
|
564
|
+
function emitDockerCompose(cwd) {
|
|
565
|
+
return [
|
|
566
|
+
"services:",
|
|
567
|
+
" neat:",
|
|
568
|
+
` image: ${IMAGE}`,
|
|
569
|
+
" restart: unless-stopped",
|
|
570
|
+
" environment:",
|
|
571
|
+
" NEAT_AUTH_TOKEN: ${NEAT_AUTH_TOKEN:?NEAT_AUTH_TOKEN must be set}",
|
|
572
|
+
" ports:",
|
|
573
|
+
' - "8080:8080"',
|
|
574
|
+
' - "4318:4318"',
|
|
575
|
+
' - "6328:6328"',
|
|
576
|
+
" volumes:",
|
|
577
|
+
` - ${cwd}:/workspace`,
|
|
578
|
+
" - ./neat-data:/neat-out",
|
|
579
|
+
""
|
|
580
|
+
].join("\n");
|
|
581
|
+
}
|
|
582
|
+
function emitSystemdUnit(cwd) {
|
|
583
|
+
return [
|
|
584
|
+
"# NEAT_AUTH_TOKEN is read from /etc/neat/neatd.env at startup.",
|
|
585
|
+
"# Put `NEAT_AUTH_TOKEN=<value>` in that file with mode 0640 root:root.",
|
|
586
|
+
"[Unit]",
|
|
587
|
+
"Description=NEAT daemon",
|
|
588
|
+
"After=network-online.target",
|
|
589
|
+
"Wants=network-online.target",
|
|
590
|
+
"",
|
|
591
|
+
"[Service]",
|
|
592
|
+
"Type=simple",
|
|
593
|
+
"ExecStart=/usr/local/bin/neatd start --foreground",
|
|
594
|
+
`WorkingDirectory=${cwd}`,
|
|
595
|
+
"EnvironmentFile=/etc/neat/neatd.env",
|
|
596
|
+
"Restart=always",
|
|
597
|
+
"RestartSec=5",
|
|
598
|
+
"",
|
|
599
|
+
"[Install]",
|
|
600
|
+
"WantedBy=multi-user.target",
|
|
601
|
+
""
|
|
602
|
+
].join("\n");
|
|
603
|
+
}
|
|
604
|
+
function emitDockerRunSnippet() {
|
|
605
|
+
return [
|
|
606
|
+
"#!/usr/bin/env bash",
|
|
607
|
+
"set -euo pipefail",
|
|
608
|
+
"",
|
|
609
|
+
"# Generate a fresh token once, then store it where your secrets live.",
|
|
610
|
+
': "${NEAT_AUTH_TOKEN:?NEAT_AUTH_TOKEN must be set}"',
|
|
611
|
+
"",
|
|
612
|
+
`docker run -d --name neat \\`,
|
|
613
|
+
' -e NEAT_AUTH_TOKEN="$NEAT_AUTH_TOKEN" \\',
|
|
614
|
+
" -p 8080:8080 -p 4318:4318 -p 6328:6328 \\",
|
|
615
|
+
' -v "$PWD":/workspace -v /var/lib/neat:/neat-out \\',
|
|
616
|
+
` ${IMAGE}`,
|
|
617
|
+
""
|
|
618
|
+
].join("\n");
|
|
619
|
+
}
|
|
620
|
+
function renderOtelEnvBlock2(token, host = "<host>") {
|
|
621
|
+
return [
|
|
622
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT=https://${host}:4318`,
|
|
623
|
+
`OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${token}`,
|
|
624
|
+
"OTEL_SERVICE_NAME=<service>"
|
|
625
|
+
].join("\n");
|
|
626
|
+
}
|
|
627
|
+
async function runDeploy(opts = {}) {
|
|
628
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
629
|
+
const substrate = await detectSubstrate(opts);
|
|
630
|
+
const token = generateToken();
|
|
631
|
+
switch (substrate) {
|
|
632
|
+
case "docker-compose": {
|
|
633
|
+
const artifactPath = path3.join(cwd, "docker-compose.neat.yml");
|
|
634
|
+
const contents = emitDockerCompose(cwd);
|
|
635
|
+
await fs3.writeFile(artifactPath, contents, "utf8");
|
|
636
|
+
return {
|
|
637
|
+
substrate,
|
|
638
|
+
artifactPath,
|
|
639
|
+
token,
|
|
640
|
+
contents,
|
|
641
|
+
startCommand: `NEAT_AUTH_TOKEN=${token} docker compose -f ${path3.basename(artifactPath)} up -d`
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
case "systemd": {
|
|
645
|
+
const artifactPath = path3.join(cwd, "neat.service");
|
|
646
|
+
const contents = emitSystemdUnit(cwd);
|
|
647
|
+
await fs3.writeFile(artifactPath, contents, "utf8");
|
|
648
|
+
return {
|
|
649
|
+
substrate,
|
|
650
|
+
artifactPath,
|
|
651
|
+
token,
|
|
652
|
+
contents,
|
|
653
|
+
startCommand: [
|
|
654
|
+
`sudo install -m 0640 -o root -g root <(echo NEAT_AUTH_TOKEN=${token}) /etc/neat/neatd.env`,
|
|
655
|
+
`sudo install -m 0644 neat.service /etc/systemd/system/neat.service`,
|
|
656
|
+
"sudo systemctl daemon-reload && sudo systemctl enable --now neat"
|
|
657
|
+
].join(" && ")
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
case "docker-run":
|
|
661
|
+
default: {
|
|
662
|
+
const contents = emitDockerRunSnippet();
|
|
663
|
+
return {
|
|
664
|
+
substrate: "docker-run",
|
|
665
|
+
token,
|
|
666
|
+
contents,
|
|
667
|
+
startCommand: `NEAT_AUTH_TOKEN=${token} bash <(cat <<'EOF'
|
|
668
|
+
${contents}EOF
|
|
669
|
+
)`
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
398
675
|
// src/installers/javascript.ts
|
|
399
|
-
import { promises as
|
|
400
|
-
import
|
|
676
|
+
import { promises as fs4 } from "fs";
|
|
677
|
+
import path4 from "path";
|
|
401
678
|
|
|
402
679
|
// src/installers/templates.ts
|
|
403
680
|
var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
|
|
@@ -444,6 +721,151 @@ function renderEnvNeat(serviceName) {
|
|
|
444
721
|
""
|
|
445
722
|
].join("\n");
|
|
446
723
|
}
|
|
724
|
+
var NEXT_INSTRUMENTATION_HEADER = "// Generated by `neat init --apply` (ADR-073). Next.js instrumentation hook.";
|
|
725
|
+
var NEXT_INSTRUMENTATION_TS = `${NEXT_INSTRUMENTATION_HEADER}
|
|
726
|
+
export async function register() {
|
|
727
|
+
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
728
|
+
await import('./instrumentation.node')
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
`;
|
|
732
|
+
var NEXT_INSTRUMENTATION_JS = `${NEXT_INSTRUMENTATION_HEADER}
|
|
733
|
+
export async function register() {
|
|
734
|
+
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
735
|
+
await import('./instrumentation.node')
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
`;
|
|
739
|
+
var NEXT_INSTRUMENTATION_NODE_TS = `${NEXT_INSTRUMENTATION_HEADER}
|
|
740
|
+
// Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
|
|
741
|
+
// the OTel SDK starts so the exporter target and service name are in scope.
|
|
742
|
+
import { fileURLToPath } from 'node:url'
|
|
743
|
+
import path from 'node:path'
|
|
744
|
+
import dotenv from 'dotenv'
|
|
745
|
+
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
746
|
+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
747
|
+
|
|
748
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
749
|
+
dotenv.config({ path: path.join(here, '.env.neat') })
|
|
750
|
+
|
|
751
|
+
const sdk = new NodeSDK({
|
|
752
|
+
serviceName: process.env.OTEL_SERVICE_NAME,
|
|
753
|
+
instrumentations: [getNodeAutoInstrumentations()],
|
|
754
|
+
})
|
|
755
|
+
sdk.start()
|
|
756
|
+
`;
|
|
757
|
+
var NEXT_INSTRUMENTATION_NODE_JS = `${NEXT_INSTRUMENTATION_HEADER}
|
|
758
|
+
// Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
|
|
759
|
+
// the OTel SDK starts so the exporter target and service name are in scope.
|
|
760
|
+
import { fileURLToPath } from 'node:url'
|
|
761
|
+
import path from 'node:path'
|
|
762
|
+
import dotenv from 'dotenv'
|
|
763
|
+
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
764
|
+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
765
|
+
|
|
766
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
767
|
+
dotenv.config({ path: path.join(here, '.env.neat') })
|
|
768
|
+
|
|
769
|
+
const sdk = new NodeSDK({
|
|
770
|
+
serviceName: process.env.OTEL_SERVICE_NAME,
|
|
771
|
+
instrumentations: [getNodeAutoInstrumentations()],
|
|
772
|
+
})
|
|
773
|
+
sdk.start()
|
|
774
|
+
`;
|
|
775
|
+
var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
|
|
776
|
+
var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
777
|
+
// Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
|
|
778
|
+
// the OTel SDK starts so the exporter target and service name are in scope.
|
|
779
|
+
import { fileURLToPath } from 'node:url'
|
|
780
|
+
import path from 'node:path'
|
|
781
|
+
import dotenv from 'dotenv'
|
|
782
|
+
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
783
|
+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
784
|
+
|
|
785
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
786
|
+
dotenv.config({ path: path.join(here, '.env.neat') })
|
|
787
|
+
|
|
788
|
+
const sdk = new NodeSDK({
|
|
789
|
+
serviceName: process.env.OTEL_SERVICE_NAME,
|
|
790
|
+
instrumentations: [getNodeAutoInstrumentations()],
|
|
791
|
+
})
|
|
792
|
+
sdk.start()
|
|
793
|
+
`;
|
|
794
|
+
var FRAMEWORK_OTEL_INIT_JS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
795
|
+
// Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
|
|
796
|
+
// the OTel SDK starts so the exporter target and service name are in scope.
|
|
797
|
+
const path = require('node:path')
|
|
798
|
+
try {
|
|
799
|
+
require('dotenv').config({ path: path.join(__dirname, '.env.neat') })
|
|
800
|
+
} catch (err) {
|
|
801
|
+
// dotenv unavailable \u2014 fall through to process.env.
|
|
802
|
+
}
|
|
803
|
+
const { NodeSDK } = require('@opentelemetry/sdk-node')
|
|
804
|
+
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
|
|
805
|
+
|
|
806
|
+
const sdk = new NodeSDK({
|
|
807
|
+
serviceName: process.env.OTEL_SERVICE_NAME,
|
|
808
|
+
instrumentations: [getNodeAutoInstrumentations()],
|
|
809
|
+
})
|
|
810
|
+
sdk.start()
|
|
811
|
+
`;
|
|
812
|
+
var REMIX_OTEL_SERVER_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
|
|
813
|
+
var REMIX_OTEL_SERVER_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
|
|
814
|
+
var SVELTEKIT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
|
|
815
|
+
var SVELTEKIT_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
|
|
816
|
+
var SVELTEKIT_HOOKS_SERVER_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
817
|
+
import './otel-init'
|
|
818
|
+
|
|
819
|
+
import type { Handle } from '@sveltejs/kit'
|
|
820
|
+
|
|
821
|
+
export const handle: Handle = async ({ event, resolve }) => {
|
|
822
|
+
return resolve(event)
|
|
823
|
+
}
|
|
824
|
+
`;
|
|
825
|
+
var SVELTEKIT_HOOKS_SERVER_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
826
|
+
import './otel-init'
|
|
827
|
+
|
|
828
|
+
/** @type {import('@sveltejs/kit').Handle} */
|
|
829
|
+
export const handle = async ({ event, resolve }) => {
|
|
830
|
+
return resolve(event)
|
|
831
|
+
}
|
|
832
|
+
`;
|
|
833
|
+
var NUXT_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
|
|
834
|
+
var NUXT_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
|
|
835
|
+
var NUXT_OTEL_PLUGIN_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
836
|
+
import './otel-init'
|
|
837
|
+
|
|
838
|
+
export default defineNitroPlugin(() => {
|
|
839
|
+
// OTel SDK is initialised at module-load via the side-effect import above.
|
|
840
|
+
})
|
|
841
|
+
`;
|
|
842
|
+
var NUXT_OTEL_PLUGIN_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
843
|
+
require('./otel-init')
|
|
844
|
+
|
|
845
|
+
module.exports = defineNitroPlugin(() => {
|
|
846
|
+
// OTel SDK is initialised at module-load via the side-effect import above.
|
|
847
|
+
})
|
|
848
|
+
`;
|
|
849
|
+
var ASTRO_OTEL_INIT_TS = FRAMEWORK_OTEL_INIT_TS_BODY;
|
|
850
|
+
var ASTRO_OTEL_INIT_JS = FRAMEWORK_OTEL_INIT_JS_BODY;
|
|
851
|
+
var ASTRO_MIDDLEWARE_TS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
852
|
+
import './otel-init'
|
|
853
|
+
|
|
854
|
+
import { defineMiddleware } from 'astro:middleware'
|
|
855
|
+
|
|
856
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
857
|
+
return next()
|
|
858
|
+
})
|
|
859
|
+
`;
|
|
860
|
+
var ASTRO_MIDDLEWARE_JS = `${FRAMEWORK_OTEL_INIT_HEADER}
|
|
861
|
+
import './otel-init'
|
|
862
|
+
|
|
863
|
+
import { defineMiddleware } from 'astro:middleware'
|
|
864
|
+
|
|
865
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
866
|
+
return next()
|
|
867
|
+
})
|
|
868
|
+
`;
|
|
447
869
|
|
|
448
870
|
// src/installers/javascript.ts
|
|
449
871
|
var SDK_PACKAGES = [
|
|
@@ -466,7 +888,7 @@ var OTEL_ENV = {
|
|
|
466
888
|
};
|
|
467
889
|
async function readPackageJson(serviceDir) {
|
|
468
890
|
try {
|
|
469
|
-
const raw = await
|
|
891
|
+
const raw = await fs4.readFile(path4.join(serviceDir, "package.json"), "utf8");
|
|
470
892
|
return JSON.parse(raw);
|
|
471
893
|
} catch {
|
|
472
894
|
return null;
|
|
@@ -474,7 +896,7 @@ async function readPackageJson(serviceDir) {
|
|
|
474
896
|
}
|
|
475
897
|
async function exists(p) {
|
|
476
898
|
try {
|
|
477
|
-
await
|
|
899
|
+
await fs4.stat(p);
|
|
478
900
|
return true;
|
|
479
901
|
} catch {
|
|
480
902
|
return false;
|
|
@@ -484,6 +906,93 @@ async function detect(serviceDir) {
|
|
|
484
906
|
const pkg = await readPackageJson(serviceDir);
|
|
485
907
|
return pkg !== null && typeof pkg.name === "string";
|
|
486
908
|
}
|
|
909
|
+
var NEXT_CONFIG_CANDIDATES = ["next.config.js", "next.config.ts", "next.config.mjs"];
|
|
910
|
+
async function findNextConfig(serviceDir) {
|
|
911
|
+
for (const name of NEXT_CONFIG_CANDIDATES) {
|
|
912
|
+
const candidate = path4.join(serviceDir, name);
|
|
913
|
+
if (await exists(candidate)) return candidate;
|
|
914
|
+
}
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
function hasNextDependency(pkg) {
|
|
918
|
+
return pkg.dependencies?.next !== void 0 || pkg.devDependencies?.next !== void 0;
|
|
919
|
+
}
|
|
920
|
+
function allDeps(pkg) {
|
|
921
|
+
return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
922
|
+
}
|
|
923
|
+
var REMIX_ENTRY_CANDIDATES = [
|
|
924
|
+
"app/entry.server.ts",
|
|
925
|
+
"app/entry.server.tsx",
|
|
926
|
+
"app/entry.server.js",
|
|
927
|
+
"app/entry.server.jsx"
|
|
928
|
+
];
|
|
929
|
+
function hasRemixDependency(pkg) {
|
|
930
|
+
const deps = allDeps(pkg);
|
|
931
|
+
if ("remix" in deps) return true;
|
|
932
|
+
for (const name of Object.keys(deps)) {
|
|
933
|
+
if (name.startsWith("@remix-run/")) return true;
|
|
934
|
+
}
|
|
935
|
+
return false;
|
|
936
|
+
}
|
|
937
|
+
async function findRemixEntry(serviceDir) {
|
|
938
|
+
for (const rel of REMIX_ENTRY_CANDIDATES) {
|
|
939
|
+
const candidate = path4.join(serviceDir, rel);
|
|
940
|
+
if (await exists(candidate)) return candidate;
|
|
941
|
+
}
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
var SVELTEKIT_HOOKS_CANDIDATES = ["src/hooks.server.ts", "src/hooks.server.js"];
|
|
945
|
+
var SVELTEKIT_CONFIG_CANDIDATES = ["svelte.config.js", "svelte.config.ts"];
|
|
946
|
+
function hasSvelteKitDependency(pkg) {
|
|
947
|
+
return "@sveltejs/kit" in allDeps(pkg);
|
|
948
|
+
}
|
|
949
|
+
async function findSvelteKitHooks(serviceDir) {
|
|
950
|
+
for (const rel of SVELTEKIT_HOOKS_CANDIDATES) {
|
|
951
|
+
const candidate = path4.join(serviceDir, rel);
|
|
952
|
+
if (await exists(candidate)) return candidate;
|
|
953
|
+
}
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
async function findSvelteKitConfig(serviceDir) {
|
|
957
|
+
for (const rel of SVELTEKIT_CONFIG_CANDIDATES) {
|
|
958
|
+
const candidate = path4.join(serviceDir, rel);
|
|
959
|
+
if (await exists(candidate)) return candidate;
|
|
960
|
+
}
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
var NUXT_CONFIG_CANDIDATES = ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"];
|
|
964
|
+
function hasNuxtDependency(pkg) {
|
|
965
|
+
return "nuxt" in allDeps(pkg);
|
|
966
|
+
}
|
|
967
|
+
async function findNuxtConfig(serviceDir) {
|
|
968
|
+
for (const name of NUXT_CONFIG_CANDIDATES) {
|
|
969
|
+
const candidate = path4.join(serviceDir, name);
|
|
970
|
+
if (await exists(candidate)) return candidate;
|
|
971
|
+
}
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
var ASTRO_CONFIG_CANDIDATES = ["astro.config.mjs", "astro.config.ts", "astro.config.js"];
|
|
975
|
+
function hasAstroDependency(pkg) {
|
|
976
|
+
return "astro" in allDeps(pkg);
|
|
977
|
+
}
|
|
978
|
+
async function findAstroConfig(serviceDir) {
|
|
979
|
+
for (const name of ASTRO_CONFIG_CANDIDATES) {
|
|
980
|
+
const candidate = path4.join(serviceDir, name);
|
|
981
|
+
if (await exists(candidate)) return candidate;
|
|
982
|
+
}
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
function parseNextMajor(range) {
|
|
986
|
+
if (!range) return null;
|
|
987
|
+
const cleaned = range.trim().replace(/^[\^~>=<\s]+/, "");
|
|
988
|
+
const match = cleaned.match(/^(\d+)/);
|
|
989
|
+
if (!match) return null;
|
|
990
|
+
const n = Number(match[1]);
|
|
991
|
+
return Number.isFinite(n) ? n : null;
|
|
992
|
+
}
|
|
993
|
+
async function isTypeScriptProject(serviceDir) {
|
|
994
|
+
return exists(path4.join(serviceDir, "tsconfig.json"));
|
|
995
|
+
}
|
|
487
996
|
var INDEX_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".cjs"];
|
|
488
997
|
var INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `index${ext}`);
|
|
489
998
|
var SRC_INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `src/index${ext}`);
|
|
@@ -528,7 +1037,7 @@ function entryFromScript(script) {
|
|
|
528
1037
|
}
|
|
529
1038
|
async function resolveEntry(serviceDir, pkg) {
|
|
530
1039
|
if (typeof pkg.main === "string" && pkg.main.length > 0) {
|
|
531
|
-
const candidate =
|
|
1040
|
+
const candidate = path4.resolve(serviceDir, pkg.main);
|
|
532
1041
|
if (await exists(candidate)) return candidate;
|
|
533
1042
|
}
|
|
534
1043
|
if (pkg.bin) {
|
|
@@ -542,68 +1051,417 @@ async function resolveEntry(serviceDir, pkg) {
|
|
|
542
1051
|
if (typeof first === "string") binEntry = first;
|
|
543
1052
|
}
|
|
544
1053
|
if (binEntry) {
|
|
545
|
-
const candidate =
|
|
1054
|
+
const candidate = path4.resolve(serviceDir, binEntry);
|
|
546
1055
|
if (await exists(candidate)) return candidate;
|
|
547
1056
|
}
|
|
548
1057
|
}
|
|
549
1058
|
const startEntry = entryFromScript(pkg.scripts?.start);
|
|
550
1059
|
if (startEntry) {
|
|
551
|
-
const candidate =
|
|
1060
|
+
const candidate = path4.resolve(serviceDir, startEntry);
|
|
552
1061
|
if (await exists(candidate)) return candidate;
|
|
553
1062
|
}
|
|
554
1063
|
const devEntry = entryFromScript(pkg.scripts?.dev);
|
|
555
1064
|
if (devEntry) {
|
|
556
|
-
const candidate =
|
|
1065
|
+
const candidate = path4.resolve(serviceDir, devEntry);
|
|
557
1066
|
if (await exists(candidate)) return candidate;
|
|
558
1067
|
}
|
|
559
1068
|
for (const rel of SRC_INDEX_CANDIDATES) {
|
|
560
|
-
const candidate =
|
|
1069
|
+
const candidate = path4.join(serviceDir, rel);
|
|
561
1070
|
if (await exists(candidate)) return candidate;
|
|
562
1071
|
}
|
|
563
1072
|
for (const rel of SRC_NAMED_CANDIDATES) {
|
|
564
|
-
const candidate =
|
|
1073
|
+
const candidate = path4.join(serviceDir, rel);
|
|
565
1074
|
if (await exists(candidate)) return candidate;
|
|
566
1075
|
}
|
|
567
|
-
for (const name of INDEX_CANDIDATES) {
|
|
568
|
-
const candidate =
|
|
569
|
-
if (await exists(candidate)) return candidate;
|
|
1076
|
+
for (const name of INDEX_CANDIDATES) {
|
|
1077
|
+
const candidate = path4.join(serviceDir, name);
|
|
1078
|
+
if (await exists(candidate)) return candidate;
|
|
1079
|
+
}
|
|
1080
|
+
return null;
|
|
1081
|
+
}
|
|
1082
|
+
function dispatchEntry(entryFile, pkg) {
|
|
1083
|
+
const ext = path4.extname(entryFile).toLowerCase();
|
|
1084
|
+
if (ext === ".ts" || ext === ".tsx") return "ts";
|
|
1085
|
+
if (ext === ".mjs") return "esm";
|
|
1086
|
+
if (ext === ".cjs") return "cjs";
|
|
1087
|
+
return pkg.type === "module" ? "esm" : "cjs";
|
|
1088
|
+
}
|
|
1089
|
+
function otelInitFilename(flavor) {
|
|
1090
|
+
if (flavor === "ts") return "otel-init.ts";
|
|
1091
|
+
if (flavor === "esm") return "otel-init.mjs";
|
|
1092
|
+
return "otel-init.cjs";
|
|
1093
|
+
}
|
|
1094
|
+
function otelInitContents(flavor) {
|
|
1095
|
+
if (flavor === "ts") return OTEL_INIT_TS;
|
|
1096
|
+
if (flavor === "esm") return OTEL_INIT_ESM;
|
|
1097
|
+
return OTEL_INIT_CJS;
|
|
1098
|
+
}
|
|
1099
|
+
function injectionLine(flavor, entryFile, otelInitFile) {
|
|
1100
|
+
let rel = path4.relative(path4.dirname(entryFile), otelInitFile);
|
|
1101
|
+
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
1102
|
+
rel = rel.split(path4.sep).join("/");
|
|
1103
|
+
if (flavor === "cjs") return `require('${rel}')`;
|
|
1104
|
+
if (flavor === "esm") return `import '${rel}'`;
|
|
1105
|
+
const tsRel = rel.replace(/\.ts$/, "");
|
|
1106
|
+
return `import '${tsRel}'`;
|
|
1107
|
+
}
|
|
1108
|
+
function lineIsOtelInjection(line) {
|
|
1109
|
+
const trimmed = line.trim();
|
|
1110
|
+
if (trimmed.length === 0) return false;
|
|
1111
|
+
return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
|
|
1112
|
+
}
|
|
1113
|
+
async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
|
|
1114
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
1115
|
+
const instrumentationFile = path4.join(serviceDir, useTs ? "instrumentation.ts" : "instrumentation.js");
|
|
1116
|
+
const instrumentationNodeFile = path4.join(
|
|
1117
|
+
serviceDir,
|
|
1118
|
+
useTs ? "instrumentation.node.ts" : "instrumentation.node.js"
|
|
1119
|
+
);
|
|
1120
|
+
const envNeatFile = path4.join(serviceDir, ".env.neat");
|
|
1121
|
+
const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
1122
|
+
const dependencyEdits = [];
|
|
1123
|
+
for (const sdk of SDK_PACKAGES) {
|
|
1124
|
+
if (sdk.name in existingDeps) continue;
|
|
1125
|
+
dependencyEdits.push({
|
|
1126
|
+
file: manifestPath,
|
|
1127
|
+
kind: "add",
|
|
1128
|
+
name: sdk.name,
|
|
1129
|
+
version: sdk.version
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
const generatedFiles = [];
|
|
1133
|
+
if (!await exists(instrumentationFile)) {
|
|
1134
|
+
generatedFiles.push({
|
|
1135
|
+
file: instrumentationFile,
|
|
1136
|
+
contents: useTs ? NEXT_INSTRUMENTATION_TS : NEXT_INSTRUMENTATION_JS,
|
|
1137
|
+
skipIfExists: true
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
if (!await exists(instrumentationNodeFile)) {
|
|
1141
|
+
generatedFiles.push({
|
|
1142
|
+
file: instrumentationNodeFile,
|
|
1143
|
+
contents: useTs ? NEXT_INSTRUMENTATION_NODE_TS : NEXT_INSTRUMENTATION_NODE_JS,
|
|
1144
|
+
skipIfExists: true
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
if (!await exists(envNeatFile)) {
|
|
1148
|
+
generatedFiles.push({
|
|
1149
|
+
file: envNeatFile,
|
|
1150
|
+
contents: renderEnvNeat(pkg.name ?? path4.basename(serviceDir)),
|
|
1151
|
+
skipIfExists: true
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
let nextConfigEdit;
|
|
1155
|
+
const nextRange = pkg.dependencies?.next ?? pkg.devDependencies?.next;
|
|
1156
|
+
const nextMajor = parseNextMajor(nextRange);
|
|
1157
|
+
if (nextMajor !== null && nextMajor < 15) {
|
|
1158
|
+
try {
|
|
1159
|
+
const raw = await fs4.readFile(nextConfigPath, "utf8");
|
|
1160
|
+
if (!raw.includes("instrumentationHook")) {
|
|
1161
|
+
nextConfigEdit = {
|
|
1162
|
+
file: nextConfigPath,
|
|
1163
|
+
reason: `enable experimental.instrumentationHook (Next ${nextMajor} requires the opt-in flag)`
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
} catch {
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && nextConfigEdit === void 0;
|
|
1170
|
+
if (empty) {
|
|
1171
|
+
return {
|
|
1172
|
+
language: "javascript",
|
|
1173
|
+
serviceDir,
|
|
1174
|
+
dependencyEdits: [],
|
|
1175
|
+
entrypointEdits: [],
|
|
1176
|
+
envEdits: [],
|
|
1177
|
+
generatedFiles: [],
|
|
1178
|
+
framework: "next"
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
return {
|
|
1182
|
+
language: "javascript",
|
|
1183
|
+
serviceDir,
|
|
1184
|
+
dependencyEdits,
|
|
1185
|
+
entrypointEdits: [],
|
|
1186
|
+
envEdits: [OTEL_ENV],
|
|
1187
|
+
generatedFiles,
|
|
1188
|
+
framework: "next",
|
|
1189
|
+
...nextConfigEdit ? { nextConfigEdit } : {}
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
function buildDependencyEdits(pkg, manifestPath) {
|
|
1193
|
+
const existingDeps = allDeps(pkg);
|
|
1194
|
+
const edits = [];
|
|
1195
|
+
for (const sdk of SDK_PACKAGES) {
|
|
1196
|
+
if (sdk.name in existingDeps) continue;
|
|
1197
|
+
edits.push({
|
|
1198
|
+
file: manifestPath,
|
|
1199
|
+
kind: "add",
|
|
1200
|
+
name: sdk.name,
|
|
1201
|
+
version: sdk.version
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
return edits;
|
|
1205
|
+
}
|
|
1206
|
+
async function queueEnvNeat(serviceDir, pkg, generatedFiles) {
|
|
1207
|
+
const envNeatFile = path4.join(serviceDir, ".env.neat");
|
|
1208
|
+
if (!await exists(envNeatFile)) {
|
|
1209
|
+
generatedFiles.push({
|
|
1210
|
+
file: envNeatFile,
|
|
1211
|
+
contents: renderEnvNeat(pkg.name ?? path4.basename(serviceDir)),
|
|
1212
|
+
skipIfExists: true
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
function fileImportsOtelHook(raw, specifiers) {
|
|
1217
|
+
const lines = raw.split(/\r?\n/);
|
|
1218
|
+
for (const line of lines) {
|
|
1219
|
+
const trimmed = line.trim();
|
|
1220
|
+
for (const spec of specifiers) {
|
|
1221
|
+
const escaped = spec.replace(/\./g, "\\.");
|
|
1222
|
+
const pattern = new RegExp(
|
|
1223
|
+
`(?:import\\s+['"]${escaped}['"]|require\\(['"]${escaped}['"]\\))`
|
|
1224
|
+
);
|
|
1225
|
+
if (pattern.test(trimmed)) return true;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
return false;
|
|
1229
|
+
}
|
|
1230
|
+
async function planRemix(serviceDir, pkg, manifestPath, entryFile) {
|
|
1231
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
1232
|
+
const otelServerFile = path4.join(
|
|
1233
|
+
serviceDir,
|
|
1234
|
+
useTs ? "app/otel.server.ts" : "app/otel.server.js"
|
|
1235
|
+
);
|
|
1236
|
+
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
1237
|
+
const generatedFiles = [];
|
|
1238
|
+
if (!await exists(otelServerFile)) {
|
|
1239
|
+
generatedFiles.push({
|
|
1240
|
+
file: otelServerFile,
|
|
1241
|
+
contents: useTs ? REMIX_OTEL_SERVER_TS : REMIX_OTEL_SERVER_JS,
|
|
1242
|
+
skipIfExists: true
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
await queueEnvNeat(serviceDir, pkg, generatedFiles);
|
|
1246
|
+
const entrypointEdits = [];
|
|
1247
|
+
try {
|
|
1248
|
+
const raw = await fs4.readFile(entryFile, "utf8");
|
|
1249
|
+
if (!fileImportsOtelHook(raw, ["./otel.server"])) {
|
|
1250
|
+
const lines = raw.split(/\r?\n/);
|
|
1251
|
+
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
1252
|
+
entrypointEdits.push({
|
|
1253
|
+
file: entryFile,
|
|
1254
|
+
before: firstReal,
|
|
1255
|
+
after: "import './otel.server'"
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
} catch {
|
|
1259
|
+
}
|
|
1260
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
|
|
1261
|
+
if (empty) {
|
|
1262
|
+
return {
|
|
1263
|
+
language: "javascript",
|
|
1264
|
+
serviceDir,
|
|
1265
|
+
dependencyEdits: [],
|
|
1266
|
+
entrypointEdits: [],
|
|
1267
|
+
envEdits: [],
|
|
1268
|
+
generatedFiles: [],
|
|
1269
|
+
framework: "remix"
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
return {
|
|
1273
|
+
language: "javascript",
|
|
1274
|
+
serviceDir,
|
|
1275
|
+
dependencyEdits,
|
|
1276
|
+
entrypointEdits,
|
|
1277
|
+
envEdits: [OTEL_ENV],
|
|
1278
|
+
generatedFiles,
|
|
1279
|
+
framework: "remix"
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile) {
|
|
1283
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
1284
|
+
const otelInitFile = path4.join(
|
|
1285
|
+
serviceDir,
|
|
1286
|
+
useTs ? "src/otel-init.ts" : "src/otel-init.js"
|
|
1287
|
+
);
|
|
1288
|
+
const resolvedHooksFile = hooksFile ?? path4.join(serviceDir, useTs ? "src/hooks.server.ts" : "src/hooks.server.js");
|
|
1289
|
+
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
1290
|
+
const generatedFiles = [];
|
|
1291
|
+
const entrypointEdits = [];
|
|
1292
|
+
if (!await exists(otelInitFile)) {
|
|
1293
|
+
generatedFiles.push({
|
|
1294
|
+
file: otelInitFile,
|
|
1295
|
+
contents: useTs ? SVELTEKIT_OTEL_INIT_TS : SVELTEKIT_OTEL_INIT_JS,
|
|
1296
|
+
skipIfExists: true
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
await queueEnvNeat(serviceDir, pkg, generatedFiles);
|
|
1300
|
+
if (hooksFile === null) {
|
|
1301
|
+
generatedFiles.push({
|
|
1302
|
+
file: resolvedHooksFile,
|
|
1303
|
+
contents: useTs ? SVELTEKIT_HOOKS_SERVER_TS : SVELTEKIT_HOOKS_SERVER_JS,
|
|
1304
|
+
skipIfExists: true
|
|
1305
|
+
});
|
|
1306
|
+
} else {
|
|
1307
|
+
try {
|
|
1308
|
+
const raw = await fs4.readFile(hooksFile, "utf8");
|
|
1309
|
+
if (!fileImportsOtelHook(raw, ["./otel-init"])) {
|
|
1310
|
+
const lines = raw.split(/\r?\n/);
|
|
1311
|
+
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
1312
|
+
entrypointEdits.push({
|
|
1313
|
+
file: hooksFile,
|
|
1314
|
+
before: firstReal,
|
|
1315
|
+
after: "import './otel-init'"
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
} catch {
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
|
|
1322
|
+
if (empty) {
|
|
1323
|
+
return {
|
|
1324
|
+
language: "javascript",
|
|
1325
|
+
serviceDir,
|
|
1326
|
+
dependencyEdits: [],
|
|
1327
|
+
entrypointEdits: [],
|
|
1328
|
+
envEdits: [],
|
|
1329
|
+
generatedFiles: [],
|
|
1330
|
+
framework: "sveltekit"
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
return {
|
|
1334
|
+
language: "javascript",
|
|
1335
|
+
serviceDir,
|
|
1336
|
+
dependencyEdits,
|
|
1337
|
+
entrypointEdits,
|
|
1338
|
+
envEdits: [OTEL_ENV],
|
|
1339
|
+
generatedFiles,
|
|
1340
|
+
framework: "sveltekit"
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
async function planNuxt(serviceDir, pkg, manifestPath) {
|
|
1344
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
1345
|
+
const otelPluginFile = path4.join(
|
|
1346
|
+
serviceDir,
|
|
1347
|
+
useTs ? "server/plugins/otel.ts" : "server/plugins/otel.js"
|
|
1348
|
+
);
|
|
1349
|
+
const otelInitFile = path4.join(
|
|
1350
|
+
serviceDir,
|
|
1351
|
+
useTs ? "server/plugins/otel-init.ts" : "server/plugins/otel-init.js"
|
|
1352
|
+
);
|
|
1353
|
+
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
1354
|
+
const generatedFiles = [];
|
|
1355
|
+
if (!await exists(otelInitFile)) {
|
|
1356
|
+
generatedFiles.push({
|
|
1357
|
+
file: otelInitFile,
|
|
1358
|
+
contents: useTs ? NUXT_OTEL_INIT_TS : NUXT_OTEL_INIT_JS,
|
|
1359
|
+
skipIfExists: true
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
if (!await exists(otelPluginFile)) {
|
|
1363
|
+
generatedFiles.push({
|
|
1364
|
+
file: otelPluginFile,
|
|
1365
|
+
contents: useTs ? NUXT_OTEL_PLUGIN_TS : NUXT_OTEL_PLUGIN_JS,
|
|
1366
|
+
skipIfExists: true
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
await queueEnvNeat(serviceDir, pkg, generatedFiles);
|
|
1370
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0;
|
|
1371
|
+
if (empty) {
|
|
1372
|
+
return {
|
|
1373
|
+
language: "javascript",
|
|
1374
|
+
serviceDir,
|
|
1375
|
+
dependencyEdits: [],
|
|
1376
|
+
entrypointEdits: [],
|
|
1377
|
+
envEdits: [],
|
|
1378
|
+
generatedFiles: [],
|
|
1379
|
+
framework: "nuxt"
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
return {
|
|
1383
|
+
language: "javascript",
|
|
1384
|
+
serviceDir,
|
|
1385
|
+
dependencyEdits,
|
|
1386
|
+
entrypointEdits: [],
|
|
1387
|
+
envEdits: [OTEL_ENV],
|
|
1388
|
+
generatedFiles,
|
|
1389
|
+
framework: "nuxt"
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
var ASTRO_MIDDLEWARE_CANDIDATES = ["src/middleware.ts", "src/middleware.js"];
|
|
1393
|
+
async function findAstroMiddleware(serviceDir) {
|
|
1394
|
+
for (const rel of ASTRO_MIDDLEWARE_CANDIDATES) {
|
|
1395
|
+
const candidate = path4.join(serviceDir, rel);
|
|
1396
|
+
if (await exists(candidate)) return candidate;
|
|
1397
|
+
}
|
|
1398
|
+
return null;
|
|
1399
|
+
}
|
|
1400
|
+
async function planAstro(serviceDir, pkg, manifestPath) {
|
|
1401
|
+
const useTs = await isTypeScriptProject(serviceDir);
|
|
1402
|
+
const otelInitFile = path4.join(
|
|
1403
|
+
serviceDir,
|
|
1404
|
+
useTs ? "src/otel-init.ts" : "src/otel-init.js"
|
|
1405
|
+
);
|
|
1406
|
+
const existingMiddleware = await findAstroMiddleware(serviceDir);
|
|
1407
|
+
const middlewareFile = existingMiddleware ?? path4.join(serviceDir, useTs ? "src/middleware.ts" : "src/middleware.js");
|
|
1408
|
+
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
1409
|
+
const generatedFiles = [];
|
|
1410
|
+
const entrypointEdits = [];
|
|
1411
|
+
if (!await exists(otelInitFile)) {
|
|
1412
|
+
generatedFiles.push({
|
|
1413
|
+
file: otelInitFile,
|
|
1414
|
+
contents: useTs ? ASTRO_OTEL_INIT_TS : ASTRO_OTEL_INIT_JS,
|
|
1415
|
+
skipIfExists: true
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
await queueEnvNeat(serviceDir, pkg, generatedFiles);
|
|
1419
|
+
if (existingMiddleware === null) {
|
|
1420
|
+
generatedFiles.push({
|
|
1421
|
+
file: middlewareFile,
|
|
1422
|
+
contents: useTs ? ASTRO_MIDDLEWARE_TS : ASTRO_MIDDLEWARE_JS,
|
|
1423
|
+
skipIfExists: true
|
|
1424
|
+
});
|
|
1425
|
+
} else {
|
|
1426
|
+
try {
|
|
1427
|
+
const raw = await fs4.readFile(existingMiddleware, "utf8");
|
|
1428
|
+
if (!fileImportsOtelHook(raw, ["./otel-init"])) {
|
|
1429
|
+
const lines = raw.split(/\r?\n/);
|
|
1430
|
+
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
1431
|
+
entrypointEdits.push({
|
|
1432
|
+
file: existingMiddleware,
|
|
1433
|
+
before: firstReal,
|
|
1434
|
+
after: "import './otel-init'"
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
} catch {
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
const empty = dependencyEdits.length === 0 && generatedFiles.length === 0 && entrypointEdits.length === 0;
|
|
1441
|
+
if (empty) {
|
|
1442
|
+
return {
|
|
1443
|
+
language: "javascript",
|
|
1444
|
+
serviceDir,
|
|
1445
|
+
dependencyEdits: [],
|
|
1446
|
+
entrypointEdits: [],
|
|
1447
|
+
envEdits: [],
|
|
1448
|
+
generatedFiles: [],
|
|
1449
|
+
framework: "astro"
|
|
1450
|
+
};
|
|
570
1451
|
}
|
|
571
|
-
return
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
}
|
|
580
|
-
function otelInitFilename(flavor) {
|
|
581
|
-
if (flavor === "ts") return "otel-init.ts";
|
|
582
|
-
if (flavor === "esm") return "otel-init.mjs";
|
|
583
|
-
return "otel-init.cjs";
|
|
584
|
-
}
|
|
585
|
-
function otelInitContents(flavor) {
|
|
586
|
-
if (flavor === "ts") return OTEL_INIT_TS;
|
|
587
|
-
if (flavor === "esm") return OTEL_INIT_ESM;
|
|
588
|
-
return OTEL_INIT_CJS;
|
|
589
|
-
}
|
|
590
|
-
function injectionLine(flavor, entryFile, otelInitFile) {
|
|
591
|
-
let rel = path2.relative(path2.dirname(entryFile), otelInitFile);
|
|
592
|
-
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
593
|
-
rel = rel.split(path2.sep).join("/");
|
|
594
|
-
if (flavor === "cjs") return `require('${rel}')`;
|
|
595
|
-
if (flavor === "esm") return `import '${rel}'`;
|
|
596
|
-
const tsRel = rel.replace(/\.ts$/, "");
|
|
597
|
-
return `import '${tsRel}'`;
|
|
598
|
-
}
|
|
599
|
-
function lineIsOtelInjection(line) {
|
|
600
|
-
const trimmed = line.trim();
|
|
601
|
-
if (trimmed.length === 0) return false;
|
|
602
|
-
return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
|
|
1452
|
+
return {
|
|
1453
|
+
language: "javascript",
|
|
1454
|
+
serviceDir,
|
|
1455
|
+
dependencyEdits,
|
|
1456
|
+
entrypointEdits,
|
|
1457
|
+
envEdits: [OTEL_ENV],
|
|
1458
|
+
generatedFiles,
|
|
1459
|
+
framework: "astro"
|
|
1460
|
+
};
|
|
603
1461
|
}
|
|
604
1462
|
async function plan(serviceDir) {
|
|
605
1463
|
const pkg = await readPackageJson(serviceDir);
|
|
606
|
-
const manifestPath =
|
|
1464
|
+
const manifestPath = path4.join(serviceDir, "package.json");
|
|
607
1465
|
const empty = {
|
|
608
1466
|
language: "javascript",
|
|
609
1467
|
serviceDir,
|
|
@@ -613,13 +1471,44 @@ async function plan(serviceDir) {
|
|
|
613
1471
|
generatedFiles: []
|
|
614
1472
|
};
|
|
615
1473
|
if (!pkg) return empty;
|
|
1474
|
+
if (hasNextDependency(pkg)) {
|
|
1475
|
+
const nextConfig = await findNextConfig(serviceDir);
|
|
1476
|
+
if (nextConfig) {
|
|
1477
|
+
return planNext(serviceDir, pkg, manifestPath, nextConfig);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
if (hasRemixDependency(pkg)) {
|
|
1481
|
+
const remixEntry = await findRemixEntry(serviceDir);
|
|
1482
|
+
if (remixEntry) {
|
|
1483
|
+
return planRemix(serviceDir, pkg, manifestPath, remixEntry);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
if (hasSvelteKitDependency(pkg)) {
|
|
1487
|
+
const hooks = await findSvelteKitHooks(serviceDir);
|
|
1488
|
+
const config = await findSvelteKitConfig(serviceDir);
|
|
1489
|
+
if (hooks || config) {
|
|
1490
|
+
return planSvelteKit(serviceDir, pkg, manifestPath, hooks);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (hasNuxtDependency(pkg)) {
|
|
1494
|
+
const nuxtConfig = await findNuxtConfig(serviceDir);
|
|
1495
|
+
if (nuxtConfig) {
|
|
1496
|
+
return planNuxt(serviceDir, pkg, manifestPath);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
if (hasAstroDependency(pkg)) {
|
|
1500
|
+
const astroConfig = await findAstroConfig(serviceDir);
|
|
1501
|
+
if (astroConfig) {
|
|
1502
|
+
return planAstro(serviceDir, pkg, manifestPath);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
616
1505
|
const entryFile = await resolveEntry(serviceDir, pkg);
|
|
617
1506
|
if (!entryFile) {
|
|
618
1507
|
return { ...empty, libOnly: true };
|
|
619
1508
|
}
|
|
620
1509
|
const flavor = dispatchEntry(entryFile, pkg);
|
|
621
|
-
const otelInitFile =
|
|
622
|
-
const envNeatFile =
|
|
1510
|
+
const otelInitFile = path4.join(path4.dirname(entryFile), otelInitFilename(flavor));
|
|
1511
|
+
const envNeatFile = path4.join(serviceDir, ".env.neat");
|
|
623
1512
|
const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
624
1513
|
const dependencyEdits = [];
|
|
625
1514
|
for (const sdk of SDK_PACKAGES) {
|
|
@@ -633,7 +1522,7 @@ async function plan(serviceDir) {
|
|
|
633
1522
|
}
|
|
634
1523
|
const entrypointEdits = [];
|
|
635
1524
|
try {
|
|
636
|
-
const raw = await
|
|
1525
|
+
const raw = await fs4.readFile(entryFile, "utf8");
|
|
637
1526
|
const lines = raw.split(/\r?\n/);
|
|
638
1527
|
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
639
1528
|
if (!lineIsOtelInjection(firstReal)) {
|
|
@@ -658,7 +1547,7 @@ async function plan(serviceDir) {
|
|
|
658
1547
|
if (!await exists(envNeatFile)) {
|
|
659
1548
|
generatedFiles.push({
|
|
660
1549
|
file: envNeatFile,
|
|
661
|
-
contents: renderEnvNeat(pkg.name ??
|
|
1550
|
+
contents: renderEnvNeat(pkg.name ?? path4.basename(serviceDir)),
|
|
662
1551
|
skipIfExists: true
|
|
663
1552
|
});
|
|
664
1553
|
}
|
|
@@ -677,18 +1566,29 @@ async function plan(serviceDir) {
|
|
|
677
1566
|
};
|
|
678
1567
|
}
|
|
679
1568
|
function isAllowedWritePath(serviceDir, target) {
|
|
680
|
-
const rel =
|
|
1569
|
+
const rel = path4.relative(serviceDir, target);
|
|
681
1570
|
if (rel.startsWith("..")) return false;
|
|
682
|
-
const base =
|
|
1571
|
+
const base = path4.basename(target);
|
|
683
1572
|
if (base === "package.json") return true;
|
|
684
1573
|
if (base === ".env.neat") return true;
|
|
685
1574
|
if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
|
|
1575
|
+
if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
|
|
1576
|
+
if (/^next\.config\.(?:js|mjs|ts)$/.test(base)) return true;
|
|
1577
|
+
const relPosix = rel.split(path4.sep).join("/");
|
|
1578
|
+
if (relPosix === "app/otel.server.ts" || relPosix === "app/otel.server.js") return true;
|
|
1579
|
+
if (/^app\/entry\.server\.(?:tsx?|jsx?)$/.test(relPosix)) return true;
|
|
1580
|
+
if (relPosix === "src/otel-init.ts" || relPosix === "src/otel-init.js") return true;
|
|
1581
|
+
if (relPosix === "src/hooks.server.ts" || relPosix === "src/hooks.server.js") return true;
|
|
1582
|
+
if (relPosix === "server/plugins/otel.ts" || relPosix === "server/plugins/otel.js") return true;
|
|
1583
|
+
if (relPosix === "server/plugins/otel-init.ts" || relPosix === "server/plugins/otel-init.js") return true;
|
|
1584
|
+
if (relPosix === "src/middleware.ts" || relPosix === "src/middleware.js") return true;
|
|
686
1585
|
return false;
|
|
687
1586
|
}
|
|
688
1587
|
async function writeAtomic(file, contents) {
|
|
1588
|
+
await fs4.mkdir(path4.dirname(file), { recursive: true });
|
|
689
1589
|
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
690
|
-
await
|
|
691
|
-
await
|
|
1590
|
+
await fs4.writeFile(tmp, contents, "utf8");
|
|
1591
|
+
await fs4.rename(tmp, file);
|
|
692
1592
|
}
|
|
693
1593
|
async function apply(installPlan) {
|
|
694
1594
|
const { serviceDir } = installPlan;
|
|
@@ -700,7 +1600,7 @@ async function apply(installPlan) {
|
|
|
700
1600
|
writtenFiles: []
|
|
701
1601
|
};
|
|
702
1602
|
}
|
|
703
|
-
if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0) {
|
|
1603
|
+
if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0 && installPlan.nextConfigEdit === void 0) {
|
|
704
1604
|
return {
|
|
705
1605
|
serviceDir,
|
|
706
1606
|
outcome: "already-instrumented",
|
|
@@ -711,6 +1611,7 @@ async function apply(installPlan) {
|
|
|
711
1611
|
for (const d of installPlan.dependencyEdits) allTargets.add(d.file);
|
|
712
1612
|
for (const e of installPlan.entrypointEdits) allTargets.add(e.file);
|
|
713
1613
|
for (const g of installPlan.generatedFiles ?? []) allTargets.add(g.file);
|
|
1614
|
+
if (installPlan.nextConfigEdit) allTargets.add(installPlan.nextConfigEdit.file);
|
|
714
1615
|
for (const target of allTargets) {
|
|
715
1616
|
const isEntryEdit = installPlan.entrypointEdits.some((e) => e.file === target);
|
|
716
1617
|
if (isEntryEdit) continue;
|
|
@@ -725,7 +1626,7 @@ async function apply(installPlan) {
|
|
|
725
1626
|
for (const target of allTargets) {
|
|
726
1627
|
if (await exists(target)) {
|
|
727
1628
|
try {
|
|
728
|
-
originals.set(target, await
|
|
1629
|
+
originals.set(target, await fs4.readFile(target, "utf8"));
|
|
729
1630
|
} catch {
|
|
730
1631
|
}
|
|
731
1632
|
}
|
|
@@ -780,6 +1681,17 @@ async function apply(installPlan) {
|
|
|
780
1681
|
await writeAtomic(ep.file, newRaw);
|
|
781
1682
|
writtenFiles.push(ep.file);
|
|
782
1683
|
}
|
|
1684
|
+
if (installPlan.nextConfigEdit) {
|
|
1685
|
+
const target = installPlan.nextConfigEdit.file;
|
|
1686
|
+
const raw = originals.get(target);
|
|
1687
|
+
if (raw !== void 0 && !raw.includes("instrumentationHook")) {
|
|
1688
|
+
const updated = injectInstrumentationHook(raw);
|
|
1689
|
+
if (updated !== null) {
|
|
1690
|
+
await writeAtomic(target, updated);
|
|
1691
|
+
writtenFiles.push(target);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
783
1695
|
} catch (err) {
|
|
784
1696
|
await rollback(installPlan, originals, createdFiles);
|
|
785
1697
|
throw err;
|
|
@@ -795,14 +1707,14 @@ async function rollback(installPlan, originals, createdFiles) {
|
|
|
795
1707
|
const removed = [];
|
|
796
1708
|
for (const [file, raw] of originals.entries()) {
|
|
797
1709
|
try {
|
|
798
|
-
await
|
|
1710
|
+
await fs4.writeFile(file, raw, "utf8");
|
|
799
1711
|
restored.push(file);
|
|
800
1712
|
} catch {
|
|
801
1713
|
}
|
|
802
1714
|
}
|
|
803
1715
|
for (const file of createdFiles) {
|
|
804
1716
|
try {
|
|
805
|
-
await
|
|
1717
|
+
await fs4.unlink(file);
|
|
806
1718
|
removed.push(file);
|
|
807
1719
|
} catch {
|
|
808
1720
|
}
|
|
@@ -817,8 +1729,26 @@ async function rollback(installPlan, originals, createdFiles) {
|
|
|
817
1729
|
...removed.map((f) => `removed: ${f}`),
|
|
818
1730
|
""
|
|
819
1731
|
];
|
|
820
|
-
const rollbackPath =
|
|
821
|
-
await
|
|
1732
|
+
const rollbackPath = path4.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
1733
|
+
await fs4.writeFile(rollbackPath, lines.join("\n"), "utf8");
|
|
1734
|
+
}
|
|
1735
|
+
function injectInstrumentationHook(raw) {
|
|
1736
|
+
if (raw.includes("instrumentationHook")) return raw;
|
|
1737
|
+
const anchors = [
|
|
1738
|
+
{ pattern: /(module\.exports\s*=\s*\{)/, label: "cjs-default" },
|
|
1739
|
+
{ pattern: /(export\s+default\s*\{)/, label: "esm-default" },
|
|
1740
|
+
{ pattern: /(?:const|let|var)\s+\w+(?:\s*:\s*[^=]+)?\s*=\s*(\{)/, label: "named-config" }
|
|
1741
|
+
];
|
|
1742
|
+
for (const { pattern } of anchors) {
|
|
1743
|
+
const match = pattern.exec(raw);
|
|
1744
|
+
if (!match) continue;
|
|
1745
|
+
const insertAfter = match.index + match[0].length;
|
|
1746
|
+
const before = raw.slice(0, insertAfter);
|
|
1747
|
+
const after = raw.slice(insertAfter);
|
|
1748
|
+
const injection = "\n experimental: { instrumentationHook: true },";
|
|
1749
|
+
return `${before}${injection}${after}`;
|
|
1750
|
+
}
|
|
1751
|
+
return null;
|
|
822
1752
|
}
|
|
823
1753
|
var javascriptInstaller = {
|
|
824
1754
|
name: "javascript",
|
|
@@ -828,8 +1758,8 @@ var javascriptInstaller = {
|
|
|
828
1758
|
};
|
|
829
1759
|
|
|
830
1760
|
// src/installers/python.ts
|
|
831
|
-
import { promises as
|
|
832
|
-
import
|
|
1761
|
+
import { promises as fs5 } from "fs";
|
|
1762
|
+
import path5 from "path";
|
|
833
1763
|
var SDK_PACKAGES2 = [
|
|
834
1764
|
{ name: "opentelemetry-distro", version: ">=0.49b0" },
|
|
835
1765
|
{ name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
|
|
@@ -841,7 +1771,7 @@ var OTEL_ENV2 = {
|
|
|
841
1771
|
};
|
|
842
1772
|
async function exists2(p) {
|
|
843
1773
|
try {
|
|
844
|
-
await
|
|
1774
|
+
await fs5.stat(p);
|
|
845
1775
|
return true;
|
|
846
1776
|
} catch {
|
|
847
1777
|
return false;
|
|
@@ -850,7 +1780,7 @@ async function exists2(p) {
|
|
|
850
1780
|
async function detect2(serviceDir) {
|
|
851
1781
|
const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
|
|
852
1782
|
for (const m of markers) {
|
|
853
|
-
if (await exists2(
|
|
1783
|
+
if (await exists2(path5.join(serviceDir, m))) return true;
|
|
854
1784
|
}
|
|
855
1785
|
return false;
|
|
856
1786
|
}
|
|
@@ -860,9 +1790,9 @@ function reqPackageName(line) {
|
|
|
860
1790
|
return head.replace(/[<>=!~].*$/, "").toLowerCase();
|
|
861
1791
|
}
|
|
862
1792
|
async function planRequirementsTxtEdits(serviceDir) {
|
|
863
|
-
const file =
|
|
1793
|
+
const file = path5.join(serviceDir, "requirements.txt");
|
|
864
1794
|
if (!await exists2(file)) return null;
|
|
865
|
-
const raw = await
|
|
1795
|
+
const raw = await fs5.readFile(file, "utf8");
|
|
866
1796
|
const presentNames = new Set(
|
|
867
1797
|
raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
|
|
868
1798
|
);
|
|
@@ -870,9 +1800,9 @@ async function planRequirementsTxtEdits(serviceDir) {
|
|
|
870
1800
|
return { manifest: file, missing: [...missing] };
|
|
871
1801
|
}
|
|
872
1802
|
async function planProcfileEdits(serviceDir) {
|
|
873
|
-
const procfile =
|
|
1803
|
+
const procfile = path5.join(serviceDir, "Procfile");
|
|
874
1804
|
if (!await exists2(procfile)) return [];
|
|
875
|
-
const raw = await
|
|
1805
|
+
const raw = await fs5.readFile(procfile, "utf8");
|
|
876
1806
|
const edits = [];
|
|
877
1807
|
for (const line of raw.split(/\r?\n/)) {
|
|
878
1808
|
if (line.length === 0) continue;
|
|
@@ -924,8 +1854,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
|
|
|
924
1854
|
const next = `${original}${trailing}${newlines.join("\n")}
|
|
925
1855
|
`;
|
|
926
1856
|
const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
|
|
927
|
-
await
|
|
928
|
-
await
|
|
1857
|
+
await fs5.writeFile(tmp, next, "utf8");
|
|
1858
|
+
await fs5.rename(tmp, manifest);
|
|
929
1859
|
}
|
|
930
1860
|
async function applyProcfile(procfile, edits, original) {
|
|
931
1861
|
let next = original;
|
|
@@ -934,8 +1864,8 @@ async function applyProcfile(procfile, edits, original) {
|
|
|
934
1864
|
next = next.replace(e.before, e.after);
|
|
935
1865
|
}
|
|
936
1866
|
const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
|
|
937
|
-
await
|
|
938
|
-
await
|
|
1867
|
+
await fs5.writeFile(tmp, next, "utf8");
|
|
1868
|
+
await fs5.rename(tmp, procfile);
|
|
939
1869
|
}
|
|
940
1870
|
async function apply2(installPlan) {
|
|
941
1871
|
const { serviceDir } = installPlan;
|
|
@@ -948,7 +1878,7 @@ async function apply2(installPlan) {
|
|
|
948
1878
|
const originals = /* @__PURE__ */ new Map();
|
|
949
1879
|
for (const file of touched) {
|
|
950
1880
|
try {
|
|
951
|
-
originals.set(file, await
|
|
1881
|
+
originals.set(file, await fs5.readFile(file, "utf8"));
|
|
952
1882
|
} catch {
|
|
953
1883
|
}
|
|
954
1884
|
}
|
|
@@ -959,7 +1889,7 @@ async function apply2(installPlan) {
|
|
|
959
1889
|
if (raw === void 0) {
|
|
960
1890
|
throw new Error(`python installer: cannot read ${file} during apply`);
|
|
961
1891
|
}
|
|
962
|
-
const base =
|
|
1892
|
+
const base = path5.basename(file);
|
|
963
1893
|
if (base === "requirements.txt") {
|
|
964
1894
|
const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
|
|
965
1895
|
if (edits.length > 0) {
|
|
@@ -984,7 +1914,7 @@ async function rollback2(installPlan, originals) {
|
|
|
984
1914
|
const restored = [];
|
|
985
1915
|
for (const [file, raw] of originals.entries()) {
|
|
986
1916
|
try {
|
|
987
|
-
await
|
|
1917
|
+
await fs5.writeFile(file, raw, "utf8");
|
|
988
1918
|
restored.push(file);
|
|
989
1919
|
} catch {
|
|
990
1920
|
}
|
|
@@ -998,8 +1928,8 @@ async function rollback2(installPlan, originals) {
|
|
|
998
1928
|
...restored.map((f) => `restored: ${f}`),
|
|
999
1929
|
""
|
|
1000
1930
|
];
|
|
1001
|
-
const rollbackPath =
|
|
1002
|
-
await
|
|
1931
|
+
const rollbackPath = path5.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
1932
|
+
await fs5.writeFile(rollbackPath, lines.join("\n"), "utf8");
|
|
1003
1933
|
}
|
|
1004
1934
|
var pythonInstaller = {
|
|
1005
1935
|
name: "python",
|
|
@@ -1010,7 +1940,7 @@ var pythonInstaller = {
|
|
|
1010
1940
|
|
|
1011
1941
|
// src/installers/shared.ts
|
|
1012
1942
|
function isEmptyPlan(plan3) {
|
|
1013
|
-
return plan3.dependencyEdits.length === 0 && plan3.entrypointEdits.length === 0 && plan3.envEdits.length === 0 && (plan3.generatedFiles?.length ?? 0) === 0;
|
|
1943
|
+
return plan3.dependencyEdits.length === 0 && plan3.entrypointEdits.length === 0 && plan3.envEdits.length === 0 && (plan3.generatedFiles?.length ?? 0) === 0 && plan3.nextConfigEdit === void 0;
|
|
1014
1944
|
}
|
|
1015
1945
|
|
|
1016
1946
|
// src/installers/index.ts
|
|
@@ -1109,15 +2039,256 @@ function renderPatch(sections) {
|
|
|
1109
2039
|
}
|
|
1110
2040
|
lines.push("");
|
|
1111
2041
|
}
|
|
2042
|
+
if (plan3.nextConfigEdit) {
|
|
2043
|
+
lines.push("### next.config (framework flag)");
|
|
2044
|
+
lines.push(`--- ${plan3.nextConfigEdit.file}`);
|
|
2045
|
+
lines.push(`+ experimental: { instrumentationHook: true }, // ${plan3.nextConfigEdit.reason}`);
|
|
2046
|
+
lines.push("");
|
|
2047
|
+
}
|
|
1112
2048
|
}
|
|
1113
2049
|
return lines.join("\n");
|
|
1114
2050
|
}
|
|
1115
2051
|
|
|
1116
|
-
// src/
|
|
1117
|
-
import {
|
|
2052
|
+
// src/orchestrator.ts
|
|
2053
|
+
import { promises as fs6 } from "fs";
|
|
2054
|
+
import http from "http";
|
|
2055
|
+
import path6 from "path";
|
|
2056
|
+
import { spawn as spawn2 } from "child_process";
|
|
2057
|
+
import readline from "readline";
|
|
2058
|
+
async function extractAndPersist(opts) {
|
|
2059
|
+
const services = await discoverServices(opts.scanPath);
|
|
2060
|
+
const languages = [...new Set(services.map((s) => s.node.language))].sort();
|
|
2061
|
+
const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
|
|
2062
|
+
resetGraph(graphKey);
|
|
2063
|
+
const graph = getGraph(graphKey);
|
|
2064
|
+
const projectPaths = pathsForProject(graphKey, path6.join(opts.scanPath, "neat-out"));
|
|
2065
|
+
const extraction = await extractFromDirectory(graph, opts.scanPath, {
|
|
2066
|
+
errorsPath: projectPaths.errorsPath
|
|
2067
|
+
});
|
|
2068
|
+
if (!opts.dryRun) {
|
|
2069
|
+
await saveGraphToDisk(graph, projectPaths.snapshotPath);
|
|
2070
|
+
}
|
|
2071
|
+
return {
|
|
2072
|
+
graph,
|
|
2073
|
+
graphKey,
|
|
2074
|
+
services,
|
|
2075
|
+
languages,
|
|
2076
|
+
nodesAdded: extraction.nodesAdded,
|
|
2077
|
+
edgesAdded: extraction.edgesAdded,
|
|
2078
|
+
snapshotPath: projectPaths.snapshotPath,
|
|
2079
|
+
errorsPath: projectPaths.errorsPath
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
async function applyInstallersOver(services) {
|
|
2083
|
+
let instrumented = 0;
|
|
2084
|
+
let already = 0;
|
|
2085
|
+
let libOnly = 0;
|
|
2086
|
+
for (const svc of services) {
|
|
2087
|
+
const installer = await pickInstaller(svc.dir);
|
|
2088
|
+
if (!installer) continue;
|
|
2089
|
+
const plan3 = await installer.plan(svc.dir);
|
|
2090
|
+
if (isEmptyPlan(plan3) && !plan3.libOnly) {
|
|
2091
|
+
already++;
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
const outcome = await installer.apply(plan3);
|
|
2095
|
+
if (outcome.outcome === "instrumented") instrumented++;
|
|
2096
|
+
else if (outcome.outcome === "already-instrumented") already++;
|
|
2097
|
+
else if (outcome.outcome === "lib-only") libOnly++;
|
|
2098
|
+
}
|
|
2099
|
+
return { instrumented, alreadyInstrumented: already, libOnly };
|
|
2100
|
+
}
|
|
2101
|
+
async function promptYesNo(question) {
|
|
2102
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2103
|
+
return new Promise((resolve) => {
|
|
2104
|
+
rl.question(`${question} [Y/n] `, (answer) => {
|
|
2105
|
+
rl.close();
|
|
2106
|
+
const trimmed = answer.trim().toLowerCase();
|
|
2107
|
+
resolve(trimmed === "" || trimmed === "y" || trimmed === "yes");
|
|
2108
|
+
});
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
var DEFAULT_DAEMON_READY_TIMEOUT_MS = 15e3;
|
|
2112
|
+
async function checkDaemonHealth(restPort) {
|
|
2113
|
+
return new Promise((resolve) => {
|
|
2114
|
+
const req = http.get(`http://127.0.0.1:${restPort}/health`, (res) => {
|
|
2115
|
+
const ok = res.statusCode !== void 0 && res.statusCode >= 200 && res.statusCode < 300;
|
|
2116
|
+
res.resume();
|
|
2117
|
+
resolve(ok);
|
|
2118
|
+
});
|
|
2119
|
+
req.on("error", () => resolve(false));
|
|
2120
|
+
req.setTimeout(1e3, () => {
|
|
2121
|
+
req.destroy();
|
|
2122
|
+
resolve(false);
|
|
2123
|
+
});
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
async function waitForDaemonReady(restPort, timeoutMs) {
|
|
2127
|
+
const deadline = Date.now() + timeoutMs;
|
|
2128
|
+
while (Date.now() < deadline) {
|
|
2129
|
+
if (await checkDaemonHealth(restPort)) return true;
|
|
2130
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
2131
|
+
}
|
|
2132
|
+
return false;
|
|
2133
|
+
}
|
|
2134
|
+
function spawnDaemonDetached() {
|
|
2135
|
+
const here = path6.dirname(new URL(import.meta.url).pathname);
|
|
2136
|
+
const candidates = [
|
|
2137
|
+
path6.join(here, "neatd.cjs"),
|
|
2138
|
+
path6.join(here, "neatd.js")
|
|
2139
|
+
];
|
|
2140
|
+
let entry2 = null;
|
|
2141
|
+
const fsSync = __require("fs");
|
|
2142
|
+
for (const c of candidates) {
|
|
2143
|
+
try {
|
|
2144
|
+
fsSync.accessSync(c);
|
|
2145
|
+
entry2 = c;
|
|
2146
|
+
break;
|
|
2147
|
+
} catch {
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
if (!entry2) {
|
|
2151
|
+
throw new Error(`orchestrator: cannot locate neatd entry in ${here}`);
|
|
2152
|
+
}
|
|
2153
|
+
const child = spawn2(process.execPath, [entry2, "start"], {
|
|
2154
|
+
detached: true,
|
|
2155
|
+
stdio: "ignore",
|
|
2156
|
+
env: process.env
|
|
2157
|
+
});
|
|
2158
|
+
child.unref();
|
|
2159
|
+
}
|
|
2160
|
+
function openBrowser(url) {
|
|
2161
|
+
if (!process.stdout.isTTY) return "failed";
|
|
2162
|
+
const platform = process.platform;
|
|
2163
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
2164
|
+
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
2165
|
+
try {
|
|
2166
|
+
const child = spawn2(cmd, args, { detached: true, stdio: "ignore" });
|
|
2167
|
+
child.on("error", () => {
|
|
2168
|
+
});
|
|
2169
|
+
child.unref();
|
|
2170
|
+
return "opened";
|
|
2171
|
+
} catch {
|
|
2172
|
+
return "failed";
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
async function runOrchestrator(opts) {
|
|
2176
|
+
const result = {
|
|
2177
|
+
exitCode: 0,
|
|
2178
|
+
steps: {
|
|
2179
|
+
discovery: { services: 0, languages: [] },
|
|
2180
|
+
extraction: { nodesAdded: 0, edgesAdded: 0 },
|
|
2181
|
+
gitignore: "unchanged",
|
|
2182
|
+
apply: { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, skipped: false },
|
|
2183
|
+
daemon: "skipped",
|
|
2184
|
+
browser: "skipped"
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
const stat = await fs6.stat(opts.scanPath).catch(() => null);
|
|
2188
|
+
if (!stat || !stat.isDirectory()) {
|
|
2189
|
+
console.error(`neat: ${opts.scanPath} is not a directory`);
|
|
2190
|
+
result.exitCode = 2;
|
|
2191
|
+
return result;
|
|
2192
|
+
}
|
|
2193
|
+
console.log(`neat: ${opts.scanPath}`);
|
|
2194
|
+
console.log("");
|
|
2195
|
+
const persisted = await extractAndPersist({
|
|
2196
|
+
scanPath: opts.scanPath,
|
|
2197
|
+
project: opts.project,
|
|
2198
|
+
projectExplicit: opts.projectExplicit
|
|
2199
|
+
});
|
|
2200
|
+
const { graph, services, languages } = persisted;
|
|
2201
|
+
result.steps.discovery = { services: services.length, languages };
|
|
2202
|
+
console.log(`discovered ${services.length} service(s) across ${languages.length} language(s)`);
|
|
2203
|
+
let runApply = !opts.noInstrument;
|
|
2204
|
+
if (runApply && !opts.yes && process.stdout.isTTY && process.stdin.isTTY) {
|
|
2205
|
+
runApply = await promptYesNo("instrument your services and open the dashboard?");
|
|
2206
|
+
}
|
|
2207
|
+
result.steps.extraction = {
|
|
2208
|
+
nodesAdded: persisted.nodesAdded,
|
|
2209
|
+
edgesAdded: persisted.edgesAdded
|
|
2210
|
+
};
|
|
2211
|
+
const gi = await ensureNeatOutIgnored(opts.scanPath);
|
|
2212
|
+
result.steps.gitignore = gi.action;
|
|
2213
|
+
if (gi.action !== "unchanged") {
|
|
2214
|
+
console.log(`${gi.action} .gitignore (neat-out/)`);
|
|
2215
|
+
}
|
|
2216
|
+
try {
|
|
2217
|
+
await addProject({
|
|
2218
|
+
name: opts.project,
|
|
2219
|
+
path: opts.scanPath,
|
|
2220
|
+
languages,
|
|
2221
|
+
status: "active"
|
|
2222
|
+
});
|
|
2223
|
+
} catch (err) {
|
|
2224
|
+
if (!(err instanceof ProjectNameCollisionError)) throw err;
|
|
2225
|
+
console.error(`neat: ${err.message}`);
|
|
2226
|
+
console.error("pass --project <other-name> to register under a different name.");
|
|
2227
|
+
result.exitCode = 1;
|
|
2228
|
+
return result;
|
|
2229
|
+
}
|
|
2230
|
+
if (!runApply) {
|
|
2231
|
+
result.steps.apply.skipped = true;
|
|
2232
|
+
console.log("skipped instrumentation (--no-instrument)");
|
|
2233
|
+
} else {
|
|
2234
|
+
const tally = await applyInstallersOver(services);
|
|
2235
|
+
result.steps.apply = { ...tally, skipped: false };
|
|
2236
|
+
console.log(
|
|
2237
|
+
`instrumented ${tally.instrumented}, already ${tally.alreadyInstrumented}, lib-only ${tally.libOnly}`
|
|
2238
|
+
);
|
|
2239
|
+
}
|
|
2240
|
+
const restPort = Number(process.env.PORT ?? 8080);
|
|
2241
|
+
const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
|
|
2242
|
+
if (await checkDaemonHealth(restPort)) {
|
|
2243
|
+
result.steps.daemon = "already-running";
|
|
2244
|
+
} else {
|
|
2245
|
+
try {
|
|
2246
|
+
spawnDaemonDetached();
|
|
2247
|
+
} catch (err) {
|
|
2248
|
+
console.error(`neat: daemon spawn failed \u2014 ${err.message}`);
|
|
2249
|
+
result.exitCode = 1;
|
|
2250
|
+
return result;
|
|
2251
|
+
}
|
|
2252
|
+
const ready = await waitForDaemonReady(restPort, timeoutMs);
|
|
2253
|
+
result.steps.daemon = ready ? "spawned" : "timed-out";
|
|
2254
|
+
if (!ready) {
|
|
2255
|
+
console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
|
|
2256
|
+
result.exitCode = 1;
|
|
2257
|
+
return result;
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
const dashboardUrl = opts.dashboardUrl ?? "http://localhost:6328";
|
|
2261
|
+
if (opts.noOpen || !process.stdout.isTTY) {
|
|
2262
|
+
result.steps.browser = "skipped";
|
|
2263
|
+
} else {
|
|
2264
|
+
result.steps.browser = openBrowser(dashboardUrl);
|
|
2265
|
+
}
|
|
2266
|
+
printSummary(result, graph, dashboardUrl);
|
|
2267
|
+
return result;
|
|
2268
|
+
}
|
|
2269
|
+
function printSummary(result, graph, dashboardUrl) {
|
|
2270
|
+
const nodes = [];
|
|
2271
|
+
graph.forEachNode((_id, attrs) => nodes.push(attrs));
|
|
2272
|
+
const edges = [];
|
|
2273
|
+
graph.forEachEdge((_id, attrs) => edges.push(attrs));
|
|
2274
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
2275
|
+
for (const n of nodes) byNode.set(n.type, (byNode.get(n.type) ?? 0) + 1);
|
|
2276
|
+
const byEdge = /* @__PURE__ */ new Map();
|
|
2277
|
+
for (const e of edges) byEdge.set(e.type, (byEdge.get(e.type) ?? 0) + 1);
|
|
2278
|
+
console.log("");
|
|
2279
|
+
console.log("=== summary ===");
|
|
2280
|
+
console.log(`graph: ${graph.order} nodes, ${graph.size} edges`);
|
|
2281
|
+
for (const [t, c] of [...byNode.entries()].sort()) console.log(` ${t}: ${c}`);
|
|
2282
|
+
for (const [t, c] of [...byEdge.entries()].sort()) console.log(` ${t}: ${c}`);
|
|
2283
|
+
console.log("");
|
|
2284
|
+
console.log(`dashboard: ${dashboardUrl}`);
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
// src/cli-verbs.ts
|
|
2288
|
+
import path7 from "path";
|
|
1118
2289
|
|
|
1119
2290
|
// src/cli-client.ts
|
|
1120
|
-
import { Provenance } from "@neat.is/types";
|
|
2291
|
+
import { Provenance as Provenance2 } from "@neat.is/types";
|
|
1121
2292
|
var HttpError = class extends Error {
|
|
1122
2293
|
constructor(status, message, responseBody = "") {
|
|
1123
2294
|
super(message);
|
|
@@ -1134,13 +2305,16 @@ var TransportError = class extends Error {
|
|
|
1134
2305
|
this.name = "TransportError";
|
|
1135
2306
|
}
|
|
1136
2307
|
};
|
|
1137
|
-
function createHttpClient(baseUrl) {
|
|
2308
|
+
function createHttpClient(baseUrl, bearerToken) {
|
|
1138
2309
|
const root = baseUrl.replace(/\/$/, "");
|
|
2310
|
+
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
1139
2311
|
return {
|
|
1140
|
-
async get(
|
|
2312
|
+
async get(path9) {
|
|
1141
2313
|
let res;
|
|
1142
2314
|
try {
|
|
1143
|
-
res = await fetch(`${root}${
|
|
2315
|
+
res = await fetch(`${root}${path9}`, {
|
|
2316
|
+
headers: { ...authHeader }
|
|
2317
|
+
});
|
|
1144
2318
|
} catch (err) {
|
|
1145
2319
|
throw new TransportError(
|
|
1146
2320
|
`cannot reach neat-core at ${root}: ${err.message}`
|
|
@@ -1150,18 +2324,18 @@ function createHttpClient(baseUrl) {
|
|
|
1150
2324
|
const body = await res.text().catch(() => "");
|
|
1151
2325
|
throw new HttpError(
|
|
1152
2326
|
res.status,
|
|
1153
|
-
`${res.status} ${res.statusText} on GET ${
|
|
2327
|
+
`${res.status} ${res.statusText} on GET ${path9}: ${body}`,
|
|
1154
2328
|
body
|
|
1155
2329
|
);
|
|
1156
2330
|
}
|
|
1157
2331
|
return await res.json();
|
|
1158
2332
|
},
|
|
1159
|
-
async post(
|
|
2333
|
+
async post(path9, body) {
|
|
1160
2334
|
let res;
|
|
1161
2335
|
try {
|
|
1162
|
-
res = await fetch(`${root}${
|
|
2336
|
+
res = await fetch(`${root}${path9}`, {
|
|
1163
2337
|
method: "POST",
|
|
1164
|
-
headers: { "content-type": "application/json" },
|
|
2338
|
+
headers: { "content-type": "application/json", ...authHeader },
|
|
1165
2339
|
body: JSON.stringify(body)
|
|
1166
2340
|
});
|
|
1167
2341
|
} catch (err) {
|
|
@@ -1173,7 +2347,7 @@ function createHttpClient(baseUrl) {
|
|
|
1173
2347
|
const text = await res.text().catch(() => "");
|
|
1174
2348
|
throw new HttpError(
|
|
1175
2349
|
res.status,
|
|
1176
|
-
`${res.status} ${res.statusText} on POST ${
|
|
2350
|
+
`${res.status} ${res.statusText} on POST ${path9}: ${text}`,
|
|
1177
2351
|
text
|
|
1178
2352
|
);
|
|
1179
2353
|
}
|
|
@@ -1187,12 +2361,12 @@ function projectPath(project, suffix) {
|
|
|
1187
2361
|
}
|
|
1188
2362
|
async function runRootCause(client, input) {
|
|
1189
2363
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
1190
|
-
const
|
|
2364
|
+
const path9 = projectPath(
|
|
1191
2365
|
input.project,
|
|
1192
2366
|
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
1193
2367
|
);
|
|
1194
2368
|
try {
|
|
1195
|
-
const result = await client.get(
|
|
2369
|
+
const result = await client.get(path9);
|
|
1196
2370
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
1197
2371
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
1198
2372
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -1218,12 +2392,12 @@ async function runRootCause(client, input) {
|
|
|
1218
2392
|
}
|
|
1219
2393
|
async function runBlastRadius(client, input) {
|
|
1220
2394
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
1221
|
-
const
|
|
2395
|
+
const path9 = projectPath(
|
|
1222
2396
|
input.project,
|
|
1223
2397
|
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
1224
2398
|
);
|
|
1225
2399
|
try {
|
|
1226
|
-
const result = await client.get(
|
|
2400
|
+
const result = await client.get(path9);
|
|
1227
2401
|
if (result.totalAffected === 0) {
|
|
1228
2402
|
return {
|
|
1229
2403
|
summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
|
|
@@ -1252,17 +2426,17 @@ async function runBlastRadius(client, input) {
|
|
|
1252
2426
|
}
|
|
1253
2427
|
}
|
|
1254
2428
|
function formatBlastEntry(n) {
|
|
1255
|
-
const tag = n.edgeProvenance ===
|
|
2429
|
+
const tag = n.edgeProvenance === Provenance2.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
1256
2430
|
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
1257
2431
|
}
|
|
1258
2432
|
async function runDependencies(client, input) {
|
|
1259
2433
|
const depth = input.depth ?? 3;
|
|
1260
|
-
const
|
|
2434
|
+
const path9 = projectPath(
|
|
1261
2435
|
input.project,
|
|
1262
2436
|
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
1263
2437
|
);
|
|
1264
2438
|
try {
|
|
1265
|
-
const result = await client.get(
|
|
2439
|
+
const result = await client.get(path9);
|
|
1266
2440
|
if (result.total === 0) {
|
|
1267
2441
|
return {
|
|
1268
2442
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -1298,9 +2472,9 @@ async function runObservedDependencies(client, input) {
|
|
|
1298
2472
|
const edges = await client.get(
|
|
1299
2473
|
projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
|
|
1300
2474
|
);
|
|
1301
|
-
const observed = edges.outbound.filter((e) => e.provenance ===
|
|
2475
|
+
const observed = edges.outbound.filter((e) => e.provenance === Provenance2.OBSERVED);
|
|
1302
2476
|
if (observed.length === 0) {
|
|
1303
|
-
const hasExtracted = edges.outbound.some((e) => e.provenance ===
|
|
2477
|
+
const hasExtracted = edges.outbound.some((e) => e.provenance === Provenance2.EXTRACTED);
|
|
1304
2478
|
const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
1305
2479
|
return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
|
|
1306
2480
|
}
|
|
@@ -1308,7 +2482,7 @@ async function runObservedDependencies(client, input) {
|
|
|
1308
2482
|
return {
|
|
1309
2483
|
summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
1310
2484
|
block: blockLines.join("\n"),
|
|
1311
|
-
provenance:
|
|
2485
|
+
provenance: Provenance2.OBSERVED
|
|
1312
2486
|
};
|
|
1313
2487
|
} catch (err) {
|
|
1314
2488
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -1343,9 +2517,9 @@ function formatDuration(ms) {
|
|
|
1343
2517
|
return `${Math.round(h / 24)}d`;
|
|
1344
2518
|
}
|
|
1345
2519
|
async function runIncidents(client, input) {
|
|
1346
|
-
const
|
|
2520
|
+
const path9 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
1347
2521
|
try {
|
|
1348
|
-
const body = await client.get(
|
|
2522
|
+
const body = await client.get(path9);
|
|
1349
2523
|
const events = body.events;
|
|
1350
2524
|
if (events.length === 0) {
|
|
1351
2525
|
return {
|
|
@@ -1362,7 +2536,7 @@ async function runIncidents(client, input) {
|
|
|
1362
2536
|
return {
|
|
1363
2537
|
summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
1364
2538
|
block: blockLines.join("\n"),
|
|
1365
|
-
provenance:
|
|
2539
|
+
provenance: Provenance2.OBSERVED
|
|
1366
2540
|
};
|
|
1367
2541
|
} catch (err) {
|
|
1368
2542
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -1471,7 +2645,7 @@ async function runStaleEdges(client, input) {
|
|
|
1471
2645
|
return {
|
|
1472
2646
|
summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
|
|
1473
2647
|
block: blockLines.join("\n"),
|
|
1474
|
-
provenance:
|
|
2648
|
+
provenance: Provenance2.STALE
|
|
1475
2649
|
};
|
|
1476
2650
|
}
|
|
1477
2651
|
async function runPolicies(client, input) {
|
|
@@ -1611,8 +2785,181 @@ function exitCodeForError(err) {
|
|
|
1611
2785
|
if (err instanceof HttpError) return 1;
|
|
1612
2786
|
return 1;
|
|
1613
2787
|
}
|
|
2788
|
+
function createSnapshotPushClient(baseUrl, token) {
|
|
2789
|
+
return createHttpClient(baseUrl, token && token.length > 0 ? token : void 0);
|
|
2790
|
+
}
|
|
2791
|
+
async function pushSnapshotToRemote(input) {
|
|
2792
|
+
const client = createSnapshotPushClient(input.baseUrl, input.token);
|
|
2793
|
+
if (typeof client.post !== "function") {
|
|
2794
|
+
throw new Error("HttpClient does not support POST \u2014 required for snapshot push");
|
|
2795
|
+
}
|
|
2796
|
+
return client.post(
|
|
2797
|
+
`/projects/${encodeURIComponent(input.project)}/snapshot`,
|
|
2798
|
+
{ snapshot: input.snapshot }
|
|
2799
|
+
);
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
// src/cli-verbs.ts
|
|
2803
|
+
async function resolveProjectEntry(opts) {
|
|
2804
|
+
const entries = await listProjects();
|
|
2805
|
+
if (opts.project) {
|
|
2806
|
+
const match = entries.find((e) => e.name === opts.project);
|
|
2807
|
+
return match ?? null;
|
|
2808
|
+
}
|
|
2809
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
2810
|
+
const resolvedCwd = await normalizeProjectPath(cwd);
|
|
2811
|
+
for (const entry2 of entries) {
|
|
2812
|
+
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path7.sep}`)) {
|
|
2813
|
+
return entry2;
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
return null;
|
|
2817
|
+
}
|
|
2818
|
+
async function checkDaemonHealth2(baseUrl) {
|
|
2819
|
+
try {
|
|
2820
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/health`, {
|
|
2821
|
+
signal: AbortSignal.timeout(1500)
|
|
2822
|
+
});
|
|
2823
|
+
return res.ok;
|
|
2824
|
+
} catch {
|
|
2825
|
+
return false;
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
function snapshotForGraph(persisted) {
|
|
2829
|
+
return {
|
|
2830
|
+
schemaVersion: 3,
|
|
2831
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2832
|
+
graph: persisted.graph.export()
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2835
|
+
function emitResult(result, json) {
|
|
2836
|
+
if (json) {
|
|
2837
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
2838
|
+
return;
|
|
2839
|
+
}
|
|
2840
|
+
const verb = result.mode === "dry-run" ? "dry-run" : result.mode === "remote" ? "pushed" : "synced";
|
|
2841
|
+
console.log(
|
|
2842
|
+
`${verb}: ${result.project} \u2014 ${result.nodesAdded} node(s) and ${result.edgesAdded} edge(s) added` + (result.snapshotPath ? `; snapshot at ${result.snapshotPath}` : "")
|
|
2843
|
+
);
|
|
2844
|
+
if (!result.apply.skipped) {
|
|
2845
|
+
const { instrumented, alreadyInstrumented, libOnly } = result.apply;
|
|
2846
|
+
console.log(
|
|
2847
|
+
`instrumented ${instrumented}, already ${alreadyInstrumented}, lib-only ${libOnly}`
|
|
2848
|
+
);
|
|
2849
|
+
} else {
|
|
2850
|
+
console.log("skipped instrumentation (--no-instrument)");
|
|
2851
|
+
}
|
|
2852
|
+
for (const warn of result.warnings) console.error(warn);
|
|
2853
|
+
}
|
|
2854
|
+
async function runSync(opts) {
|
|
2855
|
+
const entry2 = await resolveProjectEntry(opts);
|
|
2856
|
+
if (!entry2) {
|
|
2857
|
+
const target = opts.project ?? opts.cwd ?? process.cwd();
|
|
2858
|
+
console.error(
|
|
2859
|
+
`neat sync: no registered project ${opts.project ? `named "${opts.project}"` : `covers ${target}`}. Run \`neat <path>\` or \`neat init <path>\` first.`
|
|
2860
|
+
);
|
|
2861
|
+
return {
|
|
2862
|
+
exitCode: 1,
|
|
2863
|
+
project: opts.project ?? "",
|
|
2864
|
+
scanPath: target,
|
|
2865
|
+
nodesAdded: 0,
|
|
2866
|
+
edgesAdded: 0,
|
|
2867
|
+
snapshotPath: null,
|
|
2868
|
+
mode: opts.to ? "remote" : "local",
|
|
2869
|
+
daemon: "skipped",
|
|
2870
|
+
apply: { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, skipped: true },
|
|
2871
|
+
warnings: []
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
const persisted = await extractAndPersist({
|
|
2875
|
+
scanPath: entry2.path,
|
|
2876
|
+
project: entry2.name,
|
|
2877
|
+
projectExplicit: true,
|
|
2878
|
+
dryRun: true
|
|
2879
|
+
});
|
|
2880
|
+
let snapshotPath = null;
|
|
2881
|
+
if (!opts.dryRun) {
|
|
2882
|
+
const target = persisted.snapshotPath;
|
|
2883
|
+
await saveGraphToDisk(persisted.graph, target);
|
|
2884
|
+
snapshotPath = target;
|
|
2885
|
+
}
|
|
2886
|
+
const skipApply = opts.dryRun || opts.noInstrument;
|
|
2887
|
+
const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services);
|
|
2888
|
+
const warnings = [];
|
|
2889
|
+
let daemonState = "skipped";
|
|
2890
|
+
let exitCode = 0;
|
|
2891
|
+
const mode = opts.dryRun ? "dry-run" : opts.to ? "remote" : "local";
|
|
2892
|
+
if (!opts.dryRun) {
|
|
2893
|
+
const snapshot = snapshotForGraph(persisted);
|
|
2894
|
+
if (opts.to) {
|
|
2895
|
+
const token = opts.token ?? process.env.NEAT_REMOTE_TOKEN;
|
|
2896
|
+
try {
|
|
2897
|
+
await pushSnapshotToRemote({
|
|
2898
|
+
baseUrl: opts.to,
|
|
2899
|
+
token,
|
|
2900
|
+
project: entry2.name,
|
|
2901
|
+
snapshot
|
|
2902
|
+
});
|
|
2903
|
+
daemonState = "remote-ok";
|
|
2904
|
+
} catch (err) {
|
|
2905
|
+
if (err instanceof HttpError) {
|
|
2906
|
+
console.error(`neat sync: ${err.message}`);
|
|
2907
|
+
exitCode = 1;
|
|
2908
|
+
} else if (err instanceof TransportError) {
|
|
2909
|
+
console.error(`neat sync: ${err.message}`);
|
|
2910
|
+
exitCode = 3;
|
|
2911
|
+
} else {
|
|
2912
|
+
console.error(`neat sync: ${err.message}`);
|
|
2913
|
+
exitCode = 1;
|
|
2914
|
+
}
|
|
2915
|
+
daemonState = "skipped";
|
|
2916
|
+
}
|
|
2917
|
+
} else {
|
|
2918
|
+
const daemonUrl = opts.daemonUrl ?? process.env.NEAT_API_URL ?? "http://localhost:8080";
|
|
2919
|
+
const healthy = await checkDaemonHealth2(daemonUrl);
|
|
2920
|
+
if (healthy) {
|
|
2921
|
+
try {
|
|
2922
|
+
await pushSnapshotToRemote({
|
|
2923
|
+
baseUrl: daemonUrl,
|
|
2924
|
+
token: process.env.NEAT_AUTH_TOKEN,
|
|
2925
|
+
project: entry2.name,
|
|
2926
|
+
snapshot
|
|
2927
|
+
});
|
|
2928
|
+
daemonState = "reloaded";
|
|
2929
|
+
} catch (err) {
|
|
2930
|
+
warnings.push(
|
|
2931
|
+
`neat sync: daemon merge failed \u2014 ${err.message}. Snapshot is on disk at ${snapshotPath}.`
|
|
2932
|
+
);
|
|
2933
|
+
daemonState = "down";
|
|
2934
|
+
exitCode = 2;
|
|
2935
|
+
}
|
|
2936
|
+
} else {
|
|
2937
|
+
warnings.push(
|
|
2938
|
+
"neat sync: daemon not running; snapshot updated, run `neatd start` to serve it"
|
|
2939
|
+
);
|
|
2940
|
+
daemonState = "down";
|
|
2941
|
+
exitCode = 2;
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
const result = {
|
|
2946
|
+
exitCode,
|
|
2947
|
+
project: entry2.name,
|
|
2948
|
+
scanPath: entry2.path,
|
|
2949
|
+
nodesAdded: persisted.nodesAdded,
|
|
2950
|
+
edgesAdded: persisted.edgesAdded,
|
|
2951
|
+
snapshotPath,
|
|
2952
|
+
mode,
|
|
2953
|
+
daemon: daemonState,
|
|
2954
|
+
apply: { ...applyTally, skipped: skipApply },
|
|
2955
|
+
warnings
|
|
2956
|
+
};
|
|
2957
|
+
emitResult(result, opts.json);
|
|
2958
|
+
return result;
|
|
2959
|
+
}
|
|
1614
2960
|
|
|
1615
2961
|
// src/cli.ts
|
|
2962
|
+
import { DivergenceTypeSchema } from "@neat.is/types";
|
|
1616
2963
|
function usage() {
|
|
1617
2964
|
console.log("usage: neat <command> [args] [--project <name>]");
|
|
1618
2965
|
console.log("");
|
|
@@ -1637,6 +2984,18 @@ function usage() {
|
|
|
1637
2984
|
console.log(" Flags:");
|
|
1638
2985
|
console.log(" --print-config print the JSON snippet to stdout");
|
|
1639
2986
|
console.log(" --apply merge mcpServers.neat into ~/.claude.json");
|
|
2987
|
+
console.log(" deploy Detect the deploy substrate, generate NEAT_AUTH_TOKEN,");
|
|
2988
|
+
console.log(" emit a docker-compose / systemd / docker run artifact, and");
|
|
2989
|
+
console.log(" print the OTel env-vars block to paste into your platform.");
|
|
2990
|
+
console.log(" sync Re-run discovery, extraction, and SDK apply against the");
|
|
2991
|
+
console.log(" registered project, then notify the running daemon.");
|
|
2992
|
+
console.log(" Flags:");
|
|
2993
|
+
console.log(" --project <name> target a registered project by name");
|
|
2994
|
+
console.log(" --to <url> push the snapshot to a remote daemon");
|
|
2995
|
+
console.log(" --token <token> bearer token for --to (or $NEAT_REMOTE_TOKEN)");
|
|
2996
|
+
console.log(" --dry-run run extraction in-memory; do not write");
|
|
2997
|
+
console.log(" --no-instrument skip the SDK install apply step");
|
|
2998
|
+
console.log(" --json emit the delta summary as JSON");
|
|
1640
2999
|
console.log("");
|
|
1641
3000
|
console.log("query commands (mirror the MCP tools, ADR-050):");
|
|
1642
3001
|
console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
|
|
@@ -1690,7 +3049,9 @@ var STRING_FLAGS = [
|
|
|
1690
3049
|
["--error-id", "errorId"],
|
|
1691
3050
|
["--hypothetical-action", "hypotheticalAction"],
|
|
1692
3051
|
["--type", "type"],
|
|
1693
|
-
["--min-confidence", "minConfidence"]
|
|
3052
|
+
["--min-confidence", "minConfidence"],
|
|
3053
|
+
["--to", "to"],
|
|
3054
|
+
["--token", "token"]
|
|
1694
3055
|
];
|
|
1695
3056
|
function parseArgs(rest) {
|
|
1696
3057
|
const positional = [];
|
|
@@ -1699,6 +3060,10 @@ function parseArgs(rest) {
|
|
|
1699
3060
|
apply: false,
|
|
1700
3061
|
dryRun: false,
|
|
1701
3062
|
noInstall: false,
|
|
3063
|
+
noInstrument: false,
|
|
3064
|
+
noOpen: false,
|
|
3065
|
+
yes: false,
|
|
3066
|
+
verbose: false,
|
|
1702
3067
|
printConfig: false,
|
|
1703
3068
|
json: false,
|
|
1704
3069
|
depth: null,
|
|
@@ -1711,6 +3076,8 @@ function parseArgs(rest) {
|
|
|
1711
3076
|
hypotheticalAction: null,
|
|
1712
3077
|
type: null,
|
|
1713
3078
|
minConfidence: null,
|
|
3079
|
+
to: null,
|
|
3080
|
+
token: null,
|
|
1714
3081
|
positional: []
|
|
1715
3082
|
};
|
|
1716
3083
|
for (let i = 0; i < rest.length; i++) {
|
|
@@ -1727,6 +3094,22 @@ function parseArgs(rest) {
|
|
|
1727
3094
|
out.noInstall = true;
|
|
1728
3095
|
continue;
|
|
1729
3096
|
}
|
|
3097
|
+
if (arg === "--no-instrument") {
|
|
3098
|
+
out.noInstrument = true;
|
|
3099
|
+
continue;
|
|
3100
|
+
}
|
|
3101
|
+
if (arg === "--no-open") {
|
|
3102
|
+
out.noOpen = true;
|
|
3103
|
+
continue;
|
|
3104
|
+
}
|
|
3105
|
+
if (arg === "--yes" || arg === "-y") {
|
|
3106
|
+
out.yes = true;
|
|
3107
|
+
continue;
|
|
3108
|
+
}
|
|
3109
|
+
if (arg === "--verbose" || arg === "-v") {
|
|
3110
|
+
out.verbose = true;
|
|
3111
|
+
continue;
|
|
3112
|
+
}
|
|
1730
3113
|
if (arg === "--print-config") {
|
|
1731
3114
|
out.printConfig = true;
|
|
1732
3115
|
continue;
|
|
@@ -1782,34 +3165,6 @@ function assignFlag(out, field, value) {
|
|
|
1782
3165
|
;
|
|
1783
3166
|
out[field] = value;
|
|
1784
3167
|
}
|
|
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
3168
|
function printBanner() {
|
|
1814
3169
|
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
3170
|
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 +3215,7 @@ async function buildPatchSections(services) {
|
|
|
1860
3215
|
}
|
|
1861
3216
|
async function runInit(opts) {
|
|
1862
3217
|
const written = [];
|
|
1863
|
-
const stat = await
|
|
3218
|
+
const stat = await fs7.stat(opts.scanPath).catch(() => null);
|
|
1864
3219
|
if (!stat || !stat.isDirectory()) {
|
|
1865
3220
|
console.error(`neat init: ${opts.scanPath} is not a directory`);
|
|
1866
3221
|
return { exitCode: 2, writtenFiles: written };
|
|
@@ -1869,11 +3224,15 @@ async function runInit(opts) {
|
|
|
1869
3224
|
printDiscoveryReport(opts, services);
|
|
1870
3225
|
const sections = opts.noInstall ? [] : await buildPatchSections(services);
|
|
1871
3226
|
const patch = renderPatch(sections);
|
|
1872
|
-
const patchPath =
|
|
3227
|
+
const patchPath = path8.join(opts.scanPath, "neat.patch");
|
|
1873
3228
|
if (opts.dryRun) {
|
|
1874
|
-
await
|
|
3229
|
+
await fs7.writeFile(patchPath, patch, "utf8");
|
|
1875
3230
|
written.push(patchPath);
|
|
1876
3231
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
3232
|
+
const gitignorePath = path8.join(opts.scanPath, ".gitignore");
|
|
3233
|
+
const gitignoreExists = await fs7.stat(gitignorePath).then(() => true).catch(() => false);
|
|
3234
|
+
const verb = gitignoreExists ? "append" : "create";
|
|
3235
|
+
console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
|
|
1877
3236
|
console.log("rerun without --dry-run to register and snapshot.");
|
|
1878
3237
|
return { exitCode: 0, writtenFiles: written };
|
|
1879
3238
|
}
|
|
@@ -1882,12 +3241,16 @@ async function runInit(opts) {
|
|
|
1882
3241
|
const graph = getGraph(graphKey);
|
|
1883
3242
|
const projectPaths = pathsForProject(
|
|
1884
3243
|
graphKey,
|
|
1885
|
-
|
|
3244
|
+
path8.join(opts.scanPath, "neat-out")
|
|
1886
3245
|
);
|
|
1887
|
-
const errorsPath =
|
|
3246
|
+
const errorsPath = path8.join(path8.dirname(opts.outPath), path8.basename(projectPaths.errorsPath));
|
|
1888
3247
|
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
1889
3248
|
await saveGraphToDisk(graph, opts.outPath);
|
|
1890
3249
|
written.push(opts.outPath);
|
|
3250
|
+
const gitignoreResult = await ensureNeatOutIgnored(opts.scanPath);
|
|
3251
|
+
if (gitignoreResult.action !== "unchanged") {
|
|
3252
|
+
written.push(gitignoreResult.file);
|
|
3253
|
+
}
|
|
1891
3254
|
const languages = [...new Set(services.map((s) => s.node.language))].sort();
|
|
1892
3255
|
try {
|
|
1893
3256
|
await addProject({
|
|
@@ -1930,35 +3293,27 @@ async function runInit(opts) {
|
|
|
1930
3293
|
console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
1931
3294
|
}
|
|
1932
3295
|
} else {
|
|
1933
|
-
await
|
|
3296
|
+
await fs7.writeFile(patchPath, patch, "utf8");
|
|
1934
3297
|
written.push(patchPath);
|
|
1935
3298
|
}
|
|
1936
3299
|
}
|
|
1937
|
-
const nodes = [];
|
|
1938
|
-
graph.forEachNode((_id, attrs) => nodes.push(attrs));
|
|
1939
|
-
const edges = [];
|
|
1940
|
-
graph.forEachEdge((_id, attrs) => edges.push(attrs));
|
|
1941
3300
|
console.log("");
|
|
1942
|
-
console.log("=== neat init: summary ===");
|
|
1943
3301
|
console.log(`snapshot: ${opts.outPath}`);
|
|
1944
3302
|
console.log(`added: ${result.nodesAdded} nodes, ${result.edgesAdded} edges`);
|
|
1945
|
-
console.log(
|
|
1946
|
-
|
|
3303
|
+
console.log("");
|
|
3304
|
+
const divergenceResult = computeDivergences(graph);
|
|
3305
|
+
console.log(
|
|
3306
|
+
renderValueForwardSummary({
|
|
3307
|
+
graph,
|
|
3308
|
+
divergences: divergenceResult.divergences,
|
|
3309
|
+
verbose: opts.verbose
|
|
3310
|
+
})
|
|
3311
|
+
);
|
|
1947
3312
|
console.log(formatExtractionBanner(result.extractionErrors));
|
|
1948
3313
|
if (result.extractionErrors > 0) {
|
|
1949
3314
|
console.log(`errors: ${errorsPath}`);
|
|
1950
3315
|
}
|
|
1951
3316
|
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
3317
|
if (result.extractionErrors > 0 && isStrictExtractionEnabled()) {
|
|
1963
3318
|
return { exitCode: 4, writtenFiles: written };
|
|
1964
3319
|
}
|
|
@@ -1978,9 +3333,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
1978
3333
|
};
|
|
1979
3334
|
function claudeConfigPath() {
|
|
1980
3335
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
1981
|
-
if (override && override.length > 0) return
|
|
3336
|
+
if (override && override.length > 0) return path8.resolve(override);
|
|
1982
3337
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
1983
|
-
return
|
|
3338
|
+
return path8.join(home, ".claude.json");
|
|
1984
3339
|
}
|
|
1985
3340
|
async function runSkill(opts) {
|
|
1986
3341
|
const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -1992,7 +3347,7 @@ async function runSkill(opts) {
|
|
|
1992
3347
|
const target = claudeConfigPath();
|
|
1993
3348
|
let existing = {};
|
|
1994
3349
|
try {
|
|
1995
|
-
existing = JSON.parse(await
|
|
3350
|
+
existing = JSON.parse(await fs7.readFile(target, "utf8"));
|
|
1996
3351
|
} catch (err) {
|
|
1997
3352
|
if (err.code !== "ENOENT") {
|
|
1998
3353
|
console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
|
|
@@ -2004,8 +3359,8 @@ async function runSkill(opts) {
|
|
|
2004
3359
|
...existing,
|
|
2005
3360
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
2006
3361
|
};
|
|
2007
|
-
await
|
|
2008
|
-
await
|
|
3362
|
+
await fs7.mkdir(path8.dirname(target), { recursive: true });
|
|
3363
|
+
await fs7.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
2009
3364
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
2010
3365
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
2011
3366
|
return { exitCode: 0 };
|
|
@@ -2039,12 +3394,12 @@ async function main() {
|
|
|
2039
3394
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
2040
3395
|
process.exit(2);
|
|
2041
3396
|
}
|
|
2042
|
-
const scanPath =
|
|
3397
|
+
const scanPath = path8.resolve(target);
|
|
2043
3398
|
const projectExplicit = parsed.project !== null;
|
|
2044
|
-
const projectName = projectExplicit ? project :
|
|
3399
|
+
const projectName = projectExplicit ? project : path8.basename(scanPath);
|
|
2045
3400
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
2046
|
-
const fallback = pathsForProject(projectKey,
|
|
2047
|
-
const outPath =
|
|
3401
|
+
const fallback = pathsForProject(projectKey, path8.join(scanPath, "neat-out")).snapshotPath;
|
|
3402
|
+
const outPath = path8.resolve(process.env.NEAT_OUT_PATH ?? fallback);
|
|
2048
3403
|
const result = await runInit({
|
|
2049
3404
|
scanPath,
|
|
2050
3405
|
outPath,
|
|
@@ -2052,7 +3407,8 @@ async function main() {
|
|
|
2052
3407
|
projectExplicit,
|
|
2053
3408
|
apply: apply3,
|
|
2054
3409
|
dryRun,
|
|
2055
|
-
noInstall
|
|
3410
|
+
noInstall,
|
|
3411
|
+
verbose: parsed.verbose
|
|
2056
3412
|
});
|
|
2057
3413
|
if (result.exitCode !== 0) process.exit(result.exitCode);
|
|
2058
3414
|
return;
|
|
@@ -2064,21 +3420,21 @@ async function main() {
|
|
|
2064
3420
|
usage();
|
|
2065
3421
|
process.exit(2);
|
|
2066
3422
|
}
|
|
2067
|
-
const scanPath =
|
|
2068
|
-
const stat = await
|
|
3423
|
+
const scanPath = path8.resolve(target);
|
|
3424
|
+
const stat = await fs7.stat(scanPath).catch(() => null);
|
|
2069
3425
|
if (!stat || !stat.isDirectory()) {
|
|
2070
3426
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
2071
3427
|
process.exit(2);
|
|
2072
3428
|
}
|
|
2073
|
-
const projectPaths = pathsForProject(project,
|
|
2074
|
-
const outPath =
|
|
2075
|
-
const errorsPath =
|
|
2076
|
-
process.env.NEAT_ERRORS_PATH ??
|
|
3429
|
+
const projectPaths = pathsForProject(project, path8.join(scanPath, "neat-out"));
|
|
3430
|
+
const outPath = path8.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
|
|
3431
|
+
const errorsPath = path8.resolve(
|
|
3432
|
+
process.env.NEAT_ERRORS_PATH ?? path8.join(path8.dirname(outPath), path8.basename(projectPaths.errorsPath))
|
|
2077
3433
|
);
|
|
2078
|
-
const staleEventsPath =
|
|
2079
|
-
process.env.NEAT_STALE_EVENTS_PATH ??
|
|
3434
|
+
const staleEventsPath = path8.resolve(
|
|
3435
|
+
process.env.NEAT_STALE_EVENTS_PATH ?? path8.join(path8.dirname(outPath), path8.basename(projectPaths.staleEventsPath))
|
|
2080
3436
|
);
|
|
2081
|
-
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ?
|
|
3437
|
+
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path8.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
|
|
2082
3438
|
const handle = await startWatch(getGraph(project), {
|
|
2083
3439
|
scanPath,
|
|
2084
3440
|
outPath,
|
|
@@ -2171,15 +3527,74 @@ async function main() {
|
|
|
2171
3527
|
console.log("note: neat-out/, policy.json, and other files at the project path were left in place.");
|
|
2172
3528
|
return;
|
|
2173
3529
|
}
|
|
3530
|
+
if (cmd === "deploy") {
|
|
3531
|
+
const artifact = await runDeploy();
|
|
3532
|
+
const block = renderOtelEnvBlock2(artifact.token);
|
|
3533
|
+
console.log();
|
|
3534
|
+
console.log(`Substrate detected: ${artifact.substrate}`);
|
|
3535
|
+
if (artifact.artifactPath) {
|
|
3536
|
+
console.log(`Artifact written: ${artifact.artifactPath}`);
|
|
3537
|
+
} else {
|
|
3538
|
+
console.log("No on-disk artifact \u2014 copy the snippet below into your substrate.");
|
|
3539
|
+
console.log();
|
|
3540
|
+
console.log(artifact.contents);
|
|
3541
|
+
}
|
|
3542
|
+
console.log();
|
|
3543
|
+
console.log("NEAT_AUTH_TOKEN (store this \u2014 it will not be printed again):");
|
|
3544
|
+
console.log(` ${artifact.token}`);
|
|
3545
|
+
console.log();
|
|
3546
|
+
console.log("For your application's deploy platform, set these env vars:");
|
|
3547
|
+
console.log(block.split("\n").map((l) => ` ${l}`).join("\n"));
|
|
3548
|
+
console.log();
|
|
3549
|
+
console.log("Once NEAT is running, your dashboard will be at:");
|
|
3550
|
+
console.log(" https://<host>:6328");
|
|
3551
|
+
console.log();
|
|
3552
|
+
console.log("To start NEAT, run:");
|
|
3553
|
+
console.log(` ${artifact.startCommand}`);
|
|
3554
|
+
return;
|
|
3555
|
+
}
|
|
3556
|
+
if (cmd === "sync") {
|
|
3557
|
+
const result = await runSync({
|
|
3558
|
+
...parsed.project ? { project: parsed.project } : {},
|
|
3559
|
+
...parsed.to ? { to: parsed.to } : {},
|
|
3560
|
+
...parsed.token ? { token: parsed.token } : {},
|
|
3561
|
+
dryRun: parsed.dryRun,
|
|
3562
|
+
noInstrument: parsed.noInstrument,
|
|
3563
|
+
json: parsed.json
|
|
3564
|
+
});
|
|
3565
|
+
if (result.exitCode !== 0) process.exit(result.exitCode);
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
2174
3568
|
if (QUERY_VERBS.has(cmd)) {
|
|
2175
3569
|
const code = await runQueryVerb(cmd, parsed);
|
|
2176
3570
|
if (code !== 0) process.exit(code);
|
|
2177
3571
|
return;
|
|
2178
3572
|
}
|
|
3573
|
+
const orchestratorCode = await tryOrchestrator(cmd, parsed);
|
|
3574
|
+
if (orchestratorCode !== null) {
|
|
3575
|
+
if (orchestratorCode !== 0) process.exit(orchestratorCode);
|
|
3576
|
+
return;
|
|
3577
|
+
}
|
|
2179
3578
|
console.error(`neat: unknown command "${cmd}"`);
|
|
2180
3579
|
usage();
|
|
2181
3580
|
process.exit(1);
|
|
2182
3581
|
}
|
|
3582
|
+
async function tryOrchestrator(cmd, parsed) {
|
|
3583
|
+
const scanPath = path8.resolve(cmd);
|
|
3584
|
+
const stat = await fs7.stat(scanPath).catch(() => null);
|
|
3585
|
+
if (!stat || !stat.isDirectory()) return null;
|
|
3586
|
+
const projectExplicit = parsed.project !== null;
|
|
3587
|
+
const projectName = projectExplicit ? parsed.project : path8.basename(scanPath);
|
|
3588
|
+
const result = await runOrchestrator({
|
|
3589
|
+
scanPath,
|
|
3590
|
+
project: projectName,
|
|
3591
|
+
projectExplicit,
|
|
3592
|
+
noInstrument: parsed.noInstrument,
|
|
3593
|
+
noOpen: parsed.noOpen,
|
|
3594
|
+
yes: parsed.yes
|
|
3595
|
+
});
|
|
3596
|
+
return result.exitCode;
|
|
3597
|
+
}
|
|
2183
3598
|
var QUERY_VERBS = /* @__PURE__ */ new Set([
|
|
2184
3599
|
"root-cause",
|
|
2185
3600
|
"blast-radius",
|