@neat.is/core 0.7.9 → 0.7.10

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/cli.cjs CHANGED
@@ -61,8 +61,8 @@ function mountBearerAuth(app, opts) {
61
61
  ]);
62
62
  const publicRead = opts.publicRead === true;
63
63
  app.addHook("preHandler", (req, reply, done) => {
64
- const path82 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
65
- if (exactUnauthPaths.has(path82) || PROJECT_SCOPED_UNAUTH_PATTERN.test(path82)) {
64
+ const path84 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
65
+ if (exactUnauthPaths.has(path84) || PROJECT_SCOPED_UNAUTH_PATTERN.test(path84)) {
66
66
  done();
67
67
  return;
68
68
  }
@@ -415,8 +415,8 @@ function websocketChannelPathOf(attrs) {
415
415
  const v = attrs[key];
416
416
  if (typeof v === "string" && v.length > 0) {
417
417
  const q = v.indexOf("?");
418
- const path82 = q === -1 ? v : v.slice(0, q);
419
- if (path82.length > 0) return path82;
418
+ const path84 = q === -1 ? v : v.slice(0, q);
419
+ if (path84.length > 0) return path84;
420
420
  }
421
421
  }
422
422
  return void 0;
@@ -771,9 +771,9 @@ __export(cli_exports, {
771
771
  });
772
772
  module.exports = __toCommonJS(cli_exports);
773
773
  init_cjs_shims();
774
- var import_node_path81 = __toESM(require("path"), 1);
774
+ var import_node_path83 = __toESM(require("path"), 1);
775
775
  var import_node_os8 = __toESM(require("os"), 1);
776
- var import_node_fs46 = require("fs");
776
+ var import_node_fs48 = require("fs");
777
777
 
778
778
  // src/banner.ts
779
779
  init_cjs_shims();
@@ -1341,19 +1341,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1341
1341
  function longestIncomingWalk(graph, start, maxDepth) {
1342
1342
  let best = { path: [start], edges: [] };
1343
1343
  const visited = /* @__PURE__ */ new Set([start]);
1344
- function step(node, path82, edges) {
1345
- if (path82.length > best.path.length) {
1346
- best = { path: [...path82], edges: [...edges] };
1344
+ function step(node, path84, edges) {
1345
+ if (path84.length > best.path.length) {
1346
+ best = { path: [...path84], edges: [...edges] };
1347
1347
  }
1348
- if (path82.length - 1 >= maxDepth) return;
1348
+ if (path84.length - 1 >= maxDepth) return;
1349
1349
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1350
1350
  for (const [srcId, edge] of incoming) {
1351
1351
  if (visited.has(srcId)) continue;
1352
1352
  visited.add(srcId);
1353
- path82.push(srcId);
1353
+ path84.push(srcId);
1354
1354
  edges.push(edge);
1355
- step(srcId, path82, edges);
1356
- path82.pop();
1355
+ step(srcId, path84, edges);
1356
+ path84.pop();
1357
1357
  edges.pop();
1358
1358
  visited.delete(srcId);
1359
1359
  }
@@ -1560,26 +1560,26 @@ function dominantFailingCall(graph, serviceId9, visited) {
1560
1560
  return best;
1561
1561
  }
1562
1562
  function followFailingCallChain(graph, originServiceId, maxDepth) {
1563
- const path82 = [originServiceId];
1563
+ const path84 = [originServiceId];
1564
1564
  const edges = [];
1565
1565
  const visited = /* @__PURE__ */ new Set([originServiceId]);
1566
1566
  let current = originServiceId;
1567
1567
  for (let depth = 0; depth < maxDepth; depth++) {
1568
1568
  const hop = dominantFailingCall(graph, current, visited);
1569
1569
  if (!hop) break;
1570
- path82.push(hop.nextService);
1570
+ path84.push(hop.nextService);
1571
1571
  edges.push(hop.edge);
1572
1572
  visited.add(hop.nextService);
1573
1573
  current = hop.nextService;
1574
1574
  }
1575
1575
  if (edges.length === 0) return null;
1576
- return { path: path82, edges, culprit: current };
1576
+ return { path: path84, edges, culprit: current };
1577
1577
  }
1578
1578
  function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1579
1579
  const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
1580
1580
  if (!chain) return null;
1581
1581
  const culprit = chain.culprit;
1582
- const path82 = [...chain.path];
1582
+ const path84 = [...chain.path];
1583
1583
  const edgeProvenances = chain.edges.map((e) => e.provenance);
1584
1584
  const baseConfidence = confidenceFromMix(chain.edges);
1585
1585
  const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
@@ -1587,14 +1587,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1587
1587
  if (loc) {
1588
1588
  let rootCauseNode = culprit;
1589
1589
  if (loc.fileNode) {
1590
- path82.push(loc.fileNode);
1590
+ path84.push(loc.fileNode);
1591
1591
  edgeProvenances.push(import_types.Provenance.OBSERVED);
1592
1592
  rootCauseNode = loc.fileNode;
1593
1593
  }
1594
1594
  return import_types.RootCauseResultSchema.parse({
1595
1595
  rootCauseNode,
1596
1596
  rootCauseReason: loc.rootCauseReason,
1597
- traversalPath: path82,
1597
+ traversalPath: path84,
1598
1598
  edgeProvenances,
1599
1599
  confidence,
1600
1600
  ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
@@ -1606,7 +1606,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1606
1606
  return import_types.RootCauseResultSchema.parse({
1607
1607
  rootCauseNode: culprit,
1608
1608
  rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
1609
- traversalPath: path82,
1609
+ traversalPath: path84,
1610
1610
  edgeProvenances,
1611
1611
  confidence,
1612
1612
  fixRecommendation: `Inspect ${culpritName}'s failing handler`
@@ -2274,6 +2274,7 @@ var import_yaml = require("yaml");
2274
2274
  var import_types3 = require("@neat.is/types");
2275
2275
  var SERVICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".py", ".go", ".rb", ".php"]);
2276
2276
  var CONFIG_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
2277
+ var JSON_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["app.json", "app.config.json", "eas.json"]);
2277
2278
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
2278
2279
  "node_modules",
2279
2280
  ".git",
@@ -2313,6 +2314,7 @@ async function isPythonVenvDir(dir) {
2313
2314
  function isConfigFile(name) {
2314
2315
  const ext = import_node_path4.default.extname(name);
2315
2316
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
2317
+ if (JSON_CONFIG_FILENAMES.has(name)) return { match: true, fileType: "json" };
2316
2318
  if (name === ".env" || name.startsWith(".env.")) {
2317
2319
  if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
2318
2320
  if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
@@ -3141,8 +3143,8 @@ function chiRoutesFromSource(source, parser) {
3141
3143
  chiWalk(tree.rootNode, "", out);
3142
3144
  return out;
3143
3145
  }
3144
- function stripChiRegex(path82) {
3145
- return path82.replace(/\{([^{}:]+):[^{}]*\}/g, "{$1}");
3146
+ function stripChiRegex(path84) {
3147
+ return path84.replace(/\{([^{}:]+):[^{}]*\}/g, "{$1}");
3146
3148
  }
3147
3149
  function chiWalk(node, prefix, out) {
3148
3150
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -3814,9 +3816,9 @@ function rubyRocketRoute(args) {
3814
3816
  if (!pair || pair.type !== "pair") continue;
3815
3817
  const k = pair.childForFieldName("key");
3816
3818
  if (k?.type !== "string") continue;
3817
- const path82 = rubyLiteral(k);
3818
- if (path82 === null) continue;
3819
- return { path: path82, target: rubyLiteral(pair.childForFieldName("value")) };
3819
+ const path84 = rubyLiteral(k);
3820
+ if (path84 === null) continue;
3821
+ return { path: path84, target: rubyLiteral(pair.childForFieldName("value")) };
3820
3822
  }
3821
3823
  return null;
3822
3824
  }
@@ -4436,7 +4438,7 @@ async function expressMountPrefixes(files, serviceDir, tsPaths) {
4436
4438
  };
4437
4439
  const filePrefix = /* @__PURE__ */ new Map();
4438
4440
  const conflicted = /* @__PURE__ */ new Set();
4439
- const apply4 = (file, prefix) => {
4441
+ const apply6 = (file, prefix) => {
4440
4442
  if (conflicted.has(file)) return;
4441
4443
  const existing = filePrefix.get(file);
4442
4444
  if (existing === void 0) filePrefix.set(file, prefix);
@@ -4453,7 +4455,7 @@ async function expressMountPrefixes(files, serviceDir, tsPaths) {
4453
4455
  const info = fileInfo.get(file);
4454
4456
  const rv = info?.routerVars.get(name);
4455
4457
  if (!info || !rv) return;
4456
- if (rv.declares && info.appVars.size === 0) apply4(file, accPrefix);
4458
+ if (rv.declares && info.appVars.size === 0) apply6(file, accPrefix);
4457
4459
  for (const m of rv.mounts) {
4458
4460
  if (!m.target) continue;
4459
4461
  const t = resolveTarget(m.target, file);
@@ -5429,6 +5431,21 @@ async function appendErrorEvent(ctx, ev) {
5429
5431
  await import_node_fs8.promises.mkdir(import_node_path9.default.dirname(ctx.errorsPath), { recursive: true });
5430
5432
  await import_node_fs8.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
5431
5433
  }
5434
+ async function appendConnectorIncident(errorsPath, input) {
5435
+ const ev = {
5436
+ id: input.id,
5437
+ timestamp: input.timestamp,
5438
+ service: input.service,
5439
+ traceId: input.id,
5440
+ spanId: input.id,
5441
+ errorType: input.errorType,
5442
+ errorMessage: input.errorMessage,
5443
+ ...input.attributes && Object.keys(input.attributes).length > 0 ? { attributes: input.attributes } : {},
5444
+ affectedNode: input.affectedNode
5445
+ };
5446
+ await import_node_fs8.promises.mkdir(import_node_path9.default.dirname(errorsPath), { recursive: true });
5447
+ await import_node_fs8.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
5448
+ }
5432
5449
  function incidentAffectedNode(span, graph, scanPath) {
5433
5450
  const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : (0, import_types8.serviceId)(span.service, span.env);
5434
5451
  const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
@@ -11242,8 +11259,41 @@ var import_tree_sitter14 = __toESM(require("tree-sitter"), 1);
11242
11259
  var import_tree_sitter_go3 = __toESM(require("tree-sitter-go"), 1);
11243
11260
  var import_types35 = require("@neat.is/types");
11244
11261
  init_otel();
11245
- var SQL_METHODS = /* @__PURE__ */ new Set(["Exec", "ExecContext", "Query", "QueryContext", "QueryRow", "QueryRowContext"]);
11246
11262
  var PARSE_CHUNK10 = 16384;
11263
+ var DATABASE_SQL_METHODS = /* @__PURE__ */ new Set([
11264
+ "Query",
11265
+ "QueryContext",
11266
+ "QueryRow",
11267
+ "QueryRowContext",
11268
+ "Exec",
11269
+ "ExecContext",
11270
+ "Prepare",
11271
+ "PrepareContext"
11272
+ ]);
11273
+ var SQLX_METHODS = /* @__PURE__ */ new Set([
11274
+ "Get",
11275
+ "Select",
11276
+ "Queryx",
11277
+ "QueryRowx",
11278
+ "NamedExec",
11279
+ "NamedQuery",
11280
+ "MustExec",
11281
+ "Preparex",
11282
+ "GetContext",
11283
+ "SelectContext"
11284
+ ]);
11285
+ var DATABASE_SQL_IMPORT = "database/sql";
11286
+ var SQLX_IMPORT = "github.com/jmoiron/sqlx";
11287
+ function makeGoParser3() {
11288
+ const p = new import_tree_sitter14.default();
11289
+ p.setLanguage(import_tree_sitter_go3.default);
11290
+ return p;
11291
+ }
11292
+ function parseSource10(parser, source) {
11293
+ return parser.parse(
11294
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK10)
11295
+ );
11296
+ }
11247
11297
  function walk7(node, visit) {
11248
11298
  visit(node);
11249
11299
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -11251,25 +11301,54 @@ function walk7(node, visit) {
11251
11301
  if (child) walk7(child, visit);
11252
11302
  }
11253
11303
  }
11304
+ function goStringLiteralValue(node) {
11305
+ if (!node) return null;
11306
+ if (node.type === "interpreted_string_literal" || node.type === "raw_string_literal") {
11307
+ const t = node.text;
11308
+ return t.length >= 2 ? t.slice(1, -1) : "";
11309
+ }
11310
+ return null;
11311
+ }
11312
+ function goImportsAny(root, names) {
11313
+ let found = false;
11314
+ walk7(root, (node) => {
11315
+ if (found || node.type !== "import_spec") return;
11316
+ for (let i = 0; i < node.namedChildCount; i++) {
11317
+ const value = goStringLiteralValue(node.namedChild(i));
11318
+ if (value !== null && names.has(value)) found = true;
11319
+ }
11320
+ });
11321
+ return found;
11322
+ }
11323
+ function firstStringLiteralArg(argsNode) {
11324
+ if (!argsNode) return null;
11325
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
11326
+ const value = goStringLiteralValue(argsNode.namedChild(i));
11327
+ if (value !== null) return value;
11328
+ }
11329
+ return null;
11330
+ }
11254
11331
  function goSqlEndpointsFromFile(file, serviceDir) {
11255
11332
  if (import_node_path48.default.extname(file.path) !== ".go") return [];
11256
- const parser = new import_tree_sitter14.default();
11257
- parser.setLanguage(import_tree_sitter_go3.default);
11258
- const tree = parser.parse(
11259
- (index) => index >= file.content.length ? "" : file.content.slice(index, index + PARSE_CHUNK10)
11260
- );
11333
+ if (!file.content.includes(DATABASE_SQL_IMPORT) && !file.content.includes(SQLX_IMPORT)) return [];
11334
+ const tree = parseSource10(makeGoParser3(), file.content);
11335
+ const importsDatabaseSql = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([DATABASE_SQL_IMPORT]));
11336
+ const importsSqlx = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([SQLX_IMPORT]));
11337
+ if (!importsDatabaseSql && !importsSqlx) return [];
11261
11338
  const out = [];
11262
11339
  walk7(tree.rootNode, (node) => {
11263
11340
  if (node.type !== "call_expression") return;
11264
11341
  const fn = node.childForFieldName("function");
11265
11342
  if (fn?.type !== "selector_expression") return;
11266
11343
  const method = fn.childForFieldName("field")?.text;
11267
- if (!method || !SQL_METHODS.has(method)) return;
11268
- const arg = node.childForFieldName("arguments")?.namedChild(0);
11269
- if (!arg || arg.type !== "interpreted_string_literal" && arg.type !== "raw_string_literal") return;
11270
- const sql = arg.text.slice(1, -1);
11344
+ if (!method) return;
11345
+ const recognized = DATABASE_SQL_METHODS.has(method) || importsSqlx && SQLX_METHODS.has(method);
11346
+ if (!recognized) return;
11347
+ const sql = firstStringLiteralArg(node.childForFieldName("arguments"));
11348
+ if (sql === null) return;
11271
11349
  const table = tableFromSqlStatement(sql);
11272
11350
  if (!table) return;
11351
+ const columns = columnsFromSqlStatement(sql);
11273
11352
  const line = node.startPosition.row + 1;
11274
11353
  out.push({
11275
11354
  infraId: (0, import_types35.infraId)("sql-table", table),
@@ -11277,7 +11356,12 @@ function goSqlEndpointsFromFile(file, serviceDir) {
11277
11356
  kind: "sql-table",
11278
11357
  edgeType: "CALLS",
11279
11358
  confidenceKind: "verified-call-site",
11280
- evidence: { file: toPosix(import_node_path48.default.relative(serviceDir, file.path)), line, snippet: snippet(file.content, line) }
11359
+ ...columns.length > 0 ? { columns } : {},
11360
+ evidence: {
11361
+ file: toPosix(import_node_path48.default.relative(serviceDir, file.path)),
11362
+ line,
11363
+ snippet: snippet(file.content, line)
11364
+ }
11281
11365
  });
11282
11366
  });
11283
11367
  return out;
@@ -11291,12 +11375,12 @@ var import_tree_sitter_go4 = __toESM(require("tree-sitter-go"), 1);
11291
11375
  var import_types36 = require("@neat.is/types");
11292
11376
  var GORM_IMPORT_RE = /gorm\.io\/gorm/;
11293
11377
  var PARSE_CHUNK11 = 16384;
11294
- function makeGoParser3() {
11378
+ function makeGoParser4() {
11295
11379
  const p = new import_tree_sitter15.default();
11296
11380
  p.setLanguage(import_tree_sitter_go4.default);
11297
11381
  return p;
11298
11382
  }
11299
- function parseSource10(parser, source) {
11383
+ function parseSource11(parser, source) {
11300
11384
  return parser.parse(
11301
11385
  (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK11)
11302
11386
  );
@@ -11720,7 +11804,7 @@ function collectColumns(struct, structs, seen, prefix, out, emitted) {
11720
11804
  function gormEndpointsFromFile(file, serviceDir) {
11721
11805
  if (import_node_path49.default.extname(file.path) !== ".go") return [];
11722
11806
  if (!GORM_IMPORT_RE.test(file.content)) return [];
11723
- const tree = parseSource10(makeGoParser3(), file.content);
11807
+ const tree = parseSource11(makeGoParser4(), file.content);
11724
11808
  const { structs, models, tableFor } = analyze(tree);
11725
11809
  const out = [];
11726
11810
  const seenTables = /* @__PURE__ */ new Set();
@@ -11751,7 +11835,7 @@ function gormEndpointsFromFile(file, serviceDir) {
11751
11835
  function gormForeignKeys(file, serviceDir) {
11752
11836
  if (import_node_path49.default.extname(file.path) !== ".go") return [];
11753
11837
  if (!GORM_IMPORT_RE.test(file.content)) return [];
11754
- const tree = parseSource10(makeGoParser3(), file.content);
11838
+ const tree = parseSource11(makeGoParser4(), file.content);
11755
11839
  const { structs, models, tableFor } = analyze(tree);
11756
11840
  const out = [];
11757
11841
  const seen = /* @__PURE__ */ new Set();
@@ -13978,7 +14062,7 @@ var import_chokidar = __toESM(require("chokidar"), 1);
13978
14062
  init_cjs_shims();
13979
14063
  var import_fastify2 = __toESM(require("fastify"), 1);
13980
14064
  var import_cors = __toESM(require("@fastify/cors"), 1);
13981
- var import_types81 = require("@neat.is/types");
14065
+ var import_types86 = require("@neat.is/types");
13982
14066
 
13983
14067
  // src/extend/index.ts
13984
14068
  init_cjs_shims();
@@ -15307,6 +15391,23 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
15307
15391
  unresolved++;
15308
15392
  continue;
15309
15393
  }
15394
+ if (signal.incident) {
15395
+ ensureServiceNode(graph, resolved.serviceName, NO_ENV);
15396
+ if (!ctx.errorsPath || !graph.hasNode(resolved.targetNodeId)) {
15397
+ unresolved++;
15398
+ continue;
15399
+ }
15400
+ await appendConnectorIncident(ctx.errorsPath, {
15401
+ id: signal.incident.id,
15402
+ timestamp: signal.incident.timestamp,
15403
+ service: signal.incident.service,
15404
+ errorType: signal.incident.errorType,
15405
+ errorMessage: signal.incident.errorMessage,
15406
+ ...signal.incident.attributes ? { attributes: signal.incident.attributes } : {},
15407
+ affectedNode: resolved.targetNodeId
15408
+ });
15409
+ continue;
15410
+ }
15310
15411
  if (resolved.ensureInfraNode) {
15311
15412
  const { kind, name, provider } = resolved.ensureInfraNode;
15312
15413
  ensureInfraNode(graph, kind, name, provider);
@@ -15772,10 +15873,10 @@ var SUPABASE_RPC_TARGET_KIND = "supabase-rpc";
15772
15873
  // src/connectors/supabase/map.ts
15773
15874
  var REST_RPC_PATH_RE = /^\/rest\/v1\/rpc\/([^/?]+)/;
15774
15875
  var REST_TABLE_PATH_RE = /^\/rest\/v1\/([^/?]+)/;
15775
- function targetFromRestPath(path82) {
15776
- const rpcMatch = REST_RPC_PATH_RE.exec(path82);
15876
+ function targetFromRestPath(path84) {
15877
+ const rpcMatch = REST_RPC_PATH_RE.exec(path84);
15777
15878
  if (rpcMatch) return { targetKind: SUPABASE_RPC_TARGET_KIND, name: rpcMatch[1] };
15778
- const tableMatch = REST_TABLE_PATH_RE.exec(path82);
15879
+ const tableMatch = REST_TABLE_PATH_RE.exec(path84);
15779
15880
  if (tableMatch) return { targetKind: SUPABASE_TABLE_TARGET_KIND, name: tableMatch[1] };
15780
15881
  return null;
15781
15882
  }
@@ -16379,9 +16480,9 @@ function parseFirebaseTargetName(targetName) {
16379
16480
  const secondSep = rest.indexOf(FIELD_SEP);
16380
16481
  if (secondSep === -1) return null;
16381
16482
  const method = rest.slice(0, secondSep);
16382
- const path82 = rest.slice(secondSep + 1);
16383
- if (!resourceName || !method || !path82) return null;
16384
- return { resourceName, method, path: path82 };
16483
+ const path84 = rest.slice(secondSep + 1);
16484
+ if (!resourceName || !method || !path84) return null;
16485
+ return { resourceName, method, path: path84 };
16385
16486
  }
16386
16487
  function resourceNameFor(type, labels) {
16387
16488
  if (!labels) return null;
@@ -16419,14 +16520,14 @@ function mapLogEntryToSignal(entry2) {
16419
16520
  if (!req) return null;
16420
16521
  if (typeof req.requestMethod !== "string" || req.requestMethod.length === 0) return null;
16421
16522
  const method = req.requestMethod.toUpperCase();
16422
- const path82 = pathFromRequestUrl(req.requestUrl);
16423
- if (path82 === null) return null;
16523
+ const path84 = pathFromRequestUrl(req.requestUrl);
16524
+ if (path84 === null) return null;
16424
16525
  const timestamp = entry2.timestamp;
16425
16526
  if (typeof timestamp !== "string" || timestamp.length === 0) return null;
16426
16527
  const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD2;
16427
16528
  return {
16428
16529
  targetKind: resourceType,
16429
- targetName: packFirebaseTargetName({ resourceName, method, path: path82 }),
16530
+ targetName: packFirebaseTargetName({ resourceName, method, path: path84 }),
16430
16531
  callCount: 1,
16431
16532
  errorCount: isError ? 1 : 0,
16432
16533
  lastObservedIso: timestamp
@@ -16633,7 +16734,7 @@ function mapEventToSignal(event) {
16633
16734
  if (Number.isNaN(observedAt.getTime())) return null;
16634
16735
  const statusCode = metadata?.statusCode;
16635
16736
  const isError = typeof statusCode === "number" && statusCode >= ERROR_STATUS_THRESHOLD3;
16636
- const path82 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
16737
+ const path84 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
16637
16738
  return {
16638
16739
  targetKind: CLOUDFLARE_TARGET_KIND,
16639
16740
  targetName: scriptName,
@@ -16641,7 +16742,7 @@ function mapEventToSignal(event) {
16641
16742
  errorCount: isError ? 1 : 0,
16642
16743
  lastObservedIso: observedAt.toISOString(),
16643
16744
  method,
16644
- ...path82 ? { path: path82 } : {},
16745
+ ...path84 ? { path: path84 } : {},
16645
16746
  ...typeof statusCode === "number" ? { statusCode } : {},
16646
16747
  ...typeof metadata?.duration === "number" ? { duration: metadata.duration } : {}
16647
16748
  };
@@ -16687,8 +16788,8 @@ function findTaggedWorkerFileNode(graph, workerName) {
16687
16788
  });
16688
16789
  return found;
16689
16790
  }
16690
- function findMatchingRouteNode(graph, serviceName, method, path82) {
16691
- const normalizedPath = normalizePathTemplate(path82);
16791
+ function findMatchingRouteNode(graph, serviceName, method, path84) {
16792
+ const normalizedPath = normalizePathTemplate(path84);
16692
16793
  let found = null;
16693
16794
  graph.forEachNode((id, attrs) => {
16694
16795
  if (found) return;
@@ -16705,10 +16806,10 @@ function createCloudflareResolveTarget(config, graph) {
16705
16806
  return (signal) => {
16706
16807
  if (signal.targetKind !== CLOUDFLARE_TARGET_KIND) return null;
16707
16808
  const scriptName = signal.targetName;
16708
- const { method, path: path82 } = signal;
16809
+ const { method, path: path84 } = signal;
16709
16810
  const resolveRouteGrain = (serviceName, wholeFileId) => {
16710
- if (!method || !path82) return wholeFileId;
16711
- return findMatchingRouteNode(graph, serviceName, method, path82) ?? wholeFileId;
16811
+ if (!method || !path84) return wholeFileId;
16812
+ return findMatchingRouteNode(graph, serviceName, method, path84) ?? wholeFileId;
16712
16813
  };
16713
16814
  const mapping = config.workers?.[scriptName];
16714
16815
  if (mapping) {
@@ -17060,9 +17161,9 @@ function parseCloudRunTargetName(targetName) {
17060
17161
  const secondSep = rest.indexOf(FIELD_SEP2);
17061
17162
  if (secondSep === -1) return null;
17062
17163
  const method = rest.slice(0, secondSep);
17063
- const path82 = rest.slice(secondSep + 1);
17064
- if (!serviceName || !method || !path82) return null;
17065
- return { serviceName, method, path: path82 };
17164
+ const path84 = rest.slice(secondSep + 1);
17165
+ if (!serviceName || !method || !path84) return null;
17166
+ return { serviceName, method, path: path84 };
17066
17167
  }
17067
17168
 
17068
17169
  // src/connectors/cloud-run/map.ts
@@ -17091,14 +17192,14 @@ function mapLogEntryToSignal2(entry2) {
17091
17192
  if (!req) return null;
17092
17193
  if (typeof req.requestMethod !== "string" || req.requestMethod.length === 0) return null;
17093
17194
  const method = req.requestMethod.toUpperCase();
17094
- const path82 = pathFromRequestUrl2(req.requestUrl);
17095
- if (path82 === null) return null;
17195
+ const path84 = pathFromRequestUrl2(req.requestUrl);
17196
+ if (path84 === null) return null;
17096
17197
  const timestamp = entry2.timestamp;
17097
17198
  if (typeof timestamp !== "string" || timestamp.length === 0) return null;
17098
17199
  const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD4;
17099
17200
  return {
17100
17201
  targetKind: CLOUD_RUN_TARGET_KIND,
17101
- targetName: packCloudRunTargetName({ serviceName, method, path: path82 }),
17202
+ targetName: packCloudRunTargetName({ serviceName, method, path: path84 }),
17102
17203
  callCount: 1,
17103
17204
  errorCount: isError ? 1 : 0,
17104
17205
  lastObservedIso: timestamp
@@ -17137,14 +17238,14 @@ function createCloudRunResolveTarget(graph, config) {
17137
17238
  if (signal.targetKind !== CLOUD_RUN_TARGET_KIND) return null;
17138
17239
  const identity = parseCloudRunTargetName(signal.targetName);
17139
17240
  if (!identity) return null;
17140
- const { serviceName: gcpServiceName, method, path: path82 } = identity;
17241
+ const { serviceName: gcpServiceName, method, path: path84 } = identity;
17141
17242
  const mappedService = config.serviceMap?.[gcpServiceName];
17142
17243
  if (mappedService) {
17143
17244
  const routeNodeId = findMatchingRouteNode2(
17144
17245
  graph,
17145
17246
  mappedService,
17146
17247
  method,
17147
- normalizePathTemplate(path82)
17248
+ normalizePathTemplate(path84)
17148
17249
  );
17149
17250
  if (routeNodeId) {
17150
17251
  return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types71.EdgeType.CALLS };
@@ -17541,6 +17642,338 @@ function createPlanetscaleConnector(graph, config, deps = {}) {
17541
17642
  };
17542
17643
  }
17543
17644
 
17645
+ // src/connectors/eas/index.ts
17646
+ init_cjs_shims();
17647
+
17648
+ // src/connectors/eas/client.ts
17649
+ init_cjs_shims();
17650
+
17651
+ // src/connectors/eas/types.ts
17652
+ init_cjs_shims();
17653
+ function readEasCredentials(raw) {
17654
+ const token = raw["token"];
17655
+ if (typeof token !== "string" || token.length === 0) {
17656
+ throw new Error("eas connector: credentials.token (EXPO_TOKEN) must be a non-empty string");
17657
+ }
17658
+ return { token };
17659
+ }
17660
+ var EAS_STATUS_ERRORED = "ERRORED";
17661
+ var TRANSIENT_BUILD_PHASES = /* @__PURE__ */ new Set([
17662
+ "SPIN_UP_BUILDER",
17663
+ "PREPARE_CREDENTIALS",
17664
+ "RESTORE_CACHE",
17665
+ "UPLOAD_APPLICATION_ARCHIVE"
17666
+ ]);
17667
+ var INTERNAL_ERROR_CODE = /INTERNAL_SERVER_ERROR|EAS_BUILD_.*INTERNAL|_INTERNAL_ERROR|UNKNOWN_ERROR/i;
17668
+ function isTransientFailure(err) {
17669
+ if (!err) return false;
17670
+ const phase = typeof err.buildPhase === "string" ? err.buildPhase : void 0;
17671
+ if (phase) {
17672
+ if (TRANSIENT_BUILD_PHASES.has(phase)) return true;
17673
+ if (/^SPIN_UP|_CREDENTIALS$|^RESTORE_CACHE/i.test(phase)) return true;
17674
+ }
17675
+ const code = typeof err.errorCode === "string" ? err.errorCode : void 0;
17676
+ if (code && INTERNAL_ERROR_CODE.test(code)) return true;
17677
+ return false;
17678
+ }
17679
+ var FIELD_SEP3 = "\0";
17680
+ var EAS_TARGET_KIND = "eas-build";
17681
+ function packEasTargetName(identity) {
17682
+ return [identity.serviceName, identity.phase].join(FIELD_SEP3);
17683
+ }
17684
+ function parseEasTargetName(targetName) {
17685
+ const sep = targetName.indexOf(FIELD_SEP3);
17686
+ if (sep === -1) return null;
17687
+ const serviceName = targetName.slice(0, sep);
17688
+ const phase = targetName.slice(sep + 1);
17689
+ if (!serviceName) return null;
17690
+ return { serviceName, phase };
17691
+ }
17692
+
17693
+ // src/connectors/eas/client.ts
17694
+ var DEFAULT_EAS_API_URL = "https://api.expo.dev/graphql";
17695
+ var DEFAULT_PAGE_SIZE = 50;
17696
+ var DEFAULT_MAX_PAGES = 10;
17697
+ var DEFAULT_MAX_LOG_BYTES = 16 * 1024;
17698
+ var DEFAULT_MAX_LOOKBACK_MS6 = 7 * 24 * 60 * 60 * 1e3;
17699
+ var BUILDS_QUERY = `
17700
+ query NeatEasErroredBuilds($appId: String!, $offset: Int!, $limit: Int!) {
17701
+ app {
17702
+ byId(appId: $appId) {
17703
+ builds(offset: $offset, limit: $limit, filter: { status: ERRORED }) {
17704
+ id
17705
+ status
17706
+ platform
17707
+ buildProfile
17708
+ gitCommitHash
17709
+ gitCommitMessage
17710
+ gitRef
17711
+ isGitWorkingTreeDirty
17712
+ createdAt
17713
+ completedAt
17714
+ error {
17715
+ buildPhase
17716
+ errorCode
17717
+ message
17718
+ docsUrl
17719
+ }
17720
+ logFileUrls
17721
+ }
17722
+ }
17723
+ }
17724
+ }
17725
+ `;
17726
+ async function easGraphQL(apiUrl, token, query, variables, accountKey, fetchImpl) {
17727
+ const res = await junctionFetch(
17728
+ apiUrl,
17729
+ {
17730
+ method: "POST",
17731
+ headers: {
17732
+ "Content-Type": "application/json",
17733
+ ...bearerAuthHeader(token)
17734
+ },
17735
+ body: JSON.stringify({ query, variables })
17736
+ },
17737
+ // accountKey: the Expo app id — the per-(provider, accountKey) rate-limit
17738
+ // bucket (ADR-131), the closest thing this connector carries to "one account".
17739
+ { provider: "eas", accountKey, ...fetchImpl ? { fetchImpl } : {} }
17740
+ );
17741
+ if (!res.ok) {
17742
+ throw new Error(`Expo GraphQL request failed: ${res.status} ${res.statusText}`);
17743
+ }
17744
+ const body = await res.json();
17745
+ if (body.errors && body.errors.length > 0) {
17746
+ throw new Error(`Expo GraphQL errors: ${body.errors.map((e) => e.message).join("; ")}`);
17747
+ }
17748
+ if (!body.data) throw new Error("Expo GraphQL response carried no data");
17749
+ return body.data;
17750
+ }
17751
+ async function fetchErroredBuilds(token, config, fetchImpl) {
17752
+ const apiUrl = config.apiUrl ?? DEFAULT_EAS_API_URL;
17753
+ const pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE;
17754
+ const maxPages = config.maxPages ?? DEFAULT_MAX_PAGES;
17755
+ const out = [];
17756
+ const seen = /* @__PURE__ */ new Set();
17757
+ for (let page = 0; page < maxPages; page++) {
17758
+ const data = await easGraphQL(
17759
+ apiUrl,
17760
+ token,
17761
+ BUILDS_QUERY,
17762
+ { appId: config.appId, offset: page * pageSize, limit: pageSize },
17763
+ config.appId,
17764
+ fetchImpl
17765
+ );
17766
+ const builds = data.app?.byId?.builds;
17767
+ if (!Array.isArray(builds)) break;
17768
+ let added = 0;
17769
+ for (const b of builds) {
17770
+ if (!b || typeof b.id !== "string" || b.id.length === 0) continue;
17771
+ if (b.status !== EAS_STATUS_ERRORED) continue;
17772
+ if (seen.has(b.id)) continue;
17773
+ seen.add(b.id);
17774
+ out.push(b);
17775
+ added++;
17776
+ }
17777
+ if (builds.length < pageSize) break;
17778
+ if (added === 0) break;
17779
+ }
17780
+ return out;
17781
+ }
17782
+ async function fetchBuildLogs(logFileUrls, maxBytes = DEFAULT_MAX_LOG_BYTES, fetchImpl) {
17783
+ if (!Array.isArray(logFileUrls) || logFileUrls.length === 0) return "";
17784
+ const doFetch = fetchImpl ?? fetch;
17785
+ const chunks = [];
17786
+ for (const url of logFileUrls) {
17787
+ if (typeof url !== "string" || url.length === 0) continue;
17788
+ try {
17789
+ const res = await doFetch(url);
17790
+ if (!res.ok) continue;
17791
+ chunks.push(await res.text());
17792
+ } catch {
17793
+ }
17794
+ }
17795
+ const joined = chunks.join("\n");
17796
+ return joined.length > maxBytes ? joined.slice(joined.length - maxBytes) : joined;
17797
+ }
17798
+
17799
+ // src/connectors/eas/map.ts
17800
+ init_cjs_shims();
17801
+ function buildEventTime(build) {
17802
+ if (typeof build.completedAt === "string" && build.completedAt.length > 0) return build.completedAt;
17803
+ if (typeof build.createdAt === "string" && build.createdAt.length > 0) return build.createdAt;
17804
+ return (/* @__PURE__ */ new Date()).toISOString();
17805
+ }
17806
+ function incidentMessage2(build) {
17807
+ const err = build.error ?? {};
17808
+ const phase = typeof err.buildPhase === "string" && err.buildPhase.length > 0 ? ` at ${err.buildPhase}` : "";
17809
+ const detail = typeof err.message === "string" && err.message.trim().length > 0 && err.message.trim() || typeof err.errorCode === "string" && err.errorCode.length > 0 && err.errorCode || "no error detail reported";
17810
+ let msg = `EAS build failed${phase}: ${detail}`;
17811
+ if (build.isGitWorkingTreeDirty === true) {
17812
+ msg += " (built from a dirty working tree \u2014 the commit may not represent what built)";
17813
+ }
17814
+ return msg;
17815
+ }
17816
+ function incidentAttributes(build) {
17817
+ const attrs = {};
17818
+ const err = build.error ?? {};
17819
+ const put = (k, v) => {
17820
+ if (typeof v === "string" && v.length === 0) return;
17821
+ if (v !== void 0 && v !== null) attrs[k] = v;
17822
+ };
17823
+ put("eas.buildId", build.id);
17824
+ put("eas.platform", build.platform ?? void 0);
17825
+ put("eas.buildProfile", build.buildProfile ?? void 0);
17826
+ put("eas.buildPhase", err.buildPhase ?? void 0);
17827
+ put("eas.errorCode", err.errorCode ?? void 0);
17828
+ put("eas.docsUrl", err.docsUrl ?? void 0);
17829
+ put("eas.gitCommitHash", build.gitCommitHash ?? void 0);
17830
+ put("eas.gitRef", build.gitRef ?? void 0);
17831
+ put("eas.gitCommitMessage", build.gitCommitMessage ?? void 0);
17832
+ if (typeof build.isGitWorkingTreeDirty === "boolean") {
17833
+ attrs["eas.gitWorkingTreeDirty"] = build.isGitWorkingTreeDirty;
17834
+ if (build.isGitWorkingTreeDirty) attrs["eas.confidence"] = "low";
17835
+ }
17836
+ put("eas.createdAt", build.createdAt ?? void 0);
17837
+ put("eas.completedAt", build.completedAt ?? void 0);
17838
+ if (typeof build.logsText === "string" && build.logsText.length > 0) {
17839
+ attrs["eas.logs"] = build.logsText;
17840
+ }
17841
+ return attrs;
17842
+ }
17843
+ function mapBuildToSignal(build, serviceName) {
17844
+ if (!build || typeof build !== "object") return null;
17845
+ if (build.status !== EAS_STATUS_ERRORED) return null;
17846
+ if (!build.error) return null;
17847
+ if (isTransientFailure(build.error)) return null;
17848
+ const timestamp = buildEventTime(build);
17849
+ const phase = typeof build.error.buildPhase === "string" ? build.error.buildPhase : "";
17850
+ return {
17851
+ targetKind: EAS_TARGET_KIND,
17852
+ targetName: packEasTargetName({ serviceName, phase }),
17853
+ // Incident-only — no edge, so no call/error count to replay.
17854
+ callCount: 0,
17855
+ errorCount: 0,
17856
+ lastObservedIso: timestamp,
17857
+ incident: {
17858
+ id: `eas:build:${build.id}`,
17859
+ timestamp,
17860
+ service: serviceName,
17861
+ errorType: "eas-build-failure",
17862
+ errorMessage: incidentMessage2(build),
17863
+ attributes: incidentAttributes(build)
17864
+ }
17865
+ };
17866
+ }
17867
+ function mapBuildsToSignals(builds, serviceName) {
17868
+ const out = [];
17869
+ for (const build of builds) {
17870
+ const signal = mapBuildToSignal(build, serviceName);
17871
+ if (signal) out.push(signal);
17872
+ }
17873
+ return out;
17874
+ }
17875
+
17876
+ // src/connectors/eas/resolve.ts
17877
+ init_cjs_shims();
17878
+ var import_types83 = require("@neat.is/types");
17879
+ var NO_ENV2 = "unknown";
17880
+ var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
17881
+ var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
17882
+ "READ_APP_CONFIG",
17883
+ "CONFIGURE_EXPO_UPDATES",
17884
+ "CALCULATE_EXPO_UPDATES_RUNTIME_VERSION"
17885
+ ]);
17886
+ function configBasenamesForPhase(phase) {
17887
+ if (EAS_JSON_PHASES.has(phase)) return ["eas.json"];
17888
+ if (APP_CONFIG_PHASES.has(phase)) return ["app.json", "app.config.json"];
17889
+ return [];
17890
+ }
17891
+ function configNodeService(graph, configNodeId) {
17892
+ for (const edgeId of graph.inboundEdges(configNodeId)) {
17893
+ const edge = graph.getEdgeAttributes(edgeId);
17894
+ if (edge.type !== import_types83.EdgeType.CONFIGURED_BY) continue;
17895
+ const parsed = (0, import_types83.parseFileId)(edge.source);
17896
+ if (parsed) return parsed.service;
17897
+ }
17898
+ return null;
17899
+ }
17900
+ function findConfigNode(graph, basenames, serviceName) {
17901
+ let scoped = null;
17902
+ let anyMatch = null;
17903
+ graph.forEachNode((id, attrs) => {
17904
+ if (scoped) return;
17905
+ const node = attrs;
17906
+ if (node.type !== import_types83.NodeType.ConfigNode) return;
17907
+ if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
17908
+ if (anyMatch === null) anyMatch = id;
17909
+ if (configNodeService(graph, id) === serviceName) scoped = id;
17910
+ });
17911
+ return scoped ?? anyMatch;
17912
+ }
17913
+ function createEasResolveTarget(graph) {
17914
+ return (signal) => {
17915
+ if (signal.targetKind !== EAS_TARGET_KIND) return null;
17916
+ const identity = parseEasTargetName(signal.targetName);
17917
+ if (!identity) return null;
17918
+ const { serviceName, phase } = identity;
17919
+ const basenames = configBasenamesForPhase(phase);
17920
+ if (basenames.length > 0) {
17921
+ const configNodeId = findConfigNode(graph, basenames, serviceName);
17922
+ if (configNodeId) {
17923
+ return { targetNodeId: configNodeId, serviceName, edgeType: import_types83.EdgeType.CALLS };
17924
+ }
17925
+ }
17926
+ return {
17927
+ targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV2),
17928
+ serviceName,
17929
+ edgeType: import_types83.EdgeType.CALLS
17930
+ };
17931
+ };
17932
+ }
17933
+
17934
+ // src/connectors/eas/index.ts
17935
+ function isBuildSince(build, sinceIso) {
17936
+ const t = Date.parse(buildEventTime(build));
17937
+ const s = Date.parse(sinceIso);
17938
+ if (Number.isNaN(t) || Number.isNaN(s)) return true;
17939
+ return t > s;
17940
+ }
17941
+ function boundedSinceIso2(since, now, maxLookbackMs) {
17942
+ const floor = new Date(now.getTime() - maxLookbackMs);
17943
+ if (!since) return floor.toISOString();
17944
+ const sinceMs = new Date(since).getTime();
17945
+ if (Number.isNaN(sinceMs)) return floor.toISOString();
17946
+ return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
17947
+ }
17948
+ var EasConnector = class {
17949
+ constructor(config, fetchImpl) {
17950
+ this.config = config;
17951
+ this.fetchImpl = fetchImpl;
17952
+ }
17953
+ config;
17954
+ fetchImpl;
17955
+ provider = "eas";
17956
+ async poll(ctx) {
17957
+ const creds = readEasCredentials(ctx.credentials);
17958
+ const serviceName = this.config.serviceName ?? this.config.appId;
17959
+ const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS6;
17960
+ const sinceIso = boundedSinceIso2(ctx.since, /* @__PURE__ */ new Date(), maxLookbackMs);
17961
+ const builds = await fetchErroredBuilds(creds.token, this.config, this.fetchImpl);
17962
+ const fresh = builds.filter((b) => isBuildSince(b, sinceIso));
17963
+ const maxLogBytes = this.config.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES;
17964
+ for (const build of fresh) {
17965
+ build.logsText = await fetchBuildLogs(build.logFileUrls, maxLogBytes, this.fetchImpl);
17966
+ }
17967
+ return mapBuildsToSignals(fresh, serviceName);
17968
+ }
17969
+ };
17970
+ function createEasConnector(graph, config, fetchImpl) {
17971
+ return {
17972
+ connector: new EasConnector(config, fetchImpl),
17973
+ resolveTarget: createEasResolveTarget(graph)
17974
+ };
17975
+ }
17976
+
17544
17977
  // src/connectors/registry.ts
17545
17978
  var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
17546
17979
  async function authProbe(input) {
@@ -17828,6 +18261,41 @@ var PROVIDER_DISPATCH = {
17828
18261
  ...fetchImpl ? { fetchImpl } : {}
17829
18262
  });
17830
18263
  }
18264
+ },
18265
+ eas: {
18266
+ provider: "eas",
18267
+ // The secret is a single robot-user EXPO_TOKEN; a single-string credential
18268
+ // maps to `token`. `appId` is non-secret config (connector-config.md §7.1).
18269
+ primaryCredentialKey: "token",
18270
+ requiredCredentialFields: ["token"],
18271
+ requiredOptionFields: ["appId"],
18272
+ build(graph, options) {
18273
+ return createEasConnector(graph, options);
18274
+ },
18275
+ // Runs the connector's real `builds` query at limit 1 — the exact read poll()
18276
+ // performs, minus the pages — so the probe checks both that the EXPO_TOKEN
18277
+ // authenticates and that this app id is reachable, the same probe-the-real-
18278
+ // query discipline Railway and Cloud Run use over a trivial `{ __typename }`.
18279
+ // A bad or wrong-scoped token comes back as an Expo GraphQL error, which
18280
+ // `fetchErroredBuilds` throws on, so it fails honestly here rather than
18281
+ // silently at the first poll.
18282
+ async validate({ credentials, options, fetchImpl }) {
18283
+ const cfg = options;
18284
+ const appId = String(cfg.appId ?? "");
18285
+ if (!appId) return { ok: false, reason: "eas: appId is required to validate" };
18286
+ const probeConfig = {
18287
+ appId,
18288
+ pageSize: 1,
18289
+ maxPages: 1,
18290
+ ...cfg.apiUrl ? { apiUrl: cfg.apiUrl } : {}
18291
+ };
18292
+ try {
18293
+ await fetchErroredBuilds(String(credentials.token ?? ""), probeConfig, fetchImpl);
18294
+ return { ok: true };
18295
+ } catch (err) {
18296
+ return { ok: false, reason: `eas auth check failed: ${err.message}` };
18297
+ }
18298
+ }
17831
18299
  }
17832
18300
  };
17833
18301
  function vercelCredsFrom(credentials) {
@@ -18040,7 +18508,11 @@ async function startConnectorPolling(input) {
18040
18508
  const stopFns = all.map(
18041
18509
  (registration) => startConnectorPollLoop(
18042
18510
  registration.connector,
18043
- { projectDir: input.projectDir, credentials: registration.credentials },
18511
+ {
18512
+ projectDir: input.projectDir,
18513
+ credentials: registration.credentials,
18514
+ ...input.errorsPath ? { errorsPath: input.errorsPath } : {}
18515
+ },
18044
18516
  input.graph,
18045
18517
  registration.resolveTarget,
18046
18518
  { intervalMs: registration.intervalMs, connectorId: registration.id }
@@ -18258,11 +18730,11 @@ function registerRoutes(scope, ctx) {
18258
18730
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
18259
18731
  const parsed = [];
18260
18732
  for (const c of candidates) {
18261
- const r = import_types81.DivergenceTypeSchema.safeParse(c);
18733
+ const r = import_types86.DivergenceTypeSchema.safeParse(c);
18262
18734
  if (!r.success) {
18263
18735
  return reply.code(400).send({
18264
18736
  error: `unknown divergence type "${c}"`,
18265
- allowed: import_types81.DivergenceTypeSchema.options
18737
+ allowed: import_types86.DivergenceTypeSchema.options
18266
18738
  });
18267
18739
  }
18268
18740
  parsed.push(r.data);
@@ -18369,10 +18841,15 @@ function registerRoutes(scope, ctx) {
18369
18841
  }
18370
18842
  const reg = built.registration;
18371
18843
  const at = (/* @__PURE__ */ new Date()).toISOString();
18844
+ const incidentsPath = errorsPathFor(proj);
18372
18845
  try {
18373
18846
  const result = await ctx.runPoll(
18374
18847
  reg.connector,
18375
- { projectDir: proj.scanPath ?? "", credentials: reg.credentials },
18848
+ {
18849
+ projectDir: proj.scanPath ?? "",
18850
+ credentials: reg.credentials,
18851
+ ...incidentsPath ? { errorsPath: incidentsPath } : {}
18852
+ },
18376
18853
  proj.graph,
18377
18854
  reg.resolveTarget
18378
18855
  );
@@ -18571,7 +19048,7 @@ function registerRoutes(scope, ctx) {
18571
19048
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
18572
19049
  let violations = await log.readAll();
18573
19050
  if (req.query.severity) {
18574
- const sev = import_types81.PolicySeveritySchema.safeParse(req.query.severity);
19051
+ const sev = import_types86.PolicySeveritySchema.safeParse(req.query.severity);
18575
19052
  if (!sev.success) {
18576
19053
  return reply.code(400).send({
18577
19054
  error: "invalid severity",
@@ -18610,7 +19087,7 @@ function registerRoutes(scope, ctx) {
18610
19087
  scope.post("/policies/check", async (req, reply) => {
18611
19088
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
18612
19089
  if (!proj) return;
18613
- const parsed = import_types81.PoliciesCheckBodySchema.safeParse(req.body ?? {});
19090
+ const parsed = import_types86.PoliciesCheckBodySchema.safeParse(req.body ?? {});
18614
19091
  if (!parsed.success) {
18615
19092
  return reply.code(400).send({
18616
19093
  error: "invalid /policies/check body",
@@ -18943,7 +19420,7 @@ var import_node_fs34 = require("fs");
18943
19420
  var import_node_path68 = __toESM(require("path"), 1);
18944
19421
 
18945
19422
  // src/daemon.ts
18946
- var import_types82 = require("@neat.is/types");
19423
+ var import_types87 = require("@neat.is/types");
18947
19424
  function daemonJsonPath(scanPath) {
18948
19425
  return import_node_path69.default.join(scanPath, "neat-out", "daemon.json");
18949
19426
  }
@@ -19601,6 +20078,9 @@ async function startWatch(graph, opts) {
19601
20078
  project: projectName,
19602
20079
  graph,
19603
20080
  projectDir: opts.scanPath,
20081
+ // Incident ledger for an incident-emitting connector (ADR-185), same path
20082
+ // `neat watch`'s own error-span writer already uses.
20083
+ errorsPath: opts.errorsPath,
19604
20084
  ...opts.neatHome ? { home: opts.neatHome } : {},
19605
20085
  onSkip: (skipped, reason) => console.warn(
19606
20086
  `neat watch: connector "${skipped.id}" (${skipped.provider}) skipped for project "${projectName}" \u2014 ${reason}`
@@ -22229,14 +22709,519 @@ async function apply3(installPlan) {
22229
22709
  await import_node_fs41.promises.writeFile(generated.file, generated.contents, "utf8");
22230
22710
  writtenFiles.push(generated.file);
22231
22711
  }
22232
- return { serviceDir: installPlan.serviceDir, outcome: writtenFiles.length ? "instrumented" : "already-instrumented", writtenFiles };
22712
+ const wroteManifest = writtenFiles.some((f) => f.split(/[\\/]/).pop() === "go.mod");
22713
+ return {
22714
+ serviceDir: installPlan.serviceDir,
22715
+ outcome: writtenFiles.length ? "instrumented" : "already-instrumented",
22716
+ writtenFiles,
22717
+ ...wroteManifest ? { followUpInstall: "go mod download" } : {}
22718
+ };
22233
22719
  }
22234
22720
  var goInstaller = { name: "go", detect: detect3, plan: plan3, apply: apply3 };
22235
22721
 
22722
+ // src/installers/ruby.ts
22723
+ init_cjs_shims();
22724
+ var import_node_fs42 = require("fs");
22725
+ var import_node_path76 = __toESM(require("path"), 1);
22726
+ var RUBY_MARKERS = [
22727
+ "Gemfile",
22728
+ "Gemfile.lock"
22729
+ ];
22730
+ var RUBY_GEMS = [
22731
+ { name: "opentelemetry-sdk", version: "~> 1.5" },
22732
+ { name: "opentelemetry-exporter-otlp", version: "~> 0.29" },
22733
+ { name: "opentelemetry-instrumentation-all", version: "~> 0.62" }
22734
+ ];
22735
+ var NEAT_OTEL_STAMP2 = "neat-otel-init v1";
22736
+ var INITIALIZER_REL = import_node_path76.default.join("config", "initializers", "neat_otel.rb");
22737
+ function neatOtelRb(opts = {}) {
22738
+ const service = opts.project ?? "ruby-service";
22739
+ const endpoint2 = opts.project ? `http://localhost:4318/projects/${opts.project}/v1/traces` : "http://localhost:4318/v1/traces";
22740
+ return `# ${NEAT_OTEL_STAMP2} \u2014 generated by NEAT. Safe to re-generate; do not edit.
22741
+ # Rails auto-loads this at boot (config/initializers/*). It points the
22742
+ # OpenTelemetry SDK at your NEAT daemon, enables the Ruby auto-instrumentation
22743
+ # set, and installs a span processor that stamps code.file.path /
22744
+ # code.line.number / code.function.name on the CLIENT/PRODUCER spans your app
22745
+ # issues, so NEAT fuses each runtime span onto the source file that made the
22746
+ # call (docs/contracts/file-awareness.md). Absolute paths are emitted here;
22747
+ # ingest anchors them against the service root. If the OpenTelemetry gems are
22748
+ # not installed this file degrades to a no-op rather than breaking boot.
22749
+
22750
+ begin
22751
+ require 'opentelemetry/sdk'
22752
+ require 'opentelemetry/exporter/otlp'
22753
+ require 'opentelemetry/instrumentation/all'
22754
+ _neat_otel_loaded = true
22755
+ rescue LoadError
22756
+ _neat_otel_loaded = false
22757
+ end
22758
+
22759
+ if _neat_otel_loaded && ENV['NEAT_CALLSITE_DISABLED'] != '1'
22760
+ # Walk the Ruby call stack to the first application frame and stamp the stable
22761
+ # OTel source attributes on CLIENT/PRODUCER spans. SERVER spans are created
22762
+ # before the handler runs, so they stay route/service-grained, honestly.
22763
+ class NeatCallSiteSpanProcessor
22764
+ def initialize(root)
22765
+ @root = root.to_s.end_with?(File::SEPARATOR) ? root.to_s : root.to_s + File::SEPARATOR
22766
+ end
22767
+
22768
+ def on_start(span, _parent_context)
22769
+ kind = span.kind
22770
+ return unless kind == OpenTelemetry::Trace::SpanKind::CLIENT ||
22771
+ kind == OpenTelemetry::Trace::SpanKind::PRODUCER
22772
+ caller_locations(1).each do |loc|
22773
+ file = loc.absolute_path || loc.path
22774
+ next if file.nil?
22775
+ next unless file.start_with?(@root)
22776
+ next if file.include?('/vendor/') || file.include?('/.bundle/')
22777
+ next if file.end_with?('neat_otel.rb')
22778
+ span.set_attribute('code.file.path', file)
22779
+ span.set_attribute('code.line.number', loc.lineno)
22780
+ span.set_attribute('code.function.name', loc.label.to_s)
22781
+ break
22782
+ end
22783
+ rescue StandardError
22784
+ # never break the host application
22785
+ end
22786
+
22787
+ def on_finish(_span); end
22788
+
22789
+ def force_flush(timeout: nil)
22790
+ OpenTelemetry::SDK::Trace::Export::SUCCESS
22791
+ end
22792
+
22793
+ def shutdown(timeout: nil)
22794
+ OpenTelemetry::SDK::Trace::Export::SUCCESS
22795
+ end
22796
+ end
22797
+
22798
+ _neat_root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
22799
+ _neat_service = ENV.fetch('OTEL_SERVICE_NAME', '${service}')
22800
+ _neat_endpoint = ENV.fetch('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', '${endpoint2}')
22801
+
22802
+ OpenTelemetry::SDK.configure do |c|
22803
+ c.service_name = _neat_service
22804
+ c.use_all
22805
+ c.add_span_processor(
22806
+ OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
22807
+ OpenTelemetry::Exporter::OTLP::Exporter.new(endpoint: _neat_endpoint)
22808
+ )
22809
+ )
22810
+ c.add_span_processor(NeatCallSiteSpanProcessor.new(_neat_root))
22811
+ end
22812
+ end
22813
+ `;
22814
+ }
22815
+ async function exists6(p) {
22816
+ try {
22817
+ await import_node_fs42.promises.stat(p);
22818
+ return true;
22819
+ } catch {
22820
+ return false;
22821
+ }
22822
+ }
22823
+ async function detect4(serviceDir) {
22824
+ for (const marker of RUBY_MARKERS) {
22825
+ if (await exists6(import_node_path76.default.join(serviceDir, marker))) return true;
22826
+ }
22827
+ return false;
22828
+ }
22829
+ async function isRailsApp(serviceDir, gemfile) {
22830
+ if (gemfile && /^\s*gem\s+['"]rails['"]/m.test(gemfile)) return true;
22831
+ for (const marker of ["config/application.rb", "config/environment.rb", "bin/rails"]) {
22832
+ if (await exists6(import_node_path76.default.join(serviceDir, marker))) return true;
22833
+ }
22834
+ return false;
22835
+ }
22836
+ function gemPresent(gemfile, name) {
22837
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22838
+ return new RegExp(`^\\s*gem\\s+['"]${escaped}['"]`, "m").test(gemfile);
22839
+ }
22840
+ async function readGemfile(serviceDir) {
22841
+ const file = import_node_path76.default.join(serviceDir, "Gemfile");
22842
+ if (!await exists6(file)) return null;
22843
+ return { file, body: await import_node_fs42.promises.readFile(file, "utf8") };
22844
+ }
22845
+ async function plan4(serviceDir, opts) {
22846
+ const empty = {
22847
+ language: "ruby",
22848
+ serviceDir,
22849
+ dependencyEdits: [],
22850
+ entrypointEdits: [],
22851
+ envEdits: []
22852
+ };
22853
+ const gemfile = await readGemfile(serviceDir);
22854
+ const dependencyEdits = [];
22855
+ if (gemfile) {
22856
+ for (const gem of RUBY_GEMS) {
22857
+ if (!gemPresent(gemfile.body, gem.name)) {
22858
+ dependencyEdits.push({ file: gemfile.file, kind: "add", name: gem.name, version: gem.version });
22859
+ }
22860
+ }
22861
+ }
22862
+ const rails = await isRailsApp(serviceDir, gemfile?.body ?? null);
22863
+ const initializer = import_node_path76.default.join(serviceDir, INITIALIZER_REL);
22864
+ const generatedFiles = [];
22865
+ if (rails && !await exists6(initializer)) {
22866
+ generatedFiles.push({
22867
+ file: initializer,
22868
+ contents: neatOtelRb({ project: opts?.project }),
22869
+ skipIfExists: true
22870
+ });
22871
+ }
22872
+ if (dependencyEdits.length === 0 && generatedFiles.length === 0) {
22873
+ return empty;
22874
+ }
22875
+ const envEdits = [
22876
+ { file: null, key: "OTEL_EXPORTER_OTLP_ENDPOINT", value: "http://localhost:4318" }
22877
+ ];
22878
+ return {
22879
+ language: "ruby",
22880
+ serviceDir,
22881
+ dependencyEdits,
22882
+ entrypointEdits: [],
22883
+ envEdits,
22884
+ ...generatedFiles.length > 0 ? { generatedFiles } : {}
22885
+ };
22886
+ }
22887
+ async function writeFileAtomic2(file, contents) {
22888
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
22889
+ await import_node_fs42.promises.writeFile(tmp, contents, "utf8");
22890
+ await import_node_fs42.promises.rename(tmp, file);
22891
+ }
22892
+ async function applyGemfile(file, edits, original) {
22893
+ const lines = edits.filter((e) => e.kind === "add").map((e) => `gem '${e.name}', '${e.version}'`);
22894
+ const banner = `
22895
+ # ${NEAT_OTEL_STAMP2} \u2014 OpenTelemetry gems added by NEAT
22896
+ `;
22897
+ const trailing = original.endsWith("\n") ? "" : "\n";
22898
+ await writeFileAtomic2(file, `${original}${trailing}${banner}${lines.join("\n")}
22899
+ `);
22900
+ }
22901
+ async function rollback3(serviceDir, language, originals, created) {
22902
+ const restored = [];
22903
+ for (const [file, raw] of originals.entries()) {
22904
+ try {
22905
+ await import_node_fs42.promises.writeFile(file, raw, "utf8");
22906
+ restored.push(file);
22907
+ } catch {
22908
+ }
22909
+ }
22910
+ const removed = [];
22911
+ for (const file of created) {
22912
+ try {
22913
+ await import_node_fs42.promises.rm(file, { force: true });
22914
+ removed.push(file);
22915
+ } catch {
22916
+ }
22917
+ }
22918
+ const body = [
22919
+ "# neat-rollback.patch",
22920
+ "",
22921
+ `# Generated after a partial apply failure in the ${language} installer.`,
22922
+ "# Files listed below were restored to their pre-apply contents.",
22923
+ "",
22924
+ ...restored.map((f) => `restored: ${f}`),
22925
+ ...removed.map((f) => `removed: ${f}`),
22926
+ ""
22927
+ ];
22928
+ await import_node_fs42.promises.writeFile(import_node_path76.default.join(serviceDir, "neat-rollback.patch"), body.join("\n"), "utf8");
22929
+ }
22930
+ async function apply4(installPlan) {
22931
+ const { serviceDir } = installPlan;
22932
+ const generatedFiles = installPlan.generatedFiles ?? [];
22933
+ const manifests = new Set(installPlan.dependencyEdits.map((e) => e.file));
22934
+ if (manifests.size === 0 && generatedFiles.length === 0) {
22935
+ return { serviceDir, outcome: "already-instrumented", writtenFiles: [] };
22936
+ }
22937
+ const originals = /* @__PURE__ */ new Map();
22938
+ for (const file of manifests) {
22939
+ try {
22940
+ originals.set(file, await import_node_fs42.promises.readFile(file, "utf8"));
22941
+ } catch {
22942
+ }
22943
+ }
22944
+ const writtenFiles = [];
22945
+ const created = [];
22946
+ try {
22947
+ for (const gf of generatedFiles) {
22948
+ if (await exists6(gf.file)) continue;
22949
+ await import_node_fs42.promises.mkdir(import_node_path76.default.dirname(gf.file), { recursive: true });
22950
+ await writeFileAtomic2(gf.file, gf.contents);
22951
+ writtenFiles.push(gf.file);
22952
+ created.push(gf.file);
22953
+ }
22954
+ for (const file of manifests) {
22955
+ const raw = originals.get(file);
22956
+ if (raw === void 0) throw new Error(`ruby installer: cannot read ${file} during apply`);
22957
+ const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
22958
+ if (edits.length > 0) {
22959
+ await applyGemfile(file, edits, raw);
22960
+ writtenFiles.push(file);
22961
+ }
22962
+ }
22963
+ } catch (err) {
22964
+ await rollback3(serviceDir, installPlan.language, originals, created);
22965
+ throw err;
22966
+ }
22967
+ const wroteManifest = writtenFiles.some((f) => import_node_path76.default.basename(f) === "Gemfile");
22968
+ return {
22969
+ serviceDir,
22970
+ outcome: writtenFiles.length > 0 ? "instrumented" : "already-instrumented",
22971
+ writtenFiles,
22972
+ ...wroteManifest ? { followUpInstall: "bundle install" } : {}
22973
+ };
22974
+ }
22975
+ var rubyInstaller = { name: "ruby", detect: detect4, plan: plan4, apply: apply4 };
22976
+
22977
+ // src/installers/php.ts
22978
+ init_cjs_shims();
22979
+ var import_node_fs43 = require("fs");
22980
+ var import_node_path77 = __toESM(require("path"), 1);
22981
+ var PHP_MARKERS = [
22982
+ "composer.json",
22983
+ "composer.lock"
22984
+ ];
22985
+ var NEAT_OTEL_FILENAME2 = "neat_otel.php";
22986
+ var NEAT_OTEL_STAMP3 = "neat-otel-init v1";
22987
+ var LARAVEL_PACKAGE = "open-telemetry/opentelemetry-auto-laravel";
22988
+ var PHP_PACKAGES = [
22989
+ { name: "open-telemetry/sdk", version: "^1.0" },
22990
+ { name: "open-telemetry/exporter-otlp", version: "^1.0" },
22991
+ { name: "php-http/guzzle7-adapter", version: "^1.0" },
22992
+ { name: LARAVEL_PACKAGE, version: "^0.1" }
22993
+ ];
22994
+ var PHP_PECL_CAVEAT = "PHP auto-instrumentation requires the `opentelemetry` PECL extension (`pecl install opentelemetry`, then `extension=opentelemetry.so` in php.ini). NEAT cannot install a PECL extension via composer \u2014 until it is loaded no spans are produced. See neat_otel.php and ADR-186.";
22995
+ function neatOtelPhp(opts = {}) {
22996
+ const service = opts.project ?? "php-service";
22997
+ const endpoint2 = opts.project ? `http://localhost:4318/projects/${opts.project}/v1/traces` : "http://localhost:4318/v1/traces";
22998
+ return `<?php
22999
+ // ${NEAT_OTEL_STAMP3} \u2014 generated by NEAT. Safe to re-generate; do not edit.
23000
+ //
23001
+ // REQUIRED SYSTEM STEP \u2014 NEAT CANNOT DO THIS FOR YOU:
23002
+ // PHP OpenTelemetry auto-instrumentation needs the \`opentelemetry\` PECL
23003
+ // extension, a system-level install composer cannot provide:
23004
+ // pecl install opentelemetry
23005
+ // then enable it in your php.ini:
23006
+ // extension=opentelemetry.so
23007
+ // Verify with \`php -m | grep opentelemetry\`. Until the extension is loaded
23008
+ // the Laravel auto-instrumentation hooks never fire and no spans are emitted.
23009
+ //
23010
+ // Wire this file so it runs before the framework boots \u2014 either set
23011
+ // auto_prepend_file = /absolute/path/to/neat_otel.php
23012
+ // in php.ini / .user.ini, or require it at the very top of public/index.php and
23013
+ // artisan. It points the exporter at your NEAT daemon and turns on the SDK
23014
+ // autoloader; the auto-laravel instrumentation then produces route, DB, cache,
23015
+ // and queue spans that fuse onto your extracted routes and Eloquent tables.
23016
+ //
23017
+ // FILE-GRAIN (code.file.path call-site attribution) is a documented follow-up
23018
+ // for PHP \u2014 see ADR-186. Route, table, and service grain land now.
23019
+
23020
+ declare(strict_types=1);
23021
+
23022
+ // Degrade to a no-op when the extension isn't present, so a bare app still
23023
+ // boots (never break the host application).
23024
+ if (!extension_loaded('opentelemetry')) {
23025
+ return;
23026
+ }
23027
+
23028
+ // Point the exporter at NEAT unless the operator already set these. The
23029
+ // endpoint is NEAT's project-scoped traces path (ADR-183).
23030
+ $neat_defaults = [
23031
+ 'OTEL_PHP_AUTOLOAD_ENABLED' => 'true',
23032
+ 'OTEL_SERVICE_NAME' => '${service}',
23033
+ 'OTEL_TRACES_EXPORTER' => 'otlp',
23034
+ 'OTEL_EXPORTER_OTLP_PROTOCOL' => 'http/json',
23035
+ 'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT' => '${endpoint2}',
23036
+ 'OTEL_PROPAGATORS' => 'baggage,tracecontext',
23037
+ ];
23038
+ foreach ($neat_defaults as $neat_key => $neat_value) {
23039
+ if (getenv($neat_key) === false && !isset($_SERVER[$neat_key]) && !isset($_ENV[$neat_key])) {
23040
+ putenv($neat_key . '=' . $neat_value);
23041
+ $_SERVER[$neat_key] = $neat_value;
23042
+ $_ENV[$neat_key] = $neat_value;
23043
+ }
23044
+ }
23045
+ `;
23046
+ }
23047
+ async function exists7(p) {
23048
+ try {
23049
+ await import_node_fs43.promises.stat(p);
23050
+ return true;
23051
+ } catch {
23052
+ return false;
23053
+ }
23054
+ }
23055
+ async function detect5(serviceDir) {
23056
+ for (const marker of PHP_MARKERS) {
23057
+ if (await exists7(import_node_path77.default.join(serviceDir, marker))) return true;
23058
+ }
23059
+ return false;
23060
+ }
23061
+ function readComposerObject(body) {
23062
+ let parsed = null;
23063
+ try {
23064
+ parsed = JSON.parse(body);
23065
+ } catch {
23066
+ parsed = null;
23067
+ }
23068
+ const obj = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
23069
+ const require2 = obj.require && typeof obj.require === "object" && !Array.isArray(obj.require) ? obj.require : {};
23070
+ const requireDev = obj["require-dev"] && typeof obj["require-dev"] === "object" && !Array.isArray(obj["require-dev"]) ? obj["require-dev"] : {};
23071
+ return { require: require2, requireDev };
23072
+ }
23073
+ async function plan5(serviceDir, opts) {
23074
+ const empty = {
23075
+ language: "php",
23076
+ serviceDir,
23077
+ dependencyEdits: [],
23078
+ entrypointEdits: [],
23079
+ envEdits: []
23080
+ };
23081
+ const composerPath = import_node_path77.default.join(serviceDir, "composer.json");
23082
+ const hasComposer = await exists7(composerPath);
23083
+ const dependencyEdits = [];
23084
+ if (hasComposer) {
23085
+ const body = await import_node_fs43.promises.readFile(composerPath, "utf8");
23086
+ const { require: require2, requireDev } = readComposerObject(body);
23087
+ const laravel = "laravel/framework" in require2 || "laravel/framework" in requireDev || await exists7(import_node_path77.default.join(serviceDir, "artisan"));
23088
+ const wanted = laravel ? PHP_PACKAGES : PHP_PACKAGES.filter((p) => p.name !== LARAVEL_PACKAGE);
23089
+ for (const pkg of wanted) {
23090
+ if (!(pkg.name in require2)) {
23091
+ dependencyEdits.push({ file: composerPath, kind: "add", name: pkg.name, version: pkg.version });
23092
+ }
23093
+ }
23094
+ }
23095
+ const bootstrap = import_node_path77.default.join(serviceDir, NEAT_OTEL_FILENAME2);
23096
+ const generatedFiles = [];
23097
+ if (hasComposer && !await exists7(bootstrap)) {
23098
+ generatedFiles.push({ file: bootstrap, contents: neatOtelPhp({ project: opts?.project }), skipIfExists: true });
23099
+ }
23100
+ if (dependencyEdits.length === 0 && generatedFiles.length === 0) {
23101
+ return empty;
23102
+ }
23103
+ const envEdits = [
23104
+ { file: null, key: "OTEL_PHP_AUTOLOAD_ENABLED", value: "true" },
23105
+ { file: null, key: "OTEL_EXPORTER_OTLP_ENDPOINT", value: "http://localhost:4318" }
23106
+ ];
23107
+ return {
23108
+ language: "php",
23109
+ serviceDir,
23110
+ dependencyEdits,
23111
+ entrypointEdits: [],
23112
+ envEdits,
23113
+ ...generatedFiles.length > 0 ? { generatedFiles } : {}
23114
+ };
23115
+ }
23116
+ async function writeFileAtomic3(file, contents) {
23117
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
23118
+ await import_node_fs43.promises.writeFile(tmp, contents, "utf8");
23119
+ await import_node_fs43.promises.rename(tmp, file);
23120
+ }
23121
+ async function applyComposerJson(file, edits, original) {
23122
+ let parsed;
23123
+ try {
23124
+ parsed = JSON.parse(original);
23125
+ } catch {
23126
+ throw new Error(`php installer: composer.json at ${file} is not valid JSON`);
23127
+ }
23128
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
23129
+ throw new Error(`php installer: composer.json at ${file} is not a JSON object`);
23130
+ }
23131
+ const obj = parsed;
23132
+ const require2 = obj.require && typeof obj.require === "object" && !Array.isArray(obj.require) ? obj.require : {};
23133
+ for (const e of edits) {
23134
+ if (e.kind !== "add") continue;
23135
+ if (!(e.name in require2)) require2[e.name] = e.version;
23136
+ }
23137
+ obj.require = require2;
23138
+ await writeFileAtomic3(file, JSON.stringify(obj, null, 2) + "\n");
23139
+ }
23140
+ async function rollback4(serviceDir, language, originals, created) {
23141
+ const restored = [];
23142
+ for (const [file, raw] of originals.entries()) {
23143
+ try {
23144
+ await import_node_fs43.promises.writeFile(file, raw, "utf8");
23145
+ restored.push(file);
23146
+ } catch {
23147
+ }
23148
+ }
23149
+ const removed = [];
23150
+ for (const file of created) {
23151
+ try {
23152
+ await import_node_fs43.promises.rm(file, { force: true });
23153
+ removed.push(file);
23154
+ } catch {
23155
+ }
23156
+ }
23157
+ const body = [
23158
+ "# neat-rollback.patch",
23159
+ "",
23160
+ `# Generated after a partial apply failure in the ${language} installer.`,
23161
+ "# Files listed below were restored to their pre-apply contents.",
23162
+ "",
23163
+ ...restored.map((f) => `restored: ${f}`),
23164
+ ...removed.map((f) => `removed: ${f}`),
23165
+ ""
23166
+ ];
23167
+ await import_node_fs43.promises.writeFile(import_node_path77.default.join(serviceDir, "neat-rollback.patch"), body.join("\n"), "utf8");
23168
+ }
23169
+ async function apply5(installPlan) {
23170
+ const { serviceDir } = installPlan;
23171
+ const generatedFiles = installPlan.generatedFiles ?? [];
23172
+ const manifests = new Set(installPlan.dependencyEdits.map((e) => e.file));
23173
+ if (manifests.size === 0 && generatedFiles.length === 0) {
23174
+ return { serviceDir, outcome: "already-instrumented", writtenFiles: [], reason: PHP_PECL_CAVEAT };
23175
+ }
23176
+ const originals = /* @__PURE__ */ new Map();
23177
+ for (const file of manifests) {
23178
+ try {
23179
+ originals.set(file, await import_node_fs43.promises.readFile(file, "utf8"));
23180
+ } catch {
23181
+ }
23182
+ }
23183
+ const writtenFiles = [];
23184
+ const created = [];
23185
+ try {
23186
+ for (const gf of generatedFiles) {
23187
+ if (await exists7(gf.file)) continue;
23188
+ await import_node_fs43.promises.mkdir(import_node_path77.default.dirname(gf.file), { recursive: true });
23189
+ await writeFileAtomic3(gf.file, gf.contents);
23190
+ writtenFiles.push(gf.file);
23191
+ created.push(gf.file);
23192
+ }
23193
+ for (const file of manifests) {
23194
+ const raw = originals.get(file);
23195
+ if (raw === void 0) throw new Error(`php installer: cannot read ${file} during apply`);
23196
+ const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
23197
+ if (edits.length > 0) {
23198
+ await applyComposerJson(file, edits, raw);
23199
+ writtenFiles.push(file);
23200
+ }
23201
+ }
23202
+ } catch (err) {
23203
+ await rollback4(serviceDir, installPlan.language, originals, created);
23204
+ throw err;
23205
+ }
23206
+ if (writtenFiles.length > 0) {
23207
+ console.warn(`neat: PHP instrumentation staged in ${import_node_path77.default.basename(serviceDir)}, but a system step remains:
23208
+ ${PHP_PECL_CAVEAT}`);
23209
+ }
23210
+ const wroteManifest = writtenFiles.some((f) => import_node_path77.default.basename(f) === "composer.json");
23211
+ return {
23212
+ serviceDir,
23213
+ outcome: writtenFiles.length > 0 ? "instrumented" : "already-instrumented",
23214
+ writtenFiles,
23215
+ reason: PHP_PECL_CAVEAT,
23216
+ ...wroteManifest ? { followUpInstall: "composer install" } : {}
23217
+ };
23218
+ }
23219
+ var phpInstaller = { name: "php", detect: detect5, plan: plan5, apply: apply5 };
23220
+
22236
23221
  // src/installers/shared.ts
22237
23222
  init_cjs_shims();
22238
- function isEmptyPlan(plan4) {
22239
- return plan4.dependencyEdits.length === 0 && plan4.entrypointEdits.length === 0 && plan4.envEdits.length === 0 && (plan4.generatedFiles?.length ?? 0) === 0 && plan4.nextConfigEdit === void 0;
23223
+ function isEmptyPlan(plan6) {
23224
+ return plan6.dependencyEdits.length === 0 && plan6.entrypointEdits.length === 0 && plan6.envEdits.length === 0 && (plan6.generatedFiles?.length ?? 0) === 0 && plan6.nextConfigEdit === void 0;
22240
23225
  }
22241
23226
 
22242
23227
  // src/installers/index.ts
@@ -22248,9 +23233,16 @@ var FORBIDDEN_LOCKFILES = /* @__PURE__ */ new Set([
22248
23233
  "Pipfile.lock",
22249
23234
  "Gemfile.lock",
22250
23235
  "Cargo.lock",
22251
- "go.sum"
23236
+ "go.sum",
23237
+ "composer.lock"
22252
23238
  ]);
22253
- var INSTALLERS = [javascriptInstaller, pythonInstaller, goInstaller];
23239
+ var INSTALLERS = [
23240
+ javascriptInstaller,
23241
+ pythonInstaller,
23242
+ goInstaller,
23243
+ rubyInstaller,
23244
+ phpInstaller
23245
+ ];
22254
23246
  async function pickInstaller(serviceDir) {
22255
23247
  for (const inst of INSTALLERS) {
22256
23248
  if (await inst.detect(serviceDir)) return inst;
@@ -22265,7 +23257,7 @@ function renderPatch(sections) {
22265
23257
  "No SDK installers matched the discovered services. Two reasons this",
22266
23258
  "normally happens:",
22267
23259
  " - the project uses a language NEAT does not yet instrument",
22268
- " (Java / Ruby / .NET / Rust are out of MVP scope per ADR-047);",
23260
+ " (Java / .NET / Rust are out of scope per ADR-047);",
22269
23261
  " - the SDK is already installed, so the installer returned an empty",
22270
23262
  " plan.",
22271
23263
  "",
@@ -22275,22 +23267,22 @@ function renderPatch(sections) {
22275
23267
  }
22276
23268
  const lines = ["# neat install plan", ""];
22277
23269
  for (const section of sections) {
22278
- const { installer, plan: plan4 } = section;
22279
- lines.push(`## ${installer} (${plan4.language}) \u2014 ${plan4.serviceDir}`);
23270
+ const { installer, plan: plan6 } = section;
23271
+ lines.push(`## ${installer} (${plan6.language}) \u2014 ${plan6.serviceDir}`);
22280
23272
  lines.push("");
22281
- if (plan4.libOnly) {
23273
+ if (plan6.libOnly) {
22282
23274
  lines.push("### skipped \u2014 no resolvable entry point (lib-only)");
22283
23275
  lines.push("");
22284
23276
  continue;
22285
23277
  }
22286
- if (plan4.entryFile) {
22287
- lines.push(`entry: ${plan4.entryFile}`);
23278
+ if (plan6.entryFile) {
23279
+ lines.push(`entry: ${plan6.entryFile}`);
22288
23280
  lines.push("");
22289
23281
  }
22290
- if (plan4.dependencyEdits.length > 0) {
23282
+ if (plan6.dependencyEdits.length > 0) {
22291
23283
  lines.push("### dependencies");
22292
23284
  const byFile = /* @__PURE__ */ new Map();
22293
- for (const dep of plan4.dependencyEdits) {
23285
+ for (const dep of plan6.dependencyEdits) {
22294
23286
  const base = dep.file.split(/[\\/]/).pop() ?? dep.file;
22295
23287
  if (FORBIDDEN_LOCKFILES.has(base)) {
22296
23288
  throw new Error(
@@ -22309,9 +23301,9 @@ function renderPatch(sections) {
22309
23301
  }
22310
23302
  lines.push("");
22311
23303
  }
22312
- if (plan4.generatedFiles && plan4.generatedFiles.length > 0) {
23304
+ if (plan6.generatedFiles && plan6.generatedFiles.length > 0) {
22313
23305
  lines.push("### generated files");
22314
- for (const gen of plan4.generatedFiles) {
23306
+ for (const gen of plan6.generatedFiles) {
22315
23307
  lines.push(`--- (new file) ${gen.file}`);
22316
23308
  for (const ln of gen.contents.split(/\r?\n/)) {
22317
23309
  lines.push(`+ ${ln}`);
@@ -22319,26 +23311,26 @@ function renderPatch(sections) {
22319
23311
  }
22320
23312
  lines.push("");
22321
23313
  }
22322
- if (plan4.entrypointEdits.length > 0) {
23314
+ if (plan6.entrypointEdits.length > 0) {
22323
23315
  lines.push("### entry-point injection");
22324
- for (const e of plan4.entrypointEdits) {
23316
+ for (const e of plan6.entrypointEdits) {
22325
23317
  lines.push(`--- ${e.file}`);
22326
23318
  lines.push(`+ ${e.after}`);
22327
23319
  lines.push(` ${e.before}`);
22328
23320
  }
22329
23321
  lines.push("");
22330
23322
  }
22331
- if (plan4.envEdits.length > 0) {
23323
+ if (plan6.envEdits.length > 0) {
22332
23324
  lines.push("### env (written to <package-dir>/.env.neat)");
22333
- for (const env of plan4.envEdits) {
23325
+ for (const env of plan6.envEdits) {
22334
23326
  lines.push(`- ${env.key}=${env.value}`);
22335
23327
  }
22336
23328
  lines.push("");
22337
23329
  }
22338
- if (plan4.nextConfigEdit) {
23330
+ if (plan6.nextConfigEdit) {
22339
23331
  lines.push("### next.config (framework flag)");
22340
- lines.push(`--- ${plan4.nextConfigEdit.file}`);
22341
- lines.push(`+ experimental: { instrumentationHook: true }, // ${plan4.nextConfigEdit.reason}`);
23332
+ lines.push(`--- ${plan6.nextConfigEdit.file}`);
23333
+ lines.push(`+ experimental: { instrumentationHook: true }, // ${plan6.nextConfigEdit.reason}`);
22342
23334
  lines.push("");
22343
23335
  }
22344
23336
  }
@@ -22347,10 +23339,10 @@ function renderPatch(sections) {
22347
23339
 
22348
23340
  // src/orchestrator.ts
22349
23341
  init_cjs_shims();
22350
- var import_node_fs42 = require("fs");
23342
+ var import_node_fs44 = require("fs");
22351
23343
  var import_node_http = __toESM(require("http"), 1);
22352
23344
  var import_node_net = __toESM(require("net"), 1);
22353
- var import_node_path76 = __toESM(require("path"), 1);
23345
+ var import_node_path78 = __toESM(require("path"), 1);
22354
23346
  var import_node_url4 = require("url");
22355
23347
  var import_node_child_process3 = require("child_process");
22356
23348
  var import_node_readline = __toESM(require("readline"), 1);
@@ -22360,7 +23352,7 @@ async function extractAndPersist(opts) {
22360
23352
  const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
22361
23353
  resetGraph(graphKey);
22362
23354
  const graph = getGraph(graphKey);
22363
- const projectPaths = pathsForProject(graphKey, import_node_path76.default.join(opts.scanPath, "neat-out"));
23355
+ const projectPaths = pathsForProject(graphKey, import_node_path78.default.join(opts.scanPath, "neat-out"));
22364
23356
  const extraction = await extractFromDirectory(graph, opts.scanPath, {
22365
23357
  errorsPath: projectPaths.errorsPath
22366
23358
  });
@@ -22391,28 +23383,34 @@ async function applyInstallersOver(services, project, options = {}) {
22391
23383
  let cloudflareWorkers = 0;
22392
23384
  let electron = 0;
22393
23385
  const installPlans = /* @__PURE__ */ new Map();
23386
+ const dependencyInstructions = /* @__PURE__ */ new Map();
22394
23387
  for (const svc of services) {
22395
23388
  const installer = await pickInstaller(svc.dir);
22396
23389
  if (!installer) continue;
22397
- const plan4 = await installer.plan(svc.dir, { project });
22398
- if (isEmptyPlan(plan4) && !plan4.libOnly && plan4.runtimeKind === void 0) {
23390
+ const plan6 = await installer.plan(svc.dir, { project });
23391
+ if (isEmptyPlan(plan6) && !plan6.libOnly && plan6.runtimeKind === void 0) {
22399
23392
  already++;
22400
23393
  continue;
22401
23394
  }
22402
- const outcome = await installer.apply(plan4);
23395
+ const outcome = await installer.apply(plan6);
22403
23396
  if (outcome.outcome === "instrumented") {
22404
23397
  instrumented++;
22405
- if (plan4.dependencyEdits.length > 0) {
22406
- const cmd = await resolveManager(svc.dir);
22407
- const key = `${cmd.pm}:${cmd.cwd}`;
22408
- if (!installPlans.has(key)) installPlans.set(key, cmd);
23398
+ if (plan6.dependencyEdits.length > 0) {
23399
+ const manifest = import_node_path78.default.basename(plan6.dependencyEdits[0].file);
23400
+ if (manifest === "package.json") {
23401
+ const cmd = await resolveManager(svc.dir);
23402
+ const key = `${cmd.pm}:${cmd.cwd}`;
23403
+ if (!installPlans.has(key)) installPlans.set(key, cmd);
23404
+ } else if (outcome.followUpInstall) {
23405
+ dependencyInstructions.set(svc.dir, outcome.followUpInstall);
23406
+ }
22409
23407
  }
22410
23408
  } else if (outcome.outcome === "already-instrumented") already++;
22411
23409
  else if (outcome.outcome === "lib-only") {
22412
23410
  libOnly++;
22413
23411
  const appDeps = svc.pkg ? appFrameworkDependencies(svc.pkg) : [];
22414
23412
  if (appDeps.length > 0) {
22415
- const svcName = import_node_path76.default.basename(svc.dir);
23413
+ const svcName = import_node_path78.default.basename(svc.dir);
22416
23414
  const list = appDeps.join(", ");
22417
23415
  console.warn(
22418
23416
  `neat: runtime layer won't engage for ${svcName}: no entry point found.
@@ -22425,7 +23423,7 @@ async function applyInstallersOver(services, project, options = {}) {
22425
23423
  console.log(`skipping ${svc.dir}: browser bundle; browser-OTel support lands in a future release.`);
22426
23424
  } else if (outcome.outcome === "react-native") {
22427
23425
  reactNative++;
22428
- const svcName = import_node_path76.default.basename(svc.dir);
23426
+ const svcName = import_node_path78.default.basename(svc.dir);
22429
23427
  console.log(
22430
23428
  `neat: ${svc.dir} detected as React Native / Expo
22431
23429
  The installer doesn't cover this runtime deterministically.
@@ -22436,7 +23434,7 @@ async function applyInstallersOver(services, project, options = {}) {
22436
23434
  );
22437
23435
  } else if (outcome.outcome === "bun") {
22438
23436
  bun++;
22439
- const svcName = import_node_path76.default.basename(svc.dir);
23437
+ const svcName = import_node_path78.default.basename(svc.dir);
22440
23438
  console.log(
22441
23439
  `neat: ${svc.dir} detected as Bun
22442
23440
  The installer doesn't cover this runtime deterministically.
@@ -22447,7 +23445,7 @@ async function applyInstallersOver(services, project, options = {}) {
22447
23445
  );
22448
23446
  } else if (outcome.outcome === "deno") {
22449
23447
  deno++;
22450
- const svcName = import_node_path76.default.basename(svc.dir);
23448
+ const svcName = import_node_path78.default.basename(svc.dir);
22451
23449
  console.log(
22452
23450
  `neat: ${svc.dir} detected as Deno
22453
23451
  The installer doesn't cover this runtime deterministically.
@@ -22458,7 +23456,7 @@ async function applyInstallersOver(services, project, options = {}) {
22458
23456
  );
22459
23457
  } else if (outcome.outcome === "cloudflare-workers") {
22460
23458
  cloudflareWorkers++;
22461
- const svcName = import_node_path76.default.basename(svc.dir);
23459
+ const svcName = import_node_path78.default.basename(svc.dir);
22462
23460
  console.log(
22463
23461
  `neat: ${svc.dir} detected as Cloudflare Workers
22464
23462
  The installer doesn't cover this runtime deterministically.
@@ -22469,7 +23467,7 @@ async function applyInstallersOver(services, project, options = {}) {
22469
23467
  );
22470
23468
  } else if (outcome.outcome === "electron") {
22471
23469
  electron++;
22472
- const svcName = import_node_path76.default.basename(svc.dir);
23470
+ const svcName = import_node_path78.default.basename(svc.dir);
22473
23471
  console.log(
22474
23472
  `neat: ${svc.dir} detected as Electron
22475
23473
  The installer doesn't cover this runtime deterministically.
@@ -22482,7 +23480,7 @@ async function applyInstallersOver(services, project, options = {}) {
22482
23480
  if (svc.pkg && (outcome.outcome === "instrumented" || outcome.outcome === "already-instrumented")) {
22483
23481
  const gaps = uninstrumentedLibraries(svc.pkg);
22484
23482
  if (gaps.length > 0) {
22485
- const svcName = import_node_path76.default.basename(svc.dir);
23483
+ const svcName = import_node_path78.default.basename(svc.dir);
22486
23484
  const list = gaps.join(", ");
22487
23485
  const subject = gaps.length === 1 ? "this library" : "these libraries";
22488
23486
  const aux = gaps.length === 1 ? "isn't" : "aren't";
@@ -22510,6 +23508,11 @@ async function applyInstallersOver(services, project, options = {}) {
22510
23508
  }
22511
23509
  }
22512
23510
  }
23511
+ for (const [dir, command] of dependencyInstructions) {
23512
+ console.log(
23513
+ `neat: dependencies staged in ${dir}; run \`${command}\` to install them \u2014 NEAT does not run it for you.`
23514
+ );
23515
+ }
22513
23516
  return {
22514
23517
  instrumented,
22515
23518
  alreadyInstrumented: already,
@@ -22520,7 +23523,8 @@ async function applyInstallersOver(services, project, options = {}) {
22520
23523
  deno,
22521
23524
  cloudflareWorkers,
22522
23525
  electron,
22523
- packageManagerInstalls
23526
+ packageManagerInstalls,
23527
+ dependencyInstructions: [...dependencyInstructions].map(([dir, command]) => ({ dir, command }))
22524
23528
  };
22525
23529
  }
22526
23530
  async function promptYesNo(question) {
@@ -22688,24 +23692,24 @@ async function persistedPortsFor(scanPath) {
22688
23692
  return { rest: record.ports.rest, otlp: record.ports.otlp, web: record.ports.web };
22689
23693
  }
22690
23694
  async function acquireSpawnLock(scanPath) {
22691
- const lockPath = import_node_path76.default.join(scanPath, "neat-out", "daemon.spawn.lock");
22692
- await import_node_fs42.promises.mkdir(import_node_path76.default.dirname(lockPath), { recursive: true });
23695
+ const lockPath = import_node_path78.default.join(scanPath, "neat-out", "daemon.spawn.lock");
23696
+ await import_node_fs44.promises.mkdir(import_node_path78.default.dirname(lockPath), { recursive: true });
22693
23697
  const STALE_LOCK_MS = 6e4;
22694
23698
  try {
22695
- const fd = await import_node_fs42.promises.open(lockPath, "wx");
23699
+ const fd = await import_node_fs44.promises.open(lockPath, "wx");
22696
23700
  await fd.writeFile(`${process.pid}
22697
23701
  `, "utf8");
22698
23702
  await fd.close();
22699
23703
  return async () => {
22700
- await import_node_fs42.promises.unlink(lockPath).catch(() => {
23704
+ await import_node_fs44.promises.unlink(lockPath).catch(() => {
22701
23705
  });
22702
23706
  };
22703
23707
  } catch (err) {
22704
23708
  if (err.code !== "EEXIST") return null;
22705
23709
  try {
22706
- const stat = await import_node_fs42.promises.stat(lockPath);
23710
+ const stat = await import_node_fs44.promises.stat(lockPath);
22707
23711
  if (Date.now() - stat.mtimeMs > STALE_LOCK_MS) {
22708
- await import_node_fs42.promises.unlink(lockPath).catch(() => {
23712
+ await import_node_fs44.promises.unlink(lockPath).catch(() => {
22709
23713
  });
22710
23714
  return acquireSpawnLock(scanPath);
22711
23715
  }
@@ -22734,13 +23738,13 @@ async function healthIsForProject(restPort, project) {
22734
23738
  return false;
22735
23739
  }
22736
23740
  function daemonLogPath(projectPath3) {
22737
- return import_node_path76.default.join(projectPath3, "neat-out", "daemon.log");
23741
+ return import_node_path78.default.join(projectPath3, "neat-out", "daemon.log");
22738
23742
  }
22739
23743
  function spawnDaemonDetached(spec) {
22740
- const here = import_node_path76.default.dirname((0, import_node_url4.fileURLToPath)(importMetaUrl));
23744
+ const here = import_node_path78.default.dirname((0, import_node_url4.fileURLToPath)(importMetaUrl));
22741
23745
  const candidates = [
22742
- import_node_path76.default.join(here, "neatd.cjs"),
22743
- import_node_path76.default.join(here, "neatd.js")
23746
+ import_node_path78.default.join(here, "neatd.cjs"),
23747
+ import_node_path78.default.join(here, "neatd.js")
22744
23748
  ];
22745
23749
  let entry2 = null;
22746
23750
  const fsSync = require("fs");
@@ -22770,7 +23774,7 @@ function spawnDaemonDetached(spec) {
22770
23774
  let logFd = null;
22771
23775
  if (spec) {
22772
23776
  const logPath = daemonLogPath(spec.projectPath);
22773
- fsSync.mkdirSync(import_node_path76.default.dirname(logPath), { recursive: true });
23777
+ fsSync.mkdirSync(import_node_path78.default.dirname(logPath), { recursive: true });
22774
23778
  logFd = fsSync.openSync(logPath, "a");
22775
23779
  }
22776
23780
  const child = (0, import_node_child_process3.spawn)(process.execPath, [entry2, "start"], {
@@ -22809,7 +23813,7 @@ async function runOrchestrator(opts) {
22809
23813
  browser: "skipped"
22810
23814
  }
22811
23815
  };
22812
- const stat = await import_node_fs42.promises.stat(opts.scanPath).catch(() => null);
23816
+ const stat = await import_node_fs44.promises.stat(opts.scanPath).catch(() => null);
22813
23817
  if (!stat || !stat.isDirectory()) {
22814
23818
  console.error(`neat: ${opts.scanPath} is not a directory`);
22815
23819
  result.exitCode = 2;
@@ -22969,7 +23973,7 @@ async function runOrchestrator(opts) {
22969
23973
  result.steps.browser = openBrowser(dashboardUrl);
22970
23974
  }
22971
23975
  const daemonRunning = result.steps.daemon === "spawned" || result.steps.daemon === "already-running";
22972
- const daemonLog = daemonRunning ? import_node_path76.default.relative(opts.scanPath, daemonLogPath(opts.scanPath)) : null;
23976
+ const daemonLog = daemonRunning ? import_node_path78.default.relative(opts.scanPath, daemonLogPath(opts.scanPath)) : null;
22973
23977
  printSummary(result, graph, dashboardUrl, daemonLog);
22974
23978
  return result;
22975
23979
  }
@@ -23428,27 +24432,27 @@ async function runConnectorCommand(rawArgs, deps = {}) {
23428
24432
 
23429
24433
  // src/hooks-cli.ts
23430
24434
  init_cjs_shims();
23431
- var import_node_path77 = __toESM(require("path"), 1);
24435
+ var import_node_path79 = __toESM(require("path"), 1);
23432
24436
  var import_node_os5 = __toESM(require("os"), 1);
23433
- var import_node_fs43 = require("fs");
24437
+ var import_node_fs45 = require("fs");
23434
24438
  var import_node_url5 = require("url");
23435
24439
  var HOOK_FILENAME = "neat-search-nudge.mjs";
23436
24440
  var GUIDE_FILENAME = "GRAPH_FIRST.md";
23437
24441
  var GUIDE_INSTALL_NAME = "neat-graph-first.md";
23438
24442
  var HOOK_MATCHER = "Grep|Glob|Bash";
23439
24443
  function moduleDir() {
23440
- return typeof __dirname !== "undefined" ? __dirname : import_node_path77.default.dirname((0, import_node_url5.fileURLToPath)(importMetaUrl));
24444
+ return typeof __dirname !== "undefined" ? __dirname : import_node_path79.default.dirname((0, import_node_url5.fileURLToPath)(importMetaUrl));
23441
24445
  }
23442
24446
  async function readSkillAsset(rel) {
23443
24447
  const here = moduleDir();
23444
24448
  const candidates = [
23445
- import_node_path77.default.resolve(here, "../../claude-skill", rel),
23446
- import_node_path77.default.resolve(here, "../../../claude-skill", rel),
23447
- import_node_path77.default.resolve(here, "../claude-skill", rel)
24449
+ import_node_path79.default.resolve(here, "../../claude-skill", rel),
24450
+ import_node_path79.default.resolve(here, "../../../claude-skill", rel),
24451
+ import_node_path79.default.resolve(here, "../claude-skill", rel)
23448
24452
  ];
23449
24453
  for (const candidate of candidates) {
23450
24454
  try {
23451
- return await import_node_fs43.promises.readFile(candidate, "utf8");
24455
+ return await import_node_fs45.promises.readFile(candidate, "utf8");
23452
24456
  } catch {
23453
24457
  }
23454
24458
  }
@@ -23458,17 +24462,17 @@ async function readSkillAsset(rel) {
23458
24462
  }
23459
24463
  function neatHome3() {
23460
24464
  const override = process.env.NEAT_HOME;
23461
- if (override && override.length > 0) return import_node_path77.default.resolve(override);
23462
- return import_node_path77.default.join(import_node_os5.default.homedir(), ".neat");
24465
+ if (override && override.length > 0) return import_node_path79.default.resolve(override);
24466
+ return import_node_path79.default.join(import_node_os5.default.homedir(), ".neat");
23463
24467
  }
23464
24468
  function claudeSettingsPath() {
23465
24469
  const override = process.env.NEAT_CLAUDE_SETTINGS;
23466
- if (override && override.length > 0) return import_node_path77.default.resolve(override);
24470
+ if (override && override.length > 0) return import_node_path79.default.resolve(override);
23467
24471
  const home = process.env.HOME ?? process.env.USERPROFILE ?? import_node_os5.default.homedir();
23468
- return import_node_path77.default.join(home, ".claude", "settings.json");
24472
+ return import_node_path79.default.join(home, ".claude", "settings.json");
23469
24473
  }
23470
24474
  function installedHookPath() {
23471
- return import_node_path77.default.join(neatHome3(), "hooks", HOOK_FILENAME);
24475
+ return import_node_path79.default.join(neatHome3(), "hooks", HOOK_FILENAME);
23472
24476
  }
23473
24477
  function isNeatSearchEntry(entry2) {
23474
24478
  return (entry2.hooks ?? []).some(
@@ -23501,14 +24505,14 @@ async function runHooks(opts) {
23501
24505
  const hookScript = await readSkillAsset(`hooks/${HOOK_FILENAME}`);
23502
24506
  const guide = await readSkillAsset(GUIDE_FILENAME);
23503
24507
  const scriptPath = installedHookPath();
23504
- await import_node_fs43.promises.mkdir(import_node_path77.default.dirname(scriptPath), { recursive: true });
23505
- await import_node_fs43.promises.writeFile(scriptPath, hookScript, { mode: 493 });
23506
- const guidePath = import_node_path77.default.join(neatHome3(), GUIDE_INSTALL_NAME);
23507
- await import_node_fs43.promises.writeFile(guidePath, guide, "utf8");
24508
+ await import_node_fs45.promises.mkdir(import_node_path79.default.dirname(scriptPath), { recursive: true });
24509
+ await import_node_fs45.promises.writeFile(scriptPath, hookScript, { mode: 493 });
24510
+ const guidePath = import_node_path79.default.join(neatHome3(), GUIDE_INSTALL_NAME);
24511
+ await import_node_fs45.promises.writeFile(guidePath, guide, "utf8");
23508
24512
  const settingsFile = claudeSettingsPath();
23509
24513
  let settings = {};
23510
24514
  try {
23511
- settings = JSON.parse(await import_node_fs43.promises.readFile(settingsFile, "utf8"));
24515
+ settings = JSON.parse(await import_node_fs45.promises.readFile(settingsFile, "utf8"));
23512
24516
  } catch (err) {
23513
24517
  if (err.code !== "ENOENT") {
23514
24518
  console.error(
@@ -23530,8 +24534,8 @@ async function runHooks(opts) {
23530
24534
  ...settings,
23531
24535
  hooks: { ...hooks, PreToolUse: preToolUse }
23532
24536
  };
23533
- await import_node_fs43.promises.mkdir(import_node_path77.default.dirname(settingsFile), { recursive: true });
23534
- await import_node_fs43.promises.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
24537
+ await import_node_fs45.promises.mkdir(import_node_path79.default.dirname(settingsFile), { recursive: true });
24538
+ await import_node_fs45.promises.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
23535
24539
  console.log(`neat hooks: installed the search-nudge hook`);
23536
24540
  console.log(` script: ${scriptPath}`);
23537
24541
  console.log(` settings: ${settingsFile} (PreToolUse \u2192 ${HOOK_MATCHER})`);
@@ -23602,9 +24606,9 @@ async function runHooksCommand(args) {
23602
24606
 
23603
24607
  // src/codex-cli.ts
23604
24608
  init_cjs_shims();
23605
- var import_node_path78 = __toESM(require("path"), 1);
24609
+ var import_node_path80 = __toESM(require("path"), 1);
23606
24610
  var import_node_os6 = __toESM(require("os"), 1);
23607
- var import_node_fs44 = require("fs");
24611
+ var import_node_fs46 = require("fs");
23608
24612
  var import_node_util = require("util");
23609
24613
  var import_smol_toml5 = require("smol-toml");
23610
24614
  var CODEX_MCP_SERVER = {
@@ -23622,14 +24626,14 @@ var NEAT_GRAPH_FIRST_START = "<!-- neat:graph-first -->";
23622
24626
  var NEAT_GRAPH_FIRST_END = "<!-- /neat:graph-first -->";
23623
24627
  function codexConfigPath() {
23624
24628
  const override = process.env.NEAT_CODEX_CONFIG;
23625
- if (override && override.length > 0) return import_node_path78.default.resolve(override);
24629
+ if (override && override.length > 0) return import_node_path80.default.resolve(override);
23626
24630
  const home = process.env.HOME ?? process.env.USERPROFILE ?? import_node_os6.default.homedir();
23627
- return import_node_path78.default.join(home, ".codex", "config.toml");
24631
+ return import_node_path80.default.join(home, ".codex", "config.toml");
23628
24632
  }
23629
24633
  function agentsFilePath() {
23630
24634
  const override = process.env.NEAT_CODEX_AGENTS;
23631
- if (override && override.length > 0) return import_node_path78.default.resolve(override);
23632
- return import_node_path78.default.join(process.cwd(), "AGENTS.md");
24635
+ if (override && override.length > 0) return import_node_path80.default.resolve(override);
24636
+ return import_node_path80.default.join(process.cwd(), "AGENTS.md");
23633
24637
  }
23634
24638
  function isTableHeader(line) {
23635
24639
  return /^\s*\[\[?[^\]]+\]\]?\s*$/.test(line);
@@ -23763,7 +24767,7 @@ async function runCodex(opts) {
23763
24767
  const agentsPath = agentsFilePath();
23764
24768
  let configRaw = "";
23765
24769
  try {
23766
- configRaw = await import_node_fs44.promises.readFile(configPath, "utf8");
24770
+ configRaw = await import_node_fs46.promises.readFile(configPath, "utf8");
23767
24771
  } catch (err) {
23768
24772
  if (err.code !== "ENOENT") {
23769
24773
  console.error(`neat codex: failed to read ${configPath} \u2014 ${err.message}`);
@@ -23772,7 +24776,7 @@ async function runCodex(opts) {
23772
24776
  }
23773
24777
  let agentsRaw = "";
23774
24778
  try {
23775
- agentsRaw = await import_node_fs44.promises.readFile(agentsPath, "utf8");
24779
+ agentsRaw = await import_node_fs46.promises.readFile(agentsPath, "utf8");
23776
24780
  } catch (err) {
23777
24781
  if (err.code !== "ENOENT") {
23778
24782
  console.error(`neat codex: failed to read ${agentsPath} \u2014 ${err.message}`);
@@ -23812,15 +24816,15 @@ async function runCodex(opts) {
23812
24816
  return { exitCode: 0 };
23813
24817
  }
23814
24818
  if (config.changed) {
23815
- await import_node_fs44.promises.mkdir(import_node_path78.default.dirname(configPath), { recursive: true });
23816
- await import_node_fs44.promises.writeFile(configPath, config.text, "utf8");
24819
+ await import_node_fs46.promises.mkdir(import_node_path80.default.dirname(configPath), { recursive: true });
24820
+ await import_node_fs46.promises.writeFile(configPath, config.text, "utf8");
23817
24821
  console.log(`neat codex: wrote [mcp_servers.neat] to ${configPath}`);
23818
24822
  } else {
23819
24823
  console.log(`neat codex: ${configPath} already has NEAT's MCP server`);
23820
24824
  }
23821
24825
  if (agents.changed) {
23822
- await import_node_fs44.promises.mkdir(import_node_path78.default.dirname(agentsPath), { recursive: true });
23823
- await import_node_fs44.promises.writeFile(agentsPath, agents.text, "utf8");
24826
+ await import_node_fs46.promises.mkdir(import_node_path80.default.dirname(agentsPath), { recursive: true });
24827
+ await import_node_fs46.promises.writeFile(agentsPath, agents.text, "utf8");
23824
24828
  console.log(`neat codex: wrote the graph-first block to ${agentsPath}`);
23825
24829
  } else {
23826
24830
  console.log(`neat codex: ${agentsPath} already has the graph-first block`);
@@ -23877,9 +24881,9 @@ async function runCodexCommand(args) {
23877
24881
 
23878
24882
  // src/editors-cli.ts
23879
24883
  init_cjs_shims();
23880
- var import_node_path79 = __toESM(require("path"), 1);
24884
+ var import_node_path81 = __toESM(require("path"), 1);
23881
24885
  var import_node_os7 = __toESM(require("os"), 1);
23882
- var import_node_fs45 = require("fs");
24886
+ var import_node_fs47 = require("fs");
23883
24887
  var import_node_util2 = require("util");
23884
24888
  var jsonc = __toESM(require("jsonc-parser"), 1);
23885
24889
  var NEAT_MCP_SERVER = {
@@ -23903,17 +24907,17 @@ function homeDir() {
23903
24907
  }
23904
24908
  function xdgConfigDir() {
23905
24909
  const xdg = process.env.XDG_CONFIG_HOME;
23906
- return xdg && xdg.length > 0 ? import_node_path79.default.resolve(xdg) : import_node_path79.default.join(homeDir(), ".config");
24910
+ return xdg && xdg.length > 0 ? import_node_path81.default.resolve(xdg) : import_node_path81.default.join(homeDir(), ".config");
23907
24911
  }
23908
24912
  function envOverride(name) {
23909
24913
  const v = process.env[name];
23910
- return v && v.length > 0 ? import_node_path79.default.resolve(v) : void 0;
24914
+ return v && v.length > 0 ? import_node_path81.default.resolve(v) : void 0;
23911
24915
  }
23912
24916
  var CURSOR_CLIENT = {
23913
24917
  id: "cursor",
23914
24918
  label: "Cursor",
23915
24919
  docsUrl: "https://docs.cursor.com/context/mcp",
23916
- mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ?? import_node_path79.default.join(homeDir(), ".cursor", "mcp.json"),
24920
+ mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ?? import_node_path81.default.join(homeDir(), ".cursor", "mcp.json"),
23917
24921
  mcpContainerKey: "mcpServers",
23918
24922
  format: "json",
23919
24923
  // Cursor still reads a single `.cursorrules` at the project root (the modern
@@ -23925,7 +24929,7 @@ var DEVIN_CLIENT = {
23925
24929
  id: "devin",
23926
24930
  label: "Devin Desktop (Cascade)",
23927
24931
  docsUrl: "https://docs.devin.ai/desktop/cascade/mcp",
23928
- mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? import_node_path79.default.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
24932
+ mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? import_node_path81.default.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
23929
24933
  mcpContainerKey: "mcpServers",
23930
24934
  format: "json",
23931
24935
  rulesFileName: ".windsurfrules"
@@ -23934,7 +24938,7 @@ var GEMINI_CLIENT = {
23934
24938
  id: "gemini",
23935
24939
  label: "Gemini CLI",
23936
24940
  docsUrl: "https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md",
23937
- mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? import_node_path79.default.join(homeDir(), ".gemini", "settings.json"),
24941
+ mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? import_node_path81.default.join(homeDir(), ".gemini", "settings.json"),
23938
24942
  mcpContainerKey: "mcpServers",
23939
24943
  format: "json",
23940
24944
  rulesFileName: "GEMINI.md"
@@ -23943,7 +24947,7 @@ var QWEN_CLIENT = {
23943
24947
  id: "qwen",
23944
24948
  label: "Qwen Code",
23945
24949
  docsUrl: "https://qwenlm.github.io/qwen-code-docs/en/users/features/mcp/",
23946
- mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? import_node_path79.default.join(homeDir(), ".qwen", "settings.json"),
24950
+ mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? import_node_path81.default.join(homeDir(), ".qwen", "settings.json"),
23947
24951
  mcpContainerKey: "mcpServers",
23948
24952
  format: "json",
23949
24953
  rulesFileName: "QWEN.md"
@@ -23952,7 +24956,7 @@ var AMAZONQ_CLIENT = {
23952
24956
  id: "amazonq",
23953
24957
  label: "Amazon Q Developer CLI",
23954
24958
  docsUrl: "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-configuration.html",
23955
- mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? import_node_path79.default.join(homeDir(), ".aws", "amazonq", "mcp.json"),
24959
+ mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? import_node_path81.default.join(homeDir(), ".aws", "amazonq", "mcp.json"),
23956
24960
  mcpContainerKey: "mcpServers",
23957
24961
  format: "json"
23958
24962
  };
@@ -23960,7 +24964,7 @@ var ROOCODE_CLIENT = {
23960
24964
  id: "roocode",
23961
24965
  label: "Roo Code",
23962
24966
  docsUrl: "https://roocodeinc.github.io/Roo-Code/features/mcp/using-mcp-in-roo",
23963
- mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? import_node_path79.default.join(process.cwd(), ".roo", "mcp.json"),
24967
+ mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? import_node_path81.default.join(process.cwd(), ".roo", "mcp.json"),
23964
24968
  mcpContainerKey: "mcpServers",
23965
24969
  format: "json"
23966
24970
  };
@@ -23973,9 +24977,9 @@ var ZED_CLIENT = {
23973
24977
  if (override) return override;
23974
24978
  if (process.platform === "win32") {
23975
24979
  const appData = process.env.APPDATA;
23976
- if (appData && appData.length > 0) return import_node_path79.default.join(appData, "Zed", "settings.json");
24980
+ if (appData && appData.length > 0) return import_node_path81.default.join(appData, "Zed", "settings.json");
23977
24981
  }
23978
- return import_node_path79.default.join(homeDir(), ".config", "zed", "settings.json");
24982
+ return import_node_path81.default.join(homeDir(), ".config", "zed", "settings.json");
23979
24983
  },
23980
24984
  mcpContainerKey: "context_servers",
23981
24985
  format: "jsonc",
@@ -23985,7 +24989,7 @@ var OPENCODE_CLIENT = {
23985
24989
  id: "opencode",
23986
24990
  label: "OpenCode",
23987
24991
  docsUrl: "https://opencode.ai/docs/mcp-servers/",
23988
- mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? import_node_path79.default.join(xdgConfigDir(), "opencode", "opencode.json"),
24992
+ mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? import_node_path81.default.join(xdgConfigDir(), "opencode", "opencode.json"),
23989
24993
  mcpContainerKey: "mcp",
23990
24994
  format: "json",
23991
24995
  serverEntry: NEAT_OPENCODE_SERVER,
@@ -23995,7 +24999,7 @@ var CRUSH_CLIENT = {
23995
24999
  id: "crush",
23996
25000
  label: "Crush",
23997
25001
  docsUrl: "https://charmbracelet-crush.mintlify.app/configuration/mcp",
23998
- mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? import_node_path79.default.join(xdgConfigDir(), "crush", "crush.json"),
25002
+ mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? import_node_path81.default.join(xdgConfigDir(), "crush", "crush.json"),
23999
25003
  mcpContainerKey: "mcp",
24000
25004
  format: "json",
24001
25005
  serverEntry: NEAT_CRUSH_SERVER,
@@ -24058,7 +25062,7 @@ async function planMcp(client, mcpPath) {
24058
25062
  const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
24059
25063
  let raw = "";
24060
25064
  try {
24061
- raw = await import_node_fs45.promises.readFile(mcpPath, "utf8");
25065
+ raw = await import_node_fs47.promises.readFile(mcpPath, "utf8");
24062
25066
  } catch (err) {
24063
25067
  const e = err;
24064
25068
  if (e.code === "ENOENT") {
@@ -24100,7 +25104,7 @@ async function runEditorInstall(client, opts) {
24100
25104
  const mcpPath = client.mcpConfigPath();
24101
25105
  const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
24102
25106
  const hasRules = typeof client.rulesFileName === "string";
24103
- const rulesPath = hasRules ? import_node_path79.default.join(opts.projectDir, client.rulesFileName) : "";
25107
+ const rulesPath = hasRules ? import_node_path81.default.join(opts.projectDir, client.rulesFileName) : "";
24104
25108
  const mcp = await planMcp(client, mcpPath);
24105
25109
  if (mcp === null) return { exitCode: 1 };
24106
25110
  let existingRules = "";
@@ -24109,7 +25113,7 @@ async function runEditorInstall(client, opts) {
24109
25113
  let block = "";
24110
25114
  if (hasRules) {
24111
25115
  try {
24112
- existingRules = await import_node_fs45.promises.readFile(rulesPath, "utf8");
25116
+ existingRules = await import_node_fs47.promises.readFile(rulesPath, "utf8");
24113
25117
  } catch (err) {
24114
25118
  if (err.code !== "ENOENT") {
24115
25119
  console.error(`neat ${client.id}: failed to read ${rulesPath} \u2014 ${err.message}`);
@@ -24143,11 +25147,11 @@ async function runEditorInstall(client, opts) {
24143
25147
  );
24144
25148
  return { exitCode: 0 };
24145
25149
  }
24146
- await import_node_fs45.promises.mkdir(import_node_path79.default.dirname(mcpPath), { recursive: true });
24147
- await import_node_fs45.promises.writeFile(mcpPath, mcp.text, "utf8");
25150
+ await import_node_fs47.promises.mkdir(import_node_path81.default.dirname(mcpPath), { recursive: true });
25151
+ await import_node_fs47.promises.writeFile(mcpPath, mcp.text, "utf8");
24148
25152
  if (hasRules) {
24149
- await import_node_fs45.promises.mkdir(import_node_path79.default.dirname(rulesPath), { recursive: true });
24150
- await import_node_fs45.promises.writeFile(rulesPath, newRules, "utf8");
25153
+ await import_node_fs47.promises.mkdir(import_node_path81.default.dirname(rulesPath), { recursive: true });
25154
+ await import_node_fs47.promises.writeFile(rulesPath, newRules, "utf8");
24151
25155
  }
24152
25156
  console.log(`neat ${client.id}: wired NEAT into ${client.label}`);
24153
25157
  console.log(` MCP server: ${mcpPath} (${client.mcpContainerKey}.neat \u2192 npx -y @neat.is/mcp)`);
@@ -24183,11 +25187,11 @@ function usage3(client) {
24183
25187
  }
24184
25188
  async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
24185
25189
  const client = CLIENTS[clientId];
24186
- let apply4 = false;
25190
+ let apply6 = false;
24187
25191
  for (const arg of args) {
24188
25192
  switch (arg) {
24189
25193
  case "--apply":
24190
- apply4 = true;
25194
+ apply6 = true;
24191
25195
  break;
24192
25196
  case "-h":
24193
25197
  case "--help":
@@ -24200,7 +25204,7 @@ async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
24200
25204
  }
24201
25205
  }
24202
25206
  try {
24203
- const { exitCode } = await runEditorInstall(client, { apply: apply4, projectDir });
25207
+ const { exitCode } = await runEditorInstall(client, { apply: apply6, projectDir });
24204
25208
  return exitCode;
24205
25209
  } catch (err) {
24206
25210
  console.error(err.message);
@@ -24210,11 +25214,11 @@ async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
24210
25214
 
24211
25215
  // src/monitor.ts
24212
25216
  init_cjs_shims();
24213
- var import_types84 = require("@neat.is/types");
25217
+ var import_types89 = require("@neat.is/types");
24214
25218
 
24215
25219
  // src/cli-client.ts
24216
25220
  init_cjs_shims();
24217
- var import_types83 = require("@neat.is/types");
25221
+ var import_types88 = require("@neat.is/types");
24218
25222
  var HttpError = class extends Error {
24219
25223
  constructor(status2, message, responseBody = "") {
24220
25224
  super(message);
@@ -24239,10 +25243,10 @@ function createHttpClient(baseUrl, bearerToken) {
24239
25243
  const root = baseUrl.replace(/\/$/, "");
24240
25244
  const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
24241
25245
  return {
24242
- async get(path82) {
25246
+ async get(path84) {
24243
25247
  let res;
24244
25248
  try {
24245
- res = await fetch(`${root}${path82}`, {
25249
+ res = await fetch(`${root}${path84}`, {
24246
25250
  headers: { ...authHeader }
24247
25251
  });
24248
25252
  } catch (err) {
@@ -24254,16 +25258,16 @@ function createHttpClient(baseUrl, bearerToken) {
24254
25258
  const body = await res.text().catch(() => "");
24255
25259
  throw new HttpError(
24256
25260
  res.status,
24257
- `${res.status} ${res.statusText} on GET ${path82}: ${body}`,
25261
+ `${res.status} ${res.statusText} on GET ${path84}: ${body}`,
24258
25262
  body
24259
25263
  );
24260
25264
  }
24261
25265
  return await res.json();
24262
25266
  },
24263
- async post(path82, body) {
25267
+ async post(path84, body) {
24264
25268
  let res;
24265
25269
  try {
24266
- res = await fetch(`${root}${path82}`, {
25270
+ res = await fetch(`${root}${path84}`, {
24267
25271
  method: "POST",
24268
25272
  headers: { "content-type": "application/json", ...authHeader },
24269
25273
  body: JSON.stringify(body)
@@ -24277,7 +25281,7 @@ function createHttpClient(baseUrl, bearerToken) {
24277
25281
  const text = await res.text().catch(() => "");
24278
25282
  throw new HttpError(
24279
25283
  res.status,
24280
- `${res.status} ${res.statusText} on POST ${path82}: ${text}`,
25284
+ `${res.status} ${res.statusText} on POST ${path84}: ${text}`,
24281
25285
  text
24282
25286
  );
24283
25287
  }
@@ -24291,12 +25295,12 @@ function projectPath(project, suffix) {
24291
25295
  }
24292
25296
  async function runRootCause(client, input) {
24293
25297
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
24294
- const path82 = projectPath(
25298
+ const path84 = projectPath(
24295
25299
  input.project,
24296
25300
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
24297
25301
  );
24298
25302
  try {
24299
- const result = await client.get(path82);
25303
+ const result = await client.get(path84);
24300
25304
  const arrowPath = result.traversalPath.join(" \u2190 ");
24301
25305
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
24302
25306
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -24322,12 +25326,12 @@ async function runRootCause(client, input) {
24322
25326
  }
24323
25327
  async function runBlastRadius(client, input) {
24324
25328
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
24325
- const path82 = projectPath(
25329
+ const path84 = projectPath(
24326
25330
  input.project,
24327
25331
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
24328
25332
  );
24329
25333
  try {
24330
- const result = await client.get(path82);
25334
+ const result = await client.get(path84);
24331
25335
  if (result.totalAffected === 0) {
24332
25336
  return {
24333
25337
  summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
@@ -24356,17 +25360,17 @@ async function runBlastRadius(client, input) {
24356
25360
  }
24357
25361
  }
24358
25362
  function formatBlastEntry(n) {
24359
- const tag = n.edgeProvenance === import_types83.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
25363
+ const tag = n.edgeProvenance === import_types88.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
24360
25364
  return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
24361
25365
  }
24362
25366
  async function runDependencies(client, input) {
24363
25367
  const depth = input.depth ?? 3;
24364
- const path82 = projectPath(
25368
+ const path84 = projectPath(
24365
25369
  input.project,
24366
25370
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
24367
25371
  );
24368
25372
  try {
24369
- const result = await client.get(path82);
25373
+ const result = await client.get(path84);
24370
25374
  if (result.total === 0) {
24371
25375
  return {
24372
25376
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -24413,7 +25417,7 @@ async function runObservedDependencies(client, input) {
24413
25417
  if (result.observed) {
24414
25418
  return {
24415
25419
  summary: `${input.nodeId} makes no outbound runtime calls, but OTel has observed it receiving traffic on ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 it's a pure receiver.`,
24416
- provenance: import_types83.Provenance.OBSERVED
25420
+ provenance: import_types88.Provenance.OBSERVED
24417
25421
  };
24418
25422
  }
24419
25423
  const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
@@ -24423,7 +25427,7 @@ async function runObservedDependencies(client, input) {
24423
25427
  return {
24424
25428
  summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
24425
25429
  block: blockLines.join("\n"),
24426
- provenance: import_types83.Provenance.OBSERVED
25430
+ provenance: import_types88.Provenance.OBSERVED
24427
25431
  };
24428
25432
  } catch (err) {
24429
25433
  if (err instanceof HttpError && err.status === 404) {
@@ -24458,9 +25462,9 @@ function formatDuration(ms) {
24458
25462
  return `${Math.round(h / 24)}d`;
24459
25463
  }
24460
25464
  async function runIncidents(client, input) {
24461
- const path82 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
25465
+ const path84 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
24462
25466
  try {
24463
- const body = await client.get(path82);
25467
+ const body = await client.get(path84);
24464
25468
  const events = body.events;
24465
25469
  if (events.length === 0) {
24466
25470
  return {
@@ -24477,7 +25481,7 @@ async function runIncidents(client, input) {
24477
25481
  return {
24478
25482
  summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
24479
25483
  block: blockLines.join("\n"),
24480
- provenance: import_types83.Provenance.OBSERVED
25484
+ provenance: import_types88.Provenance.OBSERVED
24481
25485
  };
24482
25486
  } catch (err) {
24483
25487
  if (err instanceof HttpError && err.status === 404) {
@@ -24586,7 +25590,7 @@ async function runStaleEdges(client, input) {
24586
25590
  return {
24587
25591
  summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
24588
25592
  block: blockLines.join("\n"),
24589
- provenance: import_types83.Provenance.STALE
25593
+ provenance: import_types88.Provenance.STALE
24590
25594
  };
24591
25595
  }
24592
25596
  async function runPolicies(client, input) {
@@ -24745,10 +25749,10 @@ async function pushSnapshotToRemote(input) {
24745
25749
 
24746
25750
  // src/monitor.ts
24747
25751
  var OBSERVED_DEP_EDGE_TYPES = /* @__PURE__ */ new Set([
24748
- import_types84.EdgeType.CALLS,
24749
- import_types84.EdgeType.CONNECTS_TO,
24750
- import_types84.EdgeType.PUBLISHES_TO,
24751
- import_types84.EdgeType.CONSUMES_FROM
25752
+ import_types89.EdgeType.CALLS,
25753
+ import_types89.EdgeType.CONNECTS_TO,
25754
+ import_types89.EdgeType.PUBLISHES_TO,
25755
+ import_types89.EdgeType.CONSUMES_FROM
24752
25756
  ]);
24753
25757
  function divergenceKey(d) {
24754
25758
  const column = "column" in d && d.column ? d.column : "";
@@ -24793,7 +25797,7 @@ function formatDivergenceLine2(d) {
24793
25797
  }
24794
25798
  }
24795
25799
  function formatStaleLine(edgeId) {
24796
- const parsed = (0, import_types84.parseEdgeId)(edgeId);
25800
+ const parsed = (0, import_types89.parseEdgeId)(edgeId);
24797
25801
  if (parsed) {
24798
25802
  return `\u22EF stale ${parsed.source} \u2192 ${parsed.target} (observed edge went quiet)`;
24799
25803
  }
@@ -24806,7 +25810,7 @@ function divergenceJson(d) {
24806
25810
  return JSON.stringify({ kind: "divergence", ...d });
24807
25811
  }
24808
25812
  function staleJson(edgeId) {
24809
- const parsed = (0, import_types84.parseEdgeId)(edgeId);
25813
+ const parsed = (0, import_types89.parseEdgeId)(edgeId);
24810
25814
  return JSON.stringify({
24811
25815
  kind: "stale",
24812
25816
  edgeId,
@@ -24876,7 +25880,7 @@ var MonitorEmitter = class {
24876
25880
  // ignores non-OBSERVED edges and non-dependency edge types (structural
24877
25881
  // ownership), so only real runtime dependencies reach stdout.
24878
25882
  emitObservedEdge(edge) {
24879
- if (edge.provenance !== import_types84.Provenance.OBSERVED) return false;
25883
+ if (edge.provenance !== import_types89.Provenance.OBSERVED) return false;
24880
25884
  if (!OBSERVED_DEP_EDGE_TYPES.has(edge.type)) return false;
24881
25885
  const key = `edge|${edge.id}`;
24882
25886
  if (this.seen.has(key)) return false;
@@ -25034,7 +26038,7 @@ async function runMonitor(opts) {
25034
26038
  case "edge-added": {
25035
26039
  const payload = safeParse(frame.data);
25036
26040
  const edge = payload?.edge;
25037
- if (edge && edge.provenance === import_types84.Provenance.OBSERVED) {
26041
+ if (edge && edge.provenance === import_types89.Provenance.OBSERVED) {
25038
26042
  emitter.emitObservedEdge(edge);
25039
26043
  divergences.schedule();
25040
26044
  }
@@ -25114,7 +26118,7 @@ function sleep(ms, signal) {
25114
26118
 
25115
26119
  // src/cli-verbs.ts
25116
26120
  init_cjs_shims();
25117
- var import_node_path80 = __toESM(require("path"), 1);
26121
+ var import_node_path82 = __toESM(require("path"), 1);
25118
26122
  async function resolveProjectEntry(opts) {
25119
26123
  const entries = await listProjects();
25120
26124
  if (opts.project) {
@@ -25124,7 +26128,7 @@ async function resolveProjectEntry(opts) {
25124
26128
  const cwd = opts.cwd ?? process.cwd();
25125
26129
  const resolvedCwd = await normalizeProjectPath(cwd);
25126
26130
  for (const entry2 of entries) {
25127
- if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path80.default.sep}`)) {
26131
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path82.default.sep}`)) {
25128
26132
  return entry2;
25129
26133
  }
25130
26134
  }
@@ -25277,7 +26281,7 @@ async function runSync(opts) {
25277
26281
  }
25278
26282
 
25279
26283
  // src/cli.ts
25280
- var import_types85 = require("@neat.is/types");
26284
+ var import_types90 = require("@neat.is/types");
25281
26285
  function isNpxInvocation() {
25282
26286
  if (process.env.npm_command === "exec") return true;
25283
26287
  const execpath = process.env.npm_execpath ?? "";
@@ -25631,15 +26635,15 @@ async function buildPatchSections(services, project) {
25631
26635
  for (const svc of services) {
25632
26636
  const installer = await pickInstaller(svc.dir);
25633
26637
  if (!installer) continue;
25634
- const plan4 = await installer.plan(svc.dir, { project });
25635
- if (isEmptyPlan(plan4) && !plan4.libOnly && plan4.runtimeKind === void 0) continue;
25636
- sections.push({ installer: installer.name, plan: plan4 });
26638
+ const plan6 = await installer.plan(svc.dir, { project });
26639
+ if (isEmptyPlan(plan6) && !plan6.libOnly && plan6.runtimeKind === void 0) continue;
26640
+ sections.push({ installer: installer.name, plan: plan6 });
25637
26641
  }
25638
26642
  return sections;
25639
26643
  }
25640
26644
  async function runInit(opts) {
25641
26645
  const written = [];
25642
- const stat = await import_node_fs46.promises.stat(opts.scanPath).catch(() => null);
26646
+ const stat = await import_node_fs48.promises.stat(opts.scanPath).catch(() => null);
25643
26647
  if (!stat || !stat.isDirectory()) {
25644
26648
  console.error(`neat init: ${opts.scanPath} is not a directory`);
25645
26649
  return { exitCode: 2, writtenFiles: written };
@@ -25648,13 +26652,13 @@ async function runInit(opts) {
25648
26652
  printDiscoveryReport(opts, services);
25649
26653
  const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
25650
26654
  const patch = renderPatch(sections);
25651
- const patchPath = import_node_path81.default.join(opts.scanPath, "neat.patch");
26655
+ const patchPath = import_node_path83.default.join(opts.scanPath, "neat.patch");
25652
26656
  if (opts.dryRun) {
25653
- await import_node_fs46.promises.writeFile(patchPath, patch, "utf8");
26657
+ await import_node_fs48.promises.writeFile(patchPath, patch, "utf8");
25654
26658
  written.push(patchPath);
25655
26659
  console.log(`dry-run: patch written to ${patchPath}`);
25656
- const gitignorePath = import_node_path81.default.join(opts.scanPath, ".gitignore");
25657
- const gitignoreExists = await import_node_fs46.promises.stat(gitignorePath).then(() => true).catch(() => false);
26660
+ const gitignorePath = import_node_path83.default.join(opts.scanPath, ".gitignore");
26661
+ const gitignoreExists = await import_node_fs48.promises.stat(gitignorePath).then(() => true).catch(() => false);
25658
26662
  const verb = gitignoreExists ? "append" : "create";
25659
26663
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
25660
26664
  console.log("rerun without --dry-run to register and snapshot.");
@@ -25665,9 +26669,9 @@ async function runInit(opts) {
25665
26669
  const graph = getGraph(graphKey);
25666
26670
  const projectPaths = pathsForProject(
25667
26671
  graphKey,
25668
- import_node_path81.default.join(opts.scanPath, "neat-out")
26672
+ import_node_path83.default.join(opts.scanPath, "neat-out")
25669
26673
  );
25670
- const errorsPath = import_node_path81.default.join(import_node_path81.default.dirname(opts.outPath), import_node_path81.default.basename(projectPaths.errorsPath));
26674
+ const errorsPath = import_node_path83.default.join(import_node_path83.default.dirname(opts.outPath), import_node_path83.default.basename(projectPaths.errorsPath));
25671
26675
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
25672
26676
  await saveGraphToDisk(graph, opts.outPath);
25673
26677
  written.push(opts.outPath);
@@ -25746,7 +26750,7 @@ async function runInit(opts) {
25746
26750
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
25747
26751
  }
25748
26752
  } else {
25749
- await import_node_fs46.promises.writeFile(patchPath, patch, "utf8");
26753
+ await import_node_fs48.promises.writeFile(patchPath, patch, "utf8");
25750
26754
  written.push(patchPath);
25751
26755
  }
25752
26756
  }
@@ -25786,9 +26790,9 @@ var CLAUDE_SKILL_CONFIG = {
25786
26790
  };
25787
26791
  function claudeConfigPath() {
25788
26792
  const override = process.env.NEAT_CLAUDE_CONFIG;
25789
- if (override && override.length > 0) return import_node_path81.default.resolve(override);
26793
+ if (override && override.length > 0) return import_node_path83.default.resolve(override);
25790
26794
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
25791
- return import_node_path81.default.join(home, ".claude.json");
26795
+ return import_node_path83.default.join(home, ".claude.json");
25792
26796
  }
25793
26797
  async function runSkill(opts) {
25794
26798
  const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -25800,7 +26804,7 @@ async function runSkill(opts) {
25800
26804
  const target = claudeConfigPath();
25801
26805
  let existing = {};
25802
26806
  try {
25803
- existing = JSON.parse(await import_node_fs46.promises.readFile(target, "utf8"));
26807
+ existing = JSON.parse(await import_node_fs48.promises.readFile(target, "utf8"));
25804
26808
  } catch (err) {
25805
26809
  if (err.code !== "ENOENT") {
25806
26810
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -25812,8 +26816,8 @@ async function runSkill(opts) {
25812
26816
  ...existing,
25813
26817
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
25814
26818
  };
25815
- await import_node_fs46.promises.mkdir(import_node_path81.default.dirname(target), { recursive: true });
25816
- await import_node_fs46.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
26819
+ await import_node_fs48.promises.mkdir(import_node_path83.default.dirname(target), { recursive: true });
26820
+ await import_node_fs48.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
25817
26821
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
25818
26822
  console.log("restart Claude Code to pick up the new MCP server.");
25819
26823
  console.log("");
@@ -25886,7 +26890,7 @@ async function main() {
25886
26890
  }
25887
26891
  const cmd = argvParsed.positional[0];
25888
26892
  const parsed = { ...argvParsed, positional: argvParsed.positional.slice(1) };
25889
- const { positional, apply: apply4, dryRun, noInstall } = parsed;
26893
+ const { positional, apply: apply6, dryRun, noInstall } = parsed;
25890
26894
  const project = parsed.project ?? DEFAULT_PROJECT;
25891
26895
  if (cmd === "init") {
25892
26896
  const target = positional[0];
@@ -25895,22 +26899,22 @@ async function main() {
25895
26899
  usage4();
25896
26900
  process.exit(2);
25897
26901
  }
25898
- if (apply4 && dryRun) {
26902
+ if (apply6 && dryRun) {
25899
26903
  console.error("neat init: --apply and --dry-run are mutually exclusive");
25900
26904
  process.exit(2);
25901
26905
  }
25902
- const scanPath = import_node_path81.default.resolve(target);
26906
+ const scanPath = import_node_path83.default.resolve(target);
25903
26907
  const projectExplicit = parsed.project !== null;
25904
- const projectName = projectExplicit ? project : import_node_path81.default.basename(scanPath);
26908
+ const projectName = projectExplicit ? project : import_node_path83.default.basename(scanPath);
25905
26909
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
25906
- const fallback = pathsForProject(projectKey, import_node_path81.default.join(scanPath, "neat-out")).snapshotPath;
25907
- const outPath = import_node_path81.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
26910
+ const fallback = pathsForProject(projectKey, import_node_path83.default.join(scanPath, "neat-out")).snapshotPath;
26911
+ const outPath = import_node_path83.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
25908
26912
  const result = await runInit({
25909
26913
  scanPath,
25910
26914
  outPath,
25911
26915
  project: projectName,
25912
26916
  projectExplicit,
25913
- apply: apply4,
26917
+ apply: apply6,
25914
26918
  dryRun,
25915
26919
  noInstall,
25916
26920
  verbose: parsed.verbose
@@ -25925,21 +26929,21 @@ async function main() {
25925
26929
  usage4();
25926
26930
  process.exit(2);
25927
26931
  }
25928
- const scanPath = import_node_path81.default.resolve(target);
25929
- const stat = await import_node_fs46.promises.stat(scanPath).catch(() => null);
26932
+ const scanPath = import_node_path83.default.resolve(target);
26933
+ const stat = await import_node_fs48.promises.stat(scanPath).catch(() => null);
25930
26934
  if (!stat || !stat.isDirectory()) {
25931
26935
  console.error(`neat watch: ${scanPath} is not a directory`);
25932
26936
  process.exit(2);
25933
26937
  }
25934
- const projectPaths = pathsForProject(project, import_node_path81.default.join(scanPath, "neat-out"));
25935
- const outPath = import_node_path81.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
25936
- const errorsPath = import_node_path81.default.resolve(
25937
- process.env.NEAT_ERRORS_PATH ?? import_node_path81.default.join(import_node_path81.default.dirname(outPath), import_node_path81.default.basename(projectPaths.errorsPath))
26938
+ const projectPaths = pathsForProject(project, import_node_path83.default.join(scanPath, "neat-out"));
26939
+ const outPath = import_node_path83.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
26940
+ const errorsPath = import_node_path83.default.resolve(
26941
+ process.env.NEAT_ERRORS_PATH ?? import_node_path83.default.join(import_node_path83.default.dirname(outPath), import_node_path83.default.basename(projectPaths.errorsPath))
25938
26942
  );
25939
- const staleEventsPath = import_node_path81.default.resolve(
25940
- process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path81.default.join(import_node_path81.default.dirname(outPath), import_node_path81.default.basename(projectPaths.staleEventsPath))
26943
+ const staleEventsPath = import_node_path83.default.resolve(
26944
+ process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path83.default.join(import_node_path83.default.dirname(outPath), import_node_path83.default.basename(projectPaths.staleEventsPath))
25941
26945
  );
25942
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path81.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
26946
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path83.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
25943
26947
  const handle = await startWatch(getGraph(project), {
25944
26948
  scanPath,
25945
26949
  outPath,
@@ -25948,7 +26952,7 @@ async function main() {
25948
26952
  project,
25949
26953
  // Resolve NEAT_HOME so a `neat watch` picks up connectors added to
25950
26954
  // ~/.neat/connectors.json (#871). Same resolution the rest of the CLI uses.
25951
- neatHome: process.env.NEAT_HOME ? import_node_path81.default.resolve(process.env.NEAT_HOME) : import_node_path81.default.join(import_node_os8.default.homedir(), ".neat"),
26955
+ neatHome: process.env.NEAT_HOME ? import_node_path83.default.resolve(process.env.NEAT_HOME) : import_node_path83.default.join(import_node_os8.default.homedir(), ".neat"),
25952
26956
  ...embeddingsCachePath ? { embeddingsCachePath } : {},
25953
26957
  host: process.env.HOST ?? "0.0.0.0",
25954
26958
  port: Number(process.env.PORT ?? 8080),
@@ -26130,11 +27134,11 @@ async function main() {
26130
27134
  process.exit(1);
26131
27135
  }
26132
27136
  async function tryOrchestrator(cmd, parsed) {
26133
- const scanPath = import_node_path81.default.resolve(cmd);
26134
- const stat = await import_node_fs46.promises.stat(scanPath).catch(() => null);
27137
+ const scanPath = import_node_path83.default.resolve(cmd);
27138
+ const stat = await import_node_fs48.promises.stat(scanPath).catch(() => null);
26135
27139
  if (!stat || !stat.isDirectory()) return null;
26136
27140
  const projectExplicit = parsed.project !== null;
26137
- const projectName = projectExplicit ? parsed.project : import_node_path81.default.basename(scanPath);
27141
+ const projectName = projectExplicit ? parsed.project : import_node_path83.default.basename(scanPath);
26138
27142
  const result = await runOrchestrator({
26139
27143
  scanPath,
26140
27144
  project: projectName,
@@ -26323,10 +27327,10 @@ async function runQueryVerb(cmd, parsed) {
26323
27327
  const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
26324
27328
  const out = [];
26325
27329
  for (const p of parts) {
26326
- const r = import_types85.DivergenceTypeSchema.safeParse(p);
27330
+ const r = import_types90.DivergenceTypeSchema.safeParse(p);
26327
27331
  if (!r.success) {
26328
27332
  console.error(
26329
- `neat divergences: unknown --type "${p}". allowed: ${import_types85.DivergenceTypeSchema.options.join(", ")}`
27333
+ `neat divergences: unknown --type "${p}". allowed: ${import_types90.DivergenceTypeSchema.options.join(", ")}`
26330
27334
  );
26331
27335
  return 2;
26332
27336
  }