@neat.is/core 0.9.12-dev.20260831 → 0.9.12

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.
@@ -2,10 +2,12 @@ import {
2
2
  DEFAULT_PROJECT,
3
3
  Projects,
4
4
  attachGraphToEventBus,
5
+ bearerAuthHeader,
5
6
  buildApi,
6
7
  extractFromDirectory,
7
8
  getGraph,
8
9
  handleSpan,
10
+ junctionFetch,
9
11
  listProjects,
10
12
  loadGraphFromDisk,
11
13
  makeErrorSpanWriter,
@@ -13,14 +15,17 @@ import {
13
15
  pruneRegistry,
14
16
  registryPath,
15
17
  resetGraph,
18
+ resolveCredential,
19
+ resolveFusedServiceId,
16
20
  saveGraphToDisk,
17
21
  setStatus,
22
+ startConnectorPollLoop,
18
23
  startConnectorPolling,
19
24
  startPersistLoop,
20
25
  startStalenessLoop,
21
26
  touchLastSeen,
22
27
  writeAtomically
23
- } from "./chunk-3UAUMPIY.js";
28
+ } from "./chunk-RBZNXA5L.js";
24
29
  import {
25
30
  assertBindAuthority,
26
31
  buildOtelReceiver,
@@ -36,12 +41,441 @@ import {
36
41
  unlinkSync,
37
42
  writeFileSync
38
43
  } from "fs";
39
- import path2 from "path";
44
+ import path3 from "path";
40
45
  import { createRequire } from "module";
41
46
 
47
+ // src/connectors/kubernetes/client.ts
48
+ import { Agent, request as httpsRequest } from "https";
49
+ function deploymentsPath(namespace) {
50
+ return `/apis/apps/v1/namespaces/${namespace}/deployments`;
51
+ }
52
+ function podsPath(namespace) {
53
+ return `/api/v1/namespaces/${namespace}/pods`;
54
+ }
55
+ function makeK8sFetchImpl(transport) {
56
+ const agent = new Agent({
57
+ ...transport.ca ? { ca: transport.ca } : {},
58
+ ...transport.clientCert ? { cert: transport.clientCert } : {},
59
+ ...transport.clientKey ? { key: transport.clientKey } : {},
60
+ rejectUnauthorized: !transport.insecureSkipTlsVerify
61
+ });
62
+ return ((url, init) => new Promise((resolve, reject) => {
63
+ const u = new URL(String(url));
64
+ const req = httpsRequest(
65
+ u,
66
+ {
67
+ method: (init?.method ?? "GET").toUpperCase(),
68
+ headers: init?.headers ?? {},
69
+ agent
70
+ },
71
+ (res) => {
72
+ const chunks = [];
73
+ res.on("data", (c) => chunks.push(c));
74
+ res.on("end", () => {
75
+ const body = Buffer.concat(chunks).toString("utf8");
76
+ const status = res.statusCode ?? 0;
77
+ resolve({
78
+ ok: status >= 200 && status < 300,
79
+ status,
80
+ statusText: res.statusMessage ?? "",
81
+ json: async () => JSON.parse(body),
82
+ text: async () => body
83
+ });
84
+ });
85
+ }
86
+ );
87
+ req.on("error", reject);
88
+ const signal = init?.signal;
89
+ if (signal) {
90
+ if (signal.aborted) req.destroy(new Error("aborted"));
91
+ else signal.addEventListener("abort", () => req.destroy(new Error("aborted")), { once: true });
92
+ }
93
+ req.end();
94
+ }));
95
+ }
96
+ async function listResource(transport, namespace, path4, opts = {}) {
97
+ const base = opts.apiUrl ?? transport.server;
98
+ const url = `${base.replace(/\/$/, "")}${path4}`;
99
+ const fetchImpl = opts.fetchImpl ?? makeK8sFetchImpl(transport);
100
+ const res = await junctionFetch(
101
+ url,
102
+ { method: "GET", headers: { ...transport.token ? bearerAuthHeader(transport.token) : {}, Accept: "application/json" } },
103
+ // accountKey: the (cluster, namespace) pair — an identifier, safe to log, the
104
+ // rate-limit bucket for one namespace on one cluster (ADR-131).
105
+ { provider: "kubernetes", accountKey: `${safeHost(transport.server)}/${namespace}`, fetchImpl }
106
+ );
107
+ if (!res.ok) {
108
+ throw new Error(`kubernetes ${path4} failed: ${res.status} ${res.statusText}`);
109
+ }
110
+ const json = await res.json();
111
+ return Array.isArray(json.items) ? json.items : [];
112
+ }
113
+ function safeHost(server) {
114
+ try {
115
+ return new URL(server).host;
116
+ } catch {
117
+ return "cluster";
118
+ }
119
+ }
120
+ async function fetchDeployments(transport, namespace, opts = {}) {
121
+ return listResource(transport, namespace, deploymentsPath(namespace), opts);
122
+ }
123
+ async function fetchPods(transport, namespace, opts = {}) {
124
+ return listResource(transport, namespace, podsPath(namespace), opts);
125
+ }
126
+
127
+ // src/connectors/kubernetes/kubeconfig.ts
128
+ import { readFileSync } from "fs";
129
+ import { parse as parseYaml } from "yaml";
130
+ function pemFrom(dataField, pathField) {
131
+ if (typeof dataField === "string" && dataField.length > 0) {
132
+ return Buffer.from(dataField, "base64").toString("utf8");
133
+ }
134
+ if (typeof pathField === "string" && pathField.length > 0) {
135
+ return readFileSync(pathField, "utf8");
136
+ }
137
+ return void 0;
138
+ }
139
+ function named(list, name) {
140
+ if (!Array.isArray(list)) return void 0;
141
+ const hit = list.find((e) => e && e.name === name);
142
+ return hit;
143
+ }
144
+ function parseKubeconfig(kubeconfig) {
145
+ const looksInline = /\n/.test(kubeconfig) || /(^|\s)clusters\s*:/.test(kubeconfig);
146
+ const text = looksInline ? kubeconfig : readFileSync(kubeconfig, "utf8");
147
+ let doc;
148
+ try {
149
+ doc = parseYaml(text);
150
+ } catch {
151
+ throw new Error("kubernetes connector: kubeconfig is not valid YAML");
152
+ }
153
+ if (!doc || typeof doc !== "object") {
154
+ throw new Error("kubernetes connector: kubeconfig is empty or malformed");
155
+ }
156
+ const currentContext = doc["current-context"];
157
+ if (typeof currentContext !== "string" || currentContext.length === 0) {
158
+ throw new Error("kubernetes connector: kubeconfig has no current-context");
159
+ }
160
+ const ctxEntry = named(doc["contexts"], currentContext);
161
+ const ctx = ctxEntry?.context;
162
+ if (!ctx) {
163
+ throw new Error(`kubernetes connector: kubeconfig context "${currentContext}" not found`);
164
+ }
165
+ const clusterEntry = named(doc["clusters"], String(ctx["cluster"] ?? ""));
166
+ const cluster = clusterEntry?.cluster;
167
+ if (!cluster || typeof cluster["server"] !== "string") {
168
+ throw new Error("kubernetes connector: kubeconfig current context has no cluster server");
169
+ }
170
+ const userEntry = named(doc["users"], String(ctx["user"] ?? ""));
171
+ const user = userEntry?.user ?? {};
172
+ const transport = {
173
+ server: cluster["server"],
174
+ insecureSkipTlsVerify: cluster["insecure-skip-tls-verify"] === true
175
+ };
176
+ const ca = pemFrom(cluster["certificate-authority-data"], cluster["certificate-authority"]);
177
+ if (ca) transport.ca = ca;
178
+ if (typeof user["token"] === "string" && user["token"].length > 0) transport.token = user["token"];
179
+ const clientCert = pemFrom(user["client-certificate-data"], user["client-certificate"]);
180
+ const clientKey = pemFrom(user["client-key-data"], user["client-key"]);
181
+ if (clientCert) transport.clientCert = clientCert;
182
+ if (clientKey) transport.clientKey = clientKey;
183
+ return transport;
184
+ }
185
+ function resolveK8sTransport(creds, config) {
186
+ if (creds.kubeconfig) return parseKubeconfig(creds.kubeconfig);
187
+ if (!config.apiServerUrl) {
188
+ throw new Error("kubernetes connector: options.apiServerUrl is required with a token credential");
189
+ }
190
+ const transport = {
191
+ server: config.apiServerUrl,
192
+ insecureSkipTlsVerify: config.insecureSkipTlsVerify === true
193
+ };
194
+ if (creds.token) transport.token = creds.token;
195
+ if (config.caCert) transport.ca = config.caCert;
196
+ return transport;
197
+ }
198
+
199
+ // src/connectors/kubernetes/types.ts
200
+ function readK8sCredentials(raw) {
201
+ const token = typeof raw["token"] === "string" && raw["token"].length > 0 ? raw["token"] : void 0;
202
+ const kubeconfig = typeof raw["kubeconfig"] === "string" && raw["kubeconfig"].length > 0 ? raw["kubeconfig"] : void 0;
203
+ if (!token && !kubeconfig) {
204
+ throw new Error("kubernetes connector: credentials must carry a token or a kubeconfig");
205
+ }
206
+ const out = {};
207
+ if (token) out.token = token;
208
+ if (kubeconfig) out.kubeconfig = kubeconfig;
209
+ return out;
210
+ }
211
+ var IMAGE_PULL_REASONS = /* @__PURE__ */ new Set(["ImagePullBackOff", "ErrImagePull", "InvalidImageName"]);
212
+ var CRASH_LOOP_REASON = "CrashLoopBackOff";
213
+ var FIELD_SEP = "\0";
214
+ var K8S_TARGET_KIND = "k8s-workload";
215
+ function packK8sTargetName(identity) {
216
+ return [identity.serviceName, identity.fault].join(FIELD_SEP);
217
+ }
218
+ function parseK8sTargetName(targetName) {
219
+ const sep = targetName.indexOf(FIELD_SEP);
220
+ if (sep === -1) return null;
221
+ const serviceName = targetName.slice(0, sep);
222
+ const fault = targetName.slice(sep + 1);
223
+ if (!serviceName || !fault) return null;
224
+ return { serviceName, fault };
225
+ }
226
+
227
+ // src/connectors/kubernetes/map.ts
228
+ function serviceNameFor(deployment, config) {
229
+ const name = deployment.metadata?.name ?? "";
230
+ return config.serviceMap?.[name] ?? name;
231
+ }
232
+ function podMatchesSelector(pod, selector) {
233
+ if (!selector || Object.keys(selector).length === 0) return false;
234
+ const labels = pod.metadata?.labels ?? {};
235
+ return Object.entries(selector).every(([k, v]) => labels[k] === v);
236
+ }
237
+ function podsForDeployment(deployment, pods) {
238
+ const selector = deployment.spec?.selector?.matchLabels;
239
+ return pods.filter((p) => podMatchesSelector(p, selector));
240
+ }
241
+ function nowIso() {
242
+ return (/* @__PURE__ */ new Date()).toISOString();
243
+ }
244
+ function podLevelFault(deployment, pods) {
245
+ const name = deployment.metadata?.name ?? "";
246
+ const owned = podsForDeployment(deployment, pods);
247
+ let crash = null;
248
+ for (const pod of owned) {
249
+ for (const cs of pod.status?.containerStatuses ?? []) {
250
+ const reason = cs.state?.waiting?.reason;
251
+ if (typeof reason !== "string") continue;
252
+ if (IMAGE_PULL_REASONS.has(reason)) {
253
+ const image = typeof cs.image === "string" ? cs.image : "unknown image";
254
+ const attrs = { "k8s.image": image, "k8s.waitingReason": reason };
255
+ const wm = cs.state?.waiting?.message;
256
+ if (typeof wm === "string" && wm.length > 0) attrs["k8s.waitingMessage"] = wm;
257
+ return {
258
+ fault: "image-pull",
259
+ message: `Deployment ${name} cannot pull image ${image} (${reason})`,
260
+ timestamp: pod.status?.startTime ?? nowIso(),
261
+ attributes: attrs
262
+ };
263
+ }
264
+ if (reason === CRASH_LOOP_REASON && !crash) crash = { cs, pod };
265
+ }
266
+ }
267
+ if (crash) {
268
+ const { cs, pod } = crash;
269
+ const term = cs.lastState?.terminated;
270
+ const termReason = typeof term?.reason === "string" ? term.reason : void 0;
271
+ const termMsg = typeof term?.message === "string" ? term.message.trim() : void 0;
272
+ const restarts = typeof cs.restartCount === "number" ? cs.restartCount : 0;
273
+ const detail = termReason ? `last terminated: ${termReason}${termMsg ? ` \u2014 ${termMsg}` : ""}${typeof term?.exitCode === "number" ? ` (exit ${term.exitCode})` : ""}` : "no last-termination detail reported";
274
+ const attrs = { "k8s.waitingReason": CRASH_LOOP_REASON, "k8s.restartCount": restarts };
275
+ if (termReason) attrs["k8s.terminatedReason"] = termReason;
276
+ if (termMsg) attrs["k8s.terminatedMessage"] = termMsg;
277
+ if (typeof term?.exitCode === "number") attrs["k8s.exitCode"] = term.exitCode;
278
+ if (typeof cs.image === "string") attrs["k8s.image"] = cs.image;
279
+ return {
280
+ fault: "crash-loop",
281
+ message: `Deployment ${name} is crashlooping (restarts: ${restarts}); ${detail}`,
282
+ timestamp: term?.finishedAt ?? pod.status?.startTime ?? nowIso(),
283
+ attributes: attrs
284
+ };
285
+ }
286
+ return null;
287
+ }
288
+ function classifyDeployment(deployment, pods, expectedZero) {
289
+ const name = deployment.metadata?.name ?? "";
290
+ const desired = typeof deployment.spec?.replicas === "number" ? deployment.spec.replicas : 1;
291
+ const ready = typeof deployment.status?.readyReplicas === "number" ? deployment.status.readyReplicas : 0;
292
+ if (desired === 0) {
293
+ if (expectedZero?.has(name)) return null;
294
+ return {
295
+ fault: "scaled-to-zero",
296
+ message: `Deployment ${name} is scaled to 0 \u2014 no running pods (desired 0)`,
297
+ timestamp: nowIso(),
298
+ attributes: { "k8s.desiredReplicas": 0, "k8s.readyReplicas": ready }
299
+ };
300
+ }
301
+ if (ready >= desired) return null;
302
+ const podFault = podLevelFault(deployment, pods);
303
+ if (podFault) {
304
+ podFault.attributes["k8s.desiredReplicas"] = desired;
305
+ podFault.attributes["k8s.readyReplicas"] = ready;
306
+ return podFault;
307
+ }
308
+ return {
309
+ fault: "no-ready-replicas",
310
+ message: `Deployment ${name} has no ready replicas (desired ${desired}, ready ${ready})`,
311
+ timestamp: nowIso(),
312
+ attributes: { "k8s.desiredReplicas": desired, "k8s.readyReplicas": ready }
313
+ };
314
+ }
315
+ function mapDeploymentToSignal(deployment, pods, config) {
316
+ const name = deployment.metadata?.name;
317
+ if (typeof name !== "string" || name.length === 0) return null;
318
+ const expectedZero = config.expectedZero ? new Set(config.expectedZero) : void 0;
319
+ const finding = classifyDeployment(deployment, pods, expectedZero);
320
+ if (!finding) return null;
321
+ const serviceName = serviceNameFor(deployment, config);
322
+ const namespace = deployment.metadata?.namespace ?? config.namespace;
323
+ const attributes = {
324
+ "k8s.namespace": namespace,
325
+ "k8s.deployment": name,
326
+ "k8s.fault": finding.fault,
327
+ ...finding.attributes
328
+ };
329
+ return {
330
+ targetKind: K8S_TARGET_KIND,
331
+ targetName: packK8sTargetName({ serviceName, fault: finding.fault }),
332
+ // Incident-only — no edge, so no call/error count to replay.
333
+ callCount: 0,
334
+ errorCount: 0,
335
+ lastObservedIso: finding.timestamp,
336
+ incident: {
337
+ id: `k8s:deploy:${namespace}:${name}:${finding.fault}`,
338
+ timestamp: finding.timestamp,
339
+ service: serviceName,
340
+ errorType: "k8s-deploy-failure",
341
+ errorMessage: finding.message,
342
+ attributes
343
+ }
344
+ };
345
+ }
346
+ function mapWorkloadsToSignals(deployments, pods, config) {
347
+ const out = [];
348
+ for (const deployment of deployments) {
349
+ const signal = mapDeploymentToSignal(deployment, pods, config);
350
+ if (signal) out.push(signal);
351
+ }
352
+ return out;
353
+ }
354
+
355
+ // src/connectors/kubernetes/resolve.ts
356
+ import { EdgeType } from "@neat.is/types";
357
+ var NO_ENV = "unknown";
358
+ function createK8sResolveTarget(graph) {
359
+ return (signal) => {
360
+ if (signal.targetKind !== K8S_TARGET_KIND) return null;
361
+ const identity = parseK8sTargetName(signal.targetName);
362
+ if (!identity) return null;
363
+ const { serviceName } = identity;
364
+ return {
365
+ targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV),
366
+ serviceName,
367
+ edgeType: EdgeType.CALLS
368
+ };
369
+ };
370
+ }
371
+
372
+ // src/connectors/kubernetes/substrate.ts
373
+ import { readFile } from "fs/promises";
374
+ import os from "os";
375
+ import path from "path";
376
+ function defaultHome() {
377
+ const override = process.env.NEAT_HOME;
378
+ if (override && override.length > 0) return path.resolve(override);
379
+ return path.join(os.homedir(), ".neat");
380
+ }
381
+ function k8sSubstrateConfigPath(home = defaultHome()) {
382
+ return path.join(home, "k8s.json");
383
+ }
384
+ async function readK8sSubstrateConfig(home = defaultHome()) {
385
+ let raw;
386
+ try {
387
+ raw = await readFile(k8sSubstrateConfigPath(home), "utf8");
388
+ } catch {
389
+ return { version: 1, deployments: [] };
390
+ }
391
+ try {
392
+ const parsed = JSON.parse(raw);
393
+ if (!parsed || !Array.isArray(parsed.deployments)) return { version: 1, deployments: [] };
394
+ return parsed;
395
+ } catch {
396
+ return { version: 1, deployments: [] };
397
+ }
398
+ }
399
+ async function startK8sSubstratePolling(input) {
400
+ const config = await readK8sSubstrateConfig(input.home);
401
+ const env = input.env ?? process.env;
402
+ const stops = [];
403
+ for (const entry of config.deployments) {
404
+ if (entry.project !== void 0 && entry.project !== input.project) continue;
405
+ if (typeof entry.namespace !== "string" || entry.namespace.length === 0) {
406
+ input.onSkip?.(entry, "missing namespace");
407
+ continue;
408
+ }
409
+ let credentials;
410
+ try {
411
+ const resolved = resolveCredential(entry.credential, env);
412
+ credentials = resolved.kind === "fields" ? { ...resolved.fields } : { token: resolved.value };
413
+ } catch (err) {
414
+ input.onSkip?.(entry, err.message);
415
+ continue;
416
+ }
417
+ const cfg = {
418
+ namespace: entry.namespace,
419
+ ...entry.apiServerUrl ? { apiServerUrl: entry.apiServerUrl } : {},
420
+ ...entry.caCert ? { caCert: entry.caCert } : {},
421
+ ...entry.insecureSkipTlsVerify ? { insecureSkipTlsVerify: true } : {},
422
+ ...entry.serviceMap ? { serviceMap: entry.serviceMap } : {},
423
+ ...entry.expectedZero ? { expectedZero: entry.expectedZero } : {}
424
+ };
425
+ const { connector, resolveTarget } = createKubernetesConnector(input.graph, cfg, input.fetchImpl);
426
+ const stop = startConnectorPollLoop(
427
+ connector,
428
+ {
429
+ projectDir: input.projectDir,
430
+ project: input.project,
431
+ credentials,
432
+ ...input.errorsPath ? { errorsPath: input.errorsPath } : {}
433
+ },
434
+ input.graph,
435
+ resolveTarget,
436
+ { connectorId: `k8s:${entry.id}`, ...entry.intervalMs ? { intervalMs: entry.intervalMs } : {} }
437
+ );
438
+ stops.push(stop);
439
+ }
440
+ return () => {
441
+ for (const stop of stops) stop();
442
+ };
443
+ }
444
+
445
+ // src/connectors/kubernetes/index.ts
446
+ var KubernetesConnector = class {
447
+ constructor(config, fetchImpl) {
448
+ this.config = config;
449
+ this.fetchImpl = fetchImpl;
450
+ }
451
+ config;
452
+ fetchImpl;
453
+ provider = "kubernetes";
454
+ async poll(ctx) {
455
+ const creds = readK8sCredentials(ctx.credentials);
456
+ const transport = resolveK8sTransport(creds, this.config);
457
+ const namespace = this.config.namespace;
458
+ const opts = {
459
+ ...this.config.apiUrl ? { apiUrl: this.config.apiUrl } : {},
460
+ ...this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}
461
+ };
462
+ const [deployments, pods] = await Promise.all([
463
+ fetchDeployments(transport, namespace, opts),
464
+ fetchPods(transport, namespace, opts)
465
+ ]);
466
+ return mapWorkloadsToSignals(deployments, pods, this.config);
467
+ }
468
+ };
469
+ function createKubernetesConnector(graph, config, fetchImpl) {
470
+ return {
471
+ connector: new KubernetesConnector(config, fetchImpl),
472
+ resolveTarget: createK8sResolveTarget(graph)
473
+ };
474
+ }
475
+
42
476
  // src/unrouted.ts
43
477
  import { promises as fs } from "fs";
44
- import path from "path";
478
+ import path2 from "path";
45
479
  function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
46
480
  return {
47
481
  timestamp: now.toISOString(),
@@ -51,34 +485,34 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
51
485
  };
52
486
  }
53
487
  async function appendUnroutedSpan(neatHome, record) {
54
- const target = path.join(neatHome, "errors.ndjson");
488
+ const target = path2.join(neatHome, "errors.ndjson");
55
489
  await fs.mkdir(neatHome, { recursive: true });
56
490
  await fs.appendFile(target, JSON.stringify(record) + "\n", "utf8");
57
491
  }
58
492
  function unroutedErrorsPath(neatHome) {
59
- return path.join(neatHome, "errors.ndjson");
493
+ return path2.join(neatHome, "errors.ndjson");
60
494
  }
61
495
 
62
496
  // src/daemon.ts
63
497
  import { NodeType } from "@neat.is/types";
64
498
  function daemonJsonPath(scanPath) {
65
- return path2.join(scanPath, "neat-out", "daemon.json");
499
+ return path3.join(scanPath, "neat-out", "daemon.json");
66
500
  }
67
501
  function daemonsDiscoveryDir(home) {
68
502
  const base = home && home.length > 0 ? home : neatHomeFromEnv();
69
- return path2.join(base, "daemons");
503
+ return path3.join(base, "daemons");
70
504
  }
71
505
  function daemonDiscoveryPath(project, home) {
72
- return path2.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
506
+ return path3.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
73
507
  }
74
508
  function sanitizeDiscoveryName(project) {
75
509
  return project.replace(/[^A-Za-z0-9._-]/g, "_");
76
510
  }
77
511
  function neatHomeFromEnv() {
78
512
  const env = process.env.NEAT_HOME;
79
- if (env && env.length > 0) return path2.resolve(env);
513
+ if (env && env.length > 0) return path3.resolve(env);
80
514
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
81
- return path2.join(home, ".neat");
515
+ return path3.join(home, ".neat");
82
516
  }
83
517
  async function readDaemonRecord(scanPath) {
84
518
  try {
@@ -153,17 +587,21 @@ function teardownSlot(slot) {
153
587
  slot.stopConnectors();
154
588
  } catch {
155
589
  }
590
+ try {
591
+ slot.stopK8sSubstrate();
592
+ } catch {
593
+ }
156
594
  try {
157
595
  slot.detachEvents();
158
596
  } catch {
159
597
  }
160
598
  }
161
599
  function neatHomeFor(opts) {
162
- if (opts.neatHome && opts.neatHome.length > 0) return path2.resolve(opts.neatHome);
600
+ if (opts.neatHome && opts.neatHome.length > 0) return path3.resolve(opts.neatHome);
163
601
  const env = process.env.NEAT_HOME;
164
- if (env && env.length > 0) return path2.resolve(env);
602
+ if (env && env.length > 0) return path3.resolve(env);
165
603
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
166
- return path2.join(home, ".neat");
604
+ return path3.join(home, ".neat");
167
605
  }
168
606
  function routeSpanToProject(serviceName, projects) {
169
607
  if (!serviceName) return DEFAULT_PROJECT;
@@ -215,7 +653,7 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
215
653
  );
216
654
  }
217
655
  async function bootstrapProject(entry, connectors = [], neatHome) {
218
- const paths = pathsForProject(entry.name, path2.join(entry.path, "neat-out"));
656
+ const paths = pathsForProject(entry.name, path3.join(entry.path, "neat-out"));
219
657
  try {
220
658
  const stat = await fs2.stat(entry.path);
221
659
  if (!stat.isDirectory()) {
@@ -237,6 +675,8 @@ async function bootstrapProject(entry, connectors = [], neatHome) {
237
675
  },
238
676
  stopConnectors: () => {
239
677
  },
678
+ stopK8sSubstrate: () => {
679
+ },
240
680
  detachEvents: () => {
241
681
  },
242
682
  status: "broken",
@@ -269,6 +709,14 @@ async function bootstrapProject(entry, connectors = [], neatHome) {
269
709
  `neatd: connector "${skipped.id}" (${skipped.provider}) skipped for project "${entry.name}" \u2014 ${reason}`
270
710
  )
271
711
  });
712
+ const stopK8sSubstrate = await startK8sSubstratePolling({
713
+ project: entry.name,
714
+ graph,
715
+ projectDir: entry.path,
716
+ errorsPath: paths.errorsPath,
717
+ ...neatHome ? { home: neatHome } : {},
718
+ onSkip: (skipped, reason) => console.warn(`neatd: k8s substrate "${skipped.id}" skipped for project "${entry.name}" \u2014 ${reason}`)
719
+ });
272
720
  await touchLastSeen(entry.name).catch(() => {
273
721
  });
274
722
  return {
@@ -279,6 +727,7 @@ async function bootstrapProject(entry, connectors = [], neatHome) {
279
727
  stopPersist,
280
728
  stopStaleness,
281
729
  stopConnectors,
730
+ stopK8sSubstrate,
282
731
  detachEvents,
283
732
  status: "active"
284
733
  };
@@ -335,7 +784,7 @@ async function startDaemon(opts = {}) {
335
784
  const projectArg = typeof opts.project === "string" && opts.project.length > 0 ? opts.project : process.env.NEAT_PROJECT && process.env.NEAT_PROJECT.length > 0 ? process.env.NEAT_PROJECT : null;
336
785
  const projectPathArg = opts.projectPath && opts.projectPath.length > 0 ? opts.projectPath : process.env.NEAT_PROJECT_PATH && process.env.NEAT_PROJECT_PATH.length > 0 ? process.env.NEAT_PROJECT_PATH : null;
337
786
  const singleProject = projectArg;
338
- const singleProjectPath = singleProject && projectPathArg ? path2.resolve(projectPathArg) : null;
787
+ const singleProjectPath = singleProject && projectPathArg ? path3.resolve(projectPathArg) : null;
339
788
  if (singleProject && !singleProjectPath) {
340
789
  throw new Error(
341
790
  `neatd: project "${singleProject}" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`
@@ -350,7 +799,7 @@ async function startDaemon(opts = {}) {
350
799
  );
351
800
  }
352
801
  }
353
- const pidPath = path2.join(home, "neatd.pid");
802
+ const pidPath = path3.join(home, "neatd.pid");
354
803
  await writeAtomically(pidPath, `${process.pid}
355
804
  `);
356
805
  const slots = /* @__PURE__ */ new Map();
@@ -822,8 +1271,8 @@ async function startDaemon(opts = {}) {
822
1271
  let registryWatcher = null;
823
1272
  let reloadTimer = null;
824
1273
  if (!singleProject) try {
825
- const regDir = path2.dirname(regPath);
826
- const regBase = path2.basename(regPath);
1274
+ const regDir = path3.dirname(regPath);
1275
+ const regBase = path3.basename(regPath);
827
1276
  registryWatcher = watch(regDir, (_eventType, filename) => {
828
1277
  if (filename !== null && filename !== regBase) return;
829
1278
  if (reloadTimer) clearTimeout(reloadTimer);
@@ -901,4 +1350,4 @@ export {
901
1350
  resolveHost,
902
1351
  startDaemon
903
1352
  };
904
- //# sourceMappingURL=chunk-UNO3X6ZV.js.map
1353
+ //# sourceMappingURL=chunk-WE3AFQYL.js.map