@neat.is/core 0.10.1-dev.20260928 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-ERE47MCR.js → chunk-5SJHA5ZH.js} +2 -2
- package/dist/{chunk-HVBD4ULO.js → chunk-AWIK6T3G.js} +3 -3
- package/dist/{chunk-CNOJ4T6I.js → chunk-E3YFFIHE.js} +2 -2
- package/dist/{chunk-GDGUY4T6.js → chunk-M7CGATVC.js} +3 -2
- package/dist/cli.cjs +547 -255
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +2 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +468 -179
- package/dist/cli.js.map +1 -1
- package/dist/index.js +4 -4
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-Y5K4WGWO.js → otel-grpc-CU73Y5WT.js} +3 -3
- package/dist/server.js +3 -3
- package/package.json +3 -3
- /package/dist/{chunk-ERE47MCR.js.map → chunk-5SJHA5ZH.js.map} +0 -0
- /package/dist/{chunk-HVBD4ULO.js.map → chunk-AWIK6T3G.js.map} +0 -0
- /package/dist/{chunk-CNOJ4T6I.js.map → chunk-E3YFFIHE.js.map} +0 -0
- /package/dist/{chunk-GDGUY4T6.js.map → chunk-M7CGATVC.js.map} +0 -0
- /package/dist/{otel-grpc-Y5K4WGWO.js.map → otel-grpc-CU73Y5WT.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
resolveNeatVersion,
|
|
9
9
|
startK8sSubstratePolling,
|
|
10
10
|
writeDaemonRecord
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-AWIK6T3G.js";
|
|
12
12
|
import {
|
|
13
13
|
buildSearchIndex
|
|
14
14
|
} from "./chunk-BC53SCT7.js";
|
|
@@ -78,23 +78,24 @@ import {
|
|
|
78
78
|
startStalenessLoop,
|
|
79
79
|
upsertConnectorEntry,
|
|
80
80
|
validateConnectorEntry
|
|
81
|
-
} from "./chunk-
|
|
81
|
+
} from "./chunk-E3YFFIHE.js";
|
|
82
82
|
import {
|
|
83
83
|
startOtelGrpcReceiver
|
|
84
|
-
} from "./chunk-
|
|
84
|
+
} from "./chunk-5SJHA5ZH.js";
|
|
85
85
|
import {
|
|
86
86
|
__dirname,
|
|
87
87
|
__require,
|
|
88
88
|
assertBindAuthority,
|
|
89
89
|
buildOtelReceiver,
|
|
90
|
+
isLoopbackHost,
|
|
90
91
|
listenSteppingOtlp,
|
|
91
92
|
readAuthEnv
|
|
92
|
-
} from "./chunk-
|
|
93
|
+
} from "./chunk-M7CGATVC.js";
|
|
93
94
|
|
|
94
95
|
// src/cli.ts
|
|
95
|
-
import
|
|
96
|
+
import path20 from "path";
|
|
96
97
|
import os5 from "os";
|
|
97
|
-
import { promises as
|
|
98
|
+
import { promises as fs18 } from "fs";
|
|
98
99
|
|
|
99
100
|
// src/banner.ts
|
|
100
101
|
import path from "path";
|
|
@@ -4452,7 +4453,8 @@ async function runOrchestrator(opts) {
|
|
|
4452
4453
|
} catch (err) {
|
|
4453
4454
|
if (!(err instanceof ProjectNameCollisionError)) throw err;
|
|
4454
4455
|
console.error(`neat: ${err.message}`);
|
|
4455
|
-
console.error(
|
|
4456
|
+
console.error(`neat: run \`neat <path> --project <other-name>\` to register it under a different name,`);
|
|
4457
|
+
console.error(`neat: or \`neat uninstall ${opts.project}\` if the old registration is stale.`);
|
|
4456
4458
|
result.exitCode = 1;
|
|
4457
4459
|
return result;
|
|
4458
4460
|
}
|
|
@@ -5245,6 +5247,196 @@ async function clearActiveProfile(home = neatHome()) {
|
|
|
5245
5247
|
});
|
|
5246
5248
|
}
|
|
5247
5249
|
|
|
5250
|
+
// src/daemon-probe.ts
|
|
5251
|
+
var PROBE_ATTEMPT_TIMEOUT_MS = 3e4;
|
|
5252
|
+
var PROBE_TOTAL_BUDGET_MS = 12e4;
|
|
5253
|
+
var PROBE_RETRY_PAUSE_MS = 2e3;
|
|
5254
|
+
function isTimeoutError(err) {
|
|
5255
|
+
const name = err?.name;
|
|
5256
|
+
return name === "TimeoutError" || name === "AbortError";
|
|
5257
|
+
}
|
|
5258
|
+
async function probeOnce(fetchImpl, root, token, timeoutMs) {
|
|
5259
|
+
let res;
|
|
5260
|
+
try {
|
|
5261
|
+
res = await fetchImpl(`${root}/health`, {
|
|
5262
|
+
headers: { authorization: `Bearer ${token}` },
|
|
5263
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
5264
|
+
});
|
|
5265
|
+
} catch (err) {
|
|
5266
|
+
if (isTimeoutError(err)) return { kind: "timeout" };
|
|
5267
|
+
return { kind: "unreachable", detail: err.message };
|
|
5268
|
+
}
|
|
5269
|
+
if (res.status === 401 || res.status === 403) return { kind: "unauthorized", status: res.status };
|
|
5270
|
+
if (!res.ok) return { kind: "not-neat", status: res.status };
|
|
5271
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
5272
|
+
if (!contentType.includes("json")) return { kind: "not-neat", status: res.status };
|
|
5273
|
+
return { kind: "ok" };
|
|
5274
|
+
}
|
|
5275
|
+
async function probeDaemon(fetchImpl, endpoint, token, hooks) {
|
|
5276
|
+
const root = endpoint.replace(/\/$/, "");
|
|
5277
|
+
const deadline = hooks.now() + PROBE_TOTAL_BUDGET_MS;
|
|
5278
|
+
let warned = false;
|
|
5279
|
+
for (; ; ) {
|
|
5280
|
+
const result = await probeOnce(fetchImpl, root, token, PROBE_ATTEMPT_TIMEOUT_MS);
|
|
5281
|
+
if (result.kind !== "timeout") return result;
|
|
5282
|
+
if (!warned) {
|
|
5283
|
+
hooks.onWaiting();
|
|
5284
|
+
warned = true;
|
|
5285
|
+
}
|
|
5286
|
+
if (hooks.now() >= deadline) {
|
|
5287
|
+
return { kind: "unreachable", detail: `no response after ${Math.round(PROBE_TOTAL_BUDGET_MS / 1e3)}s` };
|
|
5288
|
+
}
|
|
5289
|
+
await hooks.sleep(PROBE_RETRY_PAUSE_MS);
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5292
|
+
|
|
5293
|
+
// src/login-connect.ts
|
|
5294
|
+
import { execFile } from "child_process";
|
|
5295
|
+
import { promisify } from "util";
|
|
5296
|
+
import { promises as fs11 } from "fs";
|
|
5297
|
+
import path12 from "path";
|
|
5298
|
+
var execFileAsync = promisify(execFile);
|
|
5299
|
+
async function hasLocalSnapshot(cwd) {
|
|
5300
|
+
try {
|
|
5301
|
+
await fs11.access(path12.join(cwd, "neat-out", "graph.json"));
|
|
5302
|
+
return true;
|
|
5303
|
+
} catch {
|
|
5304
|
+
return false;
|
|
5305
|
+
}
|
|
5306
|
+
}
|
|
5307
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
5308
|
+
var POLL_BUDGET_MS = 10 * 6e4;
|
|
5309
|
+
var CP_TIMEOUT_MS = 1e4;
|
|
5310
|
+
async function detectGitHubRepo(cwd) {
|
|
5311
|
+
const read = async (args) => {
|
|
5312
|
+
try {
|
|
5313
|
+
const { stdout } = await execFileAsync("git", args, { cwd, timeout: 5e3 });
|
|
5314
|
+
return stdout.trim();
|
|
5315
|
+
} catch {
|
|
5316
|
+
return null;
|
|
5317
|
+
}
|
|
5318
|
+
};
|
|
5319
|
+
let url = await read(["remote", "get-url", "origin"]);
|
|
5320
|
+
if (!url) {
|
|
5321
|
+
const names = await read(["remote"]);
|
|
5322
|
+
const first = names?.split(/\r?\n/).find((l) => l.trim().length > 0)?.trim();
|
|
5323
|
+
if (!first) return null;
|
|
5324
|
+
url = await read(["remote", "get-url", first]);
|
|
5325
|
+
}
|
|
5326
|
+
if (!url) return null;
|
|
5327
|
+
return parseGitHubRemote(url);
|
|
5328
|
+
}
|
|
5329
|
+
function parseGitHubRemote(url) {
|
|
5330
|
+
const trimmed = url.trim().replace(/\.git$/, "");
|
|
5331
|
+
const m = trimmed.match(/^https?:\/\/(?:[^@/]+@)?github\.com\/([^/]+)\/([^/]+)$/) ?? trimmed.match(/^git@github\.com:([^/]+)\/([^/]+)$/) ?? trimmed.match(/^ssh:\/\/git@github\.com\/([^/]+)\/([^/]+)$/);
|
|
5332
|
+
if (!m) return null;
|
|
5333
|
+
const owner = m[1];
|
|
5334
|
+
const name = m[2];
|
|
5335
|
+
if (!owner || !name) return null;
|
|
5336
|
+
return { owner, name };
|
|
5337
|
+
}
|
|
5338
|
+
async function cpGetJson(fetchImpl, cpUrl, path21, accessToken) {
|
|
5339
|
+
try {
|
|
5340
|
+
const res = await fetchImpl(`${cpUrl}${path21}`, {
|
|
5341
|
+
headers: { authorization: `Bearer ${accessToken}`, accept: "application/json" },
|
|
5342
|
+
signal: AbortSignal.timeout(CP_TIMEOUT_MS)
|
|
5343
|
+
});
|
|
5344
|
+
if (!res.ok) return { ok: false };
|
|
5345
|
+
return { ok: true, body: await res.json() };
|
|
5346
|
+
} catch {
|
|
5347
|
+
return { ok: false };
|
|
5348
|
+
}
|
|
5349
|
+
}
|
|
5350
|
+
async function listBoundRepos(fetchImpl, cpUrl, accessToken, projectId) {
|
|
5351
|
+
const res = await cpGetJson(
|
|
5352
|
+
fetchImpl,
|
|
5353
|
+
cpUrl,
|
|
5354
|
+
`/me/projects/${encodeURIComponent(projectId)}/repos`,
|
|
5355
|
+
accessToken
|
|
5356
|
+
);
|
|
5357
|
+
if (!res.ok || !Array.isArray(res.body)) return null;
|
|
5358
|
+
return res.body;
|
|
5359
|
+
}
|
|
5360
|
+
async function waitForFirstProject(fetchImpl, cpUrl, accessToken, deps) {
|
|
5361
|
+
const pollMs = deps.pollMs ?? POLL_INTERVAL_MS;
|
|
5362
|
+
const deadline = deps.now() + (deps.budgetMs ?? POLL_BUDGET_MS);
|
|
5363
|
+
for (; ; ) {
|
|
5364
|
+
if (deps.signal?.aborted) return { kind: "aborted" };
|
|
5365
|
+
const me = await cpGetJson(fetchImpl, cpUrl, "/me", accessToken);
|
|
5366
|
+
if (me.ok) {
|
|
5367
|
+
const running = (me.body.projects ?? []).find((p) => p.status === "running");
|
|
5368
|
+
if (running) return { kind: "project", project: running };
|
|
5369
|
+
}
|
|
5370
|
+
if (deps.now() >= deadline) return { kind: "timeout" };
|
|
5371
|
+
await deps.sleep(pollMs);
|
|
5372
|
+
if (deps.signal?.aborted) return { kind: "aborted" };
|
|
5373
|
+
}
|
|
5374
|
+
}
|
|
5375
|
+
function bindUrl(webUrl, projectId) {
|
|
5376
|
+
return `${webUrl}/config/repos?project=${encodeURIComponent(projectId)}`;
|
|
5377
|
+
}
|
|
5378
|
+
function onboardingUrl(webUrl) {
|
|
5379
|
+
return `${webUrl}/onboarding`;
|
|
5380
|
+
}
|
|
5381
|
+
async function runPostLoginConnect(input) {
|
|
5382
|
+
const d = input.deps;
|
|
5383
|
+
const out = d.out ?? ((line) => console.log(line));
|
|
5384
|
+
const fetchImpl = d.fetchImpl ?? fetch;
|
|
5385
|
+
const cwd = d.cwd ?? process.cwd();
|
|
5386
|
+
const detect6 = d.detectRepo ?? detectGitHubRepo;
|
|
5387
|
+
const readLine = d.readLine;
|
|
5388
|
+
const repo = await detect6(cwd);
|
|
5389
|
+
out("");
|
|
5390
|
+
if (!repo) {
|
|
5391
|
+
out("This directory has no GitHub remote, so it can't be bound to the project.");
|
|
5392
|
+
out(` Push its graph instead: neat sync --to ${input.project.name}`);
|
|
5393
|
+
out(` Manage projects and repos: ${input.webUrl}`);
|
|
5394
|
+
return;
|
|
5395
|
+
}
|
|
5396
|
+
const slug = `${repo.owner}/${repo.name}`;
|
|
5397
|
+
const bound = await listBoundRepos(fetchImpl, input.cpUrl, input.accessToken, input.project.id);
|
|
5398
|
+
const match = bound?.find((r) => r.owner === repo.owner && r.name === repo.name);
|
|
5399
|
+
if (match) {
|
|
5400
|
+
const state = match.syncStatus ? ` (${match.syncStatus})` : "";
|
|
5401
|
+
out(`${slug} is bound to ${input.project.name}${state} \u2014 hosted NEAT extracts it for you.`);
|
|
5402
|
+
out(` See the graph: ${input.webUrl}`);
|
|
5403
|
+
return;
|
|
5404
|
+
}
|
|
5405
|
+
out(`${slug} isn't part of ${input.project.name} yet \u2014 logging in connected your account, not this repo.`);
|
|
5406
|
+
out("");
|
|
5407
|
+
const installed = input.me?.github?.installed;
|
|
5408
|
+
const bindLine = installed === false ? "1) Install the GitHub App and bind this repo \u2014 hosted NEAT keeps it in sync" : installed === true ? "1) Bind this repo \u2014 hosted NEAT clones and extracts it, and keeps it in sync" : "1) Bind this repo through the console \u2014 hosted NEAT keeps it in sync";
|
|
5409
|
+
out(bindLine);
|
|
5410
|
+
out(`2) Push the graph on this machine now \u2014 a snapshot, not a subscription`);
|
|
5411
|
+
out("3) Neither, for now");
|
|
5412
|
+
const answer = readLine ? (await readLine("Choose 1, 2 or 3 (Enter skips): "))?.trim() : void 0;
|
|
5413
|
+
if (answer === "1") {
|
|
5414
|
+
const url = bindUrl(input.webUrl, input.project.id);
|
|
5415
|
+
const opened = (d.openBrowser ?? (() => false))(url);
|
|
5416
|
+
out("");
|
|
5417
|
+
out(opened ? `Opened ${url}` : `Open this to bind the repo:
|
|
5418
|
+
${url}`);
|
|
5419
|
+
return;
|
|
5420
|
+
}
|
|
5421
|
+
if (answer === "2") {
|
|
5422
|
+
const hasGraph = await (d.hasLocalGraph ?? hasLocalSnapshot)(cwd);
|
|
5423
|
+
out("");
|
|
5424
|
+
if (!hasGraph) {
|
|
5425
|
+
out("There is no local graph to push yet. Build one, then push it:");
|
|
5426
|
+
out(" neat");
|
|
5427
|
+
out(` neat sync --to ${input.project.name}`);
|
|
5428
|
+
return;
|
|
5429
|
+
}
|
|
5430
|
+
out("Push the local graph with:");
|
|
5431
|
+
out(` neat sync --to ${input.project.name}`);
|
|
5432
|
+
return;
|
|
5433
|
+
}
|
|
5434
|
+
out("");
|
|
5435
|
+
out("No problem \u2014 when you want this repo in the graph:");
|
|
5436
|
+
out(` Bind it: ${bindUrl(input.webUrl, input.project.id)}`);
|
|
5437
|
+
out(` Or push it: neat sync --to ${input.project.name}`);
|
|
5438
|
+
}
|
|
5439
|
+
|
|
5248
5440
|
// src/login-sso.ts
|
|
5249
5441
|
var DEFAULT_CP_URL = "https://api.neat.is";
|
|
5250
5442
|
var DEFAULT_WEB_URL = "https://app.neat.is";
|
|
@@ -5270,6 +5462,7 @@ function pickProject(projects, want) {
|
|
|
5270
5462
|
return {
|
|
5271
5463
|
error: {
|
|
5272
5464
|
code: 1,
|
|
5465
|
+
kind: "no-project",
|
|
5273
5466
|
message: `no running project to connect to \u2014 create + provision one in the console first${listed}`
|
|
5274
5467
|
}
|
|
5275
5468
|
};
|
|
@@ -5320,7 +5513,7 @@ async function exchangeCredential(cpUrl, accessToken, opts, deps) {
|
|
|
5320
5513
|
if (!cred.endpoint || !cred.authToken) {
|
|
5321
5514
|
return { error: { code: 1, message: "the credential response was missing endpoint/authToken" } };
|
|
5322
5515
|
}
|
|
5323
|
-
return { project, cred };
|
|
5516
|
+
return { project, cred, me };
|
|
5324
5517
|
}
|
|
5325
5518
|
function defaultOpenBrowser(url) {
|
|
5326
5519
|
const platform = process.platform;
|
|
@@ -5402,12 +5595,50 @@ async function runSsoLogin(opts, deps) {
|
|
|
5402
5595
|
}
|
|
5403
5596
|
accessToken = lb.token;
|
|
5404
5597
|
}
|
|
5405
|
-
const
|
|
5598
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
5599
|
+
const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
5600
|
+
const now = deps.now ?? Date.now;
|
|
5601
|
+
let ex = await exchangeCredential(opts.cpUrl, accessToken, { project: opts.project }, deps);
|
|
5602
|
+
if ("error" in ex && ex.error.kind === "no-project" && !opts.json) {
|
|
5603
|
+
const url = onboardingUrl(opts.webUrl);
|
|
5604
|
+
out("");
|
|
5605
|
+
out("You don't have a project yet \u2014 let's make one.");
|
|
5606
|
+
const opened = (deps.openBrowser ?? defaultOpenBrowser)(url);
|
|
5607
|
+
out(opened ? `Opened ${url}` : `Open this to create your first project:
|
|
5608
|
+
${url}`);
|
|
5609
|
+
out("Waiting for it to come up \u2014 Ctrl-C to stop and finish later.");
|
|
5610
|
+
const waited = await waitForFirstProject(fetchImpl, opts.cpUrl, accessToken, {
|
|
5611
|
+
sleep: sleep2,
|
|
5612
|
+
now,
|
|
5613
|
+
...deps.signal ? { signal: deps.signal } : {}
|
|
5614
|
+
});
|
|
5615
|
+
if (waited.kind === "aborted") {
|
|
5616
|
+
out("Stopped. Run `neat login` again once your project is up.");
|
|
5617
|
+
return 130;
|
|
5618
|
+
}
|
|
5619
|
+
if (waited.kind === "timeout") {
|
|
5620
|
+
err("neat login: no project came up while waiting \u2014 run `neat login` again once it does");
|
|
5621
|
+
return 1;
|
|
5622
|
+
}
|
|
5623
|
+
out(`${waited.project.name} is up.`);
|
|
5624
|
+
ex = await exchangeCredential(opts.cpUrl, accessToken, { project: opts.project }, deps);
|
|
5625
|
+
}
|
|
5406
5626
|
if ("error" in ex) {
|
|
5407
5627
|
err(`neat login: ${ex.error.message}`);
|
|
5408
5628
|
return ex.error.code;
|
|
5409
5629
|
}
|
|
5410
|
-
const { project, cred } = ex;
|
|
5630
|
+
const { project, cred, me } = ex;
|
|
5631
|
+
const probe = await probeDaemon(fetchImpl, cred.endpoint, cred.authToken, {
|
|
5632
|
+
sleep: sleep2,
|
|
5633
|
+
now,
|
|
5634
|
+
onWaiting: () => err("Waking the hosted daemon \u2014 a cold instance can take up to a minute\u2026")
|
|
5635
|
+
});
|
|
5636
|
+
if (probe.kind !== "ok") {
|
|
5637
|
+
const why = probe.kind === "unreachable" ? probe.detail : probe.kind === "unauthorized" ? `it rejected the credential (HTTP ${probe.status})` : `it answered HTTP ${probe.status} and doesn't look like NEAT`;
|
|
5638
|
+
err(`neat login: "${project.name}" is provisioned, but its daemon isn't answering \u2014 ${why}.`);
|
|
5639
|
+
err(`Nothing was changed. Check the project at ${opts.webUrl} and run \`neat login\` again.`);
|
|
5640
|
+
return probe.kind === "unreachable" ? 3 : 1;
|
|
5641
|
+
}
|
|
5411
5642
|
await upsertProfile(
|
|
5412
5643
|
{ name: opts.name, endpoint: cred.endpoint, authToken: cred.authToken },
|
|
5413
5644
|
{ makeActive: true, ...deps.home ? { home: deps.home } : {} }
|
|
@@ -5435,10 +5666,20 @@ async function runSsoLogin(opts, deps) {
|
|
|
5435
5666
|
out(` OTEL_EXPORTER_OTLP_ENDPOINT=${cred.ingestEndpoint}`);
|
|
5436
5667
|
out(` OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${cred.otelToken}`);
|
|
5437
5668
|
}
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5669
|
+
await runPostLoginConnect({
|
|
5670
|
+
cpUrl: opts.cpUrl,
|
|
5671
|
+
webUrl: opts.webUrl,
|
|
5672
|
+
accessToken,
|
|
5673
|
+
project: { id: project.id, name: project.name },
|
|
5674
|
+
me,
|
|
5675
|
+
deps: {
|
|
5676
|
+
fetchImpl,
|
|
5677
|
+
out,
|
|
5678
|
+
...deps.readLine ? { readLine: deps.readLine } : {},
|
|
5679
|
+
...deps.openBrowser ? { openBrowser: deps.openBrowser } : { openBrowser: defaultOpenBrowser },
|
|
5680
|
+
...deps.connect ?? {}
|
|
5681
|
+
}
|
|
5682
|
+
});
|
|
5442
5683
|
out("");
|
|
5443
5684
|
out("Run `neat logout` to switch back to your local daemon.");
|
|
5444
5685
|
}
|
|
@@ -5545,8 +5786,8 @@ async function runConnectCommand(rawArgs, deps = {}) {
|
|
|
5545
5786
|
}
|
|
5546
5787
|
|
|
5547
5788
|
// src/doctor-cli.ts
|
|
5548
|
-
import { promises as
|
|
5549
|
-
import
|
|
5789
|
+
import { promises as fs12 } from "fs";
|
|
5790
|
+
import path13 from "path";
|
|
5550
5791
|
|
|
5551
5792
|
// src/cli-client.ts
|
|
5552
5793
|
import { Provenance as Provenance3 } from "@neat.is/types";
|
|
@@ -5574,10 +5815,10 @@ function createHttpClient(baseUrl, bearerToken, fetchImpl = fetch) {
|
|
|
5574
5815
|
const root = baseUrl.replace(/\/$/, "");
|
|
5575
5816
|
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
5576
5817
|
return {
|
|
5577
|
-
async get(
|
|
5818
|
+
async get(path21) {
|
|
5578
5819
|
let res;
|
|
5579
5820
|
try {
|
|
5580
|
-
res = await fetchImpl(`${root}${
|
|
5821
|
+
res = await fetchImpl(`${root}${path21}`, {
|
|
5581
5822
|
headers: { ...authHeader }
|
|
5582
5823
|
});
|
|
5583
5824
|
} catch (err) {
|
|
@@ -5589,16 +5830,16 @@ function createHttpClient(baseUrl, bearerToken, fetchImpl = fetch) {
|
|
|
5589
5830
|
const body = await res.text().catch(() => "");
|
|
5590
5831
|
throw new HttpError(
|
|
5591
5832
|
res.status,
|
|
5592
|
-
`${res.status} ${res.statusText} on GET ${
|
|
5833
|
+
`${res.status} ${res.statusText} on GET ${path21}: ${body}`,
|
|
5593
5834
|
body
|
|
5594
5835
|
);
|
|
5595
5836
|
}
|
|
5596
5837
|
return await res.json();
|
|
5597
5838
|
},
|
|
5598
|
-
async post(
|
|
5839
|
+
async post(path21, body) {
|
|
5599
5840
|
let res;
|
|
5600
5841
|
try {
|
|
5601
|
-
res = await fetchImpl(`${root}${
|
|
5842
|
+
res = await fetchImpl(`${root}${path21}`, {
|
|
5602
5843
|
method: "POST",
|
|
5603
5844
|
headers: { "content-type": "application/json", ...authHeader },
|
|
5604
5845
|
body: JSON.stringify(body)
|
|
@@ -5612,7 +5853,7 @@ function createHttpClient(baseUrl, bearerToken, fetchImpl = fetch) {
|
|
|
5612
5853
|
const text = await res.text().catch(() => "");
|
|
5613
5854
|
throw new HttpError(
|
|
5614
5855
|
res.status,
|
|
5615
|
-
`${res.status} ${res.statusText} on POST ${
|
|
5856
|
+
`${res.status} ${res.statusText} on POST ${path21}: ${text}`,
|
|
5616
5857
|
text
|
|
5617
5858
|
);
|
|
5618
5859
|
}
|
|
@@ -5626,12 +5867,12 @@ function projectPath(project, suffix) {
|
|
|
5626
5867
|
}
|
|
5627
5868
|
async function runRootCause(client, input) {
|
|
5628
5869
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
5629
|
-
const
|
|
5870
|
+
const path21 = projectPath(
|
|
5630
5871
|
input.project,
|
|
5631
5872
|
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
5632
5873
|
);
|
|
5633
5874
|
try {
|
|
5634
|
-
const result = await client.get(
|
|
5875
|
+
const result = await client.get(path21);
|
|
5635
5876
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
5636
5877
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
5637
5878
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -5657,12 +5898,12 @@ async function runRootCause(client, input) {
|
|
|
5657
5898
|
}
|
|
5658
5899
|
async function runBlastRadius(client, input) {
|
|
5659
5900
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
5660
|
-
const
|
|
5901
|
+
const path21 = projectPath(
|
|
5661
5902
|
input.project,
|
|
5662
5903
|
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
5663
5904
|
);
|
|
5664
5905
|
try {
|
|
5665
|
-
const result = await client.get(
|
|
5906
|
+
const result = await client.get(path21);
|
|
5666
5907
|
if (result.totalAffected === 0) {
|
|
5667
5908
|
return {
|
|
5668
5909
|
summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
|
|
@@ -5696,12 +5937,12 @@ function formatBlastEntry(n) {
|
|
|
5696
5937
|
}
|
|
5697
5938
|
async function runDependencies(client, input) {
|
|
5698
5939
|
const depth = input.depth ?? 3;
|
|
5699
|
-
const
|
|
5940
|
+
const path21 = projectPath(
|
|
5700
5941
|
input.project,
|
|
5701
5942
|
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
5702
5943
|
);
|
|
5703
5944
|
try {
|
|
5704
|
-
const result = await client.get(
|
|
5945
|
+
const result = await client.get(path21);
|
|
5705
5946
|
if (result.total === 0) {
|
|
5706
5947
|
return {
|
|
5707
5948
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -5793,9 +6034,9 @@ function formatDuration(ms) {
|
|
|
5793
6034
|
return `${Math.round(h / 24)}d`;
|
|
5794
6035
|
}
|
|
5795
6036
|
async function runIncidents(client, input) {
|
|
5796
|
-
const
|
|
6037
|
+
const path21 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
5797
6038
|
try {
|
|
5798
|
-
const body = await client.get(
|
|
6039
|
+
const body = await client.get(path21);
|
|
5799
6040
|
const events = body.events;
|
|
5800
6041
|
if (events.length === 0) {
|
|
5801
6042
|
return {
|
|
@@ -6159,7 +6400,7 @@ function checkNode(nodeVersion) {
|
|
|
6159
6400
|
}
|
|
6160
6401
|
async function neatOutExists(cwd) {
|
|
6161
6402
|
try {
|
|
6162
|
-
const st = await
|
|
6403
|
+
const st = await fs12.stat(path13.join(cwd, "neat-out"));
|
|
6163
6404
|
return st.isDirectory();
|
|
6164
6405
|
} catch {
|
|
6165
6406
|
return false;
|
|
@@ -6278,9 +6519,6 @@ async function runDoctorCommand(argv, deps = {}) {
|
|
|
6278
6519
|
|
|
6279
6520
|
// src/login-cli.ts
|
|
6280
6521
|
import readline3 from "readline/promises";
|
|
6281
|
-
var PROBE_ATTEMPT_TIMEOUT_MS = 3e4;
|
|
6282
|
-
var PROBE_TOTAL_BUDGET_MS = 12e4;
|
|
6283
|
-
var PROBE_RETRY_PAUSE_MS = 2e3;
|
|
6284
6522
|
var DEFAULT_PROFILE_NAME = "hosted";
|
|
6285
6523
|
function readFlagValue(argv, i) {
|
|
6286
6524
|
const arg = argv[i];
|
|
@@ -6345,44 +6583,6 @@ function printLoginHelp(out) {
|
|
|
6345
6583
|
out(' the profile (default "hosted"); the paste token can also come from NEAT_LOGIN_TOKEN.');
|
|
6346
6584
|
out(" Exit 0 on success, 1 rejected/needs-action, 2 misuse, 3 unreachable.");
|
|
6347
6585
|
}
|
|
6348
|
-
function isTimeoutError(err) {
|
|
6349
|
-
const name = err?.name;
|
|
6350
|
-
return name === "TimeoutError" || name === "AbortError";
|
|
6351
|
-
}
|
|
6352
|
-
async function probeOnce(fetchImpl, root, token, timeoutMs) {
|
|
6353
|
-
let res;
|
|
6354
|
-
try {
|
|
6355
|
-
res = await fetchImpl(`${root}/health`, {
|
|
6356
|
-
headers: { authorization: `Bearer ${token}` },
|
|
6357
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
6358
|
-
});
|
|
6359
|
-
} catch (err) {
|
|
6360
|
-
if (isTimeoutError(err)) return { kind: "timeout" };
|
|
6361
|
-
return { kind: "unreachable", detail: err.message };
|
|
6362
|
-
}
|
|
6363
|
-
if (res.status === 401 || res.status === 403) return { kind: "unauthorized", status: res.status };
|
|
6364
|
-
if (!res.ok) return { kind: "not-neat", status: res.status };
|
|
6365
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
6366
|
-
if (!contentType.includes("json")) return { kind: "not-neat", status: res.status };
|
|
6367
|
-
return { kind: "ok" };
|
|
6368
|
-
}
|
|
6369
|
-
async function probeDaemon(fetchImpl, endpoint, token, hooks) {
|
|
6370
|
-
const root = endpoint.replace(/\/$/, "");
|
|
6371
|
-
const deadline = hooks.now() + PROBE_TOTAL_BUDGET_MS;
|
|
6372
|
-
let warned = false;
|
|
6373
|
-
for (; ; ) {
|
|
6374
|
-
const result = await probeOnce(fetchImpl, root, token, PROBE_ATTEMPT_TIMEOUT_MS);
|
|
6375
|
-
if (result.kind !== "timeout") return result;
|
|
6376
|
-
if (!warned) {
|
|
6377
|
-
hooks.onWaiting();
|
|
6378
|
-
warned = true;
|
|
6379
|
-
}
|
|
6380
|
-
if (hooks.now() >= deadline) {
|
|
6381
|
-
return { kind: "unreachable", detail: `no response after ${Math.round(PROBE_TOTAL_BUDGET_MS / 1e3)}s` };
|
|
6382
|
-
}
|
|
6383
|
-
await hooks.sleep(PROBE_RETRY_PAUSE_MS);
|
|
6384
|
-
}
|
|
6385
|
-
}
|
|
6386
6586
|
async function runLoginCommand(argv, deps = {}) {
|
|
6387
6587
|
const out = deps.out ?? ((line) => console.log(line));
|
|
6388
6588
|
const err = deps.err ?? ((line) => console.error(line));
|
|
@@ -6415,8 +6615,13 @@ async function runLoginCommand(argv, deps = {}) {
|
|
|
6415
6615
|
fetchImpl,
|
|
6416
6616
|
out,
|
|
6417
6617
|
err,
|
|
6618
|
+
readLine,
|
|
6619
|
+
sleep: sleep2,
|
|
6620
|
+
now,
|
|
6418
6621
|
...deps.openBrowser ? { openBrowser: deps.openBrowser } : {},
|
|
6419
|
-
...deps.home ? { home: deps.home } : {}
|
|
6622
|
+
...deps.home ? { home: deps.home } : {},
|
|
6623
|
+
...deps.signal ? { signal: deps.signal } : {},
|
|
6624
|
+
...deps.connect ? { connect: deps.connect } : {}
|
|
6420
6625
|
}
|
|
6421
6626
|
);
|
|
6422
6627
|
}
|
|
@@ -6572,8 +6777,8 @@ async function defaultReadSecret(prompt) {
|
|
|
6572
6777
|
}
|
|
6573
6778
|
|
|
6574
6779
|
// src/welcome.ts
|
|
6575
|
-
import { promises as
|
|
6576
|
-
import
|
|
6780
|
+
import { promises as fs13 } from "fs";
|
|
6781
|
+
import path14 from "path";
|
|
6577
6782
|
import readline4 from "readline/promises";
|
|
6578
6783
|
var WORDMARK = [
|
|
6579
6784
|
"\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
|
|
@@ -6632,15 +6837,17 @@ async function runFrontDoor(deps, out) {
|
|
|
6632
6837
|
const login = deps.login ?? (() => Promise.resolve(0));
|
|
6633
6838
|
const orchestrator = deps.orchestrator ?? (() => Promise.resolve(0));
|
|
6634
6839
|
const cwd = deps.cwd ?? process.cwd();
|
|
6840
|
+
const flagGiven = deps.instrumentFlagGiven ?? false;
|
|
6841
|
+
const readRegistry2 = deps.readRegistry ?? (() => readRegistry());
|
|
6635
6842
|
printHeader(out);
|
|
6636
6843
|
out("Welcome to NEAT. Let's get you a graph of this system.");
|
|
6637
6844
|
out("");
|
|
6638
6845
|
const keyReader = deps.readKey !== void 0 ? { read: deps.readKey, done: () => {
|
|
6639
6846
|
} } : createKeyReader();
|
|
6640
6847
|
const picked = keyReader !== null ? await selectOption(out, keyReader, deps.moveCursorUp ?? defaultMoveCursorUp) : await selectByNumber(out, readLine);
|
|
6641
|
-
if (picked === void 0) return runSelfHosted(out, readLine, orchestrator, cwd);
|
|
6848
|
+
if (picked === void 0) return runSelfHosted(out, readLine, orchestrator, cwd, readRegistry2, flagGiven);
|
|
6642
6849
|
if (picked === 0) return login(["--browser"]);
|
|
6643
|
-
return runSelfHosted(out, readLine, orchestrator, cwd);
|
|
6850
|
+
return runSelfHosted(out, readLine, orchestrator, cwd, readRegistry2, flagGiven);
|
|
6644
6851
|
}
|
|
6645
6852
|
var MENU_OPTIONS = [
|
|
6646
6853
|
"Log me into Hosted Neat",
|
|
@@ -6743,7 +6950,7 @@ function decodeKey(buf) {
|
|
|
6743
6950
|
function defaultMoveCursorUp(rows) {
|
|
6744
6951
|
if (rows > 0) process.stdout.write(`\x1B[${rows}A`);
|
|
6745
6952
|
}
|
|
6746
|
-
async function runSelfHosted(out, readLine, orchestrator, cwd) {
|
|
6953
|
+
async function runSelfHosted(out, readLine, orchestrator, cwd, readRegistry2, instrumentFlagGiven) {
|
|
6747
6954
|
const wantsPrompt = (await readLine("Print a copy-paste setup prompt for your coding agent? [Y/n]: "))?.trim().toLowerCase();
|
|
6748
6955
|
if (wantsPrompt !== "n" && wantsPrompt !== "no") {
|
|
6749
6956
|
out("");
|
|
@@ -6754,9 +6961,69 @@ async function runSelfHosted(out, readLine, orchestrator, cwd) {
|
|
|
6754
6961
|
out("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
6755
6962
|
out("");
|
|
6756
6963
|
}
|
|
6964
|
+
const project = await resolveProjectName(out, readLine, cwd, readRegistry2);
|
|
6965
|
+
const declined = instrumentFlagGiven ? void 0 : await askToInstrument(out, readLine);
|
|
6757
6966
|
out("Building your local graph now\u2026");
|
|
6758
6967
|
out("");
|
|
6759
|
-
|
|
6968
|
+
const overrides = {
|
|
6969
|
+
...project !== void 0 ? { project } : {},
|
|
6970
|
+
...declined === void 0 ? {} : declined ? { noInstrument: true } : { yes: true }
|
|
6971
|
+
};
|
|
6972
|
+
return orchestrator(cwd, Object.keys(overrides).length > 0 ? overrides : void 0);
|
|
6973
|
+
}
|
|
6974
|
+
async function askToInstrument(out, readLine) {
|
|
6975
|
+
out("Instrument the services for OpenTelemetry now?");
|
|
6976
|
+
out("This edits package.json / requirements.txt / go.mod and runs the package manager.");
|
|
6977
|
+
const answer = (await readLine("[Y/n]: "))?.trim().toLowerCase();
|
|
6978
|
+
const declined = answer === "n" || answer === "no";
|
|
6979
|
+
if (declined) {
|
|
6980
|
+
out("");
|
|
6981
|
+
out("Skipping instrumentation \u2014 the graph will hold the declared side only.");
|
|
6982
|
+
out("Run `neat init . --apply` when you want the runtime half.");
|
|
6983
|
+
}
|
|
6984
|
+
out("");
|
|
6985
|
+
return declined;
|
|
6986
|
+
}
|
|
6987
|
+
async function resolveProjectName(out, readLine, cwd, readRegistry2) {
|
|
6988
|
+
const here = path14.resolve(cwd);
|
|
6989
|
+
const base = path14.basename(here);
|
|
6990
|
+
let taken;
|
|
6991
|
+
try {
|
|
6992
|
+
const { projects } = await readRegistry2();
|
|
6993
|
+
taken = new Map(projects.map((p) => [p.name, p.path]));
|
|
6994
|
+
} catch {
|
|
6995
|
+
return void 0;
|
|
6996
|
+
}
|
|
6997
|
+
const clash = taken.get(base);
|
|
6998
|
+
if (clash === void 0 || path14.resolve(clash) === here) return void 0;
|
|
6999
|
+
out("");
|
|
7000
|
+
out(`A project named \`${base}\` is already registered (${clash}).`);
|
|
7001
|
+
const suggested = firstFreeName(base, taken);
|
|
7002
|
+
for (; ; ) {
|
|
7003
|
+
const answer = (await readLine(`Name this one: [${suggested}] `))?.trim();
|
|
7004
|
+
if (answer === void 0) return suggested;
|
|
7005
|
+
const chosen = answer.length === 0 ? suggested : answer;
|
|
7006
|
+
if (!isUsableProjectName(chosen)) {
|
|
7007
|
+
out(`"${chosen}" won't work as a project name \u2014 letters, digits, dot, dash and underscore.`);
|
|
7008
|
+
continue;
|
|
7009
|
+
}
|
|
7010
|
+
const other = taken.get(chosen);
|
|
7011
|
+
if (other !== void 0 && path14.resolve(other) !== here) {
|
|
7012
|
+
out(`\`${chosen}\` is registered too (${other}).`);
|
|
7013
|
+
continue;
|
|
7014
|
+
}
|
|
7015
|
+
out("");
|
|
7016
|
+
return chosen;
|
|
7017
|
+
}
|
|
7018
|
+
}
|
|
7019
|
+
function firstFreeName(base, taken) {
|
|
7020
|
+
for (let n = 2; ; n++) {
|
|
7021
|
+
const candidate = `${base}-${n}`;
|
|
7022
|
+
if (!taken.has(candidate)) return candidate;
|
|
7023
|
+
}
|
|
7024
|
+
}
|
|
7025
|
+
function isUsableProjectName(name) {
|
|
7026
|
+
return name.length > 0 && name !== "." && name !== ".." && /^[A-Za-z0-9._-]+$/.test(name);
|
|
6760
7027
|
}
|
|
6761
7028
|
async function shouldShowWelcome(deps = {}) {
|
|
6762
7029
|
const stdinTTY = deps.stdinIsTTY ?? Boolean(process.stdin.isTTY);
|
|
@@ -6766,9 +7033,9 @@ async function shouldShowWelcome(deps = {}) {
|
|
|
6766
7033
|
const readRegistry2 = deps.readRegistry ?? (() => readRegistry());
|
|
6767
7034
|
const hasSnapshot = deps.hasSnapshot ?? defaultHasSnapshot;
|
|
6768
7035
|
try {
|
|
6769
|
-
const here =
|
|
7036
|
+
const here = path14.resolve(cwd);
|
|
6770
7037
|
const { projects } = await readRegistry2();
|
|
6771
|
-
if (projects.some((p) =>
|
|
7038
|
+
if (projects.some((p) => path14.resolve(p.path) === here)) return false;
|
|
6772
7039
|
if (await hasSnapshot(here)) return false;
|
|
6773
7040
|
return true;
|
|
6774
7041
|
} catch {
|
|
@@ -6777,7 +7044,7 @@ async function shouldShowWelcome(deps = {}) {
|
|
|
6777
7044
|
}
|
|
6778
7045
|
async function defaultHasSnapshot(cwd) {
|
|
6779
7046
|
try {
|
|
6780
|
-
const entries = await
|
|
7047
|
+
const entries = await fs13.readdir(path14.join(cwd, "neat-out"));
|
|
6781
7048
|
return entries.some((e) => e.endsWith(".json"));
|
|
6782
7049
|
} catch {
|
|
6783
7050
|
return false;
|
|
@@ -6794,27 +7061,27 @@ async function defaultReadLine2(prompt) {
|
|
|
6794
7061
|
}
|
|
6795
7062
|
|
|
6796
7063
|
// src/hooks-cli.ts
|
|
6797
|
-
import
|
|
7064
|
+
import path15 from "path";
|
|
6798
7065
|
import os2 from "os";
|
|
6799
|
-
import { promises as
|
|
7066
|
+
import { promises as fs14 } from "fs";
|
|
6800
7067
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
6801
7068
|
var HOOK_FILENAME = "neat-search-nudge.mjs";
|
|
6802
7069
|
var GUIDE_FILENAME = "GRAPH_FIRST.md";
|
|
6803
7070
|
var GUIDE_INSTALL_NAME = "neat-graph-first.md";
|
|
6804
7071
|
var HOOK_MATCHER = "Grep|Glob|Bash";
|
|
6805
7072
|
function moduleDir() {
|
|
6806
|
-
return typeof __dirname !== "undefined" ? __dirname :
|
|
7073
|
+
return typeof __dirname !== "undefined" ? __dirname : path15.dirname(fileURLToPath3(import.meta.url));
|
|
6807
7074
|
}
|
|
6808
7075
|
async function readSkillAsset(rel) {
|
|
6809
7076
|
const here = moduleDir();
|
|
6810
7077
|
const candidates = [
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
7078
|
+
path15.resolve(here, "../../claude-skill", rel),
|
|
7079
|
+
path15.resolve(here, "../../../claude-skill", rel),
|
|
7080
|
+
path15.resolve(here, "../claude-skill", rel)
|
|
6814
7081
|
];
|
|
6815
7082
|
for (const candidate of candidates) {
|
|
6816
7083
|
try {
|
|
6817
|
-
return await
|
|
7084
|
+
return await fs14.readFile(candidate, "utf8");
|
|
6818
7085
|
} catch {
|
|
6819
7086
|
}
|
|
6820
7087
|
}
|
|
@@ -6824,20 +7091,20 @@ async function readSkillAsset(rel) {
|
|
|
6824
7091
|
}
|
|
6825
7092
|
function neatHome2() {
|
|
6826
7093
|
const override = process.env.NEAT_HOME;
|
|
6827
|
-
if (override && override.length > 0) return
|
|
6828
|
-
return
|
|
7094
|
+
if (override && override.length > 0) return path15.resolve(override);
|
|
7095
|
+
return path15.join(os2.homedir(), ".neat");
|
|
6829
7096
|
}
|
|
6830
7097
|
function claudeSettingsPath() {
|
|
6831
7098
|
const override = process.env.NEAT_CLAUDE_SETTINGS;
|
|
6832
|
-
if (override && override.length > 0) return
|
|
7099
|
+
if (override && override.length > 0) return path15.resolve(override);
|
|
6833
7100
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? os2.homedir();
|
|
6834
|
-
return
|
|
7101
|
+
return path15.join(home, ".claude", "settings.json");
|
|
6835
7102
|
}
|
|
6836
7103
|
function installedHookPath() {
|
|
6837
|
-
return
|
|
7104
|
+
return path15.join(neatHome2(), "hooks", HOOK_FILENAME);
|
|
6838
7105
|
}
|
|
6839
7106
|
function gateFlagPath() {
|
|
6840
|
-
return
|
|
7107
|
+
return path15.join(neatHome2(), "hooks", "gate-enabled");
|
|
6841
7108
|
}
|
|
6842
7109
|
function isNeatSearchEntry(entry2) {
|
|
6843
7110
|
return (entry2.hooks ?? []).some(
|
|
@@ -6870,14 +7137,14 @@ async function runHooks(opts) {
|
|
|
6870
7137
|
const hookScript = await readSkillAsset(`hooks/${HOOK_FILENAME}`);
|
|
6871
7138
|
const guide = await readSkillAsset(GUIDE_FILENAME);
|
|
6872
7139
|
const scriptPath = installedHookPath();
|
|
6873
|
-
await
|
|
6874
|
-
await
|
|
6875
|
-
const guidePath =
|
|
6876
|
-
await
|
|
7140
|
+
await fs14.mkdir(path15.dirname(scriptPath), { recursive: true });
|
|
7141
|
+
await fs14.writeFile(scriptPath, hookScript, { mode: 493 });
|
|
7142
|
+
const guidePath = path15.join(neatHome2(), GUIDE_INSTALL_NAME);
|
|
7143
|
+
await fs14.writeFile(guidePath, guide, "utf8");
|
|
6877
7144
|
const settingsFile = claudeSettingsPath();
|
|
6878
7145
|
let settings = {};
|
|
6879
7146
|
try {
|
|
6880
|
-
settings = JSON.parse(await
|
|
7147
|
+
settings = JSON.parse(await fs14.readFile(settingsFile, "utf8"));
|
|
6881
7148
|
} catch (err) {
|
|
6882
7149
|
if (err.code !== "ENOENT") {
|
|
6883
7150
|
console.error(
|
|
@@ -6899,14 +7166,14 @@ async function runHooks(opts) {
|
|
|
6899
7166
|
...settings,
|
|
6900
7167
|
hooks: { ...hooks, PreToolUse: preToolUse }
|
|
6901
7168
|
};
|
|
6902
|
-
await
|
|
6903
|
-
await
|
|
7169
|
+
await fs14.mkdir(path15.dirname(settingsFile), { recursive: true });
|
|
7170
|
+
await fs14.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
6904
7171
|
const flag = gateFlagPath();
|
|
6905
7172
|
if (opts.gate) {
|
|
6906
|
-
await
|
|
6907
|
-
await
|
|
7173
|
+
await fs14.mkdir(path15.dirname(flag), { recursive: true });
|
|
7174
|
+
await fs14.writeFile(flag, "1\n", "utf8");
|
|
6908
7175
|
} else {
|
|
6909
|
-
await
|
|
7176
|
+
await fs14.rm(flag, { force: true });
|
|
6910
7177
|
}
|
|
6911
7178
|
const mode = opts.gate ? "GATE (deny search until you ask the graph)" : "nudge (search still runs)";
|
|
6912
7179
|
console.log(`neat hooks: installed the search hook in ${opts.gate ? "gate" : "nudge"} mode`);
|
|
@@ -6995,8 +7262,8 @@ async function runHooksCommand(args) {
|
|
|
6995
7262
|
}
|
|
6996
7263
|
|
|
6997
7264
|
// src/claude-cli.ts
|
|
6998
|
-
import
|
|
6999
|
-
import { promises as
|
|
7265
|
+
import path16 from "path";
|
|
7266
|
+
import { promises as fs15 } from "fs";
|
|
7000
7267
|
var NEAT_SECTION_HEADING = "## neat";
|
|
7001
7268
|
var NEAT_DIRECTIVE_BODY = `This project has NEAT wired in: a live, fused semantic graph of the system \u2014
|
|
7002
7269
|
code and runtime behaviour (OpenTelemetry) in one model, every fact tagged with
|
|
@@ -7030,8 +7297,8 @@ ${NEAT_DIRECTIVE_BODY}
|
|
|
7030
7297
|
}
|
|
7031
7298
|
function claudeMdPath() {
|
|
7032
7299
|
const override = process.env.NEAT_CLAUDE_MD;
|
|
7033
|
-
if (override && override.length > 0) return
|
|
7034
|
-
return
|
|
7300
|
+
if (override && override.length > 0) return path16.resolve(override);
|
|
7301
|
+
return path16.join(process.cwd(), "CLAUDE.md");
|
|
7035
7302
|
}
|
|
7036
7303
|
function splitAroundSection(raw) {
|
|
7037
7304
|
const lines = raw.split("\n");
|
|
@@ -7059,7 +7326,7 @@ function compose(before, after) {
|
|
|
7059
7326
|
}
|
|
7060
7327
|
async function readIfExists(file) {
|
|
7061
7328
|
try {
|
|
7062
|
-
return await
|
|
7329
|
+
return await fs15.readFile(file, "utf8");
|
|
7063
7330
|
} catch (err) {
|
|
7064
7331
|
if (err.code === "ENOENT") return null;
|
|
7065
7332
|
throw err;
|
|
@@ -7070,8 +7337,8 @@ async function runInstall() {
|
|
|
7070
7337
|
const raw = await readIfExists(file) ?? "";
|
|
7071
7338
|
const { before, after, found } = splitAroundSection(raw);
|
|
7072
7339
|
const next = compose(before, after);
|
|
7073
|
-
await
|
|
7074
|
-
await
|
|
7340
|
+
await fs15.mkdir(path16.dirname(file), { recursive: true });
|
|
7341
|
+
await fs15.writeFile(file, next, "utf8");
|
|
7075
7342
|
const verb = raw.length === 0 ? "created" : found ? "refreshed" : "added";
|
|
7076
7343
|
console.log(`neat claude: ${verb} the \`${NEAT_SECTION_HEADING}\` section in ${file}`);
|
|
7077
7344
|
console.log("Your agent will now reach for `neat ask` before Read/Grep/Bash. Restart the");
|
|
@@ -7092,7 +7359,7 @@ async function runUninstall() {
|
|
|
7092
7359
|
}
|
|
7093
7360
|
const remaining = [before, after].filter((s) => s.length > 0).join("\n\n");
|
|
7094
7361
|
const next = remaining.length > 0 ? remaining.replace(/\n*$/, "") + "\n" : "";
|
|
7095
|
-
await
|
|
7362
|
+
await fs15.writeFile(file, next, "utf8");
|
|
7096
7363
|
console.log(`neat claude: removed the \`${NEAT_SECTION_HEADING}\` section from ${file}.`);
|
|
7097
7364
|
return { exitCode: 0 };
|
|
7098
7365
|
}
|
|
@@ -7134,9 +7401,9 @@ async function runClaudeCommand(args) {
|
|
|
7134
7401
|
}
|
|
7135
7402
|
|
|
7136
7403
|
// src/codex-cli.ts
|
|
7137
|
-
import
|
|
7404
|
+
import path17 from "path";
|
|
7138
7405
|
import os3 from "os";
|
|
7139
|
-
import { promises as
|
|
7406
|
+
import { promises as fs16 } from "fs";
|
|
7140
7407
|
import { isDeepStrictEqual } from "util";
|
|
7141
7408
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
7142
7409
|
var CODEX_MCP_SERVER = {
|
|
@@ -7154,14 +7421,14 @@ var NEAT_GRAPH_FIRST_START = "<!-- neat:graph-first -->";
|
|
|
7154
7421
|
var NEAT_GRAPH_FIRST_END = "<!-- /neat:graph-first -->";
|
|
7155
7422
|
function codexConfigPath() {
|
|
7156
7423
|
const override = process.env.NEAT_CODEX_CONFIG;
|
|
7157
|
-
if (override && override.length > 0) return
|
|
7424
|
+
if (override && override.length > 0) return path17.resolve(override);
|
|
7158
7425
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? os3.homedir();
|
|
7159
|
-
return
|
|
7426
|
+
return path17.join(home, ".codex", "config.toml");
|
|
7160
7427
|
}
|
|
7161
7428
|
function agentsFilePath() {
|
|
7162
7429
|
const override = process.env.NEAT_CODEX_AGENTS;
|
|
7163
|
-
if (override && override.length > 0) return
|
|
7164
|
-
return
|
|
7430
|
+
if (override && override.length > 0) return path17.resolve(override);
|
|
7431
|
+
return path17.join(process.cwd(), "AGENTS.md");
|
|
7165
7432
|
}
|
|
7166
7433
|
function isTableHeader(line) {
|
|
7167
7434
|
return /^\s*\[\[?[^\]]+\]\]?\s*$/.test(line);
|
|
@@ -7295,7 +7562,7 @@ async function runCodex(opts) {
|
|
|
7295
7562
|
const agentsPath = agentsFilePath();
|
|
7296
7563
|
let configRaw = "";
|
|
7297
7564
|
try {
|
|
7298
|
-
configRaw = await
|
|
7565
|
+
configRaw = await fs16.readFile(configPath, "utf8");
|
|
7299
7566
|
} catch (err) {
|
|
7300
7567
|
if (err.code !== "ENOENT") {
|
|
7301
7568
|
console.error(`neat codex: failed to read ${configPath} \u2014 ${err.message}`);
|
|
@@ -7304,7 +7571,7 @@ async function runCodex(opts) {
|
|
|
7304
7571
|
}
|
|
7305
7572
|
let agentsRaw = "";
|
|
7306
7573
|
try {
|
|
7307
|
-
agentsRaw = await
|
|
7574
|
+
agentsRaw = await fs16.readFile(agentsPath, "utf8");
|
|
7308
7575
|
} catch (err) {
|
|
7309
7576
|
if (err.code !== "ENOENT") {
|
|
7310
7577
|
console.error(`neat codex: failed to read ${agentsPath} \u2014 ${err.message}`);
|
|
@@ -7344,15 +7611,15 @@ async function runCodex(opts) {
|
|
|
7344
7611
|
return { exitCode: 0 };
|
|
7345
7612
|
}
|
|
7346
7613
|
if (config.changed) {
|
|
7347
|
-
await
|
|
7348
|
-
await
|
|
7614
|
+
await fs16.mkdir(path17.dirname(configPath), { recursive: true });
|
|
7615
|
+
await fs16.writeFile(configPath, config.text, "utf8");
|
|
7349
7616
|
console.log(`neat codex: wrote [mcp_servers.neat] to ${configPath}`);
|
|
7350
7617
|
} else {
|
|
7351
7618
|
console.log(`neat codex: ${configPath} already has NEAT's MCP server`);
|
|
7352
7619
|
}
|
|
7353
7620
|
if (agents.changed) {
|
|
7354
|
-
await
|
|
7355
|
-
await
|
|
7621
|
+
await fs16.mkdir(path17.dirname(agentsPath), { recursive: true });
|
|
7622
|
+
await fs16.writeFile(agentsPath, agents.text, "utf8");
|
|
7356
7623
|
console.log(`neat codex: wrote the graph-first block to ${agentsPath}`);
|
|
7357
7624
|
} else {
|
|
7358
7625
|
console.log(`neat codex: ${agentsPath} already has the graph-first block`);
|
|
@@ -7408,9 +7675,9 @@ async function runCodexCommand(args) {
|
|
|
7408
7675
|
}
|
|
7409
7676
|
|
|
7410
7677
|
// src/editors-cli.ts
|
|
7411
|
-
import
|
|
7678
|
+
import path18 from "path";
|
|
7412
7679
|
import os4 from "os";
|
|
7413
|
-
import { promises as
|
|
7680
|
+
import { promises as fs17 } from "fs";
|
|
7414
7681
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
7415
7682
|
import * as jsonc from "jsonc-parser";
|
|
7416
7683
|
var NEAT_MCP_SERVER = {
|
|
@@ -7434,17 +7701,17 @@ function homeDir() {
|
|
|
7434
7701
|
}
|
|
7435
7702
|
function xdgConfigDir() {
|
|
7436
7703
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
7437
|
-
return xdg && xdg.length > 0 ?
|
|
7704
|
+
return xdg && xdg.length > 0 ? path18.resolve(xdg) : path18.join(homeDir(), ".config");
|
|
7438
7705
|
}
|
|
7439
7706
|
function envOverride(name) {
|
|
7440
7707
|
const v = process.env[name];
|
|
7441
|
-
return v && v.length > 0 ?
|
|
7708
|
+
return v && v.length > 0 ? path18.resolve(v) : void 0;
|
|
7442
7709
|
}
|
|
7443
7710
|
var CURSOR_CLIENT = {
|
|
7444
7711
|
id: "cursor",
|
|
7445
7712
|
label: "Cursor",
|
|
7446
7713
|
docsUrl: "https://docs.cursor.com/context/mcp",
|
|
7447
|
-
mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ??
|
|
7714
|
+
mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ?? path18.join(homeDir(), ".cursor", "mcp.json"),
|
|
7448
7715
|
mcpContainerKey: "mcpServers",
|
|
7449
7716
|
format: "json",
|
|
7450
7717
|
// Cursor still reads a single `.cursorrules` at the project root (the modern
|
|
@@ -7456,7 +7723,7 @@ var DEVIN_CLIENT = {
|
|
|
7456
7723
|
id: "devin",
|
|
7457
7724
|
label: "Devin Desktop (Cascade)",
|
|
7458
7725
|
docsUrl: "https://docs.devin.ai/desktop/cascade/mcp",
|
|
7459
|
-
mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ??
|
|
7726
|
+
mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? path18.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
|
|
7460
7727
|
mcpContainerKey: "mcpServers",
|
|
7461
7728
|
format: "json",
|
|
7462
7729
|
rulesFileName: ".windsurfrules"
|
|
@@ -7465,7 +7732,7 @@ var GEMINI_CLIENT = {
|
|
|
7465
7732
|
id: "gemini",
|
|
7466
7733
|
label: "Gemini CLI",
|
|
7467
7734
|
docsUrl: "https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md",
|
|
7468
|
-
mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ??
|
|
7735
|
+
mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? path18.join(homeDir(), ".gemini", "settings.json"),
|
|
7469
7736
|
mcpContainerKey: "mcpServers",
|
|
7470
7737
|
format: "json",
|
|
7471
7738
|
rulesFileName: "GEMINI.md"
|
|
@@ -7474,7 +7741,7 @@ var QWEN_CLIENT = {
|
|
|
7474
7741
|
id: "qwen",
|
|
7475
7742
|
label: "Qwen Code",
|
|
7476
7743
|
docsUrl: "https://qwenlm.github.io/qwen-code-docs/en/users/features/mcp/",
|
|
7477
|
-
mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ??
|
|
7744
|
+
mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? path18.join(homeDir(), ".qwen", "settings.json"),
|
|
7478
7745
|
mcpContainerKey: "mcpServers",
|
|
7479
7746
|
format: "json",
|
|
7480
7747
|
rulesFileName: "QWEN.md"
|
|
@@ -7483,7 +7750,7 @@ var AMAZONQ_CLIENT = {
|
|
|
7483
7750
|
id: "amazonq",
|
|
7484
7751
|
label: "Amazon Q Developer CLI",
|
|
7485
7752
|
docsUrl: "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-configuration.html",
|
|
7486
|
-
mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ??
|
|
7753
|
+
mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? path18.join(homeDir(), ".aws", "amazonq", "mcp.json"),
|
|
7487
7754
|
mcpContainerKey: "mcpServers",
|
|
7488
7755
|
format: "json"
|
|
7489
7756
|
};
|
|
@@ -7491,7 +7758,7 @@ var ROOCODE_CLIENT = {
|
|
|
7491
7758
|
id: "roocode",
|
|
7492
7759
|
label: "Roo Code",
|
|
7493
7760
|
docsUrl: "https://roocodeinc.github.io/Roo-Code/features/mcp/using-mcp-in-roo",
|
|
7494
|
-
mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ??
|
|
7761
|
+
mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? path18.join(process.cwd(), ".roo", "mcp.json"),
|
|
7495
7762
|
mcpContainerKey: "mcpServers",
|
|
7496
7763
|
format: "json"
|
|
7497
7764
|
};
|
|
@@ -7504,9 +7771,9 @@ var ZED_CLIENT = {
|
|
|
7504
7771
|
if (override) return override;
|
|
7505
7772
|
if (process.platform === "win32") {
|
|
7506
7773
|
const appData = process.env.APPDATA;
|
|
7507
|
-
if (appData && appData.length > 0) return
|
|
7774
|
+
if (appData && appData.length > 0) return path18.join(appData, "Zed", "settings.json");
|
|
7508
7775
|
}
|
|
7509
|
-
return
|
|
7776
|
+
return path18.join(homeDir(), ".config", "zed", "settings.json");
|
|
7510
7777
|
},
|
|
7511
7778
|
mcpContainerKey: "context_servers",
|
|
7512
7779
|
format: "jsonc",
|
|
@@ -7516,7 +7783,7 @@ var OPENCODE_CLIENT = {
|
|
|
7516
7783
|
id: "opencode",
|
|
7517
7784
|
label: "OpenCode",
|
|
7518
7785
|
docsUrl: "https://opencode.ai/docs/mcp-servers/",
|
|
7519
|
-
mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ??
|
|
7786
|
+
mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? path18.join(xdgConfigDir(), "opencode", "opencode.json"),
|
|
7520
7787
|
mcpContainerKey: "mcp",
|
|
7521
7788
|
format: "json",
|
|
7522
7789
|
serverEntry: NEAT_OPENCODE_SERVER,
|
|
@@ -7526,7 +7793,7 @@ var CRUSH_CLIENT = {
|
|
|
7526
7793
|
id: "crush",
|
|
7527
7794
|
label: "Crush",
|
|
7528
7795
|
docsUrl: "https://charmbracelet-crush.mintlify.app/configuration/mcp",
|
|
7529
|
-
mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ??
|
|
7796
|
+
mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? path18.join(xdgConfigDir(), "crush", "crush.json"),
|
|
7530
7797
|
mcpContainerKey: "mcp",
|
|
7531
7798
|
format: "json",
|
|
7532
7799
|
serverEntry: NEAT_CRUSH_SERVER,
|
|
@@ -7589,7 +7856,7 @@ async function planMcp(client, mcpPath) {
|
|
|
7589
7856
|
const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
|
|
7590
7857
|
let raw = "";
|
|
7591
7858
|
try {
|
|
7592
|
-
raw = await
|
|
7859
|
+
raw = await fs17.readFile(mcpPath, "utf8");
|
|
7593
7860
|
} catch (err) {
|
|
7594
7861
|
const e = err;
|
|
7595
7862
|
if (e.code === "ENOENT") {
|
|
@@ -7631,7 +7898,7 @@ async function runEditorInstall(client, opts) {
|
|
|
7631
7898
|
const mcpPath = client.mcpConfigPath();
|
|
7632
7899
|
const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
|
|
7633
7900
|
const hasRules = typeof client.rulesFileName === "string";
|
|
7634
|
-
const rulesPath = hasRules ?
|
|
7901
|
+
const rulesPath = hasRules ? path18.join(opts.projectDir, client.rulesFileName) : "";
|
|
7635
7902
|
const mcp = await planMcp(client, mcpPath);
|
|
7636
7903
|
if (mcp === null) return { exitCode: 1 };
|
|
7637
7904
|
let existingRules = "";
|
|
@@ -7640,7 +7907,7 @@ async function runEditorInstall(client, opts) {
|
|
|
7640
7907
|
let block = "";
|
|
7641
7908
|
if (hasRules) {
|
|
7642
7909
|
try {
|
|
7643
|
-
existingRules = await
|
|
7910
|
+
existingRules = await fs17.readFile(rulesPath, "utf8");
|
|
7644
7911
|
} catch (err) {
|
|
7645
7912
|
if (err.code !== "ENOENT") {
|
|
7646
7913
|
console.error(`neat ${client.id}: failed to read ${rulesPath} \u2014 ${err.message}`);
|
|
@@ -7674,11 +7941,11 @@ async function runEditorInstall(client, opts) {
|
|
|
7674
7941
|
);
|
|
7675
7942
|
return { exitCode: 0 };
|
|
7676
7943
|
}
|
|
7677
|
-
await
|
|
7678
|
-
await
|
|
7944
|
+
await fs17.mkdir(path18.dirname(mcpPath), { recursive: true });
|
|
7945
|
+
await fs17.writeFile(mcpPath, mcp.text, "utf8");
|
|
7679
7946
|
if (hasRules) {
|
|
7680
|
-
await
|
|
7681
|
-
await
|
|
7947
|
+
await fs17.mkdir(path18.dirname(rulesPath), { recursive: true });
|
|
7948
|
+
await fs17.writeFile(rulesPath, newRules, "utf8");
|
|
7682
7949
|
}
|
|
7683
7950
|
console.log(`neat ${client.id}: wired NEAT into ${client.label}`);
|
|
7684
7951
|
console.log(` MCP server: ${mcpPath} (${client.mcpContainerKey}.neat \u2192 npx -y @neat.is/mcp)`);
|
|
@@ -8178,7 +8445,7 @@ function sleep(ms, signal) {
|
|
|
8178
8445
|
}
|
|
8179
8446
|
|
|
8180
8447
|
// src/cli-verbs.ts
|
|
8181
|
-
import
|
|
8448
|
+
import path19 from "path";
|
|
8182
8449
|
async function resolveProjectEntry(opts) {
|
|
8183
8450
|
const entries = await listProjects();
|
|
8184
8451
|
if (opts.project) {
|
|
@@ -8188,7 +8455,7 @@ async function resolveProjectEntry(opts) {
|
|
|
8188
8455
|
const cwd = opts.cwd ?? process.cwd();
|
|
8189
8456
|
const resolvedCwd = await normalizeProjectPath(cwd);
|
|
8190
8457
|
for (const entry2 of entries) {
|
|
8191
|
-
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${
|
|
8458
|
+
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path19.sep}`)) {
|
|
8192
8459
|
return entry2;
|
|
8193
8460
|
}
|
|
8194
8461
|
}
|
|
@@ -8765,7 +9032,7 @@ async function buildPatchSections(services, project) {
|
|
|
8765
9032
|
}
|
|
8766
9033
|
async function runInit(opts) {
|
|
8767
9034
|
const written = [];
|
|
8768
|
-
const stat = await
|
|
9035
|
+
const stat = await fs18.stat(opts.scanPath).catch(() => null);
|
|
8769
9036
|
if (!stat || !stat.isDirectory()) {
|
|
8770
9037
|
console.error(`neat init: ${opts.scanPath} is not a directory`);
|
|
8771
9038
|
return { exitCode: 2, writtenFiles: written };
|
|
@@ -8774,13 +9041,13 @@ async function runInit(opts) {
|
|
|
8774
9041
|
printDiscoveryReport(opts, services);
|
|
8775
9042
|
const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
|
|
8776
9043
|
const patch = renderPatch(sections);
|
|
8777
|
-
const patchPath =
|
|
9044
|
+
const patchPath = path20.join(opts.scanPath, "neat.patch");
|
|
8778
9045
|
if (opts.dryRun) {
|
|
8779
|
-
await
|
|
9046
|
+
await fs18.writeFile(patchPath, patch, "utf8");
|
|
8780
9047
|
written.push(patchPath);
|
|
8781
9048
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
8782
|
-
const gitignorePath =
|
|
8783
|
-
const gitignoreExists = await
|
|
9049
|
+
const gitignorePath = path20.join(opts.scanPath, ".gitignore");
|
|
9050
|
+
const gitignoreExists = await fs18.stat(gitignorePath).then(() => true).catch(() => false);
|
|
8784
9051
|
const verb = gitignoreExists ? "append" : "create";
|
|
8785
9052
|
console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
|
|
8786
9053
|
console.log("rerun without --dry-run to register and snapshot.");
|
|
@@ -8791,9 +9058,9 @@ async function runInit(opts) {
|
|
|
8791
9058
|
const graph = getGraph(graphKey);
|
|
8792
9059
|
const projectPaths = pathsForProject(
|
|
8793
9060
|
graphKey,
|
|
8794
|
-
|
|
9061
|
+
path20.join(opts.scanPath, "neat-out")
|
|
8795
9062
|
);
|
|
8796
|
-
const errorsPath =
|
|
9063
|
+
const errorsPath = path20.join(path20.dirname(opts.outPath), path20.basename(projectPaths.errorsPath));
|
|
8797
9064
|
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
8798
9065
|
await saveGraphToDisk(graph, opts.outPath);
|
|
8799
9066
|
written.push(opts.outPath);
|
|
@@ -8872,7 +9139,7 @@ async function runInit(opts) {
|
|
|
8872
9139
|
console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
8873
9140
|
}
|
|
8874
9141
|
} else {
|
|
8875
|
-
await
|
|
9142
|
+
await fs18.writeFile(patchPath, patch, "utf8");
|
|
8876
9143
|
written.push(patchPath);
|
|
8877
9144
|
}
|
|
8878
9145
|
}
|
|
@@ -8913,9 +9180,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
8913
9180
|
};
|
|
8914
9181
|
function claudeConfigPath() {
|
|
8915
9182
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
8916
|
-
if (override && override.length > 0) return
|
|
9183
|
+
if (override && override.length > 0) return path20.resolve(override);
|
|
8917
9184
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
8918
|
-
return
|
|
9185
|
+
return path20.join(home, ".claude.json");
|
|
8919
9186
|
}
|
|
8920
9187
|
async function runSkill(opts) {
|
|
8921
9188
|
const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -8927,7 +9194,7 @@ async function runSkill(opts) {
|
|
|
8927
9194
|
const target = claudeConfigPath();
|
|
8928
9195
|
let existing = {};
|
|
8929
9196
|
try {
|
|
8930
|
-
existing = JSON.parse(await
|
|
9197
|
+
existing = JSON.parse(await fs18.readFile(target, "utf8"));
|
|
8931
9198
|
} catch (err) {
|
|
8932
9199
|
if (err.code !== "ENOENT") {
|
|
8933
9200
|
console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
|
|
@@ -8939,8 +9206,8 @@ async function runSkill(opts) {
|
|
|
8939
9206
|
...existing,
|
|
8940
9207
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
8941
9208
|
};
|
|
8942
|
-
await
|
|
8943
|
-
await
|
|
9209
|
+
await fs18.mkdir(path20.dirname(target), { recursive: true });
|
|
9210
|
+
await fs18.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
8944
9211
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
8945
9212
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
8946
9213
|
console.log("");
|
|
@@ -9061,12 +9328,12 @@ async function main() {
|
|
|
9061
9328
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
9062
9329
|
process.exit(2);
|
|
9063
9330
|
}
|
|
9064
|
-
const scanPath =
|
|
9331
|
+
const scanPath = path20.resolve(target);
|
|
9065
9332
|
const projectExplicit = parsed.project !== null;
|
|
9066
|
-
const projectName = projectExplicit ? project :
|
|
9333
|
+
const projectName = projectExplicit ? project : path20.basename(scanPath);
|
|
9067
9334
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
9068
|
-
const fallback = pathsForProject(projectKey,
|
|
9069
|
-
const outPath =
|
|
9335
|
+
const fallback = pathsForProject(projectKey, path20.join(scanPath, "neat-out")).snapshotPath;
|
|
9336
|
+
const outPath = path20.resolve(process.env.NEAT_OUT_PATH ?? fallback);
|
|
9070
9337
|
const result = await runInit({
|
|
9071
9338
|
scanPath,
|
|
9072
9339
|
outPath,
|
|
@@ -9087,21 +9354,21 @@ async function main() {
|
|
|
9087
9354
|
usage5();
|
|
9088
9355
|
process.exit(2);
|
|
9089
9356
|
}
|
|
9090
|
-
const scanPath =
|
|
9091
|
-
const stat = await
|
|
9357
|
+
const scanPath = path20.resolve(target);
|
|
9358
|
+
const stat = await fs18.stat(scanPath).catch(() => null);
|
|
9092
9359
|
if (!stat || !stat.isDirectory()) {
|
|
9093
9360
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
9094
9361
|
process.exit(2);
|
|
9095
9362
|
}
|
|
9096
|
-
const projectPaths = pathsForProject(project,
|
|
9097
|
-
const outPath =
|
|
9098
|
-
const errorsPath =
|
|
9099
|
-
process.env.NEAT_ERRORS_PATH ??
|
|
9363
|
+
const projectPaths = pathsForProject(project, path20.join(scanPath, "neat-out"));
|
|
9364
|
+
const outPath = path20.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
|
|
9365
|
+
const errorsPath = path20.resolve(
|
|
9366
|
+
process.env.NEAT_ERRORS_PATH ?? path20.join(path20.dirname(outPath), path20.basename(projectPaths.errorsPath))
|
|
9100
9367
|
);
|
|
9101
|
-
const staleEventsPath =
|
|
9102
|
-
process.env.NEAT_STALE_EVENTS_PATH ??
|
|
9368
|
+
const staleEventsPath = path20.resolve(
|
|
9369
|
+
process.env.NEAT_STALE_EVENTS_PATH ?? path20.join(path20.dirname(outPath), path20.basename(projectPaths.staleEventsPath))
|
|
9103
9370
|
);
|
|
9104
|
-
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ?
|
|
9371
|
+
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path20.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
|
|
9105
9372
|
const handle = await startWatch(getGraph(project), {
|
|
9106
9373
|
scanPath,
|
|
9107
9374
|
outPath,
|
|
@@ -9110,7 +9377,7 @@ async function main() {
|
|
|
9110
9377
|
project,
|
|
9111
9378
|
// Resolve NEAT_HOME so a `neat watch` picks up connectors added to
|
|
9112
9379
|
// ~/.neat/connectors.json (#871). Same resolution the rest of the CLI uses.
|
|
9113
|
-
neatHome: process.env.NEAT_HOME ?
|
|
9380
|
+
neatHome: process.env.NEAT_HOME ? path20.resolve(process.env.NEAT_HOME) : path20.join(os5.homedir(), ".neat"),
|
|
9114
9381
|
...embeddingsCachePath ? { embeddingsCachePath } : {},
|
|
9115
9382
|
host: process.env.HOST ?? "0.0.0.0",
|
|
9116
9383
|
port: Number(process.env.PORT ?? 8080),
|
|
@@ -9292,11 +9559,11 @@ async function main() {
|
|
|
9292
9559
|
process.exit(1);
|
|
9293
9560
|
}
|
|
9294
9561
|
async function tryOrchestrator(cmd, parsed) {
|
|
9295
|
-
const scanPath =
|
|
9296
|
-
const stat = await
|
|
9562
|
+
const scanPath = path20.resolve(cmd);
|
|
9563
|
+
const stat = await fs18.stat(scanPath).catch(() => null);
|
|
9297
9564
|
if (!stat || !stat.isDirectory()) return null;
|
|
9298
9565
|
const projectExplicit = parsed.project !== null;
|
|
9299
|
-
const projectName = projectExplicit ? parsed.project :
|
|
9566
|
+
const projectName = projectExplicit ? parsed.project : path20.basename(scanPath);
|
|
9300
9567
|
const result = await runOrchestrator({
|
|
9301
9568
|
scanPath,
|
|
9302
9569
|
project: projectName,
|
|
@@ -9311,7 +9578,18 @@ async function tryOrchestrator(cmd, parsed) {
|
|
|
9311
9578
|
async function runWelcomeFlow(parsed) {
|
|
9312
9579
|
return runWelcome({
|
|
9313
9580
|
login: (loginArgv) => runLoginCommand(loginArgv),
|
|
9314
|
-
|
|
9581
|
+
// What the person chose at the door reaches the orchestrator as the flags they
|
|
9582
|
+
// stand for: the name as `--project`, the instrument answer as `--no-instrument`
|
|
9583
|
+
// or as `yes` (which keeps the orchestrator's own prompt quiet, since the door
|
|
9584
|
+
// already asked).
|
|
9585
|
+
orchestrator: async (cwd, opts) => await tryOrchestrator(cwd, {
|
|
9586
|
+
...parsed,
|
|
9587
|
+
...opts?.project !== void 0 ? { project: opts.project } : {},
|
|
9588
|
+
...opts?.noInstrument !== void 0 ? { noInstrument: opts.noInstrument } : {},
|
|
9589
|
+
...opts?.yes !== void 0 ? { yes: opts.yes } : {}
|
|
9590
|
+
}) ?? 0,
|
|
9591
|
+
// An explicit flag is an answer; don't ask again.
|
|
9592
|
+
instrumentFlagGiven: parsed.noInstrument || parsed.dryRun
|
|
9315
9593
|
});
|
|
9316
9594
|
}
|
|
9317
9595
|
var QUERY_VERBS = /* @__PURE__ */ new Set([
|
|
@@ -9344,6 +9622,13 @@ var ProjectResolutionError = class extends Error {
|
|
|
9344
9622
|
}
|
|
9345
9623
|
exitCode;
|
|
9346
9624
|
};
|
|
9625
|
+
function isLoopbackEndpoint(endpoint) {
|
|
9626
|
+
try {
|
|
9627
|
+
return isLoopbackHost(new URL(endpoint).hostname.replace(/^\[|\]$/g, ""));
|
|
9628
|
+
} catch {
|
|
9629
|
+
return false;
|
|
9630
|
+
}
|
|
9631
|
+
}
|
|
9347
9632
|
async function resolveProjectForVerb(client, parsed) {
|
|
9348
9633
|
const explicit = resolveProjectFlag(parsed);
|
|
9349
9634
|
if (explicit) return explicit;
|
|
@@ -9574,6 +9859,9 @@ async function runQueryVerb(cmd, parsed) {
|
|
|
9574
9859
|
console.error(`neat ${cmd}: ${detail.trim()}`);
|
|
9575
9860
|
} else if (err instanceof TransportError) {
|
|
9576
9861
|
console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (endpoint=${target.endpoint})`);
|
|
9862
|
+
if (isLoopbackEndpoint(target.endpoint)) {
|
|
9863
|
+
console.error(`neat ${cmd}: start one with \`neat watch .\`, or \`neat\` to extract and start in one step.`);
|
|
9864
|
+
}
|
|
9577
9865
|
} else {
|
|
9578
9866
|
console.error(`neat ${cmd}: ${err.message}`);
|
|
9579
9867
|
}
|
|
@@ -9629,6 +9917,7 @@ export {
|
|
|
9629
9917
|
QUERY_VERBS,
|
|
9630
9918
|
UnknownProfileError,
|
|
9631
9919
|
commandPrefix,
|
|
9920
|
+
isLoopbackEndpoint,
|
|
9632
9921
|
isNpxInvocation,
|
|
9633
9922
|
main,
|
|
9634
9923
|
parseArgs,
|