@neat.is/core 0.4.20-dev.20260630 → 0.4.22
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-MTXF77TN.js → chunk-GAFTW2OX.js} +2 -2
- package/dist/{chunk-GNAX2CBF.js → chunk-OQ7CHOY2.js} +859 -159
- package/dist/chunk-OQ7CHOY2.js.map +1 -0
- package/dist/{chunk-62ELQVIP.js → chunk-XRE4OQWJ.js} +57 -9
- package/dist/chunk-XRE4OQWJ.js.map +1 -0
- package/dist/{chunk-BGPWBRLU.js → chunk-ZX7PCMGZ.js} +21 -2
- package/dist/{chunk-BGPWBRLU.js.map → chunk-ZX7PCMGZ.js.map} +1 -1
- package/dist/cli.cjs +1232 -354
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +2 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +164 -60
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +884 -150
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +928 -150
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.d.cts +17 -0
- package/dist/neatd.d.ts +17 -0
- package/dist/neatd.js +27 -3
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-I67ONCRM.js → otel-grpc-QYHUJ4OY.js} +3 -3
- package/dist/server.cjs +806 -121
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/package.json +2 -2
- package/proto/opentelemetry/proto/trace/v1/trace.proto +8 -2
- package/dist/chunk-62ELQVIP.js.map +0 -1
- package/dist/chunk-GNAX2CBF.js.map +0 -1
- /package/dist/{chunk-MTXF77TN.js.map → chunk-GAFTW2OX.js.map} +0 -0
- /package/dist/{otel-grpc-I67ONCRM.js.map → otel-grpc-QYHUJ4OY.js.map} +0 -0
package/dist/cli.d.cts
CHANGED
|
@@ -78,6 +78,7 @@ declare class ProjectResolutionError extends Error {
|
|
|
78
78
|
constructor(message: string, exitCode?: number);
|
|
79
79
|
}
|
|
80
80
|
declare function resolveProjectForVerb(client: HttpClient, parsed: ParsedArgs): Promise<string | undefined>;
|
|
81
|
+
declare function resolveDaemonUrl(project?: string): Promise<string>;
|
|
81
82
|
declare function runQueryVerb(cmd: string, parsed: ParsedArgs): Promise<number>;
|
|
82
83
|
|
|
83
|
-
export { CLAUDE_SKILL_CONFIG, type InitOptions, type InitResult, ProjectResolutionError, QUERY_VERBS, type SkillOptions, commandPrefix, isNpxInvocation, main, parseArgs, printBanner, readPackageVersion, resolveProjectForVerb, runInit, runQueryVerb, runSkill, usage };
|
|
84
|
+
export { CLAUDE_SKILL_CONFIG, type InitOptions, type InitResult, ProjectResolutionError, QUERY_VERBS, type SkillOptions, commandPrefix, isNpxInvocation, main, parseArgs, printBanner, readPackageVersion, resolveDaemonUrl, resolveProjectForVerb, runInit, runQueryVerb, runSkill, usage };
|
package/dist/cli.d.ts
CHANGED
|
@@ -78,6 +78,7 @@ declare class ProjectResolutionError extends Error {
|
|
|
78
78
|
constructor(message: string, exitCode?: number);
|
|
79
79
|
}
|
|
80
80
|
declare function resolveProjectForVerb(client: HttpClient, parsed: ParsedArgs): Promise<string | undefined>;
|
|
81
|
+
declare function resolveDaemonUrl(project?: string): Promise<string>;
|
|
81
82
|
declare function runQueryVerb(cmd: string, parsed: ParsedArgs): Promise<number>;
|
|
82
83
|
|
|
83
|
-
export { CLAUDE_SKILL_CONFIG, type InitOptions, type InitResult, ProjectResolutionError, QUERY_VERBS, type SkillOptions, commandPrefix, isNpxInvocation, main, parseArgs, printBanner, readPackageVersion, resolveProjectForVerb, runInit, runQueryVerb, runSkill, usage };
|
|
84
|
+
export { CLAUDE_SKILL_CONFIG, type InitOptions, type InitResult, ProjectResolutionError, QUERY_VERBS, type SkillOptions, commandPrefix, isNpxInvocation, main, parseArgs, printBanner, readPackageVersion, resolveDaemonUrl, resolveProjectForVerb, runInit, runQueryVerb, runSkill, usage };
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
clearDaemonRecord,
|
|
4
|
+
portFromListenAddress,
|
|
5
|
+
readDaemonRecord,
|
|
6
|
+
resolveHost,
|
|
7
|
+
resolveNeatVersion,
|
|
8
|
+
writeDaemonRecord
|
|
9
|
+
} from "./chunk-XRE4OQWJ.js";
|
|
5
10
|
import {
|
|
6
11
|
buildSearchIndex
|
|
7
12
|
} from "./chunk-XOOCA5T7.js";
|
|
@@ -14,6 +19,7 @@ import {
|
|
|
14
19
|
addCallEdges,
|
|
15
20
|
addConfigNodes,
|
|
16
21
|
addDatabasesAndCompat,
|
|
22
|
+
addFiles,
|
|
17
23
|
addImports,
|
|
18
24
|
addInfra,
|
|
19
25
|
addProject,
|
|
@@ -53,17 +59,18 @@ import {
|
|
|
53
59
|
signalDaemonStop,
|
|
54
60
|
startPersistLoop,
|
|
55
61
|
startStalenessLoop
|
|
56
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-OQ7CHOY2.js";
|
|
57
63
|
import {
|
|
58
64
|
startOtelGrpcReceiver
|
|
59
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-GAFTW2OX.js";
|
|
60
66
|
import {
|
|
61
67
|
__dirname,
|
|
62
68
|
__require,
|
|
63
69
|
assertBindAuthority,
|
|
64
70
|
buildOtelReceiver,
|
|
71
|
+
listenSteppingOtlp,
|
|
65
72
|
readAuthEnv
|
|
66
|
-
} from "./chunk-
|
|
73
|
+
} from "./chunk-ZX7PCMGZ.js";
|
|
67
74
|
|
|
68
75
|
// src/cli.ts
|
|
69
76
|
import path9 from "path";
|
|
@@ -241,6 +248,7 @@ import chokidar from "chokidar";
|
|
|
241
248
|
var ALL_PHASES = [
|
|
242
249
|
"services",
|
|
243
250
|
"aliases",
|
|
251
|
+
"files",
|
|
244
252
|
"imports",
|
|
245
253
|
"databases",
|
|
246
254
|
"configs",
|
|
@@ -265,6 +273,7 @@ function classifyChange(relPath) {
|
|
|
265
273
|
phases.add("aliases");
|
|
266
274
|
}
|
|
267
275
|
if (/\.(?:js|jsx|mjs|cjs|ts|tsx|py)$/.test(base)) {
|
|
276
|
+
phases.add("files");
|
|
268
277
|
phases.add("imports");
|
|
269
278
|
phases.add("calls");
|
|
270
279
|
}
|
|
@@ -287,6 +296,11 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
|
|
|
287
296
|
if (phases.has("aliases")) {
|
|
288
297
|
await addServiceAliases(graph, scanPath, services);
|
|
289
298
|
}
|
|
299
|
+
if (phases.has("files")) {
|
|
300
|
+
const r = await addFiles(graph, services);
|
|
301
|
+
nodesAdded += r.nodesAdded;
|
|
302
|
+
edgesAdded += r.edgesAdded;
|
|
303
|
+
}
|
|
290
304
|
if (phases.has("imports")) {
|
|
291
305
|
const r = await addImports(graph, services);
|
|
292
306
|
nodesAdded += r.nodesAdded;
|
|
@@ -321,6 +335,7 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
|
|
|
321
335
|
durationMs: Date.now() - started
|
|
322
336
|
};
|
|
323
337
|
}
|
|
338
|
+
var DEFAULT_WATCH_WEB_PORT = 6328;
|
|
324
339
|
var IGNORED_WATCH_GLOBS = [
|
|
325
340
|
"**/node_modules/**",
|
|
326
341
|
"**/.git/**",
|
|
@@ -469,7 +484,7 @@ async function startWatch(graph, opts) {
|
|
|
469
484
|
searchIndex
|
|
470
485
|
});
|
|
471
486
|
const api = await buildApi({ projects: registry });
|
|
472
|
-
await api.listen({ port, host });
|
|
487
|
+
const restAddress = await api.listen({ port, host });
|
|
473
488
|
console.log(`neat-core listening on http://${host}:${port}`);
|
|
474
489
|
console.log(` scan path: ${opts.scanPath} (watching for changes)`);
|
|
475
490
|
console.log(` snapshot path: ${opts.outPath}`);
|
|
@@ -482,10 +497,41 @@ async function startWatch(graph, opts) {
|
|
|
482
497
|
writeErrorEventInline: false,
|
|
483
498
|
onPolicyTrigger
|
|
484
499
|
});
|
|
485
|
-
const onErrorSpanSync = makeErrorSpanWriter(opts.errorsPath);
|
|
500
|
+
const onErrorSpanSync = makeErrorSpanWriter(opts.errorsPath, graph, opts.scanPath);
|
|
486
501
|
const otelHttp = await buildOtelReceiver({ onSpan, onErrorSpanSync });
|
|
487
|
-
await otelHttp
|
|
488
|
-
|
|
502
|
+
const otelAddress = await listenSteppingOtlp(otelHttp, otelPort, host);
|
|
503
|
+
const boundOtelPort = portFromListenAddress(otelAddress, otelPort);
|
|
504
|
+
console.log(`neat-core OTLP receiver on ${otelAddress}/v1/traces`);
|
|
505
|
+
const boundRestPort = portFromListenAddress(restAddress, port);
|
|
506
|
+
const daemonRecord = {
|
|
507
|
+
project: projectName,
|
|
508
|
+
projectPath: opts.scanPath,
|
|
509
|
+
pid: process.pid,
|
|
510
|
+
status: "running",
|
|
511
|
+
ports: {
|
|
512
|
+
rest: boundRestPort,
|
|
513
|
+
otlp: boundOtelPort,
|
|
514
|
+
// watch serves no dashboard of its own; record the canonical web port so
|
|
515
|
+
// the record shape is valid. Only ports.otlp is load-bearing here.
|
|
516
|
+
web: DEFAULT_WATCH_WEB_PORT
|
|
517
|
+
},
|
|
518
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
519
|
+
neatVersion: resolveNeatVersion()
|
|
520
|
+
};
|
|
521
|
+
try {
|
|
522
|
+
await writeDaemonRecord(daemonRecord);
|
|
523
|
+
console.log(
|
|
524
|
+
`neat watch: wrote daemon.json (REST ${boundRestPort} / OTLP ${boundOtelPort})`
|
|
525
|
+
);
|
|
526
|
+
} catch (err) {
|
|
527
|
+
await api.close().catch(() => {
|
|
528
|
+
});
|
|
529
|
+
await otelHttp.close().catch(() => {
|
|
530
|
+
});
|
|
531
|
+
throw new Error(
|
|
532
|
+
`neat watch: failed to write daemon.json \u2014 ${err.message}`
|
|
533
|
+
);
|
|
534
|
+
}
|
|
489
535
|
let grpcReceiver = null;
|
|
490
536
|
if (opts.otelGrpc) {
|
|
491
537
|
const grpcPort = opts.otelGrpcPort ?? 4317;
|
|
@@ -598,6 +644,8 @@ async function startWatch(graph, opts) {
|
|
|
598
644
|
await api.close();
|
|
599
645
|
await otelHttp.close();
|
|
600
646
|
if (grpcReceiver) await grpcReceiver.stop();
|
|
647
|
+
await clearDaemonRecord(daemonRecord).catch(() => {
|
|
648
|
+
});
|
|
601
649
|
};
|
|
602
650
|
return { api, stop };
|
|
603
651
|
}
|
|
@@ -1448,9 +1496,23 @@ var WEB_FRAMEWORK_DEPS = [
|
|
|
1448
1496
|
"polka",
|
|
1449
1497
|
"micro"
|
|
1450
1498
|
];
|
|
1451
|
-
|
|
1499
|
+
var WORKER_FRAMEWORK_DEPS = [
|
|
1500
|
+
"bullmq",
|
|
1501
|
+
"bull",
|
|
1502
|
+
"bee-queue",
|
|
1503
|
+
"agenda",
|
|
1504
|
+
"kafkajs",
|
|
1505
|
+
"amqplib",
|
|
1506
|
+
"amqp-connection-manager",
|
|
1507
|
+
"nats",
|
|
1508
|
+
"node-resque",
|
|
1509
|
+
"pg-boss",
|
|
1510
|
+
"graphile-worker"
|
|
1511
|
+
];
|
|
1512
|
+
var APP_FRAMEWORK_DEPS = [...WEB_FRAMEWORK_DEPS, ...WORKER_FRAMEWORK_DEPS];
|
|
1513
|
+
function appFrameworkDependencies(pkg) {
|
|
1452
1514
|
const deps = allDeps(pkg);
|
|
1453
|
-
return
|
|
1515
|
+
return APP_FRAMEWORK_DEPS.filter((name) => name in deps);
|
|
1454
1516
|
}
|
|
1455
1517
|
function uninstrumentedLibraries(pkg) {
|
|
1456
1518
|
const deps = allDeps(pkg);
|
|
@@ -1537,10 +1599,10 @@ async function isTypeScriptProject(serviceDir) {
|
|
|
1537
1599
|
var INDEX_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".cjs"];
|
|
1538
1600
|
var INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `index${ext}`);
|
|
1539
1601
|
var SRC_INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `src/index${ext}`);
|
|
1540
|
-
var SRC_NAMED_CANDIDATES = ["server", "main", "app"].flatMap(
|
|
1602
|
+
var SRC_NAMED_CANDIDATES = ["server", "main", "app", "worker"].flatMap(
|
|
1541
1603
|
(name) => INDEX_EXTENSIONS.map((ext) => `src/${name}${ext}`)
|
|
1542
1604
|
);
|
|
1543
|
-
var ROOT_NAMED_CANDIDATES = ["server", "app", "main"].flatMap(
|
|
1605
|
+
var ROOT_NAMED_CANDIDATES = ["server", "app", "main", "worker"].flatMap(
|
|
1544
1606
|
(name) => INDEX_EXTENSIONS.map((ext) => `${name}${ext}`)
|
|
1545
1607
|
);
|
|
1546
1608
|
var SCRIPT_LAUNCHERS = /* @__PURE__ */ new Set([
|
|
@@ -2797,11 +2859,13 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
2797
2859
|
} else if (outcome.outcome === "already-instrumented") already++;
|
|
2798
2860
|
else if (outcome.outcome === "lib-only") {
|
|
2799
2861
|
libOnly++;
|
|
2800
|
-
|
|
2862
|
+
const appDeps = svc.pkg ? appFrameworkDependencies(svc.pkg) : [];
|
|
2863
|
+
if (appDeps.length > 0) {
|
|
2801
2864
|
const svcName = path7.basename(svc.dir);
|
|
2865
|
+
const list = appDeps.join(", ");
|
|
2802
2866
|
console.warn(
|
|
2803
2867
|
`neat: runtime layer won't engage for ${svcName}: no entry point found.
|
|
2804
|
-
${svc.dir} depends on a
|
|
2868
|
+
${svc.dir} depends on ${list} \u2014 a runnable app \u2014 but neat couldn't resolve an entry to instrument.
|
|
2805
2869
|
Add a "start" script to package.json, or point neat at the entry file directly.`
|
|
2806
2870
|
);
|
|
2807
2871
|
}
|
|
@@ -2869,10 +2933,11 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
2869
2933
|
if (gaps.length > 0) {
|
|
2870
2934
|
const svcName = path7.basename(svc.dir);
|
|
2871
2935
|
const list = gaps.join(", ");
|
|
2872
|
-
const
|
|
2936
|
+
const subject = gaps.length === 1 ? "this library" : "these libraries";
|
|
2937
|
+
const aux = gaps.length === 1 ? "isn't" : "aren't";
|
|
2873
2938
|
console.warn(
|
|
2874
2939
|
`neat: calls to ${list} won't be observed by default in ${svcName}.
|
|
2875
|
-
${
|
|
2940
|
+
${subject} ${aux} in the default instrumentation set, so they produce no OBSERVED edges.
|
|
2876
2941
|
Run \`neat list-uninstrumented\` to review them, then \`neat extend\` to capture them.`
|
|
2877
2942
|
);
|
|
2878
2943
|
}
|
|
@@ -2966,29 +3031,22 @@ async function probeProjectHealth(restPort, name) {
|
|
|
2966
3031
|
});
|
|
2967
3032
|
});
|
|
2968
3033
|
}
|
|
2969
|
-
async function snapshotProjectStatus(restPort, body) {
|
|
3034
|
+
async function snapshotProjectStatus(restPort, project, body) {
|
|
2970
3035
|
if (body.projects && body.projects.length > 0) {
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
status: p.status ?? "active"
|
|
2974
|
-
}
|
|
2975
|
-
}
|
|
2976
|
-
|
|
2977
|
-
if (entries.length === 0) return [];
|
|
2978
|
-
return Promise.all(
|
|
2979
|
-
entries.map(async (entry2) => ({
|
|
2980
|
-
name: entry2.name,
|
|
2981
|
-
status: await probeProjectHealth(restPort, entry2.name)
|
|
2982
|
-
}))
|
|
2983
|
-
);
|
|
3036
|
+
const mine = body.projects.filter((p) => p.name === project);
|
|
3037
|
+
if (mine.length > 0) {
|
|
3038
|
+
return mine.map((p) => ({ name: p.name, status: p.status ?? "active" }));
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
return [{ name: project, status: await probeProjectHealth(restPort, project) }];
|
|
2984
3042
|
}
|
|
2985
|
-
async function waitForDaemonReady(restPort, timeoutMs) {
|
|
3043
|
+
async function waitForDaemonReady(restPort, project, timeoutMs) {
|
|
2986
3044
|
const deadline = Date.now() + timeoutMs;
|
|
2987
3045
|
let lastBootstrapping = [];
|
|
2988
3046
|
while (Date.now() < deadline) {
|
|
2989
3047
|
const body = await fetchDaemonHealth(restPort);
|
|
2990
3048
|
if (body !== null) {
|
|
2991
|
-
const projects2 = await snapshotProjectStatus(restPort, body);
|
|
3049
|
+
const projects2 = await snapshotProjectStatus(restPort, project, body);
|
|
2992
3050
|
const bootstrapping = projects2.filter((p) => p.status === "bootstrapping").map((p) => p.name);
|
|
2993
3051
|
const broken = projects2.filter((p) => p.status === "broken").map((p) => p.name);
|
|
2994
3052
|
if (bootstrapping.length === 0) {
|
|
@@ -3007,7 +3065,7 @@ async function waitForDaemonReady(restPort, timeoutMs) {
|
|
|
3007
3065
|
await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
|
|
3008
3066
|
}
|
|
3009
3067
|
const final = await fetchDaemonHealth(restPort);
|
|
3010
|
-
const projects = final ? await snapshotProjectStatus(restPort, final) : [];
|
|
3068
|
+
const projects = final ? await snapshotProjectStatus(restPort, project, final) : [];
|
|
3011
3069
|
return {
|
|
3012
3070
|
ready: false,
|
|
3013
3071
|
brokenProjects: projects.filter((p) => p.status === "broken").map((p) => p.name),
|
|
@@ -3015,14 +3073,37 @@ async function waitForDaemonReady(restPort, timeoutMs) {
|
|
|
3015
3073
|
};
|
|
3016
3074
|
}
|
|
3017
3075
|
var NEAT_PORTS = [8080, 4318, 6328];
|
|
3018
|
-
|
|
3076
|
+
function probeBind(port, host) {
|
|
3019
3077
|
return new Promise((resolve) => {
|
|
3020
3078
|
const server = net.createServer();
|
|
3021
|
-
server.once("error", () =>
|
|
3022
|
-
|
|
3023
|
-
|
|
3079
|
+
server.once("error", (err) => {
|
|
3080
|
+
resolve(err.code === "EADDRINUSE" ? "in-use" : "unavailable");
|
|
3081
|
+
});
|
|
3082
|
+
server.once("listening", () => server.close(() => resolve("free")));
|
|
3083
|
+
server.listen(port, host);
|
|
3024
3084
|
});
|
|
3025
3085
|
}
|
|
3086
|
+
function crossFamilyHost(host) {
|
|
3087
|
+
switch (host) {
|
|
3088
|
+
case "127.0.0.1":
|
|
3089
|
+
case "localhost":
|
|
3090
|
+
return "::1";
|
|
3091
|
+
case "::1":
|
|
3092
|
+
return "127.0.0.1";
|
|
3093
|
+
case "0.0.0.0":
|
|
3094
|
+
return "::";
|
|
3095
|
+
case "::":
|
|
3096
|
+
return "0.0.0.0";
|
|
3097
|
+
default:
|
|
3098
|
+
return null;
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
async function isPortFree(port, host = "127.0.0.1") {
|
|
3102
|
+
if (await probeBind(port, host) !== "free") return false;
|
|
3103
|
+
const sibling = crossFamilyHost(host);
|
|
3104
|
+
if (sibling && await probeBind(port, sibling) === "in-use") return false;
|
|
3105
|
+
return true;
|
|
3106
|
+
}
|
|
3026
3107
|
function formatPortCollisionMessage(port) {
|
|
3027
3108
|
return [
|
|
3028
3109
|
`neat: port ${port} is in use; the NEAT daemon needs it.`,
|
|
@@ -3032,13 +3113,13 @@ function formatPortCollisionMessage(port) {
|
|
|
3032
3113
|
}
|
|
3033
3114
|
var PORT_ALLOCATION_ATTEMPTS = 8;
|
|
3034
3115
|
var PORT_STRIDE = 1;
|
|
3035
|
-
async function tripleFree(ports) {
|
|
3116
|
+
async function tripleFree(ports, host = "127.0.0.1") {
|
|
3036
3117
|
for (const p of [ports.rest, ports.otlp, ports.web]) {
|
|
3037
|
-
if (!await isPortFree(p)) return false;
|
|
3118
|
+
if (!await isPortFree(p, host)) return false;
|
|
3038
3119
|
}
|
|
3039
3120
|
return true;
|
|
3040
3121
|
}
|
|
3041
|
-
async function allocatePorts() {
|
|
3122
|
+
async function allocatePorts(host = "127.0.0.1") {
|
|
3042
3123
|
const [baseRest, baseOtlp, baseWeb] = NEAT_PORTS;
|
|
3043
3124
|
for (let i = 0; i < PORT_ALLOCATION_ATTEMPTS; i++) {
|
|
3044
3125
|
const candidate = {
|
|
@@ -3046,7 +3127,7 @@ async function allocatePorts() {
|
|
|
3046
3127
|
otlp: baseOtlp + i * PORT_STRIDE,
|
|
3047
3128
|
web: baseWeb + i * PORT_STRIDE
|
|
3048
3129
|
};
|
|
3049
|
-
if (await tripleFree(candidate)) return candidate;
|
|
3130
|
+
if (await tripleFree(candidate, host)) return candidate;
|
|
3050
3131
|
}
|
|
3051
3132
|
return null;
|
|
3052
3133
|
}
|
|
@@ -3252,16 +3333,20 @@ async function runOrchestrator(opts) {
|
|
|
3252
3333
|
}
|
|
3253
3334
|
}
|
|
3254
3335
|
const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
|
|
3336
|
+
const bindHost = resolveHost(
|
|
3337
|
+
{},
|
|
3338
|
+
typeof process.env.NEAT_AUTH_TOKEN === "string" && process.env.NEAT_AUTH_TOKEN.length > 0
|
|
3339
|
+
);
|
|
3255
3340
|
const persistedPorts = await persistedPortsFor(opts.scanPath);
|
|
3256
3341
|
let allocated = null;
|
|
3257
3342
|
if (persistedPorts && await healthIsForProject(persistedPorts.rest, currentProjectName)) {
|
|
3258
3343
|
result.steps.daemon = "already-running";
|
|
3259
3344
|
allocated = persistedPorts;
|
|
3260
3345
|
} else {
|
|
3261
|
-
if (persistedPorts && await isPortFree(persistedPorts.rest) && await tripleFree(persistedPorts)) {
|
|
3346
|
+
if (persistedPorts && await isPortFree(persistedPorts.rest, bindHost) && await tripleFree(persistedPorts, bindHost)) {
|
|
3262
3347
|
allocated = persistedPorts;
|
|
3263
3348
|
} else {
|
|
3264
|
-
allocated = await allocatePorts();
|
|
3349
|
+
allocated = await allocatePorts(bindHost);
|
|
3265
3350
|
}
|
|
3266
3351
|
if (!allocated) {
|
|
3267
3352
|
for (const line of formatPortCollisionMessage(NEAT_PORTS[0])) {
|
|
@@ -3290,7 +3375,7 @@ async function runOrchestrator(opts) {
|
|
|
3290
3375
|
projectPath: opts.scanPath,
|
|
3291
3376
|
ports: allocated
|
|
3292
3377
|
});
|
|
3293
|
-
const ready = await waitForDaemonReady(allocated.rest, timeoutMs);
|
|
3378
|
+
const ready = await waitForDaemonReady(allocated.rest, currentProjectName, timeoutMs);
|
|
3294
3379
|
result.steps.daemon = ready.ready ? "spawned" : "timed-out";
|
|
3295
3380
|
if (!ready.ready) {
|
|
3296
3381
|
console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
|
|
@@ -3472,7 +3557,7 @@ async function runBlastRadius(client, input) {
|
|
|
3472
3557
|
const result = await client.get(path10);
|
|
3473
3558
|
if (result.totalAffected === 0) {
|
|
3474
3559
|
return {
|
|
3475
|
-
summary: `${result.origin} has no
|
|
3560
|
+
summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
|
|
3476
3561
|
};
|
|
3477
3562
|
}
|
|
3478
3563
|
const sorted = [...result.affectedNodes].sort(
|
|
@@ -3485,7 +3570,7 @@ async function runBlastRadius(client, input) {
|
|
|
3485
3570
|
);
|
|
3486
3571
|
const provenances = [...new Set(sorted.map((n) => n.edgeProvenance))];
|
|
3487
3572
|
return {
|
|
3488
|
-
summary: `Blast radius for ${result.origin}: ${result.totalAffected}
|
|
3573
|
+
summary: `Blast radius for ${result.origin}: ${result.totalAffected} dependent node${result.totalAffected === 1 ? "" : "s"} would break if it changed.`,
|
|
3489
3574
|
block: blockLines.join("\n"),
|
|
3490
3575
|
confidence: Number.isFinite(minConfidence) ? minConfidence : void 0,
|
|
3491
3576
|
provenance: provenances.length ? provenances : void 0
|
|
@@ -3539,20 +3624,31 @@ async function runDependencies(client, input) {
|
|
|
3539
3624
|
throw err;
|
|
3540
3625
|
}
|
|
3541
3626
|
}
|
|
3627
|
+
function observedDepLine(nodeId, e) {
|
|
3628
|
+
const via = e.source !== nodeId ? ` (via ${e.source})` : "";
|
|
3629
|
+
return ` \u2022 ${e.target} \u2014 ${e.type}${via}${edgeMeta(e)}`;
|
|
3630
|
+
}
|
|
3542
3631
|
async function runObservedDependencies(client, input) {
|
|
3543
3632
|
try {
|
|
3544
|
-
const
|
|
3545
|
-
projectPath(
|
|
3633
|
+
const result = await client.get(
|
|
3634
|
+
projectPath(
|
|
3635
|
+
input.project,
|
|
3636
|
+
`/graph/observed-dependencies/${encodeURIComponent(input.nodeId)}`
|
|
3637
|
+
)
|
|
3546
3638
|
);
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3639
|
+
if (result.dependencies.length === 0) {
|
|
3640
|
+
if (result.observed) {
|
|
3641
|
+
return {
|
|
3642
|
+
summary: `${input.nodeId} makes no outbound runtime calls, but OTel has observed it receiving traffic on ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 it's a pure receiver.`,
|
|
3643
|
+
provenance: Provenance2.OBSERVED
|
|
3644
|
+
};
|
|
3645
|
+
}
|
|
3646
|
+
const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
3551
3647
|
return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
|
|
3552
3648
|
}
|
|
3553
|
-
const blockLines =
|
|
3649
|
+
const blockLines = result.dependencies.map((e) => observedDepLine(input.nodeId, e));
|
|
3554
3650
|
return {
|
|
3555
|
-
summary: `${input.nodeId} has ${
|
|
3651
|
+
summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
3556
3652
|
block: blockLines.join("\n"),
|
|
3557
3653
|
provenance: Provenance2.OBSERVED
|
|
3558
3654
|
};
|
|
@@ -4099,7 +4195,7 @@ function usage() {
|
|
|
4099
4195
|
console.log("query commands (mirror the MCP tools, ADR-050):");
|
|
4100
4196
|
console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
|
|
4101
4197
|
console.log(` example: ${neat} root-cause service:<name>`);
|
|
4102
|
-
console.log(" blast-radius <node-id> BFS
|
|
4198
|
+
console.log(" blast-radius <node-id> BFS inbound \u2014 the dependents that break if this dies.");
|
|
4103
4199
|
console.log(` example: ${neat} blast-radius database:<host>`);
|
|
4104
4200
|
console.log(" dependencies <node-id> Transitive outbound dependencies.");
|
|
4105
4201
|
console.log(" Flags: --depth N (default 3, max 10)");
|
|
@@ -4826,11 +4922,18 @@ Pass --project <name> to choose:
|
|
|
4826
4922
|
${names}`
|
|
4827
4923
|
);
|
|
4828
4924
|
}
|
|
4829
|
-
function resolveDaemonUrl() {
|
|
4830
|
-
|
|
4925
|
+
async function resolveDaemonUrl(project) {
|
|
4926
|
+
const explicit = process.env.NEAT_API_URL ?? process.env.NEAT_CORE_URL;
|
|
4927
|
+
if (explicit) return explicit;
|
|
4928
|
+
if (project) {
|
|
4929
|
+
const daemon = await findDaemonByProject(project);
|
|
4930
|
+
if (daemon) return `http://localhost:${daemon.record.ports.rest}`;
|
|
4931
|
+
}
|
|
4932
|
+
return "http://localhost:8080";
|
|
4831
4933
|
}
|
|
4832
4934
|
async function runQueryVerb(cmd, parsed) {
|
|
4833
|
-
const
|
|
4935
|
+
const requestedProject = resolveProjectFlag(parsed);
|
|
4936
|
+
const baseUrl = await resolveDaemonUrl(requestedProject);
|
|
4834
4937
|
const client = createHttpClient(baseUrl, resolveAuthToken());
|
|
4835
4938
|
const positional = parsed.positional;
|
|
4836
4939
|
let makeWork;
|
|
@@ -4986,7 +5089,7 @@ async function runQueryVerb(cmd, parsed) {
|
|
|
4986
5089
|
const detail = err.responseBody.length > 0 ? err.responseBody : err.message;
|
|
4987
5090
|
console.error(`neat ${cmd}: ${detail.trim()}`);
|
|
4988
5091
|
} else if (err instanceof TransportError) {
|
|
4989
|
-
console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (
|
|
5092
|
+
console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (endpoint=${baseUrl})`);
|
|
4990
5093
|
} else {
|
|
4991
5094
|
console.error(`neat ${cmd}: ${err.message}`);
|
|
4992
5095
|
}
|
|
@@ -5010,6 +5113,7 @@ export {
|
|
|
5010
5113
|
parseArgs,
|
|
5011
5114
|
printBanner,
|
|
5012
5115
|
readPackageVersion,
|
|
5116
|
+
resolveDaemonUrl,
|
|
5013
5117
|
resolveProjectForVerb,
|
|
5014
5118
|
runInit,
|
|
5015
5119
|
runQueryVerb,
|