@neat.is/core 0.4.17 → 0.4.19

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-ZV7GHZ2D.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,
@@ -37,25 +42,27 @@ import {
37
42
  pathsForProject,
38
43
  promoteFrontierNodes,
39
44
  pruneRegistry,
45
+ removeDaemonRecord,
40
46
  removeProject,
41
47
  resetGraph,
42
48
  retireEdgesByFile,
43
49
  runPackageManagerInstall,
44
50
  saveGraphToDisk,
45
51
  setStatus,
52
+ signalDaemonStop,
46
53
  startPersistLoop,
47
54
  startStalenessLoop
48
- } from "./chunk-LUDSPX5N.js";
55
+ } from "./chunk-Q5EDVWKZ.js";
49
56
  import {
50
57
  startOtelGrpcReceiver
51
- } from "./chunk-GHPHVXYM.js";
58
+ } from "./chunk-MTXF77TN.js";
52
59
  import {
53
60
  __dirname,
54
61
  __require,
55
62
  assertBindAuthority,
56
63
  buildOtelReceiver,
57
64
  readAuthEnv
58
- } from "./chunk-6CO7C4IU.js";
65
+ } from "./chunk-BGPWBRLU.js";
59
66
 
60
67
  // src/cli.ts
61
68
  import path9 from "path";
@@ -745,9 +752,50 @@ import semver from "semver";
745
752
 
746
753
  // src/installers/templates.ts
747
754
  var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
748
- 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).";
749
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";
750
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
+ })()`;
751
799
  function neatCaptureSource(ts) {
752
800
  const spanT = ts ? ": any" : "";
753
801
  const strOpt = ts ? ": string | undefined" : "";
@@ -1036,7 +1084,7 @@ try {
1036
1084
  var OTEL_INIT_CJS = `${OTEL_INIT_HEADER}
1037
1085
  ${OTEL_INIT_STAMP}
1038
1086
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
1039
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
1087
+ ${OTEL_ENDPOINT_RESOLVER_CJS}
1040
1088
  ${OTEL_OTLP_PROTOCOL_JS}
1041
1089
  ${OTEL_OTLP_HEADERS_JS}
1042
1090
 
@@ -1054,8 +1102,9 @@ ${neatWireCaptureSource(false)}
1054
1102
  `;
1055
1103
  var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
1056
1104
  ${OTEL_INIT_STAMP}
1105
+ ${OTEL_ESM_NODE_IMPORTS}
1057
1106
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
1058
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
1107
+ ${OTEL_ENDPOINT_RESOLVER_ESM}
1059
1108
  ${OTEL_OTLP_PROTOCOL_JS}
1060
1109
  ${OTEL_OTLP_HEADERS_JS}
1061
1110
 
@@ -1078,8 +1127,9 @@ ${OTEL_INIT_STAMP}
1078
1127
  // strict user tsconfig would reject; suppressing type-checking here keeps the
1079
1128
  // generated file from breaking the host project's \`tsc\` gate (#427) without
1080
1129
  // constraining the runtime logic. The file is regenerated, never hand-edited.
1130
+ ${OTEL_ESM_NODE_IMPORTS}
1081
1131
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
1082
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
1132
+ ${OTEL_ENDPOINT_RESOLVER_ESM}
1083
1133
  ${OTEL_OTLP_PROTOCOL_JS}
1084
1134
  ${OTEL_OTLP_HEADERS_JS}
1085
1135
 
@@ -1101,11 +1151,14 @@ ${registrations.join("\n")}
1101
1151
  `;
1102
1152
  return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName).replace(/__INSTRUMENTATION_BLOCK__\n?/g, block);
1103
1153
  }
1104
- function renderEnvNeat(serviceName, projectName) {
1154
+ function renderEnvNeat(serviceName, _projectName) {
1105
1155
  return [
1106
1156
  "# Generated by `neat init --apply` (ADR-069).",
1107
1157
  `OTEL_SERVICE_NAME=${serviceName}`,
1108
- `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",
1109
1162
  "OTEL_EXPORTER_OTLP_PROTOCOL=http/json",
1110
1163
  "# Set NEAT_OTEL_TOKEN to the daemon's OTLP secret to authenticate exported spans (#410).",
1111
1164
  "# NEAT_OTEL_TOKEN=",
@@ -1128,8 +1181,9 @@ export async function register() {
1128
1181
  }
1129
1182
  `;
1130
1183
  var NEXT_INSTRUMENTATION_NODE_TS = `${NEXT_INSTRUMENTATION_HEADER}
1184
+ ${OTEL_ESM_NODE_IMPORTS}
1131
1185
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
1132
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
1186
+ ${OTEL_ENDPOINT_RESOLVER_ESM}
1133
1187
  ${OTEL_OTLP_PROTOCOL_JS}
1134
1188
  ${OTEL_OTLP_HEADERS_JS}
1135
1189
 
@@ -1142,7 +1196,7 @@ new NodeSDK({ instrumentations }).start()
1142
1196
  `;
1143
1197
  var NEXT_INSTRUMENTATION_NODE_JS = `${NEXT_INSTRUMENTATION_HEADER}
1144
1198
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
1145
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
1199
+ ${OTEL_ENDPOINT_RESOLVER_CJS}
1146
1200
  ${OTEL_OTLP_PROTOCOL_JS}
1147
1201
  ${OTEL_OTLP_HEADERS_JS}
1148
1202
 
@@ -1161,8 +1215,9 @@ ${registrations.join("\n")}
1161
1215
  }
1162
1216
  var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
1163
1217
  var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
1218
+ ${OTEL_ESM_NODE_IMPORTS}
1164
1219
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
1165
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
1220
+ ${OTEL_ENDPOINT_RESOLVER_ESM}
1166
1221
  ${OTEL_OTLP_PROTOCOL_JS}
1167
1222
  ${OTEL_OTLP_HEADERS_JS}
1168
1223
 
@@ -1177,7 +1232,7 @@ sdk.start()
1177
1232
  `;
1178
1233
  var FRAMEWORK_OTEL_INIT_JS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
1179
1234
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
1180
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
1235
+ ${OTEL_ENDPOINT_RESOLVER_CJS}
1181
1236
  ${OTEL_OTLP_PROTOCOL_JS}
1182
1237
  ${OTEL_OTLP_HEADERS_JS}
1183
1238
 
@@ -2834,10 +2889,6 @@ async function fetchDaemonHealth(restPort) {
2834
2889
  });
2835
2890
  });
2836
2891
  }
2837
- async function checkDaemonHealth(restPort) {
2838
- const body = await fetchDaemonHealth(restPort);
2839
- return body !== null;
2840
- }
2841
2892
  async function probeProjectHealth(restPort, name) {
2842
2893
  return new Promise((resolve) => {
2843
2894
  const req = http.get(
@@ -2913,12 +2964,6 @@ async function isPortFree(port) {
2913
2964
  server.listen(port, "127.0.0.1");
2914
2965
  });
2915
2966
  }
2916
- async function probePortsFree() {
2917
- for (const port of NEAT_PORTS) {
2918
- if (!await isPortFree(port)) return { free: false, held: port };
2919
- }
2920
- return { free: true };
2921
- }
2922
2967
  function formatPortCollisionMessage(port) {
2923
2968
  return [
2924
2969
  `neat: port ${port} is in use; the NEAT daemon needs it.`,
@@ -2926,7 +2971,78 @@ function formatPortCollisionMessage(port) {
2926
2971
  ` \`lsof -i :${port}\` to find the holding process.`
2927
2972
  ];
2928
2973
  }
2929
- 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) {
2930
3046
  const here = path7.dirname(new URL(import.meta.url).pathname);
2931
3047
  const candidates = [
2932
3048
  path7.join(here, "neatd.cjs"),
@@ -2950,6 +3066,13 @@ function spawnDaemonDetached() {
2950
3066
  if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
2951
3067
  env.HOST = "127.0.0.1";
2952
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
+ }
2953
3076
  const child = spawn2(process.execPath, [entry2, "start"], {
2954
3077
  detached: true,
2955
3078
  // stderr inherits the orchestrator's fd so the daemon's
@@ -3069,48 +3192,75 @@ async function runOrchestrator(opts) {
3069
3192
  result.exitCode = 1;
3070
3193
  }
3071
3194
  }
3072
- const restPort = Number(process.env.PORT ?? 8080);
3073
3195
  const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
3074
- if (await checkDaemonHealth(restPort)) {
3196
+ const persistedPorts = await persistedPortsFor(opts.scanPath);
3197
+ let allocated = null;
3198
+ if (persistedPorts && await healthIsForProject(persistedPorts.rest, currentProjectName)) {
3075
3199
  result.steps.daemon = "already-running";
3200
+ allocated = persistedPorts;
3076
3201
  } else {
3077
- const probe = await probePortsFree();
3078
- if (!probe.free) {
3079
- 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])) {
3080
3209
  console.error(line);
3081
3210
  }
3082
3211
  result.exitCode = 3;
3083
3212
  return result;
3084
3213
  }
3085
- try {
3086
- spawnDaemonDetached();
3087
- } catch (err) {
3088
- console.error(`neat: daemon spawn failed \u2014 ${err.message}`);
3089
- result.exitCode = 1;
3090
- return result;
3091
- }
3092
- const ready = await waitForDaemonReady(restPort, timeoutMs);
3093
- result.steps.daemon = ready.ready ? "spawned" : "timed-out";
3094
- if (!ready.ready) {
3095
- console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
3096
- if (ready.stillBootstrapping.length > 0) {
3097
- console.error(
3098
- `neat: still bootstrapping: ${ready.stillBootstrapping.join(", ")}`
3099
- );
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;
3100
3223
  }
3101
- if (ready.brokenProjects.length > 0) {
3102
- 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();
3103
3259
  }
3104
- result.exitCode = 1;
3105
- return result;
3106
- }
3107
- if (ready.brokenProjects.length > 0) {
3108
- console.warn(
3109
- `neat: ${ready.brokenProjects.length} project(s) reported broken: ${ready.brokenProjects.join(", ")}`
3110
- );
3111
3260
  }
3112
3261
  }
3113
- const dashboardUrl = opts.dashboardUrl ?? "http://localhost:6328";
3262
+ const webPort = allocated?.web ?? NEAT_PORTS[2];
3263
+ const dashboardUrl = opts.dashboardUrl ?? `http://localhost:${webPort}`;
3114
3264
  if (opts.noOpen || !process.stdout.isTTY) {
3115
3265
  result.steps.browser = "skipped";
3116
3266
  } else {
@@ -3678,7 +3828,7 @@ async function resolveProjectEntry(opts) {
3678
3828
  }
3679
3829
  return null;
3680
3830
  }
3681
- async function checkDaemonHealth2(baseUrl) {
3831
+ async function checkDaemonHealth(baseUrl) {
3682
3832
  try {
3683
3833
  const res = await fetch(`${baseUrl.replace(/\/$/, "")}/health`, {
3684
3834
  signal: AbortSignal.timeout(1500)
@@ -3779,7 +3929,7 @@ async function runSync(opts) {
3779
3929
  }
3780
3930
  } else {
3781
3931
  const daemonUrl = opts.daemonUrl ?? process.env.NEAT_API_URL ?? "http://localhost:8080";
3782
- const healthy = await checkDaemonHealth2(daemonUrl);
3932
+ const healthy = await checkDaemonHealth(daemonUrl);
3783
3933
  if (healthy) {
3784
3934
  try {
3785
3935
  await pushSnapshotToRemote({
@@ -3853,11 +4003,15 @@ function usage() {
3853
4003
  console.log(" watch <path> Start neat-core, watch <path>, re-extract on changes.");
3854
4004
  console.log(" PORT (default 8080), OTEL_PORT (4318), HOST (0.0.0.0)");
3855
4005
  console.log(" control listeners. NEAT_OTLP_GRPC=true also opens 4317.");
3856
- console.log(" list List every project registered in the machine-level registry.");
3857
- console.log(" pause <name> Mark a project paused \u2014 daemon stops watching until resumed.");
3858
- 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.");
3859
4013
  console.log(" uninstall <name>");
3860
- 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");
3861
4015
  console.log(" neat-out/, policy.json, or any user file.");
3862
4016
  console.log(" prune Drop registry entries whose path is gone from disk.");
3863
4017
  console.log(" Flags: --json emit the removed list as JSON");
@@ -4054,6 +4208,14 @@ function printVersion() {
4054
4208
  process.stdout.write(`${readPackageVersion()}
4055
4209
  `);
4056
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
+ }
4057
4219
  function printDiscoveryReport(opts, services) {
4058
4220
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
4059
4221
  const mode = opts.dryRun ? "dry-run" : opts.apply ? "apply" : "patch-only";
@@ -4382,16 +4544,14 @@ async function main() {
4382
4544
  process.on("SIGINT", shutdown);
4383
4545
  return;
4384
4546
  }
4385
- if (cmd === "list") {
4386
- const projects = await listProjects();
4387
- if (projects.length === 0) {
4388
- 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.");
4389
4551
  return;
4390
4552
  }
4391
- for (const p of projects) {
4392
- const seen = p.lastSeenAt ? p.lastSeenAt : "never";
4393
- const langs = p.languages.length > 0 ? p.languages.join(",") : "-";
4394
- console.log(`${p.name} ${p.status} ${langs} ${p.path} last-seen=${seen}`);
4553
+ for (const r of rows) {
4554
+ console.log(formatMachineProjectRow(r));
4395
4555
  }
4396
4556
  return;
4397
4557
  }
@@ -4402,6 +4562,15 @@ async function main() {
4402
4562
  usage();
4403
4563
  process.exit(2);
4404
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
+ }
4405
4574
  try {
4406
4575
  const entry2 = await setStatus(name, "paused");
4407
4576
  console.log(`paused: ${entry2.name} (${entry2.path})`);
@@ -4418,6 +4587,15 @@ async function main() {
4418
4587
  usage();
4419
4588
  process.exit(2);
4420
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
+ }
4421
4599
  try {
4422
4600
  const entry2 = await setStatus(name, "active");
4423
4601
  console.log(`resumed: ${entry2.name} (${entry2.path})`);
@@ -4439,12 +4617,20 @@ async function main() {
4439
4617
  usage();
4440
4618
  process.exit(2);
4441
4619
  }
4620
+ const daemon = await findDaemonByProject(name);
4442
4621
  const removed = await removeProject(name);
4443
- if (!removed) {
4622
+ if (!daemon && !removed) {
4444
4623
  console.error(`neat uninstall: no project named "${name}"`);
4445
4624
  process.exit(1);
4446
4625
  }
4447
- 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})`);
4448
4634
  console.log("note: neat-out/, policy.json, and other files at the project path were left in place.");
4449
4635
  return;
4450
4636
  }