@neat.is/core 0.4.17 → 0.4.18

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.cjs CHANGED
@@ -473,8 +473,10 @@ async function buildOtelReceiver(opts) {
473
473
  for (const s of spans) projectQueue.push({ project, span: s });
474
474
  projectDrainPromise = projectDrainPromise.then(() => drainProject());
475
475
  };
476
+ const offersProjectRouting = opts.onProjectSpan !== void 0;
476
477
  const legacyEndpointWarned = /* @__PURE__ */ new Set();
477
478
  function warnLegacyEndpoint(serviceName) {
479
+ if (!offersProjectRouting) return;
478
480
  if (legacyEndpointWarned.has(serviceName)) return;
479
481
  legacyEndpointWarned.add(serviceName);
480
482
  console.warn(
@@ -7029,6 +7031,7 @@ init_otel_grpc();
7029
7031
  init_cjs_shims();
7030
7032
  var import_node_fs25 = require("fs");
7031
7033
  var import_node_path42 = __toESM(require("path"), 1);
7034
+ var import_node_module = require("module");
7032
7035
  init_otel();
7033
7036
  init_auth();
7034
7037
 
@@ -7054,6 +7057,59 @@ function unroutedErrorsPath(neatHome2) {
7054
7057
  }
7055
7058
 
7056
7059
  // src/daemon.ts
7060
+ function daemonJsonPath(scanPath) {
7061
+ return import_node_path42.default.join(scanPath, "neat-out", "daemon.json");
7062
+ }
7063
+ function daemonsDiscoveryDir(home) {
7064
+ const base = home && home.length > 0 ? home : neatHomeFromEnv();
7065
+ return import_node_path42.default.join(base, "daemons");
7066
+ }
7067
+ function daemonDiscoveryPath(project, home) {
7068
+ return import_node_path42.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
7069
+ }
7070
+ function sanitizeDiscoveryName(project) {
7071
+ return project.replace(/[^A-Za-z0-9._-]/g, "_");
7072
+ }
7073
+ function neatHomeFromEnv() {
7074
+ const env = process.env.NEAT_HOME;
7075
+ if (env && env.length > 0) return import_node_path42.default.resolve(env);
7076
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
7077
+ return import_node_path42.default.join(home, ".neat");
7078
+ }
7079
+ function resolveNeatVersion() {
7080
+ if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
7081
+ return process.env.NEAT_LOCAL_VERSION;
7082
+ }
7083
+ try {
7084
+ const req = (0, import_node_module.createRequire)(importMetaUrl);
7085
+ const pkg = req("../package.json");
7086
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
7087
+ } catch {
7088
+ return "0.0.0";
7089
+ }
7090
+ }
7091
+ async function writeDaemonRecord(record, home) {
7092
+ const body = JSON.stringify(record, null, 2) + "\n";
7093
+ await writeAtomically(daemonJsonPath(record.projectPath), body);
7094
+ try {
7095
+ await writeAtomically(daemonDiscoveryPath(record.project, home), body);
7096
+ } catch (err) {
7097
+ console.warn(
7098
+ `neatd: could not write discovery copy for "${record.project}" \u2014 ${err.message}`
7099
+ );
7100
+ }
7101
+ }
7102
+ async function clearDaemonRecord(record, home) {
7103
+ try {
7104
+ const stopped = { ...record, status: "stopped" };
7105
+ await writeAtomically(daemonJsonPath(record.projectPath), JSON.stringify(stopped, null, 2) + "\n");
7106
+ } catch {
7107
+ }
7108
+ try {
7109
+ await import_node_fs25.promises.unlink(daemonDiscoveryPath(record.project, home));
7110
+ } catch {
7111
+ }
7112
+ }
7057
7113
  function teardownSlot(slot) {
7058
7114
  try {
7059
7115
  slot.stopPersist();
@@ -7170,6 +7226,23 @@ function resolveOtlpPort(opts) {
7170
7226
  }
7171
7227
  return 4318;
7172
7228
  }
7229
+ function resolveWebPort() {
7230
+ const env = process.env.NEAT_WEB_PORT;
7231
+ if (env && env.length > 0) {
7232
+ const n = Number.parseInt(env, 10);
7233
+ if (Number.isFinite(n)) return n;
7234
+ }
7235
+ return 6328;
7236
+ }
7237
+ function portFromListenAddress(address, fallback) {
7238
+ try {
7239
+ const port = new URL(address).port;
7240
+ const n = Number.parseInt(port, 10);
7241
+ if (Number.isFinite(n) && n > 0) return n;
7242
+ } catch {
7243
+ }
7244
+ return fallback;
7245
+ }
7173
7246
  function resolveHost(opts, authTokenSet) {
7174
7247
  if (opts.host && opts.host.length > 0) return opts.host;
7175
7248
  const env = process.env.HOST;
@@ -7180,13 +7253,24 @@ function resolveHost(opts, authTokenSet) {
7180
7253
  async function startDaemon(opts = {}) {
7181
7254
  const home = neatHomeFor(opts);
7182
7255
  const regPath = registryPath();
7183
- try {
7184
- await import_node_fs25.promises.access(regPath);
7185
- } catch {
7256
+ 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;
7257
+ 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;
7258
+ const singleProject = projectArg;
7259
+ const singleProjectPath = singleProject && projectPathArg ? import_node_path42.default.resolve(projectPathArg) : null;
7260
+ if (singleProject && !singleProjectPath) {
7186
7261
  throw new Error(
7187
- `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
7262
+ `neatd: project "${singleProject}" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`
7188
7263
  );
7189
7264
  }
7265
+ if (!singleProject) {
7266
+ try {
7267
+ await import_node_fs25.promises.access(regPath);
7268
+ } catch {
7269
+ throw new Error(
7270
+ `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
7271
+ );
7272
+ }
7273
+ }
7190
7274
  const pidPath = import_node_path42.default.join(home, "neatd.pid");
7191
7275
  await writeAtomically(pidPath, `${process.pid}
7192
7276
  `);
@@ -7275,21 +7359,37 @@ async function startDaemon(opts = {}) {
7275
7359
  });
7276
7360
  }
7277
7361
  }
7362
+ async function enumerateProjects() {
7363
+ if (singleProject && singleProjectPath) {
7364
+ return [
7365
+ {
7366
+ name: singleProject,
7367
+ path: singleProjectPath,
7368
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
7369
+ languages: [],
7370
+ status: "active"
7371
+ }
7372
+ ];
7373
+ }
7374
+ return listProjects();
7375
+ }
7278
7376
  async function loadAll() {
7279
- try {
7280
- const pruned = await pruneRegistry();
7281
- for (const entry of pruned) {
7282
- console.log(
7283
- `neatd: pruned project "${entry.name}" \u2014 registered path ${entry.path} is gone`
7284
- );
7285
- slots.delete(entry.name);
7286
- bootstrapStatus.delete(entry.name);
7287
- bootstrapStartedAt.delete(entry.name);
7377
+ if (!singleProject) {
7378
+ try {
7379
+ const pruned = await pruneRegistry();
7380
+ for (const entry of pruned) {
7381
+ console.log(
7382
+ `neatd: pruned project "${entry.name}" \u2014 registered path ${entry.path} is gone`
7383
+ );
7384
+ slots.delete(entry.name);
7385
+ bootstrapStatus.delete(entry.name);
7386
+ bootstrapStartedAt.delete(entry.name);
7387
+ }
7388
+ } catch (err) {
7389
+ console.warn(`neatd: registry prune skipped \u2014 ${err.message}`);
7288
7390
  }
7289
- } catch (err) {
7290
- console.warn(`neatd: registry prune skipped \u2014 ${err.message}`);
7291
7391
  }
7292
- const projects = await listProjects();
7392
+ const projects = await enumerateProjects();
7293
7393
  const seen = /* @__PURE__ */ new Set();
7294
7394
  const pending = [];
7295
7395
  for (const entry of projects) {
@@ -7314,7 +7414,7 @@ async function startDaemon(opts = {}) {
7314
7414
  }
7315
7415
  await Promise.allSettled(pending);
7316
7416
  }
7317
- const initialEntries = await listProjects().catch(() => []);
7417
+ const initialEntries = await enumerateProjects().catch(() => []);
7318
7418
  for (const entry of initialEntries) {
7319
7419
  bootstrapStatus.set(entry.name, "bootstrapping");
7320
7420
  bootstrapStartedAt.set(entry.name, Date.now());
@@ -7324,6 +7424,7 @@ async function startDaemon(opts = {}) {
7324
7424
  let otlpApp = null;
7325
7425
  let restAddress = "";
7326
7426
  let otlpAddress = "";
7427
+ let daemonRecord = null;
7327
7428
  if (bind) {
7328
7429
  const auth = readAuthEnv();
7329
7430
  const host = resolveHost(opts, Boolean(auth.authToken));
@@ -7348,6 +7449,20 @@ async function startDaemon(opts = {}) {
7348
7449
  }
7349
7450
  }
7350
7451
  });
7452
+ if (singleProject) {
7453
+ restApp.addHook("onSend", async (req, _reply, payload) => {
7454
+ if (req.url.split("?")[0] !== "/health") return payload;
7455
+ if (typeof payload !== "string") return payload;
7456
+ try {
7457
+ const body = JSON.parse(payload);
7458
+ if (typeof body !== "object" || body === null) return payload;
7459
+ body.project = singleProject;
7460
+ return JSON.stringify(body);
7461
+ } catch {
7462
+ return payload;
7463
+ }
7464
+ });
7465
+ }
7351
7466
  restAddress = await restApp.listen({ port: restPort, host });
7352
7467
  console.log(`neatd: REST listening on ${restAddress}`);
7353
7468
  } catch (err) {
@@ -7363,6 +7478,25 @@ async function startDaemon(opts = {}) {
7363
7478
  );
7364
7479
  }
7365
7480
  async function resolveTargetSlot(serviceName, traceId) {
7481
+ if (singleProject) {
7482
+ let slot2 = slots.get(singleProject);
7483
+ if (!slot2) {
7484
+ slot2 = await tryRecoverSlot({
7485
+ name: singleProject,
7486
+ path: singleProjectPath,
7487
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
7488
+ languages: [],
7489
+ status: "active"
7490
+ });
7491
+ } else if (slot2.status === "broken") {
7492
+ slot2 = await tryRecoverSlot(slot2.entry);
7493
+ }
7494
+ if (!slot2 || slot2.status !== "active") {
7495
+ warnDroppedSpan(singleProject, slot2?.errorReason ?? "unknown");
7496
+ return null;
7497
+ }
7498
+ return slot2;
7499
+ }
7366
7500
  const liveEntries = await listProjects().catch(() => []);
7367
7501
  const target = routeSpanToProject(serviceName, liveEntries);
7368
7502
  let slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
@@ -7465,6 +7599,42 @@ async function startDaemon(opts = {}) {
7465
7599
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
7466
7600
  );
7467
7601
  }
7602
+ if (singleProject && singleProjectPath) {
7603
+ const ports = {
7604
+ rest: portFromListenAddress(restAddress, restPort),
7605
+ otlp: portFromListenAddress(otlpAddress, otlpPort),
7606
+ // The daemon doesn't bind the web port itself (neatd spawns the web
7607
+ // child); it records the allocated value passed through so the
7608
+ // dashboard and `neat ps` agree on where to look.
7609
+ web: typeof opts.webPort === "number" ? opts.webPort : resolveWebPort()
7610
+ };
7611
+ daemonRecord = {
7612
+ project: singleProject,
7613
+ projectPath: singleProjectPath,
7614
+ pid: process.pid,
7615
+ status: "running",
7616
+ ports,
7617
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
7618
+ neatVersion: resolveNeatVersion()
7619
+ };
7620
+ try {
7621
+ await writeDaemonRecord(daemonRecord, home);
7622
+ console.log(
7623
+ `neatd: project "${singleProject}" \u2192 REST ${ports.rest} / OTLP ${ports.otlp} / web ${ports.web} (daemon.json written)`
7624
+ );
7625
+ } catch (err) {
7626
+ for (const slot of slots.values()) teardownSlot(slot);
7627
+ if (restApp) await restApp.close().catch(() => {
7628
+ });
7629
+ if (otlpApp) await otlpApp.close().catch(() => {
7630
+ });
7631
+ await import_node_fs25.promises.unlink(pidPath).catch(() => {
7632
+ });
7633
+ throw new Error(
7634
+ `neatd: failed to write daemon.json for "${singleProject}" \u2014 ${err.message}`
7635
+ );
7636
+ }
7637
+ }
7468
7638
  }
7469
7639
  const initialBootstrap = loadAll().catch((err) => {
7470
7640
  console.warn(`neatd: initial bootstrap pass failed \u2014 ${err.message}`);
@@ -7504,7 +7674,7 @@ async function startDaemon(opts = {}) {
7504
7674
  const REGISTRY_RELOAD_DEBOUNCE_MS = 500;
7505
7675
  let registryWatcher = null;
7506
7676
  let reloadTimer = null;
7507
- try {
7677
+ if (!singleProject) try {
7508
7678
  const regDir = import_node_path42.default.dirname(regPath);
7509
7679
  const regBase = import_node_path42.default.basename(regPath);
7510
7680
  registryWatcher = (0, import_node_fs25.watch)(regDir, (_eventType, filename) => {
@@ -7547,6 +7717,9 @@ async function startDaemon(opts = {}) {
7547
7717
  for (const slot of slots.values()) {
7548
7718
  teardownSlot(slot);
7549
7719
  }
7720
+ if (daemonRecord) {
7721
+ await clearDaemonRecord(daemonRecord, home);
7722
+ }
7550
7723
  await import_node_fs25.promises.unlink(pidPath).catch(() => {
7551
7724
  });
7552
7725
  };
@@ -7558,7 +7731,8 @@ async function startDaemon(opts = {}) {
7558
7731
  restAddress,
7559
7732
  otlpAddress,
7560
7733
  bootstrap: tracker,
7561
- initialBootstrap
7734
+ initialBootstrap,
7735
+ daemonRecord
7562
7736
  };
7563
7737
  }
7564
7738
  // Annotate the CommonJS export names for ESM import in node: