@neat.is/core 0.2.6 → 0.2.8
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-T2U4U256.js → chunk-FTKDVKBC.js} +76 -13
- package/dist/chunk-FTKDVKBC.js.map +1 -0
- package/dist/{chunk-6SFEITLJ.js → chunk-GAYTAGEH.js} +298 -13
- package/dist/chunk-GAYTAGEH.js.map +1 -0
- package/dist/{chunk-6JT6L2OV.js → chunk-HWO746IM.js} +8 -10
- package/dist/{chunk-6JT6L2OV.js.map → chunk-HWO746IM.js.map} +1 -1
- package/dist/cli.cjs +1129 -249
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +22 -1
- package/dist/cli.d.ts +22 -1
- package/dist/cli.js +757 -39
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +303 -197
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +16 -18
- package/dist/neatd.cjs +32 -2
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -3
- package/dist/neatd.js.map +1 -1
- package/dist/server.cjs +190 -49
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +2 -2
- package/package.json +5 -3
- package/dist/chunk-6SFEITLJ.js.map +0 -1
- package/dist/chunk-T2U4U256.js.map +0 -1
- package/dist/chunk-WX55TLUT.js +0 -184
- package/dist/chunk-WX55TLUT.js.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -101,8 +101,8 @@ function reshapeGrpcRequest(req) {
|
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
103
|
function resolveProtoRoot() {
|
|
104
|
-
const here =
|
|
105
|
-
return
|
|
104
|
+
const here = import_node_path30.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
105
|
+
return import_node_path30.default.resolve(here, "..", "proto");
|
|
106
106
|
}
|
|
107
107
|
function loadTraceService() {
|
|
108
108
|
const protoRoot = resolveProtoRoot();
|
|
@@ -157,13 +157,13 @@ async function startOtelGrpcReceiver(opts) {
|
|
|
157
157
|
})
|
|
158
158
|
};
|
|
159
159
|
}
|
|
160
|
-
var import_node_url,
|
|
160
|
+
var import_node_url, import_node_path30, grpc, protoLoader;
|
|
161
161
|
var init_otel_grpc = __esm({
|
|
162
162
|
"src/otel-grpc.ts"() {
|
|
163
163
|
"use strict";
|
|
164
164
|
init_cjs_shims();
|
|
165
165
|
import_node_url = require("url");
|
|
166
|
-
|
|
166
|
+
import_node_path30 = __toESM(require("path"), 1);
|
|
167
167
|
grpc = __toESM(require("@grpc/grpc-js"), 1);
|
|
168
168
|
protoLoader = __toESM(require("@grpc/proto-loader"), 1);
|
|
169
169
|
init_otel();
|
|
@@ -260,10 +260,10 @@ function parseOtlpRequest(body) {
|
|
|
260
260
|
}
|
|
261
261
|
function loadProtobufDecoder() {
|
|
262
262
|
if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
|
|
263
|
-
const here =
|
|
264
|
-
const protoRoot =
|
|
263
|
+
const here = import_node_path31.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
264
|
+
const protoRoot = import_node_path31.default.resolve(here, "..", "proto");
|
|
265
265
|
const root = new import_protobufjs.default.Root();
|
|
266
|
-
root.resolvePath = (_origin, target) =>
|
|
266
|
+
root.resolvePath = (_origin, target) => import_node_path31.default.resolve(protoRoot, target);
|
|
267
267
|
root.loadSync(
|
|
268
268
|
"opentelemetry/proto/collector/trace/v1/trace_service.proto",
|
|
269
269
|
{ keepCase: true }
|
|
@@ -355,12 +355,12 @@ async function buildOtelReceiver(opts) {
|
|
|
355
355
|
};
|
|
356
356
|
return decorated;
|
|
357
357
|
}
|
|
358
|
-
var
|
|
358
|
+
var import_node_path31, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
|
|
359
359
|
var init_otel = __esm({
|
|
360
360
|
"src/otel.ts"() {
|
|
361
361
|
"use strict";
|
|
362
362
|
init_cjs_shims();
|
|
363
|
-
|
|
363
|
+
import_node_path31 = __toESM(require("path"), 1);
|
|
364
364
|
import_node_url2 = require("url");
|
|
365
365
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
366
366
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
@@ -372,7 +372,10 @@ var init_otel = __esm({
|
|
|
372
372
|
var cli_exports = {};
|
|
373
373
|
__export(cli_exports, {
|
|
374
374
|
CLAUDE_SKILL_CONFIG: () => CLAUDE_SKILL_CONFIG,
|
|
375
|
+
QUERY_VERBS: () => QUERY_VERBS,
|
|
376
|
+
parseArgs: () => parseArgs,
|
|
375
377
|
runInit: () => runInit,
|
|
378
|
+
runQueryVerb: () => runQueryVerb,
|
|
376
379
|
runSkill: () => runSkill
|
|
377
380
|
});
|
|
378
381
|
module.exports = __toCommonJS(cli_exports);
|
|
@@ -735,6 +738,68 @@ function deprecatedApis() {
|
|
|
735
738
|
return currentMatrix().deprecatedApis ?? [];
|
|
736
739
|
}
|
|
737
740
|
|
|
741
|
+
// src/events.ts
|
|
742
|
+
init_cjs_shims();
|
|
743
|
+
var import_node_events = require("events");
|
|
744
|
+
var EVENT_BUS_CHANNEL = "event";
|
|
745
|
+
var NeatEventBus = class extends import_node_events.EventEmitter {
|
|
746
|
+
};
|
|
747
|
+
var eventBus = new NeatEventBus();
|
|
748
|
+
eventBus.setMaxListeners(0);
|
|
749
|
+
function emitNeatEvent(envelope) {
|
|
750
|
+
eventBus.emit(EVENT_BUS_CHANNEL, envelope);
|
|
751
|
+
}
|
|
752
|
+
function attachGraphToEventBus(graph, opts) {
|
|
753
|
+
const { project } = opts;
|
|
754
|
+
const onNodeAdded = (payload) => {
|
|
755
|
+
emitNeatEvent({
|
|
756
|
+
type: "node-added",
|
|
757
|
+
project,
|
|
758
|
+
payload: { node: payload.attributes }
|
|
759
|
+
});
|
|
760
|
+
};
|
|
761
|
+
const onNodeDropped = (payload) => {
|
|
762
|
+
emitNeatEvent({
|
|
763
|
+
type: "node-removed",
|
|
764
|
+
project,
|
|
765
|
+
payload: { id: payload.key }
|
|
766
|
+
});
|
|
767
|
+
};
|
|
768
|
+
const onEdgeAdded = (payload) => {
|
|
769
|
+
emitNeatEvent({
|
|
770
|
+
type: "edge-added",
|
|
771
|
+
project,
|
|
772
|
+
payload: { edge: payload.attributes }
|
|
773
|
+
});
|
|
774
|
+
};
|
|
775
|
+
const onEdgeDropped = (payload) => {
|
|
776
|
+
emitNeatEvent({
|
|
777
|
+
type: "edge-removed",
|
|
778
|
+
project,
|
|
779
|
+
payload: { id: payload.key }
|
|
780
|
+
});
|
|
781
|
+
};
|
|
782
|
+
const onNodeAttrsUpdated = (payload) => {
|
|
783
|
+
emitNeatEvent({
|
|
784
|
+
type: "node-updated",
|
|
785
|
+
project,
|
|
786
|
+
payload: { id: payload.key, changes: payload.attributes }
|
|
787
|
+
});
|
|
788
|
+
};
|
|
789
|
+
graph.on("nodeAdded", onNodeAdded);
|
|
790
|
+
graph.on("nodeDropped", onNodeDropped);
|
|
791
|
+
graph.on("edgeAdded", onEdgeAdded);
|
|
792
|
+
graph.on("edgeDropped", onEdgeDropped);
|
|
793
|
+
graph.on("nodeAttributesUpdated", onNodeAttrsUpdated);
|
|
794
|
+
return () => {
|
|
795
|
+
graph.off("nodeAdded", onNodeAdded);
|
|
796
|
+
graph.off("nodeDropped", onNodeDropped);
|
|
797
|
+
graph.off("edgeAdded", onEdgeAdded);
|
|
798
|
+
graph.off("edgeDropped", onEdgeDropped);
|
|
799
|
+
graph.off("nodeAttributesUpdated", onNodeAttrsUpdated);
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
738
803
|
// src/traverse.ts
|
|
739
804
|
init_cjs_shims();
|
|
740
805
|
var import_types = require("@neat.is/types");
|
|
@@ -1298,9 +1363,11 @@ async function loadPolicyFile(policyPath) {
|
|
|
1298
1363
|
}
|
|
1299
1364
|
var PolicyViolationsLog = class {
|
|
1300
1365
|
path;
|
|
1366
|
+
project;
|
|
1301
1367
|
seen = null;
|
|
1302
|
-
constructor(logPath) {
|
|
1368
|
+
constructor(logPath, project = DEFAULT_PROJECT) {
|
|
1303
1369
|
this.path = logPath;
|
|
1370
|
+
this.project = project;
|
|
1304
1371
|
}
|
|
1305
1372
|
async append(v) {
|
|
1306
1373
|
if (!this.seen) await this.hydrate();
|
|
@@ -1308,6 +1375,11 @@ var PolicyViolationsLog = class {
|
|
|
1308
1375
|
this.seen.add(v.id);
|
|
1309
1376
|
await import_node_fs2.promises.mkdir(import_node_path2.default.dirname(this.path), { recursive: true });
|
|
1310
1377
|
await import_node_fs2.promises.appendFile(this.path, JSON.stringify(v) + "\n", "utf8");
|
|
1378
|
+
emitNeatEvent({
|
|
1379
|
+
type: "policy-violation",
|
|
1380
|
+
project: this.project,
|
|
1381
|
+
payload: { violation: v }
|
|
1382
|
+
});
|
|
1311
1383
|
return true;
|
|
1312
1384
|
}
|
|
1313
1385
|
async readAll() {
|
|
@@ -1777,6 +1849,7 @@ async function markStaleEdges(graph, options = {}) {
|
|
|
1777
1849
|
const thresholds = options.thresholds ?? loadStaleThresholdsFromEnv();
|
|
1778
1850
|
const now = options.now ?? Date.now();
|
|
1779
1851
|
const events = [];
|
|
1852
|
+
const project = options.project ?? DEFAULT_PROJECT;
|
|
1780
1853
|
graph.forEachEdge((id, attrs) => {
|
|
1781
1854
|
const e = attrs;
|
|
1782
1855
|
if (e.provenance !== import_types3.Provenance.OBSERVED) return;
|
|
@@ -1796,6 +1869,15 @@ async function markStaleEdges(graph, options = {}) {
|
|
|
1796
1869
|
lastObserved: e.lastObserved,
|
|
1797
1870
|
transitionedAt: new Date(now).toISOString()
|
|
1798
1871
|
});
|
|
1872
|
+
emitNeatEvent({
|
|
1873
|
+
type: "stale-transition",
|
|
1874
|
+
project,
|
|
1875
|
+
payload: {
|
|
1876
|
+
edgeId: id,
|
|
1877
|
+
from: import_types3.Provenance.OBSERVED,
|
|
1878
|
+
to: import_types3.Provenance.STALE
|
|
1879
|
+
}
|
|
1880
|
+
});
|
|
1799
1881
|
}
|
|
1800
1882
|
});
|
|
1801
1883
|
if (options.staleEventsPath && events.length > 0) {
|
|
@@ -1826,7 +1908,8 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
1826
1908
|
try {
|
|
1827
1909
|
await markStaleEdges(graph, {
|
|
1828
1910
|
thresholds: options.thresholds,
|
|
1829
|
-
staleEventsPath: options.staleEventsPath
|
|
1911
|
+
staleEventsPath: options.staleEventsPath,
|
|
1912
|
+
project: options.project
|
|
1830
1913
|
});
|
|
1831
1914
|
if (options.onPolicyTrigger) await options.onPolicyTrigger(graph);
|
|
1832
1915
|
} catch (err) {
|
|
@@ -3056,7 +3139,15 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3056
3139
|
const seenTargets = /* @__PURE__ */ new Map();
|
|
3057
3140
|
for (const file of files) {
|
|
3058
3141
|
const parser = import_node_path18.default.extname(file.path) === ".py" ? pyParser : jsParser;
|
|
3059
|
-
|
|
3142
|
+
let targets;
|
|
3143
|
+
try {
|
|
3144
|
+
targets = callsFromSource(file.content, parser, knownHosts);
|
|
3145
|
+
} catch (err) {
|
|
3146
|
+
console.warn(
|
|
3147
|
+
`[neat] http call extraction skipped ${file.path}: ${err.message}`
|
|
3148
|
+
);
|
|
3149
|
+
continue;
|
|
3150
|
+
}
|
|
3060
3151
|
for (const t of targets) {
|
|
3061
3152
|
const targetId = hostToNodeId.get(t);
|
|
3062
3153
|
if (!targetId || targetId === service.node.id) continue;
|
|
@@ -3585,11 +3676,22 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
3585
3676
|
const phase5 = await addInfra(graph, scanPath, services);
|
|
3586
3677
|
const frontiersPromoted = promoteFrontierNodes(graph);
|
|
3587
3678
|
if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
|
|
3588
|
-
|
|
3679
|
+
const result = {
|
|
3589
3680
|
nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
3590
3681
|
edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
3591
3682
|
frontiersPromoted
|
|
3592
3683
|
};
|
|
3684
|
+
emitNeatEvent({
|
|
3685
|
+
type: "extraction-complete",
|
|
3686
|
+
project: opts.project ?? DEFAULT_PROJECT,
|
|
3687
|
+
payload: {
|
|
3688
|
+
project: opts.project ?? DEFAULT_PROJECT,
|
|
3689
|
+
fileCount: services.length,
|
|
3690
|
+
nodesAdded: result.nodesAdded,
|
|
3691
|
+
edgesAdded: result.edgesAdded
|
|
3692
|
+
}
|
|
3693
|
+
});
|
|
3694
|
+
return result;
|
|
3593
3695
|
}
|
|
3594
3696
|
|
|
3595
3697
|
// src/persist.ts
|
|
@@ -3678,14 +3780,14 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
3678
3780
|
|
|
3679
3781
|
// src/watch.ts
|
|
3680
3782
|
init_cjs_shims();
|
|
3681
|
-
var
|
|
3783
|
+
var import_node_path33 = __toESM(require("path"), 1);
|
|
3682
3784
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
3683
3785
|
|
|
3684
3786
|
// src/api.ts
|
|
3685
3787
|
init_cjs_shims();
|
|
3686
3788
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
3687
3789
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
3688
|
-
var
|
|
3790
|
+
var import_types19 = require("@neat.is/types");
|
|
3689
3791
|
|
|
3690
3792
|
// src/diff.ts
|
|
3691
3793
|
init_cjs_shims();
|
|
@@ -3816,6 +3918,216 @@ var Projects = class {
|
|
|
3816
3918
|
}
|
|
3817
3919
|
};
|
|
3818
3920
|
|
|
3921
|
+
// src/registry.ts
|
|
3922
|
+
init_cjs_shims();
|
|
3923
|
+
var import_node_fs17 = require("fs");
|
|
3924
|
+
var import_node_os2 = __toESM(require("os"), 1);
|
|
3925
|
+
var import_node_path29 = __toESM(require("path"), 1);
|
|
3926
|
+
var import_types18 = require("@neat.is/types");
|
|
3927
|
+
var LOCK_TIMEOUT_MS = 5e3;
|
|
3928
|
+
var LOCK_RETRY_MS = 50;
|
|
3929
|
+
function neatHome() {
|
|
3930
|
+
const override = process.env.NEAT_HOME;
|
|
3931
|
+
if (override && override.length > 0) return import_node_path29.default.resolve(override);
|
|
3932
|
+
return import_node_path29.default.join(import_node_os2.default.homedir(), ".neat");
|
|
3933
|
+
}
|
|
3934
|
+
function registryPath() {
|
|
3935
|
+
return import_node_path29.default.join(neatHome(), "projects.json");
|
|
3936
|
+
}
|
|
3937
|
+
function registryLockPath() {
|
|
3938
|
+
return import_node_path29.default.join(neatHome(), "projects.json.lock");
|
|
3939
|
+
}
|
|
3940
|
+
async function normalizeProjectPath(input) {
|
|
3941
|
+
const resolved = import_node_path29.default.resolve(input);
|
|
3942
|
+
try {
|
|
3943
|
+
return await import_node_fs17.promises.realpath(resolved);
|
|
3944
|
+
} catch {
|
|
3945
|
+
return resolved;
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
async function writeAtomically(target, contents) {
|
|
3949
|
+
await import_node_fs17.promises.mkdir(import_node_path29.default.dirname(target), { recursive: true });
|
|
3950
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
3951
|
+
const fd = await import_node_fs17.promises.open(tmp, "w");
|
|
3952
|
+
try {
|
|
3953
|
+
await fd.writeFile(contents, "utf8");
|
|
3954
|
+
await fd.sync();
|
|
3955
|
+
} finally {
|
|
3956
|
+
await fd.close();
|
|
3957
|
+
}
|
|
3958
|
+
await import_node_fs17.promises.rename(tmp, target);
|
|
3959
|
+
}
|
|
3960
|
+
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
3961
|
+
const deadline = Date.now() + timeoutMs;
|
|
3962
|
+
await import_node_fs17.promises.mkdir(import_node_path29.default.dirname(lockPath), { recursive: true });
|
|
3963
|
+
while (true) {
|
|
3964
|
+
try {
|
|
3965
|
+
const fd = await import_node_fs17.promises.open(lockPath, "wx");
|
|
3966
|
+
await fd.close();
|
|
3967
|
+
return;
|
|
3968
|
+
} catch (err) {
|
|
3969
|
+
const code = err.code;
|
|
3970
|
+
if (code !== "EEXIST") throw err;
|
|
3971
|
+
if (Date.now() >= deadline) {
|
|
3972
|
+
throw new Error(
|
|
3973
|
+
`neat registry: timed out after ${timeoutMs}ms waiting for ${lockPath}. Another neat process is holding the lock; if no such process exists, remove the file by hand.`
|
|
3974
|
+
);
|
|
3975
|
+
}
|
|
3976
|
+
await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
async function releaseLock(lockPath) {
|
|
3981
|
+
await import_node_fs17.promises.unlink(lockPath).catch(() => {
|
|
3982
|
+
});
|
|
3983
|
+
}
|
|
3984
|
+
async function withLock(fn) {
|
|
3985
|
+
const lock = registryLockPath();
|
|
3986
|
+
await acquireLock(lock);
|
|
3987
|
+
try {
|
|
3988
|
+
return await fn();
|
|
3989
|
+
} finally {
|
|
3990
|
+
await releaseLock(lock);
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
async function readRegistry() {
|
|
3994
|
+
const file = registryPath();
|
|
3995
|
+
let raw;
|
|
3996
|
+
try {
|
|
3997
|
+
raw = await import_node_fs17.promises.readFile(file, "utf8");
|
|
3998
|
+
} catch (err) {
|
|
3999
|
+
if (err.code === "ENOENT") {
|
|
4000
|
+
return { version: 1, projects: [] };
|
|
4001
|
+
}
|
|
4002
|
+
throw err;
|
|
4003
|
+
}
|
|
4004
|
+
const parsed = JSON.parse(raw);
|
|
4005
|
+
return import_types18.RegistryFileSchema.parse(parsed);
|
|
4006
|
+
}
|
|
4007
|
+
async function writeRegistry(reg) {
|
|
4008
|
+
const validated = import_types18.RegistryFileSchema.parse(reg);
|
|
4009
|
+
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
4010
|
+
}
|
|
4011
|
+
var ProjectNameCollisionError = class extends Error {
|
|
4012
|
+
projectName;
|
|
4013
|
+
constructor(name) {
|
|
4014
|
+
super(`neat registry: a project named "${name}" is already registered`);
|
|
4015
|
+
this.name = "ProjectNameCollisionError";
|
|
4016
|
+
this.projectName = name;
|
|
4017
|
+
}
|
|
4018
|
+
};
|
|
4019
|
+
async function addProject(opts) {
|
|
4020
|
+
const resolvedPath = await normalizeProjectPath(opts.path);
|
|
4021
|
+
return withLock(async () => {
|
|
4022
|
+
const reg = await readRegistry();
|
|
4023
|
+
const byName = reg.projects.find((p) => p.name === opts.name);
|
|
4024
|
+
const byPath = reg.projects.find((p) => p.path === resolvedPath);
|
|
4025
|
+
if (byName && byName.path !== resolvedPath) {
|
|
4026
|
+
throw new ProjectNameCollisionError(opts.name);
|
|
4027
|
+
}
|
|
4028
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4029
|
+
if (byName && byName.path === resolvedPath) {
|
|
4030
|
+
byName.lastSeenAt = now;
|
|
4031
|
+
if (opts.languages) byName.languages = opts.languages;
|
|
4032
|
+
if (opts.status) byName.status = opts.status;
|
|
4033
|
+
await writeRegistry(reg);
|
|
4034
|
+
return byName;
|
|
4035
|
+
}
|
|
4036
|
+
if (byPath && byPath.name !== opts.name) {
|
|
4037
|
+
throw new ProjectNameCollisionError(byPath.name);
|
|
4038
|
+
}
|
|
4039
|
+
const entry2 = {
|
|
4040
|
+
name: opts.name,
|
|
4041
|
+
path: resolvedPath,
|
|
4042
|
+
registeredAt: now,
|
|
4043
|
+
languages: opts.languages ?? [],
|
|
4044
|
+
status: opts.status ?? "active"
|
|
4045
|
+
};
|
|
4046
|
+
reg.projects.push(entry2);
|
|
4047
|
+
await writeRegistry(reg);
|
|
4048
|
+
return entry2;
|
|
4049
|
+
});
|
|
4050
|
+
}
|
|
4051
|
+
async function listProjects() {
|
|
4052
|
+
const reg = await readRegistry();
|
|
4053
|
+
return reg.projects;
|
|
4054
|
+
}
|
|
4055
|
+
async function setStatus(name, status2) {
|
|
4056
|
+
return withLock(async () => {
|
|
4057
|
+
const reg = await readRegistry();
|
|
4058
|
+
const entry2 = reg.projects.find((p) => p.name === name);
|
|
4059
|
+
if (!entry2) throw new Error(`neat registry: no project named "${name}"`);
|
|
4060
|
+
entry2.status = status2;
|
|
4061
|
+
await writeRegistry(reg);
|
|
4062
|
+
return entry2;
|
|
4063
|
+
});
|
|
4064
|
+
}
|
|
4065
|
+
async function removeProject(name) {
|
|
4066
|
+
return withLock(async () => {
|
|
4067
|
+
const reg = await readRegistry();
|
|
4068
|
+
const idx = reg.projects.findIndex((p) => p.name === name);
|
|
4069
|
+
if (idx < 0) return void 0;
|
|
4070
|
+
const [removed] = reg.projects.splice(idx, 1);
|
|
4071
|
+
await writeRegistry(reg);
|
|
4072
|
+
return removed;
|
|
4073
|
+
});
|
|
4074
|
+
}
|
|
4075
|
+
|
|
4076
|
+
// src/streaming.ts
|
|
4077
|
+
init_cjs_shims();
|
|
4078
|
+
var SSE_HEARTBEAT_MS = 3e4;
|
|
4079
|
+
var SSE_BACKPRESSURE_CAP = 1e3;
|
|
4080
|
+
function handleSse(req, reply, opts) {
|
|
4081
|
+
const heartbeatMs = opts.heartbeatMs ?? SSE_HEARTBEAT_MS;
|
|
4082
|
+
const backpressureCap = opts.backpressureCap ?? SSE_BACKPRESSURE_CAP;
|
|
4083
|
+
reply.raw.setHeader("Content-Type", "text/event-stream");
|
|
4084
|
+
reply.raw.setHeader("Cache-Control", "no-cache, no-transform");
|
|
4085
|
+
reply.raw.setHeader("Connection", "keep-alive");
|
|
4086
|
+
reply.raw.setHeader("X-Accel-Buffering", "no");
|
|
4087
|
+
reply.raw.flushHeaders?.();
|
|
4088
|
+
let pending = 0;
|
|
4089
|
+
let dropped = false;
|
|
4090
|
+
const closeConnection = () => {
|
|
4091
|
+
if (dropped) return;
|
|
4092
|
+
dropped = true;
|
|
4093
|
+
eventBus.off(EVENT_BUS_CHANNEL, listener);
|
|
4094
|
+
clearInterval(heartbeat);
|
|
4095
|
+
if (!reply.raw.writableEnded) reply.raw.end();
|
|
4096
|
+
};
|
|
4097
|
+
const writeFrame = (frame) => {
|
|
4098
|
+
if (dropped) return;
|
|
4099
|
+
if (pending >= backpressureCap) {
|
|
4100
|
+
const errFrame = `event: error
|
|
4101
|
+
data: ${JSON.stringify({ reason: "backpressure" })}
|
|
4102
|
+
|
|
4103
|
+
`;
|
|
4104
|
+
reply.raw.write(errFrame);
|
|
4105
|
+
closeConnection();
|
|
4106
|
+
return;
|
|
4107
|
+
}
|
|
4108
|
+
pending++;
|
|
4109
|
+
reply.raw.write(frame, () => {
|
|
4110
|
+
pending = Math.max(0, pending - 1);
|
|
4111
|
+
});
|
|
4112
|
+
};
|
|
4113
|
+
const listener = (envelope) => {
|
|
4114
|
+
if (envelope.project !== opts.project) return;
|
|
4115
|
+
writeFrame(`event: ${envelope.type}
|
|
4116
|
+
data: ${JSON.stringify(envelope.payload)}
|
|
4117
|
+
|
|
4118
|
+
`);
|
|
4119
|
+
};
|
|
4120
|
+
eventBus.on(EVENT_BUS_CHANNEL, listener);
|
|
4121
|
+
const heartbeat = setInterval(() => {
|
|
4122
|
+
if (dropped) return;
|
|
4123
|
+
reply.raw.write(":heartbeat\n\n");
|
|
4124
|
+
}, heartbeatMs);
|
|
4125
|
+
if (typeof heartbeat.unref === "function") heartbeat.unref();
|
|
4126
|
+
req.raw.on("close", closeConnection);
|
|
4127
|
+
reply.raw.on("close", closeConnection);
|
|
4128
|
+
reply.raw.on("error", closeConnection);
|
|
4129
|
+
}
|
|
4130
|
+
|
|
3819
4131
|
// src/api.ts
|
|
3820
4132
|
function serializeGraph(graph) {
|
|
3821
4133
|
const nodes = [];
|
|
@@ -3864,6 +4176,11 @@ function buildLegacyRegistry(opts) {
|
|
|
3864
4176
|
}
|
|
3865
4177
|
function registerRoutes(scope, ctx) {
|
|
3866
4178
|
const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
|
|
4179
|
+
scope.get("/events", (req, reply) => {
|
|
4180
|
+
const proj = resolveProject(registry, req, reply);
|
|
4181
|
+
if (!proj) return;
|
|
4182
|
+
handleSse(req, reply, { project: proj.name });
|
|
4183
|
+
});
|
|
3867
4184
|
scope.get("/health", async (req, reply) => {
|
|
3868
4185
|
const proj = resolveProject(registry, req, reply);
|
|
3869
4186
|
if (!proj) return;
|
|
@@ -4074,7 +4391,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4074
4391
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
4075
4392
|
let violations = await log.readAll();
|
|
4076
4393
|
if (req.query.severity) {
|
|
4077
|
-
const sev =
|
|
4394
|
+
const sev = import_types19.PolicySeveritySchema.safeParse(req.query.severity);
|
|
4078
4395
|
if (!sev.success) {
|
|
4079
4396
|
return reply.code(400).send({
|
|
4080
4397
|
error: "invalid severity",
|
|
@@ -4091,7 +4408,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4091
4408
|
scope.post("/policies/check", async (req, reply) => {
|
|
4092
4409
|
const proj = resolveProject(registry, req, reply);
|
|
4093
4410
|
if (!proj) return;
|
|
4094
|
-
const parsed =
|
|
4411
|
+
const parsed = import_types19.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
4095
4412
|
if (!parsed.success) {
|
|
4096
4413
|
return reply.code(400).send({
|
|
4097
4414
|
error: "invalid /policies/check body",
|
|
@@ -4155,17 +4472,16 @@ async function buildApi(opts) {
|
|
|
4155
4472
|
staleEventsPathFor,
|
|
4156
4473
|
policyFilePathFor
|
|
4157
4474
|
};
|
|
4158
|
-
app.get("/projects", async () =>
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
}));
|
|
4475
|
+
app.get("/projects", async (_req, reply) => {
|
|
4476
|
+
try {
|
|
4477
|
+
return await listProjects();
|
|
4478
|
+
} catch (err) {
|
|
4479
|
+
return reply.code(500).send({
|
|
4480
|
+
error: "failed to read project registry",
|
|
4481
|
+
details: err.message
|
|
4482
|
+
});
|
|
4483
|
+
}
|
|
4484
|
+
});
|
|
4169
4485
|
registerRoutes(app, routeCtx);
|
|
4170
4486
|
await app.register(
|
|
4171
4487
|
async (scope) => {
|
|
@@ -4178,13 +4494,13 @@ async function buildApi(opts) {
|
|
|
4178
4494
|
|
|
4179
4495
|
// src/extract/retire.ts
|
|
4180
4496
|
init_cjs_shims();
|
|
4181
|
-
var
|
|
4497
|
+
var import_types20 = require("@neat.is/types");
|
|
4182
4498
|
function retireEdgesByFile(graph, file) {
|
|
4183
4499
|
const normalized = file.split("\\").join("/");
|
|
4184
4500
|
const toDrop = [];
|
|
4185
4501
|
graph.forEachEdge((id, attrs) => {
|
|
4186
4502
|
const edge = attrs;
|
|
4187
|
-
if (edge.provenance !==
|
|
4503
|
+
if (edge.provenance !== import_types20.Provenance.EXTRACTED) return;
|
|
4188
4504
|
if (!edge.evidence?.file) return;
|
|
4189
4505
|
if (edge.evidence.file === normalized) toDrop.push(id);
|
|
4190
4506
|
});
|
|
@@ -4198,8 +4514,8 @@ init_otel_grpc();
|
|
|
4198
4514
|
|
|
4199
4515
|
// src/search.ts
|
|
4200
4516
|
init_cjs_shims();
|
|
4201
|
-
var
|
|
4202
|
-
var
|
|
4517
|
+
var import_node_fs18 = require("fs");
|
|
4518
|
+
var import_node_path32 = __toESM(require("path"), 1);
|
|
4203
4519
|
var import_node_crypto = require("crypto");
|
|
4204
4520
|
var DEFAULT_LIMIT = 10;
|
|
4205
4521
|
var NOMIC_DIM = 768;
|
|
@@ -4329,7 +4645,7 @@ async function pickEmbedder() {
|
|
|
4329
4645
|
}
|
|
4330
4646
|
async function readCache(cachePath) {
|
|
4331
4647
|
try {
|
|
4332
|
-
const raw = await
|
|
4648
|
+
const raw = await import_node_fs18.promises.readFile(cachePath, "utf8");
|
|
4333
4649
|
const parsed = JSON.parse(raw);
|
|
4334
4650
|
if (parsed.version !== 1) return null;
|
|
4335
4651
|
return parsed;
|
|
@@ -4338,8 +4654,8 @@ async function readCache(cachePath) {
|
|
|
4338
4654
|
}
|
|
4339
4655
|
}
|
|
4340
4656
|
async function writeCache(cachePath, cache) {
|
|
4341
|
-
await
|
|
4342
|
-
await
|
|
4657
|
+
await import_node_fs18.promises.mkdir(import_node_path32.default.dirname(cachePath), { recursive: true });
|
|
4658
|
+
await import_node_fs18.promises.writeFile(cachePath, JSON.stringify(cache));
|
|
4343
4659
|
}
|
|
4344
4660
|
var VectorIndex = class {
|
|
4345
4661
|
constructor(embedder, cachePath) {
|
|
@@ -4494,8 +4810,8 @@ var ALL_PHASES = [
|
|
|
4494
4810
|
];
|
|
4495
4811
|
function classifyChange(relPath) {
|
|
4496
4812
|
const phases = /* @__PURE__ */ new Set();
|
|
4497
|
-
const base =
|
|
4498
|
-
const segments = relPath.split(
|
|
4813
|
+
const base = import_node_path33.default.basename(relPath).toLowerCase();
|
|
4814
|
+
const segments = relPath.split(import_node_path33.default.sep).map((s) => s.toLowerCase());
|
|
4499
4815
|
if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
|
|
4500
4816
|
phases.add("services");
|
|
4501
4817
|
phases.add("aliases");
|
|
@@ -4518,7 +4834,8 @@ function classifyChange(relPath) {
|
|
|
4518
4834
|
}
|
|
4519
4835
|
return phases;
|
|
4520
4836
|
}
|
|
4521
|
-
async function runExtractPhases(graph, scanPath, phases) {
|
|
4837
|
+
async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJECT) {
|
|
4838
|
+
void project;
|
|
4522
4839
|
const started = Date.now();
|
|
4523
4840
|
await ensureCompatLoaded();
|
|
4524
4841
|
const services = await discoverServices(scanPath);
|
|
@@ -4574,9 +4891,11 @@ function shouldIgnore(absPath) {
|
|
|
4574
4891
|
}
|
|
4575
4892
|
async function startWatch(graph, opts) {
|
|
4576
4893
|
const debounceMs = opts.debounceMs ?? 1e3;
|
|
4894
|
+
const projectName = opts.project ?? DEFAULT_PROJECT;
|
|
4577
4895
|
await loadGraphFromDisk(graph, opts.outPath);
|
|
4578
|
-
const
|
|
4579
|
-
const
|
|
4896
|
+
const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
|
|
4897
|
+
const policyFilePath = import_node_path33.default.join(opts.scanPath, "policy.json");
|
|
4898
|
+
const policyViolationsPath = import_node_path33.default.join(import_node_path33.default.dirname(opts.outPath), "policy-violations.ndjson");
|
|
4580
4899
|
let policies = [];
|
|
4581
4900
|
try {
|
|
4582
4901
|
policies = await loadPolicyFile(policyFilePath);
|
|
@@ -4586,7 +4905,7 @@ async function startWatch(graph, opts) {
|
|
|
4586
4905
|
} catch (err) {
|
|
4587
4906
|
console.warn(`policies: failed to load ${policyFilePath} \u2014 ${err.message}`);
|
|
4588
4907
|
}
|
|
4589
|
-
const policyLog = new PolicyViolationsLog(policyViolationsPath);
|
|
4908
|
+
const policyLog = new PolicyViolationsLog(policyViolationsPath, projectName);
|
|
4590
4909
|
const onPolicyTrigger = async (g) => {
|
|
4591
4910
|
if (policies.length === 0) return;
|
|
4592
4911
|
try {
|
|
@@ -4596,20 +4915,36 @@ async function startWatch(graph, opts) {
|
|
|
4596
4915
|
console.warn(`policies: evaluation failed \u2014 ${err.message}`);
|
|
4597
4916
|
}
|
|
4598
4917
|
};
|
|
4599
|
-
const initial = await runExtractPhases(
|
|
4918
|
+
const initial = await runExtractPhases(
|
|
4919
|
+
graph,
|
|
4920
|
+
opts.scanPath,
|
|
4921
|
+
new Set(ALL_PHASES),
|
|
4922
|
+
projectName
|
|
4923
|
+
);
|
|
4600
4924
|
console.log(
|
|
4601
4925
|
`extract: ${initial.nodesAdded} new nodes, ${initial.edgesAdded} new edges (graph total ${graph.order}/${graph.size})`
|
|
4602
4926
|
);
|
|
4927
|
+
emitNeatEvent({
|
|
4928
|
+
type: "extraction-complete",
|
|
4929
|
+
project: projectName,
|
|
4930
|
+
payload: {
|
|
4931
|
+
project: projectName,
|
|
4932
|
+
fileCount: 0,
|
|
4933
|
+
nodesAdded: initial.nodesAdded,
|
|
4934
|
+
edgesAdded: initial.edgesAdded
|
|
4935
|
+
}
|
|
4936
|
+
});
|
|
4603
4937
|
await onPolicyTrigger(graph);
|
|
4604
4938
|
const stopPersist = startPersistLoop(graph, opts.outPath);
|
|
4605
4939
|
const stopStaleness = startStalenessLoop(graph, {
|
|
4606
4940
|
staleEventsPath: opts.staleEventsPath,
|
|
4941
|
+
project: projectName,
|
|
4607
4942
|
onPolicyTrigger
|
|
4608
4943
|
});
|
|
4609
4944
|
const host = opts.host ?? "0.0.0.0";
|
|
4610
4945
|
const port = opts.port ?? 8080;
|
|
4611
4946
|
const otelPort = opts.otelPort ?? 4318;
|
|
4612
|
-
const cachePath = opts.embeddingsCachePath ??
|
|
4947
|
+
const cachePath = opts.embeddingsCachePath ?? import_node_path33.default.join(import_node_path33.default.dirname(opts.outPath), "embeddings.json");
|
|
4613
4948
|
let searchIndex;
|
|
4614
4949
|
try {
|
|
4615
4950
|
searchIndex = await buildSearchIndex(graph, { cachePath });
|
|
@@ -4619,7 +4954,6 @@ async function startWatch(graph, opts) {
|
|
|
4619
4954
|
`semantic_search: index build failed (${err.message}); falling back to inline substring`
|
|
4620
4955
|
);
|
|
4621
4956
|
}
|
|
4622
|
-
const projectName = opts.project ?? DEFAULT_PROJECT;
|
|
4623
4957
|
const registry = new Projects();
|
|
4624
4958
|
registry.set(projectName, {
|
|
4625
4959
|
graph,
|
|
@@ -4628,7 +4962,7 @@ async function startWatch(graph, opts) {
|
|
|
4628
4962
|
// Paths are derived from the explicit options the watch caller passes
|
|
4629
4963
|
// — pathsForProject is only used to fill in the embeddings/snapshot
|
|
4630
4964
|
// fields so the registry shape is complete.
|
|
4631
|
-
...pathsForProject(projectName,
|
|
4965
|
+
...pathsForProject(projectName, import_node_path33.default.dirname(opts.outPath)),
|
|
4632
4966
|
snapshotPath: opts.outPath,
|
|
4633
4967
|
errorsPath: opts.errorsPath,
|
|
4634
4968
|
staleEventsPath: opts.staleEventsPath
|
|
@@ -4644,6 +4978,7 @@ async function startWatch(graph, opts) {
|
|
|
4644
4978
|
const onSpan = makeSpanHandler({
|
|
4645
4979
|
graph,
|
|
4646
4980
|
errorsPath: opts.errorsPath,
|
|
4981
|
+
project: projectName,
|
|
4647
4982
|
writeErrorEventInline: false,
|
|
4648
4983
|
onPolicyTrigger
|
|
4649
4984
|
});
|
|
@@ -4657,6 +4992,7 @@ async function startWatch(graph, opts) {
|
|
|
4657
4992
|
const onSpanGrpc = makeSpanHandler({
|
|
4658
4993
|
graph,
|
|
4659
4994
|
errorsPath: opts.errorsPath,
|
|
4995
|
+
project: projectName,
|
|
4660
4996
|
onPolicyTrigger
|
|
4661
4997
|
});
|
|
4662
4998
|
const r = await startOtelGrpcReceiver({ onSpan: onSpanGrpc, host, port: grpcPort });
|
|
@@ -4676,10 +5012,20 @@ async function startWatch(graph, opts) {
|
|
|
4676
5012
|
try {
|
|
4677
5013
|
let retired = 0;
|
|
4678
5014
|
for (const p of paths) retired += retireEdgesByFile(graph, p);
|
|
4679
|
-
const result = await runExtractPhases(graph, opts.scanPath, phases);
|
|
5015
|
+
const result = await runExtractPhases(graph, opts.scanPath, phases, projectName);
|
|
4680
5016
|
console.log(
|
|
4681
5017
|
`[watch] re-extract phases=${result.phases.join(",")} retired=${retired} +${result.nodesAdded}n/+${result.edgesAdded}e in ${result.durationMs}ms`
|
|
4682
5018
|
);
|
|
5019
|
+
emitNeatEvent({
|
|
5020
|
+
type: "extraction-complete",
|
|
5021
|
+
project: projectName,
|
|
5022
|
+
payload: {
|
|
5023
|
+
project: projectName,
|
|
5024
|
+
fileCount: paths.size,
|
|
5025
|
+
nodesAdded: result.nodesAdded,
|
|
5026
|
+
edgesAdded: result.edgesAdded
|
|
5027
|
+
}
|
|
5028
|
+
});
|
|
4683
5029
|
if (searchIndex) {
|
|
4684
5030
|
try {
|
|
4685
5031
|
await searchIndex.refresh(graph);
|
|
@@ -4701,9 +5047,9 @@ async function startWatch(graph, opts) {
|
|
|
4701
5047
|
};
|
|
4702
5048
|
const onPath = (absPath) => {
|
|
4703
5049
|
if (shouldIgnore(absPath)) return;
|
|
4704
|
-
const rel =
|
|
5050
|
+
const rel = import_node_path33.default.relative(opts.scanPath, absPath);
|
|
4705
5051
|
if (!rel || rel.startsWith("..")) return;
|
|
4706
|
-
pendingPaths.add(rel.split(
|
|
5052
|
+
pendingPaths.add(rel.split(import_node_path33.default.sep).join("/"));
|
|
4707
5053
|
const phases = classifyChange(rel);
|
|
4708
5054
|
if (phases.size === 0) {
|
|
4709
5055
|
for (const p of ALL_PHASES) pending.add(p);
|
|
@@ -4738,6 +5084,7 @@ async function startWatch(graph, opts) {
|
|
|
4738
5084
|
await watcher.close();
|
|
4739
5085
|
stopStaleness();
|
|
4740
5086
|
stopPersist();
|
|
5087
|
+
detachEventBus();
|
|
4741
5088
|
await api.close();
|
|
4742
5089
|
await otelHttp.close();
|
|
4743
5090
|
if (grpcReceiver) await grpcReceiver.stop();
|
|
@@ -4745,182 +5092,27 @@ async function startWatch(graph, opts) {
|
|
|
4745
5092
|
return { api, stop };
|
|
4746
5093
|
}
|
|
4747
5094
|
|
|
4748
|
-
// src/
|
|
5095
|
+
// src/installers/index.ts
|
|
4749
5096
|
init_cjs_shims();
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
var
|
|
4754
|
-
var
|
|
4755
|
-
var
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
try {
|
|
4770
|
-
return await import_node_fs18.promises.realpath(resolved);
|
|
4771
|
-
} catch {
|
|
4772
|
-
return resolved;
|
|
4773
|
-
}
|
|
4774
|
-
}
|
|
4775
|
-
async function writeAtomically(target, contents) {
|
|
4776
|
-
await import_node_fs18.promises.mkdir(import_node_path33.default.dirname(target), { recursive: true });
|
|
4777
|
-
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
4778
|
-
const fd = await import_node_fs18.promises.open(tmp, "w");
|
|
4779
|
-
try {
|
|
4780
|
-
await fd.writeFile(contents, "utf8");
|
|
4781
|
-
await fd.sync();
|
|
4782
|
-
} finally {
|
|
4783
|
-
await fd.close();
|
|
4784
|
-
}
|
|
4785
|
-
await import_node_fs18.promises.rename(tmp, target);
|
|
4786
|
-
}
|
|
4787
|
-
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
4788
|
-
const deadline = Date.now() + timeoutMs;
|
|
4789
|
-
await import_node_fs18.promises.mkdir(import_node_path33.default.dirname(lockPath), { recursive: true });
|
|
4790
|
-
while (true) {
|
|
4791
|
-
try {
|
|
4792
|
-
const fd = await import_node_fs18.promises.open(lockPath, "wx");
|
|
4793
|
-
await fd.close();
|
|
4794
|
-
return;
|
|
4795
|
-
} catch (err) {
|
|
4796
|
-
const code = err.code;
|
|
4797
|
-
if (code !== "EEXIST") throw err;
|
|
4798
|
-
if (Date.now() >= deadline) {
|
|
4799
|
-
throw new Error(
|
|
4800
|
-
`neat registry: timed out after ${timeoutMs}ms waiting for ${lockPath}. Another neat process is holding the lock; if no such process exists, remove the file by hand.`
|
|
4801
|
-
);
|
|
4802
|
-
}
|
|
4803
|
-
await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
|
|
4804
|
-
}
|
|
4805
|
-
}
|
|
4806
|
-
}
|
|
4807
|
-
async function releaseLock(lockPath) {
|
|
4808
|
-
await import_node_fs18.promises.unlink(lockPath).catch(() => {
|
|
4809
|
-
});
|
|
4810
|
-
}
|
|
4811
|
-
async function withLock(fn) {
|
|
4812
|
-
const lock = registryLockPath();
|
|
4813
|
-
await acquireLock(lock);
|
|
4814
|
-
try {
|
|
4815
|
-
return await fn();
|
|
4816
|
-
} finally {
|
|
4817
|
-
await releaseLock(lock);
|
|
4818
|
-
}
|
|
4819
|
-
}
|
|
4820
|
-
async function readRegistry() {
|
|
4821
|
-
const file = registryPath();
|
|
4822
|
-
let raw;
|
|
4823
|
-
try {
|
|
4824
|
-
raw = await import_node_fs18.promises.readFile(file, "utf8");
|
|
4825
|
-
} catch (err) {
|
|
4826
|
-
if (err.code === "ENOENT") {
|
|
4827
|
-
return { version: 1, projects: [] };
|
|
4828
|
-
}
|
|
4829
|
-
throw err;
|
|
4830
|
-
}
|
|
4831
|
-
const parsed = JSON.parse(raw);
|
|
4832
|
-
return import_types20.RegistryFileSchema.parse(parsed);
|
|
4833
|
-
}
|
|
4834
|
-
async function writeRegistry(reg) {
|
|
4835
|
-
const validated = import_types20.RegistryFileSchema.parse(reg);
|
|
4836
|
-
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
4837
|
-
}
|
|
4838
|
-
var ProjectNameCollisionError = class extends Error {
|
|
4839
|
-
projectName;
|
|
4840
|
-
constructor(name) {
|
|
4841
|
-
super(`neat registry: a project named "${name}" is already registered`);
|
|
4842
|
-
this.name = "ProjectNameCollisionError";
|
|
4843
|
-
this.projectName = name;
|
|
4844
|
-
}
|
|
4845
|
-
};
|
|
4846
|
-
async function addProject(opts) {
|
|
4847
|
-
const resolvedPath = await normalizeProjectPath(opts.path);
|
|
4848
|
-
return withLock(async () => {
|
|
4849
|
-
const reg = await readRegistry();
|
|
4850
|
-
const byName = reg.projects.find((p) => p.name === opts.name);
|
|
4851
|
-
const byPath = reg.projects.find((p) => p.path === resolvedPath);
|
|
4852
|
-
if (byName && byName.path !== resolvedPath) {
|
|
4853
|
-
throw new ProjectNameCollisionError(opts.name);
|
|
4854
|
-
}
|
|
4855
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4856
|
-
if (byName && byName.path === resolvedPath) {
|
|
4857
|
-
byName.lastSeenAt = now;
|
|
4858
|
-
if (opts.languages) byName.languages = opts.languages;
|
|
4859
|
-
if (opts.status) byName.status = opts.status;
|
|
4860
|
-
await writeRegistry(reg);
|
|
4861
|
-
return byName;
|
|
4862
|
-
}
|
|
4863
|
-
if (byPath && byPath.name !== opts.name) {
|
|
4864
|
-
throw new ProjectNameCollisionError(byPath.name);
|
|
4865
|
-
}
|
|
4866
|
-
const entry2 = {
|
|
4867
|
-
name: opts.name,
|
|
4868
|
-
path: resolvedPath,
|
|
4869
|
-
registeredAt: now,
|
|
4870
|
-
languages: opts.languages ?? [],
|
|
4871
|
-
status: opts.status ?? "active"
|
|
4872
|
-
};
|
|
4873
|
-
reg.projects.push(entry2);
|
|
4874
|
-
await writeRegistry(reg);
|
|
4875
|
-
return entry2;
|
|
4876
|
-
});
|
|
4877
|
-
}
|
|
4878
|
-
async function listProjects() {
|
|
4879
|
-
const reg = await readRegistry();
|
|
4880
|
-
return reg.projects;
|
|
4881
|
-
}
|
|
4882
|
-
async function setStatus(name, status2) {
|
|
4883
|
-
return withLock(async () => {
|
|
4884
|
-
const reg = await readRegistry();
|
|
4885
|
-
const entry2 = reg.projects.find((p) => p.name === name);
|
|
4886
|
-
if (!entry2) throw new Error(`neat registry: no project named "${name}"`);
|
|
4887
|
-
entry2.status = status2;
|
|
4888
|
-
await writeRegistry(reg);
|
|
4889
|
-
return entry2;
|
|
4890
|
-
});
|
|
4891
|
-
}
|
|
4892
|
-
async function removeProject(name) {
|
|
4893
|
-
return withLock(async () => {
|
|
4894
|
-
const reg = await readRegistry();
|
|
4895
|
-
const idx = reg.projects.findIndex((p) => p.name === name);
|
|
4896
|
-
if (idx < 0) return void 0;
|
|
4897
|
-
const [removed] = reg.projects.splice(idx, 1);
|
|
4898
|
-
await writeRegistry(reg);
|
|
4899
|
-
return removed;
|
|
4900
|
-
});
|
|
4901
|
-
}
|
|
4902
|
-
|
|
4903
|
-
// src/installers/index.ts
|
|
4904
|
-
init_cjs_shims();
|
|
4905
|
-
|
|
4906
|
-
// src/installers/javascript.ts
|
|
4907
|
-
init_cjs_shims();
|
|
4908
|
-
var import_node_fs19 = require("fs");
|
|
4909
|
-
var import_node_path34 = __toESM(require("path"), 1);
|
|
4910
|
-
var SDK_PACKAGES = [
|
|
4911
|
-
{ name: "@opentelemetry/api", version: "^1.9.0" },
|
|
4912
|
-
{ name: "@opentelemetry/sdk-node", version: "^0.57.0" },
|
|
4913
|
-
{ name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
|
|
4914
|
-
];
|
|
4915
|
-
var AUTO_INSTRUMENT_REQUIRE = "--require @opentelemetry/auto-instrumentations-node/register";
|
|
4916
|
-
var OTEL_ENV = {
|
|
4917
|
-
// null target — NEAT does not write `.env` itself; the user sets the env
|
|
4918
|
-
// var in their orchestration layer.
|
|
4919
|
-
file: null,
|
|
4920
|
-
key: "OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
4921
|
-
value: "http://localhost:4318"
|
|
4922
|
-
};
|
|
4923
|
-
async function readPackageJson(serviceDir) {
|
|
5097
|
+
|
|
5098
|
+
// src/installers/javascript.ts
|
|
5099
|
+
init_cjs_shims();
|
|
5100
|
+
var import_node_fs19 = require("fs");
|
|
5101
|
+
var import_node_path34 = __toESM(require("path"), 1);
|
|
5102
|
+
var SDK_PACKAGES = [
|
|
5103
|
+
{ name: "@opentelemetry/api", version: "^1.9.0" },
|
|
5104
|
+
{ name: "@opentelemetry/sdk-node", version: "^0.57.0" },
|
|
5105
|
+
{ name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
|
|
5106
|
+
];
|
|
5107
|
+
var AUTO_INSTRUMENT_REQUIRE = "--require @opentelemetry/auto-instrumentations-node/register";
|
|
5108
|
+
var OTEL_ENV = {
|
|
5109
|
+
// null target — NEAT does not write `.env` itself; the user sets the env
|
|
5110
|
+
// var in their orchestration layer.
|
|
5111
|
+
file: null,
|
|
5112
|
+
key: "OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
5113
|
+
value: "http://localhost:4318"
|
|
5114
|
+
};
|
|
5115
|
+
async function readPackageJson(serviceDir) {
|
|
4924
5116
|
try {
|
|
4925
5117
|
const raw = await import_node_fs19.promises.readFile(import_node_path34.default.join(serviceDir, "package.json"), "utf8");
|
|
4926
5118
|
return JSON.parse(raw);
|
|
@@ -5300,11 +5492,457 @@ function renderPatch(sections) {
|
|
|
5300
5492
|
return lines.join("\n");
|
|
5301
5493
|
}
|
|
5302
5494
|
|
|
5495
|
+
// src/cli-client.ts
|
|
5496
|
+
init_cjs_shims();
|
|
5497
|
+
var import_types21 = require("@neat.is/types");
|
|
5498
|
+
var HttpError = class extends Error {
|
|
5499
|
+
constructor(status2, message, responseBody = "") {
|
|
5500
|
+
super(message);
|
|
5501
|
+
this.status = status2;
|
|
5502
|
+
this.responseBody = responseBody;
|
|
5503
|
+
this.name = "HttpError";
|
|
5504
|
+
}
|
|
5505
|
+
status;
|
|
5506
|
+
responseBody;
|
|
5507
|
+
};
|
|
5508
|
+
var TransportError = class extends Error {
|
|
5509
|
+
constructor(message) {
|
|
5510
|
+
super(message);
|
|
5511
|
+
this.name = "TransportError";
|
|
5512
|
+
}
|
|
5513
|
+
};
|
|
5514
|
+
function createHttpClient(baseUrl) {
|
|
5515
|
+
const root = baseUrl.replace(/\/$/, "");
|
|
5516
|
+
return {
|
|
5517
|
+
async get(path37) {
|
|
5518
|
+
let res;
|
|
5519
|
+
try {
|
|
5520
|
+
res = await fetch(`${root}${path37}`);
|
|
5521
|
+
} catch (err) {
|
|
5522
|
+
throw new TransportError(
|
|
5523
|
+
`cannot reach neat-core at ${root}: ${err.message}`
|
|
5524
|
+
);
|
|
5525
|
+
}
|
|
5526
|
+
if (!res.ok) {
|
|
5527
|
+
const body = await res.text().catch(() => "");
|
|
5528
|
+
throw new HttpError(
|
|
5529
|
+
res.status,
|
|
5530
|
+
`${res.status} ${res.statusText} on GET ${path37}: ${body}`,
|
|
5531
|
+
body
|
|
5532
|
+
);
|
|
5533
|
+
}
|
|
5534
|
+
return await res.json();
|
|
5535
|
+
},
|
|
5536
|
+
async post(path37, body) {
|
|
5537
|
+
let res;
|
|
5538
|
+
try {
|
|
5539
|
+
res = await fetch(`${root}${path37}`, {
|
|
5540
|
+
method: "POST",
|
|
5541
|
+
headers: { "content-type": "application/json" },
|
|
5542
|
+
body: JSON.stringify(body)
|
|
5543
|
+
});
|
|
5544
|
+
} catch (err) {
|
|
5545
|
+
throw new TransportError(
|
|
5546
|
+
`cannot reach neat-core at ${root}: ${err.message}`
|
|
5547
|
+
);
|
|
5548
|
+
}
|
|
5549
|
+
if (!res.ok) {
|
|
5550
|
+
const text = await res.text().catch(() => "");
|
|
5551
|
+
throw new HttpError(
|
|
5552
|
+
res.status,
|
|
5553
|
+
`${res.status} ${res.statusText} on POST ${path37}: ${text}`,
|
|
5554
|
+
text
|
|
5555
|
+
);
|
|
5556
|
+
}
|
|
5557
|
+
return await res.json();
|
|
5558
|
+
}
|
|
5559
|
+
};
|
|
5560
|
+
}
|
|
5561
|
+
function projectPath(project, suffix) {
|
|
5562
|
+
if (!project) return suffix;
|
|
5563
|
+
return `/projects/${encodeURIComponent(project)}${suffix}`;
|
|
5564
|
+
}
|
|
5565
|
+
async function runRootCause(client, input) {
|
|
5566
|
+
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
5567
|
+
const path37 = projectPath(
|
|
5568
|
+
input.project,
|
|
5569
|
+
`/traverse/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
5570
|
+
);
|
|
5571
|
+
try {
|
|
5572
|
+
const result = await client.get(path37);
|
|
5573
|
+
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
5574
|
+
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
5575
|
+
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
5576
|
+
const blockLines = [
|
|
5577
|
+
`Traversal path: ${arrowPath}`,
|
|
5578
|
+
`Edge provenances: ${provenances}`
|
|
5579
|
+
];
|
|
5580
|
+
if (result.fixRecommendation) blockLines.push(`Recommended fix: ${result.fixRecommendation}`);
|
|
5581
|
+
return {
|
|
5582
|
+
summary,
|
|
5583
|
+
block: blockLines.join("\n"),
|
|
5584
|
+
confidence: result.confidence,
|
|
5585
|
+
provenance: result.edgeProvenances.length ? result.edgeProvenances : void 0
|
|
5586
|
+
};
|
|
5587
|
+
} catch (err) {
|
|
5588
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
5589
|
+
return {
|
|
5590
|
+
summary: `No root cause found for ${input.errorNode}. The node may be healthy, or it may not exist in the graph.`
|
|
5591
|
+
};
|
|
5592
|
+
}
|
|
5593
|
+
throw err;
|
|
5594
|
+
}
|
|
5595
|
+
}
|
|
5596
|
+
async function runBlastRadius(client, input) {
|
|
5597
|
+
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
5598
|
+
const path37 = projectPath(
|
|
5599
|
+
input.project,
|
|
5600
|
+
`/traverse/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
5601
|
+
);
|
|
5602
|
+
try {
|
|
5603
|
+
const result = await client.get(path37);
|
|
5604
|
+
if (result.totalAffected === 0) {
|
|
5605
|
+
return {
|
|
5606
|
+
summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
|
|
5607
|
+
};
|
|
5608
|
+
}
|
|
5609
|
+
const sorted = [...result.affectedNodes].sort(
|
|
5610
|
+
(a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
|
|
5611
|
+
);
|
|
5612
|
+
const blockLines = sorted.map(formatBlastEntry);
|
|
5613
|
+
const minConfidence = sorted.reduce(
|
|
5614
|
+
(m, n) => Math.min(m, n.confidence),
|
|
5615
|
+
Number.POSITIVE_INFINITY
|
|
5616
|
+
);
|
|
5617
|
+
const provenances = [...new Set(sorted.map((n) => n.edgeProvenance))];
|
|
5618
|
+
return {
|
|
5619
|
+
summary: `Blast radius for ${result.origin}: ${result.totalAffected} affected node${result.totalAffected === 1 ? "" : "s"} reachable downstream.`,
|
|
5620
|
+
block: blockLines.join("\n"),
|
|
5621
|
+
confidence: Number.isFinite(minConfidence) ? minConfidence : void 0,
|
|
5622
|
+
provenance: provenances.length ? provenances : void 0
|
|
5623
|
+
};
|
|
5624
|
+
} catch (err) {
|
|
5625
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
5626
|
+
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
5627
|
+
}
|
|
5628
|
+
throw err;
|
|
5629
|
+
}
|
|
5630
|
+
}
|
|
5631
|
+
function formatBlastEntry(n) {
|
|
5632
|
+
const tag = n.edgeProvenance === import_types21.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
5633
|
+
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
5634
|
+
}
|
|
5635
|
+
async function runDependencies(client, input) {
|
|
5636
|
+
const depth = input.depth ?? 3;
|
|
5637
|
+
const path37 = projectPath(
|
|
5638
|
+
input.project,
|
|
5639
|
+
`/graph/node/${encodeURIComponent(input.nodeId)}/dependencies?depth=${depth}`
|
|
5640
|
+
);
|
|
5641
|
+
try {
|
|
5642
|
+
const result = await client.get(path37);
|
|
5643
|
+
if (result.total === 0) {
|
|
5644
|
+
return {
|
|
5645
|
+
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
5646
|
+
};
|
|
5647
|
+
}
|
|
5648
|
+
const byDistance = /* @__PURE__ */ new Map();
|
|
5649
|
+
for (const dep of result.dependencies) {
|
|
5650
|
+
const ring = byDistance.get(dep.distance) ?? [];
|
|
5651
|
+
ring.push(dep);
|
|
5652
|
+
byDistance.set(dep.distance, ring);
|
|
5653
|
+
}
|
|
5654
|
+
const blockLines = [];
|
|
5655
|
+
for (const distance of [...byDistance.keys()].sort((a, b) => a - b)) {
|
|
5656
|
+
const label = distance === 1 ? "Direct (distance 1)" : `Distance ${distance}`;
|
|
5657
|
+
blockLines.push(`${label}:`);
|
|
5658
|
+
for (const dep of byDistance.get(distance)) {
|
|
5659
|
+
blockLines.push(` \u2022 ${dep.nodeId} \u2014 ${dep.edgeType} (${dep.provenance})`);
|
|
5660
|
+
}
|
|
5661
|
+
}
|
|
5662
|
+
const provenances = [...new Set(result.dependencies.map((d) => d.provenance))];
|
|
5663
|
+
const directCount = byDistance.get(1)?.length ?? 0;
|
|
5664
|
+
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).`;
|
|
5665
|
+
return { summary, block: blockLines.join("\n"), provenance: provenances };
|
|
5666
|
+
} catch (err) {
|
|
5667
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
5668
|
+
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
5669
|
+
}
|
|
5670
|
+
throw err;
|
|
5671
|
+
}
|
|
5672
|
+
}
|
|
5673
|
+
async function runObservedDependencies(client, input) {
|
|
5674
|
+
try {
|
|
5675
|
+
const edges = await client.get(
|
|
5676
|
+
projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
|
|
5677
|
+
);
|
|
5678
|
+
const observed = edges.outbound.filter((e) => e.provenance === import_types21.Provenance.OBSERVED);
|
|
5679
|
+
if (observed.length === 0) {
|
|
5680
|
+
const hasExtracted = edges.outbound.some((e) => e.provenance === import_types21.Provenance.EXTRACTED);
|
|
5681
|
+
const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
5682
|
+
return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
|
|
5683
|
+
}
|
|
5684
|
+
const blockLines = observed.map((e) => ` \u2022 ${e.target} \u2014 ${e.type}${edgeMeta(e)}`);
|
|
5685
|
+
return {
|
|
5686
|
+
summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
5687
|
+
block: blockLines.join("\n"),
|
|
5688
|
+
provenance: import_types21.Provenance.OBSERVED
|
|
5689
|
+
};
|
|
5690
|
+
} catch (err) {
|
|
5691
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
5692
|
+
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
5693
|
+
}
|
|
5694
|
+
throw err;
|
|
5695
|
+
}
|
|
5696
|
+
}
|
|
5697
|
+
function edgeMeta(e) {
|
|
5698
|
+
const bits = [];
|
|
5699
|
+
if (e.signal) {
|
|
5700
|
+
bits.push(`spans=${e.signal.spanCount}`);
|
|
5701
|
+
if (e.signal.errorCount > 0) bits.push(`errors=${e.signal.errorCount}`);
|
|
5702
|
+
if (e.signal.lastObservedAgeMs !== void 0) {
|
|
5703
|
+
bits.push(`age=${formatDuration(e.signal.lastObservedAgeMs)}`);
|
|
5704
|
+
}
|
|
5705
|
+
} else if (e.callCount !== void 0) {
|
|
5706
|
+
bits.push(`callCount=${e.callCount}`);
|
|
5707
|
+
}
|
|
5708
|
+
if (e.lastObserved) bits.push(`lastObserved=${e.lastObserved}`);
|
|
5709
|
+
if (e.confidence !== void 0) bits.push(`confidence=${e.confidence}`);
|
|
5710
|
+
return bits.length ? ` [${bits.join(", ")}]` : "";
|
|
5711
|
+
}
|
|
5712
|
+
function formatDuration(ms) {
|
|
5713
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
5714
|
+
const s = Math.round(ms / 1e3);
|
|
5715
|
+
if (s < 60) return `${s}s`;
|
|
5716
|
+
const m = Math.round(s / 60);
|
|
5717
|
+
if (m < 60) return `${m}m`;
|
|
5718
|
+
const h = Math.round(m / 60);
|
|
5719
|
+
if (h < 48) return `${h}h`;
|
|
5720
|
+
return `${Math.round(h / 24)}d`;
|
|
5721
|
+
}
|
|
5722
|
+
async function runIncidents(client, input) {
|
|
5723
|
+
const path37 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
5724
|
+
try {
|
|
5725
|
+
const events = await client.get(path37);
|
|
5726
|
+
if (events.length === 0) {
|
|
5727
|
+
return {
|
|
5728
|
+
summary: input.nodeId ? `No incidents recorded against ${input.nodeId}.` : "No incidents recorded."
|
|
5729
|
+
};
|
|
5730
|
+
}
|
|
5731
|
+
const ordered = [...events].reverse().slice(0, input.limit ?? 20);
|
|
5732
|
+
const blockLines = [];
|
|
5733
|
+
for (const ev of ordered) {
|
|
5734
|
+
blockLines.push(` ${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`);
|
|
5735
|
+
blockLines.push(` trace=${ev.traceId} span=${ev.spanId}`);
|
|
5736
|
+
}
|
|
5737
|
+
const target = input.nodeId ?? "the project";
|
|
5738
|
+
return {
|
|
5739
|
+
summary: `${target} has ${events.length} recorded incident${events.length === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
5740
|
+
block: blockLines.join("\n"),
|
|
5741
|
+
provenance: import_types21.Provenance.OBSERVED
|
|
5742
|
+
};
|
|
5743
|
+
} catch (err) {
|
|
5744
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
5745
|
+
return { summary: `Node ${input.nodeId ?? ""} not found in the graph.` };
|
|
5746
|
+
}
|
|
5747
|
+
throw err;
|
|
5748
|
+
}
|
|
5749
|
+
}
|
|
5750
|
+
async function runSearch(client, input) {
|
|
5751
|
+
const result = await client.get(
|
|
5752
|
+
projectPath(input.project, `/search?q=${encodeURIComponent(input.query)}`)
|
|
5753
|
+
);
|
|
5754
|
+
if (result.matches.length === 0) {
|
|
5755
|
+
return { summary: `No matches for "${input.query}".` };
|
|
5756
|
+
}
|
|
5757
|
+
const provider = result.provider ?? "substring";
|
|
5758
|
+
const blockLines = [];
|
|
5759
|
+
let topScore;
|
|
5760
|
+
for (const n of result.matches) {
|
|
5761
|
+
const score = provider !== "substring" && typeof n.score === "number" ? n.score : void 0;
|
|
5762
|
+
const scoreBit = score !== void 0 ? ` [score=${score.toFixed(2)}]` : "";
|
|
5763
|
+
if (score !== void 0 && (topScore === void 0 || score > topScore)) topScore = score;
|
|
5764
|
+
blockLines.push(
|
|
5765
|
+
` \u2022 ${n.id} (${n.type}) \u2014 ${n.name ?? n.id}${scoreBit}`
|
|
5766
|
+
);
|
|
5767
|
+
}
|
|
5768
|
+
return {
|
|
5769
|
+
summary: `Found ${result.matches.length} match${result.matches.length === 1 ? "" : "es"} for "${input.query}" via ${provider} provider.`,
|
|
5770
|
+
block: blockLines.join("\n"),
|
|
5771
|
+
confidence: topScore
|
|
5772
|
+
};
|
|
5773
|
+
}
|
|
5774
|
+
async function runDiff(client, input) {
|
|
5775
|
+
const result = await client.get(
|
|
5776
|
+
projectPath(
|
|
5777
|
+
input.project,
|
|
5778
|
+
`/graph/diff?against=${encodeURIComponent(input.againstSnapshot)}`
|
|
5779
|
+
)
|
|
5780
|
+
);
|
|
5781
|
+
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;
|
|
5782
|
+
const baseLabel = result.base.exportedAt ?? "unknown";
|
|
5783
|
+
if (total === 0) {
|
|
5784
|
+
return {
|
|
5785
|
+
summary: `No differences between the current graph and ${input.againstSnapshot} (base exportedAt=${baseLabel}).`
|
|
5786
|
+
};
|
|
5787
|
+
}
|
|
5788
|
+
const blockLines = [
|
|
5789
|
+
` base exportedAt: ${baseLabel}`,
|
|
5790
|
+
` current exportedAt: ${result.current.exportedAt}`,
|
|
5791
|
+
""
|
|
5792
|
+
];
|
|
5793
|
+
if (result.added.nodes.length || result.added.edges.length) {
|
|
5794
|
+
blockLines.push("Added:");
|
|
5795
|
+
for (const n of result.added.nodes) blockLines.push(` + node ${n.id} (${n.type})`);
|
|
5796
|
+
for (const e of result.added.edges)
|
|
5797
|
+
blockLines.push(` + edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
|
|
5798
|
+
blockLines.push("");
|
|
5799
|
+
}
|
|
5800
|
+
if (result.removed.nodes.length || result.removed.edges.length) {
|
|
5801
|
+
blockLines.push("Removed:");
|
|
5802
|
+
for (const n of result.removed.nodes) blockLines.push(` - node ${n.id} (${n.type})`);
|
|
5803
|
+
for (const e of result.removed.edges)
|
|
5804
|
+
blockLines.push(` - edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
|
|
5805
|
+
blockLines.push("");
|
|
5806
|
+
}
|
|
5807
|
+
if (result.changed.nodes.length || result.changed.edges.length) {
|
|
5808
|
+
blockLines.push("Changed:");
|
|
5809
|
+
for (const c of result.changed.nodes) {
|
|
5810
|
+
blockLines.push(` ~ node ${c.id} \u2014 ${summariseAttrDiff(c.before, c.after)}`);
|
|
5811
|
+
}
|
|
5812
|
+
for (const c of result.changed.edges) {
|
|
5813
|
+
const provBit = c.before.provenance !== c.after.provenance ? `provenance ${c.before.provenance} \u2192 ${c.after.provenance}` : summariseAttrDiff(c.before, c.after);
|
|
5814
|
+
blockLines.push(` ~ edge ${c.id} \u2014 ${provBit}`);
|
|
5815
|
+
}
|
|
5816
|
+
}
|
|
5817
|
+
return {
|
|
5818
|
+
summary: `Diff against ${input.againstSnapshot}: ${total} change${total === 1 ? "" : "s"} between the snapshot and the live graph.`,
|
|
5819
|
+
block: blockLines.join("\n").trimEnd()
|
|
5820
|
+
};
|
|
5821
|
+
}
|
|
5822
|
+
function summariseAttrDiff(before, after) {
|
|
5823
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
5824
|
+
const changed = [];
|
|
5825
|
+
for (const k of keys) {
|
|
5826
|
+
if (JSON.stringify(before[k]) !== JSON.stringify(after[k])) changed.push(k);
|
|
5827
|
+
}
|
|
5828
|
+
return changed.length === 0 ? "attributes differ" : `fields changed: ${changed.sort().join(", ")}`;
|
|
5829
|
+
}
|
|
5830
|
+
async function runStaleEdges(client, input) {
|
|
5831
|
+
const params = new URLSearchParams();
|
|
5832
|
+
if (input.limit !== void 0) params.set("limit", String(input.limit));
|
|
5833
|
+
if (input.edgeType) params.set("edgeType", input.edgeType);
|
|
5834
|
+
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
5835
|
+
const events = await client.get(
|
|
5836
|
+
projectPath(input.project, `/incidents/stale${qs}`)
|
|
5837
|
+
);
|
|
5838
|
+
if (events.length === 0) {
|
|
5839
|
+
return {
|
|
5840
|
+
summary: input.edgeType ? `No stale ${input.edgeType} edges recorded.` : "No stale-edge transitions recorded yet."
|
|
5841
|
+
};
|
|
5842
|
+
}
|
|
5843
|
+
const blockLines = events.map(
|
|
5844
|
+
(e) => ` ${e.transitionedAt} \u2014 ${e.source} -[${e.edgeType}]-> ${e.target} (last seen ${e.lastObserved}, threshold ${formatDuration(e.thresholdMs)})`
|
|
5845
|
+
);
|
|
5846
|
+
return {
|
|
5847
|
+
summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
|
|
5848
|
+
block: blockLines.join("\n"),
|
|
5849
|
+
provenance: import_types21.Provenance.STALE
|
|
5850
|
+
};
|
|
5851
|
+
}
|
|
5852
|
+
async function runPolicies(client, input) {
|
|
5853
|
+
let violations;
|
|
5854
|
+
let allowed = true;
|
|
5855
|
+
let hypothetical;
|
|
5856
|
+
if (input.hypotheticalAction) {
|
|
5857
|
+
if (typeof client.post !== "function") {
|
|
5858
|
+
throw new Error("HttpClient does not support POST \u2014 required for policies dry-run");
|
|
5859
|
+
}
|
|
5860
|
+
const body = await client.post(
|
|
5861
|
+
projectPath(input.project, "/policies/check"),
|
|
5862
|
+
{ hypotheticalAction: input.hypotheticalAction }
|
|
5863
|
+
);
|
|
5864
|
+
violations = body.violations;
|
|
5865
|
+
allowed = body.allowed;
|
|
5866
|
+
hypothetical = body.hypotheticalAction;
|
|
5867
|
+
} else {
|
|
5868
|
+
const params = new URLSearchParams();
|
|
5869
|
+
if (input.policyId) params.set("policyId", input.policyId);
|
|
5870
|
+
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
5871
|
+
violations = await client.get(
|
|
5872
|
+
projectPath(input.project, `/policies/violations${qs}`)
|
|
5873
|
+
);
|
|
5874
|
+
allowed = violations.every((v) => v.onViolation !== "block");
|
|
5875
|
+
}
|
|
5876
|
+
if (input.nodeId) {
|
|
5877
|
+
violations = violations.filter(
|
|
5878
|
+
(v) => v.subject.nodeId === input.nodeId || v.subject.path?.includes(input.nodeId)
|
|
5879
|
+
);
|
|
5880
|
+
}
|
|
5881
|
+
if (violations.length === 0) {
|
|
5882
|
+
return {
|
|
5883
|
+
summary: hypothetical ? `No violations would result from the hypothetical action (${hypothetical.kind}).` : "No policy violations recorded."
|
|
5884
|
+
};
|
|
5885
|
+
}
|
|
5886
|
+
const blockCount = violations.filter((v) => v.onViolation === "block").length;
|
|
5887
|
+
const summaryParts = [];
|
|
5888
|
+
if (hypothetical) {
|
|
5889
|
+
summaryParts.push(
|
|
5890
|
+
`Hypothetical ${hypothetical.kind} would surface ${violations.length} violation${violations.length === 1 ? "" : "s"}`
|
|
5891
|
+
);
|
|
5892
|
+
} else {
|
|
5893
|
+
summaryParts.push(
|
|
5894
|
+
`${violations.length} policy violation${violations.length === 1 ? "" : "s"} currently recorded`
|
|
5895
|
+
);
|
|
5896
|
+
}
|
|
5897
|
+
if (blockCount > 0) summaryParts.push(`${blockCount} of which block`);
|
|
5898
|
+
if (!allowed && hypothetical) summaryParts.push("action denied");
|
|
5899
|
+
const summary = summaryParts.join("; ") + ".";
|
|
5900
|
+
const blockLines = violations.map((v) => {
|
|
5901
|
+
const subject = v.subject.nodeId ?? v.subject.edgeId ?? v.subject.path?.[0] ?? "(global)";
|
|
5902
|
+
return ` \u2022 [${v.severity}/${v.onViolation}] ${v.policyName}: ${v.message} \u2014 ${subject}`;
|
|
5903
|
+
});
|
|
5904
|
+
const severities = [...new Set(violations.map((v) => v.severity))];
|
|
5905
|
+
return {
|
|
5906
|
+
summary,
|
|
5907
|
+
block: blockLines.join("\n"),
|
|
5908
|
+
confidence: hypothetical ? 0.7 : 1,
|
|
5909
|
+
provenance: severities.join(" ")
|
|
5910
|
+
};
|
|
5911
|
+
}
|
|
5912
|
+
function formatFooter(confidence, provenance) {
|
|
5913
|
+
const c = confidence === void 0 ? "n/a" : confidence.toFixed(2);
|
|
5914
|
+
const p = provenance === void 0 ? "n/a" : Array.isArray(provenance) ? [...new Set(provenance)].join(", ") : provenance;
|
|
5915
|
+
return `confidence: ${c} \xB7 provenance: ${p}`;
|
|
5916
|
+
}
|
|
5917
|
+
function formatHuman(result) {
|
|
5918
|
+
const sections = [result.summary.trim()];
|
|
5919
|
+
if (result.block && result.block.trim().length > 0) sections.push(result.block.trimEnd());
|
|
5920
|
+
sections.push(formatFooter(result.confidence, result.provenance));
|
|
5921
|
+
return sections.join("\n\n");
|
|
5922
|
+
}
|
|
5923
|
+
function formatJson(result) {
|
|
5924
|
+
return JSON.stringify(
|
|
5925
|
+
{
|
|
5926
|
+
summary: result.summary,
|
|
5927
|
+
block: result.block ?? "",
|
|
5928
|
+
confidence: result.confidence ?? null,
|
|
5929
|
+
provenance: result.provenance ?? null
|
|
5930
|
+
},
|
|
5931
|
+
null,
|
|
5932
|
+
2
|
|
5933
|
+
);
|
|
5934
|
+
}
|
|
5935
|
+
function exitCodeForError(err) {
|
|
5936
|
+
if (err instanceof TransportError) return 3;
|
|
5937
|
+
if (err instanceof HttpError) return 1;
|
|
5938
|
+
return 1;
|
|
5939
|
+
}
|
|
5940
|
+
|
|
5303
5941
|
// src/cli.ts
|
|
5304
5942
|
function usage() {
|
|
5305
5943
|
console.log("usage: neat <command> [args] [--project <name>]");
|
|
5306
5944
|
console.log("");
|
|
5307
|
-
console.log("commands:");
|
|
5945
|
+
console.log("lifecycle commands:");
|
|
5308
5946
|
console.log(" init <path> One-time install: discover, extract, register, plan SDK install.");
|
|
5309
5947
|
console.log(" Snapshot lands in <path>/neat-out/graph.json by default");
|
|
5310
5948
|
console.log(" (or <path>/neat-out/<project>.json for non-default).");
|
|
@@ -5326,51 +5964,133 @@ function usage() {
|
|
|
5326
5964
|
console.log(" --print-config print the JSON snippet to stdout");
|
|
5327
5965
|
console.log(" --apply merge mcpServers.neat into ~/.claude.json");
|
|
5328
5966
|
console.log("");
|
|
5967
|
+
console.log("query commands (mirror the MCP tools, ADR-050):");
|
|
5968
|
+
console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
|
|
5969
|
+
console.log(" example: neat root-cause service:<name>");
|
|
5970
|
+
console.log(" blast-radius <node-id> BFS outbound \u2014 what would break if this dies.");
|
|
5971
|
+
console.log(" example: neat blast-radius database:<host>");
|
|
5972
|
+
console.log(" dependencies <node-id> Transitive outbound dependencies.");
|
|
5973
|
+
console.log(" Flags: --depth N (default 3, max 10)");
|
|
5974
|
+
console.log(" example: neat dependencies service:<name> --depth 2");
|
|
5975
|
+
console.log(" observed-dependencies <node-id> OBSERVED-only outbound edges (runtime traffic).");
|
|
5976
|
+
console.log(" example: neat observed-dependencies service:<name>");
|
|
5977
|
+
console.log(" incidents [<node-id>] Recent error events; per-node when an id is given.");
|
|
5978
|
+
console.log(" Flags: --limit N (default 20)");
|
|
5979
|
+
console.log(" example: neat incidents service:<name> --limit 5");
|
|
5980
|
+
console.log(" search <query> Semantic (or substring) match on node names/ids.");
|
|
5981
|
+
console.log(' example: neat search "checkout"');
|
|
5982
|
+
console.log(" diff --against <snapshot> Compare the live graph to a saved snapshot.");
|
|
5983
|
+
console.log(" example: neat diff --against ./snapshots/baseline.json");
|
|
5984
|
+
console.log(" stale-edges Recent OBSERVED \u2192 STALE transitions.");
|
|
5985
|
+
console.log(" Flags: --limit N, --edge-type CALLS|CONNECTS_TO|...");
|
|
5986
|
+
console.log(" example: neat stale-edges --edge-type CALLS");
|
|
5987
|
+
console.log(" policies Current policy violations.");
|
|
5988
|
+
console.log(" Flags: --node <id>, --hypothetical-action <json>");
|
|
5989
|
+
console.log(" example: neat policies --node service:<name> --json");
|
|
5990
|
+
console.log("");
|
|
5329
5991
|
console.log("flags:");
|
|
5330
5992
|
console.log(' --project <name> Name the project this command targets. Default: "default".');
|
|
5331
|
-
|
|
5993
|
+
console.log(" --json Emit machine-readable JSON instead of human text. Query verbs only.");
|
|
5994
|
+
console.log("");
|
|
5995
|
+
console.log("exit codes:");
|
|
5996
|
+
console.log(" 0 success");
|
|
5997
|
+
console.log(" 1 server error (4xx/5xx body printed to stderr)");
|
|
5998
|
+
console.log(" 2 misuse (missing args, bad flags) \u2014 handled before any network call");
|
|
5999
|
+
console.log(" 3 daemon not reachable (connection refused / timeout)");
|
|
6000
|
+
console.log("");
|
|
6001
|
+
console.log("environment:");
|
|
6002
|
+
console.log(" NEAT_API_URL base URL for the core REST API (default http://localhost:8080)");
|
|
6003
|
+
console.log(" NEAT_PROJECT project name when --project isn't passed");
|
|
6004
|
+
}
|
|
6005
|
+
var STRING_FLAGS = [
|
|
6006
|
+
["--project", "project"],
|
|
6007
|
+
["--depth", "depth"],
|
|
6008
|
+
["--limit", "limit"],
|
|
6009
|
+
["--edge-type", "edgeType"],
|
|
6010
|
+
["--node", "node"],
|
|
6011
|
+
["--since", "since"],
|
|
6012
|
+
["--against", "against"],
|
|
6013
|
+
["--error-id", "errorId"],
|
|
6014
|
+
["--hypothetical-action", "hypotheticalAction"]
|
|
6015
|
+
];
|
|
5332
6016
|
function parseArgs(rest) {
|
|
5333
6017
|
const positional = [];
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
6018
|
+
const out = {
|
|
6019
|
+
project: null,
|
|
6020
|
+
apply: false,
|
|
6021
|
+
dryRun: false,
|
|
6022
|
+
noInstall: false,
|
|
6023
|
+
printConfig: false,
|
|
6024
|
+
json: false,
|
|
6025
|
+
depth: null,
|
|
6026
|
+
limit: null,
|
|
6027
|
+
edgeType: null,
|
|
6028
|
+
node: null,
|
|
6029
|
+
since: null,
|
|
6030
|
+
against: null,
|
|
6031
|
+
errorId: null,
|
|
6032
|
+
hypotheticalAction: null,
|
|
6033
|
+
positional: []
|
|
6034
|
+
};
|
|
5339
6035
|
for (let i = 0; i < rest.length; i++) {
|
|
5340
6036
|
const arg = rest[i];
|
|
5341
|
-
if (arg === "--project") {
|
|
5342
|
-
const next = rest[i + 1];
|
|
5343
|
-
if (!next) {
|
|
5344
|
-
console.error("neat: --project requires a value");
|
|
5345
|
-
process.exit(2);
|
|
5346
|
-
}
|
|
5347
|
-
project = next;
|
|
5348
|
-
i++;
|
|
5349
|
-
continue;
|
|
5350
|
-
}
|
|
5351
|
-
if (arg.startsWith("--project=")) {
|
|
5352
|
-
project = arg.slice("--project=".length);
|
|
5353
|
-
continue;
|
|
5354
|
-
}
|
|
5355
6037
|
if (arg === "--apply") {
|
|
5356
|
-
|
|
6038
|
+
out.apply = true;
|
|
5357
6039
|
continue;
|
|
5358
6040
|
}
|
|
5359
6041
|
if (arg === "--dry-run") {
|
|
5360
|
-
dryRun = true;
|
|
6042
|
+
out.dryRun = true;
|
|
5361
6043
|
continue;
|
|
5362
6044
|
}
|
|
5363
6045
|
if (arg === "--no-install") {
|
|
5364
|
-
noInstall = true;
|
|
6046
|
+
out.noInstall = true;
|
|
5365
6047
|
continue;
|
|
5366
6048
|
}
|
|
5367
6049
|
if (arg === "--print-config") {
|
|
5368
|
-
printConfig = true;
|
|
6050
|
+
out.printConfig = true;
|
|
6051
|
+
continue;
|
|
6052
|
+
}
|
|
6053
|
+
if (arg === "--json") {
|
|
6054
|
+
out.json = true;
|
|
5369
6055
|
continue;
|
|
5370
6056
|
}
|
|
6057
|
+
let matched = false;
|
|
6058
|
+
for (const [flag, field] of STRING_FLAGS) {
|
|
6059
|
+
if (arg === flag) {
|
|
6060
|
+
const next = rest[i + 1];
|
|
6061
|
+
if (next === void 0) {
|
|
6062
|
+
console.error(`neat: ${flag} requires a value`);
|
|
6063
|
+
process.exit(2);
|
|
6064
|
+
}
|
|
6065
|
+
assignFlag(out, field, next);
|
|
6066
|
+
i++;
|
|
6067
|
+
matched = true;
|
|
6068
|
+
break;
|
|
6069
|
+
}
|
|
6070
|
+
if (arg.startsWith(`${flag}=`)) {
|
|
6071
|
+
assignFlag(out, field, arg.slice(flag.length + 1));
|
|
6072
|
+
matched = true;
|
|
6073
|
+
break;
|
|
6074
|
+
}
|
|
6075
|
+
}
|
|
6076
|
+
if (matched) continue;
|
|
5371
6077
|
positional.push(arg);
|
|
5372
6078
|
}
|
|
5373
|
-
|
|
6079
|
+
out.positional = positional;
|
|
6080
|
+
return out;
|
|
6081
|
+
}
|
|
6082
|
+
function assignFlag(out, field, value) {
|
|
6083
|
+
if (field === "depth" || field === "limit") {
|
|
6084
|
+
const n = Number(value);
|
|
6085
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
6086
|
+
console.error(`neat: --${field === "depth" ? "depth" : "limit"} must be a positive integer`);
|
|
6087
|
+
process.exit(2);
|
|
6088
|
+
}
|
|
6089
|
+
out[field] = n;
|
|
6090
|
+
return;
|
|
6091
|
+
}
|
|
6092
|
+
;
|
|
6093
|
+
out[field] = value;
|
|
5374
6094
|
}
|
|
5375
6095
|
function summarise(nodes, edges) {
|
|
5376
6096
|
const byNode = /* @__PURE__ */ new Map();
|
|
@@ -5734,10 +6454,167 @@ async function main() {
|
|
|
5734
6454
|
console.log("note: neat-out/, policy.json, and other files at the project path were left in place.");
|
|
5735
6455
|
return;
|
|
5736
6456
|
}
|
|
6457
|
+
if (QUERY_VERBS.has(cmd)) {
|
|
6458
|
+
const code = await runQueryVerb(cmd, parsed);
|
|
6459
|
+
if (code !== 0) process.exit(code);
|
|
6460
|
+
return;
|
|
6461
|
+
}
|
|
5737
6462
|
console.error(`neat: unknown command "${cmd}"`);
|
|
5738
6463
|
usage();
|
|
5739
6464
|
process.exit(1);
|
|
5740
6465
|
}
|
|
6466
|
+
var QUERY_VERBS = /* @__PURE__ */ new Set([
|
|
6467
|
+
"root-cause",
|
|
6468
|
+
"blast-radius",
|
|
6469
|
+
"dependencies",
|
|
6470
|
+
"observed-dependencies",
|
|
6471
|
+
"incidents",
|
|
6472
|
+
"search",
|
|
6473
|
+
"diff",
|
|
6474
|
+
"stale-edges",
|
|
6475
|
+
"policies"
|
|
6476
|
+
]);
|
|
6477
|
+
function resolveProjectFlag(parsed) {
|
|
6478
|
+
if (parsed.project) return parsed.project;
|
|
6479
|
+
const env = process.env.NEAT_PROJECT;
|
|
6480
|
+
if (env && env.length > 0 && env !== DEFAULT_PROJECT) return env;
|
|
6481
|
+
return void 0;
|
|
6482
|
+
}
|
|
6483
|
+
async function runQueryVerb(cmd, parsed) {
|
|
6484
|
+
const baseUrl = process.env.NEAT_API_URL ?? "http://localhost:8080";
|
|
6485
|
+
const client = createHttpClient(baseUrl);
|
|
6486
|
+
const project = resolveProjectFlag(parsed);
|
|
6487
|
+
const positional = parsed.positional;
|
|
6488
|
+
let work;
|
|
6489
|
+
switch (cmd) {
|
|
6490
|
+
case "root-cause": {
|
|
6491
|
+
const node = positional[0];
|
|
6492
|
+
if (!node) {
|
|
6493
|
+
console.error("neat root-cause: missing <node-id>");
|
|
6494
|
+
return 2;
|
|
6495
|
+
}
|
|
6496
|
+
work = runRootCause(client, {
|
|
6497
|
+
errorNode: node,
|
|
6498
|
+
...parsed.errorId ? { errorId: parsed.errorId } : {},
|
|
6499
|
+
...project ? { project } : {}
|
|
6500
|
+
});
|
|
6501
|
+
break;
|
|
6502
|
+
}
|
|
6503
|
+
case "blast-radius": {
|
|
6504
|
+
const node = positional[0];
|
|
6505
|
+
if (!node) {
|
|
6506
|
+
console.error("neat blast-radius: missing <node-id>");
|
|
6507
|
+
return 2;
|
|
6508
|
+
}
|
|
6509
|
+
work = runBlastRadius(client, {
|
|
6510
|
+
nodeId: node,
|
|
6511
|
+
...parsed.depth !== null ? { depth: parsed.depth } : {},
|
|
6512
|
+
...project ? { project } : {}
|
|
6513
|
+
});
|
|
6514
|
+
break;
|
|
6515
|
+
}
|
|
6516
|
+
case "dependencies": {
|
|
6517
|
+
const node = positional[0];
|
|
6518
|
+
if (!node) {
|
|
6519
|
+
console.error("neat dependencies: missing <node-id>");
|
|
6520
|
+
return 2;
|
|
6521
|
+
}
|
|
6522
|
+
work = runDependencies(client, {
|
|
6523
|
+
nodeId: node,
|
|
6524
|
+
...parsed.depth !== null ? { depth: parsed.depth } : {},
|
|
6525
|
+
...project ? { project } : {}
|
|
6526
|
+
});
|
|
6527
|
+
break;
|
|
6528
|
+
}
|
|
6529
|
+
case "observed-dependencies": {
|
|
6530
|
+
const node = positional[0];
|
|
6531
|
+
if (!node) {
|
|
6532
|
+
console.error("neat observed-dependencies: missing <node-id>");
|
|
6533
|
+
return 2;
|
|
6534
|
+
}
|
|
6535
|
+
work = runObservedDependencies(client, {
|
|
6536
|
+
nodeId: node,
|
|
6537
|
+
...project ? { project } : {}
|
|
6538
|
+
});
|
|
6539
|
+
break;
|
|
6540
|
+
}
|
|
6541
|
+
case "incidents": {
|
|
6542
|
+
work = runIncidents(client, {
|
|
6543
|
+
...positional[0] ? { nodeId: positional[0] } : {},
|
|
6544
|
+
...parsed.limit !== null ? { limit: parsed.limit } : {},
|
|
6545
|
+
...project ? { project } : {}
|
|
6546
|
+
});
|
|
6547
|
+
break;
|
|
6548
|
+
}
|
|
6549
|
+
case "search": {
|
|
6550
|
+
const q = positional.join(" ").trim();
|
|
6551
|
+
if (!q) {
|
|
6552
|
+
console.error("neat search: missing <query>");
|
|
6553
|
+
return 2;
|
|
6554
|
+
}
|
|
6555
|
+
work = runSearch(client, { query: q, ...project ? { project } : {} });
|
|
6556
|
+
break;
|
|
6557
|
+
}
|
|
6558
|
+
case "diff": {
|
|
6559
|
+
const against = parsed.against ?? parsed.since;
|
|
6560
|
+
if (!against) {
|
|
6561
|
+
console.error("neat diff: --against <snapshot-path> is required");
|
|
6562
|
+
return 2;
|
|
6563
|
+
}
|
|
6564
|
+
work = runDiff(client, {
|
|
6565
|
+
againstSnapshot: against,
|
|
6566
|
+
...project ? { project } : {}
|
|
6567
|
+
});
|
|
6568
|
+
break;
|
|
6569
|
+
}
|
|
6570
|
+
case "stale-edges": {
|
|
6571
|
+
work = runStaleEdges(client, {
|
|
6572
|
+
...parsed.limit !== null ? { limit: parsed.limit } : {},
|
|
6573
|
+
...parsed.edgeType ? { edgeType: parsed.edgeType } : {},
|
|
6574
|
+
...project ? { project } : {}
|
|
6575
|
+
});
|
|
6576
|
+
break;
|
|
6577
|
+
}
|
|
6578
|
+
case "policies": {
|
|
6579
|
+
let hypothetical;
|
|
6580
|
+
if (parsed.hypotheticalAction) {
|
|
6581
|
+
try {
|
|
6582
|
+
hypothetical = JSON.parse(parsed.hypotheticalAction);
|
|
6583
|
+
} catch (err) {
|
|
6584
|
+
console.error(
|
|
6585
|
+
`neat policies: --hypothetical-action must be valid JSON: ${err.message}`
|
|
6586
|
+
);
|
|
6587
|
+
return 2;
|
|
6588
|
+
}
|
|
6589
|
+
}
|
|
6590
|
+
work = runPolicies(client, {
|
|
6591
|
+
...parsed.node ? { nodeId: parsed.node } : {},
|
|
6592
|
+
...hypothetical ? { hypotheticalAction: hypothetical } : {},
|
|
6593
|
+
...project ? { project } : {}
|
|
6594
|
+
});
|
|
6595
|
+
break;
|
|
6596
|
+
}
|
|
6597
|
+
default:
|
|
6598
|
+
console.error(`neat: unknown query verb "${cmd}"`);
|
|
6599
|
+
return 2;
|
|
6600
|
+
}
|
|
6601
|
+
try {
|
|
6602
|
+
const result = await work;
|
|
6603
|
+
if (parsed.json) process.stdout.write(formatJson(result) + "\n");
|
|
6604
|
+
else process.stdout.write(formatHuman(result) + "\n");
|
|
6605
|
+
return 0;
|
|
6606
|
+
} catch (err) {
|
|
6607
|
+
if (err instanceof HttpError) {
|
|
6608
|
+
const detail = err.responseBody.length > 0 ? err.responseBody : err.message;
|
|
6609
|
+
console.error(`neat ${cmd}: ${detail.trim()}`);
|
|
6610
|
+
} else if (err instanceof TransportError) {
|
|
6611
|
+
console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (NEAT_API_URL=${process.env.NEAT_API_URL ?? "http://localhost:8080"})`);
|
|
6612
|
+
} else {
|
|
6613
|
+
console.error(`neat ${cmd}: ${err.message}`);
|
|
6614
|
+
}
|
|
6615
|
+
return exitCodeForError(err);
|
|
6616
|
+
}
|
|
6617
|
+
}
|
|
5741
6618
|
var entry = process.argv[1] ?? "";
|
|
5742
6619
|
if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsWith("/neat")) {
|
|
5743
6620
|
main().catch((err) => {
|
|
@@ -5748,7 +6625,10 @@ if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsW
|
|
|
5748
6625
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5749
6626
|
0 && (module.exports = {
|
|
5750
6627
|
CLAUDE_SKILL_CONFIG,
|
|
6628
|
+
QUERY_VERBS,
|
|
6629
|
+
parseArgs,
|
|
5751
6630
|
runInit,
|
|
6631
|
+
runQueryVerb,
|
|
5752
6632
|
runSkill
|
|
5753
6633
|
});
|
|
5754
6634
|
//# sourceMappingURL=cli.cjs.map
|