@neat.is/core 0.10.1-dev.20260927 → 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-EJPNEEJA.js → chunk-AWIK6T3G.js} +95 -49
- package/dist/chunk-AWIK6T3G.js.map +1 -0
- package/dist/{chunk-J3VVEB45.js → chunk-E3YFFIHE.js} +136 -8
- package/dist/chunk-E3YFFIHE.js.map +1 -0
- package/dist/{chunk-GDGUY4T6.js → chunk-M7CGATVC.js} +3 -2
- package/dist/cli.cjs +820 -265
- 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 +608 -183
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +226 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +226 -52
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-Y5K4WGWO.js → otel-grpc-CU73Y5WT.js} +3 -3
- package/dist/server.cjs +134 -6
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/package.json +3 -3
- package/dist/chunk-EJPNEEJA.js.map +0 -1
- package/dist/chunk-J3VVEB45.js.map +0 -1
- /package/dist/{chunk-ERE47MCR.js.map → chunk-5SJHA5ZH.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";
|
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
pruneRegistry,
|
|
64
64
|
readConnectorsConfig,
|
|
65
65
|
readErrorEvents,
|
|
66
|
+
readRegistry,
|
|
66
67
|
removeConnectorEntry,
|
|
67
68
|
removeDaemonRecord,
|
|
68
69
|
removeProject,
|
|
@@ -77,23 +78,24 @@ import {
|
|
|
77
78
|
startStalenessLoop,
|
|
78
79
|
upsertConnectorEntry,
|
|
79
80
|
validateConnectorEntry
|
|
80
|
-
} from "./chunk-
|
|
81
|
+
} from "./chunk-E3YFFIHE.js";
|
|
81
82
|
import {
|
|
82
83
|
startOtelGrpcReceiver
|
|
83
|
-
} from "./chunk-
|
|
84
|
+
} from "./chunk-5SJHA5ZH.js";
|
|
84
85
|
import {
|
|
85
86
|
__dirname,
|
|
86
87
|
__require,
|
|
87
88
|
assertBindAuthority,
|
|
88
89
|
buildOtelReceiver,
|
|
90
|
+
isLoopbackHost,
|
|
89
91
|
listenSteppingOtlp,
|
|
90
92
|
readAuthEnv
|
|
91
|
-
} from "./chunk-
|
|
93
|
+
} from "./chunk-M7CGATVC.js";
|
|
92
94
|
|
|
93
95
|
// src/cli.ts
|
|
94
|
-
import
|
|
96
|
+
import path20 from "path";
|
|
95
97
|
import os5 from "os";
|
|
96
|
-
import { promises as
|
|
98
|
+
import { promises as fs18 } from "fs";
|
|
97
99
|
|
|
98
100
|
// src/banner.ts
|
|
99
101
|
import path from "path";
|
|
@@ -4451,7 +4453,8 @@ async function runOrchestrator(opts) {
|
|
|
4451
4453
|
} catch (err) {
|
|
4452
4454
|
if (!(err instanceof ProjectNameCollisionError)) throw err;
|
|
4453
4455
|
console.error(`neat: ${err.message}`);
|
|
4454
|
-
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.`);
|
|
4455
4458
|
result.exitCode = 1;
|
|
4456
4459
|
return result;
|
|
4457
4460
|
}
|
|
@@ -5244,8 +5247,198 @@ async function clearActiveProfile(home = neatHome()) {
|
|
|
5244
5247
|
});
|
|
5245
5248
|
}
|
|
5246
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
|
+
|
|
5247
5440
|
// src/login-sso.ts
|
|
5248
|
-
var DEFAULT_CP_URL = "https://neat
|
|
5441
|
+
var DEFAULT_CP_URL = "https://api.neat.is";
|
|
5249
5442
|
var DEFAULT_WEB_URL = "https://app.neat.is";
|
|
5250
5443
|
var CALLBACK_TIMEOUT_MS = 5 * 6e4;
|
|
5251
5444
|
function resolveCpUrl(env = process.env, override) {
|
|
@@ -5269,6 +5462,7 @@ function pickProject(projects, want) {
|
|
|
5269
5462
|
return {
|
|
5270
5463
|
error: {
|
|
5271
5464
|
code: 1,
|
|
5465
|
+
kind: "no-project",
|
|
5272
5466
|
message: `no running project to connect to \u2014 create + provision one in the console first${listed}`
|
|
5273
5467
|
}
|
|
5274
5468
|
};
|
|
@@ -5319,7 +5513,7 @@ async function exchangeCredential(cpUrl, accessToken, opts, deps) {
|
|
|
5319
5513
|
if (!cred.endpoint || !cred.authToken) {
|
|
5320
5514
|
return { error: { code: 1, message: "the credential response was missing endpoint/authToken" } };
|
|
5321
5515
|
}
|
|
5322
|
-
return { project, cred };
|
|
5516
|
+
return { project, cred, me };
|
|
5323
5517
|
}
|
|
5324
5518
|
function defaultOpenBrowser(url) {
|
|
5325
5519
|
const platform = process.platform;
|
|
@@ -5401,12 +5595,50 @@ async function runSsoLogin(opts, deps) {
|
|
|
5401
5595
|
}
|
|
5402
5596
|
accessToken = lb.token;
|
|
5403
5597
|
}
|
|
5404
|
-
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
|
+
}
|
|
5405
5626
|
if ("error" in ex) {
|
|
5406
5627
|
err(`neat login: ${ex.error.message}`);
|
|
5407
5628
|
return ex.error.code;
|
|
5408
5629
|
}
|
|
5409
|
-
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
|
+
}
|
|
5410
5642
|
await upsertProfile(
|
|
5411
5643
|
{ name: opts.name, endpoint: cred.endpoint, authToken: cred.authToken },
|
|
5412
5644
|
{ makeActive: true, ...deps.home ? { home: deps.home } : {} }
|
|
@@ -5434,6 +5666,21 @@ async function runSsoLogin(opts, deps) {
|
|
|
5434
5666
|
out(` OTEL_EXPORTER_OTLP_ENDPOINT=${cred.ingestEndpoint}`);
|
|
5435
5667
|
out(` OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${cred.otelToken}`);
|
|
5436
5668
|
}
|
|
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
|
+
});
|
|
5683
|
+
out("");
|
|
5437
5684
|
out("Run `neat logout` to switch back to your local daemon.");
|
|
5438
5685
|
}
|
|
5439
5686
|
return 0;
|
|
@@ -5539,8 +5786,8 @@ async function runConnectCommand(rawArgs, deps = {}) {
|
|
|
5539
5786
|
}
|
|
5540
5787
|
|
|
5541
5788
|
// src/doctor-cli.ts
|
|
5542
|
-
import { promises as
|
|
5543
|
-
import
|
|
5789
|
+
import { promises as fs12 } from "fs";
|
|
5790
|
+
import path13 from "path";
|
|
5544
5791
|
|
|
5545
5792
|
// src/cli-client.ts
|
|
5546
5793
|
import { Provenance as Provenance3 } from "@neat.is/types";
|
|
@@ -5568,10 +5815,10 @@ function createHttpClient(baseUrl, bearerToken, fetchImpl = fetch) {
|
|
|
5568
5815
|
const root = baseUrl.replace(/\/$/, "");
|
|
5569
5816
|
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
5570
5817
|
return {
|
|
5571
|
-
async get(
|
|
5818
|
+
async get(path21) {
|
|
5572
5819
|
let res;
|
|
5573
5820
|
try {
|
|
5574
|
-
res = await fetchImpl(`${root}${
|
|
5821
|
+
res = await fetchImpl(`${root}${path21}`, {
|
|
5575
5822
|
headers: { ...authHeader }
|
|
5576
5823
|
});
|
|
5577
5824
|
} catch (err) {
|
|
@@ -5583,16 +5830,16 @@ function createHttpClient(baseUrl, bearerToken, fetchImpl = fetch) {
|
|
|
5583
5830
|
const body = await res.text().catch(() => "");
|
|
5584
5831
|
throw new HttpError(
|
|
5585
5832
|
res.status,
|
|
5586
|
-
`${res.status} ${res.statusText} on GET ${
|
|
5833
|
+
`${res.status} ${res.statusText} on GET ${path21}: ${body}`,
|
|
5587
5834
|
body
|
|
5588
5835
|
);
|
|
5589
5836
|
}
|
|
5590
5837
|
return await res.json();
|
|
5591
5838
|
},
|
|
5592
|
-
async post(
|
|
5839
|
+
async post(path21, body) {
|
|
5593
5840
|
let res;
|
|
5594
5841
|
try {
|
|
5595
|
-
res = await fetchImpl(`${root}${
|
|
5842
|
+
res = await fetchImpl(`${root}${path21}`, {
|
|
5596
5843
|
method: "POST",
|
|
5597
5844
|
headers: { "content-type": "application/json", ...authHeader },
|
|
5598
5845
|
body: JSON.stringify(body)
|
|
@@ -5606,7 +5853,7 @@ function createHttpClient(baseUrl, bearerToken, fetchImpl = fetch) {
|
|
|
5606
5853
|
const text = await res.text().catch(() => "");
|
|
5607
5854
|
throw new HttpError(
|
|
5608
5855
|
res.status,
|
|
5609
|
-
`${res.status} ${res.statusText} on POST ${
|
|
5856
|
+
`${res.status} ${res.statusText} on POST ${path21}: ${text}`,
|
|
5610
5857
|
text
|
|
5611
5858
|
);
|
|
5612
5859
|
}
|
|
@@ -5620,12 +5867,12 @@ function projectPath(project, suffix) {
|
|
|
5620
5867
|
}
|
|
5621
5868
|
async function runRootCause(client, input) {
|
|
5622
5869
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
5623
|
-
const
|
|
5870
|
+
const path21 = projectPath(
|
|
5624
5871
|
input.project,
|
|
5625
5872
|
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
5626
5873
|
);
|
|
5627
5874
|
try {
|
|
5628
|
-
const result = await client.get(
|
|
5875
|
+
const result = await client.get(path21);
|
|
5629
5876
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
5630
5877
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
5631
5878
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -5651,12 +5898,12 @@ async function runRootCause(client, input) {
|
|
|
5651
5898
|
}
|
|
5652
5899
|
async function runBlastRadius(client, input) {
|
|
5653
5900
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
5654
|
-
const
|
|
5901
|
+
const path21 = projectPath(
|
|
5655
5902
|
input.project,
|
|
5656
5903
|
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
5657
5904
|
);
|
|
5658
5905
|
try {
|
|
5659
|
-
const result = await client.get(
|
|
5906
|
+
const result = await client.get(path21);
|
|
5660
5907
|
if (result.totalAffected === 0) {
|
|
5661
5908
|
return {
|
|
5662
5909
|
summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
|
|
@@ -5690,12 +5937,12 @@ function formatBlastEntry(n) {
|
|
|
5690
5937
|
}
|
|
5691
5938
|
async function runDependencies(client, input) {
|
|
5692
5939
|
const depth = input.depth ?? 3;
|
|
5693
|
-
const
|
|
5940
|
+
const path21 = projectPath(
|
|
5694
5941
|
input.project,
|
|
5695
5942
|
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
5696
5943
|
);
|
|
5697
5944
|
try {
|
|
5698
|
-
const result = await client.get(
|
|
5945
|
+
const result = await client.get(path21);
|
|
5699
5946
|
if (result.total === 0) {
|
|
5700
5947
|
return {
|
|
5701
5948
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -5787,9 +6034,9 @@ function formatDuration(ms) {
|
|
|
5787
6034
|
return `${Math.round(h / 24)}d`;
|
|
5788
6035
|
}
|
|
5789
6036
|
async function runIncidents(client, input) {
|
|
5790
|
-
const
|
|
6037
|
+
const path21 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
5791
6038
|
try {
|
|
5792
|
-
const body = await client.get(
|
|
6039
|
+
const body = await client.get(path21);
|
|
5793
6040
|
const events = body.events;
|
|
5794
6041
|
if (events.length === 0) {
|
|
5795
6042
|
return {
|
|
@@ -6153,7 +6400,7 @@ function checkNode(nodeVersion) {
|
|
|
6153
6400
|
}
|
|
6154
6401
|
async function neatOutExists(cwd) {
|
|
6155
6402
|
try {
|
|
6156
|
-
const st = await
|
|
6403
|
+
const st = await fs12.stat(path13.join(cwd, "neat-out"));
|
|
6157
6404
|
return st.isDirectory();
|
|
6158
6405
|
} catch {
|
|
6159
6406
|
return false;
|
|
@@ -6272,9 +6519,6 @@ async function runDoctorCommand(argv, deps = {}) {
|
|
|
6272
6519
|
|
|
6273
6520
|
// src/login-cli.ts
|
|
6274
6521
|
import readline3 from "readline/promises";
|
|
6275
|
-
var PROBE_ATTEMPT_TIMEOUT_MS = 3e4;
|
|
6276
|
-
var PROBE_TOTAL_BUDGET_MS = 12e4;
|
|
6277
|
-
var PROBE_RETRY_PAUSE_MS = 2e3;
|
|
6278
6522
|
var DEFAULT_PROFILE_NAME = "hosted";
|
|
6279
6523
|
function readFlagValue(argv, i) {
|
|
6280
6524
|
const arg = argv[i];
|
|
@@ -6339,44 +6583,6 @@ function printLoginHelp(out) {
|
|
|
6339
6583
|
out(' the profile (default "hosted"); the paste token can also come from NEAT_LOGIN_TOKEN.');
|
|
6340
6584
|
out(" Exit 0 on success, 1 rejected/needs-action, 2 misuse, 3 unreachable.");
|
|
6341
6585
|
}
|
|
6342
|
-
function isTimeoutError(err) {
|
|
6343
|
-
const name = err?.name;
|
|
6344
|
-
return name === "TimeoutError" || name === "AbortError";
|
|
6345
|
-
}
|
|
6346
|
-
async function probeOnce(fetchImpl, root, token, timeoutMs) {
|
|
6347
|
-
let res;
|
|
6348
|
-
try {
|
|
6349
|
-
res = await fetchImpl(`${root}/health`, {
|
|
6350
|
-
headers: { authorization: `Bearer ${token}` },
|
|
6351
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
6352
|
-
});
|
|
6353
|
-
} catch (err) {
|
|
6354
|
-
if (isTimeoutError(err)) return { kind: "timeout" };
|
|
6355
|
-
return { kind: "unreachable", detail: err.message };
|
|
6356
|
-
}
|
|
6357
|
-
if (res.status === 401 || res.status === 403) return { kind: "unauthorized", status: res.status };
|
|
6358
|
-
if (!res.ok) return { kind: "not-neat", status: res.status };
|
|
6359
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
6360
|
-
if (!contentType.includes("json")) return { kind: "not-neat", status: res.status };
|
|
6361
|
-
return { kind: "ok" };
|
|
6362
|
-
}
|
|
6363
|
-
async function probeDaemon(fetchImpl, endpoint, token, hooks) {
|
|
6364
|
-
const root = endpoint.replace(/\/$/, "");
|
|
6365
|
-
const deadline = hooks.now() + PROBE_TOTAL_BUDGET_MS;
|
|
6366
|
-
let warned = false;
|
|
6367
|
-
for (; ; ) {
|
|
6368
|
-
const result = await probeOnce(fetchImpl, root, token, PROBE_ATTEMPT_TIMEOUT_MS);
|
|
6369
|
-
if (result.kind !== "timeout") return result;
|
|
6370
|
-
if (!warned) {
|
|
6371
|
-
hooks.onWaiting();
|
|
6372
|
-
warned = true;
|
|
6373
|
-
}
|
|
6374
|
-
if (hooks.now() >= deadline) {
|
|
6375
|
-
return { kind: "unreachable", detail: `no response after ${Math.round(PROBE_TOTAL_BUDGET_MS / 1e3)}s` };
|
|
6376
|
-
}
|
|
6377
|
-
await hooks.sleep(PROBE_RETRY_PAUSE_MS);
|
|
6378
|
-
}
|
|
6379
|
-
}
|
|
6380
6586
|
async function runLoginCommand(argv, deps = {}) {
|
|
6381
6587
|
const out = deps.out ?? ((line) => console.log(line));
|
|
6382
6588
|
const err = deps.err ?? ((line) => console.error(line));
|
|
@@ -6409,8 +6615,13 @@ async function runLoginCommand(argv, deps = {}) {
|
|
|
6409
6615
|
fetchImpl,
|
|
6410
6616
|
out,
|
|
6411
6617
|
err,
|
|
6618
|
+
readLine,
|
|
6619
|
+
sleep: sleep2,
|
|
6620
|
+
now,
|
|
6412
6621
|
...deps.openBrowser ? { openBrowser: deps.openBrowser } : {},
|
|
6413
|
-
...deps.home ? { home: deps.home } : {}
|
|
6622
|
+
...deps.home ? { home: deps.home } : {},
|
|
6623
|
+
...deps.signal ? { signal: deps.signal } : {},
|
|
6624
|
+
...deps.connect ? { connect: deps.connect } : {}
|
|
6414
6625
|
}
|
|
6415
6626
|
);
|
|
6416
6627
|
}
|
|
@@ -6566,6 +6777,8 @@ async function defaultReadSecret(prompt) {
|
|
|
6566
6777
|
}
|
|
6567
6778
|
|
|
6568
6779
|
// src/welcome.ts
|
|
6780
|
+
import { promises as fs13 } from "fs";
|
|
6781
|
+
import path14 from "path";
|
|
6569
6782
|
import readline4 from "readline/promises";
|
|
6570
6783
|
var WORDMARK = [
|
|
6571
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",
|
|
@@ -6598,29 +6811,146 @@ function printHeader(out) {
|
|
|
6598
6811
|
}
|
|
6599
6812
|
async function runWelcome(deps = {}) {
|
|
6600
6813
|
const out = deps.out ?? ((line) => console.log(line));
|
|
6814
|
+
try {
|
|
6815
|
+
return await runFrontDoor(deps, out);
|
|
6816
|
+
} catch (err) {
|
|
6817
|
+
if (isPromptCancelled(err)) {
|
|
6818
|
+
out("");
|
|
6819
|
+
return 130;
|
|
6820
|
+
}
|
|
6821
|
+
throw err;
|
|
6822
|
+
}
|
|
6823
|
+
}
|
|
6824
|
+
var PromptCancelled = class extends Error {
|
|
6825
|
+
constructor() {
|
|
6826
|
+
super("prompt cancelled");
|
|
6827
|
+
this.name = "PromptCancelled";
|
|
6828
|
+
}
|
|
6829
|
+
};
|
|
6830
|
+
function isPromptCancelled(err) {
|
|
6831
|
+
if (err instanceof PromptCancelled) return true;
|
|
6832
|
+
const e = err;
|
|
6833
|
+
return e?.code === "ABORT_ERR" || e?.name === "AbortError";
|
|
6834
|
+
}
|
|
6835
|
+
async function runFrontDoor(deps, out) {
|
|
6601
6836
|
const readLine = deps.readLine ?? defaultReadLine2;
|
|
6602
6837
|
const login = deps.login ?? (() => Promise.resolve(0));
|
|
6603
6838
|
const orchestrator = deps.orchestrator ?? (() => Promise.resolve(0));
|
|
6604
6839
|
const cwd = deps.cwd ?? process.cwd();
|
|
6840
|
+
const flagGiven = deps.instrumentFlagGiven ?? false;
|
|
6841
|
+
const readRegistry2 = deps.readRegistry ?? (() => readRegistry());
|
|
6605
6842
|
printHeader(out);
|
|
6606
6843
|
out("Welcome to NEAT. Let's get you a graph of this system.");
|
|
6607
6844
|
out("");
|
|
6608
|
-
|
|
6609
|
-
|
|
6845
|
+
const keyReader = deps.readKey !== void 0 ? { read: deps.readKey, done: () => {
|
|
6846
|
+
} } : createKeyReader();
|
|
6847
|
+
const picked = keyReader !== null ? await selectOption(out, keyReader, deps.moveCursorUp ?? defaultMoveCursorUp) : await selectByNumber(out, readLine);
|
|
6848
|
+
if (picked === void 0) return runSelfHosted(out, readLine, orchestrator, cwd, readRegistry2, flagGiven);
|
|
6849
|
+
if (picked === 0) return login(["--browser"]);
|
|
6850
|
+
return runSelfHosted(out, readLine, orchestrator, cwd, readRegistry2, flagGiven);
|
|
6851
|
+
}
|
|
6852
|
+
var MENU_OPTIONS = [
|
|
6853
|
+
"Log me into Hosted Neat",
|
|
6854
|
+
"I'd like to self-host or use it locally (copy a prompt)"
|
|
6855
|
+
];
|
|
6856
|
+
async function selectOption(out, reader, moveCursorUp) {
|
|
6857
|
+
let cursor = 0;
|
|
6858
|
+
const render = () => {
|
|
6859
|
+
for (const [i, label] of MENU_OPTIONS.entries()) {
|
|
6860
|
+
out(i === cursor ? ` \u276F ${i + 1}) ${label}` : ` ${i + 1}) ${label}`);
|
|
6861
|
+
}
|
|
6862
|
+
};
|
|
6863
|
+
try {
|
|
6864
|
+
render();
|
|
6865
|
+
out("");
|
|
6866
|
+
out(" \u2191/\u2193 to move \xB7 Enter to choose \xB7 1 or 2 to jump");
|
|
6867
|
+
for (; ; ) {
|
|
6868
|
+
const key = await reader.read();
|
|
6869
|
+
if (key === void 0) return void 0;
|
|
6870
|
+
if (key === "cancel") throw new PromptCancelled();
|
|
6871
|
+
if (key === "enter") return cursor;
|
|
6872
|
+
if (key === "select-1") return 0;
|
|
6873
|
+
if (key === "select-2") return 1;
|
|
6874
|
+
if (key === "up") cursor = (cursor + MENU_OPTIONS.length - 1) % MENU_OPTIONS.length;
|
|
6875
|
+
else if (key === "down") cursor = (cursor + 1) % MENU_OPTIONS.length;
|
|
6876
|
+
else continue;
|
|
6877
|
+
moveCursorUp(MENU_OPTIONS.length + 2);
|
|
6878
|
+
render();
|
|
6879
|
+
out("");
|
|
6880
|
+
out(" \u2191/\u2193 to move \xB7 Enter to choose \xB7 1 or 2 to jump");
|
|
6881
|
+
}
|
|
6882
|
+
} finally {
|
|
6883
|
+
reader.done();
|
|
6884
|
+
}
|
|
6885
|
+
}
|
|
6886
|
+
async function selectByNumber(out, readLine) {
|
|
6887
|
+
for (const [i, label] of MENU_OPTIONS.entries()) out(` ${i + 1}) ${label}`);
|
|
6610
6888
|
out("");
|
|
6611
6889
|
for (; ; ) {
|
|
6612
6890
|
const choice = (await readLine("Choose 1 or 2 (default 2): "))?.trim();
|
|
6613
|
-
if (choice === void 0) return
|
|
6614
|
-
if (choice === "1")
|
|
6615
|
-
|
|
6616
|
-
}
|
|
6617
|
-
if (choice === "2" || choice === "") {
|
|
6618
|
-
return runSelfHosted(out, readLine, orchestrator, cwd);
|
|
6619
|
-
}
|
|
6620
|
-
out(`"${choice}" isn't 1 or 2 \u2014 try again, or press Enter for self-hosted.`);
|
|
6891
|
+
if (choice === void 0) return void 0;
|
|
6892
|
+
if (choice === "1") return 0;
|
|
6893
|
+
if (choice === "2" || choice === "") return 1;
|
|
6894
|
+
out(`"${choice}" isn't 1 or 2 \u2014 try again, or press Enter for the local path.`);
|
|
6621
6895
|
}
|
|
6622
6896
|
}
|
|
6623
|
-
|
|
6897
|
+
function createKeyReader() {
|
|
6898
|
+
const stdin = process.stdin;
|
|
6899
|
+
if (!stdin.isTTY || typeof stdin.setRawMode !== "function") return null;
|
|
6900
|
+
stdin.setRawMode(true);
|
|
6901
|
+
stdin.resume();
|
|
6902
|
+
let closed = false;
|
|
6903
|
+
let ended = false;
|
|
6904
|
+
const markEnded = () => {
|
|
6905
|
+
ended = true;
|
|
6906
|
+
};
|
|
6907
|
+
stdin.once("end", markEnded);
|
|
6908
|
+
stdin.once("close", markEnded);
|
|
6909
|
+
return {
|
|
6910
|
+
read: () => new Promise((resolve) => {
|
|
6911
|
+
if (ended) {
|
|
6912
|
+
resolve(void 0);
|
|
6913
|
+
return;
|
|
6914
|
+
}
|
|
6915
|
+
const finish = (value) => {
|
|
6916
|
+
stdin.removeListener("data", onData);
|
|
6917
|
+
stdin.removeListener("end", onClosed);
|
|
6918
|
+
stdin.removeListener("close", onClosed);
|
|
6919
|
+
resolve(value);
|
|
6920
|
+
};
|
|
6921
|
+
const onData = (buf) => finish(decodeKey(buf));
|
|
6922
|
+
const onClosed = () => {
|
|
6923
|
+
ended = true;
|
|
6924
|
+
finish(void 0);
|
|
6925
|
+
};
|
|
6926
|
+
stdin.on("data", onData);
|
|
6927
|
+
stdin.once("end", onClosed);
|
|
6928
|
+
stdin.once("close", onClosed);
|
|
6929
|
+
}),
|
|
6930
|
+
done: () => {
|
|
6931
|
+
if (closed) return;
|
|
6932
|
+
closed = true;
|
|
6933
|
+
stdin.removeListener("end", markEnded);
|
|
6934
|
+
stdin.removeListener("close", markEnded);
|
|
6935
|
+
stdin.setRawMode(false);
|
|
6936
|
+
stdin.pause();
|
|
6937
|
+
}
|
|
6938
|
+
};
|
|
6939
|
+
}
|
|
6940
|
+
function decodeKey(buf) {
|
|
6941
|
+
const s = buf.toString("utf8");
|
|
6942
|
+
if (s === "\x1B[A" || s === "k") return "up";
|
|
6943
|
+
if (s === "\x1B[B" || s === "j") return "down";
|
|
6944
|
+
if (s === "\r" || s === "\n") return "enter";
|
|
6945
|
+
if (s === "1") return "select-1";
|
|
6946
|
+
if (s === "2") return "select-2";
|
|
6947
|
+
if (s === "" || s === "\x1B") return "cancel";
|
|
6948
|
+
return "ignore";
|
|
6949
|
+
}
|
|
6950
|
+
function defaultMoveCursorUp(rows) {
|
|
6951
|
+
if (rows > 0) process.stdout.write(`\x1B[${rows}A`);
|
|
6952
|
+
}
|
|
6953
|
+
async function runSelfHosted(out, readLine, orchestrator, cwd, readRegistry2, instrumentFlagGiven) {
|
|
6624
6954
|
const wantsPrompt = (await readLine("Print a copy-paste setup prompt for your coding agent? [Y/n]: "))?.trim().toLowerCase();
|
|
6625
6955
|
if (wantsPrompt !== "n" && wantsPrompt !== "no") {
|
|
6626
6956
|
out("");
|
|
@@ -6631,18 +6961,91 @@ async function runSelfHosted(out, readLine, orchestrator, cwd) {
|
|
|
6631
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");
|
|
6632
6962
|
out("");
|
|
6633
6963
|
}
|
|
6964
|
+
const project = await resolveProjectName(out, readLine, cwd, readRegistry2);
|
|
6965
|
+
const declined = instrumentFlagGiven ? void 0 : await askToInstrument(out, readLine);
|
|
6634
6966
|
out("Building your local graph now\u2026");
|
|
6635
6967
|
out("");
|
|
6636
|
-
|
|
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);
|
|
6637
7027
|
}
|
|
6638
7028
|
async function shouldShowWelcome(deps = {}) {
|
|
6639
7029
|
const stdinTTY = deps.stdinIsTTY ?? Boolean(process.stdin.isTTY);
|
|
6640
7030
|
const stdoutTTY = deps.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
|
|
6641
7031
|
if (!stdinTTY || !stdoutTTY) return false;
|
|
6642
|
-
const
|
|
7032
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
7033
|
+
const readRegistry2 = deps.readRegistry ?? (() => readRegistry());
|
|
7034
|
+
const hasSnapshot = deps.hasSnapshot ?? defaultHasSnapshot;
|
|
6643
7035
|
try {
|
|
6644
|
-
const
|
|
6645
|
-
|
|
7036
|
+
const here = path14.resolve(cwd);
|
|
7037
|
+
const { projects } = await readRegistry2();
|
|
7038
|
+
if (projects.some((p) => path14.resolve(p.path) === here)) return false;
|
|
7039
|
+
if (await hasSnapshot(here)) return false;
|
|
7040
|
+
return true;
|
|
7041
|
+
} catch {
|
|
7042
|
+
return false;
|
|
7043
|
+
}
|
|
7044
|
+
}
|
|
7045
|
+
async function defaultHasSnapshot(cwd) {
|
|
7046
|
+
try {
|
|
7047
|
+
const entries = await fs13.readdir(path14.join(cwd, "neat-out"));
|
|
7048
|
+
return entries.some((e) => e.endsWith(".json"));
|
|
6646
7049
|
} catch {
|
|
6647
7050
|
return false;
|
|
6648
7051
|
}
|
|
@@ -6658,27 +7061,27 @@ async function defaultReadLine2(prompt) {
|
|
|
6658
7061
|
}
|
|
6659
7062
|
|
|
6660
7063
|
// src/hooks-cli.ts
|
|
6661
|
-
import
|
|
7064
|
+
import path15 from "path";
|
|
6662
7065
|
import os2 from "os";
|
|
6663
|
-
import { promises as
|
|
7066
|
+
import { promises as fs14 } from "fs";
|
|
6664
7067
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
6665
7068
|
var HOOK_FILENAME = "neat-search-nudge.mjs";
|
|
6666
7069
|
var GUIDE_FILENAME = "GRAPH_FIRST.md";
|
|
6667
7070
|
var GUIDE_INSTALL_NAME = "neat-graph-first.md";
|
|
6668
7071
|
var HOOK_MATCHER = "Grep|Glob|Bash";
|
|
6669
7072
|
function moduleDir() {
|
|
6670
|
-
return typeof __dirname !== "undefined" ? __dirname :
|
|
7073
|
+
return typeof __dirname !== "undefined" ? __dirname : path15.dirname(fileURLToPath3(import.meta.url));
|
|
6671
7074
|
}
|
|
6672
7075
|
async function readSkillAsset(rel) {
|
|
6673
7076
|
const here = moduleDir();
|
|
6674
7077
|
const candidates = [
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
7078
|
+
path15.resolve(here, "../../claude-skill", rel),
|
|
7079
|
+
path15.resolve(here, "../../../claude-skill", rel),
|
|
7080
|
+
path15.resolve(here, "../claude-skill", rel)
|
|
6678
7081
|
];
|
|
6679
7082
|
for (const candidate of candidates) {
|
|
6680
7083
|
try {
|
|
6681
|
-
return await
|
|
7084
|
+
return await fs14.readFile(candidate, "utf8");
|
|
6682
7085
|
} catch {
|
|
6683
7086
|
}
|
|
6684
7087
|
}
|
|
@@ -6688,20 +7091,20 @@ async function readSkillAsset(rel) {
|
|
|
6688
7091
|
}
|
|
6689
7092
|
function neatHome2() {
|
|
6690
7093
|
const override = process.env.NEAT_HOME;
|
|
6691
|
-
if (override && override.length > 0) return
|
|
6692
|
-
return
|
|
7094
|
+
if (override && override.length > 0) return path15.resolve(override);
|
|
7095
|
+
return path15.join(os2.homedir(), ".neat");
|
|
6693
7096
|
}
|
|
6694
7097
|
function claudeSettingsPath() {
|
|
6695
7098
|
const override = process.env.NEAT_CLAUDE_SETTINGS;
|
|
6696
|
-
if (override && override.length > 0) return
|
|
7099
|
+
if (override && override.length > 0) return path15.resolve(override);
|
|
6697
7100
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? os2.homedir();
|
|
6698
|
-
return
|
|
7101
|
+
return path15.join(home, ".claude", "settings.json");
|
|
6699
7102
|
}
|
|
6700
7103
|
function installedHookPath() {
|
|
6701
|
-
return
|
|
7104
|
+
return path15.join(neatHome2(), "hooks", HOOK_FILENAME);
|
|
6702
7105
|
}
|
|
6703
7106
|
function gateFlagPath() {
|
|
6704
|
-
return
|
|
7107
|
+
return path15.join(neatHome2(), "hooks", "gate-enabled");
|
|
6705
7108
|
}
|
|
6706
7109
|
function isNeatSearchEntry(entry2) {
|
|
6707
7110
|
return (entry2.hooks ?? []).some(
|
|
@@ -6734,14 +7137,14 @@ async function runHooks(opts) {
|
|
|
6734
7137
|
const hookScript = await readSkillAsset(`hooks/${HOOK_FILENAME}`);
|
|
6735
7138
|
const guide = await readSkillAsset(GUIDE_FILENAME);
|
|
6736
7139
|
const scriptPath = installedHookPath();
|
|
6737
|
-
await
|
|
6738
|
-
await
|
|
6739
|
-
const guidePath =
|
|
6740
|
-
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");
|
|
6741
7144
|
const settingsFile = claudeSettingsPath();
|
|
6742
7145
|
let settings = {};
|
|
6743
7146
|
try {
|
|
6744
|
-
settings = JSON.parse(await
|
|
7147
|
+
settings = JSON.parse(await fs14.readFile(settingsFile, "utf8"));
|
|
6745
7148
|
} catch (err) {
|
|
6746
7149
|
if (err.code !== "ENOENT") {
|
|
6747
7150
|
console.error(
|
|
@@ -6763,14 +7166,14 @@ async function runHooks(opts) {
|
|
|
6763
7166
|
...settings,
|
|
6764
7167
|
hooks: { ...hooks, PreToolUse: preToolUse }
|
|
6765
7168
|
};
|
|
6766
|
-
await
|
|
6767
|
-
await
|
|
7169
|
+
await fs14.mkdir(path15.dirname(settingsFile), { recursive: true });
|
|
7170
|
+
await fs14.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
6768
7171
|
const flag = gateFlagPath();
|
|
6769
7172
|
if (opts.gate) {
|
|
6770
|
-
await
|
|
6771
|
-
await
|
|
7173
|
+
await fs14.mkdir(path15.dirname(flag), { recursive: true });
|
|
7174
|
+
await fs14.writeFile(flag, "1\n", "utf8");
|
|
6772
7175
|
} else {
|
|
6773
|
-
await
|
|
7176
|
+
await fs14.rm(flag, { force: true });
|
|
6774
7177
|
}
|
|
6775
7178
|
const mode = opts.gate ? "GATE (deny search until you ask the graph)" : "nudge (search still runs)";
|
|
6776
7179
|
console.log(`neat hooks: installed the search hook in ${opts.gate ? "gate" : "nudge"} mode`);
|
|
@@ -6859,8 +7262,8 @@ async function runHooksCommand(args) {
|
|
|
6859
7262
|
}
|
|
6860
7263
|
|
|
6861
7264
|
// src/claude-cli.ts
|
|
6862
|
-
import
|
|
6863
|
-
import { promises as
|
|
7265
|
+
import path16 from "path";
|
|
7266
|
+
import { promises as fs15 } from "fs";
|
|
6864
7267
|
var NEAT_SECTION_HEADING = "## neat";
|
|
6865
7268
|
var NEAT_DIRECTIVE_BODY = `This project has NEAT wired in: a live, fused semantic graph of the system \u2014
|
|
6866
7269
|
code and runtime behaviour (OpenTelemetry) in one model, every fact tagged with
|
|
@@ -6894,8 +7297,8 @@ ${NEAT_DIRECTIVE_BODY}
|
|
|
6894
7297
|
}
|
|
6895
7298
|
function claudeMdPath() {
|
|
6896
7299
|
const override = process.env.NEAT_CLAUDE_MD;
|
|
6897
|
-
if (override && override.length > 0) return
|
|
6898
|
-
return
|
|
7300
|
+
if (override && override.length > 0) return path16.resolve(override);
|
|
7301
|
+
return path16.join(process.cwd(), "CLAUDE.md");
|
|
6899
7302
|
}
|
|
6900
7303
|
function splitAroundSection(raw) {
|
|
6901
7304
|
const lines = raw.split("\n");
|
|
@@ -6923,7 +7326,7 @@ function compose(before, after) {
|
|
|
6923
7326
|
}
|
|
6924
7327
|
async function readIfExists(file) {
|
|
6925
7328
|
try {
|
|
6926
|
-
return await
|
|
7329
|
+
return await fs15.readFile(file, "utf8");
|
|
6927
7330
|
} catch (err) {
|
|
6928
7331
|
if (err.code === "ENOENT") return null;
|
|
6929
7332
|
throw err;
|
|
@@ -6934,8 +7337,8 @@ async function runInstall() {
|
|
|
6934
7337
|
const raw = await readIfExists(file) ?? "";
|
|
6935
7338
|
const { before, after, found } = splitAroundSection(raw);
|
|
6936
7339
|
const next = compose(before, after);
|
|
6937
|
-
await
|
|
6938
|
-
await
|
|
7340
|
+
await fs15.mkdir(path16.dirname(file), { recursive: true });
|
|
7341
|
+
await fs15.writeFile(file, next, "utf8");
|
|
6939
7342
|
const verb = raw.length === 0 ? "created" : found ? "refreshed" : "added";
|
|
6940
7343
|
console.log(`neat claude: ${verb} the \`${NEAT_SECTION_HEADING}\` section in ${file}`);
|
|
6941
7344
|
console.log("Your agent will now reach for `neat ask` before Read/Grep/Bash. Restart the");
|
|
@@ -6956,7 +7359,7 @@ async function runUninstall() {
|
|
|
6956
7359
|
}
|
|
6957
7360
|
const remaining = [before, after].filter((s) => s.length > 0).join("\n\n");
|
|
6958
7361
|
const next = remaining.length > 0 ? remaining.replace(/\n*$/, "") + "\n" : "";
|
|
6959
|
-
await
|
|
7362
|
+
await fs15.writeFile(file, next, "utf8");
|
|
6960
7363
|
console.log(`neat claude: removed the \`${NEAT_SECTION_HEADING}\` section from ${file}.`);
|
|
6961
7364
|
return { exitCode: 0 };
|
|
6962
7365
|
}
|
|
@@ -6998,9 +7401,9 @@ async function runClaudeCommand(args) {
|
|
|
6998
7401
|
}
|
|
6999
7402
|
|
|
7000
7403
|
// src/codex-cli.ts
|
|
7001
|
-
import
|
|
7404
|
+
import path17 from "path";
|
|
7002
7405
|
import os3 from "os";
|
|
7003
|
-
import { promises as
|
|
7406
|
+
import { promises as fs16 } from "fs";
|
|
7004
7407
|
import { isDeepStrictEqual } from "util";
|
|
7005
7408
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
7006
7409
|
var CODEX_MCP_SERVER = {
|
|
@@ -7018,14 +7421,14 @@ var NEAT_GRAPH_FIRST_START = "<!-- neat:graph-first -->";
|
|
|
7018
7421
|
var NEAT_GRAPH_FIRST_END = "<!-- /neat:graph-first -->";
|
|
7019
7422
|
function codexConfigPath() {
|
|
7020
7423
|
const override = process.env.NEAT_CODEX_CONFIG;
|
|
7021
|
-
if (override && override.length > 0) return
|
|
7424
|
+
if (override && override.length > 0) return path17.resolve(override);
|
|
7022
7425
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? os3.homedir();
|
|
7023
|
-
return
|
|
7426
|
+
return path17.join(home, ".codex", "config.toml");
|
|
7024
7427
|
}
|
|
7025
7428
|
function agentsFilePath() {
|
|
7026
7429
|
const override = process.env.NEAT_CODEX_AGENTS;
|
|
7027
|
-
if (override && override.length > 0) return
|
|
7028
|
-
return
|
|
7430
|
+
if (override && override.length > 0) return path17.resolve(override);
|
|
7431
|
+
return path17.join(process.cwd(), "AGENTS.md");
|
|
7029
7432
|
}
|
|
7030
7433
|
function isTableHeader(line) {
|
|
7031
7434
|
return /^\s*\[\[?[^\]]+\]\]?\s*$/.test(line);
|
|
@@ -7159,7 +7562,7 @@ async function runCodex(opts) {
|
|
|
7159
7562
|
const agentsPath = agentsFilePath();
|
|
7160
7563
|
let configRaw = "";
|
|
7161
7564
|
try {
|
|
7162
|
-
configRaw = await
|
|
7565
|
+
configRaw = await fs16.readFile(configPath, "utf8");
|
|
7163
7566
|
} catch (err) {
|
|
7164
7567
|
if (err.code !== "ENOENT") {
|
|
7165
7568
|
console.error(`neat codex: failed to read ${configPath} \u2014 ${err.message}`);
|
|
@@ -7168,7 +7571,7 @@ async function runCodex(opts) {
|
|
|
7168
7571
|
}
|
|
7169
7572
|
let agentsRaw = "";
|
|
7170
7573
|
try {
|
|
7171
|
-
agentsRaw = await
|
|
7574
|
+
agentsRaw = await fs16.readFile(agentsPath, "utf8");
|
|
7172
7575
|
} catch (err) {
|
|
7173
7576
|
if (err.code !== "ENOENT") {
|
|
7174
7577
|
console.error(`neat codex: failed to read ${agentsPath} \u2014 ${err.message}`);
|
|
@@ -7208,15 +7611,15 @@ async function runCodex(opts) {
|
|
|
7208
7611
|
return { exitCode: 0 };
|
|
7209
7612
|
}
|
|
7210
7613
|
if (config.changed) {
|
|
7211
|
-
await
|
|
7212
|
-
await
|
|
7614
|
+
await fs16.mkdir(path17.dirname(configPath), { recursive: true });
|
|
7615
|
+
await fs16.writeFile(configPath, config.text, "utf8");
|
|
7213
7616
|
console.log(`neat codex: wrote [mcp_servers.neat] to ${configPath}`);
|
|
7214
7617
|
} else {
|
|
7215
7618
|
console.log(`neat codex: ${configPath} already has NEAT's MCP server`);
|
|
7216
7619
|
}
|
|
7217
7620
|
if (agents.changed) {
|
|
7218
|
-
await
|
|
7219
|
-
await
|
|
7621
|
+
await fs16.mkdir(path17.dirname(agentsPath), { recursive: true });
|
|
7622
|
+
await fs16.writeFile(agentsPath, agents.text, "utf8");
|
|
7220
7623
|
console.log(`neat codex: wrote the graph-first block to ${agentsPath}`);
|
|
7221
7624
|
} else {
|
|
7222
7625
|
console.log(`neat codex: ${agentsPath} already has the graph-first block`);
|
|
@@ -7272,9 +7675,9 @@ async function runCodexCommand(args) {
|
|
|
7272
7675
|
}
|
|
7273
7676
|
|
|
7274
7677
|
// src/editors-cli.ts
|
|
7275
|
-
import
|
|
7678
|
+
import path18 from "path";
|
|
7276
7679
|
import os4 from "os";
|
|
7277
|
-
import { promises as
|
|
7680
|
+
import { promises as fs17 } from "fs";
|
|
7278
7681
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
7279
7682
|
import * as jsonc from "jsonc-parser";
|
|
7280
7683
|
var NEAT_MCP_SERVER = {
|
|
@@ -7298,17 +7701,17 @@ function homeDir() {
|
|
|
7298
7701
|
}
|
|
7299
7702
|
function xdgConfigDir() {
|
|
7300
7703
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
7301
|
-
return xdg && xdg.length > 0 ?
|
|
7704
|
+
return xdg && xdg.length > 0 ? path18.resolve(xdg) : path18.join(homeDir(), ".config");
|
|
7302
7705
|
}
|
|
7303
7706
|
function envOverride(name) {
|
|
7304
7707
|
const v = process.env[name];
|
|
7305
|
-
return v && v.length > 0 ?
|
|
7708
|
+
return v && v.length > 0 ? path18.resolve(v) : void 0;
|
|
7306
7709
|
}
|
|
7307
7710
|
var CURSOR_CLIENT = {
|
|
7308
7711
|
id: "cursor",
|
|
7309
7712
|
label: "Cursor",
|
|
7310
7713
|
docsUrl: "https://docs.cursor.com/context/mcp",
|
|
7311
|
-
mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ??
|
|
7714
|
+
mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ?? path18.join(homeDir(), ".cursor", "mcp.json"),
|
|
7312
7715
|
mcpContainerKey: "mcpServers",
|
|
7313
7716
|
format: "json",
|
|
7314
7717
|
// Cursor still reads a single `.cursorrules` at the project root (the modern
|
|
@@ -7320,7 +7723,7 @@ var DEVIN_CLIENT = {
|
|
|
7320
7723
|
id: "devin",
|
|
7321
7724
|
label: "Devin Desktop (Cascade)",
|
|
7322
7725
|
docsUrl: "https://docs.devin.ai/desktop/cascade/mcp",
|
|
7323
|
-
mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ??
|
|
7726
|
+
mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? path18.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
|
|
7324
7727
|
mcpContainerKey: "mcpServers",
|
|
7325
7728
|
format: "json",
|
|
7326
7729
|
rulesFileName: ".windsurfrules"
|
|
@@ -7329,7 +7732,7 @@ var GEMINI_CLIENT = {
|
|
|
7329
7732
|
id: "gemini",
|
|
7330
7733
|
label: "Gemini CLI",
|
|
7331
7734
|
docsUrl: "https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md",
|
|
7332
|
-
mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ??
|
|
7735
|
+
mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? path18.join(homeDir(), ".gemini", "settings.json"),
|
|
7333
7736
|
mcpContainerKey: "mcpServers",
|
|
7334
7737
|
format: "json",
|
|
7335
7738
|
rulesFileName: "GEMINI.md"
|
|
@@ -7338,7 +7741,7 @@ var QWEN_CLIENT = {
|
|
|
7338
7741
|
id: "qwen",
|
|
7339
7742
|
label: "Qwen Code",
|
|
7340
7743
|
docsUrl: "https://qwenlm.github.io/qwen-code-docs/en/users/features/mcp/",
|
|
7341
|
-
mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ??
|
|
7744
|
+
mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? path18.join(homeDir(), ".qwen", "settings.json"),
|
|
7342
7745
|
mcpContainerKey: "mcpServers",
|
|
7343
7746
|
format: "json",
|
|
7344
7747
|
rulesFileName: "QWEN.md"
|
|
@@ -7347,7 +7750,7 @@ var AMAZONQ_CLIENT = {
|
|
|
7347
7750
|
id: "amazonq",
|
|
7348
7751
|
label: "Amazon Q Developer CLI",
|
|
7349
7752
|
docsUrl: "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-configuration.html",
|
|
7350
|
-
mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ??
|
|
7753
|
+
mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? path18.join(homeDir(), ".aws", "amazonq", "mcp.json"),
|
|
7351
7754
|
mcpContainerKey: "mcpServers",
|
|
7352
7755
|
format: "json"
|
|
7353
7756
|
};
|
|
@@ -7355,7 +7758,7 @@ var ROOCODE_CLIENT = {
|
|
|
7355
7758
|
id: "roocode",
|
|
7356
7759
|
label: "Roo Code",
|
|
7357
7760
|
docsUrl: "https://roocodeinc.github.io/Roo-Code/features/mcp/using-mcp-in-roo",
|
|
7358
|
-
mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ??
|
|
7761
|
+
mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? path18.join(process.cwd(), ".roo", "mcp.json"),
|
|
7359
7762
|
mcpContainerKey: "mcpServers",
|
|
7360
7763
|
format: "json"
|
|
7361
7764
|
};
|
|
@@ -7368,9 +7771,9 @@ var ZED_CLIENT = {
|
|
|
7368
7771
|
if (override) return override;
|
|
7369
7772
|
if (process.platform === "win32") {
|
|
7370
7773
|
const appData = process.env.APPDATA;
|
|
7371
|
-
if (appData && appData.length > 0) return
|
|
7774
|
+
if (appData && appData.length > 0) return path18.join(appData, "Zed", "settings.json");
|
|
7372
7775
|
}
|
|
7373
|
-
return
|
|
7776
|
+
return path18.join(homeDir(), ".config", "zed", "settings.json");
|
|
7374
7777
|
},
|
|
7375
7778
|
mcpContainerKey: "context_servers",
|
|
7376
7779
|
format: "jsonc",
|
|
@@ -7380,7 +7783,7 @@ var OPENCODE_CLIENT = {
|
|
|
7380
7783
|
id: "opencode",
|
|
7381
7784
|
label: "OpenCode",
|
|
7382
7785
|
docsUrl: "https://opencode.ai/docs/mcp-servers/",
|
|
7383
|
-
mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ??
|
|
7786
|
+
mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? path18.join(xdgConfigDir(), "opencode", "opencode.json"),
|
|
7384
7787
|
mcpContainerKey: "mcp",
|
|
7385
7788
|
format: "json",
|
|
7386
7789
|
serverEntry: NEAT_OPENCODE_SERVER,
|
|
@@ -7390,7 +7793,7 @@ var CRUSH_CLIENT = {
|
|
|
7390
7793
|
id: "crush",
|
|
7391
7794
|
label: "Crush",
|
|
7392
7795
|
docsUrl: "https://charmbracelet-crush.mintlify.app/configuration/mcp",
|
|
7393
|
-
mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ??
|
|
7796
|
+
mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? path18.join(xdgConfigDir(), "crush", "crush.json"),
|
|
7394
7797
|
mcpContainerKey: "mcp",
|
|
7395
7798
|
format: "json",
|
|
7396
7799
|
serverEntry: NEAT_CRUSH_SERVER,
|
|
@@ -7453,7 +7856,7 @@ async function planMcp(client, mcpPath) {
|
|
|
7453
7856
|
const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
|
|
7454
7857
|
let raw = "";
|
|
7455
7858
|
try {
|
|
7456
|
-
raw = await
|
|
7859
|
+
raw = await fs17.readFile(mcpPath, "utf8");
|
|
7457
7860
|
} catch (err) {
|
|
7458
7861
|
const e = err;
|
|
7459
7862
|
if (e.code === "ENOENT") {
|
|
@@ -7495,7 +7898,7 @@ async function runEditorInstall(client, opts) {
|
|
|
7495
7898
|
const mcpPath = client.mcpConfigPath();
|
|
7496
7899
|
const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
|
|
7497
7900
|
const hasRules = typeof client.rulesFileName === "string";
|
|
7498
|
-
const rulesPath = hasRules ?
|
|
7901
|
+
const rulesPath = hasRules ? path18.join(opts.projectDir, client.rulesFileName) : "";
|
|
7499
7902
|
const mcp = await planMcp(client, mcpPath);
|
|
7500
7903
|
if (mcp === null) return { exitCode: 1 };
|
|
7501
7904
|
let existingRules = "";
|
|
@@ -7504,7 +7907,7 @@ async function runEditorInstall(client, opts) {
|
|
|
7504
7907
|
let block = "";
|
|
7505
7908
|
if (hasRules) {
|
|
7506
7909
|
try {
|
|
7507
|
-
existingRules = await
|
|
7910
|
+
existingRules = await fs17.readFile(rulesPath, "utf8");
|
|
7508
7911
|
} catch (err) {
|
|
7509
7912
|
if (err.code !== "ENOENT") {
|
|
7510
7913
|
console.error(`neat ${client.id}: failed to read ${rulesPath} \u2014 ${err.message}`);
|
|
@@ -7538,11 +7941,11 @@ async function runEditorInstall(client, opts) {
|
|
|
7538
7941
|
);
|
|
7539
7942
|
return { exitCode: 0 };
|
|
7540
7943
|
}
|
|
7541
|
-
await
|
|
7542
|
-
await
|
|
7944
|
+
await fs17.mkdir(path18.dirname(mcpPath), { recursive: true });
|
|
7945
|
+
await fs17.writeFile(mcpPath, mcp.text, "utf8");
|
|
7543
7946
|
if (hasRules) {
|
|
7544
|
-
await
|
|
7545
|
-
await
|
|
7947
|
+
await fs17.mkdir(path18.dirname(rulesPath), { recursive: true });
|
|
7948
|
+
await fs17.writeFile(rulesPath, newRules, "utf8");
|
|
7546
7949
|
}
|
|
7547
7950
|
console.log(`neat ${client.id}: wired NEAT into ${client.label}`);
|
|
7548
7951
|
console.log(` MCP server: ${mcpPath} (${client.mcpContainerKey}.neat \u2192 npx -y @neat.is/mcp)`);
|
|
@@ -8042,7 +8445,7 @@ function sleep(ms, signal) {
|
|
|
8042
8445
|
}
|
|
8043
8446
|
|
|
8044
8447
|
// src/cli-verbs.ts
|
|
8045
|
-
import
|
|
8448
|
+
import path19 from "path";
|
|
8046
8449
|
async function resolveProjectEntry(opts) {
|
|
8047
8450
|
const entries = await listProjects();
|
|
8048
8451
|
if (opts.project) {
|
|
@@ -8052,7 +8455,7 @@ async function resolveProjectEntry(opts) {
|
|
|
8052
8455
|
const cwd = opts.cwd ?? process.cwd();
|
|
8053
8456
|
const resolvedCwd = await normalizeProjectPath(cwd);
|
|
8054
8457
|
for (const entry2 of entries) {
|
|
8055
|
-
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${
|
|
8458
|
+
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path19.sep}`)) {
|
|
8056
8459
|
return entry2;
|
|
8057
8460
|
}
|
|
8058
8461
|
}
|
|
@@ -8629,7 +9032,7 @@ async function buildPatchSections(services, project) {
|
|
|
8629
9032
|
}
|
|
8630
9033
|
async function runInit(opts) {
|
|
8631
9034
|
const written = [];
|
|
8632
|
-
const stat = await
|
|
9035
|
+
const stat = await fs18.stat(opts.scanPath).catch(() => null);
|
|
8633
9036
|
if (!stat || !stat.isDirectory()) {
|
|
8634
9037
|
console.error(`neat init: ${opts.scanPath} is not a directory`);
|
|
8635
9038
|
return { exitCode: 2, writtenFiles: written };
|
|
@@ -8638,13 +9041,13 @@ async function runInit(opts) {
|
|
|
8638
9041
|
printDiscoveryReport(opts, services);
|
|
8639
9042
|
const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
|
|
8640
9043
|
const patch = renderPatch(sections);
|
|
8641
|
-
const patchPath =
|
|
9044
|
+
const patchPath = path20.join(opts.scanPath, "neat.patch");
|
|
8642
9045
|
if (opts.dryRun) {
|
|
8643
|
-
await
|
|
9046
|
+
await fs18.writeFile(patchPath, patch, "utf8");
|
|
8644
9047
|
written.push(patchPath);
|
|
8645
9048
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
8646
|
-
const gitignorePath =
|
|
8647
|
-
const gitignoreExists = await
|
|
9049
|
+
const gitignorePath = path20.join(opts.scanPath, ".gitignore");
|
|
9050
|
+
const gitignoreExists = await fs18.stat(gitignorePath).then(() => true).catch(() => false);
|
|
8648
9051
|
const verb = gitignoreExists ? "append" : "create";
|
|
8649
9052
|
console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
|
|
8650
9053
|
console.log("rerun without --dry-run to register and snapshot.");
|
|
@@ -8655,9 +9058,9 @@ async function runInit(opts) {
|
|
|
8655
9058
|
const graph = getGraph(graphKey);
|
|
8656
9059
|
const projectPaths = pathsForProject(
|
|
8657
9060
|
graphKey,
|
|
8658
|
-
|
|
9061
|
+
path20.join(opts.scanPath, "neat-out")
|
|
8659
9062
|
);
|
|
8660
|
-
const errorsPath =
|
|
9063
|
+
const errorsPath = path20.join(path20.dirname(opts.outPath), path20.basename(projectPaths.errorsPath));
|
|
8661
9064
|
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
8662
9065
|
await saveGraphToDisk(graph, opts.outPath);
|
|
8663
9066
|
written.push(opts.outPath);
|
|
@@ -8736,7 +9139,7 @@ async function runInit(opts) {
|
|
|
8736
9139
|
console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
8737
9140
|
}
|
|
8738
9141
|
} else {
|
|
8739
|
-
await
|
|
9142
|
+
await fs18.writeFile(patchPath, patch, "utf8");
|
|
8740
9143
|
written.push(patchPath);
|
|
8741
9144
|
}
|
|
8742
9145
|
}
|
|
@@ -8777,9 +9180,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
8777
9180
|
};
|
|
8778
9181
|
function claudeConfigPath() {
|
|
8779
9182
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
8780
|
-
if (override && override.length > 0) return
|
|
9183
|
+
if (override && override.length > 0) return path20.resolve(override);
|
|
8781
9184
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
8782
|
-
return
|
|
9185
|
+
return path20.join(home, ".claude.json");
|
|
8783
9186
|
}
|
|
8784
9187
|
async function runSkill(opts) {
|
|
8785
9188
|
const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -8791,7 +9194,7 @@ async function runSkill(opts) {
|
|
|
8791
9194
|
const target = claudeConfigPath();
|
|
8792
9195
|
let existing = {};
|
|
8793
9196
|
try {
|
|
8794
|
-
existing = JSON.parse(await
|
|
9197
|
+
existing = JSON.parse(await fs18.readFile(target, "utf8"));
|
|
8795
9198
|
} catch (err) {
|
|
8796
9199
|
if (err.code !== "ENOENT") {
|
|
8797
9200
|
console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
|
|
@@ -8803,8 +9206,8 @@ async function runSkill(opts) {
|
|
|
8803
9206
|
...existing,
|
|
8804
9207
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
8805
9208
|
};
|
|
8806
|
-
await
|
|
8807
|
-
await
|
|
9209
|
+
await fs18.mkdir(path20.dirname(target), { recursive: true });
|
|
9210
|
+
await fs18.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
8808
9211
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
8809
9212
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
8810
9213
|
console.log("");
|
|
@@ -8830,7 +9233,7 @@ async function runSkill(opts) {
|
|
|
8830
9233
|
async function main() {
|
|
8831
9234
|
const argv = process.argv.slice(2);
|
|
8832
9235
|
const cmd0 = argv[0];
|
|
8833
|
-
if (
|
|
9236
|
+
if (argv.some((a) => a === "-h" || a === "--help")) {
|
|
8834
9237
|
usage5();
|
|
8835
9238
|
process.exit(0);
|
|
8836
9239
|
}
|
|
@@ -8925,12 +9328,12 @@ async function main() {
|
|
|
8925
9328
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
8926
9329
|
process.exit(2);
|
|
8927
9330
|
}
|
|
8928
|
-
const scanPath =
|
|
9331
|
+
const scanPath = path20.resolve(target);
|
|
8929
9332
|
const projectExplicit = parsed.project !== null;
|
|
8930
|
-
const projectName = projectExplicit ? project :
|
|
9333
|
+
const projectName = projectExplicit ? project : path20.basename(scanPath);
|
|
8931
9334
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
8932
|
-
const fallback = pathsForProject(projectKey,
|
|
8933
|
-
const outPath =
|
|
9335
|
+
const fallback = pathsForProject(projectKey, path20.join(scanPath, "neat-out")).snapshotPath;
|
|
9336
|
+
const outPath = path20.resolve(process.env.NEAT_OUT_PATH ?? fallback);
|
|
8934
9337
|
const result = await runInit({
|
|
8935
9338
|
scanPath,
|
|
8936
9339
|
outPath,
|
|
@@ -8951,21 +9354,21 @@ async function main() {
|
|
|
8951
9354
|
usage5();
|
|
8952
9355
|
process.exit(2);
|
|
8953
9356
|
}
|
|
8954
|
-
const scanPath =
|
|
8955
|
-
const stat = await
|
|
9357
|
+
const scanPath = path20.resolve(target);
|
|
9358
|
+
const stat = await fs18.stat(scanPath).catch(() => null);
|
|
8956
9359
|
if (!stat || !stat.isDirectory()) {
|
|
8957
9360
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
8958
9361
|
process.exit(2);
|
|
8959
9362
|
}
|
|
8960
|
-
const projectPaths = pathsForProject(project,
|
|
8961
|
-
const outPath =
|
|
8962
|
-
const errorsPath =
|
|
8963
|
-
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))
|
|
8964
9367
|
);
|
|
8965
|
-
const staleEventsPath =
|
|
8966
|
-
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))
|
|
8967
9370
|
);
|
|
8968
|
-
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;
|
|
8969
9372
|
const handle = await startWatch(getGraph(project), {
|
|
8970
9373
|
scanPath,
|
|
8971
9374
|
outPath,
|
|
@@ -8974,7 +9377,7 @@ async function main() {
|
|
|
8974
9377
|
project,
|
|
8975
9378
|
// Resolve NEAT_HOME so a `neat watch` picks up connectors added to
|
|
8976
9379
|
// ~/.neat/connectors.json (#871). Same resolution the rest of the CLI uses.
|
|
8977
|
-
neatHome: process.env.NEAT_HOME ?
|
|
9380
|
+
neatHome: process.env.NEAT_HOME ? path20.resolve(process.env.NEAT_HOME) : path20.join(os5.homedir(), ".neat"),
|
|
8978
9381
|
...embeddingsCachePath ? { embeddingsCachePath } : {},
|
|
8979
9382
|
host: process.env.HOST ?? "0.0.0.0",
|
|
8980
9383
|
port: Number(process.env.PORT ?? 8080),
|
|
@@ -9156,11 +9559,11 @@ async function main() {
|
|
|
9156
9559
|
process.exit(1);
|
|
9157
9560
|
}
|
|
9158
9561
|
async function tryOrchestrator(cmd, parsed) {
|
|
9159
|
-
const scanPath =
|
|
9160
|
-
const stat = await
|
|
9562
|
+
const scanPath = path20.resolve(cmd);
|
|
9563
|
+
const stat = await fs18.stat(scanPath).catch(() => null);
|
|
9161
9564
|
if (!stat || !stat.isDirectory()) return null;
|
|
9162
9565
|
const projectExplicit = parsed.project !== null;
|
|
9163
|
-
const projectName = projectExplicit ? parsed.project :
|
|
9566
|
+
const projectName = projectExplicit ? parsed.project : path20.basename(scanPath);
|
|
9164
9567
|
const result = await runOrchestrator({
|
|
9165
9568
|
scanPath,
|
|
9166
9569
|
project: projectName,
|
|
@@ -9175,7 +9578,18 @@ async function tryOrchestrator(cmd, parsed) {
|
|
|
9175
9578
|
async function runWelcomeFlow(parsed) {
|
|
9176
9579
|
return runWelcome({
|
|
9177
9580
|
login: (loginArgv) => runLoginCommand(loginArgv),
|
|
9178
|
-
|
|
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
|
|
9179
9593
|
});
|
|
9180
9594
|
}
|
|
9181
9595
|
var QUERY_VERBS = /* @__PURE__ */ new Set([
|
|
@@ -9208,6 +9622,13 @@ var ProjectResolutionError = class extends Error {
|
|
|
9208
9622
|
}
|
|
9209
9623
|
exitCode;
|
|
9210
9624
|
};
|
|
9625
|
+
function isLoopbackEndpoint(endpoint) {
|
|
9626
|
+
try {
|
|
9627
|
+
return isLoopbackHost(new URL(endpoint).hostname.replace(/^\[|\]$/g, ""));
|
|
9628
|
+
} catch {
|
|
9629
|
+
return false;
|
|
9630
|
+
}
|
|
9631
|
+
}
|
|
9211
9632
|
async function resolveProjectForVerb(client, parsed) {
|
|
9212
9633
|
const explicit = resolveProjectFlag(parsed);
|
|
9213
9634
|
if (explicit) return explicit;
|
|
@@ -9438,6 +9859,9 @@ async function runQueryVerb(cmd, parsed) {
|
|
|
9438
9859
|
console.error(`neat ${cmd}: ${detail.trim()}`);
|
|
9439
9860
|
} else if (err instanceof TransportError) {
|
|
9440
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
|
+
}
|
|
9441
9865
|
} else {
|
|
9442
9866
|
console.error(`neat ${cmd}: ${err.message}`);
|
|
9443
9867
|
}
|
|
@@ -9493,6 +9917,7 @@ export {
|
|
|
9493
9917
|
QUERY_VERBS,
|
|
9494
9918
|
UnknownProfileError,
|
|
9495
9919
|
commandPrefix,
|
|
9920
|
+
isLoopbackEndpoint,
|
|
9496
9921
|
isNpxInvocation,
|
|
9497
9922
|
main,
|
|
9498
9923
|
parseArgs,
|