@neat.is/core 0.4.0 → 0.4.2

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");
@@ -886,6 +901,10 @@ var OTEL_ENV = {
886
901
  key: "OTEL_EXPORTER_OTLP_ENDPOINT",
887
902
  value: "http://localhost:4318"
888
903
  };
904
+ function envServiceName(pkg, serviceDir, project) {
905
+ if (project && project.length > 0) return project;
906
+ return pkg.name ?? path4.basename(serviceDir);
907
+ }
889
908
  async function readPackageJson(serviceDir) {
890
909
  try {
891
910
  const raw = await fs4.readFile(path4.join(serviceDir, "package.json"), "utf8");
@@ -1110,7 +1129,7 @@ function lineIsOtelInjection(line) {
1110
1129
  if (trimmed.length === 0) return false;
1111
1130
  return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
1112
1131
  }
1113
- async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
1132
+ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project) {
1114
1133
  const useTs = await isTypeScriptProject(serviceDir);
1115
1134
  const instrumentationFile = path4.join(serviceDir, useTs ? "instrumentation.ts" : "instrumentation.js");
1116
1135
  const instrumentationNodeFile = path4.join(
@@ -1147,7 +1166,7 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath) {
1147
1166
  if (!await exists(envNeatFile)) {
1148
1167
  generatedFiles.push({
1149
1168
  file: envNeatFile,
1150
- contents: renderEnvNeat(pkg.name ?? path4.basename(serviceDir)),
1169
+ contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
1151
1170
  skipIfExists: true
1152
1171
  });
1153
1172
  }
@@ -1203,12 +1222,12 @@ function buildDependencyEdits(pkg, manifestPath) {
1203
1222
  }
1204
1223
  return edits;
1205
1224
  }
1206
- async function queueEnvNeat(serviceDir, pkg, generatedFiles) {
1225
+ async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
1207
1226
  const envNeatFile = path4.join(serviceDir, ".env.neat");
1208
1227
  if (!await exists(envNeatFile)) {
1209
1228
  generatedFiles.push({
1210
1229
  file: envNeatFile,
1211
- contents: renderEnvNeat(pkg.name ?? path4.basename(serviceDir)),
1230
+ contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
1212
1231
  skipIfExists: true
1213
1232
  });
1214
1233
  }
@@ -1227,7 +1246,7 @@ function fileImportsOtelHook(raw, specifiers) {
1227
1246
  }
1228
1247
  return false;
1229
1248
  }
1230
- async function planRemix(serviceDir, pkg, manifestPath, entryFile) {
1249
+ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
1231
1250
  const useTs = await isTypeScriptProject(serviceDir);
1232
1251
  const otelServerFile = path4.join(
1233
1252
  serviceDir,
@@ -1242,7 +1261,7 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile) {
1242
1261
  skipIfExists: true
1243
1262
  });
1244
1263
  }
1245
- await queueEnvNeat(serviceDir, pkg, generatedFiles);
1264
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
1246
1265
  const entrypointEdits = [];
1247
1266
  try {
1248
1267
  const raw = await fs4.readFile(entryFile, "utf8");
@@ -1279,7 +1298,7 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile) {
1279
1298
  framework: "remix"
1280
1299
  };
1281
1300
  }
1282
- async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile) {
1301
+ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project) {
1283
1302
  const useTs = await isTypeScriptProject(serviceDir);
1284
1303
  const otelInitFile = path4.join(
1285
1304
  serviceDir,
@@ -1296,7 +1315,7 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile) {
1296
1315
  skipIfExists: true
1297
1316
  });
1298
1317
  }
1299
- await queueEnvNeat(serviceDir, pkg, generatedFiles);
1318
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
1300
1319
  if (hooksFile === null) {
1301
1320
  generatedFiles.push({
1302
1321
  file: resolvedHooksFile,
@@ -1340,7 +1359,7 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile) {
1340
1359
  framework: "sveltekit"
1341
1360
  };
1342
1361
  }
1343
- async function planNuxt(serviceDir, pkg, manifestPath) {
1362
+ async function planNuxt(serviceDir, pkg, manifestPath, project) {
1344
1363
  const useTs = await isTypeScriptProject(serviceDir);
1345
1364
  const otelPluginFile = path4.join(
1346
1365
  serviceDir,
@@ -1366,7 +1385,7 @@ async function planNuxt(serviceDir, pkg, manifestPath) {
1366
1385
  skipIfExists: true
1367
1386
  });
1368
1387
  }
1369
- await queueEnvNeat(serviceDir, pkg, generatedFiles);
1388
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
1370
1389
  const empty = dependencyEdits.length === 0 && generatedFiles.length === 0;
1371
1390
  if (empty) {
1372
1391
  return {
@@ -1397,7 +1416,7 @@ async function findAstroMiddleware(serviceDir) {
1397
1416
  }
1398
1417
  return null;
1399
1418
  }
1400
- async function planAstro(serviceDir, pkg, manifestPath) {
1419
+ async function planAstro(serviceDir, pkg, manifestPath, project) {
1401
1420
  const useTs = await isTypeScriptProject(serviceDir);
1402
1421
  const otelInitFile = path4.join(
1403
1422
  serviceDir,
@@ -1415,7 +1434,7 @@ async function planAstro(serviceDir, pkg, manifestPath) {
1415
1434
  skipIfExists: true
1416
1435
  });
1417
1436
  }
1418
- await queueEnvNeat(serviceDir, pkg, generatedFiles);
1437
+ await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
1419
1438
  if (existingMiddleware === null) {
1420
1439
  generatedFiles.push({
1421
1440
  file: middlewareFile,
@@ -1459,9 +1478,10 @@ async function planAstro(serviceDir, pkg, manifestPath) {
1459
1478
  framework: "astro"
1460
1479
  };
1461
1480
  }
1462
- async function plan(serviceDir) {
1481
+ async function plan(serviceDir, opts) {
1463
1482
  const pkg = await readPackageJson(serviceDir);
1464
1483
  const manifestPath = path4.join(serviceDir, "package.json");
1484
+ const project = opts?.project;
1465
1485
  const empty = {
1466
1486
  language: "javascript",
1467
1487
  serviceDir,
@@ -1474,32 +1494,32 @@ async function plan(serviceDir) {
1474
1494
  if (hasNextDependency(pkg)) {
1475
1495
  const nextConfig = await findNextConfig(serviceDir);
1476
1496
  if (nextConfig) {
1477
- return planNext(serviceDir, pkg, manifestPath, nextConfig);
1497
+ return planNext(serviceDir, pkg, manifestPath, nextConfig, project);
1478
1498
  }
1479
1499
  }
1480
1500
  if (hasRemixDependency(pkg)) {
1481
1501
  const remixEntry = await findRemixEntry(serviceDir);
1482
1502
  if (remixEntry) {
1483
- return planRemix(serviceDir, pkg, manifestPath, remixEntry);
1503
+ return planRemix(serviceDir, pkg, manifestPath, remixEntry, project);
1484
1504
  }
1485
1505
  }
1486
1506
  if (hasSvelteKitDependency(pkg)) {
1487
1507
  const hooks = await findSvelteKitHooks(serviceDir);
1488
1508
  const config = await findSvelteKitConfig(serviceDir);
1489
1509
  if (hooks || config) {
1490
- return planSvelteKit(serviceDir, pkg, manifestPath, hooks);
1510
+ return planSvelteKit(serviceDir, pkg, manifestPath, hooks, project);
1491
1511
  }
1492
1512
  }
1493
1513
  if (hasNuxtDependency(pkg)) {
1494
1514
  const nuxtConfig = await findNuxtConfig(serviceDir);
1495
1515
  if (nuxtConfig) {
1496
- return planNuxt(serviceDir, pkg, manifestPath);
1516
+ return planNuxt(serviceDir, pkg, manifestPath, project);
1497
1517
  }
1498
1518
  }
1499
1519
  if (hasAstroDependency(pkg)) {
1500
1520
  const astroConfig = await findAstroConfig(serviceDir);
1501
1521
  if (astroConfig) {
1502
- return planAstro(serviceDir, pkg, manifestPath);
1522
+ return planAstro(serviceDir, pkg, manifestPath, project);
1503
1523
  }
1504
1524
  }
1505
1525
  const entryFile = await resolveEntry(serviceDir, pkg);
@@ -1547,7 +1567,7 @@ async function plan(serviceDir) {
1547
1567
  if (!await exists(envNeatFile)) {
1548
1568
  generatedFiles.push({
1549
1569
  file: envNeatFile,
1550
- contents: renderEnvNeat(pkg.name ?? path4.basename(serviceDir)),
1570
+ contents: renderEnvNeat(envServiceName(pkg, serviceDir, project)),
1551
1571
  skipIfExists: true
1552
1572
  });
1553
1573
  }
@@ -2079,14 +2099,14 @@ async function extractAndPersist(opts) {
2079
2099
  errorsPath: projectPaths.errorsPath
2080
2100
  };
2081
2101
  }
2082
- async function applyInstallersOver(services) {
2102
+ async function applyInstallersOver(services, project) {
2083
2103
  let instrumented = 0;
2084
2104
  let already = 0;
2085
2105
  let libOnly = 0;
2086
2106
  for (const svc of services) {
2087
2107
  const installer = await pickInstaller(svc.dir);
2088
2108
  if (!installer) continue;
2089
- const plan3 = await installer.plan(svc.dir);
2109
+ const plan3 = await installer.plan(svc.dir, { project });
2090
2110
  if (isEmptyPlan(plan3) && !plan3.libOnly) {
2091
2111
  already++;
2092
2112
  continue;
@@ -2108,28 +2128,106 @@ async function promptYesNo(question) {
2108
2128
  });
2109
2129
  });
2110
2130
  }
2111
- var DEFAULT_DAEMON_READY_TIMEOUT_MS = 15e3;
2112
- async function checkDaemonHealth(restPort) {
2131
+ var DEFAULT_DAEMON_READY_TIMEOUT_MS = 6e4;
2132
+ var PROBE_INTERVAL_MS = 500;
2133
+ async function fetchDaemonHealth(restPort) {
2113
2134
  return new Promise((resolve) => {
2114
2135
  const req = http.get(`http://127.0.0.1:${restPort}/health`, (res) => {
2115
2136
  const ok = res.statusCode !== void 0 && res.statusCode >= 200 && res.statusCode < 300;
2116
- res.resume();
2117
- resolve(ok);
2137
+ if (!ok) {
2138
+ res.resume();
2139
+ resolve(null);
2140
+ return;
2141
+ }
2142
+ let body = "";
2143
+ res.setEncoding("utf8");
2144
+ res.on("data", (chunk) => {
2145
+ body += chunk;
2146
+ });
2147
+ res.on("end", () => {
2148
+ try {
2149
+ resolve(JSON.parse(body));
2150
+ } catch {
2151
+ resolve({ ok: true });
2152
+ }
2153
+ });
2118
2154
  });
2119
- req.on("error", () => resolve(false));
2155
+ req.on("error", () => resolve(null));
2120
2156
  req.setTimeout(1e3, () => {
2121
2157
  req.destroy();
2122
- resolve(false);
2158
+ resolve(null);
2123
2159
  });
2124
2160
  });
2125
2161
  }
2162
+ async function checkDaemonHealth(restPort) {
2163
+ const body = await fetchDaemonHealth(restPort);
2164
+ return body !== null;
2165
+ }
2166
+ async function probeProjectHealth(restPort, name) {
2167
+ return new Promise((resolve) => {
2168
+ const req = http.get(
2169
+ `http://127.0.0.1:${restPort}/projects/${encodeURIComponent(name)}/health`,
2170
+ (res) => {
2171
+ const code = res.statusCode ?? 0;
2172
+ res.resume();
2173
+ if (code >= 200 && code < 300) resolve("active");
2174
+ else resolve("bootstrapping");
2175
+ }
2176
+ );
2177
+ req.on("error", () => resolve("bootstrapping"));
2178
+ req.setTimeout(1e3, () => {
2179
+ req.destroy();
2180
+ resolve("bootstrapping");
2181
+ });
2182
+ });
2183
+ }
2184
+ async function snapshotProjectStatus(restPort, body) {
2185
+ if (body.projects && body.projects.length > 0) {
2186
+ return body.projects.map((p) => ({
2187
+ name: p.name,
2188
+ status: p.status ?? "active"
2189
+ }));
2190
+ }
2191
+ const entries = await listProjects().catch(() => []);
2192
+ if (entries.length === 0) return [];
2193
+ return Promise.all(
2194
+ entries.map(async (entry2) => ({
2195
+ name: entry2.name,
2196
+ status: await probeProjectHealth(restPort, entry2.name)
2197
+ }))
2198
+ );
2199
+ }
2126
2200
  async function waitForDaemonReady(restPort, timeoutMs) {
2127
2201
  const deadline = Date.now() + timeoutMs;
2202
+ let lastBootstrapping = [];
2128
2203
  while (Date.now() < deadline) {
2129
- if (await checkDaemonHealth(restPort)) return true;
2130
- await new Promise((r) => setTimeout(r, 300));
2204
+ const body = await fetchDaemonHealth(restPort);
2205
+ if (body !== null) {
2206
+ const projects2 = await snapshotProjectStatus(restPort, body);
2207
+ const bootstrapping = projects2.filter((p) => p.status === "bootstrapping").map((p) => p.name);
2208
+ const broken = projects2.filter((p) => p.status === "broken").map((p) => p.name);
2209
+ if (bootstrapping.length === 0) {
2210
+ return { ready: true, brokenProjects: broken, stillBootstrapping: [] };
2211
+ }
2212
+ const key = bootstrapping.slice().sort().join(",");
2213
+ const prevKey = lastBootstrapping.slice().sort().join(",");
2214
+ if (key !== prevKey) {
2215
+ const plural = bootstrapping.length === 1 ? "" : "s";
2216
+ console.log(
2217
+ `neat: waiting on ${bootstrapping.length} project${plural}: ${bootstrapping.join(", ")}`
2218
+ );
2219
+ lastBootstrapping = bootstrapping;
2220
+ }
2221
+ }
2222
+ await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
2131
2223
  }
2132
- return false;
2224
+ const final = await fetchDaemonHealth(restPort);
2225
+ const projects = final ? await snapshotProjectStatus(restPort, final) : [];
2226
+ return {
2227
+ ready: false,
2228
+ brokenProjects: projects.filter((p) => p.status === "broken").map((p) => p.name),
2229
+ stillBootstrapping: projects.filter((p) => p.status === "bootstrapping").map((p) => p.name)
2230
+ };
2133
2231
  }
2134
2232
  function spawnDaemonDetached() {
2135
2233
  const here = path6.dirname(new URL(import.meta.url).pathname);
@@ -2150,12 +2248,21 @@ function spawnDaemonDetached() {
2150
2248
  if (!entry2) {
2151
2249
  throw new Error(`orchestrator: cannot locate neatd entry in ${here}`);
2152
2250
  }
2251
+ const env = { ...process.env };
2252
+ const hasToken = typeof env.NEAT_AUTH_TOKEN === "string" && env.NEAT_AUTH_TOKEN.length > 0;
2253
+ if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
2254
+ env.HOST = "127.0.0.1";
2255
+ }
2153
2256
  const child = spawn2(process.execPath, [entry2, "start"], {
2154
2257
  detached: true,
2155
- stdio: "ignore",
2156
- env: process.env
2258
+ // stderr inherits the orchestrator's fd so the daemon's
2259
+ // `BindAuthorityError` message lands in front of the operator instead
2260
+ // of being swallowed (issue #341).
2261
+ stdio: ["ignore", "ignore", "inherit"],
2262
+ env
2157
2263
  });
2158
2264
  child.unref();
2265
+ return child;
2159
2266
  }
2160
2267
  function openBrowser(url) {
2161
2268
  if (!process.stdout.isTTY) return "failed";
@@ -2231,7 +2338,7 @@ async function runOrchestrator(opts) {
2231
2338
  result.steps.apply.skipped = true;
2232
2339
  console.log("skipped instrumentation (--no-instrument)");
2233
2340
  } else {
2234
- const tally = await applyInstallersOver(services);
2341
+ const tally = await applyInstallersOver(services, opts.project);
2235
2342
  result.steps.apply = { ...tally, skipped: false };
2236
2343
  console.log(
2237
2344
  `instrumented ${tally.instrumented}, already ${tally.alreadyInstrumented}, lib-only ${tally.libOnly}`
@@ -2250,12 +2357,25 @@ async function runOrchestrator(opts) {
2250
2357
  return result;
2251
2358
  }
2252
2359
  const ready = await waitForDaemonReady(restPort, timeoutMs);
2253
- result.steps.daemon = ready ? "spawned" : "timed-out";
2254
- if (!ready) {
2360
+ result.steps.daemon = ready.ready ? "spawned" : "timed-out";
2361
+ if (!ready.ready) {
2255
2362
  console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
2363
+ if (ready.stillBootstrapping.length > 0) {
2364
+ console.error(
2365
+ `neat: still bootstrapping: ${ready.stillBootstrapping.join(", ")}`
2366
+ );
2367
+ }
2368
+ if (ready.brokenProjects.length > 0) {
2369
+ console.error(`neat: broken projects: ${ready.brokenProjects.join(", ")}`);
2370
+ }
2256
2371
  result.exitCode = 1;
2257
2372
  return result;
2258
2373
  }
2374
+ if (ready.brokenProjects.length > 0) {
2375
+ console.warn(
2376
+ `neat: ${ready.brokenProjects.length} project(s) reported broken: ${ready.brokenProjects.join(", ")}`
2377
+ );
2378
+ }
2259
2379
  }
2260
2380
  const dashboardUrl = opts.dashboardUrl ?? "http://localhost:6328";
2261
2381
  if (opts.noOpen || !process.stdout.isTTY) {
@@ -2884,7 +3004,7 @@ async function runSync(opts) {
2884
3004
  snapshotPath = target;
2885
3005
  }
2886
3006
  const skipApply = opts.dryRun || opts.noInstrument;
2887
- const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services);
3007
+ const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0 } : await applyInstallersOver(persisted.services, entry2.name);
2888
3008
  const warnings = [];
2889
3009
  let daemonState = "skipped";
2890
3010
  let exitCode = 0;
@@ -2980,6 +3100,8 @@ function usage() {
2980
3100
  console.log(" uninstall <name>");
2981
3101
  console.log(" Remove a project from the registry. Does not touch");
2982
3102
  console.log(" neat-out/, policy.json, or any user file.");
3103
+ console.log(" version Print the installed @neat.is/core version and exit.");
3104
+ console.log(" Aliases: --version, -v.");
2983
3105
  console.log(" skill Install or print the Claude Code MCP drop-in.");
2984
3106
  console.log(" Flags:");
2985
3107
  console.log(" --print-config print the JSON snippet to stdout");
@@ -3106,7 +3228,7 @@ function parseArgs(rest) {
3106
3228
  out.yes = true;
3107
3229
  continue;
3108
3230
  }
3109
- if (arg === "--verbose" || arg === "-v") {
3231
+ if (arg === "--verbose") {
3110
3232
  out.verbose = true;
3111
3233
  continue;
3112
3234
  }
@@ -3165,6 +3287,28 @@ function assignFlag(out, field, value) {
3165
3287
  ;
3166
3288
  out[field] = value;
3167
3289
  }
3290
+ function readPackageVersion() {
3291
+ const here = typeof __dirname !== "undefined" ? __dirname : path8.dirname(fileURLToPath(import.meta.url));
3292
+ const candidates = [
3293
+ path8.resolve(here, "../package.json"),
3294
+ path8.resolve(here, "../../package.json")
3295
+ ];
3296
+ for (const candidate of candidates) {
3297
+ try {
3298
+ const raw = readFileSync(candidate, "utf8");
3299
+ const parsed = JSON.parse(raw);
3300
+ if (parsed.name === "@neat.is/core" && typeof parsed.version === "string") {
3301
+ return parsed.version;
3302
+ }
3303
+ } catch {
3304
+ }
3305
+ }
3306
+ return "unknown";
3307
+ }
3308
+ function printVersion() {
3309
+ process.stdout.write(`${readPackageVersion()}
3310
+ `);
3311
+ }
3168
3312
  function printBanner() {
3169
3313
  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
3314
  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 +3318,7 @@ function printBanner() {
3174
3318
  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
3319
  console.log("");
3176
3320
  console.log(" Network Expressive Architecting Tool");
3177
- console.log(" neat.is \xB7 v0.2.5 \xB7 BSL 1.1");
3321
+ console.log(" neat.is \xB7 v0.4.0 \xB7 Apache 2.0");
3178
3322
  console.log("");
3179
3323
  }
3180
3324
  function printDiscoveryReport(opts, services) {
@@ -3202,12 +3346,12 @@ function printDiscoveryReport(opts, services) {
3202
3346
  }
3203
3347
  console.log("");
3204
3348
  }
3205
- async function buildPatchSections(services) {
3349
+ async function buildPatchSections(services, project) {
3206
3350
  const sections = [];
3207
3351
  for (const svc of services) {
3208
3352
  const installer = await pickInstaller(svc.dir);
3209
3353
  if (!installer) continue;
3210
- const plan3 = await installer.plan(svc.dir);
3354
+ const plan3 = await installer.plan(svc.dir, { project });
3211
3355
  if (isEmptyPlan(plan3) && !plan3.libOnly) continue;
3212
3356
  sections.push({ installer: installer.name, plan: plan3 });
3213
3357
  }
@@ -3222,7 +3366,7 @@ async function runInit(opts) {
3222
3366
  }
3223
3367
  const services = await discoverServices(opts.scanPath);
3224
3368
  printDiscoveryReport(opts, services);
3225
- const sections = opts.noInstall ? [] : await buildPatchSections(services);
3369
+ const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
3226
3370
  const patch = renderPatch(sections);
3227
3371
  const patchPath = path8.join(opts.scanPath, "neat.patch");
3228
3372
  if (opts.dryRun) {
@@ -3380,6 +3524,10 @@ async function main() {
3380
3524
  usage();
3381
3525
  process.exit(0);
3382
3526
  }
3527
+ if (cmd === "--version" || cmd === "-v" || cmd === "version") {
3528
+ printVersion();
3529
+ process.exit(0);
3530
+ }
3383
3531
  const parsed = parseArgs(rest);
3384
3532
  const { positional, apply: apply3, dryRun, noInstall } = parsed;
3385
3533
  const project = parsed.project ?? DEFAULT_PROJECT;
@@ -3785,6 +3933,7 @@ export {
3785
3933
  CLAUDE_SKILL_CONFIG,
3786
3934
  QUERY_VERBS,
3787
3935
  parseArgs,
3936
+ readPackageVersion,
3788
3937
  runInit,
3789
3938
  runQueryVerb,
3790
3939
  runSkill