@neat.is/core 0.3.1 → 0.3.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/{chunk-66Z5IEFY.js → chunk-ICFH326H.js} +2 -2
- package/dist/{chunk-FIXKIYNF.js → chunk-VABXPLDT.js} +457 -206
- package/dist/chunk-VABXPLDT.js.map +1 -0
- package/dist/cli.cjs +601 -306
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +100 -53
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +451 -221
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +470 -228
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +16 -4
- package/dist/neatd.js.map +1 -1
- package/dist/server.cjs +420 -190
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-FIXKIYNF.js.map +0 -1
- /package/dist/{chunk-66Z5IEFY.js.map → chunk-ICFH326H.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -21,7 +21,9 @@ import {
|
|
|
21
21
|
ensureCompatLoaded,
|
|
22
22
|
evaluateAllPolicies,
|
|
23
23
|
extractFromDirectory,
|
|
24
|
+
formatExtractionBanner,
|
|
24
25
|
getGraph,
|
|
26
|
+
isStrictExtractionEnabled,
|
|
25
27
|
listProjects,
|
|
26
28
|
loadGraphFromDisk,
|
|
27
29
|
loadPolicyFile,
|
|
@@ -31,11 +33,12 @@ import {
|
|
|
31
33
|
promoteFrontierNodes,
|
|
32
34
|
removeProject,
|
|
33
35
|
resetGraph,
|
|
36
|
+
retireEdgesByFile,
|
|
34
37
|
saveGraphToDisk,
|
|
35
38
|
setStatus,
|
|
36
39
|
startPersistLoop,
|
|
37
40
|
startStalenessLoop
|
|
38
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-VABXPLDT.js";
|
|
39
42
|
import {
|
|
40
43
|
startOtelGrpcReceiver
|
|
41
44
|
} from "./chunk-G3PDTGOW.js";
|
|
@@ -45,28 +48,12 @@ import {
|
|
|
45
48
|
|
|
46
49
|
// src/cli.ts
|
|
47
50
|
import path4 from "path";
|
|
48
|
-
import { promises as
|
|
51
|
+
import { promises as fs4 } from "fs";
|
|
49
52
|
|
|
50
53
|
// src/watch.ts
|
|
54
|
+
import fs from "fs";
|
|
51
55
|
import path from "path";
|
|
52
56
|
import chokidar from "chokidar";
|
|
53
|
-
|
|
54
|
-
// src/extract/retire.ts
|
|
55
|
-
import { Provenance } from "@neat.is/types";
|
|
56
|
-
function retireEdgesByFile(graph, file) {
|
|
57
|
-
const normalized = file.split("\\").join("/");
|
|
58
|
-
const toDrop = [];
|
|
59
|
-
graph.forEachEdge((id, attrs) => {
|
|
60
|
-
const edge = attrs;
|
|
61
|
-
if (edge.provenance !== Provenance.EXTRACTED) return;
|
|
62
|
-
if (!edge.evidence?.file) return;
|
|
63
|
-
if (edge.evidence.file === normalized) toDrop.push(id);
|
|
64
|
-
});
|
|
65
|
-
for (const id of toDrop) graph.dropEdge(id);
|
|
66
|
-
return toDrop.length;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// src/watch.ts
|
|
70
57
|
var ALL_PHASES = [
|
|
71
58
|
"services",
|
|
72
59
|
"aliases",
|
|
@@ -143,6 +130,16 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
|
|
|
143
130
|
durationMs: Date.now() - started
|
|
144
131
|
};
|
|
145
132
|
}
|
|
133
|
+
var IGNORED_WATCH_GLOBS = [
|
|
134
|
+
"**/node_modules/**",
|
|
135
|
+
"**/.git/**",
|
|
136
|
+
"**/dist/**",
|
|
137
|
+
"**/build/**",
|
|
138
|
+
"**/.turbo/**",
|
|
139
|
+
"**/.next/**",
|
|
140
|
+
"**/neat-out/**",
|
|
141
|
+
"**/.DS_Store"
|
|
142
|
+
];
|
|
146
143
|
var IGNORED_WATCH_PATHS = [
|
|
147
144
|
/(?:^|[\\/])node_modules[\\/]/,
|
|
148
145
|
/(?:^|[\\/])\.git[\\/]/,
|
|
@@ -156,6 +153,35 @@ var IGNORED_WATCH_PATHS = [
|
|
|
156
153
|
function shouldIgnore(absPath) {
|
|
157
154
|
return IGNORED_WATCH_PATHS.some((re) => re.test(absPath));
|
|
158
155
|
}
|
|
156
|
+
var DARWIN_POLLING_DIR_THRESHOLD = 400;
|
|
157
|
+
function countWatchableDirs(scanPath, limit) {
|
|
158
|
+
let count = 0;
|
|
159
|
+
const visit = (dir, depth) => {
|
|
160
|
+
if (count >= limit) return;
|
|
161
|
+
let entries;
|
|
162
|
+
try {
|
|
163
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
164
|
+
} catch {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
for (const e of entries) {
|
|
168
|
+
if (count >= limit) return;
|
|
169
|
+
if (!e.isDirectory()) continue;
|
|
170
|
+
if (IGNORED_WATCH_PATHS.some((re) => re.test(path.join(dir, e.name) + path.sep))) continue;
|
|
171
|
+
count++;
|
|
172
|
+
if (depth < 2) visit(path.join(dir, e.name), depth + 1);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
visit(scanPath, 0);
|
|
176
|
+
return count;
|
|
177
|
+
}
|
|
178
|
+
function shouldUsePolling(scanPath) {
|
|
179
|
+
const env = process.env.NEAT_WATCH_POLLING;
|
|
180
|
+
if (env === "1" || env === "true") return true;
|
|
181
|
+
if (env === "0" || env === "false") return false;
|
|
182
|
+
if (process.platform !== "darwin") return false;
|
|
183
|
+
return countWatchableDirs(scanPath, DARWIN_POLLING_DIR_THRESHOLD) >= DARWIN_POLLING_DIR_THRESHOLD;
|
|
184
|
+
}
|
|
159
185
|
async function startWatch(graph, opts) {
|
|
160
186
|
const debounceMs = opts.debounceMs ?? 1e3;
|
|
161
187
|
const projectName = opts.project ?? DEFAULT_PROJECT;
|
|
@@ -325,10 +351,19 @@ async function startWatch(graph, opts) {
|
|
|
325
351
|
}
|
|
326
352
|
schedule();
|
|
327
353
|
};
|
|
354
|
+
const usePolling = shouldUsePolling(opts.scanPath);
|
|
355
|
+
if (usePolling) {
|
|
356
|
+
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";
|
|
357
|
+
console.log(`[${projectName}] watch: usePolling=true (${reason})`);
|
|
358
|
+
}
|
|
328
359
|
const watcher = chokidar.watch(opts.scanPath, {
|
|
329
360
|
ignoreInitial: true,
|
|
330
|
-
|
|
361
|
+
// Glob array prunes at descent time (#233) so chokidar never opens a
|
|
362
|
+
// kqueue handle for `node_modules` and friends. The function backstop
|
|
363
|
+
// catches any path that slipped through and matches the regex set.
|
|
364
|
+
ignored: [...IGNORED_WATCH_GLOBS, (p) => shouldIgnore(p)],
|
|
331
365
|
persistent: true,
|
|
366
|
+
usePolling,
|
|
332
367
|
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
|
|
333
368
|
});
|
|
334
369
|
watcher.on("add", onPath);
|
|
@@ -360,7 +395,7 @@ async function startWatch(graph, opts) {
|
|
|
360
395
|
}
|
|
361
396
|
|
|
362
397
|
// src/installers/javascript.ts
|
|
363
|
-
import { promises as
|
|
398
|
+
import { promises as fs2 } from "fs";
|
|
364
399
|
import path2 from "path";
|
|
365
400
|
var SDK_PACKAGES = [
|
|
366
401
|
{ name: "@opentelemetry/api", version: "^1.9.0" },
|
|
@@ -377,7 +412,7 @@ var OTEL_ENV = {
|
|
|
377
412
|
};
|
|
378
413
|
async function readPackageJson(serviceDir) {
|
|
379
414
|
try {
|
|
380
|
-
const raw = await
|
|
415
|
+
const raw = await fs2.readFile(path2.join(serviceDir, "package.json"), "utf8");
|
|
381
416
|
return JSON.parse(raw);
|
|
382
417
|
} catch {
|
|
383
418
|
return null;
|
|
@@ -443,7 +478,7 @@ async function apply(installPlan) {
|
|
|
443
478
|
const originals = /* @__PURE__ */ new Map();
|
|
444
479
|
for (const file of touched) {
|
|
445
480
|
try {
|
|
446
|
-
originals.set(file, await
|
|
481
|
+
originals.set(file, await fs2.readFile(file, "utf8"));
|
|
447
482
|
} catch {
|
|
448
483
|
}
|
|
449
484
|
}
|
|
@@ -469,8 +504,8 @@ async function apply(installPlan) {
|
|
|
469
504
|
}
|
|
470
505
|
const newRaw = JSON.stringify(pkg, null, 2) + "\n";
|
|
471
506
|
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
472
|
-
await
|
|
473
|
-
await
|
|
507
|
+
await fs2.writeFile(tmp, newRaw, "utf8");
|
|
508
|
+
await fs2.rename(tmp, file);
|
|
474
509
|
}
|
|
475
510
|
} catch (err) {
|
|
476
511
|
await rollback(installPlan, originals);
|
|
@@ -481,7 +516,7 @@ async function rollback(installPlan, originals) {
|
|
|
481
516
|
const restored = [];
|
|
482
517
|
for (const [file, raw] of originals.entries()) {
|
|
483
518
|
try {
|
|
484
|
-
await
|
|
519
|
+
await fs2.writeFile(file, raw, "utf8");
|
|
485
520
|
restored.push(file);
|
|
486
521
|
} catch {
|
|
487
522
|
}
|
|
@@ -496,7 +531,7 @@ async function rollback(installPlan, originals) {
|
|
|
496
531
|
""
|
|
497
532
|
];
|
|
498
533
|
const rollbackPath = path2.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
499
|
-
await
|
|
534
|
+
await fs2.writeFile(rollbackPath, lines.join("\n"), "utf8");
|
|
500
535
|
}
|
|
501
536
|
var javascriptInstaller = {
|
|
502
537
|
name: "javascript",
|
|
@@ -506,7 +541,7 @@ var javascriptInstaller = {
|
|
|
506
541
|
};
|
|
507
542
|
|
|
508
543
|
// src/installers/python.ts
|
|
509
|
-
import { promises as
|
|
544
|
+
import { promises as fs3 } from "fs";
|
|
510
545
|
import path3 from "path";
|
|
511
546
|
var SDK_PACKAGES2 = [
|
|
512
547
|
{ name: "opentelemetry-distro", version: ">=0.49b0" },
|
|
@@ -519,7 +554,7 @@ var OTEL_ENV2 = {
|
|
|
519
554
|
};
|
|
520
555
|
async function exists(p) {
|
|
521
556
|
try {
|
|
522
|
-
await
|
|
557
|
+
await fs3.stat(p);
|
|
523
558
|
return true;
|
|
524
559
|
} catch {
|
|
525
560
|
return false;
|
|
@@ -540,7 +575,7 @@ function reqPackageName(line) {
|
|
|
540
575
|
async function planRequirementsTxtEdits(serviceDir) {
|
|
541
576
|
const file = path3.join(serviceDir, "requirements.txt");
|
|
542
577
|
if (!await exists(file)) return null;
|
|
543
|
-
const raw = await
|
|
578
|
+
const raw = await fs3.readFile(file, "utf8");
|
|
544
579
|
const presentNames = new Set(
|
|
545
580
|
raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
|
|
546
581
|
);
|
|
@@ -550,7 +585,7 @@ async function planRequirementsTxtEdits(serviceDir) {
|
|
|
550
585
|
async function planProcfileEdits(serviceDir) {
|
|
551
586
|
const procfile = path3.join(serviceDir, "Procfile");
|
|
552
587
|
if (!await exists(procfile)) return [];
|
|
553
|
-
const raw = await
|
|
588
|
+
const raw = await fs3.readFile(procfile, "utf8");
|
|
554
589
|
const edits = [];
|
|
555
590
|
for (const line of raw.split(/\r?\n/)) {
|
|
556
591
|
if (line.length === 0) continue;
|
|
@@ -602,8 +637,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
|
|
|
602
637
|
const next = `${original}${trailing}${newlines.join("\n")}
|
|
603
638
|
`;
|
|
604
639
|
const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
|
|
605
|
-
await
|
|
606
|
-
await
|
|
640
|
+
await fs3.writeFile(tmp, next, "utf8");
|
|
641
|
+
await fs3.rename(tmp, manifest);
|
|
607
642
|
}
|
|
608
643
|
async function applyProcfile(procfile, edits, original) {
|
|
609
644
|
let next = original;
|
|
@@ -612,8 +647,8 @@ async function applyProcfile(procfile, edits, original) {
|
|
|
612
647
|
next = next.replace(e.before, e.after);
|
|
613
648
|
}
|
|
614
649
|
const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
|
|
615
|
-
await
|
|
616
|
-
await
|
|
650
|
+
await fs3.writeFile(tmp, next, "utf8");
|
|
651
|
+
await fs3.rename(tmp, procfile);
|
|
617
652
|
}
|
|
618
653
|
async function apply2(installPlan) {
|
|
619
654
|
const touched = /* @__PURE__ */ new Set();
|
|
@@ -623,7 +658,7 @@ async function apply2(installPlan) {
|
|
|
623
658
|
const originals = /* @__PURE__ */ new Map();
|
|
624
659
|
for (const file of touched) {
|
|
625
660
|
try {
|
|
626
|
-
originals.set(file, await
|
|
661
|
+
originals.set(file, await fs3.readFile(file, "utf8"));
|
|
627
662
|
} catch {
|
|
628
663
|
}
|
|
629
664
|
}
|
|
@@ -651,7 +686,7 @@ async function rollback2(installPlan, originals) {
|
|
|
651
686
|
const restored = [];
|
|
652
687
|
for (const [file, raw] of originals.entries()) {
|
|
653
688
|
try {
|
|
654
|
-
await
|
|
689
|
+
await fs3.writeFile(file, raw, "utf8");
|
|
655
690
|
restored.push(file);
|
|
656
691
|
} catch {
|
|
657
692
|
}
|
|
@@ -666,7 +701,7 @@ async function rollback2(installPlan, originals) {
|
|
|
666
701
|
""
|
|
667
702
|
];
|
|
668
703
|
const rollbackPath = path3.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
669
|
-
await
|
|
704
|
+
await fs3.writeFile(rollbackPath, lines.join("\n"), "utf8");
|
|
670
705
|
}
|
|
671
706
|
var pythonInstaller = {
|
|
672
707
|
name: "python",
|
|
@@ -757,7 +792,7 @@ function renderPatch(sections) {
|
|
|
757
792
|
import { DivergenceTypeSchema } from "@neat.is/types";
|
|
758
793
|
|
|
759
794
|
// src/cli-client.ts
|
|
760
|
-
import { Provenance
|
|
795
|
+
import { Provenance } from "@neat.is/types";
|
|
761
796
|
var HttpError = class extends Error {
|
|
762
797
|
constructor(status, message, responseBody = "") {
|
|
763
798
|
super(message);
|
|
@@ -892,7 +927,7 @@ async function runBlastRadius(client, input) {
|
|
|
892
927
|
}
|
|
893
928
|
}
|
|
894
929
|
function formatBlastEntry(n) {
|
|
895
|
-
const tag = n.edgeProvenance ===
|
|
930
|
+
const tag = n.edgeProvenance === Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
896
931
|
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
897
932
|
}
|
|
898
933
|
async function runDependencies(client, input) {
|
|
@@ -938,9 +973,9 @@ async function runObservedDependencies(client, input) {
|
|
|
938
973
|
const edges = await client.get(
|
|
939
974
|
projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
|
|
940
975
|
);
|
|
941
|
-
const observed = edges.outbound.filter((e) => e.provenance ===
|
|
976
|
+
const observed = edges.outbound.filter((e) => e.provenance === Provenance.OBSERVED);
|
|
942
977
|
if (observed.length === 0) {
|
|
943
|
-
const hasExtracted = edges.outbound.some((e) => e.provenance ===
|
|
978
|
+
const hasExtracted = edges.outbound.some((e) => e.provenance === Provenance.EXTRACTED);
|
|
944
979
|
const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
945
980
|
return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
|
|
946
981
|
}
|
|
@@ -948,7 +983,7 @@ async function runObservedDependencies(client, input) {
|
|
|
948
983
|
return {
|
|
949
984
|
summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
950
985
|
block: blockLines.join("\n"),
|
|
951
|
-
provenance:
|
|
986
|
+
provenance: Provenance.OBSERVED
|
|
952
987
|
};
|
|
953
988
|
} catch (err) {
|
|
954
989
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -1002,7 +1037,7 @@ async function runIncidents(client, input) {
|
|
|
1002
1037
|
return {
|
|
1003
1038
|
summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
1004
1039
|
block: blockLines.join("\n"),
|
|
1005
|
-
provenance:
|
|
1040
|
+
provenance: Provenance.OBSERVED
|
|
1006
1041
|
};
|
|
1007
1042
|
} catch (err) {
|
|
1008
1043
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -1111,7 +1146,7 @@ async function runStaleEdges(client, input) {
|
|
|
1111
1146
|
return {
|
|
1112
1147
|
summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
|
|
1113
1148
|
block: blockLines.join("\n"),
|
|
1114
|
-
provenance:
|
|
1149
|
+
provenance: Provenance.STALE
|
|
1115
1150
|
};
|
|
1116
1151
|
}
|
|
1117
1152
|
async function runPolicies(client, input) {
|
|
@@ -1500,7 +1535,7 @@ async function buildPatchSections(services) {
|
|
|
1500
1535
|
}
|
|
1501
1536
|
async function runInit(opts) {
|
|
1502
1537
|
const written = [];
|
|
1503
|
-
const stat = await
|
|
1538
|
+
const stat = await fs4.stat(opts.scanPath).catch(() => null);
|
|
1504
1539
|
if (!stat || !stat.isDirectory()) {
|
|
1505
1540
|
console.error(`neat init: ${opts.scanPath} is not a directory`);
|
|
1506
1541
|
return { exitCode: 2, writtenFiles: written };
|
|
@@ -1511,7 +1546,7 @@ async function runInit(opts) {
|
|
|
1511
1546
|
const patch = renderPatch(sections);
|
|
1512
1547
|
const patchPath = path4.join(opts.scanPath, "neat.patch");
|
|
1513
1548
|
if (opts.dryRun) {
|
|
1514
|
-
await
|
|
1549
|
+
await fs4.writeFile(patchPath, patch, "utf8");
|
|
1515
1550
|
written.push(patchPath);
|
|
1516
1551
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
1517
1552
|
console.log("rerun without --dry-run to register and snapshot.");
|
|
@@ -1520,7 +1555,12 @@ async function runInit(opts) {
|
|
|
1520
1555
|
const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
|
|
1521
1556
|
resetGraph(graphKey);
|
|
1522
1557
|
const graph = getGraph(graphKey);
|
|
1523
|
-
const
|
|
1558
|
+
const projectPaths = pathsForProject(
|
|
1559
|
+
graphKey,
|
|
1560
|
+
path4.join(opts.scanPath, "neat-out")
|
|
1561
|
+
);
|
|
1562
|
+
const errorsPath = path4.join(path4.dirname(opts.outPath), path4.basename(projectPaths.errorsPath));
|
|
1563
|
+
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
1524
1564
|
await saveGraphToDisk(graph, opts.outPath);
|
|
1525
1565
|
written.push(opts.outPath);
|
|
1526
1566
|
const languages = [...new Set(services.map((s) => s.node.language))].sort();
|
|
@@ -1551,7 +1591,7 @@ async function runInit(opts) {
|
|
|
1551
1591
|
console.log("patch applied. Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
1552
1592
|
}
|
|
1553
1593
|
} else {
|
|
1554
|
-
await
|
|
1594
|
+
await fs4.writeFile(patchPath, patch, "utf8");
|
|
1555
1595
|
written.push(patchPath);
|
|
1556
1596
|
}
|
|
1557
1597
|
}
|
|
@@ -1565,6 +1605,10 @@ async function runInit(opts) {
|
|
|
1565
1605
|
console.log(`added: ${result.nodesAdded} nodes, ${result.edgesAdded} edges`);
|
|
1566
1606
|
console.log(`total: ${graph.order} nodes, ${graph.size} edges`);
|
|
1567
1607
|
console.log(summarise(nodes, edges));
|
|
1608
|
+
console.log(formatExtractionBanner(result.extractionErrors));
|
|
1609
|
+
if (result.extractionErrors > 0) {
|
|
1610
|
+
console.log(`errors: ${errorsPath}`);
|
|
1611
|
+
}
|
|
1568
1612
|
const incompatibilities = findIncompatibilities(nodes);
|
|
1569
1613
|
if (incompatibilities.length > 0) {
|
|
1570
1614
|
console.log("");
|
|
@@ -1575,6 +1619,9 @@ async function runInit(opts) {
|
|
|
1575
1619
|
}
|
|
1576
1620
|
}
|
|
1577
1621
|
}
|
|
1622
|
+
if (result.extractionErrors > 0 && isStrictExtractionEnabled()) {
|
|
1623
|
+
return { exitCode: 4, writtenFiles: written };
|
|
1624
|
+
}
|
|
1578
1625
|
return { exitCode: 0, writtenFiles: written };
|
|
1579
1626
|
}
|
|
1580
1627
|
var CLAUDE_SKILL_CONFIG = {
|
|
@@ -1605,7 +1652,7 @@ async function runSkill(opts) {
|
|
|
1605
1652
|
const target = claudeConfigPath();
|
|
1606
1653
|
let existing = {};
|
|
1607
1654
|
try {
|
|
1608
|
-
existing = JSON.parse(await
|
|
1655
|
+
existing = JSON.parse(await fs4.readFile(target, "utf8"));
|
|
1609
1656
|
} catch (err) {
|
|
1610
1657
|
if (err.code !== "ENOENT") {
|
|
1611
1658
|
console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
|
|
@@ -1617,8 +1664,8 @@ async function runSkill(opts) {
|
|
|
1617
1664
|
...existing,
|
|
1618
1665
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
1619
1666
|
};
|
|
1620
|
-
await
|
|
1621
|
-
await
|
|
1667
|
+
await fs4.mkdir(path4.dirname(target), { recursive: true });
|
|
1668
|
+
await fs4.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
1622
1669
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
1623
1670
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
1624
1671
|
return { exitCode: 0 };
|
|
@@ -1678,7 +1725,7 @@ async function main() {
|
|
|
1678
1725
|
process.exit(2);
|
|
1679
1726
|
}
|
|
1680
1727
|
const scanPath = path4.resolve(target);
|
|
1681
|
-
const stat = await
|
|
1728
|
+
const stat = await fs4.stat(scanPath).catch(() => null);
|
|
1682
1729
|
if (!stat || !stat.isDirectory()) {
|
|
1683
1730
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
1684
1731
|
process.exit(2);
|