@neat.is/core 0.7.8 → 0.7.10
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-LN75Z624.js → chunk-6T7ZHODF.js} +554 -65
- package/dist/chunk-6T7ZHODF.js.map +1 -0
- package/dist/{chunk-BIY46Q6U.js → chunk-BC53SCT7.js} +36 -2
- package/dist/chunk-BC53SCT7.js.map +1 -0
- package/dist/{chunk-OVRVDSUO.js → chunk-D4OX6MUX.js} +58 -4
- package/dist/chunk-D4OX6MUX.js.map +1 -0
- package/dist/{chunk-4GCV46AP.js → chunk-FCO5Z3RW.js} +2 -2
- package/dist/{chunk-Y43UCVZS.js → chunk-UUYCTH2E.js} +24 -8
- package/dist/chunk-UUYCTH2E.js.map +1 -0
- package/dist/cli.cjs +1392 -314
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +704 -176
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +645 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +645 -77
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-DPSAGRUU.js → otel-grpc-CO47JRIN.js} +3 -3
- package/dist/server.cjs +618 -74
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +4 -4
- package/package.json +2 -2
- package/dist/chunk-BIY46Q6U.js.map +0 -1
- package/dist/chunk-LN75Z624.js.map +0 -1
- package/dist/chunk-OVRVDSUO.js.map +0 -1
- package/dist/chunk-Y43UCVZS.js.map +0 -1
- /package/dist/{chunk-4GCV46AP.js.map → chunk-FCO5Z3RW.js.map} +0 -0
- /package/dist/{otel-grpc-DPSAGRUU.js.map → otel-grpc-CO47JRIN.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
resolveHost,
|
|
7
7
|
resolveNeatVersion,
|
|
8
8
|
writeDaemonRecord
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-D4OX6MUX.js";
|
|
10
10
|
import {
|
|
11
11
|
buildSearchIndex
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-BC53SCT7.js";
|
|
13
13
|
import {
|
|
14
14
|
DEFAULT_PROJECT,
|
|
15
15
|
PolicyViolationsLog,
|
|
@@ -74,10 +74,10 @@ import {
|
|
|
74
74
|
startStalenessLoop,
|
|
75
75
|
upsertConnectorEntry,
|
|
76
76
|
validateConnectorEntry
|
|
77
|
-
} from "./chunk-
|
|
77
|
+
} from "./chunk-6T7ZHODF.js";
|
|
78
78
|
import {
|
|
79
79
|
startOtelGrpcReceiver
|
|
80
|
-
} from "./chunk-
|
|
80
|
+
} from "./chunk-FCO5Z3RW.js";
|
|
81
81
|
import {
|
|
82
82
|
__dirname,
|
|
83
83
|
__require,
|
|
@@ -85,12 +85,12 @@ import {
|
|
|
85
85
|
buildOtelReceiver,
|
|
86
86
|
listenSteppingOtlp,
|
|
87
87
|
readAuthEnv
|
|
88
|
-
} from "./chunk-
|
|
88
|
+
} from "./chunk-UUYCTH2E.js";
|
|
89
89
|
|
|
90
90
|
// src/cli.ts
|
|
91
|
-
import
|
|
91
|
+
import path15 from "path";
|
|
92
92
|
import os4 from "os";
|
|
93
|
-
import { promises as
|
|
93
|
+
import { promises as fs13 } from "fs";
|
|
94
94
|
|
|
95
95
|
// src/banner.ts
|
|
96
96
|
import path from "path";
|
|
@@ -487,6 +487,9 @@ async function startWatch(graph, opts) {
|
|
|
487
487
|
project: projectName,
|
|
488
488
|
graph,
|
|
489
489
|
projectDir: opts.scanPath,
|
|
490
|
+
// Incident ledger for an incident-emitting connector (ADR-185), same path
|
|
491
|
+
// `neat watch`'s own error-span writer already uses.
|
|
492
|
+
errorsPath: opts.errorsPath,
|
|
490
493
|
...opts.neatHome ? { home: opts.neatHome } : {},
|
|
491
494
|
onSkip: (skipped, reason) => console.warn(
|
|
492
495
|
`neat watch: connector "${skipped.id}" (${skipped.provider}) skipped for project "${projectName}" \u2014 ${reason}`
|
|
@@ -854,7 +857,8 @@ var OTEL_ENDPOINT_RESOLVER_CJS = `;(function () {
|
|
|
854
857
|
try {
|
|
855
858
|
const __rec = JSON.parse(__neatFs.readFileSync(__neatPath.join(__neatDir, 'neat-out', 'daemon.json'), 'utf8'))
|
|
856
859
|
if (__rec && __rec.ports && typeof __rec.ports.otlp === 'number') {
|
|
857
|
-
|
|
860
|
+
const __neatProj = (typeof __rec.project === 'string' && __rec.project) || '__PROJECT__'
|
|
861
|
+
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://localhost:' + __rec.ports.otlp + '/projects/' + __neatProj + '/v1/traces'
|
|
858
862
|
break
|
|
859
863
|
}
|
|
860
864
|
} catch (_e) {}
|
|
@@ -863,7 +867,7 @@ var OTEL_ENDPOINT_RESOLVER_CJS = `;(function () {
|
|
|
863
867
|
__neatDir = __parent
|
|
864
868
|
}
|
|
865
869
|
} catch (_e) {}
|
|
866
|
-
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/v1/traces'
|
|
870
|
+
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
|
|
867
871
|
})()`;
|
|
868
872
|
var OTEL_ESM_NODE_IMPORTS = "import { readFileSync as __neatReadFileSync } from 'node:fs'\nimport { join as __neatJoin, dirname as __neatDirname } from 'node:path'";
|
|
869
873
|
var OTEL_ENDPOINT_RESOLVER_ESM = `;(function () {
|
|
@@ -874,7 +878,8 @@ var OTEL_ENDPOINT_RESOLVER_ESM = `;(function () {
|
|
|
874
878
|
try {
|
|
875
879
|
const __rec = JSON.parse(__neatReadFileSync(__neatJoin(__neatDir, 'neat-out', 'daemon.json'), 'utf8'))
|
|
876
880
|
if (__rec && __rec.ports && typeof __rec.ports.otlp === 'number') {
|
|
877
|
-
|
|
881
|
+
const __neatProj = (typeof __rec.project === 'string' && __rec.project) || '__PROJECT__'
|
|
882
|
+
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://localhost:' + __rec.ports.otlp + '/projects/' + __neatProj + '/v1/traces'
|
|
878
883
|
break
|
|
879
884
|
}
|
|
880
885
|
} catch (_e) {}
|
|
@@ -883,7 +888,7 @@ var OTEL_ENDPOINT_RESOLVER_ESM = `;(function () {
|
|
|
883
888
|
__neatDir = __parent
|
|
884
889
|
}
|
|
885
890
|
} catch (_e) {}
|
|
886
|
-
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/v1/traces'
|
|
891
|
+
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
|
|
887
892
|
})()`;
|
|
888
893
|
function neatCaptureSource(ts) {
|
|
889
894
|
const spanT = ts ? ": any" : "";
|
|
@@ -1285,14 +1290,15 @@ ${registrations.join("\n")}
|
|
|
1285
1290
|
`;
|
|
1286
1291
|
return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName).replace(/__INSTRUMENTATION_BLOCK__\n?/g, block);
|
|
1287
1292
|
}
|
|
1288
|
-
function renderEnvNeat(serviceName,
|
|
1293
|
+
function renderEnvNeat(serviceName, projectName) {
|
|
1289
1294
|
return [
|
|
1290
1295
|
"# Generated by `neat init --apply` (ADR-069).",
|
|
1291
1296
|
`OTEL_SERVICE_NAME=${serviceName}`,
|
|
1292
1297
|
"# Advisory only \u2014 the generated otel-init resolves the live endpoint from",
|
|
1293
|
-
"# <project>/neat-out/daemon.json (ports.otlp) at boot (ADR-096).
|
|
1294
|
-
"# canonical default the daemon takes when its first-choice OTLP
|
|
1295
|
-
"
|
|
1298
|
+
"# <project>/neat-out/daemon.json (ports.otlp + project) at boot (ADR-096).",
|
|
1299
|
+
"# This is the canonical default the daemon takes when its first-choice OTLP",
|
|
1300
|
+
"# port is free; the project scope routes the span without service.name guessing (#879).",
|
|
1301
|
+
`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/projects/${projectName}/v1/traces`,
|
|
1296
1302
|
"OTEL_EXPORTER_OTLP_PROTOCOL=http/json",
|
|
1297
1303
|
"# Set NEAT_OTEL_TOKEN to the daemon's OTLP secret to authenticate exported spans (#410).",
|
|
1298
1304
|
"# NEAT_OTEL_TOKEN=",
|
|
@@ -1358,7 +1364,7 @@ var NEXT_INSTRUMENTATION_EDGE_TS = `${NEXT_INSTRUMENTATION_EDGE_HEADER}
|
|
|
1358
1364
|
import { registerOTel } from '@vercel/otel'
|
|
1359
1365
|
|
|
1360
1366
|
process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
|
|
1361
|
-
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/v1/traces'
|
|
1367
|
+
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
|
|
1362
1368
|
${OTEL_OTLP_PROTOCOL_JS}
|
|
1363
1369
|
${OTEL_OTLP_HEADERS_JS}
|
|
1364
1370
|
|
|
@@ -3104,13 +3110,516 @@ async function apply3(installPlan) {
|
|
|
3104
3110
|
await fs6.writeFile(generated.file, generated.contents, "utf8");
|
|
3105
3111
|
writtenFiles.push(generated.file);
|
|
3106
3112
|
}
|
|
3107
|
-
|
|
3113
|
+
const wroteManifest = writtenFiles.some((f) => f.split(/[\\/]/).pop() === "go.mod");
|
|
3114
|
+
return {
|
|
3115
|
+
serviceDir: installPlan.serviceDir,
|
|
3116
|
+
outcome: writtenFiles.length ? "instrumented" : "already-instrumented",
|
|
3117
|
+
writtenFiles,
|
|
3118
|
+
...wroteManifest ? { followUpInstall: "go mod download" } : {}
|
|
3119
|
+
};
|
|
3108
3120
|
}
|
|
3109
3121
|
var goInstaller = { name: "go", detect: detect3, plan: plan3, apply: apply3 };
|
|
3110
3122
|
|
|
3123
|
+
// src/installers/ruby.ts
|
|
3124
|
+
import { promises as fs7 } from "fs";
|
|
3125
|
+
import path8 from "path";
|
|
3126
|
+
var RUBY_MARKERS = [
|
|
3127
|
+
"Gemfile",
|
|
3128
|
+
"Gemfile.lock"
|
|
3129
|
+
];
|
|
3130
|
+
var RUBY_GEMS = [
|
|
3131
|
+
{ name: "opentelemetry-sdk", version: "~> 1.5" },
|
|
3132
|
+
{ name: "opentelemetry-exporter-otlp", version: "~> 0.29" },
|
|
3133
|
+
{ name: "opentelemetry-instrumentation-all", version: "~> 0.62" }
|
|
3134
|
+
];
|
|
3135
|
+
var NEAT_OTEL_STAMP2 = "neat-otel-init v1";
|
|
3136
|
+
var INITIALIZER_REL = path8.join("config", "initializers", "neat_otel.rb");
|
|
3137
|
+
function neatOtelRb(opts = {}) {
|
|
3138
|
+
const service = opts.project ?? "ruby-service";
|
|
3139
|
+
const endpoint = opts.project ? `http://localhost:4318/projects/${opts.project}/v1/traces` : "http://localhost:4318/v1/traces";
|
|
3140
|
+
return `# ${NEAT_OTEL_STAMP2} \u2014 generated by NEAT. Safe to re-generate; do not edit.
|
|
3141
|
+
# Rails auto-loads this at boot (config/initializers/*). It points the
|
|
3142
|
+
# OpenTelemetry SDK at your NEAT daemon, enables the Ruby auto-instrumentation
|
|
3143
|
+
# set, and installs a span processor that stamps code.file.path /
|
|
3144
|
+
# code.line.number / code.function.name on the CLIENT/PRODUCER spans your app
|
|
3145
|
+
# issues, so NEAT fuses each runtime span onto the source file that made the
|
|
3146
|
+
# call (docs/contracts/file-awareness.md). Absolute paths are emitted here;
|
|
3147
|
+
# ingest anchors them against the service root. If the OpenTelemetry gems are
|
|
3148
|
+
# not installed this file degrades to a no-op rather than breaking boot.
|
|
3149
|
+
|
|
3150
|
+
begin
|
|
3151
|
+
require 'opentelemetry/sdk'
|
|
3152
|
+
require 'opentelemetry/exporter/otlp'
|
|
3153
|
+
require 'opentelemetry/instrumentation/all'
|
|
3154
|
+
_neat_otel_loaded = true
|
|
3155
|
+
rescue LoadError
|
|
3156
|
+
_neat_otel_loaded = false
|
|
3157
|
+
end
|
|
3158
|
+
|
|
3159
|
+
if _neat_otel_loaded && ENV['NEAT_CALLSITE_DISABLED'] != '1'
|
|
3160
|
+
# Walk the Ruby call stack to the first application frame and stamp the stable
|
|
3161
|
+
# OTel source attributes on CLIENT/PRODUCER spans. SERVER spans are created
|
|
3162
|
+
# before the handler runs, so they stay route/service-grained, honestly.
|
|
3163
|
+
class NeatCallSiteSpanProcessor
|
|
3164
|
+
def initialize(root)
|
|
3165
|
+
@root = root.to_s.end_with?(File::SEPARATOR) ? root.to_s : root.to_s + File::SEPARATOR
|
|
3166
|
+
end
|
|
3167
|
+
|
|
3168
|
+
def on_start(span, _parent_context)
|
|
3169
|
+
kind = span.kind
|
|
3170
|
+
return unless kind == OpenTelemetry::Trace::SpanKind::CLIENT ||
|
|
3171
|
+
kind == OpenTelemetry::Trace::SpanKind::PRODUCER
|
|
3172
|
+
caller_locations(1).each do |loc|
|
|
3173
|
+
file = loc.absolute_path || loc.path
|
|
3174
|
+
next if file.nil?
|
|
3175
|
+
next unless file.start_with?(@root)
|
|
3176
|
+
next if file.include?('/vendor/') || file.include?('/.bundle/')
|
|
3177
|
+
next if file.end_with?('neat_otel.rb')
|
|
3178
|
+
span.set_attribute('code.file.path', file)
|
|
3179
|
+
span.set_attribute('code.line.number', loc.lineno)
|
|
3180
|
+
span.set_attribute('code.function.name', loc.label.to_s)
|
|
3181
|
+
break
|
|
3182
|
+
end
|
|
3183
|
+
rescue StandardError
|
|
3184
|
+
# never break the host application
|
|
3185
|
+
end
|
|
3186
|
+
|
|
3187
|
+
def on_finish(_span); end
|
|
3188
|
+
|
|
3189
|
+
def force_flush(timeout: nil)
|
|
3190
|
+
OpenTelemetry::SDK::Trace::Export::SUCCESS
|
|
3191
|
+
end
|
|
3192
|
+
|
|
3193
|
+
def shutdown(timeout: nil)
|
|
3194
|
+
OpenTelemetry::SDK::Trace::Export::SUCCESS
|
|
3195
|
+
end
|
|
3196
|
+
end
|
|
3197
|
+
|
|
3198
|
+
_neat_root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
|
|
3199
|
+
_neat_service = ENV.fetch('OTEL_SERVICE_NAME', '${service}')
|
|
3200
|
+
_neat_endpoint = ENV.fetch('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', '${endpoint}')
|
|
3201
|
+
|
|
3202
|
+
OpenTelemetry::SDK.configure do |c|
|
|
3203
|
+
c.service_name = _neat_service
|
|
3204
|
+
c.use_all
|
|
3205
|
+
c.add_span_processor(
|
|
3206
|
+
OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
|
|
3207
|
+
OpenTelemetry::Exporter::OTLP::Exporter.new(endpoint: _neat_endpoint)
|
|
3208
|
+
)
|
|
3209
|
+
)
|
|
3210
|
+
c.add_span_processor(NeatCallSiteSpanProcessor.new(_neat_root))
|
|
3211
|
+
end
|
|
3212
|
+
end
|
|
3213
|
+
`;
|
|
3214
|
+
}
|
|
3215
|
+
async function exists4(p) {
|
|
3216
|
+
try {
|
|
3217
|
+
await fs7.stat(p);
|
|
3218
|
+
return true;
|
|
3219
|
+
} catch {
|
|
3220
|
+
return false;
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
async function detect4(serviceDir) {
|
|
3224
|
+
for (const marker of RUBY_MARKERS) {
|
|
3225
|
+
if (await exists4(path8.join(serviceDir, marker))) return true;
|
|
3226
|
+
}
|
|
3227
|
+
return false;
|
|
3228
|
+
}
|
|
3229
|
+
async function isRailsApp(serviceDir, gemfile) {
|
|
3230
|
+
if (gemfile && /^\s*gem\s+['"]rails['"]/m.test(gemfile)) return true;
|
|
3231
|
+
for (const marker of ["config/application.rb", "config/environment.rb", "bin/rails"]) {
|
|
3232
|
+
if (await exists4(path8.join(serviceDir, marker))) return true;
|
|
3233
|
+
}
|
|
3234
|
+
return false;
|
|
3235
|
+
}
|
|
3236
|
+
function gemPresent(gemfile, name) {
|
|
3237
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3238
|
+
return new RegExp(`^\\s*gem\\s+['"]${escaped}['"]`, "m").test(gemfile);
|
|
3239
|
+
}
|
|
3240
|
+
async function readGemfile(serviceDir) {
|
|
3241
|
+
const file = path8.join(serviceDir, "Gemfile");
|
|
3242
|
+
if (!await exists4(file)) return null;
|
|
3243
|
+
return { file, body: await fs7.readFile(file, "utf8") };
|
|
3244
|
+
}
|
|
3245
|
+
async function plan4(serviceDir, opts) {
|
|
3246
|
+
const empty = {
|
|
3247
|
+
language: "ruby",
|
|
3248
|
+
serviceDir,
|
|
3249
|
+
dependencyEdits: [],
|
|
3250
|
+
entrypointEdits: [],
|
|
3251
|
+
envEdits: []
|
|
3252
|
+
};
|
|
3253
|
+
const gemfile = await readGemfile(serviceDir);
|
|
3254
|
+
const dependencyEdits = [];
|
|
3255
|
+
if (gemfile) {
|
|
3256
|
+
for (const gem of RUBY_GEMS) {
|
|
3257
|
+
if (!gemPresent(gemfile.body, gem.name)) {
|
|
3258
|
+
dependencyEdits.push({ file: gemfile.file, kind: "add", name: gem.name, version: gem.version });
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
const rails = await isRailsApp(serviceDir, gemfile?.body ?? null);
|
|
3263
|
+
const initializer = path8.join(serviceDir, INITIALIZER_REL);
|
|
3264
|
+
const generatedFiles = [];
|
|
3265
|
+
if (rails && !await exists4(initializer)) {
|
|
3266
|
+
generatedFiles.push({
|
|
3267
|
+
file: initializer,
|
|
3268
|
+
contents: neatOtelRb({ project: opts?.project }),
|
|
3269
|
+
skipIfExists: true
|
|
3270
|
+
});
|
|
3271
|
+
}
|
|
3272
|
+
if (dependencyEdits.length === 0 && generatedFiles.length === 0) {
|
|
3273
|
+
return empty;
|
|
3274
|
+
}
|
|
3275
|
+
const envEdits = [
|
|
3276
|
+
{ file: null, key: "OTEL_EXPORTER_OTLP_ENDPOINT", value: "http://localhost:4318" }
|
|
3277
|
+
];
|
|
3278
|
+
return {
|
|
3279
|
+
language: "ruby",
|
|
3280
|
+
serviceDir,
|
|
3281
|
+
dependencyEdits,
|
|
3282
|
+
entrypointEdits: [],
|
|
3283
|
+
envEdits,
|
|
3284
|
+
...generatedFiles.length > 0 ? { generatedFiles } : {}
|
|
3285
|
+
};
|
|
3286
|
+
}
|
|
3287
|
+
async function writeFileAtomic2(file, contents) {
|
|
3288
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
3289
|
+
await fs7.writeFile(tmp, contents, "utf8");
|
|
3290
|
+
await fs7.rename(tmp, file);
|
|
3291
|
+
}
|
|
3292
|
+
async function applyGemfile(file, edits, original) {
|
|
3293
|
+
const lines = edits.filter((e) => e.kind === "add").map((e) => `gem '${e.name}', '${e.version}'`);
|
|
3294
|
+
const banner = `
|
|
3295
|
+
# ${NEAT_OTEL_STAMP2} \u2014 OpenTelemetry gems added by NEAT
|
|
3296
|
+
`;
|
|
3297
|
+
const trailing = original.endsWith("\n") ? "" : "\n";
|
|
3298
|
+
await writeFileAtomic2(file, `${original}${trailing}${banner}${lines.join("\n")}
|
|
3299
|
+
`);
|
|
3300
|
+
}
|
|
3301
|
+
async function rollback3(serviceDir, language, originals, created) {
|
|
3302
|
+
const restored = [];
|
|
3303
|
+
for (const [file, raw] of originals.entries()) {
|
|
3304
|
+
try {
|
|
3305
|
+
await fs7.writeFile(file, raw, "utf8");
|
|
3306
|
+
restored.push(file);
|
|
3307
|
+
} catch {
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
const removed = [];
|
|
3311
|
+
for (const file of created) {
|
|
3312
|
+
try {
|
|
3313
|
+
await fs7.rm(file, { force: true });
|
|
3314
|
+
removed.push(file);
|
|
3315
|
+
} catch {
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
const body = [
|
|
3319
|
+
"# neat-rollback.patch",
|
|
3320
|
+
"",
|
|
3321
|
+
`# Generated after a partial apply failure in the ${language} installer.`,
|
|
3322
|
+
"# Files listed below were restored to their pre-apply contents.",
|
|
3323
|
+
"",
|
|
3324
|
+
...restored.map((f) => `restored: ${f}`),
|
|
3325
|
+
...removed.map((f) => `removed: ${f}`),
|
|
3326
|
+
""
|
|
3327
|
+
];
|
|
3328
|
+
await fs7.writeFile(path8.join(serviceDir, "neat-rollback.patch"), body.join("\n"), "utf8");
|
|
3329
|
+
}
|
|
3330
|
+
async function apply4(installPlan) {
|
|
3331
|
+
const { serviceDir } = installPlan;
|
|
3332
|
+
const generatedFiles = installPlan.generatedFiles ?? [];
|
|
3333
|
+
const manifests = new Set(installPlan.dependencyEdits.map((e) => e.file));
|
|
3334
|
+
if (manifests.size === 0 && generatedFiles.length === 0) {
|
|
3335
|
+
return { serviceDir, outcome: "already-instrumented", writtenFiles: [] };
|
|
3336
|
+
}
|
|
3337
|
+
const originals = /* @__PURE__ */ new Map();
|
|
3338
|
+
for (const file of manifests) {
|
|
3339
|
+
try {
|
|
3340
|
+
originals.set(file, await fs7.readFile(file, "utf8"));
|
|
3341
|
+
} catch {
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
const writtenFiles = [];
|
|
3345
|
+
const created = [];
|
|
3346
|
+
try {
|
|
3347
|
+
for (const gf of generatedFiles) {
|
|
3348
|
+
if (await exists4(gf.file)) continue;
|
|
3349
|
+
await fs7.mkdir(path8.dirname(gf.file), { recursive: true });
|
|
3350
|
+
await writeFileAtomic2(gf.file, gf.contents);
|
|
3351
|
+
writtenFiles.push(gf.file);
|
|
3352
|
+
created.push(gf.file);
|
|
3353
|
+
}
|
|
3354
|
+
for (const file of manifests) {
|
|
3355
|
+
const raw = originals.get(file);
|
|
3356
|
+
if (raw === void 0) throw new Error(`ruby installer: cannot read ${file} during apply`);
|
|
3357
|
+
const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
|
|
3358
|
+
if (edits.length > 0) {
|
|
3359
|
+
await applyGemfile(file, edits, raw);
|
|
3360
|
+
writtenFiles.push(file);
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
} catch (err) {
|
|
3364
|
+
await rollback3(serviceDir, installPlan.language, originals, created);
|
|
3365
|
+
throw err;
|
|
3366
|
+
}
|
|
3367
|
+
const wroteManifest = writtenFiles.some((f) => path8.basename(f) === "Gemfile");
|
|
3368
|
+
return {
|
|
3369
|
+
serviceDir,
|
|
3370
|
+
outcome: writtenFiles.length > 0 ? "instrumented" : "already-instrumented",
|
|
3371
|
+
writtenFiles,
|
|
3372
|
+
...wroteManifest ? { followUpInstall: "bundle install" } : {}
|
|
3373
|
+
};
|
|
3374
|
+
}
|
|
3375
|
+
var rubyInstaller = { name: "ruby", detect: detect4, plan: plan4, apply: apply4 };
|
|
3376
|
+
|
|
3377
|
+
// src/installers/php.ts
|
|
3378
|
+
import { promises as fs8 } from "fs";
|
|
3379
|
+
import path9 from "path";
|
|
3380
|
+
var PHP_MARKERS = [
|
|
3381
|
+
"composer.json",
|
|
3382
|
+
"composer.lock"
|
|
3383
|
+
];
|
|
3384
|
+
var NEAT_OTEL_FILENAME2 = "neat_otel.php";
|
|
3385
|
+
var NEAT_OTEL_STAMP3 = "neat-otel-init v1";
|
|
3386
|
+
var LARAVEL_PACKAGE = "open-telemetry/opentelemetry-auto-laravel";
|
|
3387
|
+
var PHP_PACKAGES = [
|
|
3388
|
+
{ name: "open-telemetry/sdk", version: "^1.0" },
|
|
3389
|
+
{ name: "open-telemetry/exporter-otlp", version: "^1.0" },
|
|
3390
|
+
{ name: "php-http/guzzle7-adapter", version: "^1.0" },
|
|
3391
|
+
{ name: LARAVEL_PACKAGE, version: "^0.1" }
|
|
3392
|
+
];
|
|
3393
|
+
var PHP_PECL_CAVEAT = "PHP auto-instrumentation requires the `opentelemetry` PECL extension (`pecl install opentelemetry`, then `extension=opentelemetry.so` in php.ini). NEAT cannot install a PECL extension via composer \u2014 until it is loaded no spans are produced. See neat_otel.php and ADR-186.";
|
|
3394
|
+
function neatOtelPhp(opts = {}) {
|
|
3395
|
+
const service = opts.project ?? "php-service";
|
|
3396
|
+
const endpoint = opts.project ? `http://localhost:4318/projects/${opts.project}/v1/traces` : "http://localhost:4318/v1/traces";
|
|
3397
|
+
return `<?php
|
|
3398
|
+
// ${NEAT_OTEL_STAMP3} \u2014 generated by NEAT. Safe to re-generate; do not edit.
|
|
3399
|
+
//
|
|
3400
|
+
// REQUIRED SYSTEM STEP \u2014 NEAT CANNOT DO THIS FOR YOU:
|
|
3401
|
+
// PHP OpenTelemetry auto-instrumentation needs the \`opentelemetry\` PECL
|
|
3402
|
+
// extension, a system-level install composer cannot provide:
|
|
3403
|
+
// pecl install opentelemetry
|
|
3404
|
+
// then enable it in your php.ini:
|
|
3405
|
+
// extension=opentelemetry.so
|
|
3406
|
+
// Verify with \`php -m | grep opentelemetry\`. Until the extension is loaded
|
|
3407
|
+
// the Laravel auto-instrumentation hooks never fire and no spans are emitted.
|
|
3408
|
+
//
|
|
3409
|
+
// Wire this file so it runs before the framework boots \u2014 either set
|
|
3410
|
+
// auto_prepend_file = /absolute/path/to/neat_otel.php
|
|
3411
|
+
// in php.ini / .user.ini, or require it at the very top of public/index.php and
|
|
3412
|
+
// artisan. It points the exporter at your NEAT daemon and turns on the SDK
|
|
3413
|
+
// autoloader; the auto-laravel instrumentation then produces route, DB, cache,
|
|
3414
|
+
// and queue spans that fuse onto your extracted routes and Eloquent tables.
|
|
3415
|
+
//
|
|
3416
|
+
// FILE-GRAIN (code.file.path call-site attribution) is a documented follow-up
|
|
3417
|
+
// for PHP \u2014 see ADR-186. Route, table, and service grain land now.
|
|
3418
|
+
|
|
3419
|
+
declare(strict_types=1);
|
|
3420
|
+
|
|
3421
|
+
// Degrade to a no-op when the extension isn't present, so a bare app still
|
|
3422
|
+
// boots (never break the host application).
|
|
3423
|
+
if (!extension_loaded('opentelemetry')) {
|
|
3424
|
+
return;
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3427
|
+
// Point the exporter at NEAT unless the operator already set these. The
|
|
3428
|
+
// endpoint is NEAT's project-scoped traces path (ADR-183).
|
|
3429
|
+
$neat_defaults = [
|
|
3430
|
+
'OTEL_PHP_AUTOLOAD_ENABLED' => 'true',
|
|
3431
|
+
'OTEL_SERVICE_NAME' => '${service}',
|
|
3432
|
+
'OTEL_TRACES_EXPORTER' => 'otlp',
|
|
3433
|
+
'OTEL_EXPORTER_OTLP_PROTOCOL' => 'http/json',
|
|
3434
|
+
'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT' => '${endpoint}',
|
|
3435
|
+
'OTEL_PROPAGATORS' => 'baggage,tracecontext',
|
|
3436
|
+
];
|
|
3437
|
+
foreach ($neat_defaults as $neat_key => $neat_value) {
|
|
3438
|
+
if (getenv($neat_key) === false && !isset($_SERVER[$neat_key]) && !isset($_ENV[$neat_key])) {
|
|
3439
|
+
putenv($neat_key . '=' . $neat_value);
|
|
3440
|
+
$_SERVER[$neat_key] = $neat_value;
|
|
3441
|
+
$_ENV[$neat_key] = $neat_value;
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
`;
|
|
3445
|
+
}
|
|
3446
|
+
async function exists5(p) {
|
|
3447
|
+
try {
|
|
3448
|
+
await fs8.stat(p);
|
|
3449
|
+
return true;
|
|
3450
|
+
} catch {
|
|
3451
|
+
return false;
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
async function detect5(serviceDir) {
|
|
3455
|
+
for (const marker of PHP_MARKERS) {
|
|
3456
|
+
if (await exists5(path9.join(serviceDir, marker))) return true;
|
|
3457
|
+
}
|
|
3458
|
+
return false;
|
|
3459
|
+
}
|
|
3460
|
+
function readComposerObject(body) {
|
|
3461
|
+
let parsed = null;
|
|
3462
|
+
try {
|
|
3463
|
+
parsed = JSON.parse(body);
|
|
3464
|
+
} catch {
|
|
3465
|
+
parsed = null;
|
|
3466
|
+
}
|
|
3467
|
+
const obj = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
3468
|
+
const require2 = obj.require && typeof obj.require === "object" && !Array.isArray(obj.require) ? obj.require : {};
|
|
3469
|
+
const requireDev = obj["require-dev"] && typeof obj["require-dev"] === "object" && !Array.isArray(obj["require-dev"]) ? obj["require-dev"] : {};
|
|
3470
|
+
return { require: require2, requireDev };
|
|
3471
|
+
}
|
|
3472
|
+
async function plan5(serviceDir, opts) {
|
|
3473
|
+
const empty = {
|
|
3474
|
+
language: "php",
|
|
3475
|
+
serviceDir,
|
|
3476
|
+
dependencyEdits: [],
|
|
3477
|
+
entrypointEdits: [],
|
|
3478
|
+
envEdits: []
|
|
3479
|
+
};
|
|
3480
|
+
const composerPath = path9.join(serviceDir, "composer.json");
|
|
3481
|
+
const hasComposer = await exists5(composerPath);
|
|
3482
|
+
const dependencyEdits = [];
|
|
3483
|
+
if (hasComposer) {
|
|
3484
|
+
const body = await fs8.readFile(composerPath, "utf8");
|
|
3485
|
+
const { require: require2, requireDev } = readComposerObject(body);
|
|
3486
|
+
const laravel = "laravel/framework" in require2 || "laravel/framework" in requireDev || await exists5(path9.join(serviceDir, "artisan"));
|
|
3487
|
+
const wanted = laravel ? PHP_PACKAGES : PHP_PACKAGES.filter((p) => p.name !== LARAVEL_PACKAGE);
|
|
3488
|
+
for (const pkg of wanted) {
|
|
3489
|
+
if (!(pkg.name in require2)) {
|
|
3490
|
+
dependencyEdits.push({ file: composerPath, kind: "add", name: pkg.name, version: pkg.version });
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
const bootstrap = path9.join(serviceDir, NEAT_OTEL_FILENAME2);
|
|
3495
|
+
const generatedFiles = [];
|
|
3496
|
+
if (hasComposer && !await exists5(bootstrap)) {
|
|
3497
|
+
generatedFiles.push({ file: bootstrap, contents: neatOtelPhp({ project: opts?.project }), skipIfExists: true });
|
|
3498
|
+
}
|
|
3499
|
+
if (dependencyEdits.length === 0 && generatedFiles.length === 0) {
|
|
3500
|
+
return empty;
|
|
3501
|
+
}
|
|
3502
|
+
const envEdits = [
|
|
3503
|
+
{ file: null, key: "OTEL_PHP_AUTOLOAD_ENABLED", value: "true" },
|
|
3504
|
+
{ file: null, key: "OTEL_EXPORTER_OTLP_ENDPOINT", value: "http://localhost:4318" }
|
|
3505
|
+
];
|
|
3506
|
+
return {
|
|
3507
|
+
language: "php",
|
|
3508
|
+
serviceDir,
|
|
3509
|
+
dependencyEdits,
|
|
3510
|
+
entrypointEdits: [],
|
|
3511
|
+
envEdits,
|
|
3512
|
+
...generatedFiles.length > 0 ? { generatedFiles } : {}
|
|
3513
|
+
};
|
|
3514
|
+
}
|
|
3515
|
+
async function writeFileAtomic3(file, contents) {
|
|
3516
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
3517
|
+
await fs8.writeFile(tmp, contents, "utf8");
|
|
3518
|
+
await fs8.rename(tmp, file);
|
|
3519
|
+
}
|
|
3520
|
+
async function applyComposerJson(file, edits, original) {
|
|
3521
|
+
let parsed;
|
|
3522
|
+
try {
|
|
3523
|
+
parsed = JSON.parse(original);
|
|
3524
|
+
} catch {
|
|
3525
|
+
throw new Error(`php installer: composer.json at ${file} is not valid JSON`);
|
|
3526
|
+
}
|
|
3527
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
3528
|
+
throw new Error(`php installer: composer.json at ${file} is not a JSON object`);
|
|
3529
|
+
}
|
|
3530
|
+
const obj = parsed;
|
|
3531
|
+
const require2 = obj.require && typeof obj.require === "object" && !Array.isArray(obj.require) ? obj.require : {};
|
|
3532
|
+
for (const e of edits) {
|
|
3533
|
+
if (e.kind !== "add") continue;
|
|
3534
|
+
if (!(e.name in require2)) require2[e.name] = e.version;
|
|
3535
|
+
}
|
|
3536
|
+
obj.require = require2;
|
|
3537
|
+
await writeFileAtomic3(file, JSON.stringify(obj, null, 2) + "\n");
|
|
3538
|
+
}
|
|
3539
|
+
async function rollback4(serviceDir, language, originals, created) {
|
|
3540
|
+
const restored = [];
|
|
3541
|
+
for (const [file, raw] of originals.entries()) {
|
|
3542
|
+
try {
|
|
3543
|
+
await fs8.writeFile(file, raw, "utf8");
|
|
3544
|
+
restored.push(file);
|
|
3545
|
+
} catch {
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
const removed = [];
|
|
3549
|
+
for (const file of created) {
|
|
3550
|
+
try {
|
|
3551
|
+
await fs8.rm(file, { force: true });
|
|
3552
|
+
removed.push(file);
|
|
3553
|
+
} catch {
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
const body = [
|
|
3557
|
+
"# neat-rollback.patch",
|
|
3558
|
+
"",
|
|
3559
|
+
`# Generated after a partial apply failure in the ${language} installer.`,
|
|
3560
|
+
"# Files listed below were restored to their pre-apply contents.",
|
|
3561
|
+
"",
|
|
3562
|
+
...restored.map((f) => `restored: ${f}`),
|
|
3563
|
+
...removed.map((f) => `removed: ${f}`),
|
|
3564
|
+
""
|
|
3565
|
+
];
|
|
3566
|
+
await fs8.writeFile(path9.join(serviceDir, "neat-rollback.patch"), body.join("\n"), "utf8");
|
|
3567
|
+
}
|
|
3568
|
+
async function apply5(installPlan) {
|
|
3569
|
+
const { serviceDir } = installPlan;
|
|
3570
|
+
const generatedFiles = installPlan.generatedFiles ?? [];
|
|
3571
|
+
const manifests = new Set(installPlan.dependencyEdits.map((e) => e.file));
|
|
3572
|
+
if (manifests.size === 0 && generatedFiles.length === 0) {
|
|
3573
|
+
return { serviceDir, outcome: "already-instrumented", writtenFiles: [], reason: PHP_PECL_CAVEAT };
|
|
3574
|
+
}
|
|
3575
|
+
const originals = /* @__PURE__ */ new Map();
|
|
3576
|
+
for (const file of manifests) {
|
|
3577
|
+
try {
|
|
3578
|
+
originals.set(file, await fs8.readFile(file, "utf8"));
|
|
3579
|
+
} catch {
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
const writtenFiles = [];
|
|
3583
|
+
const created = [];
|
|
3584
|
+
try {
|
|
3585
|
+
for (const gf of generatedFiles) {
|
|
3586
|
+
if (await exists5(gf.file)) continue;
|
|
3587
|
+
await fs8.mkdir(path9.dirname(gf.file), { recursive: true });
|
|
3588
|
+
await writeFileAtomic3(gf.file, gf.contents);
|
|
3589
|
+
writtenFiles.push(gf.file);
|
|
3590
|
+
created.push(gf.file);
|
|
3591
|
+
}
|
|
3592
|
+
for (const file of manifests) {
|
|
3593
|
+
const raw = originals.get(file);
|
|
3594
|
+
if (raw === void 0) throw new Error(`php installer: cannot read ${file} during apply`);
|
|
3595
|
+
const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
|
|
3596
|
+
if (edits.length > 0) {
|
|
3597
|
+
await applyComposerJson(file, edits, raw);
|
|
3598
|
+
writtenFiles.push(file);
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
} catch (err) {
|
|
3602
|
+
await rollback4(serviceDir, installPlan.language, originals, created);
|
|
3603
|
+
throw err;
|
|
3604
|
+
}
|
|
3605
|
+
if (writtenFiles.length > 0) {
|
|
3606
|
+
console.warn(`neat: PHP instrumentation staged in ${path9.basename(serviceDir)}, but a system step remains:
|
|
3607
|
+
${PHP_PECL_CAVEAT}`);
|
|
3608
|
+
}
|
|
3609
|
+
const wroteManifest = writtenFiles.some((f) => path9.basename(f) === "composer.json");
|
|
3610
|
+
return {
|
|
3611
|
+
serviceDir,
|
|
3612
|
+
outcome: writtenFiles.length > 0 ? "instrumented" : "already-instrumented",
|
|
3613
|
+
writtenFiles,
|
|
3614
|
+
reason: PHP_PECL_CAVEAT,
|
|
3615
|
+
...wroteManifest ? { followUpInstall: "composer install" } : {}
|
|
3616
|
+
};
|
|
3617
|
+
}
|
|
3618
|
+
var phpInstaller = { name: "php", detect: detect5, plan: plan5, apply: apply5 };
|
|
3619
|
+
|
|
3111
3620
|
// src/installers/shared.ts
|
|
3112
|
-
function isEmptyPlan(
|
|
3113
|
-
return
|
|
3621
|
+
function isEmptyPlan(plan6) {
|
|
3622
|
+
return plan6.dependencyEdits.length === 0 && plan6.entrypointEdits.length === 0 && plan6.envEdits.length === 0 && (plan6.generatedFiles?.length ?? 0) === 0 && plan6.nextConfigEdit === void 0;
|
|
3114
3623
|
}
|
|
3115
3624
|
|
|
3116
3625
|
// src/installers/index.ts
|
|
@@ -3122,9 +3631,16 @@ var FORBIDDEN_LOCKFILES = /* @__PURE__ */ new Set([
|
|
|
3122
3631
|
"Pipfile.lock",
|
|
3123
3632
|
"Gemfile.lock",
|
|
3124
3633
|
"Cargo.lock",
|
|
3125
|
-
"go.sum"
|
|
3634
|
+
"go.sum",
|
|
3635
|
+
"composer.lock"
|
|
3126
3636
|
]);
|
|
3127
|
-
var INSTALLERS = [
|
|
3637
|
+
var INSTALLERS = [
|
|
3638
|
+
javascriptInstaller,
|
|
3639
|
+
pythonInstaller,
|
|
3640
|
+
goInstaller,
|
|
3641
|
+
rubyInstaller,
|
|
3642
|
+
phpInstaller
|
|
3643
|
+
];
|
|
3128
3644
|
async function pickInstaller(serviceDir) {
|
|
3129
3645
|
for (const inst of INSTALLERS) {
|
|
3130
3646
|
if (await inst.detect(serviceDir)) return inst;
|
|
@@ -3139,7 +3655,7 @@ function renderPatch(sections) {
|
|
|
3139
3655
|
"No SDK installers matched the discovered services. Two reasons this",
|
|
3140
3656
|
"normally happens:",
|
|
3141
3657
|
" - the project uses a language NEAT does not yet instrument",
|
|
3142
|
-
" (Java /
|
|
3658
|
+
" (Java / .NET / Rust are out of scope per ADR-047);",
|
|
3143
3659
|
" - the SDK is already installed, so the installer returned an empty",
|
|
3144
3660
|
" plan.",
|
|
3145
3661
|
"",
|
|
@@ -3149,22 +3665,22 @@ function renderPatch(sections) {
|
|
|
3149
3665
|
}
|
|
3150
3666
|
const lines = ["# neat install plan", ""];
|
|
3151
3667
|
for (const section of sections) {
|
|
3152
|
-
const { installer, plan:
|
|
3153
|
-
lines.push(`## ${installer} (${
|
|
3668
|
+
const { installer, plan: plan6 } = section;
|
|
3669
|
+
lines.push(`## ${installer} (${plan6.language}) \u2014 ${plan6.serviceDir}`);
|
|
3154
3670
|
lines.push("");
|
|
3155
|
-
if (
|
|
3671
|
+
if (plan6.libOnly) {
|
|
3156
3672
|
lines.push("### skipped \u2014 no resolvable entry point (lib-only)");
|
|
3157
3673
|
lines.push("");
|
|
3158
3674
|
continue;
|
|
3159
3675
|
}
|
|
3160
|
-
if (
|
|
3161
|
-
lines.push(`entry: ${
|
|
3676
|
+
if (plan6.entryFile) {
|
|
3677
|
+
lines.push(`entry: ${plan6.entryFile}`);
|
|
3162
3678
|
lines.push("");
|
|
3163
3679
|
}
|
|
3164
|
-
if (
|
|
3680
|
+
if (plan6.dependencyEdits.length > 0) {
|
|
3165
3681
|
lines.push("### dependencies");
|
|
3166
3682
|
const byFile = /* @__PURE__ */ new Map();
|
|
3167
|
-
for (const dep of
|
|
3683
|
+
for (const dep of plan6.dependencyEdits) {
|
|
3168
3684
|
const base = dep.file.split(/[\\/]/).pop() ?? dep.file;
|
|
3169
3685
|
if (FORBIDDEN_LOCKFILES.has(base)) {
|
|
3170
3686
|
throw new Error(
|
|
@@ -3183,9 +3699,9 @@ function renderPatch(sections) {
|
|
|
3183
3699
|
}
|
|
3184
3700
|
lines.push("");
|
|
3185
3701
|
}
|
|
3186
|
-
if (
|
|
3702
|
+
if (plan6.generatedFiles && plan6.generatedFiles.length > 0) {
|
|
3187
3703
|
lines.push("### generated files");
|
|
3188
|
-
for (const gen of
|
|
3704
|
+
for (const gen of plan6.generatedFiles) {
|
|
3189
3705
|
lines.push(`--- (new file) ${gen.file}`);
|
|
3190
3706
|
for (const ln of gen.contents.split(/\r?\n/)) {
|
|
3191
3707
|
lines.push(`+ ${ln}`);
|
|
@@ -3193,26 +3709,26 @@ function renderPatch(sections) {
|
|
|
3193
3709
|
}
|
|
3194
3710
|
lines.push("");
|
|
3195
3711
|
}
|
|
3196
|
-
if (
|
|
3712
|
+
if (plan6.entrypointEdits.length > 0) {
|
|
3197
3713
|
lines.push("### entry-point injection");
|
|
3198
|
-
for (const e of
|
|
3714
|
+
for (const e of plan6.entrypointEdits) {
|
|
3199
3715
|
lines.push(`--- ${e.file}`);
|
|
3200
3716
|
lines.push(`+ ${e.after}`);
|
|
3201
3717
|
lines.push(` ${e.before}`);
|
|
3202
3718
|
}
|
|
3203
3719
|
lines.push("");
|
|
3204
3720
|
}
|
|
3205
|
-
if (
|
|
3721
|
+
if (plan6.envEdits.length > 0) {
|
|
3206
3722
|
lines.push("### env (written to <package-dir>/.env.neat)");
|
|
3207
|
-
for (const env of
|
|
3723
|
+
for (const env of plan6.envEdits) {
|
|
3208
3724
|
lines.push(`- ${env.key}=${env.value}`);
|
|
3209
3725
|
}
|
|
3210
3726
|
lines.push("");
|
|
3211
3727
|
}
|
|
3212
|
-
if (
|
|
3728
|
+
if (plan6.nextConfigEdit) {
|
|
3213
3729
|
lines.push("### next.config (framework flag)");
|
|
3214
|
-
lines.push(`--- ${
|
|
3215
|
-
lines.push(`+ experimental: { instrumentationHook: true }, // ${
|
|
3730
|
+
lines.push(`--- ${plan6.nextConfigEdit.file}`);
|
|
3731
|
+
lines.push(`+ experimental: { instrumentationHook: true }, // ${plan6.nextConfigEdit.reason}`);
|
|
3216
3732
|
lines.push("");
|
|
3217
3733
|
}
|
|
3218
3734
|
}
|
|
@@ -3220,10 +3736,10 @@ function renderPatch(sections) {
|
|
|
3220
3736
|
}
|
|
3221
3737
|
|
|
3222
3738
|
// src/orchestrator.ts
|
|
3223
|
-
import { promises as
|
|
3739
|
+
import { promises as fs9 } from "fs";
|
|
3224
3740
|
import http from "http";
|
|
3225
3741
|
import net from "net";
|
|
3226
|
-
import
|
|
3742
|
+
import path10 from "path";
|
|
3227
3743
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3228
3744
|
import { spawn as spawn2 } from "child_process";
|
|
3229
3745
|
import readline from "readline";
|
|
@@ -3233,7 +3749,7 @@ async function extractAndPersist(opts) {
|
|
|
3233
3749
|
const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
|
|
3234
3750
|
resetGraph(graphKey);
|
|
3235
3751
|
const graph = getGraph(graphKey);
|
|
3236
|
-
const projectPaths = pathsForProject(graphKey,
|
|
3752
|
+
const projectPaths = pathsForProject(graphKey, path10.join(opts.scanPath, "neat-out"));
|
|
3237
3753
|
const extraction = await extractFromDirectory(graph, opts.scanPath, {
|
|
3238
3754
|
errorsPath: projectPaths.errorsPath
|
|
3239
3755
|
});
|
|
@@ -3264,28 +3780,34 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3264
3780
|
let cloudflareWorkers = 0;
|
|
3265
3781
|
let electron = 0;
|
|
3266
3782
|
const installPlans = /* @__PURE__ */ new Map();
|
|
3783
|
+
const dependencyInstructions = /* @__PURE__ */ new Map();
|
|
3267
3784
|
for (const svc of services) {
|
|
3268
3785
|
const installer = await pickInstaller(svc.dir);
|
|
3269
3786
|
if (!installer) continue;
|
|
3270
|
-
const
|
|
3271
|
-
if (isEmptyPlan(
|
|
3787
|
+
const plan6 = await installer.plan(svc.dir, { project });
|
|
3788
|
+
if (isEmptyPlan(plan6) && !plan6.libOnly && plan6.runtimeKind === void 0) {
|
|
3272
3789
|
already++;
|
|
3273
3790
|
continue;
|
|
3274
3791
|
}
|
|
3275
|
-
const outcome = await installer.apply(
|
|
3792
|
+
const outcome = await installer.apply(plan6);
|
|
3276
3793
|
if (outcome.outcome === "instrumented") {
|
|
3277
3794
|
instrumented++;
|
|
3278
|
-
if (
|
|
3279
|
-
const
|
|
3280
|
-
|
|
3281
|
-
|
|
3795
|
+
if (plan6.dependencyEdits.length > 0) {
|
|
3796
|
+
const manifest = path10.basename(plan6.dependencyEdits[0].file);
|
|
3797
|
+
if (manifest === "package.json") {
|
|
3798
|
+
const cmd = await resolveManager(svc.dir);
|
|
3799
|
+
const key = `${cmd.pm}:${cmd.cwd}`;
|
|
3800
|
+
if (!installPlans.has(key)) installPlans.set(key, cmd);
|
|
3801
|
+
} else if (outcome.followUpInstall) {
|
|
3802
|
+
dependencyInstructions.set(svc.dir, outcome.followUpInstall);
|
|
3803
|
+
}
|
|
3282
3804
|
}
|
|
3283
3805
|
} else if (outcome.outcome === "already-instrumented") already++;
|
|
3284
3806
|
else if (outcome.outcome === "lib-only") {
|
|
3285
3807
|
libOnly++;
|
|
3286
3808
|
const appDeps = svc.pkg ? appFrameworkDependencies(svc.pkg) : [];
|
|
3287
3809
|
if (appDeps.length > 0) {
|
|
3288
|
-
const svcName =
|
|
3810
|
+
const svcName = path10.basename(svc.dir);
|
|
3289
3811
|
const list = appDeps.join(", ");
|
|
3290
3812
|
console.warn(
|
|
3291
3813
|
`neat: runtime layer won't engage for ${svcName}: no entry point found.
|
|
@@ -3298,7 +3820,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3298
3820
|
console.log(`skipping ${svc.dir}: browser bundle; browser-OTel support lands in a future release.`);
|
|
3299
3821
|
} else if (outcome.outcome === "react-native") {
|
|
3300
3822
|
reactNative++;
|
|
3301
|
-
const svcName =
|
|
3823
|
+
const svcName = path10.basename(svc.dir);
|
|
3302
3824
|
console.log(
|
|
3303
3825
|
`neat: ${svc.dir} detected as React Native / Expo
|
|
3304
3826
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -3309,7 +3831,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3309
3831
|
);
|
|
3310
3832
|
} else if (outcome.outcome === "bun") {
|
|
3311
3833
|
bun++;
|
|
3312
|
-
const svcName =
|
|
3834
|
+
const svcName = path10.basename(svc.dir);
|
|
3313
3835
|
console.log(
|
|
3314
3836
|
`neat: ${svc.dir} detected as Bun
|
|
3315
3837
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -3320,7 +3842,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3320
3842
|
);
|
|
3321
3843
|
} else if (outcome.outcome === "deno") {
|
|
3322
3844
|
deno++;
|
|
3323
|
-
const svcName =
|
|
3845
|
+
const svcName = path10.basename(svc.dir);
|
|
3324
3846
|
console.log(
|
|
3325
3847
|
`neat: ${svc.dir} detected as Deno
|
|
3326
3848
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -3331,7 +3853,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3331
3853
|
);
|
|
3332
3854
|
} else if (outcome.outcome === "cloudflare-workers") {
|
|
3333
3855
|
cloudflareWorkers++;
|
|
3334
|
-
const svcName =
|
|
3856
|
+
const svcName = path10.basename(svc.dir);
|
|
3335
3857
|
console.log(
|
|
3336
3858
|
`neat: ${svc.dir} detected as Cloudflare Workers
|
|
3337
3859
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -3342,7 +3864,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3342
3864
|
);
|
|
3343
3865
|
} else if (outcome.outcome === "electron") {
|
|
3344
3866
|
electron++;
|
|
3345
|
-
const svcName =
|
|
3867
|
+
const svcName = path10.basename(svc.dir);
|
|
3346
3868
|
console.log(
|
|
3347
3869
|
`neat: ${svc.dir} detected as Electron
|
|
3348
3870
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -3355,7 +3877,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3355
3877
|
if (svc.pkg && (outcome.outcome === "instrumented" || outcome.outcome === "already-instrumented")) {
|
|
3356
3878
|
const gaps = uninstrumentedLibraries(svc.pkg);
|
|
3357
3879
|
if (gaps.length > 0) {
|
|
3358
|
-
const svcName =
|
|
3880
|
+
const svcName = path10.basename(svc.dir);
|
|
3359
3881
|
const list = gaps.join(", ");
|
|
3360
3882
|
const subject = gaps.length === 1 ? "this library" : "these libraries";
|
|
3361
3883
|
const aux = gaps.length === 1 ? "isn't" : "aren't";
|
|
@@ -3383,6 +3905,11 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3383
3905
|
}
|
|
3384
3906
|
}
|
|
3385
3907
|
}
|
|
3908
|
+
for (const [dir, command] of dependencyInstructions) {
|
|
3909
|
+
console.log(
|
|
3910
|
+
`neat: dependencies staged in ${dir}; run \`${command}\` to install them \u2014 NEAT does not run it for you.`
|
|
3911
|
+
);
|
|
3912
|
+
}
|
|
3386
3913
|
return {
|
|
3387
3914
|
instrumented,
|
|
3388
3915
|
alreadyInstrumented: already,
|
|
@@ -3393,7 +3920,8 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3393
3920
|
deno,
|
|
3394
3921
|
cloudflareWorkers,
|
|
3395
3922
|
electron,
|
|
3396
|
-
packageManagerInstalls
|
|
3923
|
+
packageManagerInstalls,
|
|
3924
|
+
dependencyInstructions: [...dependencyInstructions].map(([dir, command]) => ({ dir, command }))
|
|
3397
3925
|
};
|
|
3398
3926
|
}
|
|
3399
3927
|
async function promptYesNo(question) {
|
|
@@ -3561,24 +4089,24 @@ async function persistedPortsFor(scanPath) {
|
|
|
3561
4089
|
return { rest: record.ports.rest, otlp: record.ports.otlp, web: record.ports.web };
|
|
3562
4090
|
}
|
|
3563
4091
|
async function acquireSpawnLock(scanPath) {
|
|
3564
|
-
const lockPath =
|
|
3565
|
-
await
|
|
4092
|
+
const lockPath = path10.join(scanPath, "neat-out", "daemon.spawn.lock");
|
|
4093
|
+
await fs9.mkdir(path10.dirname(lockPath), { recursive: true });
|
|
3566
4094
|
const STALE_LOCK_MS = 6e4;
|
|
3567
4095
|
try {
|
|
3568
|
-
const fd = await
|
|
4096
|
+
const fd = await fs9.open(lockPath, "wx");
|
|
3569
4097
|
await fd.writeFile(`${process.pid}
|
|
3570
4098
|
`, "utf8");
|
|
3571
4099
|
await fd.close();
|
|
3572
4100
|
return async () => {
|
|
3573
|
-
await
|
|
4101
|
+
await fs9.unlink(lockPath).catch(() => {
|
|
3574
4102
|
});
|
|
3575
4103
|
};
|
|
3576
4104
|
} catch (err) {
|
|
3577
4105
|
if (err.code !== "EEXIST") return null;
|
|
3578
4106
|
try {
|
|
3579
|
-
const stat = await
|
|
4107
|
+
const stat = await fs9.stat(lockPath);
|
|
3580
4108
|
if (Date.now() - stat.mtimeMs > STALE_LOCK_MS) {
|
|
3581
|
-
await
|
|
4109
|
+
await fs9.unlink(lockPath).catch(() => {
|
|
3582
4110
|
});
|
|
3583
4111
|
return acquireSpawnLock(scanPath);
|
|
3584
4112
|
}
|
|
@@ -3607,13 +4135,13 @@ async function healthIsForProject(restPort, project) {
|
|
|
3607
4135
|
return false;
|
|
3608
4136
|
}
|
|
3609
4137
|
function daemonLogPath(projectPath3) {
|
|
3610
|
-
return
|
|
4138
|
+
return path10.join(projectPath3, "neat-out", "daemon.log");
|
|
3611
4139
|
}
|
|
3612
4140
|
function spawnDaemonDetached(spec) {
|
|
3613
|
-
const here =
|
|
4141
|
+
const here = path10.dirname(fileURLToPath2(import.meta.url));
|
|
3614
4142
|
const candidates = [
|
|
3615
|
-
|
|
3616
|
-
|
|
4143
|
+
path10.join(here, "neatd.cjs"),
|
|
4144
|
+
path10.join(here, "neatd.js")
|
|
3617
4145
|
];
|
|
3618
4146
|
let entry2 = null;
|
|
3619
4147
|
const fsSync = __require("fs");
|
|
@@ -3643,7 +4171,7 @@ function spawnDaemonDetached(spec) {
|
|
|
3643
4171
|
let logFd = null;
|
|
3644
4172
|
if (spec) {
|
|
3645
4173
|
const logPath = daemonLogPath(spec.projectPath);
|
|
3646
|
-
fsSync.mkdirSync(
|
|
4174
|
+
fsSync.mkdirSync(path10.dirname(logPath), { recursive: true });
|
|
3647
4175
|
logFd = fsSync.openSync(logPath, "a");
|
|
3648
4176
|
}
|
|
3649
4177
|
const child = spawn2(process.execPath, [entry2, "start"], {
|
|
@@ -3682,7 +4210,7 @@ async function runOrchestrator(opts) {
|
|
|
3682
4210
|
browser: "skipped"
|
|
3683
4211
|
}
|
|
3684
4212
|
};
|
|
3685
|
-
const stat = await
|
|
4213
|
+
const stat = await fs9.stat(opts.scanPath).catch(() => null);
|
|
3686
4214
|
if (!stat || !stat.isDirectory()) {
|
|
3687
4215
|
console.error(`neat: ${opts.scanPath} is not a directory`);
|
|
3688
4216
|
result.exitCode = 2;
|
|
@@ -3842,7 +4370,7 @@ async function runOrchestrator(opts) {
|
|
|
3842
4370
|
result.steps.browser = openBrowser(dashboardUrl);
|
|
3843
4371
|
}
|
|
3844
4372
|
const daemonRunning = result.steps.daemon === "spawned" || result.steps.daemon === "already-running";
|
|
3845
|
-
const daemonLog = daemonRunning ?
|
|
4373
|
+
const daemonLog = daemonRunning ? path10.relative(opts.scanPath, daemonLogPath(opts.scanPath)) : null;
|
|
3846
4374
|
printSummary(result, graph, dashboardUrl, daemonLog);
|
|
3847
4375
|
return result;
|
|
3848
4376
|
}
|
|
@@ -4299,27 +4827,27 @@ async function runConnectorCommand(rawArgs, deps = {}) {
|
|
|
4299
4827
|
}
|
|
4300
4828
|
|
|
4301
4829
|
// src/hooks-cli.ts
|
|
4302
|
-
import
|
|
4830
|
+
import path11 from "path";
|
|
4303
4831
|
import os from "os";
|
|
4304
|
-
import { promises as
|
|
4832
|
+
import { promises as fs10 } from "fs";
|
|
4305
4833
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4306
4834
|
var HOOK_FILENAME = "neat-search-nudge.mjs";
|
|
4307
4835
|
var GUIDE_FILENAME = "GRAPH_FIRST.md";
|
|
4308
4836
|
var GUIDE_INSTALL_NAME = "neat-graph-first.md";
|
|
4309
4837
|
var HOOK_MATCHER = "Grep|Glob|Bash";
|
|
4310
4838
|
function moduleDir() {
|
|
4311
|
-
return typeof __dirname !== "undefined" ? __dirname :
|
|
4839
|
+
return typeof __dirname !== "undefined" ? __dirname : path11.dirname(fileURLToPath3(import.meta.url));
|
|
4312
4840
|
}
|
|
4313
4841
|
async function readSkillAsset(rel) {
|
|
4314
4842
|
const here = moduleDir();
|
|
4315
4843
|
const candidates = [
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4844
|
+
path11.resolve(here, "../../claude-skill", rel),
|
|
4845
|
+
path11.resolve(here, "../../../claude-skill", rel),
|
|
4846
|
+
path11.resolve(here, "../claude-skill", rel)
|
|
4319
4847
|
];
|
|
4320
4848
|
for (const candidate of candidates) {
|
|
4321
4849
|
try {
|
|
4322
|
-
return await
|
|
4850
|
+
return await fs10.readFile(candidate, "utf8");
|
|
4323
4851
|
} catch {
|
|
4324
4852
|
}
|
|
4325
4853
|
}
|
|
@@ -4329,17 +4857,17 @@ async function readSkillAsset(rel) {
|
|
|
4329
4857
|
}
|
|
4330
4858
|
function neatHome() {
|
|
4331
4859
|
const override = process.env.NEAT_HOME;
|
|
4332
|
-
if (override && override.length > 0) return
|
|
4333
|
-
return
|
|
4860
|
+
if (override && override.length > 0) return path11.resolve(override);
|
|
4861
|
+
return path11.join(os.homedir(), ".neat");
|
|
4334
4862
|
}
|
|
4335
4863
|
function claudeSettingsPath() {
|
|
4336
4864
|
const override = process.env.NEAT_CLAUDE_SETTINGS;
|
|
4337
|
-
if (override && override.length > 0) return
|
|
4865
|
+
if (override && override.length > 0) return path11.resolve(override);
|
|
4338
4866
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? os.homedir();
|
|
4339
|
-
return
|
|
4867
|
+
return path11.join(home, ".claude", "settings.json");
|
|
4340
4868
|
}
|
|
4341
4869
|
function installedHookPath() {
|
|
4342
|
-
return
|
|
4870
|
+
return path11.join(neatHome(), "hooks", HOOK_FILENAME);
|
|
4343
4871
|
}
|
|
4344
4872
|
function isNeatSearchEntry(entry2) {
|
|
4345
4873
|
return (entry2.hooks ?? []).some(
|
|
@@ -4372,14 +4900,14 @@ async function runHooks(opts) {
|
|
|
4372
4900
|
const hookScript = await readSkillAsset(`hooks/${HOOK_FILENAME}`);
|
|
4373
4901
|
const guide = await readSkillAsset(GUIDE_FILENAME);
|
|
4374
4902
|
const scriptPath = installedHookPath();
|
|
4375
|
-
await
|
|
4376
|
-
await
|
|
4377
|
-
const guidePath =
|
|
4378
|
-
await
|
|
4903
|
+
await fs10.mkdir(path11.dirname(scriptPath), { recursive: true });
|
|
4904
|
+
await fs10.writeFile(scriptPath, hookScript, { mode: 493 });
|
|
4905
|
+
const guidePath = path11.join(neatHome(), GUIDE_INSTALL_NAME);
|
|
4906
|
+
await fs10.writeFile(guidePath, guide, "utf8");
|
|
4379
4907
|
const settingsFile = claudeSettingsPath();
|
|
4380
4908
|
let settings = {};
|
|
4381
4909
|
try {
|
|
4382
|
-
settings = JSON.parse(await
|
|
4910
|
+
settings = JSON.parse(await fs10.readFile(settingsFile, "utf8"));
|
|
4383
4911
|
} catch (err) {
|
|
4384
4912
|
if (err.code !== "ENOENT") {
|
|
4385
4913
|
console.error(
|
|
@@ -4401,8 +4929,8 @@ async function runHooks(opts) {
|
|
|
4401
4929
|
...settings,
|
|
4402
4930
|
hooks: { ...hooks, PreToolUse: preToolUse }
|
|
4403
4931
|
};
|
|
4404
|
-
await
|
|
4405
|
-
await
|
|
4932
|
+
await fs10.mkdir(path11.dirname(settingsFile), { recursive: true });
|
|
4933
|
+
await fs10.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
4406
4934
|
console.log(`neat hooks: installed the search-nudge hook`);
|
|
4407
4935
|
console.log(` script: ${scriptPath}`);
|
|
4408
4936
|
console.log(` settings: ${settingsFile} (PreToolUse \u2192 ${HOOK_MATCHER})`);
|
|
@@ -4472,9 +5000,9 @@ async function runHooksCommand(args) {
|
|
|
4472
5000
|
}
|
|
4473
5001
|
|
|
4474
5002
|
// src/codex-cli.ts
|
|
4475
|
-
import
|
|
5003
|
+
import path12 from "path";
|
|
4476
5004
|
import os2 from "os";
|
|
4477
|
-
import { promises as
|
|
5005
|
+
import { promises as fs11 } from "fs";
|
|
4478
5006
|
import { isDeepStrictEqual } from "util";
|
|
4479
5007
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
4480
5008
|
var CODEX_MCP_SERVER = {
|
|
@@ -4492,14 +5020,14 @@ var NEAT_GRAPH_FIRST_START = "<!-- neat:graph-first -->";
|
|
|
4492
5020
|
var NEAT_GRAPH_FIRST_END = "<!-- /neat:graph-first -->";
|
|
4493
5021
|
function codexConfigPath() {
|
|
4494
5022
|
const override = process.env.NEAT_CODEX_CONFIG;
|
|
4495
|
-
if (override && override.length > 0) return
|
|
5023
|
+
if (override && override.length > 0) return path12.resolve(override);
|
|
4496
5024
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? os2.homedir();
|
|
4497
|
-
return
|
|
5025
|
+
return path12.join(home, ".codex", "config.toml");
|
|
4498
5026
|
}
|
|
4499
5027
|
function agentsFilePath() {
|
|
4500
5028
|
const override = process.env.NEAT_CODEX_AGENTS;
|
|
4501
|
-
if (override && override.length > 0) return
|
|
4502
|
-
return
|
|
5029
|
+
if (override && override.length > 0) return path12.resolve(override);
|
|
5030
|
+
return path12.join(process.cwd(), "AGENTS.md");
|
|
4503
5031
|
}
|
|
4504
5032
|
function isTableHeader(line) {
|
|
4505
5033
|
return /^\s*\[\[?[^\]]+\]\]?\s*$/.test(line);
|
|
@@ -4633,7 +5161,7 @@ async function runCodex(opts) {
|
|
|
4633
5161
|
const agentsPath = agentsFilePath();
|
|
4634
5162
|
let configRaw = "";
|
|
4635
5163
|
try {
|
|
4636
|
-
configRaw = await
|
|
5164
|
+
configRaw = await fs11.readFile(configPath, "utf8");
|
|
4637
5165
|
} catch (err) {
|
|
4638
5166
|
if (err.code !== "ENOENT") {
|
|
4639
5167
|
console.error(`neat codex: failed to read ${configPath} \u2014 ${err.message}`);
|
|
@@ -4642,7 +5170,7 @@ async function runCodex(opts) {
|
|
|
4642
5170
|
}
|
|
4643
5171
|
let agentsRaw = "";
|
|
4644
5172
|
try {
|
|
4645
|
-
agentsRaw = await
|
|
5173
|
+
agentsRaw = await fs11.readFile(agentsPath, "utf8");
|
|
4646
5174
|
} catch (err) {
|
|
4647
5175
|
if (err.code !== "ENOENT") {
|
|
4648
5176
|
console.error(`neat codex: failed to read ${agentsPath} \u2014 ${err.message}`);
|
|
@@ -4682,15 +5210,15 @@ async function runCodex(opts) {
|
|
|
4682
5210
|
return { exitCode: 0 };
|
|
4683
5211
|
}
|
|
4684
5212
|
if (config.changed) {
|
|
4685
|
-
await
|
|
4686
|
-
await
|
|
5213
|
+
await fs11.mkdir(path12.dirname(configPath), { recursive: true });
|
|
5214
|
+
await fs11.writeFile(configPath, config.text, "utf8");
|
|
4687
5215
|
console.log(`neat codex: wrote [mcp_servers.neat] to ${configPath}`);
|
|
4688
5216
|
} else {
|
|
4689
5217
|
console.log(`neat codex: ${configPath} already has NEAT's MCP server`);
|
|
4690
5218
|
}
|
|
4691
5219
|
if (agents.changed) {
|
|
4692
|
-
await
|
|
4693
|
-
await
|
|
5220
|
+
await fs11.mkdir(path12.dirname(agentsPath), { recursive: true });
|
|
5221
|
+
await fs11.writeFile(agentsPath, agents.text, "utf8");
|
|
4694
5222
|
console.log(`neat codex: wrote the graph-first block to ${agentsPath}`);
|
|
4695
5223
|
} else {
|
|
4696
5224
|
console.log(`neat codex: ${agentsPath} already has the graph-first block`);
|
|
@@ -4746,9 +5274,9 @@ async function runCodexCommand(args) {
|
|
|
4746
5274
|
}
|
|
4747
5275
|
|
|
4748
5276
|
// src/editors-cli.ts
|
|
4749
|
-
import
|
|
5277
|
+
import path13 from "path";
|
|
4750
5278
|
import os3 from "os";
|
|
4751
|
-
import { promises as
|
|
5279
|
+
import { promises as fs12 } from "fs";
|
|
4752
5280
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
4753
5281
|
import * as jsonc from "jsonc-parser";
|
|
4754
5282
|
var NEAT_MCP_SERVER = {
|
|
@@ -4772,17 +5300,17 @@ function homeDir() {
|
|
|
4772
5300
|
}
|
|
4773
5301
|
function xdgConfigDir() {
|
|
4774
5302
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
4775
|
-
return xdg && xdg.length > 0 ?
|
|
5303
|
+
return xdg && xdg.length > 0 ? path13.resolve(xdg) : path13.join(homeDir(), ".config");
|
|
4776
5304
|
}
|
|
4777
5305
|
function envOverride(name) {
|
|
4778
5306
|
const v = process.env[name];
|
|
4779
|
-
return v && v.length > 0 ?
|
|
5307
|
+
return v && v.length > 0 ? path13.resolve(v) : void 0;
|
|
4780
5308
|
}
|
|
4781
5309
|
var CURSOR_CLIENT = {
|
|
4782
5310
|
id: "cursor",
|
|
4783
5311
|
label: "Cursor",
|
|
4784
5312
|
docsUrl: "https://docs.cursor.com/context/mcp",
|
|
4785
|
-
mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ??
|
|
5313
|
+
mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ?? path13.join(homeDir(), ".cursor", "mcp.json"),
|
|
4786
5314
|
mcpContainerKey: "mcpServers",
|
|
4787
5315
|
format: "json",
|
|
4788
5316
|
// Cursor still reads a single `.cursorrules` at the project root (the modern
|
|
@@ -4794,7 +5322,7 @@ var DEVIN_CLIENT = {
|
|
|
4794
5322
|
id: "devin",
|
|
4795
5323
|
label: "Devin Desktop (Cascade)",
|
|
4796
5324
|
docsUrl: "https://docs.devin.ai/desktop/cascade/mcp",
|
|
4797
|
-
mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ??
|
|
5325
|
+
mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? path13.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
|
|
4798
5326
|
mcpContainerKey: "mcpServers",
|
|
4799
5327
|
format: "json",
|
|
4800
5328
|
rulesFileName: ".windsurfrules"
|
|
@@ -4803,7 +5331,7 @@ var GEMINI_CLIENT = {
|
|
|
4803
5331
|
id: "gemini",
|
|
4804
5332
|
label: "Gemini CLI",
|
|
4805
5333
|
docsUrl: "https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md",
|
|
4806
|
-
mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ??
|
|
5334
|
+
mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? path13.join(homeDir(), ".gemini", "settings.json"),
|
|
4807
5335
|
mcpContainerKey: "mcpServers",
|
|
4808
5336
|
format: "json",
|
|
4809
5337
|
rulesFileName: "GEMINI.md"
|
|
@@ -4812,7 +5340,7 @@ var QWEN_CLIENT = {
|
|
|
4812
5340
|
id: "qwen",
|
|
4813
5341
|
label: "Qwen Code",
|
|
4814
5342
|
docsUrl: "https://qwenlm.github.io/qwen-code-docs/en/users/features/mcp/",
|
|
4815
|
-
mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ??
|
|
5343
|
+
mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? path13.join(homeDir(), ".qwen", "settings.json"),
|
|
4816
5344
|
mcpContainerKey: "mcpServers",
|
|
4817
5345
|
format: "json",
|
|
4818
5346
|
rulesFileName: "QWEN.md"
|
|
@@ -4821,7 +5349,7 @@ var AMAZONQ_CLIENT = {
|
|
|
4821
5349
|
id: "amazonq",
|
|
4822
5350
|
label: "Amazon Q Developer CLI",
|
|
4823
5351
|
docsUrl: "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-configuration.html",
|
|
4824
|
-
mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ??
|
|
5352
|
+
mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? path13.join(homeDir(), ".aws", "amazonq", "mcp.json"),
|
|
4825
5353
|
mcpContainerKey: "mcpServers",
|
|
4826
5354
|
format: "json"
|
|
4827
5355
|
};
|
|
@@ -4829,7 +5357,7 @@ var ROOCODE_CLIENT = {
|
|
|
4829
5357
|
id: "roocode",
|
|
4830
5358
|
label: "Roo Code",
|
|
4831
5359
|
docsUrl: "https://roocodeinc.github.io/Roo-Code/features/mcp/using-mcp-in-roo",
|
|
4832
|
-
mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ??
|
|
5360
|
+
mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? path13.join(process.cwd(), ".roo", "mcp.json"),
|
|
4833
5361
|
mcpContainerKey: "mcpServers",
|
|
4834
5362
|
format: "json"
|
|
4835
5363
|
};
|
|
@@ -4842,9 +5370,9 @@ var ZED_CLIENT = {
|
|
|
4842
5370
|
if (override) return override;
|
|
4843
5371
|
if (process.platform === "win32") {
|
|
4844
5372
|
const appData = process.env.APPDATA;
|
|
4845
|
-
if (appData && appData.length > 0) return
|
|
5373
|
+
if (appData && appData.length > 0) return path13.join(appData, "Zed", "settings.json");
|
|
4846
5374
|
}
|
|
4847
|
-
return
|
|
5375
|
+
return path13.join(homeDir(), ".config", "zed", "settings.json");
|
|
4848
5376
|
},
|
|
4849
5377
|
mcpContainerKey: "context_servers",
|
|
4850
5378
|
format: "jsonc",
|
|
@@ -4854,7 +5382,7 @@ var OPENCODE_CLIENT = {
|
|
|
4854
5382
|
id: "opencode",
|
|
4855
5383
|
label: "OpenCode",
|
|
4856
5384
|
docsUrl: "https://opencode.ai/docs/mcp-servers/",
|
|
4857
|
-
mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ??
|
|
5385
|
+
mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? path13.join(xdgConfigDir(), "opencode", "opencode.json"),
|
|
4858
5386
|
mcpContainerKey: "mcp",
|
|
4859
5387
|
format: "json",
|
|
4860
5388
|
serverEntry: NEAT_OPENCODE_SERVER,
|
|
@@ -4864,7 +5392,7 @@ var CRUSH_CLIENT = {
|
|
|
4864
5392
|
id: "crush",
|
|
4865
5393
|
label: "Crush",
|
|
4866
5394
|
docsUrl: "https://charmbracelet-crush.mintlify.app/configuration/mcp",
|
|
4867
|
-
mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ??
|
|
5395
|
+
mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? path13.join(xdgConfigDir(), "crush", "crush.json"),
|
|
4868
5396
|
mcpContainerKey: "mcp",
|
|
4869
5397
|
format: "json",
|
|
4870
5398
|
serverEntry: NEAT_CRUSH_SERVER,
|
|
@@ -4927,7 +5455,7 @@ async function planMcp(client, mcpPath) {
|
|
|
4927
5455
|
const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
|
|
4928
5456
|
let raw = "";
|
|
4929
5457
|
try {
|
|
4930
|
-
raw = await
|
|
5458
|
+
raw = await fs12.readFile(mcpPath, "utf8");
|
|
4931
5459
|
} catch (err) {
|
|
4932
5460
|
const e = err;
|
|
4933
5461
|
if (e.code === "ENOENT") {
|
|
@@ -4969,7 +5497,7 @@ async function runEditorInstall(client, opts) {
|
|
|
4969
5497
|
const mcpPath = client.mcpConfigPath();
|
|
4970
5498
|
const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
|
|
4971
5499
|
const hasRules = typeof client.rulesFileName === "string";
|
|
4972
|
-
const rulesPath = hasRules ?
|
|
5500
|
+
const rulesPath = hasRules ? path13.join(opts.projectDir, client.rulesFileName) : "";
|
|
4973
5501
|
const mcp = await planMcp(client, mcpPath);
|
|
4974
5502
|
if (mcp === null) return { exitCode: 1 };
|
|
4975
5503
|
let existingRules = "";
|
|
@@ -4978,7 +5506,7 @@ async function runEditorInstall(client, opts) {
|
|
|
4978
5506
|
let block = "";
|
|
4979
5507
|
if (hasRules) {
|
|
4980
5508
|
try {
|
|
4981
|
-
existingRules = await
|
|
5509
|
+
existingRules = await fs12.readFile(rulesPath, "utf8");
|
|
4982
5510
|
} catch (err) {
|
|
4983
5511
|
if (err.code !== "ENOENT") {
|
|
4984
5512
|
console.error(`neat ${client.id}: failed to read ${rulesPath} \u2014 ${err.message}`);
|
|
@@ -5012,11 +5540,11 @@ async function runEditorInstall(client, opts) {
|
|
|
5012
5540
|
);
|
|
5013
5541
|
return { exitCode: 0 };
|
|
5014
5542
|
}
|
|
5015
|
-
await
|
|
5016
|
-
await
|
|
5543
|
+
await fs12.mkdir(path13.dirname(mcpPath), { recursive: true });
|
|
5544
|
+
await fs12.writeFile(mcpPath, mcp.text, "utf8");
|
|
5017
5545
|
if (hasRules) {
|
|
5018
|
-
await
|
|
5019
|
-
await
|
|
5546
|
+
await fs12.mkdir(path13.dirname(rulesPath), { recursive: true });
|
|
5547
|
+
await fs12.writeFile(rulesPath, newRules, "utf8");
|
|
5020
5548
|
}
|
|
5021
5549
|
console.log(`neat ${client.id}: wired NEAT into ${client.label}`);
|
|
5022
5550
|
console.log(` MCP server: ${mcpPath} (${client.mcpContainerKey}.neat \u2192 npx -y @neat.is/mcp)`);
|
|
@@ -5052,11 +5580,11 @@ function usage3(client) {
|
|
|
5052
5580
|
}
|
|
5053
5581
|
async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
|
|
5054
5582
|
const client = CLIENTS[clientId];
|
|
5055
|
-
let
|
|
5583
|
+
let apply6 = false;
|
|
5056
5584
|
for (const arg of args) {
|
|
5057
5585
|
switch (arg) {
|
|
5058
5586
|
case "--apply":
|
|
5059
|
-
|
|
5587
|
+
apply6 = true;
|
|
5060
5588
|
break;
|
|
5061
5589
|
case "-h":
|
|
5062
5590
|
case "--help":
|
|
@@ -5069,7 +5597,7 @@ async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
|
|
|
5069
5597
|
}
|
|
5070
5598
|
}
|
|
5071
5599
|
try {
|
|
5072
|
-
const { exitCode } = await runEditorInstall(client, { apply:
|
|
5600
|
+
const { exitCode } = await runEditorInstall(client, { apply: apply6, projectDir });
|
|
5073
5601
|
return exitCode;
|
|
5074
5602
|
} catch (err) {
|
|
5075
5603
|
console.error(err.message);
|
|
@@ -5106,10 +5634,10 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
5106
5634
|
const root = baseUrl.replace(/\/$/, "");
|
|
5107
5635
|
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
5108
5636
|
return {
|
|
5109
|
-
async get(
|
|
5637
|
+
async get(path16) {
|
|
5110
5638
|
let res;
|
|
5111
5639
|
try {
|
|
5112
|
-
res = await fetch(`${root}${
|
|
5640
|
+
res = await fetch(`${root}${path16}`, {
|
|
5113
5641
|
headers: { ...authHeader }
|
|
5114
5642
|
});
|
|
5115
5643
|
} catch (err) {
|
|
@@ -5121,16 +5649,16 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
5121
5649
|
const body = await res.text().catch(() => "");
|
|
5122
5650
|
throw new HttpError(
|
|
5123
5651
|
res.status,
|
|
5124
|
-
`${res.status} ${res.statusText} on GET ${
|
|
5652
|
+
`${res.status} ${res.statusText} on GET ${path16}: ${body}`,
|
|
5125
5653
|
body
|
|
5126
5654
|
);
|
|
5127
5655
|
}
|
|
5128
5656
|
return await res.json();
|
|
5129
5657
|
},
|
|
5130
|
-
async post(
|
|
5658
|
+
async post(path16, body) {
|
|
5131
5659
|
let res;
|
|
5132
5660
|
try {
|
|
5133
|
-
res = await fetch(`${root}${
|
|
5661
|
+
res = await fetch(`${root}${path16}`, {
|
|
5134
5662
|
method: "POST",
|
|
5135
5663
|
headers: { "content-type": "application/json", ...authHeader },
|
|
5136
5664
|
body: JSON.stringify(body)
|
|
@@ -5144,7 +5672,7 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
5144
5672
|
const text = await res.text().catch(() => "");
|
|
5145
5673
|
throw new HttpError(
|
|
5146
5674
|
res.status,
|
|
5147
|
-
`${res.status} ${res.statusText} on POST ${
|
|
5675
|
+
`${res.status} ${res.statusText} on POST ${path16}: ${text}`,
|
|
5148
5676
|
text
|
|
5149
5677
|
);
|
|
5150
5678
|
}
|
|
@@ -5158,12 +5686,12 @@ function projectPath(project, suffix) {
|
|
|
5158
5686
|
}
|
|
5159
5687
|
async function runRootCause(client, input) {
|
|
5160
5688
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
5161
|
-
const
|
|
5689
|
+
const path16 = projectPath(
|
|
5162
5690
|
input.project,
|
|
5163
5691
|
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
5164
5692
|
);
|
|
5165
5693
|
try {
|
|
5166
|
-
const result = await client.get(
|
|
5694
|
+
const result = await client.get(path16);
|
|
5167
5695
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
5168
5696
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
5169
5697
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -5189,12 +5717,12 @@ async function runRootCause(client, input) {
|
|
|
5189
5717
|
}
|
|
5190
5718
|
async function runBlastRadius(client, input) {
|
|
5191
5719
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
5192
|
-
const
|
|
5720
|
+
const path16 = projectPath(
|
|
5193
5721
|
input.project,
|
|
5194
5722
|
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
5195
5723
|
);
|
|
5196
5724
|
try {
|
|
5197
|
-
const result = await client.get(
|
|
5725
|
+
const result = await client.get(path16);
|
|
5198
5726
|
if (result.totalAffected === 0) {
|
|
5199
5727
|
return {
|
|
5200
5728
|
summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
|
|
@@ -5228,12 +5756,12 @@ function formatBlastEntry(n) {
|
|
|
5228
5756
|
}
|
|
5229
5757
|
async function runDependencies(client, input) {
|
|
5230
5758
|
const depth = input.depth ?? 3;
|
|
5231
|
-
const
|
|
5759
|
+
const path16 = projectPath(
|
|
5232
5760
|
input.project,
|
|
5233
5761
|
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
5234
5762
|
);
|
|
5235
5763
|
try {
|
|
5236
|
-
const result = await client.get(
|
|
5764
|
+
const result = await client.get(path16);
|
|
5237
5765
|
if (result.total === 0) {
|
|
5238
5766
|
return {
|
|
5239
5767
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -5325,9 +5853,9 @@ function formatDuration(ms) {
|
|
|
5325
5853
|
return `${Math.round(h / 24)}d`;
|
|
5326
5854
|
}
|
|
5327
5855
|
async function runIncidents(client, input) {
|
|
5328
|
-
const
|
|
5856
|
+
const path16 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
5329
5857
|
try {
|
|
5330
|
-
const body = await client.get(
|
|
5858
|
+
const body = await client.get(path16);
|
|
5331
5859
|
const events = body.events;
|
|
5332
5860
|
if (events.length === 0) {
|
|
5333
5861
|
return {
|
|
@@ -5980,7 +6508,7 @@ function sleep(ms, signal) {
|
|
|
5980
6508
|
}
|
|
5981
6509
|
|
|
5982
6510
|
// src/cli-verbs.ts
|
|
5983
|
-
import
|
|
6511
|
+
import path14 from "path";
|
|
5984
6512
|
async function resolveProjectEntry(opts) {
|
|
5985
6513
|
const entries = await listProjects();
|
|
5986
6514
|
if (opts.project) {
|
|
@@ -5990,7 +6518,7 @@ async function resolveProjectEntry(opts) {
|
|
|
5990
6518
|
const cwd = opts.cwd ?? process.cwd();
|
|
5991
6519
|
const resolvedCwd = await normalizeProjectPath(cwd);
|
|
5992
6520
|
for (const entry2 of entries) {
|
|
5993
|
-
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${
|
|
6521
|
+
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path14.sep}`)) {
|
|
5994
6522
|
return entry2;
|
|
5995
6523
|
}
|
|
5996
6524
|
}
|
|
@@ -6497,15 +7025,15 @@ async function buildPatchSections(services, project) {
|
|
|
6497
7025
|
for (const svc of services) {
|
|
6498
7026
|
const installer = await pickInstaller(svc.dir);
|
|
6499
7027
|
if (!installer) continue;
|
|
6500
|
-
const
|
|
6501
|
-
if (isEmptyPlan(
|
|
6502
|
-
sections.push({ installer: installer.name, plan:
|
|
7028
|
+
const plan6 = await installer.plan(svc.dir, { project });
|
|
7029
|
+
if (isEmptyPlan(plan6) && !plan6.libOnly && plan6.runtimeKind === void 0) continue;
|
|
7030
|
+
sections.push({ installer: installer.name, plan: plan6 });
|
|
6503
7031
|
}
|
|
6504
7032
|
return sections;
|
|
6505
7033
|
}
|
|
6506
7034
|
async function runInit(opts) {
|
|
6507
7035
|
const written = [];
|
|
6508
|
-
const stat = await
|
|
7036
|
+
const stat = await fs13.stat(opts.scanPath).catch(() => null);
|
|
6509
7037
|
if (!stat || !stat.isDirectory()) {
|
|
6510
7038
|
console.error(`neat init: ${opts.scanPath} is not a directory`);
|
|
6511
7039
|
return { exitCode: 2, writtenFiles: written };
|
|
@@ -6514,13 +7042,13 @@ async function runInit(opts) {
|
|
|
6514
7042
|
printDiscoveryReport(opts, services);
|
|
6515
7043
|
const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
|
|
6516
7044
|
const patch = renderPatch(sections);
|
|
6517
|
-
const patchPath =
|
|
7045
|
+
const patchPath = path15.join(opts.scanPath, "neat.patch");
|
|
6518
7046
|
if (opts.dryRun) {
|
|
6519
|
-
await
|
|
7047
|
+
await fs13.writeFile(patchPath, patch, "utf8");
|
|
6520
7048
|
written.push(patchPath);
|
|
6521
7049
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
6522
|
-
const gitignorePath =
|
|
6523
|
-
const gitignoreExists = await
|
|
7050
|
+
const gitignorePath = path15.join(opts.scanPath, ".gitignore");
|
|
7051
|
+
const gitignoreExists = await fs13.stat(gitignorePath).then(() => true).catch(() => false);
|
|
6524
7052
|
const verb = gitignoreExists ? "append" : "create";
|
|
6525
7053
|
console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
|
|
6526
7054
|
console.log("rerun without --dry-run to register and snapshot.");
|
|
@@ -6531,9 +7059,9 @@ async function runInit(opts) {
|
|
|
6531
7059
|
const graph = getGraph(graphKey);
|
|
6532
7060
|
const projectPaths = pathsForProject(
|
|
6533
7061
|
graphKey,
|
|
6534
|
-
|
|
7062
|
+
path15.join(opts.scanPath, "neat-out")
|
|
6535
7063
|
);
|
|
6536
|
-
const errorsPath =
|
|
7064
|
+
const errorsPath = path15.join(path15.dirname(opts.outPath), path15.basename(projectPaths.errorsPath));
|
|
6537
7065
|
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
6538
7066
|
await saveGraphToDisk(graph, opts.outPath);
|
|
6539
7067
|
written.push(opts.outPath);
|
|
@@ -6612,7 +7140,7 @@ async function runInit(opts) {
|
|
|
6612
7140
|
console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
6613
7141
|
}
|
|
6614
7142
|
} else {
|
|
6615
|
-
await
|
|
7143
|
+
await fs13.writeFile(patchPath, patch, "utf8");
|
|
6616
7144
|
written.push(patchPath);
|
|
6617
7145
|
}
|
|
6618
7146
|
}
|
|
@@ -6652,9 +7180,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
6652
7180
|
};
|
|
6653
7181
|
function claudeConfigPath() {
|
|
6654
7182
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
6655
|
-
if (override && override.length > 0) return
|
|
7183
|
+
if (override && override.length > 0) return path15.resolve(override);
|
|
6656
7184
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
6657
|
-
return
|
|
7185
|
+
return path15.join(home, ".claude.json");
|
|
6658
7186
|
}
|
|
6659
7187
|
async function runSkill(opts) {
|
|
6660
7188
|
const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -6666,7 +7194,7 @@ async function runSkill(opts) {
|
|
|
6666
7194
|
const target = claudeConfigPath();
|
|
6667
7195
|
let existing = {};
|
|
6668
7196
|
try {
|
|
6669
|
-
existing = JSON.parse(await
|
|
7197
|
+
existing = JSON.parse(await fs13.readFile(target, "utf8"));
|
|
6670
7198
|
} catch (err) {
|
|
6671
7199
|
if (err.code !== "ENOENT") {
|
|
6672
7200
|
console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
|
|
@@ -6678,8 +7206,8 @@ async function runSkill(opts) {
|
|
|
6678
7206
|
...existing,
|
|
6679
7207
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
6680
7208
|
};
|
|
6681
|
-
await
|
|
6682
|
-
await
|
|
7209
|
+
await fs13.mkdir(path15.dirname(target), { recursive: true });
|
|
7210
|
+
await fs13.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
6683
7211
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
6684
7212
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
6685
7213
|
console.log("");
|
|
@@ -6752,7 +7280,7 @@ async function main() {
|
|
|
6752
7280
|
}
|
|
6753
7281
|
const cmd = argvParsed.positional[0];
|
|
6754
7282
|
const parsed = { ...argvParsed, positional: argvParsed.positional.slice(1) };
|
|
6755
|
-
const { positional, apply:
|
|
7283
|
+
const { positional, apply: apply6, dryRun, noInstall } = parsed;
|
|
6756
7284
|
const project = parsed.project ?? DEFAULT_PROJECT;
|
|
6757
7285
|
if (cmd === "init") {
|
|
6758
7286
|
const target = positional[0];
|
|
@@ -6761,22 +7289,22 @@ async function main() {
|
|
|
6761
7289
|
usage4();
|
|
6762
7290
|
process.exit(2);
|
|
6763
7291
|
}
|
|
6764
|
-
if (
|
|
7292
|
+
if (apply6 && dryRun) {
|
|
6765
7293
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
6766
7294
|
process.exit(2);
|
|
6767
7295
|
}
|
|
6768
|
-
const scanPath =
|
|
7296
|
+
const scanPath = path15.resolve(target);
|
|
6769
7297
|
const projectExplicit = parsed.project !== null;
|
|
6770
|
-
const projectName = projectExplicit ? project :
|
|
7298
|
+
const projectName = projectExplicit ? project : path15.basename(scanPath);
|
|
6771
7299
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
6772
|
-
const fallback = pathsForProject(projectKey,
|
|
6773
|
-
const outPath =
|
|
7300
|
+
const fallback = pathsForProject(projectKey, path15.join(scanPath, "neat-out")).snapshotPath;
|
|
7301
|
+
const outPath = path15.resolve(process.env.NEAT_OUT_PATH ?? fallback);
|
|
6774
7302
|
const result = await runInit({
|
|
6775
7303
|
scanPath,
|
|
6776
7304
|
outPath,
|
|
6777
7305
|
project: projectName,
|
|
6778
7306
|
projectExplicit,
|
|
6779
|
-
apply:
|
|
7307
|
+
apply: apply6,
|
|
6780
7308
|
dryRun,
|
|
6781
7309
|
noInstall,
|
|
6782
7310
|
verbose: parsed.verbose
|
|
@@ -6791,21 +7319,21 @@ async function main() {
|
|
|
6791
7319
|
usage4();
|
|
6792
7320
|
process.exit(2);
|
|
6793
7321
|
}
|
|
6794
|
-
const scanPath =
|
|
6795
|
-
const stat = await
|
|
7322
|
+
const scanPath = path15.resolve(target);
|
|
7323
|
+
const stat = await fs13.stat(scanPath).catch(() => null);
|
|
6796
7324
|
if (!stat || !stat.isDirectory()) {
|
|
6797
7325
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
6798
7326
|
process.exit(2);
|
|
6799
7327
|
}
|
|
6800
|
-
const projectPaths = pathsForProject(project,
|
|
6801
|
-
const outPath =
|
|
6802
|
-
const errorsPath =
|
|
6803
|
-
process.env.NEAT_ERRORS_PATH ??
|
|
7328
|
+
const projectPaths = pathsForProject(project, path15.join(scanPath, "neat-out"));
|
|
7329
|
+
const outPath = path15.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
|
|
7330
|
+
const errorsPath = path15.resolve(
|
|
7331
|
+
process.env.NEAT_ERRORS_PATH ?? path15.join(path15.dirname(outPath), path15.basename(projectPaths.errorsPath))
|
|
6804
7332
|
);
|
|
6805
|
-
const staleEventsPath =
|
|
6806
|
-
process.env.NEAT_STALE_EVENTS_PATH ??
|
|
7333
|
+
const staleEventsPath = path15.resolve(
|
|
7334
|
+
process.env.NEAT_STALE_EVENTS_PATH ?? path15.join(path15.dirname(outPath), path15.basename(projectPaths.staleEventsPath))
|
|
6807
7335
|
);
|
|
6808
|
-
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ?
|
|
7336
|
+
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path15.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
|
|
6809
7337
|
const handle = await startWatch(getGraph(project), {
|
|
6810
7338
|
scanPath,
|
|
6811
7339
|
outPath,
|
|
@@ -6814,7 +7342,7 @@ async function main() {
|
|
|
6814
7342
|
project,
|
|
6815
7343
|
// Resolve NEAT_HOME so a `neat watch` picks up connectors added to
|
|
6816
7344
|
// ~/.neat/connectors.json (#871). Same resolution the rest of the CLI uses.
|
|
6817
|
-
neatHome: process.env.NEAT_HOME ?
|
|
7345
|
+
neatHome: process.env.NEAT_HOME ? path15.resolve(process.env.NEAT_HOME) : path15.join(os4.homedir(), ".neat"),
|
|
6818
7346
|
...embeddingsCachePath ? { embeddingsCachePath } : {},
|
|
6819
7347
|
host: process.env.HOST ?? "0.0.0.0",
|
|
6820
7348
|
port: Number(process.env.PORT ?? 8080),
|
|
@@ -6996,11 +7524,11 @@ async function main() {
|
|
|
6996
7524
|
process.exit(1);
|
|
6997
7525
|
}
|
|
6998
7526
|
async function tryOrchestrator(cmd, parsed) {
|
|
6999
|
-
const scanPath =
|
|
7000
|
-
const stat = await
|
|
7527
|
+
const scanPath = path15.resolve(cmd);
|
|
7528
|
+
const stat = await fs13.stat(scanPath).catch(() => null);
|
|
7001
7529
|
if (!stat || !stat.isDirectory()) return null;
|
|
7002
7530
|
const projectExplicit = parsed.project !== null;
|
|
7003
|
-
const projectName = projectExplicit ? parsed.project :
|
|
7531
|
+
const projectName = projectExplicit ? parsed.project : path15.basename(scanPath);
|
|
7004
7532
|
const result = await runOrchestrator({
|
|
7005
7533
|
scanPath,
|
|
7006
7534
|
project: projectName,
|