@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/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-J5CEKCTR.js";
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 path9 from "path";
58
- import { promises as fs8, readFileSync } from "fs";
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: 3 \u2014 file-first call-site capture (ADR-089) + OTLP bearer auth (#410).";
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 neatCallsiteProcessorSource(ts) {
703
- const stackT = ts ? ": string" : "";
707
+ function neatCaptureSource(ts) {
704
708
  const spanT = ts ? ": any" : "";
705
709
  const strOpt = ts ? ": string | undefined" : "";
706
- return `function __neatPickUserFrame(stack${stackT}) {
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
- const frame = __neatPickUserFrame(new Error().stack)
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.setAttribute('code.filepath', frame.filepath)
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 = neatCallsiteProcessorSource(false);
751
- var CALLSITE_PROCESSOR_TS = neatCallsiteProcessorSource(true);
752
- function neatRegisterCallsiteSource(ts) {
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 && typeof delegate.addSpanProcessor === 'function') {
758
- delegate.addSpanProcessor(new NeatCallSiteSpanProcessor())
759
- }
760
- } catch {
761
- // capture is best-effort; span export is unaffected
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
- ${neatRegisterCallsiteSource(false)}
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
- ${neatRegisterCallsiteSource(false)}
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
- ${neatRegisterCallsiteSource(true)}
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) continue;
1294
- dependencyEdits.push({
1295
- file: manifestPath,
1296
- kind: "add",
1297
- name: sdk.name,
1298
- version: sdk.version
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) continue;
1304
- dependencyEdits.push({
1305
- file: manifestPath,
1306
- kind: "add",
1307
- name: inst.pkg,
1308
- version: inst.version
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) continue;
1756
- dependencyEdits.push({
1757
- file: manifestPath,
1758
- kind: "add",
1759
- name: sdk.name,
1760
- version: sdk.version
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) continue;
1766
- dependencyEdits.push({
1767
- file: manifestPath,
1768
- kind: "add",
1769
- name: inst.pkg,
1770
- version: inst.version
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 fs7 } from "fs";
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
- var LOCKFILE_PRIORITY = [
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, path7.join(opts.scanPath, "neat-out"));
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
- console.log(`skipping ${svc.dir}: React Native target; browser-OTel support lands in a future release.`);
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 = path7.dirname(new URL(import.meta.url).pathname);
2878
+ const here = path6.dirname(new URL(import.meta.url).pathname);
2629
2879
  const candidates = [
2630
- path7.join(here, "neatd.cjs"),
2631
- path7.join(here, "neatd.js")
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 = spawn3(process.execPath, [entry2, "start"], {
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 = spawn3(cmd, args, { detached: true, stdio: "ignore" });
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 fs7.stat(opts.scanPath).catch(() => null);
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 path8 from "path";
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(path10) {
3109
+ async get(path9) {
2860
3110
  let res;
2861
3111
  try {
2862
- res = await fetch(`${root}${path10}`, {
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 ${path10}: ${body}`,
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(path10, body) {
3130
+ async post(path9, body) {
2881
3131
  let res;
2882
3132
  try {
2883
- res = await fetch(`${root}${path10}`, {
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 ${path10}: ${text}`,
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 path10 = projectPath(
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(path10);
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 path10 = projectPath(
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(path10);
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 path10 = projectPath(
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(path10);
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 path10 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
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(path10);
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}${path8.sep}`)) {
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 : path9.dirname(fileURLToPath(import.meta.url));
3969
+ const here = typeof __dirname !== "undefined" ? __dirname : path8.dirname(fileURLToPath(import.meta.url));
3720
3970
  const candidates = [
3721
- path9.resolve(here, "../package.json"),
3722
- path9.resolve(here, "../../package.json")
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 fs8.stat(opts.scanPath).catch(() => null);
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 = path9.join(opts.scanPath, "neat.patch");
4049
+ const patchPath = path8.join(opts.scanPath, "neat.patch");
3800
4050
  if (opts.dryRun) {
3801
- await fs8.writeFile(patchPath, patch, "utf8");
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 = path9.join(opts.scanPath, ".gitignore");
3805
- const gitignoreExists = await fs8.stat(gitignorePath).then(() => true).catch(() => false);
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
- path9.join(opts.scanPath, "neat-out")
4066
+ path8.join(opts.scanPath, "neat-out")
3817
4067
  );
3818
- const errorsPath = path9.join(path9.dirname(opts.outPath), path9.basename(projectPaths.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 fs8.writeFile(patchPath, patch, "utf8");
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 path9.resolve(override);
4187
+ if (override && override.length > 0) return path8.resolve(override);
3938
4188
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
3939
- return path9.join(home, ".claude.json");
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 fs8.readFile(target, "utf8"));
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 fs8.mkdir(path9.dirname(target), { recursive: true });
3964
- await fs8.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
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 = path9.resolve(target);
4252
+ const scanPath = path8.resolve(target);
4003
4253
  const projectExplicit = parsed.project !== null;
4004
- const projectName = projectExplicit ? project : path9.basename(scanPath);
4254
+ const projectName = projectExplicit ? project : path8.basename(scanPath);
4005
4255
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
4006
- const fallback = pathsForProject(projectKey, path9.join(scanPath, "neat-out")).snapshotPath;
4007
- const outPath = path9.resolve(process.env.NEAT_OUT_PATH ?? fallback);
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 = path9.resolve(target);
4029
- const stat = await fs8.stat(scanPath).catch(() => null);
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, path9.join(scanPath, "neat-out"));
4035
- const outPath = path9.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
4036
- const errorsPath = path9.resolve(
4037
- process.env.NEAT_ERRORS_PATH ?? path9.join(path9.dirname(outPath), path9.basename(projectPaths.errorsPath))
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 = path9.resolve(
4040
- process.env.NEAT_STALE_EVENTS_PATH ?? path9.join(path9.dirname(outPath), path9.basename(projectPaths.staleEventsPath))
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 ? path9.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
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 = path9.resolve(cmd);
4189
- const stat = await fs8.stat(scanPath).catch(() => null);
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 : path9.basename(scanPath);
4442
+ const projectName = projectExplicit ? parsed.project : path8.basename(scanPath);
4193
4443
  const result = await runOrchestrator({
4194
4444
  scanPath,
4195
4445
  project: projectName,