@neat.is/core 0.7.9 → 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-N5TPODCX.js → chunk-6T7ZHODF.js} +486 -18
- package/dist/chunk-6T7ZHODF.js.map +1 -0
- package/dist/{chunk-ILG3SMD5.js → chunk-D4OX6MUX.js} +6 -2
- package/dist/chunk-D4OX6MUX.js.map +1 -0
- package/dist/cli.cjs +1256 -252
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +689 -164
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +505 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +505 -24
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +494 -21
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-ILG3SMD5.js.map +0 -1
- package/dist/chunk-N5TPODCX.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ 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
12
|
} from "./chunk-BC53SCT7.js";
|
|
@@ -74,7 +74,7 @@ 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
80
|
} from "./chunk-FCO5Z3RW.js";
|
|
@@ -88,9 +88,9 @@ import {
|
|
|
88
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}`
|
|
@@ -3107,13 +3110,516 @@ async function apply3(installPlan) {
|
|
|
3107
3110
|
await fs6.writeFile(generated.file, generated.contents, "utf8");
|
|
3108
3111
|
writtenFiles.push(generated.file);
|
|
3109
3112
|
}
|
|
3110
|
-
|
|
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
|
+
};
|
|
3111
3120
|
}
|
|
3112
3121
|
var goInstaller = { name: "go", detect: detect3, plan: plan3, apply: apply3 };
|
|
3113
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
|
+
|
|
3114
3620
|
// src/installers/shared.ts
|
|
3115
|
-
function isEmptyPlan(
|
|
3116
|
-
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;
|
|
3117
3623
|
}
|
|
3118
3624
|
|
|
3119
3625
|
// src/installers/index.ts
|
|
@@ -3125,9 +3631,16 @@ var FORBIDDEN_LOCKFILES = /* @__PURE__ */ new Set([
|
|
|
3125
3631
|
"Pipfile.lock",
|
|
3126
3632
|
"Gemfile.lock",
|
|
3127
3633
|
"Cargo.lock",
|
|
3128
|
-
"go.sum"
|
|
3634
|
+
"go.sum",
|
|
3635
|
+
"composer.lock"
|
|
3129
3636
|
]);
|
|
3130
|
-
var INSTALLERS = [
|
|
3637
|
+
var INSTALLERS = [
|
|
3638
|
+
javascriptInstaller,
|
|
3639
|
+
pythonInstaller,
|
|
3640
|
+
goInstaller,
|
|
3641
|
+
rubyInstaller,
|
|
3642
|
+
phpInstaller
|
|
3643
|
+
];
|
|
3131
3644
|
async function pickInstaller(serviceDir) {
|
|
3132
3645
|
for (const inst of INSTALLERS) {
|
|
3133
3646
|
if (await inst.detect(serviceDir)) return inst;
|
|
@@ -3142,7 +3655,7 @@ function renderPatch(sections) {
|
|
|
3142
3655
|
"No SDK installers matched the discovered services. Two reasons this",
|
|
3143
3656
|
"normally happens:",
|
|
3144
3657
|
" - the project uses a language NEAT does not yet instrument",
|
|
3145
|
-
" (Java /
|
|
3658
|
+
" (Java / .NET / Rust are out of scope per ADR-047);",
|
|
3146
3659
|
" - the SDK is already installed, so the installer returned an empty",
|
|
3147
3660
|
" plan.",
|
|
3148
3661
|
"",
|
|
@@ -3152,22 +3665,22 @@ function renderPatch(sections) {
|
|
|
3152
3665
|
}
|
|
3153
3666
|
const lines = ["# neat install plan", ""];
|
|
3154
3667
|
for (const section of sections) {
|
|
3155
|
-
const { installer, plan:
|
|
3156
|
-
lines.push(`## ${installer} (${
|
|
3668
|
+
const { installer, plan: plan6 } = section;
|
|
3669
|
+
lines.push(`## ${installer} (${plan6.language}) \u2014 ${plan6.serviceDir}`);
|
|
3157
3670
|
lines.push("");
|
|
3158
|
-
if (
|
|
3671
|
+
if (plan6.libOnly) {
|
|
3159
3672
|
lines.push("### skipped \u2014 no resolvable entry point (lib-only)");
|
|
3160
3673
|
lines.push("");
|
|
3161
3674
|
continue;
|
|
3162
3675
|
}
|
|
3163
|
-
if (
|
|
3164
|
-
lines.push(`entry: ${
|
|
3676
|
+
if (plan6.entryFile) {
|
|
3677
|
+
lines.push(`entry: ${plan6.entryFile}`);
|
|
3165
3678
|
lines.push("");
|
|
3166
3679
|
}
|
|
3167
|
-
if (
|
|
3680
|
+
if (plan6.dependencyEdits.length > 0) {
|
|
3168
3681
|
lines.push("### dependencies");
|
|
3169
3682
|
const byFile = /* @__PURE__ */ new Map();
|
|
3170
|
-
for (const dep of
|
|
3683
|
+
for (const dep of plan6.dependencyEdits) {
|
|
3171
3684
|
const base = dep.file.split(/[\\/]/).pop() ?? dep.file;
|
|
3172
3685
|
if (FORBIDDEN_LOCKFILES.has(base)) {
|
|
3173
3686
|
throw new Error(
|
|
@@ -3186,9 +3699,9 @@ function renderPatch(sections) {
|
|
|
3186
3699
|
}
|
|
3187
3700
|
lines.push("");
|
|
3188
3701
|
}
|
|
3189
|
-
if (
|
|
3702
|
+
if (plan6.generatedFiles && plan6.generatedFiles.length > 0) {
|
|
3190
3703
|
lines.push("### generated files");
|
|
3191
|
-
for (const gen of
|
|
3704
|
+
for (const gen of plan6.generatedFiles) {
|
|
3192
3705
|
lines.push(`--- (new file) ${gen.file}`);
|
|
3193
3706
|
for (const ln of gen.contents.split(/\r?\n/)) {
|
|
3194
3707
|
lines.push(`+ ${ln}`);
|
|
@@ -3196,26 +3709,26 @@ function renderPatch(sections) {
|
|
|
3196
3709
|
}
|
|
3197
3710
|
lines.push("");
|
|
3198
3711
|
}
|
|
3199
|
-
if (
|
|
3712
|
+
if (plan6.entrypointEdits.length > 0) {
|
|
3200
3713
|
lines.push("### entry-point injection");
|
|
3201
|
-
for (const e of
|
|
3714
|
+
for (const e of plan6.entrypointEdits) {
|
|
3202
3715
|
lines.push(`--- ${e.file}`);
|
|
3203
3716
|
lines.push(`+ ${e.after}`);
|
|
3204
3717
|
lines.push(` ${e.before}`);
|
|
3205
3718
|
}
|
|
3206
3719
|
lines.push("");
|
|
3207
3720
|
}
|
|
3208
|
-
if (
|
|
3721
|
+
if (plan6.envEdits.length > 0) {
|
|
3209
3722
|
lines.push("### env (written to <package-dir>/.env.neat)");
|
|
3210
|
-
for (const env of
|
|
3723
|
+
for (const env of plan6.envEdits) {
|
|
3211
3724
|
lines.push(`- ${env.key}=${env.value}`);
|
|
3212
3725
|
}
|
|
3213
3726
|
lines.push("");
|
|
3214
3727
|
}
|
|
3215
|
-
if (
|
|
3728
|
+
if (plan6.nextConfigEdit) {
|
|
3216
3729
|
lines.push("### next.config (framework flag)");
|
|
3217
|
-
lines.push(`--- ${
|
|
3218
|
-
lines.push(`+ experimental: { instrumentationHook: true }, // ${
|
|
3730
|
+
lines.push(`--- ${plan6.nextConfigEdit.file}`);
|
|
3731
|
+
lines.push(`+ experimental: { instrumentationHook: true }, // ${plan6.nextConfigEdit.reason}`);
|
|
3219
3732
|
lines.push("");
|
|
3220
3733
|
}
|
|
3221
3734
|
}
|
|
@@ -3223,10 +3736,10 @@ function renderPatch(sections) {
|
|
|
3223
3736
|
}
|
|
3224
3737
|
|
|
3225
3738
|
// src/orchestrator.ts
|
|
3226
|
-
import { promises as
|
|
3739
|
+
import { promises as fs9 } from "fs";
|
|
3227
3740
|
import http from "http";
|
|
3228
3741
|
import net from "net";
|
|
3229
|
-
import
|
|
3742
|
+
import path10 from "path";
|
|
3230
3743
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3231
3744
|
import { spawn as spawn2 } from "child_process";
|
|
3232
3745
|
import readline from "readline";
|
|
@@ -3236,7 +3749,7 @@ async function extractAndPersist(opts) {
|
|
|
3236
3749
|
const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
|
|
3237
3750
|
resetGraph(graphKey);
|
|
3238
3751
|
const graph = getGraph(graphKey);
|
|
3239
|
-
const projectPaths = pathsForProject(graphKey,
|
|
3752
|
+
const projectPaths = pathsForProject(graphKey, path10.join(opts.scanPath, "neat-out"));
|
|
3240
3753
|
const extraction = await extractFromDirectory(graph, opts.scanPath, {
|
|
3241
3754
|
errorsPath: projectPaths.errorsPath
|
|
3242
3755
|
});
|
|
@@ -3267,28 +3780,34 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3267
3780
|
let cloudflareWorkers = 0;
|
|
3268
3781
|
let electron = 0;
|
|
3269
3782
|
const installPlans = /* @__PURE__ */ new Map();
|
|
3783
|
+
const dependencyInstructions = /* @__PURE__ */ new Map();
|
|
3270
3784
|
for (const svc of services) {
|
|
3271
3785
|
const installer = await pickInstaller(svc.dir);
|
|
3272
3786
|
if (!installer) continue;
|
|
3273
|
-
const
|
|
3274
|
-
if (isEmptyPlan(
|
|
3787
|
+
const plan6 = await installer.plan(svc.dir, { project });
|
|
3788
|
+
if (isEmptyPlan(plan6) && !plan6.libOnly && plan6.runtimeKind === void 0) {
|
|
3275
3789
|
already++;
|
|
3276
3790
|
continue;
|
|
3277
3791
|
}
|
|
3278
|
-
const outcome = await installer.apply(
|
|
3792
|
+
const outcome = await installer.apply(plan6);
|
|
3279
3793
|
if (outcome.outcome === "instrumented") {
|
|
3280
3794
|
instrumented++;
|
|
3281
|
-
if (
|
|
3282
|
-
const
|
|
3283
|
-
|
|
3284
|
-
|
|
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
|
+
}
|
|
3285
3804
|
}
|
|
3286
3805
|
} else if (outcome.outcome === "already-instrumented") already++;
|
|
3287
3806
|
else if (outcome.outcome === "lib-only") {
|
|
3288
3807
|
libOnly++;
|
|
3289
3808
|
const appDeps = svc.pkg ? appFrameworkDependencies(svc.pkg) : [];
|
|
3290
3809
|
if (appDeps.length > 0) {
|
|
3291
|
-
const svcName =
|
|
3810
|
+
const svcName = path10.basename(svc.dir);
|
|
3292
3811
|
const list = appDeps.join(", ");
|
|
3293
3812
|
console.warn(
|
|
3294
3813
|
`neat: runtime layer won't engage for ${svcName}: no entry point found.
|
|
@@ -3301,7 +3820,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3301
3820
|
console.log(`skipping ${svc.dir}: browser bundle; browser-OTel support lands in a future release.`);
|
|
3302
3821
|
} else if (outcome.outcome === "react-native") {
|
|
3303
3822
|
reactNative++;
|
|
3304
|
-
const svcName =
|
|
3823
|
+
const svcName = path10.basename(svc.dir);
|
|
3305
3824
|
console.log(
|
|
3306
3825
|
`neat: ${svc.dir} detected as React Native / Expo
|
|
3307
3826
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -3312,7 +3831,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3312
3831
|
);
|
|
3313
3832
|
} else if (outcome.outcome === "bun") {
|
|
3314
3833
|
bun++;
|
|
3315
|
-
const svcName =
|
|
3834
|
+
const svcName = path10.basename(svc.dir);
|
|
3316
3835
|
console.log(
|
|
3317
3836
|
`neat: ${svc.dir} detected as Bun
|
|
3318
3837
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -3323,7 +3842,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3323
3842
|
);
|
|
3324
3843
|
} else if (outcome.outcome === "deno") {
|
|
3325
3844
|
deno++;
|
|
3326
|
-
const svcName =
|
|
3845
|
+
const svcName = path10.basename(svc.dir);
|
|
3327
3846
|
console.log(
|
|
3328
3847
|
`neat: ${svc.dir} detected as Deno
|
|
3329
3848
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -3334,7 +3853,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3334
3853
|
);
|
|
3335
3854
|
} else if (outcome.outcome === "cloudflare-workers") {
|
|
3336
3855
|
cloudflareWorkers++;
|
|
3337
|
-
const svcName =
|
|
3856
|
+
const svcName = path10.basename(svc.dir);
|
|
3338
3857
|
console.log(
|
|
3339
3858
|
`neat: ${svc.dir} detected as Cloudflare Workers
|
|
3340
3859
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -3345,7 +3864,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3345
3864
|
);
|
|
3346
3865
|
} else if (outcome.outcome === "electron") {
|
|
3347
3866
|
electron++;
|
|
3348
|
-
const svcName =
|
|
3867
|
+
const svcName = path10.basename(svc.dir);
|
|
3349
3868
|
console.log(
|
|
3350
3869
|
`neat: ${svc.dir} detected as Electron
|
|
3351
3870
|
The installer doesn't cover this runtime deterministically.
|
|
@@ -3358,7 +3877,7 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3358
3877
|
if (svc.pkg && (outcome.outcome === "instrumented" || outcome.outcome === "already-instrumented")) {
|
|
3359
3878
|
const gaps = uninstrumentedLibraries(svc.pkg);
|
|
3360
3879
|
if (gaps.length > 0) {
|
|
3361
|
-
const svcName =
|
|
3880
|
+
const svcName = path10.basename(svc.dir);
|
|
3362
3881
|
const list = gaps.join(", ");
|
|
3363
3882
|
const subject = gaps.length === 1 ? "this library" : "these libraries";
|
|
3364
3883
|
const aux = gaps.length === 1 ? "isn't" : "aren't";
|
|
@@ -3386,6 +3905,11 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3386
3905
|
}
|
|
3387
3906
|
}
|
|
3388
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
|
+
}
|
|
3389
3913
|
return {
|
|
3390
3914
|
instrumented,
|
|
3391
3915
|
alreadyInstrumented: already,
|
|
@@ -3396,7 +3920,8 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
3396
3920
|
deno,
|
|
3397
3921
|
cloudflareWorkers,
|
|
3398
3922
|
electron,
|
|
3399
|
-
packageManagerInstalls
|
|
3923
|
+
packageManagerInstalls,
|
|
3924
|
+
dependencyInstructions: [...dependencyInstructions].map(([dir, command]) => ({ dir, command }))
|
|
3400
3925
|
};
|
|
3401
3926
|
}
|
|
3402
3927
|
async function promptYesNo(question) {
|
|
@@ -3564,24 +4089,24 @@ async function persistedPortsFor(scanPath) {
|
|
|
3564
4089
|
return { rest: record.ports.rest, otlp: record.ports.otlp, web: record.ports.web };
|
|
3565
4090
|
}
|
|
3566
4091
|
async function acquireSpawnLock(scanPath) {
|
|
3567
|
-
const lockPath =
|
|
3568
|
-
await
|
|
4092
|
+
const lockPath = path10.join(scanPath, "neat-out", "daemon.spawn.lock");
|
|
4093
|
+
await fs9.mkdir(path10.dirname(lockPath), { recursive: true });
|
|
3569
4094
|
const STALE_LOCK_MS = 6e4;
|
|
3570
4095
|
try {
|
|
3571
|
-
const fd = await
|
|
4096
|
+
const fd = await fs9.open(lockPath, "wx");
|
|
3572
4097
|
await fd.writeFile(`${process.pid}
|
|
3573
4098
|
`, "utf8");
|
|
3574
4099
|
await fd.close();
|
|
3575
4100
|
return async () => {
|
|
3576
|
-
await
|
|
4101
|
+
await fs9.unlink(lockPath).catch(() => {
|
|
3577
4102
|
});
|
|
3578
4103
|
};
|
|
3579
4104
|
} catch (err) {
|
|
3580
4105
|
if (err.code !== "EEXIST") return null;
|
|
3581
4106
|
try {
|
|
3582
|
-
const stat = await
|
|
4107
|
+
const stat = await fs9.stat(lockPath);
|
|
3583
4108
|
if (Date.now() - stat.mtimeMs > STALE_LOCK_MS) {
|
|
3584
|
-
await
|
|
4109
|
+
await fs9.unlink(lockPath).catch(() => {
|
|
3585
4110
|
});
|
|
3586
4111
|
return acquireSpawnLock(scanPath);
|
|
3587
4112
|
}
|
|
@@ -3610,13 +4135,13 @@ async function healthIsForProject(restPort, project) {
|
|
|
3610
4135
|
return false;
|
|
3611
4136
|
}
|
|
3612
4137
|
function daemonLogPath(projectPath3) {
|
|
3613
|
-
return
|
|
4138
|
+
return path10.join(projectPath3, "neat-out", "daemon.log");
|
|
3614
4139
|
}
|
|
3615
4140
|
function spawnDaemonDetached(spec) {
|
|
3616
|
-
const here =
|
|
4141
|
+
const here = path10.dirname(fileURLToPath2(import.meta.url));
|
|
3617
4142
|
const candidates = [
|
|
3618
|
-
|
|
3619
|
-
|
|
4143
|
+
path10.join(here, "neatd.cjs"),
|
|
4144
|
+
path10.join(here, "neatd.js")
|
|
3620
4145
|
];
|
|
3621
4146
|
let entry2 = null;
|
|
3622
4147
|
const fsSync = __require("fs");
|
|
@@ -3646,7 +4171,7 @@ function spawnDaemonDetached(spec) {
|
|
|
3646
4171
|
let logFd = null;
|
|
3647
4172
|
if (spec) {
|
|
3648
4173
|
const logPath = daemonLogPath(spec.projectPath);
|
|
3649
|
-
fsSync.mkdirSync(
|
|
4174
|
+
fsSync.mkdirSync(path10.dirname(logPath), { recursive: true });
|
|
3650
4175
|
logFd = fsSync.openSync(logPath, "a");
|
|
3651
4176
|
}
|
|
3652
4177
|
const child = spawn2(process.execPath, [entry2, "start"], {
|
|
@@ -3685,7 +4210,7 @@ async function runOrchestrator(opts) {
|
|
|
3685
4210
|
browser: "skipped"
|
|
3686
4211
|
}
|
|
3687
4212
|
};
|
|
3688
|
-
const stat = await
|
|
4213
|
+
const stat = await fs9.stat(opts.scanPath).catch(() => null);
|
|
3689
4214
|
if (!stat || !stat.isDirectory()) {
|
|
3690
4215
|
console.error(`neat: ${opts.scanPath} is not a directory`);
|
|
3691
4216
|
result.exitCode = 2;
|
|
@@ -3845,7 +4370,7 @@ async function runOrchestrator(opts) {
|
|
|
3845
4370
|
result.steps.browser = openBrowser(dashboardUrl);
|
|
3846
4371
|
}
|
|
3847
4372
|
const daemonRunning = result.steps.daemon === "spawned" || result.steps.daemon === "already-running";
|
|
3848
|
-
const daemonLog = daemonRunning ?
|
|
4373
|
+
const daemonLog = daemonRunning ? path10.relative(opts.scanPath, daemonLogPath(opts.scanPath)) : null;
|
|
3849
4374
|
printSummary(result, graph, dashboardUrl, daemonLog);
|
|
3850
4375
|
return result;
|
|
3851
4376
|
}
|
|
@@ -4302,27 +4827,27 @@ async function runConnectorCommand(rawArgs, deps = {}) {
|
|
|
4302
4827
|
}
|
|
4303
4828
|
|
|
4304
4829
|
// src/hooks-cli.ts
|
|
4305
|
-
import
|
|
4830
|
+
import path11 from "path";
|
|
4306
4831
|
import os from "os";
|
|
4307
|
-
import { promises as
|
|
4832
|
+
import { promises as fs10 } from "fs";
|
|
4308
4833
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4309
4834
|
var HOOK_FILENAME = "neat-search-nudge.mjs";
|
|
4310
4835
|
var GUIDE_FILENAME = "GRAPH_FIRST.md";
|
|
4311
4836
|
var GUIDE_INSTALL_NAME = "neat-graph-first.md";
|
|
4312
4837
|
var HOOK_MATCHER = "Grep|Glob|Bash";
|
|
4313
4838
|
function moduleDir() {
|
|
4314
|
-
return typeof __dirname !== "undefined" ? __dirname :
|
|
4839
|
+
return typeof __dirname !== "undefined" ? __dirname : path11.dirname(fileURLToPath3(import.meta.url));
|
|
4315
4840
|
}
|
|
4316
4841
|
async function readSkillAsset(rel) {
|
|
4317
4842
|
const here = moduleDir();
|
|
4318
4843
|
const candidates = [
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4844
|
+
path11.resolve(here, "../../claude-skill", rel),
|
|
4845
|
+
path11.resolve(here, "../../../claude-skill", rel),
|
|
4846
|
+
path11.resolve(here, "../claude-skill", rel)
|
|
4322
4847
|
];
|
|
4323
4848
|
for (const candidate of candidates) {
|
|
4324
4849
|
try {
|
|
4325
|
-
return await
|
|
4850
|
+
return await fs10.readFile(candidate, "utf8");
|
|
4326
4851
|
} catch {
|
|
4327
4852
|
}
|
|
4328
4853
|
}
|
|
@@ -4332,17 +4857,17 @@ async function readSkillAsset(rel) {
|
|
|
4332
4857
|
}
|
|
4333
4858
|
function neatHome() {
|
|
4334
4859
|
const override = process.env.NEAT_HOME;
|
|
4335
|
-
if (override && override.length > 0) return
|
|
4336
|
-
return
|
|
4860
|
+
if (override && override.length > 0) return path11.resolve(override);
|
|
4861
|
+
return path11.join(os.homedir(), ".neat");
|
|
4337
4862
|
}
|
|
4338
4863
|
function claudeSettingsPath() {
|
|
4339
4864
|
const override = process.env.NEAT_CLAUDE_SETTINGS;
|
|
4340
|
-
if (override && override.length > 0) return
|
|
4865
|
+
if (override && override.length > 0) return path11.resolve(override);
|
|
4341
4866
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? os.homedir();
|
|
4342
|
-
return
|
|
4867
|
+
return path11.join(home, ".claude", "settings.json");
|
|
4343
4868
|
}
|
|
4344
4869
|
function installedHookPath() {
|
|
4345
|
-
return
|
|
4870
|
+
return path11.join(neatHome(), "hooks", HOOK_FILENAME);
|
|
4346
4871
|
}
|
|
4347
4872
|
function isNeatSearchEntry(entry2) {
|
|
4348
4873
|
return (entry2.hooks ?? []).some(
|
|
@@ -4375,14 +4900,14 @@ async function runHooks(opts) {
|
|
|
4375
4900
|
const hookScript = await readSkillAsset(`hooks/${HOOK_FILENAME}`);
|
|
4376
4901
|
const guide = await readSkillAsset(GUIDE_FILENAME);
|
|
4377
4902
|
const scriptPath = installedHookPath();
|
|
4378
|
-
await
|
|
4379
|
-
await
|
|
4380
|
-
const guidePath =
|
|
4381
|
-
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");
|
|
4382
4907
|
const settingsFile = claudeSettingsPath();
|
|
4383
4908
|
let settings = {};
|
|
4384
4909
|
try {
|
|
4385
|
-
settings = JSON.parse(await
|
|
4910
|
+
settings = JSON.parse(await fs10.readFile(settingsFile, "utf8"));
|
|
4386
4911
|
} catch (err) {
|
|
4387
4912
|
if (err.code !== "ENOENT") {
|
|
4388
4913
|
console.error(
|
|
@@ -4404,8 +4929,8 @@ async function runHooks(opts) {
|
|
|
4404
4929
|
...settings,
|
|
4405
4930
|
hooks: { ...hooks, PreToolUse: preToolUse }
|
|
4406
4931
|
};
|
|
4407
|
-
await
|
|
4408
|
-
await
|
|
4932
|
+
await fs10.mkdir(path11.dirname(settingsFile), { recursive: true });
|
|
4933
|
+
await fs10.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
4409
4934
|
console.log(`neat hooks: installed the search-nudge hook`);
|
|
4410
4935
|
console.log(` script: ${scriptPath}`);
|
|
4411
4936
|
console.log(` settings: ${settingsFile} (PreToolUse \u2192 ${HOOK_MATCHER})`);
|
|
@@ -4475,9 +5000,9 @@ async function runHooksCommand(args) {
|
|
|
4475
5000
|
}
|
|
4476
5001
|
|
|
4477
5002
|
// src/codex-cli.ts
|
|
4478
|
-
import
|
|
5003
|
+
import path12 from "path";
|
|
4479
5004
|
import os2 from "os";
|
|
4480
|
-
import { promises as
|
|
5005
|
+
import { promises as fs11 } from "fs";
|
|
4481
5006
|
import { isDeepStrictEqual } from "util";
|
|
4482
5007
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
4483
5008
|
var CODEX_MCP_SERVER = {
|
|
@@ -4495,14 +5020,14 @@ var NEAT_GRAPH_FIRST_START = "<!-- neat:graph-first -->";
|
|
|
4495
5020
|
var NEAT_GRAPH_FIRST_END = "<!-- /neat:graph-first -->";
|
|
4496
5021
|
function codexConfigPath() {
|
|
4497
5022
|
const override = process.env.NEAT_CODEX_CONFIG;
|
|
4498
|
-
if (override && override.length > 0) return
|
|
5023
|
+
if (override && override.length > 0) return path12.resolve(override);
|
|
4499
5024
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? os2.homedir();
|
|
4500
|
-
return
|
|
5025
|
+
return path12.join(home, ".codex", "config.toml");
|
|
4501
5026
|
}
|
|
4502
5027
|
function agentsFilePath() {
|
|
4503
5028
|
const override = process.env.NEAT_CODEX_AGENTS;
|
|
4504
|
-
if (override && override.length > 0) return
|
|
4505
|
-
return
|
|
5029
|
+
if (override && override.length > 0) return path12.resolve(override);
|
|
5030
|
+
return path12.join(process.cwd(), "AGENTS.md");
|
|
4506
5031
|
}
|
|
4507
5032
|
function isTableHeader(line) {
|
|
4508
5033
|
return /^\s*\[\[?[^\]]+\]\]?\s*$/.test(line);
|
|
@@ -4636,7 +5161,7 @@ async function runCodex(opts) {
|
|
|
4636
5161
|
const agentsPath = agentsFilePath();
|
|
4637
5162
|
let configRaw = "";
|
|
4638
5163
|
try {
|
|
4639
|
-
configRaw = await
|
|
5164
|
+
configRaw = await fs11.readFile(configPath, "utf8");
|
|
4640
5165
|
} catch (err) {
|
|
4641
5166
|
if (err.code !== "ENOENT") {
|
|
4642
5167
|
console.error(`neat codex: failed to read ${configPath} \u2014 ${err.message}`);
|
|
@@ -4645,7 +5170,7 @@ async function runCodex(opts) {
|
|
|
4645
5170
|
}
|
|
4646
5171
|
let agentsRaw = "";
|
|
4647
5172
|
try {
|
|
4648
|
-
agentsRaw = await
|
|
5173
|
+
agentsRaw = await fs11.readFile(agentsPath, "utf8");
|
|
4649
5174
|
} catch (err) {
|
|
4650
5175
|
if (err.code !== "ENOENT") {
|
|
4651
5176
|
console.error(`neat codex: failed to read ${agentsPath} \u2014 ${err.message}`);
|
|
@@ -4685,15 +5210,15 @@ async function runCodex(opts) {
|
|
|
4685
5210
|
return { exitCode: 0 };
|
|
4686
5211
|
}
|
|
4687
5212
|
if (config.changed) {
|
|
4688
|
-
await
|
|
4689
|
-
await
|
|
5213
|
+
await fs11.mkdir(path12.dirname(configPath), { recursive: true });
|
|
5214
|
+
await fs11.writeFile(configPath, config.text, "utf8");
|
|
4690
5215
|
console.log(`neat codex: wrote [mcp_servers.neat] to ${configPath}`);
|
|
4691
5216
|
} else {
|
|
4692
5217
|
console.log(`neat codex: ${configPath} already has NEAT's MCP server`);
|
|
4693
5218
|
}
|
|
4694
5219
|
if (agents.changed) {
|
|
4695
|
-
await
|
|
4696
|
-
await
|
|
5220
|
+
await fs11.mkdir(path12.dirname(agentsPath), { recursive: true });
|
|
5221
|
+
await fs11.writeFile(agentsPath, agents.text, "utf8");
|
|
4697
5222
|
console.log(`neat codex: wrote the graph-first block to ${agentsPath}`);
|
|
4698
5223
|
} else {
|
|
4699
5224
|
console.log(`neat codex: ${agentsPath} already has the graph-first block`);
|
|
@@ -4749,9 +5274,9 @@ async function runCodexCommand(args) {
|
|
|
4749
5274
|
}
|
|
4750
5275
|
|
|
4751
5276
|
// src/editors-cli.ts
|
|
4752
|
-
import
|
|
5277
|
+
import path13 from "path";
|
|
4753
5278
|
import os3 from "os";
|
|
4754
|
-
import { promises as
|
|
5279
|
+
import { promises as fs12 } from "fs";
|
|
4755
5280
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
4756
5281
|
import * as jsonc from "jsonc-parser";
|
|
4757
5282
|
var NEAT_MCP_SERVER = {
|
|
@@ -4775,17 +5300,17 @@ function homeDir() {
|
|
|
4775
5300
|
}
|
|
4776
5301
|
function xdgConfigDir() {
|
|
4777
5302
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
4778
|
-
return xdg && xdg.length > 0 ?
|
|
5303
|
+
return xdg && xdg.length > 0 ? path13.resolve(xdg) : path13.join(homeDir(), ".config");
|
|
4779
5304
|
}
|
|
4780
5305
|
function envOverride(name) {
|
|
4781
5306
|
const v = process.env[name];
|
|
4782
|
-
return v && v.length > 0 ?
|
|
5307
|
+
return v && v.length > 0 ? path13.resolve(v) : void 0;
|
|
4783
5308
|
}
|
|
4784
5309
|
var CURSOR_CLIENT = {
|
|
4785
5310
|
id: "cursor",
|
|
4786
5311
|
label: "Cursor",
|
|
4787
5312
|
docsUrl: "https://docs.cursor.com/context/mcp",
|
|
4788
|
-
mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ??
|
|
5313
|
+
mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ?? path13.join(homeDir(), ".cursor", "mcp.json"),
|
|
4789
5314
|
mcpContainerKey: "mcpServers",
|
|
4790
5315
|
format: "json",
|
|
4791
5316
|
// Cursor still reads a single `.cursorrules` at the project root (the modern
|
|
@@ -4797,7 +5322,7 @@ var DEVIN_CLIENT = {
|
|
|
4797
5322
|
id: "devin",
|
|
4798
5323
|
label: "Devin Desktop (Cascade)",
|
|
4799
5324
|
docsUrl: "https://docs.devin.ai/desktop/cascade/mcp",
|
|
4800
|
-
mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ??
|
|
5325
|
+
mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? path13.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
|
|
4801
5326
|
mcpContainerKey: "mcpServers",
|
|
4802
5327
|
format: "json",
|
|
4803
5328
|
rulesFileName: ".windsurfrules"
|
|
@@ -4806,7 +5331,7 @@ var GEMINI_CLIENT = {
|
|
|
4806
5331
|
id: "gemini",
|
|
4807
5332
|
label: "Gemini CLI",
|
|
4808
5333
|
docsUrl: "https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md",
|
|
4809
|
-
mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ??
|
|
5334
|
+
mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? path13.join(homeDir(), ".gemini", "settings.json"),
|
|
4810
5335
|
mcpContainerKey: "mcpServers",
|
|
4811
5336
|
format: "json",
|
|
4812
5337
|
rulesFileName: "GEMINI.md"
|
|
@@ -4815,7 +5340,7 @@ var QWEN_CLIENT = {
|
|
|
4815
5340
|
id: "qwen",
|
|
4816
5341
|
label: "Qwen Code",
|
|
4817
5342
|
docsUrl: "https://qwenlm.github.io/qwen-code-docs/en/users/features/mcp/",
|
|
4818
|
-
mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ??
|
|
5343
|
+
mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? path13.join(homeDir(), ".qwen", "settings.json"),
|
|
4819
5344
|
mcpContainerKey: "mcpServers",
|
|
4820
5345
|
format: "json",
|
|
4821
5346
|
rulesFileName: "QWEN.md"
|
|
@@ -4824,7 +5349,7 @@ var AMAZONQ_CLIENT = {
|
|
|
4824
5349
|
id: "amazonq",
|
|
4825
5350
|
label: "Amazon Q Developer CLI",
|
|
4826
5351
|
docsUrl: "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-configuration.html",
|
|
4827
|
-
mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ??
|
|
5352
|
+
mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? path13.join(homeDir(), ".aws", "amazonq", "mcp.json"),
|
|
4828
5353
|
mcpContainerKey: "mcpServers",
|
|
4829
5354
|
format: "json"
|
|
4830
5355
|
};
|
|
@@ -4832,7 +5357,7 @@ var ROOCODE_CLIENT = {
|
|
|
4832
5357
|
id: "roocode",
|
|
4833
5358
|
label: "Roo Code",
|
|
4834
5359
|
docsUrl: "https://roocodeinc.github.io/Roo-Code/features/mcp/using-mcp-in-roo",
|
|
4835
|
-
mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ??
|
|
5360
|
+
mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? path13.join(process.cwd(), ".roo", "mcp.json"),
|
|
4836
5361
|
mcpContainerKey: "mcpServers",
|
|
4837
5362
|
format: "json"
|
|
4838
5363
|
};
|
|
@@ -4845,9 +5370,9 @@ var ZED_CLIENT = {
|
|
|
4845
5370
|
if (override) return override;
|
|
4846
5371
|
if (process.platform === "win32") {
|
|
4847
5372
|
const appData = process.env.APPDATA;
|
|
4848
|
-
if (appData && appData.length > 0) return
|
|
5373
|
+
if (appData && appData.length > 0) return path13.join(appData, "Zed", "settings.json");
|
|
4849
5374
|
}
|
|
4850
|
-
return
|
|
5375
|
+
return path13.join(homeDir(), ".config", "zed", "settings.json");
|
|
4851
5376
|
},
|
|
4852
5377
|
mcpContainerKey: "context_servers",
|
|
4853
5378
|
format: "jsonc",
|
|
@@ -4857,7 +5382,7 @@ var OPENCODE_CLIENT = {
|
|
|
4857
5382
|
id: "opencode",
|
|
4858
5383
|
label: "OpenCode",
|
|
4859
5384
|
docsUrl: "https://opencode.ai/docs/mcp-servers/",
|
|
4860
|
-
mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ??
|
|
5385
|
+
mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? path13.join(xdgConfigDir(), "opencode", "opencode.json"),
|
|
4861
5386
|
mcpContainerKey: "mcp",
|
|
4862
5387
|
format: "json",
|
|
4863
5388
|
serverEntry: NEAT_OPENCODE_SERVER,
|
|
@@ -4867,7 +5392,7 @@ var CRUSH_CLIENT = {
|
|
|
4867
5392
|
id: "crush",
|
|
4868
5393
|
label: "Crush",
|
|
4869
5394
|
docsUrl: "https://charmbracelet-crush.mintlify.app/configuration/mcp",
|
|
4870
|
-
mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ??
|
|
5395
|
+
mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? path13.join(xdgConfigDir(), "crush", "crush.json"),
|
|
4871
5396
|
mcpContainerKey: "mcp",
|
|
4872
5397
|
format: "json",
|
|
4873
5398
|
serverEntry: NEAT_CRUSH_SERVER,
|
|
@@ -4930,7 +5455,7 @@ async function planMcp(client, mcpPath) {
|
|
|
4930
5455
|
const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
|
|
4931
5456
|
let raw = "";
|
|
4932
5457
|
try {
|
|
4933
|
-
raw = await
|
|
5458
|
+
raw = await fs12.readFile(mcpPath, "utf8");
|
|
4934
5459
|
} catch (err) {
|
|
4935
5460
|
const e = err;
|
|
4936
5461
|
if (e.code === "ENOENT") {
|
|
@@ -4972,7 +5497,7 @@ async function runEditorInstall(client, opts) {
|
|
|
4972
5497
|
const mcpPath = client.mcpConfigPath();
|
|
4973
5498
|
const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
|
|
4974
5499
|
const hasRules = typeof client.rulesFileName === "string";
|
|
4975
|
-
const rulesPath = hasRules ?
|
|
5500
|
+
const rulesPath = hasRules ? path13.join(opts.projectDir, client.rulesFileName) : "";
|
|
4976
5501
|
const mcp = await planMcp(client, mcpPath);
|
|
4977
5502
|
if (mcp === null) return { exitCode: 1 };
|
|
4978
5503
|
let existingRules = "";
|
|
@@ -4981,7 +5506,7 @@ async function runEditorInstall(client, opts) {
|
|
|
4981
5506
|
let block = "";
|
|
4982
5507
|
if (hasRules) {
|
|
4983
5508
|
try {
|
|
4984
|
-
existingRules = await
|
|
5509
|
+
existingRules = await fs12.readFile(rulesPath, "utf8");
|
|
4985
5510
|
} catch (err) {
|
|
4986
5511
|
if (err.code !== "ENOENT") {
|
|
4987
5512
|
console.error(`neat ${client.id}: failed to read ${rulesPath} \u2014 ${err.message}`);
|
|
@@ -5015,11 +5540,11 @@ async function runEditorInstall(client, opts) {
|
|
|
5015
5540
|
);
|
|
5016
5541
|
return { exitCode: 0 };
|
|
5017
5542
|
}
|
|
5018
|
-
await
|
|
5019
|
-
await
|
|
5543
|
+
await fs12.mkdir(path13.dirname(mcpPath), { recursive: true });
|
|
5544
|
+
await fs12.writeFile(mcpPath, mcp.text, "utf8");
|
|
5020
5545
|
if (hasRules) {
|
|
5021
|
-
await
|
|
5022
|
-
await
|
|
5546
|
+
await fs12.mkdir(path13.dirname(rulesPath), { recursive: true });
|
|
5547
|
+
await fs12.writeFile(rulesPath, newRules, "utf8");
|
|
5023
5548
|
}
|
|
5024
5549
|
console.log(`neat ${client.id}: wired NEAT into ${client.label}`);
|
|
5025
5550
|
console.log(` MCP server: ${mcpPath} (${client.mcpContainerKey}.neat \u2192 npx -y @neat.is/mcp)`);
|
|
@@ -5055,11 +5580,11 @@ function usage3(client) {
|
|
|
5055
5580
|
}
|
|
5056
5581
|
async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
|
|
5057
5582
|
const client = CLIENTS[clientId];
|
|
5058
|
-
let
|
|
5583
|
+
let apply6 = false;
|
|
5059
5584
|
for (const arg of args) {
|
|
5060
5585
|
switch (arg) {
|
|
5061
5586
|
case "--apply":
|
|
5062
|
-
|
|
5587
|
+
apply6 = true;
|
|
5063
5588
|
break;
|
|
5064
5589
|
case "-h":
|
|
5065
5590
|
case "--help":
|
|
@@ -5072,7 +5597,7 @@ async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
|
|
|
5072
5597
|
}
|
|
5073
5598
|
}
|
|
5074
5599
|
try {
|
|
5075
|
-
const { exitCode } = await runEditorInstall(client, { apply:
|
|
5600
|
+
const { exitCode } = await runEditorInstall(client, { apply: apply6, projectDir });
|
|
5076
5601
|
return exitCode;
|
|
5077
5602
|
} catch (err) {
|
|
5078
5603
|
console.error(err.message);
|
|
@@ -5109,10 +5634,10 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
5109
5634
|
const root = baseUrl.replace(/\/$/, "");
|
|
5110
5635
|
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
5111
5636
|
return {
|
|
5112
|
-
async get(
|
|
5637
|
+
async get(path16) {
|
|
5113
5638
|
let res;
|
|
5114
5639
|
try {
|
|
5115
|
-
res = await fetch(`${root}${
|
|
5640
|
+
res = await fetch(`${root}${path16}`, {
|
|
5116
5641
|
headers: { ...authHeader }
|
|
5117
5642
|
});
|
|
5118
5643
|
} catch (err) {
|
|
@@ -5124,16 +5649,16 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
5124
5649
|
const body = await res.text().catch(() => "");
|
|
5125
5650
|
throw new HttpError(
|
|
5126
5651
|
res.status,
|
|
5127
|
-
`${res.status} ${res.statusText} on GET ${
|
|
5652
|
+
`${res.status} ${res.statusText} on GET ${path16}: ${body}`,
|
|
5128
5653
|
body
|
|
5129
5654
|
);
|
|
5130
5655
|
}
|
|
5131
5656
|
return await res.json();
|
|
5132
5657
|
},
|
|
5133
|
-
async post(
|
|
5658
|
+
async post(path16, body) {
|
|
5134
5659
|
let res;
|
|
5135
5660
|
try {
|
|
5136
|
-
res = await fetch(`${root}${
|
|
5661
|
+
res = await fetch(`${root}${path16}`, {
|
|
5137
5662
|
method: "POST",
|
|
5138
5663
|
headers: { "content-type": "application/json", ...authHeader },
|
|
5139
5664
|
body: JSON.stringify(body)
|
|
@@ -5147,7 +5672,7 @@ function createHttpClient(baseUrl, bearerToken) {
|
|
|
5147
5672
|
const text = await res.text().catch(() => "");
|
|
5148
5673
|
throw new HttpError(
|
|
5149
5674
|
res.status,
|
|
5150
|
-
`${res.status} ${res.statusText} on POST ${
|
|
5675
|
+
`${res.status} ${res.statusText} on POST ${path16}: ${text}`,
|
|
5151
5676
|
text
|
|
5152
5677
|
);
|
|
5153
5678
|
}
|
|
@@ -5161,12 +5686,12 @@ function projectPath(project, suffix) {
|
|
|
5161
5686
|
}
|
|
5162
5687
|
async function runRootCause(client, input) {
|
|
5163
5688
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
5164
|
-
const
|
|
5689
|
+
const path16 = projectPath(
|
|
5165
5690
|
input.project,
|
|
5166
5691
|
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
5167
5692
|
);
|
|
5168
5693
|
try {
|
|
5169
|
-
const result = await client.get(
|
|
5694
|
+
const result = await client.get(path16);
|
|
5170
5695
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
5171
5696
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
5172
5697
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -5192,12 +5717,12 @@ async function runRootCause(client, input) {
|
|
|
5192
5717
|
}
|
|
5193
5718
|
async function runBlastRadius(client, input) {
|
|
5194
5719
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
5195
|
-
const
|
|
5720
|
+
const path16 = projectPath(
|
|
5196
5721
|
input.project,
|
|
5197
5722
|
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
5198
5723
|
);
|
|
5199
5724
|
try {
|
|
5200
|
-
const result = await client.get(
|
|
5725
|
+
const result = await client.get(path16);
|
|
5201
5726
|
if (result.totalAffected === 0) {
|
|
5202
5727
|
return {
|
|
5203
5728
|
summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
|
|
@@ -5231,12 +5756,12 @@ function formatBlastEntry(n) {
|
|
|
5231
5756
|
}
|
|
5232
5757
|
async function runDependencies(client, input) {
|
|
5233
5758
|
const depth = input.depth ?? 3;
|
|
5234
|
-
const
|
|
5759
|
+
const path16 = projectPath(
|
|
5235
5760
|
input.project,
|
|
5236
5761
|
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
5237
5762
|
);
|
|
5238
5763
|
try {
|
|
5239
|
-
const result = await client.get(
|
|
5764
|
+
const result = await client.get(path16);
|
|
5240
5765
|
if (result.total === 0) {
|
|
5241
5766
|
return {
|
|
5242
5767
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -5328,9 +5853,9 @@ function formatDuration(ms) {
|
|
|
5328
5853
|
return `${Math.round(h / 24)}d`;
|
|
5329
5854
|
}
|
|
5330
5855
|
async function runIncidents(client, input) {
|
|
5331
|
-
const
|
|
5856
|
+
const path16 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
5332
5857
|
try {
|
|
5333
|
-
const body = await client.get(
|
|
5858
|
+
const body = await client.get(path16);
|
|
5334
5859
|
const events = body.events;
|
|
5335
5860
|
if (events.length === 0) {
|
|
5336
5861
|
return {
|
|
@@ -5983,7 +6508,7 @@ function sleep(ms, signal) {
|
|
|
5983
6508
|
}
|
|
5984
6509
|
|
|
5985
6510
|
// src/cli-verbs.ts
|
|
5986
|
-
import
|
|
6511
|
+
import path14 from "path";
|
|
5987
6512
|
async function resolveProjectEntry(opts) {
|
|
5988
6513
|
const entries = await listProjects();
|
|
5989
6514
|
if (opts.project) {
|
|
@@ -5993,7 +6518,7 @@ async function resolveProjectEntry(opts) {
|
|
|
5993
6518
|
const cwd = opts.cwd ?? process.cwd();
|
|
5994
6519
|
const resolvedCwd = await normalizeProjectPath(cwd);
|
|
5995
6520
|
for (const entry2 of entries) {
|
|
5996
|
-
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${
|
|
6521
|
+
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path14.sep}`)) {
|
|
5997
6522
|
return entry2;
|
|
5998
6523
|
}
|
|
5999
6524
|
}
|
|
@@ -6500,15 +7025,15 @@ async function buildPatchSections(services, project) {
|
|
|
6500
7025
|
for (const svc of services) {
|
|
6501
7026
|
const installer = await pickInstaller(svc.dir);
|
|
6502
7027
|
if (!installer) continue;
|
|
6503
|
-
const
|
|
6504
|
-
if (isEmptyPlan(
|
|
6505
|
-
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 });
|
|
6506
7031
|
}
|
|
6507
7032
|
return sections;
|
|
6508
7033
|
}
|
|
6509
7034
|
async function runInit(opts) {
|
|
6510
7035
|
const written = [];
|
|
6511
|
-
const stat = await
|
|
7036
|
+
const stat = await fs13.stat(opts.scanPath).catch(() => null);
|
|
6512
7037
|
if (!stat || !stat.isDirectory()) {
|
|
6513
7038
|
console.error(`neat init: ${opts.scanPath} is not a directory`);
|
|
6514
7039
|
return { exitCode: 2, writtenFiles: written };
|
|
@@ -6517,13 +7042,13 @@ async function runInit(opts) {
|
|
|
6517
7042
|
printDiscoveryReport(opts, services);
|
|
6518
7043
|
const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
|
|
6519
7044
|
const patch = renderPatch(sections);
|
|
6520
|
-
const patchPath =
|
|
7045
|
+
const patchPath = path15.join(opts.scanPath, "neat.patch");
|
|
6521
7046
|
if (opts.dryRun) {
|
|
6522
|
-
await
|
|
7047
|
+
await fs13.writeFile(patchPath, patch, "utf8");
|
|
6523
7048
|
written.push(patchPath);
|
|
6524
7049
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
6525
|
-
const gitignorePath =
|
|
6526
|
-
const gitignoreExists = await
|
|
7050
|
+
const gitignorePath = path15.join(opts.scanPath, ".gitignore");
|
|
7051
|
+
const gitignoreExists = await fs13.stat(gitignorePath).then(() => true).catch(() => false);
|
|
6527
7052
|
const verb = gitignoreExists ? "append" : "create";
|
|
6528
7053
|
console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
|
|
6529
7054
|
console.log("rerun without --dry-run to register and snapshot.");
|
|
@@ -6534,9 +7059,9 @@ async function runInit(opts) {
|
|
|
6534
7059
|
const graph = getGraph(graphKey);
|
|
6535
7060
|
const projectPaths = pathsForProject(
|
|
6536
7061
|
graphKey,
|
|
6537
|
-
|
|
7062
|
+
path15.join(opts.scanPath, "neat-out")
|
|
6538
7063
|
);
|
|
6539
|
-
const errorsPath =
|
|
7064
|
+
const errorsPath = path15.join(path15.dirname(opts.outPath), path15.basename(projectPaths.errorsPath));
|
|
6540
7065
|
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
6541
7066
|
await saveGraphToDisk(graph, opts.outPath);
|
|
6542
7067
|
written.push(opts.outPath);
|
|
@@ -6615,7 +7140,7 @@ async function runInit(opts) {
|
|
|
6615
7140
|
console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
6616
7141
|
}
|
|
6617
7142
|
} else {
|
|
6618
|
-
await
|
|
7143
|
+
await fs13.writeFile(patchPath, patch, "utf8");
|
|
6619
7144
|
written.push(patchPath);
|
|
6620
7145
|
}
|
|
6621
7146
|
}
|
|
@@ -6655,9 +7180,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
6655
7180
|
};
|
|
6656
7181
|
function claudeConfigPath() {
|
|
6657
7182
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
6658
|
-
if (override && override.length > 0) return
|
|
7183
|
+
if (override && override.length > 0) return path15.resolve(override);
|
|
6659
7184
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
6660
|
-
return
|
|
7185
|
+
return path15.join(home, ".claude.json");
|
|
6661
7186
|
}
|
|
6662
7187
|
async function runSkill(opts) {
|
|
6663
7188
|
const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -6669,7 +7194,7 @@ async function runSkill(opts) {
|
|
|
6669
7194
|
const target = claudeConfigPath();
|
|
6670
7195
|
let existing = {};
|
|
6671
7196
|
try {
|
|
6672
|
-
existing = JSON.parse(await
|
|
7197
|
+
existing = JSON.parse(await fs13.readFile(target, "utf8"));
|
|
6673
7198
|
} catch (err) {
|
|
6674
7199
|
if (err.code !== "ENOENT") {
|
|
6675
7200
|
console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
|
|
@@ -6681,8 +7206,8 @@ async function runSkill(opts) {
|
|
|
6681
7206
|
...existing,
|
|
6682
7207
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
6683
7208
|
};
|
|
6684
|
-
await
|
|
6685
|
-
await
|
|
7209
|
+
await fs13.mkdir(path15.dirname(target), { recursive: true });
|
|
7210
|
+
await fs13.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
6686
7211
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
6687
7212
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
6688
7213
|
console.log("");
|
|
@@ -6755,7 +7280,7 @@ async function main() {
|
|
|
6755
7280
|
}
|
|
6756
7281
|
const cmd = argvParsed.positional[0];
|
|
6757
7282
|
const parsed = { ...argvParsed, positional: argvParsed.positional.slice(1) };
|
|
6758
|
-
const { positional, apply:
|
|
7283
|
+
const { positional, apply: apply6, dryRun, noInstall } = parsed;
|
|
6759
7284
|
const project = parsed.project ?? DEFAULT_PROJECT;
|
|
6760
7285
|
if (cmd === "init") {
|
|
6761
7286
|
const target = positional[0];
|
|
@@ -6764,22 +7289,22 @@ async function main() {
|
|
|
6764
7289
|
usage4();
|
|
6765
7290
|
process.exit(2);
|
|
6766
7291
|
}
|
|
6767
|
-
if (
|
|
7292
|
+
if (apply6 && dryRun) {
|
|
6768
7293
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
6769
7294
|
process.exit(2);
|
|
6770
7295
|
}
|
|
6771
|
-
const scanPath =
|
|
7296
|
+
const scanPath = path15.resolve(target);
|
|
6772
7297
|
const projectExplicit = parsed.project !== null;
|
|
6773
|
-
const projectName = projectExplicit ? project :
|
|
7298
|
+
const projectName = projectExplicit ? project : path15.basename(scanPath);
|
|
6774
7299
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
6775
|
-
const fallback = pathsForProject(projectKey,
|
|
6776
|
-
const outPath =
|
|
7300
|
+
const fallback = pathsForProject(projectKey, path15.join(scanPath, "neat-out")).snapshotPath;
|
|
7301
|
+
const outPath = path15.resolve(process.env.NEAT_OUT_PATH ?? fallback);
|
|
6777
7302
|
const result = await runInit({
|
|
6778
7303
|
scanPath,
|
|
6779
7304
|
outPath,
|
|
6780
7305
|
project: projectName,
|
|
6781
7306
|
projectExplicit,
|
|
6782
|
-
apply:
|
|
7307
|
+
apply: apply6,
|
|
6783
7308
|
dryRun,
|
|
6784
7309
|
noInstall,
|
|
6785
7310
|
verbose: parsed.verbose
|
|
@@ -6794,21 +7319,21 @@ async function main() {
|
|
|
6794
7319
|
usage4();
|
|
6795
7320
|
process.exit(2);
|
|
6796
7321
|
}
|
|
6797
|
-
const scanPath =
|
|
6798
|
-
const stat = await
|
|
7322
|
+
const scanPath = path15.resolve(target);
|
|
7323
|
+
const stat = await fs13.stat(scanPath).catch(() => null);
|
|
6799
7324
|
if (!stat || !stat.isDirectory()) {
|
|
6800
7325
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
6801
7326
|
process.exit(2);
|
|
6802
7327
|
}
|
|
6803
|
-
const projectPaths = pathsForProject(project,
|
|
6804
|
-
const outPath =
|
|
6805
|
-
const errorsPath =
|
|
6806
|
-
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))
|
|
6807
7332
|
);
|
|
6808
|
-
const staleEventsPath =
|
|
6809
|
-
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))
|
|
6810
7335
|
);
|
|
6811
|
-
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;
|
|
6812
7337
|
const handle = await startWatch(getGraph(project), {
|
|
6813
7338
|
scanPath,
|
|
6814
7339
|
outPath,
|
|
@@ -6817,7 +7342,7 @@ async function main() {
|
|
|
6817
7342
|
project,
|
|
6818
7343
|
// Resolve NEAT_HOME so a `neat watch` picks up connectors added to
|
|
6819
7344
|
// ~/.neat/connectors.json (#871). Same resolution the rest of the CLI uses.
|
|
6820
|
-
neatHome: process.env.NEAT_HOME ?
|
|
7345
|
+
neatHome: process.env.NEAT_HOME ? path15.resolve(process.env.NEAT_HOME) : path15.join(os4.homedir(), ".neat"),
|
|
6821
7346
|
...embeddingsCachePath ? { embeddingsCachePath } : {},
|
|
6822
7347
|
host: process.env.HOST ?? "0.0.0.0",
|
|
6823
7348
|
port: Number(process.env.PORT ?? 8080),
|
|
@@ -6999,11 +7524,11 @@ async function main() {
|
|
|
6999
7524
|
process.exit(1);
|
|
7000
7525
|
}
|
|
7001
7526
|
async function tryOrchestrator(cmd, parsed) {
|
|
7002
|
-
const scanPath =
|
|
7003
|
-
const stat = await
|
|
7527
|
+
const scanPath = path15.resolve(cmd);
|
|
7528
|
+
const stat = await fs13.stat(scanPath).catch(() => null);
|
|
7004
7529
|
if (!stat || !stat.isDirectory()) return null;
|
|
7005
7530
|
const projectExplicit = parsed.project !== null;
|
|
7006
|
-
const projectName = projectExplicit ? parsed.project :
|
|
7531
|
+
const projectName = projectExplicit ? parsed.project : path15.basename(scanPath);
|
|
7007
7532
|
const result = await runOrchestrator({
|
|
7008
7533
|
scanPath,
|
|
7009
7534
|
project: projectName,
|