@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.
package/dist/neatd.cjs CHANGED
@@ -24,12 +24,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/neatd.ts
27
- var import_node_fs18 = require("fs");
28
- var import_node_path31 = __toESM(require("path"), 1);
27
+ var import_node_fs19 = require("fs");
28
+ var import_node_path32 = __toESM(require("path"), 1);
29
29
 
30
30
  // src/daemon.ts
31
- var import_node_fs17 = require("fs");
32
- var import_node_path30 = __toESM(require("path"), 1);
31
+ var import_node_fs18 = require("fs");
32
+ var import_node_path31 = __toESM(require("path"), 1);
33
33
 
34
34
  // src/graph.ts
35
35
  var import_graphology = __toESM(require("graphology"), 1);
@@ -929,10 +929,10 @@ function pickLater(a, b) {
929
929
  }
930
930
 
931
931
  // src/extract/services.ts
932
- var import_node_fs6 = require("fs");
933
- var import_node_path6 = __toESM(require("path"), 1);
932
+ var import_node_fs7 = require("fs");
933
+ var import_node_path7 = __toESM(require("path"), 1);
934
934
  var import_ignore = __toESM(require("ignore"), 1);
935
- var import_minimatch = require("minimatch");
935
+ var import_minimatch2 = require("minimatch");
936
936
  var import_types5 = require("@neat.is/types");
937
937
 
938
938
  // src/extract/shared.ts
@@ -1043,6 +1043,72 @@ function pythonToPackage(service) {
1043
1043
  };
1044
1044
  }
1045
1045
 
1046
+ // src/extract/owners.ts
1047
+ var import_node_fs6 = require("fs");
1048
+ var import_node_path6 = __toESM(require("path"), 1);
1049
+ var import_minimatch = require("minimatch");
1050
+ async function loadCodeowners(scanPath) {
1051
+ const candidates = [
1052
+ import_node_path6.default.join(scanPath, "CODEOWNERS"),
1053
+ import_node_path6.default.join(scanPath, ".github", "CODEOWNERS")
1054
+ ];
1055
+ for (const file of candidates) {
1056
+ if (await exists(file)) {
1057
+ const raw = await import_node_fs6.promises.readFile(file, "utf8");
1058
+ return parseCodeowners(raw);
1059
+ }
1060
+ }
1061
+ return null;
1062
+ }
1063
+ function parseCodeowners(raw) {
1064
+ const rules = [];
1065
+ for (const line of raw.split("\n")) {
1066
+ const trimmed = line.trim();
1067
+ if (!trimmed || trimmed.startsWith("#")) continue;
1068
+ const match = /^(\S+)\s+(.+)$/.exec(trimmed);
1069
+ if (!match) continue;
1070
+ rules.push({ pattern: match[1], owners: match[2].trim() });
1071
+ }
1072
+ return { rules };
1073
+ }
1074
+ function matchOwner(file, repoPath) {
1075
+ const normalized = repoPath.split(import_node_path6.default.sep).join("/");
1076
+ for (const rule of file.rules) {
1077
+ if (matchesPattern(rule.pattern, normalized)) return rule.owners;
1078
+ }
1079
+ return null;
1080
+ }
1081
+ function matchesPattern(rawPattern, repoPath) {
1082
+ let pattern = rawPattern.startsWith("/") ? rawPattern.slice(1) : rawPattern;
1083
+ if (pattern === "*") return !repoPath.includes("/");
1084
+ if (pattern === "**" || pattern === "") return true;
1085
+ if (pattern.endsWith("/")) pattern = pattern + "**";
1086
+ if ((0, import_minimatch.minimatch)(repoPath, pattern, { dot: true })) return true;
1087
+ if (!pattern.includes("*") && (0, import_minimatch.minimatch)(repoPath, pattern + "/**", { dot: true })) return true;
1088
+ return false;
1089
+ }
1090
+ async function readPackageJsonAuthor(serviceDir) {
1091
+ const pkgPath = import_node_path6.default.join(serviceDir, "package.json");
1092
+ if (!await exists(pkgPath)) return null;
1093
+ try {
1094
+ const pkg = await readJson(pkgPath);
1095
+ if (!pkg.author) return null;
1096
+ if (typeof pkg.author === "string") return pkg.author;
1097
+ if (typeof pkg.author === "object" && typeof pkg.author.name === "string") return pkg.author.name;
1098
+ return null;
1099
+ } catch {
1100
+ return null;
1101
+ }
1102
+ }
1103
+ async function computeServiceOwner(codeowners, repoPath, serviceDir) {
1104
+ if (codeowners && repoPath !== void 0) {
1105
+ const owner = matchOwner(codeowners, repoPath);
1106
+ if (owner) return owner;
1107
+ }
1108
+ const author = await readPackageJsonAuthor(serviceDir);
1109
+ return author ?? void 0;
1110
+ }
1111
+
1046
1112
  // src/extract/services.ts
1047
1113
  var DEFAULT_SCAN_DEPTH = 5;
1048
1114
  function parseScanDepth() {
@@ -1059,21 +1125,21 @@ function workspaceGlobs(pkg) {
1059
1125
  return null;
1060
1126
  }
1061
1127
  async function loadGitignore(scanPath) {
1062
- const gitignorePath = import_node_path6.default.join(scanPath, ".gitignore");
1128
+ const gitignorePath = import_node_path7.default.join(scanPath, ".gitignore");
1063
1129
  if (!await exists(gitignorePath)) return null;
1064
- const raw = await import_node_fs6.promises.readFile(gitignorePath, "utf8");
1130
+ const raw = await import_node_fs7.promises.readFile(gitignorePath, "utf8");
1065
1131
  return (0, import_ignore.default)().add(raw);
1066
1132
  }
1067
1133
  async function walkDirs(start, scanPath, options, visit) {
1068
1134
  async function recurse(current, depth) {
1069
1135
  if (depth > options.maxDepth) return;
1070
- const entries = await import_node_fs6.promises.readdir(current, { withFileTypes: true }).catch(() => []);
1136
+ const entries = await import_node_fs7.promises.readdir(current, { withFileTypes: true }).catch(() => []);
1071
1137
  for (const entry2 of entries) {
1072
1138
  if (!entry2.isDirectory()) continue;
1073
1139
  if (IGNORED_DIRS.has(entry2.name)) continue;
1074
- const child = import_node_path6.default.join(current, entry2.name);
1140
+ const child = import_node_path7.default.join(current, entry2.name);
1075
1141
  if (options.ig) {
1076
- const rel = import_node_path6.default.relative(scanPath, child).split(import_node_path6.default.sep).join("/");
1142
+ const rel = import_node_path7.default.relative(scanPath, child).split(import_node_path7.default.sep).join("/");
1077
1143
  if (rel && options.ig.ignores(rel + "/")) continue;
1078
1144
  }
1079
1145
  await visit(child);
@@ -1088,8 +1154,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
1088
1154
  for (const raw of globs) {
1089
1155
  const pattern = raw.replace(/^\.\//, "");
1090
1156
  if (!pattern.includes("*")) {
1091
- const candidate = import_node_path6.default.join(scanPath, pattern);
1092
- if (await exists(import_node_path6.default.join(candidate, "package.json"))) found.add(candidate);
1157
+ const candidate = import_node_path7.default.join(scanPath, pattern);
1158
+ if (await exists(import_node_path7.default.join(candidate, "package.json"))) found.add(candidate);
1093
1159
  continue;
1094
1160
  }
1095
1161
  const segments = pattern.split("/");
@@ -1098,13 +1164,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
1098
1164
  if (seg.includes("*")) break;
1099
1165
  staticSegments.push(seg);
1100
1166
  }
1101
- const start = import_node_path6.default.join(scanPath, ...staticSegments);
1167
+ const start = import_node_path7.default.join(scanPath, ...staticSegments);
1102
1168
  if (!await exists(start)) continue;
1103
1169
  const hasDoubleStar = pattern.includes("**");
1104
1170
  const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
1105
1171
  await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
1106
- const rel = import_node_path6.default.relative(scanPath, dir).split(import_node_path6.default.sep).join("/");
1107
- if ((0, import_minimatch.minimatch)(rel, pattern) && await exists(import_node_path6.default.join(dir, "package.json"))) {
1172
+ const rel = import_node_path7.default.relative(scanPath, dir).split(import_node_path7.default.sep).join("/");
1173
+ if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path7.default.join(dir, "package.json"))) {
1108
1174
  found.add(dir);
1109
1175
  }
1110
1176
  });
@@ -1112,9 +1178,17 @@ async function expandWorkspaceGlobs(scanPath, globs) {
1112
1178
  return [...found];
1113
1179
  }
1114
1180
  async function discoverNodeService(scanPath, dir) {
1115
- const pkgPath = import_node_path6.default.join(dir, "package.json");
1181
+ const pkgPath = import_node_path7.default.join(dir, "package.json");
1116
1182
  if (!await exists(pkgPath)) return null;
1117
- const pkg = await readJson(pkgPath);
1183
+ let pkg;
1184
+ try {
1185
+ pkg = await readJson(pkgPath);
1186
+ } catch (err) {
1187
+ console.warn(
1188
+ `[neat] services skipped ${import_node_path7.default.relative(scanPath, pkgPath)}: ${err.message}`
1189
+ );
1190
+ return null;
1191
+ }
1118
1192
  if (!pkg.name) return null;
1119
1193
  const node = {
1120
1194
  id: (0, import_types5.serviceId)(pkg.name),
@@ -1123,7 +1197,7 @@ async function discoverNodeService(scanPath, dir) {
1123
1197
  language: "javascript",
1124
1198
  version: pkg.version,
1125
1199
  dependencies: pkg.dependencies ?? {},
1126
- repoPath: import_node_path6.default.relative(scanPath, dir),
1200
+ repoPath: import_node_path7.default.relative(scanPath, dir),
1127
1201
  ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
1128
1202
  };
1129
1203
  return { pkg, dir, node };
@@ -1139,13 +1213,22 @@ async function discoverPyService(scanPath, dir) {
1139
1213
  language: "python",
1140
1214
  version: py.version,
1141
1215
  dependencies: py.dependencies,
1142
- repoPath: import_node_path6.default.relative(scanPath, dir)
1216
+ repoPath: import_node_path7.default.relative(scanPath, dir)
1143
1217
  };
1144
1218
  return { pkg, dir, node };
1145
1219
  }
1146
1220
  async function discoverServices(scanPath) {
1147
- const rootPkgPath = import_node_path6.default.join(scanPath, "package.json");
1148
- const rootPkg = await exists(rootPkgPath) ? await readJson(rootPkgPath) : null;
1221
+ const rootPkgPath = import_node_path7.default.join(scanPath, "package.json");
1222
+ let rootPkg = null;
1223
+ if (await exists(rootPkgPath)) {
1224
+ try {
1225
+ rootPkg = await readJson(rootPkgPath);
1226
+ } catch (err) {
1227
+ console.warn(
1228
+ `[neat] services workspaces skipped ${import_node_path7.default.relative(scanPath, rootPkgPath)}: ${err.message}`
1229
+ );
1230
+ }
1231
+ }
1149
1232
  const wsGlobs = rootPkg ? workspaceGlobs(rootPkg) : null;
1150
1233
  const candidateDirs = [];
1151
1234
  if (wsGlobs) {
@@ -1158,9 +1241,9 @@ async function discoverServices(scanPath) {
1158
1241
  scanPath,
1159
1242
  { maxDepth: parseScanDepth(), ig },
1160
1243
  async (dir) => {
1161
- if (await exists(import_node_path6.default.join(dir, "package.json"))) {
1244
+ if (await exists(import_node_path7.default.join(dir, "package.json"))) {
1162
1245
  candidateDirs.push(dir);
1163
- } else if (await exists(import_node_path6.default.join(dir, "pyproject.toml")) || await exists(import_node_path6.default.join(dir, "requirements.txt")) || await exists(import_node_path6.default.join(dir, "setup.py"))) {
1246
+ } else if (await exists(import_node_path7.default.join(dir, "pyproject.toml")) || await exists(import_node_path7.default.join(dir, "requirements.txt")) || await exists(import_node_path7.default.join(dir, "setup.py"))) {
1164
1247
  candidateDirs.push(dir);
1165
1248
  }
1166
1249
  }
@@ -1174,8 +1257,8 @@ async function discoverServices(scanPath) {
1174
1257
  if (!service) continue;
1175
1258
  const existingDir = seen.get(service.node.name);
1176
1259
  if (existingDir !== void 0) {
1177
- const a = import_node_path6.default.relative(scanPath, existingDir) || ".";
1178
- const b = import_node_path6.default.relative(scanPath, dir) || ".";
1260
+ const a = import_node_path7.default.relative(scanPath, existingDir) || ".";
1261
+ const b = import_node_path7.default.relative(scanPath, dir) || ".";
1179
1262
  console.warn(
1180
1263
  `[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
1181
1264
  );
@@ -1184,6 +1267,11 @@ async function discoverServices(scanPath) {
1184
1267
  seen.set(service.node.name, dir);
1185
1268
  out.push(service);
1186
1269
  }
1270
+ const codeowners = await loadCodeowners(scanPath);
1271
+ for (const service of out) {
1272
+ const owner = await computeServiceOwner(codeowners, service.node.repoPath, service.dir);
1273
+ if (owner !== void 0) service.node.owner = owner;
1274
+ }
1187
1275
  return out;
1188
1276
  }
1189
1277
  function addServiceNodes(graph, services) {
@@ -1206,8 +1294,8 @@ function addServiceNodes(graph, services) {
1206
1294
  }
1207
1295
 
1208
1296
  // src/extract/aliases.ts
1209
- var import_node_path7 = __toESM(require("path"), 1);
1210
- var import_node_fs7 = require("fs");
1297
+ var import_node_path8 = __toESM(require("path"), 1);
1298
+ var import_node_fs8 = require("fs");
1211
1299
  var import_yaml2 = require("yaml");
1212
1300
  var import_types6 = require("@neat.is/types");
1213
1301
  var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
@@ -1234,21 +1322,29 @@ function indexServicesByName(services) {
1234
1322
  const map = /* @__PURE__ */ new Map();
1235
1323
  for (const s of services) {
1236
1324
  map.set(s.node.name, s.node.id);
1237
- map.set(import_node_path7.default.basename(s.dir), s.node.id);
1325
+ map.set(import_node_path8.default.basename(s.dir), s.node.id);
1238
1326
  }
1239
1327
  return map;
1240
1328
  }
1241
1329
  async function collectComposeAliases(graph, scanPath, serviceIndex) {
1242
1330
  let composePath = null;
1243
1331
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
1244
- const abs = import_node_path7.default.join(scanPath, name);
1332
+ const abs = import_node_path8.default.join(scanPath, name);
1245
1333
  if (await exists(abs)) {
1246
1334
  composePath = abs;
1247
1335
  break;
1248
1336
  }
1249
1337
  }
1250
1338
  if (!composePath) return;
1251
- const compose = await readYaml(composePath);
1339
+ let compose;
1340
+ try {
1341
+ compose = await readYaml(composePath);
1342
+ } catch (err) {
1343
+ console.warn(
1344
+ `[neat] aliases compose skipped ${import_node_path8.default.relative(scanPath, composePath)}: ${err.message}`
1345
+ );
1346
+ return;
1347
+ }
1252
1348
  if (!compose?.services) return;
1253
1349
  for (const [composeName, svc] of Object.entries(compose.services)) {
1254
1350
  const serviceId3 = serviceIndex.get(composeName);
@@ -1287,9 +1383,17 @@ function parseDockerfileLabels(content) {
1287
1383
  }
1288
1384
  async function collectDockerfileAliases(graph, services) {
1289
1385
  for (const service of services) {
1290
- const dockerfilePath = import_node_path7.default.join(service.dir, "Dockerfile");
1386
+ const dockerfilePath = import_node_path8.default.join(service.dir, "Dockerfile");
1291
1387
  if (!await exists(dockerfilePath)) continue;
1292
- const content = await import_node_fs7.promises.readFile(dockerfilePath, "utf8");
1388
+ let content;
1389
+ try {
1390
+ content = await import_node_fs8.promises.readFile(dockerfilePath, "utf8");
1391
+ } catch (err) {
1392
+ console.warn(
1393
+ `[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
1394
+ );
1395
+ continue;
1396
+ }
1293
1397
  const aliases = parseDockerfileLabels(content);
1294
1398
  if (aliases.length > 0) addAliases(graph, service.node.id, aliases);
1295
1399
  }
@@ -1297,13 +1401,13 @@ async function collectDockerfileAliases(graph, services) {
1297
1401
  async function walkYamlFiles(start, depth = 0, max = 5) {
1298
1402
  if (depth > max) return [];
1299
1403
  const out = [];
1300
- const entries = await import_node_fs7.promises.readdir(start, { withFileTypes: true }).catch(() => []);
1404
+ const entries = await import_node_fs8.promises.readdir(start, { withFileTypes: true }).catch(() => []);
1301
1405
  for (const entry2 of entries) {
1302
1406
  if (entry2.isDirectory()) {
1303
1407
  if (IGNORED_DIRS.has(entry2.name)) continue;
1304
- out.push(...await walkYamlFiles(import_node_path7.default.join(start, entry2.name), depth + 1, max));
1305
- } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path7.default.extname(entry2.name))) {
1306
- out.push(import_node_path7.default.join(start, entry2.name));
1408
+ out.push(...await walkYamlFiles(import_node_path8.default.join(start, entry2.name), depth + 1, max));
1409
+ } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path8.default.extname(entry2.name))) {
1410
+ out.push(import_node_path8.default.join(start, entry2.name));
1307
1411
  }
1308
1412
  }
1309
1413
  return out;
@@ -1330,7 +1434,7 @@ function k8sServiceTarget(doc, byName) {
1330
1434
  async function collectK8sAliases(graph, scanPath, serviceIndex) {
1331
1435
  const files = await walkYamlFiles(scanPath);
1332
1436
  for (const file of files) {
1333
- const content = await import_node_fs7.promises.readFile(file, "utf8");
1437
+ const content = await import_node_fs8.promises.readFile(file, "utf8");
1334
1438
  let docs;
1335
1439
  try {
1336
1440
  docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -1354,13 +1458,13 @@ async function addServiceAliases(graph, scanPath, services) {
1354
1458
  }
1355
1459
 
1356
1460
  // src/extract/databases/index.ts
1357
- var import_node_path15 = __toESM(require("path"), 1);
1461
+ var import_node_path16 = __toESM(require("path"), 1);
1358
1462
  var import_types7 = require("@neat.is/types");
1359
1463
 
1360
1464
  // src/extract/databases/db-config-yaml.ts
1361
- var import_node_path8 = __toESM(require("path"), 1);
1465
+ var import_node_path9 = __toESM(require("path"), 1);
1362
1466
  async function parse(serviceDir) {
1363
- const yamlPath = import_node_path8.default.join(serviceDir, "db-config.yaml");
1467
+ const yamlPath = import_node_path9.default.join(serviceDir, "db-config.yaml");
1364
1468
  if (!await exists(yamlPath)) return [];
1365
1469
  const raw = await readYaml(yamlPath);
1366
1470
  return [
@@ -1377,12 +1481,12 @@ async function parse(serviceDir) {
1377
1481
  var dbConfigYamlParser = { name: "db-config.yaml", parse };
1378
1482
 
1379
1483
  // src/extract/databases/dotenv.ts
1380
- var import_node_fs9 = require("fs");
1381
- var import_node_path10 = __toESM(require("path"), 1);
1484
+ var import_node_fs10 = require("fs");
1485
+ var import_node_path11 = __toESM(require("path"), 1);
1382
1486
 
1383
1487
  // src/extract/databases/shared.ts
1384
- var import_node_fs8 = require("fs");
1385
- var import_node_path9 = __toESM(require("path"), 1);
1488
+ var import_node_fs9 = require("fs");
1489
+ var import_node_path10 = __toESM(require("path"), 1);
1386
1490
  function schemeToEngine(scheme) {
1387
1491
  const s = scheme.toLowerCase().split("+")[0];
1388
1492
  switch (s) {
@@ -1421,14 +1525,14 @@ function parseConnectionString(url) {
1421
1525
  }
1422
1526
  async function readIfExists(filePath) {
1423
1527
  try {
1424
- return await import_node_fs8.promises.readFile(filePath, "utf8");
1528
+ return await import_node_fs9.promises.readFile(filePath, "utf8");
1425
1529
  } catch {
1426
1530
  return null;
1427
1531
  }
1428
1532
  }
1429
1533
  async function findFirst(serviceDir, candidates) {
1430
1534
  for (const rel of candidates) {
1431
- const abs = import_node_path9.default.join(serviceDir, rel);
1535
+ const abs = import_node_path10.default.join(serviceDir, rel);
1432
1536
  const content = await readIfExists(abs);
1433
1537
  if (content !== null) return abs;
1434
1538
  }
@@ -1479,15 +1583,15 @@ function parseDotenvLine(line) {
1479
1583
  return { key, value };
1480
1584
  }
1481
1585
  async function parse2(serviceDir) {
1482
- const entries = await import_node_fs9.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
1586
+ const entries = await import_node_fs10.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
1483
1587
  const configs = [];
1484
1588
  const seen = /* @__PURE__ */ new Set();
1485
1589
  for (const entry2 of entries) {
1486
1590
  if (!entry2.isFile()) continue;
1487
1591
  const match = isConfigFile(entry2.name);
1488
1592
  if (!match.match || match.fileType !== "env") continue;
1489
- const filePath = import_node_path10.default.join(serviceDir, entry2.name);
1490
- const content = await import_node_fs9.promises.readFile(filePath, "utf8");
1593
+ const filePath = import_node_path11.default.join(serviceDir, entry2.name);
1594
+ const content = await import_node_fs10.promises.readFile(filePath, "utf8");
1491
1595
  for (const line of content.split("\n")) {
1492
1596
  const parsed = parseDotenvLine(line);
1493
1597
  if (!parsed) continue;
@@ -1505,9 +1609,9 @@ async function parse2(serviceDir) {
1505
1609
  var dotenvParser = { name: ".env", parse: parse2 };
1506
1610
 
1507
1611
  // src/extract/databases/prisma.ts
1508
- var import_node_path11 = __toESM(require("path"), 1);
1612
+ var import_node_path12 = __toESM(require("path"), 1);
1509
1613
  async function parse3(serviceDir) {
1510
- const schemaPath = import_node_path11.default.join(serviceDir, "prisma", "schema.prisma");
1614
+ const schemaPath = import_node_path12.default.join(serviceDir, "prisma", "schema.prisma");
1511
1615
  const content = await readIfExists(schemaPath);
1512
1616
  if (!content) return [];
1513
1617
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -1636,10 +1740,10 @@ async function parse5(serviceDir) {
1636
1740
  var knexParser = { name: "knex", parse: parse5 };
1637
1741
 
1638
1742
  // src/extract/databases/ormconfig.ts
1639
- var import_node_path12 = __toESM(require("path"), 1);
1743
+ var import_node_path13 = __toESM(require("path"), 1);
1640
1744
  async function parse6(serviceDir) {
1641
1745
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
1642
- const abs = import_node_path12.default.join(serviceDir, candidate);
1746
+ const abs = import_node_path13.default.join(serviceDir, candidate);
1643
1747
  if (!await exists(abs)) continue;
1644
1748
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
1645
1749
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -1697,9 +1801,9 @@ async function parse7(serviceDir) {
1697
1801
  var typeormParser = { name: "typeorm", parse: parse7 };
1698
1802
 
1699
1803
  // src/extract/databases/sequelize.ts
1700
- var import_node_path13 = __toESM(require("path"), 1);
1804
+ var import_node_path14 = __toESM(require("path"), 1);
1701
1805
  async function parse8(serviceDir) {
1702
- const configPath = import_node_path13.default.join(serviceDir, "config", "config.json");
1806
+ const configPath = import_node_path14.default.join(serviceDir, "config", "config.json");
1703
1807
  if (!await exists(configPath)) return [];
1704
1808
  const raw = await readJson(configPath);
1705
1809
  const out = [];
@@ -1725,7 +1829,7 @@ async function parse8(serviceDir) {
1725
1829
  var sequelizeParser = { name: "sequelize", parse: parse8 };
1726
1830
 
1727
1831
  // src/extract/databases/docker-compose.ts
1728
- var import_node_path14 = __toESM(require("path"), 1);
1832
+ var import_node_path15 = __toESM(require("path"), 1);
1729
1833
  function portFromService(svc) {
1730
1834
  for (const raw of svc.ports ?? []) {
1731
1835
  const str = String(raw);
@@ -1752,7 +1856,7 @@ function databaseFromEnv(svc) {
1752
1856
  }
1753
1857
  async function parse9(serviceDir) {
1754
1858
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
1755
- const abs = import_node_path14.default.join(serviceDir, name);
1859
+ const abs = import_node_path15.default.join(serviceDir, name);
1756
1860
  if (!await exists(abs)) continue;
1757
1861
  const raw = await readYaml(abs);
1758
1862
  if (!raw?.services) return [];
@@ -1932,7 +2036,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
1932
2036
  provenance: import_types7.Provenance.EXTRACTED,
1933
2037
  ...config.sourceFile ? {
1934
2038
  evidence: {
1935
- file: import_node_path15.default.relative(scanPath, config.sourceFile).split(import_node_path15.default.sep).join("/")
2039
+ file: import_node_path16.default.relative(scanPath, config.sourceFile).split(import_node_path16.default.sep).join("/")
1936
2040
  }
1937
2041
  } : {}
1938
2042
  };
@@ -1959,15 +2063,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
1959
2063
  }
1960
2064
 
1961
2065
  // src/extract/configs.ts
1962
- var import_node_fs10 = require("fs");
1963
- var import_node_path16 = __toESM(require("path"), 1);
2066
+ var import_node_fs11 = require("fs");
2067
+ var import_node_path17 = __toESM(require("path"), 1);
1964
2068
  var import_types8 = require("@neat.is/types");
1965
2069
  async function walkConfigFiles(dir) {
1966
2070
  const out = [];
1967
2071
  async function walk(current) {
1968
- const entries = await import_node_fs10.promises.readdir(current, { withFileTypes: true });
2072
+ const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true });
1969
2073
  for (const entry2 of entries) {
1970
- const full = import_node_path16.default.join(current, entry2.name);
2074
+ const full = import_node_path17.default.join(current, entry2.name);
1971
2075
  if (entry2.isDirectory()) {
1972
2076
  if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
1973
2077
  } else if (entry2.isFile() && isConfigFile(entry2.name).match) {
@@ -1984,13 +2088,13 @@ async function addConfigNodes(graph, services, scanPath) {
1984
2088
  for (const service of services) {
1985
2089
  const configFiles = await walkConfigFiles(service.dir);
1986
2090
  for (const file of configFiles) {
1987
- const relPath = import_node_path16.default.relative(scanPath, file);
2091
+ const relPath = import_node_path17.default.relative(scanPath, file);
1988
2092
  const node = {
1989
2093
  id: (0, import_types8.configId)(relPath),
1990
2094
  type: import_types8.NodeType.ConfigNode,
1991
- name: import_node_path16.default.basename(file),
2095
+ name: import_node_path17.default.basename(file),
1992
2096
  path: relPath,
1993
- fileType: isConfigFile(import_node_path16.default.basename(file)).fileType
2097
+ fileType: isConfigFile(import_node_path17.default.basename(file)).fileType
1994
2098
  };
1995
2099
  if (!graph.hasNode(node.id)) {
1996
2100
  graph.addNode(node.id, node);
@@ -2002,7 +2106,7 @@ async function addConfigNodes(graph, services, scanPath) {
2002
2106
  target: node.id,
2003
2107
  type: import_types8.EdgeType.CONFIGURED_BY,
2004
2108
  provenance: import_types8.Provenance.EXTRACTED,
2005
- evidence: { file: relPath.split(import_node_path16.default.sep).join("/") }
2109
+ evidence: { file: relPath.split(import_node_path17.default.sep).join("/") }
2006
2110
  };
2007
2111
  if (!graph.hasEdge(edge.id)) {
2008
2112
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -2017,24 +2121,24 @@ async function addConfigNodes(graph, services, scanPath) {
2017
2121
  var import_types14 = require("@neat.is/types");
2018
2122
 
2019
2123
  // src/extract/calls/http.ts
2020
- var import_node_path18 = __toESM(require("path"), 1);
2124
+ var import_node_path19 = __toESM(require("path"), 1);
2021
2125
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
2022
2126
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
2023
2127
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
2024
2128
  var import_types9 = require("@neat.is/types");
2025
2129
 
2026
2130
  // src/extract/calls/shared.ts
2027
- var import_node_fs11 = require("fs");
2028
- var import_node_path17 = __toESM(require("path"), 1);
2131
+ var import_node_fs12 = require("fs");
2132
+ var import_node_path18 = __toESM(require("path"), 1);
2029
2133
  async function walkSourceFiles(dir) {
2030
2134
  const out = [];
2031
2135
  async function walk(current) {
2032
- const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2136
+ const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2033
2137
  for (const entry2 of entries) {
2034
- const full = import_node_path17.default.join(current, entry2.name);
2138
+ const full = import_node_path18.default.join(current, entry2.name);
2035
2139
  if (entry2.isDirectory()) {
2036
2140
  if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
2037
- } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path17.default.extname(entry2.name))) {
2141
+ } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path18.default.extname(entry2.name))) {
2038
2142
  out.push(full);
2039
2143
  }
2040
2144
  }
@@ -2047,7 +2151,7 @@ async function loadSourceFiles(dir) {
2047
2151
  const out = [];
2048
2152
  for (const p of paths) {
2049
2153
  try {
2050
- const content = await import_node_fs11.promises.readFile(p, "utf8");
2154
+ const content = await import_node_fs12.promises.readFile(p, "utf8");
2051
2155
  out.push({ path: p, content });
2052
2156
  } catch {
2053
2157
  }
@@ -2103,9 +2207,9 @@ async function addHttpCallEdges(graph, services) {
2103
2207
  const knownHosts = /* @__PURE__ */ new Set();
2104
2208
  const hostToNodeId = /* @__PURE__ */ new Map();
2105
2209
  for (const service of services) {
2106
- knownHosts.add(import_node_path18.default.basename(service.dir));
2210
+ knownHosts.add(import_node_path19.default.basename(service.dir));
2107
2211
  knownHosts.add(service.pkg.name);
2108
- hostToNodeId.set(import_node_path18.default.basename(service.dir), service.node.id);
2212
+ hostToNodeId.set(import_node_path19.default.basename(service.dir), service.node.id);
2109
2213
  hostToNodeId.set(service.pkg.name, service.node.id);
2110
2214
  }
2111
2215
  let edgesAdded = 0;
@@ -2113,7 +2217,7 @@ async function addHttpCallEdges(graph, services) {
2113
2217
  const files = await loadSourceFiles(service.dir);
2114
2218
  const seenTargets = /* @__PURE__ */ new Map();
2115
2219
  for (const file of files) {
2116
- const parser = import_node_path18.default.extname(file.path) === ".py" ? pyParser : jsParser;
2220
+ const parser = import_node_path19.default.extname(file.path) === ".py" ? pyParser : jsParser;
2117
2221
  let targets;
2118
2222
  try {
2119
2223
  targets = callsFromSource(file.content, parser, knownHosts);
@@ -2141,7 +2245,7 @@ async function addHttpCallEdges(graph, services) {
2141
2245
  type: import_types9.EdgeType.CALLS,
2142
2246
  provenance: import_types9.Provenance.EXTRACTED,
2143
2247
  evidence: {
2144
- file: import_node_path18.default.relative(service.dir, evidenceFile.file),
2248
+ file: import_node_path19.default.relative(service.dir, evidenceFile.file),
2145
2249
  line,
2146
2250
  snippet: snippet(fileContent, line)
2147
2251
  }
@@ -2156,7 +2260,7 @@ async function addHttpCallEdges(graph, services) {
2156
2260
  }
2157
2261
 
2158
2262
  // src/extract/calls/kafka.ts
2159
- var import_node_path19 = __toESM(require("path"), 1);
2263
+ var import_node_path20 = __toESM(require("path"), 1);
2160
2264
  var import_types10 = require("@neat.is/types");
2161
2265
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
2162
2266
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
@@ -2183,7 +2287,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
2183
2287
  kind: "kafka-topic",
2184
2288
  edgeType,
2185
2289
  evidence: {
2186
- file: import_node_path19.default.relative(serviceDir, file.path),
2290
+ file: import_node_path20.default.relative(serviceDir, file.path),
2187
2291
  line,
2188
2292
  snippet: snippet(file.content, line)
2189
2293
  }
@@ -2195,7 +2299,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
2195
2299
  }
2196
2300
 
2197
2301
  // src/extract/calls/redis.ts
2198
- var import_node_path20 = __toESM(require("path"), 1);
2302
+ var import_node_path21 = __toESM(require("path"), 1);
2199
2303
  var import_types11 = require("@neat.is/types");
2200
2304
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
2201
2305
  function redisEndpointsFromFile(file, serviceDir) {
@@ -2214,7 +2318,7 @@ function redisEndpointsFromFile(file, serviceDir) {
2214
2318
  kind: "redis",
2215
2319
  edgeType: "CALLS",
2216
2320
  evidence: {
2217
- file: import_node_path20.default.relative(serviceDir, file.path),
2321
+ file: import_node_path21.default.relative(serviceDir, file.path),
2218
2322
  line,
2219
2323
  snippet: snippet(file.content, line)
2220
2324
  }
@@ -2224,7 +2328,7 @@ function redisEndpointsFromFile(file, serviceDir) {
2224
2328
  }
2225
2329
 
2226
2330
  // src/extract/calls/aws.ts
2227
- var import_node_path21 = __toESM(require("path"), 1);
2331
+ var import_node_path22 = __toESM(require("path"), 1);
2228
2332
  var import_types12 = require("@neat.is/types");
2229
2333
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
2230
2334
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
@@ -2254,7 +2358,7 @@ function awsEndpointsFromFile(file, serviceDir) {
2254
2358
  kind,
2255
2359
  edgeType: "CALLS",
2256
2360
  evidence: {
2257
- file: import_node_path21.default.relative(serviceDir, file.path),
2361
+ file: import_node_path22.default.relative(serviceDir, file.path),
2258
2362
  line,
2259
2363
  snippet: snippet(file.content, line)
2260
2364
  }
@@ -2278,7 +2382,7 @@ function awsEndpointsFromFile(file, serviceDir) {
2278
2382
  }
2279
2383
 
2280
2384
  // src/extract/calls/grpc.ts
2281
- var import_node_path22 = __toESM(require("path"), 1);
2385
+ var import_node_path23 = __toESM(require("path"), 1);
2282
2386
  var import_types13 = require("@neat.is/types");
2283
2387
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
2284
2388
  function isLikelyAddress(value) {
@@ -2303,7 +2407,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
2303
2407
  kind: "grpc-service",
2304
2408
  edgeType: "CALLS",
2305
2409
  evidence: {
2306
- file: import_node_path22.default.relative(serviceDir, file.path),
2410
+ file: import_node_path23.default.relative(serviceDir, file.path),
2307
2411
  line,
2308
2412
  snippet: snippet(file.content, line)
2309
2413
  }
@@ -2379,7 +2483,7 @@ async function addCallEdges(graph, services) {
2379
2483
  }
2380
2484
 
2381
2485
  // src/extract/infra/docker-compose.ts
2382
- var import_node_path23 = __toESM(require("path"), 1);
2486
+ var import_node_path24 = __toESM(require("path"), 1);
2383
2487
  var import_types16 = require("@neat.is/types");
2384
2488
 
2385
2489
  // src/extract/infra/shared.ts
@@ -2416,7 +2520,7 @@ function dependsOnList(value) {
2416
2520
  }
2417
2521
  function serviceNameToServiceNode(name, services) {
2418
2522
  for (const s of services) {
2419
- if (s.node.name === name || import_node_path23.default.basename(s.dir) === name) return s.node.id;
2523
+ if (s.node.name === name || import_node_path24.default.basename(s.dir) === name) return s.node.id;
2420
2524
  }
2421
2525
  return null;
2422
2526
  }
@@ -2425,16 +2529,24 @@ async function addComposeInfra(graph, scanPath, services) {
2425
2529
  let edgesAdded = 0;
2426
2530
  let composePath = null;
2427
2531
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2428
- const abs = import_node_path23.default.join(scanPath, name);
2532
+ const abs = import_node_path24.default.join(scanPath, name);
2429
2533
  if (await exists(abs)) {
2430
2534
  composePath = abs;
2431
2535
  break;
2432
2536
  }
2433
2537
  }
2434
2538
  if (!composePath) return { nodesAdded, edgesAdded };
2435
- const compose = await readYaml(composePath);
2539
+ let compose;
2540
+ try {
2541
+ compose = await readYaml(composePath);
2542
+ } catch (err) {
2543
+ console.warn(
2544
+ `[neat] infra docker-compose skipped ${import_node_path24.default.relative(scanPath, composePath)}: ${err.message}`
2545
+ );
2546
+ return { nodesAdded, edgesAdded };
2547
+ }
2436
2548
  if (!compose?.services) return { nodesAdded, edgesAdded };
2437
- const evidenceFile = import_node_path23.default.relative(scanPath, composePath).split(import_node_path23.default.sep).join("/");
2549
+ const evidenceFile = import_node_path24.default.relative(scanPath, composePath).split(import_node_path24.default.sep).join("/");
2438
2550
  const composeNameToNodeId = /* @__PURE__ */ new Map();
2439
2551
  for (const [composeName, svc] of Object.entries(compose.services)) {
2440
2552
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -2474,8 +2586,8 @@ async function addComposeInfra(graph, scanPath, services) {
2474
2586
  }
2475
2587
 
2476
2588
  // src/extract/infra/dockerfile.ts
2477
- var import_node_path24 = __toESM(require("path"), 1);
2478
- var import_node_fs12 = require("fs");
2589
+ var import_node_path25 = __toESM(require("path"), 1);
2590
+ var import_node_fs13 = require("fs");
2479
2591
  var import_types17 = require("@neat.is/types");
2480
2592
  function runtimeImage(content) {
2481
2593
  const lines = content.split("\n");
@@ -2495,9 +2607,17 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
2495
2607
  let nodesAdded = 0;
2496
2608
  let edgesAdded = 0;
2497
2609
  for (const service of services) {
2498
- const dockerfilePath = import_node_path24.default.join(service.dir, "Dockerfile");
2610
+ const dockerfilePath = import_node_path25.default.join(service.dir, "Dockerfile");
2499
2611
  if (!await exists(dockerfilePath)) continue;
2500
- const content = await import_node_fs12.promises.readFile(dockerfilePath, "utf8");
2612
+ let content;
2613
+ try {
2614
+ content = await import_node_fs13.promises.readFile(dockerfilePath, "utf8");
2615
+ } catch (err) {
2616
+ console.warn(
2617
+ `[neat] infra dockerfile skipped ${import_node_path25.default.relative(scanPath, dockerfilePath)}: ${err.message}`
2618
+ );
2619
+ continue;
2620
+ }
2501
2621
  const image = runtimeImage(content);
2502
2622
  if (!image) continue;
2503
2623
  const node = makeInfraNode("container-image", image);
@@ -2514,7 +2634,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
2514
2634
  type: import_types17.EdgeType.RUNS_ON,
2515
2635
  provenance: import_types17.Provenance.EXTRACTED,
2516
2636
  evidence: {
2517
- file: import_node_path24.default.relative(scanPath, dockerfilePath).split(import_node_path24.default.sep).join("/")
2637
+ file: import_node_path25.default.relative(scanPath, dockerfilePath).split(import_node_path25.default.sep).join("/")
2518
2638
  }
2519
2639
  };
2520
2640
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -2525,19 +2645,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
2525
2645
  }
2526
2646
 
2527
2647
  // src/extract/infra/terraform.ts
2528
- var import_node_fs13 = require("fs");
2529
- var import_node_path25 = __toESM(require("path"), 1);
2648
+ var import_node_fs14 = require("fs");
2649
+ var import_node_path26 = __toESM(require("path"), 1);
2530
2650
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
2531
2651
  async function walkTfFiles(start, depth = 0, max = 5) {
2532
2652
  if (depth > max) return [];
2533
2653
  const out = [];
2534
- const entries = await import_node_fs13.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2654
+ const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2535
2655
  for (const entry2 of entries) {
2536
2656
  if (entry2.isDirectory()) {
2537
2657
  if (IGNORED_DIRS.has(entry2.name) || entry2.name === ".terraform") continue;
2538
- out.push(...await walkTfFiles(import_node_path25.default.join(start, entry2.name), depth + 1, max));
2658
+ out.push(...await walkTfFiles(import_node_path26.default.join(start, entry2.name), depth + 1, max));
2539
2659
  } else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
2540
- out.push(import_node_path25.default.join(start, entry2.name));
2660
+ out.push(import_node_path26.default.join(start, entry2.name));
2541
2661
  }
2542
2662
  }
2543
2663
  return out;
@@ -2546,7 +2666,7 @@ async function addTerraformResources(graph, scanPath) {
2546
2666
  let nodesAdded = 0;
2547
2667
  const files = await walkTfFiles(scanPath);
2548
2668
  for (const file of files) {
2549
- const content = await import_node_fs13.promises.readFile(file, "utf8");
2669
+ const content = await import_node_fs14.promises.readFile(file, "utf8");
2550
2670
  RESOURCE_RE.lastIndex = 0;
2551
2671
  let m;
2552
2672
  while ((m = RESOURCE_RE.exec(content)) !== null) {
@@ -2563,8 +2683,8 @@ async function addTerraformResources(graph, scanPath) {
2563
2683
  }
2564
2684
 
2565
2685
  // src/extract/infra/k8s.ts
2566
- var import_node_fs14 = require("fs");
2567
- var import_node_path26 = __toESM(require("path"), 1);
2686
+ var import_node_fs15 = require("fs");
2687
+ var import_node_path27 = __toESM(require("path"), 1);
2568
2688
  var import_yaml3 = require("yaml");
2569
2689
  var K8S_KIND_TO_INFRA_KIND = {
2570
2690
  Service: "k8s-service",
@@ -2578,13 +2698,13 @@ var K8S_KIND_TO_INFRA_KIND = {
2578
2698
  async function walkYamlFiles2(start, depth = 0, max = 5) {
2579
2699
  if (depth > max) return [];
2580
2700
  const out = [];
2581
- const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2701
+ const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2582
2702
  for (const entry2 of entries) {
2583
2703
  if (entry2.isDirectory()) {
2584
2704
  if (IGNORED_DIRS.has(entry2.name)) continue;
2585
- out.push(...await walkYamlFiles2(import_node_path26.default.join(start, entry2.name), depth + 1, max));
2586
- } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path26.default.extname(entry2.name))) {
2587
- out.push(import_node_path26.default.join(start, entry2.name));
2705
+ out.push(...await walkYamlFiles2(import_node_path27.default.join(start, entry2.name), depth + 1, max));
2706
+ } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path27.default.extname(entry2.name))) {
2707
+ out.push(import_node_path27.default.join(start, entry2.name));
2588
2708
  }
2589
2709
  }
2590
2710
  return out;
@@ -2593,7 +2713,7 @@ async function addK8sResources(graph, scanPath) {
2593
2713
  let nodesAdded = 0;
2594
2714
  const files = await walkYamlFiles2(scanPath);
2595
2715
  for (const file of files) {
2596
- const content = await import_node_fs14.promises.readFile(file, "utf8");
2716
+ const content = await import_node_fs15.promises.readFile(file, "utf8");
2597
2717
  let docs;
2598
2718
  try {
2599
2719
  docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -2658,8 +2778,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
2658
2778
  }
2659
2779
 
2660
2780
  // src/persist.ts
2661
- var import_node_fs15 = require("fs");
2662
- var import_node_path27 = __toESM(require("path"), 1);
2781
+ var import_node_fs16 = require("fs");
2782
+ var import_node_path28 = __toESM(require("path"), 1);
2663
2783
  var SCHEMA_VERSION = 2;
2664
2784
  function migrateV1ToV2(payload) {
2665
2785
  const nodes = payload.graph.nodes;
@@ -2673,7 +2793,7 @@ function migrateV1ToV2(payload) {
2673
2793
  return { ...payload, schemaVersion: 2 };
2674
2794
  }
2675
2795
  async function ensureDir(filePath) {
2676
- await import_node_fs15.promises.mkdir(import_node_path27.default.dirname(filePath), { recursive: true });
2796
+ await import_node_fs16.promises.mkdir(import_node_path28.default.dirname(filePath), { recursive: true });
2677
2797
  }
2678
2798
  async function saveGraphToDisk(graph, outPath) {
2679
2799
  await ensureDir(outPath);
@@ -2683,13 +2803,13 @@ async function saveGraphToDisk(graph, outPath) {
2683
2803
  graph: graph.export()
2684
2804
  };
2685
2805
  const tmp = `${outPath}.tmp`;
2686
- await import_node_fs15.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
2687
- await import_node_fs15.promises.rename(tmp, outPath);
2806
+ await import_node_fs16.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
2807
+ await import_node_fs16.promises.rename(tmp, outPath);
2688
2808
  }
2689
2809
  async function loadGraphFromDisk(graph, outPath) {
2690
2810
  let raw;
2691
2811
  try {
2692
- raw = await import_node_fs15.promises.readFile(outPath, "utf8");
2812
+ raw = await import_node_fs16.promises.readFile(outPath, "utf8");
2693
2813
  } catch (err) {
2694
2814
  if (err.code === "ENOENT") return;
2695
2815
  throw err;
@@ -2741,62 +2861,62 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
2741
2861
  }
2742
2862
 
2743
2863
  // src/projects.ts
2744
- var import_node_path28 = __toESM(require("path"), 1);
2864
+ var import_node_path29 = __toESM(require("path"), 1);
2745
2865
  function pathsForProject(project, baseDir) {
2746
2866
  if (project === DEFAULT_PROJECT) {
2747
2867
  return {
2748
- snapshotPath: import_node_path28.default.join(baseDir, "graph.json"),
2749
- errorsPath: import_node_path28.default.join(baseDir, "errors.ndjson"),
2750
- staleEventsPath: import_node_path28.default.join(baseDir, "stale-events.ndjson"),
2751
- embeddingsCachePath: import_node_path28.default.join(baseDir, "embeddings.json"),
2752
- policyViolationsPath: import_node_path28.default.join(baseDir, "policy-violations.ndjson")
2868
+ snapshotPath: import_node_path29.default.join(baseDir, "graph.json"),
2869
+ errorsPath: import_node_path29.default.join(baseDir, "errors.ndjson"),
2870
+ staleEventsPath: import_node_path29.default.join(baseDir, "stale-events.ndjson"),
2871
+ embeddingsCachePath: import_node_path29.default.join(baseDir, "embeddings.json"),
2872
+ policyViolationsPath: import_node_path29.default.join(baseDir, "policy-violations.ndjson")
2753
2873
  };
2754
2874
  }
2755
2875
  return {
2756
- snapshotPath: import_node_path28.default.join(baseDir, `${project}.json`),
2757
- errorsPath: import_node_path28.default.join(baseDir, `errors.${project}.ndjson`),
2758
- staleEventsPath: import_node_path28.default.join(baseDir, `stale-events.${project}.ndjson`),
2759
- embeddingsCachePath: import_node_path28.default.join(baseDir, `embeddings.${project}.json`),
2760
- policyViolationsPath: import_node_path28.default.join(baseDir, `policy-violations.${project}.ndjson`)
2876
+ snapshotPath: import_node_path29.default.join(baseDir, `${project}.json`),
2877
+ errorsPath: import_node_path29.default.join(baseDir, `errors.${project}.ndjson`),
2878
+ staleEventsPath: import_node_path29.default.join(baseDir, `stale-events.${project}.ndjson`),
2879
+ embeddingsCachePath: import_node_path29.default.join(baseDir, `embeddings.${project}.json`),
2880
+ policyViolationsPath: import_node_path29.default.join(baseDir, `policy-violations.${project}.ndjson`)
2761
2881
  };
2762
2882
  }
2763
2883
 
2764
2884
  // src/registry.ts
2765
- var import_node_fs16 = require("fs");
2885
+ var import_node_fs17 = require("fs");
2766
2886
  var import_node_os2 = __toESM(require("os"), 1);
2767
- var import_node_path29 = __toESM(require("path"), 1);
2887
+ var import_node_path30 = __toESM(require("path"), 1);
2768
2888
  var import_types18 = require("@neat.is/types");
2769
2889
  var LOCK_TIMEOUT_MS = 5e3;
2770
2890
  var LOCK_RETRY_MS = 50;
2771
2891
  function neatHome() {
2772
2892
  const override = process.env.NEAT_HOME;
2773
- if (override && override.length > 0) return import_node_path29.default.resolve(override);
2774
- return import_node_path29.default.join(import_node_os2.default.homedir(), ".neat");
2893
+ if (override && override.length > 0) return import_node_path30.default.resolve(override);
2894
+ return import_node_path30.default.join(import_node_os2.default.homedir(), ".neat");
2775
2895
  }
2776
2896
  function registryPath() {
2777
- return import_node_path29.default.join(neatHome(), "projects.json");
2897
+ return import_node_path30.default.join(neatHome(), "projects.json");
2778
2898
  }
2779
2899
  function registryLockPath() {
2780
- return import_node_path29.default.join(neatHome(), "projects.json.lock");
2900
+ return import_node_path30.default.join(neatHome(), "projects.json.lock");
2781
2901
  }
2782
2902
  async function writeAtomically(target, contents) {
2783
- await import_node_fs16.promises.mkdir(import_node_path29.default.dirname(target), { recursive: true });
2903
+ await import_node_fs17.promises.mkdir(import_node_path30.default.dirname(target), { recursive: true });
2784
2904
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
2785
- const fd = await import_node_fs16.promises.open(tmp, "w");
2905
+ const fd = await import_node_fs17.promises.open(tmp, "w");
2786
2906
  try {
2787
2907
  await fd.writeFile(contents, "utf8");
2788
2908
  await fd.sync();
2789
2909
  } finally {
2790
2910
  await fd.close();
2791
2911
  }
2792
- await import_node_fs16.promises.rename(tmp, target);
2912
+ await import_node_fs17.promises.rename(tmp, target);
2793
2913
  }
2794
2914
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
2795
2915
  const deadline = Date.now() + timeoutMs;
2796
- await import_node_fs16.promises.mkdir(import_node_path29.default.dirname(lockPath), { recursive: true });
2916
+ await import_node_fs17.promises.mkdir(import_node_path30.default.dirname(lockPath), { recursive: true });
2797
2917
  while (true) {
2798
2918
  try {
2799
- const fd = await import_node_fs16.promises.open(lockPath, "wx");
2919
+ const fd = await import_node_fs17.promises.open(lockPath, "wx");
2800
2920
  await fd.close();
2801
2921
  return;
2802
2922
  } catch (err) {
@@ -2812,7 +2932,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
2812
2932
  }
2813
2933
  }
2814
2934
  async function releaseLock(lockPath) {
2815
- await import_node_fs16.promises.unlink(lockPath).catch(() => {
2935
+ await import_node_fs17.promises.unlink(lockPath).catch(() => {
2816
2936
  });
2817
2937
  }
2818
2938
  async function withLock(fn) {
@@ -2828,7 +2948,7 @@ async function readRegistry() {
2828
2948
  const file = registryPath();
2829
2949
  let raw;
2830
2950
  try {
2831
- raw = await import_node_fs16.promises.readFile(file, "utf8");
2951
+ raw = await import_node_fs17.promises.readFile(file, "utf8");
2832
2952
  } catch (err) {
2833
2953
  if (err.code === "ENOENT") {
2834
2954
  return { version: 1, projects: [] };
@@ -2868,15 +2988,15 @@ async function touchLastSeen(name, at = (/* @__PURE__ */ new Date()).toISOString
2868
2988
 
2869
2989
  // src/daemon.ts
2870
2990
  function neatHomeFor(opts) {
2871
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path30.default.resolve(opts.neatHome);
2991
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path31.default.resolve(opts.neatHome);
2872
2992
  const env = process.env.NEAT_HOME;
2873
- if (env && env.length > 0) return import_node_path30.default.resolve(env);
2993
+ if (env && env.length > 0) return import_node_path31.default.resolve(env);
2874
2994
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
2875
- return import_node_path30.default.join(home, ".neat");
2995
+ return import_node_path31.default.join(home, ".neat");
2876
2996
  }
2877
2997
  async function bootstrapProject(entry2) {
2878
2998
  try {
2879
- const stat = await import_node_fs17.promises.stat(entry2.path);
2999
+ const stat = await import_node_fs18.promises.stat(entry2.path);
2880
3000
  if (!stat.isDirectory()) {
2881
3001
  throw new Error(`registered path ${entry2.path} is not a directory`);
2882
3002
  }
@@ -2899,7 +3019,7 @@ async function bootstrapProject(entry2) {
2899
3019
  const graph = getGraph(entry2.name);
2900
3020
  const outPath = pathsForProject(
2901
3021
  entry2.name,
2902
- import_node_path30.default.join(entry2.path, "neat-out")
3022
+ import_node_path31.default.join(entry2.path, "neat-out")
2903
3023
  ).snapshotPath;
2904
3024
  await loadGraphFromDisk(graph, outPath);
2905
3025
  await extractFromDirectory(graph, entry2.path);
@@ -2918,13 +3038,13 @@ async function startDaemon(opts = {}) {
2918
3038
  const home = neatHomeFor(opts);
2919
3039
  const regPath = registryPath();
2920
3040
  try {
2921
- await import_node_fs17.promises.access(regPath);
3041
+ await import_node_fs18.promises.access(regPath);
2922
3042
  } catch {
2923
3043
  throw new Error(
2924
3044
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
2925
3045
  );
2926
3046
  }
2927
- const pidPath = import_node_path30.default.join(home, "neatd.pid");
3047
+ const pidPath = import_node_path31.default.join(home, "neatd.pid");
2928
3048
  await writeAtomically(pidPath, `${process.pid}
2929
3049
  `);
2930
3050
  const slots = /* @__PURE__ */ new Map();
@@ -2990,7 +3110,7 @@ async function startDaemon(opts = {}) {
2990
3110
  } catch {
2991
3111
  }
2992
3112
  }
2993
- await import_node_fs17.promises.unlink(pidPath).catch(() => {
3113
+ await import_node_fs18.promises.unlink(pidPath).catch(() => {
2994
3114
  });
2995
3115
  };
2996
3116
  return { slots, reload, stop, pidPath };
@@ -2999,14 +3119,14 @@ async function startDaemon(opts = {}) {
2999
3119
  // src/neatd.ts
3000
3120
  function neatHome2() {
3001
3121
  if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {
3002
- return import_node_path31.default.resolve(process.env.NEAT_HOME);
3122
+ return import_node_path32.default.resolve(process.env.NEAT_HOME);
3003
3123
  }
3004
3124
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
3005
- return import_node_path31.default.join(home, ".neat");
3125
+ return import_node_path32.default.join(home, ".neat");
3006
3126
  }
3007
3127
  async function readPid() {
3008
3128
  try {
3009
- const raw = await import_node_fs18.promises.readFile(import_node_path31.default.join(neatHome2(), "neatd.pid"), "utf8");
3129
+ const raw = await import_node_fs19.promises.readFile(import_node_path32.default.join(neatHome2(), "neatd.pid"), "utf8");
3010
3130
  const n = Number.parseInt(raw.trim(), 10);
3011
3131
  return Number.isFinite(n) ? n : null;
3012
3132
  } catch {