@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.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
resolveNeatVersion,
|
|
9
9
|
startK8sSubstratePolling,
|
|
10
10
|
writeDaemonRecord
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-MGQSR6QZ.js";
|
|
12
12
|
import {
|
|
13
13
|
buildSearchIndex
|
|
14
14
|
} from "./chunk-BC53SCT7.js";
|
|
@@ -77,7 +77,7 @@ import {
|
|
|
77
77
|
startStalenessLoop,
|
|
78
78
|
upsertConnectorEntry,
|
|
79
79
|
validateConnectorEntry
|
|
80
|
-
} from "./chunk-
|
|
80
|
+
} from "./chunk-KIV7OB2K.js";
|
|
81
81
|
import {
|
|
82
82
|
startOtelGrpcReceiver
|
|
83
83
|
} from "./chunk-ERE47MCR.js";
|
|
@@ -4861,1139 +4861,1251 @@ async function runConnectorCommand(rawArgs, deps = {}) {
|
|
|
4861
4861
|
}
|
|
4862
4862
|
}
|
|
4863
4863
|
|
|
4864
|
-
// src/
|
|
4864
|
+
// src/hosted-connect-cli.ts
|
|
4865
|
+
import { spawn as spawn4 } from "child_process";
|
|
4866
|
+
|
|
4867
|
+
// src/login-sso.ts
|
|
4868
|
+
import { createServer } from "http";
|
|
4869
|
+
import { spawn as spawn3 } from "child_process";
|
|
4870
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
4871
|
+
|
|
4872
|
+
// src/profiles.ts
|
|
4865
4873
|
import { promises as fs10 } from "fs";
|
|
4874
|
+
import os from "os";
|
|
4866
4875
|
import path11 from "path";
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
super(message);
|
|
4873
|
-
this.status = status;
|
|
4874
|
-
this.responseBody = responseBody;
|
|
4875
|
-
this.name = "HttpError";
|
|
4876
|
-
}
|
|
4877
|
-
status;
|
|
4878
|
-
responseBody;
|
|
4879
|
-
};
|
|
4880
|
-
var TransportError = class extends Error {
|
|
4881
|
-
constructor(message) {
|
|
4882
|
-
super(message);
|
|
4883
|
-
this.name = "TransportError";
|
|
4884
|
-
}
|
|
4885
|
-
};
|
|
4886
|
-
function resolveAuthToken(env = process.env) {
|
|
4887
|
-
const t = env.NEAT_AUTH_TOKEN;
|
|
4888
|
-
return t && t.length > 0 ? t : void 0;
|
|
4876
|
+
var PROFILES_CONFIG_VERSION = 1;
|
|
4877
|
+
function neatHome() {
|
|
4878
|
+
const override = process.env.NEAT_HOME;
|
|
4879
|
+
if (override && override.length > 0) return path11.resolve(override);
|
|
4880
|
+
return path11.join(os.homedir(), ".neat");
|
|
4889
4881
|
}
|
|
4890
|
-
function
|
|
4891
|
-
|
|
4892
|
-
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
4893
|
-
return {
|
|
4894
|
-
async get(path19) {
|
|
4895
|
-
let res;
|
|
4896
|
-
try {
|
|
4897
|
-
res = await fetch(`${root}${path19}`, {
|
|
4898
|
-
headers: { ...authHeader }
|
|
4899
|
-
});
|
|
4900
|
-
} catch (err) {
|
|
4901
|
-
throw new TransportError(
|
|
4902
|
-
`cannot reach neat-core at ${root}: ${err.message}`
|
|
4903
|
-
);
|
|
4904
|
-
}
|
|
4905
|
-
if (!res.ok) {
|
|
4906
|
-
const body = await res.text().catch(() => "");
|
|
4907
|
-
throw new HttpError(
|
|
4908
|
-
res.status,
|
|
4909
|
-
`${res.status} ${res.statusText} on GET ${path19}: ${body}`,
|
|
4910
|
-
body
|
|
4911
|
-
);
|
|
4912
|
-
}
|
|
4913
|
-
return await res.json();
|
|
4914
|
-
},
|
|
4915
|
-
async post(path19, body) {
|
|
4916
|
-
let res;
|
|
4917
|
-
try {
|
|
4918
|
-
res = await fetch(`${root}${path19}`, {
|
|
4919
|
-
method: "POST",
|
|
4920
|
-
headers: { "content-type": "application/json", ...authHeader },
|
|
4921
|
-
body: JSON.stringify(body)
|
|
4922
|
-
});
|
|
4923
|
-
} catch (err) {
|
|
4924
|
-
throw new TransportError(
|
|
4925
|
-
`cannot reach neat-core at ${root}: ${err.message}`
|
|
4926
|
-
);
|
|
4927
|
-
}
|
|
4928
|
-
if (!res.ok) {
|
|
4929
|
-
const text = await res.text().catch(() => "");
|
|
4930
|
-
throw new HttpError(
|
|
4931
|
-
res.status,
|
|
4932
|
-
`${res.status} ${res.statusText} on POST ${path19}: ${text}`,
|
|
4933
|
-
text
|
|
4934
|
-
);
|
|
4935
|
-
}
|
|
4936
|
-
return await res.json();
|
|
4937
|
-
}
|
|
4938
|
-
};
|
|
4882
|
+
function profilesConfigPath(home = neatHome()) {
|
|
4883
|
+
return path11.join(home, "profiles.json");
|
|
4939
4884
|
}
|
|
4940
|
-
function
|
|
4941
|
-
|
|
4942
|
-
return `/projects/${encodeURIComponent(project)}${suffix}`;
|
|
4885
|
+
function profilesConfigLockPath(home = neatHome()) {
|
|
4886
|
+
return path11.join(home, "profiles.json.lock");
|
|
4943
4887
|
}
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
input.project,
|
|
4948
|
-
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
4949
|
-
);
|
|
4888
|
+
var MODE_MASK_LOOSER_THAN_0600 = 63;
|
|
4889
|
+
async function warnIfModeLooserThan0600(file) {
|
|
4890
|
+
if (process.platform === "win32") return;
|
|
4950
4891
|
try {
|
|
4951
|
-
const
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
`Edge provenances: ${provenances}`
|
|
4958
|
-
];
|
|
4959
|
-
if (result.fixRecommendation) blockLines.push(`Recommended fix: ${result.fixRecommendation}`);
|
|
4960
|
-
return {
|
|
4961
|
-
summary,
|
|
4962
|
-
block: blockLines.join("\n"),
|
|
4963
|
-
confidence: result.confidence,
|
|
4964
|
-
provenance: result.edgeProvenances.length ? result.edgeProvenances : void 0
|
|
4965
|
-
};
|
|
4966
|
-
} catch (err) {
|
|
4967
|
-
if (err instanceof HttpError && err.status === 404) {
|
|
4968
|
-
return {
|
|
4969
|
-
summary: `No root cause found for ${input.errorNode}. The node may be healthy, or it may not exist in the graph.`
|
|
4970
|
-
};
|
|
4892
|
+
const stat = await fs10.stat(file);
|
|
4893
|
+
if ((stat.mode & MODE_MASK_LOOSER_THAN_0600) !== 0) {
|
|
4894
|
+
const mode = (stat.mode & 511).toString(8).padStart(3, "0");
|
|
4895
|
+
console.warn(
|
|
4896
|
+
`[neat] ${file} is mode 0${mode}, looser than the 0600 this file's token calls for \u2014 run \`chmod 600 ${file}\``
|
|
4897
|
+
);
|
|
4971
4898
|
}
|
|
4972
|
-
|
|
4899
|
+
} catch {
|
|
4973
4900
|
}
|
|
4974
4901
|
}
|
|
4975
|
-
async function
|
|
4976
|
-
const
|
|
4977
|
-
|
|
4978
|
-
input.project,
|
|
4979
|
-
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
4980
|
-
);
|
|
4902
|
+
async function readProfilesConfig(home = neatHome()) {
|
|
4903
|
+
const file = profilesConfigPath(home);
|
|
4904
|
+
let raw;
|
|
4981
4905
|
try {
|
|
4982
|
-
|
|
4983
|
-
if (result.totalAffected === 0) {
|
|
4984
|
-
return {
|
|
4985
|
-
summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
|
|
4986
|
-
};
|
|
4987
|
-
}
|
|
4988
|
-
const sorted = [...result.affectedNodes].sort(
|
|
4989
|
-
(a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
|
|
4990
|
-
);
|
|
4991
|
-
const blockLines = sorted.map(formatBlastEntry);
|
|
4992
|
-
const minConfidence = sorted.reduce(
|
|
4993
|
-
(m, n) => Math.min(m, n.confidence),
|
|
4994
|
-
Number.POSITIVE_INFINITY
|
|
4995
|
-
);
|
|
4996
|
-
const provenances = [...new Set(sorted.map((n) => n.edgeProvenance))];
|
|
4997
|
-
return {
|
|
4998
|
-
summary: `Blast radius for ${result.origin}: ${result.totalAffected} dependent node${result.totalAffected === 1 ? "" : "s"} would break if it changed.`,
|
|
4999
|
-
block: blockLines.join("\n"),
|
|
5000
|
-
confidence: Number.isFinite(minConfidence) ? minConfidence : void 0,
|
|
5001
|
-
provenance: provenances.length ? provenances : void 0
|
|
5002
|
-
};
|
|
4906
|
+
raw = await fs10.readFile(file, "utf8");
|
|
5003
4907
|
} catch (err) {
|
|
5004
|
-
if (err
|
|
5005
|
-
return {
|
|
4908
|
+
if (err.code === "ENOENT") {
|
|
4909
|
+
return { version: PROFILES_CONFIG_VERSION, profiles: [] };
|
|
5006
4910
|
}
|
|
5007
4911
|
throw err;
|
|
5008
4912
|
}
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
const tag = n.edgeProvenance === Provenance2.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
5012
|
-
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
5013
|
-
}
|
|
5014
|
-
async function runDependencies(client, input) {
|
|
5015
|
-
const depth = input.depth ?? 3;
|
|
5016
|
-
const path19 = projectPath(
|
|
5017
|
-
input.project,
|
|
5018
|
-
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
5019
|
-
);
|
|
4913
|
+
await warnIfModeLooserThan0600(file);
|
|
4914
|
+
let parsed;
|
|
5020
4915
|
try {
|
|
5021
|
-
|
|
5022
|
-
if (result.total === 0) {
|
|
5023
|
-
return {
|
|
5024
|
-
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
5025
|
-
};
|
|
5026
|
-
}
|
|
5027
|
-
const byDistance = /* @__PURE__ */ new Map();
|
|
5028
|
-
for (const dep of result.dependencies) {
|
|
5029
|
-
const ring = byDistance.get(dep.distance) ?? [];
|
|
5030
|
-
ring.push(dep);
|
|
5031
|
-
byDistance.set(dep.distance, ring);
|
|
5032
|
-
}
|
|
5033
|
-
const blockLines = [];
|
|
5034
|
-
for (const distance of [...byDistance.keys()].sort((a, b) => a - b)) {
|
|
5035
|
-
const label = distance === 1 ? "Direct (distance 1)" : `Distance ${distance}`;
|
|
5036
|
-
blockLines.push(`${label}:`);
|
|
5037
|
-
for (const dep of byDistance.get(distance)) {
|
|
5038
|
-
blockLines.push(` \u2022 ${dep.nodeId} \u2014 ${dep.edgeType} (${dep.provenance})`);
|
|
5039
|
-
}
|
|
5040
|
-
}
|
|
5041
|
-
const provenances = [...new Set(result.dependencies.map((d) => d.provenance))];
|
|
5042
|
-
const directCount = byDistance.get(1)?.length ?? 0;
|
|
5043
|
-
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).`;
|
|
5044
|
-
return { summary, block: blockLines.join("\n"), provenance: provenances };
|
|
4916
|
+
parsed = JSON.parse(raw);
|
|
5045
4917
|
} catch (err) {
|
|
5046
|
-
|
|
5047
|
-
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
5048
|
-
}
|
|
5049
|
-
throw err;
|
|
4918
|
+
throw new Error(`${file} is not valid JSON: ${err.message}`);
|
|
5050
4919
|
}
|
|
4920
|
+
return validateConfig(parsed, file);
|
|
5051
4921
|
}
|
|
5052
|
-
function
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
}
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
);
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
return {
|
|
5076
|
-
summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
5077
|
-
block: blockLines.join("\n"),
|
|
5078
|
-
provenance: Provenance2.OBSERVED
|
|
5079
|
-
};
|
|
5080
|
-
} catch (err) {
|
|
5081
|
-
if (err instanceof HttpError && err.status === 404) {
|
|
5082
|
-
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
4922
|
+
function validateConfig(parsed, file) {
|
|
4923
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4924
|
+
throw new Error(`${file} must be a JSON object with a "profiles" array`);
|
|
4925
|
+
}
|
|
4926
|
+
const obj = parsed;
|
|
4927
|
+
const version = obj.version === void 0 ? PROFILES_CONFIG_VERSION : obj.version;
|
|
4928
|
+
if (typeof version !== "number" || !Number.isInteger(version)) {
|
|
4929
|
+
throw new Error(`${file}: "version" must be an integer`);
|
|
4930
|
+
}
|
|
4931
|
+
const rawProfiles = obj.profiles;
|
|
4932
|
+
if (!Array.isArray(rawProfiles)) {
|
|
4933
|
+
throw new Error(`${file}: "profiles" must be an array`);
|
|
4934
|
+
}
|
|
4935
|
+
const profiles = rawProfiles.map((entry2, i) => validateEntry(entry2, i, file));
|
|
4936
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4937
|
+
for (const p of profiles) {
|
|
4938
|
+
if (seen.has(p.name)) throw new Error(`${file}: duplicate profile name "${p.name}"`);
|
|
4939
|
+
seen.add(p.name);
|
|
4940
|
+
}
|
|
4941
|
+
let active;
|
|
4942
|
+
if (obj.active !== void 0) {
|
|
4943
|
+
if (typeof obj.active !== "string" || obj.active.length === 0) {
|
|
4944
|
+
throw new Error(`${file}: "active" must be a non-empty string when present`);
|
|
5083
4945
|
}
|
|
5084
|
-
|
|
4946
|
+
active = seen.has(obj.active) ? obj.active : void 0;
|
|
5085
4947
|
}
|
|
4948
|
+
return { version, ...active ? { active } : {}, profiles };
|
|
5086
4949
|
}
|
|
5087
|
-
function
|
|
5088
|
-
const
|
|
5089
|
-
if (
|
|
5090
|
-
|
|
5091
|
-
if (e.signal.errorCount > 0) bits.push(`errors=${e.signal.errorCount}`);
|
|
5092
|
-
if (e.signal.lastObservedAgeMs !== void 0) {
|
|
5093
|
-
bits.push(`age=${formatDuration(e.signal.lastObservedAgeMs)}`);
|
|
5094
|
-
}
|
|
5095
|
-
} else if (e.callCount !== void 0) {
|
|
5096
|
-
bits.push(`callCount=${e.callCount}`);
|
|
4950
|
+
function validateEntry(entry2, index, file) {
|
|
4951
|
+
const where = `${file}: profiles[${index}]`;
|
|
4952
|
+
if (typeof entry2 !== "object" || entry2 === null || Array.isArray(entry2)) {
|
|
4953
|
+
throw new Error(`${where} must be an object`);
|
|
5097
4954
|
}
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
}
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
const h = Math.round(m / 60);
|
|
5109
|
-
if (h < 48) return `${h}h`;
|
|
5110
|
-
return `${Math.round(h / 24)}d`;
|
|
5111
|
-
}
|
|
5112
|
-
async function runIncidents(client, input) {
|
|
5113
|
-
const path19 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
4955
|
+
const e = entry2;
|
|
4956
|
+
const name = e.name;
|
|
4957
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
4958
|
+
throw new Error(`${where}.name must be a non-empty string`);
|
|
4959
|
+
}
|
|
4960
|
+
const endpoint = e.endpoint;
|
|
4961
|
+
if (typeof endpoint !== "string" || endpoint.length === 0) {
|
|
4962
|
+
throw new Error(`${where}.endpoint must be a non-empty string`);
|
|
4963
|
+
}
|
|
4964
|
+
let parsedUrl;
|
|
5114
4965
|
try {
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
return {
|
|
5119
|
-
summary: input.nodeId ? `No incidents recorded against ${input.nodeId}.` : "No incidents recorded."
|
|
5120
|
-
};
|
|
5121
|
-
}
|
|
5122
|
-
const ordered = [...events].reverse().slice(0, input.limit ?? 20);
|
|
5123
|
-
const blockLines = [];
|
|
5124
|
-
for (const ev of ordered) {
|
|
5125
|
-
blockLines.push(` ${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`);
|
|
5126
|
-
blockLines.push(` trace=${ev.traceId} span=${ev.spanId}`);
|
|
5127
|
-
}
|
|
5128
|
-
const target = input.nodeId ?? "the project";
|
|
5129
|
-
return {
|
|
5130
|
-
summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
5131
|
-
block: blockLines.join("\n"),
|
|
5132
|
-
provenance: Provenance2.OBSERVED
|
|
5133
|
-
};
|
|
5134
|
-
} catch (err) {
|
|
5135
|
-
if (err instanceof HttpError && err.status === 404) {
|
|
5136
|
-
return { summary: `Node ${input.nodeId ?? ""} not found in the graph.` };
|
|
5137
|
-
}
|
|
5138
|
-
throw err;
|
|
4966
|
+
parsedUrl = new URL(endpoint);
|
|
4967
|
+
} catch {
|
|
4968
|
+
throw new Error(`${where}.endpoint must be an absolute URL (got "${endpoint}")`);
|
|
5139
4969
|
}
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
const result = await client.get(
|
|
5143
|
-
projectPath(input.project, `/search?q=${encodeURIComponent(input.query)}`)
|
|
5144
|
-
);
|
|
5145
|
-
if (result.matches.length === 0) {
|
|
5146
|
-
return { summary: `No matches for "${input.query}".` };
|
|
4970
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
4971
|
+
throw new Error(`${where}.endpoint must be an http(s) URL (got "${parsedUrl.protocol}")`);
|
|
5147
4972
|
}
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
let topScore;
|
|
5151
|
-
for (const n of result.matches) {
|
|
5152
|
-
const score = provider !== "substring" && typeof n.score === "number" ? n.score : void 0;
|
|
5153
|
-
const scoreBit = score !== void 0 ? ` [score=${score.toFixed(2)}]` : "";
|
|
5154
|
-
if (score !== void 0 && (topScore === void 0 || score > topScore)) topScore = score;
|
|
5155
|
-
blockLines.push(
|
|
5156
|
-
` \u2022 ${n.id} (${n.type}) \u2014 ${n.name ?? n.id}${scoreBit}`
|
|
5157
|
-
);
|
|
4973
|
+
if (e.authToken !== void 0 && (typeof e.authToken !== "string" || e.authToken.length === 0)) {
|
|
4974
|
+
throw new Error(`${where}.authToken must be a non-empty string when present`);
|
|
5158
4975
|
}
|
|
5159
4976
|
return {
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
4977
|
+
name,
|
|
4978
|
+
endpoint,
|
|
4979
|
+
...typeof e.authToken === "string" ? { authToken: e.authToken } : {}
|
|
5163
4980
|
};
|
|
5164
4981
|
}
|
|
5165
|
-
async function
|
|
5166
|
-
const
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
);
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
blockLines.push(` - edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
|
|
5196
|
-
blockLines.push("");
|
|
4982
|
+
async function resolveProfile(name, home = neatHome()) {
|
|
4983
|
+
const { profiles } = await readProfilesConfig(home);
|
|
4984
|
+
return profiles.find((p) => p.name === name);
|
|
4985
|
+
}
|
|
4986
|
+
async function getActiveProfile(home = neatHome()) {
|
|
4987
|
+
const { active, profiles } = await readProfilesConfig(home);
|
|
4988
|
+
if (!active) return void 0;
|
|
4989
|
+
return profiles.find((p) => p.name === active);
|
|
4990
|
+
}
|
|
4991
|
+
function serialize(config) {
|
|
4992
|
+
const names = new Set(config.profiles.map((p) => p.name));
|
|
4993
|
+
const active = config.active && names.has(config.active) ? config.active : void 0;
|
|
4994
|
+
const out = {
|
|
4995
|
+
version: config.version ?? PROFILES_CONFIG_VERSION,
|
|
4996
|
+
...active ? { active } : {},
|
|
4997
|
+
profiles: config.profiles
|
|
4998
|
+
};
|
|
4999
|
+
return `${JSON.stringify(out, null, 2)}
|
|
5000
|
+
`;
|
|
5001
|
+
}
|
|
5002
|
+
async function writeConfigAtomic(config, home) {
|
|
5003
|
+
const file = profilesConfigPath(home);
|
|
5004
|
+
await fs10.mkdir(path11.dirname(file), { recursive: true });
|
|
5005
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
5006
|
+
const fd = await fs10.open(tmp, "w", 384);
|
|
5007
|
+
try {
|
|
5008
|
+
await fd.writeFile(serialize(config), "utf8");
|
|
5009
|
+
await fd.sync();
|
|
5010
|
+
} finally {
|
|
5011
|
+
await fd.close();
|
|
5197
5012
|
}
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5013
|
+
await fs10.rename(tmp, file);
|
|
5014
|
+
}
|
|
5015
|
+
var LOCK_RETRY_MS = 50;
|
|
5016
|
+
var LOCK_TIMEOUT_MS = 5e3;
|
|
5017
|
+
async function acquireLock(lockPath) {
|
|
5018
|
+
await fs10.mkdir(path11.dirname(lockPath), { recursive: true });
|
|
5019
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
5020
|
+
for (; ; ) {
|
|
5021
|
+
try {
|
|
5022
|
+
const fd = await fs10.open(lockPath, "wx");
|
|
5023
|
+
await fd.writeFile(`${process.pid}
|
|
5024
|
+
`, "utf8");
|
|
5025
|
+
await fd.close();
|
|
5026
|
+
return;
|
|
5027
|
+
} catch (err) {
|
|
5028
|
+
if (err.code !== "EEXIST") throw err;
|
|
5029
|
+
if (Date.now() >= deadline) {
|
|
5030
|
+
throw new Error(
|
|
5031
|
+
`timed out acquiring ${lockPath} after ${LOCK_TIMEOUT_MS}ms \u2014 if no other neat process is running, remove the stale lock file`
|
|
5032
|
+
);
|
|
5033
|
+
}
|
|
5034
|
+
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
|
|
5206
5035
|
}
|
|
5207
5036
|
}
|
|
5208
|
-
return {
|
|
5209
|
-
summary: `Diff against ${input.againstSnapshot}: ${total} change${total === 1 ? "" : "s"} between the snapshot and the live graph.`,
|
|
5210
|
-
block: blockLines.join("\n").trimEnd()
|
|
5211
|
-
};
|
|
5212
5037
|
}
|
|
5213
|
-
function
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5038
|
+
async function releaseLock(lockPath) {
|
|
5039
|
+
await fs10.rm(lockPath, { force: true });
|
|
5040
|
+
}
|
|
5041
|
+
async function withProfilesLock(home, fn) {
|
|
5042
|
+
const lockPath = profilesConfigLockPath(home);
|
|
5043
|
+
await acquireLock(lockPath);
|
|
5044
|
+
try {
|
|
5045
|
+
return await fn();
|
|
5046
|
+
} finally {
|
|
5047
|
+
await releaseLock(lockPath);
|
|
5218
5048
|
}
|
|
5219
|
-
return changed.length === 0 ? "attributes differ" : `fields changed: ${changed.sort().join(", ")}`;
|
|
5220
5049
|
}
|
|
5221
|
-
async function
|
|
5222
|
-
const
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5050
|
+
async function upsertProfile(profile, opts = {}) {
|
|
5051
|
+
const home = opts.home ?? neatHome();
|
|
5052
|
+
const validated = validateEntry(profile, 0, profilesConfigPath(home));
|
|
5053
|
+
await withProfilesLock(home, async () => {
|
|
5054
|
+
const config = await readProfilesConfig(home);
|
|
5055
|
+
const others = config.profiles.filter((p) => p.name !== validated.name);
|
|
5056
|
+
const profiles = [...others, validated];
|
|
5057
|
+
const makeActive = opts.makeActive ?? config.profiles.length === 0;
|
|
5058
|
+
const active = makeActive ? validated.name : config.active;
|
|
5059
|
+
await writeConfigAtomic({ version: config.version, ...active ? { active } : {}, profiles }, home);
|
|
5060
|
+
});
|
|
5061
|
+
}
|
|
5062
|
+
async function removeProfile(name, home = neatHome()) {
|
|
5063
|
+
return withProfilesLock(home, async () => {
|
|
5064
|
+
const config = await readProfilesConfig(home);
|
|
5065
|
+
const profiles = config.profiles.filter((p) => p.name !== name);
|
|
5066
|
+
if (profiles.length === config.profiles.length) return false;
|
|
5067
|
+
const active = config.active === name ? void 0 : config.active;
|
|
5068
|
+
await writeConfigAtomic({ version: config.version, ...active ? { active } : {}, profiles }, home);
|
|
5069
|
+
return true;
|
|
5070
|
+
});
|
|
5071
|
+
}
|
|
5072
|
+
async function clearActiveProfile(home = neatHome()) {
|
|
5073
|
+
return withProfilesLock(home, async () => {
|
|
5074
|
+
const config = await readProfilesConfig(home);
|
|
5075
|
+
if (!config.active) return false;
|
|
5076
|
+
await writeConfigAtomic({ version: config.version, profiles: config.profiles }, home);
|
|
5077
|
+
return true;
|
|
5078
|
+
});
|
|
5079
|
+
}
|
|
5080
|
+
|
|
5081
|
+
// src/login-sso.ts
|
|
5082
|
+
var DEFAULT_CP_URL = "https://neat-control-plane-bg5yqctn2q-nw.a.run.app";
|
|
5083
|
+
var DEFAULT_WEB_URL = "https://app.neat.is";
|
|
5084
|
+
var CALLBACK_TIMEOUT_MS = 5 * 6e4;
|
|
5085
|
+
function resolveCpUrl(env = process.env, override) {
|
|
5086
|
+
const v = override ?? env.NEAT_CP_URL;
|
|
5087
|
+
return (v && v.length > 0 ? v : DEFAULT_CP_URL).replace(/\/+$/, "");
|
|
5088
|
+
}
|
|
5089
|
+
function resolveWebUrl(env = process.env, override) {
|
|
5090
|
+
const v = override ?? env.NEAT_WEB_URL;
|
|
5091
|
+
return (v && v.length > 0 ? v : DEFAULT_WEB_URL).replace(/\/+$/, "");
|
|
5092
|
+
}
|
|
5093
|
+
function pickProject(projects, want) {
|
|
5094
|
+
if (want) {
|
|
5095
|
+
const p = projects.find((x) => x.id === want || x.name === want);
|
|
5096
|
+
if (!p) return { error: { code: 1, message: `no project named or id'd "${want}" on this account` } };
|
|
5097
|
+
return { project: p };
|
|
5098
|
+
}
|
|
5099
|
+
const running = projects.filter((p) => p.status === "running");
|
|
5100
|
+
if (running.length === 1) return { project: running[0] };
|
|
5101
|
+
if (running.length === 0) {
|
|
5102
|
+
const listed = projects.length ? ` (have: ${projects.map((p) => `${p.name} [${p.status}]`).join(", ")})` : "";
|
|
5231
5103
|
return {
|
|
5232
|
-
|
|
5104
|
+
error: {
|
|
5105
|
+
code: 1,
|
|
5106
|
+
message: `no running project to connect to \u2014 create + provision one in the console first${listed}`
|
|
5107
|
+
}
|
|
5233
5108
|
};
|
|
5234
5109
|
}
|
|
5235
|
-
const blockLines = events.map(
|
|
5236
|
-
(e) => ` ${e.transitionedAt} \u2014 ${e.source} -[${e.edgeType}]-> ${e.target} (last seen ${e.lastObserved}, threshold ${formatDuration(e.thresholdMs)})`
|
|
5237
|
-
);
|
|
5238
5110
|
return {
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5111
|
+
error: {
|
|
5112
|
+
code: 2,
|
|
5113
|
+
message: `several running projects \u2014 pass --project <name>: ${running.map((p) => p.name).join(", ")}`
|
|
5114
|
+
}
|
|
5242
5115
|
};
|
|
5243
5116
|
}
|
|
5244
|
-
async function
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
let
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
}
|
|
5252
|
-
const body = await client.post(
|
|
5253
|
-
projectPath(input.project, "/policies/check"),
|
|
5254
|
-
{ hypotheticalAction: input.hypotheticalAction }
|
|
5255
|
-
);
|
|
5256
|
-
violations = body.violations;
|
|
5257
|
-
allowed = body.allowed;
|
|
5258
|
-
hypothetical = body.hypotheticalAction;
|
|
5259
|
-
} else {
|
|
5260
|
-
const params = new URLSearchParams();
|
|
5261
|
-
if (input.policyId) params.set("policyId", input.policyId);
|
|
5262
|
-
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
5263
|
-
const body = await client.get(
|
|
5264
|
-
projectPath(input.project, `/policies/violations${qs}`)
|
|
5265
|
-
);
|
|
5266
|
-
violations = body.violations;
|
|
5267
|
-
allowed = violations.every((v) => v.onViolation !== "block");
|
|
5117
|
+
async function exchangeCredential(cpUrl, accessToken, opts, deps) {
|
|
5118
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
5119
|
+
const auth = { authorization: `Bearer ${accessToken}` };
|
|
5120
|
+
let meRes;
|
|
5121
|
+
try {
|
|
5122
|
+
meRes = await fetchImpl(`${cpUrl}/me`, { headers: auth });
|
|
5123
|
+
} catch (e) {
|
|
5124
|
+
return { error: { code: 3, message: `can't reach the control plane at ${cpUrl} \u2014 ${e.message}` } };
|
|
5268
5125
|
}
|
|
5269
|
-
if (
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5126
|
+
if (meRes.status === 401) return { error: { code: 1, message: "your session is expired or invalid \u2014 log in again" } };
|
|
5127
|
+
if (!meRes.ok) return { error: { code: 1, message: `the control plane returned HTTP ${meRes.status} on /me` } };
|
|
5128
|
+
const me = await meRes.json().catch(() => ({}));
|
|
5129
|
+
const projects = Array.isArray(me.projects) ? me.projects : [];
|
|
5130
|
+
const picked = pickProject(projects, opts.project);
|
|
5131
|
+
if ("error" in picked) return picked;
|
|
5132
|
+
const project = picked.project;
|
|
5133
|
+
let credRes;
|
|
5134
|
+
try {
|
|
5135
|
+
credRes = await fetchImpl(`${cpUrl}/me/projects/${encodeURIComponent(project.id)}/cli-credential`, {
|
|
5136
|
+
headers: auth
|
|
5137
|
+
});
|
|
5138
|
+
} catch (e) {
|
|
5139
|
+
return { error: { code: 3, message: `can't reach the control plane \u2014 ${e.message}` } };
|
|
5273
5140
|
}
|
|
5274
|
-
if (
|
|
5141
|
+
if (credRes.status === 409) {
|
|
5275
5142
|
return {
|
|
5276
|
-
|
|
5143
|
+
error: {
|
|
5144
|
+
code: 1,
|
|
5145
|
+
message: `project "${project.name}" isn't provisioned yet (status: ${project.status}) \u2014 provision it first`
|
|
5146
|
+
}
|
|
5277
5147
|
};
|
|
5278
5148
|
}
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
if (
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
} else {
|
|
5286
|
-
summaryParts.push(
|
|
5287
|
-
`${violations.length} policy violation${violations.length === 1 ? "" : "s"} currently recorded`
|
|
5288
|
-
);
|
|
5149
|
+
if (credRes.status === 404) return { error: { code: 1, message: `project "${project.name}" was not found, or isn't yours` } };
|
|
5150
|
+
if (credRes.status === 401) return { error: { code: 1, message: "your session is expired or invalid \u2014 log in again" } };
|
|
5151
|
+
if (!credRes.ok) return { error: { code: 1, message: `the control plane returned HTTP ${credRes.status} for the credential` } };
|
|
5152
|
+
const cred = await credRes.json().catch(() => ({}));
|
|
5153
|
+
if (!cred.endpoint || !cred.authToken) {
|
|
5154
|
+
return { error: { code: 1, message: "the credential response was missing endpoint/authToken" } };
|
|
5289
5155
|
}
|
|
5290
|
-
|
|
5291
|
-
if (!allowed && hypothetical) summaryParts.push("action denied");
|
|
5292
|
-
const summary = summaryParts.join("; ") + ".";
|
|
5293
|
-
const blockLines = violations.map((v) => {
|
|
5294
|
-
const subject = v.subject.nodeId ?? v.subject.edgeId ?? v.subject.path?.[0] ?? "(global)";
|
|
5295
|
-
return ` \u2022 [${v.severity}/${v.onViolation}] ${v.policyName}: ${v.message} \u2014 ${subject}`;
|
|
5296
|
-
});
|
|
5297
|
-
const severities = [...new Set(violations.map((v) => v.severity))];
|
|
5298
|
-
return {
|
|
5299
|
-
summary,
|
|
5300
|
-
block: blockLines.join("\n"),
|
|
5301
|
-
confidence: hypothetical ? 0.7 : 1,
|
|
5302
|
-
provenance: severities.join(" ")
|
|
5303
|
-
};
|
|
5156
|
+
return { project, cred };
|
|
5304
5157
|
}
|
|
5305
|
-
function
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5158
|
+
function defaultOpenBrowser(url) {
|
|
5159
|
+
const platform = process.platform;
|
|
5160
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
5161
|
+
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
5162
|
+
try {
|
|
5163
|
+
const child = spawn3(cmd, args, { detached: true, stdio: "ignore" });
|
|
5164
|
+
child.on("error", () => {
|
|
5165
|
+
});
|
|
5166
|
+
child.unref();
|
|
5167
|
+
return true;
|
|
5168
|
+
} catch {
|
|
5169
|
+
return false;
|
|
5170
|
+
}
|
|
5171
|
+
}
|
|
5172
|
+
async function loopbackReceiveToken(webUrl, deps, opts = {}) {
|
|
5173
|
+
const out = deps.out ?? (() => {
|
|
5174
|
+
});
|
|
5175
|
+
const openFn = deps.openBrowser ?? defaultOpenBrowser;
|
|
5176
|
+
const state = randomBytes2(16).toString("hex");
|
|
5177
|
+
return new Promise((resolve) => {
|
|
5178
|
+
let settled = false;
|
|
5179
|
+
let timer;
|
|
5180
|
+
const done = (r) => {
|
|
5181
|
+
if (settled) return;
|
|
5182
|
+
settled = true;
|
|
5183
|
+
clearTimeout(timer);
|
|
5184
|
+
try {
|
|
5185
|
+
server.close();
|
|
5186
|
+
} catch {
|
|
5311
5187
|
}
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
const at = d.location ? ` at ${d.location}` : "";
|
|
5321
|
-
const member = d.symbol ? ` ${d.symbol}` : "";
|
|
5322
|
-
return ` \u2022 [${d.type}] ${d.source}${member}${at} (${d.mismatchKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
5323
|
-
}
|
|
5324
|
-
case "observed-failing": {
|
|
5325
|
-
if (d.edgeType) {
|
|
5326
|
-
const rate = d.errorRate !== void 0 ? ` ${Math.round(d.errorRate * 100)}% errors` : "";
|
|
5327
|
-
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} (${d.edgeType}) \u2014${rate} (${d.failureKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
5188
|
+
resolve(r);
|
|
5189
|
+
};
|
|
5190
|
+
const server = createServer((req, res) => {
|
|
5191
|
+
const reqUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
5192
|
+
if (reqUrl.pathname !== "/callback") {
|
|
5193
|
+
res.writeHead(404);
|
|
5194
|
+
res.end();
|
|
5195
|
+
return;
|
|
5328
5196
|
}
|
|
5329
|
-
const
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5197
|
+
const token = reqUrl.searchParams.get("token");
|
|
5198
|
+
const gotState = reqUrl.searchParams.get("state");
|
|
5199
|
+
if (!token || gotState !== state) {
|
|
5200
|
+
res.writeHead(400, { "content-type": "text/html" });
|
|
5201
|
+
res.end("<h1>NEAT login failed</h1><p>Invalid or mismatched token. You can close this tab.</p>");
|
|
5202
|
+
done({ error: { code: 1, message: "the browser returned an invalid or mismatched token" } });
|
|
5203
|
+
return;
|
|
5335
5204
|
}
|
|
5336
|
-
|
|
5205
|
+
res.writeHead(200, { "content-type": "text/html" });
|
|
5206
|
+
res.end("<h1>You're logged in to NEAT.</h1><p>You can close this tab and return to the terminal.</p>");
|
|
5207
|
+
done({ token });
|
|
5208
|
+
});
|
|
5209
|
+
server.on("error", (e) => done({ error: { code: 3, message: `couldn't start the local login listener \u2014 ${e.message}` } }));
|
|
5210
|
+
timer = setTimeout(
|
|
5211
|
+
() => done({ error: { code: 1, message: "timed out waiting for the browser login" } }),
|
|
5212
|
+
opts.timeoutMs ?? CALLBACK_TIMEOUT_MS
|
|
5213
|
+
);
|
|
5214
|
+
server.listen(0, "127.0.0.1", () => {
|
|
5215
|
+
const addr = server.address();
|
|
5216
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
5217
|
+
const callback = `http://127.0.0.1:${port}/callback`;
|
|
5218
|
+
const authUrl = `${webUrl}/cli/auth?callback=${encodeURIComponent(callback)}&state=${state}`;
|
|
5219
|
+
out("Opening your browser to log in to NEAT\u2026");
|
|
5220
|
+
out(`If it doesn't open, visit:
|
|
5221
|
+
${authUrl}`);
|
|
5222
|
+
openFn(authUrl);
|
|
5223
|
+
});
|
|
5224
|
+
});
|
|
5225
|
+
}
|
|
5226
|
+
async function runSsoLogin(opts, deps) {
|
|
5227
|
+
const out = deps.out ?? ((l) => console.log(l));
|
|
5228
|
+
const err = deps.err ?? ((l) => console.error(l));
|
|
5229
|
+
let accessToken = opts.ssoToken;
|
|
5230
|
+
if (!accessToken) {
|
|
5231
|
+
const lb = await loopbackReceiveToken(opts.webUrl, deps, { timeoutMs: opts.timeoutMs ?? CALLBACK_TIMEOUT_MS });
|
|
5232
|
+
if ("error" in lb) {
|
|
5233
|
+
err(`neat login: ${lb.error.message}`);
|
|
5234
|
+
return lb.error.code;
|
|
5337
5235
|
}
|
|
5236
|
+
accessToken = lb.token;
|
|
5338
5237
|
}
|
|
5339
|
-
}
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
if (input.minConfidence !== void 0) {
|
|
5344
|
-
params.set("minConfidence", String(input.minConfidence));
|
|
5345
|
-
}
|
|
5346
|
-
if (input.node) params.set("node", input.node);
|
|
5347
|
-
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
5348
|
-
const result = await client.get(
|
|
5349
|
-
projectPath(input.project, `/graph/divergences${qs}`)
|
|
5350
|
-
);
|
|
5351
|
-
if (result.totalAffected === 0) {
|
|
5352
|
-
return {
|
|
5353
|
-
summary: "No divergences found between the declared (EXTRACTED) and observed (OBSERVED) views of the graph."
|
|
5354
|
-
};
|
|
5355
|
-
}
|
|
5356
|
-
const headline = result.divergences[0];
|
|
5357
|
-
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}`;
|
|
5358
|
-
const blockLines = [];
|
|
5359
|
-
for (const d of result.divergences) {
|
|
5360
|
-
blockLines.push(formatDivergenceLine(d));
|
|
5361
|
-
blockLines.push(` reason: ${d.reason}`);
|
|
5362
|
-
blockLines.push(` recommendation: ${d.recommendation}`);
|
|
5238
|
+
const ex = await exchangeCredential(opts.cpUrl, accessToken, { project: opts.project }, deps);
|
|
5239
|
+
if ("error" in ex) {
|
|
5240
|
+
err(`neat login: ${ex.error.message}`);
|
|
5241
|
+
return ex.error.code;
|
|
5363
5242
|
}
|
|
5364
|
-
const
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
return {
|
|
5369
|
-
summary,
|
|
5370
|
-
block: blockLines.join("\n"),
|
|
5371
|
-
confidence: maxConfidence,
|
|
5372
|
-
provenance: "composite (EXTRACTED + OBSERVED)"
|
|
5373
|
-
};
|
|
5374
|
-
}
|
|
5375
|
-
async function runAsk(client, input) {
|
|
5376
|
-
const result = await client.get(
|
|
5377
|
-
projectPath(input.project, `/graph/ask?q=${encodeURIComponent(input.question)}`)
|
|
5243
|
+
const { project, cred } = ex;
|
|
5244
|
+
await upsertProfile(
|
|
5245
|
+
{ name: opts.name, endpoint: cred.endpoint, authToken: cred.authToken },
|
|
5246
|
+
{ makeActive: true, ...deps.home ? { home: deps.home } : {} }
|
|
5378
5247
|
);
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5248
|
+
if (opts.json) {
|
|
5249
|
+
out(
|
|
5250
|
+
JSON.stringify(
|
|
5251
|
+
{
|
|
5252
|
+
status: "logged-in",
|
|
5253
|
+
profile: opts.name,
|
|
5254
|
+
project: project.name,
|
|
5255
|
+
endpoint: cred.endpoint,
|
|
5256
|
+
...cred.ingestEndpoint ? { ingestEndpoint: cred.ingestEndpoint } : {}
|
|
5257
|
+
},
|
|
5258
|
+
null,
|
|
5259
|
+
2
|
|
5260
|
+
)
|
|
5383
5261
|
);
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
blockLines.push(` \u2022 ${fact.text}${tag}`);
|
|
5262
|
+
} else {
|
|
5263
|
+
out(`Logged in \u2014 profile "${opts.name}" \u2192 ${project.name} (${cred.endpoint})`);
|
|
5264
|
+
out("The neat CLI and the MCP server now read this hosted graph by default.");
|
|
5265
|
+
if (cred.ingestEndpoint && cred.otelToken) {
|
|
5266
|
+
out("");
|
|
5267
|
+
out("To fill the OBSERVED layer, instrument your app to send OpenTelemetry to the hosted daemon:");
|
|
5268
|
+
out(` OTEL_EXPORTER_OTLP_ENDPOINT=${cred.ingestEndpoint}`);
|
|
5269
|
+
out(` OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${cred.otelToken}`);
|
|
5393
5270
|
}
|
|
5271
|
+
out("Run `neat logout` to switch back to your local daemon.");
|
|
5394
5272
|
}
|
|
5395
|
-
return
|
|
5396
|
-
summary: result.answer,
|
|
5397
|
-
block: blockLines.join("\n").trim(),
|
|
5398
|
-
...result.confidence !== void 0 ? { confidence: result.confidence } : {},
|
|
5399
|
-
...result.provenance.length > 0 ? { provenance: result.provenance } : {}
|
|
5400
|
-
};
|
|
5401
|
-
}
|
|
5402
|
-
function formatFooter(confidence, provenance) {
|
|
5403
|
-
const c = confidence === void 0 ? "n/a" : confidence.toFixed(2);
|
|
5404
|
-
const p = provenance === void 0 ? "n/a" : Array.isArray(provenance) ? [...new Set(provenance)].join(", ") : provenance;
|
|
5405
|
-
return `confidence: ${c} \xB7 provenance: ${p}`;
|
|
5406
|
-
}
|
|
5407
|
-
function formatHuman(result) {
|
|
5408
|
-
const sections = [result.summary.trim()];
|
|
5409
|
-
if (result.block && result.block.trim().length > 0) sections.push(result.block.trimEnd());
|
|
5410
|
-
sections.push(formatFooter(result.confidence, result.provenance));
|
|
5411
|
-
return sections.join("\n\n");
|
|
5412
|
-
}
|
|
5413
|
-
function formatJson(result) {
|
|
5414
|
-
return JSON.stringify(
|
|
5415
|
-
{
|
|
5416
|
-
summary: result.summary,
|
|
5417
|
-
block: result.block ?? "",
|
|
5418
|
-
confidence: result.confidence ?? null,
|
|
5419
|
-
provenance: result.provenance ?? null
|
|
5420
|
-
},
|
|
5421
|
-
null,
|
|
5422
|
-
2
|
|
5423
|
-
);
|
|
5273
|
+
return 0;
|
|
5424
5274
|
}
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5275
|
+
|
|
5276
|
+
// src/hosted-connect-cli.ts
|
|
5277
|
+
function defaultOpenBrowser2(url) {
|
|
5278
|
+
const platform = process.platform;
|
|
5279
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
5280
|
+
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
5281
|
+
try {
|
|
5282
|
+
const child = spawn4(cmd, args, { detached: true, stdio: "ignore" });
|
|
5283
|
+
child.on("error", () => {
|
|
5284
|
+
});
|
|
5285
|
+
child.unref();
|
|
5286
|
+
return true;
|
|
5287
|
+
} catch {
|
|
5288
|
+
return false;
|
|
5289
|
+
}
|
|
5429
5290
|
}
|
|
5430
|
-
function
|
|
5431
|
-
|
|
5291
|
+
function resolveCreds(env) {
|
|
5292
|
+
const apiKey = env.NEAT_API_KEY;
|
|
5293
|
+
const projectId = env.NEAT_CP_PROJECT_ID;
|
|
5294
|
+
if (!apiKey) return { error: "not logged in \u2014 run `neat login`, or set NEAT_API_KEY (a neat_pat_\u2026 key)" };
|
|
5295
|
+
if (!projectId) return { error: "no hosted project \u2014 run `neat login`, or set NEAT_CP_PROJECT_ID" };
|
|
5296
|
+
return { cpUrl: resolveCpUrl(env).replace(/\/+$/, ""), apiKey, projectId };
|
|
5432
5297
|
}
|
|
5433
|
-
async function
|
|
5434
|
-
const
|
|
5435
|
-
|
|
5436
|
-
|
|
5298
|
+
async function runConnectCommand(rawArgs, deps = {}) {
|
|
5299
|
+
const env = deps.env ?? process.env;
|
|
5300
|
+
const out = deps.out ?? ((l) => console.log(l));
|
|
5301
|
+
const err = deps.err ?? ((l) => console.error(l));
|
|
5302
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
5303
|
+
const open = deps.openBrowser ?? defaultOpenBrowser2;
|
|
5304
|
+
const pollMs = deps.pollMs ?? 2e3;
|
|
5305
|
+
const timeoutMs = deps.timeoutMs ?? 18e4;
|
|
5306
|
+
const sleep2 = deps.sleepImpl ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
5307
|
+
const provider = rawArgs.find((a) => !a.startsWith("-"));
|
|
5308
|
+
if (!provider || rawArgs.includes("-h") || rawArgs.includes("--help")) {
|
|
5309
|
+
out("usage: neat connect <provider> connect a provider to your hosted NEAT project over OAuth");
|
|
5310
|
+
out(" Opens the provider's consent screen in your browser and waits for the connection to land.");
|
|
5311
|
+
out(" Needs a hosted login (`neat login`) or NEAT_CP_URL + NEAT_API_KEY + NEAT_CP_PROJECT_ID.");
|
|
5312
|
+
out(" For a local/self-hosted daemon, use `neat connector add` instead.");
|
|
5313
|
+
return provider ? 0 : 2;
|
|
5314
|
+
}
|
|
5315
|
+
const creds = resolveCreds(env);
|
|
5316
|
+
if ("error" in creds) {
|
|
5317
|
+
err(`neat connect: ${creds.error}`);
|
|
5318
|
+
return 2;
|
|
5437
5319
|
}
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5320
|
+
const connectionsUrl = `${creds.cpUrl}/me/projects/${encodeURIComponent(creds.projectId)}/connections`;
|
|
5321
|
+
let authorizeUrl;
|
|
5322
|
+
try {
|
|
5323
|
+
const res = await fetchImpl(`${connectionsUrl}/${encodeURIComponent(provider)}/authorize`, {
|
|
5324
|
+
method: "POST",
|
|
5325
|
+
headers: { authorization: `Bearer ${creds.apiKey}` }
|
|
5326
|
+
});
|
|
5327
|
+
if (res.status === 401) {
|
|
5328
|
+
err("neat connect: not authorized \u2014 run `neat login` (or check NEAT_API_KEY).");
|
|
5329
|
+
return 1;
|
|
5330
|
+
}
|
|
5331
|
+
if (res.status === 404 || res.status === 501) {
|
|
5332
|
+
err(`neat connect: ${provider} isn't available to connect over OAuth yet.`);
|
|
5333
|
+
return 1;
|
|
5334
|
+
}
|
|
5335
|
+
if (!res.ok) {
|
|
5336
|
+
err(`neat connect: couldn't start connecting ${provider} (HTTP ${res.status}).`);
|
|
5337
|
+
return 1;
|
|
5338
|
+
}
|
|
5339
|
+
const body = await res.json().catch(() => ({}));
|
|
5340
|
+
if (!body.authorizeUrl) {
|
|
5341
|
+
err(`neat connect: the control plane returned no authorize URL for ${provider}.`);
|
|
5342
|
+
return 1;
|
|
5343
|
+
}
|
|
5344
|
+
authorizeUrl = body.authorizeUrl;
|
|
5345
|
+
} catch (e) {
|
|
5346
|
+
err(`neat connect: couldn't reach the control plane at ${creds.cpUrl} \u2014 ${e.message}`);
|
|
5347
|
+
return 1;
|
|
5348
|
+
}
|
|
5349
|
+
out(`Opening ${provider}'s consent screen in your browser\u2026`);
|
|
5350
|
+
if (!open(authorizeUrl)) out("Could not open a browser automatically \u2014 open this URL to authorize:");
|
|
5351
|
+
out(` ${authorizeUrl}`);
|
|
5352
|
+
out("Waiting for you to authorize\u2026");
|
|
5353
|
+
const deadline = Date.now() + timeoutMs;
|
|
5354
|
+
while (Date.now() < deadline) {
|
|
5355
|
+
await sleep2(pollMs);
|
|
5356
|
+
try {
|
|
5357
|
+
const res = await fetchImpl(connectionsUrl, { headers: { authorization: `Bearer ${creds.apiKey}` } });
|
|
5358
|
+
if (res.ok) {
|
|
5359
|
+
const conns = await res.json().catch(() => []);
|
|
5360
|
+
const hit = Array.isArray(conns) ? conns.find((c) => c.provider === provider) : void 0;
|
|
5361
|
+
if (hit) {
|
|
5362
|
+
out(`\u2713 ${provider} connected${hit.status ? ` (${hit.status})` : ""}.`);
|
|
5363
|
+
return 0;
|
|
5364
|
+
}
|
|
5365
|
+
}
|
|
5366
|
+
} catch {
|
|
5367
|
+
}
|
|
5368
|
+
}
|
|
5369
|
+
err(
|
|
5370
|
+
`neat connect: timed out waiting for ${provider}. If you authorized, run \`neat connect ${provider}\` again to re-check \u2014 the connection may still land.`
|
|
5441
5371
|
);
|
|
5372
|
+
return 1;
|
|
5442
5373
|
}
|
|
5443
5374
|
|
|
5444
5375
|
// src/doctor-cli.ts
|
|
5445
|
-
|
|
5376
|
+
import { promises as fs11 } from "fs";
|
|
5377
|
+
import path12 from "path";
|
|
5378
|
+
|
|
5379
|
+
// src/cli-client.ts
|
|
5380
|
+
import { Provenance as Provenance2 } from "@neat.is/types";
|
|
5381
|
+
var HttpError = class extends Error {
|
|
5382
|
+
constructor(status, message, responseBody = "") {
|
|
5383
|
+
super(message);
|
|
5384
|
+
this.status = status;
|
|
5385
|
+
this.responseBody = responseBody;
|
|
5386
|
+
this.name = "HttpError";
|
|
5387
|
+
}
|
|
5388
|
+
status;
|
|
5389
|
+
responseBody;
|
|
5390
|
+
};
|
|
5391
|
+
var TransportError = class extends Error {
|
|
5392
|
+
constructor(message) {
|
|
5393
|
+
super(message);
|
|
5394
|
+
this.name = "TransportError";
|
|
5395
|
+
}
|
|
5396
|
+
};
|
|
5397
|
+
function resolveAuthToken(env = process.env) {
|
|
5398
|
+
const t = env.NEAT_AUTH_TOKEN;
|
|
5399
|
+
return t && t.length > 0 ? t : void 0;
|
|
5400
|
+
}
|
|
5401
|
+
function createHttpClient(baseUrl, bearerToken, fetchImpl = fetch) {
|
|
5402
|
+
const root = baseUrl.replace(/\/$/, "");
|
|
5403
|
+
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
5446
5404
|
return {
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5405
|
+
async get(path19) {
|
|
5406
|
+
let res;
|
|
5407
|
+
try {
|
|
5408
|
+
res = await fetchImpl(`${root}${path19}`, {
|
|
5409
|
+
headers: { ...authHeader }
|
|
5410
|
+
});
|
|
5411
|
+
} catch (err) {
|
|
5412
|
+
throw new TransportError(
|
|
5413
|
+
`cannot reach neat-core at ${root}: ${err.message}`
|
|
5414
|
+
);
|
|
5415
|
+
}
|
|
5416
|
+
if (!res.ok) {
|
|
5417
|
+
const body = await res.text().catch(() => "");
|
|
5418
|
+
throw new HttpError(
|
|
5419
|
+
res.status,
|
|
5420
|
+
`${res.status} ${res.statusText} on GET ${path19}: ${body}`,
|
|
5421
|
+
body
|
|
5422
|
+
);
|
|
5423
|
+
}
|
|
5424
|
+
return await res.json();
|
|
5425
|
+
},
|
|
5426
|
+
async post(path19, body) {
|
|
5427
|
+
let res;
|
|
5428
|
+
try {
|
|
5429
|
+
res = await fetchImpl(`${root}${path19}`, {
|
|
5430
|
+
method: "POST",
|
|
5431
|
+
headers: { "content-type": "application/json", ...authHeader },
|
|
5432
|
+
body: JSON.stringify(body)
|
|
5433
|
+
});
|
|
5434
|
+
} catch (err) {
|
|
5435
|
+
throw new TransportError(
|
|
5436
|
+
`cannot reach neat-core at ${root}: ${err.message}`
|
|
5437
|
+
);
|
|
5438
|
+
}
|
|
5439
|
+
if (!res.ok) {
|
|
5440
|
+
const text = await res.text().catch(() => "");
|
|
5441
|
+
throw new HttpError(
|
|
5442
|
+
res.status,
|
|
5443
|
+
`${res.status} ${res.statusText} on POST ${path19}: ${text}`,
|
|
5444
|
+
text
|
|
5445
|
+
);
|
|
5446
|
+
}
|
|
5447
|
+
return await res.json();
|
|
5448
|
+
}
|
|
5453
5449
|
};
|
|
5454
5450
|
}
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
const major = Number.parseInt(nodeVersion.split(".")[0] ?? "", 10);
|
|
5459
|
-
const ok = Number.isFinite(major) && major >= NODE_FLOOR;
|
|
5460
|
-
return ok ? { name: "node", ok, detail: `v${nodeVersion} (>= ${NODE_FLOOR} required)` } : {
|
|
5461
|
-
name: "node",
|
|
5462
|
-
ok,
|
|
5463
|
-
detail: `v${nodeVersion} \u2014 NEAT needs Node ${NODE_FLOOR} or newer`,
|
|
5464
|
-
fix: `install Node ${NODE_FLOOR}.x (e.g. \`nvm install ${NODE_FLOOR}\`) and re-run`
|
|
5465
|
-
};
|
|
5451
|
+
function projectPath(project, suffix) {
|
|
5452
|
+
if (!project) return suffix;
|
|
5453
|
+
return `/projects/${encodeURIComponent(project)}${suffix}`;
|
|
5466
5454
|
}
|
|
5467
|
-
async function
|
|
5455
|
+
async function runRootCause(client, input) {
|
|
5456
|
+
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
5457
|
+
const path19 = projectPath(
|
|
5458
|
+
input.project,
|
|
5459
|
+
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
5460
|
+
);
|
|
5468
5461
|
try {
|
|
5469
|
-
const
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
}
|
|
5475
|
-
|
|
5476
|
-
|
|
5462
|
+
const result = await client.get(path19);
|
|
5463
|
+
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
5464
|
+
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
5465
|
+
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
5466
|
+
const blockLines = [
|
|
5467
|
+
`Traversal path: ${arrowPath}`,
|
|
5468
|
+
`Edge provenances: ${provenances}`
|
|
5469
|
+
];
|
|
5470
|
+
if (result.fixRecommendation) blockLines.push(`Recommended fix: ${result.fixRecommendation}`);
|
|
5477
5471
|
return {
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5472
|
+
summary,
|
|
5473
|
+
block: blockLines.join("\n"),
|
|
5474
|
+
confidence: result.confidence,
|
|
5475
|
+
provenance: result.edgeProvenances.length ? result.edgeProvenances : void 0
|
|
5481
5476
|
};
|
|
5477
|
+
} catch (err) {
|
|
5478
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
5479
|
+
return {
|
|
5480
|
+
summary: `No root cause found for ${input.errorNode}. The node may be healthy, or it may not exist in the graph.`
|
|
5481
|
+
};
|
|
5482
|
+
}
|
|
5483
|
+
throw err;
|
|
5482
5484
|
}
|
|
5483
|
-
|
|
5485
|
+
}
|
|
5486
|
+
async function runBlastRadius(client, input) {
|
|
5487
|
+
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
5488
|
+
const path19 = projectPath(
|
|
5489
|
+
input.project,
|
|
5490
|
+
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
5491
|
+
);
|
|
5492
|
+
try {
|
|
5493
|
+
const result = await client.get(path19);
|
|
5494
|
+
if (result.totalAffected === 0) {
|
|
5495
|
+
return {
|
|
5496
|
+
summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
|
|
5497
|
+
};
|
|
5498
|
+
}
|
|
5499
|
+
const sorted = [...result.affectedNodes].sort(
|
|
5500
|
+
(a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
|
|
5501
|
+
);
|
|
5502
|
+
const blockLines = sorted.map(formatBlastEntry);
|
|
5503
|
+
const minConfidence = sorted.reduce(
|
|
5504
|
+
(m, n) => Math.min(m, n.confidence),
|
|
5505
|
+
Number.POSITIVE_INFINITY
|
|
5506
|
+
);
|
|
5507
|
+
const provenances = [...new Set(sorted.map((n) => n.edgeProvenance))];
|
|
5484
5508
|
return {
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5509
|
+
summary: `Blast radius for ${result.origin}: ${result.totalAffected} dependent node${result.totalAffected === 1 ? "" : "s"} would break if it changed.`,
|
|
5510
|
+
block: blockLines.join("\n"),
|
|
5511
|
+
confidence: Number.isFinite(minConfidence) ? minConfidence : void 0,
|
|
5512
|
+
provenance: provenances.length ? provenances : void 0
|
|
5488
5513
|
};
|
|
5514
|
+
} catch (err) {
|
|
5515
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
5516
|
+
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
5517
|
+
}
|
|
5518
|
+
throw err;
|
|
5489
5519
|
}
|
|
5490
|
-
return {
|
|
5491
|
-
name: "project",
|
|
5492
|
-
ok: false,
|
|
5493
|
-
detail: "no NEAT project in this directory",
|
|
5494
|
-
fix: "set one up: `neat .`"
|
|
5495
|
-
};
|
|
5496
5520
|
}
|
|
5497
|
-
function
|
|
5498
|
-
const
|
|
5499
|
-
|
|
5500
|
-
if (record) return `http://localhost:${record.ports.rest}`;
|
|
5501
|
-
return "http://localhost:8080";
|
|
5502
|
-
}
|
|
5503
|
-
function summariseHealth(url, body) {
|
|
5504
|
-
const projects = body.projects ?? [];
|
|
5505
|
-
const nodes = projects.reduce((n, p) => n + (p.nodeCount ?? 0), 0);
|
|
5506
|
-
const edges = projects.reduce((n, p) => n + (p.edgeCount ?? 0), 0);
|
|
5507
|
-
const proj = body.project ?? projects[0]?.name;
|
|
5508
|
-
const graph = projects.length > 0 ? ` \u2014 ${nodes} nodes / ${edges} edges` : "";
|
|
5509
|
-
return `up at ${url}${proj ? ` (project "${proj}"${graph})` : ""}`;
|
|
5521
|
+
function formatBlastEntry(n) {
|
|
5522
|
+
const tag = n.edgeProvenance === Provenance2.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
5523
|
+
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
5510
5524
|
}
|
|
5511
|
-
async function
|
|
5512
|
-
const
|
|
5513
|
-
const
|
|
5514
|
-
|
|
5525
|
+
async function runDependencies(client, input) {
|
|
5526
|
+
const depth = input.depth ?? 3;
|
|
5527
|
+
const path19 = projectPath(
|
|
5528
|
+
input.project,
|
|
5529
|
+
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
5530
|
+
);
|
|
5515
5531
|
try {
|
|
5516
|
-
const
|
|
5517
|
-
|
|
5518
|
-
signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS)
|
|
5519
|
-
});
|
|
5520
|
-
if (res.status === 401 || res.status === 403) {
|
|
5532
|
+
const result = await client.get(path19);
|
|
5533
|
+
if (result.total === 0) {
|
|
5521
5534
|
return {
|
|
5522
|
-
|
|
5523
|
-
ok: false,
|
|
5524
|
-
detail: `up at ${url}, but rejected the request (${res.status})`,
|
|
5525
|
-
fix: "set NEAT_AUTH_TOKEN to the daemon's token"
|
|
5535
|
+
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
5526
5536
|
};
|
|
5527
5537
|
}
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
fix: "check the daemon logs"
|
|
5534
|
-
};
|
|
5538
|
+
const byDistance = /* @__PURE__ */ new Map();
|
|
5539
|
+
for (const dep of result.dependencies) {
|
|
5540
|
+
const ring = byDistance.get(dep.distance) ?? [];
|
|
5541
|
+
ring.push(dep);
|
|
5542
|
+
byDistance.set(dep.distance, ring);
|
|
5535
5543
|
}
|
|
5536
|
-
const
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5544
|
+
const blockLines = [];
|
|
5545
|
+
for (const distance of [...byDistance.keys()].sort((a, b) => a - b)) {
|
|
5546
|
+
const label = distance === 1 ? "Direct (distance 1)" : `Distance ${distance}`;
|
|
5547
|
+
blockLines.push(`${label}:`);
|
|
5548
|
+
for (const dep of byDistance.get(distance)) {
|
|
5549
|
+
blockLines.push(` \u2022 ${dep.nodeId} \u2014 ${dep.edgeType} (${dep.provenance})`);
|
|
5550
|
+
}
|
|
5551
|
+
}
|
|
5552
|
+
const provenances = [...new Set(result.dependencies.map((d) => d.provenance))];
|
|
5553
|
+
const directCount = byDistance.get(1)?.length ?? 0;
|
|
5554
|
+
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).`;
|
|
5555
|
+
return { summary, block: blockLines.join("\n"), provenance: provenances };
|
|
5556
|
+
} catch (err) {
|
|
5557
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
5558
|
+
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
5559
|
+
}
|
|
5560
|
+
throw err;
|
|
5545
5561
|
}
|
|
5546
5562
|
}
|
|
5547
|
-
|
|
5548
|
-
const
|
|
5549
|
-
|
|
5550
|
-
return [checkNode(d.nodeVersion), await checkProject(d.cwd, record), await checkDaemon(d, record)];
|
|
5563
|
+
function observedDepLine(nodeId, e) {
|
|
5564
|
+
const via = e.source !== nodeId ? ` (via ${e.source})` : "";
|
|
5565
|
+
return ` \u2022 ${e.target} \u2014 ${e.type}${via}${edgeMeta(e)}`;
|
|
5551
5566
|
}
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5567
|
+
async function runObservedDependencies(client, input) {
|
|
5568
|
+
try {
|
|
5569
|
+
const result = await client.get(
|
|
5570
|
+
projectPath(
|
|
5571
|
+
input.project,
|
|
5572
|
+
`/graph/observed-dependencies/${encodeURIComponent(input.nodeId)}`
|
|
5573
|
+
)
|
|
5574
|
+
);
|
|
5575
|
+
if (result.dependencies.length === 0) {
|
|
5576
|
+
if (result.observed) {
|
|
5577
|
+
return {
|
|
5578
|
+
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.`,
|
|
5579
|
+
provenance: Provenance2.OBSERVED
|
|
5580
|
+
};
|
|
5581
|
+
}
|
|
5582
|
+
const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
5583
|
+
return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
|
|
5584
|
+
}
|
|
5585
|
+
const blockLines = result.dependencies.map((e) => observedDepLine(input.nodeId, e));
|
|
5586
|
+
return {
|
|
5587
|
+
summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
5588
|
+
block: blockLines.join("\n"),
|
|
5589
|
+
provenance: Provenance2.OBSERVED
|
|
5590
|
+
};
|
|
5591
|
+
} catch (err) {
|
|
5592
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
5593
|
+
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
5594
|
+
}
|
|
5595
|
+
throw err;
|
|
5560
5596
|
}
|
|
5561
|
-
out("");
|
|
5562
|
-
const failed = checks.filter((c) => !c.ok).length;
|
|
5563
|
-
out(failed === 0 ? "all good." : `${failed} check${failed === 1 ? "" : "s"} failed.`);
|
|
5564
5597
|
}
|
|
5565
|
-
|
|
5566
|
-
const
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
if (
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
out(" Probe this directory's NEAT setup: Node version, project, daemon.");
|
|
5573
|
-
out(" Exit 0 when every check passes, 1 when any fails.");
|
|
5574
|
-
return 0;
|
|
5575
|
-
} else {
|
|
5576
|
-
out(`neat doctor: unknown argument "${arg}"`);
|
|
5577
|
-
return 2;
|
|
5598
|
+
function edgeMeta(e) {
|
|
5599
|
+
const bits = [];
|
|
5600
|
+
if (e.signal) {
|
|
5601
|
+
bits.push(`spans=${e.signal.spanCount}`);
|
|
5602
|
+
if (e.signal.errorCount > 0) bits.push(`errors=${e.signal.errorCount}`);
|
|
5603
|
+
if (e.signal.lastObservedAgeMs !== void 0) {
|
|
5604
|
+
bits.push(`age=${formatDuration(e.signal.lastObservedAgeMs)}`);
|
|
5578
5605
|
}
|
|
5606
|
+
} else if (e.callCount !== void 0) {
|
|
5607
|
+
bits.push(`callCount=${e.callCount}`);
|
|
5579
5608
|
}
|
|
5580
|
-
|
|
5581
|
-
if (
|
|
5582
|
-
|
|
5583
|
-
return checks.every((c) => c.ok) ? 0 : 1;
|
|
5584
|
-
}
|
|
5585
|
-
|
|
5586
|
-
// src/login-cli.ts
|
|
5587
|
-
import readline3 from "readline/promises";
|
|
5588
|
-
|
|
5589
|
-
// src/profiles.ts
|
|
5590
|
-
import { promises as fs11 } from "fs";
|
|
5591
|
-
import os from "os";
|
|
5592
|
-
import path12 from "path";
|
|
5593
|
-
var PROFILES_CONFIG_VERSION = 1;
|
|
5594
|
-
function neatHome() {
|
|
5595
|
-
const override = process.env.NEAT_HOME;
|
|
5596
|
-
if (override && override.length > 0) return path12.resolve(override);
|
|
5597
|
-
return path12.join(os.homedir(), ".neat");
|
|
5598
|
-
}
|
|
5599
|
-
function profilesConfigPath(home = neatHome()) {
|
|
5600
|
-
return path12.join(home, "profiles.json");
|
|
5609
|
+
if (e.lastObserved) bits.push(`lastObserved=${e.lastObserved}`);
|
|
5610
|
+
if (e.confidence !== void 0) bits.push(`confidence=${e.confidence}`);
|
|
5611
|
+
return bits.length ? ` [${bits.join(", ")}]` : "";
|
|
5601
5612
|
}
|
|
5602
|
-
function
|
|
5603
|
-
return
|
|
5613
|
+
function formatDuration(ms) {
|
|
5614
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
5615
|
+
const s = Math.round(ms / 1e3);
|
|
5616
|
+
if (s < 60) return `${s}s`;
|
|
5617
|
+
const m = Math.round(s / 60);
|
|
5618
|
+
if (m < 60) return `${m}m`;
|
|
5619
|
+
const h = Math.round(m / 60);
|
|
5620
|
+
if (h < 48) return `${h}h`;
|
|
5621
|
+
return `${Math.round(h / 24)}d`;
|
|
5604
5622
|
}
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
if (process.platform === "win32") return;
|
|
5623
|
+
async function runIncidents(client, input) {
|
|
5624
|
+
const path19 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
5608
5625
|
try {
|
|
5609
|
-
const
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
`
|
|
5614
|
-
|
|
5626
|
+
const body = await client.get(path19);
|
|
5627
|
+
const events = body.events;
|
|
5628
|
+
if (events.length === 0) {
|
|
5629
|
+
return {
|
|
5630
|
+
summary: input.nodeId ? `No incidents recorded against ${input.nodeId}.` : "No incidents recorded."
|
|
5631
|
+
};
|
|
5615
5632
|
}
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5633
|
+
const ordered = [...events].reverse().slice(0, input.limit ?? 20);
|
|
5634
|
+
const blockLines = [];
|
|
5635
|
+
for (const ev of ordered) {
|
|
5636
|
+
blockLines.push(` ${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`);
|
|
5637
|
+
blockLines.push(` trace=${ev.traceId} span=${ev.spanId}`);
|
|
5638
|
+
}
|
|
5639
|
+
const target = input.nodeId ?? "the project";
|
|
5640
|
+
return {
|
|
5641
|
+
summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
5642
|
+
block: blockLines.join("\n"),
|
|
5643
|
+
provenance: Provenance2.OBSERVED
|
|
5644
|
+
};
|
|
5624
5645
|
} catch (err) {
|
|
5625
|
-
if (err.
|
|
5626
|
-
return {
|
|
5646
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
5647
|
+
return { summary: `Node ${input.nodeId ?? ""} not found in the graph.` };
|
|
5627
5648
|
}
|
|
5628
5649
|
throw err;
|
|
5629
5650
|
}
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5651
|
+
}
|
|
5652
|
+
async function runSearch(client, input) {
|
|
5653
|
+
const result = await client.get(
|
|
5654
|
+
projectPath(input.project, `/search?q=${encodeURIComponent(input.query)}`)
|
|
5655
|
+
);
|
|
5656
|
+
if (result.matches.length === 0) {
|
|
5657
|
+
return { summary: `No matches for "${input.query}".` };
|
|
5636
5658
|
}
|
|
5637
|
-
|
|
5659
|
+
const provider = result.provider ?? "substring";
|
|
5660
|
+
const blockLines = [];
|
|
5661
|
+
let topScore;
|
|
5662
|
+
for (const n of result.matches) {
|
|
5663
|
+
const score = provider !== "substring" && typeof n.score === "number" ? n.score : void 0;
|
|
5664
|
+
const scoreBit = score !== void 0 ? ` [score=${score.toFixed(2)}]` : "";
|
|
5665
|
+
if (score !== void 0 && (topScore === void 0 || score > topScore)) topScore = score;
|
|
5666
|
+
blockLines.push(
|
|
5667
|
+
` \u2022 ${n.id} (${n.type}) \u2014 ${n.name ?? n.id}${scoreBit}`
|
|
5668
|
+
);
|
|
5669
|
+
}
|
|
5670
|
+
return {
|
|
5671
|
+
summary: `Found ${result.matches.length} match${result.matches.length === 1 ? "" : "es"} for "${input.query}" via ${provider} provider.`,
|
|
5672
|
+
block: blockLines.join("\n"),
|
|
5673
|
+
confidence: topScore
|
|
5674
|
+
};
|
|
5638
5675
|
}
|
|
5639
|
-
function
|
|
5640
|
-
|
|
5641
|
-
|
|
5676
|
+
async function runDiff(client, input) {
|
|
5677
|
+
const result = await client.get(
|
|
5678
|
+
projectPath(
|
|
5679
|
+
input.project,
|
|
5680
|
+
`/graph/diff?against=${encodeURIComponent(input.againstSnapshot)}`
|
|
5681
|
+
)
|
|
5682
|
+
);
|
|
5683
|
+
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;
|
|
5684
|
+
const baseLabel = result.base.exportedAt ?? "unknown";
|
|
5685
|
+
if (total === 0) {
|
|
5686
|
+
return {
|
|
5687
|
+
summary: `No differences between the current graph and ${input.againstSnapshot} (base exportedAt=${baseLabel}).`
|
|
5688
|
+
};
|
|
5642
5689
|
}
|
|
5643
|
-
const
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5690
|
+
const blockLines = [
|
|
5691
|
+
` base exportedAt: ${baseLabel}`,
|
|
5692
|
+
` current exportedAt: ${result.current.exportedAt}`,
|
|
5693
|
+
""
|
|
5694
|
+
];
|
|
5695
|
+
if (result.added.nodes.length || result.added.edges.length) {
|
|
5696
|
+
blockLines.push("Added:");
|
|
5697
|
+
for (const n of result.added.nodes) blockLines.push(` + node ${n.id} (${n.type})`);
|
|
5698
|
+
for (const e of result.added.edges)
|
|
5699
|
+
blockLines.push(` + edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
|
|
5700
|
+
blockLines.push("");
|
|
5647
5701
|
}
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5702
|
+
if (result.removed.nodes.length || result.removed.edges.length) {
|
|
5703
|
+
blockLines.push("Removed:");
|
|
5704
|
+
for (const n of result.removed.nodes) blockLines.push(` - node ${n.id} (${n.type})`);
|
|
5705
|
+
for (const e of result.removed.edges)
|
|
5706
|
+
blockLines.push(` - edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
|
|
5707
|
+
blockLines.push("");
|
|
5651
5708
|
}
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5709
|
+
if (result.changed.nodes.length || result.changed.edges.length) {
|
|
5710
|
+
blockLines.push("Changed:");
|
|
5711
|
+
for (const c of result.changed.nodes) {
|
|
5712
|
+
blockLines.push(` ~ node ${c.id} \u2014 ${summariseAttrDiff(c.before, c.after)}`);
|
|
5713
|
+
}
|
|
5714
|
+
for (const c of result.changed.edges) {
|
|
5715
|
+
const provBit = c.before.provenance !== c.after.provenance ? `provenance ${c.before.provenance} \u2192 ${c.after.provenance}` : summariseAttrDiff(c.before, c.after);
|
|
5716
|
+
blockLines.push(` ~ edge ${c.id} \u2014 ${provBit}`);
|
|
5717
|
+
}
|
|
5657
5718
|
}
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5719
|
+
return {
|
|
5720
|
+
summary: `Diff against ${input.againstSnapshot}: ${total} change${total === 1 ? "" : "s"} between the snapshot and the live graph.`,
|
|
5721
|
+
block: blockLines.join("\n").trimEnd()
|
|
5722
|
+
};
|
|
5723
|
+
}
|
|
5724
|
+
function summariseAttrDiff(before, after) {
|
|
5725
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
5726
|
+
const changed = [];
|
|
5727
|
+
for (const k of keys) {
|
|
5728
|
+
if (JSON.stringify(before[k]) !== JSON.stringify(after[k])) changed.push(k);
|
|
5729
|
+
}
|
|
5730
|
+
return changed.length === 0 ? "attributes differ" : `fields changed: ${changed.sort().join(", ")}`;
|
|
5731
|
+
}
|
|
5732
|
+
async function runStaleEdges(client, input) {
|
|
5733
|
+
const params = new URLSearchParams();
|
|
5734
|
+
if (input.limit !== void 0) params.set("limit", String(input.limit));
|
|
5735
|
+
if (input.edgeType) params.set("edgeType", input.edgeType);
|
|
5736
|
+
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
5737
|
+
const body = await client.get(
|
|
5738
|
+
projectPath(input.project, `/stale-events${qs}`)
|
|
5739
|
+
);
|
|
5740
|
+
const events = body.events;
|
|
5741
|
+
if (events.length === 0) {
|
|
5742
|
+
return {
|
|
5743
|
+
summary: input.edgeType ? `No stale ${input.edgeType} edges recorded.` : "No stale-edge transitions recorded yet."
|
|
5744
|
+
};
|
|
5745
|
+
}
|
|
5746
|
+
const blockLines = events.map(
|
|
5747
|
+
(e) => ` ${e.transitionedAt} \u2014 ${e.source} -[${e.edgeType}]-> ${e.target} (last seen ${e.lastObserved}, threshold ${formatDuration(e.thresholdMs)})`
|
|
5748
|
+
);
|
|
5749
|
+
return {
|
|
5750
|
+
summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
|
|
5751
|
+
block: blockLines.join("\n"),
|
|
5752
|
+
provenance: Provenance2.STALE
|
|
5753
|
+
};
|
|
5754
|
+
}
|
|
5755
|
+
async function runPolicies(client, input) {
|
|
5756
|
+
let violations;
|
|
5757
|
+
let allowed = true;
|
|
5758
|
+
let hypothetical;
|
|
5759
|
+
if (input.hypotheticalAction) {
|
|
5760
|
+
if (typeof client.post !== "function") {
|
|
5761
|
+
throw new Error("HttpClient does not support POST \u2014 required for policies dry-run");
|
|
5662
5762
|
}
|
|
5663
|
-
|
|
5763
|
+
const body = await client.post(
|
|
5764
|
+
projectPath(input.project, "/policies/check"),
|
|
5765
|
+
{ hypotheticalAction: input.hypotheticalAction }
|
|
5766
|
+
);
|
|
5767
|
+
violations = body.violations;
|
|
5768
|
+
allowed = body.allowed;
|
|
5769
|
+
hypothetical = body.hypotheticalAction;
|
|
5770
|
+
} else {
|
|
5771
|
+
const params = new URLSearchParams();
|
|
5772
|
+
if (input.policyId) params.set("policyId", input.policyId);
|
|
5773
|
+
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
5774
|
+
const body = await client.get(
|
|
5775
|
+
projectPath(input.project, `/policies/violations${qs}`)
|
|
5776
|
+
);
|
|
5777
|
+
violations = body.violations;
|
|
5778
|
+
allowed = violations.every((v) => v.onViolation !== "block");
|
|
5664
5779
|
}
|
|
5665
|
-
|
|
5780
|
+
if (input.nodeId) {
|
|
5781
|
+
violations = violations.filter(
|
|
5782
|
+
(v) => v.subject.nodeId === input.nodeId || v.subject.path?.includes(input.nodeId)
|
|
5783
|
+
);
|
|
5784
|
+
}
|
|
5785
|
+
if (violations.length === 0) {
|
|
5786
|
+
return {
|
|
5787
|
+
summary: hypothetical ? `No violations would result from the hypothetical action (${hypothetical.kind}).` : "No policy violations recorded."
|
|
5788
|
+
};
|
|
5789
|
+
}
|
|
5790
|
+
const blockCount = violations.filter((v) => v.onViolation === "block").length;
|
|
5791
|
+
const summaryParts = [];
|
|
5792
|
+
if (hypothetical) {
|
|
5793
|
+
summaryParts.push(
|
|
5794
|
+
`Hypothetical ${hypothetical.kind} would surface ${violations.length} violation${violations.length === 1 ? "" : "s"}`
|
|
5795
|
+
);
|
|
5796
|
+
} else {
|
|
5797
|
+
summaryParts.push(
|
|
5798
|
+
`${violations.length} policy violation${violations.length === 1 ? "" : "s"} currently recorded`
|
|
5799
|
+
);
|
|
5800
|
+
}
|
|
5801
|
+
if (blockCount > 0) summaryParts.push(`${blockCount} of which block`);
|
|
5802
|
+
if (!allowed && hypothetical) summaryParts.push("action denied");
|
|
5803
|
+
const summary = summaryParts.join("; ") + ".";
|
|
5804
|
+
const blockLines = violations.map((v) => {
|
|
5805
|
+
const subject = v.subject.nodeId ?? v.subject.edgeId ?? v.subject.path?.[0] ?? "(global)";
|
|
5806
|
+
return ` \u2022 [${v.severity}/${v.onViolation}] ${v.policyName}: ${v.message} \u2014 ${subject}`;
|
|
5807
|
+
});
|
|
5808
|
+
const severities = [...new Set(violations.map((v) => v.severity))];
|
|
5809
|
+
return {
|
|
5810
|
+
summary,
|
|
5811
|
+
block: blockLines.join("\n"),
|
|
5812
|
+
confidence: hypothetical ? 0.7 : 1,
|
|
5813
|
+
provenance: severities.join(" ")
|
|
5814
|
+
};
|
|
5666
5815
|
}
|
|
5667
|
-
function
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5816
|
+
function formatDivergenceLine(d) {
|
|
5817
|
+
switch (d.type) {
|
|
5818
|
+
case "missing-observed":
|
|
5819
|
+
case "missing-extracted":
|
|
5820
|
+
if (d.column) {
|
|
5821
|
+
return ` \u2022 [${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
5822
|
+
}
|
|
5823
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} (${d.edgeType}) \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
5824
|
+
case "version-mismatch":
|
|
5825
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared ${d.extractedVersion}, observed engine ${d.observedVersion} (${d.compatibility})`;
|
|
5826
|
+
case "host-mismatch":
|
|
5827
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared host ${d.extractedHost}, observed host ${d.observedHost}`;
|
|
5828
|
+
case "compat-violation":
|
|
5829
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.rule.kind}${d.rule.package ? ` (${d.rule.package})` : ""}`;
|
|
5830
|
+
case "observed-symbol-mismatch": {
|
|
5831
|
+
const at = d.location ? ` at ${d.location}` : "";
|
|
5832
|
+
const member = d.symbol ? ` ${d.symbol}` : "";
|
|
5833
|
+
return ` \u2022 [${d.type}] ${d.source}${member}${at} (${d.mismatchKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
5834
|
+
}
|
|
5835
|
+
case "observed-failing": {
|
|
5836
|
+
if (d.edgeType) {
|
|
5837
|
+
const rate = d.errorRate !== void 0 ? ` ${Math.round(d.errorRate * 100)}% errors` : "";
|
|
5838
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} (${d.edgeType}) \u2014${rate} (${d.failureKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
5839
|
+
}
|
|
5840
|
+
const at = d.location ? ` at ${d.location}` : "";
|
|
5841
|
+
return ` \u2022 [${d.type}] ${d.source}${at} (${d.failureKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
5842
|
+
}
|
|
5843
|
+
case "deploy-mismatch": {
|
|
5844
|
+
if (d.kind === "image") {
|
|
5845
|
+
return ` \u2022 [${d.type}] ${d.source} \u2014 declared image ${d.declaredImage ?? "?"}, running ${d.observedImage ?? "?"} \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
5846
|
+
}
|
|
5847
|
+
return ` \u2022 [${d.type}] ${d.source} \u2014 declared ${d.declaredReplicas ?? "?"} replicas, ${d.observedReplicas ?? "?"} ready \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
5848
|
+
}
|
|
5671
5849
|
}
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5850
|
+
}
|
|
5851
|
+
async function runDivergences(client, input) {
|
|
5852
|
+
const params = new URLSearchParams();
|
|
5853
|
+
if (input.type && input.type.length > 0) params.set("type", input.type.join(","));
|
|
5854
|
+
if (input.minConfidence !== void 0) {
|
|
5855
|
+
params.set("minConfidence", String(input.minConfidence));
|
|
5676
5856
|
}
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5857
|
+
if (input.node) params.set("node", input.node);
|
|
5858
|
+
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
5859
|
+
const result = await client.get(
|
|
5860
|
+
projectPath(input.project, `/graph/divergences${qs}`)
|
|
5861
|
+
);
|
|
5862
|
+
if (result.totalAffected === 0) {
|
|
5863
|
+
return {
|
|
5864
|
+
summary: "No divergences found between the declared (EXTRACTED) and observed (OBSERVED) views of the graph."
|
|
5865
|
+
};
|
|
5680
5866
|
}
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5867
|
+
const headline = result.divergences[0];
|
|
5868
|
+
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}`;
|
|
5869
|
+
const blockLines = [];
|
|
5870
|
+
for (const d of result.divergences) {
|
|
5871
|
+
blockLines.push(formatDivergenceLine(d));
|
|
5872
|
+
blockLines.push(` reason: ${d.reason}`);
|
|
5873
|
+
blockLines.push(` recommendation: ${d.recommendation}`);
|
|
5686
5874
|
}
|
|
5687
|
-
|
|
5688
|
-
|
|
5875
|
+
const maxConfidence = result.divergences.reduce(
|
|
5876
|
+
(m, d) => Math.max(m, d.confidence),
|
|
5877
|
+
0
|
|
5878
|
+
);
|
|
5879
|
+
return {
|
|
5880
|
+
summary,
|
|
5881
|
+
block: blockLines.join("\n"),
|
|
5882
|
+
confidence: maxConfidence,
|
|
5883
|
+
provenance: "composite (EXTRACTED + OBSERVED)"
|
|
5884
|
+
};
|
|
5885
|
+
}
|
|
5886
|
+
async function runAsk(client, input) {
|
|
5887
|
+
const result = await client.get(
|
|
5888
|
+
projectPath(input.project, `/graph/ask?q=${encodeURIComponent(input.question)}`)
|
|
5889
|
+
);
|
|
5890
|
+
const blockLines = [];
|
|
5891
|
+
if (result.matched.length > 0) {
|
|
5892
|
+
blockLines.push(
|
|
5893
|
+
`Matched: ${result.matched.map((m) => `${m.nodeId} [${m.via} ${m.score.toFixed(2)}]`).join(", ")}`
|
|
5894
|
+
);
|
|
5895
|
+
blockLines.push(`Intent: ${result.intent}`);
|
|
5896
|
+
} else if (result.scope === "global") {
|
|
5897
|
+
blockLines.push(`Graph-wide answer (${result.intent}) \u2014 no entity named.`);
|
|
5689
5898
|
}
|
|
5690
|
-
|
|
5691
|
-
|
|
5899
|
+
for (const section of result.sections) {
|
|
5900
|
+
blockLines.push("", section.heading + ":");
|
|
5901
|
+
for (const fact of section.facts) {
|
|
5902
|
+
const tag = fact.provenance ? ` [${fact.provenance}${fact.confidence !== void 0 ? ` ${fact.confidence.toFixed(2)}` : ""}]` : fact.confidence !== void 0 ? ` [confidence ${fact.confidence.toFixed(2)}]` : "";
|
|
5903
|
+
blockLines.push(` \u2022 ${fact.text}${tag}`);
|
|
5904
|
+
}
|
|
5692
5905
|
}
|
|
5693
5906
|
return {
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
...
|
|
5907
|
+
summary: result.answer,
|
|
5908
|
+
block: blockLines.join("\n").trim(),
|
|
5909
|
+
...result.confidence !== void 0 ? { confidence: result.confidence } : {},
|
|
5910
|
+
...result.provenance.length > 0 ? { provenance: result.provenance } : {}
|
|
5697
5911
|
};
|
|
5698
5912
|
}
|
|
5699
|
-
|
|
5700
|
-
const
|
|
5701
|
-
|
|
5702
|
-
}
|
|
5703
|
-
async function getActiveProfile(home = neatHome()) {
|
|
5704
|
-
const { active, profiles } = await readProfilesConfig(home);
|
|
5705
|
-
if (!active) return void 0;
|
|
5706
|
-
return profiles.find((p) => p.name === active);
|
|
5913
|
+
function formatFooter(confidence, provenance) {
|
|
5914
|
+
const c = confidence === void 0 ? "n/a" : confidence.toFixed(2);
|
|
5915
|
+
const p = provenance === void 0 ? "n/a" : Array.isArray(provenance) ? [...new Set(provenance)].join(", ") : provenance;
|
|
5916
|
+
return `confidence: ${c} \xB7 provenance: ${p}`;
|
|
5707
5917
|
}
|
|
5708
|
-
function
|
|
5709
|
-
const
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
...active ? { active } : {},
|
|
5714
|
-
profiles: config.profiles
|
|
5715
|
-
};
|
|
5716
|
-
return `${JSON.stringify(out, null, 2)}
|
|
5717
|
-
`;
|
|
5918
|
+
function formatHuman(result) {
|
|
5919
|
+
const sections = [result.summary.trim()];
|
|
5920
|
+
if (result.block && result.block.trim().length > 0) sections.push(result.block.trimEnd());
|
|
5921
|
+
sections.push(formatFooter(result.confidence, result.provenance));
|
|
5922
|
+
return sections.join("\n\n");
|
|
5718
5923
|
}
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
await fs11.rename(tmp, file);
|
|
5924
|
+
function formatJson(result) {
|
|
5925
|
+
return JSON.stringify(
|
|
5926
|
+
{
|
|
5927
|
+
summary: result.summary,
|
|
5928
|
+
block: result.block ?? "",
|
|
5929
|
+
confidence: result.confidence ?? null,
|
|
5930
|
+
provenance: result.provenance ?? null
|
|
5931
|
+
},
|
|
5932
|
+
null,
|
|
5933
|
+
2
|
|
5934
|
+
);
|
|
5731
5935
|
}
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
5737
|
-
for (; ; ) {
|
|
5738
|
-
try {
|
|
5739
|
-
const fd = await fs11.open(lockPath, "wx");
|
|
5740
|
-
await fd.writeFile(`${process.pid}
|
|
5741
|
-
`, "utf8");
|
|
5742
|
-
await fd.close();
|
|
5743
|
-
return;
|
|
5744
|
-
} catch (err) {
|
|
5745
|
-
if (err.code !== "EEXIST") throw err;
|
|
5746
|
-
if (Date.now() >= deadline) {
|
|
5747
|
-
throw new Error(
|
|
5748
|
-
`timed out acquiring ${lockPath} after ${LOCK_TIMEOUT_MS}ms \u2014 if no other neat process is running, remove the stale lock file`
|
|
5749
|
-
);
|
|
5750
|
-
}
|
|
5751
|
-
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
|
|
5752
|
-
}
|
|
5753
|
-
}
|
|
5936
|
+
function exitCodeForError(err) {
|
|
5937
|
+
if (err instanceof TransportError) return 3;
|
|
5938
|
+
if (err instanceof HttpError) return 1;
|
|
5939
|
+
return 1;
|
|
5754
5940
|
}
|
|
5755
|
-
|
|
5756
|
-
|
|
5941
|
+
function createSnapshotPushClient(baseUrl, token, fetchImpl) {
|
|
5942
|
+
return createHttpClient(baseUrl, token && token.length > 0 ? token : void 0, fetchImpl);
|
|
5757
5943
|
}
|
|
5758
|
-
async function
|
|
5759
|
-
const
|
|
5760
|
-
await acquireLock(lockPath);
|
|
5944
|
+
async function resolveRemoteProjectName(input, fetchImpl) {
|
|
5945
|
+
const client = createSnapshotPushClient(input.baseUrl, input.token, fetchImpl);
|
|
5761
5946
|
try {
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5947
|
+
const projects = await client.get("/projects");
|
|
5948
|
+
const hosted = Array.isArray(projects) ? projects.find((p) => p?.hostedHere) : void 0;
|
|
5949
|
+
return hosted?.name ?? input.fallback;
|
|
5950
|
+
} catch {
|
|
5951
|
+
return input.fallback;
|
|
5765
5952
|
}
|
|
5766
5953
|
}
|
|
5767
|
-
async function
|
|
5768
|
-
const
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
await writeConfigAtomic({ version: config.version, ...active ? { active } : {}, profiles }, home);
|
|
5777
|
-
});
|
|
5778
|
-
}
|
|
5779
|
-
async function removeProfile(name, home = neatHome()) {
|
|
5780
|
-
return withProfilesLock(home, async () => {
|
|
5781
|
-
const config = await readProfilesConfig(home);
|
|
5782
|
-
const profiles = config.profiles.filter((p) => p.name !== name);
|
|
5783
|
-
if (profiles.length === config.profiles.length) return false;
|
|
5784
|
-
const active = config.active === name ? void 0 : config.active;
|
|
5785
|
-
await writeConfigAtomic({ version: config.version, ...active ? { active } : {}, profiles }, home);
|
|
5786
|
-
return true;
|
|
5787
|
-
});
|
|
5788
|
-
}
|
|
5789
|
-
async function clearActiveProfile(home = neatHome()) {
|
|
5790
|
-
return withProfilesLock(home, async () => {
|
|
5791
|
-
const config = await readProfilesConfig(home);
|
|
5792
|
-
if (!config.active) return false;
|
|
5793
|
-
await writeConfigAtomic({ version: config.version, profiles: config.profiles }, home);
|
|
5794
|
-
return true;
|
|
5795
|
-
});
|
|
5954
|
+
async function pushSnapshotToRemote(input) {
|
|
5955
|
+
const client = createSnapshotPushClient(input.baseUrl, input.token);
|
|
5956
|
+
if (typeof client.post !== "function") {
|
|
5957
|
+
throw new Error("HttpClient does not support POST \u2014 required for snapshot push");
|
|
5958
|
+
}
|
|
5959
|
+
return client.post(
|
|
5960
|
+
`/projects/${encodeURIComponent(input.project)}/snapshot`,
|
|
5961
|
+
{ snapshot: input.snapshot }
|
|
5962
|
+
);
|
|
5796
5963
|
}
|
|
5797
5964
|
|
|
5798
|
-
// src/
|
|
5799
|
-
|
|
5800
|
-
import { spawn as spawn3 } from "child_process";
|
|
5801
|
-
import { randomBytes as randomBytes2 } from "crypto";
|
|
5802
|
-
var DEFAULT_CP_URL = "https://neat-control-plane-bg5yqctn2q-nw.a.run.app";
|
|
5803
|
-
var DEFAULT_WEB_URL = "https://app.neat.is";
|
|
5804
|
-
var CALLBACK_TIMEOUT_MS = 5 * 6e4;
|
|
5805
|
-
function resolveCpUrl(env = process.env, override) {
|
|
5806
|
-
const v = override ?? env.NEAT_CP_URL;
|
|
5807
|
-
return (v && v.length > 0 ? v : DEFAULT_CP_URL).replace(/\/+$/, "");
|
|
5808
|
-
}
|
|
5809
|
-
function resolveWebUrl(env = process.env, override) {
|
|
5810
|
-
const v = override ?? env.NEAT_WEB_URL;
|
|
5811
|
-
return (v && v.length > 0 ? v : DEFAULT_WEB_URL).replace(/\/+$/, "");
|
|
5812
|
-
}
|
|
5813
|
-
function pickProject(projects, want) {
|
|
5814
|
-
if (want) {
|
|
5815
|
-
const p = projects.find((x) => x.id === want || x.name === want);
|
|
5816
|
-
if (!p) return { error: { code: 1, message: `no project named or id'd "${want}" on this account` } };
|
|
5817
|
-
return { project: p };
|
|
5818
|
-
}
|
|
5819
|
-
const running = projects.filter((p) => p.status === "running");
|
|
5820
|
-
if (running.length === 1) return { project: running[0] };
|
|
5821
|
-
if (running.length === 0) {
|
|
5822
|
-
const listed = projects.length ? ` (have: ${projects.map((p) => `${p.name} [${p.status}]`).join(", ")})` : "";
|
|
5823
|
-
return {
|
|
5824
|
-
error: {
|
|
5825
|
-
code: 1,
|
|
5826
|
-
message: `no running project to connect to \u2014 create + provision one in the console first${listed}`
|
|
5827
|
-
}
|
|
5828
|
-
};
|
|
5829
|
-
}
|
|
5965
|
+
// src/doctor-cli.ts
|
|
5966
|
+
function resolveDeps2(deps) {
|
|
5830
5967
|
return {
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
const project = picked.project;
|
|
5853
|
-
let credRes;
|
|
5968
|
+
cwd: deps.cwd ?? process.cwd(),
|
|
5969
|
+
env: deps.env ?? process.env,
|
|
5970
|
+
nodeVersion: deps.nodeVersion ?? process.versions.node,
|
|
5971
|
+
fetchImpl: deps.fetchImpl ?? fetch,
|
|
5972
|
+
readRecord: deps.readRecord ?? readDaemonRecord,
|
|
5973
|
+
out: deps.out ?? ((line) => console.log(line))
|
|
5974
|
+
};
|
|
5975
|
+
}
|
|
5976
|
+
var NODE_FLOOR = 20;
|
|
5977
|
+
var HEALTH_TIMEOUT_MS = 3e3;
|
|
5978
|
+
function checkNode(nodeVersion) {
|
|
5979
|
+
const major = Number.parseInt(nodeVersion.split(".")[0] ?? "", 10);
|
|
5980
|
+
const ok = Number.isFinite(major) && major >= NODE_FLOOR;
|
|
5981
|
+
return ok ? { name: "node", ok, detail: `v${nodeVersion} (>= ${NODE_FLOOR} required)` } : {
|
|
5982
|
+
name: "node",
|
|
5983
|
+
ok,
|
|
5984
|
+
detail: `v${nodeVersion} \u2014 NEAT needs Node ${NODE_FLOOR} or newer`,
|
|
5985
|
+
fix: `install Node ${NODE_FLOOR}.x (e.g. \`nvm install ${NODE_FLOOR}\`) and re-run`
|
|
5986
|
+
};
|
|
5987
|
+
}
|
|
5988
|
+
async function neatOutExists(cwd) {
|
|
5854
5989
|
try {
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
return { error: { code: 3, message: `can't reach the control plane \u2014 ${e.message}` } };
|
|
5990
|
+
const st = await fs11.stat(path12.join(cwd, "neat-out"));
|
|
5991
|
+
return st.isDirectory();
|
|
5992
|
+
} catch {
|
|
5993
|
+
return false;
|
|
5860
5994
|
}
|
|
5861
|
-
|
|
5995
|
+
}
|
|
5996
|
+
async function checkProject(cwd, record) {
|
|
5997
|
+
if (record) {
|
|
5862
5998
|
return {
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
}
|
|
5999
|
+
name: "project",
|
|
6000
|
+
ok: true,
|
|
6001
|
+
detail: `"${record.project}" \u2014 set up in this directory (daemon record on REST ${record.ports.rest})`
|
|
5867
6002
|
};
|
|
5868
6003
|
}
|
|
5869
|
-
if (
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
6004
|
+
if (await neatOutExists(cwd)) {
|
|
6005
|
+
return {
|
|
6006
|
+
name: "project",
|
|
6007
|
+
ok: true,
|
|
6008
|
+
detail: "set up in this directory (no live daemon record \u2014 it may be stopped)"
|
|
6009
|
+
};
|
|
5875
6010
|
}
|
|
5876
|
-
return {
|
|
6011
|
+
return {
|
|
6012
|
+
name: "project",
|
|
6013
|
+
ok: false,
|
|
6014
|
+
detail: "no NEAT project in this directory",
|
|
6015
|
+
fix: "set one up: `neat .`"
|
|
6016
|
+
};
|
|
5877
6017
|
}
|
|
5878
|
-
function
|
|
5879
|
-
const
|
|
5880
|
-
|
|
5881
|
-
|
|
6018
|
+
function resolveHealthUrl(env, record) {
|
|
6019
|
+
const explicit = env.NEAT_API_URL ?? env.NEAT_CORE_URL;
|
|
6020
|
+
if (explicit && explicit.length > 0) return explicit.replace(/\/$/, "");
|
|
6021
|
+
if (record) return `http://localhost:${record.ports.rest}`;
|
|
6022
|
+
return "http://localhost:8080";
|
|
6023
|
+
}
|
|
6024
|
+
function summariseHealth(url, body) {
|
|
6025
|
+
const projects = body.projects ?? [];
|
|
6026
|
+
const nodes = projects.reduce((n, p) => n + (p.nodeCount ?? 0), 0);
|
|
6027
|
+
const edges = projects.reduce((n, p) => n + (p.edgeCount ?? 0), 0);
|
|
6028
|
+
const proj = body.project ?? projects[0]?.name;
|
|
6029
|
+
const graph = projects.length > 0 ? ` \u2014 ${nodes} nodes / ${edges} edges` : "";
|
|
6030
|
+
return `up at ${url}${proj ? ` (project "${proj}"${graph})` : ""}`;
|
|
6031
|
+
}
|
|
6032
|
+
async function checkDaemon(deps, record) {
|
|
6033
|
+
const url = resolveHealthUrl(deps.env, record);
|
|
6034
|
+
const token = resolveAuthToken(deps.env);
|
|
6035
|
+
const headers = token ? { authorization: `Bearer ${token}` } : {};
|
|
5882
6036
|
try {
|
|
5883
|
-
const
|
|
5884
|
-
|
|
6037
|
+
const res = await deps.fetchImpl(`${url}/health`, {
|
|
6038
|
+
headers,
|
|
6039
|
+
signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS)
|
|
5885
6040
|
});
|
|
5886
|
-
|
|
5887
|
-
|
|
6041
|
+
if (res.status === 401 || res.status === 403) {
|
|
6042
|
+
return {
|
|
6043
|
+
name: "daemon",
|
|
6044
|
+
ok: false,
|
|
6045
|
+
detail: `up at ${url}, but rejected the request (${res.status})`,
|
|
6046
|
+
fix: "set NEAT_AUTH_TOKEN to the daemon's token"
|
|
6047
|
+
};
|
|
6048
|
+
}
|
|
6049
|
+
if (!res.ok) {
|
|
6050
|
+
return {
|
|
6051
|
+
name: "daemon",
|
|
6052
|
+
ok: false,
|
|
6053
|
+
detail: `reachable at ${url} but /health returned ${res.status}`,
|
|
6054
|
+
fix: "check the daemon logs"
|
|
6055
|
+
};
|
|
6056
|
+
}
|
|
6057
|
+
const body = await res.json().catch(() => ({}));
|
|
6058
|
+
return { name: "daemon", ok: true, detail: summariseHealth(url, body) };
|
|
5888
6059
|
} catch {
|
|
5889
|
-
return
|
|
6060
|
+
return {
|
|
6061
|
+
name: "daemon",
|
|
6062
|
+
ok: false,
|
|
6063
|
+
detail: `down \u2014 nothing answering at ${url}`,
|
|
6064
|
+
fix: "start it: `neat .` (or `neat watch`)"
|
|
6065
|
+
};
|
|
5890
6066
|
}
|
|
5891
6067
|
}
|
|
5892
|
-
async function
|
|
5893
|
-
const
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
const state = randomBytes2(16).toString("hex");
|
|
5897
|
-
return new Promise((resolve) => {
|
|
5898
|
-
let settled = false;
|
|
5899
|
-
let timer;
|
|
5900
|
-
const done = (r) => {
|
|
5901
|
-
if (settled) return;
|
|
5902
|
-
settled = true;
|
|
5903
|
-
clearTimeout(timer);
|
|
5904
|
-
try {
|
|
5905
|
-
server.close();
|
|
5906
|
-
} catch {
|
|
5907
|
-
}
|
|
5908
|
-
resolve(r);
|
|
5909
|
-
};
|
|
5910
|
-
const server = createServer((req, res) => {
|
|
5911
|
-
const reqUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
5912
|
-
if (reqUrl.pathname !== "/callback") {
|
|
5913
|
-
res.writeHead(404);
|
|
5914
|
-
res.end();
|
|
5915
|
-
return;
|
|
5916
|
-
}
|
|
5917
|
-
const token = reqUrl.searchParams.get("token");
|
|
5918
|
-
const gotState = reqUrl.searchParams.get("state");
|
|
5919
|
-
if (!token || gotState !== state) {
|
|
5920
|
-
res.writeHead(400, { "content-type": "text/html" });
|
|
5921
|
-
res.end("<h1>NEAT login failed</h1><p>Invalid or mismatched token. You can close this tab.</p>");
|
|
5922
|
-
done({ error: { code: 1, message: "the browser returned an invalid or mismatched token" } });
|
|
5923
|
-
return;
|
|
5924
|
-
}
|
|
5925
|
-
res.writeHead(200, { "content-type": "text/html" });
|
|
5926
|
-
res.end("<h1>You're logged in to NEAT.</h1><p>You can close this tab and return to the terminal.</p>");
|
|
5927
|
-
done({ token });
|
|
5928
|
-
});
|
|
5929
|
-
server.on("error", (e) => done({ error: { code: 3, message: `couldn't start the local login listener \u2014 ${e.message}` } }));
|
|
5930
|
-
timer = setTimeout(
|
|
5931
|
-
() => done({ error: { code: 1, message: "timed out waiting for the browser login" } }),
|
|
5932
|
-
opts.timeoutMs ?? CALLBACK_TIMEOUT_MS
|
|
5933
|
-
);
|
|
5934
|
-
server.listen(0, "127.0.0.1", () => {
|
|
5935
|
-
const addr = server.address();
|
|
5936
|
-
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
5937
|
-
const callback = `http://127.0.0.1:${port}/callback`;
|
|
5938
|
-
const authUrl = `${webUrl}/cli/auth?callback=${encodeURIComponent(callback)}&state=${state}`;
|
|
5939
|
-
out("Opening your browser to log in to NEAT\u2026");
|
|
5940
|
-
out(`If it doesn't open, visit:
|
|
5941
|
-
${authUrl}`);
|
|
5942
|
-
openFn(authUrl);
|
|
5943
|
-
});
|
|
5944
|
-
});
|
|
6068
|
+
async function runDoctorChecks(deps = {}) {
|
|
6069
|
+
const d = resolveDeps2(deps);
|
|
6070
|
+
const record = await d.readRecord(d.cwd).catch(() => null);
|
|
6071
|
+
return [checkNode(d.nodeVersion), await checkProject(d.cwd, record), await checkDaemon(d, record)];
|
|
5945
6072
|
}
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
const
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
return lb.error.code;
|
|
5955
|
-
}
|
|
5956
|
-
accessToken = lb.token;
|
|
5957
|
-
}
|
|
5958
|
-
const ex = await exchangeCredential(opts.cpUrl, accessToken, { project: opts.project }, deps);
|
|
5959
|
-
if ("error" in ex) {
|
|
5960
|
-
err(`neat login: ${ex.error.message}`);
|
|
5961
|
-
return ex.error.code;
|
|
6073
|
+
var NAME_COL = "project".length;
|
|
6074
|
+
function renderHuman(checks, out) {
|
|
6075
|
+
out("neat doctor \u2014 checking this project's setup");
|
|
6076
|
+
out("");
|
|
6077
|
+
for (const c of checks) {
|
|
6078
|
+
const mark = c.ok ? "\u2713" : "\u2717";
|
|
6079
|
+
out(` ${mark} ${c.name.padEnd(NAME_COL)} ${c.detail}`);
|
|
6080
|
+
if (!c.ok && c.fix) out(` ${" ".repeat(NAME_COL + 3)}fix: ${c.fix}`);
|
|
5962
6081
|
}
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
)
|
|
5981
|
-
);
|
|
5982
|
-
} else {
|
|
5983
|
-
out(`Logged in \u2014 profile "${opts.name}" \u2192 ${project.name} (${cred.endpoint})`);
|
|
5984
|
-
out("The neat CLI and the MCP server now read this hosted graph by default.");
|
|
5985
|
-
if (cred.ingestEndpoint && cred.otelToken) {
|
|
5986
|
-
out("");
|
|
5987
|
-
out("To fill the OBSERVED layer, instrument your app to send OpenTelemetry to the hosted daemon:");
|
|
5988
|
-
out(` OTEL_EXPORTER_OTLP_ENDPOINT=${cred.ingestEndpoint}`);
|
|
5989
|
-
out(` OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${cred.otelToken}`);
|
|
6082
|
+
out("");
|
|
6083
|
+
const failed = checks.filter((c) => !c.ok).length;
|
|
6084
|
+
out(failed === 0 ? "all good." : `${failed} check${failed === 1 ? "" : "s"} failed.`);
|
|
6085
|
+
}
|
|
6086
|
+
async function runDoctorCommand(argv, deps = {}) {
|
|
6087
|
+
const out = deps.out ?? ((line) => console.log(line));
|
|
6088
|
+
let json = false;
|
|
6089
|
+
for (const arg of argv) {
|
|
6090
|
+
if (arg === "--json") json = true;
|
|
6091
|
+
else if (arg === "-h" || arg === "--help") {
|
|
6092
|
+
out("usage: neat doctor [--json]");
|
|
6093
|
+
out(" Probe this directory's NEAT setup: Node version, project, daemon.");
|
|
6094
|
+
out(" Exit 0 when every check passes, 1 when any fails.");
|
|
6095
|
+
return 0;
|
|
6096
|
+
} else {
|
|
6097
|
+
out(`neat doctor: unknown argument "${arg}"`);
|
|
6098
|
+
return 2;
|
|
5990
6099
|
}
|
|
5991
|
-
out("Run `neat logout` to switch back to your local daemon.");
|
|
5992
6100
|
}
|
|
5993
|
-
|
|
6101
|
+
const checks = await runDoctorChecks(deps);
|
|
6102
|
+
if (json) out(JSON.stringify({ ok: checks.every((c) => c.ok), checks }, null, 2));
|
|
6103
|
+
else renderHuman(checks, out);
|
|
6104
|
+
return checks.every((c) => c.ok) ? 0 : 1;
|
|
5994
6105
|
}
|
|
5995
6106
|
|
|
5996
6107
|
// src/login-cli.ts
|
|
6108
|
+
import readline3 from "readline/promises";
|
|
5997
6109
|
var PROBE_ATTEMPT_TIMEOUT_MS = 3e4;
|
|
5998
6110
|
var PROBE_TOTAL_BUDGET_MS = 12e4;
|
|
5999
6111
|
var PROBE_RETRY_PAUSE_MS = 2e3;
|
|
@@ -7781,6 +7893,14 @@ async function checkDaemonHealth(baseUrl) {
|
|
|
7781
7893
|
return false;
|
|
7782
7894
|
}
|
|
7783
7895
|
}
|
|
7896
|
+
function daemonHint(body) {
|
|
7897
|
+
try {
|
|
7898
|
+
const parsed = JSON.parse(body);
|
|
7899
|
+
return typeof parsed.hint === "string" ? parsed.hint : void 0;
|
|
7900
|
+
} catch {
|
|
7901
|
+
return void 0;
|
|
7902
|
+
}
|
|
7903
|
+
}
|
|
7784
7904
|
function snapshotForGraph(persisted) {
|
|
7785
7905
|
return {
|
|
7786
7906
|
// Stamp the live schema version the daemon validates against on the
|
|
@@ -7848,21 +7968,34 @@ async function runSync(opts) {
|
|
|
7848
7968
|
let daemonState = "skipped";
|
|
7849
7969
|
let exitCode = 0;
|
|
7850
7970
|
const mode = opts.dryRun ? "dry-run" : opts.to ? "remote" : "local";
|
|
7971
|
+
let pushedProject = entry2.name;
|
|
7851
7972
|
if (!opts.dryRun) {
|
|
7852
7973
|
const snapshot = snapshotForGraph(persisted);
|
|
7853
7974
|
if (opts.to) {
|
|
7854
7975
|
const token = opts.token ?? process.env.NEAT_REMOTE_TOKEN;
|
|
7976
|
+
pushedProject = await resolveRemoteProjectName({
|
|
7977
|
+
baseUrl: opts.to,
|
|
7978
|
+
token,
|
|
7979
|
+
fallback: entry2.name
|
|
7980
|
+
});
|
|
7855
7981
|
try {
|
|
7856
7982
|
await pushSnapshotToRemote({
|
|
7857
7983
|
baseUrl: opts.to,
|
|
7858
7984
|
token,
|
|
7859
|
-
project:
|
|
7985
|
+
project: pushedProject,
|
|
7860
7986
|
snapshot
|
|
7861
7987
|
});
|
|
7862
7988
|
daemonState = "remote-ok";
|
|
7863
7989
|
} catch (err) {
|
|
7864
7990
|
if (err instanceof HttpError) {
|
|
7865
|
-
|
|
7991
|
+
if (err.status === 404) {
|
|
7992
|
+
const hint = daemonHint(err.responseBody);
|
|
7993
|
+
console.error(
|
|
7994
|
+
`neat sync: the daemon at ${opts.to} has no project "${pushedProject}" to receive this snapshot. ` + (hint ?? "GET /projects lists what it serves (see hostedHere).")
|
|
7995
|
+
);
|
|
7996
|
+
} else {
|
|
7997
|
+
console.error(`neat sync: ${err.message}`);
|
|
7998
|
+
}
|
|
7866
7999
|
exitCode = 1;
|
|
7867
8000
|
} else if (err instanceof TransportError) {
|
|
7868
8001
|
console.error(`neat sync: ${err.message}`);
|
|
@@ -7903,7 +8036,7 @@ async function runSync(opts) {
|
|
|
7903
8036
|
}
|
|
7904
8037
|
const result = {
|
|
7905
8038
|
exitCode,
|
|
7906
|
-
project:
|
|
8039
|
+
project: pushedProject,
|
|
7907
8040
|
scanPath: entry2.path,
|
|
7908
8041
|
nodesAdded: persisted.nodesAdded,
|
|
7909
8042
|
edgesAdded: persisted.edgesAdded,
|
|
@@ -8518,6 +8651,11 @@ async function main() {
|
|
|
8518
8651
|
if (code !== 0) process.exit(code);
|
|
8519
8652
|
return;
|
|
8520
8653
|
}
|
|
8654
|
+
if (cmd0 === "connect") {
|
|
8655
|
+
const code = await runConnectCommand(argv.slice(1));
|
|
8656
|
+
if (code !== 0) process.exit(code);
|
|
8657
|
+
return;
|
|
8658
|
+
}
|
|
8521
8659
|
if (cmd0 === "doctor") {
|
|
8522
8660
|
const code = await runDoctorCommand(argv.slice(1));
|
|
8523
8661
|
if (code !== 0) process.exit(code);
|