@neat.is/core 0.9.18-dev.20260917 → 0.9.18-dev.20260918
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-SSLPBQWY.js → chunk-KIV7OB2K.js} +4 -2
- package/dist/{chunk-SSLPBQWY.js.map → chunk-KIV7OB2K.js.map} +1 -1
- package/dist/{chunk-NV4WWJSU.js → chunk-MGQSR6QZ.js} +132 -2
- package/dist/chunk-MGQSR6QZ.js.map +1 -0
- package/dist/cli.cjs +1143 -998
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1129 -991
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +132 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +132 -1
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-NV4WWJSU.js.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -19501,7 +19501,8 @@ function startConnectorPollLoop(connector, ctx, graph, resolveTarget, options =
|
|
|
19501
19501
|
void (async () => {
|
|
19502
19502
|
const tickStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
19503
19503
|
try {
|
|
19504
|
-
const
|
|
19504
|
+
const credentials = options.refreshCredentials ? await options.refreshCredentials() : ctx.credentials;
|
|
19505
|
+
const result = await runConnectorPoll(connector, { ...ctx, credentials, since }, graph, resolveTarget);
|
|
19505
19506
|
since = tickStartedAt;
|
|
19506
19507
|
if (connectorId) {
|
|
19507
19508
|
recordConnectorPoll(connectorId, {
|
|
@@ -24292,6 +24293,11 @@ var import_node_fs43 = require("fs");
|
|
|
24292
24293
|
var import_node_path79 = __toESM(require("path"), 1);
|
|
24293
24294
|
var import_node_module = require("module");
|
|
24294
24295
|
init_otel();
|
|
24296
|
+
|
|
24297
|
+
// src/connectors/hosted.ts
|
|
24298
|
+
init_cjs_shims();
|
|
24299
|
+
|
|
24300
|
+
// src/daemon.ts
|
|
24295
24301
|
init_auth();
|
|
24296
24302
|
|
|
24297
24303
|
// src/unrouted.ts
|
|
@@ -29331,1144 +29337,1257 @@ async function runConnectorCommand(rawArgs, deps = {}) {
|
|
|
29331
29337
|
}
|
|
29332
29338
|
}
|
|
29333
29339
|
|
|
29334
|
-
// src/
|
|
29340
|
+
// src/hosted-connect-cli.ts
|
|
29335
29341
|
init_cjs_shims();
|
|
29336
|
-
var
|
|
29337
|
-
var import_node_path89 = __toESM(require("path"), 1);
|
|
29342
|
+
var import_node_child_process5 = require("child_process");
|
|
29338
29343
|
|
|
29339
|
-
// src/
|
|
29344
|
+
// src/login-sso.ts
|
|
29340
29345
|
init_cjs_shims();
|
|
29341
|
-
var
|
|
29342
|
-
var
|
|
29343
|
-
|
|
29344
|
-
|
|
29345
|
-
|
|
29346
|
-
|
|
29347
|
-
|
|
29348
|
-
|
|
29349
|
-
|
|
29350
|
-
|
|
29351
|
-
|
|
29352
|
-
|
|
29353
|
-
|
|
29354
|
-
|
|
29355
|
-
this.name = "TransportError";
|
|
29356
|
-
}
|
|
29357
|
-
};
|
|
29358
|
-
function resolveAuthToken(env = process.env) {
|
|
29359
|
-
const t = env.NEAT_AUTH_TOKEN;
|
|
29360
|
-
return t && t.length > 0 ? t : void 0;
|
|
29346
|
+
var import_node_http2 = require("http");
|
|
29347
|
+
var import_node_child_process4 = require("child_process");
|
|
29348
|
+
var import_node_crypto6 = require("crypto");
|
|
29349
|
+
|
|
29350
|
+
// src/profiles.ts
|
|
29351
|
+
init_cjs_shims();
|
|
29352
|
+
var import_node_fs53 = require("fs");
|
|
29353
|
+
var import_node_os6 = __toESM(require("os"), 1);
|
|
29354
|
+
var import_node_path89 = __toESM(require("path"), 1);
|
|
29355
|
+
var PROFILES_CONFIG_VERSION = 1;
|
|
29356
|
+
function neatHome3() {
|
|
29357
|
+
const override = process.env.NEAT_HOME;
|
|
29358
|
+
if (override && override.length > 0) return import_node_path89.default.resolve(override);
|
|
29359
|
+
return import_node_path89.default.join(import_node_os6.default.homedir(), ".neat");
|
|
29361
29360
|
}
|
|
29362
|
-
function
|
|
29363
|
-
|
|
29364
|
-
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
29365
|
-
return {
|
|
29366
|
-
async get(path97) {
|
|
29367
|
-
let res;
|
|
29368
|
-
try {
|
|
29369
|
-
res = await fetch(`${root}${path97}`, {
|
|
29370
|
-
headers: { ...authHeader }
|
|
29371
|
-
});
|
|
29372
|
-
} catch (err) {
|
|
29373
|
-
throw new TransportError(
|
|
29374
|
-
`cannot reach neat-core at ${root}: ${err.message}`
|
|
29375
|
-
);
|
|
29376
|
-
}
|
|
29377
|
-
if (!res.ok) {
|
|
29378
|
-
const body = await res.text().catch(() => "");
|
|
29379
|
-
throw new HttpError(
|
|
29380
|
-
res.status,
|
|
29381
|
-
`${res.status} ${res.statusText} on GET ${path97}: ${body}`,
|
|
29382
|
-
body
|
|
29383
|
-
);
|
|
29384
|
-
}
|
|
29385
|
-
return await res.json();
|
|
29386
|
-
},
|
|
29387
|
-
async post(path97, body) {
|
|
29388
|
-
let res;
|
|
29389
|
-
try {
|
|
29390
|
-
res = await fetch(`${root}${path97}`, {
|
|
29391
|
-
method: "POST",
|
|
29392
|
-
headers: { "content-type": "application/json", ...authHeader },
|
|
29393
|
-
body: JSON.stringify(body)
|
|
29394
|
-
});
|
|
29395
|
-
} catch (err) {
|
|
29396
|
-
throw new TransportError(
|
|
29397
|
-
`cannot reach neat-core at ${root}: ${err.message}`
|
|
29398
|
-
);
|
|
29399
|
-
}
|
|
29400
|
-
if (!res.ok) {
|
|
29401
|
-
const text = await res.text().catch(() => "");
|
|
29402
|
-
throw new HttpError(
|
|
29403
|
-
res.status,
|
|
29404
|
-
`${res.status} ${res.statusText} on POST ${path97}: ${text}`,
|
|
29405
|
-
text
|
|
29406
|
-
);
|
|
29407
|
-
}
|
|
29408
|
-
return await res.json();
|
|
29409
|
-
}
|
|
29410
|
-
};
|
|
29361
|
+
function profilesConfigPath(home = neatHome3()) {
|
|
29362
|
+
return import_node_path89.default.join(home, "profiles.json");
|
|
29411
29363
|
}
|
|
29412
|
-
function
|
|
29413
|
-
|
|
29414
|
-
return `/projects/${encodeURIComponent(project)}${suffix}`;
|
|
29364
|
+
function profilesConfigLockPath(home = neatHome3()) {
|
|
29365
|
+
return import_node_path89.default.join(home, "profiles.json.lock");
|
|
29415
29366
|
}
|
|
29416
|
-
|
|
29417
|
-
|
|
29418
|
-
|
|
29419
|
-
input.project,
|
|
29420
|
-
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
29421
|
-
);
|
|
29367
|
+
var MODE_MASK_LOOSER_THAN_06002 = 63;
|
|
29368
|
+
async function warnIfModeLooserThan06002(file) {
|
|
29369
|
+
if (process.platform === "win32") return;
|
|
29422
29370
|
try {
|
|
29423
|
-
const
|
|
29424
|
-
|
|
29425
|
-
|
|
29426
|
-
|
|
29427
|
-
|
|
29428
|
-
|
|
29429
|
-
`Edge provenances: ${provenances}`
|
|
29430
|
-
];
|
|
29431
|
-
if (result.fixRecommendation) blockLines.push(`Recommended fix: ${result.fixRecommendation}`);
|
|
29432
|
-
return {
|
|
29433
|
-
summary,
|
|
29434
|
-
block: blockLines.join("\n"),
|
|
29435
|
-
confidence: result.confidence,
|
|
29436
|
-
provenance: result.edgeProvenances.length ? result.edgeProvenances : void 0
|
|
29437
|
-
};
|
|
29438
|
-
} catch (err) {
|
|
29439
|
-
if (err instanceof HttpError && err.status === 404) {
|
|
29440
|
-
return {
|
|
29441
|
-
summary: `No root cause found for ${input.errorNode}. The node may be healthy, or it may not exist in the graph.`
|
|
29442
|
-
};
|
|
29371
|
+
const stat = await import_node_fs53.promises.stat(file);
|
|
29372
|
+
if ((stat.mode & MODE_MASK_LOOSER_THAN_06002) !== 0) {
|
|
29373
|
+
const mode = (stat.mode & 511).toString(8).padStart(3, "0");
|
|
29374
|
+
console.warn(
|
|
29375
|
+
`[neat] ${file} is mode 0${mode}, looser than the 0600 this file's token calls for \u2014 run \`chmod 600 ${file}\``
|
|
29376
|
+
);
|
|
29443
29377
|
}
|
|
29444
|
-
|
|
29378
|
+
} catch {
|
|
29445
29379
|
}
|
|
29446
29380
|
}
|
|
29447
|
-
async function
|
|
29448
|
-
const
|
|
29449
|
-
|
|
29450
|
-
input.project,
|
|
29451
|
-
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
29452
|
-
);
|
|
29381
|
+
async function readProfilesConfig(home = neatHome3()) {
|
|
29382
|
+
const file = profilesConfigPath(home);
|
|
29383
|
+
let raw;
|
|
29453
29384
|
try {
|
|
29454
|
-
|
|
29455
|
-
if (result.totalAffected === 0) {
|
|
29456
|
-
return {
|
|
29457
|
-
summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
|
|
29458
|
-
};
|
|
29459
|
-
}
|
|
29460
|
-
const sorted = [...result.affectedNodes].sort(
|
|
29461
|
-
(a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
|
|
29462
|
-
);
|
|
29463
|
-
const blockLines = sorted.map(formatBlastEntry);
|
|
29464
|
-
const minConfidence = sorted.reduce(
|
|
29465
|
-
(m, n) => Math.min(m, n.confidence),
|
|
29466
|
-
Number.POSITIVE_INFINITY
|
|
29467
|
-
);
|
|
29468
|
-
const provenances = [...new Set(sorted.map((n) => n.edgeProvenance))];
|
|
29469
|
-
return {
|
|
29470
|
-
summary: `Blast radius for ${result.origin}: ${result.totalAffected} dependent node${result.totalAffected === 1 ? "" : "s"} would break if it changed.`,
|
|
29471
|
-
block: blockLines.join("\n"),
|
|
29472
|
-
confidence: Number.isFinite(minConfidence) ? minConfidence : void 0,
|
|
29473
|
-
provenance: provenances.length ? provenances : void 0
|
|
29474
|
-
};
|
|
29385
|
+
raw = await import_node_fs53.promises.readFile(file, "utf8");
|
|
29475
29386
|
} catch (err) {
|
|
29476
|
-
if (err
|
|
29477
|
-
return {
|
|
29387
|
+
if (err.code === "ENOENT") {
|
|
29388
|
+
return { version: PROFILES_CONFIG_VERSION, profiles: [] };
|
|
29478
29389
|
}
|
|
29479
29390
|
throw err;
|
|
29480
29391
|
}
|
|
29481
|
-
|
|
29482
|
-
|
|
29483
|
-
const tag = n.edgeProvenance === import_types108.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
29484
|
-
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
29485
|
-
}
|
|
29486
|
-
async function runDependencies(client, input) {
|
|
29487
|
-
const depth = input.depth ?? 3;
|
|
29488
|
-
const path97 = projectPath(
|
|
29489
|
-
input.project,
|
|
29490
|
-
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
29491
|
-
);
|
|
29392
|
+
await warnIfModeLooserThan06002(file);
|
|
29393
|
+
let parsed;
|
|
29492
29394
|
try {
|
|
29493
|
-
|
|
29494
|
-
if (result.total === 0) {
|
|
29495
|
-
return {
|
|
29496
|
-
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
29497
|
-
};
|
|
29498
|
-
}
|
|
29499
|
-
const byDistance = /* @__PURE__ */ new Map();
|
|
29500
|
-
for (const dep of result.dependencies) {
|
|
29501
|
-
const ring = byDistance.get(dep.distance) ?? [];
|
|
29502
|
-
ring.push(dep);
|
|
29503
|
-
byDistance.set(dep.distance, ring);
|
|
29504
|
-
}
|
|
29505
|
-
const blockLines = [];
|
|
29506
|
-
for (const distance of [...byDistance.keys()].sort((a, b) => a - b)) {
|
|
29507
|
-
const label = distance === 1 ? "Direct (distance 1)" : `Distance ${distance}`;
|
|
29508
|
-
blockLines.push(`${label}:`);
|
|
29509
|
-
for (const dep of byDistance.get(distance)) {
|
|
29510
|
-
blockLines.push(` \u2022 ${dep.nodeId} \u2014 ${dep.edgeType} (${dep.provenance})`);
|
|
29511
|
-
}
|
|
29512
|
-
}
|
|
29513
|
-
const provenances = [...new Set(result.dependencies.map((d) => d.provenance))];
|
|
29514
|
-
const directCount = byDistance.get(1)?.length ?? 0;
|
|
29515
|
-
const summary = depth === 1 ? `${input.nodeId} has ${directCount} direct dependenc${directCount === 1 ? "y" : "ies"}.` : `${input.nodeId} has ${result.total} dependenc${result.total === 1 ? "y" : "ies"} reachable to depth ${depth} (${directCount} direct).`;
|
|
29516
|
-
return { summary, block: blockLines.join("\n"), provenance: provenances };
|
|
29395
|
+
parsed = JSON.parse(raw);
|
|
29517
29396
|
} catch (err) {
|
|
29518
|
-
|
|
29519
|
-
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
29520
|
-
}
|
|
29521
|
-
throw err;
|
|
29397
|
+
throw new Error(`${file} is not valid JSON: ${err.message}`);
|
|
29522
29398
|
}
|
|
29399
|
+
return validateConfig2(parsed, file);
|
|
29523
29400
|
}
|
|
29524
|
-
function
|
|
29525
|
-
|
|
29526
|
-
|
|
29401
|
+
function validateConfig2(parsed, file) {
|
|
29402
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
29403
|
+
throw new Error(`${file} must be a JSON object with a "profiles" array`);
|
|
29404
|
+
}
|
|
29405
|
+
const obj = parsed;
|
|
29406
|
+
const version = obj.version === void 0 ? PROFILES_CONFIG_VERSION : obj.version;
|
|
29407
|
+
if (typeof version !== "number" || !Number.isInteger(version)) {
|
|
29408
|
+
throw new Error(`${file}: "version" must be an integer`);
|
|
29409
|
+
}
|
|
29410
|
+
const rawProfiles = obj.profiles;
|
|
29411
|
+
if (!Array.isArray(rawProfiles)) {
|
|
29412
|
+
throw new Error(`${file}: "profiles" must be an array`);
|
|
29413
|
+
}
|
|
29414
|
+
const profiles = rawProfiles.map((entry2, i) => validateEntry2(entry2, i, file));
|
|
29415
|
+
const seen = /* @__PURE__ */ new Set();
|
|
29416
|
+
for (const p of profiles) {
|
|
29417
|
+
if (seen.has(p.name)) throw new Error(`${file}: duplicate profile name "${p.name}"`);
|
|
29418
|
+
seen.add(p.name);
|
|
29419
|
+
}
|
|
29420
|
+
let active;
|
|
29421
|
+
if (obj.active !== void 0) {
|
|
29422
|
+
if (typeof obj.active !== "string" || obj.active.length === 0) {
|
|
29423
|
+
throw new Error(`${file}: "active" must be a non-empty string when present`);
|
|
29424
|
+
}
|
|
29425
|
+
active = seen.has(obj.active) ? obj.active : void 0;
|
|
29426
|
+
}
|
|
29427
|
+
return { version, ...active ? { active } : {}, profiles };
|
|
29527
29428
|
}
|
|
29528
|
-
|
|
29429
|
+
function validateEntry2(entry2, index, file) {
|
|
29430
|
+
const where = `${file}: profiles[${index}]`;
|
|
29431
|
+
if (typeof entry2 !== "object" || entry2 === null || Array.isArray(entry2)) {
|
|
29432
|
+
throw new Error(`${where} must be an object`);
|
|
29433
|
+
}
|
|
29434
|
+
const e = entry2;
|
|
29435
|
+
const name = e.name;
|
|
29436
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
29437
|
+
throw new Error(`${where}.name must be a non-empty string`);
|
|
29438
|
+
}
|
|
29439
|
+
const endpoint2 = e.endpoint;
|
|
29440
|
+
if (typeof endpoint2 !== "string" || endpoint2.length === 0) {
|
|
29441
|
+
throw new Error(`${where}.endpoint must be a non-empty string`);
|
|
29442
|
+
}
|
|
29443
|
+
let parsedUrl;
|
|
29529
29444
|
try {
|
|
29530
|
-
|
|
29531
|
-
|
|
29532
|
-
|
|
29533
|
-
`/graph/observed-dependencies/${encodeURIComponent(input.nodeId)}`
|
|
29534
|
-
)
|
|
29535
|
-
);
|
|
29536
|
-
if (result.dependencies.length === 0) {
|
|
29537
|
-
if (result.observed) {
|
|
29538
|
-
return {
|
|
29539
|
-
summary: `${input.nodeId} makes no outbound runtime calls, but OTel has observed it receiving traffic on ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 it's a pure receiver.`,
|
|
29540
|
-
provenance: import_types108.Provenance.OBSERVED
|
|
29541
|
-
};
|
|
29542
|
-
}
|
|
29543
|
-
const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
29544
|
-
return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
|
|
29545
|
-
}
|
|
29546
|
-
const blockLines = result.dependencies.map((e) => observedDepLine(input.nodeId, e));
|
|
29547
|
-
return {
|
|
29548
|
-
summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
29549
|
-
block: blockLines.join("\n"),
|
|
29550
|
-
provenance: import_types108.Provenance.OBSERVED
|
|
29551
|
-
};
|
|
29552
|
-
} catch (err) {
|
|
29553
|
-
if (err instanceof HttpError && err.status === 404) {
|
|
29554
|
-
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
29555
|
-
}
|
|
29556
|
-
throw err;
|
|
29445
|
+
parsedUrl = new URL(endpoint2);
|
|
29446
|
+
} catch {
|
|
29447
|
+
throw new Error(`${where}.endpoint must be an absolute URL (got "${endpoint2}")`);
|
|
29557
29448
|
}
|
|
29558
|
-
|
|
29559
|
-
|
|
29560
|
-
const bits = [];
|
|
29561
|
-
if (e.signal) {
|
|
29562
|
-
bits.push(`spans=${e.signal.spanCount}`);
|
|
29563
|
-
if (e.signal.errorCount > 0) bits.push(`errors=${e.signal.errorCount}`);
|
|
29564
|
-
if (e.signal.lastObservedAgeMs !== void 0) {
|
|
29565
|
-
bits.push(`age=${formatDuration(e.signal.lastObservedAgeMs)}`);
|
|
29566
|
-
}
|
|
29567
|
-
} else if (e.callCount !== void 0) {
|
|
29568
|
-
bits.push(`callCount=${e.callCount}`);
|
|
29449
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
29450
|
+
throw new Error(`${where}.endpoint must be an http(s) URL (got "${parsedUrl.protocol}")`);
|
|
29569
29451
|
}
|
|
29570
|
-
if (e.
|
|
29571
|
-
|
|
29572
|
-
|
|
29452
|
+
if (e.authToken !== void 0 && (typeof e.authToken !== "string" || e.authToken.length === 0)) {
|
|
29453
|
+
throw new Error(`${where}.authToken must be a non-empty string when present`);
|
|
29454
|
+
}
|
|
29455
|
+
return {
|
|
29456
|
+
name,
|
|
29457
|
+
endpoint: endpoint2,
|
|
29458
|
+
...typeof e.authToken === "string" ? { authToken: e.authToken } : {}
|
|
29459
|
+
};
|
|
29573
29460
|
}
|
|
29574
|
-
function
|
|
29575
|
-
|
|
29576
|
-
|
|
29577
|
-
if (s < 60) return `${s}s`;
|
|
29578
|
-
const m = Math.round(s / 60);
|
|
29579
|
-
if (m < 60) return `${m}m`;
|
|
29580
|
-
const h = Math.round(m / 60);
|
|
29581
|
-
if (h < 48) return `${h}h`;
|
|
29582
|
-
return `${Math.round(h / 24)}d`;
|
|
29461
|
+
async function resolveProfile(name, home = neatHome3()) {
|
|
29462
|
+
const { profiles } = await readProfilesConfig(home);
|
|
29463
|
+
return profiles.find((p) => p.name === name);
|
|
29583
29464
|
}
|
|
29584
|
-
async function
|
|
29585
|
-
const
|
|
29586
|
-
|
|
29587
|
-
|
|
29588
|
-
const events = body.events;
|
|
29589
|
-
if (events.length === 0) {
|
|
29590
|
-
return {
|
|
29591
|
-
summary: input.nodeId ? `No incidents recorded against ${input.nodeId}.` : "No incidents recorded."
|
|
29592
|
-
};
|
|
29593
|
-
}
|
|
29594
|
-
const ordered = [...events].reverse().slice(0, input.limit ?? 20);
|
|
29595
|
-
const blockLines = [];
|
|
29596
|
-
for (const ev of ordered) {
|
|
29597
|
-
blockLines.push(` ${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`);
|
|
29598
|
-
blockLines.push(` trace=${ev.traceId} span=${ev.spanId}`);
|
|
29599
|
-
}
|
|
29600
|
-
const target = input.nodeId ?? "the project";
|
|
29601
|
-
return {
|
|
29602
|
-
summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
29603
|
-
block: blockLines.join("\n"),
|
|
29604
|
-
provenance: import_types108.Provenance.OBSERVED
|
|
29605
|
-
};
|
|
29606
|
-
} catch (err) {
|
|
29607
|
-
if (err instanceof HttpError && err.status === 404) {
|
|
29608
|
-
return { summary: `Node ${input.nodeId ?? ""} not found in the graph.` };
|
|
29609
|
-
}
|
|
29610
|
-
throw err;
|
|
29611
|
-
}
|
|
29465
|
+
async function getActiveProfile(home = neatHome3()) {
|
|
29466
|
+
const { active, profiles } = await readProfilesConfig(home);
|
|
29467
|
+
if (!active) return void 0;
|
|
29468
|
+
return profiles.find((p) => p.name === active);
|
|
29612
29469
|
}
|
|
29613
|
-
|
|
29614
|
-
const
|
|
29615
|
-
|
|
29616
|
-
|
|
29617
|
-
|
|
29618
|
-
|
|
29619
|
-
|
|
29620
|
-
const provider = result.provider ?? "substring";
|
|
29621
|
-
const blockLines = [];
|
|
29622
|
-
let topScore;
|
|
29623
|
-
for (const n of result.matches) {
|
|
29624
|
-
const score = provider !== "substring" && typeof n.score === "number" ? n.score : void 0;
|
|
29625
|
-
const scoreBit = score !== void 0 ? ` [score=${score.toFixed(2)}]` : "";
|
|
29626
|
-
if (score !== void 0 && (topScore === void 0 || score > topScore)) topScore = score;
|
|
29627
|
-
blockLines.push(
|
|
29628
|
-
` \u2022 ${n.id} (${n.type}) \u2014 ${n.name ?? n.id}${scoreBit}`
|
|
29629
|
-
);
|
|
29630
|
-
}
|
|
29631
|
-
return {
|
|
29632
|
-
summary: `Found ${result.matches.length} match${result.matches.length === 1 ? "" : "es"} for "${input.query}" via ${provider} provider.`,
|
|
29633
|
-
block: blockLines.join("\n"),
|
|
29634
|
-
confidence: topScore
|
|
29470
|
+
function serialize(config) {
|
|
29471
|
+
const names = new Set(config.profiles.map((p) => p.name));
|
|
29472
|
+
const active = config.active && names.has(config.active) ? config.active : void 0;
|
|
29473
|
+
const out = {
|
|
29474
|
+
version: config.version ?? PROFILES_CONFIG_VERSION,
|
|
29475
|
+
...active ? { active } : {},
|
|
29476
|
+
profiles: config.profiles
|
|
29635
29477
|
};
|
|
29478
|
+
return `${JSON.stringify(out, null, 2)}
|
|
29479
|
+
`;
|
|
29636
29480
|
}
|
|
29637
|
-
async function
|
|
29638
|
-
const
|
|
29639
|
-
|
|
29640
|
-
|
|
29641
|
-
|
|
29642
|
-
|
|
29643
|
-
|
|
29644
|
-
|
|
29645
|
-
|
|
29646
|
-
|
|
29647
|
-
return {
|
|
29648
|
-
summary: `No differences between the current graph and ${input.againstSnapshot} (base exportedAt=${baseLabel}).`
|
|
29649
|
-
};
|
|
29650
|
-
}
|
|
29651
|
-
const blockLines = [
|
|
29652
|
-
` base exportedAt: ${baseLabel}`,
|
|
29653
|
-
` current exportedAt: ${result.current.exportedAt}`,
|
|
29654
|
-
""
|
|
29655
|
-
];
|
|
29656
|
-
if (result.added.nodes.length || result.added.edges.length) {
|
|
29657
|
-
blockLines.push("Added:");
|
|
29658
|
-
for (const n of result.added.nodes) blockLines.push(` + node ${n.id} (${n.type})`);
|
|
29659
|
-
for (const e of result.added.edges)
|
|
29660
|
-
blockLines.push(` + edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
|
|
29661
|
-
blockLines.push("");
|
|
29662
|
-
}
|
|
29663
|
-
if (result.removed.nodes.length || result.removed.edges.length) {
|
|
29664
|
-
blockLines.push("Removed:");
|
|
29665
|
-
for (const n of result.removed.nodes) blockLines.push(` - node ${n.id} (${n.type})`);
|
|
29666
|
-
for (const e of result.removed.edges)
|
|
29667
|
-
blockLines.push(` - edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
|
|
29668
|
-
blockLines.push("");
|
|
29481
|
+
async function writeConfigAtomic(config, home) {
|
|
29482
|
+
const file = profilesConfigPath(home);
|
|
29483
|
+
await import_node_fs53.promises.mkdir(import_node_path89.default.dirname(file), { recursive: true });
|
|
29484
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
29485
|
+
const fd = await import_node_fs53.promises.open(tmp, "w", 384);
|
|
29486
|
+
try {
|
|
29487
|
+
await fd.writeFile(serialize(config), "utf8");
|
|
29488
|
+
await fd.sync();
|
|
29489
|
+
} finally {
|
|
29490
|
+
await fd.close();
|
|
29669
29491
|
}
|
|
29670
|
-
|
|
29671
|
-
|
|
29672
|
-
|
|
29673
|
-
|
|
29674
|
-
|
|
29675
|
-
|
|
29676
|
-
|
|
29677
|
-
|
|
29492
|
+
await import_node_fs53.promises.rename(tmp, file);
|
|
29493
|
+
}
|
|
29494
|
+
var LOCK_RETRY_MS2 = 50;
|
|
29495
|
+
var LOCK_TIMEOUT_MS2 = 5e3;
|
|
29496
|
+
async function acquireLock2(lockPath) {
|
|
29497
|
+
await import_node_fs53.promises.mkdir(import_node_path89.default.dirname(lockPath), { recursive: true });
|
|
29498
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS2;
|
|
29499
|
+
for (; ; ) {
|
|
29500
|
+
try {
|
|
29501
|
+
const fd = await import_node_fs53.promises.open(lockPath, "wx");
|
|
29502
|
+
await fd.writeFile(`${process.pid}
|
|
29503
|
+
`, "utf8");
|
|
29504
|
+
await fd.close();
|
|
29505
|
+
return;
|
|
29506
|
+
} catch (err) {
|
|
29507
|
+
if (err.code !== "EEXIST") throw err;
|
|
29508
|
+
if (Date.now() >= deadline) {
|
|
29509
|
+
throw new Error(
|
|
29510
|
+
`timed out acquiring ${lockPath} after ${LOCK_TIMEOUT_MS2}ms \u2014 if no other neat process is running, remove the stale lock file`
|
|
29511
|
+
);
|
|
29512
|
+
}
|
|
29513
|
+
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS2));
|
|
29678
29514
|
}
|
|
29679
29515
|
}
|
|
29680
|
-
return {
|
|
29681
|
-
summary: `Diff against ${input.againstSnapshot}: ${total} change${total === 1 ? "" : "s"} between the snapshot and the live graph.`,
|
|
29682
|
-
block: blockLines.join("\n").trimEnd()
|
|
29683
|
-
};
|
|
29684
29516
|
}
|
|
29685
|
-
function
|
|
29686
|
-
|
|
29687
|
-
|
|
29688
|
-
|
|
29689
|
-
|
|
29517
|
+
async function releaseLock2(lockPath) {
|
|
29518
|
+
await import_node_fs53.promises.rm(lockPath, { force: true });
|
|
29519
|
+
}
|
|
29520
|
+
async function withProfilesLock(home, fn) {
|
|
29521
|
+
const lockPath = profilesConfigLockPath(home);
|
|
29522
|
+
await acquireLock2(lockPath);
|
|
29523
|
+
try {
|
|
29524
|
+
return await fn();
|
|
29525
|
+
} finally {
|
|
29526
|
+
await releaseLock2(lockPath);
|
|
29690
29527
|
}
|
|
29691
|
-
return changed.length === 0 ? "attributes differ" : `fields changed: ${changed.sort().join(", ")}`;
|
|
29692
29528
|
}
|
|
29693
|
-
async function
|
|
29694
|
-
const
|
|
29695
|
-
|
|
29696
|
-
|
|
29697
|
-
|
|
29698
|
-
|
|
29699
|
-
|
|
29700
|
-
|
|
29701
|
-
|
|
29702
|
-
|
|
29529
|
+
async function upsertProfile(profile, opts = {}) {
|
|
29530
|
+
const home = opts.home ?? neatHome3();
|
|
29531
|
+
const validated = validateEntry2(profile, 0, profilesConfigPath(home));
|
|
29532
|
+
await withProfilesLock(home, async () => {
|
|
29533
|
+
const config = await readProfilesConfig(home);
|
|
29534
|
+
const others = config.profiles.filter((p) => p.name !== validated.name);
|
|
29535
|
+
const profiles = [...others, validated];
|
|
29536
|
+
const makeActive = opts.makeActive ?? config.profiles.length === 0;
|
|
29537
|
+
const active = makeActive ? validated.name : config.active;
|
|
29538
|
+
await writeConfigAtomic({ version: config.version, ...active ? { active } : {}, profiles }, home);
|
|
29539
|
+
});
|
|
29540
|
+
}
|
|
29541
|
+
async function removeProfile(name, home = neatHome3()) {
|
|
29542
|
+
return withProfilesLock(home, async () => {
|
|
29543
|
+
const config = await readProfilesConfig(home);
|
|
29544
|
+
const profiles = config.profiles.filter((p) => p.name !== name);
|
|
29545
|
+
if (profiles.length === config.profiles.length) return false;
|
|
29546
|
+
const active = config.active === name ? void 0 : config.active;
|
|
29547
|
+
await writeConfigAtomic({ version: config.version, ...active ? { active } : {}, profiles }, home);
|
|
29548
|
+
return true;
|
|
29549
|
+
});
|
|
29550
|
+
}
|
|
29551
|
+
async function clearActiveProfile(home = neatHome3()) {
|
|
29552
|
+
return withProfilesLock(home, async () => {
|
|
29553
|
+
const config = await readProfilesConfig(home);
|
|
29554
|
+
if (!config.active) return false;
|
|
29555
|
+
await writeConfigAtomic({ version: config.version, profiles: config.profiles }, home);
|
|
29556
|
+
return true;
|
|
29557
|
+
});
|
|
29558
|
+
}
|
|
29559
|
+
|
|
29560
|
+
// src/login-sso.ts
|
|
29561
|
+
var DEFAULT_CP_URL = "https://neat-control-plane-bg5yqctn2q-nw.a.run.app";
|
|
29562
|
+
var DEFAULT_WEB_URL = "https://app.neat.is";
|
|
29563
|
+
var CALLBACK_TIMEOUT_MS = 5 * 6e4;
|
|
29564
|
+
function resolveCpUrl(env = process.env, override) {
|
|
29565
|
+
const v = override ?? env.NEAT_CP_URL;
|
|
29566
|
+
return (v && v.length > 0 ? v : DEFAULT_CP_URL).replace(/\/+$/, "");
|
|
29567
|
+
}
|
|
29568
|
+
function resolveWebUrl(env = process.env, override) {
|
|
29569
|
+
const v = override ?? env.NEAT_WEB_URL;
|
|
29570
|
+
return (v && v.length > 0 ? v : DEFAULT_WEB_URL).replace(/\/+$/, "");
|
|
29571
|
+
}
|
|
29572
|
+
function pickProject(projects, want) {
|
|
29573
|
+
if (want) {
|
|
29574
|
+
const p = projects.find((x) => x.id === want || x.name === want);
|
|
29575
|
+
if (!p) return { error: { code: 1, message: `no project named or id'd "${want}" on this account` } };
|
|
29576
|
+
return { project: p };
|
|
29577
|
+
}
|
|
29578
|
+
const running = projects.filter((p) => p.status === "running");
|
|
29579
|
+
if (running.length === 1) return { project: running[0] };
|
|
29580
|
+
if (running.length === 0) {
|
|
29581
|
+
const listed = projects.length ? ` (have: ${projects.map((p) => `${p.name} [${p.status}]`).join(", ")})` : "";
|
|
29703
29582
|
return {
|
|
29704
|
-
|
|
29583
|
+
error: {
|
|
29584
|
+
code: 1,
|
|
29585
|
+
message: `no running project to connect to \u2014 create + provision one in the console first${listed}`
|
|
29586
|
+
}
|
|
29705
29587
|
};
|
|
29706
29588
|
}
|
|
29707
|
-
const blockLines = events.map(
|
|
29708
|
-
(e) => ` ${e.transitionedAt} \u2014 ${e.source} -[${e.edgeType}]-> ${e.target} (last seen ${e.lastObserved}, threshold ${formatDuration(e.thresholdMs)})`
|
|
29709
|
-
);
|
|
29710
29589
|
return {
|
|
29711
|
-
|
|
29712
|
-
|
|
29713
|
-
|
|
29590
|
+
error: {
|
|
29591
|
+
code: 2,
|
|
29592
|
+
message: `several running projects \u2014 pass --project <name>: ${running.map((p) => p.name).join(", ")}`
|
|
29593
|
+
}
|
|
29714
29594
|
};
|
|
29715
29595
|
}
|
|
29716
|
-
async function
|
|
29717
|
-
|
|
29718
|
-
|
|
29719
|
-
let
|
|
29720
|
-
|
|
29721
|
-
|
|
29722
|
-
|
|
29723
|
-
}
|
|
29724
|
-
const body = await client.post(
|
|
29725
|
-
projectPath(input.project, "/policies/check"),
|
|
29726
|
-
{ hypotheticalAction: input.hypotheticalAction }
|
|
29727
|
-
);
|
|
29728
|
-
violations = body.violations;
|
|
29729
|
-
allowed = body.allowed;
|
|
29730
|
-
hypothetical = body.hypotheticalAction;
|
|
29731
|
-
} else {
|
|
29732
|
-
const params = new URLSearchParams();
|
|
29733
|
-
if (input.policyId) params.set("policyId", input.policyId);
|
|
29734
|
-
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
29735
|
-
const body = await client.get(
|
|
29736
|
-
projectPath(input.project, `/policies/violations${qs}`)
|
|
29737
|
-
);
|
|
29738
|
-
violations = body.violations;
|
|
29739
|
-
allowed = violations.every((v) => v.onViolation !== "block");
|
|
29596
|
+
async function exchangeCredential(cpUrl, accessToken, opts, deps) {
|
|
29597
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
29598
|
+
const auth = { authorization: `Bearer ${accessToken}` };
|
|
29599
|
+
let meRes;
|
|
29600
|
+
try {
|
|
29601
|
+
meRes = await fetchImpl(`${cpUrl}/me`, { headers: auth });
|
|
29602
|
+
} catch (e) {
|
|
29603
|
+
return { error: { code: 3, message: `can't reach the control plane at ${cpUrl} \u2014 ${e.message}` } };
|
|
29740
29604
|
}
|
|
29741
|
-
if (
|
|
29742
|
-
|
|
29743
|
-
|
|
29744
|
-
|
|
29605
|
+
if (meRes.status === 401) return { error: { code: 1, message: "your session is expired or invalid \u2014 log in again" } };
|
|
29606
|
+
if (!meRes.ok) return { error: { code: 1, message: `the control plane returned HTTP ${meRes.status} on /me` } };
|
|
29607
|
+
const me = await meRes.json().catch(() => ({}));
|
|
29608
|
+
const projects = Array.isArray(me.projects) ? me.projects : [];
|
|
29609
|
+
const picked = pickProject(projects, opts.project);
|
|
29610
|
+
if ("error" in picked) return picked;
|
|
29611
|
+
const project = picked.project;
|
|
29612
|
+
let credRes;
|
|
29613
|
+
try {
|
|
29614
|
+
credRes = await fetchImpl(`${cpUrl}/me/projects/${encodeURIComponent(project.id)}/cli-credential`, {
|
|
29615
|
+
headers: auth
|
|
29616
|
+
});
|
|
29617
|
+
} catch (e) {
|
|
29618
|
+
return { error: { code: 3, message: `can't reach the control plane \u2014 ${e.message}` } };
|
|
29745
29619
|
}
|
|
29746
|
-
if (
|
|
29620
|
+
if (credRes.status === 409) {
|
|
29747
29621
|
return {
|
|
29748
|
-
|
|
29622
|
+
error: {
|
|
29623
|
+
code: 1,
|
|
29624
|
+
message: `project "${project.name}" isn't provisioned yet (status: ${project.status}) \u2014 provision it first`
|
|
29625
|
+
}
|
|
29749
29626
|
};
|
|
29750
29627
|
}
|
|
29751
|
-
|
|
29752
|
-
|
|
29753
|
-
if (
|
|
29754
|
-
|
|
29755
|
-
|
|
29756
|
-
|
|
29757
|
-
} else {
|
|
29758
|
-
summaryParts.push(
|
|
29759
|
-
`${violations.length} policy violation${violations.length === 1 ? "" : "s"} currently recorded`
|
|
29760
|
-
);
|
|
29628
|
+
if (credRes.status === 404) return { error: { code: 1, message: `project "${project.name}" was not found, or isn't yours` } };
|
|
29629
|
+
if (credRes.status === 401) return { error: { code: 1, message: "your session is expired or invalid \u2014 log in again" } };
|
|
29630
|
+
if (!credRes.ok) return { error: { code: 1, message: `the control plane returned HTTP ${credRes.status} for the credential` } };
|
|
29631
|
+
const cred = await credRes.json().catch(() => ({}));
|
|
29632
|
+
if (!cred.endpoint || !cred.authToken) {
|
|
29633
|
+
return { error: { code: 1, message: "the credential response was missing endpoint/authToken" } };
|
|
29761
29634
|
}
|
|
29762
|
-
|
|
29763
|
-
if (!allowed && hypothetical) summaryParts.push("action denied");
|
|
29764
|
-
const summary = summaryParts.join("; ") + ".";
|
|
29765
|
-
const blockLines = violations.map((v) => {
|
|
29766
|
-
const subject = v.subject.nodeId ?? v.subject.edgeId ?? v.subject.path?.[0] ?? "(global)";
|
|
29767
|
-
return ` \u2022 [${v.severity}/${v.onViolation}] ${v.policyName}: ${v.message} \u2014 ${subject}`;
|
|
29768
|
-
});
|
|
29769
|
-
const severities = [...new Set(violations.map((v) => v.severity))];
|
|
29770
|
-
return {
|
|
29771
|
-
summary,
|
|
29772
|
-
block: blockLines.join("\n"),
|
|
29773
|
-
confidence: hypothetical ? 0.7 : 1,
|
|
29774
|
-
provenance: severities.join(" ")
|
|
29775
|
-
};
|
|
29635
|
+
return { project, cred };
|
|
29776
29636
|
}
|
|
29777
|
-
function
|
|
29778
|
-
|
|
29779
|
-
|
|
29780
|
-
|
|
29781
|
-
|
|
29782
|
-
|
|
29637
|
+
function defaultOpenBrowser(url) {
|
|
29638
|
+
const platform = process.platform;
|
|
29639
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
29640
|
+
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
29641
|
+
try {
|
|
29642
|
+
const child = (0, import_node_child_process4.spawn)(cmd, args, { detached: true, stdio: "ignore" });
|
|
29643
|
+
child.on("error", () => {
|
|
29644
|
+
});
|
|
29645
|
+
child.unref();
|
|
29646
|
+
return true;
|
|
29647
|
+
} catch {
|
|
29648
|
+
return false;
|
|
29649
|
+
}
|
|
29650
|
+
}
|
|
29651
|
+
async function loopbackReceiveToken(webUrl, deps, opts = {}) {
|
|
29652
|
+
const out = deps.out ?? (() => {
|
|
29653
|
+
});
|
|
29654
|
+
const openFn = deps.openBrowser ?? defaultOpenBrowser;
|
|
29655
|
+
const state = (0, import_node_crypto6.randomBytes)(16).toString("hex");
|
|
29656
|
+
return new Promise((resolve) => {
|
|
29657
|
+
let settled = false;
|
|
29658
|
+
let timer;
|
|
29659
|
+
const done = (r) => {
|
|
29660
|
+
if (settled) return;
|
|
29661
|
+
settled = true;
|
|
29662
|
+
clearTimeout(timer);
|
|
29663
|
+
try {
|
|
29664
|
+
server.close();
|
|
29665
|
+
} catch {
|
|
29783
29666
|
}
|
|
29784
|
-
|
|
29785
|
-
|
|
29786
|
-
|
|
29787
|
-
|
|
29788
|
-
|
|
29789
|
-
|
|
29790
|
-
|
|
29791
|
-
|
|
29792
|
-
const at = d.location ? ` at ${d.location}` : "";
|
|
29793
|
-
const member = d.symbol ? ` ${d.symbol}` : "";
|
|
29794
|
-
return ` \u2022 [${d.type}] ${d.source}${member}${at} (${d.mismatchKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
29795
|
-
}
|
|
29796
|
-
case "observed-failing": {
|
|
29797
|
-
if (d.edgeType) {
|
|
29798
|
-
const rate = d.errorRate !== void 0 ? ` ${Math.round(d.errorRate * 100)}% errors` : "";
|
|
29799
|
-
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} (${d.edgeType}) \u2014${rate} (${d.failureKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
29667
|
+
resolve(r);
|
|
29668
|
+
};
|
|
29669
|
+
const server = (0, import_node_http2.createServer)((req, res) => {
|
|
29670
|
+
const reqUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
29671
|
+
if (reqUrl.pathname !== "/callback") {
|
|
29672
|
+
res.writeHead(404);
|
|
29673
|
+
res.end();
|
|
29674
|
+
return;
|
|
29800
29675
|
}
|
|
29801
|
-
const
|
|
29802
|
-
|
|
29803
|
-
|
|
29804
|
-
|
|
29805
|
-
|
|
29806
|
-
|
|
29676
|
+
const token = reqUrl.searchParams.get("token");
|
|
29677
|
+
const gotState = reqUrl.searchParams.get("state");
|
|
29678
|
+
if (!token || gotState !== state) {
|
|
29679
|
+
res.writeHead(400, { "content-type": "text/html" });
|
|
29680
|
+
res.end("<h1>NEAT login failed</h1><p>Invalid or mismatched token. You can close this tab.</p>");
|
|
29681
|
+
done({ error: { code: 1, message: "the browser returned an invalid or mismatched token" } });
|
|
29682
|
+
return;
|
|
29807
29683
|
}
|
|
29808
|
-
|
|
29809
|
-
|
|
29810
|
-
|
|
29684
|
+
res.writeHead(200, { "content-type": "text/html" });
|
|
29685
|
+
res.end("<h1>You're logged in to NEAT.</h1><p>You can close this tab and return to the terminal.</p>");
|
|
29686
|
+
done({ token });
|
|
29687
|
+
});
|
|
29688
|
+
server.on("error", (e) => done({ error: { code: 3, message: `couldn't start the local login listener \u2014 ${e.message}` } }));
|
|
29689
|
+
timer = setTimeout(
|
|
29690
|
+
() => done({ error: { code: 1, message: "timed out waiting for the browser login" } }),
|
|
29691
|
+
opts.timeoutMs ?? CALLBACK_TIMEOUT_MS
|
|
29692
|
+
);
|
|
29693
|
+
server.listen(0, "127.0.0.1", () => {
|
|
29694
|
+
const addr = server.address();
|
|
29695
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
29696
|
+
const callback = `http://127.0.0.1:${port}/callback`;
|
|
29697
|
+
const authUrl = `${webUrl}/cli/auth?callback=${encodeURIComponent(callback)}&state=${state}`;
|
|
29698
|
+
out("Opening your browser to log in to NEAT\u2026");
|
|
29699
|
+
out(`If it doesn't open, visit:
|
|
29700
|
+
${authUrl}`);
|
|
29701
|
+
openFn(authUrl);
|
|
29702
|
+
});
|
|
29703
|
+
});
|
|
29811
29704
|
}
|
|
29812
|
-
async function
|
|
29813
|
-
const
|
|
29814
|
-
|
|
29815
|
-
|
|
29816
|
-
|
|
29817
|
-
|
|
29818
|
-
|
|
29819
|
-
|
|
29820
|
-
|
|
29821
|
-
|
|
29822
|
-
|
|
29823
|
-
if (result.totalAffected === 0) {
|
|
29824
|
-
return {
|
|
29825
|
-
summary: "No divergences found between the declared (EXTRACTED) and observed (OBSERVED) views of the graph."
|
|
29826
|
-
};
|
|
29705
|
+
async function runSsoLogin(opts, deps) {
|
|
29706
|
+
const out = deps.out ?? ((l) => console.log(l));
|
|
29707
|
+
const err = deps.err ?? ((l) => console.error(l));
|
|
29708
|
+
let accessToken = opts.ssoToken;
|
|
29709
|
+
if (!accessToken) {
|
|
29710
|
+
const lb = await loopbackReceiveToken(opts.webUrl, deps, { timeoutMs: opts.timeoutMs ?? CALLBACK_TIMEOUT_MS });
|
|
29711
|
+
if ("error" in lb) {
|
|
29712
|
+
err(`neat login: ${lb.error.message}`);
|
|
29713
|
+
return lb.error.code;
|
|
29714
|
+
}
|
|
29715
|
+
accessToken = lb.token;
|
|
29827
29716
|
}
|
|
29828
|
-
const
|
|
29829
|
-
|
|
29830
|
-
|
|
29831
|
-
|
|
29832
|
-
blockLines.push(formatDivergenceLine(d));
|
|
29833
|
-
blockLines.push(` reason: ${d.reason}`);
|
|
29834
|
-
blockLines.push(` recommendation: ${d.recommendation}`);
|
|
29717
|
+
const ex = await exchangeCredential(opts.cpUrl, accessToken, { project: opts.project }, deps);
|
|
29718
|
+
if ("error" in ex) {
|
|
29719
|
+
err(`neat login: ${ex.error.message}`);
|
|
29720
|
+
return ex.error.code;
|
|
29835
29721
|
}
|
|
29836
|
-
const
|
|
29837
|
-
|
|
29838
|
-
|
|
29839
|
-
|
|
29840
|
-
return {
|
|
29841
|
-
summary,
|
|
29842
|
-
block: blockLines.join("\n"),
|
|
29843
|
-
confidence: maxConfidence,
|
|
29844
|
-
provenance: "composite (EXTRACTED + OBSERVED)"
|
|
29845
|
-
};
|
|
29846
|
-
}
|
|
29847
|
-
async function runAsk(client, input) {
|
|
29848
|
-
const result = await client.get(
|
|
29849
|
-
projectPath(input.project, `/graph/ask?q=${encodeURIComponent(input.question)}`)
|
|
29722
|
+
const { project, cred } = ex;
|
|
29723
|
+
await upsertProfile(
|
|
29724
|
+
{ name: opts.name, endpoint: cred.endpoint, authToken: cred.authToken },
|
|
29725
|
+
{ makeActive: true, ...deps.home ? { home: deps.home } : {} }
|
|
29850
29726
|
);
|
|
29851
|
-
|
|
29852
|
-
|
|
29853
|
-
|
|
29854
|
-
|
|
29727
|
+
if (opts.json) {
|
|
29728
|
+
out(
|
|
29729
|
+
JSON.stringify(
|
|
29730
|
+
{
|
|
29731
|
+
status: "logged-in",
|
|
29732
|
+
profile: opts.name,
|
|
29733
|
+
project: project.name,
|
|
29734
|
+
endpoint: cred.endpoint,
|
|
29735
|
+
...cred.ingestEndpoint ? { ingestEndpoint: cred.ingestEndpoint } : {}
|
|
29736
|
+
},
|
|
29737
|
+
null,
|
|
29738
|
+
2
|
|
29739
|
+
)
|
|
29855
29740
|
);
|
|
29856
|
-
|
|
29857
|
-
|
|
29858
|
-
|
|
29859
|
-
|
|
29860
|
-
|
|
29861
|
-
|
|
29862
|
-
|
|
29863
|
-
|
|
29864
|
-
blockLines.push(` \u2022 ${fact.text}${tag}`);
|
|
29741
|
+
} else {
|
|
29742
|
+
out(`Logged in \u2014 profile "${opts.name}" \u2192 ${project.name} (${cred.endpoint})`);
|
|
29743
|
+
out("The neat CLI and the MCP server now read this hosted graph by default.");
|
|
29744
|
+
if (cred.ingestEndpoint && cred.otelToken) {
|
|
29745
|
+
out("");
|
|
29746
|
+
out("To fill the OBSERVED layer, instrument your app to send OpenTelemetry to the hosted daemon:");
|
|
29747
|
+
out(` OTEL_EXPORTER_OTLP_ENDPOINT=${cred.ingestEndpoint}`);
|
|
29748
|
+
out(` OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${cred.otelToken}`);
|
|
29865
29749
|
}
|
|
29750
|
+
out("Run `neat logout` to switch back to your local daemon.");
|
|
29866
29751
|
}
|
|
29867
|
-
return
|
|
29868
|
-
summary: result.answer,
|
|
29869
|
-
block: blockLines.join("\n").trim(),
|
|
29870
|
-
...result.confidence !== void 0 ? { confidence: result.confidence } : {},
|
|
29871
|
-
...result.provenance.length > 0 ? { provenance: result.provenance } : {}
|
|
29872
|
-
};
|
|
29873
|
-
}
|
|
29874
|
-
function formatFooter(confidence, provenance) {
|
|
29875
|
-
const c = confidence === void 0 ? "n/a" : confidence.toFixed(2);
|
|
29876
|
-
const p = provenance === void 0 ? "n/a" : Array.isArray(provenance) ? [...new Set(provenance)].join(", ") : provenance;
|
|
29877
|
-
return `confidence: ${c} \xB7 provenance: ${p}`;
|
|
29878
|
-
}
|
|
29879
|
-
function formatHuman(result) {
|
|
29880
|
-
const sections = [result.summary.trim()];
|
|
29881
|
-
if (result.block && result.block.trim().length > 0) sections.push(result.block.trimEnd());
|
|
29882
|
-
sections.push(formatFooter(result.confidence, result.provenance));
|
|
29883
|
-
return sections.join("\n\n");
|
|
29884
|
-
}
|
|
29885
|
-
function formatJson(result) {
|
|
29886
|
-
return JSON.stringify(
|
|
29887
|
-
{
|
|
29888
|
-
summary: result.summary,
|
|
29889
|
-
block: result.block ?? "",
|
|
29890
|
-
confidence: result.confidence ?? null,
|
|
29891
|
-
provenance: result.provenance ?? null
|
|
29892
|
-
},
|
|
29893
|
-
null,
|
|
29894
|
-
2
|
|
29895
|
-
);
|
|
29752
|
+
return 0;
|
|
29896
29753
|
}
|
|
29897
|
-
|
|
29898
|
-
|
|
29899
|
-
|
|
29900
|
-
|
|
29754
|
+
|
|
29755
|
+
// src/hosted-connect-cli.ts
|
|
29756
|
+
function defaultOpenBrowser2(url) {
|
|
29757
|
+
const platform = process.platform;
|
|
29758
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
29759
|
+
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
29760
|
+
try {
|
|
29761
|
+
const child = (0, import_node_child_process5.spawn)(cmd, args, { detached: true, stdio: "ignore" });
|
|
29762
|
+
child.on("error", () => {
|
|
29763
|
+
});
|
|
29764
|
+
child.unref();
|
|
29765
|
+
return true;
|
|
29766
|
+
} catch {
|
|
29767
|
+
return false;
|
|
29768
|
+
}
|
|
29901
29769
|
}
|
|
29902
|
-
function
|
|
29903
|
-
|
|
29770
|
+
function resolveCreds(env) {
|
|
29771
|
+
const apiKey = env.NEAT_API_KEY;
|
|
29772
|
+
const projectId = env.NEAT_CP_PROJECT_ID;
|
|
29773
|
+
if (!apiKey) return { error: "not logged in \u2014 run `neat login`, or set NEAT_API_KEY (a neat_pat_\u2026 key)" };
|
|
29774
|
+
if (!projectId) return { error: "no hosted project \u2014 run `neat login`, or set NEAT_CP_PROJECT_ID" };
|
|
29775
|
+
return { cpUrl: resolveCpUrl(env).replace(/\/+$/, ""), apiKey, projectId };
|
|
29904
29776
|
}
|
|
29905
|
-
async function
|
|
29906
|
-
const
|
|
29907
|
-
|
|
29908
|
-
|
|
29777
|
+
async function runConnectCommand(rawArgs, deps = {}) {
|
|
29778
|
+
const env = deps.env ?? process.env;
|
|
29779
|
+
const out = deps.out ?? ((l) => console.log(l));
|
|
29780
|
+
const err = deps.err ?? ((l) => console.error(l));
|
|
29781
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
29782
|
+
const open = deps.openBrowser ?? defaultOpenBrowser2;
|
|
29783
|
+
const pollMs = deps.pollMs ?? 2e3;
|
|
29784
|
+
const timeoutMs = deps.timeoutMs ?? 18e4;
|
|
29785
|
+
const sleep2 = deps.sleepImpl ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
29786
|
+
const provider = rawArgs.find((a) => !a.startsWith("-"));
|
|
29787
|
+
if (!provider || rawArgs.includes("-h") || rawArgs.includes("--help")) {
|
|
29788
|
+
out("usage: neat connect <provider> connect a provider to your hosted NEAT project over OAuth");
|
|
29789
|
+
out(" Opens the provider's consent screen in your browser and waits for the connection to land.");
|
|
29790
|
+
out(" Needs a hosted login (`neat login`) or NEAT_CP_URL + NEAT_API_KEY + NEAT_CP_PROJECT_ID.");
|
|
29791
|
+
out(" For a local/self-hosted daemon, use `neat connector add` instead.");
|
|
29792
|
+
return provider ? 0 : 2;
|
|
29793
|
+
}
|
|
29794
|
+
const creds = resolveCreds(env);
|
|
29795
|
+
if ("error" in creds) {
|
|
29796
|
+
err(`neat connect: ${creds.error}`);
|
|
29797
|
+
return 2;
|
|
29909
29798
|
}
|
|
29910
|
-
|
|
29911
|
-
|
|
29912
|
-
|
|
29799
|
+
const connectionsUrl = `${creds.cpUrl}/me/projects/${encodeURIComponent(creds.projectId)}/connections`;
|
|
29800
|
+
let authorizeUrl;
|
|
29801
|
+
try {
|
|
29802
|
+
const res = await fetchImpl(`${connectionsUrl}/${encodeURIComponent(provider)}/authorize`, {
|
|
29803
|
+
method: "POST",
|
|
29804
|
+
headers: { authorization: `Bearer ${creds.apiKey}` }
|
|
29805
|
+
});
|
|
29806
|
+
if (res.status === 401) {
|
|
29807
|
+
err("neat connect: not authorized \u2014 run `neat login` (or check NEAT_API_KEY).");
|
|
29808
|
+
return 1;
|
|
29809
|
+
}
|
|
29810
|
+
if (res.status === 404 || res.status === 501) {
|
|
29811
|
+
err(`neat connect: ${provider} isn't available to connect over OAuth yet.`);
|
|
29812
|
+
return 1;
|
|
29813
|
+
}
|
|
29814
|
+
if (!res.ok) {
|
|
29815
|
+
err(`neat connect: couldn't start connecting ${provider} (HTTP ${res.status}).`);
|
|
29816
|
+
return 1;
|
|
29817
|
+
}
|
|
29818
|
+
const body = await res.json().catch(() => ({}));
|
|
29819
|
+
if (!body.authorizeUrl) {
|
|
29820
|
+
err(`neat connect: the control plane returned no authorize URL for ${provider}.`);
|
|
29821
|
+
return 1;
|
|
29822
|
+
}
|
|
29823
|
+
authorizeUrl = body.authorizeUrl;
|
|
29824
|
+
} catch (e) {
|
|
29825
|
+
err(`neat connect: couldn't reach the control plane at ${creds.cpUrl} \u2014 ${e.message}`);
|
|
29826
|
+
return 1;
|
|
29827
|
+
}
|
|
29828
|
+
out(`Opening ${provider}'s consent screen in your browser\u2026`);
|
|
29829
|
+
if (!open(authorizeUrl)) out("Could not open a browser automatically \u2014 open this URL to authorize:");
|
|
29830
|
+
out(` ${authorizeUrl}`);
|
|
29831
|
+
out("Waiting for you to authorize\u2026");
|
|
29832
|
+
const deadline = Date.now() + timeoutMs;
|
|
29833
|
+
while (Date.now() < deadline) {
|
|
29834
|
+
await sleep2(pollMs);
|
|
29835
|
+
try {
|
|
29836
|
+
const res = await fetchImpl(connectionsUrl, { headers: { authorization: `Bearer ${creds.apiKey}` } });
|
|
29837
|
+
if (res.ok) {
|
|
29838
|
+
const conns = await res.json().catch(() => []);
|
|
29839
|
+
const hit = Array.isArray(conns) ? conns.find((c) => c.provider === provider) : void 0;
|
|
29840
|
+
if (hit) {
|
|
29841
|
+
out(`\u2713 ${provider} connected${hit.status ? ` (${hit.status})` : ""}.`);
|
|
29842
|
+
return 0;
|
|
29843
|
+
}
|
|
29844
|
+
}
|
|
29845
|
+
} catch {
|
|
29846
|
+
}
|
|
29847
|
+
}
|
|
29848
|
+
err(
|
|
29849
|
+
`neat connect: timed out waiting for ${provider}. If you authorized, run \`neat connect ${provider}\` again to re-check \u2014 the connection may still land.`
|
|
29913
29850
|
);
|
|
29851
|
+
return 1;
|
|
29914
29852
|
}
|
|
29915
29853
|
|
|
29916
29854
|
// src/doctor-cli.ts
|
|
29917
|
-
|
|
29918
|
-
|
|
29919
|
-
|
|
29920
|
-
|
|
29921
|
-
|
|
29922
|
-
|
|
29923
|
-
|
|
29924
|
-
|
|
29925
|
-
|
|
29926
|
-
|
|
29927
|
-
|
|
29928
|
-
|
|
29929
|
-
|
|
29930
|
-
const major = Number.parseInt(nodeVersion.split(".")[0] ?? "", 10);
|
|
29931
|
-
const ok = Number.isFinite(major) && major >= NODE_FLOOR;
|
|
29932
|
-
return ok ? { name: "node", ok, detail: `v${nodeVersion} (>= ${NODE_FLOOR} required)` } : {
|
|
29933
|
-
name: "node",
|
|
29934
|
-
ok,
|
|
29935
|
-
detail: `v${nodeVersion} \u2014 NEAT needs Node ${NODE_FLOOR} or newer`,
|
|
29936
|
-
fix: `install Node ${NODE_FLOOR}.x (e.g. \`nvm install ${NODE_FLOOR}\`) and re-run`
|
|
29937
|
-
};
|
|
29938
|
-
}
|
|
29939
|
-
async function neatOutExists(cwd) {
|
|
29940
|
-
try {
|
|
29941
|
-
const st = await import_node_fs53.promises.stat(import_node_path89.default.join(cwd, "neat-out"));
|
|
29942
|
-
return st.isDirectory();
|
|
29943
|
-
} catch {
|
|
29944
|
-
return false;
|
|
29945
|
-
}
|
|
29946
|
-
}
|
|
29947
|
-
async function checkProject(cwd, record) {
|
|
29948
|
-
if (record) {
|
|
29949
|
-
return {
|
|
29950
|
-
name: "project",
|
|
29951
|
-
ok: true,
|
|
29952
|
-
detail: `"${record.project}" \u2014 set up in this directory (daemon record on REST ${record.ports.rest})`
|
|
29953
|
-
};
|
|
29855
|
+
init_cjs_shims();
|
|
29856
|
+
var import_node_fs54 = require("fs");
|
|
29857
|
+
var import_node_path90 = __toESM(require("path"), 1);
|
|
29858
|
+
|
|
29859
|
+
// src/cli-client.ts
|
|
29860
|
+
init_cjs_shims();
|
|
29861
|
+
var import_types108 = require("@neat.is/types");
|
|
29862
|
+
var HttpError = class extends Error {
|
|
29863
|
+
constructor(status2, message, responseBody = "") {
|
|
29864
|
+
super(message);
|
|
29865
|
+
this.status = status2;
|
|
29866
|
+
this.responseBody = responseBody;
|
|
29867
|
+
this.name = "HttpError";
|
|
29954
29868
|
}
|
|
29955
|
-
|
|
29956
|
-
|
|
29957
|
-
|
|
29958
|
-
|
|
29959
|
-
|
|
29960
|
-
|
|
29869
|
+
status;
|
|
29870
|
+
responseBody;
|
|
29871
|
+
};
|
|
29872
|
+
var TransportError = class extends Error {
|
|
29873
|
+
constructor(message) {
|
|
29874
|
+
super(message);
|
|
29875
|
+
this.name = "TransportError";
|
|
29961
29876
|
}
|
|
29877
|
+
};
|
|
29878
|
+
function resolveAuthToken(env = process.env) {
|
|
29879
|
+
const t = env.NEAT_AUTH_TOKEN;
|
|
29880
|
+
return t && t.length > 0 ? t : void 0;
|
|
29881
|
+
}
|
|
29882
|
+
function createHttpClient(baseUrl, bearerToken, fetchImpl = fetch) {
|
|
29883
|
+
const root = baseUrl.replace(/\/$/, "");
|
|
29884
|
+
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
29962
29885
|
return {
|
|
29963
|
-
|
|
29964
|
-
|
|
29965
|
-
|
|
29966
|
-
|
|
29886
|
+
async get(path97) {
|
|
29887
|
+
let res;
|
|
29888
|
+
try {
|
|
29889
|
+
res = await fetchImpl(`${root}${path97}`, {
|
|
29890
|
+
headers: { ...authHeader }
|
|
29891
|
+
});
|
|
29892
|
+
} catch (err) {
|
|
29893
|
+
throw new TransportError(
|
|
29894
|
+
`cannot reach neat-core at ${root}: ${err.message}`
|
|
29895
|
+
);
|
|
29896
|
+
}
|
|
29897
|
+
if (!res.ok) {
|
|
29898
|
+
const body = await res.text().catch(() => "");
|
|
29899
|
+
throw new HttpError(
|
|
29900
|
+
res.status,
|
|
29901
|
+
`${res.status} ${res.statusText} on GET ${path97}: ${body}`,
|
|
29902
|
+
body
|
|
29903
|
+
);
|
|
29904
|
+
}
|
|
29905
|
+
return await res.json();
|
|
29906
|
+
},
|
|
29907
|
+
async post(path97, body) {
|
|
29908
|
+
let res;
|
|
29909
|
+
try {
|
|
29910
|
+
res = await fetchImpl(`${root}${path97}`, {
|
|
29911
|
+
method: "POST",
|
|
29912
|
+
headers: { "content-type": "application/json", ...authHeader },
|
|
29913
|
+
body: JSON.stringify(body)
|
|
29914
|
+
});
|
|
29915
|
+
} catch (err) {
|
|
29916
|
+
throw new TransportError(
|
|
29917
|
+
`cannot reach neat-core at ${root}: ${err.message}`
|
|
29918
|
+
);
|
|
29919
|
+
}
|
|
29920
|
+
if (!res.ok) {
|
|
29921
|
+
const text = await res.text().catch(() => "");
|
|
29922
|
+
throw new HttpError(
|
|
29923
|
+
res.status,
|
|
29924
|
+
`${res.status} ${res.statusText} on POST ${path97}: ${text}`,
|
|
29925
|
+
text
|
|
29926
|
+
);
|
|
29927
|
+
}
|
|
29928
|
+
return await res.json();
|
|
29929
|
+
}
|
|
29967
29930
|
};
|
|
29968
29931
|
}
|
|
29969
|
-
function
|
|
29970
|
-
|
|
29971
|
-
|
|
29972
|
-
if (record) return `http://localhost:${record.ports.rest}`;
|
|
29973
|
-
return "http://localhost:8080";
|
|
29974
|
-
}
|
|
29975
|
-
function summariseHealth(url, body) {
|
|
29976
|
-
const projects = body.projects ?? [];
|
|
29977
|
-
const nodes = projects.reduce((n, p) => n + (p.nodeCount ?? 0), 0);
|
|
29978
|
-
const edges = projects.reduce((n, p) => n + (p.edgeCount ?? 0), 0);
|
|
29979
|
-
const proj = body.project ?? projects[0]?.name;
|
|
29980
|
-
const graph = projects.length > 0 ? ` \u2014 ${nodes} nodes / ${edges} edges` : "";
|
|
29981
|
-
return `up at ${url}${proj ? ` (project "${proj}"${graph})` : ""}`;
|
|
29932
|
+
function projectPath(project, suffix) {
|
|
29933
|
+
if (!project) return suffix;
|
|
29934
|
+
return `/projects/${encodeURIComponent(project)}${suffix}`;
|
|
29982
29935
|
}
|
|
29983
|
-
async function
|
|
29984
|
-
const
|
|
29985
|
-
const
|
|
29986
|
-
|
|
29936
|
+
async function runRootCause(client, input) {
|
|
29937
|
+
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
29938
|
+
const path97 = projectPath(
|
|
29939
|
+
input.project,
|
|
29940
|
+
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
29941
|
+
);
|
|
29987
29942
|
try {
|
|
29988
|
-
const
|
|
29989
|
-
|
|
29990
|
-
|
|
29991
|
-
});
|
|
29992
|
-
|
|
29943
|
+
const result = await client.get(path97);
|
|
29944
|
+
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
29945
|
+
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
29946
|
+
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
29947
|
+
const blockLines = [
|
|
29948
|
+
`Traversal path: ${arrowPath}`,
|
|
29949
|
+
`Edge provenances: ${provenances}`
|
|
29950
|
+
];
|
|
29951
|
+
if (result.fixRecommendation) blockLines.push(`Recommended fix: ${result.fixRecommendation}`);
|
|
29952
|
+
return {
|
|
29953
|
+
summary,
|
|
29954
|
+
block: blockLines.join("\n"),
|
|
29955
|
+
confidence: result.confidence,
|
|
29956
|
+
provenance: result.edgeProvenances.length ? result.edgeProvenances : void 0
|
|
29957
|
+
};
|
|
29958
|
+
} catch (err) {
|
|
29959
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
29993
29960
|
return {
|
|
29994
|
-
|
|
29995
|
-
ok: false,
|
|
29996
|
-
detail: `up at ${url}, but rejected the request (${res.status})`,
|
|
29997
|
-
fix: "set NEAT_AUTH_TOKEN to the daemon's token"
|
|
29961
|
+
summary: `No root cause found for ${input.errorNode}. The node may be healthy, or it may not exist in the graph.`
|
|
29998
29962
|
};
|
|
29999
29963
|
}
|
|
30000
|
-
|
|
29964
|
+
throw err;
|
|
29965
|
+
}
|
|
29966
|
+
}
|
|
29967
|
+
async function runBlastRadius(client, input) {
|
|
29968
|
+
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
29969
|
+
const path97 = projectPath(
|
|
29970
|
+
input.project,
|
|
29971
|
+
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
29972
|
+
);
|
|
29973
|
+
try {
|
|
29974
|
+
const result = await client.get(path97);
|
|
29975
|
+
if (result.totalAffected === 0) {
|
|
30001
29976
|
return {
|
|
30002
|
-
|
|
30003
|
-
ok: false,
|
|
30004
|
-
detail: `reachable at ${url} but /health returned ${res.status}`,
|
|
30005
|
-
fix: "check the daemon logs"
|
|
29977
|
+
summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
|
|
30006
29978
|
};
|
|
30007
29979
|
}
|
|
30008
|
-
const
|
|
30009
|
-
|
|
30010
|
-
|
|
29980
|
+
const sorted = [...result.affectedNodes].sort(
|
|
29981
|
+
(a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
|
|
29982
|
+
);
|
|
29983
|
+
const blockLines = sorted.map(formatBlastEntry);
|
|
29984
|
+
const minConfidence = sorted.reduce(
|
|
29985
|
+
(m, n) => Math.min(m, n.confidence),
|
|
29986
|
+
Number.POSITIVE_INFINITY
|
|
29987
|
+
);
|
|
29988
|
+
const provenances = [...new Set(sorted.map((n) => n.edgeProvenance))];
|
|
30011
29989
|
return {
|
|
30012
|
-
|
|
30013
|
-
|
|
30014
|
-
|
|
30015
|
-
|
|
29990
|
+
summary: `Blast radius for ${result.origin}: ${result.totalAffected} dependent node${result.totalAffected === 1 ? "" : "s"} would break if it changed.`,
|
|
29991
|
+
block: blockLines.join("\n"),
|
|
29992
|
+
confidence: Number.isFinite(minConfidence) ? minConfidence : void 0,
|
|
29993
|
+
provenance: provenances.length ? provenances : void 0
|
|
30016
29994
|
};
|
|
29995
|
+
} catch (err) {
|
|
29996
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
29997
|
+
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
29998
|
+
}
|
|
29999
|
+
throw err;
|
|
30017
30000
|
}
|
|
30018
30001
|
}
|
|
30019
|
-
|
|
30020
|
-
const
|
|
30021
|
-
|
|
30022
|
-
return [checkNode(d.nodeVersion), await checkProject(d.cwd, record), await checkDaemon(d, record)];
|
|
30023
|
-
}
|
|
30024
|
-
var NAME_COL = "project".length;
|
|
30025
|
-
function renderHuman(checks, out) {
|
|
30026
|
-
out("neat doctor \u2014 checking this project's setup");
|
|
30027
|
-
out("");
|
|
30028
|
-
for (const c of checks) {
|
|
30029
|
-
const mark = c.ok ? "\u2713" : "\u2717";
|
|
30030
|
-
out(` ${mark} ${c.name.padEnd(NAME_COL)} ${c.detail}`);
|
|
30031
|
-
if (!c.ok && c.fix) out(` ${" ".repeat(NAME_COL + 3)}fix: ${c.fix}`);
|
|
30032
|
-
}
|
|
30033
|
-
out("");
|
|
30034
|
-
const failed = checks.filter((c) => !c.ok).length;
|
|
30035
|
-
out(failed === 0 ? "all good." : `${failed} check${failed === 1 ? "" : "s"} failed.`);
|
|
30002
|
+
function formatBlastEntry(n) {
|
|
30003
|
+
const tag = n.edgeProvenance === import_types108.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
30004
|
+
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
30036
30005
|
}
|
|
30037
|
-
async function
|
|
30038
|
-
const
|
|
30039
|
-
|
|
30040
|
-
|
|
30041
|
-
|
|
30042
|
-
|
|
30043
|
-
|
|
30044
|
-
|
|
30045
|
-
|
|
30046
|
-
return
|
|
30047
|
-
|
|
30048
|
-
|
|
30049
|
-
|
|
30006
|
+
async function runDependencies(client, input) {
|
|
30007
|
+
const depth = input.depth ?? 3;
|
|
30008
|
+
const path97 = projectPath(
|
|
30009
|
+
input.project,
|
|
30010
|
+
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
30011
|
+
);
|
|
30012
|
+
try {
|
|
30013
|
+
const result = await client.get(path97);
|
|
30014
|
+
if (result.total === 0) {
|
|
30015
|
+
return {
|
|
30016
|
+
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
30017
|
+
};
|
|
30018
|
+
}
|
|
30019
|
+
const byDistance = /* @__PURE__ */ new Map();
|
|
30020
|
+
for (const dep of result.dependencies) {
|
|
30021
|
+
const ring = byDistance.get(dep.distance) ?? [];
|
|
30022
|
+
ring.push(dep);
|
|
30023
|
+
byDistance.set(dep.distance, ring);
|
|
30024
|
+
}
|
|
30025
|
+
const blockLines = [];
|
|
30026
|
+
for (const distance of [...byDistance.keys()].sort((a, b) => a - b)) {
|
|
30027
|
+
const label = distance === 1 ? "Direct (distance 1)" : `Distance ${distance}`;
|
|
30028
|
+
blockLines.push(`${label}:`);
|
|
30029
|
+
for (const dep of byDistance.get(distance)) {
|
|
30030
|
+
blockLines.push(` \u2022 ${dep.nodeId} \u2014 ${dep.edgeType} (${dep.provenance})`);
|
|
30031
|
+
}
|
|
30032
|
+
}
|
|
30033
|
+
const provenances = [...new Set(result.dependencies.map((d) => d.provenance))];
|
|
30034
|
+
const directCount = byDistance.get(1)?.length ?? 0;
|
|
30035
|
+
const summary = depth === 1 ? `${input.nodeId} has ${directCount} direct dependenc${directCount === 1 ? "y" : "ies"}.` : `${input.nodeId} has ${result.total} dependenc${result.total === 1 ? "y" : "ies"} reachable to depth ${depth} (${directCount} direct).`;
|
|
30036
|
+
return { summary, block: blockLines.join("\n"), provenance: provenances };
|
|
30037
|
+
} catch (err) {
|
|
30038
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
30039
|
+
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
30050
30040
|
}
|
|
30041
|
+
throw err;
|
|
30051
30042
|
}
|
|
30052
|
-
const checks = await runDoctorChecks(deps);
|
|
30053
|
-
if (json) out(JSON.stringify({ ok: checks.every((c) => c.ok), checks }, null, 2));
|
|
30054
|
-
else renderHuman(checks, out);
|
|
30055
|
-
return checks.every((c) => c.ok) ? 0 : 1;
|
|
30056
|
-
}
|
|
30057
|
-
|
|
30058
|
-
// src/login-cli.ts
|
|
30059
|
-
init_cjs_shims();
|
|
30060
|
-
var import_promises2 = __toESM(require("readline/promises"), 1);
|
|
30061
|
-
|
|
30062
|
-
// src/profiles.ts
|
|
30063
|
-
init_cjs_shims();
|
|
30064
|
-
var import_node_fs54 = require("fs");
|
|
30065
|
-
var import_node_os6 = __toESM(require("os"), 1);
|
|
30066
|
-
var import_node_path90 = __toESM(require("path"), 1);
|
|
30067
|
-
var PROFILES_CONFIG_VERSION = 1;
|
|
30068
|
-
function neatHome3() {
|
|
30069
|
-
const override = process.env.NEAT_HOME;
|
|
30070
|
-
if (override && override.length > 0) return import_node_path90.default.resolve(override);
|
|
30071
|
-
return import_node_path90.default.join(import_node_os6.default.homedir(), ".neat");
|
|
30072
|
-
}
|
|
30073
|
-
function profilesConfigPath(home = neatHome3()) {
|
|
30074
|
-
return import_node_path90.default.join(home, "profiles.json");
|
|
30075
30043
|
}
|
|
30076
|
-
function
|
|
30077
|
-
|
|
30044
|
+
function observedDepLine(nodeId, e) {
|
|
30045
|
+
const via = e.source !== nodeId ? ` (via ${e.source})` : "";
|
|
30046
|
+
return ` \u2022 ${e.target} \u2014 ${e.type}${via}${edgeMeta(e)}`;
|
|
30078
30047
|
}
|
|
30079
|
-
|
|
30080
|
-
async function warnIfModeLooserThan06002(file) {
|
|
30081
|
-
if (process.platform === "win32") return;
|
|
30048
|
+
async function runObservedDependencies(client, input) {
|
|
30082
30049
|
try {
|
|
30083
|
-
const
|
|
30084
|
-
|
|
30085
|
-
|
|
30086
|
-
|
|
30087
|
-
|
|
30088
|
-
|
|
30050
|
+
const result = await client.get(
|
|
30051
|
+
projectPath(
|
|
30052
|
+
input.project,
|
|
30053
|
+
`/graph/observed-dependencies/${encodeURIComponent(input.nodeId)}`
|
|
30054
|
+
)
|
|
30055
|
+
);
|
|
30056
|
+
if (result.dependencies.length === 0) {
|
|
30057
|
+
if (result.observed) {
|
|
30058
|
+
return {
|
|
30059
|
+
summary: `${input.nodeId} makes no outbound runtime calls, but OTel has observed it receiving traffic on ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 it's a pure receiver.`,
|
|
30060
|
+
provenance: import_types108.Provenance.OBSERVED
|
|
30061
|
+
};
|
|
30062
|
+
}
|
|
30063
|
+
const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
30064
|
+
return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
|
|
30089
30065
|
}
|
|
30090
|
-
|
|
30066
|
+
const blockLines = result.dependencies.map((e) => observedDepLine(input.nodeId, e));
|
|
30067
|
+
return {
|
|
30068
|
+
summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
30069
|
+
block: blockLines.join("\n"),
|
|
30070
|
+
provenance: import_types108.Provenance.OBSERVED
|
|
30071
|
+
};
|
|
30072
|
+
} catch (err) {
|
|
30073
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
30074
|
+
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
30075
|
+
}
|
|
30076
|
+
throw err;
|
|
30077
|
+
}
|
|
30078
|
+
}
|
|
30079
|
+
function edgeMeta(e) {
|
|
30080
|
+
const bits = [];
|
|
30081
|
+
if (e.signal) {
|
|
30082
|
+
bits.push(`spans=${e.signal.spanCount}`);
|
|
30083
|
+
if (e.signal.errorCount > 0) bits.push(`errors=${e.signal.errorCount}`);
|
|
30084
|
+
if (e.signal.lastObservedAgeMs !== void 0) {
|
|
30085
|
+
bits.push(`age=${formatDuration(e.signal.lastObservedAgeMs)}`);
|
|
30086
|
+
}
|
|
30087
|
+
} else if (e.callCount !== void 0) {
|
|
30088
|
+
bits.push(`callCount=${e.callCount}`);
|
|
30091
30089
|
}
|
|
30090
|
+
if (e.lastObserved) bits.push(`lastObserved=${e.lastObserved}`);
|
|
30091
|
+
if (e.confidence !== void 0) bits.push(`confidence=${e.confidence}`);
|
|
30092
|
+
return bits.length ? ` [${bits.join(", ")}]` : "";
|
|
30093
|
+
}
|
|
30094
|
+
function formatDuration(ms) {
|
|
30095
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
30096
|
+
const s = Math.round(ms / 1e3);
|
|
30097
|
+
if (s < 60) return `${s}s`;
|
|
30098
|
+
const m = Math.round(s / 60);
|
|
30099
|
+
if (m < 60) return `${m}m`;
|
|
30100
|
+
const h = Math.round(m / 60);
|
|
30101
|
+
if (h < 48) return `${h}h`;
|
|
30102
|
+
return `${Math.round(h / 24)}d`;
|
|
30092
30103
|
}
|
|
30093
|
-
async function
|
|
30094
|
-
const
|
|
30095
|
-
let raw;
|
|
30104
|
+
async function runIncidents(client, input) {
|
|
30105
|
+
const path97 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
30096
30106
|
try {
|
|
30097
|
-
|
|
30107
|
+
const body = await client.get(path97);
|
|
30108
|
+
const events = body.events;
|
|
30109
|
+
if (events.length === 0) {
|
|
30110
|
+
return {
|
|
30111
|
+
summary: input.nodeId ? `No incidents recorded against ${input.nodeId}.` : "No incidents recorded."
|
|
30112
|
+
};
|
|
30113
|
+
}
|
|
30114
|
+
const ordered = [...events].reverse().slice(0, input.limit ?? 20);
|
|
30115
|
+
const blockLines = [];
|
|
30116
|
+
for (const ev of ordered) {
|
|
30117
|
+
blockLines.push(` ${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`);
|
|
30118
|
+
blockLines.push(` trace=${ev.traceId} span=${ev.spanId}`);
|
|
30119
|
+
}
|
|
30120
|
+
const target = input.nodeId ?? "the project";
|
|
30121
|
+
return {
|
|
30122
|
+
summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
30123
|
+
block: blockLines.join("\n"),
|
|
30124
|
+
provenance: import_types108.Provenance.OBSERVED
|
|
30125
|
+
};
|
|
30098
30126
|
} catch (err) {
|
|
30099
|
-
if (err.
|
|
30100
|
-
return {
|
|
30127
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
30128
|
+
return { summary: `Node ${input.nodeId ?? ""} not found in the graph.` };
|
|
30101
30129
|
}
|
|
30102
30130
|
throw err;
|
|
30103
30131
|
}
|
|
30104
|
-
|
|
30105
|
-
|
|
30106
|
-
|
|
30107
|
-
|
|
30108
|
-
|
|
30109
|
-
|
|
30132
|
+
}
|
|
30133
|
+
async function runSearch(client, input) {
|
|
30134
|
+
const result = await client.get(
|
|
30135
|
+
projectPath(input.project, `/search?q=${encodeURIComponent(input.query)}`)
|
|
30136
|
+
);
|
|
30137
|
+
if (result.matches.length === 0) {
|
|
30138
|
+
return { summary: `No matches for "${input.query}".` };
|
|
30110
30139
|
}
|
|
30111
|
-
|
|
30140
|
+
const provider = result.provider ?? "substring";
|
|
30141
|
+
const blockLines = [];
|
|
30142
|
+
let topScore;
|
|
30143
|
+
for (const n of result.matches) {
|
|
30144
|
+
const score = provider !== "substring" && typeof n.score === "number" ? n.score : void 0;
|
|
30145
|
+
const scoreBit = score !== void 0 ? ` [score=${score.toFixed(2)}]` : "";
|
|
30146
|
+
if (score !== void 0 && (topScore === void 0 || score > topScore)) topScore = score;
|
|
30147
|
+
blockLines.push(
|
|
30148
|
+
` \u2022 ${n.id} (${n.type}) \u2014 ${n.name ?? n.id}${scoreBit}`
|
|
30149
|
+
);
|
|
30150
|
+
}
|
|
30151
|
+
return {
|
|
30152
|
+
summary: `Found ${result.matches.length} match${result.matches.length === 1 ? "" : "es"} for "${input.query}" via ${provider} provider.`,
|
|
30153
|
+
block: blockLines.join("\n"),
|
|
30154
|
+
confidence: topScore
|
|
30155
|
+
};
|
|
30112
30156
|
}
|
|
30113
|
-
function
|
|
30114
|
-
|
|
30115
|
-
|
|
30157
|
+
async function runDiff(client, input) {
|
|
30158
|
+
const result = await client.get(
|
|
30159
|
+
projectPath(
|
|
30160
|
+
input.project,
|
|
30161
|
+
`/graph/diff?against=${encodeURIComponent(input.againstSnapshot)}`
|
|
30162
|
+
)
|
|
30163
|
+
);
|
|
30164
|
+
const total = result.added.nodes.length + result.added.edges.length + result.removed.nodes.length + result.removed.edges.length + result.changed.nodes.length + result.changed.edges.length;
|
|
30165
|
+
const baseLabel = result.base.exportedAt ?? "unknown";
|
|
30166
|
+
if (total === 0) {
|
|
30167
|
+
return {
|
|
30168
|
+
summary: `No differences between the current graph and ${input.againstSnapshot} (base exportedAt=${baseLabel}).`
|
|
30169
|
+
};
|
|
30116
30170
|
}
|
|
30117
|
-
const
|
|
30118
|
-
|
|
30119
|
-
|
|
30120
|
-
|
|
30171
|
+
const blockLines = [
|
|
30172
|
+
` base exportedAt: ${baseLabel}`,
|
|
30173
|
+
` current exportedAt: ${result.current.exportedAt}`,
|
|
30174
|
+
""
|
|
30175
|
+
];
|
|
30176
|
+
if (result.added.nodes.length || result.added.edges.length) {
|
|
30177
|
+
blockLines.push("Added:");
|
|
30178
|
+
for (const n of result.added.nodes) blockLines.push(` + node ${n.id} (${n.type})`);
|
|
30179
|
+
for (const e of result.added.edges)
|
|
30180
|
+
blockLines.push(` + edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
|
|
30181
|
+
blockLines.push("");
|
|
30121
30182
|
}
|
|
30122
|
-
|
|
30123
|
-
|
|
30124
|
-
|
|
30183
|
+
if (result.removed.nodes.length || result.removed.edges.length) {
|
|
30184
|
+
blockLines.push("Removed:");
|
|
30185
|
+
for (const n of result.removed.nodes) blockLines.push(` - node ${n.id} (${n.type})`);
|
|
30186
|
+
for (const e of result.removed.edges)
|
|
30187
|
+
blockLines.push(` - edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
|
|
30188
|
+
blockLines.push("");
|
|
30125
30189
|
}
|
|
30126
|
-
|
|
30127
|
-
|
|
30128
|
-
|
|
30129
|
-
|
|
30130
|
-
|
|
30190
|
+
if (result.changed.nodes.length || result.changed.edges.length) {
|
|
30191
|
+
blockLines.push("Changed:");
|
|
30192
|
+
for (const c of result.changed.nodes) {
|
|
30193
|
+
blockLines.push(` ~ node ${c.id} \u2014 ${summariseAttrDiff(c.before, c.after)}`);
|
|
30194
|
+
}
|
|
30195
|
+
for (const c of result.changed.edges) {
|
|
30196
|
+
const provBit = c.before.provenance !== c.after.provenance ? `provenance ${c.before.provenance} \u2192 ${c.after.provenance}` : summariseAttrDiff(c.before, c.after);
|
|
30197
|
+
blockLines.push(` ~ edge ${c.id} \u2014 ${provBit}`);
|
|
30198
|
+
}
|
|
30131
30199
|
}
|
|
30132
|
-
|
|
30133
|
-
|
|
30134
|
-
|
|
30135
|
-
|
|
30200
|
+
return {
|
|
30201
|
+
summary: `Diff against ${input.againstSnapshot}: ${total} change${total === 1 ? "" : "s"} between the snapshot and the live graph.`,
|
|
30202
|
+
block: blockLines.join("\n").trimEnd()
|
|
30203
|
+
};
|
|
30204
|
+
}
|
|
30205
|
+
function summariseAttrDiff(before, after) {
|
|
30206
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
30207
|
+
const changed = [];
|
|
30208
|
+
for (const k of keys) {
|
|
30209
|
+
if (JSON.stringify(before[k]) !== JSON.stringify(after[k])) changed.push(k);
|
|
30210
|
+
}
|
|
30211
|
+
return changed.length === 0 ? "attributes differ" : `fields changed: ${changed.sort().join(", ")}`;
|
|
30212
|
+
}
|
|
30213
|
+
async function runStaleEdges(client, input) {
|
|
30214
|
+
const params = new URLSearchParams();
|
|
30215
|
+
if (input.limit !== void 0) params.set("limit", String(input.limit));
|
|
30216
|
+
if (input.edgeType) params.set("edgeType", input.edgeType);
|
|
30217
|
+
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
30218
|
+
const body = await client.get(
|
|
30219
|
+
projectPath(input.project, `/stale-events${qs}`)
|
|
30220
|
+
);
|
|
30221
|
+
const events = body.events;
|
|
30222
|
+
if (events.length === 0) {
|
|
30223
|
+
return {
|
|
30224
|
+
summary: input.edgeType ? `No stale ${input.edgeType} edges recorded.` : "No stale-edge transitions recorded yet."
|
|
30225
|
+
};
|
|
30226
|
+
}
|
|
30227
|
+
const blockLines = events.map(
|
|
30228
|
+
(e) => ` ${e.transitionedAt} \u2014 ${e.source} -[${e.edgeType}]-> ${e.target} (last seen ${e.lastObserved}, threshold ${formatDuration(e.thresholdMs)})`
|
|
30229
|
+
);
|
|
30230
|
+
return {
|
|
30231
|
+
summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
|
|
30232
|
+
block: blockLines.join("\n"),
|
|
30233
|
+
provenance: import_types108.Provenance.STALE
|
|
30234
|
+
};
|
|
30235
|
+
}
|
|
30236
|
+
async function runPolicies(client, input) {
|
|
30237
|
+
let violations;
|
|
30238
|
+
let allowed = true;
|
|
30239
|
+
let hypothetical;
|
|
30240
|
+
if (input.hypotheticalAction) {
|
|
30241
|
+
if (typeof client.post !== "function") {
|
|
30242
|
+
throw new Error("HttpClient does not support POST \u2014 required for policies dry-run");
|
|
30136
30243
|
}
|
|
30137
|
-
|
|
30244
|
+
const body = await client.post(
|
|
30245
|
+
projectPath(input.project, "/policies/check"),
|
|
30246
|
+
{ hypotheticalAction: input.hypotheticalAction }
|
|
30247
|
+
);
|
|
30248
|
+
violations = body.violations;
|
|
30249
|
+
allowed = body.allowed;
|
|
30250
|
+
hypothetical = body.hypotheticalAction;
|
|
30251
|
+
} else {
|
|
30252
|
+
const params = new URLSearchParams();
|
|
30253
|
+
if (input.policyId) params.set("policyId", input.policyId);
|
|
30254
|
+
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
30255
|
+
const body = await client.get(
|
|
30256
|
+
projectPath(input.project, `/policies/violations${qs}`)
|
|
30257
|
+
);
|
|
30258
|
+
violations = body.violations;
|
|
30259
|
+
allowed = violations.every((v) => v.onViolation !== "block");
|
|
30138
30260
|
}
|
|
30139
|
-
|
|
30261
|
+
if (input.nodeId) {
|
|
30262
|
+
violations = violations.filter(
|
|
30263
|
+
(v) => v.subject.nodeId === input.nodeId || v.subject.path?.includes(input.nodeId)
|
|
30264
|
+
);
|
|
30265
|
+
}
|
|
30266
|
+
if (violations.length === 0) {
|
|
30267
|
+
return {
|
|
30268
|
+
summary: hypothetical ? `No violations would result from the hypothetical action (${hypothetical.kind}).` : "No policy violations recorded."
|
|
30269
|
+
};
|
|
30270
|
+
}
|
|
30271
|
+
const blockCount = violations.filter((v) => v.onViolation === "block").length;
|
|
30272
|
+
const summaryParts = [];
|
|
30273
|
+
if (hypothetical) {
|
|
30274
|
+
summaryParts.push(
|
|
30275
|
+
`Hypothetical ${hypothetical.kind} would surface ${violations.length} violation${violations.length === 1 ? "" : "s"}`
|
|
30276
|
+
);
|
|
30277
|
+
} else {
|
|
30278
|
+
summaryParts.push(
|
|
30279
|
+
`${violations.length} policy violation${violations.length === 1 ? "" : "s"} currently recorded`
|
|
30280
|
+
);
|
|
30281
|
+
}
|
|
30282
|
+
if (blockCount > 0) summaryParts.push(`${blockCount} of which block`);
|
|
30283
|
+
if (!allowed && hypothetical) summaryParts.push("action denied");
|
|
30284
|
+
const summary = summaryParts.join("; ") + ".";
|
|
30285
|
+
const blockLines = violations.map((v) => {
|
|
30286
|
+
const subject = v.subject.nodeId ?? v.subject.edgeId ?? v.subject.path?.[0] ?? "(global)";
|
|
30287
|
+
return ` \u2022 [${v.severity}/${v.onViolation}] ${v.policyName}: ${v.message} \u2014 ${subject}`;
|
|
30288
|
+
});
|
|
30289
|
+
const severities = [...new Set(violations.map((v) => v.severity))];
|
|
30290
|
+
return {
|
|
30291
|
+
summary,
|
|
30292
|
+
block: blockLines.join("\n"),
|
|
30293
|
+
confidence: hypothetical ? 0.7 : 1,
|
|
30294
|
+
provenance: severities.join(" ")
|
|
30295
|
+
};
|
|
30140
30296
|
}
|
|
30141
|
-
function
|
|
30142
|
-
|
|
30143
|
-
|
|
30144
|
-
|
|
30297
|
+
function formatDivergenceLine(d) {
|
|
30298
|
+
switch (d.type) {
|
|
30299
|
+
case "missing-observed":
|
|
30300
|
+
case "missing-extracted":
|
|
30301
|
+
if (d.column) {
|
|
30302
|
+
return ` \u2022 [${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
30303
|
+
}
|
|
30304
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} (${d.edgeType}) \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
30305
|
+
case "version-mismatch":
|
|
30306
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared ${d.extractedVersion}, observed engine ${d.observedVersion} (${d.compatibility})`;
|
|
30307
|
+
case "host-mismatch":
|
|
30308
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared host ${d.extractedHost}, observed host ${d.observedHost}`;
|
|
30309
|
+
case "compat-violation":
|
|
30310
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.rule.kind}${d.rule.package ? ` (${d.rule.package})` : ""}`;
|
|
30311
|
+
case "observed-symbol-mismatch": {
|
|
30312
|
+
const at = d.location ? ` at ${d.location}` : "";
|
|
30313
|
+
const member = d.symbol ? ` ${d.symbol}` : "";
|
|
30314
|
+
return ` \u2022 [${d.type}] ${d.source}${member}${at} (${d.mismatchKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
30315
|
+
}
|
|
30316
|
+
case "observed-failing": {
|
|
30317
|
+
if (d.edgeType) {
|
|
30318
|
+
const rate = d.errorRate !== void 0 ? ` ${Math.round(d.errorRate * 100)}% errors` : "";
|
|
30319
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} (${d.edgeType}) \u2014${rate} (${d.failureKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
30320
|
+
}
|
|
30321
|
+
const at = d.location ? ` at ${d.location}` : "";
|
|
30322
|
+
return ` \u2022 [${d.type}] ${d.source}${at} (${d.failureKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
30323
|
+
}
|
|
30324
|
+
case "deploy-mismatch": {
|
|
30325
|
+
if (d.kind === "image") {
|
|
30326
|
+
return ` \u2022 [${d.type}] ${d.source} \u2014 declared image ${d.declaredImage ?? "?"}, running ${d.observedImage ?? "?"} \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
30327
|
+
}
|
|
30328
|
+
return ` \u2022 [${d.type}] ${d.source} \u2014 declared ${d.declaredReplicas ?? "?"} replicas, ${d.observedReplicas ?? "?"} ready \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
30329
|
+
}
|
|
30145
30330
|
}
|
|
30146
|
-
|
|
30147
|
-
|
|
30148
|
-
|
|
30149
|
-
|
|
30331
|
+
}
|
|
30332
|
+
async function runDivergences(client, input) {
|
|
30333
|
+
const params = new URLSearchParams();
|
|
30334
|
+
if (input.type && input.type.length > 0) params.set("type", input.type.join(","));
|
|
30335
|
+
if (input.minConfidence !== void 0) {
|
|
30336
|
+
params.set("minConfidence", String(input.minConfidence));
|
|
30150
30337
|
}
|
|
30151
|
-
|
|
30152
|
-
|
|
30153
|
-
|
|
30338
|
+
if (input.node) params.set("node", input.node);
|
|
30339
|
+
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
30340
|
+
const result = await client.get(
|
|
30341
|
+
projectPath(input.project, `/graph/divergences${qs}`)
|
|
30342
|
+
);
|
|
30343
|
+
if (result.totalAffected === 0) {
|
|
30344
|
+
return {
|
|
30345
|
+
summary: "No divergences found between the declared (EXTRACTED) and observed (OBSERVED) views of the graph."
|
|
30346
|
+
};
|
|
30154
30347
|
}
|
|
30155
|
-
|
|
30156
|
-
|
|
30157
|
-
|
|
30158
|
-
|
|
30159
|
-
|
|
30348
|
+
const headline = result.divergences[0];
|
|
30349
|
+
const summary = `Found ${result.totalAffected} divergence${result.totalAffected === 1 ? "" : "s"} between code and production. Highest-confidence: ${headline.type} on ${headline.source} \u2192 ${headline.target}. ${headline.reason}`;
|
|
30350
|
+
const blockLines = [];
|
|
30351
|
+
for (const d of result.divergences) {
|
|
30352
|
+
blockLines.push(formatDivergenceLine(d));
|
|
30353
|
+
blockLines.push(` reason: ${d.reason}`);
|
|
30354
|
+
blockLines.push(` recommendation: ${d.recommendation}`);
|
|
30160
30355
|
}
|
|
30161
|
-
|
|
30162
|
-
|
|
30356
|
+
const maxConfidence = result.divergences.reduce(
|
|
30357
|
+
(m, d) => Math.max(m, d.confidence),
|
|
30358
|
+
0
|
|
30359
|
+
);
|
|
30360
|
+
return {
|
|
30361
|
+
summary,
|
|
30362
|
+
block: blockLines.join("\n"),
|
|
30363
|
+
confidence: maxConfidence,
|
|
30364
|
+
provenance: "composite (EXTRACTED + OBSERVED)"
|
|
30365
|
+
};
|
|
30366
|
+
}
|
|
30367
|
+
async function runAsk(client, input) {
|
|
30368
|
+
const result = await client.get(
|
|
30369
|
+
projectPath(input.project, `/graph/ask?q=${encodeURIComponent(input.question)}`)
|
|
30370
|
+
);
|
|
30371
|
+
const blockLines = [];
|
|
30372
|
+
if (result.matched.length > 0) {
|
|
30373
|
+
blockLines.push(
|
|
30374
|
+
`Matched: ${result.matched.map((m) => `${m.nodeId} [${m.via} ${m.score.toFixed(2)}]`).join(", ")}`
|
|
30375
|
+
);
|
|
30376
|
+
blockLines.push(`Intent: ${result.intent}`);
|
|
30377
|
+
} else if (result.scope === "global") {
|
|
30378
|
+
blockLines.push(`Graph-wide answer (${result.intent}) \u2014 no entity named.`);
|
|
30163
30379
|
}
|
|
30164
|
-
|
|
30165
|
-
|
|
30380
|
+
for (const section of result.sections) {
|
|
30381
|
+
blockLines.push("", section.heading + ":");
|
|
30382
|
+
for (const fact of section.facts) {
|
|
30383
|
+
const tag = fact.provenance ? ` [${fact.provenance}${fact.confidence !== void 0 ? ` ${fact.confidence.toFixed(2)}` : ""}]` : fact.confidence !== void 0 ? ` [confidence ${fact.confidence.toFixed(2)}]` : "";
|
|
30384
|
+
blockLines.push(` \u2022 ${fact.text}${tag}`);
|
|
30385
|
+
}
|
|
30166
30386
|
}
|
|
30167
30387
|
return {
|
|
30168
|
-
|
|
30169
|
-
|
|
30170
|
-
...
|
|
30388
|
+
summary: result.answer,
|
|
30389
|
+
block: blockLines.join("\n").trim(),
|
|
30390
|
+
...result.confidence !== void 0 ? { confidence: result.confidence } : {},
|
|
30391
|
+
...result.provenance.length > 0 ? { provenance: result.provenance } : {}
|
|
30171
30392
|
};
|
|
30172
30393
|
}
|
|
30173
|
-
|
|
30174
|
-
const
|
|
30175
|
-
|
|
30176
|
-
}
|
|
30177
|
-
async function getActiveProfile(home = neatHome3()) {
|
|
30178
|
-
const { active, profiles } = await readProfilesConfig(home);
|
|
30179
|
-
if (!active) return void 0;
|
|
30180
|
-
return profiles.find((p) => p.name === active);
|
|
30394
|
+
function formatFooter(confidence, provenance) {
|
|
30395
|
+
const c = confidence === void 0 ? "n/a" : confidence.toFixed(2);
|
|
30396
|
+
const p = provenance === void 0 ? "n/a" : Array.isArray(provenance) ? [...new Set(provenance)].join(", ") : provenance;
|
|
30397
|
+
return `confidence: ${c} \xB7 provenance: ${p}`;
|
|
30181
30398
|
}
|
|
30182
|
-
function
|
|
30183
|
-
const
|
|
30184
|
-
|
|
30185
|
-
|
|
30186
|
-
|
|
30187
|
-
...active ? { active } : {},
|
|
30188
|
-
profiles: config.profiles
|
|
30189
|
-
};
|
|
30190
|
-
return `${JSON.stringify(out, null, 2)}
|
|
30191
|
-
`;
|
|
30399
|
+
function formatHuman(result) {
|
|
30400
|
+
const sections = [result.summary.trim()];
|
|
30401
|
+
if (result.block && result.block.trim().length > 0) sections.push(result.block.trimEnd());
|
|
30402
|
+
sections.push(formatFooter(result.confidence, result.provenance));
|
|
30403
|
+
return sections.join("\n\n");
|
|
30192
30404
|
}
|
|
30193
|
-
|
|
30194
|
-
|
|
30195
|
-
|
|
30196
|
-
|
|
30197
|
-
|
|
30198
|
-
|
|
30199
|
-
|
|
30200
|
-
|
|
30201
|
-
|
|
30202
|
-
|
|
30203
|
-
|
|
30204
|
-
await import_node_fs54.promises.rename(tmp, file);
|
|
30405
|
+
function formatJson(result) {
|
|
30406
|
+
return JSON.stringify(
|
|
30407
|
+
{
|
|
30408
|
+
summary: result.summary,
|
|
30409
|
+
block: result.block ?? "",
|
|
30410
|
+
confidence: result.confidence ?? null,
|
|
30411
|
+
provenance: result.provenance ?? null
|
|
30412
|
+
},
|
|
30413
|
+
null,
|
|
30414
|
+
2
|
|
30415
|
+
);
|
|
30205
30416
|
}
|
|
30206
|
-
|
|
30207
|
-
|
|
30208
|
-
|
|
30209
|
-
|
|
30210
|
-
const deadline = Date.now() + LOCK_TIMEOUT_MS2;
|
|
30211
|
-
for (; ; ) {
|
|
30212
|
-
try {
|
|
30213
|
-
const fd = await import_node_fs54.promises.open(lockPath, "wx");
|
|
30214
|
-
await fd.writeFile(`${process.pid}
|
|
30215
|
-
`, "utf8");
|
|
30216
|
-
await fd.close();
|
|
30217
|
-
return;
|
|
30218
|
-
} catch (err) {
|
|
30219
|
-
if (err.code !== "EEXIST") throw err;
|
|
30220
|
-
if (Date.now() >= deadline) {
|
|
30221
|
-
throw new Error(
|
|
30222
|
-
`timed out acquiring ${lockPath} after ${LOCK_TIMEOUT_MS2}ms \u2014 if no other neat process is running, remove the stale lock file`
|
|
30223
|
-
);
|
|
30224
|
-
}
|
|
30225
|
-
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS2));
|
|
30226
|
-
}
|
|
30227
|
-
}
|
|
30417
|
+
function exitCodeForError(err) {
|
|
30418
|
+
if (err instanceof TransportError) return 3;
|
|
30419
|
+
if (err instanceof HttpError) return 1;
|
|
30420
|
+
return 1;
|
|
30228
30421
|
}
|
|
30229
|
-
|
|
30230
|
-
|
|
30422
|
+
function createSnapshotPushClient(baseUrl, token, fetchImpl) {
|
|
30423
|
+
return createHttpClient(baseUrl, token && token.length > 0 ? token : void 0, fetchImpl);
|
|
30231
30424
|
}
|
|
30232
|
-
async function
|
|
30233
|
-
const
|
|
30234
|
-
await acquireLock2(lockPath);
|
|
30425
|
+
async function resolveRemoteProjectName(input, fetchImpl) {
|
|
30426
|
+
const client = createSnapshotPushClient(input.baseUrl, input.token, fetchImpl);
|
|
30235
30427
|
try {
|
|
30236
|
-
|
|
30237
|
-
|
|
30238
|
-
|
|
30428
|
+
const projects = await client.get("/projects");
|
|
30429
|
+
const hosted = Array.isArray(projects) ? projects.find((p) => p?.hostedHere) : void 0;
|
|
30430
|
+
return hosted?.name ?? input.fallback;
|
|
30431
|
+
} catch {
|
|
30432
|
+
return input.fallback;
|
|
30239
30433
|
}
|
|
30240
30434
|
}
|
|
30241
|
-
async function
|
|
30242
|
-
const
|
|
30243
|
-
|
|
30244
|
-
|
|
30245
|
-
|
|
30246
|
-
|
|
30247
|
-
|
|
30248
|
-
|
|
30249
|
-
|
|
30250
|
-
await writeConfigAtomic({ version: config.version, ...active ? { active } : {}, profiles }, home);
|
|
30251
|
-
});
|
|
30252
|
-
}
|
|
30253
|
-
async function removeProfile(name, home = neatHome3()) {
|
|
30254
|
-
return withProfilesLock(home, async () => {
|
|
30255
|
-
const config = await readProfilesConfig(home);
|
|
30256
|
-
const profiles = config.profiles.filter((p) => p.name !== name);
|
|
30257
|
-
if (profiles.length === config.profiles.length) return false;
|
|
30258
|
-
const active = config.active === name ? void 0 : config.active;
|
|
30259
|
-
await writeConfigAtomic({ version: config.version, ...active ? { active } : {}, profiles }, home);
|
|
30260
|
-
return true;
|
|
30261
|
-
});
|
|
30262
|
-
}
|
|
30263
|
-
async function clearActiveProfile(home = neatHome3()) {
|
|
30264
|
-
return withProfilesLock(home, async () => {
|
|
30265
|
-
const config = await readProfilesConfig(home);
|
|
30266
|
-
if (!config.active) return false;
|
|
30267
|
-
await writeConfigAtomic({ version: config.version, profiles: config.profiles }, home);
|
|
30268
|
-
return true;
|
|
30269
|
-
});
|
|
30435
|
+
async function pushSnapshotToRemote(input) {
|
|
30436
|
+
const client = createSnapshotPushClient(input.baseUrl, input.token);
|
|
30437
|
+
if (typeof client.post !== "function") {
|
|
30438
|
+
throw new Error("HttpClient does not support POST \u2014 required for snapshot push");
|
|
30439
|
+
}
|
|
30440
|
+
return client.post(
|
|
30441
|
+
`/projects/${encodeURIComponent(input.project)}/snapshot`,
|
|
30442
|
+
{ snapshot: input.snapshot }
|
|
30443
|
+
);
|
|
30270
30444
|
}
|
|
30271
30445
|
|
|
30272
|
-
// src/
|
|
30273
|
-
|
|
30274
|
-
var import_node_http2 = require("http");
|
|
30275
|
-
var import_node_child_process4 = require("child_process");
|
|
30276
|
-
var import_node_crypto6 = require("crypto");
|
|
30277
|
-
var DEFAULT_CP_URL = "https://neat-control-plane-bg5yqctn2q-nw.a.run.app";
|
|
30278
|
-
var DEFAULT_WEB_URL = "https://app.neat.is";
|
|
30279
|
-
var CALLBACK_TIMEOUT_MS = 5 * 6e4;
|
|
30280
|
-
function resolveCpUrl(env = process.env, override) {
|
|
30281
|
-
const v = override ?? env.NEAT_CP_URL;
|
|
30282
|
-
return (v && v.length > 0 ? v : DEFAULT_CP_URL).replace(/\/+$/, "");
|
|
30283
|
-
}
|
|
30284
|
-
function resolveWebUrl(env = process.env, override) {
|
|
30285
|
-
const v = override ?? env.NEAT_WEB_URL;
|
|
30286
|
-
return (v && v.length > 0 ? v : DEFAULT_WEB_URL).replace(/\/+$/, "");
|
|
30287
|
-
}
|
|
30288
|
-
function pickProject(projects, want) {
|
|
30289
|
-
if (want) {
|
|
30290
|
-
const p = projects.find((x) => x.id === want || x.name === want);
|
|
30291
|
-
if (!p) return { error: { code: 1, message: `no project named or id'd "${want}" on this account` } };
|
|
30292
|
-
return { project: p };
|
|
30293
|
-
}
|
|
30294
|
-
const running = projects.filter((p) => p.status === "running");
|
|
30295
|
-
if (running.length === 1) return { project: running[0] };
|
|
30296
|
-
if (running.length === 0) {
|
|
30297
|
-
const listed = projects.length ? ` (have: ${projects.map((p) => `${p.name} [${p.status}]`).join(", ")})` : "";
|
|
30298
|
-
return {
|
|
30299
|
-
error: {
|
|
30300
|
-
code: 1,
|
|
30301
|
-
message: `no running project to connect to \u2014 create + provision one in the console first${listed}`
|
|
30302
|
-
}
|
|
30303
|
-
};
|
|
30304
|
-
}
|
|
30446
|
+
// src/doctor-cli.ts
|
|
30447
|
+
function resolveDeps2(deps) {
|
|
30305
30448
|
return {
|
|
30306
|
-
|
|
30307
|
-
|
|
30308
|
-
|
|
30309
|
-
|
|
30310
|
-
|
|
30311
|
-
|
|
30312
|
-
|
|
30313
|
-
|
|
30314
|
-
|
|
30315
|
-
|
|
30316
|
-
|
|
30317
|
-
|
|
30318
|
-
|
|
30319
|
-
|
|
30320
|
-
|
|
30321
|
-
|
|
30322
|
-
|
|
30323
|
-
|
|
30324
|
-
|
|
30325
|
-
|
|
30326
|
-
|
|
30327
|
-
const project = picked.project;
|
|
30328
|
-
let credRes;
|
|
30449
|
+
cwd: deps.cwd ?? process.cwd(),
|
|
30450
|
+
env: deps.env ?? process.env,
|
|
30451
|
+
nodeVersion: deps.nodeVersion ?? process.versions.node,
|
|
30452
|
+
fetchImpl: deps.fetchImpl ?? fetch,
|
|
30453
|
+
readRecord: deps.readRecord ?? readDaemonRecord,
|
|
30454
|
+
out: deps.out ?? ((line) => console.log(line))
|
|
30455
|
+
};
|
|
30456
|
+
}
|
|
30457
|
+
var NODE_FLOOR = 20;
|
|
30458
|
+
var HEALTH_TIMEOUT_MS = 3e3;
|
|
30459
|
+
function checkNode(nodeVersion) {
|
|
30460
|
+
const major = Number.parseInt(nodeVersion.split(".")[0] ?? "", 10);
|
|
30461
|
+
const ok = Number.isFinite(major) && major >= NODE_FLOOR;
|
|
30462
|
+
return ok ? { name: "node", ok, detail: `v${nodeVersion} (>= ${NODE_FLOOR} required)` } : {
|
|
30463
|
+
name: "node",
|
|
30464
|
+
ok,
|
|
30465
|
+
detail: `v${nodeVersion} \u2014 NEAT needs Node ${NODE_FLOOR} or newer`,
|
|
30466
|
+
fix: `install Node ${NODE_FLOOR}.x (e.g. \`nvm install ${NODE_FLOOR}\`) and re-run`
|
|
30467
|
+
};
|
|
30468
|
+
}
|
|
30469
|
+
async function neatOutExists(cwd) {
|
|
30329
30470
|
try {
|
|
30330
|
-
|
|
30331
|
-
|
|
30332
|
-
|
|
30333
|
-
|
|
30334
|
-
return { error: { code: 3, message: `can't reach the control plane \u2014 ${e.message}` } };
|
|
30471
|
+
const st = await import_node_fs54.promises.stat(import_node_path90.default.join(cwd, "neat-out"));
|
|
30472
|
+
return st.isDirectory();
|
|
30473
|
+
} catch {
|
|
30474
|
+
return false;
|
|
30335
30475
|
}
|
|
30336
|
-
|
|
30476
|
+
}
|
|
30477
|
+
async function checkProject(cwd, record) {
|
|
30478
|
+
if (record) {
|
|
30337
30479
|
return {
|
|
30338
|
-
|
|
30339
|
-
|
|
30340
|
-
|
|
30341
|
-
}
|
|
30480
|
+
name: "project",
|
|
30481
|
+
ok: true,
|
|
30482
|
+
detail: `"${record.project}" \u2014 set up in this directory (daemon record on REST ${record.ports.rest})`
|
|
30342
30483
|
};
|
|
30343
30484
|
}
|
|
30344
|
-
if (
|
|
30345
|
-
|
|
30346
|
-
|
|
30347
|
-
|
|
30348
|
-
|
|
30349
|
-
|
|
30485
|
+
if (await neatOutExists(cwd)) {
|
|
30486
|
+
return {
|
|
30487
|
+
name: "project",
|
|
30488
|
+
ok: true,
|
|
30489
|
+
detail: "set up in this directory (no live daemon record \u2014 it may be stopped)"
|
|
30490
|
+
};
|
|
30350
30491
|
}
|
|
30351
|
-
return {
|
|
30492
|
+
return {
|
|
30493
|
+
name: "project",
|
|
30494
|
+
ok: false,
|
|
30495
|
+
detail: "no NEAT project in this directory",
|
|
30496
|
+
fix: "set one up: `neat .`"
|
|
30497
|
+
};
|
|
30352
30498
|
}
|
|
30353
|
-
function
|
|
30354
|
-
const
|
|
30355
|
-
|
|
30356
|
-
|
|
30499
|
+
function resolveHealthUrl(env, record) {
|
|
30500
|
+
const explicit = env.NEAT_API_URL ?? env.NEAT_CORE_URL;
|
|
30501
|
+
if (explicit && explicit.length > 0) return explicit.replace(/\/$/, "");
|
|
30502
|
+
if (record) return `http://localhost:${record.ports.rest}`;
|
|
30503
|
+
return "http://localhost:8080";
|
|
30504
|
+
}
|
|
30505
|
+
function summariseHealth(url, body) {
|
|
30506
|
+
const projects = body.projects ?? [];
|
|
30507
|
+
const nodes = projects.reduce((n, p) => n + (p.nodeCount ?? 0), 0);
|
|
30508
|
+
const edges = projects.reduce((n, p) => n + (p.edgeCount ?? 0), 0);
|
|
30509
|
+
const proj = body.project ?? projects[0]?.name;
|
|
30510
|
+
const graph = projects.length > 0 ? ` \u2014 ${nodes} nodes / ${edges} edges` : "";
|
|
30511
|
+
return `up at ${url}${proj ? ` (project "${proj}"${graph})` : ""}`;
|
|
30512
|
+
}
|
|
30513
|
+
async function checkDaemon(deps, record) {
|
|
30514
|
+
const url = resolveHealthUrl(deps.env, record);
|
|
30515
|
+
const token = resolveAuthToken(deps.env);
|
|
30516
|
+
const headers = token ? { authorization: `Bearer ${token}` } : {};
|
|
30357
30517
|
try {
|
|
30358
|
-
const
|
|
30359
|
-
|
|
30518
|
+
const res = await deps.fetchImpl(`${url}/health`, {
|
|
30519
|
+
headers,
|
|
30520
|
+
signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS)
|
|
30360
30521
|
});
|
|
30361
|
-
|
|
30362
|
-
|
|
30522
|
+
if (res.status === 401 || res.status === 403) {
|
|
30523
|
+
return {
|
|
30524
|
+
name: "daemon",
|
|
30525
|
+
ok: false,
|
|
30526
|
+
detail: `up at ${url}, but rejected the request (${res.status})`,
|
|
30527
|
+
fix: "set NEAT_AUTH_TOKEN to the daemon's token"
|
|
30528
|
+
};
|
|
30529
|
+
}
|
|
30530
|
+
if (!res.ok) {
|
|
30531
|
+
return {
|
|
30532
|
+
name: "daemon",
|
|
30533
|
+
ok: false,
|
|
30534
|
+
detail: `reachable at ${url} but /health returned ${res.status}`,
|
|
30535
|
+
fix: "check the daemon logs"
|
|
30536
|
+
};
|
|
30537
|
+
}
|
|
30538
|
+
const body = await res.json().catch(() => ({}));
|
|
30539
|
+
return { name: "daemon", ok: true, detail: summariseHealth(url, body) };
|
|
30363
30540
|
} catch {
|
|
30364
|
-
return
|
|
30541
|
+
return {
|
|
30542
|
+
name: "daemon",
|
|
30543
|
+
ok: false,
|
|
30544
|
+
detail: `down \u2014 nothing answering at ${url}`,
|
|
30545
|
+
fix: "start it: `neat .` (or `neat watch`)"
|
|
30546
|
+
};
|
|
30365
30547
|
}
|
|
30366
30548
|
}
|
|
30367
|
-
async function
|
|
30368
|
-
const
|
|
30369
|
-
|
|
30370
|
-
|
|
30371
|
-
const state = (0, import_node_crypto6.randomBytes)(16).toString("hex");
|
|
30372
|
-
return new Promise((resolve) => {
|
|
30373
|
-
let settled = false;
|
|
30374
|
-
let timer;
|
|
30375
|
-
const done = (r) => {
|
|
30376
|
-
if (settled) return;
|
|
30377
|
-
settled = true;
|
|
30378
|
-
clearTimeout(timer);
|
|
30379
|
-
try {
|
|
30380
|
-
server.close();
|
|
30381
|
-
} catch {
|
|
30382
|
-
}
|
|
30383
|
-
resolve(r);
|
|
30384
|
-
};
|
|
30385
|
-
const server = (0, import_node_http2.createServer)((req, res) => {
|
|
30386
|
-
const reqUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
30387
|
-
if (reqUrl.pathname !== "/callback") {
|
|
30388
|
-
res.writeHead(404);
|
|
30389
|
-
res.end();
|
|
30390
|
-
return;
|
|
30391
|
-
}
|
|
30392
|
-
const token = reqUrl.searchParams.get("token");
|
|
30393
|
-
const gotState = reqUrl.searchParams.get("state");
|
|
30394
|
-
if (!token || gotState !== state) {
|
|
30395
|
-
res.writeHead(400, { "content-type": "text/html" });
|
|
30396
|
-
res.end("<h1>NEAT login failed</h1><p>Invalid or mismatched token. You can close this tab.</p>");
|
|
30397
|
-
done({ error: { code: 1, message: "the browser returned an invalid or mismatched token" } });
|
|
30398
|
-
return;
|
|
30399
|
-
}
|
|
30400
|
-
res.writeHead(200, { "content-type": "text/html" });
|
|
30401
|
-
res.end("<h1>You're logged in to NEAT.</h1><p>You can close this tab and return to the terminal.</p>");
|
|
30402
|
-
done({ token });
|
|
30403
|
-
});
|
|
30404
|
-
server.on("error", (e) => done({ error: { code: 3, message: `couldn't start the local login listener \u2014 ${e.message}` } }));
|
|
30405
|
-
timer = setTimeout(
|
|
30406
|
-
() => done({ error: { code: 1, message: "timed out waiting for the browser login" } }),
|
|
30407
|
-
opts.timeoutMs ?? CALLBACK_TIMEOUT_MS
|
|
30408
|
-
);
|
|
30409
|
-
server.listen(0, "127.0.0.1", () => {
|
|
30410
|
-
const addr = server.address();
|
|
30411
|
-
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
30412
|
-
const callback = `http://127.0.0.1:${port}/callback`;
|
|
30413
|
-
const authUrl = `${webUrl}/cli/auth?callback=${encodeURIComponent(callback)}&state=${state}`;
|
|
30414
|
-
out("Opening your browser to log in to NEAT\u2026");
|
|
30415
|
-
out(`If it doesn't open, visit:
|
|
30416
|
-
${authUrl}`);
|
|
30417
|
-
openFn(authUrl);
|
|
30418
|
-
});
|
|
30419
|
-
});
|
|
30549
|
+
async function runDoctorChecks(deps = {}) {
|
|
30550
|
+
const d = resolveDeps2(deps);
|
|
30551
|
+
const record = await d.readRecord(d.cwd).catch(() => null);
|
|
30552
|
+
return [checkNode(d.nodeVersion), await checkProject(d.cwd, record), await checkDaemon(d, record)];
|
|
30420
30553
|
}
|
|
30421
|
-
|
|
30422
|
-
|
|
30423
|
-
|
|
30424
|
-
|
|
30425
|
-
|
|
30426
|
-
const
|
|
30427
|
-
|
|
30428
|
-
|
|
30429
|
-
return lb.error.code;
|
|
30430
|
-
}
|
|
30431
|
-
accessToken = lb.token;
|
|
30432
|
-
}
|
|
30433
|
-
const ex = await exchangeCredential(opts.cpUrl, accessToken, { project: opts.project }, deps);
|
|
30434
|
-
if ("error" in ex) {
|
|
30435
|
-
err(`neat login: ${ex.error.message}`);
|
|
30436
|
-
return ex.error.code;
|
|
30554
|
+
var NAME_COL = "project".length;
|
|
30555
|
+
function renderHuman(checks, out) {
|
|
30556
|
+
out("neat doctor \u2014 checking this project's setup");
|
|
30557
|
+
out("");
|
|
30558
|
+
for (const c of checks) {
|
|
30559
|
+
const mark = c.ok ? "\u2713" : "\u2717";
|
|
30560
|
+
out(` ${mark} ${c.name.padEnd(NAME_COL)} ${c.detail}`);
|
|
30561
|
+
if (!c.ok && c.fix) out(` ${" ".repeat(NAME_COL + 3)}fix: ${c.fix}`);
|
|
30437
30562
|
}
|
|
30438
|
-
|
|
30439
|
-
|
|
30440
|
-
|
|
30441
|
-
|
|
30442
|
-
|
|
30443
|
-
|
|
30444
|
-
|
|
30445
|
-
|
|
30446
|
-
|
|
30447
|
-
|
|
30448
|
-
|
|
30449
|
-
|
|
30450
|
-
|
|
30451
|
-
|
|
30452
|
-
|
|
30453
|
-
|
|
30454
|
-
|
|
30455
|
-
)
|
|
30456
|
-
);
|
|
30457
|
-
} else {
|
|
30458
|
-
out(`Logged in \u2014 profile "${opts.name}" \u2192 ${project.name} (${cred.endpoint})`);
|
|
30459
|
-
out("The neat CLI and the MCP server now read this hosted graph by default.");
|
|
30460
|
-
if (cred.ingestEndpoint && cred.otelToken) {
|
|
30461
|
-
out("");
|
|
30462
|
-
out("To fill the OBSERVED layer, instrument your app to send OpenTelemetry to the hosted daemon:");
|
|
30463
|
-
out(` OTEL_EXPORTER_OTLP_ENDPOINT=${cred.ingestEndpoint}`);
|
|
30464
|
-
out(` OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${cred.otelToken}`);
|
|
30563
|
+
out("");
|
|
30564
|
+
const failed = checks.filter((c) => !c.ok).length;
|
|
30565
|
+
out(failed === 0 ? "all good." : `${failed} check${failed === 1 ? "" : "s"} failed.`);
|
|
30566
|
+
}
|
|
30567
|
+
async function runDoctorCommand(argv, deps = {}) {
|
|
30568
|
+
const out = deps.out ?? ((line) => console.log(line));
|
|
30569
|
+
let json = false;
|
|
30570
|
+
for (const arg of argv) {
|
|
30571
|
+
if (arg === "--json") json = true;
|
|
30572
|
+
else if (arg === "-h" || arg === "--help") {
|
|
30573
|
+
out("usage: neat doctor [--json]");
|
|
30574
|
+
out(" Probe this directory's NEAT setup: Node version, project, daemon.");
|
|
30575
|
+
out(" Exit 0 when every check passes, 1 when any fails.");
|
|
30576
|
+
return 0;
|
|
30577
|
+
} else {
|
|
30578
|
+
out(`neat doctor: unknown argument "${arg}"`);
|
|
30579
|
+
return 2;
|
|
30465
30580
|
}
|
|
30466
|
-
out("Run `neat logout` to switch back to your local daemon.");
|
|
30467
30581
|
}
|
|
30468
|
-
|
|
30582
|
+
const checks = await runDoctorChecks(deps);
|
|
30583
|
+
if (json) out(JSON.stringify({ ok: checks.every((c) => c.ok), checks }, null, 2));
|
|
30584
|
+
else renderHuman(checks, out);
|
|
30585
|
+
return checks.every((c) => c.ok) ? 0 : 1;
|
|
30469
30586
|
}
|
|
30470
30587
|
|
|
30471
30588
|
// src/login-cli.ts
|
|
30589
|
+
init_cjs_shims();
|
|
30590
|
+
var import_promises2 = __toESM(require("readline/promises"), 1);
|
|
30472
30591
|
var PROBE_ATTEMPT_TIMEOUT_MS = 3e4;
|
|
30473
30592
|
var PROBE_TOTAL_BUDGET_MS = 12e4;
|
|
30474
30593
|
var PROBE_RETRY_PAUSE_MS = 2e3;
|
|
@@ -32263,6 +32382,14 @@ async function checkDaemonHealth(baseUrl) {
|
|
|
32263
32382
|
return false;
|
|
32264
32383
|
}
|
|
32265
32384
|
}
|
|
32385
|
+
function daemonHint(body) {
|
|
32386
|
+
try {
|
|
32387
|
+
const parsed = JSON.parse(body);
|
|
32388
|
+
return typeof parsed.hint === "string" ? parsed.hint : void 0;
|
|
32389
|
+
} catch {
|
|
32390
|
+
return void 0;
|
|
32391
|
+
}
|
|
32392
|
+
}
|
|
32266
32393
|
function snapshotForGraph(persisted) {
|
|
32267
32394
|
return {
|
|
32268
32395
|
// Stamp the live schema version the daemon validates against on the
|
|
@@ -32330,21 +32457,34 @@ async function runSync(opts) {
|
|
|
32330
32457
|
let daemonState = "skipped";
|
|
32331
32458
|
let exitCode = 0;
|
|
32332
32459
|
const mode = opts.dryRun ? "dry-run" : opts.to ? "remote" : "local";
|
|
32460
|
+
let pushedProject = entry2.name;
|
|
32333
32461
|
if (!opts.dryRun) {
|
|
32334
32462
|
const snapshot = snapshotForGraph(persisted);
|
|
32335
32463
|
if (opts.to) {
|
|
32336
32464
|
const token = opts.token ?? process.env.NEAT_REMOTE_TOKEN;
|
|
32465
|
+
pushedProject = await resolveRemoteProjectName({
|
|
32466
|
+
baseUrl: opts.to,
|
|
32467
|
+
token,
|
|
32468
|
+
fallback: entry2.name
|
|
32469
|
+
});
|
|
32337
32470
|
try {
|
|
32338
32471
|
await pushSnapshotToRemote({
|
|
32339
32472
|
baseUrl: opts.to,
|
|
32340
32473
|
token,
|
|
32341
|
-
project:
|
|
32474
|
+
project: pushedProject,
|
|
32342
32475
|
snapshot
|
|
32343
32476
|
});
|
|
32344
32477
|
daemonState = "remote-ok";
|
|
32345
32478
|
} catch (err) {
|
|
32346
32479
|
if (err instanceof HttpError) {
|
|
32347
|
-
|
|
32480
|
+
if (err.status === 404) {
|
|
32481
|
+
const hint = daemonHint(err.responseBody);
|
|
32482
|
+
console.error(
|
|
32483
|
+
`neat sync: the daemon at ${opts.to} has no project "${pushedProject}" to receive this snapshot. ` + (hint ?? "GET /projects lists what it serves (see hostedHere).")
|
|
32484
|
+
);
|
|
32485
|
+
} else {
|
|
32486
|
+
console.error(`neat sync: ${err.message}`);
|
|
32487
|
+
}
|
|
32348
32488
|
exitCode = 1;
|
|
32349
32489
|
} else if (err instanceof TransportError) {
|
|
32350
32490
|
console.error(`neat sync: ${err.message}`);
|
|
@@ -32385,7 +32525,7 @@ async function runSync(opts) {
|
|
|
32385
32525
|
}
|
|
32386
32526
|
const result = {
|
|
32387
32527
|
exitCode,
|
|
32388
|
-
project:
|
|
32528
|
+
project: pushedProject,
|
|
32389
32529
|
scanPath: entry2.path,
|
|
32390
32530
|
nodesAdded: persisted.nodesAdded,
|
|
32391
32531
|
edgesAdded: persisted.edgesAdded,
|
|
@@ -33000,6 +33140,11 @@ async function main() {
|
|
|
33000
33140
|
if (code !== 0) process.exit(code);
|
|
33001
33141
|
return;
|
|
33002
33142
|
}
|
|
33143
|
+
if (cmd0 === "connect") {
|
|
33144
|
+
const code = await runConnectCommand(argv.slice(1));
|
|
33145
|
+
if (code !== 0) process.exit(code);
|
|
33146
|
+
return;
|
|
33147
|
+
}
|
|
33003
33148
|
if (cmd0 === "doctor") {
|
|
33004
33149
|
const code = await runDoctorCommand(argv.slice(1));
|
|
33005
33150
|
if (code !== 0) process.exit(code);
|