@neat.is/core 0.6.4 → 0.7.1
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-7QXN726V.js → chunk-6H757ZNM.js} +2 -2
- package/dist/chunk-MDBE23Y3.js +840 -0
- package/dist/chunk-MDBE23Y3.js.map +1 -0
- package/dist/{chunk-ZLAZ7PLC.js → chunk-P2ZEKJ35.js} +45 -2
- package/dist/chunk-P2ZEKJ35.js.map +1 -0
- package/dist/{chunk-UCDXHLCJ.js → chunk-RR4LWQQB.js} +3929 -677
- package/dist/chunk-RR4LWQQB.js.map +1 -0
- package/dist/cli.cjs +5030 -3535
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +284 -136
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +4554 -3210
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +121 -106
- package/dist/index.d.ts +121 -106
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +4579 -3235
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-WXKM7YBW.js → otel-grpc-APVZSB6W.js} +3 -3
- package/dist/server.cjs +4103 -884
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/package.json +3 -2
- package/dist/chunk-22X2YM5H.js +0 -2810
- package/dist/chunk-22X2YM5H.js.map +0 -1
- package/dist/chunk-UCDXHLCJ.js.map +0 -1
- package/dist/chunk-ZLAZ7PLC.js.map +0 -1
- /package/dist/{chunk-7QXN726V.js.map → chunk-6H757ZNM.js.map} +0 -0
- /package/dist/{otel-grpc-WXKM7YBW.js.map → otel-grpc-APVZSB6W.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
clearDaemonRecord,
|
|
4
|
-
deprovisionConnector,
|
|
5
|
-
getProviderFieldSchema,
|
|
6
|
-
isPushProvider,
|
|
7
|
-
knownProviderNames,
|
|
8
4
|
portFromListenAddress,
|
|
9
|
-
provisionConnector,
|
|
10
5
|
readDaemonRecord,
|
|
11
6
|
resolveHost,
|
|
12
7
|
resolveNeatVersion,
|
|
13
|
-
startConnectorPolling,
|
|
14
|
-
validateConnectorEntry,
|
|
15
8
|
writeDaemonRecord
|
|
16
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-MDBE23Y3.js";
|
|
17
10
|
import {
|
|
18
11
|
buildSearchIndex
|
|
19
12
|
} from "./chunk-BIY46Q6U.js";
|
|
@@ -36,6 +29,7 @@ import {
|
|
|
36
29
|
autoSlugConnectorId,
|
|
37
30
|
buildApi,
|
|
38
31
|
computeDivergences,
|
|
32
|
+
deprovisionConnector,
|
|
39
33
|
describeCredential,
|
|
40
34
|
detectPackageManager,
|
|
41
35
|
discoverServices,
|
|
@@ -47,8 +41,11 @@ import {
|
|
|
47
41
|
formatExtractionBanner,
|
|
48
42
|
formatPrecisionFloorBanner,
|
|
49
43
|
getGraph,
|
|
44
|
+
getProviderFieldSchema,
|
|
50
45
|
isEnvRef,
|
|
46
|
+
isPushProvider,
|
|
51
47
|
isStrictExtractionEnabled,
|
|
48
|
+
knownProviderNames,
|
|
52
49
|
listMachineProjects,
|
|
53
50
|
listProjects,
|
|
54
51
|
loadGraphFromDisk,
|
|
@@ -58,6 +55,7 @@ import {
|
|
|
58
55
|
normalizeProjectPath,
|
|
59
56
|
pathsForProject,
|
|
60
57
|
promoteFrontierNodes,
|
|
58
|
+
provisionConnector,
|
|
61
59
|
pruneRegistry,
|
|
62
60
|
readConnectorsConfig,
|
|
63
61
|
removeConnectorEntry,
|
|
@@ -69,13 +67,15 @@ import {
|
|
|
69
67
|
saveGraphToDisk,
|
|
70
68
|
setStatus,
|
|
71
69
|
signalDaemonStop,
|
|
70
|
+
startConnectorPolling,
|
|
72
71
|
startPersistLoop,
|
|
73
72
|
startStalenessLoop,
|
|
74
|
-
upsertConnectorEntry
|
|
75
|
-
|
|
73
|
+
upsertConnectorEntry,
|
|
74
|
+
validateConnectorEntry
|
|
75
|
+
} from "./chunk-RR4LWQQB.js";
|
|
76
76
|
import {
|
|
77
77
|
startOtelGrpcReceiver
|
|
78
|
-
} from "./chunk-
|
|
78
|
+
} from "./chunk-6H757ZNM.js";
|
|
79
79
|
import {
|
|
80
80
|
__dirname,
|
|
81
81
|
__require,
|
|
@@ -83,12 +83,12 @@ import {
|
|
|
83
83
|
buildOtelReceiver,
|
|
84
84
|
listenSteppingOtlp,
|
|
85
85
|
readAuthEnv
|
|
86
|
-
} from "./chunk-
|
|
86
|
+
} from "./chunk-P2ZEKJ35.js";
|
|
87
87
|
|
|
88
88
|
// src/cli.ts
|
|
89
|
-
import
|
|
89
|
+
import path11 from "path";
|
|
90
90
|
import os2 from "os";
|
|
91
|
-
import { promises as
|
|
91
|
+
import { promises as fs9 } from "fs";
|
|
92
92
|
|
|
93
93
|
// src/banner.ts
|
|
94
94
|
import path from "path";
|
|
@@ -825,7 +825,7 @@ import semver from "semver";
|
|
|
825
825
|
|
|
826
826
|
// src/installers/templates.ts
|
|
827
827
|
var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
|
|
828
|
-
var OTEL_INIT_STAMP = "// neat-template-version:
|
|
828
|
+
var OTEL_INIT_STAMP = "// neat-template-version: 8 \u2014 CommonJS TypeScript uses a synchronous bootstrap (#910); OTel init degrades to no-OBSERVED when dependencies are absent (#820); daemon.json endpoint resolution (ADR-096) + layered file-first capture (ADR-090).";
|
|
829
829
|
var OTEL_OTLP_HEADERS_JS = "if (process.env.NEAT_OTEL_TOKEN) process.env.OTEL_EXPORTER_OTLP_HEADERS ||= 'Authorization=Bearer ' + process.env.NEAT_OTEL_TOKEN";
|
|
830
830
|
var OTEL_OTLP_PROTOCOL_JS = "process.env.OTEL_EXPORTER_OTLP_PROTOCOL ||= 'http/json'";
|
|
831
831
|
var OTEL_ENDPOINT_RESOLVER_CJS = `;(function () {
|
|
@@ -1189,6 +1189,13 @@ ${neatWireCaptureSource(false)}
|
|
|
1189
1189
|
}
|
|
1190
1190
|
}
|
|
1191
1191
|
`;
|
|
1192
|
+
var OTEL_INIT_TS_CJS = OTEL_INIT_CJS.replace(
|
|
1193
|
+
`${OTEL_INIT_HEADER}
|
|
1194
|
+
${OTEL_INIT_STAMP}`,
|
|
1195
|
+
`${OTEL_INIT_HEADER}
|
|
1196
|
+
${OTEL_INIT_STAMP}
|
|
1197
|
+
// @ts-nocheck \u2014 generated CommonJS runtime shim.`
|
|
1198
|
+
);
|
|
1192
1199
|
var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
|
|
1193
1200
|
${OTEL_INIT_STAMP}
|
|
1194
1201
|
${OTEL_ESM_NODE_IMPORTS}
|
|
@@ -1460,6 +1467,13 @@ function detectNonBundledInstrumentations(pkg) {
|
|
|
1460
1467
|
registration: "instrumentations.push(new (require('@prisma/instrumentation').PrismaInstrumentation)())"
|
|
1461
1468
|
});
|
|
1462
1469
|
}
|
|
1470
|
+
if (getMajor(deps["@nestjs/core"]) >= 11) {
|
|
1471
|
+
out.push({
|
|
1472
|
+
pkg: "@opentelemetry/instrumentation-nestjs-core",
|
|
1473
|
+
version: "^0.67.0",
|
|
1474
|
+
registration: "instrumentations.push(new (require('@opentelemetry/instrumentation-nestjs-core').NestInstrumentation)())"
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1463
1477
|
return out;
|
|
1464
1478
|
}
|
|
1465
1479
|
var OTEL_ENV = {
|
|
@@ -1772,18 +1786,19 @@ async function resolveEntry(serviceDir, pkg) {
|
|
|
1772
1786
|
}
|
|
1773
1787
|
function dispatchEntry(entryFile, pkg) {
|
|
1774
1788
|
const ext = path5.extname(entryFile).toLowerCase();
|
|
1775
|
-
if (ext === ".ts" || ext === ".tsx") return "ts";
|
|
1789
|
+
if (ext === ".ts" || ext === ".tsx") return pkg.type === "module" ? "ts" : "ts-cjs";
|
|
1776
1790
|
if (ext === ".mjs") return "esm";
|
|
1777
1791
|
if (ext === ".cjs") return "cjs";
|
|
1778
1792
|
return pkg.type === "module" ? "esm" : "cjs";
|
|
1779
1793
|
}
|
|
1780
1794
|
function otelInitFilename(flavor) {
|
|
1781
|
-
if (flavor === "ts") return "otel-init.ts";
|
|
1795
|
+
if (flavor === "ts" || flavor === "ts-cjs") return "otel-init.ts";
|
|
1782
1796
|
if (flavor === "esm") return "otel-init.mjs";
|
|
1783
1797
|
return "otel-init.cjs";
|
|
1784
1798
|
}
|
|
1785
1799
|
function otelInitContents(flavor) {
|
|
1786
1800
|
if (flavor === "ts") return OTEL_INIT_TS;
|
|
1801
|
+
if (flavor === "ts-cjs") return OTEL_INIT_TS_CJS;
|
|
1787
1802
|
if (flavor === "esm") return OTEL_INIT_ESM;
|
|
1788
1803
|
return OTEL_INIT_CJS;
|
|
1789
1804
|
}
|
|
@@ -2949,9 +2964,137 @@ var pythonInstaller = {
|
|
|
2949
2964
|
apply: apply2
|
|
2950
2965
|
};
|
|
2951
2966
|
|
|
2967
|
+
// src/installers/go.ts
|
|
2968
|
+
import { promises as fs6 } from "fs";
|
|
2969
|
+
import path7 from "path";
|
|
2970
|
+
var GO_DEPS = [
|
|
2971
|
+
{ name: "go.opentelemetry.io/otel", version: "v1.38.0" },
|
|
2972
|
+
{ name: "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp", version: "v1.38.0" },
|
|
2973
|
+
{ name: "go.opentelemetry.io/otel/sdk", version: "v1.38.0" }
|
|
2974
|
+
];
|
|
2975
|
+
async function exists3(file) {
|
|
2976
|
+
return fs6.stat(file).then(() => true, () => false);
|
|
2977
|
+
}
|
|
2978
|
+
async function findMain(serviceDir) {
|
|
2979
|
+
const root = path7.join(serviceDir, "main.go");
|
|
2980
|
+
if (await exists3(root)) return root;
|
|
2981
|
+
const cmd = path7.join(serviceDir, "cmd");
|
|
2982
|
+
const entries = await fs6.readdir(cmd, { withFileTypes: true }).catch(() => []);
|
|
2983
|
+
for (const entry2 of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2984
|
+
if (!entry2.isDirectory()) continue;
|
|
2985
|
+
const candidate = path7.join(cmd, entry2.name, "main.go");
|
|
2986
|
+
if (await exists3(candidate)) return candidate;
|
|
2987
|
+
}
|
|
2988
|
+
return null;
|
|
2989
|
+
}
|
|
2990
|
+
function neatOtelGo(packageName = "main") {
|
|
2991
|
+
return `// neat-otel-init v1 \u2014 generated by NEAT. Safe to re-generate; do not edit.
|
|
2992
|
+
package ${packageName}
|
|
2993
|
+
|
|
2994
|
+
import (
|
|
2995
|
+
"context"
|
|
2996
|
+
"os"
|
|
2997
|
+
"path/filepath"
|
|
2998
|
+
"runtime"
|
|
2999
|
+
"strings"
|
|
3000
|
+
|
|
3001
|
+
"go.opentelemetry.io/otel"
|
|
3002
|
+
"go.opentelemetry.io/otel/attribute"
|
|
3003
|
+
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
|
3004
|
+
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
|
3005
|
+
"go.opentelemetry.io/otel/trace"
|
|
3006
|
+
)
|
|
3007
|
+
|
|
3008
|
+
type neatCallSiteSpanProcessor struct { root string }
|
|
3009
|
+
|
|
3010
|
+
func (p neatCallSiteSpanProcessor) OnStart(_ context.Context, span sdktrace.ReadWriteSpan) {
|
|
3011
|
+
if span.SpanKind() != trace.SpanKindClient && span.SpanKind() != trace.SpanKindProducer { return }
|
|
3012
|
+
pcs := make([]uintptr, 32)
|
|
3013
|
+
n := runtime.Callers(2, pcs)
|
|
3014
|
+
frames := runtime.CallersFrames(pcs[:n])
|
|
3015
|
+
for {
|
|
3016
|
+
frame, more := frames.Next()
|
|
3017
|
+
clean := filepath.Clean(frame.File)
|
|
3018
|
+
slash := filepath.ToSlash(clean)
|
|
3019
|
+
if clean != "" && strings.HasPrefix(clean, p.root+string(os.PathSeparator)) && !strings.Contains(slash, "/vendor/") && !strings.HasSuffix(slash, "/neat_otel.go") {
|
|
3020
|
+
span.SetAttributes(
|
|
3021
|
+
attribute.String("code.file.path", clean),
|
|
3022
|
+
attribute.Int("code.line.number", frame.Line),
|
|
3023
|
+
attribute.String("code.function.name", frame.Function),
|
|
3024
|
+
)
|
|
3025
|
+
return
|
|
3026
|
+
}
|
|
3027
|
+
if !more { return }
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
func (neatCallSiteSpanProcessor) OnEnd(sdktrace.ReadOnlySpan) {}
|
|
3031
|
+
func (neatCallSiteSpanProcessor) Shutdown(context.Context) error { return nil }
|
|
3032
|
+
func (neatCallSiteSpanProcessor) ForceFlush(context.Context) error { return nil }
|
|
3033
|
+
|
|
3034
|
+
func init() {
|
|
3035
|
+
root := os.Getenv("NEAT_SERVICE_ROOT")
|
|
3036
|
+
if root == "" {
|
|
3037
|
+
_, self, _, ok := runtime.Caller(0)
|
|
3038
|
+
if !ok { return }
|
|
3039
|
+
root = filepath.Dir(self)
|
|
3040
|
+
for {
|
|
3041
|
+
if _, err := os.Stat(filepath.Join(root, "go.mod")); err == nil { break }
|
|
3042
|
+
parent := filepath.Dir(root)
|
|
3043
|
+
if parent == root { return }
|
|
3044
|
+
root = parent
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
root, _ = filepath.Abs(root)
|
|
3048
|
+
if canonical, err := filepath.EvalSymlinks(root); err == nil { root = canonical }
|
|
3049
|
+
exporter, err := otlptracehttp.New(context.Background())
|
|
3050
|
+
if err != nil { return }
|
|
3051
|
+
provider := sdktrace.NewTracerProvider(
|
|
3052
|
+
sdktrace.WithBatcher(exporter),
|
|
3053
|
+
sdktrace.WithSpanProcessor(neatCallSiteSpanProcessor{root: filepath.Clean(root)}),
|
|
3054
|
+
)
|
|
3055
|
+
otel.SetTracerProvider(provider)
|
|
3056
|
+
}
|
|
3057
|
+
`;
|
|
3058
|
+
}
|
|
3059
|
+
async function detect3(serviceDir) {
|
|
3060
|
+
return exists3(path7.join(serviceDir, "go.mod"));
|
|
3061
|
+
}
|
|
3062
|
+
async function plan3(serviceDir) {
|
|
3063
|
+
const manifest = path7.join(serviceDir, "go.mod");
|
|
3064
|
+
const raw = await fs6.readFile(manifest, "utf8");
|
|
3065
|
+
const main2 = await findMain(serviceDir);
|
|
3066
|
+
const dependencyEdits = GO_DEPS.filter((dep) => !raw.includes(dep.name)).map((dep) => ({ file: manifest, kind: "add", ...dep }));
|
|
3067
|
+
if (!main2) return { language: "go", serviceDir, dependencyEdits: [], entrypointEdits: [], envEdits: [], libOnly: true };
|
|
3068
|
+
const source = await fs6.readFile(main2, "utf8");
|
|
3069
|
+
const packageName = source.match(/^\s*package\s+(\w+)\s*$/m)?.[1] ?? "main";
|
|
3070
|
+
const generated = path7.join(path7.dirname(main2), "neat_otel.go");
|
|
3071
|
+
const generatedFiles = await exists3(generated) ? [] : [{ file: generated, contents: neatOtelGo(packageName) }];
|
|
3072
|
+
return { language: "go", serviceDir, dependencyEdits, entrypointEdits: [], envEdits: [], generatedFiles, entryFile: main2 };
|
|
3073
|
+
}
|
|
3074
|
+
async function apply3(installPlan) {
|
|
3075
|
+
if (installPlan.libOnly) return { serviceDir: installPlan.serviceDir, outcome: "lib-only", writtenFiles: [] };
|
|
3076
|
+
const writtenFiles = [];
|
|
3077
|
+
const byManifest = /* @__PURE__ */ new Map();
|
|
3078
|
+
for (const edit of installPlan.dependencyEdits) byManifest.set(edit.file, [...byManifest.get(edit.file) ?? [], edit]);
|
|
3079
|
+
for (const [manifest, edits] of byManifest) {
|
|
3080
|
+
const raw = await fs6.readFile(manifest, "utf8");
|
|
3081
|
+
const suffix = raw.endsWith("\n") ? "" : "\n";
|
|
3082
|
+
await fs6.writeFile(manifest, `${raw}${suffix}${edits.map((e) => `require ${e.name} ${e.version}`).join("\n")}
|
|
3083
|
+
`, "utf8");
|
|
3084
|
+
writtenFiles.push(manifest);
|
|
3085
|
+
}
|
|
3086
|
+
for (const generated of installPlan.generatedFiles ?? []) {
|
|
3087
|
+
if (await exists3(generated.file)) continue;
|
|
3088
|
+
await fs6.writeFile(generated.file, generated.contents, "utf8");
|
|
3089
|
+
writtenFiles.push(generated.file);
|
|
3090
|
+
}
|
|
3091
|
+
return { serviceDir: installPlan.serviceDir, outcome: writtenFiles.length ? "instrumented" : "already-instrumented", writtenFiles };
|
|
3092
|
+
}
|
|
3093
|
+
var goInstaller = { name: "go", detect: detect3, plan: plan3, apply: apply3 };
|
|
3094
|
+
|
|
2952
3095
|
// src/installers/shared.ts
|
|
2953
|
-
function isEmptyPlan(
|
|
2954
|
-
return
|
|
3096
|
+
function isEmptyPlan(plan4) {
|
|
3097
|
+
return plan4.dependencyEdits.length === 0 && plan4.entrypointEdits.length === 0 && plan4.envEdits.length === 0 && (plan4.generatedFiles?.length ?? 0) === 0 && plan4.nextConfigEdit === void 0;
|
|
2955
3098
|
}
|
|
2956
3099
|
|
|
2957
3100
|
// src/installers/index.ts
|
|
@@ -2965,7 +3108,7 @@ var FORBIDDEN_LOCKFILES = /* @__PURE__ */ new Set([
|
|
|
2965
3108
|
"Cargo.lock",
|
|
2966
3109
|
"go.sum"
|
|
2967
3110
|
]);
|
|
2968
|
-
var INSTALLERS = [javascriptInstaller, pythonInstaller];
|
|
3111
|
+
var INSTALLERS = [javascriptInstaller, pythonInstaller, goInstaller];
|
|
2969
3112
|
async function pickInstaller(serviceDir) {
|
|
2970
3113
|
for (const inst of INSTALLERS) {
|
|
2971
3114
|
if (await inst.detect(serviceDir)) return inst;
|
|
@@ -2980,7 +3123,7 @@ function renderPatch(sections) {
|
|
|
2980
3123
|
"No SDK installers matched the discovered services. Two reasons this",
|
|
2981
3124
|
"normally happens:",
|
|
2982
3125
|
" - the project uses a language NEAT does not yet instrument",
|
|
2983
|
-
" (Java / Ruby / .NET /
|
|
3126
|
+
" (Java / Ruby / .NET / Rust are out of MVP scope per ADR-047);",
|
|
2984
3127
|
" - the SDK is already installed, so the installer returned an empty",
|
|
2985
3128
|
" plan.",
|
|
2986
3129
|
"",
|
|
@@ -2990,22 +3133,22 @@ function renderPatch(sections) {
|
|
|
2990
3133
|
}
|
|
2991
3134
|
const lines = ["# neat install plan", ""];
|
|
2992
3135
|
for (const section of sections) {
|
|
2993
|
-
const { installer, plan:
|
|
2994
|
-
lines.push(`## ${installer} (${
|
|
3136
|
+
const { installer, plan: plan4 } = section;
|
|
3137
|
+
lines.push(`## ${installer} (${plan4.language}) \u2014 ${plan4.serviceDir}`);
|
|
2995
3138
|
lines.push("");
|
|
2996
|
-
if (
|
|
3139
|
+
if (plan4.libOnly) {
|
|
2997
3140
|
lines.push("### skipped \u2014 no resolvable entry point (lib-only)");
|
|
2998
3141
|
lines.push("");
|
|
2999
3142
|
continue;
|
|
3000
3143
|
}
|
|
3001
|
-
if (
|
|
3002
|
-
lines.push(`entry: ${
|
|
3144
|
+
if (plan4.entryFile) {
|
|
3145
|
+
lines.push(`entry: ${plan4.entryFile}`);
|
|
3003
3146
|
lines.push("");
|
|
3004
3147
|
}
|
|
3005
|
-
if (
|
|
3148
|
+
if (plan4.dependencyEdits.length > 0) {
|
|
3006
3149
|
lines.push("### dependencies");
|
|
3007
3150
|
const byFile = /* @__PURE__ */ new Map();
|
|
3008
|
-
for (const dep of
|
|
3151
|
+
for (const dep of plan4.dependencyEdits) {
|
|
3009
3152
|
const base = dep.file.split(/[\\/]/).pop() ?? dep.file;
|
|
3010
3153
|
if (FORBIDDEN_LOCKFILES.has(base)) {
|
|
3011
3154
|
throw new Error(
|
|
@@ -3024,9 +3167,9 @@ function renderPatch(sections) {
|
|
|
3024
3167
|
}
|
|
3025
3168
|
lines.push("");
|
|
3026
3169
|
}
|
|
3027
|
-
if (
|
|
3170
|
+
if (plan4.generatedFiles && plan4.generatedFiles.length > 0) {
|
|
3028
3171
|
lines.push("### generated files");
|
|
3029
|
-
for (const gen of
|
|
3172
|
+
for (const gen of plan4.generatedFiles) {
|
|
3030
3173
|
lines.push(`--- (new file) ${gen.file}`);
|
|
3031
3174
|
for (const ln of gen.contents.split(/\r?\n/)) {
|
|
3032
3175
|
lines.push(`+ ${ln}`);
|
|
@@ -3034,26 +3177,26 @@ function renderPatch(sections) {
|
|
|
3034
3177
|
}
|
|
3035
3178
|
lines.push("");
|
|
3036
3179
|
}
|
|
3037
|
-
if (
|
|
3180
|
+
if (plan4.entrypointEdits.length > 0) {
|
|
3038
3181
|
lines.push("### entry-point injection");
|
|
3039
|
-
for (const e of
|
|
3182
|
+
for (const e of plan4.entrypointEdits) {
|
|
3040
3183
|
lines.push(`--- ${e.file}`);
|
|
3041
3184
|
lines.push(`+ ${e.after}`);
|
|
3042
3185
|
lines.push(` ${e.before}`);
|
|
3043
3186
|
}
|
|
3044
3187
|
lines.push("");
|
|
3045
3188
|
}
|
|
3046
|
-
if (
|
|
3189
|
+
if (plan4.envEdits.length > 0) {
|
|
3047
3190
|
lines.push("### env (written to <package-dir>/.env.neat)");
|
|
3048
|
-
for (const env of
|
|
3191
|
+
for (const env of plan4.envEdits) {
|
|
3049
3192
|
lines.push(`- ${env.key}=${env.value}`);
|
|
3050
3193
|
}
|
|
3051
3194
|
lines.push("");
|
|
3052
3195
|
}
|
|
3053
|
-
if (
|
|
3196
|
+
if (plan4.nextConfigEdit) {
|
|
3054
3197
|
lines.push("### next.config (framework flag)");
|
|
3055
|
-
lines.push(`--- ${
|
|
3056
|
-
lines.push(`+ experimental: { instrumentationHook: true }, // ${
|
|
3198
|
+
lines.push(`--- ${plan4.nextConfigEdit.file}`);
|
|
3199
|
+
lines.push(`+ experimental: { instrumentationHook: true }, // ${plan4.nextConfigEdit.reason}`);
|
|
3057
3200
|
lines.push("");
|
|
3058
3201
|
}
|
|
3059
3202
|
}
|
|
@@ -3061,10 +3204,10 @@ function renderPatch(sections) {
|
|
|
3061
3204
|
}
|
|
3062
3205
|
|
|
3063
3206
|
// src/orchestrator.ts
|
|
3064
|
-
import { promises as
|
|
3207
|
+
import { promises as fs7 } from "fs";
|
|
3065
3208
|
import http from "http";
|
|
3066
3209
|
import net from "net";
|
|
3067
|
-
import
|
|
3210
|
+
import path8 from "path";
|
|
3068
3211
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3069
3212
|
import { spawn as spawn2 } from "child_process";
|
|
3070
3213
|
import readline from "readline";
|
|
@@ -3074,7 +3217,7 @@ async function extractAndPersist(opts) {
|
|
|
3074
3217
|
const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
|
|
3075
3218
|
resetGraph(graphKey);
|
|
3076
3219
|
const graph = getGraph(graphKey);
|
|
3077
|
-
const projectPaths = pathsForProject(graphKey,
|
|
3220
|
+
const projectPaths = pathsForProject(graphKey, path8.join(opts.scanPath, "neat-out"));
|
|
3078
3221
|
const extraction = await extractFromDirectory(graph, opts.scanPath, {
|
|
3079
3222
|
errorsPath: projectPaths.errorsPath
|
|
3080
3223
|
});
|
|
@@ -3108,15 +3251,15 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3108
3251
|
for (const svc of services) {
|
|
3109
3252
|
const installer = await pickInstaller(svc.dir);
|
|
3110
3253
|
if (!installer) continue;
|
|
3111
|
-
const
|
|
3112
|
-
if (isEmptyPlan(
|
|
3254
|
+
const plan4 = await installer.plan(svc.dir, { project });
|
|
3255
|
+
if (isEmptyPlan(plan4) && !plan4.libOnly && plan4.runtimeKind === void 0) {
|
|
3113
3256
|
already++;
|
|
3114
3257
|
continue;
|
|
3115
3258
|
}
|
|
3116
|
-
const outcome = await installer.apply(
|
|
3259
|
+
const outcome = await installer.apply(plan4);
|
|
3117
3260
|
if (outcome.outcome === "instrumented") {
|
|
3118
3261
|
instrumented++;
|
|
3119
|
-
if (
|
|
3262
|
+
if (plan4.dependencyEdits.length > 0) {
|
|
3120
3263
|
const cmd = await resolveManager(svc.dir);
|
|
3121
3264
|
const key = `${cmd.pm}:${cmd.cwd}`;
|
|
3122
3265
|
if (!installPlans.has(key)) installPlans.set(key, cmd);
|
|
@@ -3126,7 +3269,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3126
3269
|
libOnly++;
|
|
3127
3270
|
const appDeps = svc.pkg ? appFrameworkDependencies(svc.pkg) : [];
|
|
3128
3271
|
if (appDeps.length > 0) {
|
|
3129
|
-
const svcName =
|
|
3272
|
+
const svcName = path8.basename(svc.dir);
|
|
3130
3273
|
const list = appDeps.join(", ");
|
|
3131
3274
|
console.warn(
|
|
3132
3275
|
`neat: runtime layer won't engage for ${svcName}: no entry point found.
|
|
@@ -3139,7 +3282,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3139
3282
|
console.log(`skipping ${svc.dir}: browser bundle; browser-OTel support lands in a future release.`);
|
|
3140
3283
|
} else if (outcome.outcome === "react-native") {
|
|
3141
3284
|
reactNative++;
|
|
3142
|
-
const svcName =
|
|
3285
|
+
const svcName = path8.basename(svc.dir);
|
|
3143
3286
|
console.log(
|
|
3144
3287
|
`neat: ${svc.dir} detected as React Native / Expo
|
|
3145
3288
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -3150,7 +3293,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3150
3293
|
);
|
|
3151
3294
|
} else if (outcome.outcome === "bun") {
|
|
3152
3295
|
bun++;
|
|
3153
|
-
const svcName =
|
|
3296
|
+
const svcName = path8.basename(svc.dir);
|
|
3154
3297
|
console.log(
|
|
3155
3298
|
`neat: ${svc.dir} detected as Bun
|
|
3156
3299
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -3161,7 +3304,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3161
3304
|
);
|
|
3162
3305
|
} else if (outcome.outcome === "deno") {
|
|
3163
3306
|
deno++;
|
|
3164
|
-
const svcName =
|
|
3307
|
+
const svcName = path8.basename(svc.dir);
|
|
3165
3308
|
console.log(
|
|
3166
3309
|
`neat: ${svc.dir} detected as Deno
|
|
3167
3310
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -3172,7 +3315,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3172
3315
|
);
|
|
3173
3316
|
} else if (outcome.outcome === "cloudflare-workers") {
|
|
3174
3317
|
cloudflareWorkers++;
|
|
3175
|
-
const svcName =
|
|
3318
|
+
const svcName = path8.basename(svc.dir);
|
|
3176
3319
|
console.log(
|
|
3177
3320
|
`neat: ${svc.dir} detected as Cloudflare Workers
|
|
3178
3321
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -3183,7 +3326,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3183
3326
|
);
|
|
3184
3327
|
} else if (outcome.outcome === "electron") {
|
|
3185
3328
|
electron++;
|
|
3186
|
-
const svcName =
|
|
3329
|
+
const svcName = path8.basename(svc.dir);
|
|
3187
3330
|
console.log(
|
|
3188
3331
|
`neat: ${svc.dir} detected as Electron
|
|
3189
3332
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -3196,7 +3339,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3196
3339
|
if (svc.pkg && (outcome.outcome === "instrumented" || outcome.outcome === "already-instrumented")) {
|
|
3197
3340
|
const gaps = uninstrumentedLibraries(svc.pkg);
|
|
3198
3341
|
if (gaps.length > 0) {
|
|
3199
|
-
const svcName =
|
|
3342
|
+
const svcName = path8.basename(svc.dir);
|
|
3200
3343
|
const list = gaps.join(", ");
|
|
3201
3344
|
const subject = gaps.length === 1 ? "this library" : "these libraries";
|
|
3202
3345
|
const aux = gaps.length === 1 ? "isn't" : "aren't";
|
|
@@ -3402,24 +3545,24 @@ async function persistedPortsFor(scanPath) {
|
|
|
3402
3545
|
return { rest: record.ports.rest, otlp: record.ports.otlp, web: record.ports.web };
|
|
3403
3546
|
}
|
|
3404
3547
|
async function acquireSpawnLock(scanPath) {
|
|
3405
|
-
const lockPath =
|
|
3406
|
-
await
|
|
3548
|
+
const lockPath = path8.join(scanPath, "neat-out", "daemon.spawn.lock");
|
|
3549
|
+
await fs7.mkdir(path8.dirname(lockPath), { recursive: true });
|
|
3407
3550
|
const STALE_LOCK_MS = 6e4;
|
|
3408
3551
|
try {
|
|
3409
|
-
const fd = await
|
|
3552
|
+
const fd = await fs7.open(lockPath, "wx");
|
|
3410
3553
|
await fd.writeFile(`${process.pid}
|
|
3411
3554
|
`, "utf8");
|
|
3412
3555
|
await fd.close();
|
|
3413
3556
|
return async () => {
|
|
3414
|
-
await
|
|
3557
|
+
await fs7.unlink(lockPath).catch(() => {
|
|
3415
3558
|
});
|
|
3416
3559
|
};
|
|
3417
3560
|
} catch (err) {
|
|
3418
3561
|
if (err.code !== "EEXIST") return null;
|
|
3419
3562
|
try {
|
|
3420
|
-
const stat = await
|
|
3563
|
+
const stat = await fs7.stat(lockPath);
|
|
3421
3564
|
if (Date.now() - stat.mtimeMs > STALE_LOCK_MS) {
|
|
3422
|
-
await
|
|
3565
|
+
await fs7.unlink(lockPath).catch(() => {
|
|
3423
3566
|
});
|
|
3424
3567
|
return acquireSpawnLock(scanPath);
|
|
3425
3568
|
}
|
|
@@ -3448,13 +3591,13 @@ async function healthIsForProject(restPort, project) {
|
|
|
3448
3591
|
return false;
|
|
3449
3592
|
}
|
|
3450
3593
|
function daemonLogPath(projectPath2) {
|
|
3451
|
-
return
|
|
3594
|
+
return path8.join(projectPath2, "neat-out", "daemon.log");
|
|
3452
3595
|
}
|
|
3453
3596
|
function spawnDaemonDetached(spec) {
|
|
3454
|
-
const here =
|
|
3597
|
+
const here = path8.dirname(fileURLToPath2(import.meta.url));
|
|
3455
3598
|
const candidates = [
|
|
3456
|
-
|
|
3457
|
-
|
|
3599
|
+
path8.join(here, "neatd.cjs"),
|
|
3600
|
+
path8.join(here, "neatd.js")
|
|
3458
3601
|
];
|
|
3459
3602
|
let entry2 = null;
|
|
3460
3603
|
const fsSync = __require("fs");
|
|
@@ -3484,7 +3627,7 @@ function spawnDaemonDetached(spec) {
|
|
|
3484
3627
|
let logFd = null;
|
|
3485
3628
|
if (spec) {
|
|
3486
3629
|
const logPath = daemonLogPath(spec.projectPath);
|
|
3487
|
-
fsSync.mkdirSync(
|
|
3630
|
+
fsSync.mkdirSync(path8.dirname(logPath), { recursive: true });
|
|
3488
3631
|
logFd = fsSync.openSync(logPath, "a");
|
|
3489
3632
|
}
|
|
3490
3633
|
const child = spawn2(process.execPath, [entry2, "start"], {
|
|
@@ -3523,7 +3666,7 @@ async function runOrchestrator(opts) {
|
|
|
3523
3666
|
browser: "skipped"
|
|
3524
3667
|
}
|
|
3525
3668
|
};
|
|
3526
|
-
const stat = await
|
|
3669
|
+
const stat = await fs7.stat(opts.scanPath).catch(() => null);
|
|
3527
3670
|
if (!stat || !stat.isDirectory()) {
|
|
3528
3671
|
console.error(`neat: ${opts.scanPath} is not a directory`);
|
|
3529
3672
|
result.exitCode = 2;
|
|
@@ -3683,7 +3826,7 @@ async function runOrchestrator(opts) {
|
|
|
3683
3826
|
result.steps.browser = openBrowser(dashboardUrl);
|
|
3684
3827
|
}
|
|
3685
3828
|
const daemonRunning = result.steps.daemon === "spawned" || result.steps.daemon === "already-running";
|
|
3686
|
-
const daemonLog = daemonRunning ?
|
|
3829
|
+
const daemonLog = daemonRunning ? path8.relative(opts.scanPath, daemonLogPath(opts.scanPath)) : null;
|
|
3687
3830
|
printSummary(result, graph, dashboardUrl, daemonLog);
|
|
3688
3831
|
return result;
|
|
3689
3832
|
}
|
|
@@ -4091,7 +4234,7 @@ async function connectorTest(deps, id) {
|
|
|
4091
4234
|
function printConnectorUsage(write) {
|
|
4092
4235
|
write("usage: neat connector <add|list|remove|test> [args]");
|
|
4093
4236
|
write(` providers: ${knownProviderNames().join(", ")}`);
|
|
4094
|
-
write(" pull (polled): supabase, railway, firebase, cloudflare");
|
|
4237
|
+
write(" pull (polled): supabase, railway, firebase, cloudflare, neon");
|
|
4095
4238
|
write(" push (drains): vercel \u2014 provisions a Vercel trace Drain that forwards");
|
|
4096
4239
|
write(" traces to the daemon's OTLP receiver; no app instrumentation");
|
|
4097
4240
|
write(" add <provider> add a connector; validates the credential first (--skip-validate to skip)");
|
|
@@ -4100,6 +4243,8 @@ function printConnectorUsage(write) {
|
|
|
4100
4243
|
write(" vercel: neat connector add vercel --token $VERCEL_TOKEN \\");
|
|
4101
4244
|
write(" --otel-token $NEAT_OTEL_TOKEN --team-id <teamId> \\");
|
|
4102
4245
|
write(" --endpoint https://<public-host>/v1/traces [--project-ids <id,id>]");
|
|
4246
|
+
write(" neon: neat connector add neon --credential $NEON_OBSERVER_URL \\");
|
|
4247
|
+
write(" --project-id <projectId> --service-name <serviceName>");
|
|
4103
4248
|
write(" list list configured connectors (credentials shown redacted)");
|
|
4104
4249
|
write(" flags: --project <name>");
|
|
4105
4250
|
write(" remove <id> remove a connector by id (a push provider also has its drain deleted)");
|
|
@@ -4138,27 +4283,27 @@ async function runConnectorCommand(rawArgs, deps = {}) {
|
|
|
4138
4283
|
}
|
|
4139
4284
|
|
|
4140
4285
|
// src/hooks-cli.ts
|
|
4141
|
-
import
|
|
4286
|
+
import path9 from "path";
|
|
4142
4287
|
import os from "os";
|
|
4143
|
-
import { promises as
|
|
4288
|
+
import { promises as fs8 } from "fs";
|
|
4144
4289
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4145
4290
|
var HOOK_FILENAME = "neat-search-nudge.mjs";
|
|
4146
4291
|
var GUIDE_FILENAME = "GRAPH_FIRST.md";
|
|
4147
4292
|
var GUIDE_INSTALL_NAME = "neat-graph-first.md";
|
|
4148
4293
|
var HOOK_MATCHER = "Grep|Glob|Bash";
|
|
4149
4294
|
function moduleDir() {
|
|
4150
|
-
return typeof __dirname !== "undefined" ? __dirname :
|
|
4295
|
+
return typeof __dirname !== "undefined" ? __dirname : path9.dirname(fileURLToPath3(import.meta.url));
|
|
4151
4296
|
}
|
|
4152
4297
|
async function readSkillAsset(rel) {
|
|
4153
4298
|
const here = moduleDir();
|
|
4154
4299
|
const candidates = [
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4300
|
+
path9.resolve(here, "../../claude-skill", rel),
|
|
4301
|
+
path9.resolve(here, "../../../claude-skill", rel),
|
|
4302
|
+
path9.resolve(here, "../claude-skill", rel)
|
|
4158
4303
|
];
|
|
4159
4304
|
for (const candidate of candidates) {
|
|
4160
4305
|
try {
|
|
4161
|
-
return await
|
|
4306
|
+
return await fs8.readFile(candidate, "utf8");
|
|
4162
4307
|
} catch {
|
|
4163
4308
|
}
|
|
4164
4309
|
}
|
|
@@ -4168,17 +4313,17 @@ async function readSkillAsset(rel) {
|
|
|
4168
4313
|
}
|
|
4169
4314
|
function neatHome() {
|
|
4170
4315
|
const override = process.env.NEAT_HOME;
|
|
4171
|
-
if (override && override.length > 0) return
|
|
4172
|
-
return
|
|
4316
|
+
if (override && override.length > 0) return path9.resolve(override);
|
|
4317
|
+
return path9.join(os.homedir(), ".neat");
|
|
4173
4318
|
}
|
|
4174
4319
|
function claudeSettingsPath() {
|
|
4175
4320
|
const override = process.env.NEAT_CLAUDE_SETTINGS;
|
|
4176
|
-
if (override && override.length > 0) return
|
|
4321
|
+
if (override && override.length > 0) return path9.resolve(override);
|
|
4177
4322
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? os.homedir();
|
|
4178
|
-
return
|
|
4323
|
+
return path9.join(home, ".claude", "settings.json");
|
|
4179
4324
|
}
|
|
4180
4325
|
function installedHookPath() {
|
|
4181
|
-
return
|
|
4326
|
+
return path9.join(neatHome(), "hooks", HOOK_FILENAME);
|
|
4182
4327
|
}
|
|
4183
4328
|
function isNeatSearchEntry(entry2) {
|
|
4184
4329
|
return (entry2.hooks ?? []).some(
|
|
@@ -4211,14 +4356,14 @@ async function runHooks(opts) {
|
|
|
4211
4356
|
const hookScript = await readSkillAsset(`hooks/${HOOK_FILENAME}`);
|
|
4212
4357
|
const guide = await readSkillAsset(GUIDE_FILENAME);
|
|
4213
4358
|
const scriptPath = installedHookPath();
|
|
4214
|
-
await
|
|
4215
|
-
await
|
|
4216
|
-
const guidePath =
|
|
4217
|
-
await
|
|
4359
|
+
await fs8.mkdir(path9.dirname(scriptPath), { recursive: true });
|
|
4360
|
+
await fs8.writeFile(scriptPath, hookScript, { mode: 493 });
|
|
4361
|
+
const guidePath = path9.join(neatHome(), GUIDE_INSTALL_NAME);
|
|
4362
|
+
await fs8.writeFile(guidePath, guide, "utf8");
|
|
4218
4363
|
const settingsFile = claudeSettingsPath();
|
|
4219
4364
|
let settings = {};
|
|
4220
4365
|
try {
|
|
4221
|
-
settings = JSON.parse(await
|
|
4366
|
+
settings = JSON.parse(await fs8.readFile(settingsFile, "utf8"));
|
|
4222
4367
|
} catch (err) {
|
|
4223
4368
|
if (err.code !== "ENOENT") {
|
|
4224
4369
|
console.error(
|
|
@@ -4240,8 +4385,8 @@ async function runHooks(opts) {
|
|
|
4240
4385
|
...settings,
|
|
4241
4386
|
hooks: { ...hooks, PreToolUse: preToolUse }
|
|
4242
4387
|
};
|
|
4243
|
-
await
|
|
4244
|
-
await
|
|
4388
|
+
await fs8.mkdir(path9.dirname(settingsFile), { recursive: true });
|
|
4389
|
+
await fs8.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
4245
4390
|
console.log(`neat hooks: installed the search-nudge hook`);
|
|
4246
4391
|
console.log(` script: ${scriptPath}`);
|
|
4247
4392
|
console.log(` settings: ${settingsFile} (PreToolUse \u2192 ${HOOK_MATCHER})`);
|
|
@@ -4311,7 +4456,7 @@ async function runHooksCommand(args) {
|
|
|
4311
4456
|
}
|
|
4312
4457
|
|
|
4313
4458
|
// src/cli-verbs.ts
|
|
4314
|
-
import
|
|
4459
|
+
import path10 from "path";
|
|
4315
4460
|
|
|
4316
4461
|
// src/cli-client.ts
|
|
4317
4462
|
import { Provenance as Provenance2 } from "@neat.is/types";
|
|
@@ -4339,10 +4484,10 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
4339
4484
|
const root = baseUrl.replace(/\/$/, "");
|
|
4340
4485
|
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
4341
4486
|
return {
|
|
4342
|
-
async get(
|
|
4487
|
+
async get(path12) {
|
|
4343
4488
|
let res;
|
|
4344
4489
|
try {
|
|
4345
|
-
res = await fetch(`${root}${
|
|
4490
|
+
res = await fetch(`${root}${path12}`, {
|
|
4346
4491
|
headers: { ...authHeader }
|
|
4347
4492
|
});
|
|
4348
4493
|
} catch (err) {
|
|
@@ -4354,16 +4499,16 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
4354
4499
|
const body = await res.text().catch(() => "");
|
|
4355
4500
|
throw new HttpError(
|
|
4356
4501
|
res.status,
|
|
4357
|
-
`${res.status} ${res.statusText} on GET ${
|
|
4502
|
+
`${res.status} ${res.statusText} on GET ${path12}: ${body}`,
|
|
4358
4503
|
body
|
|
4359
4504
|
);
|
|
4360
4505
|
}
|
|
4361
4506
|
return await res.json();
|
|
4362
4507
|
},
|
|
4363
|
-
async post(
|
|
4508
|
+
async post(path12, body) {
|
|
4364
4509
|
let res;
|
|
4365
4510
|
try {
|
|
4366
|
-
res = await fetch(`${root}${
|
|
4511
|
+
res = await fetch(`${root}${path12}`, {
|
|
4367
4512
|
method: "POST",
|
|
4368
4513
|
headers: { "content-type": "application/json", ...authHeader },
|
|
4369
4514
|
body: JSON.stringify(body)
|
|
@@ -4377,7 +4522,7 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
4377
4522
|
const text = await res.text().catch(() => "");
|
|
4378
4523
|
throw new HttpError(
|
|
4379
4524
|
res.status,
|
|
4380
|
-
`${res.status} ${res.statusText} on POST ${
|
|
4525
|
+
`${res.status} ${res.statusText} on POST ${path12}: ${text}`,
|
|
4381
4526
|
text
|
|
4382
4527
|
);
|
|
4383
4528
|
}
|
|
@@ -4391,12 +4536,12 @@ function projectPath(project, suffix) {
|
|
|
4391
4536
|
}
|
|
4392
4537
|
async function runRootCause(client, input) {
|
|
4393
4538
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
4394
|
-
const
|
|
4539
|
+
const path12 = projectPath(
|
|
4395
4540
|
input.project,
|
|
4396
4541
|
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
4397
4542
|
);
|
|
4398
4543
|
try {
|
|
4399
|
-
const result = await client.get(
|
|
4544
|
+
const result = await client.get(path12);
|
|
4400
4545
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
4401
4546
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
4402
4547
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -4422,12 +4567,12 @@ async function runRootCause(client, input) {
|
|
|
4422
4567
|
}
|
|
4423
4568
|
async function runBlastRadius(client, input) {
|
|
4424
4569
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
4425
|
-
const
|
|
4570
|
+
const path12 = projectPath(
|
|
4426
4571
|
input.project,
|
|
4427
4572
|
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
4428
4573
|
);
|
|
4429
4574
|
try {
|
|
4430
|
-
const result = await client.get(
|
|
4575
|
+
const result = await client.get(path12);
|
|
4431
4576
|
if (result.totalAffected === 0) {
|
|
4432
4577
|
return {
|
|
4433
4578
|
summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
|
|
@@ -4461,12 +4606,12 @@ function formatBlastEntry(n) {
|
|
|
4461
4606
|
}
|
|
4462
4607
|
async function runDependencies(client, input) {
|
|
4463
4608
|
const depth = input.depth ?? 3;
|
|
4464
|
-
const
|
|
4609
|
+
const path12 = projectPath(
|
|
4465
4610
|
input.project,
|
|
4466
4611
|
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
4467
4612
|
);
|
|
4468
4613
|
try {
|
|
4469
|
-
const result = await client.get(
|
|
4614
|
+
const result = await client.get(path12);
|
|
4470
4615
|
if (result.total === 0) {
|
|
4471
4616
|
return {
|
|
4472
4617
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -4558,9 +4703,9 @@ function formatDuration(ms) {
|
|
|
4558
4703
|
return `${Math.round(h / 24)}d`;
|
|
4559
4704
|
}
|
|
4560
4705
|
async function runIncidents(client, input) {
|
|
4561
|
-
const
|
|
4706
|
+
const path12 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
4562
4707
|
try {
|
|
4563
|
-
const body = await client.get(
|
|
4708
|
+
const body = await client.get(path12);
|
|
4564
4709
|
const events = body.events;
|
|
4565
4710
|
if (events.length === 0) {
|
|
4566
4711
|
return {
|
|
@@ -4754,6 +4899,9 @@ function formatDivergenceLine(d) {
|
|
|
4754
4899
|
switch (d.type) {
|
|
4755
4900
|
case "missing-observed":
|
|
4756
4901
|
case "missing-extracted":
|
|
4902
|
+
if (d.column) {
|
|
4903
|
+
return ` \u2022 [${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
4904
|
+
}
|
|
4757
4905
|
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} (${d.edgeType}) \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
4758
4906
|
case "version-mismatch":
|
|
4759
4907
|
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared ${d.extractedVersion}, observed engine ${d.observedVersion} (${d.compatibility})`;
|
|
@@ -4850,7 +4998,7 @@ async function resolveProjectEntry(opts) {
|
|
|
4850
4998
|
const cwd = opts.cwd ?? process.cwd();
|
|
4851
4999
|
const resolvedCwd = await normalizeProjectPath(cwd);
|
|
4852
5000
|
for (const entry2 of entries) {
|
|
4853
|
-
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${
|
|
5001
|
+
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path10.sep}`)) {
|
|
4854
5002
|
return entry2;
|
|
4855
5003
|
}
|
|
4856
5004
|
}
|
|
@@ -5297,15 +5445,15 @@ async function buildPatchSections(services, project) {
|
|
|
5297
5445
|
for (const svc of services) {
|
|
5298
5446
|
const installer = await pickInstaller(svc.dir);
|
|
5299
5447
|
if (!installer) continue;
|
|
5300
|
-
const
|
|
5301
|
-
if (isEmptyPlan(
|
|
5302
|
-
sections.push({ installer: installer.name, plan:
|
|
5448
|
+
const plan4 = await installer.plan(svc.dir, { project });
|
|
5449
|
+
if (isEmptyPlan(plan4) && !plan4.libOnly && plan4.runtimeKind === void 0) continue;
|
|
5450
|
+
sections.push({ installer: installer.name, plan: plan4 });
|
|
5303
5451
|
}
|
|
5304
5452
|
return sections;
|
|
5305
5453
|
}
|
|
5306
5454
|
async function runInit(opts) {
|
|
5307
5455
|
const written = [];
|
|
5308
|
-
const stat = await
|
|
5456
|
+
const stat = await fs9.stat(opts.scanPath).catch(() => null);
|
|
5309
5457
|
if (!stat || !stat.isDirectory()) {
|
|
5310
5458
|
console.error(`neat init: ${opts.scanPath} is not a directory`);
|
|
5311
5459
|
return { exitCode: 2, writtenFiles: written };
|
|
@@ -5314,13 +5462,13 @@ async function runInit(opts) {
|
|
|
5314
5462
|
printDiscoveryReport(opts, services);
|
|
5315
5463
|
const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
|
|
5316
5464
|
const patch = renderPatch(sections);
|
|
5317
|
-
const patchPath =
|
|
5465
|
+
const patchPath = path11.join(opts.scanPath, "neat.patch");
|
|
5318
5466
|
if (opts.dryRun) {
|
|
5319
|
-
await
|
|
5467
|
+
await fs9.writeFile(patchPath, patch, "utf8");
|
|
5320
5468
|
written.push(patchPath);
|
|
5321
5469
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
5322
|
-
const gitignorePath =
|
|
5323
|
-
const gitignoreExists = await
|
|
5470
|
+
const gitignorePath = path11.join(opts.scanPath, ".gitignore");
|
|
5471
|
+
const gitignoreExists = await fs9.stat(gitignorePath).then(() => true).catch(() => false);
|
|
5324
5472
|
const verb = gitignoreExists ? "append" : "create";
|
|
5325
5473
|
console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
|
|
5326
5474
|
console.log("rerun without --dry-run to register and snapshot.");
|
|
@@ -5331,9 +5479,9 @@ async function runInit(opts) {
|
|
|
5331
5479
|
const graph = getGraph(graphKey);
|
|
5332
5480
|
const projectPaths = pathsForProject(
|
|
5333
5481
|
graphKey,
|
|
5334
|
-
|
|
5482
|
+
path11.join(opts.scanPath, "neat-out")
|
|
5335
5483
|
);
|
|
5336
|
-
const errorsPath =
|
|
5484
|
+
const errorsPath = path11.join(path11.dirname(opts.outPath), path11.basename(projectPaths.errorsPath));
|
|
5337
5485
|
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
5338
5486
|
await saveGraphToDisk(graph, opts.outPath);
|
|
5339
5487
|
written.push(opts.outPath);
|
|
@@ -5412,7 +5560,7 @@ async function runInit(opts) {
|
|
|
5412
5560
|
console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
5413
5561
|
}
|
|
5414
5562
|
} else {
|
|
5415
|
-
await
|
|
5563
|
+
await fs9.writeFile(patchPath, patch, "utf8");
|
|
5416
5564
|
written.push(patchPath);
|
|
5417
5565
|
}
|
|
5418
5566
|
}
|
|
@@ -5452,9 +5600,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
5452
5600
|
};
|
|
5453
5601
|
function claudeConfigPath() {
|
|
5454
5602
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
5455
|
-
if (override && override.length > 0) return
|
|
5603
|
+
if (override && override.length > 0) return path11.resolve(override);
|
|
5456
5604
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
5457
|
-
return
|
|
5605
|
+
return path11.join(home, ".claude.json");
|
|
5458
5606
|
}
|
|
5459
5607
|
async function runSkill(opts) {
|
|
5460
5608
|
const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -5466,7 +5614,7 @@ async function runSkill(opts) {
|
|
|
5466
5614
|
const target = claudeConfigPath();
|
|
5467
5615
|
let existing = {};
|
|
5468
5616
|
try {
|
|
5469
|
-
existing = JSON.parse(await
|
|
5617
|
+
existing = JSON.parse(await fs9.readFile(target, "utf8"));
|
|
5470
5618
|
} catch (err) {
|
|
5471
5619
|
if (err.code !== "ENOENT") {
|
|
5472
5620
|
console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
|
|
@@ -5478,8 +5626,8 @@ async function runSkill(opts) {
|
|
|
5478
5626
|
...existing,
|
|
5479
5627
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
5480
5628
|
};
|
|
5481
|
-
await
|
|
5482
|
-
await
|
|
5629
|
+
await fs9.mkdir(path11.dirname(target), { recursive: true });
|
|
5630
|
+
await fs9.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
5483
5631
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
5484
5632
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
5485
5633
|
console.log("");
|
|
@@ -5531,7 +5679,7 @@ async function main() {
|
|
|
5531
5679
|
}
|
|
5532
5680
|
const cmd = argvParsed.positional[0];
|
|
5533
5681
|
const parsed = { ...argvParsed, positional: argvParsed.positional.slice(1) };
|
|
5534
|
-
const { positional, apply:
|
|
5682
|
+
const { positional, apply: apply4, dryRun, noInstall } = parsed;
|
|
5535
5683
|
const project = parsed.project ?? DEFAULT_PROJECT;
|
|
5536
5684
|
if (cmd === "init") {
|
|
5537
5685
|
const target = positional[0];
|
|
@@ -5540,22 +5688,22 @@ async function main() {
|
|
|
5540
5688
|
usage2();
|
|
5541
5689
|
process.exit(2);
|
|
5542
5690
|
}
|
|
5543
|
-
if (
|
|
5691
|
+
if (apply4 && dryRun) {
|
|
5544
5692
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
5545
5693
|
process.exit(2);
|
|
5546
5694
|
}
|
|
5547
|
-
const scanPath =
|
|
5695
|
+
const scanPath = path11.resolve(target);
|
|
5548
5696
|
const projectExplicit = parsed.project !== null;
|
|
5549
|
-
const projectName = projectExplicit ? project :
|
|
5697
|
+
const projectName = projectExplicit ? project : path11.basename(scanPath);
|
|
5550
5698
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
5551
|
-
const fallback = pathsForProject(projectKey,
|
|
5552
|
-
const outPath =
|
|
5699
|
+
const fallback = pathsForProject(projectKey, path11.join(scanPath, "neat-out")).snapshotPath;
|
|
5700
|
+
const outPath = path11.resolve(process.env.NEAT_OUT_PATH ?? fallback);
|
|
5553
5701
|
const result = await runInit({
|
|
5554
5702
|
scanPath,
|
|
5555
5703
|
outPath,
|
|
5556
5704
|
project: projectName,
|
|
5557
5705
|
projectExplicit,
|
|
5558
|
-
apply:
|
|
5706
|
+
apply: apply4,
|
|
5559
5707
|
dryRun,
|
|
5560
5708
|
noInstall,
|
|
5561
5709
|
verbose: parsed.verbose
|
|
@@ -5570,21 +5718,21 @@ async function main() {
|
|
|
5570
5718
|
usage2();
|
|
5571
5719
|
process.exit(2);
|
|
5572
5720
|
}
|
|
5573
|
-
const scanPath =
|
|
5574
|
-
const stat = await
|
|
5721
|
+
const scanPath = path11.resolve(target);
|
|
5722
|
+
const stat = await fs9.stat(scanPath).catch(() => null);
|
|
5575
5723
|
if (!stat || !stat.isDirectory()) {
|
|
5576
5724
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
5577
5725
|
process.exit(2);
|
|
5578
5726
|
}
|
|
5579
|
-
const projectPaths = pathsForProject(project,
|
|
5580
|
-
const outPath =
|
|
5581
|
-
const errorsPath =
|
|
5582
|
-
process.env.NEAT_ERRORS_PATH ??
|
|
5727
|
+
const projectPaths = pathsForProject(project, path11.join(scanPath, "neat-out"));
|
|
5728
|
+
const outPath = path11.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
|
|
5729
|
+
const errorsPath = path11.resolve(
|
|
5730
|
+
process.env.NEAT_ERRORS_PATH ?? path11.join(path11.dirname(outPath), path11.basename(projectPaths.errorsPath))
|
|
5583
5731
|
);
|
|
5584
|
-
const staleEventsPath =
|
|
5585
|
-
process.env.NEAT_STALE_EVENTS_PATH ??
|
|
5732
|
+
const staleEventsPath = path11.resolve(
|
|
5733
|
+
process.env.NEAT_STALE_EVENTS_PATH ?? path11.join(path11.dirname(outPath), path11.basename(projectPaths.staleEventsPath))
|
|
5586
5734
|
);
|
|
5587
|
-
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ?
|
|
5735
|
+
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path11.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
|
|
5588
5736
|
const handle = await startWatch(getGraph(project), {
|
|
5589
5737
|
scanPath,
|
|
5590
5738
|
outPath,
|
|
@@ -5593,7 +5741,7 @@ async function main() {
|
|
|
5593
5741
|
project,
|
|
5594
5742
|
// Resolve NEAT_HOME so a `neat watch` picks up connectors added to
|
|
5595
5743
|
// ~/.neat/connectors.json (#871). Same resolution the rest of the CLI uses.
|
|
5596
|
-
neatHome: process.env.NEAT_HOME ?
|
|
5744
|
+
neatHome: process.env.NEAT_HOME ? path11.resolve(process.env.NEAT_HOME) : path11.join(os2.homedir(), ".neat"),
|
|
5597
5745
|
...embeddingsCachePath ? { embeddingsCachePath } : {},
|
|
5598
5746
|
host: process.env.HOST ?? "0.0.0.0",
|
|
5599
5747
|
port: Number(process.env.PORT ?? 8080),
|
|
@@ -5770,11 +5918,11 @@ async function main() {
|
|
|
5770
5918
|
process.exit(1);
|
|
5771
5919
|
}
|
|
5772
5920
|
async function tryOrchestrator(cmd, parsed) {
|
|
5773
|
-
const scanPath =
|
|
5774
|
-
const stat = await
|
|
5921
|
+
const scanPath = path11.resolve(cmd);
|
|
5922
|
+
const stat = await fs9.stat(scanPath).catch(() => null);
|
|
5775
5923
|
if (!stat || !stat.isDirectory()) return null;
|
|
5776
5924
|
const projectExplicit = parsed.project !== null;
|
|
5777
|
-
const projectName = projectExplicit ? parsed.project :
|
|
5925
|
+
const projectName = projectExplicit ? parsed.project : path11.basename(scanPath);
|
|
5778
5926
|
const result = await runOrchestrator({
|
|
5779
5927
|
scanPath,
|
|
5780
5928
|
project: projectName,
|