@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/index.cjs
CHANGED
|
@@ -100,8 +100,8 @@ function reshapeGrpcRequest(req) {
|
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
function resolveProtoRoot() {
|
|
103
|
-
const here =
|
|
104
|
-
return
|
|
103
|
+
const here = import_node_path31.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
104
|
+
return import_node_path31.default.resolve(here, "..", "proto");
|
|
105
105
|
}
|
|
106
106
|
function loadTraceService() {
|
|
107
107
|
const protoRoot = resolveProtoRoot();
|
|
@@ -156,13 +156,13 @@ async function startOtelGrpcReceiver(opts) {
|
|
|
156
156
|
})
|
|
157
157
|
};
|
|
158
158
|
}
|
|
159
|
-
var import_node_url,
|
|
159
|
+
var import_node_url, import_node_path31, grpc, protoLoader;
|
|
160
160
|
var init_otel_grpc = __esm({
|
|
161
161
|
"src/otel-grpc.ts"() {
|
|
162
162
|
"use strict";
|
|
163
163
|
init_cjs_shims();
|
|
164
164
|
import_node_url = require("url");
|
|
165
|
-
|
|
165
|
+
import_node_path31 = __toESM(require("path"), 1);
|
|
166
166
|
grpc = __toESM(require("@grpc/grpc-js"), 1);
|
|
167
167
|
protoLoader = __toESM(require("@grpc/proto-loader"), 1);
|
|
168
168
|
init_otel();
|
|
@@ -259,10 +259,10 @@ function parseOtlpRequest(body) {
|
|
|
259
259
|
}
|
|
260
260
|
function loadProtobufDecoder() {
|
|
261
261
|
if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
|
|
262
|
-
const here =
|
|
263
|
-
const protoRoot =
|
|
262
|
+
const here = import_node_path32.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
263
|
+
const protoRoot = import_node_path32.default.resolve(here, "..", "proto");
|
|
264
264
|
const root = new import_protobufjs.default.Root();
|
|
265
|
-
root.resolvePath = (_origin, target) =>
|
|
265
|
+
root.resolvePath = (_origin, target) => import_node_path32.default.resolve(protoRoot, target);
|
|
266
266
|
root.loadSync(
|
|
267
267
|
"opentelemetry/proto/collector/trace/v1/trace_service.proto",
|
|
268
268
|
{ keepCase: true }
|
|
@@ -362,12 +362,12 @@ function logSpanHandler(span) {
|
|
|
362
362
|
`otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
|
|
363
363
|
);
|
|
364
364
|
}
|
|
365
|
-
var
|
|
365
|
+
var import_node_path32, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
|
|
366
366
|
var init_otel = __esm({
|
|
367
367
|
"src/otel.ts"() {
|
|
368
368
|
"use strict";
|
|
369
369
|
init_cjs_shims();
|
|
370
|
-
|
|
370
|
+
import_node_path32 = __toESM(require("path"), 1);
|
|
371
371
|
import_node_url2 = require("url");
|
|
372
372
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
373
373
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
@@ -777,6 +777,18 @@ function deprecatedApis() {
|
|
|
777
777
|
return currentMatrix().deprecatedApis ?? [];
|
|
778
778
|
}
|
|
779
779
|
|
|
780
|
+
// src/events.ts
|
|
781
|
+
init_cjs_shims();
|
|
782
|
+
var import_node_events = require("events");
|
|
783
|
+
var EVENT_BUS_CHANNEL = "event";
|
|
784
|
+
var NeatEventBus = class extends import_node_events.EventEmitter {
|
|
785
|
+
};
|
|
786
|
+
var eventBus = new NeatEventBus();
|
|
787
|
+
eventBus.setMaxListeners(0);
|
|
788
|
+
function emitNeatEvent(envelope) {
|
|
789
|
+
eventBus.emit(EVENT_BUS_CHANNEL, envelope);
|
|
790
|
+
}
|
|
791
|
+
|
|
780
792
|
// src/traverse.ts
|
|
781
793
|
init_cjs_shims();
|
|
782
794
|
var import_types = require("@neat.is/types");
|
|
@@ -864,19 +876,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
864
876
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
865
877
|
let best = { path: [start], edges: [] };
|
|
866
878
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
867
|
-
function step(node,
|
|
868
|
-
if (
|
|
869
|
-
best = { path: [...
|
|
879
|
+
function step(node, path34, edges) {
|
|
880
|
+
if (path34.length > best.path.length) {
|
|
881
|
+
best = { path: [...path34], edges: [...edges] };
|
|
870
882
|
}
|
|
871
|
-
if (
|
|
883
|
+
if (path34.length - 1 >= maxDepth) return;
|
|
872
884
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
873
885
|
for (const [srcId, edge] of incoming) {
|
|
874
886
|
if (visited.has(srcId)) continue;
|
|
875
887
|
visited.add(srcId);
|
|
876
|
-
|
|
888
|
+
path34.push(srcId);
|
|
877
889
|
edges.push(edge);
|
|
878
|
-
step(srcId,
|
|
879
|
-
|
|
890
|
+
step(srcId, path34, edges);
|
|
891
|
+
path34.pop();
|
|
880
892
|
edges.pop();
|
|
881
893
|
visited.delete(srcId);
|
|
882
894
|
}
|
|
@@ -1340,9 +1352,11 @@ async function loadPolicyFile(policyPath) {
|
|
|
1340
1352
|
}
|
|
1341
1353
|
var PolicyViolationsLog = class {
|
|
1342
1354
|
path;
|
|
1355
|
+
project;
|
|
1343
1356
|
seen = null;
|
|
1344
|
-
constructor(logPath) {
|
|
1357
|
+
constructor(logPath, project = DEFAULT_PROJECT) {
|
|
1345
1358
|
this.path = logPath;
|
|
1359
|
+
this.project = project;
|
|
1346
1360
|
}
|
|
1347
1361
|
async append(v) {
|
|
1348
1362
|
if (!this.seen) await this.hydrate();
|
|
@@ -1350,6 +1364,11 @@ var PolicyViolationsLog = class {
|
|
|
1350
1364
|
this.seen.add(v.id);
|
|
1351
1365
|
await import_node_fs2.promises.mkdir(import_node_path2.default.dirname(this.path), { recursive: true });
|
|
1352
1366
|
await import_node_fs2.promises.appendFile(this.path, JSON.stringify(v) + "\n", "utf8");
|
|
1367
|
+
emitNeatEvent({
|
|
1368
|
+
type: "policy-violation",
|
|
1369
|
+
project: this.project,
|
|
1370
|
+
payload: { violation: v }
|
|
1371
|
+
});
|
|
1353
1372
|
return true;
|
|
1354
1373
|
}
|
|
1355
1374
|
async readAll() {
|
|
@@ -1796,6 +1815,7 @@ async function markStaleEdges(graph, options = {}) {
|
|
|
1796
1815
|
const thresholds = options.thresholds ?? loadStaleThresholdsFromEnv();
|
|
1797
1816
|
const now = options.now ?? Date.now();
|
|
1798
1817
|
const events = [];
|
|
1818
|
+
const project = options.project ?? DEFAULT_PROJECT;
|
|
1799
1819
|
graph.forEachEdge((id, attrs) => {
|
|
1800
1820
|
const e = attrs;
|
|
1801
1821
|
if (e.provenance !== import_types3.Provenance.OBSERVED) return;
|
|
@@ -1815,6 +1835,15 @@ async function markStaleEdges(graph, options = {}) {
|
|
|
1815
1835
|
lastObserved: e.lastObserved,
|
|
1816
1836
|
transitionedAt: new Date(now).toISOString()
|
|
1817
1837
|
});
|
|
1838
|
+
emitNeatEvent({
|
|
1839
|
+
type: "stale-transition",
|
|
1840
|
+
project,
|
|
1841
|
+
payload: {
|
|
1842
|
+
edgeId: id,
|
|
1843
|
+
from: import_types3.Provenance.OBSERVED,
|
|
1844
|
+
to: import_types3.Provenance.STALE
|
|
1845
|
+
}
|
|
1846
|
+
});
|
|
1818
1847
|
}
|
|
1819
1848
|
});
|
|
1820
1849
|
if (options.staleEventsPath && events.length > 0) {
|
|
@@ -1845,7 +1874,8 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
1845
1874
|
try {
|
|
1846
1875
|
await markStaleEdges(graph, {
|
|
1847
1876
|
thresholds: options.thresholds,
|
|
1848
|
-
staleEventsPath: options.staleEventsPath
|
|
1877
|
+
staleEventsPath: options.staleEventsPath,
|
|
1878
|
+
project: options.project
|
|
1849
1879
|
});
|
|
1850
1880
|
if (options.onPolicyTrigger) await options.onPolicyTrigger(graph);
|
|
1851
1881
|
} catch (err) {
|
|
@@ -1872,10 +1902,10 @@ async function readErrorEvents(errorsPath) {
|
|
|
1872
1902
|
|
|
1873
1903
|
// src/extract/services.ts
|
|
1874
1904
|
init_cjs_shims();
|
|
1875
|
-
var
|
|
1876
|
-
var
|
|
1905
|
+
var import_node_fs7 = require("fs");
|
|
1906
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
1877
1907
|
var import_ignore = __toESM(require("ignore"), 1);
|
|
1878
|
-
var
|
|
1908
|
+
var import_minimatch2 = require("minimatch");
|
|
1879
1909
|
var import_types5 = require("@neat.is/types");
|
|
1880
1910
|
|
|
1881
1911
|
// src/extract/shared.ts
|
|
@@ -1988,6 +2018,73 @@ function pythonToPackage(service) {
|
|
|
1988
2018
|
};
|
|
1989
2019
|
}
|
|
1990
2020
|
|
|
2021
|
+
// src/extract/owners.ts
|
|
2022
|
+
init_cjs_shims();
|
|
2023
|
+
var import_node_fs6 = require("fs");
|
|
2024
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
2025
|
+
var import_minimatch = require("minimatch");
|
|
2026
|
+
async function loadCodeowners(scanPath) {
|
|
2027
|
+
const candidates = [
|
|
2028
|
+
import_node_path6.default.join(scanPath, "CODEOWNERS"),
|
|
2029
|
+
import_node_path6.default.join(scanPath, ".github", "CODEOWNERS")
|
|
2030
|
+
];
|
|
2031
|
+
for (const file of candidates) {
|
|
2032
|
+
if (await exists(file)) {
|
|
2033
|
+
const raw = await import_node_fs6.promises.readFile(file, "utf8");
|
|
2034
|
+
return parseCodeowners(raw);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
return null;
|
|
2038
|
+
}
|
|
2039
|
+
function parseCodeowners(raw) {
|
|
2040
|
+
const rules = [];
|
|
2041
|
+
for (const line of raw.split("\n")) {
|
|
2042
|
+
const trimmed = line.trim();
|
|
2043
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2044
|
+
const match = /^(\S+)\s+(.+)$/.exec(trimmed);
|
|
2045
|
+
if (!match) continue;
|
|
2046
|
+
rules.push({ pattern: match[1], owners: match[2].trim() });
|
|
2047
|
+
}
|
|
2048
|
+
return { rules };
|
|
2049
|
+
}
|
|
2050
|
+
function matchOwner(file, repoPath) {
|
|
2051
|
+
const normalized = repoPath.split(import_node_path6.default.sep).join("/");
|
|
2052
|
+
for (const rule of file.rules) {
|
|
2053
|
+
if (matchesPattern(rule.pattern, normalized)) return rule.owners;
|
|
2054
|
+
}
|
|
2055
|
+
return null;
|
|
2056
|
+
}
|
|
2057
|
+
function matchesPattern(rawPattern, repoPath) {
|
|
2058
|
+
let pattern = rawPattern.startsWith("/") ? rawPattern.slice(1) : rawPattern;
|
|
2059
|
+
if (pattern === "*") return !repoPath.includes("/");
|
|
2060
|
+
if (pattern === "**" || pattern === "") return true;
|
|
2061
|
+
if (pattern.endsWith("/")) pattern = pattern + "**";
|
|
2062
|
+
if ((0, import_minimatch.minimatch)(repoPath, pattern, { dot: true })) return true;
|
|
2063
|
+
if (!pattern.includes("*") && (0, import_minimatch.minimatch)(repoPath, pattern + "/**", { dot: true })) return true;
|
|
2064
|
+
return false;
|
|
2065
|
+
}
|
|
2066
|
+
async function readPackageJsonAuthor(serviceDir) {
|
|
2067
|
+
const pkgPath = import_node_path6.default.join(serviceDir, "package.json");
|
|
2068
|
+
if (!await exists(pkgPath)) return null;
|
|
2069
|
+
try {
|
|
2070
|
+
const pkg = await readJson(pkgPath);
|
|
2071
|
+
if (!pkg.author) return null;
|
|
2072
|
+
if (typeof pkg.author === "string") return pkg.author;
|
|
2073
|
+
if (typeof pkg.author === "object" && typeof pkg.author.name === "string") return pkg.author.name;
|
|
2074
|
+
return null;
|
|
2075
|
+
} catch {
|
|
2076
|
+
return null;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
async function computeServiceOwner(codeowners, repoPath, serviceDir) {
|
|
2080
|
+
if (codeowners && repoPath !== void 0) {
|
|
2081
|
+
const owner = matchOwner(codeowners, repoPath);
|
|
2082
|
+
if (owner) return owner;
|
|
2083
|
+
}
|
|
2084
|
+
const author = await readPackageJsonAuthor(serviceDir);
|
|
2085
|
+
return author ?? void 0;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
1991
2088
|
// src/extract/services.ts
|
|
1992
2089
|
var DEFAULT_SCAN_DEPTH = 5;
|
|
1993
2090
|
function parseScanDepth() {
|
|
@@ -2004,21 +2101,21 @@ function workspaceGlobs(pkg) {
|
|
|
2004
2101
|
return null;
|
|
2005
2102
|
}
|
|
2006
2103
|
async function loadGitignore(scanPath) {
|
|
2007
|
-
const gitignorePath =
|
|
2104
|
+
const gitignorePath = import_node_path7.default.join(scanPath, ".gitignore");
|
|
2008
2105
|
if (!await exists(gitignorePath)) return null;
|
|
2009
|
-
const raw = await
|
|
2106
|
+
const raw = await import_node_fs7.promises.readFile(gitignorePath, "utf8");
|
|
2010
2107
|
return (0, import_ignore.default)().add(raw);
|
|
2011
2108
|
}
|
|
2012
2109
|
async function walkDirs(start, scanPath, options, visit) {
|
|
2013
2110
|
async function recurse(current, depth) {
|
|
2014
2111
|
if (depth > options.maxDepth) return;
|
|
2015
|
-
const entries = await
|
|
2112
|
+
const entries = await import_node_fs7.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
2016
2113
|
for (const entry of entries) {
|
|
2017
2114
|
if (!entry.isDirectory()) continue;
|
|
2018
2115
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
2019
|
-
const child =
|
|
2116
|
+
const child = import_node_path7.default.join(current, entry.name);
|
|
2020
2117
|
if (options.ig) {
|
|
2021
|
-
const rel =
|
|
2118
|
+
const rel = import_node_path7.default.relative(scanPath, child).split(import_node_path7.default.sep).join("/");
|
|
2022
2119
|
if (rel && options.ig.ignores(rel + "/")) continue;
|
|
2023
2120
|
}
|
|
2024
2121
|
await visit(child);
|
|
@@ -2033,8 +2130,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2033
2130
|
for (const raw of globs) {
|
|
2034
2131
|
const pattern = raw.replace(/^\.\//, "");
|
|
2035
2132
|
if (!pattern.includes("*")) {
|
|
2036
|
-
const candidate =
|
|
2037
|
-
if (await exists(
|
|
2133
|
+
const candidate = import_node_path7.default.join(scanPath, pattern);
|
|
2134
|
+
if (await exists(import_node_path7.default.join(candidate, "package.json"))) found.add(candidate);
|
|
2038
2135
|
continue;
|
|
2039
2136
|
}
|
|
2040
2137
|
const segments = pattern.split("/");
|
|
@@ -2043,13 +2140,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2043
2140
|
if (seg.includes("*")) break;
|
|
2044
2141
|
staticSegments.push(seg);
|
|
2045
2142
|
}
|
|
2046
|
-
const start =
|
|
2143
|
+
const start = import_node_path7.default.join(scanPath, ...staticSegments);
|
|
2047
2144
|
if (!await exists(start)) continue;
|
|
2048
2145
|
const hasDoubleStar = pattern.includes("**");
|
|
2049
2146
|
const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
|
|
2050
2147
|
await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
|
|
2051
|
-
const rel =
|
|
2052
|
-
if ((0,
|
|
2148
|
+
const rel = import_node_path7.default.relative(scanPath, dir).split(import_node_path7.default.sep).join("/");
|
|
2149
|
+
if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path7.default.join(dir, "package.json"))) {
|
|
2053
2150
|
found.add(dir);
|
|
2054
2151
|
}
|
|
2055
2152
|
});
|
|
@@ -2057,9 +2154,17 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2057
2154
|
return [...found];
|
|
2058
2155
|
}
|
|
2059
2156
|
async function discoverNodeService(scanPath, dir) {
|
|
2060
|
-
const pkgPath =
|
|
2157
|
+
const pkgPath = import_node_path7.default.join(dir, "package.json");
|
|
2061
2158
|
if (!await exists(pkgPath)) return null;
|
|
2062
|
-
|
|
2159
|
+
let pkg;
|
|
2160
|
+
try {
|
|
2161
|
+
pkg = await readJson(pkgPath);
|
|
2162
|
+
} catch (err) {
|
|
2163
|
+
console.warn(
|
|
2164
|
+
`[neat] services skipped ${import_node_path7.default.relative(scanPath, pkgPath)}: ${err.message}`
|
|
2165
|
+
);
|
|
2166
|
+
return null;
|
|
2167
|
+
}
|
|
2063
2168
|
if (!pkg.name) return null;
|
|
2064
2169
|
const node = {
|
|
2065
2170
|
id: (0, import_types5.serviceId)(pkg.name),
|
|
@@ -2068,7 +2173,7 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2068
2173
|
language: "javascript",
|
|
2069
2174
|
version: pkg.version,
|
|
2070
2175
|
dependencies: pkg.dependencies ?? {},
|
|
2071
|
-
repoPath:
|
|
2176
|
+
repoPath: import_node_path7.default.relative(scanPath, dir),
|
|
2072
2177
|
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
|
|
2073
2178
|
};
|
|
2074
2179
|
return { pkg, dir, node };
|
|
@@ -2084,13 +2189,22 @@ async function discoverPyService(scanPath, dir) {
|
|
|
2084
2189
|
language: "python",
|
|
2085
2190
|
version: py.version,
|
|
2086
2191
|
dependencies: py.dependencies,
|
|
2087
|
-
repoPath:
|
|
2192
|
+
repoPath: import_node_path7.default.relative(scanPath, dir)
|
|
2088
2193
|
};
|
|
2089
2194
|
return { pkg, dir, node };
|
|
2090
2195
|
}
|
|
2091
2196
|
async function discoverServices(scanPath) {
|
|
2092
|
-
const rootPkgPath =
|
|
2093
|
-
|
|
2197
|
+
const rootPkgPath = import_node_path7.default.join(scanPath, "package.json");
|
|
2198
|
+
let rootPkg = null;
|
|
2199
|
+
if (await exists(rootPkgPath)) {
|
|
2200
|
+
try {
|
|
2201
|
+
rootPkg = await readJson(rootPkgPath);
|
|
2202
|
+
} catch (err) {
|
|
2203
|
+
console.warn(
|
|
2204
|
+
`[neat] services workspaces skipped ${import_node_path7.default.relative(scanPath, rootPkgPath)}: ${err.message}`
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2094
2208
|
const wsGlobs = rootPkg ? workspaceGlobs(rootPkg) : null;
|
|
2095
2209
|
const candidateDirs = [];
|
|
2096
2210
|
if (wsGlobs) {
|
|
@@ -2103,9 +2217,9 @@ async function discoverServices(scanPath) {
|
|
|
2103
2217
|
scanPath,
|
|
2104
2218
|
{ maxDepth: parseScanDepth(), ig },
|
|
2105
2219
|
async (dir) => {
|
|
2106
|
-
if (await exists(
|
|
2220
|
+
if (await exists(import_node_path7.default.join(dir, "package.json"))) {
|
|
2107
2221
|
candidateDirs.push(dir);
|
|
2108
|
-
} else if (await exists(
|
|
2222
|
+
} 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"))) {
|
|
2109
2223
|
candidateDirs.push(dir);
|
|
2110
2224
|
}
|
|
2111
2225
|
}
|
|
@@ -2119,8 +2233,8 @@ async function discoverServices(scanPath) {
|
|
|
2119
2233
|
if (!service) continue;
|
|
2120
2234
|
const existingDir = seen.get(service.node.name);
|
|
2121
2235
|
if (existingDir !== void 0) {
|
|
2122
|
-
const a =
|
|
2123
|
-
const b =
|
|
2236
|
+
const a = import_node_path7.default.relative(scanPath, existingDir) || ".";
|
|
2237
|
+
const b = import_node_path7.default.relative(scanPath, dir) || ".";
|
|
2124
2238
|
console.warn(
|
|
2125
2239
|
`[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
|
|
2126
2240
|
);
|
|
@@ -2129,6 +2243,11 @@ async function discoverServices(scanPath) {
|
|
|
2129
2243
|
seen.set(service.node.name, dir);
|
|
2130
2244
|
out.push(service);
|
|
2131
2245
|
}
|
|
2246
|
+
const codeowners = await loadCodeowners(scanPath);
|
|
2247
|
+
for (const service of out) {
|
|
2248
|
+
const owner = await computeServiceOwner(codeowners, service.node.repoPath, service.dir);
|
|
2249
|
+
if (owner !== void 0) service.node.owner = owner;
|
|
2250
|
+
}
|
|
2132
2251
|
return out;
|
|
2133
2252
|
}
|
|
2134
2253
|
function addServiceNodes(graph, services) {
|
|
@@ -2152,8 +2271,8 @@ function addServiceNodes(graph, services) {
|
|
|
2152
2271
|
|
|
2153
2272
|
// src/extract/aliases.ts
|
|
2154
2273
|
init_cjs_shims();
|
|
2155
|
-
var
|
|
2156
|
-
var
|
|
2274
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
2275
|
+
var import_node_fs8 = require("fs");
|
|
2157
2276
|
var import_yaml2 = require("yaml");
|
|
2158
2277
|
var import_types6 = require("@neat.is/types");
|
|
2159
2278
|
var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
@@ -2180,21 +2299,29 @@ function indexServicesByName(services) {
|
|
|
2180
2299
|
const map = /* @__PURE__ */ new Map();
|
|
2181
2300
|
for (const s of services) {
|
|
2182
2301
|
map.set(s.node.name, s.node.id);
|
|
2183
|
-
map.set(
|
|
2302
|
+
map.set(import_node_path8.default.basename(s.dir), s.node.id);
|
|
2184
2303
|
}
|
|
2185
2304
|
return map;
|
|
2186
2305
|
}
|
|
2187
2306
|
async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
2188
2307
|
let composePath = null;
|
|
2189
2308
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2190
|
-
const abs =
|
|
2309
|
+
const abs = import_node_path8.default.join(scanPath, name);
|
|
2191
2310
|
if (await exists(abs)) {
|
|
2192
2311
|
composePath = abs;
|
|
2193
2312
|
break;
|
|
2194
2313
|
}
|
|
2195
2314
|
}
|
|
2196
2315
|
if (!composePath) return;
|
|
2197
|
-
|
|
2316
|
+
let compose;
|
|
2317
|
+
try {
|
|
2318
|
+
compose = await readYaml(composePath);
|
|
2319
|
+
} catch (err) {
|
|
2320
|
+
console.warn(
|
|
2321
|
+
`[neat] aliases compose skipped ${import_node_path8.default.relative(scanPath, composePath)}: ${err.message}`
|
|
2322
|
+
);
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2198
2325
|
if (!compose?.services) return;
|
|
2199
2326
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
2200
2327
|
const serviceId3 = serviceIndex.get(composeName);
|
|
@@ -2233,9 +2360,17 @@ function parseDockerfileLabels(content) {
|
|
|
2233
2360
|
}
|
|
2234
2361
|
async function collectDockerfileAliases(graph, services) {
|
|
2235
2362
|
for (const service of services) {
|
|
2236
|
-
const dockerfilePath =
|
|
2363
|
+
const dockerfilePath = import_node_path8.default.join(service.dir, "Dockerfile");
|
|
2237
2364
|
if (!await exists(dockerfilePath)) continue;
|
|
2238
|
-
|
|
2365
|
+
let content;
|
|
2366
|
+
try {
|
|
2367
|
+
content = await import_node_fs8.promises.readFile(dockerfilePath, "utf8");
|
|
2368
|
+
} catch (err) {
|
|
2369
|
+
console.warn(
|
|
2370
|
+
`[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
|
|
2371
|
+
);
|
|
2372
|
+
continue;
|
|
2373
|
+
}
|
|
2239
2374
|
const aliases = parseDockerfileLabels(content);
|
|
2240
2375
|
if (aliases.length > 0) addAliases(graph, service.node.id, aliases);
|
|
2241
2376
|
}
|
|
@@ -2243,13 +2378,13 @@ async function collectDockerfileAliases(graph, services) {
|
|
|
2243
2378
|
async function walkYamlFiles(start, depth = 0, max = 5) {
|
|
2244
2379
|
if (depth > max) return [];
|
|
2245
2380
|
const out = [];
|
|
2246
|
-
const entries = await
|
|
2381
|
+
const entries = await import_node_fs8.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
2247
2382
|
for (const entry of entries) {
|
|
2248
2383
|
if (entry.isDirectory()) {
|
|
2249
2384
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
2250
|
-
out.push(...await walkYamlFiles(
|
|
2251
|
-
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
2252
|
-
out.push(
|
|
2385
|
+
out.push(...await walkYamlFiles(import_node_path8.default.join(start, entry.name), depth + 1, max));
|
|
2386
|
+
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path8.default.extname(entry.name))) {
|
|
2387
|
+
out.push(import_node_path8.default.join(start, entry.name));
|
|
2253
2388
|
}
|
|
2254
2389
|
}
|
|
2255
2390
|
return out;
|
|
@@ -2276,7 +2411,7 @@ function k8sServiceTarget(doc, byName) {
|
|
|
2276
2411
|
async function collectK8sAliases(graph, scanPath, serviceIndex) {
|
|
2277
2412
|
const files = await walkYamlFiles(scanPath);
|
|
2278
2413
|
for (const file of files) {
|
|
2279
|
-
const content = await
|
|
2414
|
+
const content = await import_node_fs8.promises.readFile(file, "utf8");
|
|
2280
2415
|
let docs;
|
|
2281
2416
|
try {
|
|
2282
2417
|
docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
|
|
@@ -2301,14 +2436,14 @@ async function addServiceAliases(graph, scanPath, services) {
|
|
|
2301
2436
|
|
|
2302
2437
|
// src/extract/databases/index.ts
|
|
2303
2438
|
init_cjs_shims();
|
|
2304
|
-
var
|
|
2439
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
2305
2440
|
var import_types7 = require("@neat.is/types");
|
|
2306
2441
|
|
|
2307
2442
|
// src/extract/databases/db-config-yaml.ts
|
|
2308
2443
|
init_cjs_shims();
|
|
2309
|
-
var
|
|
2444
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
2310
2445
|
async function parse(serviceDir) {
|
|
2311
|
-
const yamlPath =
|
|
2446
|
+
const yamlPath = import_node_path9.default.join(serviceDir, "db-config.yaml");
|
|
2312
2447
|
if (!await exists(yamlPath)) return [];
|
|
2313
2448
|
const raw = await readYaml(yamlPath);
|
|
2314
2449
|
return [
|
|
@@ -2326,13 +2461,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
|
|
|
2326
2461
|
|
|
2327
2462
|
// src/extract/databases/dotenv.ts
|
|
2328
2463
|
init_cjs_shims();
|
|
2329
|
-
var
|
|
2330
|
-
var
|
|
2464
|
+
var import_node_fs10 = require("fs");
|
|
2465
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
2331
2466
|
|
|
2332
2467
|
// src/extract/databases/shared.ts
|
|
2333
2468
|
init_cjs_shims();
|
|
2334
|
-
var
|
|
2335
|
-
var
|
|
2469
|
+
var import_node_fs9 = require("fs");
|
|
2470
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
2336
2471
|
function schemeToEngine(scheme) {
|
|
2337
2472
|
const s = scheme.toLowerCase().split("+")[0];
|
|
2338
2473
|
switch (s) {
|
|
@@ -2371,14 +2506,14 @@ function parseConnectionString(url) {
|
|
|
2371
2506
|
}
|
|
2372
2507
|
async function readIfExists(filePath) {
|
|
2373
2508
|
try {
|
|
2374
|
-
return await
|
|
2509
|
+
return await import_node_fs9.promises.readFile(filePath, "utf8");
|
|
2375
2510
|
} catch {
|
|
2376
2511
|
return null;
|
|
2377
2512
|
}
|
|
2378
2513
|
}
|
|
2379
2514
|
async function findFirst(serviceDir, candidates) {
|
|
2380
2515
|
for (const rel of candidates) {
|
|
2381
|
-
const abs =
|
|
2516
|
+
const abs = import_node_path10.default.join(serviceDir, rel);
|
|
2382
2517
|
const content = await readIfExists(abs);
|
|
2383
2518
|
if (content !== null) return abs;
|
|
2384
2519
|
}
|
|
@@ -2429,15 +2564,15 @@ function parseDotenvLine(line) {
|
|
|
2429
2564
|
return { key, value };
|
|
2430
2565
|
}
|
|
2431
2566
|
async function parse2(serviceDir) {
|
|
2432
|
-
const entries = await
|
|
2567
|
+
const entries = await import_node_fs10.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
|
|
2433
2568
|
const configs = [];
|
|
2434
2569
|
const seen = /* @__PURE__ */ new Set();
|
|
2435
2570
|
for (const entry of entries) {
|
|
2436
2571
|
if (!entry.isFile()) continue;
|
|
2437
2572
|
const match = isConfigFile(entry.name);
|
|
2438
2573
|
if (!match.match || match.fileType !== "env") continue;
|
|
2439
|
-
const filePath =
|
|
2440
|
-
const content = await
|
|
2574
|
+
const filePath = import_node_path11.default.join(serviceDir, entry.name);
|
|
2575
|
+
const content = await import_node_fs10.promises.readFile(filePath, "utf8");
|
|
2441
2576
|
for (const line of content.split("\n")) {
|
|
2442
2577
|
const parsed = parseDotenvLine(line);
|
|
2443
2578
|
if (!parsed) continue;
|
|
@@ -2456,9 +2591,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
|
|
|
2456
2591
|
|
|
2457
2592
|
// src/extract/databases/prisma.ts
|
|
2458
2593
|
init_cjs_shims();
|
|
2459
|
-
var
|
|
2594
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
2460
2595
|
async function parse3(serviceDir) {
|
|
2461
|
-
const schemaPath =
|
|
2596
|
+
const schemaPath = import_node_path12.default.join(serviceDir, "prisma", "schema.prisma");
|
|
2462
2597
|
const content = await readIfExists(schemaPath);
|
|
2463
2598
|
if (!content) return [];
|
|
2464
2599
|
const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
|
|
@@ -2590,10 +2725,10 @@ var knexParser = { name: "knex", parse: parse5 };
|
|
|
2590
2725
|
|
|
2591
2726
|
// src/extract/databases/ormconfig.ts
|
|
2592
2727
|
init_cjs_shims();
|
|
2593
|
-
var
|
|
2728
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
2594
2729
|
async function parse6(serviceDir) {
|
|
2595
2730
|
for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
|
|
2596
|
-
const abs =
|
|
2731
|
+
const abs = import_node_path13.default.join(serviceDir, candidate);
|
|
2597
2732
|
if (!await exists(abs)) continue;
|
|
2598
2733
|
const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
|
|
2599
2734
|
const entries = Array.isArray(raw) ? raw : [raw];
|
|
@@ -2653,9 +2788,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
|
|
|
2653
2788
|
|
|
2654
2789
|
// src/extract/databases/sequelize.ts
|
|
2655
2790
|
init_cjs_shims();
|
|
2656
|
-
var
|
|
2791
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
2657
2792
|
async function parse8(serviceDir) {
|
|
2658
|
-
const configPath =
|
|
2793
|
+
const configPath = import_node_path14.default.join(serviceDir, "config", "config.json");
|
|
2659
2794
|
if (!await exists(configPath)) return [];
|
|
2660
2795
|
const raw = await readJson(configPath);
|
|
2661
2796
|
const out = [];
|
|
@@ -2682,7 +2817,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
|
|
|
2682
2817
|
|
|
2683
2818
|
// src/extract/databases/docker-compose.ts
|
|
2684
2819
|
init_cjs_shims();
|
|
2685
|
-
var
|
|
2820
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
2686
2821
|
function portFromService(svc) {
|
|
2687
2822
|
for (const raw of svc.ports ?? []) {
|
|
2688
2823
|
const str = String(raw);
|
|
@@ -2709,7 +2844,7 @@ function databaseFromEnv(svc) {
|
|
|
2709
2844
|
}
|
|
2710
2845
|
async function parse9(serviceDir) {
|
|
2711
2846
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2712
|
-
const abs =
|
|
2847
|
+
const abs = import_node_path15.default.join(serviceDir, name);
|
|
2713
2848
|
if (!await exists(abs)) continue;
|
|
2714
2849
|
const raw = await readYaml(abs);
|
|
2715
2850
|
if (!raw?.services) return [];
|
|
@@ -2889,7 +3024,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
2889
3024
|
provenance: import_types7.Provenance.EXTRACTED,
|
|
2890
3025
|
...config.sourceFile ? {
|
|
2891
3026
|
evidence: {
|
|
2892
|
-
file:
|
|
3027
|
+
file: import_node_path16.default.relative(scanPath, config.sourceFile).split(import_node_path16.default.sep).join("/")
|
|
2893
3028
|
}
|
|
2894
3029
|
} : {}
|
|
2895
3030
|
};
|
|
@@ -2917,15 +3052,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
2917
3052
|
|
|
2918
3053
|
// src/extract/configs.ts
|
|
2919
3054
|
init_cjs_shims();
|
|
2920
|
-
var
|
|
2921
|
-
var
|
|
3055
|
+
var import_node_fs11 = require("fs");
|
|
3056
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
2922
3057
|
var import_types8 = require("@neat.is/types");
|
|
2923
3058
|
async function walkConfigFiles(dir) {
|
|
2924
3059
|
const out = [];
|
|
2925
3060
|
async function walk(current) {
|
|
2926
|
-
const entries = await
|
|
3061
|
+
const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true });
|
|
2927
3062
|
for (const entry of entries) {
|
|
2928
|
-
const full =
|
|
3063
|
+
const full = import_node_path17.default.join(current, entry.name);
|
|
2929
3064
|
if (entry.isDirectory()) {
|
|
2930
3065
|
if (!IGNORED_DIRS.has(entry.name)) await walk(full);
|
|
2931
3066
|
} else if (entry.isFile() && isConfigFile(entry.name).match) {
|
|
@@ -2942,13 +3077,13 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
2942
3077
|
for (const service of services) {
|
|
2943
3078
|
const configFiles = await walkConfigFiles(service.dir);
|
|
2944
3079
|
for (const file of configFiles) {
|
|
2945
|
-
const relPath =
|
|
3080
|
+
const relPath = import_node_path17.default.relative(scanPath, file);
|
|
2946
3081
|
const node = {
|
|
2947
3082
|
id: (0, import_types8.configId)(relPath),
|
|
2948
3083
|
type: import_types8.NodeType.ConfigNode,
|
|
2949
|
-
name:
|
|
3084
|
+
name: import_node_path17.default.basename(file),
|
|
2950
3085
|
path: relPath,
|
|
2951
|
-
fileType: isConfigFile(
|
|
3086
|
+
fileType: isConfigFile(import_node_path17.default.basename(file)).fileType
|
|
2952
3087
|
};
|
|
2953
3088
|
if (!graph.hasNode(node.id)) {
|
|
2954
3089
|
graph.addNode(node.id, node);
|
|
@@ -2960,7 +3095,7 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
2960
3095
|
target: node.id,
|
|
2961
3096
|
type: import_types8.EdgeType.CONFIGURED_BY,
|
|
2962
3097
|
provenance: import_types8.Provenance.EXTRACTED,
|
|
2963
|
-
evidence: { file: relPath.split(
|
|
3098
|
+
evidence: { file: relPath.split(import_node_path17.default.sep).join("/") }
|
|
2964
3099
|
};
|
|
2965
3100
|
if (!graph.hasEdge(edge.id)) {
|
|
2966
3101
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -2977,7 +3112,7 @@ var import_types14 = require("@neat.is/types");
|
|
|
2977
3112
|
|
|
2978
3113
|
// src/extract/calls/http.ts
|
|
2979
3114
|
init_cjs_shims();
|
|
2980
|
-
var
|
|
3115
|
+
var import_node_path19 = __toESM(require("path"), 1);
|
|
2981
3116
|
var import_tree_sitter = __toESM(require("tree-sitter"), 1);
|
|
2982
3117
|
var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
|
|
2983
3118
|
var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
|
|
@@ -2985,17 +3120,17 @@ var import_types9 = require("@neat.is/types");
|
|
|
2985
3120
|
|
|
2986
3121
|
// src/extract/calls/shared.ts
|
|
2987
3122
|
init_cjs_shims();
|
|
2988
|
-
var
|
|
2989
|
-
var
|
|
3123
|
+
var import_node_fs12 = require("fs");
|
|
3124
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
2990
3125
|
async function walkSourceFiles(dir) {
|
|
2991
3126
|
const out = [];
|
|
2992
3127
|
async function walk(current) {
|
|
2993
|
-
const entries = await
|
|
3128
|
+
const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
2994
3129
|
for (const entry of entries) {
|
|
2995
|
-
const full =
|
|
3130
|
+
const full = import_node_path18.default.join(current, entry.name);
|
|
2996
3131
|
if (entry.isDirectory()) {
|
|
2997
3132
|
if (!IGNORED_DIRS.has(entry.name)) await walk(full);
|
|
2998
|
-
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(
|
|
3133
|
+
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path18.default.extname(entry.name))) {
|
|
2999
3134
|
out.push(full);
|
|
3000
3135
|
}
|
|
3001
3136
|
}
|
|
@@ -3008,7 +3143,7 @@ async function loadSourceFiles(dir) {
|
|
|
3008
3143
|
const out = [];
|
|
3009
3144
|
for (const p of paths) {
|
|
3010
3145
|
try {
|
|
3011
|
-
const content = await
|
|
3146
|
+
const content = await import_node_fs12.promises.readFile(p, "utf8");
|
|
3012
3147
|
out.push({ path: p, content });
|
|
3013
3148
|
} catch {
|
|
3014
3149
|
}
|
|
@@ -3064,9 +3199,9 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3064
3199
|
const knownHosts = /* @__PURE__ */ new Set();
|
|
3065
3200
|
const hostToNodeId = /* @__PURE__ */ new Map();
|
|
3066
3201
|
for (const service of services) {
|
|
3067
|
-
knownHosts.add(
|
|
3202
|
+
knownHosts.add(import_node_path19.default.basename(service.dir));
|
|
3068
3203
|
knownHosts.add(service.pkg.name);
|
|
3069
|
-
hostToNodeId.set(
|
|
3204
|
+
hostToNodeId.set(import_node_path19.default.basename(service.dir), service.node.id);
|
|
3070
3205
|
hostToNodeId.set(service.pkg.name, service.node.id);
|
|
3071
3206
|
}
|
|
3072
3207
|
let edgesAdded = 0;
|
|
@@ -3074,8 +3209,16 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3074
3209
|
const files = await loadSourceFiles(service.dir);
|
|
3075
3210
|
const seenTargets = /* @__PURE__ */ new Map();
|
|
3076
3211
|
for (const file of files) {
|
|
3077
|
-
const parser =
|
|
3078
|
-
|
|
3212
|
+
const parser = import_node_path19.default.extname(file.path) === ".py" ? pyParser : jsParser;
|
|
3213
|
+
let targets;
|
|
3214
|
+
try {
|
|
3215
|
+
targets = callsFromSource(file.content, parser, knownHosts);
|
|
3216
|
+
} catch (err) {
|
|
3217
|
+
console.warn(
|
|
3218
|
+
`[neat] http call extraction skipped ${file.path}: ${err.message}`
|
|
3219
|
+
);
|
|
3220
|
+
continue;
|
|
3221
|
+
}
|
|
3079
3222
|
for (const t of targets) {
|
|
3080
3223
|
const targetId = hostToNodeId.get(t);
|
|
3081
3224
|
if (!targetId || targetId === service.node.id) continue;
|
|
@@ -3094,7 +3237,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3094
3237
|
type: import_types9.EdgeType.CALLS,
|
|
3095
3238
|
provenance: import_types9.Provenance.EXTRACTED,
|
|
3096
3239
|
evidence: {
|
|
3097
|
-
file:
|
|
3240
|
+
file: import_node_path19.default.relative(service.dir, evidenceFile.file),
|
|
3098
3241
|
line,
|
|
3099
3242
|
snippet: snippet(fileContent, line)
|
|
3100
3243
|
}
|
|
@@ -3110,7 +3253,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3110
3253
|
|
|
3111
3254
|
// src/extract/calls/kafka.ts
|
|
3112
3255
|
init_cjs_shims();
|
|
3113
|
-
var
|
|
3256
|
+
var import_node_path20 = __toESM(require("path"), 1);
|
|
3114
3257
|
var import_types10 = require("@neat.is/types");
|
|
3115
3258
|
var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3116
3259
|
var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
|
|
@@ -3137,7 +3280,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3137
3280
|
kind: "kafka-topic",
|
|
3138
3281
|
edgeType,
|
|
3139
3282
|
evidence: {
|
|
3140
|
-
file:
|
|
3283
|
+
file: import_node_path20.default.relative(serviceDir, file.path),
|
|
3141
3284
|
line,
|
|
3142
3285
|
snippet: snippet(file.content, line)
|
|
3143
3286
|
}
|
|
@@ -3150,7 +3293,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3150
3293
|
|
|
3151
3294
|
// src/extract/calls/redis.ts
|
|
3152
3295
|
init_cjs_shims();
|
|
3153
|
-
var
|
|
3296
|
+
var import_node_path21 = __toESM(require("path"), 1);
|
|
3154
3297
|
var import_types11 = require("@neat.is/types");
|
|
3155
3298
|
var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
|
|
3156
3299
|
function redisEndpointsFromFile(file, serviceDir) {
|
|
@@ -3169,7 +3312,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3169
3312
|
kind: "redis",
|
|
3170
3313
|
edgeType: "CALLS",
|
|
3171
3314
|
evidence: {
|
|
3172
|
-
file:
|
|
3315
|
+
file: import_node_path21.default.relative(serviceDir, file.path),
|
|
3173
3316
|
line,
|
|
3174
3317
|
snippet: snippet(file.content, line)
|
|
3175
3318
|
}
|
|
@@ -3180,7 +3323,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3180
3323
|
|
|
3181
3324
|
// src/extract/calls/aws.ts
|
|
3182
3325
|
init_cjs_shims();
|
|
3183
|
-
var
|
|
3326
|
+
var import_node_path22 = __toESM(require("path"), 1);
|
|
3184
3327
|
var import_types12 = require("@neat.is/types");
|
|
3185
3328
|
var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3186
3329
|
var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
@@ -3210,7 +3353,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3210
3353
|
kind,
|
|
3211
3354
|
edgeType: "CALLS",
|
|
3212
3355
|
evidence: {
|
|
3213
|
-
file:
|
|
3356
|
+
file: import_node_path22.default.relative(serviceDir, file.path),
|
|
3214
3357
|
line,
|
|
3215
3358
|
snippet: snippet(file.content, line)
|
|
3216
3359
|
}
|
|
@@ -3235,7 +3378,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3235
3378
|
|
|
3236
3379
|
// src/extract/calls/grpc.ts
|
|
3237
3380
|
init_cjs_shims();
|
|
3238
|
-
var
|
|
3381
|
+
var import_node_path23 = __toESM(require("path"), 1);
|
|
3239
3382
|
var import_types13 = require("@neat.is/types");
|
|
3240
3383
|
var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
|
|
3241
3384
|
function isLikelyAddress(value) {
|
|
@@ -3260,7 +3403,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
3260
3403
|
kind: "grpc-service",
|
|
3261
3404
|
edgeType: "CALLS",
|
|
3262
3405
|
evidence: {
|
|
3263
|
-
file:
|
|
3406
|
+
file: import_node_path23.default.relative(serviceDir, file.path),
|
|
3264
3407
|
line,
|
|
3265
3408
|
snippet: snippet(file.content, line)
|
|
3266
3409
|
}
|
|
@@ -3340,7 +3483,7 @@ init_cjs_shims();
|
|
|
3340
3483
|
|
|
3341
3484
|
// src/extract/infra/docker-compose.ts
|
|
3342
3485
|
init_cjs_shims();
|
|
3343
|
-
var
|
|
3486
|
+
var import_node_path24 = __toESM(require("path"), 1);
|
|
3344
3487
|
var import_types16 = require("@neat.is/types");
|
|
3345
3488
|
|
|
3346
3489
|
// src/extract/infra/shared.ts
|
|
@@ -3378,7 +3521,7 @@ function dependsOnList(value) {
|
|
|
3378
3521
|
}
|
|
3379
3522
|
function serviceNameToServiceNode(name, services) {
|
|
3380
3523
|
for (const s of services) {
|
|
3381
|
-
if (s.node.name === name ||
|
|
3524
|
+
if (s.node.name === name || import_node_path24.default.basename(s.dir) === name) return s.node.id;
|
|
3382
3525
|
}
|
|
3383
3526
|
return null;
|
|
3384
3527
|
}
|
|
@@ -3387,16 +3530,24 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3387
3530
|
let edgesAdded = 0;
|
|
3388
3531
|
let composePath = null;
|
|
3389
3532
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
3390
|
-
const abs =
|
|
3533
|
+
const abs = import_node_path24.default.join(scanPath, name);
|
|
3391
3534
|
if (await exists(abs)) {
|
|
3392
3535
|
composePath = abs;
|
|
3393
3536
|
break;
|
|
3394
3537
|
}
|
|
3395
3538
|
}
|
|
3396
3539
|
if (!composePath) return { nodesAdded, edgesAdded };
|
|
3397
|
-
|
|
3540
|
+
let compose;
|
|
3541
|
+
try {
|
|
3542
|
+
compose = await readYaml(composePath);
|
|
3543
|
+
} catch (err) {
|
|
3544
|
+
console.warn(
|
|
3545
|
+
`[neat] infra docker-compose skipped ${import_node_path24.default.relative(scanPath, composePath)}: ${err.message}`
|
|
3546
|
+
);
|
|
3547
|
+
return { nodesAdded, edgesAdded };
|
|
3548
|
+
}
|
|
3398
3549
|
if (!compose?.services) return { nodesAdded, edgesAdded };
|
|
3399
|
-
const evidenceFile =
|
|
3550
|
+
const evidenceFile = import_node_path24.default.relative(scanPath, composePath).split(import_node_path24.default.sep).join("/");
|
|
3400
3551
|
const composeNameToNodeId = /* @__PURE__ */ new Map();
|
|
3401
3552
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
3402
3553
|
const matchedServiceId = serviceNameToServiceNode(composeName, services);
|
|
@@ -3437,8 +3588,8 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3437
3588
|
|
|
3438
3589
|
// src/extract/infra/dockerfile.ts
|
|
3439
3590
|
init_cjs_shims();
|
|
3440
|
-
var
|
|
3441
|
-
var
|
|
3591
|
+
var import_node_path25 = __toESM(require("path"), 1);
|
|
3592
|
+
var import_node_fs13 = require("fs");
|
|
3442
3593
|
var import_types17 = require("@neat.is/types");
|
|
3443
3594
|
function runtimeImage(content) {
|
|
3444
3595
|
const lines = content.split("\n");
|
|
@@ -3458,9 +3609,17 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3458
3609
|
let nodesAdded = 0;
|
|
3459
3610
|
let edgesAdded = 0;
|
|
3460
3611
|
for (const service of services) {
|
|
3461
|
-
const dockerfilePath =
|
|
3612
|
+
const dockerfilePath = import_node_path25.default.join(service.dir, "Dockerfile");
|
|
3462
3613
|
if (!await exists(dockerfilePath)) continue;
|
|
3463
|
-
|
|
3614
|
+
let content;
|
|
3615
|
+
try {
|
|
3616
|
+
content = await import_node_fs13.promises.readFile(dockerfilePath, "utf8");
|
|
3617
|
+
} catch (err) {
|
|
3618
|
+
console.warn(
|
|
3619
|
+
`[neat] infra dockerfile skipped ${import_node_path25.default.relative(scanPath, dockerfilePath)}: ${err.message}`
|
|
3620
|
+
);
|
|
3621
|
+
continue;
|
|
3622
|
+
}
|
|
3464
3623
|
const image = runtimeImage(content);
|
|
3465
3624
|
if (!image) continue;
|
|
3466
3625
|
const node = makeInfraNode("container-image", image);
|
|
@@ -3477,7 +3636,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3477
3636
|
type: import_types17.EdgeType.RUNS_ON,
|
|
3478
3637
|
provenance: import_types17.Provenance.EXTRACTED,
|
|
3479
3638
|
evidence: {
|
|
3480
|
-
file:
|
|
3639
|
+
file: import_node_path25.default.relative(scanPath, dockerfilePath).split(import_node_path25.default.sep).join("/")
|
|
3481
3640
|
}
|
|
3482
3641
|
};
|
|
3483
3642
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3489,19 +3648,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3489
3648
|
|
|
3490
3649
|
// src/extract/infra/terraform.ts
|
|
3491
3650
|
init_cjs_shims();
|
|
3492
|
-
var
|
|
3493
|
-
var
|
|
3651
|
+
var import_node_fs14 = require("fs");
|
|
3652
|
+
var import_node_path26 = __toESM(require("path"), 1);
|
|
3494
3653
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
3495
3654
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
3496
3655
|
if (depth > max) return [];
|
|
3497
3656
|
const out = [];
|
|
3498
|
-
const entries = await
|
|
3657
|
+
const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3499
3658
|
for (const entry of entries) {
|
|
3500
3659
|
if (entry.isDirectory()) {
|
|
3501
3660
|
if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
|
|
3502
|
-
out.push(...await walkTfFiles(
|
|
3661
|
+
out.push(...await walkTfFiles(import_node_path26.default.join(start, entry.name), depth + 1, max));
|
|
3503
3662
|
} else if (entry.isFile() && entry.name.endsWith(".tf")) {
|
|
3504
|
-
out.push(
|
|
3663
|
+
out.push(import_node_path26.default.join(start, entry.name));
|
|
3505
3664
|
}
|
|
3506
3665
|
}
|
|
3507
3666
|
return out;
|
|
@@ -3510,7 +3669,7 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3510
3669
|
let nodesAdded = 0;
|
|
3511
3670
|
const files = await walkTfFiles(scanPath);
|
|
3512
3671
|
for (const file of files) {
|
|
3513
|
-
const content = await
|
|
3672
|
+
const content = await import_node_fs14.promises.readFile(file, "utf8");
|
|
3514
3673
|
RESOURCE_RE.lastIndex = 0;
|
|
3515
3674
|
let m;
|
|
3516
3675
|
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
@@ -3528,8 +3687,8 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3528
3687
|
|
|
3529
3688
|
// src/extract/infra/k8s.ts
|
|
3530
3689
|
init_cjs_shims();
|
|
3531
|
-
var
|
|
3532
|
-
var
|
|
3690
|
+
var import_node_fs15 = require("fs");
|
|
3691
|
+
var import_node_path27 = __toESM(require("path"), 1);
|
|
3533
3692
|
var import_yaml3 = require("yaml");
|
|
3534
3693
|
var K8S_KIND_TO_INFRA_KIND = {
|
|
3535
3694
|
Service: "k8s-service",
|
|
@@ -3543,13 +3702,13 @@ var K8S_KIND_TO_INFRA_KIND = {
|
|
|
3543
3702
|
async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
3544
3703
|
if (depth > max) return [];
|
|
3545
3704
|
const out = [];
|
|
3546
|
-
const entries = await
|
|
3705
|
+
const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3547
3706
|
for (const entry of entries) {
|
|
3548
3707
|
if (entry.isDirectory()) {
|
|
3549
3708
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3550
|
-
out.push(...await walkYamlFiles2(
|
|
3551
|
-
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
3552
|
-
out.push(
|
|
3709
|
+
out.push(...await walkYamlFiles2(import_node_path27.default.join(start, entry.name), depth + 1, max));
|
|
3710
|
+
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path27.default.extname(entry.name))) {
|
|
3711
|
+
out.push(import_node_path27.default.join(start, entry.name));
|
|
3553
3712
|
}
|
|
3554
3713
|
}
|
|
3555
3714
|
return out;
|
|
@@ -3558,7 +3717,7 @@ async function addK8sResources(graph, scanPath) {
|
|
|
3558
3717
|
let nodesAdded = 0;
|
|
3559
3718
|
const files = await walkYamlFiles2(scanPath);
|
|
3560
3719
|
for (const file of files) {
|
|
3561
|
-
const content = await
|
|
3720
|
+
const content = await import_node_fs15.promises.readFile(file, "utf8");
|
|
3562
3721
|
let docs;
|
|
3563
3722
|
try {
|
|
3564
3723
|
docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
|
|
@@ -3604,17 +3763,28 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
3604
3763
|
const phase5 = await addInfra(graph, scanPath, services);
|
|
3605
3764
|
const frontiersPromoted = promoteFrontierNodes(graph);
|
|
3606
3765
|
if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
|
|
3607
|
-
|
|
3766
|
+
const result = {
|
|
3608
3767
|
nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
3609
3768
|
edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
3610
3769
|
frontiersPromoted
|
|
3611
3770
|
};
|
|
3771
|
+
emitNeatEvent({
|
|
3772
|
+
type: "extraction-complete",
|
|
3773
|
+
project: opts.project ?? DEFAULT_PROJECT,
|
|
3774
|
+
payload: {
|
|
3775
|
+
project: opts.project ?? DEFAULT_PROJECT,
|
|
3776
|
+
fileCount: services.length,
|
|
3777
|
+
nodesAdded: result.nodesAdded,
|
|
3778
|
+
edgesAdded: result.edgesAdded
|
|
3779
|
+
}
|
|
3780
|
+
});
|
|
3781
|
+
return result;
|
|
3612
3782
|
}
|
|
3613
3783
|
|
|
3614
3784
|
// src/persist.ts
|
|
3615
3785
|
init_cjs_shims();
|
|
3616
|
-
var
|
|
3617
|
-
var
|
|
3786
|
+
var import_node_fs16 = require("fs");
|
|
3787
|
+
var import_node_path28 = __toESM(require("path"), 1);
|
|
3618
3788
|
var SCHEMA_VERSION = 2;
|
|
3619
3789
|
function migrateV1ToV2(payload) {
|
|
3620
3790
|
const nodes = payload.graph.nodes;
|
|
@@ -3628,7 +3798,7 @@ function migrateV1ToV2(payload) {
|
|
|
3628
3798
|
return { ...payload, schemaVersion: 2 };
|
|
3629
3799
|
}
|
|
3630
3800
|
async function ensureDir(filePath) {
|
|
3631
|
-
await
|
|
3801
|
+
await import_node_fs16.promises.mkdir(import_node_path28.default.dirname(filePath), { recursive: true });
|
|
3632
3802
|
}
|
|
3633
3803
|
async function saveGraphToDisk(graph, outPath) {
|
|
3634
3804
|
await ensureDir(outPath);
|
|
@@ -3638,13 +3808,13 @@ async function saveGraphToDisk(graph, outPath) {
|
|
|
3638
3808
|
graph: graph.export()
|
|
3639
3809
|
};
|
|
3640
3810
|
const tmp = `${outPath}.tmp`;
|
|
3641
|
-
await
|
|
3642
|
-
await
|
|
3811
|
+
await import_node_fs16.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
3812
|
+
await import_node_fs16.promises.rename(tmp, outPath);
|
|
3643
3813
|
}
|
|
3644
3814
|
async function loadGraphFromDisk(graph, outPath) {
|
|
3645
3815
|
let raw;
|
|
3646
3816
|
try {
|
|
3647
|
-
raw = await
|
|
3817
|
+
raw = await import_node_fs16.promises.readFile(outPath, "utf8");
|
|
3648
3818
|
} catch (err) {
|
|
3649
3819
|
if (err.code === "ENOENT") return;
|
|
3650
3820
|
throw err;
|
|
@@ -3699,11 +3869,11 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
3699
3869
|
init_cjs_shims();
|
|
3700
3870
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
3701
3871
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
3702
|
-
var
|
|
3872
|
+
var import_types19 = require("@neat.is/types");
|
|
3703
3873
|
|
|
3704
3874
|
// src/diff.ts
|
|
3705
3875
|
init_cjs_shims();
|
|
3706
|
-
var
|
|
3876
|
+
var import_node_fs17 = require("fs");
|
|
3707
3877
|
async function loadSnapshotForDiff(target) {
|
|
3708
3878
|
if (/^https?:\/\//i.test(target)) {
|
|
3709
3879
|
const res = await fetch(target);
|
|
@@ -3712,7 +3882,7 @@ async function loadSnapshotForDiff(target) {
|
|
|
3712
3882
|
}
|
|
3713
3883
|
return await res.json();
|
|
3714
3884
|
}
|
|
3715
|
-
const raw = await
|
|
3885
|
+
const raw = await import_node_fs17.promises.readFile(target, "utf8");
|
|
3716
3886
|
return JSON.parse(raw);
|
|
3717
3887
|
}
|
|
3718
3888
|
function indexEntries(entries) {
|
|
@@ -3780,23 +3950,23 @@ function canonicalJson(value) {
|
|
|
3780
3950
|
|
|
3781
3951
|
// src/projects.ts
|
|
3782
3952
|
init_cjs_shims();
|
|
3783
|
-
var
|
|
3953
|
+
var import_node_path29 = __toESM(require("path"), 1);
|
|
3784
3954
|
function pathsForProject(project, baseDir) {
|
|
3785
3955
|
if (project === DEFAULT_PROJECT) {
|
|
3786
3956
|
return {
|
|
3787
|
-
snapshotPath:
|
|
3788
|
-
errorsPath:
|
|
3789
|
-
staleEventsPath:
|
|
3790
|
-
embeddingsCachePath:
|
|
3791
|
-
policyViolationsPath:
|
|
3957
|
+
snapshotPath: import_node_path29.default.join(baseDir, "graph.json"),
|
|
3958
|
+
errorsPath: import_node_path29.default.join(baseDir, "errors.ndjson"),
|
|
3959
|
+
staleEventsPath: import_node_path29.default.join(baseDir, "stale-events.ndjson"),
|
|
3960
|
+
embeddingsCachePath: import_node_path29.default.join(baseDir, "embeddings.json"),
|
|
3961
|
+
policyViolationsPath: import_node_path29.default.join(baseDir, "policy-violations.ndjson")
|
|
3792
3962
|
};
|
|
3793
3963
|
}
|
|
3794
3964
|
return {
|
|
3795
|
-
snapshotPath:
|
|
3796
|
-
errorsPath:
|
|
3797
|
-
staleEventsPath:
|
|
3798
|
-
embeddingsCachePath:
|
|
3799
|
-
policyViolationsPath:
|
|
3965
|
+
snapshotPath: import_node_path29.default.join(baseDir, `${project}.json`),
|
|
3966
|
+
errorsPath: import_node_path29.default.join(baseDir, `errors.${project}.ndjson`),
|
|
3967
|
+
staleEventsPath: import_node_path29.default.join(baseDir, `stale-events.${project}.ndjson`),
|
|
3968
|
+
embeddingsCachePath: import_node_path29.default.join(baseDir, `embeddings.${project}.json`),
|
|
3969
|
+
policyViolationsPath: import_node_path29.default.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
3800
3970
|
};
|
|
3801
3971
|
}
|
|
3802
3972
|
var Projects = class {
|
|
@@ -3830,6 +4000,229 @@ var Projects = class {
|
|
|
3830
4000
|
}
|
|
3831
4001
|
};
|
|
3832
4002
|
|
|
4003
|
+
// src/registry.ts
|
|
4004
|
+
init_cjs_shims();
|
|
4005
|
+
var import_node_fs18 = require("fs");
|
|
4006
|
+
var import_node_os2 = __toESM(require("os"), 1);
|
|
4007
|
+
var import_node_path30 = __toESM(require("path"), 1);
|
|
4008
|
+
var import_types18 = require("@neat.is/types");
|
|
4009
|
+
var LOCK_TIMEOUT_MS = 5e3;
|
|
4010
|
+
var LOCK_RETRY_MS = 50;
|
|
4011
|
+
function neatHome() {
|
|
4012
|
+
const override = process.env.NEAT_HOME;
|
|
4013
|
+
if (override && override.length > 0) return import_node_path30.default.resolve(override);
|
|
4014
|
+
return import_node_path30.default.join(import_node_os2.default.homedir(), ".neat");
|
|
4015
|
+
}
|
|
4016
|
+
function registryPath() {
|
|
4017
|
+
return import_node_path30.default.join(neatHome(), "projects.json");
|
|
4018
|
+
}
|
|
4019
|
+
function registryLockPath() {
|
|
4020
|
+
return import_node_path30.default.join(neatHome(), "projects.json.lock");
|
|
4021
|
+
}
|
|
4022
|
+
async function normalizeProjectPath(input) {
|
|
4023
|
+
const resolved = import_node_path30.default.resolve(input);
|
|
4024
|
+
try {
|
|
4025
|
+
return await import_node_fs18.promises.realpath(resolved);
|
|
4026
|
+
} catch {
|
|
4027
|
+
return resolved;
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
async function writeAtomically(target, contents) {
|
|
4031
|
+
await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(target), { recursive: true });
|
|
4032
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
4033
|
+
const fd = await import_node_fs18.promises.open(tmp, "w");
|
|
4034
|
+
try {
|
|
4035
|
+
await fd.writeFile(contents, "utf8");
|
|
4036
|
+
await fd.sync();
|
|
4037
|
+
} finally {
|
|
4038
|
+
await fd.close();
|
|
4039
|
+
}
|
|
4040
|
+
await import_node_fs18.promises.rename(tmp, target);
|
|
4041
|
+
}
|
|
4042
|
+
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
4043
|
+
const deadline = Date.now() + timeoutMs;
|
|
4044
|
+
await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(lockPath), { recursive: true });
|
|
4045
|
+
while (true) {
|
|
4046
|
+
try {
|
|
4047
|
+
const fd = await import_node_fs18.promises.open(lockPath, "wx");
|
|
4048
|
+
await fd.close();
|
|
4049
|
+
return;
|
|
4050
|
+
} catch (err) {
|
|
4051
|
+
const code = err.code;
|
|
4052
|
+
if (code !== "EEXIST") throw err;
|
|
4053
|
+
if (Date.now() >= deadline) {
|
|
4054
|
+
throw new Error(
|
|
4055
|
+
`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.`
|
|
4056
|
+
);
|
|
4057
|
+
}
|
|
4058
|
+
await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
async function releaseLock(lockPath) {
|
|
4063
|
+
await import_node_fs18.promises.unlink(lockPath).catch(() => {
|
|
4064
|
+
});
|
|
4065
|
+
}
|
|
4066
|
+
async function withLock(fn) {
|
|
4067
|
+
const lock = registryLockPath();
|
|
4068
|
+
await acquireLock(lock);
|
|
4069
|
+
try {
|
|
4070
|
+
return await fn();
|
|
4071
|
+
} finally {
|
|
4072
|
+
await releaseLock(lock);
|
|
4073
|
+
}
|
|
4074
|
+
}
|
|
4075
|
+
async function readRegistry() {
|
|
4076
|
+
const file = registryPath();
|
|
4077
|
+
let raw;
|
|
4078
|
+
try {
|
|
4079
|
+
raw = await import_node_fs18.promises.readFile(file, "utf8");
|
|
4080
|
+
} catch (err) {
|
|
4081
|
+
if (err.code === "ENOENT") {
|
|
4082
|
+
return { version: 1, projects: [] };
|
|
4083
|
+
}
|
|
4084
|
+
throw err;
|
|
4085
|
+
}
|
|
4086
|
+
const parsed = JSON.parse(raw);
|
|
4087
|
+
return import_types18.RegistryFileSchema.parse(parsed);
|
|
4088
|
+
}
|
|
4089
|
+
async function writeRegistry(reg) {
|
|
4090
|
+
const validated = import_types18.RegistryFileSchema.parse(reg);
|
|
4091
|
+
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
4092
|
+
}
|
|
4093
|
+
var ProjectNameCollisionError = class extends Error {
|
|
4094
|
+
projectName;
|
|
4095
|
+
constructor(name) {
|
|
4096
|
+
super(`neat registry: a project named "${name}" is already registered`);
|
|
4097
|
+
this.name = "ProjectNameCollisionError";
|
|
4098
|
+
this.projectName = name;
|
|
4099
|
+
}
|
|
4100
|
+
};
|
|
4101
|
+
async function addProject(opts) {
|
|
4102
|
+
const resolvedPath = await normalizeProjectPath(opts.path);
|
|
4103
|
+
return withLock(async () => {
|
|
4104
|
+
const reg = await readRegistry();
|
|
4105
|
+
const byName = reg.projects.find((p) => p.name === opts.name);
|
|
4106
|
+
const byPath = reg.projects.find((p) => p.path === resolvedPath);
|
|
4107
|
+
if (byName && byName.path !== resolvedPath) {
|
|
4108
|
+
throw new ProjectNameCollisionError(opts.name);
|
|
4109
|
+
}
|
|
4110
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4111
|
+
if (byName && byName.path === resolvedPath) {
|
|
4112
|
+
byName.lastSeenAt = now;
|
|
4113
|
+
if (opts.languages) byName.languages = opts.languages;
|
|
4114
|
+
if (opts.status) byName.status = opts.status;
|
|
4115
|
+
await writeRegistry(reg);
|
|
4116
|
+
return byName;
|
|
4117
|
+
}
|
|
4118
|
+
if (byPath && byPath.name !== opts.name) {
|
|
4119
|
+
throw new ProjectNameCollisionError(byPath.name);
|
|
4120
|
+
}
|
|
4121
|
+
const entry = {
|
|
4122
|
+
name: opts.name,
|
|
4123
|
+
path: resolvedPath,
|
|
4124
|
+
registeredAt: now,
|
|
4125
|
+
languages: opts.languages ?? [],
|
|
4126
|
+
status: opts.status ?? "active"
|
|
4127
|
+
};
|
|
4128
|
+
reg.projects.push(entry);
|
|
4129
|
+
await writeRegistry(reg);
|
|
4130
|
+
return entry;
|
|
4131
|
+
});
|
|
4132
|
+
}
|
|
4133
|
+
async function getProject(name) {
|
|
4134
|
+
const reg = await readRegistry();
|
|
4135
|
+
return reg.projects.find((p) => p.name === name);
|
|
4136
|
+
}
|
|
4137
|
+
async function listProjects() {
|
|
4138
|
+
const reg = await readRegistry();
|
|
4139
|
+
return reg.projects;
|
|
4140
|
+
}
|
|
4141
|
+
async function setStatus(name, status2) {
|
|
4142
|
+
return withLock(async () => {
|
|
4143
|
+
const reg = await readRegistry();
|
|
4144
|
+
const entry = reg.projects.find((p) => p.name === name);
|
|
4145
|
+
if (!entry) throw new Error(`neat registry: no project named "${name}"`);
|
|
4146
|
+
entry.status = status2;
|
|
4147
|
+
await writeRegistry(reg);
|
|
4148
|
+
return entry;
|
|
4149
|
+
});
|
|
4150
|
+
}
|
|
4151
|
+
async function touchLastSeen(name, at = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
4152
|
+
await withLock(async () => {
|
|
4153
|
+
const reg = await readRegistry();
|
|
4154
|
+
const entry = reg.projects.find((p) => p.name === name);
|
|
4155
|
+
if (!entry) return;
|
|
4156
|
+
entry.lastSeenAt = at;
|
|
4157
|
+
await writeRegistry(reg);
|
|
4158
|
+
});
|
|
4159
|
+
}
|
|
4160
|
+
async function removeProject(name) {
|
|
4161
|
+
return withLock(async () => {
|
|
4162
|
+
const reg = await readRegistry();
|
|
4163
|
+
const idx = reg.projects.findIndex((p) => p.name === name);
|
|
4164
|
+
if (idx < 0) return void 0;
|
|
4165
|
+
const [removed] = reg.projects.splice(idx, 1);
|
|
4166
|
+
await writeRegistry(reg);
|
|
4167
|
+
return removed;
|
|
4168
|
+
});
|
|
4169
|
+
}
|
|
4170
|
+
|
|
4171
|
+
// src/streaming.ts
|
|
4172
|
+
init_cjs_shims();
|
|
4173
|
+
var SSE_HEARTBEAT_MS = 3e4;
|
|
4174
|
+
var SSE_BACKPRESSURE_CAP = 1e3;
|
|
4175
|
+
function handleSse(req, reply, opts) {
|
|
4176
|
+
const heartbeatMs = opts.heartbeatMs ?? SSE_HEARTBEAT_MS;
|
|
4177
|
+
const backpressureCap = opts.backpressureCap ?? SSE_BACKPRESSURE_CAP;
|
|
4178
|
+
reply.raw.setHeader("Content-Type", "text/event-stream");
|
|
4179
|
+
reply.raw.setHeader("Cache-Control", "no-cache, no-transform");
|
|
4180
|
+
reply.raw.setHeader("Connection", "keep-alive");
|
|
4181
|
+
reply.raw.setHeader("X-Accel-Buffering", "no");
|
|
4182
|
+
reply.raw.flushHeaders?.();
|
|
4183
|
+
let pending = 0;
|
|
4184
|
+
let dropped = false;
|
|
4185
|
+
const closeConnection = () => {
|
|
4186
|
+
if (dropped) return;
|
|
4187
|
+
dropped = true;
|
|
4188
|
+
eventBus.off(EVENT_BUS_CHANNEL, listener);
|
|
4189
|
+
clearInterval(heartbeat);
|
|
4190
|
+
if (!reply.raw.writableEnded) reply.raw.end();
|
|
4191
|
+
};
|
|
4192
|
+
const writeFrame = (frame) => {
|
|
4193
|
+
if (dropped) return;
|
|
4194
|
+
if (pending >= backpressureCap) {
|
|
4195
|
+
const errFrame = `event: error
|
|
4196
|
+
data: ${JSON.stringify({ reason: "backpressure" })}
|
|
4197
|
+
|
|
4198
|
+
`;
|
|
4199
|
+
reply.raw.write(errFrame);
|
|
4200
|
+
closeConnection();
|
|
4201
|
+
return;
|
|
4202
|
+
}
|
|
4203
|
+
pending++;
|
|
4204
|
+
reply.raw.write(frame, () => {
|
|
4205
|
+
pending = Math.max(0, pending - 1);
|
|
4206
|
+
});
|
|
4207
|
+
};
|
|
4208
|
+
const listener = (envelope) => {
|
|
4209
|
+
if (envelope.project !== opts.project) return;
|
|
4210
|
+
writeFrame(`event: ${envelope.type}
|
|
4211
|
+
data: ${JSON.stringify(envelope.payload)}
|
|
4212
|
+
|
|
4213
|
+
`);
|
|
4214
|
+
};
|
|
4215
|
+
eventBus.on(EVENT_BUS_CHANNEL, listener);
|
|
4216
|
+
const heartbeat = setInterval(() => {
|
|
4217
|
+
if (dropped) return;
|
|
4218
|
+
reply.raw.write(":heartbeat\n\n");
|
|
4219
|
+
}, heartbeatMs);
|
|
4220
|
+
if (typeof heartbeat.unref === "function") heartbeat.unref();
|
|
4221
|
+
req.raw.on("close", closeConnection);
|
|
4222
|
+
reply.raw.on("close", closeConnection);
|
|
4223
|
+
reply.raw.on("error", closeConnection);
|
|
4224
|
+
}
|
|
4225
|
+
|
|
3833
4226
|
// src/api.ts
|
|
3834
4227
|
function serializeGraph(graph) {
|
|
3835
4228
|
const nodes = [];
|
|
@@ -3878,6 +4271,11 @@ function buildLegacyRegistry(opts) {
|
|
|
3878
4271
|
}
|
|
3879
4272
|
function registerRoutes(scope, ctx) {
|
|
3880
4273
|
const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
|
|
4274
|
+
scope.get("/events", (req, reply) => {
|
|
4275
|
+
const proj = resolveProject(registry, req, reply);
|
|
4276
|
+
if (!proj) return;
|
|
4277
|
+
handleSse(req, reply, { project: proj.name });
|
|
4278
|
+
});
|
|
3881
4279
|
scope.get("/health", async (req, reply) => {
|
|
3882
4280
|
const proj = resolveProject(registry, req, reply);
|
|
3883
4281
|
if (!proj) return;
|
|
@@ -4088,7 +4486,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4088
4486
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
4089
4487
|
let violations = await log.readAll();
|
|
4090
4488
|
if (req.query.severity) {
|
|
4091
|
-
const sev =
|
|
4489
|
+
const sev = import_types19.PolicySeveritySchema.safeParse(req.query.severity);
|
|
4092
4490
|
if (!sev.success) {
|
|
4093
4491
|
return reply.code(400).send({
|
|
4094
4492
|
error: "invalid severity",
|
|
@@ -4105,7 +4503,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4105
4503
|
scope.post("/policies/check", async (req, reply) => {
|
|
4106
4504
|
const proj = resolveProject(registry, req, reply);
|
|
4107
4505
|
if (!proj) return;
|
|
4108
|
-
const parsed =
|
|
4506
|
+
const parsed = import_types19.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
4109
4507
|
if (!parsed.success) {
|
|
4110
4508
|
return reply.code(400).send({
|
|
4111
4509
|
error: "invalid /policies/check body",
|
|
@@ -4169,17 +4567,16 @@ async function buildApi(opts) {
|
|
|
4169
4567
|
staleEventsPathFor,
|
|
4170
4568
|
policyFilePathFor
|
|
4171
4569
|
};
|
|
4172
|
-
app.get("/projects", async () =>
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
}));
|
|
4570
|
+
app.get("/projects", async (_req, reply) => {
|
|
4571
|
+
try {
|
|
4572
|
+
return await listProjects();
|
|
4573
|
+
} catch (err) {
|
|
4574
|
+
return reply.code(500).send({
|
|
4575
|
+
error: "failed to read project registry",
|
|
4576
|
+
details: err.message
|
|
4577
|
+
});
|
|
4578
|
+
}
|
|
4579
|
+
});
|
|
4183
4580
|
registerRoutes(app, routeCtx);
|
|
4184
4581
|
await app.register(
|
|
4185
4582
|
async (scope) => {
|
|
@@ -4196,184 +4593,14 @@ init_otel_grpc();
|
|
|
4196
4593
|
|
|
4197
4594
|
// src/daemon.ts
|
|
4198
4595
|
init_cjs_shims();
|
|
4199
|
-
var
|
|
4200
|
-
var
|
|
4201
|
-
|
|
4202
|
-
// src/registry.ts
|
|
4203
|
-
init_cjs_shims();
|
|
4204
|
-
var import_node_fs17 = require("fs");
|
|
4205
|
-
var import_node_os2 = __toESM(require("os"), 1);
|
|
4206
|
-
var import_node_path31 = __toESM(require("path"), 1);
|
|
4207
|
-
var import_types19 = require("@neat.is/types");
|
|
4208
|
-
var LOCK_TIMEOUT_MS = 5e3;
|
|
4209
|
-
var LOCK_RETRY_MS = 50;
|
|
4210
|
-
function neatHome() {
|
|
4211
|
-
const override = process.env.NEAT_HOME;
|
|
4212
|
-
if (override && override.length > 0) return import_node_path31.default.resolve(override);
|
|
4213
|
-
return import_node_path31.default.join(import_node_os2.default.homedir(), ".neat");
|
|
4214
|
-
}
|
|
4215
|
-
function registryPath() {
|
|
4216
|
-
return import_node_path31.default.join(neatHome(), "projects.json");
|
|
4217
|
-
}
|
|
4218
|
-
function registryLockPath() {
|
|
4219
|
-
return import_node_path31.default.join(neatHome(), "projects.json.lock");
|
|
4220
|
-
}
|
|
4221
|
-
async function normalizeProjectPath(input) {
|
|
4222
|
-
const resolved = import_node_path31.default.resolve(input);
|
|
4223
|
-
try {
|
|
4224
|
-
return await import_node_fs17.promises.realpath(resolved);
|
|
4225
|
-
} catch {
|
|
4226
|
-
return resolved;
|
|
4227
|
-
}
|
|
4228
|
-
}
|
|
4229
|
-
async function writeAtomically(target, contents) {
|
|
4230
|
-
await import_node_fs17.promises.mkdir(import_node_path31.default.dirname(target), { recursive: true });
|
|
4231
|
-
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
4232
|
-
const fd = await import_node_fs17.promises.open(tmp, "w");
|
|
4233
|
-
try {
|
|
4234
|
-
await fd.writeFile(contents, "utf8");
|
|
4235
|
-
await fd.sync();
|
|
4236
|
-
} finally {
|
|
4237
|
-
await fd.close();
|
|
4238
|
-
}
|
|
4239
|
-
await import_node_fs17.promises.rename(tmp, target);
|
|
4240
|
-
}
|
|
4241
|
-
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
4242
|
-
const deadline = Date.now() + timeoutMs;
|
|
4243
|
-
await import_node_fs17.promises.mkdir(import_node_path31.default.dirname(lockPath), { recursive: true });
|
|
4244
|
-
while (true) {
|
|
4245
|
-
try {
|
|
4246
|
-
const fd = await import_node_fs17.promises.open(lockPath, "wx");
|
|
4247
|
-
await fd.close();
|
|
4248
|
-
return;
|
|
4249
|
-
} catch (err) {
|
|
4250
|
-
const code = err.code;
|
|
4251
|
-
if (code !== "EEXIST") throw err;
|
|
4252
|
-
if (Date.now() >= deadline) {
|
|
4253
|
-
throw new Error(
|
|
4254
|
-
`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.`
|
|
4255
|
-
);
|
|
4256
|
-
}
|
|
4257
|
-
await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
|
|
4258
|
-
}
|
|
4259
|
-
}
|
|
4260
|
-
}
|
|
4261
|
-
async function releaseLock(lockPath) {
|
|
4262
|
-
await import_node_fs17.promises.unlink(lockPath).catch(() => {
|
|
4263
|
-
});
|
|
4264
|
-
}
|
|
4265
|
-
async function withLock(fn) {
|
|
4266
|
-
const lock = registryLockPath();
|
|
4267
|
-
await acquireLock(lock);
|
|
4268
|
-
try {
|
|
4269
|
-
return await fn();
|
|
4270
|
-
} finally {
|
|
4271
|
-
await releaseLock(lock);
|
|
4272
|
-
}
|
|
4273
|
-
}
|
|
4274
|
-
async function readRegistry() {
|
|
4275
|
-
const file = registryPath();
|
|
4276
|
-
let raw;
|
|
4277
|
-
try {
|
|
4278
|
-
raw = await import_node_fs17.promises.readFile(file, "utf8");
|
|
4279
|
-
} catch (err) {
|
|
4280
|
-
if (err.code === "ENOENT") {
|
|
4281
|
-
return { version: 1, projects: [] };
|
|
4282
|
-
}
|
|
4283
|
-
throw err;
|
|
4284
|
-
}
|
|
4285
|
-
const parsed = JSON.parse(raw);
|
|
4286
|
-
return import_types19.RegistryFileSchema.parse(parsed);
|
|
4287
|
-
}
|
|
4288
|
-
async function writeRegistry(reg) {
|
|
4289
|
-
const validated = import_types19.RegistryFileSchema.parse(reg);
|
|
4290
|
-
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
4291
|
-
}
|
|
4292
|
-
var ProjectNameCollisionError = class extends Error {
|
|
4293
|
-
projectName;
|
|
4294
|
-
constructor(name) {
|
|
4295
|
-
super(`neat registry: a project named "${name}" is already registered`);
|
|
4296
|
-
this.name = "ProjectNameCollisionError";
|
|
4297
|
-
this.projectName = name;
|
|
4298
|
-
}
|
|
4299
|
-
};
|
|
4300
|
-
async function addProject(opts) {
|
|
4301
|
-
const resolvedPath = await normalizeProjectPath(opts.path);
|
|
4302
|
-
return withLock(async () => {
|
|
4303
|
-
const reg = await readRegistry();
|
|
4304
|
-
const byName = reg.projects.find((p) => p.name === opts.name);
|
|
4305
|
-
const byPath = reg.projects.find((p) => p.path === resolvedPath);
|
|
4306
|
-
if (byName && byName.path !== resolvedPath) {
|
|
4307
|
-
throw new ProjectNameCollisionError(opts.name);
|
|
4308
|
-
}
|
|
4309
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4310
|
-
if (byName && byName.path === resolvedPath) {
|
|
4311
|
-
byName.lastSeenAt = now;
|
|
4312
|
-
if (opts.languages) byName.languages = opts.languages;
|
|
4313
|
-
if (opts.status) byName.status = opts.status;
|
|
4314
|
-
await writeRegistry(reg);
|
|
4315
|
-
return byName;
|
|
4316
|
-
}
|
|
4317
|
-
if (byPath && byPath.name !== opts.name) {
|
|
4318
|
-
throw new ProjectNameCollisionError(byPath.name);
|
|
4319
|
-
}
|
|
4320
|
-
const entry = {
|
|
4321
|
-
name: opts.name,
|
|
4322
|
-
path: resolvedPath,
|
|
4323
|
-
registeredAt: now,
|
|
4324
|
-
languages: opts.languages ?? [],
|
|
4325
|
-
status: opts.status ?? "active"
|
|
4326
|
-
};
|
|
4327
|
-
reg.projects.push(entry);
|
|
4328
|
-
await writeRegistry(reg);
|
|
4329
|
-
return entry;
|
|
4330
|
-
});
|
|
4331
|
-
}
|
|
4332
|
-
async function getProject(name) {
|
|
4333
|
-
const reg = await readRegistry();
|
|
4334
|
-
return reg.projects.find((p) => p.name === name);
|
|
4335
|
-
}
|
|
4336
|
-
async function listProjects() {
|
|
4337
|
-
const reg = await readRegistry();
|
|
4338
|
-
return reg.projects;
|
|
4339
|
-
}
|
|
4340
|
-
async function setStatus(name, status2) {
|
|
4341
|
-
return withLock(async () => {
|
|
4342
|
-
const reg = await readRegistry();
|
|
4343
|
-
const entry = reg.projects.find((p) => p.name === name);
|
|
4344
|
-
if (!entry) throw new Error(`neat registry: no project named "${name}"`);
|
|
4345
|
-
entry.status = status2;
|
|
4346
|
-
await writeRegistry(reg);
|
|
4347
|
-
return entry;
|
|
4348
|
-
});
|
|
4349
|
-
}
|
|
4350
|
-
async function touchLastSeen(name, at = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
4351
|
-
await withLock(async () => {
|
|
4352
|
-
const reg = await readRegistry();
|
|
4353
|
-
const entry = reg.projects.find((p) => p.name === name);
|
|
4354
|
-
if (!entry) return;
|
|
4355
|
-
entry.lastSeenAt = at;
|
|
4356
|
-
await writeRegistry(reg);
|
|
4357
|
-
});
|
|
4358
|
-
}
|
|
4359
|
-
async function removeProject(name) {
|
|
4360
|
-
return withLock(async () => {
|
|
4361
|
-
const reg = await readRegistry();
|
|
4362
|
-
const idx = reg.projects.findIndex((p) => p.name === name);
|
|
4363
|
-
if (idx < 0) return void 0;
|
|
4364
|
-
const [removed] = reg.projects.splice(idx, 1);
|
|
4365
|
-
await writeRegistry(reg);
|
|
4366
|
-
return removed;
|
|
4367
|
-
});
|
|
4368
|
-
}
|
|
4369
|
-
|
|
4370
|
-
// src/daemon.ts
|
|
4596
|
+
var import_node_fs19 = require("fs");
|
|
4597
|
+
var import_node_path33 = __toESM(require("path"), 1);
|
|
4371
4598
|
function neatHomeFor(opts) {
|
|
4372
|
-
if (opts.neatHome && opts.neatHome.length > 0) return
|
|
4599
|
+
if (opts.neatHome && opts.neatHome.length > 0) return import_node_path33.default.resolve(opts.neatHome);
|
|
4373
4600
|
const env = process.env.NEAT_HOME;
|
|
4374
|
-
if (env && env.length > 0) return
|
|
4601
|
+
if (env && env.length > 0) return import_node_path33.default.resolve(env);
|
|
4375
4602
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
4376
|
-
return
|
|
4603
|
+
return import_node_path33.default.join(home, ".neat");
|
|
4377
4604
|
}
|
|
4378
4605
|
function routeSpanToProject(serviceName, projects) {
|
|
4379
4606
|
if (!serviceName) return DEFAULT_PROJECT;
|
|
@@ -4387,7 +4614,7 @@ function routeSpanToProject(serviceName, projects) {
|
|
|
4387
4614
|
}
|
|
4388
4615
|
async function bootstrapProject(entry) {
|
|
4389
4616
|
try {
|
|
4390
|
-
const stat = await
|
|
4617
|
+
const stat = await import_node_fs19.promises.stat(entry.path);
|
|
4391
4618
|
if (!stat.isDirectory()) {
|
|
4392
4619
|
throw new Error(`registered path ${entry.path} is not a directory`);
|
|
4393
4620
|
}
|
|
@@ -4410,7 +4637,7 @@ async function bootstrapProject(entry) {
|
|
|
4410
4637
|
const graph = getGraph(entry.name);
|
|
4411
4638
|
const outPath = pathsForProject(
|
|
4412
4639
|
entry.name,
|
|
4413
|
-
|
|
4640
|
+
import_node_path33.default.join(entry.path, "neat-out")
|
|
4414
4641
|
).snapshotPath;
|
|
4415
4642
|
await loadGraphFromDisk(graph, outPath);
|
|
4416
4643
|
await extractFromDirectory(graph, entry.path);
|
|
@@ -4429,13 +4656,13 @@ async function startDaemon(opts = {}) {
|
|
|
4429
4656
|
const home = neatHomeFor(opts);
|
|
4430
4657
|
const regPath = registryPath();
|
|
4431
4658
|
try {
|
|
4432
|
-
await
|
|
4659
|
+
await import_node_fs19.promises.access(regPath);
|
|
4433
4660
|
} catch {
|
|
4434
4661
|
throw new Error(
|
|
4435
4662
|
`neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
|
|
4436
4663
|
);
|
|
4437
4664
|
}
|
|
4438
|
-
const pidPath =
|
|
4665
|
+
const pidPath = import_node_path33.default.join(home, "neatd.pid");
|
|
4439
4666
|
await writeAtomically(pidPath, `${process.pid}
|
|
4440
4667
|
`);
|
|
4441
4668
|
const slots = /* @__PURE__ */ new Map();
|
|
@@ -4501,7 +4728,7 @@ async function startDaemon(opts = {}) {
|
|
|
4501
4728
|
} catch {
|
|
4502
4729
|
}
|
|
4503
4730
|
}
|
|
4504
|
-
await
|
|
4731
|
+
await import_node_fs19.promises.unlink(pidPath).catch(() => {
|
|
4505
4732
|
});
|
|
4506
4733
|
};
|
|
4507
4734
|
return { slots, reload, stop, pidPath };
|