@neat.is/core 0.4.10 → 0.4.12
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-RC3CIDZO.js → chunk-7JY6F7BY.js} +4 -2
- package/dist/{chunk-RC3CIDZO.js.map → chunk-7JY6F7BY.js.map} +1 -1
- package/dist/{chunk-J5CEKCTR.js → chunk-WDG4QEFO.js} +782 -192
- package/dist/chunk-WDG4QEFO.js.map +1 -0
- package/dist/cli.cjs +1387 -550
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +442 -192
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +831 -239
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +829 -237
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +884 -364
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +5 -2
- package/dist/chunk-J5CEKCTR.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
attachGraphToEventBus,
|
|
18
18
|
buildApi,
|
|
19
19
|
computeDivergences,
|
|
20
|
+
detectPackageManager,
|
|
20
21
|
discoverServices,
|
|
21
22
|
emitNeatEvent,
|
|
22
23
|
ensureCompatLoaded,
|
|
@@ -37,11 +38,12 @@ import {
|
|
|
37
38
|
removeProject,
|
|
38
39
|
resetGraph,
|
|
39
40
|
retireEdgesByFile,
|
|
41
|
+
runPackageManagerInstall,
|
|
40
42
|
saveGraphToDisk,
|
|
41
43
|
setStatus,
|
|
42
44
|
startPersistLoop,
|
|
43
45
|
startStalenessLoop
|
|
44
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-WDG4QEFO.js";
|
|
45
47
|
import {
|
|
46
48
|
startOtelGrpcReceiver
|
|
47
49
|
} from "./chunk-3QCRUEQD.js";
|
|
@@ -54,8 +56,8 @@ import {
|
|
|
54
56
|
} from "./chunk-HVF4S7J3.js";
|
|
55
57
|
|
|
56
58
|
// src/cli.ts
|
|
57
|
-
import
|
|
58
|
-
import { promises as
|
|
59
|
+
import path8 from "path";
|
|
60
|
+
import { promises as fs7, readFileSync } from "fs";
|
|
59
61
|
import { fileURLToPath } from "url";
|
|
60
62
|
|
|
61
63
|
// src/gitignore.ts
|
|
@@ -424,6 +426,7 @@ async function startWatch(graph, opts) {
|
|
|
424
426
|
const onSpan = makeSpanHandler({
|
|
425
427
|
graph,
|
|
426
428
|
errorsPath: opts.errorsPath,
|
|
429
|
+
scanPath: opts.scanPath,
|
|
427
430
|
project: projectName,
|
|
428
431
|
writeErrorEventInline: false,
|
|
429
432
|
onPolicyTrigger
|
|
@@ -438,6 +441,7 @@ async function startWatch(graph, opts) {
|
|
|
438
441
|
const onSpanGrpc = makeSpanHandler({
|
|
439
442
|
graph,
|
|
440
443
|
errorsPath: opts.errorsPath,
|
|
444
|
+
scanPath: opts.scanPath,
|
|
441
445
|
project: projectName,
|
|
442
446
|
onPolicyTrigger
|
|
443
447
|
});
|
|
@@ -694,16 +698,24 @@ ${contents}EOF
|
|
|
694
698
|
// src/installers/javascript.ts
|
|
695
699
|
import { promises as fs4 } from "fs";
|
|
696
700
|
import path4 from "path";
|
|
701
|
+
import semver from "semver";
|
|
697
702
|
|
|
698
703
|
// src/installers/templates.ts
|
|
699
704
|
var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
|
|
700
|
-
var OTEL_INIT_STAMP = "// neat-template-version:
|
|
705
|
+
var OTEL_INIT_STAMP = "// neat-template-version: 4 \u2014 layered file-first capture (ADR-090): stack walk + handler-entry + off-stack facades.";
|
|
701
706
|
var OTEL_OTLP_HEADERS_JS = "if (process.env.NEAT_OTEL_TOKEN) process.env.OTEL_EXPORTER_OTLP_HEADERS ||= 'Authorization=Bearer ' + process.env.NEAT_OTEL_TOKEN";
|
|
702
|
-
function
|
|
703
|
-
const stackT = ts ? ": string" : "";
|
|
707
|
+
function neatCaptureSource(ts) {
|
|
704
708
|
const spanT = ts ? ": any" : "";
|
|
705
709
|
const strOpt = ts ? ": string | undefined" : "";
|
|
706
|
-
|
|
710
|
+
const anyT = ts ? ": any" : "";
|
|
711
|
+
const fnT = ts ? ": any" : "";
|
|
712
|
+
const arrAny = ts ? ": any[]" : "";
|
|
713
|
+
return `// Context key shared by the facade/handler wraps (writers) and the
|
|
714
|
+
// processor fallback (reader). Symbol.for keeps it stable even if the wraps
|
|
715
|
+
// and the processor end up in different module instances.
|
|
716
|
+
const NEAT_USER_FRAME = Symbol.for('neat.user-frame')
|
|
717
|
+
|
|
718
|
+
function __neatPickUserFrame(stack${strOpt}) {
|
|
707
719
|
const lines = String(stack || '').split('\\n')
|
|
708
720
|
for (let i = 0; i < lines.length; i++) {
|
|
709
721
|
const raw = lines[i].trim()
|
|
@@ -712,6 +724,9 @@ function neatCallsiteProcessorSource(ts) {
|
|
|
712
724
|
if (raw.indexOf('@opentelemetry') !== -1) continue
|
|
713
725
|
if (raw.indexOf('node:') !== -1) continue
|
|
714
726
|
if (raw.indexOf('NeatCallSiteSpanProcessor') !== -1) continue
|
|
727
|
+
// Skip NEAT's own inlined wraps/helpers (all carry the __neat prefix) so a
|
|
728
|
+
// facade frame never masquerades as the user's call site.
|
|
729
|
+
if (raw.indexOf('__neat') !== -1) continue
|
|
715
730
|
const bodyText = raw.slice(3)
|
|
716
731
|
const loc = bodyText.match(/:(\\d+):(\\d+)\\)?$/)
|
|
717
732
|
if (!loc) continue
|
|
@@ -733,32 +748,245 @@ function neatCallsiteProcessorSource(ts) {
|
|
|
733
748
|
return null
|
|
734
749
|
}
|
|
735
750
|
|
|
751
|
+
// Layer 2/3 fallback source: the frame a handler-entry or facade wrap pushed
|
|
752
|
+
// into context. Reads the span's own parent context first, then the active
|
|
753
|
+
// context \u2014 the capture spike confirmed both return the value for undici.
|
|
754
|
+
function __neatFrameFromContext(parentContext${anyT}) {
|
|
755
|
+
try {
|
|
756
|
+
const fromParent =
|
|
757
|
+
parentContext && typeof parentContext.getValue === 'function'
|
|
758
|
+
? parentContext.getValue(NEAT_USER_FRAME)
|
|
759
|
+
: undefined
|
|
760
|
+
return fromParent || context.active().getValue(NEAT_USER_FRAME) || null
|
|
761
|
+
} catch (_e) {
|
|
762
|
+
return null
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function __neatSetCodeAttrs(span${spanT}, frame${anyT}) {
|
|
767
|
+
span.setAttribute('code.filepath', frame.filepath)
|
|
768
|
+
if (typeof frame.lineno === 'number') span.setAttribute('code.lineno', frame.lineno)
|
|
769
|
+
if (frame.function) span.setAttribute('code.function', frame.function)
|
|
770
|
+
}
|
|
771
|
+
|
|
736
772
|
class NeatCallSiteSpanProcessor {
|
|
737
|
-
onStart(span${spanT}) {
|
|
773
|
+
onStart(span${spanT}, parentContext${anyT}) {
|
|
738
774
|
if (!span || (span.kind !== 2 && span.kind !== 3)) return
|
|
739
|
-
|
|
775
|
+
// Layer 1 \u2014 synchronous stack walk (sync-wrapper instrumentations).
|
|
776
|
+
let frame = __neatPickUserFrame(new Error().stack)
|
|
777
|
+
// Layer 2/3 \u2014 the handler-entry or off-stack-facade frame from context.
|
|
778
|
+
if (!frame) frame = __neatFrameFromContext(parentContext)
|
|
740
779
|
if (!frame) return
|
|
741
|
-
span
|
|
742
|
-
if (typeof frame.lineno === 'number') span.setAttribute('code.lineno', frame.lineno)
|
|
743
|
-
if (frame.function) span.setAttribute('code.function', frame.function)
|
|
780
|
+
__neatSetCodeAttrs(span, frame)
|
|
744
781
|
}
|
|
745
782
|
onEnd() {}
|
|
746
783
|
forceFlush() { return Promise.resolve() }
|
|
747
784
|
shutdown() { return Promise.resolve() }
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Capture the caller's synchronous frame at the wrap point and run \`fn\` with
|
|
788
|
+
// that frame pushed into the active context (file-awareness.md \xA74 layer 3). An
|
|
789
|
+
// off-stack instrumentation that creates its span inside \`fn\` inherits the
|
|
790
|
+
// frame; the processor reads it via __neatFrameFromContext.
|
|
791
|
+
function __neatRunWithUserFrame(fn${fnT}) {
|
|
792
|
+
const frame = __neatPickUserFrame(new Error().stack)
|
|
793
|
+
if (!frame) return fn()
|
|
794
|
+
return context.with(context.active().setValue(NEAT_USER_FRAME, frame), fn)
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Handler-entry attribution (file-awareness.md \xA74 layer 2). Stamp the framework
|
|
798
|
+
// SERVER span with the handler frame captured at route registration, and push
|
|
799
|
+
// the same frame into context so downstream CLIENT/PRODUCER spans inherit the
|
|
800
|
+
// handler-file floor when their own stack carries none.
|
|
801
|
+
function __neatStampHandler(frame${anyT}, run${fnT}) {
|
|
802
|
+
try {
|
|
803
|
+
const active = trace.getActiveSpan()
|
|
804
|
+
if (active && active.kind === 1 && typeof active.setAttribute === 'function') {
|
|
805
|
+
__neatSetCodeAttrs(active, frame)
|
|
806
|
+
}
|
|
807
|
+
} catch (_e) {}
|
|
808
|
+
try {
|
|
809
|
+
return context.with(context.active().setValue(NEAT_USER_FRAME, frame), run)
|
|
810
|
+
} catch (_e) {
|
|
811
|
+
return run()
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// require-in-the-middle is a transitive dependency of the OTel instrumentation
|
|
816
|
+
// packages NEAT installs, so it resolves in any instrumented CJS service.
|
|
817
|
+
// Guarded: when it's absent (or the host runs as pure ESM, where require isn't
|
|
818
|
+
// defined) the off-stack/handler wraps degrade to the stack walk + context
|
|
819
|
+
// floor, never to a crash.
|
|
820
|
+
function __neatHook(modules${arrAny}, onload${fnT}) {
|
|
821
|
+
try {
|
|
822
|
+
const RITM = require('require-in-the-middle')
|
|
823
|
+
const Hook = RITM && RITM.Hook ? RITM.Hook : RITM
|
|
824
|
+
new Hook(modules, { internals: false }, onload)
|
|
825
|
+
return true
|
|
826
|
+
} catch (_e) {
|
|
827
|
+
return false
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Off-stack facade: Node's built-in fetch / undici. The instrumentation creates
|
|
832
|
+
// the CLIENT span inside a diagnostics_channel handler detached from the
|
|
833
|
+
// caller's stack, so wrap the global so the user frame is in context when the
|
|
834
|
+
// span is created. The capture spike (2026-05-28) validated this on real
|
|
835
|
+
// undici. .name is restored to 'fetch' for ecosystem compatibility; the inner
|
|
836
|
+
// __neat name is what the frame skip keys on.
|
|
837
|
+
function __neatWrapFetch() {
|
|
838
|
+
try {
|
|
839
|
+
const g${anyT} = globalThis
|
|
840
|
+
if (typeof g.fetch === 'function' && !g.fetch.__neatWrapped) {
|
|
841
|
+
const realFetch = g.fetch
|
|
842
|
+
// The wrapper keeps its __neat-prefixed name so the frame skip in
|
|
843
|
+
// __neatPickUserFrame never mistakes the wrapper's own frame for the
|
|
844
|
+
// user's call site (renaming it to 'fetch' would defeat the skip).
|
|
845
|
+
const __neatFetch${anyT} = function (input${anyT}, init${anyT}) {
|
|
846
|
+
return __neatRunWithUserFrame(function () { return realFetch(input, init) })
|
|
847
|
+
}
|
|
848
|
+
__neatFetch.__neatWrapped = true
|
|
849
|
+
g.fetch = __neatFetch
|
|
850
|
+
}
|
|
851
|
+
} catch (_e) {}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Off-stack facade: @prisma/client. Prisma's query engine backdates its spans
|
|
855
|
+
// from Rust, off the caller's stack. Wrap the model methods on the client
|
|
856
|
+
// prototype so the call-site frame (still synchronous at \`prisma.user.find\`)
|
|
857
|
+
// is pushed into context for the engine dispatch.
|
|
858
|
+
function __neatWrapPrisma() {
|
|
859
|
+
__neatHook(['@prisma/client'], function (exports${anyT}) {
|
|
860
|
+
try {
|
|
861
|
+
const Client = exports && exports.PrismaClient
|
|
862
|
+
if (typeof Client === 'function' && !Client.__neatWrapped) {
|
|
863
|
+
const ops = ['findUnique','findUniqueOrThrow','findFirst','findFirstOrThrow','findMany','create','createMany','update','updateMany','upsert','delete','deleteMany','count','aggregate','groupBy']
|
|
864
|
+
const __neatPrismaWrapModel = function (model${anyT}) {
|
|
865
|
+
if (!model || model.__neatWrapped) return model
|
|
866
|
+
for (let i = 0; i < ops.length; i++) {
|
|
867
|
+
const op = ops[i]
|
|
868
|
+
const orig = model[op]
|
|
869
|
+
if (typeof orig !== 'function') continue
|
|
870
|
+
model[op] = function __neatPrismaOp(${ts ? "...args: any[]" : "...args"}) {
|
|
871
|
+
const self = this
|
|
872
|
+
return __neatRunWithUserFrame(function () { return orig.apply(self, args) })
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
model.__neatWrapped = true
|
|
876
|
+
return model
|
|
877
|
+
}
|
|
878
|
+
const proto = Client.prototype
|
|
879
|
+
const handler = function (model${anyT}) { return __neatPrismaWrapModel(model) }
|
|
880
|
+
// Model accessors are lazily created getters on the instance; wrap the
|
|
881
|
+
// \`$extends\`-free common path by trapping property access via a proxy
|
|
882
|
+
// on each constructed client.
|
|
883
|
+
exports.PrismaClient = new Proxy(Client, {
|
|
884
|
+
construct(Target${anyT}, argList${anyT}, NewTarget${anyT}) {
|
|
885
|
+
const instance = Reflect.construct(Target, argList, NewTarget)
|
|
886
|
+
return new Proxy(instance, {
|
|
887
|
+
get(target${anyT}, prop${anyT}, receiver${anyT}) {
|
|
888
|
+
const value = Reflect.get(target, prop, receiver)
|
|
889
|
+
if (value && typeof value === 'object' && typeof prop === 'string' && prop[0] !== '$' && prop[0] !== '_') {
|
|
890
|
+
return handler(value)
|
|
891
|
+
}
|
|
892
|
+
return value
|
|
893
|
+
},
|
|
894
|
+
})
|
|
895
|
+
},
|
|
896
|
+
})
|
|
897
|
+
exports.PrismaClient.__neatWrapped = true
|
|
898
|
+
void proto
|
|
899
|
+
}
|
|
900
|
+
} catch (_e) {}
|
|
901
|
+
return exports
|
|
902
|
+
})
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Handler-entry facades. express / connect share the Layer model (each route
|
|
906
|
+
// handler is a function registered on a Router); the wrap captures the
|
|
907
|
+
// registration frame and stamps + propagates it when the handler runs. The
|
|
908
|
+
// registry is the extensibility seam \u2014 koa, fastify (via @fastify/otel),
|
|
909
|
+
// nestjs, restify, and hapi add an entry as their patch surface is wired.
|
|
910
|
+
function __neatWrapConnectStyle(mod${anyT}) {
|
|
911
|
+
try {
|
|
912
|
+
// express() returns an app whose route verbs live on Router.prototype and
|
|
913
|
+
// the application proto; connect apps expose \`use\`. Wrap the registration
|
|
914
|
+
// verbs so the user handler is wound with its registration frame.
|
|
915
|
+
const verbs = ['use','get','post','put','delete','patch','all','options','head']
|
|
916
|
+
const wrapTarget = function (target${anyT}) {
|
|
917
|
+
if (!target || target.__neatVerbsWrapped) return
|
|
918
|
+
for (let i = 0; i < verbs.length; i++) {
|
|
919
|
+
const verb = verbs[i]
|
|
920
|
+
const orig = target[verb]
|
|
921
|
+
if (typeof orig !== 'function') continue
|
|
922
|
+
target[verb] = function __neatVerb(${ts ? "...args: any[]" : "...args"}) {
|
|
923
|
+
const frame = __neatPickUserFrame(new Error().stack)
|
|
924
|
+
if (frame) {
|
|
925
|
+
for (let a = 0; a < args.length; a++) {
|
|
926
|
+
const h = args[a]
|
|
927
|
+
if (typeof h === 'function' && !h.__neatHandlerWrapped && h.length <= 4) {
|
|
928
|
+
const inner = h
|
|
929
|
+
const __neatHandler${anyT} = function (${ts ? "...hargs: any[]" : "...hargs"}) {
|
|
930
|
+
const self = this
|
|
931
|
+
return __neatStampHandler(frame, function () { return inner.apply(self, hargs) })
|
|
932
|
+
}
|
|
933
|
+
__neatHandler.__neatHandlerWrapped = true
|
|
934
|
+
args[a] = __neatHandler
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return orig.apply(this, args)
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
target.__neatVerbsWrapped = true
|
|
942
|
+
}
|
|
943
|
+
if (mod && mod.Router && mod.Router.prototype) wrapTarget(mod.Router.prototype)
|
|
944
|
+
if (mod && mod.application) wrapTarget(mod.application)
|
|
945
|
+
if (mod && mod.prototype) wrapTarget(mod.prototype)
|
|
946
|
+
} catch (_e) {}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function __neatInstallHandlerEntry() {
|
|
950
|
+
__neatHook(['express'], function (exports${anyT}) { __neatWrapConnectStyle(exports); return exports })
|
|
951
|
+
__neatHook(['connect'], function (exports${anyT}) { __neatWrapConnectStyle(exports); return exports })
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Install every off-stack and handler-entry wrap. Called once after sdk.start()
|
|
955
|
+
// \u2014 before user code requires the framework / Prisma modules, so the hooks land
|
|
956
|
+
// on first require. Each wrap is independently guarded.
|
|
957
|
+
function __neatInstallFacades() {
|
|
958
|
+
__neatWrapFetch()
|
|
959
|
+
__neatWrapPrisma()
|
|
960
|
+
__neatInstallHandlerEntry()
|
|
748
961
|
}`;
|
|
749
962
|
}
|
|
750
|
-
var CALLSITE_PROCESSOR_JS =
|
|
751
|
-
var CALLSITE_PROCESSOR_TS =
|
|
752
|
-
function
|
|
963
|
+
var CALLSITE_PROCESSOR_JS = neatCaptureSource(false);
|
|
964
|
+
var CALLSITE_PROCESSOR_TS = neatCaptureSource(true);
|
|
965
|
+
function neatWireCaptureSource(ts) {
|
|
753
966
|
const providerT = ts ? ": any" : "";
|
|
754
967
|
return `try {
|
|
755
968
|
const provider${providerT} = trace.getTracerProvider()
|
|
756
969
|
const delegate = provider && typeof provider.getDelegate === 'function' ? provider.getDelegate() : provider
|
|
757
|
-
if (delegate
|
|
758
|
-
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
|
|
970
|
+
if (!delegate || typeof delegate.addSpanProcessor !== 'function') {
|
|
971
|
+
throw new Error('[neat] could not resolve a TracerProvider to attach the call-site processor; file-first OBSERVED capture would be silent (file-awareness.md \xA74)')
|
|
972
|
+
}
|
|
973
|
+
const __neatProcessor = new NeatCallSiteSpanProcessor()
|
|
974
|
+
delegate.addSpanProcessor(__neatProcessor)
|
|
975
|
+
// Post-init assertion: confirm attachment on providers that expose their
|
|
976
|
+
// processor list. An un-introspectable provider is trusted (we just called
|
|
977
|
+
// its addSpanProcessor); a list that exists and lacks our processor throws.
|
|
978
|
+
const registered = delegate._registeredSpanProcessors
|
|
979
|
+
if (Array.isArray(registered) && registered.indexOf(__neatProcessor) === -1) {
|
|
980
|
+
throw new Error('[neat] call-site processor did not attach to the active TracerProvider (file-awareness.md \xA74)')
|
|
981
|
+
}
|
|
982
|
+
} catch (err) {
|
|
983
|
+
throw err
|
|
984
|
+
}
|
|
985
|
+
try {
|
|
986
|
+
__neatInstallFacades()
|
|
987
|
+
} catch (_e) {
|
|
988
|
+
// Facade install is best-effort: the stack-walk + handler-entry floor still
|
|
989
|
+
// attribute the sync-wrapper majority even if an off-stack wrap fails.
|
|
762
990
|
}`;
|
|
763
991
|
}
|
|
764
992
|
var OTEL_INIT_CJS = `${OTEL_INIT_HEADER}
|
|
@@ -769,7 +997,7 @@ ${OTEL_OTLP_HEADERS_JS}
|
|
|
769
997
|
|
|
770
998
|
const { NodeSDK } = require('@opentelemetry/sdk-node')
|
|
771
999
|
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
|
|
772
|
-
const { trace } = require('@opentelemetry/api')
|
|
1000
|
+
const { trace, context } = require('@opentelemetry/api')
|
|
773
1001
|
|
|
774
1002
|
${CALLSITE_PROCESSOR_JS}
|
|
775
1003
|
|
|
@@ -777,7 +1005,7 @@ const instrumentations = [getNodeAutoInstrumentations()]
|
|
|
777
1005
|
__INSTRUMENTATION_BLOCK__
|
|
778
1006
|
const sdk = new NodeSDK({ instrumentations })
|
|
779
1007
|
sdk.start()
|
|
780
|
-
${
|
|
1008
|
+
${neatWireCaptureSource(false)}
|
|
781
1009
|
`;
|
|
782
1010
|
var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
|
|
783
1011
|
${OTEL_INIT_STAMP}
|
|
@@ -787,7 +1015,7 @@ ${OTEL_OTLP_HEADERS_JS}
|
|
|
787
1015
|
|
|
788
1016
|
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
789
1017
|
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
790
|
-
import { trace } from '@opentelemetry/api'
|
|
1018
|
+
import { trace, context } from '@opentelemetry/api'
|
|
791
1019
|
|
|
792
1020
|
${CALLSITE_PROCESSOR_JS}
|
|
793
1021
|
|
|
@@ -795,17 +1023,22 @@ const instrumentations = [getNodeAutoInstrumentations()]
|
|
|
795
1023
|
__INSTRUMENTATION_BLOCK__
|
|
796
1024
|
const sdk = new NodeSDK({ instrumentations })
|
|
797
1025
|
sdk.start()
|
|
798
|
-
${
|
|
1026
|
+
${neatWireCaptureSource(false)}
|
|
799
1027
|
`;
|
|
800
1028
|
var OTEL_INIT_TS = `${OTEL_INIT_HEADER}
|
|
801
1029
|
${OTEL_INIT_STAMP}
|
|
1030
|
+
// @ts-nocheck \u2014 generated runtime shim. The layered capture mechanism uses
|
|
1031
|
+
// dynamic patterns (facade wraps, Proxy traps, a createRequire bridge) that a
|
|
1032
|
+
// strict user tsconfig would reject; suppressing type-checking here keeps the
|
|
1033
|
+
// generated file from breaking the host project's \`tsc\` gate (#427) without
|
|
1034
|
+
// constraining the runtime logic. The file is regenerated, never hand-edited.
|
|
802
1035
|
process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
|
|
803
1036
|
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
|
|
804
1037
|
${OTEL_OTLP_HEADERS_JS}
|
|
805
1038
|
|
|
806
1039
|
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
807
1040
|
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
808
|
-
import { trace } from '@opentelemetry/api'
|
|
1041
|
+
import { trace, context } from '@opentelemetry/api'
|
|
809
1042
|
|
|
810
1043
|
${CALLSITE_PROCESSOR_TS}
|
|
811
1044
|
|
|
@@ -813,7 +1046,7 @@ const instrumentations = [getNodeAutoInstrumentations()]
|
|
|
813
1046
|
__INSTRUMENTATION_BLOCK__
|
|
814
1047
|
const sdk = new NodeSDK({ instrumentations })
|
|
815
1048
|
sdk.start()
|
|
816
|
-
${
|
|
1049
|
+
${neatWireCaptureSource(true)}
|
|
817
1050
|
`;
|
|
818
1051
|
function renderNodeOtelInit(template, serviceName, projectName, registrations = []) {
|
|
819
1052
|
const block = registrations.length === 0 ? "" : `
|
|
@@ -1025,6 +1258,13 @@ async function detectRuntimeKind(pkgRoot, pkg) {
|
|
|
1025
1258
|
if (await exists(path4.join(pkgRoot, "vite.config.js")) || await exists(path4.join(pkgRoot, "vite.config.ts")) || await exists(path4.join(pkgRoot, "vite.config.mjs")) || "vite" in deps) {
|
|
1026
1259
|
return "browser-bundle";
|
|
1027
1260
|
}
|
|
1261
|
+
if (await exists(path4.join(pkgRoot, "wrangler.toml"))) return "cloudflare-workers";
|
|
1262
|
+
if (await exists(path4.join(pkgRoot, "bun.lockb"))) return "bun";
|
|
1263
|
+
if (await exists(path4.join(pkgRoot, "deno.json")) || await exists(path4.join(pkgRoot, "deno.lock"))) {
|
|
1264
|
+
return "deno";
|
|
1265
|
+
}
|
|
1266
|
+
const engines = pkg.engines ?? {};
|
|
1267
|
+
if ("electron" in engines) return "electron";
|
|
1028
1268
|
return "node";
|
|
1029
1269
|
}
|
|
1030
1270
|
async function readPackageJson(serviceDir) {
|
|
@@ -1064,6 +1304,12 @@ async function detect(serviceDir) {
|
|
|
1064
1304
|
const pkg = await readPackageJson(serviceDir);
|
|
1065
1305
|
return pkg !== null && typeof pkg.name === "string";
|
|
1066
1306
|
}
|
|
1307
|
+
function needsVersionUpgrade(installed, expected) {
|
|
1308
|
+
return !semver.satisfies(
|
|
1309
|
+
semver.minVersion(installed)?.version ?? installed,
|
|
1310
|
+
expected
|
|
1311
|
+
) && !semver.intersects(installed, expected);
|
|
1312
|
+
}
|
|
1067
1313
|
var NEXT_CONFIG_CANDIDATES = ["next.config.js", "next.config.ts", "next.config.mjs"];
|
|
1068
1314
|
async function findNextConfig(serviceDir) {
|
|
1069
1315
|
for (const name of NEXT_CONFIG_CANDIDATES) {
|
|
@@ -1290,23 +1536,23 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
|
|
|
1290
1536
|
const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
1291
1537
|
const dependencyEdits = [];
|
|
1292
1538
|
for (const sdk of SDK_PACKAGES) {
|
|
1293
|
-
if (sdk.name in existingDeps)
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
});
|
|
1539
|
+
if (sdk.name in existingDeps) {
|
|
1540
|
+
if (needsVersionUpgrade(existingDeps[sdk.name], sdk.version)) {
|
|
1541
|
+
dependencyEdits.push({ file: manifestPath, kind: "upgrade", name: sdk.name, version: sdk.version, fromVersion: existingDeps[sdk.name] });
|
|
1542
|
+
}
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
dependencyEdits.push({ file: manifestPath, kind: "add", name: sdk.name, version: sdk.version });
|
|
1300
1546
|
}
|
|
1301
1547
|
const nonBundled = detectNonBundledInstrumentations(pkg);
|
|
1302
1548
|
for (const inst of nonBundled) {
|
|
1303
|
-
if (inst.pkg in existingDeps)
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
});
|
|
1549
|
+
if (inst.pkg in existingDeps) {
|
|
1550
|
+
if (needsVersionUpgrade(existingDeps[inst.pkg], inst.version)) {
|
|
1551
|
+
dependencyEdits.push({ file: manifestPath, kind: "upgrade", name: inst.pkg, version: inst.version, fromVersion: existingDeps[inst.pkg] });
|
|
1552
|
+
}
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1555
|
+
dependencyEdits.push({ file: manifestPath, kind: "add", name: inst.pkg, version: inst.version });
|
|
1310
1556
|
}
|
|
1311
1557
|
const svcName = serviceNodeName(pkg, serviceDir);
|
|
1312
1558
|
const projectName = projectToken(pkg, serviceDir, project);
|
|
@@ -1752,23 +1998,23 @@ async function plan(serviceDir, opts) {
|
|
|
1752
1998
|
const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
1753
1999
|
const dependencyEdits = [];
|
|
1754
2000
|
for (const sdk of SDK_PACKAGES) {
|
|
1755
|
-
if (sdk.name in existingDeps)
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
});
|
|
2001
|
+
if (sdk.name in existingDeps) {
|
|
2002
|
+
if (needsVersionUpgrade(existingDeps[sdk.name], sdk.version)) {
|
|
2003
|
+
dependencyEdits.push({ file: manifestPath, kind: "upgrade", name: sdk.name, version: sdk.version, fromVersion: existingDeps[sdk.name] });
|
|
2004
|
+
}
|
|
2005
|
+
continue;
|
|
2006
|
+
}
|
|
2007
|
+
dependencyEdits.push({ file: manifestPath, kind: "add", name: sdk.name, version: sdk.version });
|
|
1762
2008
|
}
|
|
1763
2009
|
const nonBundled = detectNonBundledInstrumentations(pkg);
|
|
1764
2010
|
for (const inst of nonBundled) {
|
|
1765
|
-
if (inst.pkg in existingDeps)
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
});
|
|
2011
|
+
if (inst.pkg in existingDeps) {
|
|
2012
|
+
if (needsVersionUpgrade(existingDeps[inst.pkg], inst.version)) {
|
|
2013
|
+
dependencyEdits.push({ file: manifestPath, kind: "upgrade", name: inst.pkg, version: inst.version, fromVersion: existingDeps[inst.pkg] });
|
|
2014
|
+
}
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
dependencyEdits.push({ file: manifestPath, kind: "add", name: inst.pkg, version: inst.version });
|
|
1772
2018
|
}
|
|
1773
2019
|
const entrypointEdits = [];
|
|
1774
2020
|
try {
|
|
@@ -1871,6 +2117,28 @@ async function apply(installPlan) {
|
|
|
1871
2117
|
writtenFiles: []
|
|
1872
2118
|
};
|
|
1873
2119
|
}
|
|
2120
|
+
if (installPlan.runtimeKind === "bun") {
|
|
2121
|
+
return { serviceDir, outcome: "bun", reason: "Bun runtime; use BYO-OTel", writtenFiles: [] };
|
|
2122
|
+
}
|
|
2123
|
+
if (installPlan.runtimeKind === "deno") {
|
|
2124
|
+
return { serviceDir, outcome: "deno", reason: "Deno runtime; use BYO-OTel", writtenFiles: [] };
|
|
2125
|
+
}
|
|
2126
|
+
if (installPlan.runtimeKind === "cloudflare-workers") {
|
|
2127
|
+
return {
|
|
2128
|
+
serviceDir,
|
|
2129
|
+
outcome: "cloudflare-workers",
|
|
2130
|
+
reason: "Cloudflare Workers runtime; use BYO-OTel",
|
|
2131
|
+
writtenFiles: []
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
if (installPlan.runtimeKind === "electron") {
|
|
2135
|
+
return {
|
|
2136
|
+
serviceDir,
|
|
2137
|
+
outcome: "electron",
|
|
2138
|
+
reason: "Electron; use BYO-OTel",
|
|
2139
|
+
writtenFiles: []
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
1874
2142
|
if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0 && installPlan.nextConfigEdit === void 0) {
|
|
1875
2143
|
return {
|
|
1876
2144
|
serviceDir,
|
|
@@ -1921,6 +2189,8 @@ async function apply(installPlan) {
|
|
|
1921
2189
|
if (!(dep.name in (pkg.dependencies ?? {}))) {
|
|
1922
2190
|
pkg.dependencies[dep.name] = dep.version;
|
|
1923
2191
|
}
|
|
2192
|
+
} else if (dep.kind === "upgrade") {
|
|
2193
|
+
pkg.dependencies[dep.name] = dep.version;
|
|
1924
2194
|
} else {
|
|
1925
2195
|
delete pkg.dependencies[dep.name];
|
|
1926
2196
|
}
|
|
@@ -2321,99 +2591,19 @@ function renderPatch(sections) {
|
|
|
2321
2591
|
}
|
|
2322
2592
|
|
|
2323
2593
|
// src/orchestrator.ts
|
|
2324
|
-
import { promises as
|
|
2594
|
+
import { promises as fs6 } from "fs";
|
|
2325
2595
|
import http from "http";
|
|
2326
2596
|
import net from "net";
|
|
2327
|
-
import path7 from "path";
|
|
2328
|
-
import { spawn as spawn3 } from "child_process";
|
|
2329
|
-
import readline from "readline";
|
|
2330
|
-
|
|
2331
|
-
// src/installers/package-manager.ts
|
|
2332
|
-
import { promises as fs6 } from "fs";
|
|
2333
2597
|
import path6 from "path";
|
|
2334
2598
|
import { spawn as spawn2 } from "child_process";
|
|
2335
|
-
|
|
2336
|
-
{ lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
|
|
2337
|
-
{ lockfile: "pnpm-lock.yaml", pm: "pnpm", args: ["install", "--no-summary"] },
|
|
2338
|
-
{ lockfile: "yarn.lock", pm: "yarn", args: ["install", "--silent"] },
|
|
2339
|
-
{
|
|
2340
|
-
lockfile: "package-lock.json",
|
|
2341
|
-
pm: "npm",
|
|
2342
|
-
args: ["install", "--no-audit", "--no-fund", "--prefer-offline"]
|
|
2343
|
-
}
|
|
2344
|
-
];
|
|
2345
|
-
var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
|
|
2346
|
-
async function exists3(p) {
|
|
2347
|
-
try {
|
|
2348
|
-
await fs6.access(p);
|
|
2349
|
-
return true;
|
|
2350
|
-
} catch {
|
|
2351
|
-
return false;
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
async function detectPackageManager(serviceDir) {
|
|
2355
|
-
let dir = path6.resolve(serviceDir);
|
|
2356
|
-
const stops = /* @__PURE__ */ new Set();
|
|
2357
|
-
for (let i = 0; i < 64; i++) {
|
|
2358
|
-
if (stops.has(dir)) break;
|
|
2359
|
-
stops.add(dir);
|
|
2360
|
-
for (const candidate of LOCKFILE_PRIORITY) {
|
|
2361
|
-
const lockPath = path6.join(dir, candidate.lockfile);
|
|
2362
|
-
if (await exists3(lockPath)) {
|
|
2363
|
-
return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
|
|
2364
|
-
}
|
|
2365
|
-
}
|
|
2366
|
-
const parent = path6.dirname(dir);
|
|
2367
|
-
if (parent === dir) break;
|
|
2368
|
-
dir = parent;
|
|
2369
|
-
}
|
|
2370
|
-
return { pm: "npm", cwd: path6.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
|
|
2371
|
-
}
|
|
2372
|
-
async function runPackageManagerInstall(cmd) {
|
|
2373
|
-
return new Promise((resolve) => {
|
|
2374
|
-
const child = spawn2(cmd.pm, cmd.args, {
|
|
2375
|
-
cwd: cmd.cwd,
|
|
2376
|
-
// Inherit PATH + HOME so the user's installed managers resolve.
|
|
2377
|
-
env: process.env,
|
|
2378
|
-
// `false` keeps the parent in control of cleanup if the orchestrator
|
|
2379
|
-
// exits before install finishes. Cross-platform-safe.
|
|
2380
|
-
shell: false,
|
|
2381
|
-
stdio: ["ignore", "ignore", "pipe"]
|
|
2382
|
-
});
|
|
2383
|
-
let stderr = "";
|
|
2384
|
-
child.stderr?.on("data", (chunk) => {
|
|
2385
|
-
stderr += chunk.toString("utf8");
|
|
2386
|
-
});
|
|
2387
|
-
child.on("error", (err) => {
|
|
2388
|
-
resolve({
|
|
2389
|
-
pm: cmd.pm,
|
|
2390
|
-
cwd: cmd.cwd,
|
|
2391
|
-
args: cmd.args,
|
|
2392
|
-
exitCode: 127,
|
|
2393
|
-
stderr: stderr + `
|
|
2394
|
-
${err.message}`
|
|
2395
|
-
});
|
|
2396
|
-
});
|
|
2397
|
-
child.on("close", (code) => {
|
|
2398
|
-
resolve({
|
|
2399
|
-
pm: cmd.pm,
|
|
2400
|
-
cwd: cmd.cwd,
|
|
2401
|
-
args: cmd.args,
|
|
2402
|
-
exitCode: code ?? 1,
|
|
2403
|
-
stderr: stderr.trim()
|
|
2404
|
-
});
|
|
2405
|
-
});
|
|
2406
|
-
});
|
|
2407
|
-
}
|
|
2408
|
-
|
|
2409
|
-
// src/orchestrator.ts
|
|
2599
|
+
import readline from "readline";
|
|
2410
2600
|
async function extractAndPersist(opts) {
|
|
2411
2601
|
const services = await discoverServices(opts.scanPath);
|
|
2412
2602
|
const languages = [...new Set(services.map((s) => s.node.language))].sort();
|
|
2413
2603
|
const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
|
|
2414
2604
|
resetGraph(graphKey);
|
|
2415
2605
|
const graph = getGraph(graphKey);
|
|
2416
|
-
const projectPaths = pathsForProject(graphKey,
|
|
2606
|
+
const projectPaths = pathsForProject(graphKey, path6.join(opts.scanPath, "neat-out"));
|
|
2417
2607
|
const extraction = await extractFromDirectory(graph, opts.scanPath, {
|
|
2418
2608
|
errorsPath: projectPaths.errorsPath
|
|
2419
2609
|
});
|
|
@@ -2439,6 +2629,10 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
2439
2629
|
let libOnly = 0;
|
|
2440
2630
|
let browserBundle = 0;
|
|
2441
2631
|
let reactNative = 0;
|
|
2632
|
+
let bun = 0;
|
|
2633
|
+
let deno = 0;
|
|
2634
|
+
let cloudflareWorkers = 0;
|
|
2635
|
+
let electron = 0;
|
|
2442
2636
|
const installPlans = /* @__PURE__ */ new Map();
|
|
2443
2637
|
for (const svc of services) {
|
|
2444
2638
|
const installer = await pickInstaller(svc.dir);
|
|
@@ -2463,7 +2657,59 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
2463
2657
|
console.log(`skipping ${svc.dir}: browser bundle; browser-OTel support lands in a future release.`);
|
|
2464
2658
|
} else if (outcome.outcome === "react-native") {
|
|
2465
2659
|
reactNative++;
|
|
2466
|
-
|
|
2660
|
+
const svcName = path6.basename(svc.dir);
|
|
2661
|
+
console.log(
|
|
2662
|
+
`neat: ${svc.dir} detected as React Native / Expo
|
|
2663
|
+
The installer doesn't cover this runtime deterministically.
|
|
2664
|
+
Configure your OTel binding to send spans to:
|
|
2665
|
+
http://localhost:4318/projects/${project}/v1/traces
|
|
2666
|
+
Set OTEL_SERVICE_NAME=${svcName}
|
|
2667
|
+
See docs/installer-scope.md \u2192 "Manual setup for out-of-scope runtimes"`
|
|
2668
|
+
);
|
|
2669
|
+
} else if (outcome.outcome === "bun") {
|
|
2670
|
+
bun++;
|
|
2671
|
+
const svcName = path6.basename(svc.dir);
|
|
2672
|
+
console.log(
|
|
2673
|
+
`neat: ${svc.dir} detected as Bun
|
|
2674
|
+
The installer doesn't cover this runtime deterministically.
|
|
2675
|
+
Configure your OTel binding to send spans to:
|
|
2676
|
+
http://localhost:4318/projects/${project}/v1/traces
|
|
2677
|
+
Set OTEL_SERVICE_NAME=${svcName}
|
|
2678
|
+
See docs/installer-scope.md \u2192 "Manual setup for out-of-scope runtimes"`
|
|
2679
|
+
);
|
|
2680
|
+
} else if (outcome.outcome === "deno") {
|
|
2681
|
+
deno++;
|
|
2682
|
+
const svcName = path6.basename(svc.dir);
|
|
2683
|
+
console.log(
|
|
2684
|
+
`neat: ${svc.dir} detected as Deno
|
|
2685
|
+
The installer doesn't cover this runtime deterministically.
|
|
2686
|
+
Configure your OTel binding to send spans to:
|
|
2687
|
+
http://localhost:4318/projects/${project}/v1/traces
|
|
2688
|
+
Set OTEL_SERVICE_NAME=${svcName}
|
|
2689
|
+
See docs/installer-scope.md \u2192 "Manual setup for out-of-scope runtimes"`
|
|
2690
|
+
);
|
|
2691
|
+
} else if (outcome.outcome === "cloudflare-workers") {
|
|
2692
|
+
cloudflareWorkers++;
|
|
2693
|
+
const svcName = path6.basename(svc.dir);
|
|
2694
|
+
console.log(
|
|
2695
|
+
`neat: ${svc.dir} detected as Cloudflare Workers
|
|
2696
|
+
The installer doesn't cover this runtime deterministically.
|
|
2697
|
+
Configure your OTel binding to send spans to:
|
|
2698
|
+
http://localhost:4318/projects/${project}/v1/traces
|
|
2699
|
+
Set OTEL_SERVICE_NAME=${svcName}
|
|
2700
|
+
See docs/installer-scope.md \u2192 "Manual setup for out-of-scope runtimes"`
|
|
2701
|
+
);
|
|
2702
|
+
} else if (outcome.outcome === "electron") {
|
|
2703
|
+
electron++;
|
|
2704
|
+
const svcName = path6.basename(svc.dir);
|
|
2705
|
+
console.log(
|
|
2706
|
+
`neat: ${svc.dir} detected as Electron
|
|
2707
|
+
The installer doesn't cover this runtime deterministically.
|
|
2708
|
+
Configure your OTel binding to send spans to:
|
|
2709
|
+
http://localhost:4318/projects/${project}/v1/traces
|
|
2710
|
+
Set OTEL_SERVICE_NAME=${svcName}
|
|
2711
|
+
See docs/installer-scope.md \u2192 "Manual setup for out-of-scope runtimes"`
|
|
2712
|
+
);
|
|
2467
2713
|
}
|
|
2468
2714
|
}
|
|
2469
2715
|
const packageManagerInstalls = [];
|
|
@@ -2488,6 +2734,10 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
2488
2734
|
libOnly,
|
|
2489
2735
|
browserBundle,
|
|
2490
2736
|
reactNative,
|
|
2737
|
+
bun,
|
|
2738
|
+
deno,
|
|
2739
|
+
cloudflareWorkers,
|
|
2740
|
+
electron,
|
|
2491
2741
|
packageManagerInstalls
|
|
2492
2742
|
};
|
|
2493
2743
|
}
|
|
@@ -2625,10 +2875,10 @@ function formatPortCollisionMessage(port) {
|
|
|
2625
2875
|
];
|
|
2626
2876
|
}
|
|
2627
2877
|
function spawnDaemonDetached() {
|
|
2628
|
-
const here =
|
|
2878
|
+
const here = path6.dirname(new URL(import.meta.url).pathname);
|
|
2629
2879
|
const candidates = [
|
|
2630
|
-
|
|
2631
|
-
|
|
2880
|
+
path6.join(here, "neatd.cjs"),
|
|
2881
|
+
path6.join(here, "neatd.js")
|
|
2632
2882
|
];
|
|
2633
2883
|
let entry2 = null;
|
|
2634
2884
|
const fsSync = __require("fs");
|
|
@@ -2648,7 +2898,7 @@ function spawnDaemonDetached() {
|
|
|
2648
2898
|
if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
|
|
2649
2899
|
env.HOST = "127.0.0.1";
|
|
2650
2900
|
}
|
|
2651
|
-
const child =
|
|
2901
|
+
const child = spawn2(process.execPath, [entry2, "start"], {
|
|
2652
2902
|
detached: true,
|
|
2653
2903
|
// stderr inherits the orchestrator's fd so the daemon's
|
|
2654
2904
|
// `BindAuthorityError` message lands in front of the operator instead
|
|
@@ -2665,7 +2915,7 @@ function openBrowser(url) {
|
|
|
2665
2915
|
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
2666
2916
|
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
2667
2917
|
try {
|
|
2668
|
-
const child =
|
|
2918
|
+
const child = spawn2(cmd, args, { detached: true, stdio: "ignore" });
|
|
2669
2919
|
child.on("error", () => {
|
|
2670
2920
|
});
|
|
2671
2921
|
child.unref();
|
|
@@ -2686,7 +2936,7 @@ async function runOrchestrator(opts) {
|
|
|
2686
2936
|
browser: "skipped"
|
|
2687
2937
|
}
|
|
2688
2938
|
};
|
|
2689
|
-
const stat = await
|
|
2939
|
+
const stat = await fs6.stat(opts.scanPath).catch(() => null);
|
|
2690
2940
|
if (!stat || !stat.isDirectory()) {
|
|
2691
2941
|
console.error(`neat: ${opts.scanPath} is not a directory`);
|
|
2692
2942
|
result.exitCode = 2;
|
|
@@ -2828,7 +3078,7 @@ function printSummary(result, graph, dashboardUrl) {
|
|
|
2828
3078
|
}
|
|
2829
3079
|
|
|
2830
3080
|
// src/cli-verbs.ts
|
|
2831
|
-
import
|
|
3081
|
+
import path7 from "path";
|
|
2832
3082
|
|
|
2833
3083
|
// src/cli-client.ts
|
|
2834
3084
|
import { Provenance as Provenance2 } from "@neat.is/types";
|
|
@@ -2856,10 +3106,10 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
2856
3106
|
const root = baseUrl.replace(/\/$/, "");
|
|
2857
3107
|
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
2858
3108
|
return {
|
|
2859
|
-
async get(
|
|
3109
|
+
async get(path9) {
|
|
2860
3110
|
let res;
|
|
2861
3111
|
try {
|
|
2862
|
-
res = await fetch(`${root}${
|
|
3112
|
+
res = await fetch(`${root}${path9}`, {
|
|
2863
3113
|
headers: { ...authHeader }
|
|
2864
3114
|
});
|
|
2865
3115
|
} catch (err) {
|
|
@@ -2871,16 +3121,16 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
2871
3121
|
const body = await res.text().catch(() => "");
|
|
2872
3122
|
throw new HttpError(
|
|
2873
3123
|
res.status,
|
|
2874
|
-
`${res.status} ${res.statusText} on GET ${
|
|
3124
|
+
`${res.status} ${res.statusText} on GET ${path9}: ${body}`,
|
|
2875
3125
|
body
|
|
2876
3126
|
);
|
|
2877
3127
|
}
|
|
2878
3128
|
return await res.json();
|
|
2879
3129
|
},
|
|
2880
|
-
async post(
|
|
3130
|
+
async post(path9, body) {
|
|
2881
3131
|
let res;
|
|
2882
3132
|
try {
|
|
2883
|
-
res = await fetch(`${root}${
|
|
3133
|
+
res = await fetch(`${root}${path9}`, {
|
|
2884
3134
|
method: "POST",
|
|
2885
3135
|
headers: { "content-type": "application/json", ...authHeader },
|
|
2886
3136
|
body: JSON.stringify(body)
|
|
@@ -2894,7 +3144,7 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
2894
3144
|
const text = await res.text().catch(() => "");
|
|
2895
3145
|
throw new HttpError(
|
|
2896
3146
|
res.status,
|
|
2897
|
-
`${res.status} ${res.statusText} on POST ${
|
|
3147
|
+
`${res.status} ${res.statusText} on POST ${path9}: ${text}`,
|
|
2898
3148
|
text
|
|
2899
3149
|
);
|
|
2900
3150
|
}
|
|
@@ -2908,12 +3158,12 @@ function projectPath(project, suffix) {
|
|
|
2908
3158
|
}
|
|
2909
3159
|
async function runRootCause(client, input) {
|
|
2910
3160
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
2911
|
-
const
|
|
3161
|
+
const path9 = projectPath(
|
|
2912
3162
|
input.project,
|
|
2913
3163
|
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
2914
3164
|
);
|
|
2915
3165
|
try {
|
|
2916
|
-
const result = await client.get(
|
|
3166
|
+
const result = await client.get(path9);
|
|
2917
3167
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
2918
3168
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
2919
3169
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -2939,12 +3189,12 @@ async function runRootCause(client, input) {
|
|
|
2939
3189
|
}
|
|
2940
3190
|
async function runBlastRadius(client, input) {
|
|
2941
3191
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
2942
|
-
const
|
|
3192
|
+
const path9 = projectPath(
|
|
2943
3193
|
input.project,
|
|
2944
3194
|
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
2945
3195
|
);
|
|
2946
3196
|
try {
|
|
2947
|
-
const result = await client.get(
|
|
3197
|
+
const result = await client.get(path9);
|
|
2948
3198
|
if (result.totalAffected === 0) {
|
|
2949
3199
|
return {
|
|
2950
3200
|
summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
|
|
@@ -2978,12 +3228,12 @@ function formatBlastEntry(n) {
|
|
|
2978
3228
|
}
|
|
2979
3229
|
async function runDependencies(client, input) {
|
|
2980
3230
|
const depth = input.depth ?? 3;
|
|
2981
|
-
const
|
|
3231
|
+
const path9 = projectPath(
|
|
2982
3232
|
input.project,
|
|
2983
3233
|
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
2984
3234
|
);
|
|
2985
3235
|
try {
|
|
2986
|
-
const result = await client.get(
|
|
3236
|
+
const result = await client.get(path9);
|
|
2987
3237
|
if (result.total === 0) {
|
|
2988
3238
|
return {
|
|
2989
3239
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -3064,9 +3314,9 @@ function formatDuration(ms) {
|
|
|
3064
3314
|
return `${Math.round(h / 24)}d`;
|
|
3065
3315
|
}
|
|
3066
3316
|
async function runIncidents(client, input) {
|
|
3067
|
-
const
|
|
3317
|
+
const path9 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
3068
3318
|
try {
|
|
3069
|
-
const body = await client.get(
|
|
3319
|
+
const body = await client.get(path9);
|
|
3070
3320
|
const events = body.events;
|
|
3071
3321
|
if (events.length === 0) {
|
|
3072
3322
|
return {
|
|
@@ -3356,7 +3606,7 @@ async function resolveProjectEntry(opts) {
|
|
|
3356
3606
|
const cwd = opts.cwd ?? process.cwd();
|
|
3357
3607
|
const resolvedCwd = await normalizeProjectPath(cwd);
|
|
3358
3608
|
for (const entry2 of entries) {
|
|
3359
|
-
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${
|
|
3609
|
+
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path7.sep}`)) {
|
|
3360
3610
|
return entry2;
|
|
3361
3611
|
}
|
|
3362
3612
|
}
|
|
@@ -3716,10 +3966,10 @@ function assignFlag(out, field, value) {
|
|
|
3716
3966
|
out[field] = value;
|
|
3717
3967
|
}
|
|
3718
3968
|
function readPackageVersion() {
|
|
3719
|
-
const here = typeof __dirname !== "undefined" ? __dirname :
|
|
3969
|
+
const here = typeof __dirname !== "undefined" ? __dirname : path8.dirname(fileURLToPath(import.meta.url));
|
|
3720
3970
|
const candidates = [
|
|
3721
|
-
|
|
3722
|
-
|
|
3971
|
+
path8.resolve(here, "../package.json"),
|
|
3972
|
+
path8.resolve(here, "../../package.json")
|
|
3723
3973
|
];
|
|
3724
3974
|
for (const candidate of candidates) {
|
|
3725
3975
|
try {
|
|
@@ -3787,7 +4037,7 @@ async function buildPatchSections(services, project) {
|
|
|
3787
4037
|
}
|
|
3788
4038
|
async function runInit(opts) {
|
|
3789
4039
|
const written = [];
|
|
3790
|
-
const stat = await
|
|
4040
|
+
const stat = await fs7.stat(opts.scanPath).catch(() => null);
|
|
3791
4041
|
if (!stat || !stat.isDirectory()) {
|
|
3792
4042
|
console.error(`neat init: ${opts.scanPath} is not a directory`);
|
|
3793
4043
|
return { exitCode: 2, writtenFiles: written };
|
|
@@ -3796,13 +4046,13 @@ async function runInit(opts) {
|
|
|
3796
4046
|
printDiscoveryReport(opts, services);
|
|
3797
4047
|
const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
|
|
3798
4048
|
const patch = renderPatch(sections);
|
|
3799
|
-
const patchPath =
|
|
4049
|
+
const patchPath = path8.join(opts.scanPath, "neat.patch");
|
|
3800
4050
|
if (opts.dryRun) {
|
|
3801
|
-
await
|
|
4051
|
+
await fs7.writeFile(patchPath, patch, "utf8");
|
|
3802
4052
|
written.push(patchPath);
|
|
3803
4053
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
3804
|
-
const gitignorePath =
|
|
3805
|
-
const gitignoreExists = await
|
|
4054
|
+
const gitignorePath = path8.join(opts.scanPath, ".gitignore");
|
|
4055
|
+
const gitignoreExists = await fs7.stat(gitignorePath).then(() => true).catch(() => false);
|
|
3806
4056
|
const verb = gitignoreExists ? "append" : "create";
|
|
3807
4057
|
console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
|
|
3808
4058
|
console.log("rerun without --dry-run to register and snapshot.");
|
|
@@ -3813,9 +4063,9 @@ async function runInit(opts) {
|
|
|
3813
4063
|
const graph = getGraph(graphKey);
|
|
3814
4064
|
const projectPaths = pathsForProject(
|
|
3815
4065
|
graphKey,
|
|
3816
|
-
|
|
4066
|
+
path8.join(opts.scanPath, "neat-out")
|
|
3817
4067
|
);
|
|
3818
|
-
const errorsPath =
|
|
4068
|
+
const errorsPath = path8.join(path8.dirname(opts.outPath), path8.basename(projectPaths.errorsPath));
|
|
3819
4069
|
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
3820
4070
|
await saveGraphToDisk(graph, opts.outPath);
|
|
3821
4071
|
written.push(opts.outPath);
|
|
@@ -3894,7 +4144,7 @@ async function runInit(opts) {
|
|
|
3894
4144
|
console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
3895
4145
|
}
|
|
3896
4146
|
} else {
|
|
3897
|
-
await
|
|
4147
|
+
await fs7.writeFile(patchPath, patch, "utf8");
|
|
3898
4148
|
written.push(patchPath);
|
|
3899
4149
|
}
|
|
3900
4150
|
}
|
|
@@ -3934,9 +4184,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
3934
4184
|
};
|
|
3935
4185
|
function claudeConfigPath() {
|
|
3936
4186
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
3937
|
-
if (override && override.length > 0) return
|
|
4187
|
+
if (override && override.length > 0) return path8.resolve(override);
|
|
3938
4188
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
3939
|
-
return
|
|
4189
|
+
return path8.join(home, ".claude.json");
|
|
3940
4190
|
}
|
|
3941
4191
|
async function runSkill(opts) {
|
|
3942
4192
|
const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -3948,7 +4198,7 @@ async function runSkill(opts) {
|
|
|
3948
4198
|
const target = claudeConfigPath();
|
|
3949
4199
|
let existing = {};
|
|
3950
4200
|
try {
|
|
3951
|
-
existing = JSON.parse(await
|
|
4201
|
+
existing = JSON.parse(await fs7.readFile(target, "utf8"));
|
|
3952
4202
|
} catch (err) {
|
|
3953
4203
|
if (err.code !== "ENOENT") {
|
|
3954
4204
|
console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
|
|
@@ -3960,8 +4210,8 @@ async function runSkill(opts) {
|
|
|
3960
4210
|
...existing,
|
|
3961
4211
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
3962
4212
|
};
|
|
3963
|
-
await
|
|
3964
|
-
await
|
|
4213
|
+
await fs7.mkdir(path8.dirname(target), { recursive: true });
|
|
4214
|
+
await fs7.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
3965
4215
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
3966
4216
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
3967
4217
|
return { exitCode: 0 };
|
|
@@ -3999,12 +4249,12 @@ async function main() {
|
|
|
3999
4249
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
4000
4250
|
process.exit(2);
|
|
4001
4251
|
}
|
|
4002
|
-
const scanPath =
|
|
4252
|
+
const scanPath = path8.resolve(target);
|
|
4003
4253
|
const projectExplicit = parsed.project !== null;
|
|
4004
|
-
const projectName = projectExplicit ? project :
|
|
4254
|
+
const projectName = projectExplicit ? project : path8.basename(scanPath);
|
|
4005
4255
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
4006
|
-
const fallback = pathsForProject(projectKey,
|
|
4007
|
-
const outPath =
|
|
4256
|
+
const fallback = pathsForProject(projectKey, path8.join(scanPath, "neat-out")).snapshotPath;
|
|
4257
|
+
const outPath = path8.resolve(process.env.NEAT_OUT_PATH ?? fallback);
|
|
4008
4258
|
const result = await runInit({
|
|
4009
4259
|
scanPath,
|
|
4010
4260
|
outPath,
|
|
@@ -4025,21 +4275,21 @@ async function main() {
|
|
|
4025
4275
|
usage();
|
|
4026
4276
|
process.exit(2);
|
|
4027
4277
|
}
|
|
4028
|
-
const scanPath =
|
|
4029
|
-
const stat = await
|
|
4278
|
+
const scanPath = path8.resolve(target);
|
|
4279
|
+
const stat = await fs7.stat(scanPath).catch(() => null);
|
|
4030
4280
|
if (!stat || !stat.isDirectory()) {
|
|
4031
4281
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
4032
4282
|
process.exit(2);
|
|
4033
4283
|
}
|
|
4034
|
-
const projectPaths = pathsForProject(project,
|
|
4035
|
-
const outPath =
|
|
4036
|
-
const errorsPath =
|
|
4037
|
-
process.env.NEAT_ERRORS_PATH ??
|
|
4284
|
+
const projectPaths = pathsForProject(project, path8.join(scanPath, "neat-out"));
|
|
4285
|
+
const outPath = path8.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
|
|
4286
|
+
const errorsPath = path8.resolve(
|
|
4287
|
+
process.env.NEAT_ERRORS_PATH ?? path8.join(path8.dirname(outPath), path8.basename(projectPaths.errorsPath))
|
|
4038
4288
|
);
|
|
4039
|
-
const staleEventsPath =
|
|
4040
|
-
process.env.NEAT_STALE_EVENTS_PATH ??
|
|
4289
|
+
const staleEventsPath = path8.resolve(
|
|
4290
|
+
process.env.NEAT_STALE_EVENTS_PATH ?? path8.join(path8.dirname(outPath), path8.basename(projectPaths.staleEventsPath))
|
|
4041
4291
|
);
|
|
4042
|
-
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ?
|
|
4292
|
+
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path8.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
|
|
4043
4293
|
const handle = await startWatch(getGraph(project), {
|
|
4044
4294
|
scanPath,
|
|
4045
4295
|
outPath,
|
|
@@ -4185,11 +4435,11 @@ async function main() {
|
|
|
4185
4435
|
process.exit(1);
|
|
4186
4436
|
}
|
|
4187
4437
|
async function tryOrchestrator(cmd, parsed) {
|
|
4188
|
-
const scanPath =
|
|
4189
|
-
const stat = await
|
|
4438
|
+
const scanPath = path8.resolve(cmd);
|
|
4439
|
+
const stat = await fs7.stat(scanPath).catch(() => null);
|
|
4190
4440
|
if (!stat || !stat.isDirectory()) return null;
|
|
4191
4441
|
const projectExplicit = parsed.project !== null;
|
|
4192
|
-
const projectName = projectExplicit ? parsed.project :
|
|
4442
|
+
const projectName = projectExplicit ? parsed.project : path8.basename(scanPath);
|
|
4193
4443
|
const result = await runOrchestrator({
|
|
4194
4444
|
scanPath,
|
|
4195
4445
|
project: projectName,
|