@neat.is/core 0.4.16 → 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.js CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ readDaemonRecord
4
+ } from "./chunk-GXWBMJDJ.js";
2
5
  import {
3
6
  buildSearchIndex
4
7
  } from "./chunk-XOOCA5T7.js";
@@ -24,10 +27,12 @@ import {
24
27
  ensureCompatLoaded,
25
28
  evaluateAllPolicies,
26
29
  extractFromDirectory,
30
+ findDaemonByProject,
27
31
  formatExtractionBanner,
28
32
  formatPrecisionFloorBanner,
29
33
  getGraph,
30
34
  isStrictExtractionEnabled,
35
+ listMachineProjects,
31
36
  listProjects,
32
37
  loadGraphFromDisk,
33
38
  loadPolicyFile,
@@ -36,25 +41,28 @@ import {
36
41
  normalizeProjectPath,
37
42
  pathsForProject,
38
43
  promoteFrontierNodes,
44
+ pruneRegistry,
45
+ removeDaemonRecord,
39
46
  removeProject,
40
47
  resetGraph,
41
48
  retireEdgesByFile,
42
49
  runPackageManagerInstall,
43
50
  saveGraphToDisk,
44
51
  setStatus,
52
+ signalDaemonStop,
45
53
  startPersistLoop,
46
54
  startStalenessLoop
47
- } from "./chunk-XS4CGNRO.js";
55
+ } from "./chunk-T3X6XJPU.js";
48
56
  import {
49
57
  startOtelGrpcReceiver
50
- } from "./chunk-GHPHVXYM.js";
58
+ } from "./chunk-MTXF77TN.js";
51
59
  import {
52
60
  __dirname,
53
61
  __require,
54
62
  assertBindAuthority,
55
63
  buildOtelReceiver,
56
64
  readAuthEnv
57
- } from "./chunk-6CO7C4IU.js";
65
+ } from "./chunk-BGPWBRLU.js";
58
66
 
59
67
  // src/cli.ts
60
68
  import path9 from "path";
@@ -744,9 +752,50 @@ import semver from "semver";
744
752
 
745
753
  // src/installers/templates.ts
746
754
  var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
747
- var OTEL_INIT_STAMP = "// neat-template-version: 5 \u2014 layered file-first capture (ADR-090) + http/json protocol pin (#468).";
755
+ var OTEL_INIT_STAMP = "// neat-template-version: 6 \u2014 daemon.json endpoint resolution (ADR-096) + layered file-first capture (ADR-090).";
748
756
  var OTEL_OTLP_HEADERS_JS = "if (process.env.NEAT_OTEL_TOKEN) process.env.OTEL_EXPORTER_OTLP_HEADERS ||= 'Authorization=Bearer ' + process.env.NEAT_OTEL_TOKEN";
749
757
  var OTEL_OTLP_PROTOCOL_JS = "process.env.OTEL_EXPORTER_OTLP_PROTOCOL ||= 'http/json'";
758
+ var OTEL_ENDPOINT_RESOLVER_CJS = `;(function () {
759
+ try {
760
+ if (process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_ENDPOINT) return
761
+ const __neatFs = require('node:fs')
762
+ const __neatPath = require('node:path')
763
+ let __neatDir = process.cwd()
764
+ for (let __i = 0; __i < 8; __i++) {
765
+ try {
766
+ const __rec = JSON.parse(__neatFs.readFileSync(__neatPath.join(__neatDir, 'neat-out', 'daemon.json'), 'utf8'))
767
+ if (__rec && __rec.ports && typeof __rec.ports.otlp === 'number') {
768
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://localhost:' + __rec.ports.otlp + '/v1/traces'
769
+ break
770
+ }
771
+ } catch (_e) {}
772
+ const __parent = __neatPath.dirname(__neatDir)
773
+ if (__parent === __neatDir) break
774
+ __neatDir = __parent
775
+ }
776
+ } catch (_e) {}
777
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/v1/traces'
778
+ })()`;
779
+ var OTEL_ESM_NODE_IMPORTS = "import { readFileSync as __neatReadFileSync } from 'node:fs'\nimport { join as __neatJoin, dirname as __neatDirname } from 'node:path'";
780
+ var OTEL_ENDPOINT_RESOLVER_ESM = `;(function () {
781
+ try {
782
+ if (process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_ENDPOINT) return
783
+ let __neatDir = process.cwd()
784
+ for (let __i = 0; __i < 8; __i++) {
785
+ try {
786
+ const __rec = JSON.parse(__neatReadFileSync(__neatJoin(__neatDir, 'neat-out', 'daemon.json'), 'utf8'))
787
+ if (__rec && __rec.ports && typeof __rec.ports.otlp === 'number') {
788
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://localhost:' + __rec.ports.otlp + '/v1/traces'
789
+ break
790
+ }
791
+ } catch (_e) {}
792
+ const __parent = __neatDirname(__neatDir)
793
+ if (__parent === __neatDir) break
794
+ __neatDir = __parent
795
+ }
796
+ } catch (_e) {}
797
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/v1/traces'
798
+ })()`;
750
799
  function neatCaptureSource(ts) {
751
800
  const spanT = ts ? ": any" : "";
752
801
  const strOpt = ts ? ": string | undefined" : "";
@@ -1035,7 +1084,7 @@ try {
1035
1084
  var OTEL_INIT_CJS = `${OTEL_INIT_HEADER}
1036
1085
  ${OTEL_INIT_STAMP}
1037
1086
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
1038
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
1087
+ ${OTEL_ENDPOINT_RESOLVER_CJS}
1039
1088
  ${OTEL_OTLP_PROTOCOL_JS}
1040
1089
  ${OTEL_OTLP_HEADERS_JS}
1041
1090
 
@@ -1053,8 +1102,9 @@ ${neatWireCaptureSource(false)}
1053
1102
  `;
1054
1103
  var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
1055
1104
  ${OTEL_INIT_STAMP}
1105
+ ${OTEL_ESM_NODE_IMPORTS}
1056
1106
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
1057
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
1107
+ ${OTEL_ENDPOINT_RESOLVER_ESM}
1058
1108
  ${OTEL_OTLP_PROTOCOL_JS}
1059
1109
  ${OTEL_OTLP_HEADERS_JS}
1060
1110
 
@@ -1077,8 +1127,9 @@ ${OTEL_INIT_STAMP}
1077
1127
  // strict user tsconfig would reject; suppressing type-checking here keeps the
1078
1128
  // generated file from breaking the host project's \`tsc\` gate (#427) without
1079
1129
  // constraining the runtime logic. The file is regenerated, never hand-edited.
1130
+ ${OTEL_ESM_NODE_IMPORTS}
1080
1131
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
1081
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
1132
+ ${OTEL_ENDPOINT_RESOLVER_ESM}
1082
1133
  ${OTEL_OTLP_PROTOCOL_JS}
1083
1134
  ${OTEL_OTLP_HEADERS_JS}
1084
1135
 
@@ -1100,11 +1151,14 @@ ${registrations.join("\n")}
1100
1151
  `;
1101
1152
  return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName).replace(/__INSTRUMENTATION_BLOCK__\n?/g, block);
1102
1153
  }
1103
- function renderEnvNeat(serviceName, projectName) {
1154
+ function renderEnvNeat(serviceName, _projectName) {
1104
1155
  return [
1105
1156
  "# Generated by `neat init --apply` (ADR-069).",
1106
1157
  `OTEL_SERVICE_NAME=${serviceName}`,
1107
- `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/projects/${projectName}/v1/traces`,
1158
+ "# Advisory only \u2014 the generated otel-init resolves the live endpoint from",
1159
+ "# <project>/neat-out/daemon.json (ports.otlp) at boot (ADR-096). This is the",
1160
+ "# canonical default the daemon takes when its first-choice OTLP port is free.",
1161
+ "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces",
1108
1162
  "OTEL_EXPORTER_OTLP_PROTOCOL=http/json",
1109
1163
  "# Set NEAT_OTEL_TOKEN to the daemon's OTLP secret to authenticate exported spans (#410).",
1110
1164
  "# NEAT_OTEL_TOKEN=",
@@ -1127,8 +1181,9 @@ export async function register() {
1127
1181
  }
1128
1182
  `;
1129
1183
  var NEXT_INSTRUMENTATION_NODE_TS = `${NEXT_INSTRUMENTATION_HEADER}
1184
+ ${OTEL_ESM_NODE_IMPORTS}
1130
1185
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
1131
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
1186
+ ${OTEL_ENDPOINT_RESOLVER_ESM}
1132
1187
  ${OTEL_OTLP_PROTOCOL_JS}
1133
1188
  ${OTEL_OTLP_HEADERS_JS}
1134
1189
 
@@ -1141,7 +1196,7 @@ new NodeSDK({ instrumentations }).start()
1141
1196
  `;
1142
1197
  var NEXT_INSTRUMENTATION_NODE_JS = `${NEXT_INSTRUMENTATION_HEADER}
1143
1198
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
1144
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
1199
+ ${OTEL_ENDPOINT_RESOLVER_CJS}
1145
1200
  ${OTEL_OTLP_PROTOCOL_JS}
1146
1201
  ${OTEL_OTLP_HEADERS_JS}
1147
1202
 
@@ -1160,8 +1215,9 @@ ${registrations.join("\n")}
1160
1215
  }
1161
1216
  var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
1162
1217
  var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
1218
+ ${OTEL_ESM_NODE_IMPORTS}
1163
1219
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
1164
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
1220
+ ${OTEL_ENDPOINT_RESOLVER_ESM}
1165
1221
  ${OTEL_OTLP_PROTOCOL_JS}
1166
1222
  ${OTEL_OTLP_HEADERS_JS}
1167
1223
 
@@ -1176,7 +1232,7 @@ sdk.start()
1176
1232
  `;
1177
1233
  var FRAMEWORK_OTEL_INIT_JS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
1178
1234
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
1179
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
1235
+ ${OTEL_ENDPOINT_RESOLVER_CJS}
1180
1236
  ${OTEL_OTLP_PROTOCOL_JS}
1181
1237
  ${OTEL_OTLP_HEADERS_JS}
1182
1238
 
@@ -2833,10 +2889,6 @@ async function fetchDaemonHealth(restPort) {
2833
2889
  });
2834
2890
  });
2835
2891
  }
2836
- async function checkDaemonHealth(restPort) {
2837
- const body = await fetchDaemonHealth(restPort);
2838
- return body !== null;
2839
- }
2840
2892
  async function probeProjectHealth(restPort, name) {
2841
2893
  return new Promise((resolve) => {
2842
2894
  const req = http.get(
@@ -2912,12 +2964,6 @@ async function isPortFree(port) {
2912
2964
  server.listen(port, "127.0.0.1");
2913
2965
  });
2914
2966
  }
2915
- async function probePortsFree() {
2916
- for (const port of NEAT_PORTS) {
2917
- if (!await isPortFree(port)) return { free: false, held: port };
2918
- }
2919
- return { free: true };
2920
- }
2921
2967
  function formatPortCollisionMessage(port) {
2922
2968
  return [
2923
2969
  `neat: port ${port} is in use; the NEAT daemon needs it.`,
@@ -2925,7 +2971,78 @@ function formatPortCollisionMessage(port) {
2925
2971
  ` \`lsof -i :${port}\` to find the holding process.`
2926
2972
  ];
2927
2973
  }
2928
- function spawnDaemonDetached() {
2974
+ var PORT_ALLOCATION_ATTEMPTS = 8;
2975
+ var PORT_STRIDE = 1;
2976
+ async function tripleFree(ports) {
2977
+ for (const p of [ports.rest, ports.otlp, ports.web]) {
2978
+ if (!await isPortFree(p)) return false;
2979
+ }
2980
+ return true;
2981
+ }
2982
+ async function allocatePorts() {
2983
+ const [baseRest, baseOtlp, baseWeb] = NEAT_PORTS;
2984
+ for (let i = 0; i < PORT_ALLOCATION_ATTEMPTS; i++) {
2985
+ const candidate = {
2986
+ rest: baseRest + i * PORT_STRIDE,
2987
+ otlp: baseOtlp + i * PORT_STRIDE,
2988
+ web: baseWeb + i * PORT_STRIDE
2989
+ };
2990
+ if (await tripleFree(candidate)) return candidate;
2991
+ }
2992
+ return null;
2993
+ }
2994
+ async function persistedPortsFor(scanPath) {
2995
+ const record = await readDaemonRecord(scanPath);
2996
+ if (!record) return null;
2997
+ return { rest: record.ports.rest, otlp: record.ports.otlp, web: record.ports.web };
2998
+ }
2999
+ async function acquireSpawnLock(scanPath) {
3000
+ const lockPath = path7.join(scanPath, "neat-out", "daemon.spawn.lock");
3001
+ await fs6.mkdir(path7.dirname(lockPath), { recursive: true });
3002
+ const STALE_LOCK_MS = 6e4;
3003
+ try {
3004
+ const fd = await fs6.open(lockPath, "wx");
3005
+ await fd.writeFile(`${process.pid}
3006
+ `, "utf8");
3007
+ await fd.close();
3008
+ return async () => {
3009
+ await fs6.unlink(lockPath).catch(() => {
3010
+ });
3011
+ };
3012
+ } catch (err) {
3013
+ if (err.code !== "EEXIST") return null;
3014
+ try {
3015
+ const stat = await fs6.stat(lockPath);
3016
+ if (Date.now() - stat.mtimeMs > STALE_LOCK_MS) {
3017
+ await fs6.unlink(lockPath).catch(() => {
3018
+ });
3019
+ return acquireSpawnLock(scanPath);
3020
+ }
3021
+ } catch {
3022
+ return acquireSpawnLock(scanPath);
3023
+ }
3024
+ return null;
3025
+ }
3026
+ }
3027
+ async function waitForPeerDaemon(restPort, project, timeoutMs) {
3028
+ const deadline = Date.now() + timeoutMs;
3029
+ while (Date.now() < deadline) {
3030
+ if (await healthIsForProject(restPort, project)) return true;
3031
+ await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
3032
+ }
3033
+ return healthIsForProject(restPort, project);
3034
+ }
3035
+ async function healthIsForProject(restPort, project) {
3036
+ const body = await fetchDaemonHealth(restPort);
3037
+ if (body === null) return false;
3038
+ const named = body.project;
3039
+ if (typeof named === "string") return named === project;
3040
+ if (Array.isArray(body.projects)) {
3041
+ return body.projects.some((p) => p.name === project);
3042
+ }
3043
+ return false;
3044
+ }
3045
+ function spawnDaemonDetached(spec) {
2929
3046
  const here = path7.dirname(new URL(import.meta.url).pathname);
2930
3047
  const candidates = [
2931
3048
  path7.join(here, "neatd.cjs"),
@@ -2949,6 +3066,13 @@ function spawnDaemonDetached() {
2949
3066
  if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
2950
3067
  env.HOST = "127.0.0.1";
2951
3068
  }
3069
+ if (spec) {
3070
+ env.NEAT_PROJECT = spec.project;
3071
+ env.NEAT_PROJECT_PATH = spec.projectPath;
3072
+ env.PORT = String(spec.ports.rest);
3073
+ env.OTEL_PORT = String(spec.ports.otlp);
3074
+ env.NEAT_WEB_PORT = String(spec.ports.web);
3075
+ }
2952
3076
  const child = spawn2(process.execPath, [entry2, "start"], {
2953
3077
  detached: true,
2954
3078
  // stderr inherits the orchestrator's fd so the daemon's
@@ -3068,48 +3192,75 @@ async function runOrchestrator(opts) {
3068
3192
  result.exitCode = 1;
3069
3193
  }
3070
3194
  }
3071
- const restPort = Number(process.env.PORT ?? 8080);
3072
3195
  const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
3073
- if (await checkDaemonHealth(restPort)) {
3196
+ const persistedPorts = await persistedPortsFor(opts.scanPath);
3197
+ let allocated = null;
3198
+ if (persistedPorts && await healthIsForProject(persistedPorts.rest, currentProjectName)) {
3074
3199
  result.steps.daemon = "already-running";
3200
+ allocated = persistedPorts;
3075
3201
  } else {
3076
- const probe = await probePortsFree();
3077
- if (!probe.free) {
3078
- for (const line of formatPortCollisionMessage(probe.held)) {
3202
+ if (persistedPorts && await isPortFree(persistedPorts.rest) && await tripleFree(persistedPorts)) {
3203
+ allocated = persistedPorts;
3204
+ } else {
3205
+ allocated = await allocatePorts();
3206
+ }
3207
+ if (!allocated) {
3208
+ for (const line of formatPortCollisionMessage(NEAT_PORTS[0])) {
3079
3209
  console.error(line);
3080
3210
  }
3081
3211
  result.exitCode = 3;
3082
3212
  return result;
3083
3213
  }
3084
- try {
3085
- spawnDaemonDetached();
3086
- } catch (err) {
3087
- console.error(`neat: daemon spawn failed \u2014 ${err.message}`);
3088
- result.exitCode = 1;
3089
- return result;
3090
- }
3091
- const ready = await waitForDaemonReady(restPort, timeoutMs);
3092
- result.steps.daemon = ready.ready ? "spawned" : "timed-out";
3093
- if (!ready.ready) {
3094
- console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
3095
- if (ready.stillBootstrapping.length > 0) {
3096
- console.error(
3097
- `neat: still bootstrapping: ${ready.stillBootstrapping.join(", ")}`
3098
- );
3214
+ const release = await acquireSpawnLock(opts.scanPath);
3215
+ if (!release) {
3216
+ const reused = await waitForPeerDaemon(allocated.rest, currentProjectName, timeoutMs);
3217
+ if (reused) {
3218
+ result.steps.daemon = "already-running";
3219
+ } else {
3220
+ console.error("neat: another `neat` is spawning this project but its daemon did not come up in time");
3221
+ result.exitCode = 1;
3222
+ return result;
3099
3223
  }
3100
- if (ready.brokenProjects.length > 0) {
3101
- console.error(`neat: broken projects: ${ready.brokenProjects.join(", ")}`);
3224
+ } else {
3225
+ try {
3226
+ if (await healthIsForProject(allocated.rest, currentProjectName)) {
3227
+ result.steps.daemon = "already-running";
3228
+ } else {
3229
+ spawnDaemonDetached({
3230
+ project: currentProjectName,
3231
+ projectPath: opts.scanPath,
3232
+ ports: allocated
3233
+ });
3234
+ const ready = await waitForDaemonReady(allocated.rest, timeoutMs);
3235
+ result.steps.daemon = ready.ready ? "spawned" : "timed-out";
3236
+ if (!ready.ready) {
3237
+ console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
3238
+ if (ready.stillBootstrapping.length > 0) {
3239
+ console.error(`neat: still bootstrapping: ${ready.stillBootstrapping.join(", ")}`);
3240
+ }
3241
+ if (ready.brokenProjects.length > 0) {
3242
+ console.error(`neat: broken projects: ${ready.brokenProjects.join(", ")}`);
3243
+ }
3244
+ result.exitCode = 1;
3245
+ return result;
3246
+ }
3247
+ if (ready.brokenProjects.length > 0) {
3248
+ console.warn(
3249
+ `neat: ${ready.brokenProjects.length} project(s) reported broken: ${ready.brokenProjects.join(", ")}`
3250
+ );
3251
+ }
3252
+ }
3253
+ } catch (err) {
3254
+ console.error(`neat: daemon spawn failed \u2014 ${err.message}`);
3255
+ result.exitCode = 1;
3256
+ return result;
3257
+ } finally {
3258
+ await release();
3102
3259
  }
3103
- result.exitCode = 1;
3104
- return result;
3105
- }
3106
- if (ready.brokenProjects.length > 0) {
3107
- console.warn(
3108
- `neat: ${ready.brokenProjects.length} project(s) reported broken: ${ready.brokenProjects.join(", ")}`
3109
- );
3110
3260
  }
3111
3261
  }
3112
- const dashboardUrl = opts.dashboardUrl ?? "http://localhost:6328";
3262
+ const webPort = allocated?.web ?? NEAT_PORTS[2];
3263
+ const dashboardUrl = opts.dashboardUrl ?? `http://localhost:${webPort}`;
3113
3264
  if (opts.noOpen || !process.stdout.isTTY) {
3114
3265
  result.steps.browser = "skipped";
3115
3266
  } else {
@@ -3677,7 +3828,7 @@ async function resolveProjectEntry(opts) {
3677
3828
  }
3678
3829
  return null;
3679
3830
  }
3680
- async function checkDaemonHealth2(baseUrl) {
3831
+ async function checkDaemonHealth(baseUrl) {
3681
3832
  try {
3682
3833
  const res = await fetch(`${baseUrl.replace(/\/$/, "")}/health`, {
3683
3834
  signal: AbortSignal.timeout(1500)
@@ -3778,7 +3929,7 @@ async function runSync(opts) {
3778
3929
  }
3779
3930
  } else {
3780
3931
  const daemonUrl = opts.daemonUrl ?? process.env.NEAT_API_URL ?? "http://localhost:8080";
3781
- const healthy = await checkDaemonHealth2(daemonUrl);
3932
+ const healthy = await checkDaemonHealth(daemonUrl);
3782
3933
  if (healthy) {
3783
3934
  try {
3784
3935
  await pushSnapshotToRemote({
@@ -3852,12 +4003,18 @@ function usage() {
3852
4003
  console.log(" watch <path> Start neat-core, watch <path>, re-extract on changes.");
3853
4004
  console.log(" PORT (default 8080), OTEL_PORT (4318), HOST (0.0.0.0)");
3854
4005
  console.log(" control listeners. NEAT_OTLP_GRPC=true also opens 4317.");
3855
- console.log(" list List every project registered in the machine-level registry.");
3856
- console.log(" pause <name> Mark a project paused \u2014 daemon stops watching until resumed.");
3857
- console.log(" resume <name> Mark a project active again.");
4006
+ console.log(" list Report the daemons running on this machine (alias: ps).");
4007
+ console.log(" Reads ~/.neat/daemons/ and folds in any registered");
4008
+ console.log(" project no daemon has self-described yet.");
4009
+ console.log(" ps Alias of list.");
4010
+ console.log(" pause <name> Stop a project's daemon until it is started again.");
4011
+ console.log(" resume <name> Bring a paused project back; for a stopped daemon, re-run");
4012
+ console.log(" `neat <path>` to start it.");
3858
4013
  console.log(" uninstall <name>");
3859
- console.log(" Remove a project from the registry. Does not touch");
4014
+ console.log(" Stop a project's daemon and retire it. Does not touch");
3860
4015
  console.log(" neat-out/, policy.json, or any user file.");
4016
+ console.log(" prune Drop registry entries whose path is gone from disk.");
4017
+ console.log(" Flags: --json emit the removed list as JSON");
3861
4018
  console.log(" version Print the installed @neat.is/core version and exit.");
3862
4019
  console.log(" Aliases: --version, -v.");
3863
4020
  console.log(" skill Install or print the Claude Code MCP drop-in.");
@@ -3917,6 +4074,7 @@ function usage() {
3917
4074
  console.log("");
3918
4075
  console.log("environment:");
3919
4076
  console.log(" NEAT_API_URL base URL for the core REST API (default http://localhost:8080)");
4077
+ console.log(" alias: NEAT_CORE_URL (the name the MCP server reads)");
3920
4078
  console.log(" NEAT_PROJECT project name when --project isn't passed");
3921
4079
  }
3922
4080
  var STRING_FLAGS = [
@@ -4050,6 +4208,14 @@ function printVersion() {
4050
4208
  process.stdout.write(`${readPackageVersion()}
4051
4209
  `);
4052
4210
  }
4211
+ function formatMachineProjectRow(r) {
4212
+ if (r.ports) {
4213
+ const where = r.pid !== void 0 ? ` pid=${r.pid}` : "";
4214
+ return `${r.project} ${r.state} rest=${r.ports.rest} otlp=${r.ports.otlp} web=${r.ports.web} ${r.projectPath}${where}`;
4215
+ }
4216
+ const status = r.registryStatus ? ` (${r.registryStatus})` : "";
4217
+ return `${r.project} ${r.state}${status} ${r.projectPath}`;
4218
+ }
4053
4219
  function printDiscoveryReport(opts, services) {
4054
4220
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
4055
4221
  const mode = opts.dryRun ? "dry-run" : opts.apply ? "apply" : "patch-only";
@@ -4228,7 +4394,7 @@ var CLAUDE_SKILL_CONFIG = {
4228
4394
  command: "npx",
4229
4395
  args: ["-y", "@neat.is/mcp"],
4230
4396
  env: {
4231
- NEAT_API_URL: "http://localhost:8080"
4397
+ NEAT_CORE_URL: "http://localhost:8080"
4232
4398
  }
4233
4399
  }
4234
4400
  }
@@ -4274,6 +4440,9 @@ async function runSkill(opts) {
4274
4440
  console.log("");
4275
4441
  console.log("Manual install: copy mcpServers.neat from --print-config into ~/.claude.json,");
4276
4442
  console.log("then restart Claude Code. See packages/claude-skill/SKILL.md for the tool list.");
4443
+ console.log("");
4444
+ console.log("The MCP server reads NEAT_CORE_URL for the daemon URL \u2014 point it at a");
4445
+ console.log("non-default daemon by editing that value in the generated config.");
4277
4446
  return { exitCode: 0 };
4278
4447
  }
4279
4448
  async function main() {
@@ -4375,16 +4544,14 @@ async function main() {
4375
4544
  process.on("SIGINT", shutdown);
4376
4545
  return;
4377
4546
  }
4378
- if (cmd === "list") {
4379
- const projects = await listProjects();
4380
- if (projects.length === 0) {
4381
- console.log("no projects registered. run `neat init <path>` to register one.");
4547
+ if (cmd === "list" || cmd === "ps") {
4548
+ const rows = await listMachineProjects();
4549
+ if (rows.length === 0) {
4550
+ console.log("no daemons running and no projects registered. run `neat init <path>` to register one.");
4382
4551
  return;
4383
4552
  }
4384
- for (const p of projects) {
4385
- const seen = p.lastSeenAt ? p.lastSeenAt : "never";
4386
- const langs = p.languages.length > 0 ? p.languages.join(",") : "-";
4387
- console.log(`${p.name} ${p.status} ${langs} ${p.path} last-seen=${seen}`);
4553
+ for (const r of rows) {
4554
+ console.log(formatMachineProjectRow(r));
4388
4555
  }
4389
4556
  return;
4390
4557
  }
@@ -4395,6 +4562,15 @@ async function main() {
4395
4562
  usage();
4396
4563
  process.exit(2);
4397
4564
  }
4565
+ const daemon = await findDaemonByProject(name);
4566
+ if (daemon) {
4567
+ if (daemon.live && signalDaemonStop(daemon.record.pid)) {
4568
+ console.log(`paused: ${name} (${daemon.record.projectPath}) \u2014 stopped daemon pid ${daemon.record.pid}`);
4569
+ } else {
4570
+ console.log(`paused: ${name} (${daemon.record.projectPath}) \u2014 daemon was not running`);
4571
+ }
4572
+ return;
4573
+ }
4398
4574
  try {
4399
4575
  const entry2 = await setStatus(name, "paused");
4400
4576
  console.log(`paused: ${entry2.name} (${entry2.path})`);
@@ -4411,6 +4587,15 @@ async function main() {
4411
4587
  usage();
4412
4588
  process.exit(2);
4413
4589
  }
4590
+ const daemon = await findDaemonByProject(name);
4591
+ if (daemon) {
4592
+ if (daemon.live) {
4593
+ console.log(`resume: ${name} (${daemon.record.projectPath}) \u2014 daemon already running`);
4594
+ } else {
4595
+ console.log(`resume: ${name} (${daemon.record.projectPath}) \u2014 run \`neat ${daemon.record.projectPath}\` to start its daemon again`);
4596
+ }
4597
+ return;
4598
+ }
4414
4599
  try {
4415
4600
  const entry2 = await setStatus(name, "active");
4416
4601
  console.log(`resumed: ${entry2.name} (${entry2.path})`);
@@ -4432,15 +4617,36 @@ async function main() {
4432
4617
  usage();
4433
4618
  process.exit(2);
4434
4619
  }
4620
+ const daemon = await findDaemonByProject(name);
4435
4621
  const removed = await removeProject(name);
4436
- if (!removed) {
4622
+ if (!daemon && !removed) {
4437
4623
  console.error(`neat uninstall: no project named "${name}"`);
4438
4624
  process.exit(1);
4439
4625
  }
4440
- console.log(`unregistered: ${removed.name} (${removed.path})`);
4626
+ const projectPath2 = daemon?.record.projectPath ?? removed?.path ?? "(unknown path)";
4627
+ if (daemon) {
4628
+ if (daemon.live && signalDaemonStop(daemon.record.pid)) {
4629
+ console.log(`uninstall: ${name} \u2014 stopped daemon pid ${daemon.record.pid}`);
4630
+ }
4631
+ await removeDaemonRecord(daemon.source);
4632
+ }
4633
+ console.log(`unregistered: ${name} (${projectPath2})`);
4441
4634
  console.log("note: neat-out/, policy.json, and other files at the project path were left in place.");
4442
4635
  return;
4443
4636
  }
4637
+ if (cmd === "prune") {
4638
+ const removed = await pruneRegistry({ ttlMs: 0 });
4639
+ if (parsed.json) {
4640
+ console.log(JSON.stringify(removed.map((p) => ({ name: p.name, path: p.path })), null, 2));
4641
+ return;
4642
+ }
4643
+ if (removed.length === 0) {
4644
+ console.log("nothing to prune \u2014 every registered project path still exists.");
4645
+ return;
4646
+ }
4647
+ console.log(`pruned ${removed.length} project${removed.length === 1 ? "" : "s"}: ${removed.map((p) => p.name).join(", ")}`);
4648
+ return;
4649
+ }
4444
4650
  if (cmd === "deploy") {
4445
4651
  const artifact = await runDeploy();
4446
4652
  const block = renderOtelEnvBlock2(artifact.token);
@@ -4528,12 +4734,44 @@ function resolveProjectFlag(parsed) {
4528
4734
  if (env && env.length > 0 && env !== DEFAULT_PROJECT) return env;
4529
4735
  return void 0;
4530
4736
  }
4737
+ var ProjectResolutionError = class extends Error {
4738
+ constructor(message, exitCode = 2) {
4739
+ super(message);
4740
+ this.exitCode = exitCode;
4741
+ this.name = "ProjectResolutionError";
4742
+ }
4743
+ exitCode;
4744
+ };
4745
+ async function resolveProjectForVerb(client, parsed) {
4746
+ const explicit = resolveProjectFlag(parsed);
4747
+ if (explicit) return explicit;
4748
+ const projects = await client.get("/projects");
4749
+ if (projects.some((p) => p.name === DEFAULT_PROJECT)) {
4750
+ return void 0;
4751
+ }
4752
+ if (projects.length === 1) {
4753
+ return projects[0].name;
4754
+ }
4755
+ if (projects.length === 0) {
4756
+ throw new ProjectResolutionError(
4757
+ "No projects are registered with the daemon yet. Run `npx neat.is` in a repo to build a graph first, then re-run this command."
4758
+ );
4759
+ }
4760
+ const names = projects.map((p) => p.name).sort().map((n) => ` ${n}`).join("\n");
4761
+ throw new ProjectResolutionError(
4762
+ `Several projects are registered and none is named "default", so I can't pick one for you.
4763
+ Pass --project <name> to choose:
4764
+ ${names}`
4765
+ );
4766
+ }
4767
+ function resolveDaemonUrl() {
4768
+ return process.env.NEAT_API_URL ?? process.env.NEAT_CORE_URL ?? "http://localhost:8080";
4769
+ }
4531
4770
  async function runQueryVerb(cmd, parsed) {
4532
- const baseUrl = process.env.NEAT_API_URL ?? "http://localhost:8080";
4771
+ const baseUrl = resolveDaemonUrl();
4533
4772
  const client = createHttpClient(baseUrl, resolveAuthToken());
4534
- const project = resolveProjectFlag(parsed);
4535
4773
  const positional = parsed.positional;
4536
- let work;
4774
+ let makeWork;
4537
4775
  switch (cmd) {
4538
4776
  case "root-cause": {
4539
4777
  const node = positional[0];
@@ -4541,7 +4779,7 @@ async function runQueryVerb(cmd, parsed) {
4541
4779
  console.error("neat root-cause: missing <node-id>");
4542
4780
  return 2;
4543
4781
  }
4544
- work = runRootCause(client, {
4782
+ makeWork = (project) => runRootCause(client, {
4545
4783
  errorNode: node,
4546
4784
  ...parsed.errorId ? { errorId: parsed.errorId } : {},
4547
4785
  ...project ? { project } : {}
@@ -4554,7 +4792,7 @@ async function runQueryVerb(cmd, parsed) {
4554
4792
  console.error("neat blast-radius: missing <node-id>");
4555
4793
  return 2;
4556
4794
  }
4557
- work = runBlastRadius(client, {
4795
+ makeWork = (project) => runBlastRadius(client, {
4558
4796
  nodeId: node,
4559
4797
  ...parsed.depth !== null ? { depth: parsed.depth } : {},
4560
4798
  ...project ? { project } : {}
@@ -4567,7 +4805,7 @@ async function runQueryVerb(cmd, parsed) {
4567
4805
  console.error("neat dependencies: missing <node-id>");
4568
4806
  return 2;
4569
4807
  }
4570
- work = runDependencies(client, {
4808
+ makeWork = (project) => runDependencies(client, {
4571
4809
  nodeId: node,
4572
4810
  ...parsed.depth !== null ? { depth: parsed.depth } : {},
4573
4811
  ...project ? { project } : {}
@@ -4580,14 +4818,14 @@ async function runQueryVerb(cmd, parsed) {
4580
4818
  console.error("neat observed-dependencies: missing <node-id>");
4581
4819
  return 2;
4582
4820
  }
4583
- work = runObservedDependencies(client, {
4821
+ makeWork = (project) => runObservedDependencies(client, {
4584
4822
  nodeId: node,
4585
4823
  ...project ? { project } : {}
4586
4824
  });
4587
4825
  break;
4588
4826
  }
4589
4827
  case "incidents": {
4590
- work = runIncidents(client, {
4828
+ makeWork = (project) => runIncidents(client, {
4591
4829
  ...positional[0] ? { nodeId: positional[0] } : {},
4592
4830
  ...parsed.limit !== null ? { limit: parsed.limit } : {},
4593
4831
  ...project ? { project } : {}
@@ -4600,7 +4838,7 @@ async function runQueryVerb(cmd, parsed) {
4600
4838
  console.error("neat search: missing <query>");
4601
4839
  return 2;
4602
4840
  }
4603
- work = runSearch(client, { query: q, ...project ? { project } : {} });
4841
+ makeWork = (project) => runSearch(client, { query: q, ...project ? { project } : {} });
4604
4842
  break;
4605
4843
  }
4606
4844
  case "diff": {
@@ -4609,14 +4847,14 @@ async function runQueryVerb(cmd, parsed) {
4609
4847
  console.error("neat diff: --against <snapshot-path> is required");
4610
4848
  return 2;
4611
4849
  }
4612
- work = runDiff(client, {
4850
+ makeWork = (project) => runDiff(client, {
4613
4851
  againstSnapshot: against,
4614
4852
  ...project ? { project } : {}
4615
4853
  });
4616
4854
  break;
4617
4855
  }
4618
4856
  case "stale-edges": {
4619
- work = runStaleEdges(client, {
4857
+ makeWork = (project) => runStaleEdges(client, {
4620
4858
  ...parsed.limit !== null ? { limit: parsed.limit } : {},
4621
4859
  ...parsed.edgeType ? { edgeType: parsed.edgeType } : {},
4622
4860
  ...project ? { project } : {}
@@ -4635,7 +4873,7 @@ async function runQueryVerb(cmd, parsed) {
4635
4873
  return 2;
4636
4874
  }
4637
4875
  }
4638
- work = runPolicies(client, {
4876
+ makeWork = (project) => runPolicies(client, {
4639
4877
  ...parsed.node ? { nodeId: parsed.node } : {},
4640
4878
  ...hypothetical ? { hypotheticalAction: hypothetical } : {},
4641
4879
  ...project ? { project } : {}
@@ -4659,7 +4897,7 @@ async function runQueryVerb(cmd, parsed) {
4659
4897
  }
4660
4898
  typeFilter = out;
4661
4899
  }
4662
- work = runDivergences(client, {
4900
+ makeWork = (project) => runDivergences(client, {
4663
4901
  ...typeFilter ? { type: typeFilter } : {},
4664
4902
  ...parsed.minConfidence !== null ? { minConfidence: parsed.minConfidence } : {},
4665
4903
  ...parsed.node ? { node: parsed.node } : {},
@@ -4672,16 +4910,21 @@ async function runQueryVerb(cmd, parsed) {
4672
4910
  return 2;
4673
4911
  }
4674
4912
  try {
4675
- const result = await work;
4913
+ const project = await resolveProjectForVerb(client, parsed);
4914
+ const result = await makeWork(project);
4676
4915
  if (parsed.json) process.stdout.write(formatJson(result) + "\n");
4677
4916
  else process.stdout.write(formatHuman(result) + "\n");
4678
4917
  return 0;
4679
4918
  } catch (err) {
4919
+ if (err instanceof ProjectResolutionError) {
4920
+ console.error(`neat ${cmd}: ${err.message}`);
4921
+ return err.exitCode;
4922
+ }
4680
4923
  if (err instanceof HttpError) {
4681
4924
  const detail = err.responseBody.length > 0 ? err.responseBody : err.message;
4682
4925
  console.error(`neat ${cmd}: ${detail.trim()}`);
4683
4926
  } else if (err instanceof TransportError) {
4684
- console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (NEAT_API_URL=${process.env.NEAT_API_URL ?? "http://localhost:8080"})`);
4927
+ console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (NEAT_API_URL=${resolveDaemonUrl()})`);
4685
4928
  } else {
4686
4929
  console.error(`neat ${cmd}: ${err.message}`);
4687
4930
  }
@@ -4697,6 +4940,7 @@ if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsW
4697
4940
  }
4698
4941
  export {
4699
4942
  CLAUDE_SKILL_CONFIG,
4943
+ ProjectResolutionError,
4700
4944
  QUERY_VERBS,
4701
4945
  commandPrefix,
4702
4946
  isNpxInvocation,
@@ -4704,6 +4948,7 @@ export {
4704
4948
  parseArgs,
4705
4949
  printBanner,
4706
4950
  readPackageVersion,
4951
+ resolveProjectForVerb,
4707
4952
  runInit,
4708
4953
  runQueryVerb,
4709
4954
  runSkill,