@neat.is/core 0.2.8 → 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.
@@ -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, path30, edges) {
431
- if (path30.length > best.path.length) {
432
- best = { path: [...path30], edges: [...edges] };
430
+ function step(node, path31, edges) {
431
+ if (path31.length > best.path.length) {
432
+ best = { path: [...path31], edges: [...edges] };
433
433
  }
434
- if (path30.length - 1 >= maxDepth) return;
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
- path30.push(srcId);
439
+ path31.push(srcId);
440
440
  edges.push(edge);
441
- step(srcId, path30, edges);
442
- path30.pop();
441
+ step(srcId, path31, edges);
442
+ path31.pop();
443
443
  edges.pop();
444
444
  visited.delete(srcId);
445
445
  }
@@ -1561,10 +1561,10 @@ async function readErrorEvents(errorsPath) {
1561
1561
  }
1562
1562
 
1563
1563
  // src/extract/services.ts
1564
- import { promises as fs6 } from "fs";
1565
- import path6 from "path";
1564
+ import { promises as fs7 } from "fs";
1565
+ import path7 from "path";
1566
1566
  import ignore from "ignore";
1567
- import { minimatch } from "minimatch";
1567
+ import { minimatch as minimatch2 } from "minimatch";
1568
1568
  import { NodeType as NodeType4, serviceId as serviceId2 } from "@neat.is/types";
1569
1569
 
1570
1570
  // src/extract/shared.ts
@@ -1675,6 +1675,72 @@ function pythonToPackage(service) {
1675
1675
  };
1676
1676
  }
1677
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
+
1678
1744
  // src/extract/services.ts
1679
1745
  var DEFAULT_SCAN_DEPTH = 5;
1680
1746
  function parseScanDepth() {
@@ -1691,21 +1757,21 @@ function workspaceGlobs(pkg) {
1691
1757
  return null;
1692
1758
  }
1693
1759
  async function loadGitignore(scanPath) {
1694
- const gitignorePath = path6.join(scanPath, ".gitignore");
1760
+ const gitignorePath = path7.join(scanPath, ".gitignore");
1695
1761
  if (!await exists(gitignorePath)) return null;
1696
- const raw = await fs6.readFile(gitignorePath, "utf8");
1762
+ const raw = await fs7.readFile(gitignorePath, "utf8");
1697
1763
  return ignore().add(raw);
1698
1764
  }
1699
1765
  async function walkDirs(start, scanPath, options, visit) {
1700
1766
  async function recurse(current, depth) {
1701
1767
  if (depth > options.maxDepth) return;
1702
- const entries = await fs6.readdir(current, { withFileTypes: true }).catch(() => []);
1768
+ const entries = await fs7.readdir(current, { withFileTypes: true }).catch(() => []);
1703
1769
  for (const entry of entries) {
1704
1770
  if (!entry.isDirectory()) continue;
1705
1771
  if (IGNORED_DIRS.has(entry.name)) continue;
1706
- const child = path6.join(current, entry.name);
1772
+ const child = path7.join(current, entry.name);
1707
1773
  if (options.ig) {
1708
- const rel = path6.relative(scanPath, child).split(path6.sep).join("/");
1774
+ const rel = path7.relative(scanPath, child).split(path7.sep).join("/");
1709
1775
  if (rel && options.ig.ignores(rel + "/")) continue;
1710
1776
  }
1711
1777
  await visit(child);
@@ -1720,8 +1786,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
1720
1786
  for (const raw of globs) {
1721
1787
  const pattern = raw.replace(/^\.\//, "");
1722
1788
  if (!pattern.includes("*")) {
1723
- const candidate = path6.join(scanPath, pattern);
1724
- if (await exists(path6.join(candidate, "package.json"))) found.add(candidate);
1789
+ const candidate = path7.join(scanPath, pattern);
1790
+ if (await exists(path7.join(candidate, "package.json"))) found.add(candidate);
1725
1791
  continue;
1726
1792
  }
1727
1793
  const segments = pattern.split("/");
@@ -1730,13 +1796,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
1730
1796
  if (seg.includes("*")) break;
1731
1797
  staticSegments.push(seg);
1732
1798
  }
1733
- const start = path6.join(scanPath, ...staticSegments);
1799
+ const start = path7.join(scanPath, ...staticSegments);
1734
1800
  if (!await exists(start)) continue;
1735
1801
  const hasDoubleStar = pattern.includes("**");
1736
1802
  const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
1737
1803
  await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
1738
- const rel = path6.relative(scanPath, dir).split(path6.sep).join("/");
1739
- if (minimatch(rel, pattern) && await exists(path6.join(dir, "package.json"))) {
1804
+ const rel = path7.relative(scanPath, dir).split(path7.sep).join("/");
1805
+ if (minimatch2(rel, pattern) && await exists(path7.join(dir, "package.json"))) {
1740
1806
  found.add(dir);
1741
1807
  }
1742
1808
  });
@@ -1744,9 +1810,17 @@ async function expandWorkspaceGlobs(scanPath, globs) {
1744
1810
  return [...found];
1745
1811
  }
1746
1812
  async function discoverNodeService(scanPath, dir) {
1747
- const pkgPath = path6.join(dir, "package.json");
1813
+ const pkgPath = path7.join(dir, "package.json");
1748
1814
  if (!await exists(pkgPath)) return null;
1749
- const pkg = await readJson(pkgPath);
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
+ }
1750
1824
  if (!pkg.name) return null;
1751
1825
  const node = {
1752
1826
  id: serviceId2(pkg.name),
@@ -1755,7 +1829,7 @@ async function discoverNodeService(scanPath, dir) {
1755
1829
  language: "javascript",
1756
1830
  version: pkg.version,
1757
1831
  dependencies: pkg.dependencies ?? {},
1758
- repoPath: path6.relative(scanPath, dir),
1832
+ repoPath: path7.relative(scanPath, dir),
1759
1833
  ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
1760
1834
  };
1761
1835
  return { pkg, dir, node };
@@ -1771,13 +1845,22 @@ async function discoverPyService(scanPath, dir) {
1771
1845
  language: "python",
1772
1846
  version: py.version,
1773
1847
  dependencies: py.dependencies,
1774
- repoPath: path6.relative(scanPath, dir)
1848
+ repoPath: path7.relative(scanPath, dir)
1775
1849
  };
1776
1850
  return { pkg, dir, node };
1777
1851
  }
1778
1852
  async function discoverServices(scanPath) {
1779
- const rootPkgPath = path6.join(scanPath, "package.json");
1780
- const rootPkg = await exists(rootPkgPath) ? await readJson(rootPkgPath) : null;
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
+ }
1781
1864
  const wsGlobs = rootPkg ? workspaceGlobs(rootPkg) : null;
1782
1865
  const candidateDirs = [];
1783
1866
  if (wsGlobs) {
@@ -1790,9 +1873,9 @@ async function discoverServices(scanPath) {
1790
1873
  scanPath,
1791
1874
  { maxDepth: parseScanDepth(), ig },
1792
1875
  async (dir) => {
1793
- if (await exists(path6.join(dir, "package.json"))) {
1876
+ if (await exists(path7.join(dir, "package.json"))) {
1794
1877
  candidateDirs.push(dir);
1795
- } else if (await exists(path6.join(dir, "pyproject.toml")) || await exists(path6.join(dir, "requirements.txt")) || await exists(path6.join(dir, "setup.py"))) {
1878
+ } else if (await exists(path7.join(dir, "pyproject.toml")) || await exists(path7.join(dir, "requirements.txt")) || await exists(path7.join(dir, "setup.py"))) {
1796
1879
  candidateDirs.push(dir);
1797
1880
  }
1798
1881
  }
@@ -1806,8 +1889,8 @@ async function discoverServices(scanPath) {
1806
1889
  if (!service) continue;
1807
1890
  const existingDir = seen.get(service.node.name);
1808
1891
  if (existingDir !== void 0) {
1809
- const a = path6.relative(scanPath, existingDir) || ".";
1810
- const b = path6.relative(scanPath, dir) || ".";
1892
+ const a = path7.relative(scanPath, existingDir) || ".";
1893
+ const b = path7.relative(scanPath, dir) || ".";
1811
1894
  console.warn(
1812
1895
  `[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
1813
1896
  );
@@ -1816,6 +1899,11 @@ async function discoverServices(scanPath) {
1816
1899
  seen.set(service.node.name, dir);
1817
1900
  out.push(service);
1818
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
+ }
1819
1907
  return out;
1820
1908
  }
1821
1909
  function addServiceNodes(graph, services) {
@@ -1838,8 +1926,8 @@ function addServiceNodes(graph, services) {
1838
1926
  }
1839
1927
 
1840
1928
  // src/extract/aliases.ts
1841
- import path7 from "path";
1842
- import { promises as fs7 } from "fs";
1929
+ import path8 from "path";
1930
+ import { promises as fs8 } from "fs";
1843
1931
  import { parseAllDocuments } from "yaml";
1844
1932
  import { NodeType as NodeType5 } from "@neat.is/types";
1845
1933
  var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
@@ -1866,21 +1954,29 @@ function indexServicesByName(services) {
1866
1954
  const map = /* @__PURE__ */ new Map();
1867
1955
  for (const s of services) {
1868
1956
  map.set(s.node.name, s.node.id);
1869
- map.set(path7.basename(s.dir), s.node.id);
1957
+ map.set(path8.basename(s.dir), s.node.id);
1870
1958
  }
1871
1959
  return map;
1872
1960
  }
1873
1961
  async function collectComposeAliases(graph, scanPath, serviceIndex) {
1874
1962
  let composePath = null;
1875
1963
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
1876
- const abs = path7.join(scanPath, name);
1964
+ const abs = path8.join(scanPath, name);
1877
1965
  if (await exists(abs)) {
1878
1966
  composePath = abs;
1879
1967
  break;
1880
1968
  }
1881
1969
  }
1882
1970
  if (!composePath) return;
1883
- const compose = await readYaml(composePath);
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
+ }
1884
1980
  if (!compose?.services) return;
1885
1981
  for (const [composeName, svc] of Object.entries(compose.services)) {
1886
1982
  const serviceId3 = serviceIndex.get(composeName);
@@ -1919,9 +2015,17 @@ function parseDockerfileLabels(content) {
1919
2015
  }
1920
2016
  async function collectDockerfileAliases(graph, services) {
1921
2017
  for (const service of services) {
1922
- const dockerfilePath = path7.join(service.dir, "Dockerfile");
2018
+ const dockerfilePath = path8.join(service.dir, "Dockerfile");
1923
2019
  if (!await exists(dockerfilePath)) continue;
1924
- const content = await fs7.readFile(dockerfilePath, "utf8");
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
+ }
1925
2029
  const aliases = parseDockerfileLabels(content);
1926
2030
  if (aliases.length > 0) addAliases(graph, service.node.id, aliases);
1927
2031
  }
@@ -1929,13 +2033,13 @@ async function collectDockerfileAliases(graph, services) {
1929
2033
  async function walkYamlFiles(start, depth = 0, max = 5) {
1930
2034
  if (depth > max) return [];
1931
2035
  const out = [];
1932
- const entries = await fs7.readdir(start, { withFileTypes: true }).catch(() => []);
2036
+ const entries = await fs8.readdir(start, { withFileTypes: true }).catch(() => []);
1933
2037
  for (const entry of entries) {
1934
2038
  if (entry.isDirectory()) {
1935
2039
  if (IGNORED_DIRS.has(entry.name)) continue;
1936
- out.push(...await walkYamlFiles(path7.join(start, entry.name), depth + 1, max));
1937
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path7.extname(entry.name))) {
1938
- out.push(path7.join(start, entry.name));
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));
1939
2043
  }
1940
2044
  }
1941
2045
  return out;
@@ -1962,7 +2066,7 @@ function k8sServiceTarget(doc, byName) {
1962
2066
  async function collectK8sAliases(graph, scanPath, serviceIndex) {
1963
2067
  const files = await walkYamlFiles(scanPath);
1964
2068
  for (const file of files) {
1965
- const content = await fs7.readFile(file, "utf8");
2069
+ const content = await fs8.readFile(file, "utf8");
1966
2070
  let docs;
1967
2071
  try {
1968
2072
  docs = parseAllDocuments(content).map((d) => d.toJSON());
@@ -1986,13 +2090,13 @@ async function addServiceAliases(graph, scanPath, services) {
1986
2090
  }
1987
2091
 
1988
2092
  // src/extract/databases/index.ts
1989
- import path15 from "path";
2093
+ import path16 from "path";
1990
2094
  import { EdgeType as EdgeType3, NodeType as NodeType6, Provenance as Provenance4, databaseId as databaseId2 } from "@neat.is/types";
1991
2095
 
1992
2096
  // src/extract/databases/db-config-yaml.ts
1993
- import path8 from "path";
2097
+ import path9 from "path";
1994
2098
  async function parse(serviceDir) {
1995
- const yamlPath = path8.join(serviceDir, "db-config.yaml");
2099
+ const yamlPath = path9.join(serviceDir, "db-config.yaml");
1996
2100
  if (!await exists(yamlPath)) return [];
1997
2101
  const raw = await readYaml(yamlPath);
1998
2102
  return [
@@ -2009,12 +2113,12 @@ async function parse(serviceDir) {
2009
2113
  var dbConfigYamlParser = { name: "db-config.yaml", parse };
2010
2114
 
2011
2115
  // src/extract/databases/dotenv.ts
2012
- import { promises as fs9 } from "fs";
2013
- import path10 from "path";
2116
+ import { promises as fs10 } from "fs";
2117
+ import path11 from "path";
2014
2118
 
2015
2119
  // src/extract/databases/shared.ts
2016
- import { promises as fs8 } from "fs";
2017
- import path9 from "path";
2120
+ import { promises as fs9 } from "fs";
2121
+ import path10 from "path";
2018
2122
  function schemeToEngine(scheme) {
2019
2123
  const s = scheme.toLowerCase().split("+")[0];
2020
2124
  switch (s) {
@@ -2053,14 +2157,14 @@ function parseConnectionString(url) {
2053
2157
  }
2054
2158
  async function readIfExists(filePath) {
2055
2159
  try {
2056
- return await fs8.readFile(filePath, "utf8");
2160
+ return await fs9.readFile(filePath, "utf8");
2057
2161
  } catch {
2058
2162
  return null;
2059
2163
  }
2060
2164
  }
2061
2165
  async function findFirst(serviceDir, candidates) {
2062
2166
  for (const rel of candidates) {
2063
- const abs = path9.join(serviceDir, rel);
2167
+ const abs = path10.join(serviceDir, rel);
2064
2168
  const content = await readIfExists(abs);
2065
2169
  if (content !== null) return abs;
2066
2170
  }
@@ -2111,15 +2215,15 @@ function parseDotenvLine(line) {
2111
2215
  return { key, value };
2112
2216
  }
2113
2217
  async function parse2(serviceDir) {
2114
- const entries = await fs9.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2218
+ const entries = await fs10.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2115
2219
  const configs = [];
2116
2220
  const seen = /* @__PURE__ */ new Set();
2117
2221
  for (const entry of entries) {
2118
2222
  if (!entry.isFile()) continue;
2119
2223
  const match = isConfigFile(entry.name);
2120
2224
  if (!match.match || match.fileType !== "env") continue;
2121
- const filePath = path10.join(serviceDir, entry.name);
2122
- const content = await fs9.readFile(filePath, "utf8");
2225
+ const filePath = path11.join(serviceDir, entry.name);
2226
+ const content = await fs10.readFile(filePath, "utf8");
2123
2227
  for (const line of content.split("\n")) {
2124
2228
  const parsed = parseDotenvLine(line);
2125
2229
  if (!parsed) continue;
@@ -2137,9 +2241,9 @@ async function parse2(serviceDir) {
2137
2241
  var dotenvParser = { name: ".env", parse: parse2 };
2138
2242
 
2139
2243
  // src/extract/databases/prisma.ts
2140
- import path11 from "path";
2244
+ import path12 from "path";
2141
2245
  async function parse3(serviceDir) {
2142
- const schemaPath = path11.join(serviceDir, "prisma", "schema.prisma");
2246
+ const schemaPath = path12.join(serviceDir, "prisma", "schema.prisma");
2143
2247
  const content = await readIfExists(schemaPath);
2144
2248
  if (!content) return [];
2145
2249
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -2268,10 +2372,10 @@ async function parse5(serviceDir) {
2268
2372
  var knexParser = { name: "knex", parse: parse5 };
2269
2373
 
2270
2374
  // src/extract/databases/ormconfig.ts
2271
- import path12 from "path";
2375
+ import path13 from "path";
2272
2376
  async function parse6(serviceDir) {
2273
2377
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
2274
- const abs = path12.join(serviceDir, candidate);
2378
+ const abs = path13.join(serviceDir, candidate);
2275
2379
  if (!await exists(abs)) continue;
2276
2380
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
2277
2381
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -2329,9 +2433,9 @@ async function parse7(serviceDir) {
2329
2433
  var typeormParser = { name: "typeorm", parse: parse7 };
2330
2434
 
2331
2435
  // src/extract/databases/sequelize.ts
2332
- import path13 from "path";
2436
+ import path14 from "path";
2333
2437
  async function parse8(serviceDir) {
2334
- const configPath = path13.join(serviceDir, "config", "config.json");
2438
+ const configPath = path14.join(serviceDir, "config", "config.json");
2335
2439
  if (!await exists(configPath)) return [];
2336
2440
  const raw = await readJson(configPath);
2337
2441
  const out = [];
@@ -2357,7 +2461,7 @@ async function parse8(serviceDir) {
2357
2461
  var sequelizeParser = { name: "sequelize", parse: parse8 };
2358
2462
 
2359
2463
  // src/extract/databases/docker-compose.ts
2360
- import path14 from "path";
2464
+ import path15 from "path";
2361
2465
  function portFromService(svc) {
2362
2466
  for (const raw of svc.ports ?? []) {
2363
2467
  const str = String(raw);
@@ -2384,7 +2488,7 @@ function databaseFromEnv(svc) {
2384
2488
  }
2385
2489
  async function parse9(serviceDir) {
2386
2490
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2387
- const abs = path14.join(serviceDir, name);
2491
+ const abs = path15.join(serviceDir, name);
2388
2492
  if (!await exists(abs)) continue;
2389
2493
  const raw = await readYaml(abs);
2390
2494
  if (!raw?.services) return [];
@@ -2564,7 +2668,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
2564
2668
  provenance: Provenance4.EXTRACTED,
2565
2669
  ...config.sourceFile ? {
2566
2670
  evidence: {
2567
- file: path15.relative(scanPath, config.sourceFile).split(path15.sep).join("/")
2671
+ file: path16.relative(scanPath, config.sourceFile).split(path16.sep).join("/")
2568
2672
  }
2569
2673
  } : {}
2570
2674
  };
@@ -2591,15 +2695,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
2591
2695
  }
2592
2696
 
2593
2697
  // src/extract/configs.ts
2594
- import { promises as fs10 } from "fs";
2595
- import path16 from "path";
2698
+ import { promises as fs11 } from "fs";
2699
+ import path17 from "path";
2596
2700
  import { EdgeType as EdgeType4, NodeType as NodeType7, Provenance as Provenance5, configId } from "@neat.is/types";
2597
2701
  async function walkConfigFiles(dir) {
2598
2702
  const out = [];
2599
2703
  async function walk(current) {
2600
- const entries = await fs10.readdir(current, { withFileTypes: true });
2704
+ const entries = await fs11.readdir(current, { withFileTypes: true });
2601
2705
  for (const entry of entries) {
2602
- const full = path16.join(current, entry.name);
2706
+ const full = path17.join(current, entry.name);
2603
2707
  if (entry.isDirectory()) {
2604
2708
  if (!IGNORED_DIRS.has(entry.name)) await walk(full);
2605
2709
  } else if (entry.isFile() && isConfigFile(entry.name).match) {
@@ -2616,13 +2720,13 @@ async function addConfigNodes(graph, services, scanPath) {
2616
2720
  for (const service of services) {
2617
2721
  const configFiles = await walkConfigFiles(service.dir);
2618
2722
  for (const file of configFiles) {
2619
- const relPath = path16.relative(scanPath, file);
2723
+ const relPath = path17.relative(scanPath, file);
2620
2724
  const node = {
2621
2725
  id: configId(relPath),
2622
2726
  type: NodeType7.ConfigNode,
2623
- name: path16.basename(file),
2727
+ name: path17.basename(file),
2624
2728
  path: relPath,
2625
- fileType: isConfigFile(path16.basename(file)).fileType
2729
+ fileType: isConfigFile(path17.basename(file)).fileType
2626
2730
  };
2627
2731
  if (!graph.hasNode(node.id)) {
2628
2732
  graph.addNode(node.id, node);
@@ -2634,7 +2738,7 @@ async function addConfigNodes(graph, services, scanPath) {
2634
2738
  target: node.id,
2635
2739
  type: EdgeType4.CONFIGURED_BY,
2636
2740
  provenance: Provenance5.EXTRACTED,
2637
- evidence: { file: relPath.split(path16.sep).join("/") }
2741
+ evidence: { file: relPath.split(path17.sep).join("/") }
2638
2742
  };
2639
2743
  if (!graph.hasEdge(edge.id)) {
2640
2744
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -2649,24 +2753,24 @@ async function addConfigNodes(graph, services, scanPath) {
2649
2753
  import { EdgeType as EdgeType6, NodeType as NodeType8, Provenance as Provenance7 } from "@neat.is/types";
2650
2754
 
2651
2755
  // src/extract/calls/http.ts
2652
- import path18 from "path";
2756
+ import path19 from "path";
2653
2757
  import Parser from "tree-sitter";
2654
2758
  import JavaScript from "tree-sitter-javascript";
2655
2759
  import Python from "tree-sitter-python";
2656
2760
  import { EdgeType as EdgeType5, Provenance as Provenance6 } from "@neat.is/types";
2657
2761
 
2658
2762
  // src/extract/calls/shared.ts
2659
- import { promises as fs11 } from "fs";
2660
- import path17 from "path";
2763
+ import { promises as fs12 } from "fs";
2764
+ import path18 from "path";
2661
2765
  async function walkSourceFiles(dir) {
2662
2766
  const out = [];
2663
2767
  async function walk(current) {
2664
- const entries = await fs11.readdir(current, { withFileTypes: true }).catch(() => []);
2768
+ const entries = await fs12.readdir(current, { withFileTypes: true }).catch(() => []);
2665
2769
  for (const entry of entries) {
2666
- const full = path17.join(current, entry.name);
2770
+ const full = path18.join(current, entry.name);
2667
2771
  if (entry.isDirectory()) {
2668
2772
  if (!IGNORED_DIRS.has(entry.name)) await walk(full);
2669
- } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path17.extname(entry.name))) {
2773
+ } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path18.extname(entry.name))) {
2670
2774
  out.push(full);
2671
2775
  }
2672
2776
  }
@@ -2679,7 +2783,7 @@ async function loadSourceFiles(dir) {
2679
2783
  const out = [];
2680
2784
  for (const p of paths) {
2681
2785
  try {
2682
- const content = await fs11.readFile(p, "utf8");
2786
+ const content = await fs12.readFile(p, "utf8");
2683
2787
  out.push({ path: p, content });
2684
2788
  } catch {
2685
2789
  }
@@ -2735,9 +2839,9 @@ async function addHttpCallEdges(graph, services) {
2735
2839
  const knownHosts = /* @__PURE__ */ new Set();
2736
2840
  const hostToNodeId = /* @__PURE__ */ new Map();
2737
2841
  for (const service of services) {
2738
- knownHosts.add(path18.basename(service.dir));
2842
+ knownHosts.add(path19.basename(service.dir));
2739
2843
  knownHosts.add(service.pkg.name);
2740
- hostToNodeId.set(path18.basename(service.dir), service.node.id);
2844
+ hostToNodeId.set(path19.basename(service.dir), service.node.id);
2741
2845
  hostToNodeId.set(service.pkg.name, service.node.id);
2742
2846
  }
2743
2847
  let edgesAdded = 0;
@@ -2745,7 +2849,7 @@ async function addHttpCallEdges(graph, services) {
2745
2849
  const files = await loadSourceFiles(service.dir);
2746
2850
  const seenTargets = /* @__PURE__ */ new Map();
2747
2851
  for (const file of files) {
2748
- const parser = path18.extname(file.path) === ".py" ? pyParser : jsParser;
2852
+ const parser = path19.extname(file.path) === ".py" ? pyParser : jsParser;
2749
2853
  let targets;
2750
2854
  try {
2751
2855
  targets = callsFromSource(file.content, parser, knownHosts);
@@ -2773,7 +2877,7 @@ async function addHttpCallEdges(graph, services) {
2773
2877
  type: EdgeType5.CALLS,
2774
2878
  provenance: Provenance6.EXTRACTED,
2775
2879
  evidence: {
2776
- file: path18.relative(service.dir, evidenceFile.file),
2880
+ file: path19.relative(service.dir, evidenceFile.file),
2777
2881
  line,
2778
2882
  snippet: snippet(fileContent, line)
2779
2883
  }
@@ -2788,7 +2892,7 @@ async function addHttpCallEdges(graph, services) {
2788
2892
  }
2789
2893
 
2790
2894
  // src/extract/calls/kafka.ts
2791
- import path19 from "path";
2895
+ import path20 from "path";
2792
2896
  import { infraId } from "@neat.is/types";
2793
2897
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
2794
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;
@@ -2815,7 +2919,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
2815
2919
  kind: "kafka-topic",
2816
2920
  edgeType,
2817
2921
  evidence: {
2818
- file: path19.relative(serviceDir, file.path),
2922
+ file: path20.relative(serviceDir, file.path),
2819
2923
  line,
2820
2924
  snippet: snippet(file.content, line)
2821
2925
  }
@@ -2827,7 +2931,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
2827
2931
  }
2828
2932
 
2829
2933
  // src/extract/calls/redis.ts
2830
- import path20 from "path";
2934
+ import path21 from "path";
2831
2935
  import { infraId as infraId2 } from "@neat.is/types";
2832
2936
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
2833
2937
  function redisEndpointsFromFile(file, serviceDir) {
@@ -2846,7 +2950,7 @@ function redisEndpointsFromFile(file, serviceDir) {
2846
2950
  kind: "redis",
2847
2951
  edgeType: "CALLS",
2848
2952
  evidence: {
2849
- file: path20.relative(serviceDir, file.path),
2953
+ file: path21.relative(serviceDir, file.path),
2850
2954
  line,
2851
2955
  snippet: snippet(file.content, line)
2852
2956
  }
@@ -2856,7 +2960,7 @@ function redisEndpointsFromFile(file, serviceDir) {
2856
2960
  }
2857
2961
 
2858
2962
  // src/extract/calls/aws.ts
2859
- import path21 from "path";
2963
+ import path22 from "path";
2860
2964
  import { infraId as infraId3 } from "@neat.is/types";
2861
2965
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
2862
2966
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
@@ -2886,7 +2990,7 @@ function awsEndpointsFromFile(file, serviceDir) {
2886
2990
  kind,
2887
2991
  edgeType: "CALLS",
2888
2992
  evidence: {
2889
- file: path21.relative(serviceDir, file.path),
2993
+ file: path22.relative(serviceDir, file.path),
2890
2994
  line,
2891
2995
  snippet: snippet(file.content, line)
2892
2996
  }
@@ -2910,7 +3014,7 @@ function awsEndpointsFromFile(file, serviceDir) {
2910
3014
  }
2911
3015
 
2912
3016
  // src/extract/calls/grpc.ts
2913
- import path22 from "path";
3017
+ import path23 from "path";
2914
3018
  import { infraId as infraId4 } from "@neat.is/types";
2915
3019
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
2916
3020
  function isLikelyAddress(value) {
@@ -2935,7 +3039,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
2935
3039
  kind: "grpc-service",
2936
3040
  edgeType: "CALLS",
2937
3041
  evidence: {
2938
- file: path22.relative(serviceDir, file.path),
3042
+ file: path23.relative(serviceDir, file.path),
2939
3043
  line,
2940
3044
  snippet: snippet(file.content, line)
2941
3045
  }
@@ -3011,7 +3115,7 @@ async function addCallEdges(graph, services) {
3011
3115
  }
3012
3116
 
3013
3117
  // src/extract/infra/docker-compose.ts
3014
- import path23 from "path";
3118
+ import path24 from "path";
3015
3119
  import { EdgeType as EdgeType7, Provenance as Provenance8 } from "@neat.is/types";
3016
3120
 
3017
3121
  // src/extract/infra/shared.ts
@@ -3048,7 +3152,7 @@ function dependsOnList(value) {
3048
3152
  }
3049
3153
  function serviceNameToServiceNode(name, services) {
3050
3154
  for (const s of services) {
3051
- if (s.node.name === name || path23.basename(s.dir) === name) return s.node.id;
3155
+ if (s.node.name === name || path24.basename(s.dir) === name) return s.node.id;
3052
3156
  }
3053
3157
  return null;
3054
3158
  }
@@ -3057,16 +3161,24 @@ async function addComposeInfra(graph, scanPath, services) {
3057
3161
  let edgesAdded = 0;
3058
3162
  let composePath = null;
3059
3163
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3060
- const abs = path23.join(scanPath, name);
3164
+ const abs = path24.join(scanPath, name);
3061
3165
  if (await exists(abs)) {
3062
3166
  composePath = abs;
3063
3167
  break;
3064
3168
  }
3065
3169
  }
3066
3170
  if (!composePath) return { nodesAdded, edgesAdded };
3067
- const compose = await readYaml(composePath);
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
+ }
3068
3180
  if (!compose?.services) return { nodesAdded, edgesAdded };
3069
- const evidenceFile = path23.relative(scanPath, composePath).split(path23.sep).join("/");
3181
+ const evidenceFile = path24.relative(scanPath, composePath).split(path24.sep).join("/");
3070
3182
  const composeNameToNodeId = /* @__PURE__ */ new Map();
3071
3183
  for (const [composeName, svc] of Object.entries(compose.services)) {
3072
3184
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -3106,8 +3218,8 @@ async function addComposeInfra(graph, scanPath, services) {
3106
3218
  }
3107
3219
 
3108
3220
  // src/extract/infra/dockerfile.ts
3109
- import path24 from "path";
3110
- import { promises as fs12 } from "fs";
3221
+ import path25 from "path";
3222
+ import { promises as fs13 } from "fs";
3111
3223
  import { EdgeType as EdgeType8, Provenance as Provenance9 } from "@neat.is/types";
3112
3224
  function runtimeImage(content) {
3113
3225
  const lines = content.split("\n");
@@ -3127,9 +3239,17 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3127
3239
  let nodesAdded = 0;
3128
3240
  let edgesAdded = 0;
3129
3241
  for (const service of services) {
3130
- const dockerfilePath = path24.join(service.dir, "Dockerfile");
3242
+ const dockerfilePath = path25.join(service.dir, "Dockerfile");
3131
3243
  if (!await exists(dockerfilePath)) continue;
3132
- const content = await fs12.readFile(dockerfilePath, "utf8");
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
+ }
3133
3253
  const image = runtimeImage(content);
3134
3254
  if (!image) continue;
3135
3255
  const node = makeInfraNode("container-image", image);
@@ -3146,7 +3266,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3146
3266
  type: EdgeType8.RUNS_ON,
3147
3267
  provenance: Provenance9.EXTRACTED,
3148
3268
  evidence: {
3149
- file: path24.relative(scanPath, dockerfilePath).split(path24.sep).join("/")
3269
+ file: path25.relative(scanPath, dockerfilePath).split(path25.sep).join("/")
3150
3270
  }
3151
3271
  };
3152
3272
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3157,19 +3277,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3157
3277
  }
3158
3278
 
3159
3279
  // src/extract/infra/terraform.ts
3160
- import { promises as fs13 } from "fs";
3161
- import path25 from "path";
3280
+ import { promises as fs14 } from "fs";
3281
+ import path26 from "path";
3162
3282
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
3163
3283
  async function walkTfFiles(start, depth = 0, max = 5) {
3164
3284
  if (depth > max) return [];
3165
3285
  const out = [];
3166
- const entries = await fs13.readdir(start, { withFileTypes: true }).catch(() => []);
3286
+ const entries = await fs14.readdir(start, { withFileTypes: true }).catch(() => []);
3167
3287
  for (const entry of entries) {
3168
3288
  if (entry.isDirectory()) {
3169
3289
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
3170
- out.push(...await walkTfFiles(path25.join(start, entry.name), depth + 1, max));
3290
+ out.push(...await walkTfFiles(path26.join(start, entry.name), depth + 1, max));
3171
3291
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
3172
- out.push(path25.join(start, entry.name));
3292
+ out.push(path26.join(start, entry.name));
3173
3293
  }
3174
3294
  }
3175
3295
  return out;
@@ -3178,7 +3298,7 @@ async function addTerraformResources(graph, scanPath) {
3178
3298
  let nodesAdded = 0;
3179
3299
  const files = await walkTfFiles(scanPath);
3180
3300
  for (const file of files) {
3181
- const content = await fs13.readFile(file, "utf8");
3301
+ const content = await fs14.readFile(file, "utf8");
3182
3302
  RESOURCE_RE.lastIndex = 0;
3183
3303
  let m;
3184
3304
  while ((m = RESOURCE_RE.exec(content)) !== null) {
@@ -3195,8 +3315,8 @@ async function addTerraformResources(graph, scanPath) {
3195
3315
  }
3196
3316
 
3197
3317
  // src/extract/infra/k8s.ts
3198
- import { promises as fs14 } from "fs";
3199
- import path26 from "path";
3318
+ import { promises as fs15 } from "fs";
3319
+ import path27 from "path";
3200
3320
  import { parseAllDocuments as parseAllDocuments2 } from "yaml";
3201
3321
  var K8S_KIND_TO_INFRA_KIND = {
3202
3322
  Service: "k8s-service",
@@ -3210,13 +3330,13 @@ var K8S_KIND_TO_INFRA_KIND = {
3210
3330
  async function walkYamlFiles2(start, depth = 0, max = 5) {
3211
3331
  if (depth > max) return [];
3212
3332
  const out = [];
3213
- const entries = await fs14.readdir(start, { withFileTypes: true }).catch(() => []);
3333
+ const entries = await fs15.readdir(start, { withFileTypes: true }).catch(() => []);
3214
3334
  for (const entry of entries) {
3215
3335
  if (entry.isDirectory()) {
3216
3336
  if (IGNORED_DIRS.has(entry.name)) continue;
3217
- out.push(...await walkYamlFiles2(path26.join(start, entry.name), depth + 1, max));
3218
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path26.extname(entry.name))) {
3219
- out.push(path26.join(start, entry.name));
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));
3220
3340
  }
3221
3341
  }
3222
3342
  return out;
@@ -3225,7 +3345,7 @@ async function addK8sResources(graph, scanPath) {
3225
3345
  let nodesAdded = 0;
3226
3346
  const files = await walkYamlFiles2(scanPath);
3227
3347
  for (const file of files) {
3228
- const content = await fs14.readFile(file, "utf8");
3348
+ const content = await fs15.readFile(file, "utf8");
3229
3349
  let docs;
3230
3350
  try {
3231
3351
  docs = parseAllDocuments2(content).map((d) => d.toJSON());
@@ -3290,8 +3410,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3290
3410
  }
3291
3411
 
3292
3412
  // src/persist.ts
3293
- import { promises as fs15 } from "fs";
3294
- import path27 from "path";
3413
+ import { promises as fs16 } from "fs";
3414
+ import path28 from "path";
3295
3415
  var SCHEMA_VERSION = 2;
3296
3416
  function migrateV1ToV2(payload) {
3297
3417
  const nodes = payload.graph.nodes;
@@ -3305,7 +3425,7 @@ function migrateV1ToV2(payload) {
3305
3425
  return { ...payload, schemaVersion: 2 };
3306
3426
  }
3307
3427
  async function ensureDir(filePath) {
3308
- await fs15.mkdir(path27.dirname(filePath), { recursive: true });
3428
+ await fs16.mkdir(path28.dirname(filePath), { recursive: true });
3309
3429
  }
3310
3430
  async function saveGraphToDisk(graph, outPath) {
3311
3431
  await ensureDir(outPath);
@@ -3315,13 +3435,13 @@ async function saveGraphToDisk(graph, outPath) {
3315
3435
  graph: graph.export()
3316
3436
  };
3317
3437
  const tmp = `${outPath}.tmp`;
3318
- await fs15.writeFile(tmp, JSON.stringify(payload), "utf8");
3319
- await fs15.rename(tmp, outPath);
3438
+ await fs16.writeFile(tmp, JSON.stringify(payload), "utf8");
3439
+ await fs16.rename(tmp, outPath);
3320
3440
  }
3321
3441
  async function loadGraphFromDisk(graph, outPath) {
3322
3442
  let raw;
3323
3443
  try {
3324
- raw = await fs15.readFile(outPath, "utf8");
3444
+ raw = await fs16.readFile(outPath, "utf8");
3325
3445
  } catch (err) {
3326
3446
  if (err.code === "ENOENT") return;
3327
3447
  throw err;
@@ -3373,23 +3493,23 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
3373
3493
  }
3374
3494
 
3375
3495
  // src/projects.ts
3376
- import path28 from "path";
3496
+ import path29 from "path";
3377
3497
  function pathsForProject(project, baseDir) {
3378
3498
  if (project === DEFAULT_PROJECT) {
3379
3499
  return {
3380
- snapshotPath: path28.join(baseDir, "graph.json"),
3381
- errorsPath: path28.join(baseDir, "errors.ndjson"),
3382
- staleEventsPath: path28.join(baseDir, "stale-events.ndjson"),
3383
- embeddingsCachePath: path28.join(baseDir, "embeddings.json"),
3384
- policyViolationsPath: path28.join(baseDir, "policy-violations.ndjson")
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")
3385
3505
  };
3386
3506
  }
3387
3507
  return {
3388
- snapshotPath: path28.join(baseDir, `${project}.json`),
3389
- errorsPath: path28.join(baseDir, `errors.${project}.ndjson`),
3390
- staleEventsPath: path28.join(baseDir, `stale-events.${project}.ndjson`),
3391
- embeddingsCachePath: path28.join(baseDir, `embeddings.${project}.json`),
3392
- policyViolationsPath: path28.join(baseDir, `policy-violations.${project}.ndjson`)
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`)
3393
3513
  };
3394
3514
  }
3395
3515
  var Projects = class {
@@ -3428,9 +3548,9 @@ function parseExtraProjects(raw) {
3428
3548
  }
3429
3549
 
3430
3550
  // src/registry.ts
3431
- import { promises as fs16 } from "fs";
3551
+ import { promises as fs17 } from "fs";
3432
3552
  import os2 from "os";
3433
- import path29 from "path";
3553
+ import path30 from "path";
3434
3554
  import {
3435
3555
  RegistryFileSchema
3436
3556
  } from "@neat.is/types";
@@ -3438,41 +3558,41 @@ var LOCK_TIMEOUT_MS = 5e3;
3438
3558
  var LOCK_RETRY_MS = 50;
3439
3559
  function neatHome() {
3440
3560
  const override = process.env.NEAT_HOME;
3441
- if (override && override.length > 0) return path29.resolve(override);
3442
- return path29.join(os2.homedir(), ".neat");
3561
+ if (override && override.length > 0) return path30.resolve(override);
3562
+ return path30.join(os2.homedir(), ".neat");
3443
3563
  }
3444
3564
  function registryPath() {
3445
- return path29.join(neatHome(), "projects.json");
3565
+ return path30.join(neatHome(), "projects.json");
3446
3566
  }
3447
3567
  function registryLockPath() {
3448
- return path29.join(neatHome(), "projects.json.lock");
3568
+ return path30.join(neatHome(), "projects.json.lock");
3449
3569
  }
3450
3570
  async function normalizeProjectPath(input) {
3451
- const resolved = path29.resolve(input);
3571
+ const resolved = path30.resolve(input);
3452
3572
  try {
3453
- return await fs16.realpath(resolved);
3573
+ return await fs17.realpath(resolved);
3454
3574
  } catch {
3455
3575
  return resolved;
3456
3576
  }
3457
3577
  }
3458
3578
  async function writeAtomically(target, contents) {
3459
- await fs16.mkdir(path29.dirname(target), { recursive: true });
3579
+ await fs17.mkdir(path30.dirname(target), { recursive: true });
3460
3580
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
3461
- const fd = await fs16.open(tmp, "w");
3581
+ const fd = await fs17.open(tmp, "w");
3462
3582
  try {
3463
3583
  await fd.writeFile(contents, "utf8");
3464
3584
  await fd.sync();
3465
3585
  } finally {
3466
3586
  await fd.close();
3467
3587
  }
3468
- await fs16.rename(tmp, target);
3588
+ await fs17.rename(tmp, target);
3469
3589
  }
3470
3590
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
3471
3591
  const deadline = Date.now() + timeoutMs;
3472
- await fs16.mkdir(path29.dirname(lockPath), { recursive: true });
3592
+ await fs17.mkdir(path30.dirname(lockPath), { recursive: true });
3473
3593
  while (true) {
3474
3594
  try {
3475
- const fd = await fs16.open(lockPath, "wx");
3595
+ const fd = await fs17.open(lockPath, "wx");
3476
3596
  await fd.close();
3477
3597
  return;
3478
3598
  } catch (err) {
@@ -3488,7 +3608,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
3488
3608
  }
3489
3609
  }
3490
3610
  async function releaseLock(lockPath) {
3491
- await fs16.unlink(lockPath).catch(() => {
3611
+ await fs17.unlink(lockPath).catch(() => {
3492
3612
  });
3493
3613
  }
3494
3614
  async function withLock(fn) {
@@ -3504,7 +3624,7 @@ async function readRegistry() {
3504
3624
  const file = registryPath();
3505
3625
  let raw;
3506
3626
  try {
3507
- raw = await fs16.readFile(file, "utf8");
3627
+ raw = await fs17.readFile(file, "utf8");
3508
3628
  } catch (err) {
3509
3629
  if (err.code === "ENOENT") {
3510
3630
  return { version: 1, projects: [] };
@@ -3653,4 +3773,4 @@ export {
3653
3773
  touchLastSeen,
3654
3774
  removeProject
3655
3775
  };
3656
- //# sourceMappingURL=chunk-GAYTAGEH.js.map
3776
+ //# sourceMappingURL=chunk-IRPH6KL4.js.map