@neat.is/core 0.2.7 → 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 +2 -2
- 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/index.cjs
CHANGED
|
@@ -100,8 +100,8 @@ function reshapeGrpcRequest(req) {
|
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
function resolveProtoRoot() {
|
|
103
|
-
const here =
|
|
104
|
-
return
|
|
103
|
+
const here = import_node_path30.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
104
|
+
return import_node_path30.default.resolve(here, "..", "proto");
|
|
105
105
|
}
|
|
106
106
|
function loadTraceService() {
|
|
107
107
|
const protoRoot = resolveProtoRoot();
|
|
@@ -156,13 +156,13 @@ async function startOtelGrpcReceiver(opts) {
|
|
|
156
156
|
})
|
|
157
157
|
};
|
|
158
158
|
}
|
|
159
|
-
var import_node_url,
|
|
159
|
+
var import_node_url, import_node_path30, grpc, protoLoader;
|
|
160
160
|
var init_otel_grpc = __esm({
|
|
161
161
|
"src/otel-grpc.ts"() {
|
|
162
162
|
"use strict";
|
|
163
163
|
init_cjs_shims();
|
|
164
164
|
import_node_url = require("url");
|
|
165
|
-
|
|
165
|
+
import_node_path30 = __toESM(require("path"), 1);
|
|
166
166
|
grpc = __toESM(require("@grpc/grpc-js"), 1);
|
|
167
167
|
protoLoader = __toESM(require("@grpc/proto-loader"), 1);
|
|
168
168
|
init_otel();
|
|
@@ -259,10 +259,10 @@ function parseOtlpRequest(body) {
|
|
|
259
259
|
}
|
|
260
260
|
function loadProtobufDecoder() {
|
|
261
261
|
if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
|
|
262
|
-
const here =
|
|
263
|
-
const protoRoot =
|
|
262
|
+
const here = import_node_path31.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
263
|
+
const protoRoot = import_node_path31.default.resolve(here, "..", "proto");
|
|
264
264
|
const root = new import_protobufjs.default.Root();
|
|
265
|
-
root.resolvePath = (_origin, target) =>
|
|
265
|
+
root.resolvePath = (_origin, target) => import_node_path31.default.resolve(protoRoot, target);
|
|
266
266
|
root.loadSync(
|
|
267
267
|
"opentelemetry/proto/collector/trace/v1/trace_service.proto",
|
|
268
268
|
{ keepCase: true }
|
|
@@ -362,12 +362,12 @@ function logSpanHandler(span) {
|
|
|
362
362
|
`otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
|
|
363
363
|
);
|
|
364
364
|
}
|
|
365
|
-
var
|
|
365
|
+
var import_node_path31, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
|
|
366
366
|
var init_otel = __esm({
|
|
367
367
|
"src/otel.ts"() {
|
|
368
368
|
"use strict";
|
|
369
369
|
init_cjs_shims();
|
|
370
|
-
|
|
370
|
+
import_node_path31 = __toESM(require("path"), 1);
|
|
371
371
|
import_node_url2 = require("url");
|
|
372
372
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
373
373
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
@@ -777,6 +777,18 @@ function deprecatedApis() {
|
|
|
777
777
|
return currentMatrix().deprecatedApis ?? [];
|
|
778
778
|
}
|
|
779
779
|
|
|
780
|
+
// src/events.ts
|
|
781
|
+
init_cjs_shims();
|
|
782
|
+
var import_node_events = require("events");
|
|
783
|
+
var EVENT_BUS_CHANNEL = "event";
|
|
784
|
+
var NeatEventBus = class extends import_node_events.EventEmitter {
|
|
785
|
+
};
|
|
786
|
+
var eventBus = new NeatEventBus();
|
|
787
|
+
eventBus.setMaxListeners(0);
|
|
788
|
+
function emitNeatEvent(envelope) {
|
|
789
|
+
eventBus.emit(EVENT_BUS_CHANNEL, envelope);
|
|
790
|
+
}
|
|
791
|
+
|
|
780
792
|
// src/traverse.ts
|
|
781
793
|
init_cjs_shims();
|
|
782
794
|
var import_types = require("@neat.is/types");
|
|
@@ -1340,9 +1352,11 @@ async function loadPolicyFile(policyPath) {
|
|
|
1340
1352
|
}
|
|
1341
1353
|
var PolicyViolationsLog = class {
|
|
1342
1354
|
path;
|
|
1355
|
+
project;
|
|
1343
1356
|
seen = null;
|
|
1344
|
-
constructor(logPath) {
|
|
1357
|
+
constructor(logPath, project = DEFAULT_PROJECT) {
|
|
1345
1358
|
this.path = logPath;
|
|
1359
|
+
this.project = project;
|
|
1346
1360
|
}
|
|
1347
1361
|
async append(v) {
|
|
1348
1362
|
if (!this.seen) await this.hydrate();
|
|
@@ -1350,6 +1364,11 @@ var PolicyViolationsLog = class {
|
|
|
1350
1364
|
this.seen.add(v.id);
|
|
1351
1365
|
await import_node_fs2.promises.mkdir(import_node_path2.default.dirname(this.path), { recursive: true });
|
|
1352
1366
|
await import_node_fs2.promises.appendFile(this.path, JSON.stringify(v) + "\n", "utf8");
|
|
1367
|
+
emitNeatEvent({
|
|
1368
|
+
type: "policy-violation",
|
|
1369
|
+
project: this.project,
|
|
1370
|
+
payload: { violation: v }
|
|
1371
|
+
});
|
|
1353
1372
|
return true;
|
|
1354
1373
|
}
|
|
1355
1374
|
async readAll() {
|
|
@@ -1796,6 +1815,7 @@ async function markStaleEdges(graph, options = {}) {
|
|
|
1796
1815
|
const thresholds = options.thresholds ?? loadStaleThresholdsFromEnv();
|
|
1797
1816
|
const now = options.now ?? Date.now();
|
|
1798
1817
|
const events = [];
|
|
1818
|
+
const project = options.project ?? DEFAULT_PROJECT;
|
|
1799
1819
|
graph.forEachEdge((id, attrs) => {
|
|
1800
1820
|
const e = attrs;
|
|
1801
1821
|
if (e.provenance !== import_types3.Provenance.OBSERVED) return;
|
|
@@ -1815,6 +1835,15 @@ async function markStaleEdges(graph, options = {}) {
|
|
|
1815
1835
|
lastObserved: e.lastObserved,
|
|
1816
1836
|
transitionedAt: new Date(now).toISOString()
|
|
1817
1837
|
});
|
|
1838
|
+
emitNeatEvent({
|
|
1839
|
+
type: "stale-transition",
|
|
1840
|
+
project,
|
|
1841
|
+
payload: {
|
|
1842
|
+
edgeId: id,
|
|
1843
|
+
from: import_types3.Provenance.OBSERVED,
|
|
1844
|
+
to: import_types3.Provenance.STALE
|
|
1845
|
+
}
|
|
1846
|
+
});
|
|
1818
1847
|
}
|
|
1819
1848
|
});
|
|
1820
1849
|
if (options.staleEventsPath && events.length > 0) {
|
|
@@ -1845,7 +1874,8 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
1845
1874
|
try {
|
|
1846
1875
|
await markStaleEdges(graph, {
|
|
1847
1876
|
thresholds: options.thresholds,
|
|
1848
|
-
staleEventsPath: options.staleEventsPath
|
|
1877
|
+
staleEventsPath: options.staleEventsPath,
|
|
1878
|
+
project: options.project
|
|
1849
1879
|
});
|
|
1850
1880
|
if (options.onPolicyTrigger) await options.onPolicyTrigger(graph);
|
|
1851
1881
|
} catch (err) {
|
|
@@ -3075,7 +3105,15 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3075
3105
|
const seenTargets = /* @__PURE__ */ new Map();
|
|
3076
3106
|
for (const file of files) {
|
|
3077
3107
|
const parser = import_node_path18.default.extname(file.path) === ".py" ? pyParser : jsParser;
|
|
3078
|
-
|
|
3108
|
+
let targets;
|
|
3109
|
+
try {
|
|
3110
|
+
targets = callsFromSource(file.content, parser, knownHosts);
|
|
3111
|
+
} catch (err) {
|
|
3112
|
+
console.warn(
|
|
3113
|
+
`[neat] http call extraction skipped ${file.path}: ${err.message}`
|
|
3114
|
+
);
|
|
3115
|
+
continue;
|
|
3116
|
+
}
|
|
3079
3117
|
for (const t of targets) {
|
|
3080
3118
|
const targetId = hostToNodeId.get(t);
|
|
3081
3119
|
if (!targetId || targetId === service.node.id) continue;
|
|
@@ -3604,11 +3642,22 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
3604
3642
|
const phase5 = await addInfra(graph, scanPath, services);
|
|
3605
3643
|
const frontiersPromoted = promoteFrontierNodes(graph);
|
|
3606
3644
|
if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
|
|
3607
|
-
|
|
3645
|
+
const result = {
|
|
3608
3646
|
nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
3609
3647
|
edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
3610
3648
|
frontiersPromoted
|
|
3611
3649
|
};
|
|
3650
|
+
emitNeatEvent({
|
|
3651
|
+
type: "extraction-complete",
|
|
3652
|
+
project: opts.project ?? DEFAULT_PROJECT,
|
|
3653
|
+
payload: {
|
|
3654
|
+
project: opts.project ?? DEFAULT_PROJECT,
|
|
3655
|
+
fileCount: services.length,
|
|
3656
|
+
nodesAdded: result.nodesAdded,
|
|
3657
|
+
edgesAdded: result.edgesAdded
|
|
3658
|
+
}
|
|
3659
|
+
});
|
|
3660
|
+
return result;
|
|
3612
3661
|
}
|
|
3613
3662
|
|
|
3614
3663
|
// src/persist.ts
|
|
@@ -3699,7 +3748,7 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
3699
3748
|
init_cjs_shims();
|
|
3700
3749
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
3701
3750
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
3702
|
-
var
|
|
3751
|
+
var import_types19 = require("@neat.is/types");
|
|
3703
3752
|
|
|
3704
3753
|
// src/diff.ts
|
|
3705
3754
|
init_cjs_shims();
|
|
@@ -3830,6 +3879,229 @@ var Projects = class {
|
|
|
3830
3879
|
}
|
|
3831
3880
|
};
|
|
3832
3881
|
|
|
3882
|
+
// src/registry.ts
|
|
3883
|
+
init_cjs_shims();
|
|
3884
|
+
var import_node_fs17 = require("fs");
|
|
3885
|
+
var import_node_os2 = __toESM(require("os"), 1);
|
|
3886
|
+
var import_node_path29 = __toESM(require("path"), 1);
|
|
3887
|
+
var import_types18 = require("@neat.is/types");
|
|
3888
|
+
var LOCK_TIMEOUT_MS = 5e3;
|
|
3889
|
+
var LOCK_RETRY_MS = 50;
|
|
3890
|
+
function neatHome() {
|
|
3891
|
+
const override = process.env.NEAT_HOME;
|
|
3892
|
+
if (override && override.length > 0) return import_node_path29.default.resolve(override);
|
|
3893
|
+
return import_node_path29.default.join(import_node_os2.default.homedir(), ".neat");
|
|
3894
|
+
}
|
|
3895
|
+
function registryPath() {
|
|
3896
|
+
return import_node_path29.default.join(neatHome(), "projects.json");
|
|
3897
|
+
}
|
|
3898
|
+
function registryLockPath() {
|
|
3899
|
+
return import_node_path29.default.join(neatHome(), "projects.json.lock");
|
|
3900
|
+
}
|
|
3901
|
+
async function normalizeProjectPath(input) {
|
|
3902
|
+
const resolved = import_node_path29.default.resolve(input);
|
|
3903
|
+
try {
|
|
3904
|
+
return await import_node_fs17.promises.realpath(resolved);
|
|
3905
|
+
} catch {
|
|
3906
|
+
return resolved;
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
async function writeAtomically(target, contents) {
|
|
3910
|
+
await import_node_fs17.promises.mkdir(import_node_path29.default.dirname(target), { recursive: true });
|
|
3911
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
3912
|
+
const fd = await import_node_fs17.promises.open(tmp, "w");
|
|
3913
|
+
try {
|
|
3914
|
+
await fd.writeFile(contents, "utf8");
|
|
3915
|
+
await fd.sync();
|
|
3916
|
+
} finally {
|
|
3917
|
+
await fd.close();
|
|
3918
|
+
}
|
|
3919
|
+
await import_node_fs17.promises.rename(tmp, target);
|
|
3920
|
+
}
|
|
3921
|
+
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
3922
|
+
const deadline = Date.now() + timeoutMs;
|
|
3923
|
+
await import_node_fs17.promises.mkdir(import_node_path29.default.dirname(lockPath), { recursive: true });
|
|
3924
|
+
while (true) {
|
|
3925
|
+
try {
|
|
3926
|
+
const fd = await import_node_fs17.promises.open(lockPath, "wx");
|
|
3927
|
+
await fd.close();
|
|
3928
|
+
return;
|
|
3929
|
+
} catch (err) {
|
|
3930
|
+
const code = err.code;
|
|
3931
|
+
if (code !== "EEXIST") throw err;
|
|
3932
|
+
if (Date.now() >= deadline) {
|
|
3933
|
+
throw new Error(
|
|
3934
|
+
`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.`
|
|
3935
|
+
);
|
|
3936
|
+
}
|
|
3937
|
+
await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
async function releaseLock(lockPath) {
|
|
3942
|
+
await import_node_fs17.promises.unlink(lockPath).catch(() => {
|
|
3943
|
+
});
|
|
3944
|
+
}
|
|
3945
|
+
async function withLock(fn) {
|
|
3946
|
+
const lock = registryLockPath();
|
|
3947
|
+
await acquireLock(lock);
|
|
3948
|
+
try {
|
|
3949
|
+
return await fn();
|
|
3950
|
+
} finally {
|
|
3951
|
+
await releaseLock(lock);
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
async function readRegistry() {
|
|
3955
|
+
const file = registryPath();
|
|
3956
|
+
let raw;
|
|
3957
|
+
try {
|
|
3958
|
+
raw = await import_node_fs17.promises.readFile(file, "utf8");
|
|
3959
|
+
} catch (err) {
|
|
3960
|
+
if (err.code === "ENOENT") {
|
|
3961
|
+
return { version: 1, projects: [] };
|
|
3962
|
+
}
|
|
3963
|
+
throw err;
|
|
3964
|
+
}
|
|
3965
|
+
const parsed = JSON.parse(raw);
|
|
3966
|
+
return import_types18.RegistryFileSchema.parse(parsed);
|
|
3967
|
+
}
|
|
3968
|
+
async function writeRegistry(reg) {
|
|
3969
|
+
const validated = import_types18.RegistryFileSchema.parse(reg);
|
|
3970
|
+
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
3971
|
+
}
|
|
3972
|
+
var ProjectNameCollisionError = class extends Error {
|
|
3973
|
+
projectName;
|
|
3974
|
+
constructor(name) {
|
|
3975
|
+
super(`neat registry: a project named "${name}" is already registered`);
|
|
3976
|
+
this.name = "ProjectNameCollisionError";
|
|
3977
|
+
this.projectName = name;
|
|
3978
|
+
}
|
|
3979
|
+
};
|
|
3980
|
+
async function addProject(opts) {
|
|
3981
|
+
const resolvedPath = await normalizeProjectPath(opts.path);
|
|
3982
|
+
return withLock(async () => {
|
|
3983
|
+
const reg = await readRegistry();
|
|
3984
|
+
const byName = reg.projects.find((p) => p.name === opts.name);
|
|
3985
|
+
const byPath = reg.projects.find((p) => p.path === resolvedPath);
|
|
3986
|
+
if (byName && byName.path !== resolvedPath) {
|
|
3987
|
+
throw new ProjectNameCollisionError(opts.name);
|
|
3988
|
+
}
|
|
3989
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3990
|
+
if (byName && byName.path === resolvedPath) {
|
|
3991
|
+
byName.lastSeenAt = now;
|
|
3992
|
+
if (opts.languages) byName.languages = opts.languages;
|
|
3993
|
+
if (opts.status) byName.status = opts.status;
|
|
3994
|
+
await writeRegistry(reg);
|
|
3995
|
+
return byName;
|
|
3996
|
+
}
|
|
3997
|
+
if (byPath && byPath.name !== opts.name) {
|
|
3998
|
+
throw new ProjectNameCollisionError(byPath.name);
|
|
3999
|
+
}
|
|
4000
|
+
const entry = {
|
|
4001
|
+
name: opts.name,
|
|
4002
|
+
path: resolvedPath,
|
|
4003
|
+
registeredAt: now,
|
|
4004
|
+
languages: opts.languages ?? [],
|
|
4005
|
+
status: opts.status ?? "active"
|
|
4006
|
+
};
|
|
4007
|
+
reg.projects.push(entry);
|
|
4008
|
+
await writeRegistry(reg);
|
|
4009
|
+
return entry;
|
|
4010
|
+
});
|
|
4011
|
+
}
|
|
4012
|
+
async function getProject(name) {
|
|
4013
|
+
const reg = await readRegistry();
|
|
4014
|
+
return reg.projects.find((p) => p.name === name);
|
|
4015
|
+
}
|
|
4016
|
+
async function listProjects() {
|
|
4017
|
+
const reg = await readRegistry();
|
|
4018
|
+
return reg.projects;
|
|
4019
|
+
}
|
|
4020
|
+
async function setStatus(name, status2) {
|
|
4021
|
+
return withLock(async () => {
|
|
4022
|
+
const reg = await readRegistry();
|
|
4023
|
+
const entry = reg.projects.find((p) => p.name === name);
|
|
4024
|
+
if (!entry) throw new Error(`neat registry: no project named "${name}"`);
|
|
4025
|
+
entry.status = status2;
|
|
4026
|
+
await writeRegistry(reg);
|
|
4027
|
+
return entry;
|
|
4028
|
+
});
|
|
4029
|
+
}
|
|
4030
|
+
async function touchLastSeen(name, at = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
4031
|
+
await withLock(async () => {
|
|
4032
|
+
const reg = await readRegistry();
|
|
4033
|
+
const entry = reg.projects.find((p) => p.name === name);
|
|
4034
|
+
if (!entry) return;
|
|
4035
|
+
entry.lastSeenAt = at;
|
|
4036
|
+
await writeRegistry(reg);
|
|
4037
|
+
});
|
|
4038
|
+
}
|
|
4039
|
+
async function removeProject(name) {
|
|
4040
|
+
return withLock(async () => {
|
|
4041
|
+
const reg = await readRegistry();
|
|
4042
|
+
const idx = reg.projects.findIndex((p) => p.name === name);
|
|
4043
|
+
if (idx < 0) return void 0;
|
|
4044
|
+
const [removed] = reg.projects.splice(idx, 1);
|
|
4045
|
+
await writeRegistry(reg);
|
|
4046
|
+
return removed;
|
|
4047
|
+
});
|
|
4048
|
+
}
|
|
4049
|
+
|
|
4050
|
+
// src/streaming.ts
|
|
4051
|
+
init_cjs_shims();
|
|
4052
|
+
var SSE_HEARTBEAT_MS = 3e4;
|
|
4053
|
+
var SSE_BACKPRESSURE_CAP = 1e3;
|
|
4054
|
+
function handleSse(req, reply, opts) {
|
|
4055
|
+
const heartbeatMs = opts.heartbeatMs ?? SSE_HEARTBEAT_MS;
|
|
4056
|
+
const backpressureCap = opts.backpressureCap ?? SSE_BACKPRESSURE_CAP;
|
|
4057
|
+
reply.raw.setHeader("Content-Type", "text/event-stream");
|
|
4058
|
+
reply.raw.setHeader("Cache-Control", "no-cache, no-transform");
|
|
4059
|
+
reply.raw.setHeader("Connection", "keep-alive");
|
|
4060
|
+
reply.raw.setHeader("X-Accel-Buffering", "no");
|
|
4061
|
+
reply.raw.flushHeaders?.();
|
|
4062
|
+
let pending = 0;
|
|
4063
|
+
let dropped = false;
|
|
4064
|
+
const closeConnection = () => {
|
|
4065
|
+
if (dropped) return;
|
|
4066
|
+
dropped = true;
|
|
4067
|
+
eventBus.off(EVENT_BUS_CHANNEL, listener);
|
|
4068
|
+
clearInterval(heartbeat);
|
|
4069
|
+
if (!reply.raw.writableEnded) reply.raw.end();
|
|
4070
|
+
};
|
|
4071
|
+
const writeFrame = (frame) => {
|
|
4072
|
+
if (dropped) return;
|
|
4073
|
+
if (pending >= backpressureCap) {
|
|
4074
|
+
const errFrame = `event: error
|
|
4075
|
+
data: ${JSON.stringify({ reason: "backpressure" })}
|
|
4076
|
+
|
|
4077
|
+
`;
|
|
4078
|
+
reply.raw.write(errFrame);
|
|
4079
|
+
closeConnection();
|
|
4080
|
+
return;
|
|
4081
|
+
}
|
|
4082
|
+
pending++;
|
|
4083
|
+
reply.raw.write(frame, () => {
|
|
4084
|
+
pending = Math.max(0, pending - 1);
|
|
4085
|
+
});
|
|
4086
|
+
};
|
|
4087
|
+
const listener = (envelope) => {
|
|
4088
|
+
if (envelope.project !== opts.project) return;
|
|
4089
|
+
writeFrame(`event: ${envelope.type}
|
|
4090
|
+
data: ${JSON.stringify(envelope.payload)}
|
|
4091
|
+
|
|
4092
|
+
`);
|
|
4093
|
+
};
|
|
4094
|
+
eventBus.on(EVENT_BUS_CHANNEL, listener);
|
|
4095
|
+
const heartbeat = setInterval(() => {
|
|
4096
|
+
if (dropped) return;
|
|
4097
|
+
reply.raw.write(":heartbeat\n\n");
|
|
4098
|
+
}, heartbeatMs);
|
|
4099
|
+
if (typeof heartbeat.unref === "function") heartbeat.unref();
|
|
4100
|
+
req.raw.on("close", closeConnection);
|
|
4101
|
+
reply.raw.on("close", closeConnection);
|
|
4102
|
+
reply.raw.on("error", closeConnection);
|
|
4103
|
+
}
|
|
4104
|
+
|
|
3833
4105
|
// src/api.ts
|
|
3834
4106
|
function serializeGraph(graph) {
|
|
3835
4107
|
const nodes = [];
|
|
@@ -3878,6 +4150,11 @@ function buildLegacyRegistry(opts) {
|
|
|
3878
4150
|
}
|
|
3879
4151
|
function registerRoutes(scope, ctx) {
|
|
3880
4152
|
const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
|
|
4153
|
+
scope.get("/events", (req, reply) => {
|
|
4154
|
+
const proj = resolveProject(registry, req, reply);
|
|
4155
|
+
if (!proj) return;
|
|
4156
|
+
handleSse(req, reply, { project: proj.name });
|
|
4157
|
+
});
|
|
3881
4158
|
scope.get("/health", async (req, reply) => {
|
|
3882
4159
|
const proj = resolveProject(registry, req, reply);
|
|
3883
4160
|
if (!proj) return;
|
|
@@ -4088,7 +4365,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4088
4365
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
4089
4366
|
let violations = await log.readAll();
|
|
4090
4367
|
if (req.query.severity) {
|
|
4091
|
-
const sev =
|
|
4368
|
+
const sev = import_types19.PolicySeveritySchema.safeParse(req.query.severity);
|
|
4092
4369
|
if (!sev.success) {
|
|
4093
4370
|
return reply.code(400).send({
|
|
4094
4371
|
error: "invalid severity",
|
|
@@ -4105,7 +4382,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4105
4382
|
scope.post("/policies/check", async (req, reply) => {
|
|
4106
4383
|
const proj = resolveProject(registry, req, reply);
|
|
4107
4384
|
if (!proj) return;
|
|
4108
|
-
const parsed =
|
|
4385
|
+
const parsed = import_types19.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
4109
4386
|
if (!parsed.success) {
|
|
4110
4387
|
return reply.code(400).send({
|
|
4111
4388
|
error: "invalid /policies/check body",
|
|
@@ -4169,17 +4446,16 @@ async function buildApi(opts) {
|
|
|
4169
4446
|
staleEventsPathFor,
|
|
4170
4447
|
policyFilePathFor
|
|
4171
4448
|
};
|
|
4172
|
-
app.get("/projects", async () =>
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
}));
|
|
4449
|
+
app.get("/projects", async (_req, reply) => {
|
|
4450
|
+
try {
|
|
4451
|
+
return await listProjects();
|
|
4452
|
+
} catch (err) {
|
|
4453
|
+
return reply.code(500).send({
|
|
4454
|
+
error: "failed to read project registry",
|
|
4455
|
+
details: err.message
|
|
4456
|
+
});
|
|
4457
|
+
}
|
|
4458
|
+
});
|
|
4183
4459
|
registerRoutes(app, routeCtx);
|
|
4184
4460
|
await app.register(
|
|
4185
4461
|
async (scope) => {
|
|
@@ -4198,176 +4474,6 @@ init_otel_grpc();
|
|
|
4198
4474
|
init_cjs_shims();
|
|
4199
4475
|
var import_node_fs18 = require("fs");
|
|
4200
4476
|
var import_node_path32 = __toESM(require("path"), 1);
|
|
4201
|
-
|
|
4202
|
-
// src/registry.ts
|
|
4203
|
-
init_cjs_shims();
|
|
4204
|
-
var import_node_fs17 = require("fs");
|
|
4205
|
-
var import_node_os2 = __toESM(require("os"), 1);
|
|
4206
|
-
var import_node_path31 = __toESM(require("path"), 1);
|
|
4207
|
-
var import_types19 = require("@neat.is/types");
|
|
4208
|
-
var LOCK_TIMEOUT_MS = 5e3;
|
|
4209
|
-
var LOCK_RETRY_MS = 50;
|
|
4210
|
-
function neatHome() {
|
|
4211
|
-
const override = process.env.NEAT_HOME;
|
|
4212
|
-
if (override && override.length > 0) return import_node_path31.default.resolve(override);
|
|
4213
|
-
return import_node_path31.default.join(import_node_os2.default.homedir(), ".neat");
|
|
4214
|
-
}
|
|
4215
|
-
function registryPath() {
|
|
4216
|
-
return import_node_path31.default.join(neatHome(), "projects.json");
|
|
4217
|
-
}
|
|
4218
|
-
function registryLockPath() {
|
|
4219
|
-
return import_node_path31.default.join(neatHome(), "projects.json.lock");
|
|
4220
|
-
}
|
|
4221
|
-
async function normalizeProjectPath(input) {
|
|
4222
|
-
const resolved = import_node_path31.default.resolve(input);
|
|
4223
|
-
try {
|
|
4224
|
-
return await import_node_fs17.promises.realpath(resolved);
|
|
4225
|
-
} catch {
|
|
4226
|
-
return resolved;
|
|
4227
|
-
}
|
|
4228
|
-
}
|
|
4229
|
-
async function writeAtomically(target, contents) {
|
|
4230
|
-
await import_node_fs17.promises.mkdir(import_node_path31.default.dirname(target), { recursive: true });
|
|
4231
|
-
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
4232
|
-
const fd = await import_node_fs17.promises.open(tmp, "w");
|
|
4233
|
-
try {
|
|
4234
|
-
await fd.writeFile(contents, "utf8");
|
|
4235
|
-
await fd.sync();
|
|
4236
|
-
} finally {
|
|
4237
|
-
await fd.close();
|
|
4238
|
-
}
|
|
4239
|
-
await import_node_fs17.promises.rename(tmp, target);
|
|
4240
|
-
}
|
|
4241
|
-
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
4242
|
-
const deadline = Date.now() + timeoutMs;
|
|
4243
|
-
await import_node_fs17.promises.mkdir(import_node_path31.default.dirname(lockPath), { recursive: true });
|
|
4244
|
-
while (true) {
|
|
4245
|
-
try {
|
|
4246
|
-
const fd = await import_node_fs17.promises.open(lockPath, "wx");
|
|
4247
|
-
await fd.close();
|
|
4248
|
-
return;
|
|
4249
|
-
} catch (err) {
|
|
4250
|
-
const code = err.code;
|
|
4251
|
-
if (code !== "EEXIST") throw err;
|
|
4252
|
-
if (Date.now() >= deadline) {
|
|
4253
|
-
throw new Error(
|
|
4254
|
-
`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.`
|
|
4255
|
-
);
|
|
4256
|
-
}
|
|
4257
|
-
await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
|
|
4258
|
-
}
|
|
4259
|
-
}
|
|
4260
|
-
}
|
|
4261
|
-
async function releaseLock(lockPath) {
|
|
4262
|
-
await import_node_fs17.promises.unlink(lockPath).catch(() => {
|
|
4263
|
-
});
|
|
4264
|
-
}
|
|
4265
|
-
async function withLock(fn) {
|
|
4266
|
-
const lock = registryLockPath();
|
|
4267
|
-
await acquireLock(lock);
|
|
4268
|
-
try {
|
|
4269
|
-
return await fn();
|
|
4270
|
-
} finally {
|
|
4271
|
-
await releaseLock(lock);
|
|
4272
|
-
}
|
|
4273
|
-
}
|
|
4274
|
-
async function readRegistry() {
|
|
4275
|
-
const file = registryPath();
|
|
4276
|
-
let raw;
|
|
4277
|
-
try {
|
|
4278
|
-
raw = await import_node_fs17.promises.readFile(file, "utf8");
|
|
4279
|
-
} catch (err) {
|
|
4280
|
-
if (err.code === "ENOENT") {
|
|
4281
|
-
return { version: 1, projects: [] };
|
|
4282
|
-
}
|
|
4283
|
-
throw err;
|
|
4284
|
-
}
|
|
4285
|
-
const parsed = JSON.parse(raw);
|
|
4286
|
-
return import_types19.RegistryFileSchema.parse(parsed);
|
|
4287
|
-
}
|
|
4288
|
-
async function writeRegistry(reg) {
|
|
4289
|
-
const validated = import_types19.RegistryFileSchema.parse(reg);
|
|
4290
|
-
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
4291
|
-
}
|
|
4292
|
-
var ProjectNameCollisionError = class extends Error {
|
|
4293
|
-
projectName;
|
|
4294
|
-
constructor(name) {
|
|
4295
|
-
super(`neat registry: a project named "${name}" is already registered`);
|
|
4296
|
-
this.name = "ProjectNameCollisionError";
|
|
4297
|
-
this.projectName = name;
|
|
4298
|
-
}
|
|
4299
|
-
};
|
|
4300
|
-
async function addProject(opts) {
|
|
4301
|
-
const resolvedPath = await normalizeProjectPath(opts.path);
|
|
4302
|
-
return withLock(async () => {
|
|
4303
|
-
const reg = await readRegistry();
|
|
4304
|
-
const byName = reg.projects.find((p) => p.name === opts.name);
|
|
4305
|
-
const byPath = reg.projects.find((p) => p.path === resolvedPath);
|
|
4306
|
-
if (byName && byName.path !== resolvedPath) {
|
|
4307
|
-
throw new ProjectNameCollisionError(opts.name);
|
|
4308
|
-
}
|
|
4309
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4310
|
-
if (byName && byName.path === resolvedPath) {
|
|
4311
|
-
byName.lastSeenAt = now;
|
|
4312
|
-
if (opts.languages) byName.languages = opts.languages;
|
|
4313
|
-
if (opts.status) byName.status = opts.status;
|
|
4314
|
-
await writeRegistry(reg);
|
|
4315
|
-
return byName;
|
|
4316
|
-
}
|
|
4317
|
-
if (byPath && byPath.name !== opts.name) {
|
|
4318
|
-
throw new ProjectNameCollisionError(byPath.name);
|
|
4319
|
-
}
|
|
4320
|
-
const entry = {
|
|
4321
|
-
name: opts.name,
|
|
4322
|
-
path: resolvedPath,
|
|
4323
|
-
registeredAt: now,
|
|
4324
|
-
languages: opts.languages ?? [],
|
|
4325
|
-
status: opts.status ?? "active"
|
|
4326
|
-
};
|
|
4327
|
-
reg.projects.push(entry);
|
|
4328
|
-
await writeRegistry(reg);
|
|
4329
|
-
return entry;
|
|
4330
|
-
});
|
|
4331
|
-
}
|
|
4332
|
-
async function getProject(name) {
|
|
4333
|
-
const reg = await readRegistry();
|
|
4334
|
-
return reg.projects.find((p) => p.name === name);
|
|
4335
|
-
}
|
|
4336
|
-
async function listProjects() {
|
|
4337
|
-
const reg = await readRegistry();
|
|
4338
|
-
return reg.projects;
|
|
4339
|
-
}
|
|
4340
|
-
async function setStatus(name, status2) {
|
|
4341
|
-
return withLock(async () => {
|
|
4342
|
-
const reg = await readRegistry();
|
|
4343
|
-
const entry = reg.projects.find((p) => p.name === name);
|
|
4344
|
-
if (!entry) throw new Error(`neat registry: no project named "${name}"`);
|
|
4345
|
-
entry.status = status2;
|
|
4346
|
-
await writeRegistry(reg);
|
|
4347
|
-
return entry;
|
|
4348
|
-
});
|
|
4349
|
-
}
|
|
4350
|
-
async function touchLastSeen(name, at = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
4351
|
-
await withLock(async () => {
|
|
4352
|
-
const reg = await readRegistry();
|
|
4353
|
-
const entry = reg.projects.find((p) => p.name === name);
|
|
4354
|
-
if (!entry) return;
|
|
4355
|
-
entry.lastSeenAt = at;
|
|
4356
|
-
await writeRegistry(reg);
|
|
4357
|
-
});
|
|
4358
|
-
}
|
|
4359
|
-
async function removeProject(name) {
|
|
4360
|
-
return withLock(async () => {
|
|
4361
|
-
const reg = await readRegistry();
|
|
4362
|
-
const idx = reg.projects.findIndex((p) => p.name === name);
|
|
4363
|
-
if (idx < 0) return void 0;
|
|
4364
|
-
const [removed] = reg.projects.splice(idx, 1);
|
|
4365
|
-
await writeRegistry(reg);
|
|
4366
|
-
return removed;
|
|
4367
|
-
});
|
|
4368
|
-
}
|
|
4369
|
-
|
|
4370
|
-
// src/daemon.ts
|
|
4371
4477
|
function neatHomeFor(opts) {
|
|
4372
4478
|
if (opts.neatHome && opts.neatHome.length > 0) return import_node_path32.default.resolve(opts.neatHome);
|
|
4373
4479
|
const env = process.env.NEAT_HOME;
|