@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/cli.cjs CHANGED
@@ -58,9 +58,9 @@ function mountBearerAuth(app, opts) {
58
58
  const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
59
59
  const publicRead = opts.publicRead === true;
60
60
  app.addHook("preHandler", (req, reply, done) => {
61
- const path51 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
61
+ const path53 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
62
62
  for (const suffix of suffixes) {
63
- if (path51 === suffix || path51.endsWith(suffix)) {
63
+ if (path53 === suffix || path53.endsWith(suffix)) {
64
64
  done();
65
65
  return;
66
66
  }
@@ -474,8 +474,10 @@ async function buildOtelReceiver(opts) {
474
474
  for (const s of spans) projectQueue.push({ project, span: s });
475
475
  projectDrainPromise = projectDrainPromise.then(() => drainProject());
476
476
  };
477
+ const offersProjectRouting = opts.onProjectSpan !== void 0;
477
478
  const legacyEndpointWarned = /* @__PURE__ */ new Set();
478
479
  function warnLegacyEndpoint(serviceName) {
480
+ if (!offersProjectRouting) return;
479
481
  if (legacyEndpointWarned.has(serviceName)) return;
480
482
  legacyEndpointWarned.add(serviceName);
481
483
  console.warn(
@@ -611,8 +613,8 @@ __export(cli_exports, {
611
613
  });
612
614
  module.exports = __toCommonJS(cli_exports);
613
615
  init_cjs_shims();
614
- var import_node_path50 = __toESM(require("path"), 1);
615
- var import_node_fs32 = require("fs");
616
+ var import_node_path52 = __toESM(require("path"), 1);
617
+ var import_node_fs34 = require("fs");
616
618
 
617
619
  // src/banner.ts
618
620
  init_cjs_shims();
@@ -1177,19 +1179,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1177
1179
  function longestIncomingWalk(graph, start, maxDepth) {
1178
1180
  let best = { path: [start], edges: [] };
1179
1181
  const visited = /* @__PURE__ */ new Set([start]);
1180
- function step(node, path51, edges) {
1181
- if (path51.length > best.path.length) {
1182
- best = { path: [...path51], edges: [...edges] };
1182
+ function step(node, path53, edges) {
1183
+ if (path53.length > best.path.length) {
1184
+ best = { path: [...path53], edges: [...edges] };
1183
1185
  }
1184
- if (path51.length - 1 >= maxDepth) return;
1186
+ if (path53.length - 1 >= maxDepth) return;
1185
1187
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1186
1188
  for (const [srcId, edge] of incoming) {
1187
1189
  if (visited.has(srcId)) continue;
1188
1190
  visited.add(srcId);
1189
- path51.push(srcId);
1191
+ path53.push(srcId);
1190
1192
  edges.push(edge);
1191
- step(srcId, path51, edges);
1192
- path51.pop();
1193
+ step(srcId, path53, edges);
1194
+ path53.pop();
1193
1195
  edges.pop();
1194
1196
  visited.delete(srcId);
1195
1197
  }
@@ -6259,6 +6261,111 @@ function registryLockPath() {
6259
6261
  function daemonPidPath() {
6260
6262
  return import_node_path40.default.join(neatHome(), "neatd.pid");
6261
6263
  }
6264
+ function daemonsDir() {
6265
+ return import_node_path40.default.join(neatHome(), "daemons");
6266
+ }
6267
+ function isFiniteInt(v) {
6268
+ return typeof v === "number" && Number.isFinite(v);
6269
+ }
6270
+ function parseDaemonRecord(raw) {
6271
+ let obj;
6272
+ try {
6273
+ obj = JSON.parse(raw);
6274
+ } catch {
6275
+ return void 0;
6276
+ }
6277
+ if (typeof obj !== "object" || obj === null) return void 0;
6278
+ const r = obj;
6279
+ const ports = r.ports;
6280
+ if (typeof r.project !== "string" || typeof r.projectPath !== "string" || !isFiniteInt(r.pid) || r.status !== "running" && r.status !== "stopped" || typeof r.startedAt !== "string" || typeof r.neatVersion !== "string" || !ports || !isFiniteInt(ports.rest) || !isFiniteInt(ports.otlp) || !isFiniteInt(ports.web)) {
6281
+ return void 0;
6282
+ }
6283
+ return {
6284
+ project: r.project,
6285
+ projectPath: r.projectPath,
6286
+ pid: r.pid,
6287
+ status: r.status,
6288
+ ports: { rest: ports.rest, otlp: ports.otlp, web: ports.web },
6289
+ startedAt: r.startedAt,
6290
+ neatVersion: r.neatVersion
6291
+ };
6292
+ }
6293
+ var defaultDiscoveryProbe = { isPidAlive: isPidAliveDefault };
6294
+ async function discoverDaemons(probe = defaultDiscoveryProbe) {
6295
+ const dir = daemonsDir();
6296
+ let names;
6297
+ try {
6298
+ names = await import_node_fs25.promises.readdir(dir);
6299
+ } catch (err) {
6300
+ if (err.code === "ENOENT") return [];
6301
+ throw err;
6302
+ }
6303
+ const out = [];
6304
+ for (const name of names) {
6305
+ if (!name.endsWith(".json")) continue;
6306
+ const file = import_node_path40.default.join(dir, name);
6307
+ let raw;
6308
+ try {
6309
+ raw = await import_node_fs25.promises.readFile(file, "utf8");
6310
+ } catch {
6311
+ continue;
6312
+ }
6313
+ const record = parseDaemonRecord(raw);
6314
+ if (!record) continue;
6315
+ const live = record.status === "running" && probe.isPidAlive(record.pid);
6316
+ out.push({ record, live, source: file });
6317
+ }
6318
+ out.sort((a, b) => a.record.project.localeCompare(b.record.project));
6319
+ return out;
6320
+ }
6321
+ async function removeDaemonRecord(source) {
6322
+ await import_node_fs25.promises.unlink(source).catch(() => {
6323
+ });
6324
+ }
6325
+ async function listMachineProjects(probe = defaultDiscoveryProbe) {
6326
+ const discovered = await discoverDaemons(probe);
6327
+ const byPath = /* @__PURE__ */ new Map();
6328
+ for (const d of discovered) {
6329
+ const key = await normalizeProjectPath(d.record.projectPath);
6330
+ byPath.set(key, {
6331
+ project: d.record.project,
6332
+ projectPath: d.record.projectPath,
6333
+ state: d.live ? "running" : "stopped",
6334
+ ports: d.record.ports,
6335
+ pid: d.record.pid
6336
+ });
6337
+ }
6338
+ let legacy = [];
6339
+ try {
6340
+ legacy = (await readRegistry()).projects;
6341
+ } catch {
6342
+ legacy = [];
6343
+ }
6344
+ for (const entry2 of legacy) {
6345
+ const key = await normalizeProjectPath(entry2.path);
6346
+ if (byPath.has(key)) continue;
6347
+ byPath.set(key, {
6348
+ project: entry2.name,
6349
+ projectPath: entry2.path,
6350
+ state: "registered",
6351
+ registryStatus: entry2.status
6352
+ });
6353
+ }
6354
+ return [...byPath.values()].sort((a, b) => a.project.localeCompare(b.project));
6355
+ }
6356
+ async function findDaemonByProject(name, probe = defaultDiscoveryProbe) {
6357
+ const discovered = await discoverDaemons(probe);
6358
+ return discovered.find((d) => d.record.project === name);
6359
+ }
6360
+ function signalDaemonStop(pid) {
6361
+ try {
6362
+ process.kill(pid, "SIGTERM");
6363
+ return true;
6364
+ } catch (err) {
6365
+ if (err.code === "ESRCH") return false;
6366
+ throw err;
6367
+ }
6368
+ }
6262
6369
  function isPidAliveDefault(pid) {
6263
6370
  try {
6264
6371
  process.kill(pid, 0);
@@ -7994,9 +8101,50 @@ var import_semver2 = __toESM(require("semver"), 1);
7994
8101
  // src/installers/templates.ts
7995
8102
  init_cjs_shims();
7996
8103
  var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
7997
- var OTEL_INIT_STAMP = "// neat-template-version: 5 \u2014 layered file-first capture (ADR-090) + http/json protocol pin (#468).";
8104
+ var OTEL_INIT_STAMP = "// neat-template-version: 6 \u2014 daemon.json endpoint resolution (ADR-096) + layered file-first capture (ADR-090).";
7998
8105
  var OTEL_OTLP_HEADERS_JS = "if (process.env.NEAT_OTEL_TOKEN) process.env.OTEL_EXPORTER_OTLP_HEADERS ||= 'Authorization=Bearer ' + process.env.NEAT_OTEL_TOKEN";
7999
8106
  var OTEL_OTLP_PROTOCOL_JS = "process.env.OTEL_EXPORTER_OTLP_PROTOCOL ||= 'http/json'";
8107
+ var OTEL_ENDPOINT_RESOLVER_CJS = `;(function () {
8108
+ try {
8109
+ if (process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_ENDPOINT) return
8110
+ const __neatFs = require('node:fs')
8111
+ const __neatPath = require('node:path')
8112
+ let __neatDir = process.cwd()
8113
+ for (let __i = 0; __i < 8; __i++) {
8114
+ try {
8115
+ const __rec = JSON.parse(__neatFs.readFileSync(__neatPath.join(__neatDir, 'neat-out', 'daemon.json'), 'utf8'))
8116
+ if (__rec && __rec.ports && typeof __rec.ports.otlp === 'number') {
8117
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://localhost:' + __rec.ports.otlp + '/v1/traces'
8118
+ break
8119
+ }
8120
+ } catch (_e) {}
8121
+ const __parent = __neatPath.dirname(__neatDir)
8122
+ if (__parent === __neatDir) break
8123
+ __neatDir = __parent
8124
+ }
8125
+ } catch (_e) {}
8126
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/v1/traces'
8127
+ })()`;
8128
+ var OTEL_ESM_NODE_IMPORTS = "import { readFileSync as __neatReadFileSync } from 'node:fs'\nimport { join as __neatJoin, dirname as __neatDirname } from 'node:path'";
8129
+ var OTEL_ENDPOINT_RESOLVER_ESM = `;(function () {
8130
+ try {
8131
+ if (process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_ENDPOINT) return
8132
+ let __neatDir = process.cwd()
8133
+ for (let __i = 0; __i < 8; __i++) {
8134
+ try {
8135
+ const __rec = JSON.parse(__neatReadFileSync(__neatJoin(__neatDir, 'neat-out', 'daemon.json'), 'utf8'))
8136
+ if (__rec && __rec.ports && typeof __rec.ports.otlp === 'number') {
8137
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://localhost:' + __rec.ports.otlp + '/v1/traces'
8138
+ break
8139
+ }
8140
+ } catch (_e) {}
8141
+ const __parent = __neatDirname(__neatDir)
8142
+ if (__parent === __neatDir) break
8143
+ __neatDir = __parent
8144
+ }
8145
+ } catch (_e) {}
8146
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/v1/traces'
8147
+ })()`;
8000
8148
  function neatCaptureSource(ts) {
8001
8149
  const spanT = ts ? ": any" : "";
8002
8150
  const strOpt = ts ? ": string | undefined" : "";
@@ -8285,7 +8433,7 @@ try {
8285
8433
  var OTEL_INIT_CJS = `${OTEL_INIT_HEADER}
8286
8434
  ${OTEL_INIT_STAMP}
8287
8435
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
8288
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
8436
+ ${OTEL_ENDPOINT_RESOLVER_CJS}
8289
8437
  ${OTEL_OTLP_PROTOCOL_JS}
8290
8438
  ${OTEL_OTLP_HEADERS_JS}
8291
8439
 
@@ -8303,8 +8451,9 @@ ${neatWireCaptureSource(false)}
8303
8451
  `;
8304
8452
  var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
8305
8453
  ${OTEL_INIT_STAMP}
8454
+ ${OTEL_ESM_NODE_IMPORTS}
8306
8455
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
8307
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
8456
+ ${OTEL_ENDPOINT_RESOLVER_ESM}
8308
8457
  ${OTEL_OTLP_PROTOCOL_JS}
8309
8458
  ${OTEL_OTLP_HEADERS_JS}
8310
8459
 
@@ -8327,8 +8476,9 @@ ${OTEL_INIT_STAMP}
8327
8476
  // strict user tsconfig would reject; suppressing type-checking here keeps the
8328
8477
  // generated file from breaking the host project's \`tsc\` gate (#427) without
8329
8478
  // constraining the runtime logic. The file is regenerated, never hand-edited.
8479
+ ${OTEL_ESM_NODE_IMPORTS}
8330
8480
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
8331
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
8481
+ ${OTEL_ENDPOINT_RESOLVER_ESM}
8332
8482
  ${OTEL_OTLP_PROTOCOL_JS}
8333
8483
  ${OTEL_OTLP_HEADERS_JS}
8334
8484
 
@@ -8350,11 +8500,14 @@ ${registrations.join("\n")}
8350
8500
  `;
8351
8501
  return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName).replace(/__INSTRUMENTATION_BLOCK__\n?/g, block);
8352
8502
  }
8353
- function renderEnvNeat(serviceName, projectName) {
8503
+ function renderEnvNeat(serviceName, _projectName) {
8354
8504
  return [
8355
8505
  "# Generated by `neat init --apply` (ADR-069).",
8356
8506
  `OTEL_SERVICE_NAME=${serviceName}`,
8357
- `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/projects/${projectName}/v1/traces`,
8507
+ "# Advisory only \u2014 the generated otel-init resolves the live endpoint from",
8508
+ "# <project>/neat-out/daemon.json (ports.otlp) at boot (ADR-096). This is the",
8509
+ "# canonical default the daemon takes when its first-choice OTLP port is free.",
8510
+ "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces",
8358
8511
  "OTEL_EXPORTER_OTLP_PROTOCOL=http/json",
8359
8512
  "# Set NEAT_OTEL_TOKEN to the daemon's OTLP secret to authenticate exported spans (#410).",
8360
8513
  "# NEAT_OTEL_TOKEN=",
@@ -8377,8 +8530,9 @@ export async function register() {
8377
8530
  }
8378
8531
  `;
8379
8532
  var NEXT_INSTRUMENTATION_NODE_TS = `${NEXT_INSTRUMENTATION_HEADER}
8533
+ ${OTEL_ESM_NODE_IMPORTS}
8380
8534
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
8381
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
8535
+ ${OTEL_ENDPOINT_RESOLVER_ESM}
8382
8536
  ${OTEL_OTLP_PROTOCOL_JS}
8383
8537
  ${OTEL_OTLP_HEADERS_JS}
8384
8538
 
@@ -8391,7 +8545,7 @@ new NodeSDK({ instrumentations }).start()
8391
8545
  `;
8392
8546
  var NEXT_INSTRUMENTATION_NODE_JS = `${NEXT_INSTRUMENTATION_HEADER}
8393
8547
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
8394
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
8548
+ ${OTEL_ENDPOINT_RESOLVER_CJS}
8395
8549
  ${OTEL_OTLP_PROTOCOL_JS}
8396
8550
  ${OTEL_OTLP_HEADERS_JS}
8397
8551
 
@@ -8410,8 +8564,9 @@ ${registrations.join("\n")}
8410
8564
  }
8411
8565
  var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
8412
8566
  var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
8567
+ ${OTEL_ESM_NODE_IMPORTS}
8413
8568
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
8414
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
8569
+ ${OTEL_ENDPOINT_RESOLVER_ESM}
8415
8570
  ${OTEL_OTLP_PROTOCOL_JS}
8416
8571
  ${OTEL_OTLP_HEADERS_JS}
8417
8572
 
@@ -8426,7 +8581,7 @@ sdk.start()
8426
8581
  `;
8427
8582
  var FRAMEWORK_OTEL_INIT_JS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
8428
8583
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
8429
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
8584
+ ${OTEL_ENDPOINT_RESOLVER_CJS}
8430
8585
  ${OTEL_OTLP_PROTOCOL_JS}
8431
8586
  ${OTEL_OTLP_HEADERS_JS}
8432
8587
 
@@ -9895,19 +10050,51 @@ function renderPatch(sections) {
9895
10050
 
9896
10051
  // src/orchestrator.ts
9897
10052
  init_cjs_shims();
9898
- var import_node_fs31 = require("fs");
10053
+ var import_node_fs33 = require("fs");
9899
10054
  var import_node_http = __toESM(require("http"), 1);
9900
10055
  var import_node_net = __toESM(require("net"), 1);
9901
- var import_node_path48 = __toESM(require("path"), 1);
10056
+ var import_node_path50 = __toESM(require("path"), 1);
9902
10057
  var import_node_child_process3 = require("child_process");
9903
10058
  var import_node_readline = __toESM(require("readline"), 1);
10059
+
10060
+ // src/daemon.ts
10061
+ init_cjs_shims();
10062
+ var import_node_fs32 = require("fs");
10063
+ var import_node_path49 = __toESM(require("path"), 1);
10064
+ var import_node_module = require("module");
10065
+ init_otel();
10066
+ init_auth();
10067
+
10068
+ // src/unrouted.ts
10069
+ init_cjs_shims();
10070
+ var import_node_fs31 = require("fs");
10071
+ var import_node_path48 = __toESM(require("path"), 1);
10072
+
10073
+ // src/daemon.ts
10074
+ function daemonJsonPath(scanPath) {
10075
+ return import_node_path49.default.join(scanPath, "neat-out", "daemon.json");
10076
+ }
10077
+ async function readDaemonRecord(scanPath) {
10078
+ try {
10079
+ const raw = await import_node_fs32.promises.readFile(daemonJsonPath(scanPath), "utf8");
10080
+ const parsed = JSON.parse(raw);
10081
+ if (typeof parsed.project === "string" && parsed.ports && typeof parsed.ports.rest === "number" && typeof parsed.ports.otlp === "number" && typeof parsed.ports.web === "number") {
10082
+ return parsed;
10083
+ }
10084
+ return null;
10085
+ } catch {
10086
+ return null;
10087
+ }
10088
+ }
10089
+
10090
+ // src/orchestrator.ts
9904
10091
  async function extractAndPersist(opts) {
9905
10092
  const services = await discoverServices(opts.scanPath);
9906
10093
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
9907
10094
  const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
9908
10095
  resetGraph(graphKey);
9909
10096
  const graph = getGraph(graphKey);
9910
- const projectPaths = pathsForProject(graphKey, import_node_path48.default.join(opts.scanPath, "neat-out"));
10097
+ const projectPaths = pathsForProject(graphKey, import_node_path50.default.join(opts.scanPath, "neat-out"));
9911
10098
  const extraction = await extractFromDirectory(graph, opts.scanPath, {
9912
10099
  errorsPath: projectPaths.errorsPath
9913
10100
  });
@@ -9961,7 +10148,7 @@ async function applyInstallersOver(services, project, options = {}) {
9961
10148
  console.log(`skipping ${svc.dir}: browser bundle; browser-OTel support lands in a future release.`);
9962
10149
  } else if (outcome.outcome === "react-native") {
9963
10150
  reactNative++;
9964
- const svcName = import_node_path48.default.basename(svc.dir);
10151
+ const svcName = import_node_path50.default.basename(svc.dir);
9965
10152
  console.log(
9966
10153
  `neat: ${svc.dir} detected as React Native / Expo
9967
10154
  The installer doesn't cover this runtime deterministically.
@@ -9972,7 +10159,7 @@ async function applyInstallersOver(services, project, options = {}) {
9972
10159
  );
9973
10160
  } else if (outcome.outcome === "bun") {
9974
10161
  bun++;
9975
- const svcName = import_node_path48.default.basename(svc.dir);
10162
+ const svcName = import_node_path50.default.basename(svc.dir);
9976
10163
  console.log(
9977
10164
  `neat: ${svc.dir} detected as Bun
9978
10165
  The installer doesn't cover this runtime deterministically.
@@ -9983,7 +10170,7 @@ async function applyInstallersOver(services, project, options = {}) {
9983
10170
  );
9984
10171
  } else if (outcome.outcome === "deno") {
9985
10172
  deno++;
9986
- const svcName = import_node_path48.default.basename(svc.dir);
10173
+ const svcName = import_node_path50.default.basename(svc.dir);
9987
10174
  console.log(
9988
10175
  `neat: ${svc.dir} detected as Deno
9989
10176
  The installer doesn't cover this runtime deterministically.
@@ -9994,7 +10181,7 @@ async function applyInstallersOver(services, project, options = {}) {
9994
10181
  );
9995
10182
  } else if (outcome.outcome === "cloudflare-workers") {
9996
10183
  cloudflareWorkers++;
9997
- const svcName = import_node_path48.default.basename(svc.dir);
10184
+ const svcName = import_node_path50.default.basename(svc.dir);
9998
10185
  console.log(
9999
10186
  `neat: ${svc.dir} detected as Cloudflare Workers
10000
10187
  The installer doesn't cover this runtime deterministically.
@@ -10005,7 +10192,7 @@ async function applyInstallersOver(services, project, options = {}) {
10005
10192
  );
10006
10193
  } else if (outcome.outcome === "electron") {
10007
10194
  electron++;
10008
- const svcName = import_node_path48.default.basename(svc.dir);
10195
+ const svcName = import_node_path50.default.basename(svc.dir);
10009
10196
  console.log(
10010
10197
  `neat: ${svc.dir} detected as Electron
10011
10198
  The installer doesn't cover this runtime deterministically.
@@ -10086,10 +10273,6 @@ async function fetchDaemonHealth(restPort) {
10086
10273
  });
10087
10274
  });
10088
10275
  }
10089
- async function checkDaemonHealth(restPort) {
10090
- const body = await fetchDaemonHealth(restPort);
10091
- return body !== null;
10092
- }
10093
10276
  async function probeProjectHealth(restPort, name) {
10094
10277
  return new Promise((resolve) => {
10095
10278
  const req = import_node_http.default.get(
@@ -10165,12 +10348,6 @@ async function isPortFree(port) {
10165
10348
  server.listen(port, "127.0.0.1");
10166
10349
  });
10167
10350
  }
10168
- async function probePortsFree() {
10169
- for (const port of NEAT_PORTS) {
10170
- if (!await isPortFree(port)) return { free: false, held: port };
10171
- }
10172
- return { free: true };
10173
- }
10174
10351
  function formatPortCollisionMessage(port) {
10175
10352
  return [
10176
10353
  `neat: port ${port} is in use; the NEAT daemon needs it.`,
@@ -10178,11 +10355,82 @@ function formatPortCollisionMessage(port) {
10178
10355
  ` \`lsof -i :${port}\` to find the holding process.`
10179
10356
  ];
10180
10357
  }
10181
- function spawnDaemonDetached() {
10182
- const here = import_node_path48.default.dirname(new URL(importMetaUrl).pathname);
10358
+ var PORT_ALLOCATION_ATTEMPTS = 8;
10359
+ var PORT_STRIDE = 1;
10360
+ async function tripleFree(ports) {
10361
+ for (const p of [ports.rest, ports.otlp, ports.web]) {
10362
+ if (!await isPortFree(p)) return false;
10363
+ }
10364
+ return true;
10365
+ }
10366
+ async function allocatePorts() {
10367
+ const [baseRest, baseOtlp, baseWeb] = NEAT_PORTS;
10368
+ for (let i = 0; i < PORT_ALLOCATION_ATTEMPTS; i++) {
10369
+ const candidate = {
10370
+ rest: baseRest + i * PORT_STRIDE,
10371
+ otlp: baseOtlp + i * PORT_STRIDE,
10372
+ web: baseWeb + i * PORT_STRIDE
10373
+ };
10374
+ if (await tripleFree(candidate)) return candidate;
10375
+ }
10376
+ return null;
10377
+ }
10378
+ async function persistedPortsFor(scanPath) {
10379
+ const record = await readDaemonRecord(scanPath);
10380
+ if (!record) return null;
10381
+ return { rest: record.ports.rest, otlp: record.ports.otlp, web: record.ports.web };
10382
+ }
10383
+ async function acquireSpawnLock(scanPath) {
10384
+ const lockPath = import_node_path50.default.join(scanPath, "neat-out", "daemon.spawn.lock");
10385
+ await import_node_fs33.promises.mkdir(import_node_path50.default.dirname(lockPath), { recursive: true });
10386
+ const STALE_LOCK_MS = 6e4;
10387
+ try {
10388
+ const fd = await import_node_fs33.promises.open(lockPath, "wx");
10389
+ await fd.writeFile(`${process.pid}
10390
+ `, "utf8");
10391
+ await fd.close();
10392
+ return async () => {
10393
+ await import_node_fs33.promises.unlink(lockPath).catch(() => {
10394
+ });
10395
+ };
10396
+ } catch (err) {
10397
+ if (err.code !== "EEXIST") return null;
10398
+ try {
10399
+ const stat = await import_node_fs33.promises.stat(lockPath);
10400
+ if (Date.now() - stat.mtimeMs > STALE_LOCK_MS) {
10401
+ await import_node_fs33.promises.unlink(lockPath).catch(() => {
10402
+ });
10403
+ return acquireSpawnLock(scanPath);
10404
+ }
10405
+ } catch {
10406
+ return acquireSpawnLock(scanPath);
10407
+ }
10408
+ return null;
10409
+ }
10410
+ }
10411
+ async function waitForPeerDaemon(restPort, project, timeoutMs) {
10412
+ const deadline = Date.now() + timeoutMs;
10413
+ while (Date.now() < deadline) {
10414
+ if (await healthIsForProject(restPort, project)) return true;
10415
+ await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
10416
+ }
10417
+ return healthIsForProject(restPort, project);
10418
+ }
10419
+ async function healthIsForProject(restPort, project) {
10420
+ const body = await fetchDaemonHealth(restPort);
10421
+ if (body === null) return false;
10422
+ const named = body.project;
10423
+ if (typeof named === "string") return named === project;
10424
+ if (Array.isArray(body.projects)) {
10425
+ return body.projects.some((p) => p.name === project);
10426
+ }
10427
+ return false;
10428
+ }
10429
+ function spawnDaemonDetached(spec) {
10430
+ const here = import_node_path50.default.dirname(new URL(importMetaUrl).pathname);
10183
10431
  const candidates = [
10184
- import_node_path48.default.join(here, "neatd.cjs"),
10185
- import_node_path48.default.join(here, "neatd.js")
10432
+ import_node_path50.default.join(here, "neatd.cjs"),
10433
+ import_node_path50.default.join(here, "neatd.js")
10186
10434
  ];
10187
10435
  let entry2 = null;
10188
10436
  const fsSync = require("fs");
@@ -10202,6 +10450,13 @@ function spawnDaemonDetached() {
10202
10450
  if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
10203
10451
  env.HOST = "127.0.0.1";
10204
10452
  }
10453
+ if (spec) {
10454
+ env.NEAT_PROJECT = spec.project;
10455
+ env.NEAT_PROJECT_PATH = spec.projectPath;
10456
+ env.PORT = String(spec.ports.rest);
10457
+ env.OTEL_PORT = String(spec.ports.otlp);
10458
+ env.NEAT_WEB_PORT = String(spec.ports.web);
10459
+ }
10205
10460
  const child = (0, import_node_child_process3.spawn)(process.execPath, [entry2, "start"], {
10206
10461
  detached: true,
10207
10462
  // stderr inherits the orchestrator's fd so the daemon's
@@ -10240,7 +10495,7 @@ async function runOrchestrator(opts) {
10240
10495
  browser: "skipped"
10241
10496
  }
10242
10497
  };
10243
- const stat = await import_node_fs31.promises.stat(opts.scanPath).catch(() => null);
10498
+ const stat = await import_node_fs33.promises.stat(opts.scanPath).catch(() => null);
10244
10499
  if (!stat || !stat.isDirectory()) {
10245
10500
  console.error(`neat: ${opts.scanPath} is not a directory`);
10246
10501
  result.exitCode = 2;
@@ -10321,48 +10576,75 @@ async function runOrchestrator(opts) {
10321
10576
  result.exitCode = 1;
10322
10577
  }
10323
10578
  }
10324
- const restPort = Number(process.env.PORT ?? 8080);
10325
10579
  const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
10326
- if (await checkDaemonHealth(restPort)) {
10580
+ const persistedPorts = await persistedPortsFor(opts.scanPath);
10581
+ let allocated = null;
10582
+ if (persistedPorts && await healthIsForProject(persistedPorts.rest, currentProjectName)) {
10327
10583
  result.steps.daemon = "already-running";
10584
+ allocated = persistedPorts;
10328
10585
  } else {
10329
- const probe = await probePortsFree();
10330
- if (!probe.free) {
10331
- for (const line of formatPortCollisionMessage(probe.held)) {
10586
+ if (persistedPorts && await isPortFree(persistedPorts.rest) && await tripleFree(persistedPorts)) {
10587
+ allocated = persistedPorts;
10588
+ } else {
10589
+ allocated = await allocatePorts();
10590
+ }
10591
+ if (!allocated) {
10592
+ for (const line of formatPortCollisionMessage(NEAT_PORTS[0])) {
10332
10593
  console.error(line);
10333
10594
  }
10334
10595
  result.exitCode = 3;
10335
10596
  return result;
10336
10597
  }
10337
- try {
10338
- spawnDaemonDetached();
10339
- } catch (err) {
10340
- console.error(`neat: daemon spawn failed \u2014 ${err.message}`);
10341
- result.exitCode = 1;
10342
- return result;
10343
- }
10344
- const ready = await waitForDaemonReady(restPort, timeoutMs);
10345
- result.steps.daemon = ready.ready ? "spawned" : "timed-out";
10346
- if (!ready.ready) {
10347
- console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
10348
- if (ready.stillBootstrapping.length > 0) {
10349
- console.error(
10350
- `neat: still bootstrapping: ${ready.stillBootstrapping.join(", ")}`
10351
- );
10598
+ const release = await acquireSpawnLock(opts.scanPath);
10599
+ if (!release) {
10600
+ const reused = await waitForPeerDaemon(allocated.rest, currentProjectName, timeoutMs);
10601
+ if (reused) {
10602
+ result.steps.daemon = "already-running";
10603
+ } else {
10604
+ console.error("neat: another `neat` is spawning this project but its daemon did not come up in time");
10605
+ result.exitCode = 1;
10606
+ return result;
10352
10607
  }
10353
- if (ready.brokenProjects.length > 0) {
10354
- console.error(`neat: broken projects: ${ready.brokenProjects.join(", ")}`);
10608
+ } else {
10609
+ try {
10610
+ if (await healthIsForProject(allocated.rest, currentProjectName)) {
10611
+ result.steps.daemon = "already-running";
10612
+ } else {
10613
+ spawnDaemonDetached({
10614
+ project: currentProjectName,
10615
+ projectPath: opts.scanPath,
10616
+ ports: allocated
10617
+ });
10618
+ const ready = await waitForDaemonReady(allocated.rest, timeoutMs);
10619
+ result.steps.daemon = ready.ready ? "spawned" : "timed-out";
10620
+ if (!ready.ready) {
10621
+ console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
10622
+ if (ready.stillBootstrapping.length > 0) {
10623
+ console.error(`neat: still bootstrapping: ${ready.stillBootstrapping.join(", ")}`);
10624
+ }
10625
+ if (ready.brokenProjects.length > 0) {
10626
+ console.error(`neat: broken projects: ${ready.brokenProjects.join(", ")}`);
10627
+ }
10628
+ result.exitCode = 1;
10629
+ return result;
10630
+ }
10631
+ if (ready.brokenProjects.length > 0) {
10632
+ console.warn(
10633
+ `neat: ${ready.brokenProjects.length} project(s) reported broken: ${ready.brokenProjects.join(", ")}`
10634
+ );
10635
+ }
10636
+ }
10637
+ } catch (err) {
10638
+ console.error(`neat: daemon spawn failed \u2014 ${err.message}`);
10639
+ result.exitCode = 1;
10640
+ return result;
10641
+ } finally {
10642
+ await release();
10355
10643
  }
10356
- result.exitCode = 1;
10357
- return result;
10358
- }
10359
- if (ready.brokenProjects.length > 0) {
10360
- console.warn(
10361
- `neat: ${ready.brokenProjects.length} project(s) reported broken: ${ready.brokenProjects.join(", ")}`
10362
- );
10363
10644
  }
10364
10645
  }
10365
- const dashboardUrl = opts.dashboardUrl ?? "http://localhost:6328";
10646
+ const webPort = allocated?.web ?? NEAT_PORTS[2];
10647
+ const dashboardUrl = opts.dashboardUrl ?? `http://localhost:${webPort}`;
10366
10648
  if (opts.noOpen || !process.stdout.isTTY) {
10367
10649
  result.steps.browser = "skipped";
10368
10650
  } else {
@@ -10397,7 +10679,7 @@ function printSummary(result, graph, dashboardUrl) {
10397
10679
 
10398
10680
  // src/cli-verbs.ts
10399
10681
  init_cjs_shims();
10400
- var import_node_path49 = __toESM(require("path"), 1);
10682
+ var import_node_path51 = __toESM(require("path"), 1);
10401
10683
 
10402
10684
  // src/cli-client.ts
10403
10685
  init_cjs_shims();
@@ -10426,10 +10708,10 @@ function createHttpClient(baseUrl, bearerToken) {
10426
10708
  const root = baseUrl.replace(/\/$/, "");
10427
10709
  const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
10428
10710
  return {
10429
- async get(path51) {
10711
+ async get(path53) {
10430
10712
  let res;
10431
10713
  try {
10432
- res = await fetch(`${root}${path51}`, {
10714
+ res = await fetch(`${root}${path53}`, {
10433
10715
  headers: { ...authHeader }
10434
10716
  });
10435
10717
  } catch (err) {
@@ -10441,16 +10723,16 @@ function createHttpClient(baseUrl, bearerToken) {
10441
10723
  const body = await res.text().catch(() => "");
10442
10724
  throw new HttpError(
10443
10725
  res.status,
10444
- `${res.status} ${res.statusText} on GET ${path51}: ${body}`,
10726
+ `${res.status} ${res.statusText} on GET ${path53}: ${body}`,
10445
10727
  body
10446
10728
  );
10447
10729
  }
10448
10730
  return await res.json();
10449
10731
  },
10450
- async post(path51, body) {
10732
+ async post(path53, body) {
10451
10733
  let res;
10452
10734
  try {
10453
- res = await fetch(`${root}${path51}`, {
10735
+ res = await fetch(`${root}${path53}`, {
10454
10736
  method: "POST",
10455
10737
  headers: { "content-type": "application/json", ...authHeader },
10456
10738
  body: JSON.stringify(body)
@@ -10464,7 +10746,7 @@ function createHttpClient(baseUrl, bearerToken) {
10464
10746
  const text = await res.text().catch(() => "");
10465
10747
  throw new HttpError(
10466
10748
  res.status,
10467
- `${res.status} ${res.statusText} on POST ${path51}: ${text}`,
10749
+ `${res.status} ${res.statusText} on POST ${path53}: ${text}`,
10468
10750
  text
10469
10751
  );
10470
10752
  }
@@ -10478,12 +10760,12 @@ function projectPath(project, suffix) {
10478
10760
  }
10479
10761
  async function runRootCause(client, input) {
10480
10762
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
10481
- const path51 = projectPath(
10763
+ const path53 = projectPath(
10482
10764
  input.project,
10483
10765
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
10484
10766
  );
10485
10767
  try {
10486
- const result = await client.get(path51);
10768
+ const result = await client.get(path53);
10487
10769
  const arrowPath = result.traversalPath.join(" \u2190 ");
10488
10770
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
10489
10771
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -10509,12 +10791,12 @@ async function runRootCause(client, input) {
10509
10791
  }
10510
10792
  async function runBlastRadius(client, input) {
10511
10793
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
10512
- const path51 = projectPath(
10794
+ const path53 = projectPath(
10513
10795
  input.project,
10514
10796
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
10515
10797
  );
10516
10798
  try {
10517
- const result = await client.get(path51);
10799
+ const result = await client.get(path53);
10518
10800
  if (result.totalAffected === 0) {
10519
10801
  return {
10520
10802
  summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
@@ -10548,12 +10830,12 @@ function formatBlastEntry(n) {
10548
10830
  }
10549
10831
  async function runDependencies(client, input) {
10550
10832
  const depth = input.depth ?? 3;
10551
- const path51 = projectPath(
10833
+ const path53 = projectPath(
10552
10834
  input.project,
10553
10835
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
10554
10836
  );
10555
10837
  try {
10556
- const result = await client.get(path51);
10838
+ const result = await client.get(path53);
10557
10839
  if (result.total === 0) {
10558
10840
  return {
10559
10841
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -10634,9 +10916,9 @@ function formatDuration(ms) {
10634
10916
  return `${Math.round(h / 24)}d`;
10635
10917
  }
10636
10918
  async function runIncidents(client, input) {
10637
- const path51 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
10919
+ const path53 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
10638
10920
  try {
10639
- const body = await client.get(path51);
10921
+ const body = await client.get(path53);
10640
10922
  const events = body.events;
10641
10923
  if (events.length === 0) {
10642
10924
  return {
@@ -10926,13 +11208,13 @@ async function resolveProjectEntry(opts) {
10926
11208
  const cwd = opts.cwd ?? process.cwd();
10927
11209
  const resolvedCwd = await normalizeProjectPath(cwd);
10928
11210
  for (const entry2 of entries) {
10929
- if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path49.default.sep}`)) {
11211
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path51.default.sep}`)) {
10930
11212
  return entry2;
10931
11213
  }
10932
11214
  }
10933
11215
  return null;
10934
11216
  }
10935
- async function checkDaemonHealth2(baseUrl) {
11217
+ async function checkDaemonHealth(baseUrl) {
10936
11218
  try {
10937
11219
  const res = await fetch(`${baseUrl.replace(/\/$/, "")}/health`, {
10938
11220
  signal: AbortSignal.timeout(1500)
@@ -11033,7 +11315,7 @@ async function runSync(opts) {
11033
11315
  }
11034
11316
  } else {
11035
11317
  const daemonUrl = opts.daemonUrl ?? process.env.NEAT_API_URL ?? "http://localhost:8080";
11036
- const healthy = await checkDaemonHealth2(daemonUrl);
11318
+ const healthy = await checkDaemonHealth(daemonUrl);
11037
11319
  if (healthy) {
11038
11320
  try {
11039
11321
  await pushSnapshotToRemote({
@@ -11107,11 +11389,15 @@ function usage() {
11107
11389
  console.log(" watch <path> Start neat-core, watch <path>, re-extract on changes.");
11108
11390
  console.log(" PORT (default 8080), OTEL_PORT (4318), HOST (0.0.0.0)");
11109
11391
  console.log(" control listeners. NEAT_OTLP_GRPC=true also opens 4317.");
11110
- console.log(" list List every project registered in the machine-level registry.");
11111
- console.log(" pause <name> Mark a project paused \u2014 daemon stops watching until resumed.");
11112
- console.log(" resume <name> Mark a project active again.");
11392
+ console.log(" list Report the daemons running on this machine (alias: ps).");
11393
+ console.log(" Reads ~/.neat/daemons/ and folds in any registered");
11394
+ console.log(" project no daemon has self-described yet.");
11395
+ console.log(" ps Alias of list.");
11396
+ console.log(" pause <name> Stop a project's daemon until it is started again.");
11397
+ console.log(" resume <name> Bring a paused project back; for a stopped daemon, re-run");
11398
+ console.log(" `neat <path>` to start it.");
11113
11399
  console.log(" uninstall <name>");
11114
- console.log(" Remove a project from the registry. Does not touch");
11400
+ console.log(" Stop a project's daemon and retire it. Does not touch");
11115
11401
  console.log(" neat-out/, policy.json, or any user file.");
11116
11402
  console.log(" prune Drop registry entries whose path is gone from disk.");
11117
11403
  console.log(" Flags: --json emit the removed list as JSON");
@@ -11308,6 +11594,14 @@ function printVersion() {
11308
11594
  process.stdout.write(`${readPackageVersion()}
11309
11595
  `);
11310
11596
  }
11597
+ function formatMachineProjectRow(r) {
11598
+ if (r.ports) {
11599
+ const where = r.pid !== void 0 ? ` pid=${r.pid}` : "";
11600
+ return `${r.project} ${r.state} rest=${r.ports.rest} otlp=${r.ports.otlp} web=${r.ports.web} ${r.projectPath}${where}`;
11601
+ }
11602
+ const status2 = r.registryStatus ? ` (${r.registryStatus})` : "";
11603
+ return `${r.project} ${r.state}${status2} ${r.projectPath}`;
11604
+ }
11311
11605
  function printDiscoveryReport(opts, services) {
11312
11606
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
11313
11607
  const mode = opts.dryRun ? "dry-run" : opts.apply ? "apply" : "patch-only";
@@ -11346,7 +11640,7 @@ async function buildPatchSections(services, project) {
11346
11640
  }
11347
11641
  async function runInit(opts) {
11348
11642
  const written = [];
11349
- const stat = await import_node_fs32.promises.stat(opts.scanPath).catch(() => null);
11643
+ const stat = await import_node_fs34.promises.stat(opts.scanPath).catch(() => null);
11350
11644
  if (!stat || !stat.isDirectory()) {
11351
11645
  console.error(`neat init: ${opts.scanPath} is not a directory`);
11352
11646
  return { exitCode: 2, writtenFiles: written };
@@ -11355,13 +11649,13 @@ async function runInit(opts) {
11355
11649
  printDiscoveryReport(opts, services);
11356
11650
  const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
11357
11651
  const patch = renderPatch(sections);
11358
- const patchPath = import_node_path50.default.join(opts.scanPath, "neat.patch");
11652
+ const patchPath = import_node_path52.default.join(opts.scanPath, "neat.patch");
11359
11653
  if (opts.dryRun) {
11360
- await import_node_fs32.promises.writeFile(patchPath, patch, "utf8");
11654
+ await import_node_fs34.promises.writeFile(patchPath, patch, "utf8");
11361
11655
  written.push(patchPath);
11362
11656
  console.log(`dry-run: patch written to ${patchPath}`);
11363
- const gitignorePath = import_node_path50.default.join(opts.scanPath, ".gitignore");
11364
- const gitignoreExists = await import_node_fs32.promises.stat(gitignorePath).then(() => true).catch(() => false);
11657
+ const gitignorePath = import_node_path52.default.join(opts.scanPath, ".gitignore");
11658
+ const gitignoreExists = await import_node_fs34.promises.stat(gitignorePath).then(() => true).catch(() => false);
11365
11659
  const verb = gitignoreExists ? "append" : "create";
11366
11660
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
11367
11661
  console.log("rerun without --dry-run to register and snapshot.");
@@ -11372,9 +11666,9 @@ async function runInit(opts) {
11372
11666
  const graph = getGraph(graphKey);
11373
11667
  const projectPaths = pathsForProject(
11374
11668
  graphKey,
11375
- import_node_path50.default.join(opts.scanPath, "neat-out")
11669
+ import_node_path52.default.join(opts.scanPath, "neat-out")
11376
11670
  );
11377
- const errorsPath = import_node_path50.default.join(import_node_path50.default.dirname(opts.outPath), import_node_path50.default.basename(projectPaths.errorsPath));
11671
+ const errorsPath = import_node_path52.default.join(import_node_path52.default.dirname(opts.outPath), import_node_path52.default.basename(projectPaths.errorsPath));
11378
11672
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
11379
11673
  await saveGraphToDisk(graph, opts.outPath);
11380
11674
  written.push(opts.outPath);
@@ -11453,7 +11747,7 @@ async function runInit(opts) {
11453
11747
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
11454
11748
  }
11455
11749
  } else {
11456
- await import_node_fs32.promises.writeFile(patchPath, patch, "utf8");
11750
+ await import_node_fs34.promises.writeFile(patchPath, patch, "utf8");
11457
11751
  written.push(patchPath);
11458
11752
  }
11459
11753
  }
@@ -11493,9 +11787,9 @@ var CLAUDE_SKILL_CONFIG = {
11493
11787
  };
11494
11788
  function claudeConfigPath() {
11495
11789
  const override = process.env.NEAT_CLAUDE_CONFIG;
11496
- if (override && override.length > 0) return import_node_path50.default.resolve(override);
11790
+ if (override && override.length > 0) return import_node_path52.default.resolve(override);
11497
11791
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
11498
- return import_node_path50.default.join(home, ".claude.json");
11792
+ return import_node_path52.default.join(home, ".claude.json");
11499
11793
  }
11500
11794
  async function runSkill(opts) {
11501
11795
  const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -11507,7 +11801,7 @@ async function runSkill(opts) {
11507
11801
  const target = claudeConfigPath();
11508
11802
  let existing = {};
11509
11803
  try {
11510
- existing = JSON.parse(await import_node_fs32.promises.readFile(target, "utf8"));
11804
+ existing = JSON.parse(await import_node_fs34.promises.readFile(target, "utf8"));
11511
11805
  } catch (err) {
11512
11806
  if (err.code !== "ENOENT") {
11513
11807
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -11519,8 +11813,8 @@ async function runSkill(opts) {
11519
11813
  ...existing,
11520
11814
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
11521
11815
  };
11522
- await import_node_fs32.promises.mkdir(import_node_path50.default.dirname(target), { recursive: true });
11523
- await import_node_fs32.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
11816
+ await import_node_fs34.promises.mkdir(import_node_path52.default.dirname(target), { recursive: true });
11817
+ await import_node_fs34.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
11524
11818
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
11525
11819
  console.log("restart Claude Code to pick up the new MCP server.");
11526
11820
  return { exitCode: 0 };
@@ -11569,12 +11863,12 @@ async function main() {
11569
11863
  console.error("neat init: --apply and --dry-run are mutually exclusive");
11570
11864
  process.exit(2);
11571
11865
  }
11572
- const scanPath = import_node_path50.default.resolve(target);
11866
+ const scanPath = import_node_path52.default.resolve(target);
11573
11867
  const projectExplicit = parsed.project !== null;
11574
- const projectName = projectExplicit ? project : import_node_path50.default.basename(scanPath);
11868
+ const projectName = projectExplicit ? project : import_node_path52.default.basename(scanPath);
11575
11869
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
11576
- const fallback = pathsForProject(projectKey, import_node_path50.default.join(scanPath, "neat-out")).snapshotPath;
11577
- const outPath = import_node_path50.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
11870
+ const fallback = pathsForProject(projectKey, import_node_path52.default.join(scanPath, "neat-out")).snapshotPath;
11871
+ const outPath = import_node_path52.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
11578
11872
  const result = await runInit({
11579
11873
  scanPath,
11580
11874
  outPath,
@@ -11595,21 +11889,21 @@ async function main() {
11595
11889
  usage();
11596
11890
  process.exit(2);
11597
11891
  }
11598
- const scanPath = import_node_path50.default.resolve(target);
11599
- const stat = await import_node_fs32.promises.stat(scanPath).catch(() => null);
11892
+ const scanPath = import_node_path52.default.resolve(target);
11893
+ const stat = await import_node_fs34.promises.stat(scanPath).catch(() => null);
11600
11894
  if (!stat || !stat.isDirectory()) {
11601
11895
  console.error(`neat watch: ${scanPath} is not a directory`);
11602
11896
  process.exit(2);
11603
11897
  }
11604
- const projectPaths = pathsForProject(project, import_node_path50.default.join(scanPath, "neat-out"));
11605
- const outPath = import_node_path50.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
11606
- const errorsPath = import_node_path50.default.resolve(
11607
- process.env.NEAT_ERRORS_PATH ?? import_node_path50.default.join(import_node_path50.default.dirname(outPath), import_node_path50.default.basename(projectPaths.errorsPath))
11898
+ const projectPaths = pathsForProject(project, import_node_path52.default.join(scanPath, "neat-out"));
11899
+ const outPath = import_node_path52.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
11900
+ const errorsPath = import_node_path52.default.resolve(
11901
+ process.env.NEAT_ERRORS_PATH ?? import_node_path52.default.join(import_node_path52.default.dirname(outPath), import_node_path52.default.basename(projectPaths.errorsPath))
11608
11902
  );
11609
- const staleEventsPath = import_node_path50.default.resolve(
11610
- process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path50.default.join(import_node_path50.default.dirname(outPath), import_node_path50.default.basename(projectPaths.staleEventsPath))
11903
+ const staleEventsPath = import_node_path52.default.resolve(
11904
+ process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path52.default.join(import_node_path52.default.dirname(outPath), import_node_path52.default.basename(projectPaths.staleEventsPath))
11611
11905
  );
11612
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path50.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
11906
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path52.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
11613
11907
  const handle = await startWatch(getGraph(project), {
11614
11908
  scanPath,
11615
11909
  outPath,
@@ -11636,16 +11930,14 @@ async function main() {
11636
11930
  process.on("SIGINT", shutdown);
11637
11931
  return;
11638
11932
  }
11639
- if (cmd === "list") {
11640
- const projects = await listProjects();
11641
- if (projects.length === 0) {
11642
- console.log("no projects registered. run `neat init <path>` to register one.");
11933
+ if (cmd === "list" || cmd === "ps") {
11934
+ const rows = await listMachineProjects();
11935
+ if (rows.length === 0) {
11936
+ console.log("no daemons running and no projects registered. run `neat init <path>` to register one.");
11643
11937
  return;
11644
11938
  }
11645
- for (const p of projects) {
11646
- const seen = p.lastSeenAt ? p.lastSeenAt : "never";
11647
- const langs = p.languages.length > 0 ? p.languages.join(",") : "-";
11648
- console.log(`${p.name} ${p.status} ${langs} ${p.path} last-seen=${seen}`);
11939
+ for (const r of rows) {
11940
+ console.log(formatMachineProjectRow(r));
11649
11941
  }
11650
11942
  return;
11651
11943
  }
@@ -11656,6 +11948,15 @@ async function main() {
11656
11948
  usage();
11657
11949
  process.exit(2);
11658
11950
  }
11951
+ const daemon = await findDaemonByProject(name);
11952
+ if (daemon) {
11953
+ if (daemon.live && signalDaemonStop(daemon.record.pid)) {
11954
+ console.log(`paused: ${name} (${daemon.record.projectPath}) \u2014 stopped daemon pid ${daemon.record.pid}`);
11955
+ } else {
11956
+ console.log(`paused: ${name} (${daemon.record.projectPath}) \u2014 daemon was not running`);
11957
+ }
11958
+ return;
11959
+ }
11659
11960
  try {
11660
11961
  const entry2 = await setStatus(name, "paused");
11661
11962
  console.log(`paused: ${entry2.name} (${entry2.path})`);
@@ -11672,6 +11973,15 @@ async function main() {
11672
11973
  usage();
11673
11974
  process.exit(2);
11674
11975
  }
11976
+ const daemon = await findDaemonByProject(name);
11977
+ if (daemon) {
11978
+ if (daemon.live) {
11979
+ console.log(`resume: ${name} (${daemon.record.projectPath}) \u2014 daemon already running`);
11980
+ } else {
11981
+ console.log(`resume: ${name} (${daemon.record.projectPath}) \u2014 run \`neat ${daemon.record.projectPath}\` to start its daemon again`);
11982
+ }
11983
+ return;
11984
+ }
11675
11985
  try {
11676
11986
  const entry2 = await setStatus(name, "active");
11677
11987
  console.log(`resumed: ${entry2.name} (${entry2.path})`);
@@ -11693,12 +12003,20 @@ async function main() {
11693
12003
  usage();
11694
12004
  process.exit(2);
11695
12005
  }
12006
+ const daemon = await findDaemonByProject(name);
11696
12007
  const removed = await removeProject(name);
11697
- if (!removed) {
12008
+ if (!daemon && !removed) {
11698
12009
  console.error(`neat uninstall: no project named "${name}"`);
11699
12010
  process.exit(1);
11700
12011
  }
11701
- console.log(`unregistered: ${removed.name} (${removed.path})`);
12012
+ const projectPath2 = daemon?.record.projectPath ?? removed?.path ?? "(unknown path)";
12013
+ if (daemon) {
12014
+ if (daemon.live && signalDaemonStop(daemon.record.pid)) {
12015
+ console.log(`uninstall: ${name} \u2014 stopped daemon pid ${daemon.record.pid}`);
12016
+ }
12017
+ await removeDaemonRecord(daemon.source);
12018
+ }
12019
+ console.log(`unregistered: ${name} (${projectPath2})`);
11702
12020
  console.log("note: neat-out/, policy.json, and other files at the project path were left in place.");
11703
12021
  return;
11704
12022
  }
@@ -11768,11 +12086,11 @@ async function main() {
11768
12086
  process.exit(1);
11769
12087
  }
11770
12088
  async function tryOrchestrator(cmd, parsed) {
11771
- const scanPath = import_node_path50.default.resolve(cmd);
11772
- const stat = await import_node_fs32.promises.stat(scanPath).catch(() => null);
12089
+ const scanPath = import_node_path52.default.resolve(cmd);
12090
+ const stat = await import_node_fs34.promises.stat(scanPath).catch(() => null);
11773
12091
  if (!stat || !stat.isDirectory()) return null;
11774
12092
  const projectExplicit = parsed.project !== null;
11775
- const projectName = projectExplicit ? parsed.project : import_node_path50.default.basename(scanPath);
12093
+ const projectName = projectExplicit ? parsed.project : import_node_path52.default.basename(scanPath);
11776
12094
  const result = await runOrchestrator({
11777
12095
  scanPath,
11778
12096
  project: projectName,