@neat.is/core 0.4.27 → 0.4.28-dev.20260708
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-A3322JYS.js → chunk-CFDPIMRP.js} +13 -9
- package/dist/chunk-CFDPIMRP.js.map +1 -0
- package/dist/{chunk-UV5WSM7M.js → chunk-I72HTUOG.js} +2 -2
- package/dist/chunk-NY5Q6NE2.js +2566 -0
- package/dist/chunk-NY5Q6NE2.js.map +1 -0
- package/dist/{chunk-QM6BMPVJ.js → chunk-O4MRDWD7.js} +125 -12
- package/dist/chunk-O4MRDWD7.js.map +1 -0
- package/dist/cli.cjs +2454 -378
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +358 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1744 -100
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +1760 -116
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-ET5Z6KI6.js → otel-grpc-IDMIH6ZY.js} +3 -3
- package/dist/server.cjs +133 -18
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/package.json +3 -3
- package/dist/chunk-A3322JYS.js.map +0 -1
- package/dist/chunk-QM6BMPVJ.js.map +0 -1
- package/dist/chunk-XV4D7A3Z.js +0 -907
- package/dist/chunk-XV4D7A3Z.js.map +0 -1
- /package/dist/{chunk-UV5WSM7M.js.map → chunk-I72HTUOG.js.map} +0 -0
- /package/dist/{otel-grpc-ET5Z6KI6.js.map → otel-grpc-IDMIH6ZY.js.map} +0 -0
package/dist/cli.cjs
CHANGED
|
@@ -55,15 +55,16 @@ function mountBearerAuth(app, opts) {
|
|
|
55
55
|
if (!opts.token || opts.token.length === 0) return;
|
|
56
56
|
if (opts.trustProxy) return;
|
|
57
57
|
const expected = Buffer.from(opts.token, "utf8");
|
|
58
|
-
const
|
|
58
|
+
const exactUnauthPaths = /* @__PURE__ */ new Set([
|
|
59
|
+
...DEFAULT_UNAUTH_PATHS,
|
|
60
|
+
...opts.extraUnauthenticatedSuffixes ?? []
|
|
61
|
+
]);
|
|
59
62
|
const publicRead = opts.publicRead === true;
|
|
60
63
|
app.addHook("preHandler", (req, reply, done) => {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
64
|
+
const path57 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
|
|
65
|
+
if (exactUnauthPaths.has(path57) || PROJECT_SCOPED_UNAUTH_PATTERN.test(path57)) {
|
|
66
|
+
done();
|
|
67
|
+
return;
|
|
67
68
|
}
|
|
68
69
|
if (publicRead && PUBLIC_READ_METHODS.has(req.method)) {
|
|
69
70
|
done();
|
|
@@ -98,7 +99,7 @@ function readAuthEnv(env = process.env) {
|
|
|
98
99
|
publicRead: parseBoolEnv(env.NEAT_PUBLIC_READ)
|
|
99
100
|
};
|
|
100
101
|
}
|
|
101
|
-
var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, PUBLIC_READ_METHODS,
|
|
102
|
+
var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_PATHS, PROJECT_SCOPED_UNAUTH_PATTERN;
|
|
102
103
|
var init_auth = __esm({
|
|
103
104
|
"src/auth.ts"() {
|
|
104
105
|
"use strict";
|
|
@@ -119,12 +120,15 @@ var init_auth = __esm({
|
|
|
119
120
|
}
|
|
120
121
|
};
|
|
121
122
|
PUBLIC_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
122
|
-
|
|
123
|
+
DEFAULT_UNAUTH_PATHS = [
|
|
123
124
|
"/health",
|
|
124
125
|
"/healthz",
|
|
125
126
|
"/readyz",
|
|
126
127
|
"/api/config"
|
|
127
128
|
];
|
|
129
|
+
PROJECT_SCOPED_UNAUTH_PATTERN = new RegExp(
|
|
130
|
+
`^/projects/[^/]+/(?:${DEFAULT_UNAUTH_PATHS.map((p) => p.slice(1)).join("|")})$`
|
|
131
|
+
);
|
|
128
132
|
}
|
|
129
133
|
});
|
|
130
134
|
|
|
@@ -358,8 +362,8 @@ function websocketChannelPathOf(attrs) {
|
|
|
358
362
|
const v = attrs[key];
|
|
359
363
|
if (typeof v === "string" && v.length > 0) {
|
|
360
364
|
const q = v.indexOf("?");
|
|
361
|
-
const
|
|
362
|
-
if (
|
|
365
|
+
const path57 = q === -1 ? v : v.slice(0, q);
|
|
366
|
+
if (path57.length > 0) return path57;
|
|
363
367
|
}
|
|
364
368
|
}
|
|
365
369
|
return void 0;
|
|
@@ -686,8 +690,8 @@ __export(cli_exports, {
|
|
|
686
690
|
});
|
|
687
691
|
module.exports = __toCommonJS(cli_exports);
|
|
688
692
|
init_cjs_shims();
|
|
689
|
-
var
|
|
690
|
-
var
|
|
693
|
+
var import_node_path56 = __toESM(require("path"), 1);
|
|
694
|
+
var import_node_fs36 = require("fs");
|
|
691
695
|
|
|
692
696
|
// src/banner.ts
|
|
693
697
|
init_cjs_shims();
|
|
@@ -1252,19 +1256,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
1252
1256
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
1253
1257
|
let best = { path: [start], edges: [] };
|
|
1254
1258
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
1255
|
-
function step(node,
|
|
1256
|
-
if (
|
|
1257
|
-
best = { path: [...
|
|
1259
|
+
function step(node, path57, edges) {
|
|
1260
|
+
if (path57.length > best.path.length) {
|
|
1261
|
+
best = { path: [...path57], edges: [...edges] };
|
|
1258
1262
|
}
|
|
1259
|
-
if (
|
|
1263
|
+
if (path57.length - 1 >= maxDepth) return;
|
|
1260
1264
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
1261
1265
|
for (const [srcId, edge] of incoming) {
|
|
1262
1266
|
if (visited.has(srcId)) continue;
|
|
1263
1267
|
visited.add(srcId);
|
|
1264
|
-
|
|
1268
|
+
path57.push(srcId);
|
|
1265
1269
|
edges.push(edge);
|
|
1266
|
-
step(srcId,
|
|
1267
|
-
|
|
1270
|
+
step(srcId, path57, edges);
|
|
1271
|
+
path57.pop();
|
|
1268
1272
|
edges.pop();
|
|
1269
1273
|
visited.delete(srcId);
|
|
1270
1274
|
}
|
|
@@ -1279,7 +1283,7 @@ function databaseRootCauseShape(graph, origin, walk3) {
|
|
|
1279
1283
|
for (const id of walk3.path) {
|
|
1280
1284
|
const owner = resolveOwningService(graph, id);
|
|
1281
1285
|
if (!owner) continue;
|
|
1282
|
-
const { id:
|
|
1286
|
+
const { id: serviceId5, svc } = owner;
|
|
1283
1287
|
const deps = svc.dependencies ?? {};
|
|
1284
1288
|
for (const pair of candidatePairs) {
|
|
1285
1289
|
const declared = deps[pair.driver];
|
|
@@ -1292,7 +1296,7 @@ function databaseRootCauseShape(graph, origin, walk3) {
|
|
|
1292
1296
|
);
|
|
1293
1297
|
if (!result.compatible) {
|
|
1294
1298
|
return {
|
|
1295
|
-
rootCauseNode:
|
|
1299
|
+
rootCauseNode: serviceId5,
|
|
1296
1300
|
rootCauseReason: result.reason ?? "incompatible driver",
|
|
1297
1301
|
...result.minDriverVersion ? {
|
|
1298
1302
|
fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
|
|
@@ -1307,7 +1311,7 @@ function serviceRootCauseShape(graph, _origin, walk3) {
|
|
|
1307
1311
|
for (const id of walk3.path) {
|
|
1308
1312
|
const owner = resolveOwningService(graph, id);
|
|
1309
1313
|
if (!owner) continue;
|
|
1310
|
-
const { id:
|
|
1314
|
+
const { id: serviceId5, svc } = owner;
|
|
1311
1315
|
const deps = svc.dependencies ?? {};
|
|
1312
1316
|
const serviceNodeEngine = svc.nodeEngine;
|
|
1313
1317
|
for (const constraint of nodeEngineConstraints()) {
|
|
@@ -1316,7 +1320,7 @@ function serviceRootCauseShape(graph, _origin, walk3) {
|
|
|
1316
1320
|
const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
|
|
1317
1321
|
if (!result.compatible && result.reason) {
|
|
1318
1322
|
return {
|
|
1319
|
-
rootCauseNode:
|
|
1323
|
+
rootCauseNode: serviceId5,
|
|
1320
1324
|
rootCauseReason: result.reason,
|
|
1321
1325
|
...result.requiredNodeVersion ? {
|
|
1322
1326
|
fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
|
|
@@ -1331,7 +1335,7 @@ function serviceRootCauseShape(graph, _origin, walk3) {
|
|
|
1331
1335
|
const result = checkPackageConflict(conflict, declared, requiredDeclared);
|
|
1332
1336
|
if (!result.compatible && result.reason) {
|
|
1333
1337
|
return {
|
|
1334
|
-
rootCauseNode:
|
|
1338
|
+
rootCauseNode: serviceId5,
|
|
1335
1339
|
rootCauseReason: result.reason,
|
|
1336
1340
|
fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
|
|
1337
1341
|
};
|
|
@@ -1422,9 +1426,9 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
|
|
|
1422
1426
|
function isFailingCallEdge(e) {
|
|
1423
1427
|
return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
|
|
1424
1428
|
}
|
|
1425
|
-
function callSourcesForService(graph,
|
|
1426
|
-
const ids = [
|
|
1427
|
-
for (const edgeId of graph.outboundEdges(
|
|
1429
|
+
function callSourcesForService(graph, serviceId5) {
|
|
1430
|
+
const ids = [serviceId5];
|
|
1431
|
+
for (const edgeId of graph.outboundEdges(serviceId5)) {
|
|
1428
1432
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1429
1433
|
if (e.type !== import_types.EdgeType.CONTAINS) continue;
|
|
1430
1434
|
const tgt = graph.getNodeAttributes(e.target);
|
|
@@ -1441,9 +1445,9 @@ function failingCallDominates(e, id, curEdge, curId) {
|
|
|
1441
1445
|
}
|
|
1442
1446
|
return id < curId;
|
|
1443
1447
|
}
|
|
1444
|
-
function dominantFailingCall(graph,
|
|
1448
|
+
function dominantFailingCall(graph, serviceId5, visited) {
|
|
1445
1449
|
let best = null;
|
|
1446
|
-
for (const src of callSourcesForService(graph,
|
|
1450
|
+
for (const src of callSourcesForService(graph, serviceId5)) {
|
|
1447
1451
|
for (const edgeId of graph.outboundEdges(src)) {
|
|
1448
1452
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1449
1453
|
if (!isFailingCallEdge(e)) continue;
|
|
@@ -1458,26 +1462,26 @@ function dominantFailingCall(graph, serviceId4, visited) {
|
|
|
1458
1462
|
return best;
|
|
1459
1463
|
}
|
|
1460
1464
|
function followFailingCallChain(graph, originServiceId, maxDepth) {
|
|
1461
|
-
const
|
|
1465
|
+
const path57 = [originServiceId];
|
|
1462
1466
|
const edges = [];
|
|
1463
1467
|
const visited = /* @__PURE__ */ new Set([originServiceId]);
|
|
1464
1468
|
let current = originServiceId;
|
|
1465
1469
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
1466
1470
|
const hop = dominantFailingCall(graph, current, visited);
|
|
1467
1471
|
if (!hop) break;
|
|
1468
|
-
|
|
1472
|
+
path57.push(hop.nextService);
|
|
1469
1473
|
edges.push(hop.edge);
|
|
1470
1474
|
visited.add(hop.nextService);
|
|
1471
1475
|
current = hop.nextService;
|
|
1472
1476
|
}
|
|
1473
1477
|
if (edges.length === 0) return null;
|
|
1474
|
-
return { path:
|
|
1478
|
+
return { path: path57, edges, culprit: current };
|
|
1475
1479
|
}
|
|
1476
1480
|
function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
1477
1481
|
const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
|
|
1478
1482
|
if (!chain) return null;
|
|
1479
1483
|
const culprit = chain.culprit;
|
|
1480
|
-
const
|
|
1484
|
+
const path57 = [...chain.path];
|
|
1481
1485
|
const edgeProvenances = chain.edges.map((e) => e.provenance);
|
|
1482
1486
|
const baseConfidence = confidenceFromMix(chain.edges);
|
|
1483
1487
|
const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
|
|
@@ -1485,14 +1489,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
|
1485
1489
|
if (loc) {
|
|
1486
1490
|
let rootCauseNode = culprit;
|
|
1487
1491
|
if (loc.fileNode) {
|
|
1488
|
-
|
|
1492
|
+
path57.push(loc.fileNode);
|
|
1489
1493
|
edgeProvenances.push(import_types.Provenance.OBSERVED);
|
|
1490
1494
|
rootCauseNode = loc.fileNode;
|
|
1491
1495
|
}
|
|
1492
1496
|
return import_types.RootCauseResultSchema.parse({
|
|
1493
1497
|
rootCauseNode,
|
|
1494
1498
|
rootCauseReason: loc.rootCauseReason,
|
|
1495
|
-
traversalPath:
|
|
1499
|
+
traversalPath: path57,
|
|
1496
1500
|
edgeProvenances,
|
|
1497
1501
|
confidence,
|
|
1498
1502
|
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
@@ -1504,7 +1508,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
|
1504
1508
|
return import_types.RootCauseResultSchema.parse({
|
|
1505
1509
|
rootCauseNode: culprit,
|
|
1506
1510
|
rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
|
|
1507
|
-
traversalPath:
|
|
1511
|
+
traversalPath: path57,
|
|
1508
1512
|
edgeProvenances,
|
|
1509
1513
|
confidence,
|
|
1510
1514
|
fixRecommendation: `Inspect ${culpritName}'s failing handler`
|
|
@@ -3060,29 +3064,29 @@ function promoteFrontierNodes(graph, opts = {}) {
|
|
|
3060
3064
|
toPromote.push({ frontierId: id, serviceId: target });
|
|
3061
3065
|
});
|
|
3062
3066
|
let promoted = 0;
|
|
3063
|
-
for (const { frontierId: frontierId2, serviceId:
|
|
3067
|
+
for (const { frontierId: frontierId2, serviceId: serviceId5 } of toPromote) {
|
|
3064
3068
|
if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
|
|
3065
3069
|
const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
|
|
3066
3070
|
if (!gate.allowed) {
|
|
3067
3071
|
continue;
|
|
3068
3072
|
}
|
|
3069
3073
|
}
|
|
3070
|
-
rewireFrontierEdges(graph, frontierId2,
|
|
3074
|
+
rewireFrontierEdges(graph, frontierId2, serviceId5);
|
|
3071
3075
|
graph.dropNode(frontierId2);
|
|
3072
3076
|
promoted++;
|
|
3073
3077
|
}
|
|
3074
3078
|
return promoted;
|
|
3075
3079
|
}
|
|
3076
|
-
function rewireFrontierEdges(graph, frontierId2,
|
|
3080
|
+
function rewireFrontierEdges(graph, frontierId2, serviceId5) {
|
|
3077
3081
|
const inbound = [...graph.inboundEdges(frontierId2)];
|
|
3078
3082
|
const outbound = [...graph.outboundEdges(frontierId2)];
|
|
3079
3083
|
for (const edgeId of inbound) {
|
|
3080
3084
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
3081
|
-
rebuildEdge(graph, edge, edge.source,
|
|
3085
|
+
rebuildEdge(graph, edge, edge.source, serviceId5, edgeId);
|
|
3082
3086
|
}
|
|
3083
3087
|
for (const edgeId of outbound) {
|
|
3084
3088
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
3085
|
-
rebuildEdge(graph, edge,
|
|
3089
|
+
rebuildEdge(graph, edge, serviceId5, edge.target, edgeId);
|
|
3086
3090
|
}
|
|
3087
3091
|
}
|
|
3088
3092
|
function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
|
|
@@ -3233,24 +3237,58 @@ function dedupeIncidents(events) {
|
|
|
3233
3237
|
return !hasRealFailure.has(groupKey(ev));
|
|
3234
3238
|
});
|
|
3235
3239
|
}
|
|
3240
|
+
var SnapshotValidationError = class extends Error {
|
|
3241
|
+
constructor(issues) {
|
|
3242
|
+
super(`snapshot failed validation (${issues.length} invalid ${issues.length === 1 ? "entry" : "entries"})`);
|
|
3243
|
+
this.issues = issues;
|
|
3244
|
+
this.name = "SnapshotValidationError";
|
|
3245
|
+
}
|
|
3246
|
+
issues;
|
|
3247
|
+
};
|
|
3248
|
+
function describeZodIssues(error) {
|
|
3249
|
+
return error.issues.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
|
|
3250
|
+
}
|
|
3236
3251
|
function mergeSnapshot(graph, snapshot) {
|
|
3237
3252
|
const exported = snapshot.graph;
|
|
3253
|
+
const incomingNodes = Array.isArray(exported.nodes) ? exported.nodes : [];
|
|
3254
|
+
const incomingEdges = Array.isArray(exported.edges) ? exported.edges : [];
|
|
3255
|
+
const issues = [];
|
|
3256
|
+
const validNodes = [];
|
|
3257
|
+
const validEdges = [];
|
|
3258
|
+
for (const node of incomingNodes) {
|
|
3259
|
+
if (node.attributes === void 0) continue;
|
|
3260
|
+
const parsed = import_types3.GraphNodeSchema.safeParse(node.attributes);
|
|
3261
|
+
if (!parsed.success) {
|
|
3262
|
+
issues.push(`node "${node.key}": ${describeZodIssues(parsed.error)}`);
|
|
3263
|
+
continue;
|
|
3264
|
+
}
|
|
3265
|
+
validNodes.push({ key: node.key, attributes: parsed.data });
|
|
3266
|
+
}
|
|
3267
|
+
for (const edge of incomingEdges) {
|
|
3268
|
+
if (edge.attributes === void 0) continue;
|
|
3269
|
+
const parsed = import_types3.GraphEdgeSchema.safeParse(edge.attributes);
|
|
3270
|
+
if (!parsed.success) {
|
|
3271
|
+
const label = edge.key ?? `${edge.source}->${edge.target}`;
|
|
3272
|
+
issues.push(`edge "${label}": ${describeZodIssues(parsed.error)}`);
|
|
3273
|
+
continue;
|
|
3274
|
+
}
|
|
3275
|
+
const id = edge.key ?? parsed.data.id;
|
|
3276
|
+
validEdges.push({ key: id, source: edge.source, target: edge.target, attributes: parsed.data });
|
|
3277
|
+
}
|
|
3278
|
+
if (issues.length > 0) {
|
|
3279
|
+
throw new SnapshotValidationError(issues);
|
|
3280
|
+
}
|
|
3238
3281
|
let nodesAdded = 0;
|
|
3239
3282
|
let edgesAdded = 0;
|
|
3240
|
-
for (const node of
|
|
3283
|
+
for (const node of validNodes) {
|
|
3241
3284
|
if (graph.hasNode(node.key)) continue;
|
|
3242
|
-
if (!node.attributes) continue;
|
|
3243
3285
|
graph.addNode(node.key, node.attributes);
|
|
3244
3286
|
nodesAdded++;
|
|
3245
3287
|
}
|
|
3246
|
-
for (const edge of
|
|
3247
|
-
|
|
3248
|
-
if (!attrs) continue;
|
|
3249
|
-
const id = edge.key ?? attrs.id;
|
|
3250
|
-
if (!id) continue;
|
|
3251
|
-
if (graph.hasEdge(id)) continue;
|
|
3288
|
+
for (const edge of validEdges) {
|
|
3289
|
+
if (graph.hasEdge(edge.key)) continue;
|
|
3252
3290
|
if (!graph.hasNode(edge.source) || !graph.hasNode(edge.target)) continue;
|
|
3253
|
-
graph.addEdgeWithKey(
|
|
3291
|
+
graph.addEdgeWithKey(edge.key, edge.source, edge.target, edge.attributes);
|
|
3254
3292
|
edgesAdded++;
|
|
3255
3293
|
}
|
|
3256
3294
|
return { nodesAdded, edgesAdded };
|
|
@@ -3856,9 +3894,9 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
|
3856
3894
|
"StatefulSet",
|
|
3857
3895
|
"DaemonSet"
|
|
3858
3896
|
]);
|
|
3859
|
-
function addAliases(graph,
|
|
3860
|
-
if (!graph.hasNode(
|
|
3861
|
-
const node = graph.getNodeAttributes(
|
|
3897
|
+
function addAliases(graph, serviceId5, candidates) {
|
|
3898
|
+
if (!graph.hasNode(serviceId5)) return;
|
|
3899
|
+
const node = graph.getNodeAttributes(serviceId5);
|
|
3862
3900
|
if (node.type !== import_types6.NodeType.ServiceNode) return;
|
|
3863
3901
|
const set = new Set(node.aliases ?? []);
|
|
3864
3902
|
for (const c of candidates) {
|
|
@@ -3868,7 +3906,7 @@ function addAliases(graph, serviceId4, candidates) {
|
|
|
3868
3906
|
}
|
|
3869
3907
|
if (set.size === 0) return;
|
|
3870
3908
|
const updated = { ...node, aliases: [...set].sort() };
|
|
3871
|
-
graph.replaceNodeAttributes(
|
|
3909
|
+
graph.replaceNodeAttributes(serviceId5, updated);
|
|
3872
3910
|
}
|
|
3873
3911
|
function indexServicesByName(services) {
|
|
3874
3912
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -3901,12 +3939,12 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
|
3901
3939
|
}
|
|
3902
3940
|
if (!compose?.services) return;
|
|
3903
3941
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
3904
|
-
const
|
|
3905
|
-
if (!
|
|
3942
|
+
const serviceId5 = serviceIndex.get(composeName);
|
|
3943
|
+
if (!serviceId5) continue;
|
|
3906
3944
|
const aliases = /* @__PURE__ */ new Set([composeName]);
|
|
3907
3945
|
if (svc.container_name) aliases.add(svc.container_name);
|
|
3908
3946
|
if (svc.hostname) aliases.add(svc.hostname);
|
|
3909
|
-
addAliases(graph,
|
|
3947
|
+
addAliases(graph, serviceId5, aliases);
|
|
3910
3948
|
}
|
|
3911
3949
|
}
|
|
3912
3950
|
var LABEL_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -6943,13 +6981,13 @@ function bucketKey(source, target, type) {
|
|
|
6943
6981
|
return `${type}|${source}|${target}`;
|
|
6944
6982
|
}
|
|
6945
6983
|
function bucketEdges(graph) {
|
|
6946
|
-
const
|
|
6984
|
+
const buckets2 = /* @__PURE__ */ new Map();
|
|
6947
6985
|
graph.forEachEdge((id, attrs) => {
|
|
6948
6986
|
const e = attrs;
|
|
6949
6987
|
const parsed = (0, import_types26.parseEdgeId)(id);
|
|
6950
6988
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
6951
6989
|
const key = bucketKey(e.source, e.target, e.type);
|
|
6952
|
-
const cur =
|
|
6990
|
+
const cur = buckets2.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
6953
6991
|
switch (provenance) {
|
|
6954
6992
|
case import_types26.Provenance.EXTRACTED:
|
|
6955
6993
|
cur.extracted = e;
|
|
@@ -6963,9 +7001,9 @@ function bucketEdges(graph) {
|
|
|
6963
7001
|
default:
|
|
6964
7002
|
if (e.provenance === import_types26.Provenance.STALE) cur.stale = e;
|
|
6965
7003
|
}
|
|
6966
|
-
|
|
7004
|
+
buckets2.set(key, cur);
|
|
6967
7005
|
});
|
|
6968
|
-
return
|
|
7006
|
+
return buckets2;
|
|
6969
7007
|
}
|
|
6970
7008
|
function nodeIsFrontier(graph, nodeId) {
|
|
6971
7009
|
if (!graph.hasNode(nodeId)) return false;
|
|
@@ -7156,8 +7194,8 @@ function suppressHostMismatchHalves(all) {
|
|
|
7156
7194
|
}
|
|
7157
7195
|
function computeDivergences(graph, opts = {}) {
|
|
7158
7196
|
const all = [];
|
|
7159
|
-
const
|
|
7160
|
-
for (const bucket of
|
|
7197
|
+
const buckets2 = bucketEdges(graph);
|
|
7198
|
+
for (const bucket of buckets2.values()) {
|
|
7161
7199
|
for (const d of detectMissingDivergences(graph, bucket)) all.push(d);
|
|
7162
7200
|
}
|
|
7163
7201
|
graph.forEachNode((nodeId, attrs) => {
|
|
@@ -7456,8 +7494,8 @@ function formatIncompat(inc) {
|
|
|
7456
7494
|
|
|
7457
7495
|
// src/watch.ts
|
|
7458
7496
|
init_cjs_shims();
|
|
7459
|
-
var
|
|
7460
|
-
var
|
|
7497
|
+
var import_node_fs31 = __toESM(require("fs"), 1);
|
|
7498
|
+
var import_node_path50 = __toESM(require("path"), 1);
|
|
7461
7499
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
7462
7500
|
|
|
7463
7501
|
// src/api.ts
|
|
@@ -7781,6 +7819,51 @@ async function rollbackExtension(ctx, args) {
|
|
|
7781
7819
|
};
|
|
7782
7820
|
}
|
|
7783
7821
|
|
|
7822
|
+
// src/logs-store.ts
|
|
7823
|
+
init_cjs_shims();
|
|
7824
|
+
var LOGS_STORE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
7825
|
+
var LOGS_QUERY_DEFAULT_LIMIT = 100;
|
|
7826
|
+
var LOGS_QUERY_MAX_LIMIT = 1e3;
|
|
7827
|
+
var buffers = /* @__PURE__ */ new Map();
|
|
7828
|
+
var KEY_SEP = "::";
|
|
7829
|
+
function bufferKey(projectName, source) {
|
|
7830
|
+
return `${projectName}${KEY_SEP}${source}`;
|
|
7831
|
+
}
|
|
7832
|
+
function sourcesForProject(projectName) {
|
|
7833
|
+
const prefix = `${projectName}${KEY_SEP}`;
|
|
7834
|
+
const sources = [];
|
|
7835
|
+
for (const key of buffers.keys()) {
|
|
7836
|
+
if (key.startsWith(prefix)) sources.push(key.slice(prefix.length));
|
|
7837
|
+
}
|
|
7838
|
+
return sources;
|
|
7839
|
+
}
|
|
7840
|
+
function queryLogEntries(opts) {
|
|
7841
|
+
const { projectName, source, service, since, limit } = opts;
|
|
7842
|
+
const sources = source && source.length > 0 ? source : sourcesForProject(projectName);
|
|
7843
|
+
let merged = [];
|
|
7844
|
+
for (const src of sources) {
|
|
7845
|
+
const buf = buffers.get(bufferKey(projectName, src));
|
|
7846
|
+
if (buf) merged = merged.concat(buf);
|
|
7847
|
+
}
|
|
7848
|
+
if (service) {
|
|
7849
|
+
merged = merged.filter((e) => e.serviceName === service);
|
|
7850
|
+
}
|
|
7851
|
+
if (since) {
|
|
7852
|
+
const sinceMs = Date.parse(since);
|
|
7853
|
+
if (!Number.isNaN(sinceMs)) {
|
|
7854
|
+
merged = merged.filter((e) => {
|
|
7855
|
+
const t = Date.parse(e.timestamp);
|
|
7856
|
+
return Number.isNaN(t) || t >= sinceMs;
|
|
7857
|
+
});
|
|
7858
|
+
}
|
|
7859
|
+
}
|
|
7860
|
+
merged.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
|
|
7861
|
+
if (typeof limit === "number" && Number.isFinite(limit) && limit >= 0) {
|
|
7862
|
+
return merged.slice(0, limit);
|
|
7863
|
+
}
|
|
7864
|
+
return merged;
|
|
7865
|
+
}
|
|
7866
|
+
|
|
7784
7867
|
// src/diff.ts
|
|
7785
7868
|
init_cjs_shims();
|
|
7786
7869
|
var import_node_fs25 = require("fs");
|
|
@@ -8556,6 +8639,23 @@ function registerRoutes(scope, ctx) {
|
|
|
8556
8639
|
const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
|
|
8557
8640
|
return { count: sliced.length, total, events: sliced };
|
|
8558
8641
|
});
|
|
8642
|
+
scope.get("/logs", async (req, reply) => {
|
|
8643
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
8644
|
+
if (!proj) return;
|
|
8645
|
+
const rawSource = req.query.source;
|
|
8646
|
+
const sources = rawSource === void 0 ? void 0 : Array.isArray(rawSource) ? rawSource : [rawSource];
|
|
8647
|
+
const filtered = queryLogEntries({
|
|
8648
|
+
projectName: proj.name,
|
|
8649
|
+
...sources && sources.length > 0 ? { source: sources } : {},
|
|
8650
|
+
...req.query.service ? { service: req.query.service } : {},
|
|
8651
|
+
...req.query.since ? { since: req.query.since } : {}
|
|
8652
|
+
});
|
|
8653
|
+
const total = filtered.length;
|
|
8654
|
+
const limit = req.query.limit ? Number(req.query.limit) : LOGS_QUERY_DEFAULT_LIMIT;
|
|
8655
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, LOGS_QUERY_MAX_LIMIT) : LOGS_QUERY_DEFAULT_LIMIT;
|
|
8656
|
+
const sliced = filtered.slice(0, safeLimit);
|
|
8657
|
+
return { count: sliced.length, total, logs: sliced };
|
|
8658
|
+
});
|
|
8559
8659
|
const incidentHistoryHandler = async (req, reply) => {
|
|
8560
8660
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
8561
8661
|
if (!proj) return;
|
|
@@ -8651,8 +8751,16 @@ function registerRoutes(scope, ctx) {
|
|
|
8651
8751
|
if (!against) {
|
|
8652
8752
|
return reply.code(400).send({ error: "query parameter `against` is required" });
|
|
8653
8753
|
}
|
|
8754
|
+
const targetProjectName = against === "self" ? proj.name : against;
|
|
8755
|
+
const targetProject = registry.get(targetProjectName);
|
|
8756
|
+
if (!targetProject) {
|
|
8757
|
+
return reply.code(400).send({
|
|
8758
|
+
error: "unknown snapshot id \u2014 `against` must be `self` or the name of a project this server has a snapshot for",
|
|
8759
|
+
against
|
|
8760
|
+
});
|
|
8761
|
+
}
|
|
8654
8762
|
try {
|
|
8655
|
-
const snapshot = await loadSnapshotForDiff(
|
|
8763
|
+
const snapshot = await loadSnapshotForDiff(targetProject.paths.snapshotPath);
|
|
8656
8764
|
return computeGraphDiff(proj.graph, snapshot);
|
|
8657
8765
|
} catch (err) {
|
|
8658
8766
|
return reply.code(400).send({ error: "failed to load snapshot", against, detail: err.message });
|
|
@@ -8682,6 +8790,13 @@ function registerRoutes(scope, ctx) {
|
|
|
8682
8790
|
edgeCount: proj.graph.size
|
|
8683
8791
|
};
|
|
8684
8792
|
} catch (err) {
|
|
8793
|
+
if (err instanceof SnapshotValidationError) {
|
|
8794
|
+
return reply.code(400).send({
|
|
8795
|
+
error: "snapshot merge failed",
|
|
8796
|
+
details: err.message,
|
|
8797
|
+
issues: err.issues
|
|
8798
|
+
});
|
|
8799
|
+
}
|
|
8685
8800
|
return reply.code(400).send({
|
|
8686
8801
|
error: "snapshot merge failed",
|
|
8687
8802
|
details: err.message
|
|
@@ -9028,164 +9143,1779 @@ init_otel();
|
|
|
9028
9143
|
|
|
9029
9144
|
// src/daemon.ts
|
|
9030
9145
|
init_cjs_shims();
|
|
9031
|
-
var
|
|
9032
|
-
var
|
|
9146
|
+
var import_node_fs29 = require("fs");
|
|
9147
|
+
var import_node_path48 = __toESM(require("path"), 1);
|
|
9033
9148
|
var import_node_module = require("module");
|
|
9034
9149
|
init_otel();
|
|
9035
9150
|
|
|
9036
9151
|
// src/connectors/index.ts
|
|
9037
9152
|
init_cjs_shims();
|
|
9038
9153
|
|
|
9039
|
-
// src/
|
|
9040
|
-
init_auth();
|
|
9041
|
-
|
|
9042
|
-
// src/unrouted.ts
|
|
9154
|
+
// src/connectors/registry.ts
|
|
9043
9155
|
init_cjs_shims();
|
|
9044
|
-
var import_node_fs27 = require("fs");
|
|
9045
|
-
var import_node_path46 = __toESM(require("path"), 1);
|
|
9046
9156
|
|
|
9047
|
-
// src/
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
}
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
|
|
9157
|
+
// src/connectors/junction.ts
|
|
9158
|
+
init_cjs_shims();
|
|
9159
|
+
var buckets = /* @__PURE__ */ new Map();
|
|
9160
|
+
function bucketMapKey(provider, accountKey) {
|
|
9161
|
+
return `${provider}\0${accountKey}`;
|
|
9162
|
+
}
|
|
9163
|
+
function getBucket(provider, accountKey, config) {
|
|
9164
|
+
const key = bucketMapKey(provider, accountKey);
|
|
9165
|
+
const existing = buckets.get(key);
|
|
9166
|
+
if (existing && existing.capacity === config.capacity && existing.refillMs === config.refillMs) {
|
|
9167
|
+
return existing;
|
|
9168
|
+
}
|
|
9169
|
+
const fresh = { ...config, tokens: config.capacity, updatedAt: Date.now() };
|
|
9170
|
+
buckets.set(key, fresh);
|
|
9171
|
+
return fresh;
|
|
9172
|
+
}
|
|
9173
|
+
function refillBucket(bucket, now) {
|
|
9174
|
+
if (now <= bucket.updatedAt) return;
|
|
9175
|
+
const elapsed = now - bucket.updatedAt;
|
|
9176
|
+
const grant = elapsed / bucket.refillMs;
|
|
9177
|
+
if (grant <= 0) return;
|
|
9178
|
+
bucket.tokens = Math.min(bucket.capacity, bucket.tokens + grant);
|
|
9179
|
+
bucket.updatedAt = now;
|
|
9180
|
+
}
|
|
9181
|
+
var RateLimitExceededError = class extends Error {
|
|
9182
|
+
constructor(provider, accountKey) {
|
|
9183
|
+
super(
|
|
9184
|
+
`junction: rate limit exceeded for ${provider}:${accountKey} \u2014 waiting for the next token would exceed this call's wall-clock budget`
|
|
9185
|
+
);
|
|
9186
|
+
this.name = "RateLimitExceededError";
|
|
9187
|
+
}
|
|
9188
|
+
};
|
|
9189
|
+
function delay(ms) {
|
|
9190
|
+
if (ms <= 0) return Promise.resolve();
|
|
9191
|
+
return new Promise((resolve) => {
|
|
9192
|
+
const timer = setTimeout(resolve, ms);
|
|
9193
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
9194
|
+
});
|
|
9055
9195
|
}
|
|
9056
|
-
function
|
|
9057
|
-
|
|
9196
|
+
async function acquireToken(provider, accountKey, config, remainingBudgetMs) {
|
|
9197
|
+
const bucket = getBucket(provider, accountKey, config);
|
|
9198
|
+
refillBucket(bucket, Date.now());
|
|
9199
|
+
if (bucket.tokens >= 1) {
|
|
9200
|
+
bucket.tokens -= 1;
|
|
9201
|
+
return 0;
|
|
9202
|
+
}
|
|
9203
|
+
const waitMs = Math.ceil((1 - bucket.tokens) * bucket.refillMs);
|
|
9204
|
+
if (waitMs > remainingBudgetMs) {
|
|
9205
|
+
throw new RateLimitExceededError(provider, accountKey);
|
|
9206
|
+
}
|
|
9207
|
+
await delay(waitMs);
|
|
9208
|
+
refillBucket(bucket, Date.now());
|
|
9209
|
+
bucket.tokens = Math.max(0, bucket.tokens - 1);
|
|
9210
|
+
return waitMs;
|
|
9211
|
+
}
|
|
9212
|
+
var JUNCTION_DEFAULT_RATE_LIMITS = {
|
|
9213
|
+
// ~300 requests / 5 minutes (ADR-131). Burst capacity holds a third of
|
|
9214
|
+
// that ceiling; steady-state refill (1 token / 3s = 20/min = 100/5min)
|
|
9215
|
+
// stays well clear of the documented limit even under sustained polling.
|
|
9216
|
+
cloudflare: { capacity: 100, refillMs: 3e3 },
|
|
9217
|
+
// Placeholder pending a live project confirming the real cap
|
|
9218
|
+
// (docs/connectors/railway.md: "does not appear to publish one as of this
|
|
9219
|
+
// writing").
|
|
9220
|
+
railway: { capacity: 30, refillMs: 1e4 },
|
|
9221
|
+
// Placeholder pending a live rate-limit check (docs/connectors/
|
|
9222
|
+
// firebase.md: "needs-endpoint-testing against entries.list's live rate
|
|
9223
|
+
// limits").
|
|
9224
|
+
firebase: { capacity: 30, refillMs: 1e4 },
|
|
9225
|
+
// Placeholder pending a live rate-limit check (docs/connectors/supabase.md:
|
|
9226
|
+
// "the documented rate limit for this specific endpoint is unconfirmed").
|
|
9227
|
+
supabase: { capacity: 30, refillMs: 1e4 },
|
|
9228
|
+
// Not a documented API limit at all — a self-imposed ceiling on the raw
|
|
9229
|
+
// pg_stat_statements connection (see module header above).
|
|
9230
|
+
"supabase-postgres": { capacity: 20, refillMs: 3e3 }
|
|
9231
|
+
};
|
|
9232
|
+
var JUNCTION_GENERIC_RATE_LIMIT = { capacity: 20, refillMs: 5e3 };
|
|
9233
|
+
function defaultRateLimitFor(provider) {
|
|
9234
|
+
return JUNCTION_DEFAULT_RATE_LIMITS[provider] ?? JUNCTION_GENERIC_RATE_LIMIT;
|
|
9235
|
+
}
|
|
9236
|
+
var JUNCTION_DEFAULT_TIMEOUT_MS = 1e4;
|
|
9237
|
+
var JUNCTION_DEFAULT_MAX_ATTEMPTS = 3;
|
|
9238
|
+
var JUNCTION_DEFAULT_INITIAL_BACKOFF_MS = 200;
|
|
9239
|
+
var JUNCTION_DEFAULT_BACKOFF_MULTIPLIER = 4;
|
|
9240
|
+
var JUNCTION_DEFAULT_MAX_ELAPSED_MS = 3e4;
|
|
9241
|
+
var JUNCTION_DEFAULT_DB_TIMEOUT_MS = 1e4;
|
|
9242
|
+
async function backoff(attempt, initialBackoffMs, backoffMultiplier, remainingBudgetMs) {
|
|
9243
|
+
const raw = initialBackoffMs * backoffMultiplier ** (attempt - 1);
|
|
9244
|
+
const capped = Math.max(0, Math.min(raw, remainingBudgetMs));
|
|
9245
|
+
await delay(capped);
|
|
9246
|
+
}
|
|
9247
|
+
function safeUrlLabel(url) {
|
|
9248
|
+
try {
|
|
9249
|
+
const u = typeof url === "string" ? new URL(url) : url;
|
|
9250
|
+
return `${u.origin}${u.pathname}`;
|
|
9251
|
+
} catch {
|
|
9252
|
+
return String(url);
|
|
9253
|
+
}
|
|
9058
9254
|
}
|
|
9059
|
-
function
|
|
9060
|
-
|
|
9255
|
+
function logOutcome(provider, accountKey, outcome, method, label, attempt, startedAt) {
|
|
9256
|
+
const elapsedMs = Date.now() - startedAt;
|
|
9257
|
+
const line = `[neat connector] ${provider}:${accountKey} ${method} ${label} \u2014 ${outcome} (attempt ${attempt}, ${elapsedMs}ms)`;
|
|
9258
|
+
if (outcome === "success" || outcome === "retried-then-succeeded") {
|
|
9259
|
+
console.log(line);
|
|
9260
|
+
} else if (outcome === "rate-limited") {
|
|
9261
|
+
console.warn(line);
|
|
9262
|
+
} else {
|
|
9263
|
+
console.error(line);
|
|
9264
|
+
}
|
|
9061
9265
|
}
|
|
9062
|
-
function
|
|
9063
|
-
|
|
9064
|
-
if (env && env.length > 0) return import_node_path47.default.resolve(env);
|
|
9065
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
9066
|
-
return import_node_path47.default.join(home, ".neat");
|
|
9266
|
+
function bearerAuthHeader(token) {
|
|
9267
|
+
return { Authorization: `Bearer ${token}` };
|
|
9067
9268
|
}
|
|
9068
|
-
async function
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9269
|
+
async function junctionFetch(url, init = {}, policy) {
|
|
9270
|
+
const {
|
|
9271
|
+
provider,
|
|
9272
|
+
accountKey,
|
|
9273
|
+
timeoutMs = JUNCTION_DEFAULT_TIMEOUT_MS,
|
|
9274
|
+
maxAttempts = JUNCTION_DEFAULT_MAX_ATTEMPTS,
|
|
9275
|
+
maxElapsedMs = JUNCTION_DEFAULT_MAX_ELAPSED_MS,
|
|
9276
|
+
initialBackoffMs = JUNCTION_DEFAULT_INITIAL_BACKOFF_MS,
|
|
9277
|
+
backoffMultiplier = JUNCTION_DEFAULT_BACKOFF_MULTIPLIER,
|
|
9278
|
+
rateLimit = defaultRateLimitFor(provider),
|
|
9279
|
+
fetchImpl = fetch
|
|
9280
|
+
} = policy;
|
|
9281
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
9282
|
+
const label = safeUrlLabel(url);
|
|
9283
|
+
const startedAt = Date.now();
|
|
9284
|
+
let attempt = 0;
|
|
9285
|
+
let sawRetry = false;
|
|
9286
|
+
for (; ; ) {
|
|
9287
|
+
attempt++;
|
|
9288
|
+
const remainingBudget = maxElapsedMs - (Date.now() - startedAt);
|
|
9289
|
+
if (remainingBudget <= 0) {
|
|
9290
|
+
logOutcome(provider, accountKey, "retried-then-failed", method, label, attempt - 1, startedAt);
|
|
9291
|
+
throw new Error(
|
|
9292
|
+
`junction: ${provider}:${accountKey} ${method} ${label} exceeded its wall-clock budget (${maxElapsedMs}ms) after ${attempt - 1} attempt(s)`
|
|
9293
|
+
);
|
|
9294
|
+
}
|
|
9295
|
+
try {
|
|
9296
|
+
await acquireToken(provider, accountKey, rateLimit, remainingBudget);
|
|
9297
|
+
} catch (err) {
|
|
9298
|
+
if (err instanceof RateLimitExceededError) {
|
|
9299
|
+
logOutcome(provider, accountKey, "rate-limited", method, label, attempt, startedAt);
|
|
9300
|
+
}
|
|
9301
|
+
throw err;
|
|
9302
|
+
}
|
|
9303
|
+
const controller = new AbortController();
|
|
9304
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
9305
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
9306
|
+
try {
|
|
9307
|
+
const res = await fetchImpl(url, { ...init, signal: controller.signal });
|
|
9308
|
+
clearTimeout(timer);
|
|
9309
|
+
if (res.ok || res.status < 500) {
|
|
9310
|
+
logOutcome(provider, accountKey, sawRetry ? "retried-then-succeeded" : "success", method, label, attempt, startedAt);
|
|
9311
|
+
return res;
|
|
9312
|
+
}
|
|
9313
|
+
if (attempt >= maxAttempts) {
|
|
9314
|
+
logOutcome(provider, accountKey, sawRetry ? "retried-then-failed" : "failed", method, label, attempt, startedAt);
|
|
9315
|
+
return res;
|
|
9316
|
+
}
|
|
9317
|
+
sawRetry = true;
|
|
9318
|
+
await backoff(attempt, initialBackoffMs, backoffMultiplier, maxElapsedMs - (Date.now() - startedAt));
|
|
9319
|
+
} catch (err) {
|
|
9320
|
+
clearTimeout(timer);
|
|
9321
|
+
if (attempt >= maxAttempts) {
|
|
9322
|
+
logOutcome(provider, accountKey, sawRetry ? "retried-then-failed" : "failed", method, label, attempt, startedAt);
|
|
9323
|
+
throw err;
|
|
9324
|
+
}
|
|
9325
|
+
sawRetry = true;
|
|
9326
|
+
await backoff(attempt, initialBackoffMs, backoffMultiplier, maxElapsedMs - (Date.now() - startedAt));
|
|
9074
9327
|
}
|
|
9075
|
-
return null;
|
|
9076
|
-
} catch {
|
|
9077
|
-
return null;
|
|
9078
9328
|
}
|
|
9079
9329
|
}
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9330
|
+
var DbJunctionTimeoutError = class extends Error {
|
|
9331
|
+
constructor(ms) {
|
|
9332
|
+
super(`junction: db query exceeded its ${ms}ms timeout`);
|
|
9333
|
+
this.name = "DbJunctionTimeoutError";
|
|
9083
9334
|
}
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9335
|
+
};
|
|
9336
|
+
function withTimeout(run, timeoutMs) {
|
|
9337
|
+
return new Promise((resolve, reject) => {
|
|
9338
|
+
const timer = setTimeout(() => reject(new DbJunctionTimeoutError(timeoutMs)), timeoutMs);
|
|
9339
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
9340
|
+
run().then(
|
|
9341
|
+
(value) => {
|
|
9342
|
+
clearTimeout(timer);
|
|
9343
|
+
resolve(value);
|
|
9344
|
+
},
|
|
9345
|
+
(err) => {
|
|
9346
|
+
clearTimeout(timer);
|
|
9347
|
+
reject(err);
|
|
9348
|
+
}
|
|
9349
|
+
);
|
|
9350
|
+
});
|
|
9351
|
+
}
|
|
9352
|
+
var RETRYABLE_NODE_ERROR_CODES = /* @__PURE__ */ new Set(["ECONNREFUSED", "ECONNRESET", "ETIMEDOUT", "EHOSTUNREACH", "EAI_AGAIN", "EPIPE"]);
|
|
9353
|
+
function isRetryableDbError(err) {
|
|
9354
|
+
if (err instanceof DbJunctionTimeoutError) return true;
|
|
9355
|
+
const code = err?.code;
|
|
9356
|
+
if (typeof code !== "string") return false;
|
|
9357
|
+
if (code.startsWith("08") || code === "57P03") return true;
|
|
9358
|
+
return RETRYABLE_NODE_ERROR_CODES.has(code);
|
|
9359
|
+
}
|
|
9360
|
+
async function dbJunction(run, policy) {
|
|
9361
|
+
const {
|
|
9362
|
+
provider,
|
|
9363
|
+
accountKey,
|
|
9364
|
+
timeoutMs = JUNCTION_DEFAULT_DB_TIMEOUT_MS,
|
|
9365
|
+
maxAttempts = JUNCTION_DEFAULT_MAX_ATTEMPTS,
|
|
9366
|
+
maxElapsedMs = JUNCTION_DEFAULT_MAX_ELAPSED_MS,
|
|
9367
|
+
initialBackoffMs = JUNCTION_DEFAULT_INITIAL_BACKOFF_MS,
|
|
9368
|
+
backoffMultiplier = JUNCTION_DEFAULT_BACKOFF_MULTIPLIER,
|
|
9369
|
+
rateLimit = defaultRateLimitFor(provider)
|
|
9370
|
+
} = policy;
|
|
9371
|
+
const startedAt = Date.now();
|
|
9372
|
+
let attempt = 0;
|
|
9373
|
+
let sawRetry = false;
|
|
9374
|
+
for (; ; ) {
|
|
9375
|
+
attempt++;
|
|
9376
|
+
const remainingBudget = maxElapsedMs - (Date.now() - startedAt);
|
|
9377
|
+
if (remainingBudget <= 0) {
|
|
9378
|
+
logOutcome(provider, accountKey, "retried-then-failed", "QUERY", "db", attempt - 1, startedAt);
|
|
9379
|
+
throw new Error(
|
|
9380
|
+
`junction: ${provider}:${accountKey} db query exceeded its wall-clock budget (${maxElapsedMs}ms) after ${attempt - 1} attempt(s)`
|
|
9381
|
+
);
|
|
9382
|
+
}
|
|
9383
|
+
try {
|
|
9384
|
+
await acquireToken(provider, accountKey, rateLimit, remainingBudget);
|
|
9385
|
+
} catch (err) {
|
|
9386
|
+
if (err instanceof RateLimitExceededError) {
|
|
9387
|
+
logOutcome(provider, accountKey, "rate-limited", "QUERY", "db", attempt, startedAt);
|
|
9388
|
+
}
|
|
9389
|
+
throw err;
|
|
9390
|
+
}
|
|
9391
|
+
try {
|
|
9392
|
+
const result = await withTimeout(run, timeoutMs);
|
|
9393
|
+
logOutcome(provider, accountKey, sawRetry ? "retried-then-succeeded" : "success", "QUERY", "db", attempt, startedAt);
|
|
9394
|
+
return result;
|
|
9395
|
+
} catch (err) {
|
|
9396
|
+
if (!isRetryableDbError(err) || attempt >= maxAttempts) {
|
|
9397
|
+
logOutcome(provider, accountKey, sawRetry ? "retried-then-failed" : "failed", "QUERY", "db", attempt, startedAt);
|
|
9398
|
+
throw err;
|
|
9399
|
+
}
|
|
9400
|
+
sawRetry = true;
|
|
9401
|
+
await backoff(attempt, initialBackoffMs, backoffMultiplier, maxElapsedMs - (Date.now() - startedAt));
|
|
9402
|
+
}
|
|
9090
9403
|
}
|
|
9091
9404
|
}
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9405
|
+
|
|
9406
|
+
// src/connectors/supabase/index.ts
|
|
9407
|
+
init_cjs_shims();
|
|
9408
|
+
|
|
9409
|
+
// src/connectors/supabase/client.ts
|
|
9410
|
+
init_cjs_shims();
|
|
9411
|
+
var DEFAULT_SUPABASE_MANAGEMENT_API_URL = "https://api.supabase.com";
|
|
9412
|
+
var DEFAULT_LOG_LIMIT = 1e3;
|
|
9413
|
+
var SUPABASE_LOG_QUERY_MAX_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
9414
|
+
function boundedSupabaseLogWindow(since, now, maxLookbackMs) {
|
|
9415
|
+
const window = Math.min(maxLookbackMs, SUPABASE_LOG_QUERY_MAX_WINDOW_MS);
|
|
9416
|
+
const floor = new Date(now.getTime() - window);
|
|
9417
|
+
const endIso = now.toISOString();
|
|
9418
|
+
if (!since) return { startIso: floor.toISOString(), endIso, truncated: false };
|
|
9419
|
+
const sinceMs = new Date(since).getTime();
|
|
9420
|
+
if (Number.isNaN(sinceMs)) return { startIso: floor.toISOString(), endIso, truncated: false };
|
|
9421
|
+
if (sinceMs < floor.getTime()) return { startIso: floor.toISOString(), endIso, truncated: true };
|
|
9422
|
+
return { startIso: new Date(sinceMs).toISOString(), endIso, truncated: false };
|
|
9423
|
+
}
|
|
9424
|
+
function buildEdgeLogsQuery(limit) {
|
|
9425
|
+
const safeLimit = Math.max(1, Math.trunc(limit) || DEFAULT_LOG_LIMIT);
|
|
9426
|
+
return [
|
|
9427
|
+
"select",
|
|
9428
|
+
" format_timestamp('%Y-%m-%dT%H:%M:%E6SZ', timestamp) as timestamp,",
|
|
9429
|
+
" request.method as method,",
|
|
9430
|
+
" request.path as path,",
|
|
9431
|
+
" response.status_code as status_code",
|
|
9432
|
+
"from edge_logs",
|
|
9433
|
+
"cross join unnest(metadata) as metadata",
|
|
9434
|
+
"cross join unnest(metadata.request) as request",
|
|
9435
|
+
"cross join unnest(metadata.response) as response",
|
|
9436
|
+
"where regexp_contains(request.path, '^/rest/v1/')",
|
|
9437
|
+
"order by timestamp asc",
|
|
9438
|
+
`limit ${safeLimit}`
|
|
9439
|
+
].join("\n");
|
|
9440
|
+
}
|
|
9441
|
+
async function fetchSupabaseEdgeLogs(config, token, startIso, endIso, fetchImpl = fetch) {
|
|
9442
|
+
const baseUrl = config.managementApiUrl ?? DEFAULT_SUPABASE_MANAGEMENT_API_URL;
|
|
9443
|
+
const url = new URL(`${baseUrl}/v1/projects/${config.apiProjectRef}/analytics/endpoints/logs.all`);
|
|
9444
|
+
url.searchParams.set("sql", buildEdgeLogsQuery(config.logLimit ?? DEFAULT_LOG_LIMIT));
|
|
9445
|
+
url.searchParams.set("iso_timestamp_start", startIso);
|
|
9446
|
+
url.searchParams.set("iso_timestamp_end", endIso);
|
|
9447
|
+
const res = await junctionFetch(
|
|
9448
|
+
url,
|
|
9449
|
+
{ method: "GET", headers: bearerAuthHeader(token) },
|
|
9450
|
+
// accountKey: the Supabase project ref (ADR-131's own worked example) —
|
|
9451
|
+
// the Management API's rate limit is enforced per project.
|
|
9452
|
+
{ provider: "supabase", accountKey: config.apiProjectRef, fetchImpl }
|
|
9453
|
+
);
|
|
9454
|
+
if (!res.ok) {
|
|
9455
|
+
throw new Error(`supabase connector: logs.all request failed (${res.status} ${res.statusText})`);
|
|
9456
|
+
}
|
|
9457
|
+
const body = await res.json();
|
|
9458
|
+
if (body.error) {
|
|
9459
|
+
const message = typeof body.error === "string" ? body.error : body.error.message;
|
|
9460
|
+
throw new Error(`supabase connector: logs.all returned an error (${message})`);
|
|
9101
9461
|
}
|
|
9462
|
+
return body.result ?? [];
|
|
9102
9463
|
}
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9464
|
+
|
|
9465
|
+
// src/connectors/supabase/map.ts
|
|
9466
|
+
init_cjs_shims();
|
|
9467
|
+
|
|
9468
|
+
// src/connectors/supabase/types.ts
|
|
9469
|
+
init_cjs_shims();
|
|
9470
|
+
function readSupabaseCredentials(raw) {
|
|
9471
|
+
const managementToken = raw["managementToken"];
|
|
9472
|
+
if (typeof managementToken !== "string" || managementToken.length === 0) {
|
|
9473
|
+
throw new Error("supabase connector: credentials.managementToken must be a non-empty string");
|
|
9108
9474
|
}
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9475
|
+
const postgresConnectionString = raw["postgresConnectionString"];
|
|
9476
|
+
if (postgresConnectionString !== void 0 && (typeof postgresConnectionString !== "string" || postgresConnectionString.length === 0)) {
|
|
9477
|
+
throw new Error(
|
|
9478
|
+
"supabase connector: credentials.postgresConnectionString must be a non-empty string when present"
|
|
9479
|
+
);
|
|
9112
9480
|
}
|
|
9481
|
+
return {
|
|
9482
|
+
managementToken,
|
|
9483
|
+
...postgresConnectionString ? { postgresConnectionString } : {}
|
|
9484
|
+
};
|
|
9113
9485
|
}
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9486
|
+
var SUPABASE_TABLE_TARGET_KIND = "supabase-table";
|
|
9487
|
+
var SUPABASE_RPC_TARGET_KIND = "supabase-rpc";
|
|
9488
|
+
|
|
9489
|
+
// src/connectors/supabase/map.ts
|
|
9490
|
+
var REST_RPC_PATH_RE = /^\/rest\/v1\/rpc\/([^/?]+)/;
|
|
9491
|
+
var REST_TABLE_PATH_RE = /^\/rest\/v1\/([^/?]+)/;
|
|
9492
|
+
function targetFromRestPath(path57) {
|
|
9493
|
+
const rpcMatch = REST_RPC_PATH_RE.exec(path57);
|
|
9494
|
+
if (rpcMatch) return { targetKind: SUPABASE_RPC_TARGET_KIND, name: rpcMatch[1] };
|
|
9495
|
+
const tableMatch = REST_TABLE_PATH_RE.exec(path57);
|
|
9496
|
+
if (tableMatch) return { targetKind: SUPABASE_TABLE_TARGET_KIND, name: tableMatch[1] };
|
|
9497
|
+
return null;
|
|
9498
|
+
}
|
|
9499
|
+
var ERROR_STATUS_THRESHOLD = 500;
|
|
9500
|
+
function mapEdgeLogRowsToSignals(rows) {
|
|
9501
|
+
const buckets2 = /* @__PURE__ */ new Map();
|
|
9502
|
+
for (const row of rows) {
|
|
9503
|
+
const target = targetFromRestPath(row.path);
|
|
9504
|
+
if (!target) continue;
|
|
9505
|
+
const key = `${target.targetKind}:${target.name}`;
|
|
9506
|
+
const isError = row.status_code >= ERROR_STATUS_THRESHOLD;
|
|
9507
|
+
const existing = buckets2.get(key);
|
|
9508
|
+
if (existing) {
|
|
9509
|
+
existing.callCount += 1;
|
|
9510
|
+
if (isError) existing.errorCount += 1;
|
|
9511
|
+
if (row.timestamp > existing.lastObservedIso) existing.lastObservedIso = row.timestamp;
|
|
9512
|
+
} else {
|
|
9513
|
+
buckets2.set(key, {
|
|
9514
|
+
targetKind: target.targetKind,
|
|
9515
|
+
targetName: target.name,
|
|
9516
|
+
callCount: 1,
|
|
9517
|
+
errorCount: isError ? 1 : 0,
|
|
9518
|
+
lastObservedIso: row.timestamp
|
|
9519
|
+
});
|
|
9520
|
+
}
|
|
9120
9521
|
}
|
|
9121
|
-
return
|
|
9522
|
+
return [...buckets2.values()].map((b) => ({
|
|
9523
|
+
targetKind: b.targetKind,
|
|
9524
|
+
targetName: b.targetName,
|
|
9525
|
+
callCount: b.callCount,
|
|
9526
|
+
errorCount: b.errorCount,
|
|
9527
|
+
lastObservedIso: b.lastObservedIso
|
|
9528
|
+
}));
|
|
9122
9529
|
}
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
|
|
9127
|
-
if (!
|
|
9128
|
-
|
|
9530
|
+
var FROM_TABLE_RE = /\bfrom\s+"?(?:[a-z_][a-z0-9_]*"?\.)?"?([a-z_][a-z0-9_]*)"?/i;
|
|
9531
|
+
var SYSTEM_SCHEMA_PREFIXES = ["pg_", "information_schema"];
|
|
9532
|
+
function tableNameFromQueryText(query) {
|
|
9533
|
+
const match = FROM_TABLE_RE.exec(query);
|
|
9534
|
+
if (!match) return null;
|
|
9535
|
+
const name = match[1];
|
|
9536
|
+
const lower = name.toLowerCase();
|
|
9537
|
+
if (SYSTEM_SCHEMA_PREFIXES.some((prefix) => lower.startsWith(prefix))) return null;
|
|
9538
|
+
return name;
|
|
9539
|
+
}
|
|
9540
|
+
function diffPgStatStatementsToSignals(rows, previous, nowIso2) {
|
|
9541
|
+
const signals = [];
|
|
9542
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9543
|
+
for (const row of rows) {
|
|
9544
|
+
seen.add(row.queryid);
|
|
9545
|
+
const calls = Number(row.calls);
|
|
9546
|
+
const prior = previous.get(row.queryid);
|
|
9547
|
+
previous.set(row.queryid, { calls });
|
|
9548
|
+
if (!prior || calls < prior.calls) continue;
|
|
9549
|
+
const delta = calls - prior.calls;
|
|
9550
|
+
if (delta <= 0) continue;
|
|
9551
|
+
const table = tableNameFromQueryText(row.query);
|
|
9552
|
+
if (!table) continue;
|
|
9553
|
+
signals.push({
|
|
9554
|
+
targetKind: SUPABASE_TABLE_TARGET_KIND,
|
|
9555
|
+
targetName: table,
|
|
9556
|
+
callCount: delta,
|
|
9557
|
+
errorCount: 0,
|
|
9558
|
+
lastObservedIso: nowIso2
|
|
9559
|
+
});
|
|
9560
|
+
}
|
|
9561
|
+
for (const queryid of [...previous.keys()]) {
|
|
9562
|
+
if (!seen.has(queryid)) previous.delete(queryid);
|
|
9563
|
+
}
|
|
9564
|
+
return signals;
|
|
9129
9565
|
}
|
|
9130
9566
|
|
|
9131
|
-
// src/
|
|
9132
|
-
init_otel_grpc();
|
|
9133
|
-
|
|
9134
|
-
// src/search.ts
|
|
9567
|
+
// src/connectors/supabase/postgres-client.ts
|
|
9135
9568
|
init_cjs_shims();
|
|
9136
|
-
var
|
|
9137
|
-
var
|
|
9138
|
-
var
|
|
9139
|
-
var
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9569
|
+
var import_pg = __toESM(require("pg"), 1);
|
|
9570
|
+
var { Client } = import_pg.default;
|
|
9571
|
+
var DEFAULT_STATEMENT_LIMIT = 500;
|
|
9572
|
+
var STATEMENTS_QUERY = `
|
|
9573
|
+
select queryid, query, calls, total_exec_time, rows
|
|
9574
|
+
from pg_stat_statements
|
|
9575
|
+
where query ~* '^\\s*select\\b'
|
|
9576
|
+
order by calls desc
|
|
9577
|
+
limit $1
|
|
9578
|
+
`;
|
|
9579
|
+
async function fetchPgStatStatements(connectionString, limit = DEFAULT_STATEMENT_LIMIT, accountKey = "unknown", clientFactory = (cs) => new Client({ connectionString: cs })) {
|
|
9580
|
+
return dbJunction(
|
|
9581
|
+
async () => {
|
|
9582
|
+
const client = clientFactory(connectionString);
|
|
9583
|
+
await client.connect();
|
|
9584
|
+
try {
|
|
9585
|
+
await client.query("SET default_transaction_read_only = on");
|
|
9586
|
+
const result = await client.query(STATEMENTS_QUERY, [limit]);
|
|
9587
|
+
return result.rows;
|
|
9588
|
+
} finally {
|
|
9589
|
+
await client.end();
|
|
9590
|
+
}
|
|
9591
|
+
},
|
|
9592
|
+
{ provider: "supabase-postgres", accountKey }
|
|
9593
|
+
);
|
|
9144
9594
|
}
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
break;
|
|
9154
|
-
}
|
|
9155
|
-
case "DatabaseNode": {
|
|
9156
|
-
const eng = node.engine;
|
|
9157
|
-
const ver = node.engineVersion;
|
|
9158
|
-
if (eng) parts.push(`engine=${eng}`);
|
|
9159
|
-
if (ver) parts.push(`engineVersion=${ver}`);
|
|
9160
|
-
break;
|
|
9161
|
-
}
|
|
9162
|
-
case "InfraNode": {
|
|
9163
|
-
const kind = node.kind;
|
|
9164
|
-
if (kind) parts.push(`kind=${kind}`);
|
|
9165
|
-
break;
|
|
9595
|
+
|
|
9596
|
+
// src/connectors/supabase/resolve.ts
|
|
9597
|
+
init_cjs_shims();
|
|
9598
|
+
var import_types32 = require("@neat.is/types");
|
|
9599
|
+
function createSupabaseResolveTarget(graph, config) {
|
|
9600
|
+
return (signal, _ctx) => {
|
|
9601
|
+
if (signal.targetKind !== SUPABASE_TABLE_TARGET_KIND && signal.targetKind !== SUPABASE_RPC_TARGET_KIND) {
|
|
9602
|
+
return null;
|
|
9166
9603
|
}
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
break;
|
|
9604
|
+
const subResourceId = (0, import_types32.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
|
|
9605
|
+
if (graph.hasNode(subResourceId)) {
|
|
9606
|
+
return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types32.EdgeType.CALLS };
|
|
9171
9607
|
}
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
if (method) parts.push(`method=${method}`);
|
|
9176
|
-
if (tmpl) parts.push(`path=${tmpl}`);
|
|
9177
|
-
break;
|
|
9608
|
+
const projectLevelId = (0, import_types32.infraId)("supabase", config.nodeRef);
|
|
9609
|
+
if (graph.hasNode(projectLevelId)) {
|
|
9610
|
+
return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types32.EdgeType.CALLS };
|
|
9178
9611
|
}
|
|
9179
|
-
|
|
9180
|
-
|
|
9181
|
-
|
|
9182
|
-
|
|
9612
|
+
return null;
|
|
9613
|
+
};
|
|
9614
|
+
}
|
|
9615
|
+
|
|
9616
|
+
// src/connectors/supabase/index.ts
|
|
9617
|
+
var DEFAULT_MAX_LOOKBACK_MS = 24 * 60 * 60 * 1e3;
|
|
9618
|
+
var SupabaseConnector = class {
|
|
9619
|
+
// `deps.fetchPgStatStatements` defaults to the real Postgres-backed
|
|
9620
|
+
// implementation; tests override it to exercise the "both surfaces
|
|
9621
|
+
// combine" and "surface 2 only runs when a connection string is present"
|
|
9622
|
+
// behavior without a live database — the same dependency-injection seam
|
|
9623
|
+
// `fetchImpl` gives cloudflare/client.ts's tests for `fetch`.
|
|
9624
|
+
constructor(config, deps = {}) {
|
|
9625
|
+
this.config = config;
|
|
9626
|
+
this.deps = deps;
|
|
9627
|
+
}
|
|
9628
|
+
config;
|
|
9629
|
+
deps;
|
|
9630
|
+
provider = "supabase";
|
|
9631
|
+
// pg_stat_statements.calls is cumulative, not per-window (map.ts's
|
|
9632
|
+
// diffPgStatStatementsToSignals doc comment) — this Map carries the
|
|
9633
|
+
// previous poll's counts across ticks, the same way
|
|
9634
|
+
// `startConnectorPollLoop` (connectors/index.ts) carries `since` across
|
|
9635
|
+
// ticks for every connector. Lives on the instance, not `ConnectorContext`,
|
|
9636
|
+
// because `ConnectorContext` is rebuilt fresh per tick (connectors/index.ts's
|
|
9637
|
+
// `{ ...ctx, since }`) while this connector object is the one thing every
|
|
9638
|
+
// tick shares.
|
|
9639
|
+
statementBaselines = /* @__PURE__ */ new Map();
|
|
9640
|
+
async poll(ctx) {
|
|
9641
|
+
const creds = readSupabaseCredentials(ctx.credentials);
|
|
9642
|
+
const now = /* @__PURE__ */ new Date();
|
|
9643
|
+
const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS;
|
|
9644
|
+
const { startIso, endIso } = boundedSupabaseLogWindow(ctx.since, now, maxLookbackMs);
|
|
9645
|
+
const logRows = await fetchSupabaseEdgeLogs(this.config, creds.managementToken, startIso, endIso);
|
|
9646
|
+
const signals = mapEdgeLogRowsToSignals(logRows);
|
|
9647
|
+
if (creds.postgresConnectionString) {
|
|
9648
|
+
const fetchStatements = this.deps.fetchPgStatStatements ?? fetchPgStatStatements;
|
|
9649
|
+
const statementRows = await fetchStatements(
|
|
9650
|
+
creds.postgresConnectionString,
|
|
9651
|
+
this.config.statementLimit ?? DEFAULT_STATEMENT_LIMIT,
|
|
9652
|
+
this.config.apiProjectRef
|
|
9653
|
+
);
|
|
9654
|
+
signals.push(...diffPgStatStatementsToSignals(statementRows, this.statementBaselines, now.toISOString()));
|
|
9183
9655
|
}
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
9656
|
+
return signals;
|
|
9657
|
+
}
|
|
9658
|
+
};
|
|
9659
|
+
function createSupabaseConnector(graph, config, deps = {}) {
|
|
9660
|
+
return {
|
|
9661
|
+
connector: new SupabaseConnector(config, deps),
|
|
9662
|
+
resolveTarget: createSupabaseResolveTarget(graph, config)
|
|
9663
|
+
};
|
|
9664
|
+
}
|
|
9665
|
+
|
|
9666
|
+
// src/connectors/railway/index.ts
|
|
9667
|
+
init_cjs_shims();
|
|
9668
|
+
var import_types36 = require("@neat.is/types");
|
|
9669
|
+
|
|
9670
|
+
// src/connectors/railway/client.ts
|
|
9671
|
+
init_cjs_shims();
|
|
9672
|
+
var DEFAULT_RAILWAY_API_URL = "https://backboard.railway.com/graphql/v2";
|
|
9673
|
+
var DEFAULT_MAX_LOOKBACK_MS2 = 24 * 60 * 60 * 1e3;
|
|
9674
|
+
var DEFAULT_LOG_LIMIT2 = 1e3;
|
|
9675
|
+
function readRailwayToken(credentials) {
|
|
9676
|
+
const token = credentials.token;
|
|
9677
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
9678
|
+
throw new Error(
|
|
9679
|
+
"Railway connector requires ctx.credentials.token (a Project-Access-Token or account Bearer token)"
|
|
9680
|
+
);
|
|
9681
|
+
}
|
|
9682
|
+
return token;
|
|
9683
|
+
}
|
|
9684
|
+
async function railwayGraphQL(apiUrl, token, query, variables, accountKey) {
|
|
9685
|
+
const res = await junctionFetch(
|
|
9686
|
+
apiUrl,
|
|
9687
|
+
{
|
|
9688
|
+
method: "POST",
|
|
9689
|
+
headers: {
|
|
9690
|
+
"Content-Type": "application/json",
|
|
9691
|
+
// docs.railway.com/integrations/api/graphql-overview confirms
|
|
9692
|
+
// `Authorization: Bearer <token>` for an account/workspace token;
|
|
9693
|
+
// Railway also documents a dedicated `Project-Access-Token: <token>`
|
|
9694
|
+
// header for the project-scoped token ADR-127 targets specifically.
|
|
9695
|
+
// This connector sends the Bearer form — needs-endpoint-testing
|
|
9696
|
+
// whether a live Project-Access-Token requires the dedicated header
|
|
9697
|
+
// instead once this poller runs against a real project.
|
|
9698
|
+
...bearerAuthHeader(token)
|
|
9699
|
+
},
|
|
9700
|
+
body: JSON.stringify({ query, variables })
|
|
9701
|
+
},
|
|
9702
|
+
{ provider: "railway", accountKey }
|
|
9703
|
+
);
|
|
9704
|
+
if (!res.ok) {
|
|
9705
|
+
throw new Error(`Railway GraphQL request failed: ${res.status} ${res.statusText}`);
|
|
9706
|
+
}
|
|
9707
|
+
const body = await res.json();
|
|
9708
|
+
if (body.errors && body.errors.length > 0) {
|
|
9709
|
+
throw new Error(`Railway GraphQL errors: ${body.errors.map((e) => e.message).join("; ")}`);
|
|
9710
|
+
}
|
|
9711
|
+
if (!body.data) throw new Error("Railway GraphQL response carried no data");
|
|
9712
|
+
return body.data;
|
|
9713
|
+
}
|
|
9714
|
+
var HTTP_LOGS_QUERY = `
|
|
9715
|
+
query HttpLogs(
|
|
9716
|
+
$environmentId: String!
|
|
9717
|
+
$serviceId: String!
|
|
9718
|
+
$startDate: String!
|
|
9719
|
+
$endDate: String!
|
|
9720
|
+
$limit: Int
|
|
9721
|
+
) {
|
|
9722
|
+
httpLogs(
|
|
9723
|
+
environmentId: $environmentId
|
|
9724
|
+
serviceId: $serviceId
|
|
9725
|
+
startDate: $startDate
|
|
9726
|
+
endDate: $endDate
|
|
9727
|
+
limit: $limit
|
|
9728
|
+
) {
|
|
9729
|
+
timestamp
|
|
9730
|
+
method
|
|
9731
|
+
path
|
|
9732
|
+
httpStatus
|
|
9733
|
+
totalDuration
|
|
9734
|
+
requestId
|
|
9735
|
+
deploymentId
|
|
9736
|
+
edgeRegion
|
|
9737
|
+
}
|
|
9738
|
+
}
|
|
9739
|
+
`;
|
|
9740
|
+
var NETWORK_FLOW_LOGS_QUERY = `
|
|
9741
|
+
query NetworkFlowLogs(
|
|
9742
|
+
$environmentId: String!
|
|
9743
|
+
$serviceId: String!
|
|
9744
|
+
$startDate: String!
|
|
9745
|
+
$endDate: String!
|
|
9746
|
+
$limit: Int
|
|
9747
|
+
) {
|
|
9748
|
+
networkFlowLogs(
|
|
9749
|
+
environmentId: $environmentId
|
|
9750
|
+
serviceId: $serviceId
|
|
9751
|
+
startDate: $startDate
|
|
9752
|
+
endDate: $endDate
|
|
9753
|
+
limit: $limit
|
|
9754
|
+
) {
|
|
9755
|
+
timestamp
|
|
9756
|
+
peerServiceId
|
|
9757
|
+
peerKind
|
|
9758
|
+
direction
|
|
9759
|
+
byteCount
|
|
9760
|
+
packetCount
|
|
9761
|
+
dropCause
|
|
9762
|
+
}
|
|
9763
|
+
}
|
|
9764
|
+
`;
|
|
9765
|
+
async function fetchRailwayHttpLogs(config, token, startDate, endDate) {
|
|
9766
|
+
const data = await railwayGraphQL(
|
|
9767
|
+
config.apiUrl ?? DEFAULT_RAILWAY_API_URL,
|
|
9768
|
+
token,
|
|
9769
|
+
HTTP_LOGS_QUERY,
|
|
9770
|
+
{
|
|
9771
|
+
environmentId: config.environmentId,
|
|
9772
|
+
serviceId: config.serviceId,
|
|
9773
|
+
startDate,
|
|
9774
|
+
endDate,
|
|
9775
|
+
limit: config.limit ?? DEFAULT_LOG_LIMIT2
|
|
9776
|
+
},
|
|
9777
|
+
config.environmentId
|
|
9778
|
+
);
|
|
9779
|
+
return data.httpLogs;
|
|
9780
|
+
}
|
|
9781
|
+
async function fetchRailwayNetworkFlowLogs(config, token, startDate, endDate) {
|
|
9782
|
+
const data = await railwayGraphQL(
|
|
9783
|
+
config.apiUrl ?? DEFAULT_RAILWAY_API_URL,
|
|
9784
|
+
token,
|
|
9785
|
+
NETWORK_FLOW_LOGS_QUERY,
|
|
9786
|
+
{
|
|
9787
|
+
environmentId: config.environmentId,
|
|
9788
|
+
serviceId: config.serviceId,
|
|
9789
|
+
startDate,
|
|
9790
|
+
endDate,
|
|
9791
|
+
limit: config.limit ?? DEFAULT_LOG_LIMIT2
|
|
9792
|
+
},
|
|
9793
|
+
config.environmentId
|
|
9794
|
+
);
|
|
9795
|
+
return data.networkFlowLogs;
|
|
9796
|
+
}
|
|
9797
|
+
function boundedRailwayStartDate(since, now, maxLookbackMs) {
|
|
9798
|
+
const floor = new Date(now.getTime() - maxLookbackMs);
|
|
9799
|
+
if (!since) return floor.toISOString();
|
|
9800
|
+
const sinceMs = new Date(since).getTime();
|
|
9801
|
+
if (Number.isNaN(sinceMs)) return floor.toISOString();
|
|
9802
|
+
return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
|
|
9803
|
+
}
|
|
9804
|
+
|
|
9805
|
+
// src/connectors/railway/index.ts
|
|
9806
|
+
var ROUTE_TARGET_KIND = "route";
|
|
9807
|
+
var UNMATCHED_ROUTE_TARGET_KIND = "unmatched-route";
|
|
9808
|
+
var PEER_SERVICE_TARGET_KIND = "peer-service";
|
|
9809
|
+
function buildRailwayRouteIndex(graph, serviceName) {
|
|
9810
|
+
const out = [];
|
|
9811
|
+
graph.forEachNode((_id, attrs) => {
|
|
9812
|
+
const node = attrs;
|
|
9813
|
+
if (node.type !== import_types36.NodeType.RouteNode) return;
|
|
9814
|
+
const route = attrs;
|
|
9815
|
+
if (route.service !== serviceName) return;
|
|
9816
|
+
out.push({
|
|
9817
|
+
method: route.method.toUpperCase(),
|
|
9818
|
+
normalizedPath: normalizePathTemplate(route.pathTemplate),
|
|
9819
|
+
routeNodeId: route.id,
|
|
9820
|
+
path: route.path,
|
|
9821
|
+
line: route.line
|
|
9822
|
+
});
|
|
9823
|
+
});
|
|
9824
|
+
return out;
|
|
9825
|
+
}
|
|
9826
|
+
function findRailwayRoute(entries, method, normalizedPath) {
|
|
9827
|
+
return entries.find(
|
|
9828
|
+
(e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || e.method === method)
|
|
9829
|
+
);
|
|
9830
|
+
}
|
|
9831
|
+
function bucketKey2(method, normalizedPath) {
|
|
9832
|
+
return `${method} ${normalizedPath}`;
|
|
9833
|
+
}
|
|
9834
|
+
function isHttpErrorStatus(status2) {
|
|
9835
|
+
return status2 >= 400;
|
|
9836
|
+
}
|
|
9837
|
+
function upsertBucket(buckets2, key, isError, timestamp, build) {
|
|
9838
|
+
const existing = buckets2.get(key);
|
|
9839
|
+
if (existing) {
|
|
9840
|
+
existing.callCount += 1;
|
|
9841
|
+
if (isError) existing.errorCount += 1;
|
|
9842
|
+
if (timestamp > existing.lastObservedIso) existing.lastObservedIso = timestamp;
|
|
9843
|
+
return;
|
|
9844
|
+
}
|
|
9845
|
+
buckets2.set(key, { callCount: 1, errorCount: isError ? 1 : 0, lastObservedIso: timestamp, ...build() });
|
|
9846
|
+
}
|
|
9847
|
+
function mapRailwayHttpLogsToSignals(entries, routeIndex) {
|
|
9848
|
+
const buckets2 = /* @__PURE__ */ new Map();
|
|
9849
|
+
for (const entry2 of entries) {
|
|
9850
|
+
const method = entry2.method.toUpperCase();
|
|
9851
|
+
const normalizedPath = normalizePathTemplate(entry2.path);
|
|
9852
|
+
const match = findRailwayRoute(routeIndex, method, normalizedPath);
|
|
9853
|
+
const isError = isHttpErrorStatus(entry2.httpStatus);
|
|
9854
|
+
if (match) {
|
|
9855
|
+
upsertBucket(buckets2, `route:${match.routeNodeId}`, isError, entry2.timestamp, () => ({
|
|
9856
|
+
targetKind: ROUTE_TARGET_KIND,
|
|
9857
|
+
targetName: match.routeNodeId,
|
|
9858
|
+
// RouteNode.line is optional in the schema (packages/types/src/
|
|
9859
|
+
// nodes.ts) even though routes.ts always sets it today — skip the
|
|
9860
|
+
// callSite rather than fabricate a line when it's ever absent
|
|
9861
|
+
// (file-awareness.md §6).
|
|
9862
|
+
...match.line !== void 0 ? { callSite: { file: match.path, line: match.line } } : {}
|
|
9863
|
+
}));
|
|
9864
|
+
} else {
|
|
9865
|
+
upsertBucket(
|
|
9866
|
+
buckets2,
|
|
9867
|
+
`unmatched:${bucketKey2(method, normalizedPath)}`,
|
|
9868
|
+
isError,
|
|
9869
|
+
entry2.timestamp,
|
|
9870
|
+
() => ({
|
|
9871
|
+
targetKind: UNMATCHED_ROUTE_TARGET_KIND,
|
|
9872
|
+
targetName: bucketKey2(method, normalizedPath)
|
|
9873
|
+
})
|
|
9874
|
+
);
|
|
9875
|
+
}
|
|
9876
|
+
}
|
|
9877
|
+
return [...buckets2.values()].map((b) => ({
|
|
9878
|
+
targetKind: b.targetKind,
|
|
9879
|
+
targetName: b.targetName,
|
|
9880
|
+
callCount: b.callCount,
|
|
9881
|
+
errorCount: b.errorCount,
|
|
9882
|
+
lastObservedIso: b.lastObservedIso,
|
|
9883
|
+
...b.callSite ? { callSite: b.callSite } : {}
|
|
9884
|
+
}));
|
|
9885
|
+
}
|
|
9886
|
+
function mapRailwayNetworkFlowLogsToSignals(entries) {
|
|
9887
|
+
const buckets2 = /* @__PURE__ */ new Map();
|
|
9888
|
+
for (const entry2 of entries) {
|
|
9889
|
+
if (!entry2.peerServiceId) continue;
|
|
9890
|
+
const isError = entry2.dropCause !== null && entry2.dropCause !== "";
|
|
9891
|
+
upsertBucket(buckets2, entry2.peerServiceId, isError, entry2.timestamp, () => ({
|
|
9892
|
+
targetKind: PEER_SERVICE_TARGET_KIND,
|
|
9893
|
+
targetName: entry2.peerServiceId
|
|
9894
|
+
}));
|
|
9895
|
+
}
|
|
9896
|
+
return [...buckets2.values()].map((b) => ({
|
|
9897
|
+
targetKind: b.targetKind,
|
|
9898
|
+
targetName: b.targetName,
|
|
9899
|
+
callCount: b.callCount,
|
|
9900
|
+
errorCount: b.errorCount,
|
|
9901
|
+
lastObservedIso: b.lastObservedIso
|
|
9902
|
+
}));
|
|
9903
|
+
}
|
|
9904
|
+
function createRailwayResolveTarget(config) {
|
|
9905
|
+
return (signal) => {
|
|
9906
|
+
const serviceName = config.serviceNameById[config.serviceId];
|
|
9907
|
+
if (!serviceName) return null;
|
|
9908
|
+
if (signal.targetKind === ROUTE_TARGET_KIND) {
|
|
9909
|
+
return { targetNodeId: signal.targetName, serviceName, edgeType: import_types36.EdgeType.CALLS };
|
|
9910
|
+
}
|
|
9911
|
+
if (signal.targetKind === PEER_SERVICE_TARGET_KIND) {
|
|
9912
|
+
const peerName = config.serviceNameById[signal.targetName];
|
|
9913
|
+
if (!peerName) return null;
|
|
9914
|
+
return { targetNodeId: (0, import_types36.serviceId)(peerName), serviceName, edgeType: import_types36.EdgeType.CONNECTS_TO };
|
|
9915
|
+
}
|
|
9916
|
+
return null;
|
|
9917
|
+
};
|
|
9918
|
+
}
|
|
9919
|
+
function createRailwayConnector(graph, config) {
|
|
9920
|
+
return {
|
|
9921
|
+
provider: "railway",
|
|
9922
|
+
async poll(ctx) {
|
|
9923
|
+
const token = readRailwayToken(ctx.credentials);
|
|
9924
|
+
const now = /* @__PURE__ */ new Date();
|
|
9925
|
+
const maxLookbackMs = config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS2;
|
|
9926
|
+
const startDate = boundedRailwayStartDate(ctx.since, now, maxLookbackMs);
|
|
9927
|
+
const endDate = now.toISOString();
|
|
9928
|
+
const [httpLogs, flowLogs] = await Promise.all([
|
|
9929
|
+
fetchRailwayHttpLogs(config, token, startDate, endDate),
|
|
9930
|
+
fetchRailwayNetworkFlowLogs(config, token, startDate, endDate)
|
|
9931
|
+
]);
|
|
9932
|
+
const serviceName = config.serviceNameById[config.serviceId];
|
|
9933
|
+
const routeIndex = serviceName ? buildRailwayRouteIndex(graph, serviceName) : [];
|
|
9934
|
+
return [
|
|
9935
|
+
...mapRailwayHttpLogsToSignals(httpLogs, routeIndex),
|
|
9936
|
+
...mapRailwayNetworkFlowLogsToSignals(flowLogs)
|
|
9937
|
+
];
|
|
9938
|
+
}
|
|
9939
|
+
};
|
|
9940
|
+
}
|
|
9941
|
+
|
|
9942
|
+
// src/connectors/firebase/index.ts
|
|
9943
|
+
init_cjs_shims();
|
|
9944
|
+
|
|
9945
|
+
// src/connectors/firebase/logging-api.ts
|
|
9946
|
+
init_cjs_shims();
|
|
9947
|
+
function readFirebaseCredentials(raw) {
|
|
9948
|
+
const projectId = raw["projectId"];
|
|
9949
|
+
const accessToken = raw["accessToken"];
|
|
9950
|
+
if (typeof projectId !== "string" || projectId.length === 0) {
|
|
9951
|
+
throw new Error("firebase connector: credentials.projectId must be a non-empty string");
|
|
9952
|
+
}
|
|
9953
|
+
if (typeof accessToken !== "string" || accessToken.length === 0) {
|
|
9954
|
+
throw new Error("firebase connector: credentials.accessToken must be a non-empty string");
|
|
9955
|
+
}
|
|
9956
|
+
return { projectId, accessToken };
|
|
9957
|
+
}
|
|
9958
|
+
var RESOURCE_TYPES = [
|
|
9959
|
+
"cloud_function",
|
|
9960
|
+
"cloud_run_revision",
|
|
9961
|
+
"firebase_domain"
|
|
9962
|
+
];
|
|
9963
|
+
function isFirebaseResourceType(value) {
|
|
9964
|
+
return RESOURCE_TYPES.includes(value);
|
|
9965
|
+
}
|
|
9966
|
+
function buildEntriesFilter(sinceIso) {
|
|
9967
|
+
return [
|
|
9968
|
+
'resource.type = ("cloud_function" OR "cloud_run_revision" OR "firebase_domain")',
|
|
9969
|
+
"httpRequest:*",
|
|
9970
|
+
`timestamp >= "${sinceIso}"`
|
|
9971
|
+
].join(" AND ");
|
|
9972
|
+
}
|
|
9973
|
+
var DEFAULT_LOOKBACK_MS = 24 * 60 * 60 * 1e3;
|
|
9974
|
+
var ENTRIES_LIST_URL = "https://logging.googleapis.com/v2/entries:list";
|
|
9975
|
+
var PAGE_SIZE = 1e3;
|
|
9976
|
+
var MAX_PAGES = 20;
|
|
9977
|
+
async function fetchHttpRequestLogEntries(creds, sinceIso) {
|
|
9978
|
+
const filter = buildEntriesFilter(sinceIso);
|
|
9979
|
+
const out = [];
|
|
9980
|
+
let pageToken;
|
|
9981
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
9982
|
+
const body = {
|
|
9983
|
+
resourceNames: [`projects/${creds.projectId}`],
|
|
9984
|
+
filter,
|
|
9985
|
+
orderBy: "timestamp asc",
|
|
9986
|
+
pageSize: PAGE_SIZE,
|
|
9987
|
+
...pageToken ? { pageToken } : {}
|
|
9988
|
+
};
|
|
9989
|
+
const res = await junctionFetch(
|
|
9990
|
+
ENTRIES_LIST_URL,
|
|
9991
|
+
{
|
|
9992
|
+
method: "POST",
|
|
9993
|
+
headers: {
|
|
9994
|
+
...bearerAuthHeader(creds.accessToken),
|
|
9995
|
+
"Content-Type": "application/json"
|
|
9996
|
+
},
|
|
9997
|
+
body: JSON.stringify(body)
|
|
9998
|
+
},
|
|
9999
|
+
// accountKey: the GCP project id (ADR-131's own worked example for
|
|
10000
|
+
// Firebase) — one customer's Cloud Logging quota is scoped per GCP
|
|
10001
|
+
// project, not per Firebase site/function.
|
|
10002
|
+
{ provider: "firebase", accountKey: creds.projectId }
|
|
10003
|
+
);
|
|
10004
|
+
if (!res.ok) {
|
|
10005
|
+
throw new Error(`Cloud Logging entries.list failed: ${res.status} ${res.statusText}`);
|
|
10006
|
+
}
|
|
10007
|
+
const json = await res.json();
|
|
10008
|
+
out.push(...json.entries ?? []);
|
|
10009
|
+
if (!json.nextPageToken) break;
|
|
10010
|
+
pageToken = json.nextPageToken;
|
|
10011
|
+
}
|
|
10012
|
+
return out;
|
|
10013
|
+
}
|
|
10014
|
+
|
|
10015
|
+
// src/connectors/firebase/map.ts
|
|
10016
|
+
init_cjs_shims();
|
|
10017
|
+
var FIELD_SEP = "\0";
|
|
10018
|
+
function packFirebaseTargetName(identity) {
|
|
10019
|
+
return [identity.resourceName, identity.method, identity.path].join(FIELD_SEP);
|
|
10020
|
+
}
|
|
10021
|
+
function parseFirebaseTargetName(targetName) {
|
|
10022
|
+
const firstSep = targetName.indexOf(FIELD_SEP);
|
|
10023
|
+
if (firstSep === -1) return null;
|
|
10024
|
+
const resourceName = targetName.slice(0, firstSep);
|
|
10025
|
+
const rest = targetName.slice(firstSep + 1);
|
|
10026
|
+
const secondSep = rest.indexOf(FIELD_SEP);
|
|
10027
|
+
if (secondSep === -1) return null;
|
|
10028
|
+
const method = rest.slice(0, secondSep);
|
|
10029
|
+
const path57 = rest.slice(secondSep + 1);
|
|
10030
|
+
if (!resourceName || !method || !path57) return null;
|
|
10031
|
+
return { resourceName, method, path: path57 };
|
|
10032
|
+
}
|
|
10033
|
+
function resourceNameFor(type, labels) {
|
|
10034
|
+
if (!labels) return null;
|
|
10035
|
+
switch (type) {
|
|
10036
|
+
case "cloud_function":
|
|
10037
|
+
return labels["function_name"] ?? null;
|
|
10038
|
+
case "cloud_run_revision":
|
|
10039
|
+
return labels["service_name"] ?? null;
|
|
10040
|
+
case "firebase_domain":
|
|
10041
|
+
return labels["site_name"] ?? null;
|
|
10042
|
+
}
|
|
10043
|
+
}
|
|
10044
|
+
function pathFromRequestUrl(requestUrl) {
|
|
10045
|
+
if (!requestUrl) return null;
|
|
10046
|
+
if (requestUrl.startsWith("/")) {
|
|
10047
|
+
const withoutQuery = requestUrl.split("?")[0];
|
|
10048
|
+
return withoutQuery && withoutQuery.length > 0 ? withoutQuery : "/";
|
|
10049
|
+
}
|
|
10050
|
+
try {
|
|
10051
|
+
const candidate = requestUrl.startsWith("//") ? `https:${requestUrl}` : requestUrl;
|
|
10052
|
+
const parsed = new URL(candidate);
|
|
10053
|
+
return parsed.pathname || "/";
|
|
10054
|
+
} catch {
|
|
10055
|
+
return null;
|
|
10056
|
+
}
|
|
10057
|
+
}
|
|
10058
|
+
var ERROR_STATUS_THRESHOLD2 = 500;
|
|
10059
|
+
function mapLogEntryToSignal(entry2) {
|
|
10060
|
+
const resourceType = entry2.resource?.type;
|
|
10061
|
+
if (!resourceType || !isFirebaseResourceType(resourceType)) return null;
|
|
10062
|
+
const resourceName = resourceNameFor(resourceType, entry2.resource?.labels);
|
|
10063
|
+
if (!resourceName) return null;
|
|
10064
|
+
const req = entry2.httpRequest;
|
|
10065
|
+
if (!req) return null;
|
|
10066
|
+
if (!req.requestMethod) return null;
|
|
10067
|
+
const method = req.requestMethod.toUpperCase();
|
|
10068
|
+
const path57 = pathFromRequestUrl(req.requestUrl);
|
|
10069
|
+
if (path57 === null) return null;
|
|
10070
|
+
const timestamp = entry2.timestamp;
|
|
10071
|
+
if (!timestamp) return null;
|
|
10072
|
+
const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD2;
|
|
10073
|
+
return {
|
|
10074
|
+
targetKind: resourceType,
|
|
10075
|
+
targetName: packFirebaseTargetName({ resourceName, method, path: path57 }),
|
|
10076
|
+
callCount: 1,
|
|
10077
|
+
errorCount: isError ? 1 : 0,
|
|
10078
|
+
lastObservedIso: timestamp
|
|
10079
|
+
};
|
|
10080
|
+
}
|
|
10081
|
+
function mapLogEntriesToSignals(entries) {
|
|
10082
|
+
const out = [];
|
|
10083
|
+
for (const entry2 of entries) {
|
|
10084
|
+
const signal = mapLogEntryToSignal(entry2);
|
|
10085
|
+
if (signal) out.push(signal);
|
|
10086
|
+
}
|
|
10087
|
+
return out;
|
|
10088
|
+
}
|
|
10089
|
+
|
|
10090
|
+
// src/connectors/firebase/resolve.ts
|
|
10091
|
+
init_cjs_shims();
|
|
10092
|
+
var import_types37 = require("@neat.is/types");
|
|
10093
|
+
function neatServiceNameFor(resourceType, resourceName, serviceMap) {
|
|
10094
|
+
switch (resourceType) {
|
|
10095
|
+
case "cloud_function":
|
|
10096
|
+
return serviceMap.functions?.[resourceName] ?? null;
|
|
10097
|
+
case "cloud_run_revision":
|
|
10098
|
+
return serviceMap.cloudRun?.[resourceName] ?? null;
|
|
10099
|
+
case "firebase_domain":
|
|
10100
|
+
return serviceMap.hosting?.[resourceName] ?? null;
|
|
10101
|
+
}
|
|
10102
|
+
}
|
|
10103
|
+
function routeEntriesFor(graph, serviceName) {
|
|
10104
|
+
const entries = [];
|
|
10105
|
+
graph.forEachNode((_id, attrs) => {
|
|
10106
|
+
const node = attrs;
|
|
10107
|
+
if (node.type !== import_types37.NodeType.RouteNode) return;
|
|
10108
|
+
const route = attrs;
|
|
10109
|
+
if (route.service !== serviceName) return;
|
|
10110
|
+
entries.push({
|
|
10111
|
+
method: route.method.toUpperCase(),
|
|
10112
|
+
normalizedPath: normalizePathTemplate(route.pathTemplate),
|
|
10113
|
+
routeNodeId: route.id
|
|
10114
|
+
});
|
|
10115
|
+
});
|
|
10116
|
+
return entries;
|
|
10117
|
+
}
|
|
10118
|
+
function findRoute2(entries, method, normalizedPath) {
|
|
10119
|
+
return entries.find(
|
|
10120
|
+
(e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || e.method === method)
|
|
10121
|
+
);
|
|
10122
|
+
}
|
|
10123
|
+
function createFirebaseResolveTarget(graph, serviceMap) {
|
|
10124
|
+
return (signal, _ctx) => {
|
|
10125
|
+
const resourceType = signal.targetKind;
|
|
10126
|
+
if (resourceType !== "cloud_function" && resourceType !== "cloud_run_revision" && resourceType !== "firebase_domain") {
|
|
10127
|
+
return null;
|
|
10128
|
+
}
|
|
10129
|
+
const identity = parseFirebaseTargetName(signal.targetName);
|
|
10130
|
+
if (!identity) return null;
|
|
10131
|
+
const serviceName = neatServiceNameFor(resourceType, identity.resourceName, serviceMap);
|
|
10132
|
+
if (!serviceName) return null;
|
|
10133
|
+
const normalizedPath = normalizePathTemplate(identity.path);
|
|
10134
|
+
const match = findRoute2(routeEntriesFor(graph, serviceName), identity.method, normalizedPath);
|
|
10135
|
+
if (!match) return null;
|
|
10136
|
+
return {
|
|
10137
|
+
targetNodeId: match.routeNodeId,
|
|
10138
|
+
serviceName,
|
|
10139
|
+
edgeType: import_types37.EdgeType.CALLS
|
|
10140
|
+
};
|
|
10141
|
+
};
|
|
10142
|
+
}
|
|
10143
|
+
|
|
10144
|
+
// src/connectors/firebase/index.ts
|
|
10145
|
+
var FirebaseConnector = class {
|
|
10146
|
+
provider = "firebase";
|
|
10147
|
+
async poll(ctx) {
|
|
10148
|
+
const creds = readFirebaseCredentials(ctx.credentials);
|
|
10149
|
+
const sinceIso = ctx.since ?? new Date(Date.now() - DEFAULT_LOOKBACK_MS).toISOString();
|
|
10150
|
+
const entries = await fetchHttpRequestLogEntries(creds, sinceIso);
|
|
10151
|
+
return mapLogEntriesToSignals(entries);
|
|
10152
|
+
}
|
|
10153
|
+
};
|
|
10154
|
+
function createFirebaseConnector(graph, serviceMap) {
|
|
10155
|
+
return {
|
|
10156
|
+
connector: new FirebaseConnector(),
|
|
10157
|
+
resolveTarget: createFirebaseResolveTarget(graph, serviceMap)
|
|
10158
|
+
};
|
|
10159
|
+
}
|
|
10160
|
+
|
|
10161
|
+
// src/connectors/cloudflare/index.ts
|
|
10162
|
+
init_cjs_shims();
|
|
10163
|
+
|
|
10164
|
+
// src/connectors/cloudflare/connector.ts
|
|
10165
|
+
init_cjs_shims();
|
|
10166
|
+
var import_types39 = require("@neat.is/types");
|
|
10167
|
+
|
|
10168
|
+
// src/connectors/cloudflare/client.ts
|
|
10169
|
+
init_cjs_shims();
|
|
10170
|
+
var import_node_crypto3 = require("crypto");
|
|
10171
|
+
var DEFAULT_BASE_URL = "https://api.cloudflare.com/client/v4";
|
|
10172
|
+
var DEFAULT_EVENT_LIMIT = 1e3;
|
|
10173
|
+
async function queryWorkerInvocations(ctx, config, window, fetchImpl = fetch) {
|
|
10174
|
+
const token = ctx.credentials.apiToken;
|
|
10175
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
10176
|
+
throw new Error("cloudflare connector: ctx.credentials.apiToken must be a non-empty string");
|
|
10177
|
+
}
|
|
10178
|
+
const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
10179
|
+
const url = `${baseUrl}/accounts/${config.accountId}/workers/observability/telemetry/query`;
|
|
10180
|
+
const body = {
|
|
10181
|
+
// Cloudflare's schema requires an identifier per query even for an
|
|
10182
|
+
// ad-hoc, unsaved one — a fresh id per tick, never reused.
|
|
10183
|
+
queryId: `neat-connector-${(0, import_node_crypto3.randomUUID)()}`,
|
|
10184
|
+
timeframe: { from: window.fromMs, to: window.toMs },
|
|
10185
|
+
view: "events",
|
|
10186
|
+
limit: config.eventLimit ?? DEFAULT_EVENT_LIMIT,
|
|
10187
|
+
// Execute without persisting — this is a read, not a saved query
|
|
10188
|
+
// (connectors.md §2's "never writes on the read path" applies to
|
|
10189
|
+
// Cloudflare's own query-history state too).
|
|
10190
|
+
dry: true
|
|
10191
|
+
};
|
|
10192
|
+
const res = await junctionFetch(
|
|
10193
|
+
url,
|
|
10194
|
+
{
|
|
10195
|
+
method: "POST",
|
|
10196
|
+
headers: {
|
|
10197
|
+
"Content-Type": "application/json",
|
|
10198
|
+
...bearerAuthHeader(token)
|
|
10199
|
+
},
|
|
10200
|
+
body: JSON.stringify(body)
|
|
10201
|
+
},
|
|
10202
|
+
// accountKey: the Cloudflare account id (ADR-131's own worked example) —
|
|
10203
|
+
// the Telemetry Query API's ~300/5min limit is enforced per account.
|
|
10204
|
+
{ provider: "cloudflare", accountKey: config.accountId, fetchImpl }
|
|
10205
|
+
);
|
|
10206
|
+
if (!res.ok) {
|
|
10207
|
+
throw new Error(
|
|
10208
|
+
`cloudflare connector: telemetry query failed (${res.status} ${res.statusText})`
|
|
10209
|
+
);
|
|
10210
|
+
}
|
|
10211
|
+
const payload = await res.json();
|
|
10212
|
+
if (!payload.success) {
|
|
10213
|
+
const message = payload.errors?.map((e) => e.message).join("; ") || "unknown error";
|
|
10214
|
+
throw new Error(`cloudflare connector: telemetry query returned an error (${message})`);
|
|
10215
|
+
}
|
|
10216
|
+
return payload.result?.events?.events ?? [];
|
|
10217
|
+
}
|
|
10218
|
+
|
|
10219
|
+
// src/connectors/cloudflare/map.ts
|
|
10220
|
+
init_cjs_shims();
|
|
10221
|
+
|
|
10222
|
+
// src/connectors/cloudflare/types.ts
|
|
10223
|
+
init_cjs_shims();
|
|
10224
|
+
var CLOUDFLARE_TARGET_KIND = "cloudflare-worker-invocation";
|
|
10225
|
+
|
|
10226
|
+
// src/connectors/cloudflare/map.ts
|
|
10227
|
+
var HTTP_METHODS = /* @__PURE__ */ new Set([
|
|
10228
|
+
"GET",
|
|
10229
|
+
"POST",
|
|
10230
|
+
"PUT",
|
|
10231
|
+
"PATCH",
|
|
10232
|
+
"DELETE",
|
|
10233
|
+
"HEAD",
|
|
10234
|
+
"OPTIONS",
|
|
10235
|
+
"TRACE",
|
|
10236
|
+
"CONNECT"
|
|
10237
|
+
]);
|
|
10238
|
+
var LEADING_TOKEN_RE = /^(\S+)\s+\S/;
|
|
10239
|
+
function parseHttpMethodFromTrigger(trigger) {
|
|
10240
|
+
if (!trigger) return null;
|
|
10241
|
+
const match = LEADING_TOKEN_RE.exec(trigger.trim());
|
|
10242
|
+
const token = match?.[1];
|
|
10243
|
+
if (!token) return null;
|
|
10244
|
+
const method = token.toUpperCase();
|
|
10245
|
+
return HTTP_METHODS.has(method) ? method : null;
|
|
10246
|
+
}
|
|
10247
|
+
var ERROR_STATUS_THRESHOLD3 = 500;
|
|
10248
|
+
function mapEventToSignal(event) {
|
|
10249
|
+
const metadata = event.$metadata;
|
|
10250
|
+
const workers = event.$workers;
|
|
10251
|
+
const method = parseHttpMethodFromTrigger(metadata?.trigger);
|
|
10252
|
+
if (!method) return null;
|
|
10253
|
+
const scriptName = workers?.scriptName ?? metadata?.service;
|
|
10254
|
+
if (!scriptName) return null;
|
|
10255
|
+
const timestampMs = event.timestamp ?? metadata?.startTime;
|
|
10256
|
+
if (typeof timestampMs !== "number" || !Number.isFinite(timestampMs)) return null;
|
|
10257
|
+
const statusCode = metadata?.statusCode;
|
|
10258
|
+
const isError = typeof statusCode === "number" && statusCode >= ERROR_STATUS_THRESHOLD3;
|
|
10259
|
+
return {
|
|
10260
|
+
targetKind: CLOUDFLARE_TARGET_KIND,
|
|
10261
|
+
targetName: scriptName,
|
|
10262
|
+
callCount: 1,
|
|
10263
|
+
errorCount: isError ? 1 : 0,
|
|
10264
|
+
lastObservedIso: new Date(timestampMs).toISOString(),
|
|
10265
|
+
method,
|
|
10266
|
+
...typeof statusCode === "number" ? { statusCode } : {},
|
|
10267
|
+
...typeof metadata?.duration === "number" ? { duration: metadata.duration } : {}
|
|
10268
|
+
};
|
|
10269
|
+
}
|
|
10270
|
+
|
|
10271
|
+
// src/connectors/cloudflare/connector.ts
|
|
10272
|
+
var DEFAULT_MAX_LOOKBACK_MS3 = 60 * 60 * 1e3;
|
|
10273
|
+
function resolveFromMs(since, maxLookbackMs) {
|
|
10274
|
+
const cap = maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS3;
|
|
10275
|
+
const now = Date.now();
|
|
10276
|
+
const floor = now - cap;
|
|
10277
|
+
if (!since) return floor;
|
|
10278
|
+
const parsed = Date.parse(since);
|
|
10279
|
+
if (Number.isNaN(parsed)) return floor;
|
|
10280
|
+
return Math.max(parsed, floor);
|
|
10281
|
+
}
|
|
10282
|
+
var CloudflareConnector = class {
|
|
10283
|
+
constructor(config) {
|
|
10284
|
+
this.config = config;
|
|
10285
|
+
}
|
|
10286
|
+
config;
|
|
10287
|
+
provider = "cloudflare";
|
|
10288
|
+
async poll(ctx) {
|
|
10289
|
+
const toMs = Date.now();
|
|
10290
|
+
const fromMs = resolveFromMs(ctx.since, this.config.maxLookbackMs);
|
|
10291
|
+
const events = await queryWorkerInvocations(ctx, this.config, { fromMs, toMs });
|
|
10292
|
+
const signals = [];
|
|
10293
|
+
for (const event of events) {
|
|
10294
|
+
const signal = mapEventToSignal(event);
|
|
10295
|
+
if (signal) signals.push(signal);
|
|
10296
|
+
}
|
|
10297
|
+
return signals;
|
|
10298
|
+
}
|
|
10299
|
+
};
|
|
10300
|
+
function createCloudflareResolveTarget(config) {
|
|
10301
|
+
return (signal) => {
|
|
10302
|
+
if (signal.targetKind !== CLOUDFLARE_TARGET_KIND) return null;
|
|
10303
|
+
const mapping = config.workers[signal.targetName];
|
|
10304
|
+
if (!mapping) return null;
|
|
10305
|
+
return {
|
|
10306
|
+
targetNodeId: (0, import_types39.fileId)(mapping.service, mapping.entryFile),
|
|
10307
|
+
serviceName: mapping.service,
|
|
10308
|
+
edgeType: import_types39.EdgeType.CALLS
|
|
10309
|
+
};
|
|
10310
|
+
};
|
|
10311
|
+
}
|
|
10312
|
+
|
|
10313
|
+
// src/connectors-config.ts
|
|
10314
|
+
init_cjs_shims();
|
|
10315
|
+
var import_node_os4 = __toESM(require("os"), 1);
|
|
10316
|
+
var import_node_path46 = __toESM(require("path"), 1);
|
|
10317
|
+
var import_node_fs27 = require("fs");
|
|
10318
|
+
var CONNECTORS_CONFIG_VERSION = 1;
|
|
10319
|
+
var EnvRefUnsetError = class extends Error {
|
|
10320
|
+
ref;
|
|
10321
|
+
varName;
|
|
10322
|
+
constructor(ref, varName) {
|
|
10323
|
+
super(`${ref} is unset`);
|
|
10324
|
+
this.name = "EnvRefUnsetError";
|
|
10325
|
+
this.ref = ref;
|
|
10326
|
+
this.varName = varName;
|
|
10327
|
+
}
|
|
10328
|
+
};
|
|
10329
|
+
function neatHome2() {
|
|
10330
|
+
const override = process.env.NEAT_HOME;
|
|
10331
|
+
if (override && override.length > 0) return import_node_path46.default.resolve(override);
|
|
10332
|
+
return import_node_path46.default.join(import_node_os4.default.homedir(), ".neat");
|
|
10333
|
+
}
|
|
10334
|
+
function connectorsConfigPath(home = neatHome2()) {
|
|
10335
|
+
return import_node_path46.default.join(home, "connectors.json");
|
|
10336
|
+
}
|
|
10337
|
+
var MODE_MASK_LOOSER_THAN_0600 = 63;
|
|
10338
|
+
async function warnIfModeLooserThan0600(file) {
|
|
10339
|
+
if (process.platform === "win32") return;
|
|
10340
|
+
try {
|
|
10341
|
+
const stat = await import_node_fs27.promises.stat(file);
|
|
10342
|
+
if ((stat.mode & MODE_MASK_LOOSER_THAN_0600) !== 0) {
|
|
10343
|
+
const mode = (stat.mode & 511).toString(8).padStart(3, "0");
|
|
10344
|
+
console.warn(
|
|
10345
|
+
`[neat] ${file} is mode 0${mode}, looser than the 0600 this file's secrets call for \u2014 run \`chmod 600 ${file}\``
|
|
10346
|
+
);
|
|
10347
|
+
}
|
|
10348
|
+
} catch {
|
|
10349
|
+
}
|
|
10350
|
+
}
|
|
10351
|
+
async function readConnectorsConfig(home = neatHome2()) {
|
|
10352
|
+
const file = connectorsConfigPath(home);
|
|
10353
|
+
let raw;
|
|
10354
|
+
try {
|
|
10355
|
+
raw = await import_node_fs27.promises.readFile(file, "utf8");
|
|
10356
|
+
} catch (err) {
|
|
10357
|
+
if (err.code === "ENOENT") {
|
|
10358
|
+
return { version: CONNECTORS_CONFIG_VERSION, connectors: [] };
|
|
10359
|
+
}
|
|
10360
|
+
throw err;
|
|
10361
|
+
}
|
|
10362
|
+
await warnIfModeLooserThan0600(file);
|
|
10363
|
+
let parsed;
|
|
10364
|
+
try {
|
|
10365
|
+
parsed = JSON.parse(raw);
|
|
10366
|
+
} catch (err) {
|
|
10367
|
+
throw new Error(`${file} is not valid JSON: ${err.message}`);
|
|
10368
|
+
}
|
|
10369
|
+
return validateConfig(parsed, file);
|
|
10370
|
+
}
|
|
10371
|
+
function validateConfig(parsed, file) {
|
|
10372
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
10373
|
+
throw new Error(`${file} must be a JSON object with a "connectors" array`);
|
|
10374
|
+
}
|
|
10375
|
+
const obj = parsed;
|
|
10376
|
+
const version = obj.version === void 0 ? CONNECTORS_CONFIG_VERSION : obj.version;
|
|
10377
|
+
if (typeof version !== "number" || !Number.isInteger(version)) {
|
|
10378
|
+
throw new Error(`${file}: "version" must be an integer`);
|
|
10379
|
+
}
|
|
10380
|
+
const rawConnectors = obj.connectors;
|
|
10381
|
+
if (!Array.isArray(rawConnectors)) {
|
|
10382
|
+
throw new Error(`${file}: "connectors" must be an array`);
|
|
10383
|
+
}
|
|
10384
|
+
const connectors = rawConnectors.map((entry2, i) => validateEntry(entry2, i, file));
|
|
10385
|
+
return { version, connectors };
|
|
10386
|
+
}
|
|
10387
|
+
function validateEntry(entry2, index, file) {
|
|
10388
|
+
const where = `${file}: connectors[${index}]`;
|
|
10389
|
+
if (typeof entry2 !== "object" || entry2 === null || Array.isArray(entry2)) {
|
|
10390
|
+
throw new Error(`${where} must be an object`);
|
|
10391
|
+
}
|
|
10392
|
+
const e = entry2;
|
|
10393
|
+
const id = e.id;
|
|
10394
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
10395
|
+
throw new Error(`${where}.id must be a non-empty string`);
|
|
10396
|
+
}
|
|
10397
|
+
const provider = e.provider;
|
|
10398
|
+
if (typeof provider !== "string" || provider.length === 0) {
|
|
10399
|
+
throw new Error(`${where}.provider must be a non-empty string`);
|
|
10400
|
+
}
|
|
10401
|
+
if (e.project !== void 0 && typeof e.project !== "string") {
|
|
10402
|
+
throw new Error(`${where}.project must be a string when present`);
|
|
10403
|
+
}
|
|
10404
|
+
const credential = validateCredentialRef(e.credential, `${where}.credential`);
|
|
10405
|
+
let options;
|
|
10406
|
+
if (e.options !== void 0) {
|
|
10407
|
+
if (typeof e.options !== "object" || e.options === null || Array.isArray(e.options)) {
|
|
10408
|
+
throw new Error(`${where}.options must be an object when present`);
|
|
10409
|
+
}
|
|
10410
|
+
options = e.options;
|
|
10411
|
+
}
|
|
10412
|
+
return {
|
|
10413
|
+
id,
|
|
10414
|
+
provider,
|
|
10415
|
+
...typeof e.project === "string" ? { project: e.project } : {},
|
|
10416
|
+
credential,
|
|
10417
|
+
...options ? { options } : {}
|
|
10418
|
+
};
|
|
10419
|
+
}
|
|
10420
|
+
function validateCredentialRef(raw, where) {
|
|
10421
|
+
if (typeof raw === "string") {
|
|
10422
|
+
if (raw.length === 0) throw new Error(`${where} must be a non-empty string`);
|
|
10423
|
+
return raw;
|
|
10424
|
+
}
|
|
10425
|
+
if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
|
|
10426
|
+
const fields = raw;
|
|
10427
|
+
const keys = Object.keys(fields);
|
|
10428
|
+
if (keys.length === 0) throw new Error(`${where} object must have at least one field`);
|
|
10429
|
+
for (const key of keys) {
|
|
10430
|
+
const v = fields[key];
|
|
10431
|
+
if (typeof v !== "string" || v.length === 0) {
|
|
10432
|
+
throw new Error(`${where}.${key} must be a non-empty string`);
|
|
10433
|
+
}
|
|
10434
|
+
}
|
|
10435
|
+
return fields;
|
|
10436
|
+
}
|
|
10437
|
+
throw new Error(`${where} must be a string or an object of strings`);
|
|
10438
|
+
}
|
|
10439
|
+
function resolveCredential(ref, env = process.env) {
|
|
10440
|
+
if (typeof ref === "string") {
|
|
10441
|
+
return { kind: "single", value: resolveRef(ref, env) };
|
|
10442
|
+
}
|
|
10443
|
+
const fields = {};
|
|
10444
|
+
for (const [key, value] of Object.entries(ref)) {
|
|
10445
|
+
fields[key] = resolveRef(value, env);
|
|
10446
|
+
}
|
|
10447
|
+
return { kind: "fields", fields };
|
|
10448
|
+
}
|
|
10449
|
+
function resolveRef(value, env) {
|
|
10450
|
+
if (value.length > 1 && value.startsWith("$")) {
|
|
10451
|
+
const varName = value.slice(1);
|
|
10452
|
+
const resolved = env[varName];
|
|
10453
|
+
if (resolved === void 0 || resolved.length === 0) {
|
|
10454
|
+
throw new EnvRefUnsetError(value, varName);
|
|
10455
|
+
}
|
|
10456
|
+
return resolved;
|
|
10457
|
+
}
|
|
10458
|
+
return value;
|
|
10459
|
+
}
|
|
10460
|
+
var CONNECTORS_LOCK_TIMEOUT_MS = 5e3;
|
|
10461
|
+
var CONNECTORS_LOCK_RETRY_MS = 50;
|
|
10462
|
+
function connectorsConfigLockPath(home = neatHome2()) {
|
|
10463
|
+
return import_node_path46.default.join(home, "connectors.json.lock");
|
|
10464
|
+
}
|
|
10465
|
+
function isEnvRef(value) {
|
|
10466
|
+
return value.length > 1 && value.startsWith("$");
|
|
10467
|
+
}
|
|
10468
|
+
async function writeConfigAtomically0600(file, contents) {
|
|
10469
|
+
await import_node_fs27.promises.mkdir(import_node_path46.default.dirname(file), { recursive: true });
|
|
10470
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
10471
|
+
const fd = await import_node_fs27.promises.open(tmp, "w", 384);
|
|
10472
|
+
try {
|
|
10473
|
+
await fd.writeFile(contents, "utf8");
|
|
10474
|
+
await fd.chmod(384);
|
|
10475
|
+
await fd.sync();
|
|
10476
|
+
} finally {
|
|
10477
|
+
await fd.close();
|
|
10478
|
+
}
|
|
10479
|
+
await import_node_fs27.promises.rename(tmp, file);
|
|
10480
|
+
}
|
|
10481
|
+
async function acquireConnectorsLock(lockPath, timeoutMs = CONNECTORS_LOCK_TIMEOUT_MS) {
|
|
10482
|
+
const deadline = Date.now() + timeoutMs;
|
|
10483
|
+
await import_node_fs27.promises.mkdir(import_node_path46.default.dirname(lockPath), { recursive: true });
|
|
10484
|
+
for (; ; ) {
|
|
10485
|
+
try {
|
|
10486
|
+
const fd = await import_node_fs27.promises.open(lockPath, "wx");
|
|
10487
|
+
try {
|
|
10488
|
+
await fd.writeFile(`${process.pid}
|
|
10489
|
+
`, "utf8");
|
|
10490
|
+
} finally {
|
|
10491
|
+
await fd.close();
|
|
10492
|
+
}
|
|
10493
|
+
return;
|
|
10494
|
+
} catch (err) {
|
|
10495
|
+
if (err.code !== "EEXIST") throw err;
|
|
10496
|
+
if (Date.now() >= deadline) {
|
|
10497
|
+
throw new Error(
|
|
10498
|
+
`neat connector: timed out after ${timeoutMs}ms waiting for ${lockPath}. Another neat process may be writing the connector config; if none is, remove that file by hand.`
|
|
10499
|
+
);
|
|
10500
|
+
}
|
|
10501
|
+
await new Promise((r) => setTimeout(r, CONNECTORS_LOCK_RETRY_MS));
|
|
10502
|
+
}
|
|
10503
|
+
}
|
|
10504
|
+
}
|
|
10505
|
+
async function releaseConnectorsLock(lockPath) {
|
|
10506
|
+
await import_node_fs27.promises.unlink(lockPath).catch(() => {
|
|
10507
|
+
});
|
|
10508
|
+
}
|
|
10509
|
+
async function withConnectorsLock(home, fn) {
|
|
10510
|
+
const lock = connectorsConfigLockPath(home);
|
|
10511
|
+
await acquireConnectorsLock(lock);
|
|
10512
|
+
try {
|
|
10513
|
+
return await fn();
|
|
10514
|
+
} finally {
|
|
10515
|
+
await releaseConnectorsLock(lock);
|
|
10516
|
+
}
|
|
10517
|
+
}
|
|
10518
|
+
function serializeConfig(config) {
|
|
10519
|
+
return JSON.stringify(
|
|
10520
|
+
{ version: config.version ?? CONNECTORS_CONFIG_VERSION, connectors: config.connectors },
|
|
10521
|
+
null,
|
|
10522
|
+
2
|
|
10523
|
+
) + "\n";
|
|
10524
|
+
}
|
|
10525
|
+
async function upsertConnectorEntry(entry2, home = neatHome2()) {
|
|
10526
|
+
const file = connectorsConfigPath(home);
|
|
10527
|
+
return withConnectorsLock(home, async () => {
|
|
10528
|
+
const config = await readConnectorsConfig(home);
|
|
10529
|
+
validateEntry(entry2, config.connectors.length, file);
|
|
10530
|
+
const idx = config.connectors.findIndex((c) => c.id === entry2.id);
|
|
10531
|
+
const replaced = idx >= 0;
|
|
10532
|
+
if (replaced) config.connectors[idx] = entry2;
|
|
10533
|
+
else config.connectors.push(entry2);
|
|
10534
|
+
await writeConfigAtomically0600(file, serializeConfig(config));
|
|
10535
|
+
return { replaced };
|
|
10536
|
+
});
|
|
10537
|
+
}
|
|
10538
|
+
async function removeConnectorEntry(id, home = neatHome2()) {
|
|
10539
|
+
const file = connectorsConfigPath(home);
|
|
10540
|
+
return withConnectorsLock(home, async () => {
|
|
10541
|
+
const config = await readConnectorsConfig(home);
|
|
10542
|
+
const idx = config.connectors.findIndex((c) => c.id === id);
|
|
10543
|
+
if (idx < 0) return void 0;
|
|
10544
|
+
const [removed] = config.connectors.splice(idx, 1);
|
|
10545
|
+
await writeConfigAtomically0600(file, serializeConfig(config));
|
|
10546
|
+
return removed;
|
|
10547
|
+
});
|
|
10548
|
+
}
|
|
10549
|
+
function slugFragment(value) {
|
|
10550
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
10551
|
+
}
|
|
10552
|
+
function autoSlugConnectorId(provider, project, existingIds) {
|
|
10553
|
+
const base = slugFragment(provider) || "connector";
|
|
10554
|
+
if (!existingIds.has(base)) return base;
|
|
10555
|
+
const projectFrag = project ? slugFragment(project) : "";
|
|
10556
|
+
if (projectFrag) {
|
|
10557
|
+
const withProject = `${base}-${projectFrag}`;
|
|
10558
|
+
if (!existingIds.has(withProject)) return withProject;
|
|
10559
|
+
}
|
|
10560
|
+
const stem = projectFrag ? `${base}-${projectFrag}` : base;
|
|
10561
|
+
for (let n = 2; ; n++) {
|
|
10562
|
+
const candidate = `${stem}-${n}`;
|
|
10563
|
+
if (!existingIds.has(candidate)) return candidate;
|
|
10564
|
+
}
|
|
10565
|
+
}
|
|
10566
|
+
function describeCredential(ref, env = process.env) {
|
|
10567
|
+
const one = (value, field) => {
|
|
10568
|
+
if (isEnvRef(value)) {
|
|
10569
|
+
const varName = value.slice(1);
|
|
10570
|
+
const resolved = env[varName];
|
|
10571
|
+
const set = typeof resolved === "string" && resolved.length > 0;
|
|
10572
|
+
return {
|
|
10573
|
+
...field ? { field } : {},
|
|
10574
|
+
kind: "env-ref",
|
|
10575
|
+
display: value,
|
|
10576
|
+
status: set ? "set" : "unset"
|
|
10577
|
+
};
|
|
10578
|
+
}
|
|
10579
|
+
return { ...field ? { field } : {}, kind: "plaintext", display: "****" };
|
|
10580
|
+
};
|
|
10581
|
+
if (typeof ref === "string") return [one(ref)];
|
|
10582
|
+
return Object.entries(ref).map(([k, v]) => one(v, k));
|
|
10583
|
+
}
|
|
10584
|
+
|
|
10585
|
+
// src/connectors/registry.ts
|
|
10586
|
+
var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
|
|
10587
|
+
async function authProbe(input) {
|
|
10588
|
+
const { provider, accountKey, url, token, init, fetchImpl } = input;
|
|
10589
|
+
try {
|
|
10590
|
+
const res = await junctionFetch(
|
|
10591
|
+
url,
|
|
10592
|
+
{
|
|
10593
|
+
...init ?? {},
|
|
10594
|
+
headers: { ...bearerAuthHeader(token), ...init?.headers ?? {} }
|
|
10595
|
+
},
|
|
10596
|
+
{ provider, accountKey, ...fetchImpl ? { fetchImpl } : {} }
|
|
10597
|
+
);
|
|
10598
|
+
if (res.ok) return { ok: true };
|
|
10599
|
+
if (res.status === 401 || res.status === 403) {
|
|
10600
|
+
return { ok: false, reason: `${provider} rejected the credential (HTTP ${res.status})` };
|
|
10601
|
+
}
|
|
10602
|
+
return {
|
|
10603
|
+
ok: false,
|
|
10604
|
+
reason: `${provider} auth check returned HTTP ${res.status} ${res.statusText} \u2014 could not confirm the credential`
|
|
10605
|
+
};
|
|
10606
|
+
} catch (err) {
|
|
10607
|
+
return {
|
|
10608
|
+
ok: false,
|
|
10609
|
+
reason: `${provider} auth check could not reach the provider: ${err.message}`
|
|
10610
|
+
};
|
|
10611
|
+
}
|
|
10612
|
+
}
|
|
10613
|
+
var PROVIDER_DISPATCH = {
|
|
10614
|
+
supabase: {
|
|
10615
|
+
provider: "supabase",
|
|
10616
|
+
primaryCredentialKey: "managementToken",
|
|
10617
|
+
requiredCredentialFields: ["managementToken"],
|
|
10618
|
+
requiredOptionFields: ["apiProjectRef", "nodeRef", "serviceName"],
|
|
10619
|
+
build(graph, options) {
|
|
10620
|
+
return createSupabaseConnector(graph, options);
|
|
10621
|
+
},
|
|
10622
|
+
// GET /v1/projects — the Management API's own auth-gated list endpoint, the
|
|
10623
|
+
// cheapest confirmation the management token is live (the same surface
|
|
10624
|
+
// client.ts polls, minus the heavy log query).
|
|
10625
|
+
validate({ credentials, options, fetchImpl }) {
|
|
10626
|
+
const cfg = options;
|
|
10627
|
+
const baseUrl = cfg.managementApiUrl ?? DEFAULT_SUPABASE_MANAGEMENT_API_URL;
|
|
10628
|
+
return authProbe({
|
|
10629
|
+
provider: "supabase",
|
|
10630
|
+
accountKey: cfg.apiProjectRef ?? "validate",
|
|
10631
|
+
url: `${baseUrl}/v1/projects`,
|
|
10632
|
+
token: String(credentials.managementToken ?? ""),
|
|
10633
|
+
...fetchImpl ? { fetchImpl } : {}
|
|
10634
|
+
});
|
|
10635
|
+
}
|
|
10636
|
+
},
|
|
10637
|
+
railway: {
|
|
10638
|
+
provider: "railway",
|
|
10639
|
+
primaryCredentialKey: "token",
|
|
10640
|
+
requiredCredentialFields: ["token"],
|
|
10641
|
+
requiredOptionFields: ["environmentId", "serviceId", "serviceNameById"],
|
|
10642
|
+
build(graph, options) {
|
|
10643
|
+
const config = options;
|
|
10644
|
+
return {
|
|
10645
|
+
connector: createRailwayConnector(graph, config),
|
|
10646
|
+
resolveTarget: createRailwayResolveTarget(config)
|
|
10647
|
+
};
|
|
10648
|
+
},
|
|
10649
|
+
// A minimal GraphQL POST — Railway's gateway 401s an unauthenticated call
|
|
10650
|
+
// at the HTTP layer, so a 2xx confirms the token was accepted regardless of
|
|
10651
|
+
// the query's own shape (the connector's real queries are still
|
|
10652
|
+
// needs-endpoint-testing per railway/types.ts — auth is what we check).
|
|
10653
|
+
validate({ credentials, options, fetchImpl }) {
|
|
10654
|
+
const cfg = options;
|
|
10655
|
+
return authProbe({
|
|
10656
|
+
provider: "railway",
|
|
10657
|
+
accountKey: cfg.environmentId ?? "validate",
|
|
10658
|
+
url: cfg.apiUrl ?? DEFAULT_RAILWAY_API_URL,
|
|
10659
|
+
token: String(credentials.token ?? ""),
|
|
10660
|
+
init: {
|
|
10661
|
+
method: "POST",
|
|
10662
|
+
headers: { "Content-Type": "application/json" },
|
|
10663
|
+
body: JSON.stringify({ query: "{ __typename }" })
|
|
10664
|
+
},
|
|
10665
|
+
...fetchImpl ? { fetchImpl } : {}
|
|
10666
|
+
});
|
|
10667
|
+
}
|
|
10668
|
+
},
|
|
10669
|
+
firebase: {
|
|
10670
|
+
provider: "firebase",
|
|
10671
|
+
// Firebase reads both projectId and accessToken from the credential; the
|
|
10672
|
+
// single-string form maps to the secret, and the required-fields check
|
|
10673
|
+
// below catches a projectId that was never supplied.
|
|
10674
|
+
primaryCredentialKey: "accessToken",
|
|
10675
|
+
requiredCredentialFields: ["projectId", "accessToken"],
|
|
10676
|
+
requiredOptionFields: [],
|
|
10677
|
+
build(graph, options) {
|
|
10678
|
+
return createFirebaseConnector(graph, options);
|
|
10679
|
+
},
|
|
10680
|
+
// GET the project's Cloud Logging log-name list (pageSize 1) — within the
|
|
10681
|
+
// same `roles/logging.viewer` grant the connector polls under, and the
|
|
10682
|
+
// lightest call that still fails 401/403 on a bad or wrong-scoped token.
|
|
10683
|
+
validate({ credentials, fetchImpl }) {
|
|
10684
|
+
const projectId = String(credentials.projectId ?? "");
|
|
10685
|
+
return authProbe({
|
|
10686
|
+
provider: "firebase",
|
|
10687
|
+
accountKey: projectId || "validate",
|
|
10688
|
+
url: `https://logging.googleapis.com/v2/projects/${projectId}/logs?pageSize=1`,
|
|
10689
|
+
token: String(credentials.accessToken ?? ""),
|
|
10690
|
+
...fetchImpl ? { fetchImpl } : {}
|
|
10691
|
+
});
|
|
10692
|
+
}
|
|
10693
|
+
},
|
|
10694
|
+
cloudflare: {
|
|
10695
|
+
provider: "cloudflare",
|
|
10696
|
+
primaryCredentialKey: "apiToken",
|
|
10697
|
+
requiredCredentialFields: ["apiToken"],
|
|
10698
|
+
requiredOptionFields: ["accountId", "workers"],
|
|
10699
|
+
build(_graph, options) {
|
|
10700
|
+
const config = options;
|
|
10701
|
+
return {
|
|
10702
|
+
connector: new CloudflareConnector(config),
|
|
10703
|
+
resolveTarget: createCloudflareResolveTarget(config)
|
|
10704
|
+
};
|
|
10705
|
+
},
|
|
10706
|
+
// GET /user/tokens/verify — Cloudflare's own purpose-built "is this API
|
|
10707
|
+
// token live" endpoint. 200 on a valid token, 401 on an invalid one.
|
|
10708
|
+
validate({ credentials, options, fetchImpl }) {
|
|
10709
|
+
const cfg = options;
|
|
10710
|
+
const baseUrl = cfg.baseUrl ?? CLOUDFLARE_API_BASE_URL;
|
|
10711
|
+
return authProbe({
|
|
10712
|
+
provider: "cloudflare",
|
|
10713
|
+
accountKey: cfg.accountId ?? "validate",
|
|
10714
|
+
url: `${baseUrl}/user/tokens/verify`,
|
|
10715
|
+
token: String(credentials.apiToken ?? ""),
|
|
10716
|
+
...fetchImpl ? { fetchImpl } : {}
|
|
10717
|
+
});
|
|
10718
|
+
}
|
|
10719
|
+
}
|
|
10720
|
+
};
|
|
10721
|
+
function getProviderDispatch(provider) {
|
|
10722
|
+
return PROVIDER_DISPATCH[provider];
|
|
10723
|
+
}
|
|
10724
|
+
function resolveEntryCredentials(dispatch, entry2, env) {
|
|
10725
|
+
let credentials;
|
|
10726
|
+
try {
|
|
10727
|
+
const resolved = resolveCredential(entry2.credential, env);
|
|
10728
|
+
credentials = resolved.kind === "single" ? { [dispatch.primaryCredentialKey]: resolved.value } : { ...resolved.fields };
|
|
10729
|
+
} catch (err) {
|
|
10730
|
+
if (err instanceof EnvRefUnsetError) return { ok: false, kind: "unset-env", reason: err.message };
|
|
10731
|
+
return { ok: false, kind: "error", reason: err.message };
|
|
10732
|
+
}
|
|
10733
|
+
const missingCreds = dispatch.requiredCredentialFields.filter((k) => !credentials[k]);
|
|
10734
|
+
if (missingCreds.length > 0) {
|
|
10735
|
+
return {
|
|
10736
|
+
ok: false,
|
|
10737
|
+
kind: "missing-field",
|
|
10738
|
+
reason: `credential missing required field(s): ${missingCreds.join(", ")}`
|
|
10739
|
+
};
|
|
10740
|
+
}
|
|
10741
|
+
return { ok: true, credentials };
|
|
10742
|
+
}
|
|
10743
|
+
async function validateConnectorEntry(entry2, env = process.env, fetchImpl) {
|
|
10744
|
+
const dispatch = PROVIDER_DISPATCH[entry2.provider];
|
|
10745
|
+
if (!dispatch) {
|
|
10746
|
+
return { status: "unknown-provider", reason: `unknown provider "${entry2.provider}"` };
|
|
10747
|
+
}
|
|
10748
|
+
const creds = resolveEntryCredentials(dispatch, entry2, env);
|
|
10749
|
+
if (!creds.ok) {
|
|
10750
|
+
if (creds.kind === "unset-env") return { status: "unset-env", reason: creds.reason };
|
|
10751
|
+
return { status: "missing-field", reason: creds.reason };
|
|
10752
|
+
}
|
|
10753
|
+
const options = entry2.options ?? {};
|
|
10754
|
+
const missingOpts = dispatch.requiredOptionFields.filter((k) => !(k in options));
|
|
10755
|
+
if (missingOpts.length > 0) {
|
|
10756
|
+
return {
|
|
10757
|
+
status: "missing-field",
|
|
10758
|
+
reason: `options missing required field(s): ${missingOpts.join(", ")}`
|
|
10759
|
+
};
|
|
10760
|
+
}
|
|
10761
|
+
const result = await dispatch.validate({
|
|
10762
|
+
credentials: creds.credentials,
|
|
10763
|
+
options,
|
|
10764
|
+
...fetchImpl ? { fetchImpl } : {}
|
|
10765
|
+
});
|
|
10766
|
+
return result.ok ? { status: "ok" } : { status: "rejected", reason: result.reason };
|
|
10767
|
+
}
|
|
10768
|
+
|
|
10769
|
+
// src/daemon.ts
|
|
10770
|
+
init_auth();
|
|
10771
|
+
|
|
10772
|
+
// src/unrouted.ts
|
|
10773
|
+
init_cjs_shims();
|
|
10774
|
+
var import_node_fs28 = require("fs");
|
|
10775
|
+
var import_node_path47 = __toESM(require("path"), 1);
|
|
10776
|
+
|
|
10777
|
+
// src/daemon.ts
|
|
10778
|
+
var import_types42 = require("@neat.is/types");
|
|
10779
|
+
function daemonJsonPath(scanPath) {
|
|
10780
|
+
return import_node_path48.default.join(scanPath, "neat-out", "daemon.json");
|
|
10781
|
+
}
|
|
10782
|
+
function daemonsDiscoveryDir(home) {
|
|
10783
|
+
const base = home && home.length > 0 ? home : neatHomeFromEnv();
|
|
10784
|
+
return import_node_path48.default.join(base, "daemons");
|
|
10785
|
+
}
|
|
10786
|
+
function daemonDiscoveryPath(project, home) {
|
|
10787
|
+
return import_node_path48.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
|
|
10788
|
+
}
|
|
10789
|
+
function sanitizeDiscoveryName(project) {
|
|
10790
|
+
return project.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
10791
|
+
}
|
|
10792
|
+
function neatHomeFromEnv() {
|
|
10793
|
+
const env = process.env.NEAT_HOME;
|
|
10794
|
+
if (env && env.length > 0) return import_node_path48.default.resolve(env);
|
|
10795
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
10796
|
+
return import_node_path48.default.join(home, ".neat");
|
|
10797
|
+
}
|
|
10798
|
+
async function readDaemonRecord(scanPath) {
|
|
10799
|
+
try {
|
|
10800
|
+
const raw = await import_node_fs29.promises.readFile(daemonJsonPath(scanPath), "utf8");
|
|
10801
|
+
const parsed = JSON.parse(raw);
|
|
10802
|
+
if (typeof parsed.project === "string" && parsed.ports && typeof parsed.ports.rest === "number" && typeof parsed.ports.otlp === "number" && typeof parsed.ports.web === "number") {
|
|
10803
|
+
return parsed;
|
|
10804
|
+
}
|
|
10805
|
+
return null;
|
|
10806
|
+
} catch {
|
|
10807
|
+
return null;
|
|
10808
|
+
}
|
|
10809
|
+
}
|
|
10810
|
+
function resolveNeatVersion() {
|
|
10811
|
+
if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
|
|
10812
|
+
return process.env.NEAT_LOCAL_VERSION;
|
|
10813
|
+
}
|
|
10814
|
+
try {
|
|
10815
|
+
const req = (0, import_node_module.createRequire)(importMetaUrl);
|
|
10816
|
+
const pkg = req("../package.json");
|
|
10817
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
10818
|
+
} catch {
|
|
10819
|
+
return "0.0.0";
|
|
10820
|
+
}
|
|
10821
|
+
}
|
|
10822
|
+
async function writeDaemonRecord(record, home) {
|
|
10823
|
+
const body = JSON.stringify(record, null, 2) + "\n";
|
|
10824
|
+
await writeAtomically(daemonJsonPath(record.projectPath), body);
|
|
10825
|
+
try {
|
|
10826
|
+
await writeAtomically(daemonDiscoveryPath(record.project, home), body);
|
|
10827
|
+
} catch (err) {
|
|
10828
|
+
console.warn(
|
|
10829
|
+
`neatd: could not write discovery copy for "${record.project}" \u2014 ${err.message}`
|
|
10830
|
+
);
|
|
10831
|
+
}
|
|
10832
|
+
}
|
|
10833
|
+
async function clearDaemonRecord(record, home) {
|
|
10834
|
+
try {
|
|
10835
|
+
const stopped = { ...record, status: "stopped" };
|
|
10836
|
+
await writeAtomically(daemonJsonPath(record.projectPath), JSON.stringify(stopped, null, 2) + "\n");
|
|
10837
|
+
} catch {
|
|
10838
|
+
}
|
|
10839
|
+
try {
|
|
10840
|
+
await import_node_fs29.promises.unlink(daemonDiscoveryPath(record.project, home));
|
|
10841
|
+
} catch {
|
|
10842
|
+
}
|
|
10843
|
+
}
|
|
10844
|
+
function portFromListenAddress(address, fallback) {
|
|
10845
|
+
try {
|
|
10846
|
+
const port = new URL(address).port;
|
|
10847
|
+
const n = Number.parseInt(port, 10);
|
|
10848
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
10849
|
+
} catch {
|
|
10850
|
+
}
|
|
10851
|
+
return fallback;
|
|
10852
|
+
}
|
|
10853
|
+
function resolveHost(opts, authTokenSet) {
|
|
10854
|
+
if (opts.host && opts.host.length > 0) return opts.host;
|
|
10855
|
+
const env = process.env.HOST;
|
|
10856
|
+
if (env && env.length > 0) return env;
|
|
10857
|
+
if (!authTokenSet) return "127.0.0.1";
|
|
10858
|
+
return "0.0.0.0";
|
|
10859
|
+
}
|
|
10860
|
+
|
|
10861
|
+
// src/watch.ts
|
|
10862
|
+
init_otel_grpc();
|
|
10863
|
+
|
|
10864
|
+
// src/search.ts
|
|
10865
|
+
init_cjs_shims();
|
|
10866
|
+
var import_node_fs30 = require("fs");
|
|
10867
|
+
var import_node_path49 = __toESM(require("path"), 1);
|
|
10868
|
+
var import_node_crypto4 = require("crypto");
|
|
10869
|
+
var DEFAULT_LIMIT = 10;
|
|
10870
|
+
var NOMIC_DIM = 768;
|
|
10871
|
+
var MINI_LM_DIM = 384;
|
|
10872
|
+
function shouldEmbed(node) {
|
|
10873
|
+
return node.type !== "FrontierNode";
|
|
10874
|
+
}
|
|
10875
|
+
function embedText(node) {
|
|
10876
|
+
const parts = [node.id];
|
|
10877
|
+
const name = node.name;
|
|
10878
|
+
if (name) parts.push(name);
|
|
10879
|
+
switch (node.type) {
|
|
10880
|
+
case "ServiceNode": {
|
|
10881
|
+
const lang = node.language;
|
|
10882
|
+
if (lang) parts.push(`language=${lang}`);
|
|
10883
|
+
break;
|
|
10884
|
+
}
|
|
10885
|
+
case "DatabaseNode": {
|
|
10886
|
+
const eng = node.engine;
|
|
10887
|
+
const ver = node.engineVersion;
|
|
10888
|
+
if (eng) parts.push(`engine=${eng}`);
|
|
10889
|
+
if (ver) parts.push(`engineVersion=${ver}`);
|
|
10890
|
+
break;
|
|
10891
|
+
}
|
|
10892
|
+
case "InfraNode": {
|
|
10893
|
+
const kind = node.kind;
|
|
10894
|
+
if (kind) parts.push(`kind=${kind}`);
|
|
10895
|
+
break;
|
|
10896
|
+
}
|
|
10897
|
+
case "ConfigNode": {
|
|
10898
|
+
const filePath = node.path;
|
|
10899
|
+
if (filePath) parts.push(`path=${filePath}`);
|
|
10900
|
+
break;
|
|
10901
|
+
}
|
|
10902
|
+
case "RouteNode": {
|
|
10903
|
+
const method = node.method;
|
|
10904
|
+
const tmpl = node.pathTemplate;
|
|
10905
|
+
if (method) parts.push(`method=${method}`);
|
|
10906
|
+
if (tmpl) parts.push(`path=${tmpl}`);
|
|
10907
|
+
break;
|
|
10908
|
+
}
|
|
10909
|
+
case "GraphQLOperationNode": {
|
|
10910
|
+
const opType = node.operationType;
|
|
10911
|
+
if (opType) parts.push(`operationType=${opType}`);
|
|
10912
|
+
break;
|
|
10913
|
+
}
|
|
10914
|
+
case "GrpcMethodNode": {
|
|
10915
|
+
const rpcService = node.rpcService;
|
|
10916
|
+
const rpcMethod = node.rpcMethod;
|
|
10917
|
+
if (rpcService) parts.push(`rpcService=${rpcService}`);
|
|
10918
|
+
if (rpcMethod) parts.push(`rpcMethod=${rpcMethod}`);
|
|
9189
10919
|
break;
|
|
9190
10920
|
}
|
|
9191
10921
|
case "WebSocketChannelNode": {
|
|
@@ -9199,7 +10929,7 @@ function embedText(node) {
|
|
|
9199
10929
|
return parts.join(" ");
|
|
9200
10930
|
}
|
|
9201
10931
|
function attrsHash(node) {
|
|
9202
|
-
return (0,
|
|
10932
|
+
return (0, import_node_crypto4.createHash)("sha1").update(embedText(node)).digest("hex").slice(0, 16);
|
|
9203
10933
|
}
|
|
9204
10934
|
function cosine(a, b) {
|
|
9205
10935
|
if (a.length !== b.length) return 0;
|
|
@@ -9288,7 +11018,7 @@ async function pickEmbedder() {
|
|
|
9288
11018
|
}
|
|
9289
11019
|
async function readCache(cachePath) {
|
|
9290
11020
|
try {
|
|
9291
|
-
const raw = await
|
|
11021
|
+
const raw = await import_node_fs30.promises.readFile(cachePath, "utf8");
|
|
9292
11022
|
const parsed = JSON.parse(raw);
|
|
9293
11023
|
if (parsed.version !== 1) return null;
|
|
9294
11024
|
return parsed;
|
|
@@ -9297,8 +11027,8 @@ async function readCache(cachePath) {
|
|
|
9297
11027
|
}
|
|
9298
11028
|
}
|
|
9299
11029
|
async function writeCache(cachePath, cache) {
|
|
9300
|
-
await
|
|
9301
|
-
await
|
|
11030
|
+
await import_node_fs30.promises.mkdir(import_node_path49.default.dirname(cachePath), { recursive: true });
|
|
11031
|
+
await import_node_fs30.promises.writeFile(cachePath, JSON.stringify(cache));
|
|
9302
11032
|
}
|
|
9303
11033
|
var VectorIndex = class {
|
|
9304
11034
|
constructor(embedder, cachePath) {
|
|
@@ -9455,8 +11185,8 @@ var ALL_PHASES = [
|
|
|
9455
11185
|
];
|
|
9456
11186
|
function classifyChange(relPath) {
|
|
9457
11187
|
const phases = /* @__PURE__ */ new Set();
|
|
9458
|
-
const base =
|
|
9459
|
-
const segments = relPath.split(
|
|
11188
|
+
const base = import_node_path50.default.basename(relPath).toLowerCase();
|
|
11189
|
+
const segments = relPath.split(import_node_path50.default.sep).map((s) => s.toLowerCase());
|
|
9460
11190
|
if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
|
|
9461
11191
|
phases.add("services");
|
|
9462
11192
|
phases.add("aliases");
|
|
@@ -9577,16 +11307,16 @@ function countWatchableDirs(scanPath, limit) {
|
|
|
9577
11307
|
if (count >= limit) return;
|
|
9578
11308
|
let entries;
|
|
9579
11309
|
try {
|
|
9580
|
-
entries =
|
|
11310
|
+
entries = import_node_fs31.default.readdirSync(dir, { withFileTypes: true });
|
|
9581
11311
|
} catch {
|
|
9582
11312
|
return;
|
|
9583
11313
|
}
|
|
9584
11314
|
for (const e of entries) {
|
|
9585
11315
|
if (count >= limit) return;
|
|
9586
11316
|
if (!e.isDirectory()) continue;
|
|
9587
|
-
if (IGNORED_WATCH_PATHS.some((re) => re.test(
|
|
11317
|
+
if (IGNORED_WATCH_PATHS.some((re) => re.test(import_node_path50.default.join(dir, e.name) + import_node_path50.default.sep))) continue;
|
|
9588
11318
|
count++;
|
|
9589
|
-
if (depth < 2) visit(
|
|
11319
|
+
if (depth < 2) visit(import_node_path50.default.join(dir, e.name), depth + 1);
|
|
9590
11320
|
}
|
|
9591
11321
|
};
|
|
9592
11322
|
visit(scanPath, 0);
|
|
@@ -9604,8 +11334,8 @@ async function startWatch(graph, opts) {
|
|
|
9604
11334
|
const projectName = opts.project ?? DEFAULT_PROJECT;
|
|
9605
11335
|
await loadGraphFromDisk(graph, opts.outPath);
|
|
9606
11336
|
const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
|
|
9607
|
-
const policyFilePath =
|
|
9608
|
-
const policyViolationsPath =
|
|
11337
|
+
const policyFilePath = import_node_path50.default.join(opts.scanPath, "policy.json");
|
|
11338
|
+
const policyViolationsPath = import_node_path50.default.join(import_node_path50.default.dirname(opts.outPath), "policy-violations.ndjson");
|
|
9609
11339
|
let policies = [];
|
|
9610
11340
|
try {
|
|
9611
11341
|
policies = await loadPolicyFile(policyFilePath);
|
|
@@ -9656,7 +11386,7 @@ async function startWatch(graph, opts) {
|
|
|
9656
11386
|
assertBindAuthority(host, auth.authToken);
|
|
9657
11387
|
const port = opts.port ?? 8080;
|
|
9658
11388
|
const otelPort = opts.otelPort ?? 4318;
|
|
9659
|
-
const cachePath = opts.embeddingsCachePath ??
|
|
11389
|
+
const cachePath = opts.embeddingsCachePath ?? import_node_path50.default.join(import_node_path50.default.dirname(opts.outPath), "embeddings.json");
|
|
9660
11390
|
let searchIndex;
|
|
9661
11391
|
try {
|
|
9662
11392
|
searchIndex = await buildSearchIndex(graph, { cachePath });
|
|
@@ -9674,7 +11404,7 @@ async function startWatch(graph, opts) {
|
|
|
9674
11404
|
// Paths are derived from the explicit options the watch caller passes
|
|
9675
11405
|
// — pathsForProject is only used to fill in the embeddings/snapshot
|
|
9676
11406
|
// fields so the registry shape is complete.
|
|
9677
|
-
...pathsForProject(projectName,
|
|
11407
|
+
...pathsForProject(projectName, import_node_path50.default.dirname(opts.outPath)),
|
|
9678
11408
|
snapshotPath: opts.outPath,
|
|
9679
11409
|
errorsPath: opts.errorsPath,
|
|
9680
11410
|
staleEventsPath: opts.staleEventsPath
|
|
@@ -9792,9 +11522,9 @@ async function startWatch(graph, opts) {
|
|
|
9792
11522
|
};
|
|
9793
11523
|
const onPath = (absPath) => {
|
|
9794
11524
|
if (shouldIgnore(absPath)) return;
|
|
9795
|
-
const rel =
|
|
11525
|
+
const rel = import_node_path50.default.relative(opts.scanPath, absPath);
|
|
9796
11526
|
if (!rel || rel.startsWith("..")) return;
|
|
9797
|
-
pendingPaths.add(rel.split(
|
|
11527
|
+
pendingPaths.add(rel.split(import_node_path50.default.sep).join("/"));
|
|
9798
11528
|
const phases = classifyChange(rel);
|
|
9799
11529
|
if (phases.size === 0) {
|
|
9800
11530
|
for (const p of ALL_PHASES) pending.add(p);
|
|
@@ -9850,12 +11580,12 @@ async function startWatch(graph, opts) {
|
|
|
9850
11580
|
|
|
9851
11581
|
// src/deploy/detect.ts
|
|
9852
11582
|
init_cjs_shims();
|
|
9853
|
-
var
|
|
9854
|
-
var
|
|
11583
|
+
var import_node_fs32 = require("fs");
|
|
11584
|
+
var import_node_path51 = __toESM(require("path"), 1);
|
|
9855
11585
|
var import_node_child_process2 = require("child_process");
|
|
9856
|
-
var
|
|
11586
|
+
var import_node_crypto5 = require("crypto");
|
|
9857
11587
|
function generateToken() {
|
|
9858
|
-
return (0,
|
|
11588
|
+
return (0, import_node_crypto5.randomBytes)(32).toString("base64url");
|
|
9859
11589
|
}
|
|
9860
11590
|
async function probeBinary(binary, arg) {
|
|
9861
11591
|
return new Promise((resolve) => {
|
|
@@ -9951,21 +11681,21 @@ async function runDeploy(opts = {}) {
|
|
|
9951
11681
|
const token = generateToken();
|
|
9952
11682
|
switch (substrate) {
|
|
9953
11683
|
case "docker-compose": {
|
|
9954
|
-
const artifactPath =
|
|
11684
|
+
const artifactPath = import_node_path51.default.join(cwd, "docker-compose.neat.yml");
|
|
9955
11685
|
const contents = emitDockerCompose(cwd);
|
|
9956
|
-
await
|
|
11686
|
+
await import_node_fs32.promises.writeFile(artifactPath, contents, "utf8");
|
|
9957
11687
|
return {
|
|
9958
11688
|
substrate,
|
|
9959
11689
|
artifactPath,
|
|
9960
11690
|
token,
|
|
9961
11691
|
contents,
|
|
9962
|
-
startCommand: `NEAT_AUTH_TOKEN=${token} docker compose -f ${
|
|
11692
|
+
startCommand: `NEAT_AUTH_TOKEN=${token} docker compose -f ${import_node_path51.default.basename(artifactPath)} up -d`
|
|
9963
11693
|
};
|
|
9964
11694
|
}
|
|
9965
11695
|
case "systemd": {
|
|
9966
|
-
const artifactPath =
|
|
11696
|
+
const artifactPath = import_node_path51.default.join(cwd, "neat.service");
|
|
9967
11697
|
const contents = emitSystemdUnit(cwd);
|
|
9968
|
-
await
|
|
11698
|
+
await import_node_fs32.promises.writeFile(artifactPath, contents, "utf8");
|
|
9969
11699
|
return {
|
|
9970
11700
|
substrate,
|
|
9971
11701
|
artifactPath,
|
|
@@ -9998,8 +11728,8 @@ init_cjs_shims();
|
|
|
9998
11728
|
|
|
9999
11729
|
// src/installers/javascript.ts
|
|
10000
11730
|
init_cjs_shims();
|
|
10001
|
-
var
|
|
10002
|
-
var
|
|
11731
|
+
var import_node_fs33 = require("fs");
|
|
11732
|
+
var import_node_path52 = __toESM(require("path"), 1);
|
|
10003
11733
|
var import_semver2 = __toESM(require("semver"), 1);
|
|
10004
11734
|
|
|
10005
11735
|
// src/installers/templates.ts
|
|
@@ -10614,15 +12344,15 @@ var OTEL_ENV = {
|
|
|
10614
12344
|
value: "http://localhost:4318/projects/<project>/v1/traces"
|
|
10615
12345
|
};
|
|
10616
12346
|
function serviceNodeName(pkg, serviceDir) {
|
|
10617
|
-
return pkg.name ??
|
|
12347
|
+
return pkg.name ?? import_node_path52.default.basename(serviceDir);
|
|
10618
12348
|
}
|
|
10619
12349
|
function projectToken(pkg, serviceDir, project) {
|
|
10620
12350
|
if (project && project.length > 0) return project;
|
|
10621
|
-
return pkg.name ??
|
|
12351
|
+
return pkg.name ?? import_node_path52.default.basename(serviceDir);
|
|
10622
12352
|
}
|
|
10623
12353
|
async function readJsonFile(p) {
|
|
10624
12354
|
try {
|
|
10625
|
-
const raw = await
|
|
12355
|
+
const raw = await import_node_fs33.promises.readFile(p, "utf8");
|
|
10626
12356
|
return JSON.parse(raw);
|
|
10627
12357
|
} catch {
|
|
10628
12358
|
return null;
|
|
@@ -10631,16 +12361,16 @@ async function readJsonFile(p) {
|
|
|
10631
12361
|
async function detectRuntimeKind(pkgRoot, pkg) {
|
|
10632
12362
|
const deps = allDeps(pkg);
|
|
10633
12363
|
if ("react-native" in deps || "expo" in deps) return "react-native";
|
|
10634
|
-
const appJson = await readJsonFile(
|
|
12364
|
+
const appJson = await readJsonFile(import_node_path52.default.join(pkgRoot, "app.json"));
|
|
10635
12365
|
if (appJson && typeof appJson === "object" && "expo" in appJson) {
|
|
10636
12366
|
return "react-native";
|
|
10637
12367
|
}
|
|
10638
|
-
if (await exists3(
|
|
12368
|
+
if (await exists3(import_node_path52.default.join(pkgRoot, "vite.config.js")) || await exists3(import_node_path52.default.join(pkgRoot, "vite.config.ts")) || await exists3(import_node_path52.default.join(pkgRoot, "vite.config.mjs")) || "vite" in deps) {
|
|
10639
12369
|
return "browser-bundle";
|
|
10640
12370
|
}
|
|
10641
|
-
if (await exists3(
|
|
10642
|
-
if (await exists3(
|
|
10643
|
-
if (await exists3(
|
|
12371
|
+
if (await exists3(import_node_path52.default.join(pkgRoot, "wrangler.toml"))) return "cloudflare-workers";
|
|
12372
|
+
if (await exists3(import_node_path52.default.join(pkgRoot, "bun.lockb"))) return "bun";
|
|
12373
|
+
if (await exists3(import_node_path52.default.join(pkgRoot, "deno.json")) || await exists3(import_node_path52.default.join(pkgRoot, "deno.lock"))) {
|
|
10644
12374
|
return "deno";
|
|
10645
12375
|
}
|
|
10646
12376
|
const engines = pkg.engines ?? {};
|
|
@@ -10649,7 +12379,7 @@ async function detectRuntimeKind(pkgRoot, pkg) {
|
|
|
10649
12379
|
}
|
|
10650
12380
|
async function readPackageJson2(serviceDir) {
|
|
10651
12381
|
try {
|
|
10652
|
-
const raw = await
|
|
12382
|
+
const raw = await import_node_fs33.promises.readFile(import_node_path52.default.join(serviceDir, "package.json"), "utf8");
|
|
10653
12383
|
return JSON.parse(raw);
|
|
10654
12384
|
} catch {
|
|
10655
12385
|
return null;
|
|
@@ -10657,7 +12387,7 @@ async function readPackageJson2(serviceDir) {
|
|
|
10657
12387
|
}
|
|
10658
12388
|
async function exists3(p) {
|
|
10659
12389
|
try {
|
|
10660
|
-
await
|
|
12390
|
+
await import_node_fs33.promises.stat(p);
|
|
10661
12391
|
return true;
|
|
10662
12392
|
} catch {
|
|
10663
12393
|
return false;
|
|
@@ -10665,7 +12395,7 @@ async function exists3(p) {
|
|
|
10665
12395
|
}
|
|
10666
12396
|
async function readFileMaybe(p) {
|
|
10667
12397
|
try {
|
|
10668
|
-
return await
|
|
12398
|
+
return await import_node_fs33.promises.readFile(p, "utf8");
|
|
10669
12399
|
} catch {
|
|
10670
12400
|
return null;
|
|
10671
12401
|
}
|
|
@@ -10693,7 +12423,7 @@ function needsVersionUpgrade(installed, expected) {
|
|
|
10693
12423
|
var NEXT_CONFIG_CANDIDATES = ["next.config.js", "next.config.ts", "next.config.mjs"];
|
|
10694
12424
|
async function findNextConfig(serviceDir) {
|
|
10695
12425
|
for (const name of NEXT_CONFIG_CANDIDATES) {
|
|
10696
|
-
const candidate =
|
|
12426
|
+
const candidate = import_node_path52.default.join(serviceDir, name);
|
|
10697
12427
|
if (await exists3(candidate)) return candidate;
|
|
10698
12428
|
}
|
|
10699
12429
|
return null;
|
|
@@ -10762,7 +12492,7 @@ function hasRemixDependency(pkg) {
|
|
|
10762
12492
|
}
|
|
10763
12493
|
async function findRemixEntry(serviceDir) {
|
|
10764
12494
|
for (const rel of REMIX_ENTRY_CANDIDATES) {
|
|
10765
|
-
const candidate =
|
|
12495
|
+
const candidate = import_node_path52.default.join(serviceDir, rel);
|
|
10766
12496
|
if (await exists3(candidate)) return candidate;
|
|
10767
12497
|
}
|
|
10768
12498
|
return null;
|
|
@@ -10774,14 +12504,14 @@ function hasSvelteKitDependency(pkg) {
|
|
|
10774
12504
|
}
|
|
10775
12505
|
async function findSvelteKitHooks(serviceDir) {
|
|
10776
12506
|
for (const rel of SVELTEKIT_HOOKS_CANDIDATES) {
|
|
10777
|
-
const candidate =
|
|
12507
|
+
const candidate = import_node_path52.default.join(serviceDir, rel);
|
|
10778
12508
|
if (await exists3(candidate)) return candidate;
|
|
10779
12509
|
}
|
|
10780
12510
|
return null;
|
|
10781
12511
|
}
|
|
10782
12512
|
async function findSvelteKitConfig(serviceDir) {
|
|
10783
12513
|
for (const rel of SVELTEKIT_CONFIG_CANDIDATES) {
|
|
10784
|
-
const candidate =
|
|
12514
|
+
const candidate = import_node_path52.default.join(serviceDir, rel);
|
|
10785
12515
|
if (await exists3(candidate)) return candidate;
|
|
10786
12516
|
}
|
|
10787
12517
|
return null;
|
|
@@ -10792,7 +12522,7 @@ function hasNuxtDependency(pkg) {
|
|
|
10792
12522
|
}
|
|
10793
12523
|
async function findNuxtConfig(serviceDir) {
|
|
10794
12524
|
for (const name of NUXT_CONFIG_CANDIDATES) {
|
|
10795
|
-
const candidate =
|
|
12525
|
+
const candidate = import_node_path52.default.join(serviceDir, name);
|
|
10796
12526
|
if (await exists3(candidate)) return candidate;
|
|
10797
12527
|
}
|
|
10798
12528
|
return null;
|
|
@@ -10803,7 +12533,7 @@ function hasAstroDependency(pkg) {
|
|
|
10803
12533
|
}
|
|
10804
12534
|
async function findAstroConfig(serviceDir) {
|
|
10805
12535
|
for (const name of ASTRO_CONFIG_CANDIDATES) {
|
|
10806
|
-
const candidate =
|
|
12536
|
+
const candidate = import_node_path52.default.join(serviceDir, name);
|
|
10807
12537
|
if (await exists3(candidate)) return candidate;
|
|
10808
12538
|
}
|
|
10809
12539
|
return null;
|
|
@@ -10817,7 +12547,7 @@ function parseNextMajor(range) {
|
|
|
10817
12547
|
return Number.isFinite(n) ? n : null;
|
|
10818
12548
|
}
|
|
10819
12549
|
async function isTypeScriptProject(serviceDir) {
|
|
10820
|
-
return exists3(
|
|
12550
|
+
return exists3(import_node_path52.default.join(serviceDir, "tsconfig.json"));
|
|
10821
12551
|
}
|
|
10822
12552
|
var INDEX_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".cjs"];
|
|
10823
12553
|
var INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `index${ext}`);
|
|
@@ -10866,7 +12596,7 @@ function entryFromScript(script) {
|
|
|
10866
12596
|
}
|
|
10867
12597
|
async function resolveEntry(serviceDir, pkg) {
|
|
10868
12598
|
if (typeof pkg.main === "string" && pkg.main.length > 0) {
|
|
10869
|
-
const candidate =
|
|
12599
|
+
const candidate = import_node_path52.default.resolve(serviceDir, pkg.main);
|
|
10870
12600
|
if (await exists3(candidate)) return candidate;
|
|
10871
12601
|
}
|
|
10872
12602
|
if (pkg.bin) {
|
|
@@ -10880,40 +12610,40 @@ async function resolveEntry(serviceDir, pkg) {
|
|
|
10880
12610
|
if (typeof first === "string") binEntry = first;
|
|
10881
12611
|
}
|
|
10882
12612
|
if (binEntry) {
|
|
10883
|
-
const candidate =
|
|
12613
|
+
const candidate = import_node_path52.default.resolve(serviceDir, binEntry);
|
|
10884
12614
|
if (await exists3(candidate)) return candidate;
|
|
10885
12615
|
}
|
|
10886
12616
|
}
|
|
10887
12617
|
const startEntry = entryFromScript(pkg.scripts?.start);
|
|
10888
12618
|
if (startEntry) {
|
|
10889
|
-
const candidate =
|
|
12619
|
+
const candidate = import_node_path52.default.resolve(serviceDir, startEntry);
|
|
10890
12620
|
if (await exists3(candidate)) return candidate;
|
|
10891
12621
|
}
|
|
10892
12622
|
const devEntry = entryFromScript(pkg.scripts?.dev);
|
|
10893
12623
|
if (devEntry) {
|
|
10894
|
-
const candidate =
|
|
12624
|
+
const candidate = import_node_path52.default.resolve(serviceDir, devEntry);
|
|
10895
12625
|
if (await exists3(candidate)) return candidate;
|
|
10896
12626
|
}
|
|
10897
12627
|
for (const rel of SRC_INDEX_CANDIDATES) {
|
|
10898
|
-
const candidate =
|
|
12628
|
+
const candidate = import_node_path52.default.join(serviceDir, rel);
|
|
10899
12629
|
if (await exists3(candidate)) return candidate;
|
|
10900
12630
|
}
|
|
10901
12631
|
for (const rel of SRC_NAMED_CANDIDATES) {
|
|
10902
|
-
const candidate =
|
|
12632
|
+
const candidate = import_node_path52.default.join(serviceDir, rel);
|
|
10903
12633
|
if (await exists3(candidate)) return candidate;
|
|
10904
12634
|
}
|
|
10905
12635
|
for (const rel of ROOT_NAMED_CANDIDATES) {
|
|
10906
|
-
const candidate =
|
|
12636
|
+
const candidate = import_node_path52.default.join(serviceDir, rel);
|
|
10907
12637
|
if (await exists3(candidate)) return candidate;
|
|
10908
12638
|
}
|
|
10909
12639
|
for (const name of INDEX_CANDIDATES) {
|
|
10910
|
-
const candidate =
|
|
12640
|
+
const candidate = import_node_path52.default.join(serviceDir, name);
|
|
10911
12641
|
if (await exists3(candidate)) return candidate;
|
|
10912
12642
|
}
|
|
10913
12643
|
return null;
|
|
10914
12644
|
}
|
|
10915
12645
|
function dispatchEntry(entryFile, pkg) {
|
|
10916
|
-
const ext =
|
|
12646
|
+
const ext = import_node_path52.default.extname(entryFile).toLowerCase();
|
|
10917
12647
|
if (ext === ".ts" || ext === ".tsx") return "ts";
|
|
10918
12648
|
if (ext === ".mjs") return "esm";
|
|
10919
12649
|
if (ext === ".cjs") return "cjs";
|
|
@@ -10930,9 +12660,9 @@ function otelInitContents(flavor) {
|
|
|
10930
12660
|
return OTEL_INIT_CJS;
|
|
10931
12661
|
}
|
|
10932
12662
|
function injectionLine(flavor, entryFile, otelInitFile) {
|
|
10933
|
-
let rel =
|
|
12663
|
+
let rel = import_node_path52.default.relative(import_node_path52.default.dirname(entryFile), otelInitFile);
|
|
10934
12664
|
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
10935
|
-
rel = rel.split(
|
|
12665
|
+
rel = rel.split(import_node_path52.default.sep).join("/");
|
|
10936
12666
|
if (flavor === "cjs") return `require('${rel}')`;
|
|
10937
12667
|
if (flavor === "esm") return `import '${rel}'`;
|
|
10938
12668
|
const tsRel = rel.replace(/\.ts$/, "");
|
|
@@ -10945,27 +12675,27 @@ function lineIsOtelInjection(line) {
|
|
|
10945
12675
|
}
|
|
10946
12676
|
async function detectsSrcLayout(serviceDir) {
|
|
10947
12677
|
const [hasSrcApp, hasSrcPages, hasRootApp, hasRootPages] = await Promise.all([
|
|
10948
|
-
exists3(
|
|
10949
|
-
exists3(
|
|
10950
|
-
exists3(
|
|
10951
|
-
exists3(
|
|
12678
|
+
exists3(import_node_path52.default.join(serviceDir, "src", "app")),
|
|
12679
|
+
exists3(import_node_path52.default.join(serviceDir, "src", "pages")),
|
|
12680
|
+
exists3(import_node_path52.default.join(serviceDir, "app")),
|
|
12681
|
+
exists3(import_node_path52.default.join(serviceDir, "pages"))
|
|
10952
12682
|
]);
|
|
10953
12683
|
return (hasSrcApp || hasSrcPages) && !hasRootApp && !hasRootPages;
|
|
10954
12684
|
}
|
|
10955
12685
|
async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project) {
|
|
10956
12686
|
const useTs = await isTypeScriptProject(serviceDir);
|
|
10957
12687
|
const srcLayout = await detectsSrcLayout(serviceDir);
|
|
10958
|
-
const baseDir = srcLayout ?
|
|
10959
|
-
const instrumentationFile =
|
|
10960
|
-
const instrumentationNodeFile =
|
|
12688
|
+
const baseDir = srcLayout ? import_node_path52.default.join(serviceDir, "src") : serviceDir;
|
|
12689
|
+
const instrumentationFile = import_node_path52.default.join(baseDir, useTs ? "instrumentation.ts" : "instrumentation.js");
|
|
12690
|
+
const instrumentationNodeFile = import_node_path52.default.join(
|
|
10961
12691
|
baseDir,
|
|
10962
12692
|
useTs ? "instrumentation.node.ts" : "instrumentation.node.js"
|
|
10963
12693
|
);
|
|
10964
|
-
const instrumentationEdgeFile =
|
|
12694
|
+
const instrumentationEdgeFile = import_node_path52.default.join(
|
|
10965
12695
|
baseDir,
|
|
10966
12696
|
useTs ? "instrumentation.edge.ts" : "instrumentation.edge.js"
|
|
10967
12697
|
);
|
|
10968
|
-
const envNeatFile =
|
|
12698
|
+
const envNeatFile = import_node_path52.default.join(baseDir, ".env.neat");
|
|
10969
12699
|
const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
10970
12700
|
const dependencyEdits = [];
|
|
10971
12701
|
for (const sdk of SDK_PACKAGES) {
|
|
@@ -11042,7 +12772,7 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
|
|
|
11042
12772
|
const nextMajor = parseNextMajor(nextRange);
|
|
11043
12773
|
if (nextMajor !== null && nextMajor < 15) {
|
|
11044
12774
|
try {
|
|
11045
|
-
const raw = await
|
|
12775
|
+
const raw = await import_node_fs33.promises.readFile(nextConfigPath, "utf8");
|
|
11046
12776
|
if (!raw.includes("instrumentationHook")) {
|
|
11047
12777
|
nextConfigEdit = {
|
|
11048
12778
|
file: nextConfigPath,
|
|
@@ -11090,7 +12820,7 @@ function buildDependencyEdits(pkg, manifestPath) {
|
|
|
11090
12820
|
return edits;
|
|
11091
12821
|
}
|
|
11092
12822
|
async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
|
|
11093
|
-
const envNeatFile =
|
|
12823
|
+
const envNeatFile = import_node_path52.default.join(serviceDir, ".env.neat");
|
|
11094
12824
|
if (!await exists3(envNeatFile)) {
|
|
11095
12825
|
generatedFiles.push({
|
|
11096
12826
|
file: envNeatFile,
|
|
@@ -11125,7 +12855,7 @@ function fileImportsOtelHook(raw, specifiers) {
|
|
|
11125
12855
|
}
|
|
11126
12856
|
async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
|
|
11127
12857
|
const useTs = await isTypeScriptProject(serviceDir);
|
|
11128
|
-
const otelServerFile =
|
|
12858
|
+
const otelServerFile = import_node_path52.default.join(
|
|
11129
12859
|
serviceDir,
|
|
11130
12860
|
useTs ? "app/otel.server.ts" : "app/otel.server.js"
|
|
11131
12861
|
);
|
|
@@ -11146,7 +12876,7 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
|
|
|
11146
12876
|
await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
|
|
11147
12877
|
const entrypointEdits = [];
|
|
11148
12878
|
try {
|
|
11149
|
-
const raw = await
|
|
12879
|
+
const raw = await import_node_fs33.promises.readFile(entryFile, "utf8");
|
|
11150
12880
|
if (!fileImportsOtelHook(raw, ["./otel.server"])) {
|
|
11151
12881
|
const lines = raw.split(/\r?\n/);
|
|
11152
12882
|
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
@@ -11182,11 +12912,11 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
|
|
|
11182
12912
|
}
|
|
11183
12913
|
async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project) {
|
|
11184
12914
|
const useTs = await isTypeScriptProject(serviceDir);
|
|
11185
|
-
const otelInitFile =
|
|
12915
|
+
const otelInitFile = import_node_path52.default.join(
|
|
11186
12916
|
serviceDir,
|
|
11187
12917
|
useTs ? "src/otel-init.ts" : "src/otel-init.js"
|
|
11188
12918
|
);
|
|
11189
|
-
const resolvedHooksFile = hooksFile ??
|
|
12919
|
+
const resolvedHooksFile = hooksFile ?? import_node_path52.default.join(serviceDir, useTs ? "src/hooks.server.ts" : "src/hooks.server.js");
|
|
11190
12920
|
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
11191
12921
|
const generatedFiles = [];
|
|
11192
12922
|
const entrypointEdits = [];
|
|
@@ -11211,7 +12941,7 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project)
|
|
|
11211
12941
|
});
|
|
11212
12942
|
} else {
|
|
11213
12943
|
try {
|
|
11214
|
-
const raw = await
|
|
12944
|
+
const raw = await import_node_fs33.promises.readFile(hooksFile, "utf8");
|
|
11215
12945
|
if (!fileImportsOtelHook(raw, ["./otel-init"])) {
|
|
11216
12946
|
const lines = raw.split(/\r?\n/);
|
|
11217
12947
|
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
@@ -11248,11 +12978,11 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project)
|
|
|
11248
12978
|
}
|
|
11249
12979
|
async function planNuxt(serviceDir, pkg, manifestPath, project) {
|
|
11250
12980
|
const useTs = await isTypeScriptProject(serviceDir);
|
|
11251
|
-
const otelPluginFile =
|
|
12981
|
+
const otelPluginFile = import_node_path52.default.join(
|
|
11252
12982
|
serviceDir,
|
|
11253
12983
|
useTs ? "server/plugins/otel.ts" : "server/plugins/otel.js"
|
|
11254
12984
|
);
|
|
11255
|
-
const otelInitFile =
|
|
12985
|
+
const otelInitFile = import_node_path52.default.join(
|
|
11256
12986
|
serviceDir,
|
|
11257
12987
|
useTs ? "server/plugins/otel-init.ts" : "server/plugins/otel-init.js"
|
|
11258
12988
|
);
|
|
@@ -11303,19 +13033,19 @@ async function planNuxt(serviceDir, pkg, manifestPath, project) {
|
|
|
11303
13033
|
var ASTRO_MIDDLEWARE_CANDIDATES = ["src/middleware.ts", "src/middleware.js"];
|
|
11304
13034
|
async function findAstroMiddleware(serviceDir) {
|
|
11305
13035
|
for (const rel of ASTRO_MIDDLEWARE_CANDIDATES) {
|
|
11306
|
-
const candidate =
|
|
13036
|
+
const candidate = import_node_path52.default.join(serviceDir, rel);
|
|
11307
13037
|
if (await exists3(candidate)) return candidate;
|
|
11308
13038
|
}
|
|
11309
13039
|
return null;
|
|
11310
13040
|
}
|
|
11311
13041
|
async function planAstro(serviceDir, pkg, manifestPath, project) {
|
|
11312
13042
|
const useTs = await isTypeScriptProject(serviceDir);
|
|
11313
|
-
const otelInitFile =
|
|
13043
|
+
const otelInitFile = import_node_path52.default.join(
|
|
11314
13044
|
serviceDir,
|
|
11315
13045
|
useTs ? "src/otel-init.ts" : "src/otel-init.js"
|
|
11316
13046
|
);
|
|
11317
13047
|
const existingMiddleware = await findAstroMiddleware(serviceDir);
|
|
11318
|
-
const middlewareFile = existingMiddleware ??
|
|
13048
|
+
const middlewareFile = existingMiddleware ?? import_node_path52.default.join(serviceDir, useTs ? "src/middleware.ts" : "src/middleware.js");
|
|
11319
13049
|
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
11320
13050
|
const generatedFiles = [];
|
|
11321
13051
|
const entrypointEdits = [];
|
|
@@ -11340,7 +13070,7 @@ async function planAstro(serviceDir, pkg, manifestPath, project) {
|
|
|
11340
13070
|
});
|
|
11341
13071
|
} else {
|
|
11342
13072
|
try {
|
|
11343
|
-
const raw = await
|
|
13073
|
+
const raw = await import_node_fs33.promises.readFile(existingMiddleware, "utf8");
|
|
11344
13074
|
if (!fileImportsOtelHook(raw, ["./otel-init"])) {
|
|
11345
13075
|
const lines = raw.split(/\r?\n/);
|
|
11346
13076
|
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
@@ -11411,7 +13141,7 @@ async function findFrameworkDispatch(serviceDir, pkg, manifestPath, project) {
|
|
|
11411
13141
|
}
|
|
11412
13142
|
async function plan(serviceDir, opts) {
|
|
11413
13143
|
const pkg = await readPackageJson2(serviceDir);
|
|
11414
|
-
const manifestPath =
|
|
13144
|
+
const manifestPath = import_node_path52.default.join(serviceDir, "package.json");
|
|
11415
13145
|
const project = opts?.project;
|
|
11416
13146
|
const empty = {
|
|
11417
13147
|
language: "javascript",
|
|
@@ -11446,8 +13176,8 @@ async function plan(serviceDir, opts) {
|
|
|
11446
13176
|
return { ...empty, libOnly: true };
|
|
11447
13177
|
}
|
|
11448
13178
|
const flavor = dispatchEntry(entryFile, pkg);
|
|
11449
|
-
const otelInitFile =
|
|
11450
|
-
const envNeatFile =
|
|
13179
|
+
const otelInitFile = import_node_path52.default.join(import_node_path52.default.dirname(entryFile), otelInitFilename(flavor));
|
|
13180
|
+
const envNeatFile = import_node_path52.default.join(serviceDir, ".env.neat");
|
|
11451
13181
|
const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
11452
13182
|
const dependencyEdits = [];
|
|
11453
13183
|
for (const sdk of SDK_PACKAGES) {
|
|
@@ -11471,7 +13201,7 @@ async function plan(serviceDir, opts) {
|
|
|
11471
13201
|
}
|
|
11472
13202
|
const entrypointEdits = [];
|
|
11473
13203
|
try {
|
|
11474
|
-
const raw = await
|
|
13204
|
+
const raw = await import_node_fs33.promises.readFile(entryFile, "utf8");
|
|
11475
13205
|
const lines = raw.split(/\r?\n/);
|
|
11476
13206
|
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
11477
13207
|
if (!lineIsOtelInjection(firstReal)) {
|
|
@@ -11516,13 +13246,13 @@ async function plan(serviceDir, opts) {
|
|
|
11516
13246
|
};
|
|
11517
13247
|
}
|
|
11518
13248
|
function isAllowedWritePath(serviceDir, target) {
|
|
11519
|
-
const rel =
|
|
13249
|
+
const rel = import_node_path52.default.relative(serviceDir, target);
|
|
11520
13250
|
if (rel.startsWith("..")) return false;
|
|
11521
|
-
const base =
|
|
13251
|
+
const base = import_node_path52.default.basename(target);
|
|
11522
13252
|
if (base === "package.json") return true;
|
|
11523
13253
|
if (base === ".env.neat") return true;
|
|
11524
13254
|
if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
|
|
11525
|
-
const relPosix = rel.split(
|
|
13255
|
+
const relPosix = rel.split(import_node_path52.default.sep).join("/");
|
|
11526
13256
|
if (/^instrumentation(?:\.(?:node|edge))?\.(?:js|cjs|mjs|ts)$/.test(base)) {
|
|
11527
13257
|
if (relPosix === base) return true;
|
|
11528
13258
|
if (relPosix === `src/${base}`) return true;
|
|
@@ -11539,10 +13269,10 @@ function isAllowedWritePath(serviceDir, target) {
|
|
|
11539
13269
|
return false;
|
|
11540
13270
|
}
|
|
11541
13271
|
async function writeAtomic(file, contents) {
|
|
11542
|
-
await
|
|
13272
|
+
await import_node_fs33.promises.mkdir(import_node_path52.default.dirname(file), { recursive: true });
|
|
11543
13273
|
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
11544
|
-
await
|
|
11545
|
-
await
|
|
13274
|
+
await import_node_fs33.promises.writeFile(tmp, contents, "utf8");
|
|
13275
|
+
await import_node_fs33.promises.rename(tmp, file);
|
|
11546
13276
|
}
|
|
11547
13277
|
async function apply(installPlan) {
|
|
11548
13278
|
const { serviceDir } = installPlan;
|
|
@@ -11618,7 +13348,7 @@ async function apply(installPlan) {
|
|
|
11618
13348
|
for (const target of allTargets) {
|
|
11619
13349
|
if (await exists3(target)) {
|
|
11620
13350
|
try {
|
|
11621
|
-
originals.set(target, await
|
|
13351
|
+
originals.set(target, await import_node_fs33.promises.readFile(target, "utf8"));
|
|
11622
13352
|
} catch {
|
|
11623
13353
|
}
|
|
11624
13354
|
}
|
|
@@ -11701,14 +13431,14 @@ async function rollback(installPlan, originals, createdFiles) {
|
|
|
11701
13431
|
const removed = [];
|
|
11702
13432
|
for (const [file, raw] of originals.entries()) {
|
|
11703
13433
|
try {
|
|
11704
|
-
await
|
|
13434
|
+
await import_node_fs33.promises.writeFile(file, raw, "utf8");
|
|
11705
13435
|
restored.push(file);
|
|
11706
13436
|
} catch {
|
|
11707
13437
|
}
|
|
11708
13438
|
}
|
|
11709
13439
|
for (const file of createdFiles) {
|
|
11710
13440
|
try {
|
|
11711
|
-
await
|
|
13441
|
+
await import_node_fs33.promises.unlink(file);
|
|
11712
13442
|
removed.push(file);
|
|
11713
13443
|
} catch {
|
|
11714
13444
|
}
|
|
@@ -11723,8 +13453,8 @@ async function rollback(installPlan, originals, createdFiles) {
|
|
|
11723
13453
|
...removed.map((f) => `removed: ${f}`),
|
|
11724
13454
|
""
|
|
11725
13455
|
];
|
|
11726
|
-
const rollbackPath =
|
|
11727
|
-
await
|
|
13456
|
+
const rollbackPath = import_node_path52.default.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
13457
|
+
await import_node_fs33.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
|
|
11728
13458
|
}
|
|
11729
13459
|
function injectInstrumentationHook(raw) {
|
|
11730
13460
|
if (raw.includes("instrumentationHook")) return raw;
|
|
@@ -11753,8 +13483,8 @@ var javascriptInstaller = {
|
|
|
11753
13483
|
|
|
11754
13484
|
// src/installers/python.ts
|
|
11755
13485
|
init_cjs_shims();
|
|
11756
|
-
var
|
|
11757
|
-
var
|
|
13486
|
+
var import_node_fs34 = require("fs");
|
|
13487
|
+
var import_node_path53 = __toESM(require("path"), 1);
|
|
11758
13488
|
var SDK_PACKAGES2 = [
|
|
11759
13489
|
{ name: "opentelemetry-distro", version: ">=0.49b0" },
|
|
11760
13490
|
{ name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
|
|
@@ -11766,7 +13496,7 @@ var OTEL_ENV2 = {
|
|
|
11766
13496
|
};
|
|
11767
13497
|
async function exists4(p) {
|
|
11768
13498
|
try {
|
|
11769
|
-
await
|
|
13499
|
+
await import_node_fs34.promises.stat(p);
|
|
11770
13500
|
return true;
|
|
11771
13501
|
} catch {
|
|
11772
13502
|
return false;
|
|
@@ -11775,7 +13505,7 @@ async function exists4(p) {
|
|
|
11775
13505
|
async function detect2(serviceDir) {
|
|
11776
13506
|
const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
|
|
11777
13507
|
for (const m of markers) {
|
|
11778
|
-
if (await exists4(
|
|
13508
|
+
if (await exists4(import_node_path53.default.join(serviceDir, m))) return true;
|
|
11779
13509
|
}
|
|
11780
13510
|
return false;
|
|
11781
13511
|
}
|
|
@@ -11785,9 +13515,9 @@ function reqPackageName(line) {
|
|
|
11785
13515
|
return head.replace(/[<>=!~].*$/, "").toLowerCase();
|
|
11786
13516
|
}
|
|
11787
13517
|
async function planRequirementsTxtEdits(serviceDir) {
|
|
11788
|
-
const file =
|
|
13518
|
+
const file = import_node_path53.default.join(serviceDir, "requirements.txt");
|
|
11789
13519
|
if (!await exists4(file)) return null;
|
|
11790
|
-
const raw = await
|
|
13520
|
+
const raw = await import_node_fs34.promises.readFile(file, "utf8");
|
|
11791
13521
|
const presentNames = new Set(
|
|
11792
13522
|
raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
|
|
11793
13523
|
);
|
|
@@ -11795,9 +13525,9 @@ async function planRequirementsTxtEdits(serviceDir) {
|
|
|
11795
13525
|
return { manifest: file, missing: [...missing] };
|
|
11796
13526
|
}
|
|
11797
13527
|
async function planProcfileEdits(serviceDir) {
|
|
11798
|
-
const procfile =
|
|
13528
|
+
const procfile = import_node_path53.default.join(serviceDir, "Procfile");
|
|
11799
13529
|
if (!await exists4(procfile)) return [];
|
|
11800
|
-
const raw = await
|
|
13530
|
+
const raw = await import_node_fs34.promises.readFile(procfile, "utf8");
|
|
11801
13531
|
const edits = [];
|
|
11802
13532
|
for (const line of raw.split(/\r?\n/)) {
|
|
11803
13533
|
if (line.length === 0) continue;
|
|
@@ -11849,8 +13579,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
|
|
|
11849
13579
|
const next = `${original}${trailing}${newlines.join("\n")}
|
|
11850
13580
|
`;
|
|
11851
13581
|
const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
|
|
11852
|
-
await
|
|
11853
|
-
await
|
|
13582
|
+
await import_node_fs34.promises.writeFile(tmp, next, "utf8");
|
|
13583
|
+
await import_node_fs34.promises.rename(tmp, manifest);
|
|
11854
13584
|
}
|
|
11855
13585
|
async function applyProcfile(procfile, edits, original) {
|
|
11856
13586
|
let next = original;
|
|
@@ -11859,8 +13589,8 @@ async function applyProcfile(procfile, edits, original) {
|
|
|
11859
13589
|
next = next.replace(e.before, e.after);
|
|
11860
13590
|
}
|
|
11861
13591
|
const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
|
|
11862
|
-
await
|
|
11863
|
-
await
|
|
13592
|
+
await import_node_fs34.promises.writeFile(tmp, next, "utf8");
|
|
13593
|
+
await import_node_fs34.promises.rename(tmp, procfile);
|
|
11864
13594
|
}
|
|
11865
13595
|
async function apply2(installPlan) {
|
|
11866
13596
|
const { serviceDir } = installPlan;
|
|
@@ -11873,7 +13603,7 @@ async function apply2(installPlan) {
|
|
|
11873
13603
|
const originals = /* @__PURE__ */ new Map();
|
|
11874
13604
|
for (const file of touched) {
|
|
11875
13605
|
try {
|
|
11876
|
-
originals.set(file, await
|
|
13606
|
+
originals.set(file, await import_node_fs34.promises.readFile(file, "utf8"));
|
|
11877
13607
|
} catch {
|
|
11878
13608
|
}
|
|
11879
13609
|
}
|
|
@@ -11884,7 +13614,7 @@ async function apply2(installPlan) {
|
|
|
11884
13614
|
if (raw === void 0) {
|
|
11885
13615
|
throw new Error(`python installer: cannot read ${file} during apply`);
|
|
11886
13616
|
}
|
|
11887
|
-
const base =
|
|
13617
|
+
const base = import_node_path53.default.basename(file);
|
|
11888
13618
|
if (base === "requirements.txt") {
|
|
11889
13619
|
const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
|
|
11890
13620
|
if (edits.length > 0) {
|
|
@@ -11909,7 +13639,7 @@ async function rollback2(installPlan, originals) {
|
|
|
11909
13639
|
const restored = [];
|
|
11910
13640
|
for (const [file, raw] of originals.entries()) {
|
|
11911
13641
|
try {
|
|
11912
|
-
await
|
|
13642
|
+
await import_node_fs34.promises.writeFile(file, raw, "utf8");
|
|
11913
13643
|
restored.push(file);
|
|
11914
13644
|
} catch {
|
|
11915
13645
|
}
|
|
@@ -11923,8 +13653,8 @@ async function rollback2(installPlan, originals) {
|
|
|
11923
13653
|
...restored.map((f) => `restored: ${f}`),
|
|
11924
13654
|
""
|
|
11925
13655
|
];
|
|
11926
|
-
const rollbackPath =
|
|
11927
|
-
await
|
|
13656
|
+
const rollbackPath = import_node_path53.default.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
13657
|
+
await import_node_fs34.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
|
|
11928
13658
|
}
|
|
11929
13659
|
var pythonInstaller = {
|
|
11930
13660
|
name: "python",
|
|
@@ -12047,10 +13777,10 @@ function renderPatch(sections) {
|
|
|
12047
13777
|
|
|
12048
13778
|
// src/orchestrator.ts
|
|
12049
13779
|
init_cjs_shims();
|
|
12050
|
-
var
|
|
13780
|
+
var import_node_fs35 = require("fs");
|
|
12051
13781
|
var import_node_http = __toESM(require("http"), 1);
|
|
12052
13782
|
var import_node_net = __toESM(require("net"), 1);
|
|
12053
|
-
var
|
|
13783
|
+
var import_node_path54 = __toESM(require("path"), 1);
|
|
12054
13784
|
var import_node_child_process3 = require("child_process");
|
|
12055
13785
|
var import_node_readline = __toESM(require("readline"), 1);
|
|
12056
13786
|
async function extractAndPersist(opts) {
|
|
@@ -12059,7 +13789,7 @@ async function extractAndPersist(opts) {
|
|
|
12059
13789
|
const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
|
|
12060
13790
|
resetGraph(graphKey);
|
|
12061
13791
|
const graph = getGraph(graphKey);
|
|
12062
|
-
const projectPaths = pathsForProject(graphKey,
|
|
13792
|
+
const projectPaths = pathsForProject(graphKey, import_node_path54.default.join(opts.scanPath, "neat-out"));
|
|
12063
13793
|
const extraction = await extractFromDirectory(graph, opts.scanPath, {
|
|
12064
13794
|
errorsPath: projectPaths.errorsPath
|
|
12065
13795
|
});
|
|
@@ -12111,7 +13841,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
12111
13841
|
libOnly++;
|
|
12112
13842
|
const appDeps = svc.pkg ? appFrameworkDependencies(svc.pkg) : [];
|
|
12113
13843
|
if (appDeps.length > 0) {
|
|
12114
|
-
const svcName =
|
|
13844
|
+
const svcName = import_node_path54.default.basename(svc.dir);
|
|
12115
13845
|
const list = appDeps.join(", ");
|
|
12116
13846
|
console.warn(
|
|
12117
13847
|
`neat: runtime layer won't engage for ${svcName}: no entry point found.
|
|
@@ -12124,7 +13854,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
12124
13854
|
console.log(`skipping ${svc.dir}: browser bundle; browser-OTel support lands in a future release.`);
|
|
12125
13855
|
} else if (outcome.outcome === "react-native") {
|
|
12126
13856
|
reactNative++;
|
|
12127
|
-
const svcName =
|
|
13857
|
+
const svcName = import_node_path54.default.basename(svc.dir);
|
|
12128
13858
|
console.log(
|
|
12129
13859
|
`neat: ${svc.dir} detected as React Native / Expo
|
|
12130
13860
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -12135,7 +13865,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
12135
13865
|
);
|
|
12136
13866
|
} else if (outcome.outcome === "bun") {
|
|
12137
13867
|
bun++;
|
|
12138
|
-
const svcName =
|
|
13868
|
+
const svcName = import_node_path54.default.basename(svc.dir);
|
|
12139
13869
|
console.log(
|
|
12140
13870
|
`neat: ${svc.dir} detected as Bun
|
|
12141
13871
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -12146,7 +13876,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
12146
13876
|
);
|
|
12147
13877
|
} else if (outcome.outcome === "deno") {
|
|
12148
13878
|
deno++;
|
|
12149
|
-
const svcName =
|
|
13879
|
+
const svcName = import_node_path54.default.basename(svc.dir);
|
|
12150
13880
|
console.log(
|
|
12151
13881
|
`neat: ${svc.dir} detected as Deno
|
|
12152
13882
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -12157,7 +13887,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
12157
13887
|
);
|
|
12158
13888
|
} else if (outcome.outcome === "cloudflare-workers") {
|
|
12159
13889
|
cloudflareWorkers++;
|
|
12160
|
-
const svcName =
|
|
13890
|
+
const svcName = import_node_path54.default.basename(svc.dir);
|
|
12161
13891
|
console.log(
|
|
12162
13892
|
`neat: ${svc.dir} detected as Cloudflare Workers
|
|
12163
13893
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -12168,7 +13898,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
12168
13898
|
);
|
|
12169
13899
|
} else if (outcome.outcome === "electron") {
|
|
12170
13900
|
electron++;
|
|
12171
|
-
const svcName =
|
|
13901
|
+
const svcName = import_node_path54.default.basename(svc.dir);
|
|
12172
13902
|
console.log(
|
|
12173
13903
|
`neat: ${svc.dir} detected as Electron
|
|
12174
13904
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -12181,7 +13911,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
12181
13911
|
if (svc.pkg && (outcome.outcome === "instrumented" || outcome.outcome === "already-instrumented")) {
|
|
12182
13912
|
const gaps = uninstrumentedLibraries(svc.pkg);
|
|
12183
13913
|
if (gaps.length > 0) {
|
|
12184
|
-
const svcName =
|
|
13914
|
+
const svcName = import_node_path54.default.basename(svc.dir);
|
|
12185
13915
|
const list = gaps.join(", ");
|
|
12186
13916
|
const subject = gaps.length === 1 ? "this library" : "these libraries";
|
|
12187
13917
|
const aux = gaps.length === 1 ? "isn't" : "aren't";
|
|
@@ -12387,24 +14117,24 @@ async function persistedPortsFor(scanPath) {
|
|
|
12387
14117
|
return { rest: record.ports.rest, otlp: record.ports.otlp, web: record.ports.web };
|
|
12388
14118
|
}
|
|
12389
14119
|
async function acquireSpawnLock(scanPath) {
|
|
12390
|
-
const lockPath =
|
|
12391
|
-
await
|
|
14120
|
+
const lockPath = import_node_path54.default.join(scanPath, "neat-out", "daemon.spawn.lock");
|
|
14121
|
+
await import_node_fs35.promises.mkdir(import_node_path54.default.dirname(lockPath), { recursive: true });
|
|
12392
14122
|
const STALE_LOCK_MS = 6e4;
|
|
12393
14123
|
try {
|
|
12394
|
-
const fd = await
|
|
14124
|
+
const fd = await import_node_fs35.promises.open(lockPath, "wx");
|
|
12395
14125
|
await fd.writeFile(`${process.pid}
|
|
12396
14126
|
`, "utf8");
|
|
12397
14127
|
await fd.close();
|
|
12398
14128
|
return async () => {
|
|
12399
|
-
await
|
|
14129
|
+
await import_node_fs35.promises.unlink(lockPath).catch(() => {
|
|
12400
14130
|
});
|
|
12401
14131
|
};
|
|
12402
14132
|
} catch (err) {
|
|
12403
14133
|
if (err.code !== "EEXIST") return null;
|
|
12404
14134
|
try {
|
|
12405
|
-
const stat = await
|
|
14135
|
+
const stat = await import_node_fs35.promises.stat(lockPath);
|
|
12406
14136
|
if (Date.now() - stat.mtimeMs > STALE_LOCK_MS) {
|
|
12407
|
-
await
|
|
14137
|
+
await import_node_fs35.promises.unlink(lockPath).catch(() => {
|
|
12408
14138
|
});
|
|
12409
14139
|
return acquireSpawnLock(scanPath);
|
|
12410
14140
|
}
|
|
@@ -12433,13 +14163,13 @@ async function healthIsForProject(restPort, project) {
|
|
|
12433
14163
|
return false;
|
|
12434
14164
|
}
|
|
12435
14165
|
function daemonLogPath(projectPath2) {
|
|
12436
|
-
return
|
|
14166
|
+
return import_node_path54.default.join(projectPath2, "neat-out", "daemon.log");
|
|
12437
14167
|
}
|
|
12438
14168
|
function spawnDaemonDetached(spec) {
|
|
12439
|
-
const here =
|
|
14169
|
+
const here = import_node_path54.default.dirname(new URL(importMetaUrl).pathname);
|
|
12440
14170
|
const candidates = [
|
|
12441
|
-
|
|
12442
|
-
|
|
14171
|
+
import_node_path54.default.join(here, "neatd.cjs"),
|
|
14172
|
+
import_node_path54.default.join(here, "neatd.js")
|
|
12443
14173
|
];
|
|
12444
14174
|
let entry2 = null;
|
|
12445
14175
|
const fsSync = require("fs");
|
|
@@ -12469,7 +14199,7 @@ function spawnDaemonDetached(spec) {
|
|
|
12469
14199
|
let logFd = null;
|
|
12470
14200
|
if (spec) {
|
|
12471
14201
|
const logPath = daemonLogPath(spec.projectPath);
|
|
12472
|
-
fsSync.mkdirSync(
|
|
14202
|
+
fsSync.mkdirSync(import_node_path54.default.dirname(logPath), { recursive: true });
|
|
12473
14203
|
logFd = fsSync.openSync(logPath, "a");
|
|
12474
14204
|
}
|
|
12475
14205
|
const child = (0, import_node_child_process3.spawn)(process.execPath, [entry2, "start"], {
|
|
@@ -12508,7 +14238,7 @@ async function runOrchestrator(opts) {
|
|
|
12508
14238
|
browser: "skipped"
|
|
12509
14239
|
}
|
|
12510
14240
|
};
|
|
12511
|
-
const stat = await
|
|
14241
|
+
const stat = await import_node_fs35.promises.stat(opts.scanPath).catch(() => null);
|
|
12512
14242
|
if (!stat || !stat.isDirectory()) {
|
|
12513
14243
|
console.error(`neat: ${opts.scanPath} is not a directory`);
|
|
12514
14244
|
result.exitCode = 2;
|
|
@@ -12668,7 +14398,7 @@ async function runOrchestrator(opts) {
|
|
|
12668
14398
|
result.steps.browser = openBrowser(dashboardUrl);
|
|
12669
14399
|
}
|
|
12670
14400
|
const daemonRunning = result.steps.daemon === "spawned" || result.steps.daemon === "already-running";
|
|
12671
|
-
const daemonLog = daemonRunning ?
|
|
14401
|
+
const daemonLog = daemonRunning ? import_node_path54.default.relative(opts.scanPath, daemonLogPath(opts.scanPath)) : null;
|
|
12672
14402
|
printSummary(result, graph, dashboardUrl, daemonLog);
|
|
12673
14403
|
return result;
|
|
12674
14404
|
}
|
|
@@ -12704,13 +14434,343 @@ function printSummary(result, graph, dashboardUrl, daemonLog) {
|
|
|
12704
14434
|
}
|
|
12705
14435
|
}
|
|
12706
14436
|
|
|
14437
|
+
// src/connector-cli.ts
|
|
14438
|
+
init_cjs_shims();
|
|
14439
|
+
var import_node_readline2 = __toESM(require("readline"), 1);
|
|
14440
|
+
async function defaultPrompt(question) {
|
|
14441
|
+
if (!process.stdin.isTTY) return "";
|
|
14442
|
+
const rl = import_node_readline2.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
14443
|
+
try {
|
|
14444
|
+
return await new Promise((resolve) => rl.question(`${question} `, resolve));
|
|
14445
|
+
} finally {
|
|
14446
|
+
rl.close();
|
|
14447
|
+
}
|
|
14448
|
+
}
|
|
14449
|
+
function resolveDeps(deps) {
|
|
14450
|
+
return {
|
|
14451
|
+
home: deps.home,
|
|
14452
|
+
env: deps.env ?? process.env,
|
|
14453
|
+
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {},
|
|
14454
|
+
prompt: deps.prompt ?? defaultPrompt,
|
|
14455
|
+
interactive: deps.interactive ?? Boolean(process.stdin.isTTY),
|
|
14456
|
+
out: deps.out ?? ((l) => console.log(l)),
|
|
14457
|
+
err: deps.err ?? ((l) => console.error(l))
|
|
14458
|
+
};
|
|
14459
|
+
}
|
|
14460
|
+
function kebabToCamel(name) {
|
|
14461
|
+
return name.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
14462
|
+
}
|
|
14463
|
+
function coerceFieldValue(name, raw) {
|
|
14464
|
+
const t = raw.trim();
|
|
14465
|
+
if (t.startsWith("{") || t.startsWith("[")) {
|
|
14466
|
+
try {
|
|
14467
|
+
return JSON.parse(t);
|
|
14468
|
+
} catch {
|
|
14469
|
+
return raw;
|
|
14470
|
+
}
|
|
14471
|
+
}
|
|
14472
|
+
if (/Ms$/.test(name) && /^\d+$/.test(t)) return Number(t);
|
|
14473
|
+
return raw;
|
|
14474
|
+
}
|
|
14475
|
+
function parseConnectorArgs(args) {
|
|
14476
|
+
const out = {
|
|
14477
|
+
subcommand: "",
|
|
14478
|
+
positional: [],
|
|
14479
|
+
skipValidate: false,
|
|
14480
|
+
plaintext: false,
|
|
14481
|
+
fields: {}
|
|
14482
|
+
};
|
|
14483
|
+
const positional = [];
|
|
14484
|
+
for (let i = 0; i < args.length; i++) {
|
|
14485
|
+
const arg = args[i];
|
|
14486
|
+
if (arg === "--skip-validate") {
|
|
14487
|
+
out.skipValidate = true;
|
|
14488
|
+
continue;
|
|
14489
|
+
}
|
|
14490
|
+
if (arg === "--plaintext") {
|
|
14491
|
+
out.plaintext = true;
|
|
14492
|
+
continue;
|
|
14493
|
+
}
|
|
14494
|
+
if (arg.startsWith("--")) {
|
|
14495
|
+
let flag = arg;
|
|
14496
|
+
let value;
|
|
14497
|
+
const eq = arg.indexOf("=");
|
|
14498
|
+
if (eq >= 0) {
|
|
14499
|
+
flag = arg.slice(0, eq);
|
|
14500
|
+
value = arg.slice(eq + 1);
|
|
14501
|
+
} else {
|
|
14502
|
+
const next = args[i + 1];
|
|
14503
|
+
if (next === void 0 || next.startsWith("--")) {
|
|
14504
|
+
return { ok: false, error: `${flag} requires a value` };
|
|
14505
|
+
}
|
|
14506
|
+
value = next;
|
|
14507
|
+
i++;
|
|
14508
|
+
}
|
|
14509
|
+
const name = flag.slice(2);
|
|
14510
|
+
if (name === "project") {
|
|
14511
|
+
out.project = value;
|
|
14512
|
+
continue;
|
|
14513
|
+
}
|
|
14514
|
+
if (name === "credential" || name === "token") {
|
|
14515
|
+
out.credential = value;
|
|
14516
|
+
continue;
|
|
14517
|
+
}
|
|
14518
|
+
if (name === "id") {
|
|
14519
|
+
out.id = value;
|
|
14520
|
+
continue;
|
|
14521
|
+
}
|
|
14522
|
+
const field = kebabToCamel(name);
|
|
14523
|
+
out.fields[field] = coerceFieldValue(field, value);
|
|
14524
|
+
continue;
|
|
14525
|
+
}
|
|
14526
|
+
positional.push(arg);
|
|
14527
|
+
}
|
|
14528
|
+
out.subcommand = positional[0] ?? "";
|
|
14529
|
+
out.positional = positional.slice(1);
|
|
14530
|
+
return { ok: true, value: out };
|
|
14531
|
+
}
|
|
14532
|
+
async function buildCredentialRef(dispatch, args, deps) {
|
|
14533
|
+
const hint = "an env-var reference like $PROVIDER_TOKEN is recommended (stored as a pointer, not the secret); a literal value is stored as-is";
|
|
14534
|
+
if (dispatch.requiredCredentialFields.length <= 1) {
|
|
14535
|
+
const key = dispatch.primaryCredentialKey;
|
|
14536
|
+
let value = args.credential;
|
|
14537
|
+
if (value === void 0 && typeof args.fields[key] === "string") {
|
|
14538
|
+
value = args.fields[key];
|
|
14539
|
+
}
|
|
14540
|
+
if (value === void 0 && deps.interactive) {
|
|
14541
|
+
value = await deps.prompt(`Credential for ${dispatch.provider} (${hint}):`);
|
|
14542
|
+
}
|
|
14543
|
+
return (value ?? "").trim();
|
|
14544
|
+
}
|
|
14545
|
+
const out = {};
|
|
14546
|
+
for (const f of dispatch.requiredCredentialFields) {
|
|
14547
|
+
let value;
|
|
14548
|
+
if (typeof args.fields[f] === "string") value = args.fields[f];
|
|
14549
|
+
else if (f === dispatch.primaryCredentialKey && args.credential !== void 0) value = args.credential;
|
|
14550
|
+
if (value === void 0 && deps.interactive) {
|
|
14551
|
+
value = await deps.prompt(`Credential field "${f}" for ${dispatch.provider} (${hint}):`);
|
|
14552
|
+
}
|
|
14553
|
+
out[f] = (value ?? "").trim();
|
|
14554
|
+
}
|
|
14555
|
+
return out;
|
|
14556
|
+
}
|
|
14557
|
+
function credentialComplete(dispatch, ref) {
|
|
14558
|
+
if (typeof ref === "string") {
|
|
14559
|
+
return ref.length > 0 ? [] : [dispatch.primaryCredentialKey];
|
|
14560
|
+
}
|
|
14561
|
+
return dispatch.requiredCredentialFields.filter((f) => !ref[f] || ref[f].length === 0);
|
|
14562
|
+
}
|
|
14563
|
+
function plaintextFields(ref) {
|
|
14564
|
+
if (typeof ref === "string") return isEnvRef(ref) ? [] : ["credential"];
|
|
14565
|
+
return Object.entries(ref).filter(([, v]) => !isEnvRef(v)).map(([k]) => k);
|
|
14566
|
+
}
|
|
14567
|
+
async function connectorAdd(args, deps) {
|
|
14568
|
+
let provider = args.positional[0];
|
|
14569
|
+
if (!provider && deps.interactive) {
|
|
14570
|
+
provider = (await deps.prompt(`Provider (${Object.keys(PROVIDER_DISPATCH).sort().join(", ")}):`)).trim();
|
|
14571
|
+
}
|
|
14572
|
+
if (!provider) {
|
|
14573
|
+
deps.err("neat connector add: a provider is required (e.g. `neat connector add supabase`)");
|
|
14574
|
+
return 2;
|
|
14575
|
+
}
|
|
14576
|
+
const dispatch = getProviderDispatch(provider);
|
|
14577
|
+
if (!dispatch) {
|
|
14578
|
+
deps.err(
|
|
14579
|
+
`neat connector add: unknown provider "${provider}". known providers: ${Object.keys(PROVIDER_DISPATCH).sort().join(", ")}`
|
|
14580
|
+
);
|
|
14581
|
+
return 2;
|
|
14582
|
+
}
|
|
14583
|
+
let project = args.project;
|
|
14584
|
+
if (project === void 0 && deps.interactive) {
|
|
14585
|
+
const answer = (await deps.prompt("Project (blank = the project the daemon is bootstrapping):")).trim();
|
|
14586
|
+
if (answer.length > 0) project = answer;
|
|
14587
|
+
}
|
|
14588
|
+
const credential = await buildCredentialRef(dispatch, args, deps);
|
|
14589
|
+
const options = { ...args.fields };
|
|
14590
|
+
for (const k of dispatch.requiredCredentialFields) delete options[k];
|
|
14591
|
+
for (const field of dispatch.requiredOptionFields) {
|
|
14592
|
+
if (field in options) continue;
|
|
14593
|
+
if (!deps.interactive) continue;
|
|
14594
|
+
const answer = (await deps.prompt(`Option "${field}" for ${provider}:`)).trim();
|
|
14595
|
+
if (answer.length > 0) options[field] = coerceFieldValue(field, answer);
|
|
14596
|
+
}
|
|
14597
|
+
const missingCred = credentialComplete(dispatch, credential);
|
|
14598
|
+
if (missingCred.length > 0) {
|
|
14599
|
+
deps.err(
|
|
14600
|
+
`neat connector add: credential is incomplete \u2014 missing ${missingCred.join(", ")}. Pass it as a flag (e.g. \`--token $PROVIDER_TOKEN\`) or run interactively.`
|
|
14601
|
+
);
|
|
14602
|
+
return 2;
|
|
14603
|
+
}
|
|
14604
|
+
const missingOpts = dispatch.requiredOptionFields.filter((f) => !(f in options));
|
|
14605
|
+
if (missingOpts.length > 0) {
|
|
14606
|
+
deps.err(
|
|
14607
|
+
`neat connector add: missing required option(s) for ${provider}: ${missingOpts.join(", ")}. Pass e.g. \`--${missingOpts[0].replace(/([A-Z])/g, "-$1").toLowerCase()} <value>\`.`
|
|
14608
|
+
);
|
|
14609
|
+
return 2;
|
|
14610
|
+
}
|
|
14611
|
+
const existing = await readConnectorsConfig(deps.home);
|
|
14612
|
+
const existingIds = new Set(existing.connectors.map((c) => c.id));
|
|
14613
|
+
const id = args.id ?? autoSlugConnectorId(provider, project, existingIds);
|
|
14614
|
+
const entry2 = {
|
|
14615
|
+
id,
|
|
14616
|
+
provider,
|
|
14617
|
+
...project ? { project } : {},
|
|
14618
|
+
credential,
|
|
14619
|
+
...Object.keys(options).length > 0 ? { options } : {}
|
|
14620
|
+
};
|
|
14621
|
+
if (!args.skipValidate) {
|
|
14622
|
+
const outcome = await validateConnectorEntry(entry2, deps.env, deps.fetchImpl);
|
|
14623
|
+
const code = reportPreWriteValidation(outcome, deps);
|
|
14624
|
+
if (code !== 0) return code;
|
|
14625
|
+
}
|
|
14626
|
+
const plaintext = plaintextFields(credential);
|
|
14627
|
+
if (plaintext.length > 0 && !args.plaintext) {
|
|
14628
|
+
deps.err(
|
|
14629
|
+
`neat connector add: storing a literal secret at rest for ${plaintext.join(", ")} \u2014 prefer an env-var reference like \`$PROVIDER_TOKEN\` (pass --plaintext to silence this).`
|
|
14630
|
+
);
|
|
14631
|
+
}
|
|
14632
|
+
const { replaced } = await upsertConnectorEntry(entry2, deps.home);
|
|
14633
|
+
const verb = replaced ? "updated" : "added";
|
|
14634
|
+
const where = project ? `project "${project}"` : "the bootstrapping project";
|
|
14635
|
+
deps.out(`${verb} connector "${id}" (${provider}) for ${where}.`);
|
|
14636
|
+
if (args.skipValidate) {
|
|
14637
|
+
deps.out("skipped validation (--skip-validate) \u2014 the credential is checked at the next daemon poll.");
|
|
14638
|
+
}
|
|
14639
|
+
deps.out("Restart the project's daemon (or start it) to begin polling this connector.");
|
|
14640
|
+
return 0;
|
|
14641
|
+
}
|
|
14642
|
+
function reportPreWriteValidation(outcome, deps) {
|
|
14643
|
+
switch (outcome.status) {
|
|
14644
|
+
case "ok":
|
|
14645
|
+
deps.out("credential validated against the provider.");
|
|
14646
|
+
return 0;
|
|
14647
|
+
case "unset-env":
|
|
14648
|
+
deps.err(
|
|
14649
|
+
`neat connector add: ${outcome.reason}. Export the variable before adding, or pass --skip-validate to add now and set it before the daemon runs.`
|
|
14650
|
+
);
|
|
14651
|
+
return 1;
|
|
14652
|
+
case "rejected":
|
|
14653
|
+
deps.err(
|
|
14654
|
+
`neat connector add: the provider rejected the credential \u2014 ${outcome.reason}. Nothing was written. Fix the credential, or pass --skip-validate to store it anyway.`
|
|
14655
|
+
);
|
|
14656
|
+
return 1;
|
|
14657
|
+
case "missing-field":
|
|
14658
|
+
deps.err(`neat connector add: ${outcome.reason}.`);
|
|
14659
|
+
return 2;
|
|
14660
|
+
case "unknown-provider":
|
|
14661
|
+
deps.err(`neat connector add: ${outcome.reason}.`);
|
|
14662
|
+
return 2;
|
|
14663
|
+
}
|
|
14664
|
+
}
|
|
14665
|
+
async function connectorList(deps, filterProject) {
|
|
14666
|
+
const config = await readConnectorsConfig(deps.home);
|
|
14667
|
+
let entries = config.connectors;
|
|
14668
|
+
if (filterProject) entries = entries.filter((e) => e.project === filterProject);
|
|
14669
|
+
if (entries.length === 0) {
|
|
14670
|
+
deps.out(
|
|
14671
|
+
filterProject ? `no connectors configured for project "${filterProject}".` : "no connectors configured. run `neat connector add <provider>` to add one."
|
|
14672
|
+
);
|
|
14673
|
+
return 0;
|
|
14674
|
+
}
|
|
14675
|
+
deps.out("id provider project credential");
|
|
14676
|
+
for (const e of entries) {
|
|
14677
|
+
const project = e.project ?? "(bootstrapping project)";
|
|
14678
|
+
const credential = describeCredential(e.credential, deps.env).map((c) => {
|
|
14679
|
+
const status2 = c.status ? ` (${c.status})` : c.kind === "plaintext" ? " (plaintext)" : "";
|
|
14680
|
+
return c.field ? `${c.field}=${c.display}${status2}` : `${c.display}${status2}`;
|
|
14681
|
+
}).join(", ");
|
|
14682
|
+
deps.out(`${e.id} ${e.provider} ${project} ${credential}`);
|
|
14683
|
+
}
|
|
14684
|
+
return 0;
|
|
14685
|
+
}
|
|
14686
|
+
async function connectorRemove(deps, id) {
|
|
14687
|
+
if (!id) {
|
|
14688
|
+
deps.err("neat connector remove: missing <id>. run `neat connector list` to see configured ids.");
|
|
14689
|
+
return 2;
|
|
14690
|
+
}
|
|
14691
|
+
const removed = await removeConnectorEntry(id, deps.home);
|
|
14692
|
+
if (!removed) {
|
|
14693
|
+
deps.err(`neat connector remove: no connector with id "${id}". run \`neat connector list\` to see configured ids.`);
|
|
14694
|
+
return 1;
|
|
14695
|
+
}
|
|
14696
|
+
deps.out(`removed connector "${id}" (${removed.provider}).`);
|
|
14697
|
+
return 0;
|
|
14698
|
+
}
|
|
14699
|
+
async function connectorTest(deps, id) {
|
|
14700
|
+
if (!id) {
|
|
14701
|
+
deps.err("neat connector test: missing <id>. run `neat connector list` to see configured ids.");
|
|
14702
|
+
return 2;
|
|
14703
|
+
}
|
|
14704
|
+
const config = await readConnectorsConfig(deps.home);
|
|
14705
|
+
const entry2 = config.connectors.find((c) => c.id === id);
|
|
14706
|
+
if (!entry2) {
|
|
14707
|
+
deps.err(`neat connector test: no connector with id "${id}". run \`neat connector list\` to see configured ids.`);
|
|
14708
|
+
return 1;
|
|
14709
|
+
}
|
|
14710
|
+
const outcome = await validateConnectorEntry(entry2, deps.env, deps.fetchImpl);
|
|
14711
|
+
switch (outcome.status) {
|
|
14712
|
+
case "ok":
|
|
14713
|
+
deps.out(`ok: "${id}" (${entry2.provider}) authenticated against the provider.`);
|
|
14714
|
+
return 0;
|
|
14715
|
+
case "unset-env":
|
|
14716
|
+
deps.err(`unset: "${id}" (${entry2.provider}) \u2014 ${outcome.reason}. Export the variable so the daemon can resolve it.`);
|
|
14717
|
+
return 1;
|
|
14718
|
+
case "rejected":
|
|
14719
|
+
deps.err(`rejected: "${id}" (${entry2.provider}) \u2014 ${outcome.reason}.`);
|
|
14720
|
+
return 1;
|
|
14721
|
+
case "missing-field":
|
|
14722
|
+
deps.err(`incomplete: "${id}" (${entry2.provider}) \u2014 ${outcome.reason}.`);
|
|
14723
|
+
return 2;
|
|
14724
|
+
case "unknown-provider":
|
|
14725
|
+
deps.err(`unknown provider: "${id}" \u2014 ${outcome.reason}.`);
|
|
14726
|
+
return 2;
|
|
14727
|
+
}
|
|
14728
|
+
}
|
|
14729
|
+
function printConnectorUsage(write) {
|
|
14730
|
+
write("usage: neat connector <add|list|remove|test> [args]");
|
|
14731
|
+
write(" add <provider> add a connector; validates the credential first (--skip-validate to skip)");
|
|
14732
|
+
write(" flags: --project <name> --credential/--token <$VAR|value> --id <id>");
|
|
14733
|
+
write(" --<option> <value> (provider-specific) --plaintext --skip-validate");
|
|
14734
|
+
write(" list list configured connectors (credentials shown redacted)");
|
|
14735
|
+
write(" flags: --project <name>");
|
|
14736
|
+
write(" remove <id> remove a connector by id");
|
|
14737
|
+
write(" test <id> re-run the credential validation for an existing connector");
|
|
14738
|
+
}
|
|
14739
|
+
async function runConnectorCommand(rawArgs, deps = {}) {
|
|
14740
|
+
const resolved = resolveDeps(deps);
|
|
14741
|
+
const parsed = parseConnectorArgs(rawArgs);
|
|
14742
|
+
if (!parsed.ok) {
|
|
14743
|
+
resolved.err(`neat connector: ${parsed.error}`);
|
|
14744
|
+
return 2;
|
|
14745
|
+
}
|
|
14746
|
+
const a = parsed.value;
|
|
14747
|
+
switch (a.subcommand) {
|
|
14748
|
+
case "add":
|
|
14749
|
+
return connectorAdd(a, resolved);
|
|
14750
|
+
case "list":
|
|
14751
|
+
return connectorList(resolved, a.project);
|
|
14752
|
+
case "remove":
|
|
14753
|
+
return connectorRemove(resolved, a.positional[0]);
|
|
14754
|
+
case "test":
|
|
14755
|
+
return connectorTest(resolved, a.positional[0]);
|
|
14756
|
+
case "":
|
|
14757
|
+
resolved.err("neat connector: missing subcommand.");
|
|
14758
|
+
printConnectorUsage(resolved.err);
|
|
14759
|
+
return 2;
|
|
14760
|
+
default:
|
|
14761
|
+
resolved.err(`neat connector: unknown subcommand "${a.subcommand}".`);
|
|
14762
|
+
printConnectorUsage(resolved.err);
|
|
14763
|
+
return 2;
|
|
14764
|
+
}
|
|
14765
|
+
}
|
|
14766
|
+
|
|
12707
14767
|
// src/cli-verbs.ts
|
|
12708
14768
|
init_cjs_shims();
|
|
12709
|
-
var
|
|
14769
|
+
var import_node_path55 = __toESM(require("path"), 1);
|
|
12710
14770
|
|
|
12711
14771
|
// src/cli-client.ts
|
|
12712
14772
|
init_cjs_shims();
|
|
12713
|
-
var
|
|
14773
|
+
var import_types43 = require("@neat.is/types");
|
|
12714
14774
|
var HttpError = class extends Error {
|
|
12715
14775
|
constructor(status2, message, responseBody = "") {
|
|
12716
14776
|
super(message);
|
|
@@ -12735,10 +14795,10 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
12735
14795
|
const root = baseUrl.replace(/\/$/, "");
|
|
12736
14796
|
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
12737
14797
|
return {
|
|
12738
|
-
async get(
|
|
14798
|
+
async get(path57) {
|
|
12739
14799
|
let res;
|
|
12740
14800
|
try {
|
|
12741
|
-
res = await fetch(`${root}${
|
|
14801
|
+
res = await fetch(`${root}${path57}`, {
|
|
12742
14802
|
headers: { ...authHeader }
|
|
12743
14803
|
});
|
|
12744
14804
|
} catch (err) {
|
|
@@ -12750,16 +14810,16 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
12750
14810
|
const body = await res.text().catch(() => "");
|
|
12751
14811
|
throw new HttpError(
|
|
12752
14812
|
res.status,
|
|
12753
|
-
`${res.status} ${res.statusText} on GET ${
|
|
14813
|
+
`${res.status} ${res.statusText} on GET ${path57}: ${body}`,
|
|
12754
14814
|
body
|
|
12755
14815
|
);
|
|
12756
14816
|
}
|
|
12757
14817
|
return await res.json();
|
|
12758
14818
|
},
|
|
12759
|
-
async post(
|
|
14819
|
+
async post(path57, body) {
|
|
12760
14820
|
let res;
|
|
12761
14821
|
try {
|
|
12762
|
-
res = await fetch(`${root}${
|
|
14822
|
+
res = await fetch(`${root}${path57}`, {
|
|
12763
14823
|
method: "POST",
|
|
12764
14824
|
headers: { "content-type": "application/json", ...authHeader },
|
|
12765
14825
|
body: JSON.stringify(body)
|
|
@@ -12773,7 +14833,7 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
12773
14833
|
const text = await res.text().catch(() => "");
|
|
12774
14834
|
throw new HttpError(
|
|
12775
14835
|
res.status,
|
|
12776
|
-
`${res.status} ${res.statusText} on POST ${
|
|
14836
|
+
`${res.status} ${res.statusText} on POST ${path57}: ${text}`,
|
|
12777
14837
|
text
|
|
12778
14838
|
);
|
|
12779
14839
|
}
|
|
@@ -12787,12 +14847,12 @@ function projectPath(project, suffix) {
|
|
|
12787
14847
|
}
|
|
12788
14848
|
async function runRootCause(client, input) {
|
|
12789
14849
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
12790
|
-
const
|
|
14850
|
+
const path57 = projectPath(
|
|
12791
14851
|
input.project,
|
|
12792
14852
|
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
12793
14853
|
);
|
|
12794
14854
|
try {
|
|
12795
|
-
const result = await client.get(
|
|
14855
|
+
const result = await client.get(path57);
|
|
12796
14856
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
12797
14857
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
12798
14858
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -12818,12 +14878,12 @@ async function runRootCause(client, input) {
|
|
|
12818
14878
|
}
|
|
12819
14879
|
async function runBlastRadius(client, input) {
|
|
12820
14880
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
12821
|
-
const
|
|
14881
|
+
const path57 = projectPath(
|
|
12822
14882
|
input.project,
|
|
12823
14883
|
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
12824
14884
|
);
|
|
12825
14885
|
try {
|
|
12826
|
-
const result = await client.get(
|
|
14886
|
+
const result = await client.get(path57);
|
|
12827
14887
|
if (result.totalAffected === 0) {
|
|
12828
14888
|
return {
|
|
12829
14889
|
summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
|
|
@@ -12852,17 +14912,17 @@ async function runBlastRadius(client, input) {
|
|
|
12852
14912
|
}
|
|
12853
14913
|
}
|
|
12854
14914
|
function formatBlastEntry(n) {
|
|
12855
|
-
const tag = n.edgeProvenance ===
|
|
14915
|
+
const tag = n.edgeProvenance === import_types43.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
12856
14916
|
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
12857
14917
|
}
|
|
12858
14918
|
async function runDependencies(client, input) {
|
|
12859
14919
|
const depth = input.depth ?? 3;
|
|
12860
|
-
const
|
|
14920
|
+
const path57 = projectPath(
|
|
12861
14921
|
input.project,
|
|
12862
14922
|
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
12863
14923
|
);
|
|
12864
14924
|
try {
|
|
12865
|
-
const result = await client.get(
|
|
14925
|
+
const result = await client.get(path57);
|
|
12866
14926
|
if (result.total === 0) {
|
|
12867
14927
|
return {
|
|
12868
14928
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -12909,7 +14969,7 @@ async function runObservedDependencies(client, input) {
|
|
|
12909
14969
|
if (result.observed) {
|
|
12910
14970
|
return {
|
|
12911
14971
|
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.`,
|
|
12912
|
-
provenance:
|
|
14972
|
+
provenance: import_types43.Provenance.OBSERVED
|
|
12913
14973
|
};
|
|
12914
14974
|
}
|
|
12915
14975
|
const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
@@ -12919,7 +14979,7 @@ async function runObservedDependencies(client, input) {
|
|
|
12919
14979
|
return {
|
|
12920
14980
|
summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
12921
14981
|
block: blockLines.join("\n"),
|
|
12922
|
-
provenance:
|
|
14982
|
+
provenance: import_types43.Provenance.OBSERVED
|
|
12923
14983
|
};
|
|
12924
14984
|
} catch (err) {
|
|
12925
14985
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -12954,9 +15014,9 @@ function formatDuration(ms) {
|
|
|
12954
15014
|
return `${Math.round(h / 24)}d`;
|
|
12955
15015
|
}
|
|
12956
15016
|
async function runIncidents(client, input) {
|
|
12957
|
-
const
|
|
15017
|
+
const path57 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
12958
15018
|
try {
|
|
12959
|
-
const body = await client.get(
|
|
15019
|
+
const body = await client.get(path57);
|
|
12960
15020
|
const events = body.events;
|
|
12961
15021
|
if (events.length === 0) {
|
|
12962
15022
|
return {
|
|
@@ -12973,7 +15033,7 @@ async function runIncidents(client, input) {
|
|
|
12973
15033
|
return {
|
|
12974
15034
|
summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
12975
15035
|
block: blockLines.join("\n"),
|
|
12976
|
-
provenance:
|
|
15036
|
+
provenance: import_types43.Provenance.OBSERVED
|
|
12977
15037
|
};
|
|
12978
15038
|
} catch (err) {
|
|
12979
15039
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -13082,7 +15142,7 @@ async function runStaleEdges(client, input) {
|
|
|
13082
15142
|
return {
|
|
13083
15143
|
summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
|
|
13084
15144
|
block: blockLines.join("\n"),
|
|
13085
|
-
provenance:
|
|
15145
|
+
provenance: import_types43.Provenance.STALE
|
|
13086
15146
|
};
|
|
13087
15147
|
}
|
|
13088
15148
|
async function runPolicies(client, input) {
|
|
@@ -13246,7 +15306,7 @@ async function resolveProjectEntry(opts) {
|
|
|
13246
15306
|
const cwd = opts.cwd ?? process.cwd();
|
|
13247
15307
|
const resolvedCwd = await normalizeProjectPath(cwd);
|
|
13248
15308
|
for (const entry2 of entries) {
|
|
13249
|
-
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${
|
|
15309
|
+
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path55.default.sep}`)) {
|
|
13250
15310
|
return entry2;
|
|
13251
15311
|
}
|
|
13252
15312
|
}
|
|
@@ -13399,7 +15459,7 @@ async function runSync(opts) {
|
|
|
13399
15459
|
}
|
|
13400
15460
|
|
|
13401
15461
|
// src/cli.ts
|
|
13402
|
-
var
|
|
15462
|
+
var import_types44 = require("@neat.is/types");
|
|
13403
15463
|
function isNpxInvocation() {
|
|
13404
15464
|
if (process.env.npm_command === "exec") return true;
|
|
13405
15465
|
const execpath = process.env.npm_execpath ?? "";
|
|
@@ -13460,6 +15520,17 @@ function usage() {
|
|
|
13460
15520
|
console.log(" --dry-run run extraction in-memory; do not write");
|
|
13461
15521
|
console.log(" --no-instrument skip the SDK install apply step");
|
|
13462
15522
|
console.log(" --json emit the delta summary as JSON");
|
|
15523
|
+
console.log(" connector Configure pull-based OBSERVED connectors (supabase, railway,");
|
|
15524
|
+
console.log(" firebase, cloudflare). Subcommands:");
|
|
15525
|
+
console.log(" add <provider> add a connector; validates the credential");
|
|
15526
|
+
console.log(" against the provider first (--skip-validate to skip)");
|
|
15527
|
+
console.log(" flags: --project <name>, --credential/--token <$VAR|value>,");
|
|
15528
|
+
console.log(" --id <id>, --<option> <value>, --plaintext, --skip-validate");
|
|
15529
|
+
console.log(" list list configured connectors (credentials redacted)");
|
|
15530
|
+
console.log(" remove <id> remove a connector by id");
|
|
15531
|
+
console.log(" test <id> re-check an existing connector's credential");
|
|
15532
|
+
console.log(" Credentials default to an env-var reference ($VAR) resolved at");
|
|
15533
|
+
console.log(" run time; the config file is written owner-only (0600).");
|
|
13463
15534
|
console.log("");
|
|
13464
15535
|
console.log("query commands (mirror the MCP tools, ADR-050):");
|
|
13465
15536
|
console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
|
|
@@ -13681,7 +15752,7 @@ async function buildPatchSections(services, project) {
|
|
|
13681
15752
|
}
|
|
13682
15753
|
async function runInit(opts) {
|
|
13683
15754
|
const written = [];
|
|
13684
|
-
const stat = await
|
|
15755
|
+
const stat = await import_node_fs36.promises.stat(opts.scanPath).catch(() => null);
|
|
13685
15756
|
if (!stat || !stat.isDirectory()) {
|
|
13686
15757
|
console.error(`neat init: ${opts.scanPath} is not a directory`);
|
|
13687
15758
|
return { exitCode: 2, writtenFiles: written };
|
|
@@ -13690,13 +15761,13 @@ async function runInit(opts) {
|
|
|
13690
15761
|
printDiscoveryReport(opts, services);
|
|
13691
15762
|
const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
|
|
13692
15763
|
const patch = renderPatch(sections);
|
|
13693
|
-
const patchPath =
|
|
15764
|
+
const patchPath = import_node_path56.default.join(opts.scanPath, "neat.patch");
|
|
13694
15765
|
if (opts.dryRun) {
|
|
13695
|
-
await
|
|
15766
|
+
await import_node_fs36.promises.writeFile(patchPath, patch, "utf8");
|
|
13696
15767
|
written.push(patchPath);
|
|
13697
15768
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
13698
|
-
const gitignorePath =
|
|
13699
|
-
const gitignoreExists = await
|
|
15769
|
+
const gitignorePath = import_node_path56.default.join(opts.scanPath, ".gitignore");
|
|
15770
|
+
const gitignoreExists = await import_node_fs36.promises.stat(gitignorePath).then(() => true).catch(() => false);
|
|
13700
15771
|
const verb = gitignoreExists ? "append" : "create";
|
|
13701
15772
|
console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
|
|
13702
15773
|
console.log("rerun without --dry-run to register and snapshot.");
|
|
@@ -13707,9 +15778,9 @@ async function runInit(opts) {
|
|
|
13707
15778
|
const graph = getGraph(graphKey);
|
|
13708
15779
|
const projectPaths = pathsForProject(
|
|
13709
15780
|
graphKey,
|
|
13710
|
-
|
|
15781
|
+
import_node_path56.default.join(opts.scanPath, "neat-out")
|
|
13711
15782
|
);
|
|
13712
|
-
const errorsPath =
|
|
15783
|
+
const errorsPath = import_node_path56.default.join(import_node_path56.default.dirname(opts.outPath), import_node_path56.default.basename(projectPaths.errorsPath));
|
|
13713
15784
|
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
13714
15785
|
await saveGraphToDisk(graph, opts.outPath);
|
|
13715
15786
|
written.push(opts.outPath);
|
|
@@ -13788,7 +15859,7 @@ async function runInit(opts) {
|
|
|
13788
15859
|
console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
13789
15860
|
}
|
|
13790
15861
|
} else {
|
|
13791
|
-
await
|
|
15862
|
+
await import_node_fs36.promises.writeFile(patchPath, patch, "utf8");
|
|
13792
15863
|
written.push(patchPath);
|
|
13793
15864
|
}
|
|
13794
15865
|
}
|
|
@@ -13828,9 +15899,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
13828
15899
|
};
|
|
13829
15900
|
function claudeConfigPath() {
|
|
13830
15901
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
13831
|
-
if (override && override.length > 0) return
|
|
15902
|
+
if (override && override.length > 0) return import_node_path56.default.resolve(override);
|
|
13832
15903
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
13833
|
-
return
|
|
15904
|
+
return import_node_path56.default.join(home, ".claude.json");
|
|
13834
15905
|
}
|
|
13835
15906
|
async function runSkill(opts) {
|
|
13836
15907
|
const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -13842,7 +15913,7 @@ async function runSkill(opts) {
|
|
|
13842
15913
|
const target = claudeConfigPath();
|
|
13843
15914
|
let existing = {};
|
|
13844
15915
|
try {
|
|
13845
|
-
existing = JSON.parse(await
|
|
15916
|
+
existing = JSON.parse(await import_node_fs36.promises.readFile(target, "utf8"));
|
|
13846
15917
|
} catch (err) {
|
|
13847
15918
|
if (err.code !== "ENOENT") {
|
|
13848
15919
|
console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
|
|
@@ -13854,8 +15925,8 @@ async function runSkill(opts) {
|
|
|
13854
15925
|
...existing,
|
|
13855
15926
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
13856
15927
|
};
|
|
13857
|
-
await
|
|
13858
|
-
await
|
|
15928
|
+
await import_node_fs36.promises.mkdir(import_node_path56.default.dirname(target), { recursive: true });
|
|
15929
|
+
await import_node_fs36.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
13859
15930
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
13860
15931
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
13861
15932
|
return { exitCode: 0 };
|
|
@@ -13883,6 +15954,11 @@ async function main() {
|
|
|
13883
15954
|
printVersion();
|
|
13884
15955
|
process.exit(0);
|
|
13885
15956
|
}
|
|
15957
|
+
if (cmd0 === "connector") {
|
|
15958
|
+
const code = await runConnectorCommand(argv.slice(1));
|
|
15959
|
+
if (code !== 0) process.exit(code);
|
|
15960
|
+
return;
|
|
15961
|
+
}
|
|
13886
15962
|
const argvParsed = parseArgs(argv);
|
|
13887
15963
|
if (argvParsed.positional.length === 0) {
|
|
13888
15964
|
const orchestratorCode2 = await tryOrchestrator(process.cwd(), argvParsed);
|
|
@@ -13904,12 +15980,12 @@ async function main() {
|
|
|
13904
15980
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
13905
15981
|
process.exit(2);
|
|
13906
15982
|
}
|
|
13907
|
-
const scanPath =
|
|
15983
|
+
const scanPath = import_node_path56.default.resolve(target);
|
|
13908
15984
|
const projectExplicit = parsed.project !== null;
|
|
13909
|
-
const projectName = projectExplicit ? project :
|
|
15985
|
+
const projectName = projectExplicit ? project : import_node_path56.default.basename(scanPath);
|
|
13910
15986
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
13911
|
-
const fallback = pathsForProject(projectKey,
|
|
13912
|
-
const outPath =
|
|
15987
|
+
const fallback = pathsForProject(projectKey, import_node_path56.default.join(scanPath, "neat-out")).snapshotPath;
|
|
15988
|
+
const outPath = import_node_path56.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
|
|
13913
15989
|
const result = await runInit({
|
|
13914
15990
|
scanPath,
|
|
13915
15991
|
outPath,
|
|
@@ -13930,21 +16006,21 @@ async function main() {
|
|
|
13930
16006
|
usage();
|
|
13931
16007
|
process.exit(2);
|
|
13932
16008
|
}
|
|
13933
|
-
const scanPath =
|
|
13934
|
-
const stat = await
|
|
16009
|
+
const scanPath = import_node_path56.default.resolve(target);
|
|
16010
|
+
const stat = await import_node_fs36.promises.stat(scanPath).catch(() => null);
|
|
13935
16011
|
if (!stat || !stat.isDirectory()) {
|
|
13936
16012
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
13937
16013
|
process.exit(2);
|
|
13938
16014
|
}
|
|
13939
|
-
const projectPaths = pathsForProject(project,
|
|
13940
|
-
const outPath =
|
|
13941
|
-
const errorsPath =
|
|
13942
|
-
process.env.NEAT_ERRORS_PATH ??
|
|
16015
|
+
const projectPaths = pathsForProject(project, import_node_path56.default.join(scanPath, "neat-out"));
|
|
16016
|
+
const outPath = import_node_path56.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
|
|
16017
|
+
const errorsPath = import_node_path56.default.resolve(
|
|
16018
|
+
process.env.NEAT_ERRORS_PATH ?? import_node_path56.default.join(import_node_path56.default.dirname(outPath), import_node_path56.default.basename(projectPaths.errorsPath))
|
|
13943
16019
|
);
|
|
13944
|
-
const staleEventsPath =
|
|
13945
|
-
process.env.NEAT_STALE_EVENTS_PATH ??
|
|
16020
|
+
const staleEventsPath = import_node_path56.default.resolve(
|
|
16021
|
+
process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path56.default.join(import_node_path56.default.dirname(outPath), import_node_path56.default.basename(projectPaths.staleEventsPath))
|
|
13946
16022
|
);
|
|
13947
|
-
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ?
|
|
16023
|
+
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path56.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
|
|
13948
16024
|
const handle = await startWatch(getGraph(project), {
|
|
13949
16025
|
scanPath,
|
|
13950
16026
|
outPath,
|
|
@@ -14127,11 +16203,11 @@ async function main() {
|
|
|
14127
16203
|
process.exit(1);
|
|
14128
16204
|
}
|
|
14129
16205
|
async function tryOrchestrator(cmd, parsed) {
|
|
14130
|
-
const scanPath =
|
|
14131
|
-
const stat = await
|
|
16206
|
+
const scanPath = import_node_path56.default.resolve(cmd);
|
|
16207
|
+
const stat = await import_node_fs36.promises.stat(scanPath).catch(() => null);
|
|
14132
16208
|
if (!stat || !stat.isDirectory()) return null;
|
|
14133
16209
|
const projectExplicit = parsed.project !== null;
|
|
14134
|
-
const projectName = projectExplicit ? parsed.project :
|
|
16210
|
+
const projectName = projectExplicit ? parsed.project : import_node_path56.default.basename(scanPath);
|
|
14135
16211
|
const result = await runOrchestrator({
|
|
14136
16212
|
scanPath,
|
|
14137
16213
|
project: projectName,
|
|
@@ -14320,10 +16396,10 @@ async function runQueryVerb(cmd, parsed) {
|
|
|
14320
16396
|
const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
14321
16397
|
const out = [];
|
|
14322
16398
|
for (const p of parts) {
|
|
14323
|
-
const r =
|
|
16399
|
+
const r = import_types44.DivergenceTypeSchema.safeParse(p);
|
|
14324
16400
|
if (!r.success) {
|
|
14325
16401
|
console.error(
|
|
14326
|
-
`neat divergences: unknown --type "${p}". allowed: ${
|
|
16402
|
+
`neat divergences: unknown --type "${p}". allowed: ${import_types44.DivergenceTypeSchema.options.join(", ")}`
|
|
14327
16403
|
);
|
|
14328
16404
|
return 2;
|
|
14329
16405
|
}
|