@neurcode-ai/cli 0.9.61 → 0.9.62
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/LICENSE +201 -0
- package/dist/commands/control-plane.d.ts +3 -0
- package/dist/commands/control-plane.d.ts.map +1 -0
- package/dist/commands/control-plane.js +163 -0
- package/dist/commands/control-plane.js.map +1 -0
- package/dist/commands/export.d.ts +7 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +72 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/fix.d.ts +1 -0
- package/dist/commands/fix.d.ts.map +1 -1
- package/dist/commands/fix.js +3 -0
- package/dist/commands/fix.js.map +1 -1
- package/dist/commands/generate.d.ts +1 -0
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +63 -48
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/patch-apply.d.ts.map +1 -1
- package/dist/commands/patch-apply.js +1 -0
- package/dist/commands/patch-apply.js.map +1 -1
- package/dist/commands/replay.d.ts +3 -0
- package/dist/commands/replay.d.ts.map +1 -0
- package/dist/commands/replay.js +267 -0
- package/dist/commands/replay.js.map +1 -0
- package/dist/commands/verify.d.ts +7 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +281 -149
- package/dist/commands/verify.js.map +1 -1
- package/dist/commands/workspace.d.ts +3 -0
- package/dist/commands/workspace.d.ts.map +1 -0
- package/dist/commands/workspace.js +407 -0
- package/dist/commands/workspace.js.map +1 -0
- package/dist/daemon/server.d.ts +0 -17
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +1038 -53
- package/dist/daemon/server.js.map +1 -1
- package/dist/index.js +296 -12
- package/dist/index.js.map +1 -1
- package/dist/utils/cli-json.d.ts +1 -0
- package/dist/utils/cli-json.d.ts.map +1 -1
- package/dist/utils/cli-json.js +1 -0
- package/dist/utils/cli-json.js.map +1 -1
- package/dist/utils/control-plane.d.ts +171 -0
- package/dist/utils/control-plane.d.ts.map +1 -0
- package/dist/utils/control-plane.js +684 -0
- package/dist/utils/control-plane.js.map +1 -0
- package/dist/utils/execution-bus.d.ts +205 -0
- package/dist/utils/execution-bus.d.ts.map +1 -0
- package/dist/utils/execution-bus.js +1346 -0
- package/dist/utils/execution-bus.js.map +1 -0
- package/dist/utils/gitignore.d.ts +2 -2
- package/dist/utils/gitignore.d.ts.map +1 -1
- package/dist/utils/gitignore.js +27 -14
- package/dist/utils/gitignore.js.map +1 -1
- package/dist/utils/replay-runtime.d.ts +295 -0
- package/dist/utils/replay-runtime.d.ts.map +1 -0
- package/dist/utils/replay-runtime.js +1080 -0
- package/dist/utils/replay-runtime.js.map +1 -0
- package/dist/utils/runtime-events.d.ts +44 -0
- package/dist/utils/runtime-events.d.ts.map +1 -0
- package/dist/utils/runtime-events.js +213 -0
- package/dist/utils/runtime-events.js.map +1 -0
- package/dist/utils/verification-evidence.d.ts +22 -0
- package/dist/utils/verification-evidence.d.ts.map +1 -0
- package/dist/utils/verification-evidence.js +233 -0
- package/dist/utils/verification-evidence.js.map +1 -0
- package/dist/utils/workspace-runtime.d.ts +267 -0
- package/dist/utils/workspace-runtime.d.ts.map +1 -0
- package/dist/utils/workspace-runtime.js +1415 -0
- package/dist/utils/workspace-runtime.js.map +1 -0
- package/package.json +7 -8
package/dist/daemon/server.js
CHANGED
|
@@ -1,21 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Neurcode Daemon V2 — lightweight local HTTP bridge.
|
|
4
|
-
*
|
|
5
|
-
* Runs at http://localhost:4321
|
|
6
|
-
*
|
|
7
|
-
* Routes:
|
|
8
|
-
* POST /verify → neurcode verify --json
|
|
9
|
-
* POST /fix → neurcode fix --json
|
|
10
|
-
* POST /fix/apply-safe → neurcode fix --apply-safe --json
|
|
11
|
-
* POST /patch → neurcode patch + verify (auto state sync)
|
|
12
|
-
* GET /health → { ok: true, version }
|
|
13
|
-
*
|
|
14
|
-
* Implementation notes:
|
|
15
|
-
* - Uses runCliJson (internal CLI invocation utility) — no raw child_process
|
|
16
|
-
* - Only accepts requests from 127.0.0.1 / ::1 / localhost
|
|
17
|
-
* - All responses use unified shape: { success, data, error? }
|
|
18
|
-
*/
|
|
19
2
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
3
|
if (k2 === undefined) k2 = k;
|
|
21
4
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -56,10 +39,20 @@ exports.startDaemon = startDaemon;
|
|
|
56
39
|
const http = __importStar(require("node:http"));
|
|
57
40
|
const path = __importStar(require("node:path"));
|
|
58
41
|
const fs = __importStar(require("node:fs"));
|
|
59
|
-
const
|
|
42
|
+
const execution_bus_1 = require("../utils/execution-bus");
|
|
43
|
+
const runtime_events_1 = require("../utils/runtime-events");
|
|
44
|
+
const control_plane_1 = require("../utils/control-plane");
|
|
45
|
+
const workspace_runtime_1 = require("../utils/workspace-runtime");
|
|
46
|
+
const replay_runtime_1 = require("../utils/replay-runtime");
|
|
60
47
|
// ── Configuration ──────────────────────────────────────────────────────────────
|
|
61
48
|
exports.DAEMON_PORT = 4321;
|
|
62
49
|
exports.DAEMON_HOST = '127.0.0.1';
|
|
50
|
+
const SSE_RETRY_MS = 3000;
|
|
51
|
+
const runtimeEventClients = new Map();
|
|
52
|
+
let runtimeEventClientSeq = 0;
|
|
53
|
+
let runtimeEventUnsubscribe = null;
|
|
54
|
+
let runtimeEventTailTimer = null;
|
|
55
|
+
let runtimeEventTailCursor = null;
|
|
63
56
|
// ── Request helpers ────────────────────────────────────────────────────────────
|
|
64
57
|
function readBody(req) {
|
|
65
58
|
return new Promise((resolve, reject) => {
|
|
@@ -88,42 +81,318 @@ function addCorsHeaders(res, _origin) {
|
|
|
88
81
|
// any non-local TCP connection. CORS * just lets browsers read the response
|
|
89
82
|
// regardless of what origin the dashboard is served from (local dev, prod domain, etc).
|
|
90
83
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
91
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
92
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
84
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS');
|
|
85
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, x-neurcode-source, x-neurcode-actor');
|
|
93
86
|
res.setHeader('Access-Control-Max-Age', '86400');
|
|
94
87
|
}
|
|
88
|
+
function isExecutionActionType(value) {
|
|
89
|
+
if (typeof value !== 'string')
|
|
90
|
+
return false;
|
|
91
|
+
return (value === 'verify'
|
|
92
|
+
|| value === 'fix'
|
|
93
|
+
|| value === 'patch'
|
|
94
|
+
|| value === 'apply-safe'
|
|
95
|
+
|| value === 'reverify'
|
|
96
|
+
|| value === 'policy-sync'
|
|
97
|
+
|| value === 'intent-update');
|
|
98
|
+
}
|
|
95
99
|
function isLoopback(req) {
|
|
96
100
|
const addr = req.socket.remoteAddress ?? '';
|
|
97
101
|
return addr === '127.0.0.1' || addr === '::1' || addr === '::ffff:127.0.0.1';
|
|
98
102
|
}
|
|
103
|
+
function toSource(req) {
|
|
104
|
+
const raw = req.headers['x-neurcode-source'];
|
|
105
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
106
|
+
if (!value)
|
|
107
|
+
return 'daemon';
|
|
108
|
+
const normalized = value.trim().toLowerCase();
|
|
109
|
+
if (normalized === 'cli'
|
|
110
|
+
|| normalized === 'daemon'
|
|
111
|
+
|| normalized === 'dashboard'
|
|
112
|
+
|| normalized === 'vscode'
|
|
113
|
+
|| normalized === 'ci'
|
|
114
|
+
|| normalized === 'mcp'
|
|
115
|
+
|| normalized === 'cursor'
|
|
116
|
+
|| normalized === 'api') {
|
|
117
|
+
return normalized;
|
|
118
|
+
}
|
|
119
|
+
return 'unknown';
|
|
120
|
+
}
|
|
121
|
+
function toActor(req) {
|
|
122
|
+
const raw = req.headers['x-neurcode-actor'];
|
|
123
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
124
|
+
if (value && value.trim().length > 0) {
|
|
125
|
+
return value.trim().slice(0, 120);
|
|
126
|
+
}
|
|
127
|
+
const source = toSource(req);
|
|
128
|
+
if (source === 'vscode')
|
|
129
|
+
return 'vscode-user';
|
|
130
|
+
if (source === 'dashboard')
|
|
131
|
+
return 'dashboard-user';
|
|
132
|
+
if (source === 'ci')
|
|
133
|
+
return 'ci-runner';
|
|
134
|
+
return 'daemon-user';
|
|
135
|
+
}
|
|
136
|
+
function parsePositiveInt(value, fallback) {
|
|
137
|
+
if (!value)
|
|
138
|
+
return fallback;
|
|
139
|
+
const parsed = Number.parseInt(value, 10);
|
|
140
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
141
|
+
return fallback;
|
|
142
|
+
return Math.floor(parsed);
|
|
143
|
+
}
|
|
144
|
+
function toSeverityFilter(value) {
|
|
145
|
+
if (!value)
|
|
146
|
+
return undefined;
|
|
147
|
+
const normalized = value.trim().toLowerCase();
|
|
148
|
+
if (normalized === 'all'
|
|
149
|
+
|| normalized === 'blocking'
|
|
150
|
+
|| normalized === 'advisory'
|
|
151
|
+
|| normalized === 'high'
|
|
152
|
+
|| normalized === 'medium'
|
|
153
|
+
|| normalized === 'low') {
|
|
154
|
+
return normalized;
|
|
155
|
+
}
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
function readExecutionQueryOptions(requestUrl) {
|
|
159
|
+
const limit = parsePositiveInt(requestUrl.searchParams.get('limit'), 50);
|
|
160
|
+
const offset = parsePositiveInt(requestUrl.searchParams.get('offset'), 0);
|
|
161
|
+
const q = requestUrl.searchParams.get('q') || undefined;
|
|
162
|
+
const type = requestUrl.searchParams.get('type') || undefined;
|
|
163
|
+
const source = requestUrl.searchParams.get('source') || undefined;
|
|
164
|
+
const status = requestUrl.searchParams.get('status') || undefined;
|
|
165
|
+
const actor = requestUrl.searchParams.get('actor') || undefined;
|
|
166
|
+
const from = requestUrl.searchParams.get('from') || undefined;
|
|
167
|
+
const to = requestUrl.searchParams.get('to') || undefined;
|
|
168
|
+
const severity = toSeverityFilter(requestUrl.searchParams.get('severity'));
|
|
169
|
+
return {
|
|
170
|
+
limit,
|
|
171
|
+
offset,
|
|
172
|
+
q,
|
|
173
|
+
type: type,
|
|
174
|
+
source: source,
|
|
175
|
+
status: status,
|
|
176
|
+
actor,
|
|
177
|
+
from,
|
|
178
|
+
to,
|
|
179
|
+
severity,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function isRuntimeEventType(value) {
|
|
183
|
+
if (!value)
|
|
184
|
+
return false;
|
|
185
|
+
const normalized = value.trim().toLowerCase();
|
|
186
|
+
return (normalized === 'execution.started'
|
|
187
|
+
|| normalized === 'execution.progress'
|
|
188
|
+
|| normalized === 'execution.completed'
|
|
189
|
+
|| normalized === 'execution.failed'
|
|
190
|
+
|| normalized === 'verification.completed'
|
|
191
|
+
|| normalized === 'regression.detected'
|
|
192
|
+
|| normalized === 'patch.applied'
|
|
193
|
+
|| normalized === 'hotspot.updated'
|
|
194
|
+
|| normalized === 'narrative.updated'
|
|
195
|
+
|| normalized === 'evidence.generated'
|
|
196
|
+
|| normalized === 'governance.config.updated');
|
|
197
|
+
}
|
|
198
|
+
function isRuntimeEventSeverity(value) {
|
|
199
|
+
if (!value)
|
|
200
|
+
return false;
|
|
201
|
+
const normalized = value.trim().toLowerCase();
|
|
202
|
+
return normalized === 'low' || normalized === 'medium' || normalized === 'high';
|
|
203
|
+
}
|
|
204
|
+
function isExecutionSourceValue(value) {
|
|
205
|
+
if (!value)
|
|
206
|
+
return false;
|
|
207
|
+
const normalized = value.trim().toLowerCase();
|
|
208
|
+
return (normalized === 'cli'
|
|
209
|
+
|| normalized === 'daemon'
|
|
210
|
+
|| normalized === 'dashboard'
|
|
211
|
+
|| normalized === 'vscode'
|
|
212
|
+
|| normalized === 'ci'
|
|
213
|
+
|| normalized === 'mcp'
|
|
214
|
+
|| normalized === 'cursor'
|
|
215
|
+
|| normalized === 'api'
|
|
216
|
+
|| normalized === 'unknown');
|
|
217
|
+
}
|
|
218
|
+
function readRuntimeEventFilters(req, requestUrl) {
|
|
219
|
+
const cursorQuery = requestUrl.searchParams.get('cursor') || undefined;
|
|
220
|
+
const lastEventIdHeader = req.headers['last-event-id'];
|
|
221
|
+
const headerCursor = Array.isArray(lastEventIdHeader) ? lastEventIdHeader[0] : lastEventIdHeader;
|
|
222
|
+
const cursor = cursorQuery || headerCursor || undefined;
|
|
223
|
+
const typeRaw = requestUrl.searchParams.get('type');
|
|
224
|
+
const sourceRaw = requestUrl.searchParams.get('source');
|
|
225
|
+
const severityRaw = requestUrl.searchParams.get('severity');
|
|
226
|
+
const executionId = requestUrl.searchParams.get('executionId') || undefined;
|
|
227
|
+
return {
|
|
228
|
+
cursor,
|
|
229
|
+
executionId,
|
|
230
|
+
type: isRuntimeEventType(typeRaw) ? typeRaw : undefined,
|
|
231
|
+
source: isExecutionSourceValue(sourceRaw) ? sourceRaw : undefined,
|
|
232
|
+
severity: isRuntimeEventSeverity(severityRaw) ? severityRaw : undefined,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function toRuntimeEventQuery(filters, limit) {
|
|
236
|
+
return {
|
|
237
|
+
limit,
|
|
238
|
+
cursor: filters.cursor,
|
|
239
|
+
executionId: filters.executionId,
|
|
240
|
+
type: filters.type,
|
|
241
|
+
source: filters.source,
|
|
242
|
+
severity: filters.severity,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function matchesRuntimeEventFilters(event, filters) {
|
|
246
|
+
if (filters.executionId && event.executionId !== filters.executionId)
|
|
247
|
+
return false;
|
|
248
|
+
if (filters.type && event.type !== filters.type)
|
|
249
|
+
return false;
|
|
250
|
+
if (filters.source && event.source !== filters.source)
|
|
251
|
+
return false;
|
|
252
|
+
if (filters.severity && event.severity !== filters.severity)
|
|
253
|
+
return false;
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
function writeSseEvent(res, event) {
|
|
257
|
+
res.write(`id: ${event.cursor}\n`);
|
|
258
|
+
res.write(`event: ${event.type}\n`);
|
|
259
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
260
|
+
}
|
|
261
|
+
function broadcastRuntimeEvent(event) {
|
|
262
|
+
if (!runtimeEventTailCursor || runtimeEventTailCursor < event.cursor) {
|
|
263
|
+
runtimeEventTailCursor = event.cursor;
|
|
264
|
+
}
|
|
265
|
+
for (const client of runtimeEventClients.values()) {
|
|
266
|
+
if (!matchesRuntimeEventFilters(event, client.filters))
|
|
267
|
+
continue;
|
|
268
|
+
try {
|
|
269
|
+
writeSseEvent(client.res, event);
|
|
270
|
+
client.filters.cursor = event.cursor;
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
clearInterval(client.heartbeat);
|
|
274
|
+
runtimeEventClients.delete(client.id);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function closeRuntimeEventClients() {
|
|
279
|
+
for (const client of runtimeEventClients.values()) {
|
|
280
|
+
clearInterval(client.heartbeat);
|
|
281
|
+
try {
|
|
282
|
+
client.res.end();
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// no-op
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
runtimeEventClients.clear();
|
|
289
|
+
}
|
|
290
|
+
function startRuntimeEventTailer(cwd) {
|
|
291
|
+
runtimeEventTailCursor = (0, runtime_events_1.getLatestRuntimeEventCursor)(cwd);
|
|
292
|
+
if (runtimeEventTailTimer) {
|
|
293
|
+
clearInterval(runtimeEventTailTimer);
|
|
294
|
+
runtimeEventTailTimer = null;
|
|
295
|
+
}
|
|
296
|
+
runtimeEventTailTimer = setInterval(() => {
|
|
297
|
+
if (runtimeEventClients.size === 0)
|
|
298
|
+
return;
|
|
299
|
+
let cursor = runtimeEventTailCursor;
|
|
300
|
+
let loops = 0;
|
|
301
|
+
while (loops < 10) {
|
|
302
|
+
const replay = (0, runtime_events_1.queryRuntimeEvents)(cwd, {
|
|
303
|
+
limit: 100,
|
|
304
|
+
cursor: cursor || undefined,
|
|
305
|
+
});
|
|
306
|
+
if (replay.items.length === 0)
|
|
307
|
+
break;
|
|
308
|
+
for (const event of replay.items) {
|
|
309
|
+
broadcastRuntimeEvent(event);
|
|
310
|
+
}
|
|
311
|
+
cursor = replay.nextCursor;
|
|
312
|
+
runtimeEventTailCursor = cursor;
|
|
313
|
+
if (!replay.hasMore)
|
|
314
|
+
break;
|
|
315
|
+
loops += 1;
|
|
316
|
+
}
|
|
317
|
+
}, 1000);
|
|
318
|
+
}
|
|
319
|
+
function stopRuntimeEventTailer() {
|
|
320
|
+
if (!runtimeEventTailTimer)
|
|
321
|
+
return;
|
|
322
|
+
clearInterval(runtimeEventTailTimer);
|
|
323
|
+
runtimeEventTailTimer = null;
|
|
324
|
+
}
|
|
99
325
|
// ── Route handlers ─────────────────────────────────────────────────────────────
|
|
100
|
-
async function handleVerify(
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
326
|
+
async function handleVerify(req, res) {
|
|
327
|
+
const run = await (0, execution_bus_1.runExecution)({
|
|
328
|
+
type: 'verify',
|
|
329
|
+
source: toSource(req),
|
|
330
|
+
actor: toActor(req),
|
|
331
|
+
cwd: process.cwd(),
|
|
332
|
+
reverify: false,
|
|
333
|
+
});
|
|
334
|
+
if (!run.primaryPayload) {
|
|
335
|
+
failure(res, run.execution.result?.message || 'verify execution produced no payload');
|
|
104
336
|
return;
|
|
105
337
|
}
|
|
106
|
-
success(res,
|
|
338
|
+
success(res, {
|
|
339
|
+
...run.primaryPayload,
|
|
340
|
+
_execution: {
|
|
341
|
+
id: run.execution.id,
|
|
342
|
+
type: run.execution.type,
|
|
343
|
+
source: run.execution.source,
|
|
344
|
+
actor: run.execution.actor,
|
|
345
|
+
status: run.execution.status,
|
|
346
|
+
trend: run.execution.verification.diff.trend,
|
|
347
|
+
evidence: run.execution.evidence.references,
|
|
348
|
+
durationMs: run.execution.durationMs,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
107
351
|
}
|
|
108
|
-
async function handleFix(
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
352
|
+
async function handleFix(req, res) {
|
|
353
|
+
const run = await (0, execution_bus_1.runExecution)({
|
|
354
|
+
type: 'fix',
|
|
355
|
+
source: toSource(req),
|
|
356
|
+
actor: toActor(req),
|
|
357
|
+
cwd: process.cwd(),
|
|
358
|
+
reverify: true,
|
|
359
|
+
});
|
|
360
|
+
if (!run.primaryPayload) {
|
|
361
|
+
failure(res, run.execution.result?.message || 'fix execution produced no payload');
|
|
112
362
|
return;
|
|
113
363
|
}
|
|
114
|
-
success(res,
|
|
364
|
+
success(res, {
|
|
365
|
+
...run.primaryPayload,
|
|
366
|
+
verifyAfter: run.verificationPayload ?? null,
|
|
367
|
+
_execution: {
|
|
368
|
+
id: run.execution.id,
|
|
369
|
+
type: run.execution.type,
|
|
370
|
+
source: run.execution.source,
|
|
371
|
+
actor: run.execution.actor,
|
|
372
|
+
status: run.execution.status,
|
|
373
|
+
trend: run.execution.verification.diff.trend,
|
|
374
|
+
evidence: run.execution.evidence.references,
|
|
375
|
+
durationMs: run.execution.durationMs,
|
|
376
|
+
},
|
|
377
|
+
});
|
|
115
378
|
}
|
|
116
|
-
async function handleFixApplySafe(
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
379
|
+
async function handleFixApplySafe(req, res) {
|
|
380
|
+
const run = await (0, execution_bus_1.runExecution)({
|
|
381
|
+
type: 'apply-safe',
|
|
382
|
+
source: toSource(req),
|
|
383
|
+
actor: toActor(req),
|
|
384
|
+
cwd: process.cwd(),
|
|
385
|
+
reverify: true,
|
|
386
|
+
});
|
|
387
|
+
if (!run.primaryPayload) {
|
|
388
|
+
failure(res, run.execution.result?.message || 'fix --apply-safe execution produced no payload');
|
|
120
389
|
return;
|
|
121
390
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
391
|
+
success(res, {
|
|
392
|
+
...run.primaryPayload,
|
|
393
|
+
verifyAfter: run.verificationPayload ?? null,
|
|
394
|
+
execution: run.execution,
|
|
395
|
+
});
|
|
127
396
|
}
|
|
128
397
|
async function handlePatch(req, res) {
|
|
129
398
|
let body = {};
|
|
@@ -139,13 +408,545 @@ async function handlePatch(req, res) {
|
|
|
139
408
|
failure(res, 'Missing or unsafe "file" field', 400);
|
|
140
409
|
return;
|
|
141
410
|
}
|
|
142
|
-
//
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
411
|
+
// Capture file content before patch to detect real change
|
|
412
|
+
const absPath = path.resolve(process.cwd(), file);
|
|
413
|
+
let contentBefore = null;
|
|
414
|
+
try {
|
|
415
|
+
contentBefore = fs.readFileSync(absPath, 'utf-8');
|
|
416
|
+
}
|
|
417
|
+
catch { /* file may not exist */ }
|
|
418
|
+
const run = await (0, execution_bus_1.runExecution)({
|
|
419
|
+
type: 'patch',
|
|
420
|
+
source: toSource(req),
|
|
421
|
+
actor: toActor(req),
|
|
422
|
+
target: file,
|
|
423
|
+
cwd: process.cwd(),
|
|
424
|
+
reverify: true,
|
|
425
|
+
});
|
|
426
|
+
const patchData = run.primaryPayload ?? {
|
|
427
|
+
success: false,
|
|
428
|
+
file,
|
|
429
|
+
message: run.execution.result?.message || 'No applicable patch found',
|
|
430
|
+
};
|
|
431
|
+
// Validate that the file actually changed on disk
|
|
432
|
+
let changed = false;
|
|
433
|
+
if (patchData.success && contentBefore !== null) {
|
|
434
|
+
try {
|
|
435
|
+
const contentAfter = fs.readFileSync(absPath, 'utf-8');
|
|
436
|
+
changed = contentAfter !== contentBefore;
|
|
437
|
+
}
|
|
438
|
+
catch { /* ignore read error */ }
|
|
439
|
+
}
|
|
440
|
+
success(res, {
|
|
441
|
+
patch: { ...patchData, changed },
|
|
442
|
+
verify: run.verificationPayload ?? null,
|
|
443
|
+
execution: run.execution,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
async function handleExecute(req, res) {
|
|
447
|
+
let body = {};
|
|
448
|
+
try {
|
|
449
|
+
body = JSON.parse(await readBody(req));
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
failure(res, 'Invalid JSON body', 400);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const type = body.type;
|
|
456
|
+
if (!isExecutionActionType(type)) {
|
|
457
|
+
failure(res, 'Invalid or missing "type" field', 400);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const run = await (0, execution_bus_1.runExecution)({
|
|
461
|
+
type,
|
|
462
|
+
source: toSource(req),
|
|
463
|
+
actor: toActor(req),
|
|
464
|
+
target: body.target ?? null,
|
|
465
|
+
intentText: body.intentText ?? null,
|
|
466
|
+
cwd: process.cwd(),
|
|
467
|
+
reverify: body.reverify !== false,
|
|
468
|
+
ciMode: typeof body.ciMode === 'boolean' ? body.ciMode : undefined,
|
|
469
|
+
evidenceDir: typeof body.evidenceDir === 'string' ? body.evidenceDir : undefined,
|
|
470
|
+
dedupeWindowMs: typeof body.dedupeWindowMs === 'number' ? body.dedupeWindowMs : undefined,
|
|
471
|
+
});
|
|
472
|
+
success(res, {
|
|
473
|
+
execution: run.execution,
|
|
474
|
+
payload: run.primaryPayload,
|
|
475
|
+
verification: run.verificationPayload,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
async function handleListExecutions(req, res) {
|
|
479
|
+
const requestUrl = new URL(req.url || '/executions', 'http://localhost');
|
|
480
|
+
const queryOptions = readExecutionQueryOptions(requestUrl);
|
|
481
|
+
const result = (0, execution_bus_1.queryExecutions)(process.cwd(), queryOptions);
|
|
482
|
+
success(res, {
|
|
483
|
+
count: result.items.length,
|
|
484
|
+
items: result.items,
|
|
485
|
+
limit: result.limit,
|
|
486
|
+
offset: result.offset,
|
|
487
|
+
hasMore: result.hasMore,
|
|
488
|
+
nextOffset: result.nextOffset,
|
|
489
|
+
scanned: result.scanned,
|
|
490
|
+
filters: {
|
|
491
|
+
type: queryOptions.type ?? 'all',
|
|
492
|
+
source: queryOptions.source ?? 'all',
|
|
493
|
+
status: queryOptions.status ?? 'all',
|
|
494
|
+
actor: queryOptions.actor ?? '',
|
|
495
|
+
severity: queryOptions.severity ?? 'all',
|
|
496
|
+
q: queryOptions.q ?? '',
|
|
497
|
+
from: queryOptions.from ?? null,
|
|
498
|
+
to: queryOptions.to ?? null,
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
async function handleGetExecution(req, res, executionId) {
|
|
503
|
+
const record = (0, execution_bus_1.getExecutionById)(executionId, process.cwd());
|
|
504
|
+
if (!record) {
|
|
505
|
+
failure(res, `Execution not found: ${executionId}`, 404);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
success(res, record);
|
|
509
|
+
}
|
|
510
|
+
async function handleGetExecutionEvents(req, res, executionId) {
|
|
511
|
+
const record = (0, execution_bus_1.getExecutionById)(executionId, process.cwd());
|
|
512
|
+
if (!record) {
|
|
513
|
+
failure(res, `Execution not found: ${executionId}`, 404);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
success(res, {
|
|
517
|
+
id: record.id,
|
|
518
|
+
status: record.status,
|
|
519
|
+
events: record.events,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
async function handleGetExecutionTimeline(_req, res, executionId) {
|
|
523
|
+
const record = (0, execution_bus_1.getExecutionById)(executionId, process.cwd());
|
|
524
|
+
if (!record) {
|
|
525
|
+
failure(res, `Execution not found: ${executionId}`, 404);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
success(res, (0, execution_bus_1.buildExecutionTimeline)(record));
|
|
529
|
+
}
|
|
530
|
+
async function handleGetExecutionDiff(_req, res, executionId) {
|
|
531
|
+
const record = (0, execution_bus_1.getExecutionById)(executionId, process.cwd());
|
|
532
|
+
if (!record) {
|
|
533
|
+
failure(res, `Execution not found: ${executionId}`, 404);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
success(res, (0, execution_bus_1.buildExecutionDiffInspection)(record));
|
|
537
|
+
}
|
|
538
|
+
async function handleListRuntimeEvents(req, res) {
|
|
539
|
+
const requestUrl = new URL(req.url || '/events', 'http://localhost');
|
|
540
|
+
const filters = readRuntimeEventFilters(req, requestUrl);
|
|
541
|
+
const limit = parsePositiveInt(requestUrl.searchParams.get('limit'), 100);
|
|
542
|
+
const result = (0, runtime_events_1.queryRuntimeEvents)(process.cwd(), toRuntimeEventQuery(filters, limit));
|
|
543
|
+
success(res, {
|
|
544
|
+
count: result.items.length,
|
|
545
|
+
items: result.items,
|
|
546
|
+
hasMore: result.hasMore,
|
|
547
|
+
nextCursor: result.nextCursor,
|
|
548
|
+
scanned: result.scanned,
|
|
549
|
+
filters,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
function asControlPlanePatch(value) {
|
|
553
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
554
|
+
return null;
|
|
555
|
+
const record = value;
|
|
556
|
+
const allowedKeys = new Set([
|
|
557
|
+
'runtime',
|
|
558
|
+
'remediation',
|
|
559
|
+
'evidence',
|
|
560
|
+
'eventRuntime',
|
|
561
|
+
'ciGovernance',
|
|
562
|
+
'policyGovernance',
|
|
563
|
+
]);
|
|
564
|
+
const patch = {};
|
|
565
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
566
|
+
if (!allowedKeys.has(key))
|
|
567
|
+
continue;
|
|
568
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
569
|
+
continue;
|
|
570
|
+
patch[key] = entry;
|
|
571
|
+
}
|
|
572
|
+
return Object.keys(patch).length > 0 ? patch : null;
|
|
573
|
+
}
|
|
574
|
+
async function handleGetControlPlane(_req, res) {
|
|
575
|
+
const state = (0, control_plane_1.readControlPlaneState)(process.cwd());
|
|
576
|
+
const snapshots = (0, control_plane_1.readControlPlaneSnapshotHistory)(process.cwd(), 30);
|
|
577
|
+
success(res, {
|
|
578
|
+
state,
|
|
579
|
+
snapshots,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
async function handlePreviewControlPlaneUpdate(req, res) {
|
|
583
|
+
let body = {};
|
|
584
|
+
try {
|
|
585
|
+
body = JSON.parse(await readBody(req));
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
failure(res, 'Invalid JSON body', 400);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const patch = asControlPlanePatch(body.patch);
|
|
592
|
+
if (!patch) {
|
|
593
|
+
failure(res, 'Invalid or missing patch object', 400);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const preview = (0, control_plane_1.previewControlPlaneUpdate)(patch, process.cwd());
|
|
597
|
+
success(res, preview);
|
|
598
|
+
}
|
|
599
|
+
async function handleApplyControlPlaneUpdate(req, res) {
|
|
600
|
+
let body = {};
|
|
601
|
+
try {
|
|
602
|
+
body = JSON.parse(await readBody(req));
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
failure(res, 'Invalid JSON body', 400);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const patch = asControlPlanePatch(body.patch);
|
|
609
|
+
if (!patch) {
|
|
610
|
+
failure(res, 'Invalid or missing patch object', 400);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const reason = typeof body.reason === 'string' && body.reason.trim().length > 0
|
|
614
|
+
? body.reason.trim()
|
|
615
|
+
: undefined;
|
|
616
|
+
const result = (0, control_plane_1.applyControlPlaneUpdate)(patch, {
|
|
617
|
+
cwd: process.cwd(),
|
|
618
|
+
actor: toActor(req),
|
|
619
|
+
source: toSource(req),
|
|
620
|
+
reason,
|
|
621
|
+
});
|
|
622
|
+
success(res, result);
|
|
623
|
+
}
|
|
624
|
+
function asNonEmptyString(value) {
|
|
625
|
+
if (typeof value !== 'string')
|
|
626
|
+
return undefined;
|
|
627
|
+
const trimmed = value.trim();
|
|
628
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
629
|
+
}
|
|
630
|
+
function asStringArray(value) {
|
|
631
|
+
if (!Array.isArray(value))
|
|
632
|
+
return undefined;
|
|
633
|
+
const items = value
|
|
634
|
+
.filter((entry) => typeof entry === 'string')
|
|
635
|
+
.map((entry) => entry.trim())
|
|
636
|
+
.filter((entry) => entry.length > 0);
|
|
637
|
+
return items.length > 0 ? items : [];
|
|
638
|
+
}
|
|
639
|
+
function asWorkspaceCreateInput(value) {
|
|
640
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
641
|
+
return null;
|
|
642
|
+
const record = value;
|
|
643
|
+
const name = asNonEmptyString(record.name);
|
|
644
|
+
if (!name)
|
|
645
|
+
return null;
|
|
646
|
+
const input = {
|
|
647
|
+
name,
|
|
648
|
+
};
|
|
649
|
+
const id = asNonEmptyString(record.id);
|
|
650
|
+
if (id)
|
|
651
|
+
input.id = id;
|
|
652
|
+
if (record.description === null || typeof record.description === 'string') {
|
|
653
|
+
input.description = record.description;
|
|
654
|
+
}
|
|
655
|
+
if (Array.isArray(record.repositories)) {
|
|
656
|
+
input.repositories = record.repositories;
|
|
657
|
+
}
|
|
658
|
+
if (record.governance && typeof record.governance === 'object' && !Array.isArray(record.governance)) {
|
|
659
|
+
input.governance = record.governance;
|
|
660
|
+
}
|
|
661
|
+
if (record.access && typeof record.access === 'object' && !Array.isArray(record.access)) {
|
|
662
|
+
input.access = record.access;
|
|
663
|
+
}
|
|
664
|
+
return input;
|
|
665
|
+
}
|
|
666
|
+
function asWorkspacePatch(value) {
|
|
667
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
668
|
+
return null;
|
|
669
|
+
const record = value;
|
|
670
|
+
const patch = {};
|
|
671
|
+
if (typeof record.name === 'string')
|
|
672
|
+
patch.name = record.name;
|
|
673
|
+
if (record.description === null || typeof record.description === 'string')
|
|
674
|
+
patch.description = record.description;
|
|
675
|
+
if (Array.isArray(record.repositories))
|
|
676
|
+
patch.repositories = record.repositories;
|
|
677
|
+
if (record.governance && typeof record.governance === 'object' && !Array.isArray(record.governance)) {
|
|
678
|
+
patch.governance = record.governance;
|
|
679
|
+
}
|
|
680
|
+
if (record.access && typeof record.access === 'object' && !Array.isArray(record.access)) {
|
|
681
|
+
patch.access = record.access;
|
|
682
|
+
}
|
|
683
|
+
return Object.keys(patch).length > 0 ? patch : null;
|
|
684
|
+
}
|
|
685
|
+
function asWorkspaceRepositoryInput(value) {
|
|
686
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
687
|
+
return null;
|
|
688
|
+
const record = value;
|
|
689
|
+
const name = asNonEmptyString(record.name);
|
|
690
|
+
const rootPath = asNonEmptyString(record.rootPath) || asNonEmptyString(record.path);
|
|
691
|
+
if (!name || !rootPath)
|
|
692
|
+
return null;
|
|
693
|
+
return {
|
|
694
|
+
id: asNonEmptyString(record.id),
|
|
695
|
+
name,
|
|
696
|
+
rootPath,
|
|
697
|
+
services: asStringArray(record.services),
|
|
698
|
+
policyDomain: record.policyDomain === null ? null : asNonEmptyString(record.policyDomain),
|
|
699
|
+
tags: asStringArray(record.tags),
|
|
700
|
+
enabled: typeof record.enabled === 'boolean' ? record.enabled : undefined,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
async function handleListWorkspaces(req, res) {
|
|
704
|
+
const requestUrl = new URL(req.url || '/workspaces', 'http://localhost');
|
|
705
|
+
const workspaceId = asNonEmptyString(requestUrl.searchParams.get('workspaceId'));
|
|
706
|
+
const actor = asNonEmptyString(requestUrl.searchParams.get('actor'));
|
|
707
|
+
const list = (0, workspace_runtime_1.listWorkspaces)(process.cwd());
|
|
708
|
+
const snapshot = (0, workspace_runtime_1.getWorkspaceRuntimeSnapshot)({
|
|
709
|
+
cwd: process.cwd(),
|
|
710
|
+
workspaceId,
|
|
711
|
+
actor,
|
|
712
|
+
});
|
|
713
|
+
success(res, {
|
|
714
|
+
schemaVersion: 'neurcode.workspace.api.list.v1',
|
|
715
|
+
generatedAt: new Date().toISOString(),
|
|
716
|
+
count: list.length,
|
|
717
|
+
activeWorkspaceId: snapshot.activeWorkspaceId,
|
|
718
|
+
items: list,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
async function handleGetWorkspace(_req, res, workspaceId) {
|
|
722
|
+
const workspace = (0, workspace_runtime_1.getWorkspaceById)(workspaceId, process.cwd());
|
|
723
|
+
if (!workspace) {
|
|
724
|
+
failure(res, `Workspace not found: ${workspaceId}`, 404);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
success(res, workspace);
|
|
728
|
+
}
|
|
729
|
+
async function handleGetWorkspaceRuntime(req, res, workspaceId) {
|
|
730
|
+
const requestUrl = new URL(req.url || '/workspaces/runtime', 'http://localhost');
|
|
731
|
+
const queryWorkspaceId = asNonEmptyString(requestUrl.searchParams.get('workspaceId'));
|
|
732
|
+
const actor = asNonEmptyString(requestUrl.searchParams.get('actor'));
|
|
733
|
+
const snapshot = (0, workspace_runtime_1.getWorkspaceRuntimeSnapshot)({
|
|
734
|
+
cwd: process.cwd(),
|
|
735
|
+
workspaceId: workspaceId || queryWorkspaceId,
|
|
736
|
+
actor,
|
|
737
|
+
});
|
|
738
|
+
success(res, snapshot);
|
|
739
|
+
}
|
|
740
|
+
async function handleCreateWorkspace(req, res) {
|
|
741
|
+
let body = {};
|
|
742
|
+
try {
|
|
743
|
+
body = JSON.parse(await readBody(req));
|
|
744
|
+
}
|
|
745
|
+
catch {
|
|
746
|
+
failure(res, 'Invalid JSON body', 400);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
const input = asWorkspaceCreateInput(body.workspace);
|
|
750
|
+
if (!input) {
|
|
751
|
+
failure(res, 'Invalid or missing workspace payload', 400);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const result = (0, workspace_runtime_1.createWorkspace)(input, {
|
|
755
|
+
cwd: process.cwd(),
|
|
756
|
+
source: toSource(req),
|
|
757
|
+
actor: toActor(req),
|
|
758
|
+
setActive: typeof body.setActive === 'boolean' ? body.setActive : true,
|
|
759
|
+
});
|
|
760
|
+
success(res, result);
|
|
761
|
+
}
|
|
762
|
+
async function handleActivateWorkspace(req, res, workspaceId) {
|
|
763
|
+
const result = (0, workspace_runtime_1.setActiveWorkspace)(workspaceId, {
|
|
764
|
+
cwd: process.cwd(),
|
|
765
|
+
source: toSource(req),
|
|
766
|
+
actor: toActor(req),
|
|
767
|
+
});
|
|
768
|
+
success(res, result);
|
|
769
|
+
}
|
|
770
|
+
async function handleAddWorkspaceRepository(req, res, workspaceId) {
|
|
771
|
+
let body = {};
|
|
772
|
+
try {
|
|
773
|
+
body = JSON.parse(await readBody(req));
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
failure(res, 'Invalid JSON body', 400);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
const input = asWorkspaceRepositoryInput(body.repository);
|
|
780
|
+
if (!input) {
|
|
781
|
+
failure(res, 'Invalid or missing repository payload', 400);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const result = (0, workspace_runtime_1.addWorkspaceRepository)(workspaceId, input, {
|
|
785
|
+
cwd: process.cwd(),
|
|
786
|
+
source: toSource(req),
|
|
787
|
+
actor: toActor(req),
|
|
788
|
+
});
|
|
789
|
+
success(res, result);
|
|
790
|
+
}
|
|
791
|
+
async function handleUpdateWorkspace(req, res, workspaceId) {
|
|
792
|
+
let body = {};
|
|
793
|
+
try {
|
|
794
|
+
body = JSON.parse(await readBody(req));
|
|
795
|
+
}
|
|
796
|
+
catch {
|
|
797
|
+
failure(res, 'Invalid JSON body', 400);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
const patch = asWorkspacePatch(body.patch);
|
|
801
|
+
if (!patch) {
|
|
802
|
+
failure(res, 'Invalid or missing patch payload', 400);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const result = (0, workspace_runtime_1.updateWorkspace)(workspaceId, patch, {
|
|
806
|
+
cwd: process.cwd(),
|
|
807
|
+
source: toSource(req),
|
|
808
|
+
actor: toActor(req),
|
|
809
|
+
});
|
|
810
|
+
success(res, result);
|
|
811
|
+
}
|
|
812
|
+
async function handleExecuteWorkspace(req, res) {
|
|
813
|
+
let body = {};
|
|
814
|
+
try {
|
|
815
|
+
body = JSON.parse(await readBody(req));
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
failure(res, 'Invalid JSON body', 400);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (!isExecutionActionType(body.type)) {
|
|
822
|
+
failure(res, 'Invalid or missing execution type', 400);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const request = {
|
|
826
|
+
workspaceId: asNonEmptyString(body.workspaceId),
|
|
827
|
+
repositoryIds: asStringArray(body.repositoryIds),
|
|
828
|
+
type: body.type,
|
|
829
|
+
source: toSource(req),
|
|
830
|
+
actor: toActor(req),
|
|
831
|
+
target: body.target === null ? null : asNonEmptyString(body.target) || null,
|
|
832
|
+
intentText: body.intentText === null ? null : asNonEmptyString(body.intentText) || null,
|
|
833
|
+
reverify: typeof body.reverify === 'boolean' ? body.reverify : true,
|
|
834
|
+
ciMode: typeof body.ciMode === 'boolean' ? body.ciMode : undefined,
|
|
835
|
+
evidenceDir: asNonEmptyString(body.evidenceDir),
|
|
836
|
+
dedupeWindowMs: typeof body.dedupeWindowMs === 'number' ? body.dedupeWindowMs : undefined,
|
|
837
|
+
};
|
|
838
|
+
const result = await (0, workspace_runtime_1.executeWorkspaceAction)(request, {
|
|
839
|
+
cwd: process.cwd(),
|
|
840
|
+
});
|
|
841
|
+
success(res, result);
|
|
842
|
+
}
|
|
843
|
+
async function handleReplayState(req, res) {
|
|
844
|
+
const requestUrl = new URL(req.url || '/replay/state', 'http://localhost');
|
|
845
|
+
const at = asNonEmptyString(requestUrl.searchParams.get('at')) || new Date().toISOString();
|
|
846
|
+
const workspaceId = asNonEmptyString(requestUrl.searchParams.get('workspaceId'));
|
|
847
|
+
const includeEvents = requestUrl.searchParams.get('events') === 'true';
|
|
848
|
+
const eventLimit = parsePositiveInt(requestUrl.searchParams.get('eventLimit'), 400);
|
|
849
|
+
const result = (0, replay_runtime_1.replayGovernanceState)({
|
|
850
|
+
at,
|
|
851
|
+
workspaceId,
|
|
852
|
+
includeEvents,
|
|
853
|
+
eventLimit,
|
|
854
|
+
}, process.cwd());
|
|
855
|
+
success(res, result);
|
|
856
|
+
}
|
|
857
|
+
async function handleReplayExecution(_req, res, executionId) {
|
|
858
|
+
const result = (0, replay_runtime_1.replayExecution)({ executionId }, process.cwd());
|
|
859
|
+
success(res, result);
|
|
860
|
+
}
|
|
861
|
+
async function handleReplayWorkspace(req, res, workspaceId) {
|
|
862
|
+
const requestUrl = new URL(req.url || '/replay/workspace', 'http://localhost');
|
|
863
|
+
const queryWorkspaceId = asNonEmptyString(requestUrl.searchParams.get('workspaceId'));
|
|
864
|
+
const at = asNonEmptyString(requestUrl.searchParams.get('at')) || undefined;
|
|
865
|
+
const result = (0, replay_runtime_1.replayWorkspace)({
|
|
866
|
+
workspaceId: workspaceId || queryWorkspaceId || undefined,
|
|
867
|
+
at,
|
|
868
|
+
}, process.cwd());
|
|
869
|
+
success(res, result);
|
|
870
|
+
}
|
|
871
|
+
async function handleReplayTimeline(req, res) {
|
|
872
|
+
const requestUrl = new URL(req.url || '/replay/timeline', 'http://localhost');
|
|
873
|
+
const request = {
|
|
874
|
+
workspaceId: asNonEmptyString(requestUrl.searchParams.get('workspaceId')),
|
|
875
|
+
from: asNonEmptyString(requestUrl.searchParams.get('from')) || undefined,
|
|
876
|
+
to: asNonEmptyString(requestUrl.searchParams.get('to')) || undefined,
|
|
877
|
+
limit: parsePositiveInt(requestUrl.searchParams.get('limit'), 200),
|
|
878
|
+
};
|
|
879
|
+
const result = (0, replay_runtime_1.replayTimeline)(request, process.cwd());
|
|
880
|
+
success(res, result);
|
|
881
|
+
}
|
|
882
|
+
async function handleRuntimeEventStream(req, res) {
|
|
883
|
+
const requestUrl = new URL(req.url || '/events/stream', 'http://localhost');
|
|
884
|
+
const filters = readRuntimeEventFilters(req, requestUrl);
|
|
885
|
+
res.writeHead(200, {
|
|
886
|
+
'Content-Type': 'text/event-stream',
|
|
887
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
888
|
+
Connection: 'keep-alive',
|
|
889
|
+
'X-Accel-Buffering': 'no',
|
|
890
|
+
});
|
|
891
|
+
res.write(`retry: ${SSE_RETRY_MS}\n\n`);
|
|
892
|
+
let cursor = filters.cursor;
|
|
893
|
+
let replayed = 0;
|
|
894
|
+
// Cursor-safe incremental replay before registering live listener.
|
|
895
|
+
while (true) {
|
|
896
|
+
const replay = (0, runtime_events_1.queryRuntimeEvents)(process.cwd(), {
|
|
897
|
+
...toRuntimeEventQuery({ ...filters, cursor }, 200),
|
|
898
|
+
});
|
|
899
|
+
if (replay.items.length === 0)
|
|
900
|
+
break;
|
|
901
|
+
for (const event of replay.items) {
|
|
902
|
+
if (!matchesRuntimeEventFilters(event, filters))
|
|
903
|
+
continue;
|
|
904
|
+
writeSseEvent(res, event);
|
|
905
|
+
replayed += 1;
|
|
906
|
+
}
|
|
907
|
+
cursor = replay.nextCursor || cursor;
|
|
908
|
+
if (!replay.hasMore)
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
const clientId = ++runtimeEventClientSeq;
|
|
912
|
+
const heartbeat = setInterval(() => {
|
|
913
|
+
try {
|
|
914
|
+
res.write(`: keep-alive ${Date.now()}\n\n`);
|
|
915
|
+
}
|
|
916
|
+
catch {
|
|
917
|
+
const client = runtimeEventClients.get(clientId);
|
|
918
|
+
if (client) {
|
|
919
|
+
clearInterval(client.heartbeat);
|
|
920
|
+
runtimeEventClients.delete(clientId);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}, 15_000);
|
|
924
|
+
runtimeEventClients.set(clientId, {
|
|
925
|
+
id: clientId,
|
|
926
|
+
res,
|
|
927
|
+
filters: {
|
|
928
|
+
...filters,
|
|
929
|
+
cursor,
|
|
930
|
+
},
|
|
931
|
+
heartbeat,
|
|
932
|
+
});
|
|
933
|
+
const ackEvent = {
|
|
934
|
+
kind: 'stream.ready',
|
|
935
|
+
replayed,
|
|
936
|
+
cursor: cursor || (0, runtime_events_1.getLatestRuntimeEventCursor)(process.cwd()),
|
|
937
|
+
};
|
|
938
|
+
res.write(`event: stream.ready\n`);
|
|
939
|
+
res.write(`data: ${JSON.stringify(ackEvent)}\n\n`);
|
|
940
|
+
const cleanup = () => {
|
|
941
|
+
const client = runtimeEventClients.get(clientId);
|
|
942
|
+
if (!client)
|
|
943
|
+
return;
|
|
944
|
+
clearInterval(client.heartbeat);
|
|
945
|
+
runtimeEventClients.delete(clientId);
|
|
946
|
+
};
|
|
947
|
+
req.on('close', cleanup);
|
|
948
|
+
req.on('error', cleanup);
|
|
949
|
+
res.on('close', cleanup);
|
|
149
950
|
}
|
|
150
951
|
// ── Server factory ─────────────────────────────────────────────────────────────
|
|
151
952
|
function createDaemonServer() {
|
|
@@ -170,7 +971,150 @@ function createDaemonServer() {
|
|
|
170
971
|
version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version ?? version;
|
|
171
972
|
}
|
|
172
973
|
catch { /* ignore */ }
|
|
173
|
-
send(res, 200, {
|
|
974
|
+
send(res, 200, {
|
|
975
|
+
ok: true,
|
|
976
|
+
version,
|
|
977
|
+
cwd: process.cwd(),
|
|
978
|
+
executionBus: {
|
|
979
|
+
schemaVersion: 'neurcode.execution.v1',
|
|
980
|
+
supportedActions: ['verify', 'fix', 'patch', 'apply-safe', 'reverify', 'policy-sync', 'intent-update'],
|
|
981
|
+
},
|
|
982
|
+
runtimeEvents: {
|
|
983
|
+
schemaVersion: 'neurcode.runtime-event.v1',
|
|
984
|
+
streamPath: '/events/stream',
|
|
985
|
+
},
|
|
986
|
+
controlPlane: {
|
|
987
|
+
schemaVersion: 'neurcode.control-plane.v1',
|
|
988
|
+
path: '/control-plane',
|
|
989
|
+
},
|
|
990
|
+
workspaceRuntime: {
|
|
991
|
+
schemaVersion: 'neurcode.workspace-runtime.v1',
|
|
992
|
+
path: '/workspaces/runtime',
|
|
993
|
+
},
|
|
994
|
+
replayRuntime: {
|
|
995
|
+
schemaVersion: 'neurcode.replay.state.v1',
|
|
996
|
+
path: '/replay/state',
|
|
997
|
+
},
|
|
998
|
+
});
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (method === 'GET' && url.startsWith('/executions')) {
|
|
1002
|
+
if (url === '/executions' || url.startsWith('/executions?')) {
|
|
1003
|
+
await handleListExecutions(req, res);
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
const eventsMatch = url.match(/^\/executions\/([^/]+)\/events(?:\?.*)?$/);
|
|
1007
|
+
if (eventsMatch) {
|
|
1008
|
+
await handleGetExecutionEvents(req, res, decodeURIComponent(eventsMatch[1]));
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
const timelineMatch = url.match(/^\/executions\/([^/]+)\/timeline(?:\?.*)?$/);
|
|
1012
|
+
if (timelineMatch) {
|
|
1013
|
+
await handleGetExecutionTimeline(req, res, decodeURIComponent(timelineMatch[1]));
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
const diffMatch = url.match(/^\/executions\/([^/]+)\/diff(?:\?.*)?$/);
|
|
1017
|
+
if (diffMatch) {
|
|
1018
|
+
await handleGetExecutionDiff(req, res, decodeURIComponent(diffMatch[1]));
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
const detailMatch = url.match(/^\/executions\/([^/?]+)(?:\?.*)?$/);
|
|
1022
|
+
if (detailMatch) {
|
|
1023
|
+
await handleGetExecution(req, res, decodeURIComponent(detailMatch[1]));
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
if (method === 'GET' && url.startsWith('/events')) {
|
|
1028
|
+
if (url === '/events' || url.startsWith('/events?')) {
|
|
1029
|
+
await handleListRuntimeEvents(req, res);
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
if (url === '/events/stream' || url.startsWith('/events/stream?')) {
|
|
1033
|
+
await handleRuntimeEventStream(req, res);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (method === 'GET' && (url === '/control-plane' || url.startsWith('/control-plane?'))) {
|
|
1038
|
+
await handleGetControlPlane(req, res);
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
if (method === 'POST' && url === '/control-plane/preview') {
|
|
1042
|
+
await handlePreviewControlPlaneUpdate(req, res);
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
if (method === 'PUT' && url === '/control-plane') {
|
|
1046
|
+
await handleApplyControlPlaneUpdate(req, res);
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
if (method === 'GET' && url.startsWith('/workspaces')) {
|
|
1050
|
+
if (url === '/workspaces' || url.startsWith('/workspaces?')) {
|
|
1051
|
+
await handleListWorkspaces(req, res);
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
if (url === '/workspaces/runtime' || url.startsWith('/workspaces/runtime?')) {
|
|
1055
|
+
await handleGetWorkspaceRuntime(req, res);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
const runtimeMatch = url.match(/^\/workspaces\/([^/]+)\/runtime(?:\?.*)?$/);
|
|
1059
|
+
if (runtimeMatch) {
|
|
1060
|
+
await handleGetWorkspaceRuntime(req, res, decodeURIComponent(runtimeMatch[1]));
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
const detailMatch = url.match(/^\/workspaces\/([^/?]+)(?:\?.*)?$/);
|
|
1064
|
+
if (detailMatch) {
|
|
1065
|
+
await handleGetWorkspace(req, res, decodeURIComponent(detailMatch[1]));
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
if (method === 'POST' && url === '/workspaces') {
|
|
1070
|
+
await handleCreateWorkspace(req, res);
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
const activateMatch = url.match(/^\/workspaces\/([^/]+)\/activate$/);
|
|
1074
|
+
if (method === 'POST' && activateMatch) {
|
|
1075
|
+
await handleActivateWorkspace(req, res, decodeURIComponent(activateMatch[1]));
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
const addRepoMatch = url.match(/^\/workspaces\/([^/]+)\/repositories$/);
|
|
1079
|
+
if (method === 'POST' && addRepoMatch) {
|
|
1080
|
+
await handleAddWorkspaceRepository(req, res, decodeURIComponent(addRepoMatch[1]));
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const updateWorkspaceMatch = url.match(/^\/workspaces\/([^/?]+)$/);
|
|
1084
|
+
if (method === 'PUT' && updateWorkspaceMatch) {
|
|
1085
|
+
await handleUpdateWorkspace(req, res, decodeURIComponent(updateWorkspaceMatch[1]));
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
if (method === 'POST' && url === '/workspaces/execute') {
|
|
1089
|
+
await handleExecuteWorkspace(req, res);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
if (method === 'GET' && url.startsWith('/replay')) {
|
|
1093
|
+
if (url === '/replay/state' || url.startsWith('/replay/state?')) {
|
|
1094
|
+
await handleReplayState(req, res);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (url === '/replay/timeline' || url.startsWith('/replay/timeline?')) {
|
|
1098
|
+
await handleReplayTimeline(req, res);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
if (url === '/replay/workspace' || url.startsWith('/replay/workspace?')) {
|
|
1102
|
+
await handleReplayWorkspace(req, res);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
const replayWorkspaceMatch = url.match(/^\/replay\/workspace\/([^/?]+)(?:\?.*)?$/);
|
|
1106
|
+
if (replayWorkspaceMatch) {
|
|
1107
|
+
await handleReplayWorkspace(req, res, decodeURIComponent(replayWorkspaceMatch[1]));
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
const replayExecutionMatch = url.match(/^\/replay\/execution\/([^/?]+)(?:\?.*)?$/);
|
|
1111
|
+
if (replayExecutionMatch) {
|
|
1112
|
+
await handleReplayExecution(req, res, decodeURIComponent(replayExecutionMatch[1]));
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (method === 'POST' && url === '/execute') {
|
|
1117
|
+
await handleExecute(req, res);
|
|
174
1118
|
return;
|
|
175
1119
|
}
|
|
176
1120
|
if (method === 'POST' && url === '/verify') {
|
|
@@ -200,6 +1144,12 @@ function createDaemonServer() {
|
|
|
200
1144
|
// ── Start function ─────────────────────────────────────────────────────────────
|
|
201
1145
|
function startDaemon() {
|
|
202
1146
|
const server = createDaemonServer();
|
|
1147
|
+
startRuntimeEventTailer(process.cwd());
|
|
1148
|
+
if (!runtimeEventUnsubscribe) {
|
|
1149
|
+
runtimeEventUnsubscribe = (0, runtime_events_1.onRuntimeEvent)((event) => {
|
|
1150
|
+
broadcastRuntimeEvent(event);
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
203
1153
|
server.on('error', (err) => {
|
|
204
1154
|
if (err.code === 'EADDRINUSE') {
|
|
205
1155
|
console.error(`\n❌ Port ${exports.DAEMON_PORT} is already in use.\n` +
|
|
@@ -213,14 +1163,49 @@ function startDaemon() {
|
|
|
213
1163
|
});
|
|
214
1164
|
server.listen(exports.DAEMON_PORT, exports.DAEMON_HOST, () => {
|
|
215
1165
|
console.log(`\nNeurcode daemon v2 running on http://localhost:${exports.DAEMON_PORT}`);
|
|
216
|
-
console.log(` POST /verify →
|
|
217
|
-
console.log(` POST /fix →
|
|
218
|
-
console.log(` POST /fix/apply-safe →
|
|
219
|
-
console.log(` POST /patch →
|
|
1166
|
+
console.log(` POST /verify → execution bus: verify`);
|
|
1167
|
+
console.log(` POST /fix → execution bus: fix + reverify`);
|
|
1168
|
+
console.log(` POST /fix/apply-safe → execution bus: apply-safe + reverify`);
|
|
1169
|
+
console.log(` POST /patch → execution bus: patch + reverify`);
|
|
1170
|
+
console.log(` POST /execute → unified execution endpoint`);
|
|
1171
|
+
console.log(` GET /executions → execution history`);
|
|
1172
|
+
console.log(` GET /executions/:id → execution detail`);
|
|
1173
|
+
console.log(` GET /executions/:id/timeline → phase timeline + durations`);
|
|
1174
|
+
console.log(` GET /executions/:id/diff → verification + patch inspection`);
|
|
1175
|
+
console.log(` GET /events → runtime event history`);
|
|
1176
|
+
console.log(` GET /events/stream → SSE deterministic governance runtime`);
|
|
1177
|
+
console.log(` GET /control-plane → governance control-plane state + snapshots`);
|
|
1178
|
+
console.log(` POST /control-plane/preview → deterministic config impact preview`);
|
|
1179
|
+
console.log(` PUT /control-plane → apply deterministic governance config update`);
|
|
1180
|
+
console.log(` GET /workspaces → workspace catalog + active pointer`);
|
|
1181
|
+
console.log(` GET /workspaces/runtime → workspace governance runtime snapshot`);
|
|
1182
|
+
console.log(` GET /workspaces/:id/runtime → workspace-specific runtime snapshot`);
|
|
1183
|
+
console.log(` GET /workspaces/:id → workspace definition`);
|
|
1184
|
+
console.log(` POST /workspaces → create workspace`);
|
|
1185
|
+
console.log(` PUT /workspaces/:id → update workspace`);
|
|
1186
|
+
console.log(` POST /workspaces/:id/activate → set active workspace`);
|
|
1187
|
+
console.log(` POST /workspaces/:id/repositories → add repository to workspace`);
|
|
1188
|
+
console.log(` POST /workspaces/execute → workspace-scoped deterministic execution`);
|
|
1189
|
+
console.log(` GET /replay/state → deterministic governance state replay`);
|
|
1190
|
+
console.log(` GET /replay/execution/:id → deterministic execution replay`);
|
|
1191
|
+
console.log(` GET /replay/workspace/:id → deterministic workspace replay`);
|
|
1192
|
+
console.log(` GET /replay/timeline → deterministic governance timeline replay`);
|
|
220
1193
|
console.log(`\n CWD: ${process.cwd()}`);
|
|
221
1194
|
console.log(` Press Ctrl+C to stop.\n`);
|
|
222
1195
|
});
|
|
223
|
-
|
|
224
|
-
|
|
1196
|
+
const stop = () => {
|
|
1197
|
+
stopRuntimeEventTailer();
|
|
1198
|
+
closeRuntimeEventClients();
|
|
1199
|
+
if (runtimeEventUnsubscribe) {
|
|
1200
|
+
runtimeEventUnsubscribe();
|
|
1201
|
+
runtimeEventUnsubscribe = null;
|
|
1202
|
+
}
|
|
1203
|
+
server.close(() => {
|
|
1204
|
+
console.log('\nNeurcode daemon stopped.');
|
|
1205
|
+
process.exit(0);
|
|
1206
|
+
});
|
|
1207
|
+
};
|
|
1208
|
+
process.on('SIGINT', stop);
|
|
1209
|
+
process.on('SIGTERM', stop);
|
|
225
1210
|
}
|
|
226
1211
|
//# sourceMappingURL=server.js.map
|