@neat.is/core 0.4.0 → 0.4.3

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.d.cts CHANGED
@@ -40,6 +40,7 @@ interface ParsedArgs {
40
40
  }
41
41
  declare function parseArgs(rest: string[]): ParsedArgs;
42
42
 
43
+ declare function readPackageVersion(): string;
43
44
  declare function runInit(opts: InitOptions): Promise<InitResult>;
44
45
  declare const CLAUDE_SKILL_CONFIG: {
45
46
  mcpServers: {
@@ -63,4 +64,4 @@ declare function runSkill(opts: SkillOptions): Promise<{
63
64
  declare const QUERY_VERBS: Set<string>;
64
65
  declare function runQueryVerb(cmd: string, parsed: ParsedArgs): Promise<number>;
65
66
 
66
- export { CLAUDE_SKILL_CONFIG, type InitOptions, type InitResult, QUERY_VERBS, type SkillOptions, parseArgs, runInit, runQueryVerb, runSkill };
67
+ export { CLAUDE_SKILL_CONFIG, type InitOptions, type InitResult, QUERY_VERBS, type SkillOptions, parseArgs, readPackageVersion, runInit, runQueryVerb, runSkill };
package/dist/cli.d.ts CHANGED
@@ -40,6 +40,7 @@ interface ParsedArgs {
40
40
  }
41
41
  declare function parseArgs(rest: string[]): ParsedArgs;
42
42
 
43
+ declare function readPackageVersion(): string;
43
44
  declare function runInit(opts: InitOptions): Promise<InitResult>;
44
45
  declare const CLAUDE_SKILL_CONFIG: {
45
46
  mcpServers: {
@@ -63,4 +64,4 @@ declare function runSkill(opts: SkillOptions): Promise<{
63
64
  declare const QUERY_VERBS: Set<string>;
64
65
  declare function runQueryVerb(cmd: string, parsed: ParsedArgs): Promise<number>;
65
66
 
66
- export { CLAUDE_SKILL_CONFIG, type InitOptions, type InitResult, QUERY_VERBS, type SkillOptions, parseArgs, runInit, runQueryVerb, runSkill };
67
+ export { CLAUDE_SKILL_CONFIG, type InitOptions, type InitResult, QUERY_VERBS, type SkillOptions, parseArgs, readPackageVersion, runInit, runQueryVerb, runSkill };
package/dist/cli.js CHANGED
@@ -41,18 +41,20 @@ import {
41
41
  setStatus,
42
42
  startPersistLoop,
43
43
  startStalenessLoop
44
- } from "./chunk-UPW4CMOH.js";
44
+ } from "./chunk-NTQHMXWE.js";
45
45
  import {
46
46
  startOtelGrpcReceiver
47
- } from "./chunk-IXIFJKMM.js";
47
+ } from "./chunk-D5PIJFBE.js";
48
48
  import {
49
+ __dirname,
49
50
  __require,
50
51
  buildOtelReceiver
51
- } from "./chunk-4V23KYOP.js";
52
+ } from "./chunk-KYRIQIPG.js";
52
53
 
53
54
  // src/cli.ts
54
55
  import path8 from "path";
55
- import { promises as fs7 } from "fs";
56
+ import { promises as fs7, readFileSync } from "fs";
57
+ import { fileURLToPath } from "url";
56
58
 
57
59
  // src/gitignore.ts
58
60
  import { promises as fs } from "fs";
@@ -272,6 +274,14 @@ var IGNORED_WATCH_GLOBS = [
272
274
  "**/.turbo/**",
273
275
  "**/.next/**",
274
276
  "**/neat-out/**",
277
+ // Python venv shapes (issue #344). chokidar opens one watch handle per
278
+ // descended dir; a CPython venv carries 20k+ files and trivially blows
279
+ // through the macOS kqueue cap before extraction even runs.
280
+ "**/.venv/**",
281
+ "**/venv/**",
282
+ "**/__pypackages__/**",
283
+ "**/.tox/**",
284
+ "**/site-packages/**",
275
285
  "**/.DS_Store"
276
286
  ];
277
287
  var IGNORED_WATCH_PATHS = [
@@ -282,6 +292,11 @@ var IGNORED_WATCH_PATHS = [
282
292
  /(?:^|[\\/])\.turbo[\\/]/,
283
293
  /(?:^|[\\/])\.next[\\/]/,
284
294
  /(?:^|[\\/])neat-out[\\/]/,
295
+ /(?:^|[\\/])\.venv[\\/]/,
296
+ /(?:^|[\\/])venv[\\/]/,
297
+ /(?:^|[\\/])__pypackages__[\\/]/,
298
+ /(?:^|[\\/])\.tox[\\/]/,
299
+ /(?:^|[\\/])site-packages[\\/]/,
285
300
  /[\\/]?\.DS_Store$/
286
301
  ];
287
302
  function shouldIgnore(absPath) {
@@ -713,10 +728,10 @@ dotenv.config({ path: path.join(here, '.env.neat') })
713
728
 
714
729
  await import('@opentelemetry/auto-instrumentations-node/register')
715
730
  `;
716
- function renderEnvNeat(serviceName) {
731
+ function renderEnvNeat(projectName) {
717
732
  return [
718
733
  "# Generated by `neat init --apply` (ADR-069).",
719
- `OTEL_SERVICE_NAME=${serviceName}`,
734
+ `OTEL_SERVICE_NAME=${projectName}`,
720
735
  "OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318",
721
736
  ""
722
737
  ].join("\n");
@@ -737,41 +752,26 @@ export async function register() {
737
752
  }
738
753
  `;
739
754
  var NEXT_INSTRUMENTATION_NODE_TS = `${NEXT_INSTRUMENTATION_HEADER}
740
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
741
- // the OTel SDK starts so the exporter target and service name are in scope.
742
- import { fileURLToPath } from 'node:url'
743
- import path from 'node:path'
744
- import dotenv from 'dotenv'
755
+ process.env.OTEL_SERVICE_NAME ||= '__PROJECT_NAME__'
756
+ process.env.OTEL_EXPORTER_OTLP_ENDPOINT ||= 'http://localhost:4318'
757
+
745
758
  import { NodeSDK } from '@opentelemetry/sdk-node'
746
759
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
747
760
 
748
- const here = path.dirname(fileURLToPath(import.meta.url))
749
- dotenv.config({ path: path.join(here, '.env.neat') })
750
-
751
- const sdk = new NodeSDK({
752
- serviceName: process.env.OTEL_SERVICE_NAME,
753
- instrumentations: [getNodeAutoInstrumentations()],
754
- })
755
- sdk.start()
761
+ new NodeSDK({ instrumentations: [getNodeAutoInstrumentations()] }).start()
756
762
  `;
757
763
  var NEXT_INSTRUMENTATION_NODE_JS = `${NEXT_INSTRUMENTATION_HEADER}
758
- // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
759
- // the OTel SDK starts so the exporter target and service name are in scope.
760
- import { fileURLToPath } from 'node:url'
761
- import path from 'node:path'
762
- import dotenv from 'dotenv'
763
- import { NodeSDK } from '@opentelemetry/sdk-node'
764
- import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
764
+ process.env.OTEL_SERVICE_NAME ||= '__PROJECT_NAME__'
765
+ process.env.OTEL_EXPORTER_OTLP_ENDPOINT ||= 'http://localhost:4318'
765
766
 
766
- const here = path.dirname(fileURLToPath(import.meta.url))
767
- dotenv.config({ path: path.join(here, '.env.neat') })
767
+ const { NodeSDK } = require('@opentelemetry/sdk-node')
768
+ const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
768
769
 
769
- const sdk = new NodeSDK({
770
- serviceName: process.env.OTEL_SERVICE_NAME,
771
- instrumentations: [getNodeAutoInstrumentations()],
772
- })
773
- sdk.start()
770
+ new NodeSDK({ instrumentations: [getNodeAutoInstrumentations()] }).start()
774
771
  `;
772
+ function renderNextInstrumentationNode(template, projectName) {
773
+ return template.replace(/__PROJECT_NAME__/g, projectName);
774
+ }
775
775
  var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
776
776
  var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
777
777
  // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
@@ -886,6 +886,10 @@ var OTEL_ENV = {
886
886
  key: "OTEL_EXPORTER_OTLP_ENDPOINT",
887
887
  value: "http://localhost:4318"
888
888
  };
889
+ function envServiceName(pkg, serviceDir, project) {
890
+ if (project && project.length > 0) return project;
891
+ return pkg.name ?? path4.basename(serviceDir);
892
+ }
889
893
  async function readPackageJson(serviceDir) {
890
894
  try {
891
895
  const raw = await fs4.readFile(path4.join(serviceDir, "package.json"), "utf8");
@@ -1110,17 +1114,29 @@ function lineIsOtelInjection(line) {
1110
1114
  if (trimmed.length === 0) return false;
1111
1115
  return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
1112
1116
  }
1113
- async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
1117
+ async function detectsSrcLayout(serviceDir) {
1118
+ const [hasSrcApp, hasSrcPages, hasRootApp, hasRootPages] = await Promise.all([
1119
+ exists(path4.join(serviceDir, "src", "app")),
1120
+ exists(path4.join(serviceDir, "src", "pages")),
1121
+ exists(path4.join(serviceDir, "app")),
1122
+ exists(path4.join(serviceDir, "pages"))
1123
+ ]);
1124
+ return (hasSrcApp || hasSrcPages) && !hasRootApp && !hasRootPages;
1125
+ }
1126
+ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project) {
1114
1127
  const useTs = await isTypeScriptProject(serviceDir);
1115
- const instrumentationFile = path4.join(serviceDir, useTs ? "instrumentation.ts" : "instrumentation.js");
1128
+ const srcLayout = await detectsSrcLayout(serviceDir);
1129
+ const baseDir = srcLayout ? path4.join(serviceDir, "src") : serviceDir;
1130
+ const instrumentationFile = path4.join(baseDir, useTs ? "instrumentation.ts" : "instrumentation.js");
1116
1131
  const instrumentationNodeFile = path4.join(
1117
- serviceDir,
1132
+ baseDir,
1118
1133
  useTs ? "instrumentation.node.ts" : "instrumentation.node.js"
1119
1134
  );
1120
- const envNeatFile = path4.join(serviceDir, ".env.neat");
1135
+ const envNeatFile = path4.join(baseDir, ".env.neat");
1121
1136
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
1122
1137
  const dependencyEdits = [];
1123
1138
  for (const sdk of SDK_PACKAGES) {
1139
+ if (sdk.name === "dotenv") continue;
1124
1140
  if (sdk.name in existingDeps) continue;
1125
1141
  dependencyEdits.push({
1126
1142
  file: manifestPath,
@@ -1129,6 +1145,7 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
1129
1145
  version: sdk.version
1130
1146
  });
1131
1147
  }
1148
+ const projectName = envServiceName(pkg, serviceDir, project);
1132
1149
  const generatedFiles = [];
1133
1150
  if (!await exists(instrumentationFile)) {
1134
1151
  generatedFiles.push({
@@ -1140,14 +1157,17 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
1140
1157
  if (!await exists(instrumentationNodeFile)) {
1141
1158
  generatedFiles.push({
1142
1159
  file: instrumentationNodeFile,
1143
- contents: useTs ? NEXT_INSTRUMENTATION_NODE_TS : NEXT_INSTRUMENTATION_NODE_JS,
1160
+ contents: renderNextInstrumentationNode(
1161
+ useTs ? NEXT_INSTRUMENTATION_NODE_TS : NEXT_INSTRUMENTATION_NODE_JS,
1162
+ projectName
1163
+ ),
1144
1164
  skipIfExists: true
1145
1165
  });
1146
1166
  }
1147
1167
  if (!await exists(envNeatFile)) {
1148
1168
  generatedFiles.push({
1149
1169
  file: envNeatFile,
1150
- contents: renderEnvNeat(pkg.name ?? path4.basename(serviceDir)),
1170
+ contents: renderEnvNeat(projectName),
1151
1171
  skipIfExists: true
1152
1172
  });
1153
1173
  }
@@ -1203,12 +1223,12 @@ function buildDependencyEdits(pkg, manifestPath) {
1203
1223
  }
1204
1224
  return edits;
1205
1225
  }
1206
- async function queueEnvNeat(serviceDir, pkg, generatedFiles) {
1226
+ async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
1207
1227
  const envNeatFile = path4.join(serviceDir, ".env.neat");
1208
1228
  if (!await exists(envNeatFile)) {
1209
1229
  generatedFiles.push({
1210
1230
  file: envNeatFile,
1211
- contents: renderEnvNeat(pkg.name ?? path4.basename(serviceDir)),
1231
+ contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
1212
1232
  skipIfExists: true
1213
1233
  });
1214
1234
  }
@@ -1227,7 +1247,7 @@ function fileImportsOtelHook(raw, specifiers) {
1227
1247
  }
1228
1248
  return false;
1229
1249
  }
1230
- async function planRemix(serviceDir, pkg, manifestPath, entryFile) {
1250
+ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
1231
1251
  const useTs = await isTypeScriptProject(serviceDir);
1232
1252
  const otelServerFile = path4.join(
1233
1253
  serviceDir,
@@ -1242,7 +1262,7 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile) {
1242
1262
  skipIfExists: true
1243
1263
  });
1244
1264
  }
1245
- await queueEnvNeat(serviceDir, pkg, generatedFiles);
1265
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
1246
1266
  const entrypointEdits = [];
1247
1267
  try {
1248
1268
  const raw = await fs4.readFile(entryFile, "utf8");
@@ -1279,7 +1299,7 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile) {
1279
1299
  framework: "remix"
1280
1300
  };
1281
1301
  }
1282
- async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile) {
1302
+ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project) {
1283
1303
  const useTs = await isTypeScriptProject(serviceDir);
1284
1304
  const otelInitFile = path4.join(
1285
1305
  serviceDir,
@@ -1296,7 +1316,7 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile) {
1296
1316
  skipIfExists: true
1297
1317
  });
1298
1318
  }
1299
- await queueEnvNeat(serviceDir, pkg, generatedFiles);
1319
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
1300
1320
  if (hooksFile === null) {
1301
1321
  generatedFiles.push({
1302
1322
  file: resolvedHooksFile,
@@ -1340,7 +1360,7 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile) {
1340
1360
  framework: "sveltekit"
1341
1361
  };
1342
1362
  }
1343
- async function planNuxt(serviceDir, pkg, manifestPath) {
1363
+ async function planNuxt(serviceDir, pkg, manifestPath, project) {
1344
1364
  const useTs = await isTypeScriptProject(serviceDir);
1345
1365
  const otelPluginFile = path4.join(
1346
1366
  serviceDir,
@@ -1366,7 +1386,7 @@ async function planNuxt(serviceDir, pkg, manifestPath) {
1366
1386
  skipIfExists: true
1367
1387
  });
1368
1388
  }
1369
- await queueEnvNeat(serviceDir, pkg, generatedFiles);
1389
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
1370
1390
  const empty = dependencyEdits.length === 0 && generatedFiles.length === 0;
1371
1391
  if (empty) {
1372
1392
  return {
@@ -1397,7 +1417,7 @@ async function findAstroMiddleware(serviceDir) {
1397
1417
  }
1398
1418
  return null;
1399
1419
  }
1400
- async function planAstro(serviceDir, pkg, manifestPath) {
1420
+ async function planAstro(serviceDir, pkg, manifestPath, project) {
1401
1421
  const useTs = await isTypeScriptProject(serviceDir);
1402
1422
  const otelInitFile = path4.join(
1403
1423
  serviceDir,
@@ -1415,7 +1435,7 @@ async function planAstro(serviceDir, pkg, manifestPath) {
1415
1435
  skipIfExists: true
1416
1436
  });
1417
1437
  }
1418
- await queueEnvNeat(serviceDir, pkg, generatedFiles);
1438
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
1419
1439
  if (existingMiddleware === null) {
1420
1440
  generatedFiles.push({
1421
1441
  file: middlewareFile,
@@ -1459,9 +1479,10 @@ async function planAstro(serviceDir, pkg, manifestPath) {
1459
1479
  framework: "astro"
1460
1480
  };
1461
1481
  }
1462
- async function plan(serviceDir) {
1482
+ async function plan(serviceDir, opts) {
1463
1483
  const pkg = await readPackageJson(serviceDir);
1464
1484
  const manifestPath = path4.join(serviceDir, "package.json");
1485
+ const project = opts?.project;
1465
1486
  const empty = {
1466
1487
  language: "javascript",
1467
1488
  serviceDir,
@@ -1474,32 +1495,32 @@ async function plan(serviceDir) {
1474
1495
  if (hasNextDependency(pkg)) {
1475
1496
  const nextConfig = await findNextConfig(serviceDir);
1476
1497
  if (nextConfig) {
1477
- return planNext(serviceDir, pkg, manifestPath, nextConfig);
1498
+ return planNext(serviceDir, pkg, manifestPath, nextConfig, project);
1478
1499
  }
1479
1500
  }
1480
1501
  if (hasRemixDependency(pkg)) {
1481
1502
  const remixEntry = await findRemixEntry(serviceDir);
1482
1503
  if (remixEntry) {
1483
- return planRemix(serviceDir, pkg, manifestPath, remixEntry);
1504
+ return planRemix(serviceDir, pkg, manifestPath, remixEntry, project);
1484
1505
  }
1485
1506
  }
1486
1507
  if (hasSvelteKitDependency(pkg)) {
1487
1508
  const hooks = await findSvelteKitHooks(serviceDir);
1488
1509
  const config = await findSvelteKitConfig(serviceDir);
1489
1510
  if (hooks || config) {
1490
- return planSvelteKit(serviceDir, pkg, manifestPath, hooks);
1511
+ return planSvelteKit(serviceDir, pkg, manifestPath, hooks, project);
1491
1512
  }
1492
1513
  }
1493
1514
  if (hasNuxtDependency(pkg)) {
1494
1515
  const nuxtConfig = await findNuxtConfig(serviceDir);
1495
1516
  if (nuxtConfig) {
1496
- return planNuxt(serviceDir, pkg, manifestPath);
1517
+ return planNuxt(serviceDir, pkg, manifestPath, project);
1497
1518
  }
1498
1519
  }
1499
1520
  if (hasAstroDependency(pkg)) {
1500
1521
  const astroConfig = await findAstroConfig(serviceDir);
1501
1522
  if (astroConfig) {
1502
- return planAstro(serviceDir, pkg, manifestPath);
1523
+ return planAstro(serviceDir, pkg, manifestPath, project);
1503
1524
  }
1504
1525
  }
1505
1526
  const entryFile = await resolveEntry(serviceDir, pkg);
@@ -1547,7 +1568,7 @@ async function plan(serviceDir) {
1547
1568
  if (!await exists(envNeatFile)) {
1548
1569
  generatedFiles.push({
1549
1570
  file: envNeatFile,
1550
- contents: renderEnvNeat(pkg.name ?? path4.basename(serviceDir)),
1571
+ contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
1551
1572
  skipIfExists: true
1552
1573
  });
1553
1574
  }
@@ -1572,9 +1593,13 @@ function isAllowedWritePath(serviceDir, target) {
1572
1593
  if (base === "package.json") return true;
1573
1594
  if (base === ".env.neat") return true;
1574
1595
  if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
1575
- if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
1576
- if (/^next\.config\.(?:js|mjs|ts)$/.test(base)) return true;
1577
1596
  const relPosix = rel.split(path4.sep).join("/");
1597
+ if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) {
1598
+ if (relPosix === base) return true;
1599
+ if (relPosix === `src/${base}`) return true;
1600
+ return false;
1601
+ }
1602
+ if (/^next\.config\.(?:js|mjs|ts)$/.test(base)) return true;
1578
1603
  if (relPosix === "app/otel.server.ts" || relPosix === "app/otel.server.js") return true;
1579
1604
  if (/^app\/entry\.server\.(?:tsx?|jsx?)$/.test(relPosix)) return true;
1580
1605
  if (relPosix === "src/otel-init.ts" || relPosix === "src/otel-init.js") return true;
@@ -2079,14 +2104,14 @@ async function extractAndPersist(opts) {
2079
2104
  errorsPath: projectPaths.errorsPath
2080
2105
  };
2081
2106
  }
2082
- async function applyInstallersOver(services) {
2107
+ async function applyInstallersOver(services, project) {
2083
2108
  let instrumented = 0;
2084
2109
  let already = 0;
2085
2110
  let libOnly = 0;
2086
2111
  for (const svc of services) {
2087
2112
  const installer = await pickInstaller(svc.dir);
2088
2113
  if (!installer) continue;
2089
- const plan3 = await installer.plan(svc.dir);
2114
+ const plan3 = await installer.plan(svc.dir, { project });
2090
2115
  if (isEmptyPlan(plan3) && !plan3.libOnly) {
2091
2116
  already++;
2092
2117
  continue;
@@ -2108,28 +2133,106 @@ async function promptYesNo(question) {
2108
2133
  });
2109
2134
  });
2110
2135
  }
2111
- var DEFAULT_DAEMON_READY_TIMEOUT_MS = 15e3;
2112
- async function checkDaemonHealth(restPort) {
2136
+ var DEFAULT_DAEMON_READY_TIMEOUT_MS = 6e4;
2137
+ var PROBE_INTERVAL_MS = 500;
2138
+ async function fetchDaemonHealth(restPort) {
2113
2139
  return new Promise((resolve) => {
2114
2140
  const req = http.get(`http://127.0.0.1:${restPort}/health`, (res) => {
2115
2141
  const ok = res.statusCode !== void 0 && res.statusCode >= 200 && res.statusCode < 300;
2116
- res.resume();
2117
- resolve(ok);
2142
+ if (!ok) {
2143
+ res.resume();
2144
+ resolve(null);
2145
+ return;
2146
+ }
2147
+ let body = "";
2148
+ res.setEncoding("utf8");
2149
+ res.on("data", (chunk) => {
2150
+ body += chunk;
2151
+ });
2152
+ res.on("end", () => {
2153
+ try {
2154
+ resolve(JSON.parse(body));
2155
+ } catch {
2156
+ resolve({ ok: true });
2157
+ }
2158
+ });
2118
2159
  });
2119
- req.on("error", () => resolve(false));
2160
+ req.on("error", () => resolve(null));
2120
2161
  req.setTimeout(1e3, () => {
2121
2162
  req.destroy();
2122
- resolve(false);
2163
+ resolve(null);
2123
2164
  });
2124
2165
  });
2125
2166
  }
2167
+ async function checkDaemonHealth(restPort) {
2168
+ const body = await fetchDaemonHealth(restPort);
2169
+ return body !== null;
2170
+ }
2171
+ async function probeProjectHealth(restPort, name) {
2172
+ return new Promise((resolve) => {
2173
+ const req = http.get(
2174
+ `http://127.0.0.1:${restPort}/projects/${encodeURIComponent(name)}/health`,
2175
+ (res) => {
2176
+ const code = res.statusCode ?? 0;
2177
+ res.resume();
2178
+ if (code >= 200 && code < 300) resolve("active");
2179
+ else resolve("bootstrapping");
2180
+ }
2181
+ );
2182
+ req.on("error", () => resolve("bootstrapping"));
2183
+ req.setTimeout(1e3, () => {
2184
+ req.destroy();
2185
+ resolve("bootstrapping");
2186
+ });
2187
+ });
2188
+ }
2189
+ async function snapshotProjectStatus(restPort, body) {
2190
+ if (body.projects && body.projects.length > 0) {
2191
+ return body.projects.map((p) => ({
2192
+ name: p.name,
2193
+ status: p.status ?? "active"
2194
+ }));
2195
+ }
2196
+ const entries = await listProjects().catch(() => []);
2197
+ if (entries.length === 0) return [];
2198
+ return Promise.all(
2199
+ entries.map(async (entry2) => ({
2200
+ name: entry2.name,
2201
+ status: await probeProjectHealth(restPort, entry2.name)
2202
+ }))
2203
+ );
2204
+ }
2126
2205
  async function waitForDaemonReady(restPort, timeoutMs) {
2127
2206
  const deadline = Date.now() + timeoutMs;
2207
+ let lastBootstrapping = [];
2128
2208
  while (Date.now() < deadline) {
2129
- if (await checkDaemonHealth(restPort)) return true;
2130
- await new Promise((r) => setTimeout(r, 300));
2209
+ const body = await fetchDaemonHealth(restPort);
2210
+ if (body !== null) {
2211
+ const projects2 = await snapshotProjectStatus(restPort, body);
2212
+ const bootstrapping = projects2.filter((p) => p.status === "bootstrapping").map((p) => p.name);
2213
+ const broken = projects2.filter((p) => p.status === "broken").map((p) => p.name);
2214
+ if (bootstrapping.length === 0) {
2215
+ return { ready: true, brokenProjects: broken, stillBootstrapping: [] };
2216
+ }
2217
+ const key = bootstrapping.slice().sort().join(",");
2218
+ const prevKey = lastBootstrapping.slice().sort().join(",");
2219
+ if (key !== prevKey) {
2220
+ const plural = bootstrapping.length === 1 ? "" : "s";
2221
+ console.log(
2222
+ `neat: waiting on ${bootstrapping.length} project${plural}: ${bootstrapping.join(", ")}`
2223
+ );
2224
+ lastBootstrapping = bootstrapping;
2225
+ }
2226
+ }
2227
+ await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
2131
2228
  }
2132
- return false;
2229
+ const final = await fetchDaemonHealth(restPort);
2230
+ const projects = final ? await snapshotProjectStatus(restPort, final) : [];
2231
+ return {
2232
+ ready: false,
2233
+ brokenProjects: projects.filter((p) => p.status === "broken").map((p) => p.name),
2234
+ stillBootstrapping: projects.filter((p) => p.status === "bootstrapping").map((p) => p.name)
2235
+ };
2133
2236
  }
2134
2237
  function spawnDaemonDetached() {
2135
2238
  const here = path6.dirname(new URL(import.meta.url).pathname);
@@ -2150,12 +2253,21 @@ function spawnDaemonDetached() {
2150
2253
  if (!entry2) {
2151
2254
  throw new Error(`orchestrator: cannot locate neatd entry in ${here}`);
2152
2255
  }
2256
+ const env = { ...process.env };
2257
+ const hasToken = typeof env.NEAT_AUTH_TOKEN === "string" && env.NEAT_AUTH_TOKEN.length > 0;
2258
+ if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
2259
+ env.HOST = "127.0.0.1";
2260
+ }
2153
2261
  const child = spawn2(process.execPath, [entry2, "start"], {
2154
2262
  detached: true,
2155
- stdio: "ignore",
2156
- env: process.env
2263
+ // stderr inherits the orchestrator's fd so the daemon's
2264
+ // `BindAuthorityError` message lands in front of the operator instead
2265
+ // of being swallowed (issue #341).
2266
+ stdio: ["ignore", "ignore", "inherit"],
2267
+ env
2157
2268
  });
2158
2269
  child.unref();
2270
+ return child;
2159
2271
  }
2160
2272
  function openBrowser(url) {
2161
2273
  if (!process.stdout.isTTY) return "failed";
@@ -2231,7 +2343,7 @@ async function runOrchestrator(opts) {
2231
2343
  result.steps.apply.skipped = true;
2232
2344
  console.log("skipped instrumentation (--no-instrument)");
2233
2345
  } else {
2234
- const tally = await applyInstallersOver(services);
2346
+ const tally = await applyInstallersOver(services, opts.project);
2235
2347
  result.steps.apply = { ...tally, skipped: false };
2236
2348
  console.log(
2237
2349
  `instrumented ${tally.instrumented}, already ${tally.alreadyInstrumented}, lib-only ${tally.libOnly}`
@@ -2250,12 +2362,25 @@ async function runOrchestrator(opts) {
2250
2362
  return result;
2251
2363
  }
2252
2364
  const ready = await waitForDaemonReady(restPort, timeoutMs);
2253
- result.steps.daemon = ready ? "spawned" : "timed-out";
2254
- if (!ready) {
2365
+ result.steps.daemon = ready.ready ? "spawned" : "timed-out";
2366
+ if (!ready.ready) {
2255
2367
  console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
2368
+ if (ready.stillBootstrapping.length > 0) {
2369
+ console.error(
2370
+ `neat: still bootstrapping: ${ready.stillBootstrapping.join(", ")}`
2371
+ );
2372
+ }
2373
+ if (ready.brokenProjects.length > 0) {
2374
+ console.error(`neat: broken projects: ${ready.brokenProjects.join(", ")}`);
2375
+ }
2256
2376
  result.exitCode = 1;
2257
2377
  return result;
2258
2378
  }
2379
+ if (ready.brokenProjects.length > 0) {
2380
+ console.warn(
2381
+ `neat: ${ready.brokenProjects.length} project(s) reported broken: ${ready.brokenProjects.join(", ")}`
2382
+ );
2383
+ }
2259
2384
  }
2260
2385
  const dashboardUrl = opts.dashboardUrl ?? "http://localhost:6328";
2261
2386
  if (opts.noOpen || !process.stdout.isTTY) {
@@ -2884,7 +3009,7 @@ async function runSync(opts) {
2884
3009
  snapshotPath = target;
2885
3010
  }
2886
3011
  const skipApply = opts.dryRun || opts.noInstrument;
2887
- const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services);
3012
+ const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services, entry2.name);
2888
3013
  const warnings = [];
2889
3014
  let daemonState = "skipped";
2890
3015
  let exitCode = 0;
@@ -2980,6 +3105,8 @@ function usage() {
2980
3105
  console.log(" uninstall <name>");
2981
3106
  console.log(" Remove a project from the registry. Does not touch");
2982
3107
  console.log(" neat-out/, policy.json, or any user file.");
3108
+ console.log(" version Print the installed @neat.is/core version and exit.");
3109
+ console.log(" Aliases: --version, -v.");
2983
3110
  console.log(" skill Install or print the Claude Code MCP drop-in.");
2984
3111
  console.log(" Flags:");
2985
3112
  console.log(" --print-config print the JSON snippet to stdout");
@@ -3106,7 +3233,7 @@ function parseArgs(rest) {
3106
3233
  out.yes = true;
3107
3234
  continue;
3108
3235
  }
3109
- if (arg === "--verbose" || arg === "-v") {
3236
+ if (arg === "--verbose") {
3110
3237
  out.verbose = true;
3111
3238
  continue;
3112
3239
  }
@@ -3165,6 +3292,28 @@ function assignFlag(out, field, value) {
3165
3292
  ;
3166
3293
  out[field] = value;
3167
3294
  }
3295
+ function readPackageVersion() {
3296
+ const here = typeof __dirname !== "undefined" ? __dirname : path8.dirname(fileURLToPath(import.meta.url));
3297
+ const candidates = [
3298
+ path8.resolve(here, "../package.json"),
3299
+ path8.resolve(here, "../../package.json")
3300
+ ];
3301
+ for (const candidate of candidates) {
3302
+ try {
3303
+ const raw = readFileSync(candidate, "utf8");
3304
+ const parsed = JSON.parse(raw);
3305
+ if (parsed.name === "@neat.is/core" && typeof parsed.version === "string") {
3306
+ return parsed.version;
3307
+ }
3308
+ } catch {
3309
+ }
3310
+ }
3311
+ return "unknown";
3312
+ }
3313
+ function printVersion() {
3314
+ process.stdout.write(`${readPackageVersion()}
3315
+ `);
3316
+ }
3168
3317
  function printBanner() {
3169
3318
  console.log("\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557");
3170
3319
  console.log("\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D");
@@ -3174,7 +3323,7 @@ function printBanner() {
3174
3323
  console.log("\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D ");
3175
3324
  console.log("");
3176
3325
  console.log(" Network Expressive Architecting Tool");
3177
- console.log(" neat.is \xB7 v0.2.5 \xB7 BSL 1.1");
3326
+ console.log(" neat.is \xB7 v0.4.0 \xB7 Apache 2.0");
3178
3327
  console.log("");
3179
3328
  }
3180
3329
  function printDiscoveryReport(opts, services) {
@@ -3202,12 +3351,12 @@ function printDiscoveryReport(opts, services) {
3202
3351
  }
3203
3352
  console.log("");
3204
3353
  }
3205
- async function buildPatchSections(services) {
3354
+ async function buildPatchSections(services, project) {
3206
3355
  const sections = [];
3207
3356
  for (const svc of services) {
3208
3357
  const installer = await pickInstaller(svc.dir);
3209
3358
  if (!installer) continue;
3210
- const plan3 = await installer.plan(svc.dir);
3359
+ const plan3 = await installer.plan(svc.dir, { project });
3211
3360
  if (isEmptyPlan(plan3) && !plan3.libOnly) continue;
3212
3361
  sections.push({ installer: installer.name, plan: plan3 });
3213
3362
  }
@@ -3222,7 +3371,7 @@ async function runInit(opts) {
3222
3371
  }
3223
3372
  const services = await discoverServices(opts.scanPath);
3224
3373
  printDiscoveryReport(opts, services);
3225
- const sections = opts.noInstall ? [] : await buildPatchSections(services);
3374
+ const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
3226
3375
  const patch = renderPatch(sections);
3227
3376
  const patchPath = path8.join(opts.scanPath, "neat.patch");
3228
3377
  if (opts.dryRun) {
@@ -3380,6 +3529,10 @@ async function main() {
3380
3529
  usage();
3381
3530
  process.exit(0);
3382
3531
  }
3532
+ if (cmd === "--version" || cmd === "-v" || cmd === "version") {
3533
+ printVersion();
3534
+ process.exit(0);
3535
+ }
3383
3536
  const parsed = parseArgs(rest);
3384
3537
  const { positional, apply: apply3, dryRun, noInstall } = parsed;
3385
3538
  const project = parsed.project ?? DEFAULT_PROJECT;
@@ -3775,7 +3928,7 @@ async function runQueryVerb(cmd, parsed) {
3775
3928
  }
3776
3929
  }
3777
3930
  var entry = process.argv[1] ?? "";
3778
- if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsWith("/neat")) {
3931
+ if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsWith("/neat") || entry.endsWith("/neat.is")) {
3779
3932
  main().catch((err) => {
3780
3933
  console.error(err);
3781
3934
  process.exit(1);
@@ -3785,6 +3938,7 @@ export {
3785
3938
  CLAUDE_SKILL_CONFIG,
3786
3939
  QUERY_VERBS,
3787
3940
  parseArgs,
3941
+ readPackageVersion,
3788
3942
  runInit,
3789
3943
  runQueryVerb,
3790
3944
  runSkill