@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
|
@@ -427,19 +427,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
427
427
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
428
428
|
let best = { path: [start], edges: [] };
|
|
429
429
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
430
|
-
function step(node,
|
|
431
|
-
if (
|
|
432
|
-
best = { path: [...
|
|
430
|
+
function step(node, path31, edges) {
|
|
431
|
+
if (path31.length > best.path.length) {
|
|
432
|
+
best = { path: [...path31], edges: [...edges] };
|
|
433
433
|
}
|
|
434
|
-
if (
|
|
434
|
+
if (path31.length - 1 >= maxDepth) return;
|
|
435
435
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
436
436
|
for (const [srcId, edge] of incoming) {
|
|
437
437
|
if (visited.has(srcId)) continue;
|
|
438
438
|
visited.add(srcId);
|
|
439
|
-
|
|
439
|
+
path31.push(srcId);
|
|
440
440
|
edges.push(edge);
|
|
441
|
-
step(srcId,
|
|
442
|
-
|
|
441
|
+
step(srcId, path31, edges);
|
|
442
|
+
path31.pop();
|
|
443
443
|
edges.pop();
|
|
444
444
|
visited.delete(srcId);
|
|
445
445
|
}
|
|
@@ -634,6 +634,69 @@ import {
|
|
|
634
634
|
PolicyFileSchema,
|
|
635
635
|
Provenance as Provenance2
|
|
636
636
|
} from "@neat.is/types";
|
|
637
|
+
|
|
638
|
+
// src/events.ts
|
|
639
|
+
import { EventEmitter } from "events";
|
|
640
|
+
var EVENT_BUS_CHANNEL = "event";
|
|
641
|
+
var NeatEventBus = class extends EventEmitter {
|
|
642
|
+
};
|
|
643
|
+
var eventBus = new NeatEventBus();
|
|
644
|
+
eventBus.setMaxListeners(0);
|
|
645
|
+
function emitNeatEvent(envelope) {
|
|
646
|
+
eventBus.emit(EVENT_BUS_CHANNEL, envelope);
|
|
647
|
+
}
|
|
648
|
+
function attachGraphToEventBus(graph, opts) {
|
|
649
|
+
const { project } = opts;
|
|
650
|
+
const onNodeAdded = (payload) => {
|
|
651
|
+
emitNeatEvent({
|
|
652
|
+
type: "node-added",
|
|
653
|
+
project,
|
|
654
|
+
payload: { node: payload.attributes }
|
|
655
|
+
});
|
|
656
|
+
};
|
|
657
|
+
const onNodeDropped = (payload) => {
|
|
658
|
+
emitNeatEvent({
|
|
659
|
+
type: "node-removed",
|
|
660
|
+
project,
|
|
661
|
+
payload: { id: payload.key }
|
|
662
|
+
});
|
|
663
|
+
};
|
|
664
|
+
const onEdgeAdded = (payload) => {
|
|
665
|
+
emitNeatEvent({
|
|
666
|
+
type: "edge-added",
|
|
667
|
+
project,
|
|
668
|
+
payload: { edge: payload.attributes }
|
|
669
|
+
});
|
|
670
|
+
};
|
|
671
|
+
const onEdgeDropped = (payload) => {
|
|
672
|
+
emitNeatEvent({
|
|
673
|
+
type: "edge-removed",
|
|
674
|
+
project,
|
|
675
|
+
payload: { id: payload.key }
|
|
676
|
+
});
|
|
677
|
+
};
|
|
678
|
+
const onNodeAttrsUpdated = (payload) => {
|
|
679
|
+
emitNeatEvent({
|
|
680
|
+
type: "node-updated",
|
|
681
|
+
project,
|
|
682
|
+
payload: { id: payload.key, changes: payload.attributes }
|
|
683
|
+
});
|
|
684
|
+
};
|
|
685
|
+
graph.on("nodeAdded", onNodeAdded);
|
|
686
|
+
graph.on("nodeDropped", onNodeDropped);
|
|
687
|
+
graph.on("edgeAdded", onEdgeAdded);
|
|
688
|
+
graph.on("edgeDropped", onEdgeDropped);
|
|
689
|
+
graph.on("nodeAttributesUpdated", onNodeAttrsUpdated);
|
|
690
|
+
return () => {
|
|
691
|
+
graph.off("nodeAdded", onNodeAdded);
|
|
692
|
+
graph.off("nodeDropped", onNodeDropped);
|
|
693
|
+
graph.off("edgeAdded", onEdgeAdded);
|
|
694
|
+
graph.off("edgeDropped", onEdgeDropped);
|
|
695
|
+
graph.off("nodeAttributesUpdated", onNodeAttrsUpdated);
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/policy.ts
|
|
637
700
|
var DEFAULT_ACTION_BY_SEVERITY = {
|
|
638
701
|
info: "log",
|
|
639
702
|
warning: "alert",
|
|
@@ -915,9 +978,11 @@ async function loadPolicyFile(policyPath) {
|
|
|
915
978
|
}
|
|
916
979
|
var PolicyViolationsLog = class {
|
|
917
980
|
path;
|
|
981
|
+
project;
|
|
918
982
|
seen = null;
|
|
919
|
-
constructor(logPath) {
|
|
983
|
+
constructor(logPath, project = DEFAULT_PROJECT) {
|
|
920
984
|
this.path = logPath;
|
|
985
|
+
this.project = project;
|
|
921
986
|
}
|
|
922
987
|
async append(v) {
|
|
923
988
|
if (!this.seen) await this.hydrate();
|
|
@@ -925,6 +990,11 @@ var PolicyViolationsLog = class {
|
|
|
925
990
|
this.seen.add(v.id);
|
|
926
991
|
await fs2.mkdir(path2.dirname(this.path), { recursive: true });
|
|
927
992
|
await fs2.appendFile(this.path, JSON.stringify(v) + "\n", "utf8");
|
|
993
|
+
emitNeatEvent({
|
|
994
|
+
type: "policy-violation",
|
|
995
|
+
project: this.project,
|
|
996
|
+
payload: { violation: v }
|
|
997
|
+
});
|
|
928
998
|
return true;
|
|
929
999
|
}
|
|
930
1000
|
async readAll() {
|
|
@@ -1405,6 +1475,7 @@ async function markStaleEdges(graph, options = {}) {
|
|
|
1405
1475
|
const thresholds = options.thresholds ?? loadStaleThresholdsFromEnv();
|
|
1406
1476
|
const now = options.now ?? Date.now();
|
|
1407
1477
|
const events = [];
|
|
1478
|
+
const project = options.project ?? DEFAULT_PROJECT;
|
|
1408
1479
|
graph.forEachEdge((id, attrs) => {
|
|
1409
1480
|
const e = attrs;
|
|
1410
1481
|
if (e.provenance !== Provenance3.OBSERVED) return;
|
|
@@ -1424,6 +1495,15 @@ async function markStaleEdges(graph, options = {}) {
|
|
|
1424
1495
|
lastObserved: e.lastObserved,
|
|
1425
1496
|
transitionedAt: new Date(now).toISOString()
|
|
1426
1497
|
});
|
|
1498
|
+
emitNeatEvent({
|
|
1499
|
+
type: "stale-transition",
|
|
1500
|
+
project,
|
|
1501
|
+
payload: {
|
|
1502
|
+
edgeId: id,
|
|
1503
|
+
from: Provenance3.OBSERVED,
|
|
1504
|
+
to: Provenance3.STALE
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1427
1507
|
}
|
|
1428
1508
|
});
|
|
1429
1509
|
if (options.staleEventsPath && events.length > 0) {
|
|
@@ -1454,7 +1534,8 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
1454
1534
|
try {
|
|
1455
1535
|
await markStaleEdges(graph, {
|
|
1456
1536
|
thresholds: options.thresholds,
|
|
1457
|
-
staleEventsPath: options.staleEventsPath
|
|
1537
|
+
staleEventsPath: options.staleEventsPath,
|
|
1538
|
+
project: options.project
|
|
1458
1539
|
});
|
|
1459
1540
|
if (options.onPolicyTrigger) await options.onPolicyTrigger(graph);
|
|
1460
1541
|
} catch (err) {
|
|
@@ -1480,10 +1561,10 @@ async function readErrorEvents(errorsPath) {
|
|
|
1480
1561
|
}
|
|
1481
1562
|
|
|
1482
1563
|
// src/extract/services.ts
|
|
1483
|
-
import { promises as
|
|
1484
|
-
import
|
|
1564
|
+
import { promises as fs7 } from "fs";
|
|
1565
|
+
import path7 from "path";
|
|
1485
1566
|
import ignore from "ignore";
|
|
1486
|
-
import { minimatch } from "minimatch";
|
|
1567
|
+
import { minimatch as minimatch2 } from "minimatch";
|
|
1487
1568
|
import { NodeType as NodeType4, serviceId as serviceId2 } from "@neat.is/types";
|
|
1488
1569
|
|
|
1489
1570
|
// src/extract/shared.ts
|
|
@@ -1594,6 +1675,72 @@ function pythonToPackage(service) {
|
|
|
1594
1675
|
};
|
|
1595
1676
|
}
|
|
1596
1677
|
|
|
1678
|
+
// src/extract/owners.ts
|
|
1679
|
+
import { promises as fs6 } from "fs";
|
|
1680
|
+
import path6 from "path";
|
|
1681
|
+
import { minimatch } from "minimatch";
|
|
1682
|
+
async function loadCodeowners(scanPath) {
|
|
1683
|
+
const candidates = [
|
|
1684
|
+
path6.join(scanPath, "CODEOWNERS"),
|
|
1685
|
+
path6.join(scanPath, ".github", "CODEOWNERS")
|
|
1686
|
+
];
|
|
1687
|
+
for (const file of candidates) {
|
|
1688
|
+
if (await exists(file)) {
|
|
1689
|
+
const raw = await fs6.readFile(file, "utf8");
|
|
1690
|
+
return parseCodeowners(raw);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
return null;
|
|
1694
|
+
}
|
|
1695
|
+
function parseCodeowners(raw) {
|
|
1696
|
+
const rules = [];
|
|
1697
|
+
for (const line of raw.split("\n")) {
|
|
1698
|
+
const trimmed = line.trim();
|
|
1699
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1700
|
+
const match = /^(\S+)\s+(.+)$/.exec(trimmed);
|
|
1701
|
+
if (!match) continue;
|
|
1702
|
+
rules.push({ pattern: match[1], owners: match[2].trim() });
|
|
1703
|
+
}
|
|
1704
|
+
return { rules };
|
|
1705
|
+
}
|
|
1706
|
+
function matchOwner(file, repoPath) {
|
|
1707
|
+
const normalized = repoPath.split(path6.sep).join("/");
|
|
1708
|
+
for (const rule of file.rules) {
|
|
1709
|
+
if (matchesPattern(rule.pattern, normalized)) return rule.owners;
|
|
1710
|
+
}
|
|
1711
|
+
return null;
|
|
1712
|
+
}
|
|
1713
|
+
function matchesPattern(rawPattern, repoPath) {
|
|
1714
|
+
let pattern = rawPattern.startsWith("/") ? rawPattern.slice(1) : rawPattern;
|
|
1715
|
+
if (pattern === "*") return !repoPath.includes("/");
|
|
1716
|
+
if (pattern === "**" || pattern === "") return true;
|
|
1717
|
+
if (pattern.endsWith("/")) pattern = pattern + "**";
|
|
1718
|
+
if (minimatch(repoPath, pattern, { dot: true })) return true;
|
|
1719
|
+
if (!pattern.includes("*") && minimatch(repoPath, pattern + "/**", { dot: true })) return true;
|
|
1720
|
+
return false;
|
|
1721
|
+
}
|
|
1722
|
+
async function readPackageJsonAuthor(serviceDir) {
|
|
1723
|
+
const pkgPath = path6.join(serviceDir, "package.json");
|
|
1724
|
+
if (!await exists(pkgPath)) return null;
|
|
1725
|
+
try {
|
|
1726
|
+
const pkg = await readJson(pkgPath);
|
|
1727
|
+
if (!pkg.author) return null;
|
|
1728
|
+
if (typeof pkg.author === "string") return pkg.author;
|
|
1729
|
+
if (typeof pkg.author === "object" && typeof pkg.author.name === "string") return pkg.author.name;
|
|
1730
|
+
return null;
|
|
1731
|
+
} catch {
|
|
1732
|
+
return null;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
async function computeServiceOwner(codeowners, repoPath, serviceDir) {
|
|
1736
|
+
if (codeowners && repoPath !== void 0) {
|
|
1737
|
+
const owner = matchOwner(codeowners, repoPath);
|
|
1738
|
+
if (owner) return owner;
|
|
1739
|
+
}
|
|
1740
|
+
const author = await readPackageJsonAuthor(serviceDir);
|
|
1741
|
+
return author ?? void 0;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1597
1744
|
// src/extract/services.ts
|
|
1598
1745
|
var DEFAULT_SCAN_DEPTH = 5;
|
|
1599
1746
|
function parseScanDepth() {
|
|
@@ -1610,21 +1757,21 @@ function workspaceGlobs(pkg) {
|
|
|
1610
1757
|
return null;
|
|
1611
1758
|
}
|
|
1612
1759
|
async function loadGitignore(scanPath) {
|
|
1613
|
-
const gitignorePath =
|
|
1760
|
+
const gitignorePath = path7.join(scanPath, ".gitignore");
|
|
1614
1761
|
if (!await exists(gitignorePath)) return null;
|
|
1615
|
-
const raw = await
|
|
1762
|
+
const raw = await fs7.readFile(gitignorePath, "utf8");
|
|
1616
1763
|
return ignore().add(raw);
|
|
1617
1764
|
}
|
|
1618
1765
|
async function walkDirs(start, scanPath, options, visit) {
|
|
1619
1766
|
async function recurse(current, depth) {
|
|
1620
1767
|
if (depth > options.maxDepth) return;
|
|
1621
|
-
const entries = await
|
|
1768
|
+
const entries = await fs7.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
1622
1769
|
for (const entry of entries) {
|
|
1623
1770
|
if (!entry.isDirectory()) continue;
|
|
1624
1771
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
1625
|
-
const child =
|
|
1772
|
+
const child = path7.join(current, entry.name);
|
|
1626
1773
|
if (options.ig) {
|
|
1627
|
-
const rel =
|
|
1774
|
+
const rel = path7.relative(scanPath, child).split(path7.sep).join("/");
|
|
1628
1775
|
if (rel && options.ig.ignores(rel + "/")) continue;
|
|
1629
1776
|
}
|
|
1630
1777
|
await visit(child);
|
|
@@ -1639,8 +1786,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
1639
1786
|
for (const raw of globs) {
|
|
1640
1787
|
const pattern = raw.replace(/^\.\//, "");
|
|
1641
1788
|
if (!pattern.includes("*")) {
|
|
1642
|
-
const candidate =
|
|
1643
|
-
if (await exists(
|
|
1789
|
+
const candidate = path7.join(scanPath, pattern);
|
|
1790
|
+
if (await exists(path7.join(candidate, "package.json"))) found.add(candidate);
|
|
1644
1791
|
continue;
|
|
1645
1792
|
}
|
|
1646
1793
|
const segments = pattern.split("/");
|
|
@@ -1649,13 +1796,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
1649
1796
|
if (seg.includes("*")) break;
|
|
1650
1797
|
staticSegments.push(seg);
|
|
1651
1798
|
}
|
|
1652
|
-
const start =
|
|
1799
|
+
const start = path7.join(scanPath, ...staticSegments);
|
|
1653
1800
|
if (!await exists(start)) continue;
|
|
1654
1801
|
const hasDoubleStar = pattern.includes("**");
|
|
1655
1802
|
const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
|
|
1656
1803
|
await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
|
|
1657
|
-
const rel =
|
|
1658
|
-
if (
|
|
1804
|
+
const rel = path7.relative(scanPath, dir).split(path7.sep).join("/");
|
|
1805
|
+
if (minimatch2(rel, pattern) && await exists(path7.join(dir, "package.json"))) {
|
|
1659
1806
|
found.add(dir);
|
|
1660
1807
|
}
|
|
1661
1808
|
});
|
|
@@ -1663,9 +1810,17 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
1663
1810
|
return [...found];
|
|
1664
1811
|
}
|
|
1665
1812
|
async function discoverNodeService(scanPath, dir) {
|
|
1666
|
-
const pkgPath =
|
|
1813
|
+
const pkgPath = path7.join(dir, "package.json");
|
|
1667
1814
|
if (!await exists(pkgPath)) return null;
|
|
1668
|
-
|
|
1815
|
+
let pkg;
|
|
1816
|
+
try {
|
|
1817
|
+
pkg = await readJson(pkgPath);
|
|
1818
|
+
} catch (err) {
|
|
1819
|
+
console.warn(
|
|
1820
|
+
`[neat] services skipped ${path7.relative(scanPath, pkgPath)}: ${err.message}`
|
|
1821
|
+
);
|
|
1822
|
+
return null;
|
|
1823
|
+
}
|
|
1669
1824
|
if (!pkg.name) return null;
|
|
1670
1825
|
const node = {
|
|
1671
1826
|
id: serviceId2(pkg.name),
|
|
@@ -1674,7 +1829,7 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
1674
1829
|
language: "javascript",
|
|
1675
1830
|
version: pkg.version,
|
|
1676
1831
|
dependencies: pkg.dependencies ?? {},
|
|
1677
|
-
repoPath:
|
|
1832
|
+
repoPath: path7.relative(scanPath, dir),
|
|
1678
1833
|
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
|
|
1679
1834
|
};
|
|
1680
1835
|
return { pkg, dir, node };
|
|
@@ -1690,13 +1845,22 @@ async function discoverPyService(scanPath, dir) {
|
|
|
1690
1845
|
language: "python",
|
|
1691
1846
|
version: py.version,
|
|
1692
1847
|
dependencies: py.dependencies,
|
|
1693
|
-
repoPath:
|
|
1848
|
+
repoPath: path7.relative(scanPath, dir)
|
|
1694
1849
|
};
|
|
1695
1850
|
return { pkg, dir, node };
|
|
1696
1851
|
}
|
|
1697
1852
|
async function discoverServices(scanPath) {
|
|
1698
|
-
const rootPkgPath =
|
|
1699
|
-
|
|
1853
|
+
const rootPkgPath = path7.join(scanPath, "package.json");
|
|
1854
|
+
let rootPkg = null;
|
|
1855
|
+
if (await exists(rootPkgPath)) {
|
|
1856
|
+
try {
|
|
1857
|
+
rootPkg = await readJson(rootPkgPath);
|
|
1858
|
+
} catch (err) {
|
|
1859
|
+
console.warn(
|
|
1860
|
+
`[neat] services workspaces skipped ${path7.relative(scanPath, rootPkgPath)}: ${err.message}`
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1700
1864
|
const wsGlobs = rootPkg ? workspaceGlobs(rootPkg) : null;
|
|
1701
1865
|
const candidateDirs = [];
|
|
1702
1866
|
if (wsGlobs) {
|
|
@@ -1709,9 +1873,9 @@ async function discoverServices(scanPath) {
|
|
|
1709
1873
|
scanPath,
|
|
1710
1874
|
{ maxDepth: parseScanDepth(), ig },
|
|
1711
1875
|
async (dir) => {
|
|
1712
|
-
if (await exists(
|
|
1876
|
+
if (await exists(path7.join(dir, "package.json"))) {
|
|
1713
1877
|
candidateDirs.push(dir);
|
|
1714
|
-
} else if (await exists(
|
|
1878
|
+
} else if (await exists(path7.join(dir, "pyproject.toml")) || await exists(path7.join(dir, "requirements.txt")) || await exists(path7.join(dir, "setup.py"))) {
|
|
1715
1879
|
candidateDirs.push(dir);
|
|
1716
1880
|
}
|
|
1717
1881
|
}
|
|
@@ -1725,8 +1889,8 @@ async function discoverServices(scanPath) {
|
|
|
1725
1889
|
if (!service) continue;
|
|
1726
1890
|
const existingDir = seen.get(service.node.name);
|
|
1727
1891
|
if (existingDir !== void 0) {
|
|
1728
|
-
const a =
|
|
1729
|
-
const b =
|
|
1892
|
+
const a = path7.relative(scanPath, existingDir) || ".";
|
|
1893
|
+
const b = path7.relative(scanPath, dir) || ".";
|
|
1730
1894
|
console.warn(
|
|
1731
1895
|
`[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
|
|
1732
1896
|
);
|
|
@@ -1735,6 +1899,11 @@ async function discoverServices(scanPath) {
|
|
|
1735
1899
|
seen.set(service.node.name, dir);
|
|
1736
1900
|
out.push(service);
|
|
1737
1901
|
}
|
|
1902
|
+
const codeowners = await loadCodeowners(scanPath);
|
|
1903
|
+
for (const service of out) {
|
|
1904
|
+
const owner = await computeServiceOwner(codeowners, service.node.repoPath, service.dir);
|
|
1905
|
+
if (owner !== void 0) service.node.owner = owner;
|
|
1906
|
+
}
|
|
1738
1907
|
return out;
|
|
1739
1908
|
}
|
|
1740
1909
|
function addServiceNodes(graph, services) {
|
|
@@ -1757,8 +1926,8 @@ function addServiceNodes(graph, services) {
|
|
|
1757
1926
|
}
|
|
1758
1927
|
|
|
1759
1928
|
// src/extract/aliases.ts
|
|
1760
|
-
import
|
|
1761
|
-
import { promises as
|
|
1929
|
+
import path8 from "path";
|
|
1930
|
+
import { promises as fs8 } from "fs";
|
|
1762
1931
|
import { parseAllDocuments } from "yaml";
|
|
1763
1932
|
import { NodeType as NodeType5 } from "@neat.is/types";
|
|
1764
1933
|
var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
@@ -1785,21 +1954,29 @@ function indexServicesByName(services) {
|
|
|
1785
1954
|
const map = /* @__PURE__ */ new Map();
|
|
1786
1955
|
for (const s of services) {
|
|
1787
1956
|
map.set(s.node.name, s.node.id);
|
|
1788
|
-
map.set(
|
|
1957
|
+
map.set(path8.basename(s.dir), s.node.id);
|
|
1789
1958
|
}
|
|
1790
1959
|
return map;
|
|
1791
1960
|
}
|
|
1792
1961
|
async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
1793
1962
|
let composePath = null;
|
|
1794
1963
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
1795
|
-
const abs =
|
|
1964
|
+
const abs = path8.join(scanPath, name);
|
|
1796
1965
|
if (await exists(abs)) {
|
|
1797
1966
|
composePath = abs;
|
|
1798
1967
|
break;
|
|
1799
1968
|
}
|
|
1800
1969
|
}
|
|
1801
1970
|
if (!composePath) return;
|
|
1802
|
-
|
|
1971
|
+
let compose;
|
|
1972
|
+
try {
|
|
1973
|
+
compose = await readYaml(composePath);
|
|
1974
|
+
} catch (err) {
|
|
1975
|
+
console.warn(
|
|
1976
|
+
`[neat] aliases compose skipped ${path8.relative(scanPath, composePath)}: ${err.message}`
|
|
1977
|
+
);
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1803
1980
|
if (!compose?.services) return;
|
|
1804
1981
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
1805
1982
|
const serviceId3 = serviceIndex.get(composeName);
|
|
@@ -1838,9 +2015,17 @@ function parseDockerfileLabels(content) {
|
|
|
1838
2015
|
}
|
|
1839
2016
|
async function collectDockerfileAliases(graph, services) {
|
|
1840
2017
|
for (const service of services) {
|
|
1841
|
-
const dockerfilePath =
|
|
2018
|
+
const dockerfilePath = path8.join(service.dir, "Dockerfile");
|
|
1842
2019
|
if (!await exists(dockerfilePath)) continue;
|
|
1843
|
-
|
|
2020
|
+
let content;
|
|
2021
|
+
try {
|
|
2022
|
+
content = await fs8.readFile(dockerfilePath, "utf8");
|
|
2023
|
+
} catch (err) {
|
|
2024
|
+
console.warn(
|
|
2025
|
+
`[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
|
|
2026
|
+
);
|
|
2027
|
+
continue;
|
|
2028
|
+
}
|
|
1844
2029
|
const aliases = parseDockerfileLabels(content);
|
|
1845
2030
|
if (aliases.length > 0) addAliases(graph, service.node.id, aliases);
|
|
1846
2031
|
}
|
|
@@ -1848,13 +2033,13 @@ async function collectDockerfileAliases(graph, services) {
|
|
|
1848
2033
|
async function walkYamlFiles(start, depth = 0, max = 5) {
|
|
1849
2034
|
if (depth > max) return [];
|
|
1850
2035
|
const out = [];
|
|
1851
|
-
const entries = await
|
|
2036
|
+
const entries = await fs8.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
1852
2037
|
for (const entry of entries) {
|
|
1853
2038
|
if (entry.isDirectory()) {
|
|
1854
2039
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
1855
|
-
out.push(...await walkYamlFiles(
|
|
1856
|
-
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
1857
|
-
out.push(
|
|
2040
|
+
out.push(...await walkYamlFiles(path8.join(start, entry.name), depth + 1, max));
|
|
2041
|
+
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path8.extname(entry.name))) {
|
|
2042
|
+
out.push(path8.join(start, entry.name));
|
|
1858
2043
|
}
|
|
1859
2044
|
}
|
|
1860
2045
|
return out;
|
|
@@ -1881,7 +2066,7 @@ function k8sServiceTarget(doc, byName) {
|
|
|
1881
2066
|
async function collectK8sAliases(graph, scanPath, serviceIndex) {
|
|
1882
2067
|
const files = await walkYamlFiles(scanPath);
|
|
1883
2068
|
for (const file of files) {
|
|
1884
|
-
const content = await
|
|
2069
|
+
const content = await fs8.readFile(file, "utf8");
|
|
1885
2070
|
let docs;
|
|
1886
2071
|
try {
|
|
1887
2072
|
docs = parseAllDocuments(content).map((d) => d.toJSON());
|
|
@@ -1905,13 +2090,13 @@ async function addServiceAliases(graph, scanPath, services) {
|
|
|
1905
2090
|
}
|
|
1906
2091
|
|
|
1907
2092
|
// src/extract/databases/index.ts
|
|
1908
|
-
import
|
|
2093
|
+
import path16 from "path";
|
|
1909
2094
|
import { EdgeType as EdgeType3, NodeType as NodeType6, Provenance as Provenance4, databaseId as databaseId2 } from "@neat.is/types";
|
|
1910
2095
|
|
|
1911
2096
|
// src/extract/databases/db-config-yaml.ts
|
|
1912
|
-
import
|
|
2097
|
+
import path9 from "path";
|
|
1913
2098
|
async function parse(serviceDir) {
|
|
1914
|
-
const yamlPath =
|
|
2099
|
+
const yamlPath = path9.join(serviceDir, "db-config.yaml");
|
|
1915
2100
|
if (!await exists(yamlPath)) return [];
|
|
1916
2101
|
const raw = await readYaml(yamlPath);
|
|
1917
2102
|
return [
|
|
@@ -1928,12 +2113,12 @@ async function parse(serviceDir) {
|
|
|
1928
2113
|
var dbConfigYamlParser = { name: "db-config.yaml", parse };
|
|
1929
2114
|
|
|
1930
2115
|
// src/extract/databases/dotenv.ts
|
|
1931
|
-
import { promises as
|
|
1932
|
-
import
|
|
2116
|
+
import { promises as fs10 } from "fs";
|
|
2117
|
+
import path11 from "path";
|
|
1933
2118
|
|
|
1934
2119
|
// src/extract/databases/shared.ts
|
|
1935
|
-
import { promises as
|
|
1936
|
-
import
|
|
2120
|
+
import { promises as fs9 } from "fs";
|
|
2121
|
+
import path10 from "path";
|
|
1937
2122
|
function schemeToEngine(scheme) {
|
|
1938
2123
|
const s = scheme.toLowerCase().split("+")[0];
|
|
1939
2124
|
switch (s) {
|
|
@@ -1972,14 +2157,14 @@ function parseConnectionString(url) {
|
|
|
1972
2157
|
}
|
|
1973
2158
|
async function readIfExists(filePath) {
|
|
1974
2159
|
try {
|
|
1975
|
-
return await
|
|
2160
|
+
return await fs9.readFile(filePath, "utf8");
|
|
1976
2161
|
} catch {
|
|
1977
2162
|
return null;
|
|
1978
2163
|
}
|
|
1979
2164
|
}
|
|
1980
2165
|
async function findFirst(serviceDir, candidates) {
|
|
1981
2166
|
for (const rel of candidates) {
|
|
1982
|
-
const abs =
|
|
2167
|
+
const abs = path10.join(serviceDir, rel);
|
|
1983
2168
|
const content = await readIfExists(abs);
|
|
1984
2169
|
if (content !== null) return abs;
|
|
1985
2170
|
}
|
|
@@ -2030,15 +2215,15 @@ function parseDotenvLine(line) {
|
|
|
2030
2215
|
return { key, value };
|
|
2031
2216
|
}
|
|
2032
2217
|
async function parse2(serviceDir) {
|
|
2033
|
-
const entries = await
|
|
2218
|
+
const entries = await fs10.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
|
|
2034
2219
|
const configs = [];
|
|
2035
2220
|
const seen = /* @__PURE__ */ new Set();
|
|
2036
2221
|
for (const entry of entries) {
|
|
2037
2222
|
if (!entry.isFile()) continue;
|
|
2038
2223
|
const match = isConfigFile(entry.name);
|
|
2039
2224
|
if (!match.match || match.fileType !== "env") continue;
|
|
2040
|
-
const filePath =
|
|
2041
|
-
const content = await
|
|
2225
|
+
const filePath = path11.join(serviceDir, entry.name);
|
|
2226
|
+
const content = await fs10.readFile(filePath, "utf8");
|
|
2042
2227
|
for (const line of content.split("\n")) {
|
|
2043
2228
|
const parsed = parseDotenvLine(line);
|
|
2044
2229
|
if (!parsed) continue;
|
|
@@ -2056,9 +2241,9 @@ async function parse2(serviceDir) {
|
|
|
2056
2241
|
var dotenvParser = { name: ".env", parse: parse2 };
|
|
2057
2242
|
|
|
2058
2243
|
// src/extract/databases/prisma.ts
|
|
2059
|
-
import
|
|
2244
|
+
import path12 from "path";
|
|
2060
2245
|
async function parse3(serviceDir) {
|
|
2061
|
-
const schemaPath =
|
|
2246
|
+
const schemaPath = path12.join(serviceDir, "prisma", "schema.prisma");
|
|
2062
2247
|
const content = await readIfExists(schemaPath);
|
|
2063
2248
|
if (!content) return [];
|
|
2064
2249
|
const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
|
|
@@ -2187,10 +2372,10 @@ async function parse5(serviceDir) {
|
|
|
2187
2372
|
var knexParser = { name: "knex", parse: parse5 };
|
|
2188
2373
|
|
|
2189
2374
|
// src/extract/databases/ormconfig.ts
|
|
2190
|
-
import
|
|
2375
|
+
import path13 from "path";
|
|
2191
2376
|
async function parse6(serviceDir) {
|
|
2192
2377
|
for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
|
|
2193
|
-
const abs =
|
|
2378
|
+
const abs = path13.join(serviceDir, candidate);
|
|
2194
2379
|
if (!await exists(abs)) continue;
|
|
2195
2380
|
const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
|
|
2196
2381
|
const entries = Array.isArray(raw) ? raw : [raw];
|
|
@@ -2248,9 +2433,9 @@ async function parse7(serviceDir) {
|
|
|
2248
2433
|
var typeormParser = { name: "typeorm", parse: parse7 };
|
|
2249
2434
|
|
|
2250
2435
|
// src/extract/databases/sequelize.ts
|
|
2251
|
-
import
|
|
2436
|
+
import path14 from "path";
|
|
2252
2437
|
async function parse8(serviceDir) {
|
|
2253
|
-
const configPath =
|
|
2438
|
+
const configPath = path14.join(serviceDir, "config", "config.json");
|
|
2254
2439
|
if (!await exists(configPath)) return [];
|
|
2255
2440
|
const raw = await readJson(configPath);
|
|
2256
2441
|
const out = [];
|
|
@@ -2276,7 +2461,7 @@ async function parse8(serviceDir) {
|
|
|
2276
2461
|
var sequelizeParser = { name: "sequelize", parse: parse8 };
|
|
2277
2462
|
|
|
2278
2463
|
// src/extract/databases/docker-compose.ts
|
|
2279
|
-
import
|
|
2464
|
+
import path15 from "path";
|
|
2280
2465
|
function portFromService(svc) {
|
|
2281
2466
|
for (const raw of svc.ports ?? []) {
|
|
2282
2467
|
const str = String(raw);
|
|
@@ -2303,7 +2488,7 @@ function databaseFromEnv(svc) {
|
|
|
2303
2488
|
}
|
|
2304
2489
|
async function parse9(serviceDir) {
|
|
2305
2490
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2306
|
-
const abs =
|
|
2491
|
+
const abs = path15.join(serviceDir, name);
|
|
2307
2492
|
if (!await exists(abs)) continue;
|
|
2308
2493
|
const raw = await readYaml(abs);
|
|
2309
2494
|
if (!raw?.services) return [];
|
|
@@ -2483,7 +2668,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
2483
2668
|
provenance: Provenance4.EXTRACTED,
|
|
2484
2669
|
...config.sourceFile ? {
|
|
2485
2670
|
evidence: {
|
|
2486
|
-
file:
|
|
2671
|
+
file: path16.relative(scanPath, config.sourceFile).split(path16.sep).join("/")
|
|
2487
2672
|
}
|
|
2488
2673
|
} : {}
|
|
2489
2674
|
};
|
|
@@ -2510,15 +2695,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
2510
2695
|
}
|
|
2511
2696
|
|
|
2512
2697
|
// src/extract/configs.ts
|
|
2513
|
-
import { promises as
|
|
2514
|
-
import
|
|
2698
|
+
import { promises as fs11 } from "fs";
|
|
2699
|
+
import path17 from "path";
|
|
2515
2700
|
import { EdgeType as EdgeType4, NodeType as NodeType7, Provenance as Provenance5, configId } from "@neat.is/types";
|
|
2516
2701
|
async function walkConfigFiles(dir) {
|
|
2517
2702
|
const out = [];
|
|
2518
2703
|
async function walk(current) {
|
|
2519
|
-
const entries = await
|
|
2704
|
+
const entries = await fs11.readdir(current, { withFileTypes: true });
|
|
2520
2705
|
for (const entry of entries) {
|
|
2521
|
-
const full =
|
|
2706
|
+
const full = path17.join(current, entry.name);
|
|
2522
2707
|
if (entry.isDirectory()) {
|
|
2523
2708
|
if (!IGNORED_DIRS.has(entry.name)) await walk(full);
|
|
2524
2709
|
} else if (entry.isFile() && isConfigFile(entry.name).match) {
|
|
@@ -2535,13 +2720,13 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
2535
2720
|
for (const service of services) {
|
|
2536
2721
|
const configFiles = await walkConfigFiles(service.dir);
|
|
2537
2722
|
for (const file of configFiles) {
|
|
2538
|
-
const relPath =
|
|
2723
|
+
const relPath = path17.relative(scanPath, file);
|
|
2539
2724
|
const node = {
|
|
2540
2725
|
id: configId(relPath),
|
|
2541
2726
|
type: NodeType7.ConfigNode,
|
|
2542
|
-
name:
|
|
2727
|
+
name: path17.basename(file),
|
|
2543
2728
|
path: relPath,
|
|
2544
|
-
fileType: isConfigFile(
|
|
2729
|
+
fileType: isConfigFile(path17.basename(file)).fileType
|
|
2545
2730
|
};
|
|
2546
2731
|
if (!graph.hasNode(node.id)) {
|
|
2547
2732
|
graph.addNode(node.id, node);
|
|
@@ -2553,7 +2738,7 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
2553
2738
|
target: node.id,
|
|
2554
2739
|
type: EdgeType4.CONFIGURED_BY,
|
|
2555
2740
|
provenance: Provenance5.EXTRACTED,
|
|
2556
|
-
evidence: { file: relPath.split(
|
|
2741
|
+
evidence: { file: relPath.split(path17.sep).join("/") }
|
|
2557
2742
|
};
|
|
2558
2743
|
if (!graph.hasEdge(edge.id)) {
|
|
2559
2744
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -2568,24 +2753,24 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
2568
2753
|
import { EdgeType as EdgeType6, NodeType as NodeType8, Provenance as Provenance7 } from "@neat.is/types";
|
|
2569
2754
|
|
|
2570
2755
|
// src/extract/calls/http.ts
|
|
2571
|
-
import
|
|
2756
|
+
import path19 from "path";
|
|
2572
2757
|
import Parser from "tree-sitter";
|
|
2573
2758
|
import JavaScript from "tree-sitter-javascript";
|
|
2574
2759
|
import Python from "tree-sitter-python";
|
|
2575
2760
|
import { EdgeType as EdgeType5, Provenance as Provenance6 } from "@neat.is/types";
|
|
2576
2761
|
|
|
2577
2762
|
// src/extract/calls/shared.ts
|
|
2578
|
-
import { promises as
|
|
2579
|
-
import
|
|
2763
|
+
import { promises as fs12 } from "fs";
|
|
2764
|
+
import path18 from "path";
|
|
2580
2765
|
async function walkSourceFiles(dir) {
|
|
2581
2766
|
const out = [];
|
|
2582
2767
|
async function walk(current) {
|
|
2583
|
-
const entries = await
|
|
2768
|
+
const entries = await fs12.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
2584
2769
|
for (const entry of entries) {
|
|
2585
|
-
const full =
|
|
2770
|
+
const full = path18.join(current, entry.name);
|
|
2586
2771
|
if (entry.isDirectory()) {
|
|
2587
2772
|
if (!IGNORED_DIRS.has(entry.name)) await walk(full);
|
|
2588
|
-
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(
|
|
2773
|
+
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path18.extname(entry.name))) {
|
|
2589
2774
|
out.push(full);
|
|
2590
2775
|
}
|
|
2591
2776
|
}
|
|
@@ -2598,7 +2783,7 @@ async function loadSourceFiles(dir) {
|
|
|
2598
2783
|
const out = [];
|
|
2599
2784
|
for (const p of paths) {
|
|
2600
2785
|
try {
|
|
2601
|
-
const content = await
|
|
2786
|
+
const content = await fs12.readFile(p, "utf8");
|
|
2602
2787
|
out.push({ path: p, content });
|
|
2603
2788
|
} catch {
|
|
2604
2789
|
}
|
|
@@ -2654,9 +2839,9 @@ async function addHttpCallEdges(graph, services) {
|
|
|
2654
2839
|
const knownHosts = /* @__PURE__ */ new Set();
|
|
2655
2840
|
const hostToNodeId = /* @__PURE__ */ new Map();
|
|
2656
2841
|
for (const service of services) {
|
|
2657
|
-
knownHosts.add(
|
|
2842
|
+
knownHosts.add(path19.basename(service.dir));
|
|
2658
2843
|
knownHosts.add(service.pkg.name);
|
|
2659
|
-
hostToNodeId.set(
|
|
2844
|
+
hostToNodeId.set(path19.basename(service.dir), service.node.id);
|
|
2660
2845
|
hostToNodeId.set(service.pkg.name, service.node.id);
|
|
2661
2846
|
}
|
|
2662
2847
|
let edgesAdded = 0;
|
|
@@ -2664,8 +2849,16 @@ async function addHttpCallEdges(graph, services) {
|
|
|
2664
2849
|
const files = await loadSourceFiles(service.dir);
|
|
2665
2850
|
const seenTargets = /* @__PURE__ */ new Map();
|
|
2666
2851
|
for (const file of files) {
|
|
2667
|
-
const parser =
|
|
2668
|
-
|
|
2852
|
+
const parser = path19.extname(file.path) === ".py" ? pyParser : jsParser;
|
|
2853
|
+
let targets;
|
|
2854
|
+
try {
|
|
2855
|
+
targets = callsFromSource(file.content, parser, knownHosts);
|
|
2856
|
+
} catch (err) {
|
|
2857
|
+
console.warn(
|
|
2858
|
+
`[neat] http call extraction skipped ${file.path}: ${err.message}`
|
|
2859
|
+
);
|
|
2860
|
+
continue;
|
|
2861
|
+
}
|
|
2669
2862
|
for (const t of targets) {
|
|
2670
2863
|
const targetId = hostToNodeId.get(t);
|
|
2671
2864
|
if (!targetId || targetId === service.node.id) continue;
|
|
@@ -2684,7 +2877,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
2684
2877
|
type: EdgeType5.CALLS,
|
|
2685
2878
|
provenance: Provenance6.EXTRACTED,
|
|
2686
2879
|
evidence: {
|
|
2687
|
-
file:
|
|
2880
|
+
file: path19.relative(service.dir, evidenceFile.file),
|
|
2688
2881
|
line,
|
|
2689
2882
|
snippet: snippet(fileContent, line)
|
|
2690
2883
|
}
|
|
@@ -2699,7 +2892,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
2699
2892
|
}
|
|
2700
2893
|
|
|
2701
2894
|
// src/extract/calls/kafka.ts
|
|
2702
|
-
import
|
|
2895
|
+
import path20 from "path";
|
|
2703
2896
|
import { infraId } from "@neat.is/types";
|
|
2704
2897
|
var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
2705
2898
|
var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
|
|
@@ -2726,7 +2919,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
2726
2919
|
kind: "kafka-topic",
|
|
2727
2920
|
edgeType,
|
|
2728
2921
|
evidence: {
|
|
2729
|
-
file:
|
|
2922
|
+
file: path20.relative(serviceDir, file.path),
|
|
2730
2923
|
line,
|
|
2731
2924
|
snippet: snippet(file.content, line)
|
|
2732
2925
|
}
|
|
@@ -2738,7 +2931,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
2738
2931
|
}
|
|
2739
2932
|
|
|
2740
2933
|
// src/extract/calls/redis.ts
|
|
2741
|
-
import
|
|
2934
|
+
import path21 from "path";
|
|
2742
2935
|
import { infraId as infraId2 } from "@neat.is/types";
|
|
2743
2936
|
var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
|
|
2744
2937
|
function redisEndpointsFromFile(file, serviceDir) {
|
|
@@ -2757,7 +2950,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
2757
2950
|
kind: "redis",
|
|
2758
2951
|
edgeType: "CALLS",
|
|
2759
2952
|
evidence: {
|
|
2760
|
-
file:
|
|
2953
|
+
file: path21.relative(serviceDir, file.path),
|
|
2761
2954
|
line,
|
|
2762
2955
|
snippet: snippet(file.content, line)
|
|
2763
2956
|
}
|
|
@@ -2767,7 +2960,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
2767
2960
|
}
|
|
2768
2961
|
|
|
2769
2962
|
// src/extract/calls/aws.ts
|
|
2770
|
-
import
|
|
2963
|
+
import path22 from "path";
|
|
2771
2964
|
import { infraId as infraId3 } from "@neat.is/types";
|
|
2772
2965
|
var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
2773
2966
|
var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
@@ -2797,7 +2990,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
2797
2990
|
kind,
|
|
2798
2991
|
edgeType: "CALLS",
|
|
2799
2992
|
evidence: {
|
|
2800
|
-
file:
|
|
2993
|
+
file: path22.relative(serviceDir, file.path),
|
|
2801
2994
|
line,
|
|
2802
2995
|
snippet: snippet(file.content, line)
|
|
2803
2996
|
}
|
|
@@ -2821,7 +3014,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
2821
3014
|
}
|
|
2822
3015
|
|
|
2823
3016
|
// src/extract/calls/grpc.ts
|
|
2824
|
-
import
|
|
3017
|
+
import path23 from "path";
|
|
2825
3018
|
import { infraId as infraId4 } from "@neat.is/types";
|
|
2826
3019
|
var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
|
|
2827
3020
|
function isLikelyAddress(value) {
|
|
@@ -2846,7 +3039,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
2846
3039
|
kind: "grpc-service",
|
|
2847
3040
|
edgeType: "CALLS",
|
|
2848
3041
|
evidence: {
|
|
2849
|
-
file:
|
|
3042
|
+
file: path23.relative(serviceDir, file.path),
|
|
2850
3043
|
line,
|
|
2851
3044
|
snippet: snippet(file.content, line)
|
|
2852
3045
|
}
|
|
@@ -2922,7 +3115,7 @@ async function addCallEdges(graph, services) {
|
|
|
2922
3115
|
}
|
|
2923
3116
|
|
|
2924
3117
|
// src/extract/infra/docker-compose.ts
|
|
2925
|
-
import
|
|
3118
|
+
import path24 from "path";
|
|
2926
3119
|
import { EdgeType as EdgeType7, Provenance as Provenance8 } from "@neat.is/types";
|
|
2927
3120
|
|
|
2928
3121
|
// src/extract/infra/shared.ts
|
|
@@ -2959,7 +3152,7 @@ function dependsOnList(value) {
|
|
|
2959
3152
|
}
|
|
2960
3153
|
function serviceNameToServiceNode(name, services) {
|
|
2961
3154
|
for (const s of services) {
|
|
2962
|
-
if (s.node.name === name ||
|
|
3155
|
+
if (s.node.name === name || path24.basename(s.dir) === name) return s.node.id;
|
|
2963
3156
|
}
|
|
2964
3157
|
return null;
|
|
2965
3158
|
}
|
|
@@ -2968,16 +3161,24 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
2968
3161
|
let edgesAdded = 0;
|
|
2969
3162
|
let composePath = null;
|
|
2970
3163
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2971
|
-
const abs =
|
|
3164
|
+
const abs = path24.join(scanPath, name);
|
|
2972
3165
|
if (await exists(abs)) {
|
|
2973
3166
|
composePath = abs;
|
|
2974
3167
|
break;
|
|
2975
3168
|
}
|
|
2976
3169
|
}
|
|
2977
3170
|
if (!composePath) return { nodesAdded, edgesAdded };
|
|
2978
|
-
|
|
3171
|
+
let compose;
|
|
3172
|
+
try {
|
|
3173
|
+
compose = await readYaml(composePath);
|
|
3174
|
+
} catch (err) {
|
|
3175
|
+
console.warn(
|
|
3176
|
+
`[neat] infra docker-compose skipped ${path24.relative(scanPath, composePath)}: ${err.message}`
|
|
3177
|
+
);
|
|
3178
|
+
return { nodesAdded, edgesAdded };
|
|
3179
|
+
}
|
|
2979
3180
|
if (!compose?.services) return { nodesAdded, edgesAdded };
|
|
2980
|
-
const evidenceFile =
|
|
3181
|
+
const evidenceFile = path24.relative(scanPath, composePath).split(path24.sep).join("/");
|
|
2981
3182
|
const composeNameToNodeId = /* @__PURE__ */ new Map();
|
|
2982
3183
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
2983
3184
|
const matchedServiceId = serviceNameToServiceNode(composeName, services);
|
|
@@ -3017,8 +3218,8 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3017
3218
|
}
|
|
3018
3219
|
|
|
3019
3220
|
// src/extract/infra/dockerfile.ts
|
|
3020
|
-
import
|
|
3021
|
-
import { promises as
|
|
3221
|
+
import path25 from "path";
|
|
3222
|
+
import { promises as fs13 } from "fs";
|
|
3022
3223
|
import { EdgeType as EdgeType8, Provenance as Provenance9 } from "@neat.is/types";
|
|
3023
3224
|
function runtimeImage(content) {
|
|
3024
3225
|
const lines = content.split("\n");
|
|
@@ -3038,9 +3239,17 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3038
3239
|
let nodesAdded = 0;
|
|
3039
3240
|
let edgesAdded = 0;
|
|
3040
3241
|
for (const service of services) {
|
|
3041
|
-
const dockerfilePath =
|
|
3242
|
+
const dockerfilePath = path25.join(service.dir, "Dockerfile");
|
|
3042
3243
|
if (!await exists(dockerfilePath)) continue;
|
|
3043
|
-
|
|
3244
|
+
let content;
|
|
3245
|
+
try {
|
|
3246
|
+
content = await fs13.readFile(dockerfilePath, "utf8");
|
|
3247
|
+
} catch (err) {
|
|
3248
|
+
console.warn(
|
|
3249
|
+
`[neat] infra dockerfile skipped ${path25.relative(scanPath, dockerfilePath)}: ${err.message}`
|
|
3250
|
+
);
|
|
3251
|
+
continue;
|
|
3252
|
+
}
|
|
3044
3253
|
const image = runtimeImage(content);
|
|
3045
3254
|
if (!image) continue;
|
|
3046
3255
|
const node = makeInfraNode("container-image", image);
|
|
@@ -3057,7 +3266,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3057
3266
|
type: EdgeType8.RUNS_ON,
|
|
3058
3267
|
provenance: Provenance9.EXTRACTED,
|
|
3059
3268
|
evidence: {
|
|
3060
|
-
file:
|
|
3269
|
+
file: path25.relative(scanPath, dockerfilePath).split(path25.sep).join("/")
|
|
3061
3270
|
}
|
|
3062
3271
|
};
|
|
3063
3272
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3068,19 +3277,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3068
3277
|
}
|
|
3069
3278
|
|
|
3070
3279
|
// src/extract/infra/terraform.ts
|
|
3071
|
-
import { promises as
|
|
3072
|
-
import
|
|
3280
|
+
import { promises as fs14 } from "fs";
|
|
3281
|
+
import path26 from "path";
|
|
3073
3282
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
3074
3283
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
3075
3284
|
if (depth > max) return [];
|
|
3076
3285
|
const out = [];
|
|
3077
|
-
const entries = await
|
|
3286
|
+
const entries = await fs14.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3078
3287
|
for (const entry of entries) {
|
|
3079
3288
|
if (entry.isDirectory()) {
|
|
3080
3289
|
if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
|
|
3081
|
-
out.push(...await walkTfFiles(
|
|
3290
|
+
out.push(...await walkTfFiles(path26.join(start, entry.name), depth + 1, max));
|
|
3082
3291
|
} else if (entry.isFile() && entry.name.endsWith(".tf")) {
|
|
3083
|
-
out.push(
|
|
3292
|
+
out.push(path26.join(start, entry.name));
|
|
3084
3293
|
}
|
|
3085
3294
|
}
|
|
3086
3295
|
return out;
|
|
@@ -3089,7 +3298,7 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3089
3298
|
let nodesAdded = 0;
|
|
3090
3299
|
const files = await walkTfFiles(scanPath);
|
|
3091
3300
|
for (const file of files) {
|
|
3092
|
-
const content = await
|
|
3301
|
+
const content = await fs14.readFile(file, "utf8");
|
|
3093
3302
|
RESOURCE_RE.lastIndex = 0;
|
|
3094
3303
|
let m;
|
|
3095
3304
|
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
@@ -3106,8 +3315,8 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3106
3315
|
}
|
|
3107
3316
|
|
|
3108
3317
|
// src/extract/infra/k8s.ts
|
|
3109
|
-
import { promises as
|
|
3110
|
-
import
|
|
3318
|
+
import { promises as fs15 } from "fs";
|
|
3319
|
+
import path27 from "path";
|
|
3111
3320
|
import { parseAllDocuments as parseAllDocuments2 } from "yaml";
|
|
3112
3321
|
var K8S_KIND_TO_INFRA_KIND = {
|
|
3113
3322
|
Service: "k8s-service",
|
|
@@ -3121,13 +3330,13 @@ var K8S_KIND_TO_INFRA_KIND = {
|
|
|
3121
3330
|
async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
3122
3331
|
if (depth > max) return [];
|
|
3123
3332
|
const out = [];
|
|
3124
|
-
const entries = await
|
|
3333
|
+
const entries = await fs15.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3125
3334
|
for (const entry of entries) {
|
|
3126
3335
|
if (entry.isDirectory()) {
|
|
3127
3336
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3128
|
-
out.push(...await walkYamlFiles2(
|
|
3129
|
-
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
3130
|
-
out.push(
|
|
3337
|
+
out.push(...await walkYamlFiles2(path27.join(start, entry.name), depth + 1, max));
|
|
3338
|
+
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path27.extname(entry.name))) {
|
|
3339
|
+
out.push(path27.join(start, entry.name));
|
|
3131
3340
|
}
|
|
3132
3341
|
}
|
|
3133
3342
|
return out;
|
|
@@ -3136,7 +3345,7 @@ async function addK8sResources(graph, scanPath) {
|
|
|
3136
3345
|
let nodesAdded = 0;
|
|
3137
3346
|
const files = await walkYamlFiles2(scanPath);
|
|
3138
3347
|
for (const file of files) {
|
|
3139
|
-
const content = await
|
|
3348
|
+
const content = await fs15.readFile(file, "utf8");
|
|
3140
3349
|
let docs;
|
|
3141
3350
|
try {
|
|
3142
3351
|
docs = parseAllDocuments2(content).map((d) => d.toJSON());
|
|
@@ -3182,16 +3391,27 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
3182
3391
|
const phase5 = await addInfra(graph, scanPath, services);
|
|
3183
3392
|
const frontiersPromoted = promoteFrontierNodes(graph);
|
|
3184
3393
|
if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
|
|
3185
|
-
|
|
3394
|
+
const result = {
|
|
3186
3395
|
nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
3187
3396
|
edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
3188
3397
|
frontiersPromoted
|
|
3189
3398
|
};
|
|
3399
|
+
emitNeatEvent({
|
|
3400
|
+
type: "extraction-complete",
|
|
3401
|
+
project: opts.project ?? DEFAULT_PROJECT,
|
|
3402
|
+
payload: {
|
|
3403
|
+
project: opts.project ?? DEFAULT_PROJECT,
|
|
3404
|
+
fileCount: services.length,
|
|
3405
|
+
nodesAdded: result.nodesAdded,
|
|
3406
|
+
edgesAdded: result.edgesAdded
|
|
3407
|
+
}
|
|
3408
|
+
});
|
|
3409
|
+
return result;
|
|
3190
3410
|
}
|
|
3191
3411
|
|
|
3192
3412
|
// src/persist.ts
|
|
3193
|
-
import { promises as
|
|
3194
|
-
import
|
|
3413
|
+
import { promises as fs16 } from "fs";
|
|
3414
|
+
import path28 from "path";
|
|
3195
3415
|
var SCHEMA_VERSION = 2;
|
|
3196
3416
|
function migrateV1ToV2(payload) {
|
|
3197
3417
|
const nodes = payload.graph.nodes;
|
|
@@ -3205,7 +3425,7 @@ function migrateV1ToV2(payload) {
|
|
|
3205
3425
|
return { ...payload, schemaVersion: 2 };
|
|
3206
3426
|
}
|
|
3207
3427
|
async function ensureDir(filePath) {
|
|
3208
|
-
await
|
|
3428
|
+
await fs16.mkdir(path28.dirname(filePath), { recursive: true });
|
|
3209
3429
|
}
|
|
3210
3430
|
async function saveGraphToDisk(graph, outPath) {
|
|
3211
3431
|
await ensureDir(outPath);
|
|
@@ -3215,13 +3435,13 @@ async function saveGraphToDisk(graph, outPath) {
|
|
|
3215
3435
|
graph: graph.export()
|
|
3216
3436
|
};
|
|
3217
3437
|
const tmp = `${outPath}.tmp`;
|
|
3218
|
-
await
|
|
3219
|
-
await
|
|
3438
|
+
await fs16.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
3439
|
+
await fs16.rename(tmp, outPath);
|
|
3220
3440
|
}
|
|
3221
3441
|
async function loadGraphFromDisk(graph, outPath) {
|
|
3222
3442
|
let raw;
|
|
3223
3443
|
try {
|
|
3224
|
-
raw = await
|
|
3444
|
+
raw = await fs16.readFile(outPath, "utf8");
|
|
3225
3445
|
} catch (err) {
|
|
3226
3446
|
if (err.code === "ENOENT") return;
|
|
3227
3447
|
throw err;
|
|
@@ -3273,23 +3493,23 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
3273
3493
|
}
|
|
3274
3494
|
|
|
3275
3495
|
// src/projects.ts
|
|
3276
|
-
import
|
|
3496
|
+
import path29 from "path";
|
|
3277
3497
|
function pathsForProject(project, baseDir) {
|
|
3278
3498
|
if (project === DEFAULT_PROJECT) {
|
|
3279
3499
|
return {
|
|
3280
|
-
snapshotPath:
|
|
3281
|
-
errorsPath:
|
|
3282
|
-
staleEventsPath:
|
|
3283
|
-
embeddingsCachePath:
|
|
3284
|
-
policyViolationsPath:
|
|
3500
|
+
snapshotPath: path29.join(baseDir, "graph.json"),
|
|
3501
|
+
errorsPath: path29.join(baseDir, "errors.ndjson"),
|
|
3502
|
+
staleEventsPath: path29.join(baseDir, "stale-events.ndjson"),
|
|
3503
|
+
embeddingsCachePath: path29.join(baseDir, "embeddings.json"),
|
|
3504
|
+
policyViolationsPath: path29.join(baseDir, "policy-violations.ndjson")
|
|
3285
3505
|
};
|
|
3286
3506
|
}
|
|
3287
3507
|
return {
|
|
3288
|
-
snapshotPath:
|
|
3289
|
-
errorsPath:
|
|
3290
|
-
staleEventsPath:
|
|
3291
|
-
embeddingsCachePath:
|
|
3292
|
-
policyViolationsPath:
|
|
3508
|
+
snapshotPath: path29.join(baseDir, `${project}.json`),
|
|
3509
|
+
errorsPath: path29.join(baseDir, `errors.${project}.ndjson`),
|
|
3510
|
+
staleEventsPath: path29.join(baseDir, `stale-events.${project}.ndjson`),
|
|
3511
|
+
embeddingsCachePath: path29.join(baseDir, `embeddings.${project}.json`),
|
|
3512
|
+
policyViolationsPath: path29.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
3293
3513
|
};
|
|
3294
3514
|
}
|
|
3295
3515
|
var Projects = class {
|
|
@@ -3327,6 +3547,175 @@ function parseExtraProjects(raw) {
|
|
|
3327
3547
|
return raw.split(",").map((p) => p.trim()).filter((p) => p.length > 0 && p !== DEFAULT_PROJECT);
|
|
3328
3548
|
}
|
|
3329
3549
|
|
|
3550
|
+
// src/registry.ts
|
|
3551
|
+
import { promises as fs17 } from "fs";
|
|
3552
|
+
import os2 from "os";
|
|
3553
|
+
import path30 from "path";
|
|
3554
|
+
import {
|
|
3555
|
+
RegistryFileSchema
|
|
3556
|
+
} from "@neat.is/types";
|
|
3557
|
+
var LOCK_TIMEOUT_MS = 5e3;
|
|
3558
|
+
var LOCK_RETRY_MS = 50;
|
|
3559
|
+
function neatHome() {
|
|
3560
|
+
const override = process.env.NEAT_HOME;
|
|
3561
|
+
if (override && override.length > 0) return path30.resolve(override);
|
|
3562
|
+
return path30.join(os2.homedir(), ".neat");
|
|
3563
|
+
}
|
|
3564
|
+
function registryPath() {
|
|
3565
|
+
return path30.join(neatHome(), "projects.json");
|
|
3566
|
+
}
|
|
3567
|
+
function registryLockPath() {
|
|
3568
|
+
return path30.join(neatHome(), "projects.json.lock");
|
|
3569
|
+
}
|
|
3570
|
+
async function normalizeProjectPath(input) {
|
|
3571
|
+
const resolved = path30.resolve(input);
|
|
3572
|
+
try {
|
|
3573
|
+
return await fs17.realpath(resolved);
|
|
3574
|
+
} catch {
|
|
3575
|
+
return resolved;
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
async function writeAtomically(target, contents) {
|
|
3579
|
+
await fs17.mkdir(path30.dirname(target), { recursive: true });
|
|
3580
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
3581
|
+
const fd = await fs17.open(tmp, "w");
|
|
3582
|
+
try {
|
|
3583
|
+
await fd.writeFile(contents, "utf8");
|
|
3584
|
+
await fd.sync();
|
|
3585
|
+
} finally {
|
|
3586
|
+
await fd.close();
|
|
3587
|
+
}
|
|
3588
|
+
await fs17.rename(tmp, target);
|
|
3589
|
+
}
|
|
3590
|
+
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
3591
|
+
const deadline = Date.now() + timeoutMs;
|
|
3592
|
+
await fs17.mkdir(path30.dirname(lockPath), { recursive: true });
|
|
3593
|
+
while (true) {
|
|
3594
|
+
try {
|
|
3595
|
+
const fd = await fs17.open(lockPath, "wx");
|
|
3596
|
+
await fd.close();
|
|
3597
|
+
return;
|
|
3598
|
+
} catch (err) {
|
|
3599
|
+
const code = err.code;
|
|
3600
|
+
if (code !== "EEXIST") throw err;
|
|
3601
|
+
if (Date.now() >= deadline) {
|
|
3602
|
+
throw new Error(
|
|
3603
|
+
`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.`
|
|
3604
|
+
);
|
|
3605
|
+
}
|
|
3606
|
+
await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
async function releaseLock(lockPath) {
|
|
3611
|
+
await fs17.unlink(lockPath).catch(() => {
|
|
3612
|
+
});
|
|
3613
|
+
}
|
|
3614
|
+
async function withLock(fn) {
|
|
3615
|
+
const lock = registryLockPath();
|
|
3616
|
+
await acquireLock(lock);
|
|
3617
|
+
try {
|
|
3618
|
+
return await fn();
|
|
3619
|
+
} finally {
|
|
3620
|
+
await releaseLock(lock);
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
async function readRegistry() {
|
|
3624
|
+
const file = registryPath();
|
|
3625
|
+
let raw;
|
|
3626
|
+
try {
|
|
3627
|
+
raw = await fs17.readFile(file, "utf8");
|
|
3628
|
+
} catch (err) {
|
|
3629
|
+
if (err.code === "ENOENT") {
|
|
3630
|
+
return { version: 1, projects: [] };
|
|
3631
|
+
}
|
|
3632
|
+
throw err;
|
|
3633
|
+
}
|
|
3634
|
+
const parsed = JSON.parse(raw);
|
|
3635
|
+
return RegistryFileSchema.parse(parsed);
|
|
3636
|
+
}
|
|
3637
|
+
async function writeRegistry(reg) {
|
|
3638
|
+
const validated = RegistryFileSchema.parse(reg);
|
|
3639
|
+
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
3640
|
+
}
|
|
3641
|
+
var ProjectNameCollisionError = class extends Error {
|
|
3642
|
+
projectName;
|
|
3643
|
+
constructor(name) {
|
|
3644
|
+
super(`neat registry: a project named "${name}" is already registered`);
|
|
3645
|
+
this.name = "ProjectNameCollisionError";
|
|
3646
|
+
this.projectName = name;
|
|
3647
|
+
}
|
|
3648
|
+
};
|
|
3649
|
+
async function addProject(opts) {
|
|
3650
|
+
const resolvedPath = await normalizeProjectPath(opts.path);
|
|
3651
|
+
return withLock(async () => {
|
|
3652
|
+
const reg = await readRegistry();
|
|
3653
|
+
const byName = reg.projects.find((p) => p.name === opts.name);
|
|
3654
|
+
const byPath = reg.projects.find((p) => p.path === resolvedPath);
|
|
3655
|
+
if (byName && byName.path !== resolvedPath) {
|
|
3656
|
+
throw new ProjectNameCollisionError(opts.name);
|
|
3657
|
+
}
|
|
3658
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3659
|
+
if (byName && byName.path === resolvedPath) {
|
|
3660
|
+
byName.lastSeenAt = now;
|
|
3661
|
+
if (opts.languages) byName.languages = opts.languages;
|
|
3662
|
+
if (opts.status) byName.status = opts.status;
|
|
3663
|
+
await writeRegistry(reg);
|
|
3664
|
+
return byName;
|
|
3665
|
+
}
|
|
3666
|
+
if (byPath && byPath.name !== opts.name) {
|
|
3667
|
+
throw new ProjectNameCollisionError(byPath.name);
|
|
3668
|
+
}
|
|
3669
|
+
const entry = {
|
|
3670
|
+
name: opts.name,
|
|
3671
|
+
path: resolvedPath,
|
|
3672
|
+
registeredAt: now,
|
|
3673
|
+
languages: opts.languages ?? [],
|
|
3674
|
+
status: opts.status ?? "active"
|
|
3675
|
+
};
|
|
3676
|
+
reg.projects.push(entry);
|
|
3677
|
+
await writeRegistry(reg);
|
|
3678
|
+
return entry;
|
|
3679
|
+
});
|
|
3680
|
+
}
|
|
3681
|
+
async function getProject(name) {
|
|
3682
|
+
const reg = await readRegistry();
|
|
3683
|
+
return reg.projects.find((p) => p.name === name);
|
|
3684
|
+
}
|
|
3685
|
+
async function listProjects() {
|
|
3686
|
+
const reg = await readRegistry();
|
|
3687
|
+
return reg.projects;
|
|
3688
|
+
}
|
|
3689
|
+
async function setStatus(name, status) {
|
|
3690
|
+
return withLock(async () => {
|
|
3691
|
+
const reg = await readRegistry();
|
|
3692
|
+
const entry = reg.projects.find((p) => p.name === name);
|
|
3693
|
+
if (!entry) throw new Error(`neat registry: no project named "${name}"`);
|
|
3694
|
+
entry.status = status;
|
|
3695
|
+
await writeRegistry(reg);
|
|
3696
|
+
return entry;
|
|
3697
|
+
});
|
|
3698
|
+
}
|
|
3699
|
+
async function touchLastSeen(name, at = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3700
|
+
await withLock(async () => {
|
|
3701
|
+
const reg = await readRegistry();
|
|
3702
|
+
const entry = reg.projects.find((p) => p.name === name);
|
|
3703
|
+
if (!entry) return;
|
|
3704
|
+
entry.lastSeenAt = at;
|
|
3705
|
+
await writeRegistry(reg);
|
|
3706
|
+
});
|
|
3707
|
+
}
|
|
3708
|
+
async function removeProject(name) {
|
|
3709
|
+
return withLock(async () => {
|
|
3710
|
+
const reg = await readRegistry();
|
|
3711
|
+
const idx = reg.projects.findIndex((p) => p.name === name);
|
|
3712
|
+
if (idx < 0) return void 0;
|
|
3713
|
+
const [removed] = reg.projects.splice(idx, 1);
|
|
3714
|
+
await writeRegistry(reg);
|
|
3715
|
+
return removed;
|
|
3716
|
+
});
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3330
3719
|
export {
|
|
3331
3720
|
DEFAULT_PROJECT,
|
|
3332
3721
|
getGraph,
|
|
@@ -3334,6 +3723,10 @@ export {
|
|
|
3334
3723
|
checkCompatibility,
|
|
3335
3724
|
ensureCompatLoaded,
|
|
3336
3725
|
compatPairs,
|
|
3726
|
+
EVENT_BUS_CHANNEL,
|
|
3727
|
+
eventBus,
|
|
3728
|
+
emitNeatEvent,
|
|
3729
|
+
attachGraphToEventBus,
|
|
3337
3730
|
confidenceForEdge,
|
|
3338
3731
|
getRootCause,
|
|
3339
3732
|
getBlastRadius,
|
|
@@ -3366,6 +3759,18 @@ export {
|
|
|
3366
3759
|
startPersistLoop,
|
|
3367
3760
|
pathsForProject,
|
|
3368
3761
|
Projects,
|
|
3369
|
-
parseExtraProjects
|
|
3762
|
+
parseExtraProjects,
|
|
3763
|
+
registryPath,
|
|
3764
|
+
registryLockPath,
|
|
3765
|
+
normalizeProjectPath,
|
|
3766
|
+
writeAtomically,
|
|
3767
|
+
readRegistry,
|
|
3768
|
+
ProjectNameCollisionError,
|
|
3769
|
+
addProject,
|
|
3770
|
+
getProject,
|
|
3771
|
+
listProjects,
|
|
3772
|
+
setStatus,
|
|
3773
|
+
touchLastSeen,
|
|
3774
|
+
removeProject
|
|
3370
3775
|
};
|
|
3371
|
-
//# sourceMappingURL=chunk-
|
|
3776
|
+
//# sourceMappingURL=chunk-IRPH6KL4.js.map
|