@neat.is/core 0.2.7 → 0.2.9
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-T2U4U256.js → chunk-5KX7EI4F.js} +76 -13
- package/dist/chunk-5KX7EI4F.js.map +1 -0
- package/dist/{chunk-6JT6L2OV.js → chunk-BVF7MVR5.js} +8 -10
- package/dist/{chunk-6JT6L2OV.js.map → chunk-BVF7MVR5.js.map} +1 -1
- package/dist/{chunk-6SFEITLJ.js → chunk-IRPH6KL4.js} +541 -136
- package/dist/chunk-IRPH6KL4.js.map +1 -0
- package/dist/cli.cjs +1480 -479
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +22 -1
- package/dist/cli.d.ts +22 -1
- package/dist/cli.js +757 -39
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +566 -339
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +16 -18
- package/dist/neatd.cjs +303 -153
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -3
- package/dist/neatd.js.map +1 -1
- package/dist/server.cjs +430 -168
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +2 -2
- package/package.json +2 -2
- package/dist/chunk-6SFEITLJ.js.map +0 -1
- package/dist/chunk-T2U4U256.js.map +0 -1
- package/dist/chunk-WX55TLUT.js +0 -184
- package/dist/chunk-WX55TLUT.js.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -101,8 +101,8 @@ function reshapeGrpcRequest(req) {
|
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
103
|
function resolveProtoRoot() {
|
|
104
|
-
const here =
|
|
105
|
-
return
|
|
104
|
+
const here = import_node_path31.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
105
|
+
return import_node_path31.default.resolve(here, "..", "proto");
|
|
106
106
|
}
|
|
107
107
|
function loadTraceService() {
|
|
108
108
|
const protoRoot = resolveProtoRoot();
|
|
@@ -157,13 +157,13 @@ async function startOtelGrpcReceiver(opts) {
|
|
|
157
157
|
})
|
|
158
158
|
};
|
|
159
159
|
}
|
|
160
|
-
var import_node_url,
|
|
160
|
+
var import_node_url, import_node_path31, grpc, protoLoader;
|
|
161
161
|
var init_otel_grpc = __esm({
|
|
162
162
|
"src/otel-grpc.ts"() {
|
|
163
163
|
"use strict";
|
|
164
164
|
init_cjs_shims();
|
|
165
165
|
import_node_url = require("url");
|
|
166
|
-
|
|
166
|
+
import_node_path31 = __toESM(require("path"), 1);
|
|
167
167
|
grpc = __toESM(require("@grpc/grpc-js"), 1);
|
|
168
168
|
protoLoader = __toESM(require("@grpc/proto-loader"), 1);
|
|
169
169
|
init_otel();
|
|
@@ -260,10 +260,10 @@ function parseOtlpRequest(body) {
|
|
|
260
260
|
}
|
|
261
261
|
function loadProtobufDecoder() {
|
|
262
262
|
if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
|
|
263
|
-
const here =
|
|
264
|
-
const protoRoot =
|
|
263
|
+
const here = import_node_path32.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
264
|
+
const protoRoot = import_node_path32.default.resolve(here, "..", "proto");
|
|
265
265
|
const root = new import_protobufjs.default.Root();
|
|
266
|
-
root.resolvePath = (_origin, target) =>
|
|
266
|
+
root.resolvePath = (_origin, target) => import_node_path32.default.resolve(protoRoot, target);
|
|
267
267
|
root.loadSync(
|
|
268
268
|
"opentelemetry/proto/collector/trace/v1/trace_service.proto",
|
|
269
269
|
{ keepCase: true }
|
|
@@ -355,12 +355,12 @@ async function buildOtelReceiver(opts) {
|
|
|
355
355
|
};
|
|
356
356
|
return decorated;
|
|
357
357
|
}
|
|
358
|
-
var
|
|
358
|
+
var import_node_path32, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
|
|
359
359
|
var init_otel = __esm({
|
|
360
360
|
"src/otel.ts"() {
|
|
361
361
|
"use strict";
|
|
362
362
|
init_cjs_shims();
|
|
363
|
-
|
|
363
|
+
import_node_path32 = __toESM(require("path"), 1);
|
|
364
364
|
import_node_url2 = require("url");
|
|
365
365
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
366
366
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
@@ -372,13 +372,16 @@ var init_otel = __esm({
|
|
|
372
372
|
var cli_exports = {};
|
|
373
373
|
__export(cli_exports, {
|
|
374
374
|
CLAUDE_SKILL_CONFIG: () => CLAUDE_SKILL_CONFIG,
|
|
375
|
+
QUERY_VERBS: () => QUERY_VERBS,
|
|
376
|
+
parseArgs: () => parseArgs,
|
|
375
377
|
runInit: () => runInit,
|
|
378
|
+
runQueryVerb: () => runQueryVerb,
|
|
376
379
|
runSkill: () => runSkill
|
|
377
380
|
});
|
|
378
381
|
module.exports = __toCommonJS(cli_exports);
|
|
379
382
|
init_cjs_shims();
|
|
380
|
-
var
|
|
381
|
-
var
|
|
383
|
+
var import_node_path37 = __toESM(require("path"), 1);
|
|
384
|
+
var import_node_fs22 = require("fs");
|
|
382
385
|
|
|
383
386
|
// src/graph.ts
|
|
384
387
|
init_cjs_shims();
|
|
@@ -735,6 +738,68 @@ function deprecatedApis() {
|
|
|
735
738
|
return currentMatrix().deprecatedApis ?? [];
|
|
736
739
|
}
|
|
737
740
|
|
|
741
|
+
// src/events.ts
|
|
742
|
+
init_cjs_shims();
|
|
743
|
+
var import_node_events = require("events");
|
|
744
|
+
var EVENT_BUS_CHANNEL = "event";
|
|
745
|
+
var NeatEventBus = class extends import_node_events.EventEmitter {
|
|
746
|
+
};
|
|
747
|
+
var eventBus = new NeatEventBus();
|
|
748
|
+
eventBus.setMaxListeners(0);
|
|
749
|
+
function emitNeatEvent(envelope) {
|
|
750
|
+
eventBus.emit(EVENT_BUS_CHANNEL, envelope);
|
|
751
|
+
}
|
|
752
|
+
function attachGraphToEventBus(graph, opts) {
|
|
753
|
+
const { project } = opts;
|
|
754
|
+
const onNodeAdded = (payload) => {
|
|
755
|
+
emitNeatEvent({
|
|
756
|
+
type: "node-added",
|
|
757
|
+
project,
|
|
758
|
+
payload: { node: payload.attributes }
|
|
759
|
+
});
|
|
760
|
+
};
|
|
761
|
+
const onNodeDropped = (payload) => {
|
|
762
|
+
emitNeatEvent({
|
|
763
|
+
type: "node-removed",
|
|
764
|
+
project,
|
|
765
|
+
payload: { id: payload.key }
|
|
766
|
+
});
|
|
767
|
+
};
|
|
768
|
+
const onEdgeAdded = (payload) => {
|
|
769
|
+
emitNeatEvent({
|
|
770
|
+
type: "edge-added",
|
|
771
|
+
project,
|
|
772
|
+
payload: { edge: payload.attributes }
|
|
773
|
+
});
|
|
774
|
+
};
|
|
775
|
+
const onEdgeDropped = (payload) => {
|
|
776
|
+
emitNeatEvent({
|
|
777
|
+
type: "edge-removed",
|
|
778
|
+
project,
|
|
779
|
+
payload: { id: payload.key }
|
|
780
|
+
});
|
|
781
|
+
};
|
|
782
|
+
const onNodeAttrsUpdated = (payload) => {
|
|
783
|
+
emitNeatEvent({
|
|
784
|
+
type: "node-updated",
|
|
785
|
+
project,
|
|
786
|
+
payload: { id: payload.key, changes: payload.attributes }
|
|
787
|
+
});
|
|
788
|
+
};
|
|
789
|
+
graph.on("nodeAdded", onNodeAdded);
|
|
790
|
+
graph.on("nodeDropped", onNodeDropped);
|
|
791
|
+
graph.on("edgeAdded", onEdgeAdded);
|
|
792
|
+
graph.on("edgeDropped", onEdgeDropped);
|
|
793
|
+
graph.on("nodeAttributesUpdated", onNodeAttrsUpdated);
|
|
794
|
+
return () => {
|
|
795
|
+
graph.off("nodeAdded", onNodeAdded);
|
|
796
|
+
graph.off("nodeDropped", onNodeDropped);
|
|
797
|
+
graph.off("edgeAdded", onEdgeAdded);
|
|
798
|
+
graph.off("edgeDropped", onEdgeDropped);
|
|
799
|
+
graph.off("nodeAttributesUpdated", onNodeAttrsUpdated);
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
738
803
|
// src/traverse.ts
|
|
739
804
|
init_cjs_shims();
|
|
740
805
|
var import_types = require("@neat.is/types");
|
|
@@ -822,19 +887,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
822
887
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
823
888
|
let best = { path: [start], edges: [] };
|
|
824
889
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
825
|
-
function step(node,
|
|
826
|
-
if (
|
|
827
|
-
best = { path: [...
|
|
890
|
+
function step(node, path38, edges) {
|
|
891
|
+
if (path38.length > best.path.length) {
|
|
892
|
+
best = { path: [...path38], edges: [...edges] };
|
|
828
893
|
}
|
|
829
|
-
if (
|
|
894
|
+
if (path38.length - 1 >= maxDepth) return;
|
|
830
895
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
831
896
|
for (const [srcId, edge] of incoming) {
|
|
832
897
|
if (visited.has(srcId)) continue;
|
|
833
898
|
visited.add(srcId);
|
|
834
|
-
|
|
899
|
+
path38.push(srcId);
|
|
835
900
|
edges.push(edge);
|
|
836
|
-
step(srcId,
|
|
837
|
-
|
|
901
|
+
step(srcId, path38, edges);
|
|
902
|
+
path38.pop();
|
|
838
903
|
edges.pop();
|
|
839
904
|
visited.delete(srcId);
|
|
840
905
|
}
|
|
@@ -1298,9 +1363,11 @@ async function loadPolicyFile(policyPath) {
|
|
|
1298
1363
|
}
|
|
1299
1364
|
var PolicyViolationsLog = class {
|
|
1300
1365
|
path;
|
|
1366
|
+
project;
|
|
1301
1367
|
seen = null;
|
|
1302
|
-
constructor(logPath) {
|
|
1368
|
+
constructor(logPath, project = DEFAULT_PROJECT) {
|
|
1303
1369
|
this.path = logPath;
|
|
1370
|
+
this.project = project;
|
|
1304
1371
|
}
|
|
1305
1372
|
async append(v) {
|
|
1306
1373
|
if (!this.seen) await this.hydrate();
|
|
@@ -1308,6 +1375,11 @@ var PolicyViolationsLog = class {
|
|
|
1308
1375
|
this.seen.add(v.id);
|
|
1309
1376
|
await import_node_fs2.promises.mkdir(import_node_path2.default.dirname(this.path), { recursive: true });
|
|
1310
1377
|
await import_node_fs2.promises.appendFile(this.path, JSON.stringify(v) + "\n", "utf8");
|
|
1378
|
+
emitNeatEvent({
|
|
1379
|
+
type: "policy-violation",
|
|
1380
|
+
project: this.project,
|
|
1381
|
+
payload: { violation: v }
|
|
1382
|
+
});
|
|
1311
1383
|
return true;
|
|
1312
1384
|
}
|
|
1313
1385
|
async readAll() {
|
|
@@ -1777,6 +1849,7 @@ async function markStaleEdges(graph, options = {}) {
|
|
|
1777
1849
|
const thresholds = options.thresholds ?? loadStaleThresholdsFromEnv();
|
|
1778
1850
|
const now = options.now ?? Date.now();
|
|
1779
1851
|
const events = [];
|
|
1852
|
+
const project = options.project ?? DEFAULT_PROJECT;
|
|
1780
1853
|
graph.forEachEdge((id, attrs) => {
|
|
1781
1854
|
const e = attrs;
|
|
1782
1855
|
if (e.provenance !== import_types3.Provenance.OBSERVED) return;
|
|
@@ -1796,6 +1869,15 @@ async function markStaleEdges(graph, options = {}) {
|
|
|
1796
1869
|
lastObserved: e.lastObserved,
|
|
1797
1870
|
transitionedAt: new Date(now).toISOString()
|
|
1798
1871
|
});
|
|
1872
|
+
emitNeatEvent({
|
|
1873
|
+
type: "stale-transition",
|
|
1874
|
+
project,
|
|
1875
|
+
payload: {
|
|
1876
|
+
edgeId: id,
|
|
1877
|
+
from: import_types3.Provenance.OBSERVED,
|
|
1878
|
+
to: import_types3.Provenance.STALE
|
|
1879
|
+
}
|
|
1880
|
+
});
|
|
1799
1881
|
}
|
|
1800
1882
|
});
|
|
1801
1883
|
if (options.staleEventsPath && events.length > 0) {
|
|
@@ -1826,7 +1908,8 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
1826
1908
|
try {
|
|
1827
1909
|
await markStaleEdges(graph, {
|
|
1828
1910
|
thresholds: options.thresholds,
|
|
1829
|
-
staleEventsPath: options.staleEventsPath
|
|
1911
|
+
staleEventsPath: options.staleEventsPath,
|
|
1912
|
+
project: options.project
|
|
1830
1913
|
});
|
|
1831
1914
|
if (options.onPolicyTrigger) await options.onPolicyTrigger(graph);
|
|
1832
1915
|
} catch (err) {
|
|
@@ -1853,10 +1936,10 @@ async function readErrorEvents(errorsPath) {
|
|
|
1853
1936
|
|
|
1854
1937
|
// src/extract/services.ts
|
|
1855
1938
|
init_cjs_shims();
|
|
1856
|
-
var
|
|
1857
|
-
var
|
|
1939
|
+
var import_node_fs7 = require("fs");
|
|
1940
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
1858
1941
|
var import_ignore = __toESM(require("ignore"), 1);
|
|
1859
|
-
var
|
|
1942
|
+
var import_minimatch2 = require("minimatch");
|
|
1860
1943
|
var import_types5 = require("@neat.is/types");
|
|
1861
1944
|
|
|
1862
1945
|
// src/extract/shared.ts
|
|
@@ -1969,6 +2052,73 @@ function pythonToPackage(service) {
|
|
|
1969
2052
|
};
|
|
1970
2053
|
}
|
|
1971
2054
|
|
|
2055
|
+
// src/extract/owners.ts
|
|
2056
|
+
init_cjs_shims();
|
|
2057
|
+
var import_node_fs6 = require("fs");
|
|
2058
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
2059
|
+
var import_minimatch = require("minimatch");
|
|
2060
|
+
async function loadCodeowners(scanPath) {
|
|
2061
|
+
const candidates = [
|
|
2062
|
+
import_node_path6.default.join(scanPath, "CODEOWNERS"),
|
|
2063
|
+
import_node_path6.default.join(scanPath, ".github", "CODEOWNERS")
|
|
2064
|
+
];
|
|
2065
|
+
for (const file of candidates) {
|
|
2066
|
+
if (await exists(file)) {
|
|
2067
|
+
const raw = await import_node_fs6.promises.readFile(file, "utf8");
|
|
2068
|
+
return parseCodeowners(raw);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
return null;
|
|
2072
|
+
}
|
|
2073
|
+
function parseCodeowners(raw) {
|
|
2074
|
+
const rules = [];
|
|
2075
|
+
for (const line of raw.split("\n")) {
|
|
2076
|
+
const trimmed = line.trim();
|
|
2077
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2078
|
+
const match = /^(\S+)\s+(.+)$/.exec(trimmed);
|
|
2079
|
+
if (!match) continue;
|
|
2080
|
+
rules.push({ pattern: match[1], owners: match[2].trim() });
|
|
2081
|
+
}
|
|
2082
|
+
return { rules };
|
|
2083
|
+
}
|
|
2084
|
+
function matchOwner(file, repoPath) {
|
|
2085
|
+
const normalized = repoPath.split(import_node_path6.default.sep).join("/");
|
|
2086
|
+
for (const rule of file.rules) {
|
|
2087
|
+
if (matchesPattern(rule.pattern, normalized)) return rule.owners;
|
|
2088
|
+
}
|
|
2089
|
+
return null;
|
|
2090
|
+
}
|
|
2091
|
+
function matchesPattern(rawPattern, repoPath) {
|
|
2092
|
+
let pattern = rawPattern.startsWith("/") ? rawPattern.slice(1) : rawPattern;
|
|
2093
|
+
if (pattern === "*") return !repoPath.includes("/");
|
|
2094
|
+
if (pattern === "**" || pattern === "") return true;
|
|
2095
|
+
if (pattern.endsWith("/")) pattern = pattern + "**";
|
|
2096
|
+
if ((0, import_minimatch.minimatch)(repoPath, pattern, { dot: true })) return true;
|
|
2097
|
+
if (!pattern.includes("*") && (0, import_minimatch.minimatch)(repoPath, pattern + "/**", { dot: true })) return true;
|
|
2098
|
+
return false;
|
|
2099
|
+
}
|
|
2100
|
+
async function readPackageJsonAuthor(serviceDir) {
|
|
2101
|
+
const pkgPath = import_node_path6.default.join(serviceDir, "package.json");
|
|
2102
|
+
if (!await exists(pkgPath)) return null;
|
|
2103
|
+
try {
|
|
2104
|
+
const pkg = await readJson(pkgPath);
|
|
2105
|
+
if (!pkg.author) return null;
|
|
2106
|
+
if (typeof pkg.author === "string") return pkg.author;
|
|
2107
|
+
if (typeof pkg.author === "object" && typeof pkg.author.name === "string") return pkg.author.name;
|
|
2108
|
+
return null;
|
|
2109
|
+
} catch {
|
|
2110
|
+
return null;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
async function computeServiceOwner(codeowners, repoPath, serviceDir) {
|
|
2114
|
+
if (codeowners && repoPath !== void 0) {
|
|
2115
|
+
const owner = matchOwner(codeowners, repoPath);
|
|
2116
|
+
if (owner) return owner;
|
|
2117
|
+
}
|
|
2118
|
+
const author = await readPackageJsonAuthor(serviceDir);
|
|
2119
|
+
return author ?? void 0;
|
|
2120
|
+
}
|
|
2121
|
+
|
|
1972
2122
|
// src/extract/services.ts
|
|
1973
2123
|
var DEFAULT_SCAN_DEPTH = 5;
|
|
1974
2124
|
function parseScanDepth() {
|
|
@@ -1985,21 +2135,21 @@ function workspaceGlobs(pkg) {
|
|
|
1985
2135
|
return null;
|
|
1986
2136
|
}
|
|
1987
2137
|
async function loadGitignore(scanPath) {
|
|
1988
|
-
const gitignorePath =
|
|
2138
|
+
const gitignorePath = import_node_path7.default.join(scanPath, ".gitignore");
|
|
1989
2139
|
if (!await exists(gitignorePath)) return null;
|
|
1990
|
-
const raw = await
|
|
2140
|
+
const raw = await import_node_fs7.promises.readFile(gitignorePath, "utf8");
|
|
1991
2141
|
return (0, import_ignore.default)().add(raw);
|
|
1992
2142
|
}
|
|
1993
2143
|
async function walkDirs(start, scanPath, options, visit) {
|
|
1994
2144
|
async function recurse(current, depth) {
|
|
1995
2145
|
if (depth > options.maxDepth) return;
|
|
1996
|
-
const entries = await
|
|
2146
|
+
const entries = await import_node_fs7.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
1997
2147
|
for (const entry2 of entries) {
|
|
1998
2148
|
if (!entry2.isDirectory()) continue;
|
|
1999
2149
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
2000
|
-
const child =
|
|
2150
|
+
const child = import_node_path7.default.join(current, entry2.name);
|
|
2001
2151
|
if (options.ig) {
|
|
2002
|
-
const rel =
|
|
2152
|
+
const rel = import_node_path7.default.relative(scanPath, child).split(import_node_path7.default.sep).join("/");
|
|
2003
2153
|
if (rel && options.ig.ignores(rel + "/")) continue;
|
|
2004
2154
|
}
|
|
2005
2155
|
await visit(child);
|
|
@@ -2014,8 +2164,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2014
2164
|
for (const raw of globs) {
|
|
2015
2165
|
const pattern = raw.replace(/^\.\//, "");
|
|
2016
2166
|
if (!pattern.includes("*")) {
|
|
2017
|
-
const candidate =
|
|
2018
|
-
if (await exists(
|
|
2167
|
+
const candidate = import_node_path7.default.join(scanPath, pattern);
|
|
2168
|
+
if (await exists(import_node_path7.default.join(candidate, "package.json"))) found.add(candidate);
|
|
2019
2169
|
continue;
|
|
2020
2170
|
}
|
|
2021
2171
|
const segments = pattern.split("/");
|
|
@@ -2024,13 +2174,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2024
2174
|
if (seg.includes("*")) break;
|
|
2025
2175
|
staticSegments.push(seg);
|
|
2026
2176
|
}
|
|
2027
|
-
const start =
|
|
2177
|
+
const start = import_node_path7.default.join(scanPath, ...staticSegments);
|
|
2028
2178
|
if (!await exists(start)) continue;
|
|
2029
2179
|
const hasDoubleStar = pattern.includes("**");
|
|
2030
2180
|
const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
|
|
2031
2181
|
await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
|
|
2032
|
-
const rel =
|
|
2033
|
-
if ((0,
|
|
2182
|
+
const rel = import_node_path7.default.relative(scanPath, dir).split(import_node_path7.default.sep).join("/");
|
|
2183
|
+
if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path7.default.join(dir, "package.json"))) {
|
|
2034
2184
|
found.add(dir);
|
|
2035
2185
|
}
|
|
2036
2186
|
});
|
|
@@ -2038,9 +2188,17 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2038
2188
|
return [...found];
|
|
2039
2189
|
}
|
|
2040
2190
|
async function discoverNodeService(scanPath, dir) {
|
|
2041
|
-
const pkgPath =
|
|
2191
|
+
const pkgPath = import_node_path7.default.join(dir, "package.json");
|
|
2042
2192
|
if (!await exists(pkgPath)) return null;
|
|
2043
|
-
|
|
2193
|
+
let pkg;
|
|
2194
|
+
try {
|
|
2195
|
+
pkg = await readJson(pkgPath);
|
|
2196
|
+
} catch (err) {
|
|
2197
|
+
console.warn(
|
|
2198
|
+
`[neat] services skipped ${import_node_path7.default.relative(scanPath, pkgPath)}: ${err.message}`
|
|
2199
|
+
);
|
|
2200
|
+
return null;
|
|
2201
|
+
}
|
|
2044
2202
|
if (!pkg.name) return null;
|
|
2045
2203
|
const node = {
|
|
2046
2204
|
id: (0, import_types5.serviceId)(pkg.name),
|
|
@@ -2049,7 +2207,7 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2049
2207
|
language: "javascript",
|
|
2050
2208
|
version: pkg.version,
|
|
2051
2209
|
dependencies: pkg.dependencies ?? {},
|
|
2052
|
-
repoPath:
|
|
2210
|
+
repoPath: import_node_path7.default.relative(scanPath, dir),
|
|
2053
2211
|
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
|
|
2054
2212
|
};
|
|
2055
2213
|
return { pkg, dir, node };
|
|
@@ -2065,13 +2223,22 @@ async function discoverPyService(scanPath, dir) {
|
|
|
2065
2223
|
language: "python",
|
|
2066
2224
|
version: py.version,
|
|
2067
2225
|
dependencies: py.dependencies,
|
|
2068
|
-
repoPath:
|
|
2226
|
+
repoPath: import_node_path7.default.relative(scanPath, dir)
|
|
2069
2227
|
};
|
|
2070
2228
|
return { pkg, dir, node };
|
|
2071
2229
|
}
|
|
2072
2230
|
async function discoverServices(scanPath) {
|
|
2073
|
-
const rootPkgPath =
|
|
2074
|
-
|
|
2231
|
+
const rootPkgPath = import_node_path7.default.join(scanPath, "package.json");
|
|
2232
|
+
let rootPkg = null;
|
|
2233
|
+
if (await exists(rootPkgPath)) {
|
|
2234
|
+
try {
|
|
2235
|
+
rootPkg = await readJson(rootPkgPath);
|
|
2236
|
+
} catch (err) {
|
|
2237
|
+
console.warn(
|
|
2238
|
+
`[neat] services workspaces skipped ${import_node_path7.default.relative(scanPath, rootPkgPath)}: ${err.message}`
|
|
2239
|
+
);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2075
2242
|
const wsGlobs = rootPkg ? workspaceGlobs(rootPkg) : null;
|
|
2076
2243
|
const candidateDirs = [];
|
|
2077
2244
|
if (wsGlobs) {
|
|
@@ -2084,9 +2251,9 @@ async function discoverServices(scanPath) {
|
|
|
2084
2251
|
scanPath,
|
|
2085
2252
|
{ maxDepth: parseScanDepth(), ig },
|
|
2086
2253
|
async (dir) => {
|
|
2087
|
-
if (await exists(
|
|
2254
|
+
if (await exists(import_node_path7.default.join(dir, "package.json"))) {
|
|
2088
2255
|
candidateDirs.push(dir);
|
|
2089
|
-
} else if (await exists(
|
|
2256
|
+
} else if (await exists(import_node_path7.default.join(dir, "pyproject.toml")) || await exists(import_node_path7.default.join(dir, "requirements.txt")) || await exists(import_node_path7.default.join(dir, "setup.py"))) {
|
|
2090
2257
|
candidateDirs.push(dir);
|
|
2091
2258
|
}
|
|
2092
2259
|
}
|
|
@@ -2100,8 +2267,8 @@ async function discoverServices(scanPath) {
|
|
|
2100
2267
|
if (!service) continue;
|
|
2101
2268
|
const existingDir = seen.get(service.node.name);
|
|
2102
2269
|
if (existingDir !== void 0) {
|
|
2103
|
-
const a =
|
|
2104
|
-
const b =
|
|
2270
|
+
const a = import_node_path7.default.relative(scanPath, existingDir) || ".";
|
|
2271
|
+
const b = import_node_path7.default.relative(scanPath, dir) || ".";
|
|
2105
2272
|
console.warn(
|
|
2106
2273
|
`[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
|
|
2107
2274
|
);
|
|
@@ -2110,6 +2277,11 @@ async function discoverServices(scanPath) {
|
|
|
2110
2277
|
seen.set(service.node.name, dir);
|
|
2111
2278
|
out.push(service);
|
|
2112
2279
|
}
|
|
2280
|
+
const codeowners = await loadCodeowners(scanPath);
|
|
2281
|
+
for (const service of out) {
|
|
2282
|
+
const owner = await computeServiceOwner(codeowners, service.node.repoPath, service.dir);
|
|
2283
|
+
if (owner !== void 0) service.node.owner = owner;
|
|
2284
|
+
}
|
|
2113
2285
|
return out;
|
|
2114
2286
|
}
|
|
2115
2287
|
function addServiceNodes(graph, services) {
|
|
@@ -2133,8 +2305,8 @@ function addServiceNodes(graph, services) {
|
|
|
2133
2305
|
|
|
2134
2306
|
// src/extract/aliases.ts
|
|
2135
2307
|
init_cjs_shims();
|
|
2136
|
-
var
|
|
2137
|
-
var
|
|
2308
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
2309
|
+
var import_node_fs8 = require("fs");
|
|
2138
2310
|
var import_yaml2 = require("yaml");
|
|
2139
2311
|
var import_types6 = require("@neat.is/types");
|
|
2140
2312
|
var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
@@ -2161,21 +2333,29 @@ function indexServicesByName(services) {
|
|
|
2161
2333
|
const map = /* @__PURE__ */ new Map();
|
|
2162
2334
|
for (const s of services) {
|
|
2163
2335
|
map.set(s.node.name, s.node.id);
|
|
2164
|
-
map.set(
|
|
2336
|
+
map.set(import_node_path8.default.basename(s.dir), s.node.id);
|
|
2165
2337
|
}
|
|
2166
2338
|
return map;
|
|
2167
2339
|
}
|
|
2168
2340
|
async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
2169
2341
|
let composePath = null;
|
|
2170
2342
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2171
|
-
const abs =
|
|
2343
|
+
const abs = import_node_path8.default.join(scanPath, name);
|
|
2172
2344
|
if (await exists(abs)) {
|
|
2173
2345
|
composePath = abs;
|
|
2174
2346
|
break;
|
|
2175
2347
|
}
|
|
2176
2348
|
}
|
|
2177
2349
|
if (!composePath) return;
|
|
2178
|
-
|
|
2350
|
+
let compose;
|
|
2351
|
+
try {
|
|
2352
|
+
compose = await readYaml(composePath);
|
|
2353
|
+
} catch (err) {
|
|
2354
|
+
console.warn(
|
|
2355
|
+
`[neat] aliases compose skipped ${import_node_path8.default.relative(scanPath, composePath)}: ${err.message}`
|
|
2356
|
+
);
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2179
2359
|
if (!compose?.services) return;
|
|
2180
2360
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
2181
2361
|
const serviceId3 = serviceIndex.get(composeName);
|
|
@@ -2214,9 +2394,17 @@ function parseDockerfileLabels(content) {
|
|
|
2214
2394
|
}
|
|
2215
2395
|
async function collectDockerfileAliases(graph, services) {
|
|
2216
2396
|
for (const service of services) {
|
|
2217
|
-
const dockerfilePath =
|
|
2397
|
+
const dockerfilePath = import_node_path8.default.join(service.dir, "Dockerfile");
|
|
2218
2398
|
if (!await exists(dockerfilePath)) continue;
|
|
2219
|
-
|
|
2399
|
+
let content;
|
|
2400
|
+
try {
|
|
2401
|
+
content = await import_node_fs8.promises.readFile(dockerfilePath, "utf8");
|
|
2402
|
+
} catch (err) {
|
|
2403
|
+
console.warn(
|
|
2404
|
+
`[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
|
|
2405
|
+
);
|
|
2406
|
+
continue;
|
|
2407
|
+
}
|
|
2220
2408
|
const aliases = parseDockerfileLabels(content);
|
|
2221
2409
|
if (aliases.length > 0) addAliases(graph, service.node.id, aliases);
|
|
2222
2410
|
}
|
|
@@ -2224,13 +2412,13 @@ async function collectDockerfileAliases(graph, services) {
|
|
|
2224
2412
|
async function walkYamlFiles(start, depth = 0, max = 5) {
|
|
2225
2413
|
if (depth > max) return [];
|
|
2226
2414
|
const out = [];
|
|
2227
|
-
const entries = await
|
|
2415
|
+
const entries = await import_node_fs8.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
2228
2416
|
for (const entry2 of entries) {
|
|
2229
2417
|
if (entry2.isDirectory()) {
|
|
2230
2418
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
2231
|
-
out.push(...await walkYamlFiles(
|
|
2232
|
-
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
2233
|
-
out.push(
|
|
2419
|
+
out.push(...await walkYamlFiles(import_node_path8.default.join(start, entry2.name), depth + 1, max));
|
|
2420
|
+
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path8.default.extname(entry2.name))) {
|
|
2421
|
+
out.push(import_node_path8.default.join(start, entry2.name));
|
|
2234
2422
|
}
|
|
2235
2423
|
}
|
|
2236
2424
|
return out;
|
|
@@ -2257,7 +2445,7 @@ function k8sServiceTarget(doc, byName) {
|
|
|
2257
2445
|
async function collectK8sAliases(graph, scanPath, serviceIndex) {
|
|
2258
2446
|
const files = await walkYamlFiles(scanPath);
|
|
2259
2447
|
for (const file of files) {
|
|
2260
|
-
const content = await
|
|
2448
|
+
const content = await import_node_fs8.promises.readFile(file, "utf8");
|
|
2261
2449
|
let docs;
|
|
2262
2450
|
try {
|
|
2263
2451
|
docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
|
|
@@ -2282,14 +2470,14 @@ async function addServiceAliases(graph, scanPath, services) {
|
|
|
2282
2470
|
|
|
2283
2471
|
// src/extract/databases/index.ts
|
|
2284
2472
|
init_cjs_shims();
|
|
2285
|
-
var
|
|
2473
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
2286
2474
|
var import_types7 = require("@neat.is/types");
|
|
2287
2475
|
|
|
2288
2476
|
// src/extract/databases/db-config-yaml.ts
|
|
2289
2477
|
init_cjs_shims();
|
|
2290
|
-
var
|
|
2478
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
2291
2479
|
async function parse(serviceDir) {
|
|
2292
|
-
const yamlPath =
|
|
2480
|
+
const yamlPath = import_node_path9.default.join(serviceDir, "db-config.yaml");
|
|
2293
2481
|
if (!await exists(yamlPath)) return [];
|
|
2294
2482
|
const raw = await readYaml(yamlPath);
|
|
2295
2483
|
return [
|
|
@@ -2307,13 +2495,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
|
|
|
2307
2495
|
|
|
2308
2496
|
// src/extract/databases/dotenv.ts
|
|
2309
2497
|
init_cjs_shims();
|
|
2310
|
-
var
|
|
2311
|
-
var
|
|
2498
|
+
var import_node_fs10 = require("fs");
|
|
2499
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
2312
2500
|
|
|
2313
2501
|
// src/extract/databases/shared.ts
|
|
2314
2502
|
init_cjs_shims();
|
|
2315
|
-
var
|
|
2316
|
-
var
|
|
2503
|
+
var import_node_fs9 = require("fs");
|
|
2504
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
2317
2505
|
function schemeToEngine(scheme) {
|
|
2318
2506
|
const s = scheme.toLowerCase().split("+")[0];
|
|
2319
2507
|
switch (s) {
|
|
@@ -2352,14 +2540,14 @@ function parseConnectionString(url) {
|
|
|
2352
2540
|
}
|
|
2353
2541
|
async function readIfExists(filePath) {
|
|
2354
2542
|
try {
|
|
2355
|
-
return await
|
|
2543
|
+
return await import_node_fs9.promises.readFile(filePath, "utf8");
|
|
2356
2544
|
} catch {
|
|
2357
2545
|
return null;
|
|
2358
2546
|
}
|
|
2359
2547
|
}
|
|
2360
2548
|
async function findFirst(serviceDir, candidates) {
|
|
2361
2549
|
for (const rel of candidates) {
|
|
2362
|
-
const abs =
|
|
2550
|
+
const abs = import_node_path10.default.join(serviceDir, rel);
|
|
2363
2551
|
const content = await readIfExists(abs);
|
|
2364
2552
|
if (content !== null) return abs;
|
|
2365
2553
|
}
|
|
@@ -2410,15 +2598,15 @@ function parseDotenvLine(line) {
|
|
|
2410
2598
|
return { key, value };
|
|
2411
2599
|
}
|
|
2412
2600
|
async function parse2(serviceDir) {
|
|
2413
|
-
const entries = await
|
|
2601
|
+
const entries = await import_node_fs10.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
|
|
2414
2602
|
const configs = [];
|
|
2415
2603
|
const seen = /* @__PURE__ */ new Set();
|
|
2416
2604
|
for (const entry2 of entries) {
|
|
2417
2605
|
if (!entry2.isFile()) continue;
|
|
2418
2606
|
const match = isConfigFile(entry2.name);
|
|
2419
2607
|
if (!match.match || match.fileType !== "env") continue;
|
|
2420
|
-
const filePath =
|
|
2421
|
-
const content = await
|
|
2608
|
+
const filePath = import_node_path11.default.join(serviceDir, entry2.name);
|
|
2609
|
+
const content = await import_node_fs10.promises.readFile(filePath, "utf8");
|
|
2422
2610
|
for (const line of content.split("\n")) {
|
|
2423
2611
|
const parsed = parseDotenvLine(line);
|
|
2424
2612
|
if (!parsed) continue;
|
|
@@ -2437,9 +2625,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
|
|
|
2437
2625
|
|
|
2438
2626
|
// src/extract/databases/prisma.ts
|
|
2439
2627
|
init_cjs_shims();
|
|
2440
|
-
var
|
|
2628
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
2441
2629
|
async function parse3(serviceDir) {
|
|
2442
|
-
const schemaPath =
|
|
2630
|
+
const schemaPath = import_node_path12.default.join(serviceDir, "prisma", "schema.prisma");
|
|
2443
2631
|
const content = await readIfExists(schemaPath);
|
|
2444
2632
|
if (!content) return [];
|
|
2445
2633
|
const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
|
|
@@ -2571,10 +2759,10 @@ var knexParser = { name: "knex", parse: parse5 };
|
|
|
2571
2759
|
|
|
2572
2760
|
// src/extract/databases/ormconfig.ts
|
|
2573
2761
|
init_cjs_shims();
|
|
2574
|
-
var
|
|
2762
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
2575
2763
|
async function parse6(serviceDir) {
|
|
2576
2764
|
for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
|
|
2577
|
-
const abs =
|
|
2765
|
+
const abs = import_node_path13.default.join(serviceDir, candidate);
|
|
2578
2766
|
if (!await exists(abs)) continue;
|
|
2579
2767
|
const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
|
|
2580
2768
|
const entries = Array.isArray(raw) ? raw : [raw];
|
|
@@ -2634,9 +2822,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
|
|
|
2634
2822
|
|
|
2635
2823
|
// src/extract/databases/sequelize.ts
|
|
2636
2824
|
init_cjs_shims();
|
|
2637
|
-
var
|
|
2825
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
2638
2826
|
async function parse8(serviceDir) {
|
|
2639
|
-
const configPath =
|
|
2827
|
+
const configPath = import_node_path14.default.join(serviceDir, "config", "config.json");
|
|
2640
2828
|
if (!await exists(configPath)) return [];
|
|
2641
2829
|
const raw = await readJson(configPath);
|
|
2642
2830
|
const out = [];
|
|
@@ -2663,7 +2851,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
|
|
|
2663
2851
|
|
|
2664
2852
|
// src/extract/databases/docker-compose.ts
|
|
2665
2853
|
init_cjs_shims();
|
|
2666
|
-
var
|
|
2854
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
2667
2855
|
function portFromService(svc) {
|
|
2668
2856
|
for (const raw of svc.ports ?? []) {
|
|
2669
2857
|
const str = String(raw);
|
|
@@ -2690,7 +2878,7 @@ function databaseFromEnv(svc) {
|
|
|
2690
2878
|
}
|
|
2691
2879
|
async function parse9(serviceDir) {
|
|
2692
2880
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2693
|
-
const abs =
|
|
2881
|
+
const abs = import_node_path15.default.join(serviceDir, name);
|
|
2694
2882
|
if (!await exists(abs)) continue;
|
|
2695
2883
|
const raw = await readYaml(abs);
|
|
2696
2884
|
if (!raw?.services) return [];
|
|
@@ -2870,7 +3058,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
2870
3058
|
provenance: import_types7.Provenance.EXTRACTED,
|
|
2871
3059
|
...config.sourceFile ? {
|
|
2872
3060
|
evidence: {
|
|
2873
|
-
file:
|
|
3061
|
+
file: import_node_path16.default.relative(scanPath, config.sourceFile).split(import_node_path16.default.sep).join("/")
|
|
2874
3062
|
}
|
|
2875
3063
|
} : {}
|
|
2876
3064
|
};
|
|
@@ -2898,15 +3086,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
2898
3086
|
|
|
2899
3087
|
// src/extract/configs.ts
|
|
2900
3088
|
init_cjs_shims();
|
|
2901
|
-
var
|
|
2902
|
-
var
|
|
3089
|
+
var import_node_fs11 = require("fs");
|
|
3090
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
2903
3091
|
var import_types8 = require("@neat.is/types");
|
|
2904
3092
|
async function walkConfigFiles(dir) {
|
|
2905
3093
|
const out = [];
|
|
2906
3094
|
async function walk(current) {
|
|
2907
|
-
const entries = await
|
|
3095
|
+
const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true });
|
|
2908
3096
|
for (const entry2 of entries) {
|
|
2909
|
-
const full =
|
|
3097
|
+
const full = import_node_path17.default.join(current, entry2.name);
|
|
2910
3098
|
if (entry2.isDirectory()) {
|
|
2911
3099
|
if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
|
|
2912
3100
|
} else if (entry2.isFile() && isConfigFile(entry2.name).match) {
|
|
@@ -2923,13 +3111,13 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
2923
3111
|
for (const service of services) {
|
|
2924
3112
|
const configFiles = await walkConfigFiles(service.dir);
|
|
2925
3113
|
for (const file of configFiles) {
|
|
2926
|
-
const relPath =
|
|
3114
|
+
const relPath = import_node_path17.default.relative(scanPath, file);
|
|
2927
3115
|
const node = {
|
|
2928
3116
|
id: (0, import_types8.configId)(relPath),
|
|
2929
3117
|
type: import_types8.NodeType.ConfigNode,
|
|
2930
|
-
name:
|
|
3118
|
+
name: import_node_path17.default.basename(file),
|
|
2931
3119
|
path: relPath,
|
|
2932
|
-
fileType: isConfigFile(
|
|
3120
|
+
fileType: isConfigFile(import_node_path17.default.basename(file)).fileType
|
|
2933
3121
|
};
|
|
2934
3122
|
if (!graph.hasNode(node.id)) {
|
|
2935
3123
|
graph.addNode(node.id, node);
|
|
@@ -2941,7 +3129,7 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
2941
3129
|
target: node.id,
|
|
2942
3130
|
type: import_types8.EdgeType.CONFIGURED_BY,
|
|
2943
3131
|
provenance: import_types8.Provenance.EXTRACTED,
|
|
2944
|
-
evidence: { file: relPath.split(
|
|
3132
|
+
evidence: { file: relPath.split(import_node_path17.default.sep).join("/") }
|
|
2945
3133
|
};
|
|
2946
3134
|
if (!graph.hasEdge(edge.id)) {
|
|
2947
3135
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -2958,7 +3146,7 @@ var import_types14 = require("@neat.is/types");
|
|
|
2958
3146
|
|
|
2959
3147
|
// src/extract/calls/http.ts
|
|
2960
3148
|
init_cjs_shims();
|
|
2961
|
-
var
|
|
3149
|
+
var import_node_path19 = __toESM(require("path"), 1);
|
|
2962
3150
|
var import_tree_sitter = __toESM(require("tree-sitter"), 1);
|
|
2963
3151
|
var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
|
|
2964
3152
|
var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
|
|
@@ -2966,17 +3154,17 @@ var import_types9 = require("@neat.is/types");
|
|
|
2966
3154
|
|
|
2967
3155
|
// src/extract/calls/shared.ts
|
|
2968
3156
|
init_cjs_shims();
|
|
2969
|
-
var
|
|
2970
|
-
var
|
|
3157
|
+
var import_node_fs12 = require("fs");
|
|
3158
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
2971
3159
|
async function walkSourceFiles(dir) {
|
|
2972
3160
|
const out = [];
|
|
2973
3161
|
async function walk(current) {
|
|
2974
|
-
const entries = await
|
|
3162
|
+
const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
2975
3163
|
for (const entry2 of entries) {
|
|
2976
|
-
const full =
|
|
3164
|
+
const full = import_node_path18.default.join(current, entry2.name);
|
|
2977
3165
|
if (entry2.isDirectory()) {
|
|
2978
3166
|
if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
|
|
2979
|
-
} else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(
|
|
3167
|
+
} else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path18.default.extname(entry2.name))) {
|
|
2980
3168
|
out.push(full);
|
|
2981
3169
|
}
|
|
2982
3170
|
}
|
|
@@ -2989,7 +3177,7 @@ async function loadSourceFiles(dir) {
|
|
|
2989
3177
|
const out = [];
|
|
2990
3178
|
for (const p of paths) {
|
|
2991
3179
|
try {
|
|
2992
|
-
const content = await
|
|
3180
|
+
const content = await import_node_fs12.promises.readFile(p, "utf8");
|
|
2993
3181
|
out.push({ path: p, content });
|
|
2994
3182
|
} catch {
|
|
2995
3183
|
}
|
|
@@ -3045,9 +3233,9 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3045
3233
|
const knownHosts = /* @__PURE__ */ new Set();
|
|
3046
3234
|
const hostToNodeId = /* @__PURE__ */ new Map();
|
|
3047
3235
|
for (const service of services) {
|
|
3048
|
-
knownHosts.add(
|
|
3236
|
+
knownHosts.add(import_node_path19.default.basename(service.dir));
|
|
3049
3237
|
knownHosts.add(service.pkg.name);
|
|
3050
|
-
hostToNodeId.set(
|
|
3238
|
+
hostToNodeId.set(import_node_path19.default.basename(service.dir), service.node.id);
|
|
3051
3239
|
hostToNodeId.set(service.pkg.name, service.node.id);
|
|
3052
3240
|
}
|
|
3053
3241
|
let edgesAdded = 0;
|
|
@@ -3055,8 +3243,16 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3055
3243
|
const files = await loadSourceFiles(service.dir);
|
|
3056
3244
|
const seenTargets = /* @__PURE__ */ new Map();
|
|
3057
3245
|
for (const file of files) {
|
|
3058
|
-
const parser =
|
|
3059
|
-
|
|
3246
|
+
const parser = import_node_path19.default.extname(file.path) === ".py" ? pyParser : jsParser;
|
|
3247
|
+
let targets;
|
|
3248
|
+
try {
|
|
3249
|
+
targets = callsFromSource(file.content, parser, knownHosts);
|
|
3250
|
+
} catch (err) {
|
|
3251
|
+
console.warn(
|
|
3252
|
+
`[neat] http call extraction skipped ${file.path}: ${err.message}`
|
|
3253
|
+
);
|
|
3254
|
+
continue;
|
|
3255
|
+
}
|
|
3060
3256
|
for (const t of targets) {
|
|
3061
3257
|
const targetId = hostToNodeId.get(t);
|
|
3062
3258
|
if (!targetId || targetId === service.node.id) continue;
|
|
@@ -3075,7 +3271,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3075
3271
|
type: import_types9.EdgeType.CALLS,
|
|
3076
3272
|
provenance: import_types9.Provenance.EXTRACTED,
|
|
3077
3273
|
evidence: {
|
|
3078
|
-
file:
|
|
3274
|
+
file: import_node_path19.default.relative(service.dir, evidenceFile.file),
|
|
3079
3275
|
line,
|
|
3080
3276
|
snippet: snippet(fileContent, line)
|
|
3081
3277
|
}
|
|
@@ -3091,7 +3287,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3091
3287
|
|
|
3092
3288
|
// src/extract/calls/kafka.ts
|
|
3093
3289
|
init_cjs_shims();
|
|
3094
|
-
var
|
|
3290
|
+
var import_node_path20 = __toESM(require("path"), 1);
|
|
3095
3291
|
var import_types10 = require("@neat.is/types");
|
|
3096
3292
|
var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3097
3293
|
var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
|
|
@@ -3118,7 +3314,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3118
3314
|
kind: "kafka-topic",
|
|
3119
3315
|
edgeType,
|
|
3120
3316
|
evidence: {
|
|
3121
|
-
file:
|
|
3317
|
+
file: import_node_path20.default.relative(serviceDir, file.path),
|
|
3122
3318
|
line,
|
|
3123
3319
|
snippet: snippet(file.content, line)
|
|
3124
3320
|
}
|
|
@@ -3131,7 +3327,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3131
3327
|
|
|
3132
3328
|
// src/extract/calls/redis.ts
|
|
3133
3329
|
init_cjs_shims();
|
|
3134
|
-
var
|
|
3330
|
+
var import_node_path21 = __toESM(require("path"), 1);
|
|
3135
3331
|
var import_types11 = require("@neat.is/types");
|
|
3136
3332
|
var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
|
|
3137
3333
|
function redisEndpointsFromFile(file, serviceDir) {
|
|
@@ -3150,7 +3346,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3150
3346
|
kind: "redis",
|
|
3151
3347
|
edgeType: "CALLS",
|
|
3152
3348
|
evidence: {
|
|
3153
|
-
file:
|
|
3349
|
+
file: import_node_path21.default.relative(serviceDir, file.path),
|
|
3154
3350
|
line,
|
|
3155
3351
|
snippet: snippet(file.content, line)
|
|
3156
3352
|
}
|
|
@@ -3161,7 +3357,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3161
3357
|
|
|
3162
3358
|
// src/extract/calls/aws.ts
|
|
3163
3359
|
init_cjs_shims();
|
|
3164
|
-
var
|
|
3360
|
+
var import_node_path22 = __toESM(require("path"), 1);
|
|
3165
3361
|
var import_types12 = require("@neat.is/types");
|
|
3166
3362
|
var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3167
3363
|
var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
@@ -3191,7 +3387,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3191
3387
|
kind,
|
|
3192
3388
|
edgeType: "CALLS",
|
|
3193
3389
|
evidence: {
|
|
3194
|
-
file:
|
|
3390
|
+
file: import_node_path22.default.relative(serviceDir, file.path),
|
|
3195
3391
|
line,
|
|
3196
3392
|
snippet: snippet(file.content, line)
|
|
3197
3393
|
}
|
|
@@ -3216,7 +3412,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3216
3412
|
|
|
3217
3413
|
// src/extract/calls/grpc.ts
|
|
3218
3414
|
init_cjs_shims();
|
|
3219
|
-
var
|
|
3415
|
+
var import_node_path23 = __toESM(require("path"), 1);
|
|
3220
3416
|
var import_types13 = require("@neat.is/types");
|
|
3221
3417
|
var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
|
|
3222
3418
|
function isLikelyAddress(value) {
|
|
@@ -3241,7 +3437,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
3241
3437
|
kind: "grpc-service",
|
|
3242
3438
|
edgeType: "CALLS",
|
|
3243
3439
|
evidence: {
|
|
3244
|
-
file:
|
|
3440
|
+
file: import_node_path23.default.relative(serviceDir, file.path),
|
|
3245
3441
|
line,
|
|
3246
3442
|
snippet: snippet(file.content, line)
|
|
3247
3443
|
}
|
|
@@ -3321,7 +3517,7 @@ init_cjs_shims();
|
|
|
3321
3517
|
|
|
3322
3518
|
// src/extract/infra/docker-compose.ts
|
|
3323
3519
|
init_cjs_shims();
|
|
3324
|
-
var
|
|
3520
|
+
var import_node_path24 = __toESM(require("path"), 1);
|
|
3325
3521
|
var import_types16 = require("@neat.is/types");
|
|
3326
3522
|
|
|
3327
3523
|
// src/extract/infra/shared.ts
|
|
@@ -3359,7 +3555,7 @@ function dependsOnList(value) {
|
|
|
3359
3555
|
}
|
|
3360
3556
|
function serviceNameToServiceNode(name, services) {
|
|
3361
3557
|
for (const s of services) {
|
|
3362
|
-
if (s.node.name === name ||
|
|
3558
|
+
if (s.node.name === name || import_node_path24.default.basename(s.dir) === name) return s.node.id;
|
|
3363
3559
|
}
|
|
3364
3560
|
return null;
|
|
3365
3561
|
}
|
|
@@ -3368,16 +3564,24 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3368
3564
|
let edgesAdded = 0;
|
|
3369
3565
|
let composePath = null;
|
|
3370
3566
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
3371
|
-
const abs =
|
|
3567
|
+
const abs = import_node_path24.default.join(scanPath, name);
|
|
3372
3568
|
if (await exists(abs)) {
|
|
3373
3569
|
composePath = abs;
|
|
3374
3570
|
break;
|
|
3375
3571
|
}
|
|
3376
3572
|
}
|
|
3377
3573
|
if (!composePath) return { nodesAdded, edgesAdded };
|
|
3378
|
-
|
|
3574
|
+
let compose;
|
|
3575
|
+
try {
|
|
3576
|
+
compose = await readYaml(composePath);
|
|
3577
|
+
} catch (err) {
|
|
3578
|
+
console.warn(
|
|
3579
|
+
`[neat] infra docker-compose skipped ${import_node_path24.default.relative(scanPath, composePath)}: ${err.message}`
|
|
3580
|
+
);
|
|
3581
|
+
return { nodesAdded, edgesAdded };
|
|
3582
|
+
}
|
|
3379
3583
|
if (!compose?.services) return { nodesAdded, edgesAdded };
|
|
3380
|
-
const evidenceFile =
|
|
3584
|
+
const evidenceFile = import_node_path24.default.relative(scanPath, composePath).split(import_node_path24.default.sep).join("/");
|
|
3381
3585
|
const composeNameToNodeId = /* @__PURE__ */ new Map();
|
|
3382
3586
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
3383
3587
|
const matchedServiceId = serviceNameToServiceNode(composeName, services);
|
|
@@ -3418,8 +3622,8 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3418
3622
|
|
|
3419
3623
|
// src/extract/infra/dockerfile.ts
|
|
3420
3624
|
init_cjs_shims();
|
|
3421
|
-
var
|
|
3422
|
-
var
|
|
3625
|
+
var import_node_path25 = __toESM(require("path"), 1);
|
|
3626
|
+
var import_node_fs13 = require("fs");
|
|
3423
3627
|
var import_types17 = require("@neat.is/types");
|
|
3424
3628
|
function runtimeImage(content) {
|
|
3425
3629
|
const lines = content.split("\n");
|
|
@@ -3439,9 +3643,17 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3439
3643
|
let nodesAdded = 0;
|
|
3440
3644
|
let edgesAdded = 0;
|
|
3441
3645
|
for (const service of services) {
|
|
3442
|
-
const dockerfilePath =
|
|
3646
|
+
const dockerfilePath = import_node_path25.default.join(service.dir, "Dockerfile");
|
|
3443
3647
|
if (!await exists(dockerfilePath)) continue;
|
|
3444
|
-
|
|
3648
|
+
let content;
|
|
3649
|
+
try {
|
|
3650
|
+
content = await import_node_fs13.promises.readFile(dockerfilePath, "utf8");
|
|
3651
|
+
} catch (err) {
|
|
3652
|
+
console.warn(
|
|
3653
|
+
`[neat] infra dockerfile skipped ${import_node_path25.default.relative(scanPath, dockerfilePath)}: ${err.message}`
|
|
3654
|
+
);
|
|
3655
|
+
continue;
|
|
3656
|
+
}
|
|
3445
3657
|
const image = runtimeImage(content);
|
|
3446
3658
|
if (!image) continue;
|
|
3447
3659
|
const node = makeInfraNode("container-image", image);
|
|
@@ -3458,7 +3670,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3458
3670
|
type: import_types17.EdgeType.RUNS_ON,
|
|
3459
3671
|
provenance: import_types17.Provenance.EXTRACTED,
|
|
3460
3672
|
evidence: {
|
|
3461
|
-
file:
|
|
3673
|
+
file: import_node_path25.default.relative(scanPath, dockerfilePath).split(import_node_path25.default.sep).join("/")
|
|
3462
3674
|
}
|
|
3463
3675
|
};
|
|
3464
3676
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3470,19 +3682,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3470
3682
|
|
|
3471
3683
|
// src/extract/infra/terraform.ts
|
|
3472
3684
|
init_cjs_shims();
|
|
3473
|
-
var
|
|
3474
|
-
var
|
|
3685
|
+
var import_node_fs14 = require("fs");
|
|
3686
|
+
var import_node_path26 = __toESM(require("path"), 1);
|
|
3475
3687
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
3476
3688
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
3477
3689
|
if (depth > max) return [];
|
|
3478
3690
|
const out = [];
|
|
3479
|
-
const entries = await
|
|
3691
|
+
const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3480
3692
|
for (const entry2 of entries) {
|
|
3481
3693
|
if (entry2.isDirectory()) {
|
|
3482
3694
|
if (IGNORED_DIRS.has(entry2.name) || entry2.name === ".terraform") continue;
|
|
3483
|
-
out.push(...await walkTfFiles(
|
|
3695
|
+
out.push(...await walkTfFiles(import_node_path26.default.join(start, entry2.name), depth + 1, max));
|
|
3484
3696
|
} else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
|
|
3485
|
-
out.push(
|
|
3697
|
+
out.push(import_node_path26.default.join(start, entry2.name));
|
|
3486
3698
|
}
|
|
3487
3699
|
}
|
|
3488
3700
|
return out;
|
|
@@ -3491,7 +3703,7 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3491
3703
|
let nodesAdded = 0;
|
|
3492
3704
|
const files = await walkTfFiles(scanPath);
|
|
3493
3705
|
for (const file of files) {
|
|
3494
|
-
const content = await
|
|
3706
|
+
const content = await import_node_fs14.promises.readFile(file, "utf8");
|
|
3495
3707
|
RESOURCE_RE.lastIndex = 0;
|
|
3496
3708
|
let m;
|
|
3497
3709
|
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
@@ -3509,8 +3721,8 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3509
3721
|
|
|
3510
3722
|
// src/extract/infra/k8s.ts
|
|
3511
3723
|
init_cjs_shims();
|
|
3512
|
-
var
|
|
3513
|
-
var
|
|
3724
|
+
var import_node_fs15 = require("fs");
|
|
3725
|
+
var import_node_path27 = __toESM(require("path"), 1);
|
|
3514
3726
|
var import_yaml3 = require("yaml");
|
|
3515
3727
|
var K8S_KIND_TO_INFRA_KIND = {
|
|
3516
3728
|
Service: "k8s-service",
|
|
@@ -3524,13 +3736,13 @@ var K8S_KIND_TO_INFRA_KIND = {
|
|
|
3524
3736
|
async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
3525
3737
|
if (depth > max) return [];
|
|
3526
3738
|
const out = [];
|
|
3527
|
-
const entries = await
|
|
3739
|
+
const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3528
3740
|
for (const entry2 of entries) {
|
|
3529
3741
|
if (entry2.isDirectory()) {
|
|
3530
3742
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
3531
|
-
out.push(...await walkYamlFiles2(
|
|
3532
|
-
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
3533
|
-
out.push(
|
|
3743
|
+
out.push(...await walkYamlFiles2(import_node_path27.default.join(start, entry2.name), depth + 1, max));
|
|
3744
|
+
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path27.default.extname(entry2.name))) {
|
|
3745
|
+
out.push(import_node_path27.default.join(start, entry2.name));
|
|
3534
3746
|
}
|
|
3535
3747
|
}
|
|
3536
3748
|
return out;
|
|
@@ -3539,7 +3751,7 @@ async function addK8sResources(graph, scanPath) {
|
|
|
3539
3751
|
let nodesAdded = 0;
|
|
3540
3752
|
const files = await walkYamlFiles2(scanPath);
|
|
3541
3753
|
for (const file of files) {
|
|
3542
|
-
const content = await
|
|
3754
|
+
const content = await import_node_fs15.promises.readFile(file, "utf8");
|
|
3543
3755
|
let docs;
|
|
3544
3756
|
try {
|
|
3545
3757
|
docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
|
|
@@ -3585,17 +3797,28 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
3585
3797
|
const phase5 = await addInfra(graph, scanPath, services);
|
|
3586
3798
|
const frontiersPromoted = promoteFrontierNodes(graph);
|
|
3587
3799
|
if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
|
|
3588
|
-
|
|
3800
|
+
const result = {
|
|
3589
3801
|
nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
3590
3802
|
edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
3591
3803
|
frontiersPromoted
|
|
3592
3804
|
};
|
|
3805
|
+
emitNeatEvent({
|
|
3806
|
+
type: "extraction-complete",
|
|
3807
|
+
project: opts.project ?? DEFAULT_PROJECT,
|
|
3808
|
+
payload: {
|
|
3809
|
+
project: opts.project ?? DEFAULT_PROJECT,
|
|
3810
|
+
fileCount: services.length,
|
|
3811
|
+
nodesAdded: result.nodesAdded,
|
|
3812
|
+
edgesAdded: result.edgesAdded
|
|
3813
|
+
}
|
|
3814
|
+
});
|
|
3815
|
+
return result;
|
|
3593
3816
|
}
|
|
3594
3817
|
|
|
3595
3818
|
// src/persist.ts
|
|
3596
3819
|
init_cjs_shims();
|
|
3597
|
-
var
|
|
3598
|
-
var
|
|
3820
|
+
var import_node_fs16 = require("fs");
|
|
3821
|
+
var import_node_path28 = __toESM(require("path"), 1);
|
|
3599
3822
|
var SCHEMA_VERSION = 2;
|
|
3600
3823
|
function migrateV1ToV2(payload) {
|
|
3601
3824
|
const nodes = payload.graph.nodes;
|
|
@@ -3609,7 +3832,7 @@ function migrateV1ToV2(payload) {
|
|
|
3609
3832
|
return { ...payload, schemaVersion: 2 };
|
|
3610
3833
|
}
|
|
3611
3834
|
async function ensureDir(filePath) {
|
|
3612
|
-
await
|
|
3835
|
+
await import_node_fs16.promises.mkdir(import_node_path28.default.dirname(filePath), { recursive: true });
|
|
3613
3836
|
}
|
|
3614
3837
|
async function saveGraphToDisk(graph, outPath) {
|
|
3615
3838
|
await ensureDir(outPath);
|
|
@@ -3619,13 +3842,13 @@ async function saveGraphToDisk(graph, outPath) {
|
|
|
3619
3842
|
graph: graph.export()
|
|
3620
3843
|
};
|
|
3621
3844
|
const tmp = `${outPath}.tmp`;
|
|
3622
|
-
await
|
|
3623
|
-
await
|
|
3845
|
+
await import_node_fs16.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
3846
|
+
await import_node_fs16.promises.rename(tmp, outPath);
|
|
3624
3847
|
}
|
|
3625
3848
|
async function loadGraphFromDisk(graph, outPath) {
|
|
3626
3849
|
let raw;
|
|
3627
3850
|
try {
|
|
3628
|
-
raw = await
|
|
3851
|
+
raw = await import_node_fs16.promises.readFile(outPath, "utf8");
|
|
3629
3852
|
} catch (err) {
|
|
3630
3853
|
if (err.code === "ENOENT") return;
|
|
3631
3854
|
throw err;
|
|
@@ -3678,18 +3901,18 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
3678
3901
|
|
|
3679
3902
|
// src/watch.ts
|
|
3680
3903
|
init_cjs_shims();
|
|
3681
|
-
var
|
|
3904
|
+
var import_node_path34 = __toESM(require("path"), 1);
|
|
3682
3905
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
3683
3906
|
|
|
3684
3907
|
// src/api.ts
|
|
3685
3908
|
init_cjs_shims();
|
|
3686
3909
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
3687
3910
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
3688
|
-
var
|
|
3911
|
+
var import_types19 = require("@neat.is/types");
|
|
3689
3912
|
|
|
3690
3913
|
// src/diff.ts
|
|
3691
3914
|
init_cjs_shims();
|
|
3692
|
-
var
|
|
3915
|
+
var import_node_fs17 = require("fs");
|
|
3693
3916
|
async function loadSnapshotForDiff(target) {
|
|
3694
3917
|
if (/^https?:\/\//i.test(target)) {
|
|
3695
3918
|
const res = await fetch(target);
|
|
@@ -3698,7 +3921,7 @@ async function loadSnapshotForDiff(target) {
|
|
|
3698
3921
|
}
|
|
3699
3922
|
return await res.json();
|
|
3700
3923
|
}
|
|
3701
|
-
const raw = await
|
|
3924
|
+
const raw = await import_node_fs17.promises.readFile(target, "utf8");
|
|
3702
3925
|
return JSON.parse(raw);
|
|
3703
3926
|
}
|
|
3704
3927
|
function indexEntries(entries) {
|
|
@@ -3766,23 +3989,23 @@ function canonicalJson(value) {
|
|
|
3766
3989
|
|
|
3767
3990
|
// src/projects.ts
|
|
3768
3991
|
init_cjs_shims();
|
|
3769
|
-
var
|
|
3992
|
+
var import_node_path29 = __toESM(require("path"), 1);
|
|
3770
3993
|
function pathsForProject(project, baseDir) {
|
|
3771
3994
|
if (project === DEFAULT_PROJECT) {
|
|
3772
3995
|
return {
|
|
3773
|
-
snapshotPath:
|
|
3774
|
-
errorsPath:
|
|
3775
|
-
staleEventsPath:
|
|
3776
|
-
embeddingsCachePath:
|
|
3777
|
-
policyViolationsPath:
|
|
3996
|
+
snapshotPath: import_node_path29.default.join(baseDir, "graph.json"),
|
|
3997
|
+
errorsPath: import_node_path29.default.join(baseDir, "errors.ndjson"),
|
|
3998
|
+
staleEventsPath: import_node_path29.default.join(baseDir, "stale-events.ndjson"),
|
|
3999
|
+
embeddingsCachePath: import_node_path29.default.join(baseDir, "embeddings.json"),
|
|
4000
|
+
policyViolationsPath: import_node_path29.default.join(baseDir, "policy-violations.ndjson")
|
|
3778
4001
|
};
|
|
3779
4002
|
}
|
|
3780
4003
|
return {
|
|
3781
|
-
snapshotPath:
|
|
3782
|
-
errorsPath:
|
|
3783
|
-
staleEventsPath:
|
|
3784
|
-
embeddingsCachePath:
|
|
3785
|
-
policyViolationsPath:
|
|
4004
|
+
snapshotPath: import_node_path29.default.join(baseDir, `${project}.json`),
|
|
4005
|
+
errorsPath: import_node_path29.default.join(baseDir, `errors.${project}.ndjson`),
|
|
4006
|
+
staleEventsPath: import_node_path29.default.join(baseDir, `stale-events.${project}.ndjson`),
|
|
4007
|
+
embeddingsCachePath: import_node_path29.default.join(baseDir, `embeddings.${project}.json`),
|
|
4008
|
+
policyViolationsPath: import_node_path29.default.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
3786
4009
|
};
|
|
3787
4010
|
}
|
|
3788
4011
|
var Projects = class {
|
|
@@ -3816,56 +4039,271 @@ var Projects = class {
|
|
|
3816
4039
|
}
|
|
3817
4040
|
};
|
|
3818
4041
|
|
|
3819
|
-
// src/
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
4042
|
+
// src/registry.ts
|
|
4043
|
+
init_cjs_shims();
|
|
4044
|
+
var import_node_fs18 = require("fs");
|
|
4045
|
+
var import_node_os2 = __toESM(require("os"), 1);
|
|
4046
|
+
var import_node_path30 = __toESM(require("path"), 1);
|
|
4047
|
+
var import_types18 = require("@neat.is/types");
|
|
4048
|
+
var LOCK_TIMEOUT_MS = 5e3;
|
|
4049
|
+
var LOCK_RETRY_MS = 50;
|
|
4050
|
+
function neatHome() {
|
|
4051
|
+
const override = process.env.NEAT_HOME;
|
|
4052
|
+
if (override && override.length > 0) return import_node_path30.default.resolve(override);
|
|
4053
|
+
return import_node_path30.default.join(import_node_os2.default.homedir(), ".neat");
|
|
3830
4054
|
}
|
|
3831
|
-
function
|
|
3832
|
-
|
|
3833
|
-
return params.project ?? DEFAULT_PROJECT;
|
|
4055
|
+
function registryPath() {
|
|
4056
|
+
return import_node_path30.default.join(neatHome(), "projects.json");
|
|
3834
4057
|
}
|
|
3835
|
-
function
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
4058
|
+
function registryLockPath() {
|
|
4059
|
+
return import_node_path30.default.join(neatHome(), "projects.json.lock");
|
|
4060
|
+
}
|
|
4061
|
+
async function normalizeProjectPath(input) {
|
|
4062
|
+
const resolved = import_node_path30.default.resolve(input);
|
|
4063
|
+
try {
|
|
4064
|
+
return await import_node_fs18.promises.realpath(resolved);
|
|
4065
|
+
} catch {
|
|
4066
|
+
return resolved;
|
|
3841
4067
|
}
|
|
3842
|
-
return ctx;
|
|
3843
4068
|
}
|
|
3844
|
-
function
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
4069
|
+
async function writeAtomically(target, contents) {
|
|
4070
|
+
await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(target), { recursive: true });
|
|
4071
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
4072
|
+
const fd = await import_node_fs18.promises.open(tmp, "w");
|
|
4073
|
+
try {
|
|
4074
|
+
await fd.writeFile(contents, "utf8");
|
|
4075
|
+
await fd.sync();
|
|
4076
|
+
} finally {
|
|
4077
|
+
await fd.close();
|
|
3848
4078
|
}
|
|
3849
|
-
|
|
3850
|
-
const paths = pathsForProject(DEFAULT_PROJECT, "");
|
|
3851
|
-
registry.set(DEFAULT_PROJECT, {
|
|
3852
|
-
graph: opts.graph,
|
|
3853
|
-
scanPath: opts.scanPath,
|
|
3854
|
-
paths: {
|
|
3855
|
-
snapshotPath: paths.snapshotPath,
|
|
3856
|
-
errorsPath: opts.errorsPath ?? paths.errorsPath,
|
|
3857
|
-
staleEventsPath: opts.staleEventsPath ?? paths.staleEventsPath,
|
|
3858
|
-
embeddingsCachePath: paths.embeddingsCachePath,
|
|
3859
|
-
policyViolationsPath: paths.policyViolationsPath
|
|
3860
|
-
},
|
|
3861
|
-
searchIndex: opts.searchIndex
|
|
3862
|
-
});
|
|
3863
|
-
return registry;
|
|
4079
|
+
await import_node_fs18.promises.rename(tmp, target);
|
|
3864
4080
|
}
|
|
3865
|
-
function
|
|
3866
|
-
const
|
|
3867
|
-
|
|
3868
|
-
|
|
4081
|
+
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
4082
|
+
const deadline = Date.now() + timeoutMs;
|
|
4083
|
+
await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(lockPath), { recursive: true });
|
|
4084
|
+
while (true) {
|
|
4085
|
+
try {
|
|
4086
|
+
const fd = await import_node_fs18.promises.open(lockPath, "wx");
|
|
4087
|
+
await fd.close();
|
|
4088
|
+
return;
|
|
4089
|
+
} catch (err) {
|
|
4090
|
+
const code = err.code;
|
|
4091
|
+
if (code !== "EEXIST") throw err;
|
|
4092
|
+
if (Date.now() >= deadline) {
|
|
4093
|
+
throw new Error(
|
|
4094
|
+
`neat registry: timed out after ${timeoutMs}ms waiting for ${lockPath}. Another neat process is holding the lock; if no such process exists, remove the file by hand.`
|
|
4095
|
+
);
|
|
4096
|
+
}
|
|
4097
|
+
await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
}
|
|
4101
|
+
async function releaseLock(lockPath) {
|
|
4102
|
+
await import_node_fs18.promises.unlink(lockPath).catch(() => {
|
|
4103
|
+
});
|
|
4104
|
+
}
|
|
4105
|
+
async function withLock(fn) {
|
|
4106
|
+
const lock = registryLockPath();
|
|
4107
|
+
await acquireLock(lock);
|
|
4108
|
+
try {
|
|
4109
|
+
return await fn();
|
|
4110
|
+
} finally {
|
|
4111
|
+
await releaseLock(lock);
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
4114
|
+
async function readRegistry() {
|
|
4115
|
+
const file = registryPath();
|
|
4116
|
+
let raw;
|
|
4117
|
+
try {
|
|
4118
|
+
raw = await import_node_fs18.promises.readFile(file, "utf8");
|
|
4119
|
+
} catch (err) {
|
|
4120
|
+
if (err.code === "ENOENT") {
|
|
4121
|
+
return { version: 1, projects: [] };
|
|
4122
|
+
}
|
|
4123
|
+
throw err;
|
|
4124
|
+
}
|
|
4125
|
+
const parsed = JSON.parse(raw);
|
|
4126
|
+
return import_types18.RegistryFileSchema.parse(parsed);
|
|
4127
|
+
}
|
|
4128
|
+
async function writeRegistry(reg) {
|
|
4129
|
+
const validated = import_types18.RegistryFileSchema.parse(reg);
|
|
4130
|
+
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
4131
|
+
}
|
|
4132
|
+
var ProjectNameCollisionError = class extends Error {
|
|
4133
|
+
projectName;
|
|
4134
|
+
constructor(name) {
|
|
4135
|
+
super(`neat registry: a project named "${name}" is already registered`);
|
|
4136
|
+
this.name = "ProjectNameCollisionError";
|
|
4137
|
+
this.projectName = name;
|
|
4138
|
+
}
|
|
4139
|
+
};
|
|
4140
|
+
async function addProject(opts) {
|
|
4141
|
+
const resolvedPath = await normalizeProjectPath(opts.path);
|
|
4142
|
+
return withLock(async () => {
|
|
4143
|
+
const reg = await readRegistry();
|
|
4144
|
+
const byName = reg.projects.find((p) => p.name === opts.name);
|
|
4145
|
+
const byPath = reg.projects.find((p) => p.path === resolvedPath);
|
|
4146
|
+
if (byName && byName.path !== resolvedPath) {
|
|
4147
|
+
throw new ProjectNameCollisionError(opts.name);
|
|
4148
|
+
}
|
|
4149
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4150
|
+
if (byName && byName.path === resolvedPath) {
|
|
4151
|
+
byName.lastSeenAt = now;
|
|
4152
|
+
if (opts.languages) byName.languages = opts.languages;
|
|
4153
|
+
if (opts.status) byName.status = opts.status;
|
|
4154
|
+
await writeRegistry(reg);
|
|
4155
|
+
return byName;
|
|
4156
|
+
}
|
|
4157
|
+
if (byPath && byPath.name !== opts.name) {
|
|
4158
|
+
throw new ProjectNameCollisionError(byPath.name);
|
|
4159
|
+
}
|
|
4160
|
+
const entry2 = {
|
|
4161
|
+
name: opts.name,
|
|
4162
|
+
path: resolvedPath,
|
|
4163
|
+
registeredAt: now,
|
|
4164
|
+
languages: opts.languages ?? [],
|
|
4165
|
+
status: opts.status ?? "active"
|
|
4166
|
+
};
|
|
4167
|
+
reg.projects.push(entry2);
|
|
4168
|
+
await writeRegistry(reg);
|
|
4169
|
+
return entry2;
|
|
4170
|
+
});
|
|
4171
|
+
}
|
|
4172
|
+
async function listProjects() {
|
|
4173
|
+
const reg = await readRegistry();
|
|
4174
|
+
return reg.projects;
|
|
4175
|
+
}
|
|
4176
|
+
async function setStatus(name, status2) {
|
|
4177
|
+
return withLock(async () => {
|
|
4178
|
+
const reg = await readRegistry();
|
|
4179
|
+
const entry2 = reg.projects.find((p) => p.name === name);
|
|
4180
|
+
if (!entry2) throw new Error(`neat registry: no project named "${name}"`);
|
|
4181
|
+
entry2.status = status2;
|
|
4182
|
+
await writeRegistry(reg);
|
|
4183
|
+
return entry2;
|
|
4184
|
+
});
|
|
4185
|
+
}
|
|
4186
|
+
async function removeProject(name) {
|
|
4187
|
+
return withLock(async () => {
|
|
4188
|
+
const reg = await readRegistry();
|
|
4189
|
+
const idx = reg.projects.findIndex((p) => p.name === name);
|
|
4190
|
+
if (idx < 0) return void 0;
|
|
4191
|
+
const [removed] = reg.projects.splice(idx, 1);
|
|
4192
|
+
await writeRegistry(reg);
|
|
4193
|
+
return removed;
|
|
4194
|
+
});
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
// src/streaming.ts
|
|
4198
|
+
init_cjs_shims();
|
|
4199
|
+
var SSE_HEARTBEAT_MS = 3e4;
|
|
4200
|
+
var SSE_BACKPRESSURE_CAP = 1e3;
|
|
4201
|
+
function handleSse(req, reply, opts) {
|
|
4202
|
+
const heartbeatMs = opts.heartbeatMs ?? SSE_HEARTBEAT_MS;
|
|
4203
|
+
const backpressureCap = opts.backpressureCap ?? SSE_BACKPRESSURE_CAP;
|
|
4204
|
+
reply.raw.setHeader("Content-Type", "text/event-stream");
|
|
4205
|
+
reply.raw.setHeader("Cache-Control", "no-cache, no-transform");
|
|
4206
|
+
reply.raw.setHeader("Connection", "keep-alive");
|
|
4207
|
+
reply.raw.setHeader("X-Accel-Buffering", "no");
|
|
4208
|
+
reply.raw.flushHeaders?.();
|
|
4209
|
+
let pending = 0;
|
|
4210
|
+
let dropped = false;
|
|
4211
|
+
const closeConnection = () => {
|
|
4212
|
+
if (dropped) return;
|
|
4213
|
+
dropped = true;
|
|
4214
|
+
eventBus.off(EVENT_BUS_CHANNEL, listener);
|
|
4215
|
+
clearInterval(heartbeat);
|
|
4216
|
+
if (!reply.raw.writableEnded) reply.raw.end();
|
|
4217
|
+
};
|
|
4218
|
+
const writeFrame = (frame) => {
|
|
4219
|
+
if (dropped) return;
|
|
4220
|
+
if (pending >= backpressureCap) {
|
|
4221
|
+
const errFrame = `event: error
|
|
4222
|
+
data: ${JSON.stringify({ reason: "backpressure" })}
|
|
4223
|
+
|
|
4224
|
+
`;
|
|
4225
|
+
reply.raw.write(errFrame);
|
|
4226
|
+
closeConnection();
|
|
4227
|
+
return;
|
|
4228
|
+
}
|
|
4229
|
+
pending++;
|
|
4230
|
+
reply.raw.write(frame, () => {
|
|
4231
|
+
pending = Math.max(0, pending - 1);
|
|
4232
|
+
});
|
|
4233
|
+
};
|
|
4234
|
+
const listener = (envelope) => {
|
|
4235
|
+
if (envelope.project !== opts.project) return;
|
|
4236
|
+
writeFrame(`event: ${envelope.type}
|
|
4237
|
+
data: ${JSON.stringify(envelope.payload)}
|
|
4238
|
+
|
|
4239
|
+
`);
|
|
4240
|
+
};
|
|
4241
|
+
eventBus.on(EVENT_BUS_CHANNEL, listener);
|
|
4242
|
+
const heartbeat = setInterval(() => {
|
|
4243
|
+
if (dropped) return;
|
|
4244
|
+
reply.raw.write(":heartbeat\n\n");
|
|
4245
|
+
}, heartbeatMs);
|
|
4246
|
+
if (typeof heartbeat.unref === "function") heartbeat.unref();
|
|
4247
|
+
req.raw.on("close", closeConnection);
|
|
4248
|
+
reply.raw.on("close", closeConnection);
|
|
4249
|
+
reply.raw.on("error", closeConnection);
|
|
4250
|
+
}
|
|
4251
|
+
|
|
4252
|
+
// src/api.ts
|
|
4253
|
+
function serializeGraph(graph) {
|
|
4254
|
+
const nodes = [];
|
|
4255
|
+
graph.forEachNode((_id, attrs) => {
|
|
4256
|
+
nodes.push(attrs);
|
|
4257
|
+
});
|
|
4258
|
+
const edges = [];
|
|
4259
|
+
graph.forEachEdge((_id, attrs) => {
|
|
4260
|
+
edges.push(attrs);
|
|
4261
|
+
});
|
|
4262
|
+
return { nodes, edges };
|
|
4263
|
+
}
|
|
4264
|
+
function projectFromReq(req) {
|
|
4265
|
+
const params = req.params;
|
|
4266
|
+
return params.project ?? DEFAULT_PROJECT;
|
|
4267
|
+
}
|
|
4268
|
+
function resolveProject(registry, req, reply) {
|
|
4269
|
+
const name = projectFromReq(req);
|
|
4270
|
+
const ctx = registry.get(name);
|
|
4271
|
+
if (!ctx) {
|
|
4272
|
+
void reply.code(404).send({ error: "project not found", project: name });
|
|
4273
|
+
return null;
|
|
4274
|
+
}
|
|
4275
|
+
return ctx;
|
|
4276
|
+
}
|
|
4277
|
+
function buildLegacyRegistry(opts) {
|
|
4278
|
+
if (opts.projects) return opts.projects;
|
|
4279
|
+
if (!opts.graph) {
|
|
4280
|
+
throw new Error("buildApi: either `projects` or `graph` must be provided");
|
|
4281
|
+
}
|
|
4282
|
+
const registry = new Projects();
|
|
4283
|
+
const paths = pathsForProject(DEFAULT_PROJECT, "");
|
|
4284
|
+
registry.set(DEFAULT_PROJECT, {
|
|
4285
|
+
graph: opts.graph,
|
|
4286
|
+
scanPath: opts.scanPath,
|
|
4287
|
+
paths: {
|
|
4288
|
+
snapshotPath: paths.snapshotPath,
|
|
4289
|
+
errorsPath: opts.errorsPath ?? paths.errorsPath,
|
|
4290
|
+
staleEventsPath: opts.staleEventsPath ?? paths.staleEventsPath,
|
|
4291
|
+
embeddingsCachePath: paths.embeddingsCachePath,
|
|
4292
|
+
policyViolationsPath: paths.policyViolationsPath
|
|
4293
|
+
},
|
|
4294
|
+
searchIndex: opts.searchIndex
|
|
4295
|
+
});
|
|
4296
|
+
return registry;
|
|
4297
|
+
}
|
|
4298
|
+
function registerRoutes(scope, ctx) {
|
|
4299
|
+
const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
|
|
4300
|
+
scope.get("/events", (req, reply) => {
|
|
4301
|
+
const proj = resolveProject(registry, req, reply);
|
|
4302
|
+
if (!proj) return;
|
|
4303
|
+
handleSse(req, reply, { project: proj.name });
|
|
4304
|
+
});
|
|
4305
|
+
scope.get("/health", async (req, reply) => {
|
|
4306
|
+
const proj = resolveProject(registry, req, reply);
|
|
3869
4307
|
if (!proj) return;
|
|
3870
4308
|
return {
|
|
3871
4309
|
uptime: Math.floor((Date.now() - startedAt) / 1e3),
|
|
@@ -4074,7 +4512,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4074
4512
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
4075
4513
|
let violations = await log.readAll();
|
|
4076
4514
|
if (req.query.severity) {
|
|
4077
|
-
const sev =
|
|
4515
|
+
const sev = import_types19.PolicySeveritySchema.safeParse(req.query.severity);
|
|
4078
4516
|
if (!sev.success) {
|
|
4079
4517
|
return reply.code(400).send({
|
|
4080
4518
|
error: "invalid severity",
|
|
@@ -4091,7 +4529,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4091
4529
|
scope.post("/policies/check", async (req, reply) => {
|
|
4092
4530
|
const proj = resolveProject(registry, req, reply);
|
|
4093
4531
|
if (!proj) return;
|
|
4094
|
-
const parsed =
|
|
4532
|
+
const parsed = import_types19.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
4095
4533
|
if (!parsed.success) {
|
|
4096
4534
|
return reply.code(400).send({
|
|
4097
4535
|
error: "invalid /policies/check body",
|
|
@@ -4155,17 +4593,16 @@ async function buildApi(opts) {
|
|
|
4155
4593
|
staleEventsPathFor,
|
|
4156
4594
|
policyFilePathFor
|
|
4157
4595
|
};
|
|
4158
|
-
app.get("/projects", async () =>
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
}));
|
|
4596
|
+
app.get("/projects", async (_req, reply) => {
|
|
4597
|
+
try {
|
|
4598
|
+
return await listProjects();
|
|
4599
|
+
} catch (err) {
|
|
4600
|
+
return reply.code(500).send({
|
|
4601
|
+
error: "failed to read project registry",
|
|
4602
|
+
details: err.message
|
|
4603
|
+
});
|
|
4604
|
+
}
|
|
4605
|
+
});
|
|
4169
4606
|
registerRoutes(app, routeCtx);
|
|
4170
4607
|
await app.register(
|
|
4171
4608
|
async (scope) => {
|
|
@@ -4178,13 +4615,13 @@ async function buildApi(opts) {
|
|
|
4178
4615
|
|
|
4179
4616
|
// src/extract/retire.ts
|
|
4180
4617
|
init_cjs_shims();
|
|
4181
|
-
var
|
|
4618
|
+
var import_types20 = require("@neat.is/types");
|
|
4182
4619
|
function retireEdgesByFile(graph, file) {
|
|
4183
4620
|
const normalized = file.split("\\").join("/");
|
|
4184
4621
|
const toDrop = [];
|
|
4185
4622
|
graph.forEachEdge((id, attrs) => {
|
|
4186
4623
|
const edge = attrs;
|
|
4187
|
-
if (edge.provenance !==
|
|
4624
|
+
if (edge.provenance !== import_types20.Provenance.EXTRACTED) return;
|
|
4188
4625
|
if (!edge.evidence?.file) return;
|
|
4189
4626
|
if (edge.evidence.file === normalized) toDrop.push(id);
|
|
4190
4627
|
});
|
|
@@ -4198,8 +4635,8 @@ init_otel_grpc();
|
|
|
4198
4635
|
|
|
4199
4636
|
// src/search.ts
|
|
4200
4637
|
init_cjs_shims();
|
|
4201
|
-
var
|
|
4202
|
-
var
|
|
4638
|
+
var import_node_fs19 = require("fs");
|
|
4639
|
+
var import_node_path33 = __toESM(require("path"), 1);
|
|
4203
4640
|
var import_node_crypto = require("crypto");
|
|
4204
4641
|
var DEFAULT_LIMIT = 10;
|
|
4205
4642
|
var NOMIC_DIM = 768;
|
|
@@ -4329,7 +4766,7 @@ async function pickEmbedder() {
|
|
|
4329
4766
|
}
|
|
4330
4767
|
async function readCache(cachePath) {
|
|
4331
4768
|
try {
|
|
4332
|
-
const raw = await
|
|
4769
|
+
const raw = await import_node_fs19.promises.readFile(cachePath, "utf8");
|
|
4333
4770
|
const parsed = JSON.parse(raw);
|
|
4334
4771
|
if (parsed.version !== 1) return null;
|
|
4335
4772
|
return parsed;
|
|
@@ -4338,8 +4775,8 @@ async function readCache(cachePath) {
|
|
|
4338
4775
|
}
|
|
4339
4776
|
}
|
|
4340
4777
|
async function writeCache(cachePath, cache) {
|
|
4341
|
-
await
|
|
4342
|
-
await
|
|
4778
|
+
await import_node_fs19.promises.mkdir(import_node_path33.default.dirname(cachePath), { recursive: true });
|
|
4779
|
+
await import_node_fs19.promises.writeFile(cachePath, JSON.stringify(cache));
|
|
4343
4780
|
}
|
|
4344
4781
|
var VectorIndex = class {
|
|
4345
4782
|
constructor(embedder, cachePath) {
|
|
@@ -4494,8 +4931,8 @@ var ALL_PHASES = [
|
|
|
4494
4931
|
];
|
|
4495
4932
|
function classifyChange(relPath) {
|
|
4496
4933
|
const phases = /* @__PURE__ */ new Set();
|
|
4497
|
-
const base =
|
|
4498
|
-
const segments = relPath.split(
|
|
4934
|
+
const base = import_node_path34.default.basename(relPath).toLowerCase();
|
|
4935
|
+
const segments = relPath.split(import_node_path34.default.sep).map((s) => s.toLowerCase());
|
|
4499
4936
|
if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
|
|
4500
4937
|
phases.add("services");
|
|
4501
4938
|
phases.add("aliases");
|
|
@@ -4518,7 +4955,8 @@ function classifyChange(relPath) {
|
|
|
4518
4955
|
}
|
|
4519
4956
|
return phases;
|
|
4520
4957
|
}
|
|
4521
|
-
async function runExtractPhases(graph, scanPath, phases) {
|
|
4958
|
+
async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJECT) {
|
|
4959
|
+
void project;
|
|
4522
4960
|
const started = Date.now();
|
|
4523
4961
|
await ensureCompatLoaded();
|
|
4524
4962
|
const services = await discoverServices(scanPath);
|
|
@@ -4574,9 +5012,11 @@ function shouldIgnore(absPath) {
|
|
|
4574
5012
|
}
|
|
4575
5013
|
async function startWatch(graph, opts) {
|
|
4576
5014
|
const debounceMs = opts.debounceMs ?? 1e3;
|
|
5015
|
+
const projectName = opts.project ?? DEFAULT_PROJECT;
|
|
4577
5016
|
await loadGraphFromDisk(graph, opts.outPath);
|
|
4578
|
-
const
|
|
4579
|
-
const
|
|
5017
|
+
const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
|
|
5018
|
+
const policyFilePath = import_node_path34.default.join(opts.scanPath, "policy.json");
|
|
5019
|
+
const policyViolationsPath = import_node_path34.default.join(import_node_path34.default.dirname(opts.outPath), "policy-violations.ndjson");
|
|
4580
5020
|
let policies = [];
|
|
4581
5021
|
try {
|
|
4582
5022
|
policies = await loadPolicyFile(policyFilePath);
|
|
@@ -4586,7 +5026,7 @@ async function startWatch(graph, opts) {
|
|
|
4586
5026
|
} catch (err) {
|
|
4587
5027
|
console.warn(`policies: failed to load ${policyFilePath} \u2014 ${err.message}`);
|
|
4588
5028
|
}
|
|
4589
|
-
const policyLog = new PolicyViolationsLog(policyViolationsPath);
|
|
5029
|
+
const policyLog = new PolicyViolationsLog(policyViolationsPath, projectName);
|
|
4590
5030
|
const onPolicyTrigger = async (g) => {
|
|
4591
5031
|
if (policies.length === 0) return;
|
|
4592
5032
|
try {
|
|
@@ -4596,20 +5036,36 @@ async function startWatch(graph, opts) {
|
|
|
4596
5036
|
console.warn(`policies: evaluation failed \u2014 ${err.message}`);
|
|
4597
5037
|
}
|
|
4598
5038
|
};
|
|
4599
|
-
const initial = await runExtractPhases(
|
|
5039
|
+
const initial = await runExtractPhases(
|
|
5040
|
+
graph,
|
|
5041
|
+
opts.scanPath,
|
|
5042
|
+
new Set(ALL_PHASES),
|
|
5043
|
+
projectName
|
|
5044
|
+
);
|
|
4600
5045
|
console.log(
|
|
4601
5046
|
`extract: ${initial.nodesAdded} new nodes, ${initial.edgesAdded} new edges (graph total ${graph.order}/${graph.size})`
|
|
4602
5047
|
);
|
|
5048
|
+
emitNeatEvent({
|
|
5049
|
+
type: "extraction-complete",
|
|
5050
|
+
project: projectName,
|
|
5051
|
+
payload: {
|
|
5052
|
+
project: projectName,
|
|
5053
|
+
fileCount: 0,
|
|
5054
|
+
nodesAdded: initial.nodesAdded,
|
|
5055
|
+
edgesAdded: initial.edgesAdded
|
|
5056
|
+
}
|
|
5057
|
+
});
|
|
4603
5058
|
await onPolicyTrigger(graph);
|
|
4604
5059
|
const stopPersist = startPersistLoop(graph, opts.outPath);
|
|
4605
5060
|
const stopStaleness = startStalenessLoop(graph, {
|
|
4606
5061
|
staleEventsPath: opts.staleEventsPath,
|
|
5062
|
+
project: projectName,
|
|
4607
5063
|
onPolicyTrigger
|
|
4608
5064
|
});
|
|
4609
5065
|
const host = opts.host ?? "0.0.0.0";
|
|
4610
5066
|
const port = opts.port ?? 8080;
|
|
4611
5067
|
const otelPort = opts.otelPort ?? 4318;
|
|
4612
|
-
const cachePath = opts.embeddingsCachePath ??
|
|
5068
|
+
const cachePath = opts.embeddingsCachePath ?? import_node_path34.default.join(import_node_path34.default.dirname(opts.outPath), "embeddings.json");
|
|
4613
5069
|
let searchIndex;
|
|
4614
5070
|
try {
|
|
4615
5071
|
searchIndex = await buildSearchIndex(graph, { cachePath });
|
|
@@ -4619,7 +5075,6 @@ async function startWatch(graph, opts) {
|
|
|
4619
5075
|
`semantic_search: index build failed (${err.message}); falling back to inline substring`
|
|
4620
5076
|
);
|
|
4621
5077
|
}
|
|
4622
|
-
const projectName = opts.project ?? DEFAULT_PROJECT;
|
|
4623
5078
|
const registry = new Projects();
|
|
4624
5079
|
registry.set(projectName, {
|
|
4625
5080
|
graph,
|
|
@@ -4628,7 +5083,7 @@ async function startWatch(graph, opts) {
|
|
|
4628
5083
|
// Paths are derived from the explicit options the watch caller passes
|
|
4629
5084
|
// — pathsForProject is only used to fill in the embeddings/snapshot
|
|
4630
5085
|
// fields so the registry shape is complete.
|
|
4631
|
-
...pathsForProject(projectName,
|
|
5086
|
+
...pathsForProject(projectName, import_node_path34.default.dirname(opts.outPath)),
|
|
4632
5087
|
snapshotPath: opts.outPath,
|
|
4633
5088
|
errorsPath: opts.errorsPath,
|
|
4634
5089
|
staleEventsPath: opts.staleEventsPath
|
|
@@ -4644,6 +5099,7 @@ async function startWatch(graph, opts) {
|
|
|
4644
5099
|
const onSpan = makeSpanHandler({
|
|
4645
5100
|
graph,
|
|
4646
5101
|
errorsPath: opts.errorsPath,
|
|
5102
|
+
project: projectName,
|
|
4647
5103
|
writeErrorEventInline: false,
|
|
4648
5104
|
onPolicyTrigger
|
|
4649
5105
|
});
|
|
@@ -4657,6 +5113,7 @@ async function startWatch(graph, opts) {
|
|
|
4657
5113
|
const onSpanGrpc = makeSpanHandler({
|
|
4658
5114
|
graph,
|
|
4659
5115
|
errorsPath: opts.errorsPath,
|
|
5116
|
+
project: projectName,
|
|
4660
5117
|
onPolicyTrigger
|
|
4661
5118
|
});
|
|
4662
5119
|
const r = await startOtelGrpcReceiver({ onSpan: onSpanGrpc, host, port: grpcPort });
|
|
@@ -4676,10 +5133,20 @@ async function startWatch(graph, opts) {
|
|
|
4676
5133
|
try {
|
|
4677
5134
|
let retired = 0;
|
|
4678
5135
|
for (const p of paths) retired += retireEdgesByFile(graph, p);
|
|
4679
|
-
const result = await runExtractPhases(graph, opts.scanPath, phases);
|
|
5136
|
+
const result = await runExtractPhases(graph, opts.scanPath, phases, projectName);
|
|
4680
5137
|
console.log(
|
|
4681
5138
|
`[watch] re-extract phases=${result.phases.join(",")} retired=${retired} +${result.nodesAdded}n/+${result.edgesAdded}e in ${result.durationMs}ms`
|
|
4682
5139
|
);
|
|
5140
|
+
emitNeatEvent({
|
|
5141
|
+
type: "extraction-complete",
|
|
5142
|
+
project: projectName,
|
|
5143
|
+
payload: {
|
|
5144
|
+
project: projectName,
|
|
5145
|
+
fileCount: paths.size,
|
|
5146
|
+
nodesAdded: result.nodesAdded,
|
|
5147
|
+
edgesAdded: result.edgesAdded
|
|
5148
|
+
}
|
|
5149
|
+
});
|
|
4683
5150
|
if (searchIndex) {
|
|
4684
5151
|
try {
|
|
4685
5152
|
await searchIndex.refresh(graph);
|
|
@@ -4701,9 +5168,9 @@ async function startWatch(graph, opts) {
|
|
|
4701
5168
|
};
|
|
4702
5169
|
const onPath = (absPath) => {
|
|
4703
5170
|
if (shouldIgnore(absPath)) return;
|
|
4704
|
-
const rel =
|
|
5171
|
+
const rel = import_node_path34.default.relative(opts.scanPath, absPath);
|
|
4705
5172
|
if (!rel || rel.startsWith("..")) return;
|
|
4706
|
-
pendingPaths.add(rel.split(
|
|
5173
|
+
pendingPaths.add(rel.split(import_node_path34.default.sep).join("/"));
|
|
4707
5174
|
const phases = classifyChange(rel);
|
|
4708
5175
|
if (phases.size === 0) {
|
|
4709
5176
|
for (const p of ALL_PHASES) pending.add(p);
|
|
@@ -4738,6 +5205,7 @@ async function startWatch(graph, opts) {
|
|
|
4738
5205
|
await watcher.close();
|
|
4739
5206
|
stopStaleness();
|
|
4740
5207
|
stopPersist();
|
|
5208
|
+
detachEventBus();
|
|
4741
5209
|
await api.close();
|
|
4742
5210
|
await otelHttp.close();
|
|
4743
5211
|
if (grpcReceiver) await grpcReceiver.stop();
|
|
@@ -4745,192 +5213,37 @@ async function startWatch(graph, opts) {
|
|
|
4745
5213
|
return { api, stop };
|
|
4746
5214
|
}
|
|
4747
5215
|
|
|
4748
|
-
// src/
|
|
5216
|
+
// src/installers/index.ts
|
|
4749
5217
|
init_cjs_shims();
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
var
|
|
4754
|
-
var
|
|
4755
|
-
var
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
5218
|
+
|
|
5219
|
+
// src/installers/javascript.ts
|
|
5220
|
+
init_cjs_shims();
|
|
5221
|
+
var import_node_fs20 = require("fs");
|
|
5222
|
+
var import_node_path35 = __toESM(require("path"), 1);
|
|
5223
|
+
var SDK_PACKAGES = [
|
|
5224
|
+
{ name: "@opentelemetry/api", version: "^1.9.0" },
|
|
5225
|
+
{ name: "@opentelemetry/sdk-node", version: "^0.57.0" },
|
|
5226
|
+
{ name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
|
|
5227
|
+
];
|
|
5228
|
+
var AUTO_INSTRUMENT_REQUIRE = "--require @opentelemetry/auto-instrumentations-node/register";
|
|
5229
|
+
var OTEL_ENV = {
|
|
5230
|
+
// null target — NEAT does not write `.env` itself; the user sets the env
|
|
5231
|
+
// var in their orchestration layer.
|
|
5232
|
+
file: null,
|
|
5233
|
+
key: "OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
5234
|
+
value: "http://localhost:4318"
|
|
5235
|
+
};
|
|
5236
|
+
async function readPackageJson(serviceDir) {
|
|
4769
5237
|
try {
|
|
4770
|
-
|
|
5238
|
+
const raw = await import_node_fs20.promises.readFile(import_node_path35.default.join(serviceDir, "package.json"), "utf8");
|
|
5239
|
+
return JSON.parse(raw);
|
|
4771
5240
|
} catch {
|
|
4772
|
-
return
|
|
5241
|
+
return null;
|
|
4773
5242
|
}
|
|
4774
5243
|
}
|
|
4775
|
-
async function
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
const fd = await import_node_fs18.promises.open(tmp, "w");
|
|
4779
|
-
try {
|
|
4780
|
-
await fd.writeFile(contents, "utf8");
|
|
4781
|
-
await fd.sync();
|
|
4782
|
-
} finally {
|
|
4783
|
-
await fd.close();
|
|
4784
|
-
}
|
|
4785
|
-
await import_node_fs18.promises.rename(tmp, target);
|
|
4786
|
-
}
|
|
4787
|
-
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
4788
|
-
const deadline = Date.now() + timeoutMs;
|
|
4789
|
-
await import_node_fs18.promises.mkdir(import_node_path33.default.dirname(lockPath), { recursive: true });
|
|
4790
|
-
while (true) {
|
|
4791
|
-
try {
|
|
4792
|
-
const fd = await import_node_fs18.promises.open(lockPath, "wx");
|
|
4793
|
-
await fd.close();
|
|
4794
|
-
return;
|
|
4795
|
-
} catch (err) {
|
|
4796
|
-
const code = err.code;
|
|
4797
|
-
if (code !== "EEXIST") throw err;
|
|
4798
|
-
if (Date.now() >= deadline) {
|
|
4799
|
-
throw new Error(
|
|
4800
|
-
`neat registry: timed out after ${timeoutMs}ms waiting for ${lockPath}. Another neat process is holding the lock; if no such process exists, remove the file by hand.`
|
|
4801
|
-
);
|
|
4802
|
-
}
|
|
4803
|
-
await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
|
|
4804
|
-
}
|
|
4805
|
-
}
|
|
4806
|
-
}
|
|
4807
|
-
async function releaseLock(lockPath) {
|
|
4808
|
-
await import_node_fs18.promises.unlink(lockPath).catch(() => {
|
|
4809
|
-
});
|
|
4810
|
-
}
|
|
4811
|
-
async function withLock(fn) {
|
|
4812
|
-
const lock = registryLockPath();
|
|
4813
|
-
await acquireLock(lock);
|
|
4814
|
-
try {
|
|
4815
|
-
return await fn();
|
|
4816
|
-
} finally {
|
|
4817
|
-
await releaseLock(lock);
|
|
4818
|
-
}
|
|
4819
|
-
}
|
|
4820
|
-
async function readRegistry() {
|
|
4821
|
-
const file = registryPath();
|
|
4822
|
-
let raw;
|
|
4823
|
-
try {
|
|
4824
|
-
raw = await import_node_fs18.promises.readFile(file, "utf8");
|
|
4825
|
-
} catch (err) {
|
|
4826
|
-
if (err.code === "ENOENT") {
|
|
4827
|
-
return { version: 1, projects: [] };
|
|
4828
|
-
}
|
|
4829
|
-
throw err;
|
|
4830
|
-
}
|
|
4831
|
-
const parsed = JSON.parse(raw);
|
|
4832
|
-
return import_types20.RegistryFileSchema.parse(parsed);
|
|
4833
|
-
}
|
|
4834
|
-
async function writeRegistry(reg) {
|
|
4835
|
-
const validated = import_types20.RegistryFileSchema.parse(reg);
|
|
4836
|
-
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
4837
|
-
}
|
|
4838
|
-
var ProjectNameCollisionError = class extends Error {
|
|
4839
|
-
projectName;
|
|
4840
|
-
constructor(name) {
|
|
4841
|
-
super(`neat registry: a project named "${name}" is already registered`);
|
|
4842
|
-
this.name = "ProjectNameCollisionError";
|
|
4843
|
-
this.projectName = name;
|
|
4844
|
-
}
|
|
4845
|
-
};
|
|
4846
|
-
async function addProject(opts) {
|
|
4847
|
-
const resolvedPath = await normalizeProjectPath(opts.path);
|
|
4848
|
-
return withLock(async () => {
|
|
4849
|
-
const reg = await readRegistry();
|
|
4850
|
-
const byName = reg.projects.find((p) => p.name === opts.name);
|
|
4851
|
-
const byPath = reg.projects.find((p) => p.path === resolvedPath);
|
|
4852
|
-
if (byName && byName.path !== resolvedPath) {
|
|
4853
|
-
throw new ProjectNameCollisionError(opts.name);
|
|
4854
|
-
}
|
|
4855
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4856
|
-
if (byName && byName.path === resolvedPath) {
|
|
4857
|
-
byName.lastSeenAt = now;
|
|
4858
|
-
if (opts.languages) byName.languages = opts.languages;
|
|
4859
|
-
if (opts.status) byName.status = opts.status;
|
|
4860
|
-
await writeRegistry(reg);
|
|
4861
|
-
return byName;
|
|
4862
|
-
}
|
|
4863
|
-
if (byPath && byPath.name !== opts.name) {
|
|
4864
|
-
throw new ProjectNameCollisionError(byPath.name);
|
|
4865
|
-
}
|
|
4866
|
-
const entry2 = {
|
|
4867
|
-
name: opts.name,
|
|
4868
|
-
path: resolvedPath,
|
|
4869
|
-
registeredAt: now,
|
|
4870
|
-
languages: opts.languages ?? [],
|
|
4871
|
-
status: opts.status ?? "active"
|
|
4872
|
-
};
|
|
4873
|
-
reg.projects.push(entry2);
|
|
4874
|
-
await writeRegistry(reg);
|
|
4875
|
-
return entry2;
|
|
4876
|
-
});
|
|
4877
|
-
}
|
|
4878
|
-
async function listProjects() {
|
|
4879
|
-
const reg = await readRegistry();
|
|
4880
|
-
return reg.projects;
|
|
4881
|
-
}
|
|
4882
|
-
async function setStatus(name, status2) {
|
|
4883
|
-
return withLock(async () => {
|
|
4884
|
-
const reg = await readRegistry();
|
|
4885
|
-
const entry2 = reg.projects.find((p) => p.name === name);
|
|
4886
|
-
if (!entry2) throw new Error(`neat registry: no project named "${name}"`);
|
|
4887
|
-
entry2.status = status2;
|
|
4888
|
-
await writeRegistry(reg);
|
|
4889
|
-
return entry2;
|
|
4890
|
-
});
|
|
4891
|
-
}
|
|
4892
|
-
async function removeProject(name) {
|
|
4893
|
-
return withLock(async () => {
|
|
4894
|
-
const reg = await readRegistry();
|
|
4895
|
-
const idx = reg.projects.findIndex((p) => p.name === name);
|
|
4896
|
-
if (idx < 0) return void 0;
|
|
4897
|
-
const [removed] = reg.projects.splice(idx, 1);
|
|
4898
|
-
await writeRegistry(reg);
|
|
4899
|
-
return removed;
|
|
4900
|
-
});
|
|
4901
|
-
}
|
|
4902
|
-
|
|
4903
|
-
// src/installers/index.ts
|
|
4904
|
-
init_cjs_shims();
|
|
4905
|
-
|
|
4906
|
-
// src/installers/javascript.ts
|
|
4907
|
-
init_cjs_shims();
|
|
4908
|
-
var import_node_fs19 = require("fs");
|
|
4909
|
-
var import_node_path34 = __toESM(require("path"), 1);
|
|
4910
|
-
var SDK_PACKAGES = [
|
|
4911
|
-
{ name: "@opentelemetry/api", version: "^1.9.0" },
|
|
4912
|
-
{ name: "@opentelemetry/sdk-node", version: "^0.57.0" },
|
|
4913
|
-
{ name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
|
|
4914
|
-
];
|
|
4915
|
-
var AUTO_INSTRUMENT_REQUIRE = "--require @opentelemetry/auto-instrumentations-node/register";
|
|
4916
|
-
var OTEL_ENV = {
|
|
4917
|
-
// null target — NEAT does not write `.env` itself; the user sets the env
|
|
4918
|
-
// var in their orchestration layer.
|
|
4919
|
-
file: null,
|
|
4920
|
-
key: "OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
4921
|
-
value: "http://localhost:4318"
|
|
4922
|
-
};
|
|
4923
|
-
async function readPackageJson(serviceDir) {
|
|
4924
|
-
try {
|
|
4925
|
-
const raw = await import_node_fs19.promises.readFile(import_node_path34.default.join(serviceDir, "package.json"), "utf8");
|
|
4926
|
-
return JSON.parse(raw);
|
|
4927
|
-
} catch {
|
|
4928
|
-
return null;
|
|
4929
|
-
}
|
|
4930
|
-
}
|
|
4931
|
-
async function detect(serviceDir) {
|
|
4932
|
-
const pkg = await readPackageJson(serviceDir);
|
|
4933
|
-
return pkg !== null && typeof pkg.name === "string";
|
|
5244
|
+
async function detect(serviceDir) {
|
|
5245
|
+
const pkg = await readPackageJson(serviceDir);
|
|
5246
|
+
return pkg !== null && typeof pkg.name === "string";
|
|
4934
5247
|
}
|
|
4935
5248
|
function rewriteStartScript(start) {
|
|
4936
5249
|
if (start.includes(AUTO_INSTRUMENT_REQUIRE)) return start;
|
|
@@ -4941,7 +5254,7 @@ function rewriteStartScript(start) {
|
|
|
4941
5254
|
}
|
|
4942
5255
|
async function plan(serviceDir) {
|
|
4943
5256
|
const pkg = await readPackageJson(serviceDir);
|
|
4944
|
-
const manifestPath =
|
|
5257
|
+
const manifestPath = import_node_path35.default.join(serviceDir, "package.json");
|
|
4945
5258
|
const empty = {
|
|
4946
5259
|
language: "javascript",
|
|
4947
5260
|
serviceDir,
|
|
@@ -4988,7 +5301,7 @@ async function apply(installPlan) {
|
|
|
4988
5301
|
const originals = /* @__PURE__ */ new Map();
|
|
4989
5302
|
for (const file of touched) {
|
|
4990
5303
|
try {
|
|
4991
|
-
originals.set(file, await
|
|
5304
|
+
originals.set(file, await import_node_fs20.promises.readFile(file, "utf8"));
|
|
4992
5305
|
} catch {
|
|
4993
5306
|
}
|
|
4994
5307
|
}
|
|
@@ -5014,8 +5327,8 @@ async function apply(installPlan) {
|
|
|
5014
5327
|
}
|
|
5015
5328
|
const newRaw = JSON.stringify(pkg, null, 2) + "\n";
|
|
5016
5329
|
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
5017
|
-
await
|
|
5018
|
-
await
|
|
5330
|
+
await import_node_fs20.promises.writeFile(tmp, newRaw, "utf8");
|
|
5331
|
+
await import_node_fs20.promises.rename(tmp, file);
|
|
5019
5332
|
}
|
|
5020
5333
|
} catch (err) {
|
|
5021
5334
|
await rollback(installPlan, originals);
|
|
@@ -5026,7 +5339,7 @@ async function rollback(installPlan, originals) {
|
|
|
5026
5339
|
const restored = [];
|
|
5027
5340
|
for (const [file, raw] of originals.entries()) {
|
|
5028
5341
|
try {
|
|
5029
|
-
await
|
|
5342
|
+
await import_node_fs20.promises.writeFile(file, raw, "utf8");
|
|
5030
5343
|
restored.push(file);
|
|
5031
5344
|
} catch {
|
|
5032
5345
|
}
|
|
@@ -5040,8 +5353,8 @@ async function rollback(installPlan, originals) {
|
|
|
5040
5353
|
...restored.map((f) => `restored: ${f}`),
|
|
5041
5354
|
""
|
|
5042
5355
|
];
|
|
5043
|
-
const rollbackPath =
|
|
5044
|
-
await
|
|
5356
|
+
const rollbackPath = import_node_path35.default.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
5357
|
+
await import_node_fs20.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
|
|
5045
5358
|
}
|
|
5046
5359
|
var javascriptInstaller = {
|
|
5047
5360
|
name: "javascript",
|
|
@@ -5052,8 +5365,8 @@ var javascriptInstaller = {
|
|
|
5052
5365
|
|
|
5053
5366
|
// src/installers/python.ts
|
|
5054
5367
|
init_cjs_shims();
|
|
5055
|
-
var
|
|
5056
|
-
var
|
|
5368
|
+
var import_node_fs21 = require("fs");
|
|
5369
|
+
var import_node_path36 = __toESM(require("path"), 1);
|
|
5057
5370
|
var SDK_PACKAGES2 = [
|
|
5058
5371
|
{ name: "opentelemetry-distro", version: ">=0.49b0" },
|
|
5059
5372
|
{ name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
|
|
@@ -5065,7 +5378,7 @@ var OTEL_ENV2 = {
|
|
|
5065
5378
|
};
|
|
5066
5379
|
async function exists2(p) {
|
|
5067
5380
|
try {
|
|
5068
|
-
await
|
|
5381
|
+
await import_node_fs21.promises.stat(p);
|
|
5069
5382
|
return true;
|
|
5070
5383
|
} catch {
|
|
5071
5384
|
return false;
|
|
@@ -5074,7 +5387,7 @@ async function exists2(p) {
|
|
|
5074
5387
|
async function detect2(serviceDir) {
|
|
5075
5388
|
const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
|
|
5076
5389
|
for (const m of markers) {
|
|
5077
|
-
if (await exists2(
|
|
5390
|
+
if (await exists2(import_node_path36.default.join(serviceDir, m))) return true;
|
|
5078
5391
|
}
|
|
5079
5392
|
return false;
|
|
5080
5393
|
}
|
|
@@ -5084,9 +5397,9 @@ function reqPackageName(line) {
|
|
|
5084
5397
|
return head.replace(/[<>=!~].*$/, "").toLowerCase();
|
|
5085
5398
|
}
|
|
5086
5399
|
async function planRequirementsTxtEdits(serviceDir) {
|
|
5087
|
-
const file =
|
|
5400
|
+
const file = import_node_path36.default.join(serviceDir, "requirements.txt");
|
|
5088
5401
|
if (!await exists2(file)) return null;
|
|
5089
|
-
const raw = await
|
|
5402
|
+
const raw = await import_node_fs21.promises.readFile(file, "utf8");
|
|
5090
5403
|
const presentNames = new Set(
|
|
5091
5404
|
raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
|
|
5092
5405
|
);
|
|
@@ -5094,9 +5407,9 @@ async function planRequirementsTxtEdits(serviceDir) {
|
|
|
5094
5407
|
return { manifest: file, missing: [...missing] };
|
|
5095
5408
|
}
|
|
5096
5409
|
async function planProcfileEdits(serviceDir) {
|
|
5097
|
-
const procfile =
|
|
5410
|
+
const procfile = import_node_path36.default.join(serviceDir, "Procfile");
|
|
5098
5411
|
if (!await exists2(procfile)) return [];
|
|
5099
|
-
const raw = await
|
|
5412
|
+
const raw = await import_node_fs21.promises.readFile(procfile, "utf8");
|
|
5100
5413
|
const edits = [];
|
|
5101
5414
|
for (const line of raw.split(/\r?\n/)) {
|
|
5102
5415
|
if (line.length === 0) continue;
|
|
@@ -5148,8 +5461,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
|
|
|
5148
5461
|
const next = `${original}${trailing}${newlines.join("\n")}
|
|
5149
5462
|
`;
|
|
5150
5463
|
const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
|
|
5151
|
-
await
|
|
5152
|
-
await
|
|
5464
|
+
await import_node_fs21.promises.writeFile(tmp, next, "utf8");
|
|
5465
|
+
await import_node_fs21.promises.rename(tmp, manifest);
|
|
5153
5466
|
}
|
|
5154
5467
|
async function applyProcfile(procfile, edits, original) {
|
|
5155
5468
|
let next = original;
|
|
@@ -5158,8 +5471,8 @@ async function applyProcfile(procfile, edits, original) {
|
|
|
5158
5471
|
next = next.replace(e.before, e.after);
|
|
5159
5472
|
}
|
|
5160
5473
|
const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
|
|
5161
|
-
await
|
|
5162
|
-
await
|
|
5474
|
+
await import_node_fs21.promises.writeFile(tmp, next, "utf8");
|
|
5475
|
+
await import_node_fs21.promises.rename(tmp, procfile);
|
|
5163
5476
|
}
|
|
5164
5477
|
async function apply2(installPlan) {
|
|
5165
5478
|
const touched = /* @__PURE__ */ new Set();
|
|
@@ -5169,7 +5482,7 @@ async function apply2(installPlan) {
|
|
|
5169
5482
|
const originals = /* @__PURE__ */ new Map();
|
|
5170
5483
|
for (const file of touched) {
|
|
5171
5484
|
try {
|
|
5172
|
-
originals.set(file, await
|
|
5485
|
+
originals.set(file, await import_node_fs21.promises.readFile(file, "utf8"));
|
|
5173
5486
|
} catch {
|
|
5174
5487
|
}
|
|
5175
5488
|
}
|
|
@@ -5179,7 +5492,7 @@ async function apply2(installPlan) {
|
|
|
5179
5492
|
if (raw === void 0) {
|
|
5180
5493
|
throw new Error(`python installer: cannot read ${file} during apply`);
|
|
5181
5494
|
}
|
|
5182
|
-
const base =
|
|
5495
|
+
const base = import_node_path36.default.basename(file);
|
|
5183
5496
|
if (base === "requirements.txt") {
|
|
5184
5497
|
const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
|
|
5185
5498
|
if (edits.length > 0) await applyRequirementsTxt(file, edits, raw);
|
|
@@ -5197,7 +5510,7 @@ async function rollback2(installPlan, originals) {
|
|
|
5197
5510
|
const restored = [];
|
|
5198
5511
|
for (const [file, raw] of originals.entries()) {
|
|
5199
5512
|
try {
|
|
5200
|
-
await
|
|
5513
|
+
await import_node_fs21.promises.writeFile(file, raw, "utf8");
|
|
5201
5514
|
restored.push(file);
|
|
5202
5515
|
} catch {
|
|
5203
5516
|
}
|
|
@@ -5211,8 +5524,8 @@ async function rollback2(installPlan, originals) {
|
|
|
5211
5524
|
...restored.map((f) => `restored: ${f}`),
|
|
5212
5525
|
""
|
|
5213
5526
|
];
|
|
5214
|
-
const rollbackPath =
|
|
5215
|
-
await
|
|
5527
|
+
const rollbackPath = import_node_path36.default.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
5528
|
+
await import_node_fs21.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
|
|
5216
5529
|
}
|
|
5217
5530
|
var pythonInstaller = {
|
|
5218
5531
|
name: "python",
|
|
@@ -5300,11 +5613,457 @@ function renderPatch(sections) {
|
|
|
5300
5613
|
return lines.join("\n");
|
|
5301
5614
|
}
|
|
5302
5615
|
|
|
5616
|
+
// src/cli-client.ts
|
|
5617
|
+
init_cjs_shims();
|
|
5618
|
+
var import_types21 = require("@neat.is/types");
|
|
5619
|
+
var HttpError = class extends Error {
|
|
5620
|
+
constructor(status2, message, responseBody = "") {
|
|
5621
|
+
super(message);
|
|
5622
|
+
this.status = status2;
|
|
5623
|
+
this.responseBody = responseBody;
|
|
5624
|
+
this.name = "HttpError";
|
|
5625
|
+
}
|
|
5626
|
+
status;
|
|
5627
|
+
responseBody;
|
|
5628
|
+
};
|
|
5629
|
+
var TransportError = class extends Error {
|
|
5630
|
+
constructor(message) {
|
|
5631
|
+
super(message);
|
|
5632
|
+
this.name = "TransportError";
|
|
5633
|
+
}
|
|
5634
|
+
};
|
|
5635
|
+
function createHttpClient(baseUrl) {
|
|
5636
|
+
const root = baseUrl.replace(/\/$/, "");
|
|
5637
|
+
return {
|
|
5638
|
+
async get(path38) {
|
|
5639
|
+
let res;
|
|
5640
|
+
try {
|
|
5641
|
+
res = await fetch(`${root}${path38}`);
|
|
5642
|
+
} catch (err) {
|
|
5643
|
+
throw new TransportError(
|
|
5644
|
+
`cannot reach neat-core at ${root}: ${err.message}`
|
|
5645
|
+
);
|
|
5646
|
+
}
|
|
5647
|
+
if (!res.ok) {
|
|
5648
|
+
const body = await res.text().catch(() => "");
|
|
5649
|
+
throw new HttpError(
|
|
5650
|
+
res.status,
|
|
5651
|
+
`${res.status} ${res.statusText} on GET ${path38}: ${body}`,
|
|
5652
|
+
body
|
|
5653
|
+
);
|
|
5654
|
+
}
|
|
5655
|
+
return await res.json();
|
|
5656
|
+
},
|
|
5657
|
+
async post(path38, body) {
|
|
5658
|
+
let res;
|
|
5659
|
+
try {
|
|
5660
|
+
res = await fetch(`${root}${path38}`, {
|
|
5661
|
+
method: "POST",
|
|
5662
|
+
headers: { "content-type": "application/json" },
|
|
5663
|
+
body: JSON.stringify(body)
|
|
5664
|
+
});
|
|
5665
|
+
} catch (err) {
|
|
5666
|
+
throw new TransportError(
|
|
5667
|
+
`cannot reach neat-core at ${root}: ${err.message}`
|
|
5668
|
+
);
|
|
5669
|
+
}
|
|
5670
|
+
if (!res.ok) {
|
|
5671
|
+
const text = await res.text().catch(() => "");
|
|
5672
|
+
throw new HttpError(
|
|
5673
|
+
res.status,
|
|
5674
|
+
`${res.status} ${res.statusText} on POST ${path38}: ${text}`,
|
|
5675
|
+
text
|
|
5676
|
+
);
|
|
5677
|
+
}
|
|
5678
|
+
return await res.json();
|
|
5679
|
+
}
|
|
5680
|
+
};
|
|
5681
|
+
}
|
|
5682
|
+
function projectPath(project, suffix) {
|
|
5683
|
+
if (!project) return suffix;
|
|
5684
|
+
return `/projects/${encodeURIComponent(project)}${suffix}`;
|
|
5685
|
+
}
|
|
5686
|
+
async function runRootCause(client, input) {
|
|
5687
|
+
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
5688
|
+
const path38 = projectPath(
|
|
5689
|
+
input.project,
|
|
5690
|
+
`/traverse/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
5691
|
+
);
|
|
5692
|
+
try {
|
|
5693
|
+
const result = await client.get(path38);
|
|
5694
|
+
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
5695
|
+
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
5696
|
+
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
5697
|
+
const blockLines = [
|
|
5698
|
+
`Traversal path: ${arrowPath}`,
|
|
5699
|
+
`Edge provenances: ${provenances}`
|
|
5700
|
+
];
|
|
5701
|
+
if (result.fixRecommendation) blockLines.push(`Recommended fix: ${result.fixRecommendation}`);
|
|
5702
|
+
return {
|
|
5703
|
+
summary,
|
|
5704
|
+
block: blockLines.join("\n"),
|
|
5705
|
+
confidence: result.confidence,
|
|
5706
|
+
provenance: result.edgeProvenances.length ? result.edgeProvenances : void 0
|
|
5707
|
+
};
|
|
5708
|
+
} catch (err) {
|
|
5709
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
5710
|
+
return {
|
|
5711
|
+
summary: `No root cause found for ${input.errorNode}. The node may be healthy, or it may not exist in the graph.`
|
|
5712
|
+
};
|
|
5713
|
+
}
|
|
5714
|
+
throw err;
|
|
5715
|
+
}
|
|
5716
|
+
}
|
|
5717
|
+
async function runBlastRadius(client, input) {
|
|
5718
|
+
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
5719
|
+
const path38 = projectPath(
|
|
5720
|
+
input.project,
|
|
5721
|
+
`/traverse/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
5722
|
+
);
|
|
5723
|
+
try {
|
|
5724
|
+
const result = await client.get(path38);
|
|
5725
|
+
if (result.totalAffected === 0) {
|
|
5726
|
+
return {
|
|
5727
|
+
summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
|
|
5728
|
+
};
|
|
5729
|
+
}
|
|
5730
|
+
const sorted = [...result.affectedNodes].sort(
|
|
5731
|
+
(a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
|
|
5732
|
+
);
|
|
5733
|
+
const blockLines = sorted.map(formatBlastEntry);
|
|
5734
|
+
const minConfidence = sorted.reduce(
|
|
5735
|
+
(m, n) => Math.min(m, n.confidence),
|
|
5736
|
+
Number.POSITIVE_INFINITY
|
|
5737
|
+
);
|
|
5738
|
+
const provenances = [...new Set(sorted.map((n) => n.edgeProvenance))];
|
|
5739
|
+
return {
|
|
5740
|
+
summary: `Blast radius for ${result.origin}: ${result.totalAffected} affected node${result.totalAffected === 1 ? "" : "s"} reachable downstream.`,
|
|
5741
|
+
block: blockLines.join("\n"),
|
|
5742
|
+
confidence: Number.isFinite(minConfidence) ? minConfidence : void 0,
|
|
5743
|
+
provenance: provenances.length ? provenances : void 0
|
|
5744
|
+
};
|
|
5745
|
+
} catch (err) {
|
|
5746
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
5747
|
+
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
5748
|
+
}
|
|
5749
|
+
throw err;
|
|
5750
|
+
}
|
|
5751
|
+
}
|
|
5752
|
+
function formatBlastEntry(n) {
|
|
5753
|
+
const tag = n.edgeProvenance === import_types21.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
5754
|
+
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
5755
|
+
}
|
|
5756
|
+
async function runDependencies(client, input) {
|
|
5757
|
+
const depth = input.depth ?? 3;
|
|
5758
|
+
const path38 = projectPath(
|
|
5759
|
+
input.project,
|
|
5760
|
+
`/graph/node/${encodeURIComponent(input.nodeId)}/dependencies?depth=${depth}`
|
|
5761
|
+
);
|
|
5762
|
+
try {
|
|
5763
|
+
const result = await client.get(path38);
|
|
5764
|
+
if (result.total === 0) {
|
|
5765
|
+
return {
|
|
5766
|
+
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
5767
|
+
};
|
|
5768
|
+
}
|
|
5769
|
+
const byDistance = /* @__PURE__ */ new Map();
|
|
5770
|
+
for (const dep of result.dependencies) {
|
|
5771
|
+
const ring = byDistance.get(dep.distance) ?? [];
|
|
5772
|
+
ring.push(dep);
|
|
5773
|
+
byDistance.set(dep.distance, ring);
|
|
5774
|
+
}
|
|
5775
|
+
const blockLines = [];
|
|
5776
|
+
for (const distance of [...byDistance.keys()].sort((a, b) => a - b)) {
|
|
5777
|
+
const label = distance === 1 ? "Direct (distance 1)" : `Distance ${distance}`;
|
|
5778
|
+
blockLines.push(`${label}:`);
|
|
5779
|
+
for (const dep of byDistance.get(distance)) {
|
|
5780
|
+
blockLines.push(` \u2022 ${dep.nodeId} \u2014 ${dep.edgeType} (${dep.provenance})`);
|
|
5781
|
+
}
|
|
5782
|
+
}
|
|
5783
|
+
const provenances = [...new Set(result.dependencies.map((d) => d.provenance))];
|
|
5784
|
+
const directCount = byDistance.get(1)?.length ?? 0;
|
|
5785
|
+
const summary = depth === 1 ? `${input.nodeId} has ${directCount} direct dependenc${directCount === 1 ? "y" : "ies"}.` : `${input.nodeId} has ${result.total} dependenc${result.total === 1 ? "y" : "ies"} reachable to depth ${depth} (${directCount} direct).`;
|
|
5786
|
+
return { summary, block: blockLines.join("\n"), provenance: provenances };
|
|
5787
|
+
} catch (err) {
|
|
5788
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
5789
|
+
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
5790
|
+
}
|
|
5791
|
+
throw err;
|
|
5792
|
+
}
|
|
5793
|
+
}
|
|
5794
|
+
async function runObservedDependencies(client, input) {
|
|
5795
|
+
try {
|
|
5796
|
+
const edges = await client.get(
|
|
5797
|
+
projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
|
|
5798
|
+
);
|
|
5799
|
+
const observed = edges.outbound.filter((e) => e.provenance === import_types21.Provenance.OBSERVED);
|
|
5800
|
+
if (observed.length === 0) {
|
|
5801
|
+
const hasExtracted = edges.outbound.some((e) => e.provenance === import_types21.Provenance.EXTRACTED);
|
|
5802
|
+
const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
5803
|
+
return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
|
|
5804
|
+
}
|
|
5805
|
+
const blockLines = observed.map((e) => ` \u2022 ${e.target} \u2014 ${e.type}${edgeMeta(e)}`);
|
|
5806
|
+
return {
|
|
5807
|
+
summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
5808
|
+
block: blockLines.join("\n"),
|
|
5809
|
+
provenance: import_types21.Provenance.OBSERVED
|
|
5810
|
+
};
|
|
5811
|
+
} catch (err) {
|
|
5812
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
5813
|
+
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
5814
|
+
}
|
|
5815
|
+
throw err;
|
|
5816
|
+
}
|
|
5817
|
+
}
|
|
5818
|
+
function edgeMeta(e) {
|
|
5819
|
+
const bits = [];
|
|
5820
|
+
if (e.signal) {
|
|
5821
|
+
bits.push(`spans=${e.signal.spanCount}`);
|
|
5822
|
+
if (e.signal.errorCount > 0) bits.push(`errors=${e.signal.errorCount}`);
|
|
5823
|
+
if (e.signal.lastObservedAgeMs !== void 0) {
|
|
5824
|
+
bits.push(`age=${formatDuration(e.signal.lastObservedAgeMs)}`);
|
|
5825
|
+
}
|
|
5826
|
+
} else if (e.callCount !== void 0) {
|
|
5827
|
+
bits.push(`callCount=${e.callCount}`);
|
|
5828
|
+
}
|
|
5829
|
+
if (e.lastObserved) bits.push(`lastObserved=${e.lastObserved}`);
|
|
5830
|
+
if (e.confidence !== void 0) bits.push(`confidence=${e.confidence}`);
|
|
5831
|
+
return bits.length ? ` [${bits.join(", ")}]` : "";
|
|
5832
|
+
}
|
|
5833
|
+
function formatDuration(ms) {
|
|
5834
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
5835
|
+
const s = Math.round(ms / 1e3);
|
|
5836
|
+
if (s < 60) return `${s}s`;
|
|
5837
|
+
const m = Math.round(s / 60);
|
|
5838
|
+
if (m < 60) return `${m}m`;
|
|
5839
|
+
const h = Math.round(m / 60);
|
|
5840
|
+
if (h < 48) return `${h}h`;
|
|
5841
|
+
return `${Math.round(h / 24)}d`;
|
|
5842
|
+
}
|
|
5843
|
+
async function runIncidents(client, input) {
|
|
5844
|
+
const path38 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
5845
|
+
try {
|
|
5846
|
+
const events = await client.get(path38);
|
|
5847
|
+
if (events.length === 0) {
|
|
5848
|
+
return {
|
|
5849
|
+
summary: input.nodeId ? `No incidents recorded against ${input.nodeId}.` : "No incidents recorded."
|
|
5850
|
+
};
|
|
5851
|
+
}
|
|
5852
|
+
const ordered = [...events].reverse().slice(0, input.limit ?? 20);
|
|
5853
|
+
const blockLines = [];
|
|
5854
|
+
for (const ev of ordered) {
|
|
5855
|
+
blockLines.push(` ${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`);
|
|
5856
|
+
blockLines.push(` trace=${ev.traceId} span=${ev.spanId}`);
|
|
5857
|
+
}
|
|
5858
|
+
const target = input.nodeId ?? "the project";
|
|
5859
|
+
return {
|
|
5860
|
+
summary: `${target} has ${events.length} recorded incident${events.length === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
5861
|
+
block: blockLines.join("\n"),
|
|
5862
|
+
provenance: import_types21.Provenance.OBSERVED
|
|
5863
|
+
};
|
|
5864
|
+
} catch (err) {
|
|
5865
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
5866
|
+
return { summary: `Node ${input.nodeId ?? ""} not found in the graph.` };
|
|
5867
|
+
}
|
|
5868
|
+
throw err;
|
|
5869
|
+
}
|
|
5870
|
+
}
|
|
5871
|
+
async function runSearch(client, input) {
|
|
5872
|
+
const result = await client.get(
|
|
5873
|
+
projectPath(input.project, `/search?q=${encodeURIComponent(input.query)}`)
|
|
5874
|
+
);
|
|
5875
|
+
if (result.matches.length === 0) {
|
|
5876
|
+
return { summary: `No matches for "${input.query}".` };
|
|
5877
|
+
}
|
|
5878
|
+
const provider = result.provider ?? "substring";
|
|
5879
|
+
const blockLines = [];
|
|
5880
|
+
let topScore;
|
|
5881
|
+
for (const n of result.matches) {
|
|
5882
|
+
const score = provider !== "substring" && typeof n.score === "number" ? n.score : void 0;
|
|
5883
|
+
const scoreBit = score !== void 0 ? ` [score=${score.toFixed(2)}]` : "";
|
|
5884
|
+
if (score !== void 0 && (topScore === void 0 || score > topScore)) topScore = score;
|
|
5885
|
+
blockLines.push(
|
|
5886
|
+
` \u2022 ${n.id} (${n.type}) \u2014 ${n.name ?? n.id}${scoreBit}`
|
|
5887
|
+
);
|
|
5888
|
+
}
|
|
5889
|
+
return {
|
|
5890
|
+
summary: `Found ${result.matches.length} match${result.matches.length === 1 ? "" : "es"} for "${input.query}" via ${provider} provider.`,
|
|
5891
|
+
block: blockLines.join("\n"),
|
|
5892
|
+
confidence: topScore
|
|
5893
|
+
};
|
|
5894
|
+
}
|
|
5895
|
+
async function runDiff(client, input) {
|
|
5896
|
+
const result = await client.get(
|
|
5897
|
+
projectPath(
|
|
5898
|
+
input.project,
|
|
5899
|
+
`/graph/diff?against=${encodeURIComponent(input.againstSnapshot)}`
|
|
5900
|
+
)
|
|
5901
|
+
);
|
|
5902
|
+
const total = result.added.nodes.length + result.added.edges.length + result.removed.nodes.length + result.removed.edges.length + result.changed.nodes.length + result.changed.edges.length;
|
|
5903
|
+
const baseLabel = result.base.exportedAt ?? "unknown";
|
|
5904
|
+
if (total === 0) {
|
|
5905
|
+
return {
|
|
5906
|
+
summary: `No differences between the current graph and ${input.againstSnapshot} (base exportedAt=${baseLabel}).`
|
|
5907
|
+
};
|
|
5908
|
+
}
|
|
5909
|
+
const blockLines = [
|
|
5910
|
+
` base exportedAt: ${baseLabel}`,
|
|
5911
|
+
` current exportedAt: ${result.current.exportedAt}`,
|
|
5912
|
+
""
|
|
5913
|
+
];
|
|
5914
|
+
if (result.added.nodes.length || result.added.edges.length) {
|
|
5915
|
+
blockLines.push("Added:");
|
|
5916
|
+
for (const n of result.added.nodes) blockLines.push(` + node ${n.id} (${n.type})`);
|
|
5917
|
+
for (const e of result.added.edges)
|
|
5918
|
+
blockLines.push(` + edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
|
|
5919
|
+
blockLines.push("");
|
|
5920
|
+
}
|
|
5921
|
+
if (result.removed.nodes.length || result.removed.edges.length) {
|
|
5922
|
+
blockLines.push("Removed:");
|
|
5923
|
+
for (const n of result.removed.nodes) blockLines.push(` - node ${n.id} (${n.type})`);
|
|
5924
|
+
for (const e of result.removed.edges)
|
|
5925
|
+
blockLines.push(` - edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
|
|
5926
|
+
blockLines.push("");
|
|
5927
|
+
}
|
|
5928
|
+
if (result.changed.nodes.length || result.changed.edges.length) {
|
|
5929
|
+
blockLines.push("Changed:");
|
|
5930
|
+
for (const c of result.changed.nodes) {
|
|
5931
|
+
blockLines.push(` ~ node ${c.id} \u2014 ${summariseAttrDiff(c.before, c.after)}`);
|
|
5932
|
+
}
|
|
5933
|
+
for (const c of result.changed.edges) {
|
|
5934
|
+
const provBit = c.before.provenance !== c.after.provenance ? `provenance ${c.before.provenance} \u2192 ${c.after.provenance}` : summariseAttrDiff(c.before, c.after);
|
|
5935
|
+
blockLines.push(` ~ edge ${c.id} \u2014 ${provBit}`);
|
|
5936
|
+
}
|
|
5937
|
+
}
|
|
5938
|
+
return {
|
|
5939
|
+
summary: `Diff against ${input.againstSnapshot}: ${total} change${total === 1 ? "" : "s"} between the snapshot and the live graph.`,
|
|
5940
|
+
block: blockLines.join("\n").trimEnd()
|
|
5941
|
+
};
|
|
5942
|
+
}
|
|
5943
|
+
function summariseAttrDiff(before, after) {
|
|
5944
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
5945
|
+
const changed = [];
|
|
5946
|
+
for (const k of keys) {
|
|
5947
|
+
if (JSON.stringify(before[k]) !== JSON.stringify(after[k])) changed.push(k);
|
|
5948
|
+
}
|
|
5949
|
+
return changed.length === 0 ? "attributes differ" : `fields changed: ${changed.sort().join(", ")}`;
|
|
5950
|
+
}
|
|
5951
|
+
async function runStaleEdges(client, input) {
|
|
5952
|
+
const params = new URLSearchParams();
|
|
5953
|
+
if (input.limit !== void 0) params.set("limit", String(input.limit));
|
|
5954
|
+
if (input.edgeType) params.set("edgeType", input.edgeType);
|
|
5955
|
+
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
5956
|
+
const events = await client.get(
|
|
5957
|
+
projectPath(input.project, `/incidents/stale${qs}`)
|
|
5958
|
+
);
|
|
5959
|
+
if (events.length === 0) {
|
|
5960
|
+
return {
|
|
5961
|
+
summary: input.edgeType ? `No stale ${input.edgeType} edges recorded.` : "No stale-edge transitions recorded yet."
|
|
5962
|
+
};
|
|
5963
|
+
}
|
|
5964
|
+
const blockLines = events.map(
|
|
5965
|
+
(e) => ` ${e.transitionedAt} \u2014 ${e.source} -[${e.edgeType}]-> ${e.target} (last seen ${e.lastObserved}, threshold ${formatDuration(e.thresholdMs)})`
|
|
5966
|
+
);
|
|
5967
|
+
return {
|
|
5968
|
+
summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
|
|
5969
|
+
block: blockLines.join("\n"),
|
|
5970
|
+
provenance: import_types21.Provenance.STALE
|
|
5971
|
+
};
|
|
5972
|
+
}
|
|
5973
|
+
async function runPolicies(client, input) {
|
|
5974
|
+
let violations;
|
|
5975
|
+
let allowed = true;
|
|
5976
|
+
let hypothetical;
|
|
5977
|
+
if (input.hypotheticalAction) {
|
|
5978
|
+
if (typeof client.post !== "function") {
|
|
5979
|
+
throw new Error("HttpClient does not support POST \u2014 required for policies dry-run");
|
|
5980
|
+
}
|
|
5981
|
+
const body = await client.post(
|
|
5982
|
+
projectPath(input.project, "/policies/check"),
|
|
5983
|
+
{ hypotheticalAction: input.hypotheticalAction }
|
|
5984
|
+
);
|
|
5985
|
+
violations = body.violations;
|
|
5986
|
+
allowed = body.allowed;
|
|
5987
|
+
hypothetical = body.hypotheticalAction;
|
|
5988
|
+
} else {
|
|
5989
|
+
const params = new URLSearchParams();
|
|
5990
|
+
if (input.policyId) params.set("policyId", input.policyId);
|
|
5991
|
+
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
5992
|
+
violations = await client.get(
|
|
5993
|
+
projectPath(input.project, `/policies/violations${qs}`)
|
|
5994
|
+
);
|
|
5995
|
+
allowed = violations.every((v) => v.onViolation !== "block");
|
|
5996
|
+
}
|
|
5997
|
+
if (input.nodeId) {
|
|
5998
|
+
violations = violations.filter(
|
|
5999
|
+
(v) => v.subject.nodeId === input.nodeId || v.subject.path?.includes(input.nodeId)
|
|
6000
|
+
);
|
|
6001
|
+
}
|
|
6002
|
+
if (violations.length === 0) {
|
|
6003
|
+
return {
|
|
6004
|
+
summary: hypothetical ? `No violations would result from the hypothetical action (${hypothetical.kind}).` : "No policy violations recorded."
|
|
6005
|
+
};
|
|
6006
|
+
}
|
|
6007
|
+
const blockCount = violations.filter((v) => v.onViolation === "block").length;
|
|
6008
|
+
const summaryParts = [];
|
|
6009
|
+
if (hypothetical) {
|
|
6010
|
+
summaryParts.push(
|
|
6011
|
+
`Hypothetical ${hypothetical.kind} would surface ${violations.length} violation${violations.length === 1 ? "" : "s"}`
|
|
6012
|
+
);
|
|
6013
|
+
} else {
|
|
6014
|
+
summaryParts.push(
|
|
6015
|
+
`${violations.length} policy violation${violations.length === 1 ? "" : "s"} currently recorded`
|
|
6016
|
+
);
|
|
6017
|
+
}
|
|
6018
|
+
if (blockCount > 0) summaryParts.push(`${blockCount} of which block`);
|
|
6019
|
+
if (!allowed && hypothetical) summaryParts.push("action denied");
|
|
6020
|
+
const summary = summaryParts.join("; ") + ".";
|
|
6021
|
+
const blockLines = violations.map((v) => {
|
|
6022
|
+
const subject = v.subject.nodeId ?? v.subject.edgeId ?? v.subject.path?.[0] ?? "(global)";
|
|
6023
|
+
return ` \u2022 [${v.severity}/${v.onViolation}] ${v.policyName}: ${v.message} \u2014 ${subject}`;
|
|
6024
|
+
});
|
|
6025
|
+
const severities = [...new Set(violations.map((v) => v.severity))];
|
|
6026
|
+
return {
|
|
6027
|
+
summary,
|
|
6028
|
+
block: blockLines.join("\n"),
|
|
6029
|
+
confidence: hypothetical ? 0.7 : 1,
|
|
6030
|
+
provenance: severities.join(" ")
|
|
6031
|
+
};
|
|
6032
|
+
}
|
|
6033
|
+
function formatFooter(confidence, provenance) {
|
|
6034
|
+
const c = confidence === void 0 ? "n/a" : confidence.toFixed(2);
|
|
6035
|
+
const p = provenance === void 0 ? "n/a" : Array.isArray(provenance) ? [...new Set(provenance)].join(", ") : provenance;
|
|
6036
|
+
return `confidence: ${c} \xB7 provenance: ${p}`;
|
|
6037
|
+
}
|
|
6038
|
+
function formatHuman(result) {
|
|
6039
|
+
const sections = [result.summary.trim()];
|
|
6040
|
+
if (result.block && result.block.trim().length > 0) sections.push(result.block.trimEnd());
|
|
6041
|
+
sections.push(formatFooter(result.confidence, result.provenance));
|
|
6042
|
+
return sections.join("\n\n");
|
|
6043
|
+
}
|
|
6044
|
+
function formatJson(result) {
|
|
6045
|
+
return JSON.stringify(
|
|
6046
|
+
{
|
|
6047
|
+
summary: result.summary,
|
|
6048
|
+
block: result.block ?? "",
|
|
6049
|
+
confidence: result.confidence ?? null,
|
|
6050
|
+
provenance: result.provenance ?? null
|
|
6051
|
+
},
|
|
6052
|
+
null,
|
|
6053
|
+
2
|
|
6054
|
+
);
|
|
6055
|
+
}
|
|
6056
|
+
function exitCodeForError(err) {
|
|
6057
|
+
if (err instanceof TransportError) return 3;
|
|
6058
|
+
if (err instanceof HttpError) return 1;
|
|
6059
|
+
return 1;
|
|
6060
|
+
}
|
|
6061
|
+
|
|
5303
6062
|
// src/cli.ts
|
|
5304
6063
|
function usage() {
|
|
5305
6064
|
console.log("usage: neat <command> [args] [--project <name>]");
|
|
5306
6065
|
console.log("");
|
|
5307
|
-
console.log("commands:");
|
|
6066
|
+
console.log("lifecycle commands:");
|
|
5308
6067
|
console.log(" init <path> One-time install: discover, extract, register, plan SDK install.");
|
|
5309
6068
|
console.log(" Snapshot lands in <path>/neat-out/graph.json by default");
|
|
5310
6069
|
console.log(" (or <path>/neat-out/<project>.json for non-default).");
|
|
@@ -5326,51 +6085,133 @@ function usage() {
|
|
|
5326
6085
|
console.log(" --print-config print the JSON snippet to stdout");
|
|
5327
6086
|
console.log(" --apply merge mcpServers.neat into ~/.claude.json");
|
|
5328
6087
|
console.log("");
|
|
6088
|
+
console.log("query commands (mirror the MCP tools, ADR-050):");
|
|
6089
|
+
console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
|
|
6090
|
+
console.log(" example: neat root-cause service:<name>");
|
|
6091
|
+
console.log(" blast-radius <node-id> BFS outbound \u2014 what would break if this dies.");
|
|
6092
|
+
console.log(" example: neat blast-radius database:<host>");
|
|
6093
|
+
console.log(" dependencies <node-id> Transitive outbound dependencies.");
|
|
6094
|
+
console.log(" Flags: --depth N (default 3, max 10)");
|
|
6095
|
+
console.log(" example: neat dependencies service:<name> --depth 2");
|
|
6096
|
+
console.log(" observed-dependencies <node-id> OBSERVED-only outbound edges (runtime traffic).");
|
|
6097
|
+
console.log(" example: neat observed-dependencies service:<name>");
|
|
6098
|
+
console.log(" incidents [<node-id>] Recent error events; per-node when an id is given.");
|
|
6099
|
+
console.log(" Flags: --limit N (default 20)");
|
|
6100
|
+
console.log(" example: neat incidents service:<name> --limit 5");
|
|
6101
|
+
console.log(" search <query> Semantic (or substring) match on node names/ids.");
|
|
6102
|
+
console.log(' example: neat search "checkout"');
|
|
6103
|
+
console.log(" diff --against <snapshot> Compare the live graph to a saved snapshot.");
|
|
6104
|
+
console.log(" example: neat diff --against ./snapshots/baseline.json");
|
|
6105
|
+
console.log(" stale-edges Recent OBSERVED \u2192 STALE transitions.");
|
|
6106
|
+
console.log(" Flags: --limit N, --edge-type CALLS|CONNECTS_TO|...");
|
|
6107
|
+
console.log(" example: neat stale-edges --edge-type CALLS");
|
|
6108
|
+
console.log(" policies Current policy violations.");
|
|
6109
|
+
console.log(" Flags: --node <id>, --hypothetical-action <json>");
|
|
6110
|
+
console.log(" example: neat policies --node service:<name> --json");
|
|
6111
|
+
console.log("");
|
|
5329
6112
|
console.log("flags:");
|
|
5330
6113
|
console.log(' --project <name> Name the project this command targets. Default: "default".');
|
|
5331
|
-
|
|
6114
|
+
console.log(" --json Emit machine-readable JSON instead of human text. Query verbs only.");
|
|
6115
|
+
console.log("");
|
|
6116
|
+
console.log("exit codes:");
|
|
6117
|
+
console.log(" 0 success");
|
|
6118
|
+
console.log(" 1 server error (4xx/5xx body printed to stderr)");
|
|
6119
|
+
console.log(" 2 misuse (missing args, bad flags) \u2014 handled before any network call");
|
|
6120
|
+
console.log(" 3 daemon not reachable (connection refused / timeout)");
|
|
6121
|
+
console.log("");
|
|
6122
|
+
console.log("environment:");
|
|
6123
|
+
console.log(" NEAT_API_URL base URL for the core REST API (default http://localhost:8080)");
|
|
6124
|
+
console.log(" NEAT_PROJECT project name when --project isn't passed");
|
|
6125
|
+
}
|
|
6126
|
+
var STRING_FLAGS = [
|
|
6127
|
+
["--project", "project"],
|
|
6128
|
+
["--depth", "depth"],
|
|
6129
|
+
["--limit", "limit"],
|
|
6130
|
+
["--edge-type", "edgeType"],
|
|
6131
|
+
["--node", "node"],
|
|
6132
|
+
["--since", "since"],
|
|
6133
|
+
["--against", "against"],
|
|
6134
|
+
["--error-id", "errorId"],
|
|
6135
|
+
["--hypothetical-action", "hypotheticalAction"]
|
|
6136
|
+
];
|
|
5332
6137
|
function parseArgs(rest) {
|
|
5333
6138
|
const positional = [];
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
6139
|
+
const out = {
|
|
6140
|
+
project: null,
|
|
6141
|
+
apply: false,
|
|
6142
|
+
dryRun: false,
|
|
6143
|
+
noInstall: false,
|
|
6144
|
+
printConfig: false,
|
|
6145
|
+
json: false,
|
|
6146
|
+
depth: null,
|
|
6147
|
+
limit: null,
|
|
6148
|
+
edgeType: null,
|
|
6149
|
+
node: null,
|
|
6150
|
+
since: null,
|
|
6151
|
+
against: null,
|
|
6152
|
+
errorId: null,
|
|
6153
|
+
hypotheticalAction: null,
|
|
6154
|
+
positional: []
|
|
6155
|
+
};
|
|
5339
6156
|
for (let i = 0; i < rest.length; i++) {
|
|
5340
6157
|
const arg = rest[i];
|
|
5341
|
-
if (arg === "--project") {
|
|
5342
|
-
const next = rest[i + 1];
|
|
5343
|
-
if (!next) {
|
|
5344
|
-
console.error("neat: --project requires a value");
|
|
5345
|
-
process.exit(2);
|
|
5346
|
-
}
|
|
5347
|
-
project = next;
|
|
5348
|
-
i++;
|
|
5349
|
-
continue;
|
|
5350
|
-
}
|
|
5351
|
-
if (arg.startsWith("--project=")) {
|
|
5352
|
-
project = arg.slice("--project=".length);
|
|
5353
|
-
continue;
|
|
5354
|
-
}
|
|
5355
6158
|
if (arg === "--apply") {
|
|
5356
|
-
|
|
6159
|
+
out.apply = true;
|
|
5357
6160
|
continue;
|
|
5358
6161
|
}
|
|
5359
6162
|
if (arg === "--dry-run") {
|
|
5360
|
-
dryRun = true;
|
|
6163
|
+
out.dryRun = true;
|
|
5361
6164
|
continue;
|
|
5362
6165
|
}
|
|
5363
6166
|
if (arg === "--no-install") {
|
|
5364
|
-
noInstall = true;
|
|
6167
|
+
out.noInstall = true;
|
|
5365
6168
|
continue;
|
|
5366
6169
|
}
|
|
5367
6170
|
if (arg === "--print-config") {
|
|
5368
|
-
printConfig = true;
|
|
6171
|
+
out.printConfig = true;
|
|
6172
|
+
continue;
|
|
6173
|
+
}
|
|
6174
|
+
if (arg === "--json") {
|
|
6175
|
+
out.json = true;
|
|
5369
6176
|
continue;
|
|
5370
6177
|
}
|
|
6178
|
+
let matched = false;
|
|
6179
|
+
for (const [flag, field] of STRING_FLAGS) {
|
|
6180
|
+
if (arg === flag) {
|
|
6181
|
+
const next = rest[i + 1];
|
|
6182
|
+
if (next === void 0) {
|
|
6183
|
+
console.error(`neat: ${flag} requires a value`);
|
|
6184
|
+
process.exit(2);
|
|
6185
|
+
}
|
|
6186
|
+
assignFlag(out, field, next);
|
|
6187
|
+
i++;
|
|
6188
|
+
matched = true;
|
|
6189
|
+
break;
|
|
6190
|
+
}
|
|
6191
|
+
if (arg.startsWith(`${flag}=`)) {
|
|
6192
|
+
assignFlag(out, field, arg.slice(flag.length + 1));
|
|
6193
|
+
matched = true;
|
|
6194
|
+
break;
|
|
6195
|
+
}
|
|
6196
|
+
}
|
|
6197
|
+
if (matched) continue;
|
|
5371
6198
|
positional.push(arg);
|
|
5372
6199
|
}
|
|
5373
|
-
|
|
6200
|
+
out.positional = positional;
|
|
6201
|
+
return out;
|
|
6202
|
+
}
|
|
6203
|
+
function assignFlag(out, field, value) {
|
|
6204
|
+
if (field === "depth" || field === "limit") {
|
|
6205
|
+
const n = Number(value);
|
|
6206
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
6207
|
+
console.error(`neat: --${field === "depth" ? "depth" : "limit"} must be a positive integer`);
|
|
6208
|
+
process.exit(2);
|
|
6209
|
+
}
|
|
6210
|
+
out[field] = n;
|
|
6211
|
+
return;
|
|
6212
|
+
}
|
|
6213
|
+
;
|
|
6214
|
+
out[field] = value;
|
|
5374
6215
|
}
|
|
5375
6216
|
function summarise(nodes, edges) {
|
|
5376
6217
|
const byNode = /* @__PURE__ */ new Map();
|
|
@@ -5450,7 +6291,7 @@ async function buildPatchSections(services) {
|
|
|
5450
6291
|
}
|
|
5451
6292
|
async function runInit(opts) {
|
|
5452
6293
|
const written = [];
|
|
5453
|
-
const stat = await
|
|
6294
|
+
const stat = await import_node_fs22.promises.stat(opts.scanPath).catch(() => null);
|
|
5454
6295
|
if (!stat || !stat.isDirectory()) {
|
|
5455
6296
|
console.error(`neat init: ${opts.scanPath} is not a directory`);
|
|
5456
6297
|
return { exitCode: 2, writtenFiles: written };
|
|
@@ -5459,9 +6300,9 @@ async function runInit(opts) {
|
|
|
5459
6300
|
printDiscoveryReport(opts, services);
|
|
5460
6301
|
const sections = opts.noInstall ? [] : await buildPatchSections(services);
|
|
5461
6302
|
const patch = renderPatch(sections);
|
|
5462
|
-
const patchPath =
|
|
6303
|
+
const patchPath = import_node_path37.default.join(opts.scanPath, "neat.patch");
|
|
5463
6304
|
if (opts.dryRun) {
|
|
5464
|
-
await
|
|
6305
|
+
await import_node_fs22.promises.writeFile(patchPath, patch, "utf8");
|
|
5465
6306
|
written.push(patchPath);
|
|
5466
6307
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
5467
6308
|
console.log("rerun without --dry-run to register and snapshot.");
|
|
@@ -5501,7 +6342,7 @@ async function runInit(opts) {
|
|
|
5501
6342
|
console.log("patch applied. Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
5502
6343
|
}
|
|
5503
6344
|
} else {
|
|
5504
|
-
await
|
|
6345
|
+
await import_node_fs22.promises.writeFile(patchPath, patch, "utf8");
|
|
5505
6346
|
written.push(patchPath);
|
|
5506
6347
|
}
|
|
5507
6348
|
}
|
|
@@ -5541,9 +6382,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
5541
6382
|
};
|
|
5542
6383
|
function claudeConfigPath() {
|
|
5543
6384
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
5544
|
-
if (override && override.length > 0) return
|
|
6385
|
+
if (override && override.length > 0) return import_node_path37.default.resolve(override);
|
|
5545
6386
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
5546
|
-
return
|
|
6387
|
+
return import_node_path37.default.join(home, ".claude.json");
|
|
5547
6388
|
}
|
|
5548
6389
|
async function runSkill(opts) {
|
|
5549
6390
|
const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -5555,7 +6396,7 @@ async function runSkill(opts) {
|
|
|
5555
6396
|
const target = claudeConfigPath();
|
|
5556
6397
|
let existing = {};
|
|
5557
6398
|
try {
|
|
5558
|
-
existing = JSON.parse(await
|
|
6399
|
+
existing = JSON.parse(await import_node_fs22.promises.readFile(target, "utf8"));
|
|
5559
6400
|
} catch (err) {
|
|
5560
6401
|
if (err.code !== "ENOENT") {
|
|
5561
6402
|
console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
|
|
@@ -5567,8 +6408,8 @@ async function runSkill(opts) {
|
|
|
5567
6408
|
...existing,
|
|
5568
6409
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
5569
6410
|
};
|
|
5570
|
-
await
|
|
5571
|
-
await
|
|
6411
|
+
await import_node_fs22.promises.mkdir(import_node_path37.default.dirname(target), { recursive: true });
|
|
6412
|
+
await import_node_fs22.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
5572
6413
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
5573
6414
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
5574
6415
|
return { exitCode: 0 };
|
|
@@ -5602,12 +6443,12 @@ async function main() {
|
|
|
5602
6443
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
5603
6444
|
process.exit(2);
|
|
5604
6445
|
}
|
|
5605
|
-
const scanPath =
|
|
6446
|
+
const scanPath = import_node_path37.default.resolve(target);
|
|
5606
6447
|
const projectExplicit = parsed.project !== null;
|
|
5607
|
-
const projectName = projectExplicit ? project :
|
|
6448
|
+
const projectName = projectExplicit ? project : import_node_path37.default.basename(scanPath);
|
|
5608
6449
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
5609
|
-
const fallback = pathsForProject(projectKey,
|
|
5610
|
-
const outPath =
|
|
6450
|
+
const fallback = pathsForProject(projectKey, import_node_path37.default.join(scanPath, "neat-out")).snapshotPath;
|
|
6451
|
+
const outPath = import_node_path37.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
|
|
5611
6452
|
const result = await runInit({
|
|
5612
6453
|
scanPath,
|
|
5613
6454
|
outPath,
|
|
@@ -5627,21 +6468,21 @@ async function main() {
|
|
|
5627
6468
|
usage();
|
|
5628
6469
|
process.exit(2);
|
|
5629
6470
|
}
|
|
5630
|
-
const scanPath =
|
|
5631
|
-
const stat = await
|
|
6471
|
+
const scanPath = import_node_path37.default.resolve(target);
|
|
6472
|
+
const stat = await import_node_fs22.promises.stat(scanPath).catch(() => null);
|
|
5632
6473
|
if (!stat || !stat.isDirectory()) {
|
|
5633
6474
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
5634
6475
|
process.exit(2);
|
|
5635
6476
|
}
|
|
5636
|
-
const projectPaths = pathsForProject(project,
|
|
5637
|
-
const outPath =
|
|
5638
|
-
const errorsPath =
|
|
5639
|
-
process.env.NEAT_ERRORS_PATH ??
|
|
6477
|
+
const projectPaths = pathsForProject(project, import_node_path37.default.join(scanPath, "neat-out"));
|
|
6478
|
+
const outPath = import_node_path37.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
|
|
6479
|
+
const errorsPath = import_node_path37.default.resolve(
|
|
6480
|
+
process.env.NEAT_ERRORS_PATH ?? import_node_path37.default.join(import_node_path37.default.dirname(outPath), import_node_path37.default.basename(projectPaths.errorsPath))
|
|
5640
6481
|
);
|
|
5641
|
-
const staleEventsPath =
|
|
5642
|
-
process.env.NEAT_STALE_EVENTS_PATH ??
|
|
6482
|
+
const staleEventsPath = import_node_path37.default.resolve(
|
|
6483
|
+
process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path37.default.join(import_node_path37.default.dirname(outPath), import_node_path37.default.basename(projectPaths.staleEventsPath))
|
|
5643
6484
|
);
|
|
5644
|
-
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ?
|
|
6485
|
+
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path37.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
|
|
5645
6486
|
const handle = await startWatch(getGraph(project), {
|
|
5646
6487
|
scanPath,
|
|
5647
6488
|
outPath,
|
|
@@ -5734,10 +6575,167 @@ async function main() {
|
|
|
5734
6575
|
console.log("note: neat-out/, policy.json, and other files at the project path were left in place.");
|
|
5735
6576
|
return;
|
|
5736
6577
|
}
|
|
6578
|
+
if (QUERY_VERBS.has(cmd)) {
|
|
6579
|
+
const code = await runQueryVerb(cmd, parsed);
|
|
6580
|
+
if (code !== 0) process.exit(code);
|
|
6581
|
+
return;
|
|
6582
|
+
}
|
|
5737
6583
|
console.error(`neat: unknown command "${cmd}"`);
|
|
5738
6584
|
usage();
|
|
5739
6585
|
process.exit(1);
|
|
5740
6586
|
}
|
|
6587
|
+
var QUERY_VERBS = /* @__PURE__ */ new Set([
|
|
6588
|
+
"root-cause",
|
|
6589
|
+
"blast-radius",
|
|
6590
|
+
"dependencies",
|
|
6591
|
+
"observed-dependencies",
|
|
6592
|
+
"incidents",
|
|
6593
|
+
"search",
|
|
6594
|
+
"diff",
|
|
6595
|
+
"stale-edges",
|
|
6596
|
+
"policies"
|
|
6597
|
+
]);
|
|
6598
|
+
function resolveProjectFlag(parsed) {
|
|
6599
|
+
if (parsed.project) return parsed.project;
|
|
6600
|
+
const env = process.env.NEAT_PROJECT;
|
|
6601
|
+
if (env && env.length > 0 && env !== DEFAULT_PROJECT) return env;
|
|
6602
|
+
return void 0;
|
|
6603
|
+
}
|
|
6604
|
+
async function runQueryVerb(cmd, parsed) {
|
|
6605
|
+
const baseUrl = process.env.NEAT_API_URL ?? "http://localhost:8080";
|
|
6606
|
+
const client = createHttpClient(baseUrl);
|
|
6607
|
+
const project = resolveProjectFlag(parsed);
|
|
6608
|
+
const positional = parsed.positional;
|
|
6609
|
+
let work;
|
|
6610
|
+
switch (cmd) {
|
|
6611
|
+
case "root-cause": {
|
|
6612
|
+
const node = positional[0];
|
|
6613
|
+
if (!node) {
|
|
6614
|
+
console.error("neat root-cause: missing <node-id>");
|
|
6615
|
+
return 2;
|
|
6616
|
+
}
|
|
6617
|
+
work = runRootCause(client, {
|
|
6618
|
+
errorNode: node,
|
|
6619
|
+
...parsed.errorId ? { errorId: parsed.errorId } : {},
|
|
6620
|
+
...project ? { project } : {}
|
|
6621
|
+
});
|
|
6622
|
+
break;
|
|
6623
|
+
}
|
|
6624
|
+
case "blast-radius": {
|
|
6625
|
+
const node = positional[0];
|
|
6626
|
+
if (!node) {
|
|
6627
|
+
console.error("neat blast-radius: missing <node-id>");
|
|
6628
|
+
return 2;
|
|
6629
|
+
}
|
|
6630
|
+
work = runBlastRadius(client, {
|
|
6631
|
+
nodeId: node,
|
|
6632
|
+
...parsed.depth !== null ? { depth: parsed.depth } : {},
|
|
6633
|
+
...project ? { project } : {}
|
|
6634
|
+
});
|
|
6635
|
+
break;
|
|
6636
|
+
}
|
|
6637
|
+
case "dependencies": {
|
|
6638
|
+
const node = positional[0];
|
|
6639
|
+
if (!node) {
|
|
6640
|
+
console.error("neat dependencies: missing <node-id>");
|
|
6641
|
+
return 2;
|
|
6642
|
+
}
|
|
6643
|
+
work = runDependencies(client, {
|
|
6644
|
+
nodeId: node,
|
|
6645
|
+
...parsed.depth !== null ? { depth: parsed.depth } : {},
|
|
6646
|
+
...project ? { project } : {}
|
|
6647
|
+
});
|
|
6648
|
+
break;
|
|
6649
|
+
}
|
|
6650
|
+
case "observed-dependencies": {
|
|
6651
|
+
const node = positional[0];
|
|
6652
|
+
if (!node) {
|
|
6653
|
+
console.error("neat observed-dependencies: missing <node-id>");
|
|
6654
|
+
return 2;
|
|
6655
|
+
}
|
|
6656
|
+
work = runObservedDependencies(client, {
|
|
6657
|
+
nodeId: node,
|
|
6658
|
+
...project ? { project } : {}
|
|
6659
|
+
});
|
|
6660
|
+
break;
|
|
6661
|
+
}
|
|
6662
|
+
case "incidents": {
|
|
6663
|
+
work = runIncidents(client, {
|
|
6664
|
+
...positional[0] ? { nodeId: positional[0] } : {},
|
|
6665
|
+
...parsed.limit !== null ? { limit: parsed.limit } : {},
|
|
6666
|
+
...project ? { project } : {}
|
|
6667
|
+
});
|
|
6668
|
+
break;
|
|
6669
|
+
}
|
|
6670
|
+
case "search": {
|
|
6671
|
+
const q = positional.join(" ").trim();
|
|
6672
|
+
if (!q) {
|
|
6673
|
+
console.error("neat search: missing <query>");
|
|
6674
|
+
return 2;
|
|
6675
|
+
}
|
|
6676
|
+
work = runSearch(client, { query: q, ...project ? { project } : {} });
|
|
6677
|
+
break;
|
|
6678
|
+
}
|
|
6679
|
+
case "diff": {
|
|
6680
|
+
const against = parsed.against ?? parsed.since;
|
|
6681
|
+
if (!against) {
|
|
6682
|
+
console.error("neat diff: --against <snapshot-path> is required");
|
|
6683
|
+
return 2;
|
|
6684
|
+
}
|
|
6685
|
+
work = runDiff(client, {
|
|
6686
|
+
againstSnapshot: against,
|
|
6687
|
+
...project ? { project } : {}
|
|
6688
|
+
});
|
|
6689
|
+
break;
|
|
6690
|
+
}
|
|
6691
|
+
case "stale-edges": {
|
|
6692
|
+
work = runStaleEdges(client, {
|
|
6693
|
+
...parsed.limit !== null ? { limit: parsed.limit } : {},
|
|
6694
|
+
...parsed.edgeType ? { edgeType: parsed.edgeType } : {},
|
|
6695
|
+
...project ? { project } : {}
|
|
6696
|
+
});
|
|
6697
|
+
break;
|
|
6698
|
+
}
|
|
6699
|
+
case "policies": {
|
|
6700
|
+
let hypothetical;
|
|
6701
|
+
if (parsed.hypotheticalAction) {
|
|
6702
|
+
try {
|
|
6703
|
+
hypothetical = JSON.parse(parsed.hypotheticalAction);
|
|
6704
|
+
} catch (err) {
|
|
6705
|
+
console.error(
|
|
6706
|
+
`neat policies: --hypothetical-action must be valid JSON: ${err.message}`
|
|
6707
|
+
);
|
|
6708
|
+
return 2;
|
|
6709
|
+
}
|
|
6710
|
+
}
|
|
6711
|
+
work = runPolicies(client, {
|
|
6712
|
+
...parsed.node ? { nodeId: parsed.node } : {},
|
|
6713
|
+
...hypothetical ? { hypotheticalAction: hypothetical } : {},
|
|
6714
|
+
...project ? { project } : {}
|
|
6715
|
+
});
|
|
6716
|
+
break;
|
|
6717
|
+
}
|
|
6718
|
+
default:
|
|
6719
|
+
console.error(`neat: unknown query verb "${cmd}"`);
|
|
6720
|
+
return 2;
|
|
6721
|
+
}
|
|
6722
|
+
try {
|
|
6723
|
+
const result = await work;
|
|
6724
|
+
if (parsed.json) process.stdout.write(formatJson(result) + "\n");
|
|
6725
|
+
else process.stdout.write(formatHuman(result) + "\n");
|
|
6726
|
+
return 0;
|
|
6727
|
+
} catch (err) {
|
|
6728
|
+
if (err instanceof HttpError) {
|
|
6729
|
+
const detail = err.responseBody.length > 0 ? err.responseBody : err.message;
|
|
6730
|
+
console.error(`neat ${cmd}: ${detail.trim()}`);
|
|
6731
|
+
} else if (err instanceof TransportError) {
|
|
6732
|
+
console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (NEAT_API_URL=${process.env.NEAT_API_URL ?? "http://localhost:8080"})`);
|
|
6733
|
+
} else {
|
|
6734
|
+
console.error(`neat ${cmd}: ${err.message}`);
|
|
6735
|
+
}
|
|
6736
|
+
return exitCodeForError(err);
|
|
6737
|
+
}
|
|
6738
|
+
}
|
|
5741
6739
|
var entry = process.argv[1] ?? "";
|
|
5742
6740
|
if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsWith("/neat")) {
|
|
5743
6741
|
main().catch((err) => {
|
|
@@ -5748,7 +6746,10 @@ if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsW
|
|
|
5748
6746
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5749
6747
|
0 && (module.exports = {
|
|
5750
6748
|
CLAUDE_SKILL_CONFIG,
|
|
6749
|
+
QUERY_VERBS,
|
|
6750
|
+
parseArgs,
|
|
5751
6751
|
runInit,
|
|
6752
|
+
runQueryVerb,
|
|
5752
6753
|
runSkill
|
|
5753
6754
|
});
|
|
5754
6755
|
//# sourceMappingURL=cli.cjs.map
|