@neat.is/core 0.3.1 → 0.3.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.js CHANGED
@@ -45,9 +45,10 @@ import {
45
45
 
46
46
  // src/cli.ts
47
47
  import path4 from "path";
48
- import { promises as fs3 } from "fs";
48
+ import { promises as fs4 } from "fs";
49
49
 
50
50
  // src/watch.ts
51
+ import fs from "fs";
51
52
  import path from "path";
52
53
  import chokidar from "chokidar";
53
54
 
@@ -143,6 +144,16 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
143
144
  durationMs: Date.now() - started
144
145
  };
145
146
  }
147
+ var IGNORED_WATCH_GLOBS = [
148
+ "**/node_modules/**",
149
+ "**/.git/**",
150
+ "**/dist/**",
151
+ "**/build/**",
152
+ "**/.turbo/**",
153
+ "**/.next/**",
154
+ "**/neat-out/**",
155
+ "**/.DS_Store"
156
+ ];
146
157
  var IGNORED_WATCH_PATHS = [
147
158
  /(?:^|[\\/])node_modules[\\/]/,
148
159
  /(?:^|[\\/])\.git[\\/]/,
@@ -156,6 +167,35 @@ var IGNORED_WATCH_PATHS = [
156
167
  function shouldIgnore(absPath) {
157
168
  return IGNORED_WATCH_PATHS.some((re) => re.test(absPath));
158
169
  }
170
+ var DARWIN_POLLING_DIR_THRESHOLD = 400;
171
+ function countWatchableDirs(scanPath, limit) {
172
+ let count = 0;
173
+ const visit = (dir, depth) => {
174
+ if (count >= limit) return;
175
+ let entries;
176
+ try {
177
+ entries = fs.readdirSync(dir, { withFileTypes: true });
178
+ } catch {
179
+ return;
180
+ }
181
+ for (const e of entries) {
182
+ if (count >= limit) return;
183
+ if (!e.isDirectory()) continue;
184
+ if (IGNORED_WATCH_PATHS.some((re) => re.test(path.join(dir, e.name) + path.sep))) continue;
185
+ count++;
186
+ if (depth < 2) visit(path.join(dir, e.name), depth + 1);
187
+ }
188
+ };
189
+ visit(scanPath, 0);
190
+ return count;
191
+ }
192
+ function shouldUsePolling(scanPath) {
193
+ const env = process.env.NEAT_WATCH_POLLING;
194
+ if (env === "1" || env === "true") return true;
195
+ if (env === "0" || env === "false") return false;
196
+ if (process.platform !== "darwin") return false;
197
+ return countWatchableDirs(scanPath, DARWIN_POLLING_DIR_THRESHOLD) >= DARWIN_POLLING_DIR_THRESHOLD;
198
+ }
159
199
  async function startWatch(graph, opts) {
160
200
  const debounceMs = opts.debounceMs ?? 1e3;
161
201
  const projectName = opts.project ?? DEFAULT_PROJECT;
@@ -325,10 +365,19 @@ async function startWatch(graph, opts) {
325
365
  }
326
366
  schedule();
327
367
  };
368
+ const usePolling = shouldUsePolling(opts.scanPath);
369
+ if (usePolling) {
370
+ const reason = process.env.NEAT_WATCH_POLLING === "1" || process.env.NEAT_WATCH_POLLING === "true" ? "NEAT_WATCH_POLLING env override" : "darwin heuristic \u2014 large scan root, kqueue cap risk";
371
+ console.log(`[${projectName}] watch: usePolling=true (${reason})`);
372
+ }
328
373
  const watcher = chokidar.watch(opts.scanPath, {
329
374
  ignoreInitial: true,
330
- ignored: (p) => shouldIgnore(p),
375
+ // Glob array prunes at descent time (#233) so chokidar never opens a
376
+ // kqueue handle for `node_modules` and friends. The function backstop
377
+ // catches any path that slipped through and matches the regex set.
378
+ ignored: [...IGNORED_WATCH_GLOBS, (p) => shouldIgnore(p)],
331
379
  persistent: true,
380
+ usePolling,
332
381
  awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
333
382
  });
334
383
  watcher.on("add", onPath);
@@ -360,7 +409,7 @@ async function startWatch(graph, opts) {
360
409
  }
361
410
 
362
411
  // src/installers/javascript.ts
363
- import { promises as fs } from "fs";
412
+ import { promises as fs2 } from "fs";
364
413
  import path2 from "path";
365
414
  var SDK_PACKAGES = [
366
415
  { name: "@opentelemetry/api", version: "^1.9.0" },
@@ -377,7 +426,7 @@ var OTEL_ENV = {
377
426
  };
378
427
  async function readPackageJson(serviceDir) {
379
428
  try {
380
- const raw = await fs.readFile(path2.join(serviceDir, "package.json"), "utf8");
429
+ const raw = await fs2.readFile(path2.join(serviceDir, "package.json"), "utf8");
381
430
  return JSON.parse(raw);
382
431
  } catch {
383
432
  return null;
@@ -443,7 +492,7 @@ async function apply(installPlan) {
443
492
  const originals = /* @__PURE__ */ new Map();
444
493
  for (const file of touched) {
445
494
  try {
446
- originals.set(file, await fs.readFile(file, "utf8"));
495
+ originals.set(file, await fs2.readFile(file, "utf8"));
447
496
  } catch {
448
497
  }
449
498
  }
@@ -469,8 +518,8 @@ async function apply(installPlan) {
469
518
  }
470
519
  const newRaw = JSON.stringify(pkg, null, 2) + "\n";
471
520
  const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
472
- await fs.writeFile(tmp, newRaw, "utf8");
473
- await fs.rename(tmp, file);
521
+ await fs2.writeFile(tmp, newRaw, "utf8");
522
+ await fs2.rename(tmp, file);
474
523
  }
475
524
  } catch (err) {
476
525
  await rollback(installPlan, originals);
@@ -481,7 +530,7 @@ async function rollback(installPlan, originals) {
481
530
  const restored = [];
482
531
  for (const [file, raw] of originals.entries()) {
483
532
  try {
484
- await fs.writeFile(file, raw, "utf8");
533
+ await fs2.writeFile(file, raw, "utf8");
485
534
  restored.push(file);
486
535
  } catch {
487
536
  }
@@ -496,7 +545,7 @@ async function rollback(installPlan, originals) {
496
545
  ""
497
546
  ];
498
547
  const rollbackPath = path2.join(installPlan.serviceDir, "neat-rollback.patch");
499
- await fs.writeFile(rollbackPath, lines.join("\n"), "utf8");
548
+ await fs2.writeFile(rollbackPath, lines.join("\n"), "utf8");
500
549
  }
501
550
  var javascriptInstaller = {
502
551
  name: "javascript",
@@ -506,7 +555,7 @@ var javascriptInstaller = {
506
555
  };
507
556
 
508
557
  // src/installers/python.ts
509
- import { promises as fs2 } from "fs";
558
+ import { promises as fs3 } from "fs";
510
559
  import path3 from "path";
511
560
  var SDK_PACKAGES2 = [
512
561
  { name: "opentelemetry-distro", version: ">=0.49b0" },
@@ -519,7 +568,7 @@ var OTEL_ENV2 = {
519
568
  };
520
569
  async function exists(p) {
521
570
  try {
522
- await fs2.stat(p);
571
+ await fs3.stat(p);
523
572
  return true;
524
573
  } catch {
525
574
  return false;
@@ -540,7 +589,7 @@ function reqPackageName(line) {
540
589
  async function planRequirementsTxtEdits(serviceDir) {
541
590
  const file = path3.join(serviceDir, "requirements.txt");
542
591
  if (!await exists(file)) return null;
543
- const raw = await fs2.readFile(file, "utf8");
592
+ const raw = await fs3.readFile(file, "utf8");
544
593
  const presentNames = new Set(
545
594
  raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
546
595
  );
@@ -550,7 +599,7 @@ async function planRequirementsTxtEdits(serviceDir) {
550
599
  async function planProcfileEdits(serviceDir) {
551
600
  const procfile = path3.join(serviceDir, "Procfile");
552
601
  if (!await exists(procfile)) return [];
553
- const raw = await fs2.readFile(procfile, "utf8");
602
+ const raw = await fs3.readFile(procfile, "utf8");
554
603
  const edits = [];
555
604
  for (const line of raw.split(/\r?\n/)) {
556
605
  if (line.length === 0) continue;
@@ -602,8 +651,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
602
651
  const next = `${original}${trailing}${newlines.join("\n")}
603
652
  `;
604
653
  const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
605
- await fs2.writeFile(tmp, next, "utf8");
606
- await fs2.rename(tmp, manifest);
654
+ await fs3.writeFile(tmp, next, "utf8");
655
+ await fs3.rename(tmp, manifest);
607
656
  }
608
657
  async function applyProcfile(procfile, edits, original) {
609
658
  let next = original;
@@ -612,8 +661,8 @@ async function applyProcfile(procfile, edits, original) {
612
661
  next = next.replace(e.before, e.after);
613
662
  }
614
663
  const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
615
- await fs2.writeFile(tmp, next, "utf8");
616
- await fs2.rename(tmp, procfile);
664
+ await fs3.writeFile(tmp, next, "utf8");
665
+ await fs3.rename(tmp, procfile);
617
666
  }
618
667
  async function apply2(installPlan) {
619
668
  const touched = /* @__PURE__ */ new Set();
@@ -623,7 +672,7 @@ async function apply2(installPlan) {
623
672
  const originals = /* @__PURE__ */ new Map();
624
673
  for (const file of touched) {
625
674
  try {
626
- originals.set(file, await fs2.readFile(file, "utf8"));
675
+ originals.set(file, await fs3.readFile(file, "utf8"));
627
676
  } catch {
628
677
  }
629
678
  }
@@ -651,7 +700,7 @@ async function rollback2(installPlan, originals) {
651
700
  const restored = [];
652
701
  for (const [file, raw] of originals.entries()) {
653
702
  try {
654
- await fs2.writeFile(file, raw, "utf8");
703
+ await fs3.writeFile(file, raw, "utf8");
655
704
  restored.push(file);
656
705
  } catch {
657
706
  }
@@ -666,7 +715,7 @@ async function rollback2(installPlan, originals) {
666
715
  ""
667
716
  ];
668
717
  const rollbackPath = path3.join(installPlan.serviceDir, "neat-rollback.patch");
669
- await fs2.writeFile(rollbackPath, lines.join("\n"), "utf8");
718
+ await fs3.writeFile(rollbackPath, lines.join("\n"), "utf8");
670
719
  }
671
720
  var pythonInstaller = {
672
721
  name: "python",
@@ -1500,7 +1549,7 @@ async function buildPatchSections(services) {
1500
1549
  }
1501
1550
  async function runInit(opts) {
1502
1551
  const written = [];
1503
- const stat = await fs3.stat(opts.scanPath).catch(() => null);
1552
+ const stat = await fs4.stat(opts.scanPath).catch(() => null);
1504
1553
  if (!stat || !stat.isDirectory()) {
1505
1554
  console.error(`neat init: ${opts.scanPath} is not a directory`);
1506
1555
  return { exitCode: 2, writtenFiles: written };
@@ -1511,7 +1560,7 @@ async function runInit(opts) {
1511
1560
  const patch = renderPatch(sections);
1512
1561
  const patchPath = path4.join(opts.scanPath, "neat.patch");
1513
1562
  if (opts.dryRun) {
1514
- await fs3.writeFile(patchPath, patch, "utf8");
1563
+ await fs4.writeFile(patchPath, patch, "utf8");
1515
1564
  written.push(patchPath);
1516
1565
  console.log(`dry-run: patch written to ${patchPath}`);
1517
1566
  console.log("rerun without --dry-run to register and snapshot.");
@@ -1551,7 +1600,7 @@ async function runInit(opts) {
1551
1600
  console.log("patch applied. Run `npm install` (or your language equivalent) to refresh lockfiles.");
1552
1601
  }
1553
1602
  } else {
1554
- await fs3.writeFile(patchPath, patch, "utf8");
1603
+ await fs4.writeFile(patchPath, patch, "utf8");
1555
1604
  written.push(patchPath);
1556
1605
  }
1557
1606
  }
@@ -1605,7 +1654,7 @@ async function runSkill(opts) {
1605
1654
  const target = claudeConfigPath();
1606
1655
  let existing = {};
1607
1656
  try {
1608
- existing = JSON.parse(await fs3.readFile(target, "utf8"));
1657
+ existing = JSON.parse(await fs4.readFile(target, "utf8"));
1609
1658
  } catch (err) {
1610
1659
  if (err.code !== "ENOENT") {
1611
1660
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -1617,8 +1666,8 @@ async function runSkill(opts) {
1617
1666
  ...existing,
1618
1667
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
1619
1668
  };
1620
- await fs3.mkdir(path4.dirname(target), { recursive: true });
1621
- await fs3.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
1669
+ await fs4.mkdir(path4.dirname(target), { recursive: true });
1670
+ await fs4.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
1622
1671
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
1623
1672
  console.log("restart Claude Code to pick up the new MCP server.");
1624
1673
  return { exitCode: 0 };
@@ -1678,7 +1727,7 @@ async function main() {
1678
1727
  process.exit(2);
1679
1728
  }
1680
1729
  const scanPath = path4.resolve(target);
1681
- const stat = await fs3.stat(scanPath).catch(() => null);
1730
+ const stat = await fs4.stat(scanPath).catch(() => null);
1682
1731
  if (!stat || !stat.isDirectory()) {
1683
1732
  console.error(`neat watch: ${scanPath} is not a directory`);
1684
1733
  process.exit(2);