@neat.is/core 0.10.3 → 0.10.5-dev.20261001

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/index.d.cts CHANGED
@@ -45,6 +45,7 @@ interface ExtractOptions {
45
45
  onPolicyTrigger?: (graph: NeatGraph) => Promise<void> | void;
46
46
  project?: string;
47
47
  errorsPath?: string;
48
+ source?: string;
48
49
  }
49
50
  declare function extractFromDirectory(graph: NeatGraph, scanPath: string, opts?: ExtractOptions): Promise<ExtractResult>;
50
51
 
@@ -126,6 +127,29 @@ declare class Projects {
126
127
  attachSearchIndex(name: string, index: SearchIndex | undefined): void;
127
128
  }
128
129
 
130
+ /** What one pass did: whether the bound-repo list was read, and how the repos it took on came out. */
131
+ interface RepoSyncPassCounts {
132
+ listed: boolean;
133
+ synced: number;
134
+ failed: number;
135
+ }
136
+ /** A finished pass, as the on-demand trigger reports it. */
137
+ interface RepoSyncPassSummary extends RepoSyncPassCounts {
138
+ startedAt: string;
139
+ finishedAt: string;
140
+ }
141
+ /**
142
+ * The answer to "sync now" (#1293). `started` — nothing was running, a pass has begun. `queued` — a pass was
143
+ * already running, and one more will follow it. Joining the running pass outright would be wrong: it read
144
+ * the bound-repo list before this request, so a repo bound since would be missed; the follow-up re-reads the
145
+ * list. Any number of requests during one pass coalesce into that single follow-up. `lastPass` is the most
146
+ * recent pass to finish, absent until one has.
147
+ */
148
+ interface RepoSyncRequestResult {
149
+ status: 'started' | 'queued';
150
+ lastPass?: RepoSyncPassSummary;
151
+ }
152
+
129
153
  /**
130
154
  * A connector implements exactly one method. Everything downstream of
131
155
  * `poll()` — resolving a static call site, minting the OBSERVED edge — is
@@ -293,6 +317,7 @@ interface BuildApiOptions {
293
317
  authToken?: string;
294
318
  trustProxy?: boolean;
295
319
  publicRead?: boolean;
320
+ otlpReceiver?: () => FastifyInstance | undefined;
296
321
  bootstrap?: {
297
322
  status: (name: string) => 'bootstrapping' | 'active' | 'broken' | undefined;
298
323
  list: () => Array<{
@@ -306,6 +331,7 @@ interface BuildApiOptions {
306
331
  path: string;
307
332
  };
308
333
  connectorsHome?: string;
334
+ repoSync?: (project: string) => (() => RepoSyncRequestResult) | undefined;
309
335
  runPoll?: typeof runConnectorPoll;
310
336
  }
311
337
  declare function buildApi(opts: BuildApiOptions): Promise<FastifyInstance>;
@@ -597,6 +623,7 @@ interface ProjectSlot {
597
623
  stopHostedConnectors: () => void;
598
624
  stopK8sSubstrate: () => void;
599
625
  stopRepoSync: () => void;
626
+ syncRepos?: () => RepoSyncRequestResult;
600
627
  detachEvents: () => void;
601
628
  status: 'active' | 'broken';
602
629
  errorReason?: string;
package/dist/index.d.ts CHANGED
@@ -45,6 +45,7 @@ interface ExtractOptions {
45
45
  onPolicyTrigger?: (graph: NeatGraph) => Promise<void> | void;
46
46
  project?: string;
47
47
  errorsPath?: string;
48
+ source?: string;
48
49
  }
49
50
  declare function extractFromDirectory(graph: NeatGraph, scanPath: string, opts?: ExtractOptions): Promise<ExtractResult>;
50
51
 
@@ -126,6 +127,29 @@ declare class Projects {
126
127
  attachSearchIndex(name: string, index: SearchIndex | undefined): void;
127
128
  }
128
129
 
130
+ /** What one pass did: whether the bound-repo list was read, and how the repos it took on came out. */
131
+ interface RepoSyncPassCounts {
132
+ listed: boolean;
133
+ synced: number;
134
+ failed: number;
135
+ }
136
+ /** A finished pass, as the on-demand trigger reports it. */
137
+ interface RepoSyncPassSummary extends RepoSyncPassCounts {
138
+ startedAt: string;
139
+ finishedAt: string;
140
+ }
141
+ /**
142
+ * The answer to "sync now" (#1293). `started` — nothing was running, a pass has begun. `queued` — a pass was
143
+ * already running, and one more will follow it. Joining the running pass outright would be wrong: it read
144
+ * the bound-repo list before this request, so a repo bound since would be missed; the follow-up re-reads the
145
+ * list. Any number of requests during one pass coalesce into that single follow-up. `lastPass` is the most
146
+ * recent pass to finish, absent until one has.
147
+ */
148
+ interface RepoSyncRequestResult {
149
+ status: 'started' | 'queued';
150
+ lastPass?: RepoSyncPassSummary;
151
+ }
152
+
129
153
  /**
130
154
  * A connector implements exactly one method. Everything downstream of
131
155
  * `poll()` — resolving a static call site, minting the OBSERVED edge — is
@@ -293,6 +317,7 @@ interface BuildApiOptions {
293
317
  authToken?: string;
294
318
  trustProxy?: boolean;
295
319
  publicRead?: boolean;
320
+ otlpReceiver?: () => FastifyInstance | undefined;
296
321
  bootstrap?: {
297
322
  status: (name: string) => 'bootstrapping' | 'active' | 'broken' | undefined;
298
323
  list: () => Array<{
@@ -306,6 +331,7 @@ interface BuildApiOptions {
306
331
  path: string;
307
332
  };
308
333
  connectorsHome?: string;
334
+ repoSync?: (project: string) => (() => RepoSyncRequestResult) | undefined;
309
335
  runPoll?: typeof runConnectorPoll;
310
336
  }
311
337
  declare function buildApi(opts: BuildApiOptions): Promise<FastifyInstance>;
@@ -597,6 +623,7 @@ interface ProjectSlot {
597
623
  stopHostedConnectors: () => void;
598
624
  stopK8sSubstrate: () => void;
599
625
  stopRepoSync: () => void;
626
+ syncRepos?: () => RepoSyncRequestResult;
600
627
  detachEvents: () => void;
601
628
  status: 'active' | 'broken';
602
629
  errorReason?: string;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  routeSpanToProject,
3
3
  startDaemon
4
- } from "./chunk-DLQNMU3R.js";
4
+ } from "./chunk-TONL5O2E.js";
5
5
  import {
6
6
  ProjectNameCollisionError,
7
7
  addProject,
@@ -37,15 +37,15 @@ import {
37
37
  thresholdForEdgeType,
38
38
  touchLastSeen,
39
39
  writeAtomically
40
- } from "./chunk-LNIVRE4L.js";
40
+ } from "./chunk-HHAPD6JD.js";
41
41
  import {
42
42
  startOtelGrpcReceiver
43
- } from "./chunk-5SJHA5ZH.js";
43
+ } from "./chunk-LXQCL4MP.js";
44
44
  import {
45
45
  buildOtelReceiver,
46
46
  logSpanHandler,
47
47
  parseOtlpRequest
48
- } from "./chunk-M7CGATVC.js";
48
+ } from "./chunk-EFBWQIKG.js";
49
49
  export {
50
50
  ProjectNameCollisionError,
51
51
  addProject,
package/dist/neatd.cjs CHANGED
@@ -51,6 +51,10 @@ function assertBindAuthority(host, token) {
51
51
  if (isLoopbackHost2(host)) return;
52
52
  throw new BindAuthorityError(host);
53
53
  }
54
+ function isBearerDelegated(req2) {
55
+ const config = req2.routeOptions?.config;
56
+ return config?.bearerDelegated === true;
57
+ }
54
58
  function mountBearerAuth(app, opts) {
55
59
  if (!opts.token || opts.token.length === 0) return;
56
60
  if (opts.trustProxy) return;
@@ -66,6 +70,10 @@ function mountBearerAuth(app, opts) {
66
70
  done();
67
71
  return;
68
72
  }
73
+ if (isBearerDelegated(req2)) {
74
+ done();
75
+ return;
76
+ }
69
77
  if (publicRead && PUBLIC_READ_METHODS.has(req2.method)) {
70
78
  done();
71
79
  return;
@@ -99,7 +107,7 @@ function readAuthEnv(env = process.env) {
99
107
  publicRead: parseBoolEnv(env.NEAT_PUBLIC_READ)
100
108
  };
101
109
  }
102
- var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_PATHS, PROJECT_SCOPED_UNAUTH_PATTERN;
110
+ var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_PATHS, PROJECT_SCOPED_UNAUTH_PATTERN, BEARER_DELEGATED;
103
111
  var init_auth = __esm({
104
112
  "src/auth.ts"() {
105
113
  "use strict";
@@ -129,6 +137,7 @@ var init_auth = __esm({
129
137
  PROJECT_SCOPED_UNAUTH_PATTERN = new RegExp(
130
138
  `^/projects/[^/]+/(?:${DEFAULT_UNAUTH_PATHS.map((p) => p.slice(1)).join("|")})$`
131
139
  );
140
+ BEARER_DELEGATED = { bearerDelegated: true };
132
141
  }
133
142
  });
134
143
 
@@ -3353,6 +3362,10 @@ function languageForPath(relPath) {
3353
3362
  return void 0;
3354
3363
  }
3355
3364
  }
3365
+ var currentSource;
3366
+ function setExtractionSource(source) {
3367
+ currentSource = source;
3368
+ }
3356
3369
  function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3357
3370
  let nodesAdded = 0;
3358
3371
  let edgesAdded = 0;
@@ -3365,10 +3378,16 @@ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3365
3378
  service: serviceName,
3366
3379
  path: relPath,
3367
3380
  ...language ? { language } : {},
3381
+ ...currentSource ? { source: currentSource } : {},
3368
3382
  discoveredVia: "static"
3369
3383
  };
3370
3384
  graph.addNode(fileNodeId, node);
3371
3385
  nodesAdded++;
3386
+ } else if (currentSource) {
3387
+ const existing = graph.getNodeAttributes(fileNodeId);
3388
+ if (existing.source !== currentSource) {
3389
+ graph.replaceNodeAttributes(fileNodeId, { ...existing, source: currentSource });
3390
+ }
3372
3391
  }
3373
3392
  const containsId = (0, import_types4.extractedEdgeId)(serviceNodeId, fileNodeId, import_types4.EdgeType.CONTAINS);
3374
3393
  if (!graph.hasEdge(containsId)) {
@@ -16426,10 +16445,12 @@ init_cjs_shims();
16426
16445
  var import_node_fs33 = require("fs");
16427
16446
  var import_node_path68 = __toESM(require("path"), 1);
16428
16447
  var import_types58 = require("@neat.is/types");
16429
- function dropOrphanedFileNodes(graph) {
16448
+ function dropOrphanedFileNodes(graph, source) {
16430
16449
  const orphans = [];
16431
16450
  graph.forEachNode((id, attrs) => {
16432
- if (attrs.type !== import_types58.NodeType.FileNode) return;
16451
+ const node = attrs;
16452
+ if (node.type !== import_types58.NodeType.FileNode) return;
16453
+ if (source && node.source !== source) return;
16433
16454
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
16434
16455
  orphans.push(id);
16435
16456
  }
@@ -16437,12 +16458,23 @@ function dropOrphanedFileNodes(graph) {
16437
16458
  for (const id of orphans) graph.dropNode(id);
16438
16459
  return orphans.length;
16439
16460
  }
16440
- function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
16461
+ function outOfScope(graph, edge, source) {
16462
+ if (!source) return false;
16463
+ for (const endpoint2 of [edge.source, edge.target]) {
16464
+ if (!graph.hasNode(endpoint2)) continue;
16465
+ const node = graph.getNodeAttributes(endpoint2);
16466
+ if (node.type !== import_types58.NodeType.FileNode) continue;
16467
+ return node.source !== source;
16468
+ }
16469
+ return false;
16470
+ }
16471
+ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = [], source) {
16441
16472
  const toDrop = [];
16442
16473
  const bases = [scanPath, ...serviceDirs];
16443
16474
  graph.forEachEdge((id, attrs) => {
16444
16475
  const edge = attrs;
16445
16476
  if (edge.provenance !== import_types58.Provenance.EXTRACTED) return;
16477
+ if (outOfScope(graph, edge, source)) return;
16446
16478
  const evidenceFile = edge.evidence?.file;
16447
16479
  if (!evidenceFile) return;
16448
16480
  if (import_node_path68.default.isAbsolute(evidenceFile)) {
@@ -16453,7 +16485,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
16453
16485
  if (!found) toDrop.push(id);
16454
16486
  });
16455
16487
  for (const id of toDrop) graph.dropEdge(id);
16456
- dropOrphanedFileNodes(graph);
16488
+ dropOrphanedFileNodes(graph, source);
16457
16489
  return toDrop.length;
16458
16490
  }
16459
16491
 
@@ -16462,6 +16494,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
16462
16494
  await ensureCompatLoaded();
16463
16495
  drainExtractionErrors();
16464
16496
  drainSkippedFiles();
16497
+ setExtractionSource(opts.source);
16465
16498
  const services = await discoverServices(scanPath);
16466
16499
  const phase1Nodes = addServiceNodes(graph, services);
16467
16500
  await addServiceAliases(graph, scanPath, services);
@@ -16482,7 +16515,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
16482
16515
  const ghostsRetired = retireExtractedEdgesByMissingFile(
16483
16516
  graph,
16484
16517
  scanPath,
16485
- services.map((s) => s.dir)
16518
+ services.map((s) => s.dir),
16519
+ opts.source
16486
16520
  );
16487
16521
  const frontiersPromoted = promoteFrontierNodes(graph);
16488
16522
  if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
@@ -16517,6 +16551,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
16517
16551
  );
16518
16552
  }
16519
16553
  }
16554
+ setExtractionSource(void 0);
16520
16555
  const result = {
16521
16556
  nodesAdded: phase1Nodes + fileEnum.nodesAdded + symbolEnum.nodesAdded + importGraph.nodesAdded + symbolEdges.nodesAdded + actionPhase.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + routePhase.nodesAdded + grpcPhase.nodesAdded + phase4.nodesAdded + tableEdges.nodesAdded + phase5.nodesAdded + zodPhase.nodesAdded + firestoreRulesPhase.nodesAdded,
16522
16557
  edgesAdded: fileEnum.edgesAdded + symbolEnum.edgesAdded + importGraph.edgesAdded + symbolEdges.edgesAdded + actionPhase.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + routePhase.edgesAdded + grpcPhase.edgesAdded + phase4.edgesAdded + tableEdges.edgesAdded + phase5.edgesAdded + zodPhase.edgesAdded + firestoreRulesPhase.edgesAdded,
@@ -23518,6 +23553,19 @@ function registerRoutes(scope, ctx) {
23518
23553
  });
23519
23554
  }
23520
23555
  });
23556
+ scope.post("/repo-sync", async (req2, reply) => {
23557
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
23558
+ if (!proj) return;
23559
+ const syncNow = ctx.repoSync?.(proj.name);
23560
+ if (!syncNow) {
23561
+ return reply.code(404).send({
23562
+ error: "repo-sync is not running for this project",
23563
+ project: proj.name,
23564
+ hint: "Only a hosted daemon syncs bound repos. On a local daemon, POST /graph/scan re-extracts the project path."
23565
+ });
23566
+ }
23567
+ return reply.code(202).send({ project: proj.name, ...syncNow() });
23568
+ });
23521
23569
  scope.post("/graph/scan", async (req2, reply) => {
23522
23570
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
23523
23571
  if (!proj) return;
@@ -23765,6 +23813,39 @@ function registerRoutes(scope, ctx) {
23765
23813
  }
23766
23814
  });
23767
23815
  }
23816
+ var OTLP_BODY_LIMIT = 16 * 1024 * 1024;
23817
+ function otlpOnRest(getReceiver) {
23818
+ return async function plugin(scope) {
23819
+ scope.removeAllContentTypeParsers();
23820
+ scope.addContentTypeParser(
23821
+ "*",
23822
+ { parseAs: "buffer", bodyLimit: OTLP_BODY_LIMIT },
23823
+ (_req, body, done) => done(null, body)
23824
+ );
23825
+ const relay = async (req2, reply) => {
23826
+ const receiver = getReceiver();
23827
+ if (!receiver) {
23828
+ return reply.code(503).header("retry-after", "1").send({ error: "OTLP receiver is starting" });
23829
+ }
23830
+ const headers = { ...req2.headers };
23831
+ delete headers["content-length"];
23832
+ delete headers["transfer-encoding"];
23833
+ delete headers.connection;
23834
+ const res = await receiver.inject({
23835
+ method: "POST",
23836
+ url: req2.url,
23837
+ headers,
23838
+ payload: req2.body
23839
+ });
23840
+ const contentType = res.headers["content-type"];
23841
+ if (contentType !== void 0) void reply.header("content-type", contentType);
23842
+ return reply.code(res.statusCode).send(res.rawPayload);
23843
+ };
23844
+ const route = { config: BEARER_DELEGATED, bodyLimit: OTLP_BODY_LIMIT };
23845
+ scope.post("/v1/traces", route, relay);
23846
+ scope.post("/projects/:project/v1/traces", route, relay);
23847
+ };
23848
+ }
23768
23849
  async function buildApi(opts) {
23769
23850
  const app = (0, import_fastify2.default)({ logger: false, routerOptions: { maxParamLength: 1024 } });
23770
23851
  await app.register(import_cors.default, { origin: true });
@@ -23783,6 +23864,7 @@ async function buildApi(opts) {
23783
23864
  authProxy: trustProxy === true,
23784
23865
  requiresAuth
23785
23866
  }));
23867
+ if (opts.otlpReceiver) await app.register(otlpOnRest(opts.otlpReceiver));
23786
23868
  const startedAt = opts.startedAt ?? Date.now();
23787
23869
  const registry = buildLegacyRegistry(opts);
23788
23870
  const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
@@ -23813,6 +23895,7 @@ async function buildApi(opts) {
23813
23895
  bootstrap: opts.bootstrap,
23814
23896
  singleProject: opts.singleProject?.name,
23815
23897
  connectorsHome: opts.connectorsHome,
23898
+ repoSync: opts.repoSync,
23816
23899
  runPoll: opts.runPoll ?? runConnectorPoll
23817
23900
  };
23818
23901
  app.get("/health", async () => {
@@ -24363,7 +24446,7 @@ async function syncOneRepo(r, input) {
24363
24446
  await cpPostStatus(deps, r.owner, r.name, { syncStatus: "syncing", detail: "cloning" }).catch(() => {
24364
24447
  });
24365
24448
  await cloneRepo(r.cloneUrl, r.defaultBranch, dir);
24366
- const extracted = await extract(graph, dir);
24449
+ const extracted = await extract(graph, dir, { source: label });
24367
24450
  const nodes = extracted?.nodesAdded ?? 0;
24368
24451
  const edges = extracted?.edgesAdded ?? 0;
24369
24452
  await cpPostStatus(deps, r.owner, r.name, {
@@ -24371,6 +24454,7 @@ async function syncOneRepo(r, input) {
24371
24454
  detail: `extracted ${nodes} node${nodes === 1 ? "" : "s"}, ${edges} edge${edges === 1 ? "" : "s"}`,
24372
24455
  lastSyncAt: new Date(input.now?.() ?? Date.now()).toISOString()
24373
24456
  });
24457
+ return true;
24374
24458
  } catch (err) {
24375
24459
  input.onError?.(label, err);
24376
24460
  await cpPostStatus(deps, r.owner, r.name, {
@@ -24378,50 +24462,76 @@ async function syncOneRepo(r, input) {
24378
24462
  detail: scrubToken(err.message).slice(0, 300)
24379
24463
  }).catch(() => {
24380
24464
  });
24465
+ return false;
24381
24466
  } finally {
24382
24467
  if (dir) await (0, import_promises2.rm)(dir, { recursive: true, force: true }).catch(() => {
24383
24468
  });
24384
24469
  }
24385
24470
  }
24386
- async function runRepoSyncPass(input) {
24471
+ async function runRepoSyncPassCounted(input) {
24472
+ const counts = { listed: false, synced: 0, failed: 0 };
24387
24473
  let repos;
24388
24474
  try {
24389
24475
  repos = await cpGet2(`/internal/projects/${input.deps.projectId}/repos`, input.deps);
24390
24476
  } catch (err) {
24391
24477
  input.onSkip?.("(all)", `control plane repo list unreadable \u2014 ${err.message}`);
24392
- return false;
24478
+ return counts;
24393
24479
  }
24394
- if (!Array.isArray(repos)) return false;
24480
+ if (!Array.isArray(repos)) return counts;
24481
+ counts.listed = true;
24395
24482
  for (const r of repos) {
24396
24483
  if (!input.forceResync && !needsSync(r)) continue;
24397
- await syncOneRepo(r, input);
24484
+ if (await syncOneRepo(r, input)) counts.synced++;
24485
+ else counts.failed++;
24398
24486
  }
24399
- return true;
24487
+ return counts;
24400
24488
  }
24401
24489
  async function startRepoSync(input) {
24402
24490
  const intervalMs = input.intervalMs ?? DEFAULT_SYNC_INTERVAL_MS;
24491
+ const now = () => input.now?.() ?? Date.now();
24403
24492
  let stopped = false;
24404
- let running = false;
24493
+ let inFlight = null;
24494
+ let again = false;
24495
+ let lastPass;
24405
24496
  let bootResyncDone = false;
24406
- const tick = async () => {
24407
- if (stopped || running) return;
24408
- running = true;
24497
+ const run = async () => {
24409
24498
  try {
24410
- const listed = await runRepoSyncPass({ ...input, forceResync: !bootResyncDone });
24411
- if (listed) bootResyncDone = true;
24499
+ do {
24500
+ again = false;
24501
+ const startedAt = new Date(now()).toISOString();
24502
+ const counts = await runRepoSyncPassCounted({ ...input, forceResync: !bootResyncDone });
24503
+ if (counts.listed) bootResyncDone = true;
24504
+ lastPass = { ...counts, startedAt, finishedAt: new Date(now()).toISOString() };
24505
+ } while (again && !stopped);
24412
24506
  } finally {
24413
- running = false;
24507
+ inFlight = null;
24414
24508
  }
24415
24509
  };
24416
- void tick();
24417
- const timer = setInterval(() => {
24418
- void tick();
24419
- }, intervalMs);
24510
+ const tick = () => {
24511
+ if (stopped || inFlight) return;
24512
+ inFlight = run();
24513
+ };
24514
+ tick();
24515
+ const timer = setInterval(tick, intervalMs);
24420
24516
  if (typeof timer.unref === "function") timer.unref();
24421
- return () => {
24517
+ const stop = () => {
24422
24518
  stopped = true;
24423
24519
  clearInterval(timer);
24424
24520
  };
24521
+ return Object.assign(stop, {
24522
+ syncNow: () => {
24523
+ const summary = lastPass ? { lastPass } : {};
24524
+ if (inFlight) {
24525
+ again = true;
24526
+ return { status: "queued", ...summary };
24527
+ }
24528
+ inFlight = run();
24529
+ return { status: "started", ...summary };
24530
+ },
24531
+ settled: async () => {
24532
+ while (inFlight) await inFlight;
24533
+ }
24534
+ });
24425
24535
  }
24426
24536
  async function maybeStartRepoSync(input) {
24427
24537
  const env = input.env ?? process.env;
@@ -25133,7 +25243,7 @@ async function bootstrapProject(entry2, connectors = [], neatHome4) {
25133
25243
  await loadGraphFromDisk(graph, outPath);
25134
25244
  const detachEvents = attachGraphToEventBus(graph, { project: entry2.name });
25135
25245
  try {
25136
- await extractFromDirectory(graph, entry2.path);
25246
+ await extractFromDirectory(graph, entry2.path, { source: entry2.name });
25137
25247
  const stopPersist = startPersistLoop(graph, outPath, { exitOnSignal: false });
25138
25248
  const stopStaleness = startStalenessLoop(graph, {
25139
25249
  staleEventsPath: paths.staleEventsPath,
@@ -25191,6 +25301,7 @@ async function bootstrapProject(entry2, connectors = [], neatHome4) {
25191
25301
  stopHostedConnectors,
25192
25302
  stopK8sSubstrate,
25193
25303
  stopRepoSync,
25304
+ ...stopRepoSync.syncNow ? { syncRepos: stopRepoSync.syncNow } : {},
25194
25305
  detachEvents,
25195
25306
  status: "active"
25196
25307
  };
@@ -25462,7 +25573,14 @@ async function startDaemon(opts = {}) {
25462
25573
  // through the same resolved home the slot bootstrap read it from, so a
25463
25574
  // daemon given an explicit NEAT_HOME serves status for the same file it
25464
25575
  // polls.
25465
- connectorsHome: home
25576
+ connectorsHome: home,
25577
+ // #1293 — "sync this project's repos now". Looked up per request: the
25578
+ // slot, and with it the trigger, appears once bootstrap has run.
25579
+ repoSync: (name) => slots.get(name)?.syncRepos,
25580
+ // OTLP answers on this listener too, so a host that routes a single
25581
+ // port still delivers a tenant app's spans. Bound late: the receiver
25582
+ // is built below, once the REST bind has succeeded.
25583
+ otlpReceiver: () => otlpApp ?? void 0
25466
25584
  });
25467
25585
  restAddress = await restApp.listen({ port: restPort, host });
25468
25586
  console.log(
@@ -25643,7 +25761,9 @@ async function startDaemon(opts = {}) {
25643
25761
  }
25644
25762
  });
25645
25763
  otlpAddress = await listenSteppingOtlp(otlpApp, otlpPort, host);
25646
- console.log(`neatd: OTLP listening on ${otlpAddress}/v1/traces`);
25764
+ console.log(
25765
+ `neatd: OTLP listening on http://${host}:${portFromListenAddress(otlpAddress, otlpPort)}/v1/traces (also at /v1/traces on the REST port)`
25766
+ );
25647
25767
  } catch (err) {
25648
25768
  for (const slot of slots.values()) {
25649
25769
  teardownSlot(slot);