@neat.is/core 0.4.5 → 0.4.7
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-RBWL4HRB.js → chunk-7FTK47JQ.js} +277 -103
- package/dist/chunk-7FTK47JQ.js.map +1 -0
- package/dist/{chunk-6GXBAR3M.js → chunk-LS6NS72S.js} +37 -3
- package/dist/chunk-LS6NS72S.js.map +1 -0
- package/dist/cli.cjs +600 -219
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +294 -80
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +314 -115
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +314 -115
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +263 -98
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-6GXBAR3M.js.map +0 -1
- package/dist/chunk-RBWL4HRB.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
setStatus,
|
|
42
42
|
startPersistLoop,
|
|
43
43
|
startStalenessLoop
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-7FTK47JQ.js";
|
|
45
45
|
import {
|
|
46
46
|
startOtelGrpcReceiver
|
|
47
47
|
} from "./chunk-3QCRUEQD.js";
|
|
@@ -52,8 +52,8 @@ import {
|
|
|
52
52
|
} from "./chunk-HVF4S7J3.js";
|
|
53
53
|
|
|
54
54
|
// src/cli.ts
|
|
55
|
-
import
|
|
56
|
-
import { promises as
|
|
55
|
+
import path9 from "path";
|
|
56
|
+
import { promises as fs8, readFileSync } from "fs";
|
|
57
57
|
import { fileURLToPath } from "url";
|
|
58
58
|
|
|
59
59
|
// src/gitignore.ts
|
|
@@ -693,38 +693,119 @@ import path4 from "path";
|
|
|
693
693
|
|
|
694
694
|
// src/installers/templates.ts
|
|
695
695
|
var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
|
|
696
|
+
var OTEL_INIT_STAMP = "// neat-template-version: 2 \u2014 file-first call-site capture (ADR-089).";
|
|
697
|
+
function neatCallsiteProcessorSource(ts) {
|
|
698
|
+
const stackT = ts ? ": string" : "";
|
|
699
|
+
const spanT = ts ? ": any" : "";
|
|
700
|
+
const strOpt = ts ? ": string | undefined" : "";
|
|
701
|
+
return `function __neatPickUserFrame(stack${stackT}) {
|
|
702
|
+
const lines = String(stack || '').split('\\n')
|
|
703
|
+
for (let i = 0; i < lines.length; i++) {
|
|
704
|
+
const raw = lines[i].trim()
|
|
705
|
+
if (raw.indexOf('at ') !== 0) continue
|
|
706
|
+
if (raw.indexOf('node_modules') !== -1) continue
|
|
707
|
+
if (raw.indexOf('@opentelemetry') !== -1) continue
|
|
708
|
+
if (raw.indexOf('node:') !== -1) continue
|
|
709
|
+
if (raw.indexOf('NeatCallSiteSpanProcessor') !== -1) continue
|
|
710
|
+
const bodyText = raw.slice(3)
|
|
711
|
+
const loc = bodyText.match(/:(\\d+):(\\d+)\\)?$/)
|
|
712
|
+
if (!loc) continue
|
|
713
|
+
const paren = bodyText.lastIndexOf('(')
|
|
714
|
+
let filepath${strOpt}
|
|
715
|
+
let fn${strOpt}
|
|
716
|
+
if (paren !== -1) {
|
|
717
|
+
fn = bodyText.slice(0, paren).trim()
|
|
718
|
+
if (fn.indexOf('async ') === 0) fn = fn.slice(6)
|
|
719
|
+
if (fn.indexOf('new ') === 0) fn = fn.slice(4)
|
|
720
|
+
filepath = bodyText.slice(paren + 1, bodyText.length - loc[0].length)
|
|
721
|
+
} else {
|
|
722
|
+
filepath = bodyText.slice(0, bodyText.length - loc[0].length)
|
|
723
|
+
}
|
|
724
|
+
if (filepath.indexOf('file://') === 0) filepath = filepath.slice(7)
|
|
725
|
+
if (!filepath) continue
|
|
726
|
+
return { filepath: filepath, lineno: Number(loc[1]), function: fn || undefined }
|
|
727
|
+
}
|
|
728
|
+
return null
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
class NeatCallSiteSpanProcessor {
|
|
732
|
+
onStart(span${spanT}) {
|
|
733
|
+
if (!span || (span.kind !== 2 && span.kind !== 3)) return
|
|
734
|
+
const frame = __neatPickUserFrame(new Error().stack)
|
|
735
|
+
if (!frame) return
|
|
736
|
+
span.setAttribute('code.filepath', frame.filepath)
|
|
737
|
+
if (typeof frame.lineno === 'number') span.setAttribute('code.lineno', frame.lineno)
|
|
738
|
+
if (frame.function) span.setAttribute('code.function', frame.function)
|
|
739
|
+
}
|
|
740
|
+
onEnd() {}
|
|
741
|
+
forceFlush() { return Promise.resolve() }
|
|
742
|
+
shutdown() { return Promise.resolve() }
|
|
743
|
+
}`;
|
|
744
|
+
}
|
|
745
|
+
var CALLSITE_PROCESSOR_JS = neatCallsiteProcessorSource(false);
|
|
746
|
+
var CALLSITE_PROCESSOR_TS = neatCallsiteProcessorSource(true);
|
|
747
|
+
function neatRegisterCallsiteSource(ts) {
|
|
748
|
+
const providerT = ts ? ": any" : "";
|
|
749
|
+
return `try {
|
|
750
|
+
const provider${providerT} = trace.getTracerProvider()
|
|
751
|
+
const delegate = provider && typeof provider.getDelegate === 'function' ? provider.getDelegate() : provider
|
|
752
|
+
if (delegate && typeof delegate.addSpanProcessor === 'function') {
|
|
753
|
+
delegate.addSpanProcessor(new NeatCallSiteSpanProcessor())
|
|
754
|
+
}
|
|
755
|
+
} catch {
|
|
756
|
+
// capture is best-effort; span export is unaffected
|
|
757
|
+
}`;
|
|
758
|
+
}
|
|
696
759
|
var OTEL_INIT_CJS = `${OTEL_INIT_HEADER}
|
|
760
|
+
${OTEL_INIT_STAMP}
|
|
697
761
|
process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
|
|
698
762
|
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
|
|
699
763
|
|
|
700
764
|
const { NodeSDK } = require('@opentelemetry/sdk-node')
|
|
701
765
|
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
|
|
766
|
+
const { trace } = require('@opentelemetry/api')
|
|
767
|
+
|
|
768
|
+
${CALLSITE_PROCESSOR_JS}
|
|
702
769
|
|
|
703
770
|
const instrumentations = [getNodeAutoInstrumentations()]
|
|
704
771
|
__INSTRUMENTATION_BLOCK__
|
|
705
|
-
new NodeSDK({ instrumentations })
|
|
772
|
+
const sdk = new NodeSDK({ instrumentations })
|
|
773
|
+
sdk.start()
|
|
774
|
+
${neatRegisterCallsiteSource(false)}
|
|
706
775
|
`;
|
|
707
776
|
var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
|
|
777
|
+
${OTEL_INIT_STAMP}
|
|
708
778
|
process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
|
|
709
779
|
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
|
|
710
780
|
|
|
711
781
|
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
712
782
|
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
783
|
+
import { trace } from '@opentelemetry/api'
|
|
784
|
+
|
|
785
|
+
${CALLSITE_PROCESSOR_JS}
|
|
713
786
|
|
|
714
787
|
const instrumentations = [getNodeAutoInstrumentations()]
|
|
715
788
|
__INSTRUMENTATION_BLOCK__
|
|
716
|
-
new NodeSDK({ instrumentations })
|
|
789
|
+
const sdk = new NodeSDK({ instrumentations })
|
|
790
|
+
sdk.start()
|
|
791
|
+
${neatRegisterCallsiteSource(false)}
|
|
717
792
|
`;
|
|
718
793
|
var OTEL_INIT_TS = `${OTEL_INIT_HEADER}
|
|
794
|
+
${OTEL_INIT_STAMP}
|
|
719
795
|
process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
|
|
720
796
|
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
|
|
721
797
|
|
|
722
798
|
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
723
799
|
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
800
|
+
import { trace } from '@opentelemetry/api'
|
|
801
|
+
|
|
802
|
+
${CALLSITE_PROCESSOR_TS}
|
|
724
803
|
|
|
725
804
|
const instrumentations = [getNodeAutoInstrumentations()]
|
|
726
805
|
__INSTRUMENTATION_BLOCK__
|
|
727
|
-
new NodeSDK({ instrumentations })
|
|
806
|
+
const sdk = new NodeSDK({ instrumentations })
|
|
807
|
+
sdk.start()
|
|
808
|
+
${neatRegisterCallsiteSource(true)}
|
|
728
809
|
`;
|
|
729
810
|
function renderNodeOtelInit(template, serviceName, projectName, registrations = []) {
|
|
730
811
|
const block = registrations.length === 0 ? "" : `
|
|
@@ -877,13 +958,20 @@ var SDK_PACKAGES = [
|
|
|
877
958
|
{ name: "@opentelemetry/sdk-node", version: "^0.57.0" },
|
|
878
959
|
{ name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
|
|
879
960
|
];
|
|
961
|
+
function getMajor(versionRange) {
|
|
962
|
+
if (!versionRange) return 0;
|
|
963
|
+
const match = versionRange.match(/(\d+)/);
|
|
964
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
965
|
+
}
|
|
880
966
|
function detectNonBundledInstrumentations(pkg) {
|
|
881
967
|
const deps = allDeps(pkg);
|
|
882
968
|
const out = [];
|
|
883
969
|
if ("@prisma/client" in deps) {
|
|
970
|
+
const prismaMajor = getMajor(deps["@prisma/client"]);
|
|
971
|
+
const prismaInstrVersion = prismaMajor >= 6 ? "^6.0.0" : "^5.0.0";
|
|
884
972
|
out.push({
|
|
885
973
|
pkg: "@prisma/instrumentation",
|
|
886
|
-
version:
|
|
974
|
+
version: prismaInstrVersion,
|
|
887
975
|
registration: "instrumentations.push(new (require('@prisma/instrumentation').PrismaInstrumentation)())"
|
|
888
976
|
});
|
|
889
977
|
}
|
|
@@ -941,6 +1029,23 @@ async function exists(p) {
|
|
|
941
1029
|
return false;
|
|
942
1030
|
}
|
|
943
1031
|
}
|
|
1032
|
+
async function readFileMaybe(p) {
|
|
1033
|
+
try {
|
|
1034
|
+
return await fs4.readFile(p, "utf8");
|
|
1035
|
+
} catch {
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
async function planOtelInitGeneration(file, contents) {
|
|
1040
|
+
const existing = await readFileMaybe(file);
|
|
1041
|
+
if (existing === null) {
|
|
1042
|
+
return { file, contents, skipIfExists: true };
|
|
1043
|
+
}
|
|
1044
|
+
if (existing.includes(OTEL_INIT_HEADER) && !existing.includes(OTEL_INIT_STAMP)) {
|
|
1045
|
+
return { file, contents, skipIfExists: false };
|
|
1046
|
+
}
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
944
1049
|
async function detect(serviceDir) {
|
|
945
1050
|
const pkg = await readPackageJson(serviceDir);
|
|
946
1051
|
return pkg !== null && typeof pkg.name === "string";
|
|
@@ -1671,18 +1776,11 @@ async function plan(serviceDir, opts) {
|
|
|
1671
1776
|
const projectName = projectToken(pkg, serviceDir, project);
|
|
1672
1777
|
const registrations = nonBundled.map((i) => i.registration);
|
|
1673
1778
|
const generatedFiles = [];
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
svcName,
|
|
1680
|
-
projectName,
|
|
1681
|
-
registrations
|
|
1682
|
-
),
|
|
1683
|
-
skipIfExists: true
|
|
1684
|
-
});
|
|
1685
|
-
}
|
|
1779
|
+
const otelInitGen = await planOtelInitGeneration(
|
|
1780
|
+
otelInitFile,
|
|
1781
|
+
renderNodeOtelInit(otelInitContents(flavor), svcName, projectName, registrations)
|
|
1782
|
+
);
|
|
1783
|
+
if (otelInitGen) generatedFiles.push(otelInitGen);
|
|
1686
1784
|
if (!await exists(envNeatFile)) {
|
|
1687
1785
|
generatedFiles.push({
|
|
1688
1786
|
file: envNeatFile,
|
|
@@ -2209,19 +2307,99 @@ function renderPatch(sections) {
|
|
|
2209
2307
|
}
|
|
2210
2308
|
|
|
2211
2309
|
// src/orchestrator.ts
|
|
2212
|
-
import { promises as
|
|
2310
|
+
import { promises as fs7 } from "fs";
|
|
2213
2311
|
import http from "http";
|
|
2214
2312
|
import net from "net";
|
|
2313
|
+
import path7 from "path";
|
|
2314
|
+
import { spawn as spawn3 } from "child_process";
|
|
2315
|
+
import readline from "readline";
|
|
2316
|
+
|
|
2317
|
+
// src/installers/package-manager.ts
|
|
2318
|
+
import { promises as fs6 } from "fs";
|
|
2215
2319
|
import path6 from "path";
|
|
2216
2320
|
import { spawn as spawn2 } from "child_process";
|
|
2217
|
-
|
|
2321
|
+
var LOCKFILE_PRIORITY = [
|
|
2322
|
+
{ lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
|
|
2323
|
+
{ lockfile: "pnpm-lock.yaml", pm: "pnpm", args: ["install", "--no-summary"] },
|
|
2324
|
+
{ lockfile: "yarn.lock", pm: "yarn", args: ["install", "--silent"] },
|
|
2325
|
+
{
|
|
2326
|
+
lockfile: "package-lock.json",
|
|
2327
|
+
pm: "npm",
|
|
2328
|
+
args: ["install", "--no-audit", "--no-fund", "--prefer-offline"]
|
|
2329
|
+
}
|
|
2330
|
+
];
|
|
2331
|
+
var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
|
|
2332
|
+
async function exists3(p) {
|
|
2333
|
+
try {
|
|
2334
|
+
await fs6.access(p);
|
|
2335
|
+
return true;
|
|
2336
|
+
} catch {
|
|
2337
|
+
return false;
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
async function detectPackageManager(serviceDir) {
|
|
2341
|
+
let dir = path6.resolve(serviceDir);
|
|
2342
|
+
const stops = /* @__PURE__ */ new Set();
|
|
2343
|
+
for (let i = 0; i < 64; i++) {
|
|
2344
|
+
if (stops.has(dir)) break;
|
|
2345
|
+
stops.add(dir);
|
|
2346
|
+
for (const candidate of LOCKFILE_PRIORITY) {
|
|
2347
|
+
const lockPath = path6.join(dir, candidate.lockfile);
|
|
2348
|
+
if (await exists3(lockPath)) {
|
|
2349
|
+
return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
const parent = path6.dirname(dir);
|
|
2353
|
+
if (parent === dir) break;
|
|
2354
|
+
dir = parent;
|
|
2355
|
+
}
|
|
2356
|
+
return { pm: "npm", cwd: path6.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
|
|
2357
|
+
}
|
|
2358
|
+
async function runPackageManagerInstall(cmd) {
|
|
2359
|
+
return new Promise((resolve) => {
|
|
2360
|
+
const child = spawn2(cmd.pm, cmd.args, {
|
|
2361
|
+
cwd: cmd.cwd,
|
|
2362
|
+
// Inherit PATH + HOME so the user's installed managers resolve.
|
|
2363
|
+
env: process.env,
|
|
2364
|
+
// `false` keeps the parent in control of cleanup if the orchestrator
|
|
2365
|
+
// exits before install finishes. Cross-platform-safe.
|
|
2366
|
+
shell: false,
|
|
2367
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
2368
|
+
});
|
|
2369
|
+
let stderr = "";
|
|
2370
|
+
child.stderr?.on("data", (chunk) => {
|
|
2371
|
+
stderr += chunk.toString("utf8");
|
|
2372
|
+
});
|
|
2373
|
+
child.on("error", (err) => {
|
|
2374
|
+
resolve({
|
|
2375
|
+
pm: cmd.pm,
|
|
2376
|
+
cwd: cmd.cwd,
|
|
2377
|
+
args: cmd.args,
|
|
2378
|
+
exitCode: 127,
|
|
2379
|
+
stderr: stderr + `
|
|
2380
|
+
${err.message}`
|
|
2381
|
+
});
|
|
2382
|
+
});
|
|
2383
|
+
child.on("close", (code) => {
|
|
2384
|
+
resolve({
|
|
2385
|
+
pm: cmd.pm,
|
|
2386
|
+
cwd: cmd.cwd,
|
|
2387
|
+
args: cmd.args,
|
|
2388
|
+
exitCode: code ?? 1,
|
|
2389
|
+
stderr: stderr.trim()
|
|
2390
|
+
});
|
|
2391
|
+
});
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
// src/orchestrator.ts
|
|
2218
2396
|
async function extractAndPersist(opts) {
|
|
2219
2397
|
const services = await discoverServices(opts.scanPath);
|
|
2220
2398
|
const languages = [...new Set(services.map((s) => s.node.language))].sort();
|
|
2221
2399
|
const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
|
|
2222
2400
|
resetGraph(graphKey);
|
|
2223
2401
|
const graph = getGraph(graphKey);
|
|
2224
|
-
const projectPaths = pathsForProject(graphKey,
|
|
2402
|
+
const projectPaths = pathsForProject(graphKey, path7.join(opts.scanPath, "neat-out"));
|
|
2225
2403
|
const extraction = await extractFromDirectory(graph, opts.scanPath, {
|
|
2226
2404
|
errorsPath: projectPaths.errorsPath
|
|
2227
2405
|
});
|
|
@@ -2239,12 +2417,15 @@ async function extractAndPersist(opts) {
|
|
|
2239
2417
|
errorsPath: projectPaths.errorsPath
|
|
2240
2418
|
};
|
|
2241
2419
|
}
|
|
2242
|
-
async function applyInstallersOver(services, project) {
|
|
2420
|
+
async function applyInstallersOver(services, project, options = {}) {
|
|
2421
|
+
const resolveManager = options.resolveManager ?? detectPackageManager;
|
|
2422
|
+
const runInstall = options.runInstall ?? runPackageManagerInstall;
|
|
2243
2423
|
let instrumented = 0;
|
|
2244
2424
|
let already = 0;
|
|
2245
2425
|
let libOnly = 0;
|
|
2246
2426
|
let browserBundle = 0;
|
|
2247
2427
|
let reactNative = 0;
|
|
2428
|
+
const installPlans = /* @__PURE__ */ new Map();
|
|
2248
2429
|
for (const svc of services) {
|
|
2249
2430
|
const installer = await pickInstaller(svc.dir);
|
|
2250
2431
|
if (!installer) continue;
|
|
@@ -2254,8 +2435,14 @@ async function applyInstallersOver(services, project) {
|
|
|
2254
2435
|
continue;
|
|
2255
2436
|
}
|
|
2256
2437
|
const outcome = await installer.apply(plan3);
|
|
2257
|
-
if (outcome.outcome === "instrumented")
|
|
2258
|
-
|
|
2438
|
+
if (outcome.outcome === "instrumented") {
|
|
2439
|
+
instrumented++;
|
|
2440
|
+
if (plan3.dependencyEdits.length > 0) {
|
|
2441
|
+
const cmd = await resolveManager(svc.dir);
|
|
2442
|
+
const key = `${cmd.pm}:${cmd.cwd}`;
|
|
2443
|
+
if (!installPlans.has(key)) installPlans.set(key, cmd);
|
|
2444
|
+
}
|
|
2445
|
+
} else if (outcome.outcome === "already-instrumented") already++;
|
|
2259
2446
|
else if (outcome.outcome === "lib-only") libOnly++;
|
|
2260
2447
|
else if (outcome.outcome === "browser-bundle") {
|
|
2261
2448
|
browserBundle++;
|
|
@@ -2265,7 +2452,30 @@ async function applyInstallersOver(services, project) {
|
|
|
2265
2452
|
console.log(`skipping ${svc.dir}: React Native target; browser-OTel support lands in a future release.`);
|
|
2266
2453
|
}
|
|
2267
2454
|
}
|
|
2268
|
-
|
|
2455
|
+
const packageManagerInstalls = [];
|
|
2456
|
+
for (const cmd of installPlans.values()) {
|
|
2457
|
+
console.log(`running \`${cmd.pm} ${cmd.args.join(" ")}\` in ${cmd.cwd}`);
|
|
2458
|
+
const result = await runInstall(cmd);
|
|
2459
|
+
packageManagerInstalls.push(result);
|
|
2460
|
+
if (result.exitCode !== 0) {
|
|
2461
|
+
console.error(
|
|
2462
|
+
`neat: ${cmd.pm} install failed in ${cmd.cwd} (exit ${result.exitCode}); run it manually to surface the error.`
|
|
2463
|
+
);
|
|
2464
|
+
if (result.stderr.length > 0) {
|
|
2465
|
+
for (const line of result.stderr.split(/\r?\n/).slice(0, 20)) {
|
|
2466
|
+
console.error(` ${line}`);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
return {
|
|
2472
|
+
instrumented,
|
|
2473
|
+
alreadyInstrumented: already,
|
|
2474
|
+
libOnly,
|
|
2475
|
+
browserBundle,
|
|
2476
|
+
reactNative,
|
|
2477
|
+
packageManagerInstalls
|
|
2478
|
+
};
|
|
2269
2479
|
}
|
|
2270
2480
|
async function promptYesNo(question) {
|
|
2271
2481
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -2401,10 +2611,10 @@ function formatPortCollisionMessage(port) {
|
|
|
2401
2611
|
];
|
|
2402
2612
|
}
|
|
2403
2613
|
function spawnDaemonDetached() {
|
|
2404
|
-
const here =
|
|
2614
|
+
const here = path7.dirname(new URL(import.meta.url).pathname);
|
|
2405
2615
|
const candidates = [
|
|
2406
|
-
|
|
2407
|
-
|
|
2616
|
+
path7.join(here, "neatd.cjs"),
|
|
2617
|
+
path7.join(here, "neatd.js")
|
|
2408
2618
|
];
|
|
2409
2619
|
let entry2 = null;
|
|
2410
2620
|
const fsSync = __require("fs");
|
|
@@ -2424,7 +2634,7 @@ function spawnDaemonDetached() {
|
|
|
2424
2634
|
if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
|
|
2425
2635
|
env.HOST = "127.0.0.1";
|
|
2426
2636
|
}
|
|
2427
|
-
const child =
|
|
2637
|
+
const child = spawn3(process.execPath, [entry2, "start"], {
|
|
2428
2638
|
detached: true,
|
|
2429
2639
|
// stderr inherits the orchestrator's fd so the daemon's
|
|
2430
2640
|
// `BindAuthorityError` message lands in front of the operator instead
|
|
@@ -2441,7 +2651,7 @@ function openBrowser(url) {
|
|
|
2441
2651
|
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
2442
2652
|
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
2443
2653
|
try {
|
|
2444
|
-
const child =
|
|
2654
|
+
const child = spawn3(cmd, args, { detached: true, stdio: "ignore" });
|
|
2445
2655
|
child.on("error", () => {
|
|
2446
2656
|
});
|
|
2447
2657
|
child.unref();
|
|
@@ -2462,7 +2672,7 @@ async function runOrchestrator(opts) {
|
|
|
2462
2672
|
browser: "skipped"
|
|
2463
2673
|
}
|
|
2464
2674
|
};
|
|
2465
|
-
const stat = await
|
|
2675
|
+
const stat = await fs7.stat(opts.scanPath).catch(() => null);
|
|
2466
2676
|
if (!stat || !stat.isDirectory()) {
|
|
2467
2677
|
console.error(`neat: ${opts.scanPath} is not a directory`);
|
|
2468
2678
|
result.exitCode = 2;
|
|
@@ -2530,6 +2740,10 @@ async function runOrchestrator(opts) {
|
|
|
2530
2740
|
console.log(
|
|
2531
2741
|
`instrumented ${tally.instrumented}, already ${tally.alreadyInstrumented}, lib-only ${tally.libOnly}`
|
|
2532
2742
|
);
|
|
2743
|
+
const failedInstalls = tally.packageManagerInstalls.filter((i) => i.exitCode !== 0);
|
|
2744
|
+
if (failedInstalls.length > 0) {
|
|
2745
|
+
result.exitCode = 1;
|
|
2746
|
+
}
|
|
2533
2747
|
}
|
|
2534
2748
|
const restPort = Number(process.env.PORT ?? 8080);
|
|
2535
2749
|
const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
|
|
@@ -2600,7 +2814,7 @@ function printSummary(result, graph, dashboardUrl) {
|
|
|
2600
2814
|
}
|
|
2601
2815
|
|
|
2602
2816
|
// src/cli-verbs.ts
|
|
2603
|
-
import
|
|
2817
|
+
import path8 from "path";
|
|
2604
2818
|
|
|
2605
2819
|
// src/cli-client.ts
|
|
2606
2820
|
import { Provenance as Provenance2 } from "@neat.is/types";
|
|
@@ -2624,10 +2838,10 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
2624
2838
|
const root = baseUrl.replace(/\/$/, "");
|
|
2625
2839
|
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
2626
2840
|
return {
|
|
2627
|
-
async get(
|
|
2841
|
+
async get(path10) {
|
|
2628
2842
|
let res;
|
|
2629
2843
|
try {
|
|
2630
|
-
res = await fetch(`${root}${
|
|
2844
|
+
res = await fetch(`${root}${path10}`, {
|
|
2631
2845
|
headers: { ...authHeader }
|
|
2632
2846
|
});
|
|
2633
2847
|
} catch (err) {
|
|
@@ -2639,16 +2853,16 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
2639
2853
|
const body = await res.text().catch(() => "");
|
|
2640
2854
|
throw new HttpError(
|
|
2641
2855
|
res.status,
|
|
2642
|
-
`${res.status} ${res.statusText} on GET ${
|
|
2856
|
+
`${res.status} ${res.statusText} on GET ${path10}: ${body}`,
|
|
2643
2857
|
body
|
|
2644
2858
|
);
|
|
2645
2859
|
}
|
|
2646
2860
|
return await res.json();
|
|
2647
2861
|
},
|
|
2648
|
-
async post(
|
|
2862
|
+
async post(path10, body) {
|
|
2649
2863
|
let res;
|
|
2650
2864
|
try {
|
|
2651
|
-
res = await fetch(`${root}${
|
|
2865
|
+
res = await fetch(`${root}${path10}`, {
|
|
2652
2866
|
method: "POST",
|
|
2653
2867
|
headers: { "content-type": "application/json", ...authHeader },
|
|
2654
2868
|
body: JSON.stringify(body)
|
|
@@ -2662,7 +2876,7 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
2662
2876
|
const text = await res.text().catch(() => "");
|
|
2663
2877
|
throw new HttpError(
|
|
2664
2878
|
res.status,
|
|
2665
|
-
`${res.status} ${res.statusText} on POST ${
|
|
2879
|
+
`${res.status} ${res.statusText} on POST ${path10}: ${text}`,
|
|
2666
2880
|
text
|
|
2667
2881
|
);
|
|
2668
2882
|
}
|
|
@@ -2676,12 +2890,12 @@ function projectPath(project, suffix) {
|
|
|
2676
2890
|
}
|
|
2677
2891
|
async function runRootCause(client, input) {
|
|
2678
2892
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
2679
|
-
const
|
|
2893
|
+
const path10 = projectPath(
|
|
2680
2894
|
input.project,
|
|
2681
2895
|
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
2682
2896
|
);
|
|
2683
2897
|
try {
|
|
2684
|
-
const result = await client.get(
|
|
2898
|
+
const result = await client.get(path10);
|
|
2685
2899
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
2686
2900
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
2687
2901
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -2707,12 +2921,12 @@ async function runRootCause(client, input) {
|
|
|
2707
2921
|
}
|
|
2708
2922
|
async function runBlastRadius(client, input) {
|
|
2709
2923
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
2710
|
-
const
|
|
2924
|
+
const path10 = projectPath(
|
|
2711
2925
|
input.project,
|
|
2712
2926
|
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
2713
2927
|
);
|
|
2714
2928
|
try {
|
|
2715
|
-
const result = await client.get(
|
|
2929
|
+
const result = await client.get(path10);
|
|
2716
2930
|
if (result.totalAffected === 0) {
|
|
2717
2931
|
return {
|
|
2718
2932
|
summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
|
|
@@ -2746,12 +2960,12 @@ function formatBlastEntry(n) {
|
|
|
2746
2960
|
}
|
|
2747
2961
|
async function runDependencies(client, input) {
|
|
2748
2962
|
const depth = input.depth ?? 3;
|
|
2749
|
-
const
|
|
2963
|
+
const path10 = projectPath(
|
|
2750
2964
|
input.project,
|
|
2751
2965
|
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
2752
2966
|
);
|
|
2753
2967
|
try {
|
|
2754
|
-
const result = await client.get(
|
|
2968
|
+
const result = await client.get(path10);
|
|
2755
2969
|
if (result.total === 0) {
|
|
2756
2970
|
return {
|
|
2757
2971
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -2832,9 +3046,9 @@ function formatDuration(ms) {
|
|
|
2832
3046
|
return `${Math.round(h / 24)}d`;
|
|
2833
3047
|
}
|
|
2834
3048
|
async function runIncidents(client, input) {
|
|
2835
|
-
const
|
|
3049
|
+
const path10 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
2836
3050
|
try {
|
|
2837
|
-
const body = await client.get(
|
|
3051
|
+
const body = await client.get(path10);
|
|
2838
3052
|
const events = body.events;
|
|
2839
3053
|
if (events.length === 0) {
|
|
2840
3054
|
return {
|
|
@@ -3124,7 +3338,7 @@ async function resolveProjectEntry(opts) {
|
|
|
3124
3338
|
const cwd = opts.cwd ?? process.cwd();
|
|
3125
3339
|
const resolvedCwd = await normalizeProjectPath(cwd);
|
|
3126
3340
|
for (const entry2 of entries) {
|
|
3127
|
-
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${
|
|
3341
|
+
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path8.sep}`)) {
|
|
3128
3342
|
return entry2;
|
|
3129
3343
|
}
|
|
3130
3344
|
}
|
|
@@ -3484,10 +3698,10 @@ function assignFlag(out, field, value) {
|
|
|
3484
3698
|
out[field] = value;
|
|
3485
3699
|
}
|
|
3486
3700
|
function readPackageVersion() {
|
|
3487
|
-
const here = typeof __dirname !== "undefined" ? __dirname :
|
|
3701
|
+
const here = typeof __dirname !== "undefined" ? __dirname : path9.dirname(fileURLToPath(import.meta.url));
|
|
3488
3702
|
const candidates = [
|
|
3489
|
-
|
|
3490
|
-
|
|
3703
|
+
path9.resolve(here, "../package.json"),
|
|
3704
|
+
path9.resolve(here, "../../package.json")
|
|
3491
3705
|
];
|
|
3492
3706
|
for (const candidate of candidates) {
|
|
3493
3707
|
try {
|
|
@@ -3555,7 +3769,7 @@ async function buildPatchSections(services, project) {
|
|
|
3555
3769
|
}
|
|
3556
3770
|
async function runInit(opts) {
|
|
3557
3771
|
const written = [];
|
|
3558
|
-
const stat = await
|
|
3772
|
+
const stat = await fs8.stat(opts.scanPath).catch(() => null);
|
|
3559
3773
|
if (!stat || !stat.isDirectory()) {
|
|
3560
3774
|
console.error(`neat init: ${opts.scanPath} is not a directory`);
|
|
3561
3775
|
return { exitCode: 2, writtenFiles: written };
|
|
@@ -3564,13 +3778,13 @@ async function runInit(opts) {
|
|
|
3564
3778
|
printDiscoveryReport(opts, services);
|
|
3565
3779
|
const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
|
|
3566
3780
|
const patch = renderPatch(sections);
|
|
3567
|
-
const patchPath =
|
|
3781
|
+
const patchPath = path9.join(opts.scanPath, "neat.patch");
|
|
3568
3782
|
if (opts.dryRun) {
|
|
3569
|
-
await
|
|
3783
|
+
await fs8.writeFile(patchPath, patch, "utf8");
|
|
3570
3784
|
written.push(patchPath);
|
|
3571
3785
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
3572
|
-
const gitignorePath =
|
|
3573
|
-
const gitignoreExists = await
|
|
3786
|
+
const gitignorePath = path9.join(opts.scanPath, ".gitignore");
|
|
3787
|
+
const gitignoreExists = await fs8.stat(gitignorePath).then(() => true).catch(() => false);
|
|
3574
3788
|
const verb = gitignoreExists ? "append" : "create";
|
|
3575
3789
|
console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
|
|
3576
3790
|
console.log("rerun without --dry-run to register and snapshot.");
|
|
@@ -3581,9 +3795,9 @@ async function runInit(opts) {
|
|
|
3581
3795
|
const graph = getGraph(graphKey);
|
|
3582
3796
|
const projectPaths = pathsForProject(
|
|
3583
3797
|
graphKey,
|
|
3584
|
-
|
|
3798
|
+
path9.join(opts.scanPath, "neat-out")
|
|
3585
3799
|
);
|
|
3586
|
-
const errorsPath =
|
|
3800
|
+
const errorsPath = path9.join(path9.dirname(opts.outPath), path9.basename(projectPaths.errorsPath));
|
|
3587
3801
|
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
3588
3802
|
await saveGraphToDisk(graph, opts.outPath);
|
|
3589
3803
|
written.push(opts.outPath);
|
|
@@ -3662,7 +3876,7 @@ async function runInit(opts) {
|
|
|
3662
3876
|
console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
3663
3877
|
}
|
|
3664
3878
|
} else {
|
|
3665
|
-
await
|
|
3879
|
+
await fs8.writeFile(patchPath, patch, "utf8");
|
|
3666
3880
|
written.push(patchPath);
|
|
3667
3881
|
}
|
|
3668
3882
|
}
|
|
@@ -3702,9 +3916,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
3702
3916
|
};
|
|
3703
3917
|
function claudeConfigPath() {
|
|
3704
3918
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
3705
|
-
if (override && override.length > 0) return
|
|
3919
|
+
if (override && override.length > 0) return path9.resolve(override);
|
|
3706
3920
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
3707
|
-
return
|
|
3921
|
+
return path9.join(home, ".claude.json");
|
|
3708
3922
|
}
|
|
3709
3923
|
async function runSkill(opts) {
|
|
3710
3924
|
const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -3716,7 +3930,7 @@ async function runSkill(opts) {
|
|
|
3716
3930
|
const target = claudeConfigPath();
|
|
3717
3931
|
let existing = {};
|
|
3718
3932
|
try {
|
|
3719
|
-
existing = JSON.parse(await
|
|
3933
|
+
existing = JSON.parse(await fs8.readFile(target, "utf8"));
|
|
3720
3934
|
} catch (err) {
|
|
3721
3935
|
if (err.code !== "ENOENT") {
|
|
3722
3936
|
console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
|
|
@@ -3728,8 +3942,8 @@ async function runSkill(opts) {
|
|
|
3728
3942
|
...existing,
|
|
3729
3943
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
3730
3944
|
};
|
|
3731
|
-
await
|
|
3732
|
-
await
|
|
3945
|
+
await fs8.mkdir(path9.dirname(target), { recursive: true });
|
|
3946
|
+
await fs8.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
3733
3947
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
3734
3948
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
3735
3949
|
return { exitCode: 0 };
|
|
@@ -3767,12 +3981,12 @@ async function main() {
|
|
|
3767
3981
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
3768
3982
|
process.exit(2);
|
|
3769
3983
|
}
|
|
3770
|
-
const scanPath =
|
|
3984
|
+
const scanPath = path9.resolve(target);
|
|
3771
3985
|
const projectExplicit = parsed.project !== null;
|
|
3772
|
-
const projectName = projectExplicit ? project :
|
|
3986
|
+
const projectName = projectExplicit ? project : path9.basename(scanPath);
|
|
3773
3987
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
3774
|
-
const fallback = pathsForProject(projectKey,
|
|
3775
|
-
const outPath =
|
|
3988
|
+
const fallback = pathsForProject(projectKey, path9.join(scanPath, "neat-out")).snapshotPath;
|
|
3989
|
+
const outPath = path9.resolve(process.env.NEAT_OUT_PATH ?? fallback);
|
|
3776
3990
|
const result = await runInit({
|
|
3777
3991
|
scanPath,
|
|
3778
3992
|
outPath,
|
|
@@ -3793,21 +4007,21 @@ async function main() {
|
|
|
3793
4007
|
usage();
|
|
3794
4008
|
process.exit(2);
|
|
3795
4009
|
}
|
|
3796
|
-
const scanPath =
|
|
3797
|
-
const stat = await
|
|
4010
|
+
const scanPath = path9.resolve(target);
|
|
4011
|
+
const stat = await fs8.stat(scanPath).catch(() => null);
|
|
3798
4012
|
if (!stat || !stat.isDirectory()) {
|
|
3799
4013
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
3800
4014
|
process.exit(2);
|
|
3801
4015
|
}
|
|
3802
|
-
const projectPaths = pathsForProject(project,
|
|
3803
|
-
const outPath =
|
|
3804
|
-
const errorsPath =
|
|
3805
|
-
process.env.NEAT_ERRORS_PATH ??
|
|
4016
|
+
const projectPaths = pathsForProject(project, path9.join(scanPath, "neat-out"));
|
|
4017
|
+
const outPath = path9.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
|
|
4018
|
+
const errorsPath = path9.resolve(
|
|
4019
|
+
process.env.NEAT_ERRORS_PATH ?? path9.join(path9.dirname(outPath), path9.basename(projectPaths.errorsPath))
|
|
3806
4020
|
);
|
|
3807
|
-
const staleEventsPath =
|
|
3808
|
-
process.env.NEAT_STALE_EVENTS_PATH ??
|
|
4021
|
+
const staleEventsPath = path9.resolve(
|
|
4022
|
+
process.env.NEAT_STALE_EVENTS_PATH ?? path9.join(path9.dirname(outPath), path9.basename(projectPaths.staleEventsPath))
|
|
3809
4023
|
);
|
|
3810
|
-
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ?
|
|
4024
|
+
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path9.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
|
|
3811
4025
|
const handle = await startWatch(getGraph(project), {
|
|
3812
4026
|
scanPath,
|
|
3813
4027
|
outPath,
|
|
@@ -3953,11 +4167,11 @@ async function main() {
|
|
|
3953
4167
|
process.exit(1);
|
|
3954
4168
|
}
|
|
3955
4169
|
async function tryOrchestrator(cmd, parsed) {
|
|
3956
|
-
const scanPath =
|
|
3957
|
-
const stat = await
|
|
4170
|
+
const scanPath = path9.resolve(cmd);
|
|
4171
|
+
const stat = await fs8.stat(scanPath).catch(() => null);
|
|
3958
4172
|
if (!stat || !stat.isDirectory()) return null;
|
|
3959
4173
|
const projectExplicit = parsed.project !== null;
|
|
3960
|
-
const projectName = projectExplicit ? parsed.project :
|
|
4174
|
+
const projectName = projectExplicit ? parsed.project : path9.basename(scanPath);
|
|
3961
4175
|
const result = await runOrchestrator({
|
|
3962
4176
|
scanPath,
|
|
3963
4177
|
project: projectName,
|