@neat.is/core 0.4.16 → 0.4.18
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-6CO7C4IU.js → chunk-BGPWBRLU.js} +4 -2
- package/dist/{chunk-6CO7C4IU.js.map → chunk-BGPWBRLU.js.map} +1 -1
- package/dist/{chunk-VMLWUK7W.js → chunk-GXWBMJDJ.js} +210 -11
- package/dist/chunk-GXWBMJDJ.js.map +1 -0
- package/dist/{chunk-GHPHVXYM.js → chunk-MTXF77TN.js} +2 -2
- package/dist/{chunk-XS4CGNRO.js → chunk-T3X6XJPU.js} +403 -72
- package/dist/chunk-T3X6XJPU.js.map +1 -0
- package/dist/cli.cjs +964 -364
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +12 -2
- package/dist/cli.d.ts +12 -2
- package/dist/cli.js +331 -86
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +559 -150
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -13
- package/dist/index.d.ts +51 -13
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +726 -193
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +161 -37
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-QAISVAY5.js → otel-grpc-I67ONCRM.js} +3 -3
- package/dist/server.cjs +271 -97
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/package.json +2 -2
- package/dist/chunk-VMLWUK7W.js.map +0 -1
- package/dist/chunk-XS4CGNRO.js.map +0 -1
- /package/dist/{chunk-GHPHVXYM.js.map → chunk-MTXF77TN.js.map} +0 -0
- /package/dist/{otel-grpc-QAISVAY5.js.map → otel-grpc-I67ONCRM.js.map} +0 -0
package/dist/neatd.cjs
CHANGED
|
@@ -57,9 +57,9 @@ function mountBearerAuth(app, opts) {
|
|
|
57
57
|
const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
|
|
58
58
|
const publicRead = opts.publicRead === true;
|
|
59
59
|
app.addHook("preHandler", (req2, reply, done) => {
|
|
60
|
-
const
|
|
60
|
+
const path45 = (req2.url.split("?")[0] ?? "").replace(/\/+$/, "");
|
|
61
61
|
for (const suffix of suffixes) {
|
|
62
|
-
if (
|
|
62
|
+
if (path45 === suffix || path45.endsWith(suffix)) {
|
|
63
63
|
done();
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
@@ -187,8 +187,8 @@ function reshapeGrpcRequest(req2) {
|
|
|
187
187
|
};
|
|
188
188
|
}
|
|
189
189
|
function resolveProtoRoot() {
|
|
190
|
-
const here =
|
|
191
|
-
return
|
|
190
|
+
const here = import_node_path39.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
191
|
+
return import_node_path39.default.resolve(here, "..", "proto");
|
|
192
192
|
}
|
|
193
193
|
function loadTraceService() {
|
|
194
194
|
const protoRoot = resolveProtoRoot();
|
|
@@ -256,13 +256,13 @@ async function startOtelGrpcReceiver(opts) {
|
|
|
256
256
|
})
|
|
257
257
|
};
|
|
258
258
|
}
|
|
259
|
-
var import_node_url,
|
|
259
|
+
var import_node_url, import_node_path39, import_node_crypto2, grpc, protoLoader;
|
|
260
260
|
var init_otel_grpc = __esm({
|
|
261
261
|
"src/otel-grpc.ts"() {
|
|
262
262
|
"use strict";
|
|
263
263
|
init_cjs_shims();
|
|
264
264
|
import_node_url = require("url");
|
|
265
|
-
|
|
265
|
+
import_node_path39 = __toESM(require("path"), 1);
|
|
266
266
|
import_node_crypto2 = require("crypto");
|
|
267
267
|
grpc = __toESM(require("@grpc/grpc-js"), 1);
|
|
268
268
|
protoLoader = __toESM(require("@grpc/proto-loader"), 1);
|
|
@@ -372,10 +372,10 @@ function parseOtlpRequest(body) {
|
|
|
372
372
|
return out;
|
|
373
373
|
}
|
|
374
374
|
function loadProtoRoot() {
|
|
375
|
-
const here =
|
|
376
|
-
const protoRoot =
|
|
375
|
+
const here = import_node_path40.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
376
|
+
const protoRoot = import_node_path40.default.resolve(here, "..", "proto");
|
|
377
377
|
const root = new import_protobufjs.default.Root();
|
|
378
|
-
root.resolvePath = (_origin, target) =>
|
|
378
|
+
root.resolvePath = (_origin, target) => import_node_path40.default.resolve(protoRoot, target);
|
|
379
379
|
root.loadSync(
|
|
380
380
|
"opentelemetry/proto/collector/trace/v1/trace_service.proto",
|
|
381
381
|
{ keepCase: true }
|
|
@@ -473,8 +473,10 @@ async function buildOtelReceiver(opts) {
|
|
|
473
473
|
for (const s of spans) projectQueue.push({ project, span: s });
|
|
474
474
|
projectDrainPromise = projectDrainPromise.then(() => drainProject());
|
|
475
475
|
};
|
|
476
|
+
const offersProjectRouting = opts.onProjectSpan !== void 0;
|
|
476
477
|
const legacyEndpointWarned = /* @__PURE__ */ new Set();
|
|
477
478
|
function warnLegacyEndpoint(serviceName) {
|
|
479
|
+
if (!offersProjectRouting) return;
|
|
478
480
|
if (legacyEndpointWarned.has(serviceName)) return;
|
|
479
481
|
legacyEndpointWarned.add(serviceName);
|
|
480
482
|
console.warn(
|
|
@@ -571,12 +573,12 @@ async function buildOtelReceiver(opts) {
|
|
|
571
573
|
};
|
|
572
574
|
return decorated;
|
|
573
575
|
}
|
|
574
|
-
var
|
|
576
|
+
var import_node_path40, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
|
|
575
577
|
var init_otel = __esm({
|
|
576
578
|
"src/otel.ts"() {
|
|
577
579
|
"use strict";
|
|
578
580
|
init_cjs_shims();
|
|
579
|
-
|
|
581
|
+
import_node_path40 = __toESM(require("path"), 1);
|
|
580
582
|
import_node_url2 = require("url");
|
|
581
583
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
582
584
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
@@ -592,14 +594,15 @@ var init_otel = __esm({
|
|
|
592
594
|
|
|
593
595
|
// src/neatd.ts
|
|
594
596
|
init_cjs_shims();
|
|
595
|
-
var
|
|
596
|
-
var
|
|
597
|
-
var
|
|
597
|
+
var import_node_fs27 = require("fs");
|
|
598
|
+
var import_node_path44 = __toESM(require("path"), 1);
|
|
599
|
+
var import_node_module2 = require("module");
|
|
598
600
|
|
|
599
601
|
// src/daemon.ts
|
|
600
602
|
init_cjs_shims();
|
|
601
603
|
var import_node_fs25 = require("fs");
|
|
602
|
-
var
|
|
604
|
+
var import_node_path42 = __toESM(require("path"), 1);
|
|
605
|
+
var import_node_module = require("module");
|
|
603
606
|
|
|
604
607
|
// src/graph.ts
|
|
605
608
|
init_cjs_shims();
|
|
@@ -1128,19 +1131,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
1128
1131
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
1129
1132
|
let best = { path: [start], edges: [] };
|
|
1130
1133
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
1131
|
-
function step(node,
|
|
1132
|
-
if (
|
|
1133
|
-
best = { path: [...
|
|
1134
|
+
function step(node, path45, edges) {
|
|
1135
|
+
if (path45.length > best.path.length) {
|
|
1136
|
+
best = { path: [...path45], edges: [...edges] };
|
|
1134
1137
|
}
|
|
1135
|
-
if (
|
|
1138
|
+
if (path45.length - 1 >= maxDepth) return;
|
|
1136
1139
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
1137
1140
|
for (const [srcId, edge] of incoming) {
|
|
1138
1141
|
if (visited.has(srcId)) continue;
|
|
1139
1142
|
visited.add(srcId);
|
|
1140
|
-
|
|
1143
|
+
path45.push(srcId);
|
|
1141
1144
|
edges.push(edge);
|
|
1142
|
-
step(srcId,
|
|
1143
|
-
|
|
1145
|
+
step(srcId, path45, edges);
|
|
1146
|
+
path45.pop();
|
|
1144
1147
|
edges.pop();
|
|
1145
1148
|
visited.delete(srcId);
|
|
1146
1149
|
}
|
|
@@ -1658,6 +1661,41 @@ var DEFAULT_STALE_THRESHOLDS = {
|
|
|
1658
1661
|
CONFIGURED_BY: DAY_MS,
|
|
1659
1662
|
RUNS_ON: DAY_MS
|
|
1660
1663
|
};
|
|
1664
|
+
var DEFAULT_INCIDENT_THRESHOLDS = {
|
|
1665
|
+
threshold: 5,
|
|
1666
|
+
windowMs: 6e4
|
|
1667
|
+
};
|
|
1668
|
+
function loadIncidentThresholdsFromEnv() {
|
|
1669
|
+
const raw = process.env.NEAT_INCIDENT_THRESHOLDS;
|
|
1670
|
+
if (!raw) return DEFAULT_INCIDENT_THRESHOLDS;
|
|
1671
|
+
try {
|
|
1672
|
+
const overrides = JSON.parse(raw);
|
|
1673
|
+
const merged = { ...DEFAULT_INCIDENT_THRESHOLDS };
|
|
1674
|
+
if (typeof overrides.threshold === "number" && Number.isFinite(overrides.threshold) && overrides.threshold >= 1) {
|
|
1675
|
+
merged.threshold = Math.floor(overrides.threshold);
|
|
1676
|
+
}
|
|
1677
|
+
if (typeof overrides.windowMs === "number" && Number.isFinite(overrides.windowMs) && overrides.windowMs >= 0) {
|
|
1678
|
+
merged.windowMs = overrides.windowMs;
|
|
1679
|
+
}
|
|
1680
|
+
return merged;
|
|
1681
|
+
} catch (err) {
|
|
1682
|
+
console.warn(
|
|
1683
|
+
`[neat] NEAT_INCIDENT_THRESHOLDS could not be parsed (${err.message}); using defaults`
|
|
1684
|
+
);
|
|
1685
|
+
return DEFAULT_INCIDENT_THRESHOLDS;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
function httpResponseStatus(span) {
|
|
1689
|
+
for (const key of ["http.response.status_code", "http.status_code"]) {
|
|
1690
|
+
const v = span.attributes[key];
|
|
1691
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
1692
|
+
if (typeof v === "string") {
|
|
1693
|
+
const n = Number(v);
|
|
1694
|
+
if (Number.isFinite(n)) return n;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
return void 0;
|
|
1698
|
+
}
|
|
1661
1699
|
function nowIso(ctx) {
|
|
1662
1700
|
return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
|
|
1663
1701
|
}
|
|
@@ -2071,6 +2109,71 @@ function makeErrorSpanWriter(errorsPath) {
|
|
|
2071
2109
|
await import_node_fs3.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
2072
2110
|
};
|
|
2073
2111
|
}
|
|
2112
|
+
async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp, statusCode, count, firstTimestamp) {
|
|
2113
|
+
const attrs = sanitizeAttributes(span.attributes);
|
|
2114
|
+
const first = firstTimestamp ?? timestamp;
|
|
2115
|
+
const peer = pickAddress(span);
|
|
2116
|
+
const message = count > 1 ? `${count} consecutive HTTP ${statusCode} responses` + (peer ? ` to ${peer}` : "") : `HTTP ${statusCode} response` + (peer ? ` from ${peer}` : "");
|
|
2117
|
+
const ev = {
|
|
2118
|
+
id: `${span.traceId}:${span.spanId}`,
|
|
2119
|
+
timestamp,
|
|
2120
|
+
service: span.service,
|
|
2121
|
+
traceId: span.traceId,
|
|
2122
|
+
spanId: span.spanId,
|
|
2123
|
+
errorType: "http-failure",
|
|
2124
|
+
errorMessage: message,
|
|
2125
|
+
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
2126
|
+
affectedNode,
|
|
2127
|
+
httpStatusCode: statusCode,
|
|
2128
|
+
incidentCount: count,
|
|
2129
|
+
firstTimestamp: first,
|
|
2130
|
+
lastTimestamp: timestamp
|
|
2131
|
+
};
|
|
2132
|
+
await appendErrorEvent(ctx, ev);
|
|
2133
|
+
}
|
|
2134
|
+
async function advance4xxBurst(ctx, span, affectedNode, ts, nowMs, status2) {
|
|
2135
|
+
const { threshold, windowMs } = loadIncidentThresholdsFromEnv();
|
|
2136
|
+
if (!ctx.burstState) ctx.burstState = /* @__PURE__ */ new Map();
|
|
2137
|
+
const peer = pickAddress(span) ?? span.spanId;
|
|
2138
|
+
const key = `${span.service}->${peer}`;
|
|
2139
|
+
const existing = ctx.burstState.get(key);
|
|
2140
|
+
let state;
|
|
2141
|
+
if (existing && nowMs - existing.lastMs <= windowMs) {
|
|
2142
|
+
existing.count += 1;
|
|
2143
|
+
existing.lastTs = ts;
|
|
2144
|
+
existing.lastMs = nowMs;
|
|
2145
|
+
existing.codes.set(status2, (existing.codes.get(status2) ?? 0) + 1);
|
|
2146
|
+
state = existing;
|
|
2147
|
+
} else {
|
|
2148
|
+
state = {
|
|
2149
|
+
count: 1,
|
|
2150
|
+
firstTs: ts,
|
|
2151
|
+
lastTs: ts,
|
|
2152
|
+
lastMs: nowMs,
|
|
2153
|
+
codes: /* @__PURE__ */ new Map([[status2, 1]])
|
|
2154
|
+
};
|
|
2155
|
+
ctx.burstState.set(key, state);
|
|
2156
|
+
}
|
|
2157
|
+
if (state.count < threshold) return;
|
|
2158
|
+
let dominant = status2;
|
|
2159
|
+
let max = 0;
|
|
2160
|
+
for (const [code, n] of state.codes) {
|
|
2161
|
+
if (n > max) {
|
|
2162
|
+
max = n;
|
|
2163
|
+
dominant = code;
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
await recordFailingResponseIncident(
|
|
2167
|
+
ctx,
|
|
2168
|
+
span,
|
|
2169
|
+
affectedNode,
|
|
2170
|
+
state.lastTs,
|
|
2171
|
+
dominant,
|
|
2172
|
+
state.count,
|
|
2173
|
+
state.firstTs
|
|
2174
|
+
);
|
|
2175
|
+
ctx.burstState.delete(key);
|
|
2176
|
+
}
|
|
2074
2177
|
async function handleSpan(ctx, span) {
|
|
2075
2178
|
const ts = span.startTimeIso ?? nowIso(ctx);
|
|
2076
2179
|
const nowMs = ctx.now ? ctx.now() : Date.now();
|
|
@@ -2169,6 +2272,14 @@ async function handleSpan(ctx, span) {
|
|
|
2169
2272
|
await appendErrorEvent(ctx, ev);
|
|
2170
2273
|
}
|
|
2171
2274
|
}
|
|
2275
|
+
if (span.statusCode !== 2) {
|
|
2276
|
+
const status2 = httpResponseStatus(span);
|
|
2277
|
+
if (status2 !== void 0 && status2 >= 500) {
|
|
2278
|
+
await recordFailingResponseIncident(ctx, span, sourceId, ts, status2, 1);
|
|
2279
|
+
} else if (status2 !== void 0 && status2 >= 400 && spanMintsObservedEdge(span.kind)) {
|
|
2280
|
+
await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status2);
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2172
2283
|
void affectedNode;
|
|
2173
2284
|
if (ctx.onPolicyTrigger) await ctx.onPolicyTrigger(ctx.graph);
|
|
2174
2285
|
}
|
|
@@ -4115,7 +4226,7 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
4115
4226
|
|
|
4116
4227
|
// src/extract/calls/index.ts
|
|
4117
4228
|
init_cjs_shims();
|
|
4118
|
-
var
|
|
4229
|
+
var import_types17 = require("@neat.is/types");
|
|
4119
4230
|
|
|
4120
4231
|
// src/extract/calls/http.ts
|
|
4121
4232
|
init_cjs_shims();
|
|
@@ -4468,15 +4579,75 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
4468
4579
|
return out;
|
|
4469
4580
|
}
|
|
4470
4581
|
|
|
4582
|
+
// src/extract/calls/supabase.ts
|
|
4583
|
+
init_cjs_shims();
|
|
4584
|
+
var import_node_path27 = __toESM(require("path"), 1);
|
|
4585
|
+
var import_types16 = require("@neat.is/types");
|
|
4586
|
+
var SUPABASE_JS_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/supabase-js['"`]/;
|
|
4587
|
+
var SUPABASE_SSR_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/ssr['"`]/;
|
|
4588
|
+
var SUPABASE_CLIENT_RE = /\b(createClient|createServerClient|createBrowserClient)\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
|
|
4589
|
+
function hostFromLiteral(literal) {
|
|
4590
|
+
if (!literal) return null;
|
|
4591
|
+
const m = /^https?:\/\/([^/:'"`\s]+)/.exec(literal.trim());
|
|
4592
|
+
if (!m) return null;
|
|
4593
|
+
const host = m[1];
|
|
4594
|
+
if (!/\.supabase\.(co|in)$/i.test(host)) return null;
|
|
4595
|
+
return host;
|
|
4596
|
+
}
|
|
4597
|
+
function readImports2(content) {
|
|
4598
|
+
return {
|
|
4599
|
+
hasSupabaseJs: SUPABASE_JS_IMPORT_RE.test(content),
|
|
4600
|
+
hasSupabaseSsr: SUPABASE_SSR_IMPORT_RE.test(content)
|
|
4601
|
+
};
|
|
4602
|
+
}
|
|
4603
|
+
function constructorMatchesImport(name, ctx) {
|
|
4604
|
+
if (name === "createClient") return ctx.hasSupabaseJs;
|
|
4605
|
+
return ctx.hasSupabaseSsr;
|
|
4606
|
+
}
|
|
4607
|
+
function supabaseEndpointsFromFile(file, serviceDir) {
|
|
4608
|
+
const ctx = readImports2(file.content);
|
|
4609
|
+
if (!ctx.hasSupabaseJs && !ctx.hasSupabaseSsr) return [];
|
|
4610
|
+
const out = [];
|
|
4611
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4612
|
+
SUPABASE_CLIENT_RE.lastIndex = 0;
|
|
4613
|
+
let m;
|
|
4614
|
+
while ((m = SUPABASE_CLIENT_RE.exec(file.content)) !== null) {
|
|
4615
|
+
const ctor = m[1];
|
|
4616
|
+
if (!constructorMatchesImport(ctor, ctx)) continue;
|
|
4617
|
+
const host = hostFromLiteral(m[2]);
|
|
4618
|
+
const name = host ?? "env";
|
|
4619
|
+
if (seen.has(name)) continue;
|
|
4620
|
+
seen.add(name);
|
|
4621
|
+
const line = lineOf(file.content, m[0]);
|
|
4622
|
+
out.push({
|
|
4623
|
+
infraId: (0, import_types16.infraId)("supabase", name),
|
|
4624
|
+
name,
|
|
4625
|
+
kind: "supabase",
|
|
4626
|
+
edgeType: "CALLS",
|
|
4627
|
+
// `createClient(...)` from @supabase/supabase-js (or createServerClient /
|
|
4628
|
+
// createBrowserClient from @supabase/ssr) with the import in scope — a
|
|
4629
|
+
// framework-aware recognizer matched the SDK shape. Verified-call-site
|
|
4630
|
+
// tier (ADR-066), the same grade aws.ts / grpc.ts emit at.
|
|
4631
|
+
confidenceKind: "verified-call-site",
|
|
4632
|
+
evidence: {
|
|
4633
|
+
file: import_node_path27.default.relative(serviceDir, file.path),
|
|
4634
|
+
line,
|
|
4635
|
+
snippet: snippet(file.content, line)
|
|
4636
|
+
}
|
|
4637
|
+
});
|
|
4638
|
+
}
|
|
4639
|
+
return out;
|
|
4640
|
+
}
|
|
4641
|
+
|
|
4471
4642
|
// src/extract/calls/index.ts
|
|
4472
4643
|
function edgeTypeFromEndpoint(ep) {
|
|
4473
4644
|
switch (ep.edgeType) {
|
|
4474
4645
|
case "PUBLISHES_TO":
|
|
4475
|
-
return
|
|
4646
|
+
return import_types17.EdgeType.PUBLISHES_TO;
|
|
4476
4647
|
case "CONSUMES_FROM":
|
|
4477
|
-
return
|
|
4648
|
+
return import_types17.EdgeType.CONSUMES_FROM;
|
|
4478
4649
|
default:
|
|
4479
|
-
return
|
|
4650
|
+
return import_types17.EdgeType.CALLS;
|
|
4480
4651
|
}
|
|
4481
4652
|
}
|
|
4482
4653
|
function isAwsKind(kind) {
|
|
@@ -4496,6 +4667,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4496
4667
|
endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
|
|
4497
4668
|
endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
|
|
4498
4669
|
endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
|
|
4670
|
+
endpoints.push(...supabaseEndpointsFromFile(maskedFile, service.dir));
|
|
4499
4671
|
}
|
|
4500
4672
|
if (endpoints.length === 0) continue;
|
|
4501
4673
|
const seenEdges = /* @__PURE__ */ new Set();
|
|
@@ -4503,7 +4675,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4503
4675
|
if (!graph.hasNode(ep.infraId)) {
|
|
4504
4676
|
const node = {
|
|
4505
4677
|
id: ep.infraId,
|
|
4506
|
-
type:
|
|
4678
|
+
type: import_types17.NodeType.InfraNode,
|
|
4507
4679
|
name: ep.name,
|
|
4508
4680
|
// #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
|
|
4509
4681
|
// aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
|
|
@@ -4515,7 +4687,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4515
4687
|
nodesAdded++;
|
|
4516
4688
|
}
|
|
4517
4689
|
const edgeType = edgeTypeFromEndpoint(ep);
|
|
4518
|
-
const confidence = (0,
|
|
4690
|
+
const confidence = (0, import_types17.confidenceForExtracted)(ep.confidenceKind);
|
|
4519
4691
|
const relFile = toPosix2(ep.evidence.file);
|
|
4520
4692
|
const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
|
|
4521
4693
|
graph,
|
|
@@ -4525,7 +4697,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4525
4697
|
);
|
|
4526
4698
|
nodesAdded += n;
|
|
4527
4699
|
edgesAdded += e;
|
|
4528
|
-
if (!(0,
|
|
4700
|
+
if (!(0, import_types17.passesExtractedFloor)(confidence)) {
|
|
4529
4701
|
noteExtractedDropped({
|
|
4530
4702
|
source: fileNodeId,
|
|
4531
4703
|
target: ep.infraId,
|
|
@@ -4545,7 +4717,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4545
4717
|
source: fileNodeId,
|
|
4546
4718
|
target: ep.infraId,
|
|
4547
4719
|
type: edgeType,
|
|
4548
|
-
provenance:
|
|
4720
|
+
provenance: import_types17.Provenance.EXTRACTED,
|
|
4549
4721
|
confidence,
|
|
4550
4722
|
evidence: ep.evidence
|
|
4551
4723
|
};
|
|
@@ -4570,16 +4742,16 @@ init_cjs_shims();
|
|
|
4570
4742
|
|
|
4571
4743
|
// src/extract/infra/docker-compose.ts
|
|
4572
4744
|
init_cjs_shims();
|
|
4573
|
-
var
|
|
4574
|
-
var
|
|
4745
|
+
var import_node_path28 = __toESM(require("path"), 1);
|
|
4746
|
+
var import_types19 = require("@neat.is/types");
|
|
4575
4747
|
|
|
4576
4748
|
// src/extract/infra/shared.ts
|
|
4577
4749
|
init_cjs_shims();
|
|
4578
|
-
var
|
|
4750
|
+
var import_types18 = require("@neat.is/types");
|
|
4579
4751
|
function makeInfraNode(kind, name, provider = "self", extras) {
|
|
4580
4752
|
return {
|
|
4581
|
-
id: (0,
|
|
4582
|
-
type:
|
|
4753
|
+
id: (0, import_types18.infraId)(kind, name),
|
|
4754
|
+
type: import_types18.NodeType.InfraNode,
|
|
4583
4755
|
name,
|
|
4584
4756
|
provider,
|
|
4585
4757
|
kind,
|
|
@@ -4608,7 +4780,7 @@ function dependsOnList(value) {
|
|
|
4608
4780
|
}
|
|
4609
4781
|
function serviceNameToServiceNode(name, services) {
|
|
4610
4782
|
for (const s of services) {
|
|
4611
|
-
if (s.node.name === name ||
|
|
4783
|
+
if (s.node.name === name || import_node_path28.default.basename(s.dir) === name) return s.node.id;
|
|
4612
4784
|
}
|
|
4613
4785
|
return null;
|
|
4614
4786
|
}
|
|
@@ -4617,7 +4789,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
4617
4789
|
let edgesAdded = 0;
|
|
4618
4790
|
let composePath = null;
|
|
4619
4791
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
4620
|
-
const abs =
|
|
4792
|
+
const abs = import_node_path28.default.join(scanPath, name);
|
|
4621
4793
|
if (await exists(abs)) {
|
|
4622
4794
|
composePath = abs;
|
|
4623
4795
|
break;
|
|
@@ -4630,13 +4802,13 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
4630
4802
|
} catch (err) {
|
|
4631
4803
|
recordExtractionError(
|
|
4632
4804
|
"infra docker-compose",
|
|
4633
|
-
|
|
4805
|
+
import_node_path28.default.relative(scanPath, composePath),
|
|
4634
4806
|
err
|
|
4635
4807
|
);
|
|
4636
4808
|
return { nodesAdded, edgesAdded };
|
|
4637
4809
|
}
|
|
4638
4810
|
if (!compose?.services) return { nodesAdded, edgesAdded };
|
|
4639
|
-
const evidenceFile =
|
|
4811
|
+
const evidenceFile = import_node_path28.default.relative(scanPath, composePath).split(import_node_path28.default.sep).join("/");
|
|
4640
4812
|
const composeNameToNodeId = /* @__PURE__ */ new Map();
|
|
4641
4813
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
4642
4814
|
const matchedServiceId = serviceNameToServiceNode(composeName, services);
|
|
@@ -4658,15 +4830,15 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
4658
4830
|
for (const dep of dependsOnList(svc.depends_on)) {
|
|
4659
4831
|
const targetId = composeNameToNodeId.get(dep);
|
|
4660
4832
|
if (!targetId) continue;
|
|
4661
|
-
const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId,
|
|
4833
|
+
const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types19.EdgeType.DEPENDS_ON);
|
|
4662
4834
|
if (graph.hasEdge(edgeId)) continue;
|
|
4663
4835
|
const edge = {
|
|
4664
4836
|
id: edgeId,
|
|
4665
4837
|
source: sourceId,
|
|
4666
4838
|
target: targetId,
|
|
4667
|
-
type:
|
|
4668
|
-
provenance:
|
|
4669
|
-
confidence: (0,
|
|
4839
|
+
type: import_types19.EdgeType.DEPENDS_ON,
|
|
4840
|
+
provenance: import_types19.Provenance.EXTRACTED,
|
|
4841
|
+
confidence: (0, import_types19.confidenceForExtracted)("structural"),
|
|
4670
4842
|
evidence: { file: evidenceFile }
|
|
4671
4843
|
};
|
|
4672
4844
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -4678,9 +4850,9 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
4678
4850
|
|
|
4679
4851
|
// src/extract/infra/dockerfile.ts
|
|
4680
4852
|
init_cjs_shims();
|
|
4681
|
-
var
|
|
4853
|
+
var import_node_path29 = __toESM(require("path"), 1);
|
|
4682
4854
|
var import_node_fs15 = require("fs");
|
|
4683
|
-
var
|
|
4855
|
+
var import_types20 = require("@neat.is/types");
|
|
4684
4856
|
function runtimeImage(content) {
|
|
4685
4857
|
const lines = content.split("\n");
|
|
4686
4858
|
let last = null;
|
|
@@ -4699,7 +4871,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4699
4871
|
let nodesAdded = 0;
|
|
4700
4872
|
let edgesAdded = 0;
|
|
4701
4873
|
for (const service of services) {
|
|
4702
|
-
const dockerfilePath =
|
|
4874
|
+
const dockerfilePath = import_node_path29.default.join(service.dir, "Dockerfile");
|
|
4703
4875
|
if (!await exists(dockerfilePath)) continue;
|
|
4704
4876
|
let content;
|
|
4705
4877
|
try {
|
|
@@ -4707,7 +4879,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4707
4879
|
} catch (err) {
|
|
4708
4880
|
recordExtractionError(
|
|
4709
4881
|
"infra dockerfile",
|
|
4710
|
-
|
|
4882
|
+
import_node_path29.default.relative(scanPath, dockerfilePath),
|
|
4711
4883
|
err
|
|
4712
4884
|
);
|
|
4713
4885
|
continue;
|
|
@@ -4719,7 +4891,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4719
4891
|
graph.addNode(node.id, node);
|
|
4720
4892
|
nodesAdded++;
|
|
4721
4893
|
}
|
|
4722
|
-
const relDockerfile = toPosix2(
|
|
4894
|
+
const relDockerfile = toPosix2(import_node_path29.default.relative(service.dir, dockerfilePath));
|
|
4723
4895
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
4724
4896
|
graph,
|
|
4725
4897
|
service.pkg.name,
|
|
@@ -4728,17 +4900,17 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4728
4900
|
);
|
|
4729
4901
|
nodesAdded += fn;
|
|
4730
4902
|
edgesAdded += fe;
|
|
4731
|
-
const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id,
|
|
4903
|
+
const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types20.EdgeType.RUNS_ON);
|
|
4732
4904
|
if (!graph.hasEdge(edgeId)) {
|
|
4733
4905
|
const edge = {
|
|
4734
4906
|
id: edgeId,
|
|
4735
4907
|
source: fileNodeId,
|
|
4736
4908
|
target: node.id,
|
|
4737
|
-
type:
|
|
4738
|
-
provenance:
|
|
4739
|
-
confidence: (0,
|
|
4909
|
+
type: import_types20.EdgeType.RUNS_ON,
|
|
4910
|
+
provenance: import_types20.Provenance.EXTRACTED,
|
|
4911
|
+
confidence: (0, import_types20.confidenceForExtracted)("structural"),
|
|
4740
4912
|
evidence: {
|
|
4741
|
-
file: toPosix2(
|
|
4913
|
+
file: toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath))
|
|
4742
4914
|
}
|
|
4743
4915
|
};
|
|
4744
4916
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -4751,7 +4923,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4751
4923
|
// src/extract/infra/terraform.ts
|
|
4752
4924
|
init_cjs_shims();
|
|
4753
4925
|
var import_node_fs16 = require("fs");
|
|
4754
|
-
var
|
|
4926
|
+
var import_node_path30 = __toESM(require("path"), 1);
|
|
4755
4927
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
4756
4928
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
4757
4929
|
if (depth > max) return [];
|
|
@@ -4760,11 +4932,11 @@ async function walkTfFiles(start, depth = 0, max = 5) {
|
|
|
4760
4932
|
for (const entry2 of entries) {
|
|
4761
4933
|
if (entry2.isDirectory()) {
|
|
4762
4934
|
if (IGNORED_DIRS.has(entry2.name) || entry2.name === ".terraform") continue;
|
|
4763
|
-
const child =
|
|
4935
|
+
const child = import_node_path30.default.join(start, entry2.name);
|
|
4764
4936
|
if (await isPythonVenvDir(child)) continue;
|
|
4765
4937
|
out.push(...await walkTfFiles(child, depth + 1, max));
|
|
4766
4938
|
} else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
|
|
4767
|
-
out.push(
|
|
4939
|
+
out.push(import_node_path30.default.join(start, entry2.name));
|
|
4768
4940
|
}
|
|
4769
4941
|
}
|
|
4770
4942
|
return out;
|
|
@@ -4792,7 +4964,7 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
4792
4964
|
// src/extract/infra/k8s.ts
|
|
4793
4965
|
init_cjs_shims();
|
|
4794
4966
|
var import_node_fs17 = require("fs");
|
|
4795
|
-
var
|
|
4967
|
+
var import_node_path31 = __toESM(require("path"), 1);
|
|
4796
4968
|
var import_yaml3 = require("yaml");
|
|
4797
4969
|
var K8S_KIND_TO_INFRA_KIND = {
|
|
4798
4970
|
Service: "k8s-service",
|
|
@@ -4810,11 +4982,11 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
|
4810
4982
|
for (const entry2 of entries) {
|
|
4811
4983
|
if (entry2.isDirectory()) {
|
|
4812
4984
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
4813
|
-
const child =
|
|
4985
|
+
const child = import_node_path31.default.join(start, entry2.name);
|
|
4814
4986
|
if (await isPythonVenvDir(child)) continue;
|
|
4815
4987
|
out.push(...await walkYamlFiles2(child, depth + 1, max));
|
|
4816
|
-
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
4817
|
-
out.push(
|
|
4988
|
+
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path31.default.extname(entry2.name))) {
|
|
4989
|
+
out.push(import_node_path31.default.join(start, entry2.name));
|
|
4818
4990
|
}
|
|
4819
4991
|
}
|
|
4820
4992
|
return out;
|
|
@@ -4858,17 +5030,17 @@ async function addInfra(graph, scanPath, services) {
|
|
|
4858
5030
|
}
|
|
4859
5031
|
|
|
4860
5032
|
// src/extract/index.ts
|
|
4861
|
-
var
|
|
5033
|
+
var import_node_path33 = __toESM(require("path"), 1);
|
|
4862
5034
|
|
|
4863
5035
|
// src/extract/retire.ts
|
|
4864
5036
|
init_cjs_shims();
|
|
4865
5037
|
var import_node_fs18 = require("fs");
|
|
4866
|
-
var
|
|
4867
|
-
var
|
|
5038
|
+
var import_node_path32 = __toESM(require("path"), 1);
|
|
5039
|
+
var import_types21 = require("@neat.is/types");
|
|
4868
5040
|
function dropOrphanedFileNodes(graph) {
|
|
4869
5041
|
const orphans = [];
|
|
4870
5042
|
graph.forEachNode((id, attrs) => {
|
|
4871
|
-
if (attrs.type !==
|
|
5043
|
+
if (attrs.type !== import_types21.NodeType.FileNode) return;
|
|
4872
5044
|
if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
|
|
4873
5045
|
orphans.push(id);
|
|
4874
5046
|
}
|
|
@@ -4881,14 +5053,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
4881
5053
|
const bases = [scanPath, ...serviceDirs];
|
|
4882
5054
|
graph.forEachEdge((id, attrs) => {
|
|
4883
5055
|
const edge = attrs;
|
|
4884
|
-
if (edge.provenance !==
|
|
5056
|
+
if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
|
|
4885
5057
|
const evidenceFile = edge.evidence?.file;
|
|
4886
5058
|
if (!evidenceFile) return;
|
|
4887
|
-
if (
|
|
5059
|
+
if (import_node_path32.default.isAbsolute(evidenceFile)) {
|
|
4888
5060
|
if (!(0, import_node_fs18.existsSync)(evidenceFile)) toDrop.push(id);
|
|
4889
5061
|
return;
|
|
4890
5062
|
}
|
|
4891
|
-
const found = bases.some((base) => (0, import_node_fs18.existsSync)(
|
|
5063
|
+
const found = bases.some((base) => (0, import_node_fs18.existsSync)(import_node_path32.default.join(base, evidenceFile)));
|
|
4892
5064
|
if (!found) toDrop.push(id);
|
|
4893
5065
|
});
|
|
4894
5066
|
for (const id of toDrop) graph.dropEdge(id);
|
|
@@ -4928,7 +5100,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
4928
5100
|
}
|
|
4929
5101
|
const droppedEntries = drainDroppedExtracted();
|
|
4930
5102
|
if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
|
|
4931
|
-
const rejectedPath =
|
|
5103
|
+
const rejectedPath = import_node_path33.default.join(import_node_path33.default.dirname(opts.errorsPath), "rejected.ndjson");
|
|
4932
5104
|
try {
|
|
4933
5105
|
await writeRejectedExtracted(droppedEntries, rejectedPath);
|
|
4934
5106
|
} catch (err) {
|
|
@@ -4963,8 +5135,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
4963
5135
|
// src/persist.ts
|
|
4964
5136
|
init_cjs_shims();
|
|
4965
5137
|
var import_node_fs19 = require("fs");
|
|
4966
|
-
var
|
|
4967
|
-
var
|
|
5138
|
+
var import_node_path34 = __toESM(require("path"), 1);
|
|
5139
|
+
var import_types22 = require("@neat.is/types");
|
|
4968
5140
|
var SCHEMA_VERSION = 4;
|
|
4969
5141
|
function migrateV1ToV2(payload) {
|
|
4970
5142
|
const nodes = payload.graph.nodes;
|
|
@@ -4986,12 +5158,12 @@ function migrateV2ToV3(payload) {
|
|
|
4986
5158
|
for (const edge of edges) {
|
|
4987
5159
|
const attrs = edge.attributes;
|
|
4988
5160
|
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
4989
|
-
attrs.provenance =
|
|
5161
|
+
attrs.provenance = import_types22.Provenance.OBSERVED;
|
|
4990
5162
|
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
4991
5163
|
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
4992
5164
|
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
4993
5165
|
if (type && source && target) {
|
|
4994
|
-
const newId = (0,
|
|
5166
|
+
const newId = (0, import_types22.observedEdgeId)(source, target, type);
|
|
4995
5167
|
attrs.id = newId;
|
|
4996
5168
|
if (edge.key) edge.key = newId;
|
|
4997
5169
|
}
|
|
@@ -5000,7 +5172,7 @@ function migrateV2ToV3(payload) {
|
|
|
5000
5172
|
return { ...payload, schemaVersion: 3 };
|
|
5001
5173
|
}
|
|
5002
5174
|
async function ensureDir(filePath) {
|
|
5003
|
-
await import_node_fs19.promises.mkdir(
|
|
5175
|
+
await import_node_fs19.promises.mkdir(import_node_path34.default.dirname(filePath), { recursive: true });
|
|
5004
5176
|
}
|
|
5005
5177
|
async function saveGraphToDisk(graph, outPath) {
|
|
5006
5178
|
await ensureDir(outPath);
|
|
@@ -5075,23 +5247,23 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
5075
5247
|
|
|
5076
5248
|
// src/projects.ts
|
|
5077
5249
|
init_cjs_shims();
|
|
5078
|
-
var
|
|
5250
|
+
var import_node_path35 = __toESM(require("path"), 1);
|
|
5079
5251
|
function pathsForProject(project, baseDir) {
|
|
5080
5252
|
if (project === DEFAULT_PROJECT) {
|
|
5081
5253
|
return {
|
|
5082
|
-
snapshotPath:
|
|
5083
|
-
errorsPath:
|
|
5084
|
-
staleEventsPath:
|
|
5085
|
-
embeddingsCachePath:
|
|
5086
|
-
policyViolationsPath:
|
|
5254
|
+
snapshotPath: import_node_path35.default.join(baseDir, "graph.json"),
|
|
5255
|
+
errorsPath: import_node_path35.default.join(baseDir, "errors.ndjson"),
|
|
5256
|
+
staleEventsPath: import_node_path35.default.join(baseDir, "stale-events.ndjson"),
|
|
5257
|
+
embeddingsCachePath: import_node_path35.default.join(baseDir, "embeddings.json"),
|
|
5258
|
+
policyViolationsPath: import_node_path35.default.join(baseDir, "policy-violations.ndjson")
|
|
5087
5259
|
};
|
|
5088
5260
|
}
|
|
5089
5261
|
return {
|
|
5090
|
-
snapshotPath:
|
|
5091
|
-
errorsPath:
|
|
5092
|
-
staleEventsPath:
|
|
5093
|
-
embeddingsCachePath:
|
|
5094
|
-
policyViolationsPath:
|
|
5262
|
+
snapshotPath: import_node_path35.default.join(baseDir, `${project}.json`),
|
|
5263
|
+
errorsPath: import_node_path35.default.join(baseDir, `errors.${project}.ndjson`),
|
|
5264
|
+
staleEventsPath: import_node_path35.default.join(baseDir, `stale-events.${project}.ndjson`),
|
|
5265
|
+
embeddingsCachePath: import_node_path35.default.join(baseDir, `embeddings.${project}.json`),
|
|
5266
|
+
policyViolationsPath: import_node_path35.default.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
5095
5267
|
};
|
|
5096
5268
|
}
|
|
5097
5269
|
var Projects = class {
|
|
@@ -5129,19 +5301,19 @@ var Projects = class {
|
|
|
5129
5301
|
init_cjs_shims();
|
|
5130
5302
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
5131
5303
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
5132
|
-
var
|
|
5304
|
+
var import_types25 = require("@neat.is/types");
|
|
5133
5305
|
|
|
5134
5306
|
// src/extend/index.ts
|
|
5135
5307
|
init_cjs_shims();
|
|
5136
5308
|
var import_node_fs21 = require("fs");
|
|
5137
|
-
var
|
|
5309
|
+
var import_node_path37 = __toESM(require("path"), 1);
|
|
5138
5310
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
5139
5311
|
var import_instrumentation_registry = require("@neat.is/instrumentation-registry");
|
|
5140
5312
|
|
|
5141
5313
|
// src/installers/package-manager.ts
|
|
5142
5314
|
init_cjs_shims();
|
|
5143
5315
|
var import_node_fs20 = require("fs");
|
|
5144
|
-
var
|
|
5316
|
+
var import_node_path36 = __toESM(require("path"), 1);
|
|
5145
5317
|
var import_node_child_process = require("child_process");
|
|
5146
5318
|
var LOCKFILE_PRIORITY = [
|
|
5147
5319
|
{ lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
|
|
@@ -5163,22 +5335,22 @@ async function exists2(p) {
|
|
|
5163
5335
|
}
|
|
5164
5336
|
}
|
|
5165
5337
|
async function detectPackageManager(serviceDir) {
|
|
5166
|
-
let dir =
|
|
5338
|
+
let dir = import_node_path36.default.resolve(serviceDir);
|
|
5167
5339
|
const stops = /* @__PURE__ */ new Set();
|
|
5168
5340
|
for (let i = 0; i < 64; i++) {
|
|
5169
5341
|
if (stops.has(dir)) break;
|
|
5170
5342
|
stops.add(dir);
|
|
5171
5343
|
for (const candidate of LOCKFILE_PRIORITY) {
|
|
5172
|
-
const lockPath =
|
|
5344
|
+
const lockPath = import_node_path36.default.join(dir, candidate.lockfile);
|
|
5173
5345
|
if (await exists2(lockPath)) {
|
|
5174
5346
|
return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
|
|
5175
5347
|
}
|
|
5176
5348
|
}
|
|
5177
|
-
const parent =
|
|
5349
|
+
const parent = import_node_path36.default.dirname(dir);
|
|
5178
5350
|
if (parent === dir) break;
|
|
5179
5351
|
dir = parent;
|
|
5180
5352
|
}
|
|
5181
|
-
return { pm: "npm", cwd:
|
|
5353
|
+
return { pm: "npm", cwd: import_node_path36.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
|
|
5182
5354
|
}
|
|
5183
5355
|
async function runPackageManagerInstall(cmd) {
|
|
5184
5356
|
return new Promise((resolve) => {
|
|
@@ -5227,7 +5399,7 @@ async function fileExists2(p) {
|
|
|
5227
5399
|
}
|
|
5228
5400
|
}
|
|
5229
5401
|
async function readPackageJson(scanPath) {
|
|
5230
|
-
const pkgPath =
|
|
5402
|
+
const pkgPath = import_node_path37.default.join(scanPath, "package.json");
|
|
5231
5403
|
const raw = await import_node_fs21.promises.readFile(pkgPath, "utf8");
|
|
5232
5404
|
return JSON.parse(raw);
|
|
5233
5405
|
}
|
|
@@ -5238,11 +5410,11 @@ async function findHookFiles(scanPath) {
|
|
|
5238
5410
|
).sort();
|
|
5239
5411
|
}
|
|
5240
5412
|
function extendLogPath() {
|
|
5241
|
-
return process.env.NEAT_EXTEND_LOG ??
|
|
5413
|
+
return process.env.NEAT_EXTEND_LOG ?? import_node_path37.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
|
|
5242
5414
|
}
|
|
5243
5415
|
async function appendExtendLog(entry2) {
|
|
5244
5416
|
const logPath = extendLogPath();
|
|
5245
|
-
await import_node_fs21.promises.mkdir(
|
|
5417
|
+
await import_node_fs21.promises.mkdir(import_node_path37.default.dirname(logPath), { recursive: true });
|
|
5246
5418
|
await import_node_fs21.promises.appendFile(logPath, JSON.stringify(entry2) + "\n", "utf8");
|
|
5247
5419
|
}
|
|
5248
5420
|
function splicedContent(fileContent, snippet2) {
|
|
@@ -5301,7 +5473,7 @@ function lookupInstrumentation(library, installedVersion) {
|
|
|
5301
5473
|
}
|
|
5302
5474
|
async function describeProjectInstrumentation(ctx) {
|
|
5303
5475
|
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
5304
|
-
const envNeat = await fileExists2(
|
|
5476
|
+
const envNeat = await fileExists2(import_node_path37.default.join(ctx.scanPath, ".env.neat"));
|
|
5305
5477
|
const registryInstrPackages = new Set(
|
|
5306
5478
|
(0, import_instrumentation_registry.list)().map((e) => e.instrumentation_package).filter((p) => !!p)
|
|
5307
5479
|
);
|
|
@@ -5323,16 +5495,16 @@ async function applyExtension(ctx, args, options) {
|
|
|
5323
5495
|
);
|
|
5324
5496
|
}
|
|
5325
5497
|
for (const file of hookFiles) {
|
|
5326
|
-
const content = await import_node_fs21.promises.readFile(
|
|
5498
|
+
const content = await import_node_fs21.promises.readFile(import_node_path37.default.join(ctx.scanPath, file), "utf8");
|
|
5327
5499
|
if (content.includes(args.registration_snippet)) {
|
|
5328
5500
|
return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
|
|
5329
5501
|
}
|
|
5330
5502
|
}
|
|
5331
5503
|
const primaryFile = hookFiles[0];
|
|
5332
|
-
const primaryPath =
|
|
5504
|
+
const primaryPath = import_node_path37.default.join(ctx.scanPath, primaryFile);
|
|
5333
5505
|
const filesTouched = [];
|
|
5334
5506
|
const depsAdded = [];
|
|
5335
|
-
const pkgPath =
|
|
5507
|
+
const pkgPath = import_node_path37.default.join(ctx.scanPath, "package.json");
|
|
5336
5508
|
const pkg = await readPackageJson(ctx.scanPath);
|
|
5337
5509
|
if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
|
|
5338
5510
|
pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
|
|
@@ -5378,7 +5550,7 @@ async function dryRunExtension(ctx, args) {
|
|
|
5378
5550
|
};
|
|
5379
5551
|
}
|
|
5380
5552
|
for (const file of hookFiles) {
|
|
5381
|
-
const content = await import_node_fs21.promises.readFile(
|
|
5553
|
+
const content = await import_node_fs21.promises.readFile(import_node_path37.default.join(ctx.scanPath, file), "utf8");
|
|
5382
5554
|
if (content.includes(args.registration_snippet)) {
|
|
5383
5555
|
return {
|
|
5384
5556
|
library: args.library,
|
|
@@ -5400,7 +5572,7 @@ async function dryRunExtension(ctx, args) {
|
|
|
5400
5572
|
depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
|
|
5401
5573
|
filesTouched.push("package.json");
|
|
5402
5574
|
}
|
|
5403
|
-
const hookContent = await import_node_fs21.promises.readFile(
|
|
5575
|
+
const hookContent = await import_node_fs21.promises.readFile(import_node_path37.default.join(ctx.scanPath, primaryFile), "utf8");
|
|
5404
5576
|
const patched = splicedContent(hookContent, args.registration_snippet);
|
|
5405
5577
|
if (patched) {
|
|
5406
5578
|
filesTouched.push(primaryFile);
|
|
@@ -5421,7 +5593,7 @@ async function rollbackExtension(ctx, args) {
|
|
|
5421
5593
|
if (!match) {
|
|
5422
5594
|
return { undone: false, message: "no apply found for library" };
|
|
5423
5595
|
}
|
|
5424
|
-
const pkgPath =
|
|
5596
|
+
const pkgPath = import_node_path37.default.join(ctx.scanPath, "package.json");
|
|
5425
5597
|
if (await fileExists2(pkgPath)) {
|
|
5426
5598
|
const pkg = await readPackageJson(ctx.scanPath);
|
|
5427
5599
|
if (pkg.dependencies?.[match.instrumentation_package]) {
|
|
@@ -5432,7 +5604,7 @@ async function rollbackExtension(ctx, args) {
|
|
|
5432
5604
|
}
|
|
5433
5605
|
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
5434
5606
|
for (const file of hookFiles) {
|
|
5435
|
-
const filePath =
|
|
5607
|
+
const filePath = import_node_path37.default.join(ctx.scanPath, file);
|
|
5436
5608
|
const content = await import_node_fs21.promises.readFile(filePath, "utf8");
|
|
5437
5609
|
if (content.includes(match.registration_snippet)) {
|
|
5438
5610
|
const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
|
|
@@ -5448,7 +5620,7 @@ async function rollbackExtension(ctx, args) {
|
|
|
5448
5620
|
|
|
5449
5621
|
// src/divergences.ts
|
|
5450
5622
|
init_cjs_shims();
|
|
5451
|
-
var
|
|
5623
|
+
var import_types23 = require("@neat.is/types");
|
|
5452
5624
|
function bucketKey(source, target, type) {
|
|
5453
5625
|
return `${type}|${source}|${target}`;
|
|
5454
5626
|
}
|
|
@@ -5456,22 +5628,22 @@ function bucketEdges(graph) {
|
|
|
5456
5628
|
const buckets = /* @__PURE__ */ new Map();
|
|
5457
5629
|
graph.forEachEdge((id, attrs) => {
|
|
5458
5630
|
const e = attrs;
|
|
5459
|
-
const parsed = (0,
|
|
5631
|
+
const parsed = (0, import_types23.parseEdgeId)(id);
|
|
5460
5632
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
5461
5633
|
const key = bucketKey(e.source, e.target, e.type);
|
|
5462
5634
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
5463
5635
|
switch (provenance) {
|
|
5464
|
-
case
|
|
5636
|
+
case import_types23.Provenance.EXTRACTED:
|
|
5465
5637
|
cur.extracted = e;
|
|
5466
5638
|
break;
|
|
5467
|
-
case
|
|
5639
|
+
case import_types23.Provenance.OBSERVED:
|
|
5468
5640
|
cur.observed = e;
|
|
5469
5641
|
break;
|
|
5470
|
-
case
|
|
5642
|
+
case import_types23.Provenance.INFERRED:
|
|
5471
5643
|
cur.inferred = e;
|
|
5472
5644
|
break;
|
|
5473
5645
|
default:
|
|
5474
|
-
if (e.provenance ===
|
|
5646
|
+
if (e.provenance === import_types23.Provenance.STALE) cur.stale = e;
|
|
5475
5647
|
}
|
|
5476
5648
|
buckets.set(key, cur);
|
|
5477
5649
|
});
|
|
@@ -5480,7 +5652,7 @@ function bucketEdges(graph) {
|
|
|
5480
5652
|
function nodeIsFrontier(graph, nodeId) {
|
|
5481
5653
|
if (!graph.hasNode(nodeId)) return false;
|
|
5482
5654
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
5483
|
-
return attrs.type ===
|
|
5655
|
+
return attrs.type === import_types23.NodeType.FrontierNode;
|
|
5484
5656
|
}
|
|
5485
5657
|
function clampConfidence(n) {
|
|
5486
5658
|
if (!Number.isFinite(n)) return 0;
|
|
@@ -5501,7 +5673,7 @@ function gradedConfidence(edge) {
|
|
|
5501
5673
|
}
|
|
5502
5674
|
function detectMissingDivergences(graph, bucket) {
|
|
5503
5675
|
const out = [];
|
|
5504
|
-
if (bucket.type ===
|
|
5676
|
+
if (bucket.type === import_types23.EdgeType.CONTAINS) return out;
|
|
5505
5677
|
if (bucket.extracted && !bucket.observed) {
|
|
5506
5678
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
5507
5679
|
out.push({
|
|
@@ -5542,7 +5714,7 @@ function declaredHostFor(svc) {
|
|
|
5542
5714
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
5543
5715
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5544
5716
|
const e = graph.getEdgeAttributes(edgeId);
|
|
5545
|
-
if (e.type ===
|
|
5717
|
+
if (e.type === import_types23.EdgeType.CONFIGURED_BY && e.provenance === import_types23.Provenance.EXTRACTED) {
|
|
5546
5718
|
return true;
|
|
5547
5719
|
}
|
|
5548
5720
|
}
|
|
@@ -5555,10 +5727,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
5555
5727
|
const out = [];
|
|
5556
5728
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5557
5729
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
5558
|
-
if (edge.type !==
|
|
5559
|
-
if (edge.provenance !==
|
|
5730
|
+
if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
|
|
5731
|
+
if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
|
|
5560
5732
|
const target = graph.getNodeAttributes(edge.target);
|
|
5561
|
-
if (target.type !==
|
|
5733
|
+
if (target.type !== import_types23.NodeType.DatabaseNode) continue;
|
|
5562
5734
|
const observedHost = target.host?.trim();
|
|
5563
5735
|
if (!observedHost) continue;
|
|
5564
5736
|
if (observedHost === declaredHost) continue;
|
|
@@ -5580,10 +5752,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
5580
5752
|
const deps = svc.dependencies ?? {};
|
|
5581
5753
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5582
5754
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
5583
|
-
if (edge.type !==
|
|
5584
|
-
if (edge.provenance !==
|
|
5755
|
+
if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
|
|
5756
|
+
if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
|
|
5585
5757
|
const target = graph.getNodeAttributes(edge.target);
|
|
5586
|
-
if (target.type !==
|
|
5758
|
+
if (target.type !== import_types23.NodeType.DatabaseNode) continue;
|
|
5587
5759
|
for (const pair of compatPairs()) {
|
|
5588
5760
|
if (pair.engine !== target.engine) continue;
|
|
5589
5761
|
const declared = deps[pair.driver];
|
|
@@ -5644,7 +5816,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
5644
5816
|
}
|
|
5645
5817
|
graph.forEachNode((nodeId, attrs) => {
|
|
5646
5818
|
const n = attrs;
|
|
5647
|
-
if (n.type !==
|
|
5819
|
+
if (n.type !== import_types23.NodeType.ServiceNode) return;
|
|
5648
5820
|
const svc = n;
|
|
5649
5821
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
5650
5822
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
@@ -5677,7 +5849,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
5677
5849
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
5678
5850
|
return a.target.localeCompare(b.target);
|
|
5679
5851
|
});
|
|
5680
|
-
return
|
|
5852
|
+
return import_types23.DivergenceResultSchema.parse({
|
|
5681
5853
|
divergences: filtered,
|
|
5682
5854
|
totalAffected: filtered.length,
|
|
5683
5855
|
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -5765,23 +5937,23 @@ function canonicalJson(value) {
|
|
|
5765
5937
|
init_cjs_shims();
|
|
5766
5938
|
var import_node_fs23 = require("fs");
|
|
5767
5939
|
var import_node_os3 = __toESM(require("os"), 1);
|
|
5768
|
-
var
|
|
5769
|
-
var
|
|
5940
|
+
var import_node_path38 = __toESM(require("path"), 1);
|
|
5941
|
+
var import_types24 = require("@neat.is/types");
|
|
5770
5942
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
5771
5943
|
var LOCK_RETRY_MS = 50;
|
|
5772
5944
|
function neatHome() {
|
|
5773
5945
|
const override = process.env.NEAT_HOME;
|
|
5774
|
-
if (override && override.length > 0) return
|
|
5775
|
-
return
|
|
5946
|
+
if (override && override.length > 0) return import_node_path38.default.resolve(override);
|
|
5947
|
+
return import_node_path38.default.join(import_node_os3.default.homedir(), ".neat");
|
|
5776
5948
|
}
|
|
5777
5949
|
function registryPath() {
|
|
5778
|
-
return
|
|
5950
|
+
return import_node_path38.default.join(neatHome(), "projects.json");
|
|
5779
5951
|
}
|
|
5780
5952
|
function registryLockPath() {
|
|
5781
|
-
return
|
|
5953
|
+
return import_node_path38.default.join(neatHome(), "projects.json.lock");
|
|
5782
5954
|
}
|
|
5783
5955
|
function daemonPidPath() {
|
|
5784
|
-
return
|
|
5956
|
+
return import_node_path38.default.join(neatHome(), "neatd.pid");
|
|
5785
5957
|
}
|
|
5786
5958
|
function isPidAliveDefault(pid) {
|
|
5787
5959
|
try {
|
|
@@ -5841,7 +6013,7 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
|
|
|
5841
6013
|
}
|
|
5842
6014
|
}
|
|
5843
6015
|
async function writeAtomically(target, contents) {
|
|
5844
|
-
await import_node_fs23.promises.mkdir(
|
|
6016
|
+
await import_node_fs23.promises.mkdir(import_node_path38.default.dirname(target), { recursive: true });
|
|
5845
6017
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
5846
6018
|
const fd = await import_node_fs23.promises.open(tmp, "w");
|
|
5847
6019
|
try {
|
|
@@ -5854,7 +6026,7 @@ async function writeAtomically(target, contents) {
|
|
|
5854
6026
|
}
|
|
5855
6027
|
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
|
|
5856
6028
|
const deadline = Date.now() + timeoutMs;
|
|
5857
|
-
await import_node_fs23.promises.mkdir(
|
|
6029
|
+
await import_node_fs23.promises.mkdir(import_node_path38.default.dirname(lockPath), { recursive: true });
|
|
5858
6030
|
let probedHolder = false;
|
|
5859
6031
|
while (true) {
|
|
5860
6032
|
try {
|
|
@@ -5907,10 +6079,10 @@ async function readRegistry() {
|
|
|
5907
6079
|
throw err;
|
|
5908
6080
|
}
|
|
5909
6081
|
const parsed = JSON.parse(raw);
|
|
5910
|
-
return
|
|
6082
|
+
return import_types24.RegistryFileSchema.parse(parsed);
|
|
5911
6083
|
}
|
|
5912
6084
|
async function writeRegistry(reg) {
|
|
5913
|
-
const validated =
|
|
6085
|
+
const validated = import_types24.RegistryFileSchema.parse(reg);
|
|
5914
6086
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
5915
6087
|
}
|
|
5916
6088
|
async function getProject(name) {
|
|
@@ -5940,6 +6112,58 @@ async function touchLastSeen(name, at = (/* @__PURE__ */ new Date()).toISOString
|
|
|
5940
6112
|
await writeRegistry(reg);
|
|
5941
6113
|
});
|
|
5942
6114
|
}
|
|
6115
|
+
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
6116
|
+
var DEFAULT_PRUNE_TTL_MS = 7 * DAY_MS2;
|
|
6117
|
+
function pruneTtlMs() {
|
|
6118
|
+
const raw = process.env.NEAT_REGISTRY_PRUNE_TTL_MS;
|
|
6119
|
+
if (!raw) return DEFAULT_PRUNE_TTL_MS;
|
|
6120
|
+
const n = Number.parseInt(raw, 10);
|
|
6121
|
+
if (Number.isFinite(n) && n >= 0) return n;
|
|
6122
|
+
console.warn(
|
|
6123
|
+
`[neat] NEAT_REGISTRY_PRUNE_TTL_MS could not be parsed (${raw}); using default ${DEFAULT_PRUNE_TTL_MS}ms`
|
|
6124
|
+
);
|
|
6125
|
+
return DEFAULT_PRUNE_TTL_MS;
|
|
6126
|
+
}
|
|
6127
|
+
async function statPathStatus(p) {
|
|
6128
|
+
try {
|
|
6129
|
+
const stat = await import_node_fs23.promises.stat(p);
|
|
6130
|
+
return stat.isDirectory() ? "present" : "unknown";
|
|
6131
|
+
} catch (err) {
|
|
6132
|
+
return err.code === "ENOENT" ? "gone" : "unknown";
|
|
6133
|
+
}
|
|
6134
|
+
}
|
|
6135
|
+
async function pruneRegistry(opts = {}) {
|
|
6136
|
+
const ttlMs = opts.ttlMs ?? pruneTtlMs();
|
|
6137
|
+
const statPath = opts.statPath ?? statPathStatus;
|
|
6138
|
+
const now = opts.now ?? Date.now;
|
|
6139
|
+
return withLock(async () => {
|
|
6140
|
+
const reg = await readRegistry();
|
|
6141
|
+
const removed = [];
|
|
6142
|
+
const kept = [];
|
|
6143
|
+
for (const entry2 of reg.projects) {
|
|
6144
|
+
const status2 = await statPath(entry2.path);
|
|
6145
|
+
if (status2 !== "gone") {
|
|
6146
|
+
kept.push(entry2);
|
|
6147
|
+
continue;
|
|
6148
|
+
}
|
|
6149
|
+
if (ttlMs <= 0) {
|
|
6150
|
+
removed.push(entry2);
|
|
6151
|
+
continue;
|
|
6152
|
+
}
|
|
6153
|
+
const lastSeen = Date.parse(entry2.lastSeenAt ?? entry2.registeredAt);
|
|
6154
|
+
const age = now() - (Number.isFinite(lastSeen) ? lastSeen : 0);
|
|
6155
|
+
if (age > ttlMs) {
|
|
6156
|
+
removed.push(entry2);
|
|
6157
|
+
} else {
|
|
6158
|
+
kept.push(entry2);
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
6161
|
+
if (removed.length === 0) return [];
|
|
6162
|
+
reg.projects = kept;
|
|
6163
|
+
await writeRegistry(reg);
|
|
6164
|
+
return removed;
|
|
6165
|
+
});
|
|
6166
|
+
}
|
|
5943
6167
|
|
|
5944
6168
|
// src/streaming.ts
|
|
5945
6169
|
init_cjs_shims();
|
|
@@ -5953,6 +6177,7 @@ function handleSse(req2, reply, opts) {
|
|
|
5953
6177
|
reply.raw.setHeader("Connection", "keep-alive");
|
|
5954
6178
|
reply.raw.setHeader("X-Accel-Buffering", "no");
|
|
5955
6179
|
reply.raw.flushHeaders?.();
|
|
6180
|
+
reply.raw.write(":open\n\n");
|
|
5956
6181
|
let pending = 0;
|
|
5957
6182
|
let dropped = false;
|
|
5958
6183
|
const closeConnection = () => {
|
|
@@ -6132,11 +6357,11 @@ function registerRoutes(scope, ctx) {
|
|
|
6132
6357
|
const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6133
6358
|
const parsed = [];
|
|
6134
6359
|
for (const c of candidates) {
|
|
6135
|
-
const r =
|
|
6360
|
+
const r = import_types25.DivergenceTypeSchema.safeParse(c);
|
|
6136
6361
|
if (!r.success) {
|
|
6137
6362
|
return reply.code(400).send({
|
|
6138
6363
|
error: `unknown divergence type "${c}"`,
|
|
6139
|
-
allowed:
|
|
6364
|
+
allowed: import_types25.DivergenceTypeSchema.options
|
|
6140
6365
|
});
|
|
6141
6366
|
}
|
|
6142
6367
|
parsed.push(r.data);
|
|
@@ -6349,7 +6574,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6349
6574
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
6350
6575
|
let violations = await log.readAll();
|
|
6351
6576
|
if (req2.query.severity) {
|
|
6352
|
-
const sev =
|
|
6577
|
+
const sev = import_types25.PolicySeveritySchema.safeParse(req2.query.severity);
|
|
6353
6578
|
if (!sev.success) {
|
|
6354
6579
|
return reply.code(400).send({
|
|
6355
6580
|
error: "invalid severity",
|
|
@@ -6366,7 +6591,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6366
6591
|
scope.post("/policies/check", async (req2, reply) => {
|
|
6367
6592
|
const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
|
|
6368
6593
|
if (!proj) return;
|
|
6369
|
-
const parsed =
|
|
6594
|
+
const parsed = import_types25.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
|
|
6370
6595
|
if (!parsed.success) {
|
|
6371
6596
|
return reply.code(400).send({
|
|
6372
6597
|
error: "invalid /policies/check body",
|
|
@@ -6609,7 +6834,7 @@ init_auth();
|
|
|
6609
6834
|
// src/unrouted.ts
|
|
6610
6835
|
init_cjs_shims();
|
|
6611
6836
|
var import_node_fs24 = require("fs");
|
|
6612
|
-
var
|
|
6837
|
+
var import_node_path41 = __toESM(require("path"), 1);
|
|
6613
6838
|
function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
|
|
6614
6839
|
return {
|
|
6615
6840
|
timestamp: now.toISOString(),
|
|
@@ -6619,15 +6844,68 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
|
|
|
6619
6844
|
};
|
|
6620
6845
|
}
|
|
6621
6846
|
async function appendUnroutedSpan(neatHome3, record) {
|
|
6622
|
-
const target =
|
|
6847
|
+
const target = import_node_path41.default.join(neatHome3, "errors.ndjson");
|
|
6623
6848
|
await import_node_fs24.promises.mkdir(neatHome3, { recursive: true });
|
|
6624
6849
|
await import_node_fs24.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
|
|
6625
6850
|
}
|
|
6626
6851
|
function unroutedErrorsPath(neatHome3) {
|
|
6627
|
-
return
|
|
6852
|
+
return import_node_path41.default.join(neatHome3, "errors.ndjson");
|
|
6628
6853
|
}
|
|
6629
6854
|
|
|
6630
6855
|
// src/daemon.ts
|
|
6856
|
+
function daemonJsonPath(scanPath) {
|
|
6857
|
+
return import_node_path42.default.join(scanPath, "neat-out", "daemon.json");
|
|
6858
|
+
}
|
|
6859
|
+
function daemonsDiscoveryDir(home) {
|
|
6860
|
+
const base = home && home.length > 0 ? home : neatHomeFromEnv();
|
|
6861
|
+
return import_node_path42.default.join(base, "daemons");
|
|
6862
|
+
}
|
|
6863
|
+
function daemonDiscoveryPath(project, home) {
|
|
6864
|
+
return import_node_path42.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
|
|
6865
|
+
}
|
|
6866
|
+
function sanitizeDiscoveryName(project) {
|
|
6867
|
+
return project.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
6868
|
+
}
|
|
6869
|
+
function neatHomeFromEnv() {
|
|
6870
|
+
const env = process.env.NEAT_HOME;
|
|
6871
|
+
if (env && env.length > 0) return import_node_path42.default.resolve(env);
|
|
6872
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
6873
|
+
return import_node_path42.default.join(home, ".neat");
|
|
6874
|
+
}
|
|
6875
|
+
function resolveNeatVersion() {
|
|
6876
|
+
if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
|
|
6877
|
+
return process.env.NEAT_LOCAL_VERSION;
|
|
6878
|
+
}
|
|
6879
|
+
try {
|
|
6880
|
+
const req2 = (0, import_node_module.createRequire)(importMetaUrl);
|
|
6881
|
+
const pkg = req2("../package.json");
|
|
6882
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
6883
|
+
} catch {
|
|
6884
|
+
return "0.0.0";
|
|
6885
|
+
}
|
|
6886
|
+
}
|
|
6887
|
+
async function writeDaemonRecord(record, home) {
|
|
6888
|
+
const body = JSON.stringify(record, null, 2) + "\n";
|
|
6889
|
+
await writeAtomically(daemonJsonPath(record.projectPath), body);
|
|
6890
|
+
try {
|
|
6891
|
+
await writeAtomically(daemonDiscoveryPath(record.project, home), body);
|
|
6892
|
+
} catch (err) {
|
|
6893
|
+
console.warn(
|
|
6894
|
+
`neatd: could not write discovery copy for "${record.project}" \u2014 ${err.message}`
|
|
6895
|
+
);
|
|
6896
|
+
}
|
|
6897
|
+
}
|
|
6898
|
+
async function clearDaemonRecord(record, home) {
|
|
6899
|
+
try {
|
|
6900
|
+
const stopped = { ...record, status: "stopped" };
|
|
6901
|
+
await writeAtomically(daemonJsonPath(record.projectPath), JSON.stringify(stopped, null, 2) + "\n");
|
|
6902
|
+
} catch {
|
|
6903
|
+
}
|
|
6904
|
+
try {
|
|
6905
|
+
await import_node_fs25.promises.unlink(daemonDiscoveryPath(record.project, home));
|
|
6906
|
+
} catch {
|
|
6907
|
+
}
|
|
6908
|
+
}
|
|
6631
6909
|
function teardownSlot(slot) {
|
|
6632
6910
|
try {
|
|
6633
6911
|
slot.stopPersist();
|
|
@@ -6639,11 +6917,11 @@ function teardownSlot(slot) {
|
|
|
6639
6917
|
}
|
|
6640
6918
|
}
|
|
6641
6919
|
function neatHomeFor(opts) {
|
|
6642
|
-
if (opts.neatHome && opts.neatHome.length > 0) return
|
|
6920
|
+
if (opts.neatHome && opts.neatHome.length > 0) return import_node_path42.default.resolve(opts.neatHome);
|
|
6643
6921
|
const env = process.env.NEAT_HOME;
|
|
6644
|
-
if (env && env.length > 0) return
|
|
6922
|
+
if (env && env.length > 0) return import_node_path42.default.resolve(env);
|
|
6645
6923
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
6646
|
-
return
|
|
6924
|
+
return import_node_path42.default.join(home, ".neat");
|
|
6647
6925
|
}
|
|
6648
6926
|
function routeSpanToProject(serviceName, projects) {
|
|
6649
6927
|
if (!serviceName) return DEFAULT_PROJECT;
|
|
@@ -6678,7 +6956,7 @@ function isTokenContained(needle, haystack) {
|
|
|
6678
6956
|
return tokens.includes(needle);
|
|
6679
6957
|
}
|
|
6680
6958
|
async function bootstrapProject(entry2) {
|
|
6681
|
-
const paths = pathsForProject(entry2.name,
|
|
6959
|
+
const paths = pathsForProject(entry2.name, import_node_path42.default.join(entry2.path, "neat-out"));
|
|
6682
6960
|
try {
|
|
6683
6961
|
const stat = await import_node_fs25.promises.stat(entry2.path);
|
|
6684
6962
|
if (!stat.isDirectory()) {
|
|
@@ -6744,6 +7022,23 @@ function resolveOtlpPort(opts) {
|
|
|
6744
7022
|
}
|
|
6745
7023
|
return 4318;
|
|
6746
7024
|
}
|
|
7025
|
+
function resolveWebPort() {
|
|
7026
|
+
const env = process.env.NEAT_WEB_PORT;
|
|
7027
|
+
if (env && env.length > 0) {
|
|
7028
|
+
const n = Number.parseInt(env, 10);
|
|
7029
|
+
if (Number.isFinite(n)) return n;
|
|
7030
|
+
}
|
|
7031
|
+
return 6328;
|
|
7032
|
+
}
|
|
7033
|
+
function portFromListenAddress(address, fallback) {
|
|
7034
|
+
try {
|
|
7035
|
+
const port = new URL(address).port;
|
|
7036
|
+
const n = Number.parseInt(port, 10);
|
|
7037
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
7038
|
+
} catch {
|
|
7039
|
+
}
|
|
7040
|
+
return fallback;
|
|
7041
|
+
}
|
|
6747
7042
|
function resolveHost(opts, authTokenSet) {
|
|
6748
7043
|
if (opts.host && opts.host.length > 0) return opts.host;
|
|
6749
7044
|
const env = process.env.HOST;
|
|
@@ -6754,14 +7049,25 @@ function resolveHost(opts, authTokenSet) {
|
|
|
6754
7049
|
async function startDaemon(opts = {}) {
|
|
6755
7050
|
const home = neatHomeFor(opts);
|
|
6756
7051
|
const regPath = registryPath();
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
7052
|
+
const projectArg = typeof opts.project === "string" && opts.project.length > 0 ? opts.project : process.env.NEAT_PROJECT && process.env.NEAT_PROJECT.length > 0 ? process.env.NEAT_PROJECT : null;
|
|
7053
|
+
const projectPathArg = opts.projectPath && opts.projectPath.length > 0 ? opts.projectPath : process.env.NEAT_PROJECT_PATH && process.env.NEAT_PROJECT_PATH.length > 0 ? process.env.NEAT_PROJECT_PATH : null;
|
|
7054
|
+
const singleProject = projectArg;
|
|
7055
|
+
const singleProjectPath = singleProject && projectPathArg ? import_node_path42.default.resolve(projectPathArg) : null;
|
|
7056
|
+
if (singleProject && !singleProjectPath) {
|
|
6760
7057
|
throw new Error(
|
|
6761
|
-
`neatd:
|
|
7058
|
+
`neatd: project "${singleProject}" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`
|
|
6762
7059
|
);
|
|
6763
7060
|
}
|
|
6764
|
-
|
|
7061
|
+
if (!singleProject) {
|
|
7062
|
+
try {
|
|
7063
|
+
await import_node_fs25.promises.access(regPath);
|
|
7064
|
+
} catch {
|
|
7065
|
+
throw new Error(
|
|
7066
|
+
`neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
|
|
7067
|
+
);
|
|
7068
|
+
}
|
|
7069
|
+
}
|
|
7070
|
+
const pidPath = import_node_path42.default.join(home, "neatd.pid");
|
|
6765
7071
|
await writeAtomically(pidPath, `${process.pid}
|
|
6766
7072
|
`);
|
|
6767
7073
|
const slots = /* @__PURE__ */ new Map();
|
|
@@ -6849,8 +7155,37 @@ async function startDaemon(opts = {}) {
|
|
|
6849
7155
|
});
|
|
6850
7156
|
}
|
|
6851
7157
|
}
|
|
7158
|
+
async function enumerateProjects() {
|
|
7159
|
+
if (singleProject && singleProjectPath) {
|
|
7160
|
+
return [
|
|
7161
|
+
{
|
|
7162
|
+
name: singleProject,
|
|
7163
|
+
path: singleProjectPath,
|
|
7164
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7165
|
+
languages: [],
|
|
7166
|
+
status: "active"
|
|
7167
|
+
}
|
|
7168
|
+
];
|
|
7169
|
+
}
|
|
7170
|
+
return listProjects();
|
|
7171
|
+
}
|
|
6852
7172
|
async function loadAll() {
|
|
6853
|
-
|
|
7173
|
+
if (!singleProject) {
|
|
7174
|
+
try {
|
|
7175
|
+
const pruned = await pruneRegistry();
|
|
7176
|
+
for (const entry2 of pruned) {
|
|
7177
|
+
console.log(
|
|
7178
|
+
`neatd: pruned project "${entry2.name}" \u2014 registered path ${entry2.path} is gone`
|
|
7179
|
+
);
|
|
7180
|
+
slots.delete(entry2.name);
|
|
7181
|
+
bootstrapStatus.delete(entry2.name);
|
|
7182
|
+
bootstrapStartedAt.delete(entry2.name);
|
|
7183
|
+
}
|
|
7184
|
+
} catch (err) {
|
|
7185
|
+
console.warn(`neatd: registry prune skipped \u2014 ${err.message}`);
|
|
7186
|
+
}
|
|
7187
|
+
}
|
|
7188
|
+
const projects = await enumerateProjects();
|
|
6854
7189
|
const seen = /* @__PURE__ */ new Set();
|
|
6855
7190
|
const pending = [];
|
|
6856
7191
|
for (const entry2 of projects) {
|
|
@@ -6875,7 +7210,7 @@ async function startDaemon(opts = {}) {
|
|
|
6875
7210
|
}
|
|
6876
7211
|
await Promise.allSettled(pending);
|
|
6877
7212
|
}
|
|
6878
|
-
const initialEntries = await
|
|
7213
|
+
const initialEntries = await enumerateProjects().catch(() => []);
|
|
6879
7214
|
for (const entry2 of initialEntries) {
|
|
6880
7215
|
bootstrapStatus.set(entry2.name, "bootstrapping");
|
|
6881
7216
|
bootstrapStartedAt.set(entry2.name, Date.now());
|
|
@@ -6885,6 +7220,7 @@ async function startDaemon(opts = {}) {
|
|
|
6885
7220
|
let otlpApp = null;
|
|
6886
7221
|
let restAddress = "";
|
|
6887
7222
|
let otlpAddress = "";
|
|
7223
|
+
let daemonRecord = null;
|
|
6888
7224
|
if (bind) {
|
|
6889
7225
|
const auth = readAuthEnv();
|
|
6890
7226
|
const host = resolveHost(opts, Boolean(auth.authToken));
|
|
@@ -6909,6 +7245,20 @@ async function startDaemon(opts = {}) {
|
|
|
6909
7245
|
}
|
|
6910
7246
|
}
|
|
6911
7247
|
});
|
|
7248
|
+
if (singleProject) {
|
|
7249
|
+
restApp.addHook("onSend", async (req2, _reply, payload) => {
|
|
7250
|
+
if (req2.url.split("?")[0] !== "/health") return payload;
|
|
7251
|
+
if (typeof payload !== "string") return payload;
|
|
7252
|
+
try {
|
|
7253
|
+
const body = JSON.parse(payload);
|
|
7254
|
+
if (typeof body !== "object" || body === null) return payload;
|
|
7255
|
+
body.project = singleProject;
|
|
7256
|
+
return JSON.stringify(body);
|
|
7257
|
+
} catch {
|
|
7258
|
+
return payload;
|
|
7259
|
+
}
|
|
7260
|
+
});
|
|
7261
|
+
}
|
|
6912
7262
|
restAddress = await restApp.listen({ port: restPort, host });
|
|
6913
7263
|
console.log(`neatd: REST listening on ${restAddress}`);
|
|
6914
7264
|
} catch (err) {
|
|
@@ -6924,6 +7274,25 @@ async function startDaemon(opts = {}) {
|
|
|
6924
7274
|
);
|
|
6925
7275
|
}
|
|
6926
7276
|
async function resolveTargetSlot(serviceName, traceId) {
|
|
7277
|
+
if (singleProject) {
|
|
7278
|
+
let slot2 = slots.get(singleProject);
|
|
7279
|
+
if (!slot2) {
|
|
7280
|
+
slot2 = await tryRecoverSlot({
|
|
7281
|
+
name: singleProject,
|
|
7282
|
+
path: singleProjectPath,
|
|
7283
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7284
|
+
languages: [],
|
|
7285
|
+
status: "active"
|
|
7286
|
+
});
|
|
7287
|
+
} else if (slot2.status === "broken") {
|
|
7288
|
+
slot2 = await tryRecoverSlot(slot2.entry);
|
|
7289
|
+
}
|
|
7290
|
+
if (!slot2 || slot2.status !== "active") {
|
|
7291
|
+
warnDroppedSpan(singleProject, slot2?.errorReason ?? "unknown");
|
|
7292
|
+
return null;
|
|
7293
|
+
}
|
|
7294
|
+
return slot2;
|
|
7295
|
+
}
|
|
6927
7296
|
const liveEntries = await listProjects().catch(() => []);
|
|
6928
7297
|
const target = routeSpanToProject(serviceName, liveEntries);
|
|
6929
7298
|
let slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
|
|
@@ -7026,6 +7395,42 @@ async function startDaemon(opts = {}) {
|
|
|
7026
7395
|
`neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
|
|
7027
7396
|
);
|
|
7028
7397
|
}
|
|
7398
|
+
if (singleProject && singleProjectPath) {
|
|
7399
|
+
const ports = {
|
|
7400
|
+
rest: portFromListenAddress(restAddress, restPort),
|
|
7401
|
+
otlp: portFromListenAddress(otlpAddress, otlpPort),
|
|
7402
|
+
// The daemon doesn't bind the web port itself (neatd spawns the web
|
|
7403
|
+
// child); it records the allocated value passed through so the
|
|
7404
|
+
// dashboard and `neat ps` agree on where to look.
|
|
7405
|
+
web: typeof opts.webPort === "number" ? opts.webPort : resolveWebPort()
|
|
7406
|
+
};
|
|
7407
|
+
daemonRecord = {
|
|
7408
|
+
project: singleProject,
|
|
7409
|
+
projectPath: singleProjectPath,
|
|
7410
|
+
pid: process.pid,
|
|
7411
|
+
status: "running",
|
|
7412
|
+
ports,
|
|
7413
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7414
|
+
neatVersion: resolveNeatVersion()
|
|
7415
|
+
};
|
|
7416
|
+
try {
|
|
7417
|
+
await writeDaemonRecord(daemonRecord, home);
|
|
7418
|
+
console.log(
|
|
7419
|
+
`neatd: project "${singleProject}" \u2192 REST ${ports.rest} / OTLP ${ports.otlp} / web ${ports.web} (daemon.json written)`
|
|
7420
|
+
);
|
|
7421
|
+
} catch (err) {
|
|
7422
|
+
for (const slot of slots.values()) teardownSlot(slot);
|
|
7423
|
+
if (restApp) await restApp.close().catch(() => {
|
|
7424
|
+
});
|
|
7425
|
+
if (otlpApp) await otlpApp.close().catch(() => {
|
|
7426
|
+
});
|
|
7427
|
+
await import_node_fs25.promises.unlink(pidPath).catch(() => {
|
|
7428
|
+
});
|
|
7429
|
+
throw new Error(
|
|
7430
|
+
`neatd: failed to write daemon.json for "${singleProject}" \u2014 ${err.message}`
|
|
7431
|
+
);
|
|
7432
|
+
}
|
|
7433
|
+
}
|
|
7029
7434
|
}
|
|
7030
7435
|
const initialBootstrap = loadAll().catch((err) => {
|
|
7031
7436
|
console.warn(`neatd: initial bootstrap pass failed \u2014 ${err.message}`);
|
|
@@ -7065,9 +7470,9 @@ async function startDaemon(opts = {}) {
|
|
|
7065
7470
|
const REGISTRY_RELOAD_DEBOUNCE_MS = 500;
|
|
7066
7471
|
let registryWatcher = null;
|
|
7067
7472
|
let reloadTimer = null;
|
|
7068
|
-
try {
|
|
7069
|
-
const regDir =
|
|
7070
|
-
const regBase =
|
|
7473
|
+
if (!singleProject) try {
|
|
7474
|
+
const regDir = import_node_path42.default.dirname(regPath);
|
|
7475
|
+
const regBase = import_node_path42.default.basename(regPath);
|
|
7071
7476
|
registryWatcher = (0, import_node_fs25.watch)(regDir, (_eventType, filename) => {
|
|
7072
7477
|
if (filename !== null && filename !== regBase) return;
|
|
7073
7478
|
if (reloadTimer) clearTimeout(reloadTimer);
|
|
@@ -7108,6 +7513,9 @@ async function startDaemon(opts = {}) {
|
|
|
7108
7513
|
for (const slot of slots.values()) {
|
|
7109
7514
|
teardownSlot(slot);
|
|
7110
7515
|
}
|
|
7516
|
+
if (daemonRecord) {
|
|
7517
|
+
await clearDaemonRecord(daemonRecord, home);
|
|
7518
|
+
}
|
|
7111
7519
|
await import_node_fs25.promises.unlink(pidPath).catch(() => {
|
|
7112
7520
|
});
|
|
7113
7521
|
};
|
|
@@ -7119,7 +7527,8 @@ async function startDaemon(opts = {}) {
|
|
|
7119
7527
|
restAddress,
|
|
7120
7528
|
otlpAddress,
|
|
7121
7529
|
bootstrap: tracker,
|
|
7122
|
-
initialBootstrap
|
|
7530
|
+
initialBootstrap,
|
|
7531
|
+
daemonRecord
|
|
7123
7532
|
};
|
|
7124
7533
|
}
|
|
7125
7534
|
|
|
@@ -7129,9 +7538,40 @@ init_auth();
|
|
|
7129
7538
|
// src/web-spawn.ts
|
|
7130
7539
|
init_cjs_shims();
|
|
7131
7540
|
var import_node_child_process2 = require("child_process");
|
|
7541
|
+
var import_node_fs26 = require("fs");
|
|
7132
7542
|
var import_node_net = __toESM(require("net"), 1);
|
|
7133
|
-
var
|
|
7543
|
+
var import_node_path43 = __toESM(require("path"), 1);
|
|
7134
7544
|
var DEFAULT_WEB_PORT = 6328;
|
|
7545
|
+
var DEFAULT_REST_PORT = 8080;
|
|
7546
|
+
function asValidPort(value) {
|
|
7547
|
+
const n = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
|
|
7548
|
+
return Number.isInteger(n) && n > 0 && n <= 65535 ? n : null;
|
|
7549
|
+
}
|
|
7550
|
+
function projectRoot() {
|
|
7551
|
+
const fromEnv = process.env.NEAT_SCAN_PATH;
|
|
7552
|
+
return import_node_path43.default.resolve(fromEnv && fromEnv.length > 0 ? fromEnv : process.cwd());
|
|
7553
|
+
}
|
|
7554
|
+
async function readDaemonPorts(root) {
|
|
7555
|
+
try {
|
|
7556
|
+
const raw = await import_node_fs26.promises.readFile(import_node_path43.default.join(root, "neat-out", "daemon.json"), "utf8");
|
|
7557
|
+
const parsed = JSON.parse(raw);
|
|
7558
|
+
const ports = parsed?.ports ?? {};
|
|
7559
|
+
return { web: asValidPort(ports.web), rest: asValidPort(ports.rest) };
|
|
7560
|
+
} catch {
|
|
7561
|
+
return { web: null, rest: null };
|
|
7562
|
+
}
|
|
7563
|
+
}
|
|
7564
|
+
function resolveWebPorts(args) {
|
|
7565
|
+
const { daemonWeb, daemonRest, webPortEnv, apiUrlEnv, restPortArg } = args;
|
|
7566
|
+
const portFromEnv = webPortEnv && webPortEnv.length > 0 ? Number.parseInt(webPortEnv, 10) : null;
|
|
7567
|
+
if (portFromEnv !== null && (!Number.isFinite(portFromEnv) || portFromEnv <= 0 || portFromEnv > 65535)) {
|
|
7568
|
+
throw new Error(`neatd: invalid NEAT_WEB_PORT="${webPortEnv}"`);
|
|
7569
|
+
}
|
|
7570
|
+
const webPort = daemonWeb ?? portFromEnv ?? DEFAULT_WEB_PORT;
|
|
7571
|
+
const restPort = daemonRest ?? asValidPort(restPortArg) ?? DEFAULT_REST_PORT;
|
|
7572
|
+
const apiUrl = apiUrlEnv ?? `http://localhost:${restPort}`;
|
|
7573
|
+
return { webPort, apiUrl };
|
|
7574
|
+
}
|
|
7135
7575
|
async function assertPortFree(port) {
|
|
7136
7576
|
await new Promise((resolve, reject) => {
|
|
7137
7577
|
const tester = import_node_net.default.createServer();
|
|
@@ -7158,47 +7598,103 @@ function resolveWebPackageDir() {
|
|
|
7158
7598
|
eval("require")
|
|
7159
7599
|
);
|
|
7160
7600
|
const pkgJsonPath = req.resolve("@neat.is/web/package.json");
|
|
7161
|
-
return
|
|
7601
|
+
return import_node_path43.default.dirname(pkgJsonPath);
|
|
7162
7602
|
}
|
|
7163
7603
|
function resolveStandaloneServerEntry(webDir) {
|
|
7164
|
-
return
|
|
7604
|
+
return import_node_path43.default.join(webDir, ".next/standalone/packages/web/server.js");
|
|
7605
|
+
}
|
|
7606
|
+
async function pickInternalPort() {
|
|
7607
|
+
return new Promise((resolve, reject) => {
|
|
7608
|
+
const srv = import_node_net.default.createServer();
|
|
7609
|
+
srv.once("error", reject);
|
|
7610
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
7611
|
+
const addr = srv.address();
|
|
7612
|
+
if (addr && typeof addr === "object") {
|
|
7613
|
+
const port = addr.port;
|
|
7614
|
+
srv.close(() => resolve(port));
|
|
7615
|
+
} else {
|
|
7616
|
+
srv.close(() => reject(new Error("neatd: could not pick an internal web port")));
|
|
7617
|
+
}
|
|
7618
|
+
});
|
|
7619
|
+
});
|
|
7165
7620
|
}
|
|
7166
|
-
async function spawnWebUI(restPort) {
|
|
7167
|
-
const
|
|
7168
|
-
const
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7621
|
+
async function spawnWebUI(restPort, opts = {}) {
|
|
7622
|
+
const root = projectRoot();
|
|
7623
|
+
const fromDaemon = await readDaemonPorts(root);
|
|
7624
|
+
const { webPort: port, apiUrl } = resolveWebPorts({
|
|
7625
|
+
daemonWeb: fromDaemon.web,
|
|
7626
|
+
daemonRest: fromDaemon.rest,
|
|
7627
|
+
webPortEnv: process.env.NEAT_WEB_PORT,
|
|
7628
|
+
apiUrlEnv: process.env.NEAT_API_URL,
|
|
7629
|
+
restPortArg: restPort
|
|
7630
|
+
});
|
|
7172
7631
|
await assertPortFree(port);
|
|
7173
|
-
const
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7632
|
+
const serverEntry = opts.serverEntry ?? resolveStandaloneServerEntry(resolveWebPackageDir());
|
|
7633
|
+
if (!opts.skipBuildCheck) {
|
|
7634
|
+
try {
|
|
7635
|
+
require.resolve(serverEntry);
|
|
7636
|
+
} catch {
|
|
7637
|
+
throw new Error(
|
|
7638
|
+
`neatd: web UI standalone build missing at ${serverEntry}. The published @neat.is/web tarball should include it; if you're running from a monorepo checkout, run \`npm run build --workspace @neat.is/web\` first, or set NEAT_WEB_DISABLED=1 to skip the web UI.`
|
|
7639
|
+
);
|
|
7640
|
+
}
|
|
7181
7641
|
}
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7642
|
+
let child = null;
|
|
7643
|
+
let internalPort = 0;
|
|
7644
|
+
let starting = null;
|
|
7645
|
+
let stopped = false;
|
|
7646
|
+
function ensureStarted() {
|
|
7647
|
+
if (starting) return starting;
|
|
7648
|
+
starting = (async () => {
|
|
7649
|
+
internalPort = await pickInternalPort();
|
|
7650
|
+
const env = {
|
|
7651
|
+
...process.env,
|
|
7652
|
+
PORT: String(internalPort),
|
|
7653
|
+
HOSTNAME: "127.0.0.1",
|
|
7654
|
+
NEAT_API_URL: apiUrl
|
|
7655
|
+
};
|
|
7656
|
+
child = (0, import_node_child_process2.spawn)(process.execPath, [serverEntry], {
|
|
7657
|
+
cwd: import_node_path43.default.dirname(serverEntry),
|
|
7658
|
+
env,
|
|
7659
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
7660
|
+
detached: false
|
|
7661
|
+
});
|
|
7662
|
+
child.on("error", (err) => {
|
|
7663
|
+
console.error(`neatd: web UI spawn error \u2014 ${err.message}`);
|
|
7664
|
+
});
|
|
7665
|
+
await waitForListening(internalPort);
|
|
7666
|
+
console.log(`neatd: web UI ready on http://localhost:${port}`);
|
|
7667
|
+
})();
|
|
7668
|
+
return starting;
|
|
7669
|
+
}
|
|
7670
|
+
const front = import_node_net.default.createServer((socket) => {
|
|
7671
|
+
socket.on("error", () => socket.destroy());
|
|
7672
|
+
ensureStarted().then(() => {
|
|
7673
|
+
if (stopped) {
|
|
7674
|
+
socket.destroy();
|
|
7675
|
+
return;
|
|
7676
|
+
}
|
|
7677
|
+
const upstream = import_node_net.default.connect(internalPort, "127.0.0.1");
|
|
7678
|
+
upstream.on("error", () => socket.destroy());
|
|
7679
|
+
socket.pipe(upstream);
|
|
7680
|
+
upstream.pipe(socket);
|
|
7681
|
+
socket.on("close", () => upstream.destroy());
|
|
7682
|
+
upstream.on("close", () => socket.destroy());
|
|
7683
|
+
}).catch((err) => {
|
|
7684
|
+
console.error(`neatd: web UI failed to start \u2014 ${err.message}`);
|
|
7685
|
+
socket.destroy();
|
|
7686
|
+
});
|
|
7193
7687
|
});
|
|
7194
|
-
|
|
7195
|
-
|
|
7688
|
+
await new Promise((resolve, reject) => {
|
|
7689
|
+
front.once("error", reject);
|
|
7690
|
+
front.listen(port, "127.0.0.1", () => resolve());
|
|
7196
7691
|
});
|
|
7197
|
-
console.log(`neatd: web UI listening on http://localhost:${port}`);
|
|
7198
|
-
let stopped = false;
|
|
7692
|
+
console.log(`neatd: web UI listening on http://localhost:${port} (starts on first open)`);
|
|
7199
7693
|
async function stop() {
|
|
7200
|
-
if (stopped
|
|
7694
|
+
if (stopped) return;
|
|
7201
7695
|
stopped = true;
|
|
7696
|
+
await new Promise((resolve) => front.close(() => resolve()));
|
|
7697
|
+
if (!child || !child.pid) return;
|
|
7202
7698
|
try {
|
|
7203
7699
|
child.kill("SIGTERM");
|
|
7204
7700
|
} catch {
|
|
@@ -7206,18 +7702,46 @@ async function spawnWebUI(restPort) {
|
|
|
7206
7702
|
await new Promise((resolve) => {
|
|
7207
7703
|
const t = setTimeout(() => {
|
|
7208
7704
|
try {
|
|
7209
|
-
child
|
|
7705
|
+
child?.kill("SIGKILL");
|
|
7210
7706
|
} catch {
|
|
7211
7707
|
}
|
|
7212
7708
|
resolve();
|
|
7213
7709
|
}, 3e3);
|
|
7214
|
-
child
|
|
7710
|
+
child?.once("exit", () => {
|
|
7215
7711
|
clearTimeout(t);
|
|
7216
7712
|
resolve();
|
|
7217
7713
|
});
|
|
7218
7714
|
});
|
|
7219
7715
|
}
|
|
7220
|
-
return {
|
|
7716
|
+
return {
|
|
7717
|
+
get child() {
|
|
7718
|
+
return child;
|
|
7719
|
+
},
|
|
7720
|
+
port,
|
|
7721
|
+
started: () => child !== null,
|
|
7722
|
+
stop
|
|
7723
|
+
};
|
|
7724
|
+
}
|
|
7725
|
+
async function waitForListening(port, timeoutMs = 15e3) {
|
|
7726
|
+
const deadline = Date.now() + timeoutMs;
|
|
7727
|
+
for (; ; ) {
|
|
7728
|
+
const ok = await new Promise((resolve) => {
|
|
7729
|
+
const s = import_node_net.default.connect(port, "127.0.0.1");
|
|
7730
|
+
s.once("connect", () => {
|
|
7731
|
+
s.destroy();
|
|
7732
|
+
resolve(true);
|
|
7733
|
+
});
|
|
7734
|
+
s.once("error", () => {
|
|
7735
|
+
s.destroy();
|
|
7736
|
+
resolve(false);
|
|
7737
|
+
});
|
|
7738
|
+
});
|
|
7739
|
+
if (ok) return;
|
|
7740
|
+
if (Date.now() > deadline) {
|
|
7741
|
+
throw new Error(`neatd: web UI did not start listening on :${port} within ${timeoutMs}ms`);
|
|
7742
|
+
}
|
|
7743
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
7744
|
+
}
|
|
7221
7745
|
}
|
|
7222
7746
|
|
|
7223
7747
|
// src/version-skew.ts
|
|
@@ -7290,7 +7814,7 @@ function localVersion() {
|
|
|
7290
7814
|
return process.env.NEAT_LOCAL_VERSION;
|
|
7291
7815
|
}
|
|
7292
7816
|
try {
|
|
7293
|
-
const req2 = (0,
|
|
7817
|
+
const req2 = (0, import_node_module2.createRequire)(importMetaUrl);
|
|
7294
7818
|
const pkg = req2("../package.json");
|
|
7295
7819
|
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
7296
7820
|
} catch {
|
|
@@ -7299,14 +7823,14 @@ function localVersion() {
|
|
|
7299
7823
|
}
|
|
7300
7824
|
function neatHome2() {
|
|
7301
7825
|
if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {
|
|
7302
|
-
return
|
|
7826
|
+
return import_node_path44.default.resolve(process.env.NEAT_HOME);
|
|
7303
7827
|
}
|
|
7304
7828
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
7305
|
-
return
|
|
7829
|
+
return import_node_path44.default.join(home, ".neat");
|
|
7306
7830
|
}
|
|
7307
7831
|
async function readPid() {
|
|
7308
7832
|
try {
|
|
7309
|
-
const raw = await
|
|
7833
|
+
const raw = await import_node_fs27.promises.readFile(import_node_path44.default.join(neatHome2(), "neatd.pid"), "utf8");
|
|
7310
7834
|
const n = Number.parseInt(raw.trim(), 10);
|
|
7311
7835
|
return Number.isFinite(n) ? n : null;
|
|
7312
7836
|
} catch {
|
|
@@ -7320,10 +7844,19 @@ function restPortFromEnv() {
|
|
|
7320
7844
|
const raw = process.env.PORT;
|
|
7321
7845
|
return raw && raw.length > 0 ? Number.parseInt(raw, 10) : 8080;
|
|
7322
7846
|
}
|
|
7847
|
+
function webPortFromEnv() {
|
|
7848
|
+
const raw = process.env.NEAT_WEB_PORT;
|
|
7849
|
+
if (!raw || raw.length === 0) return void 0;
|
|
7850
|
+
const n = Number.parseInt(raw, 10);
|
|
7851
|
+
return Number.isFinite(n) ? n : void 0;
|
|
7852
|
+
}
|
|
7323
7853
|
async function cmdStart() {
|
|
7854
|
+
const project = process.env.NEAT_PROJECT;
|
|
7855
|
+
const projectPath = process.env.NEAT_PROJECT_PATH;
|
|
7856
|
+
const startOpts = project && project.length > 0 ? { project, projectPath, webPort: webPortFromEnv() } : {};
|
|
7324
7857
|
let handle;
|
|
7325
7858
|
try {
|
|
7326
|
-
handle = await startDaemon();
|
|
7859
|
+
handle = await startDaemon(startOpts);
|
|
7327
7860
|
} catch (err) {
|
|
7328
7861
|
if (err instanceof BindAuthorityError) {
|
|
7329
7862
|
console.error(`neatd: ${err.message}`);
|