robodev 0.35.0 → 0.37.0
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/package.json +2 -2
- package/src/dev-clients.ts +86 -10
- package/src/dev-inspect.test.ts +269 -7
- package/src/dev-inspect.ts +1064 -106
- package/src/dev-server.ts +117 -16
- package/src/dev.ts +11 -1
- package/templates/auth-chat/package.json +1 -1
- package/templates/backend/package.json +1 -1
- package/templates/empty/package.json +1 -1
- package/templates/marketplace/package.json +1 -1
- package/templates/space/package.json +1 -1
package/src/dev-server.ts
CHANGED
|
@@ -32,7 +32,13 @@ import type {
|
|
|
32
32
|
} from "@robodev-ai/sdk";
|
|
33
33
|
import type { RuntimeJob } from "@robodev-ai/runtime";
|
|
34
34
|
import { devRouteClients } from "./dev-clients.js";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
buildDashboardState,
|
|
37
|
+
isInspectPath,
|
|
38
|
+
shouldCountRequestHistogram,
|
|
39
|
+
truncateInspectPreview,
|
|
40
|
+
type DevInspectStore,
|
|
41
|
+
} from "./dev-inspect.js";
|
|
36
42
|
|
|
37
43
|
/** One compiled generation of the project. Swapped wholesale on reload. */
|
|
38
44
|
export type DevRuntime = {
|
|
@@ -151,26 +157,52 @@ async function resolveSocketUser(
|
|
|
151
157
|
return { ok: true, user };
|
|
152
158
|
}
|
|
153
159
|
|
|
160
|
+
function recordSocketHandshake(
|
|
161
|
+
inspect: DevInspectStore,
|
|
162
|
+
input: {
|
|
163
|
+
path: string;
|
|
164
|
+
name?: string;
|
|
165
|
+
userId?: string | null;
|
|
166
|
+
error?: string;
|
|
167
|
+
},
|
|
168
|
+
): void {
|
|
169
|
+
inspect.record({
|
|
170
|
+
kind: "socket",
|
|
171
|
+
socketKind: "connect",
|
|
172
|
+
path: input.path,
|
|
173
|
+
name: input.name,
|
|
174
|
+
userId: input.userId,
|
|
175
|
+
error: input.error,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
154
179
|
async function handleLocalSocket(
|
|
155
180
|
socket: WebSocket,
|
|
156
181
|
request: FastifyRequest,
|
|
157
182
|
options: DevServerOptions,
|
|
158
183
|
getRuntime: () => DevRuntime | null,
|
|
159
184
|
): Promise<void> {
|
|
185
|
+
const pathname = pathnameOf(request.url);
|
|
160
186
|
const runtime = getRuntime();
|
|
161
187
|
if (!runtime) {
|
|
188
|
+
recordSocketHandshake(options.inspect, { path: pathname, error: "not_found" });
|
|
162
189
|
socket.close(SOCKET_CLOSE_POLICY, "not_found");
|
|
163
190
|
return;
|
|
164
191
|
}
|
|
165
|
-
const pathname = pathnameOf(request.url);
|
|
166
192
|
const matched = matchSocketRoute(pathname, runtime.sockets);
|
|
167
193
|
if (!matched) {
|
|
194
|
+
recordSocketHandshake(options.inspect, { path: pathname, error: "not_found" });
|
|
168
195
|
socket.close(SOCKET_CLOSE_POLICY, "not_found");
|
|
169
196
|
return;
|
|
170
197
|
}
|
|
171
198
|
|
|
172
199
|
const auth = await resolveSocketUser(options, matched.socket.def, request);
|
|
173
200
|
if (!auth.ok) {
|
|
201
|
+
recordSocketHandshake(options.inspect, {
|
|
202
|
+
path: pathname,
|
|
203
|
+
name: matched.socket.name,
|
|
204
|
+
error: "unauthorized",
|
|
205
|
+
});
|
|
174
206
|
socket.close(SOCKET_CLOSE_POLICY, "unauthorized");
|
|
175
207
|
return;
|
|
176
208
|
}
|
|
@@ -210,12 +242,24 @@ async function handleLocalSocket(
|
|
|
210
242
|
onClose: matched.socket.def.onClose,
|
|
211
243
|
});
|
|
212
244
|
if (!session.ok) {
|
|
245
|
+
recordSocketHandshake(options.inspect, {
|
|
246
|
+
path: pathname,
|
|
247
|
+
name: matched.socket.name,
|
|
248
|
+
userId: auth.user?.id ?? null,
|
|
249
|
+
error: session.reason,
|
|
250
|
+
});
|
|
213
251
|
if (socket.readyState === socket.OPEN) {
|
|
214
252
|
socket.close(session.code, session.reason);
|
|
215
253
|
}
|
|
216
254
|
return;
|
|
217
255
|
}
|
|
218
256
|
|
|
257
|
+
recordSocketHandshake(options.inspect, {
|
|
258
|
+
path: pathname,
|
|
259
|
+
name: matched.socket.name,
|
|
260
|
+
userId: auth.user?.id ?? null,
|
|
261
|
+
});
|
|
262
|
+
|
|
219
263
|
let alive = true;
|
|
220
264
|
const pingTimer = setInterval(() => {
|
|
221
265
|
if (socket.readyState !== socket.OPEN) {
|
|
@@ -240,18 +284,52 @@ async function handleLocalSocket(
|
|
|
240
284
|
});
|
|
241
285
|
socket.on("message", (data, isBinary) => {
|
|
242
286
|
if (isBinary) {
|
|
287
|
+
options.inspect.record({
|
|
288
|
+
kind: "socket",
|
|
289
|
+
socketKind: "message",
|
|
290
|
+
path: pathname,
|
|
291
|
+
name: matched.socket.name,
|
|
292
|
+
userId: auth.user?.id ?? null,
|
|
293
|
+
error: "binary",
|
|
294
|
+
});
|
|
243
295
|
session.onBinary();
|
|
244
296
|
return;
|
|
245
297
|
}
|
|
246
298
|
const text = typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8");
|
|
299
|
+
options.inspect.record({
|
|
300
|
+
kind: "socket",
|
|
301
|
+
socketKind: "message",
|
|
302
|
+
path: pathname,
|
|
303
|
+
name: matched.socket.name,
|
|
304
|
+
userId: auth.user?.id ?? null,
|
|
305
|
+
preview: truncateInspectPreview(text),
|
|
306
|
+
});
|
|
247
307
|
session.onText(text);
|
|
248
308
|
});
|
|
249
|
-
socket.on("close", () => {
|
|
309
|
+
socket.on("close", (code, reason) => {
|
|
250
310
|
clearInterval(pingTimer);
|
|
311
|
+
options.inspect.record({
|
|
312
|
+
kind: "socket",
|
|
313
|
+
socketKind: "close",
|
|
314
|
+
path: pathname,
|
|
315
|
+
name: matched.socket.name,
|
|
316
|
+
userId: auth.user?.id ?? null,
|
|
317
|
+
code,
|
|
318
|
+
reason: reason?.toString() || undefined,
|
|
319
|
+
});
|
|
251
320
|
session.onClose();
|
|
252
321
|
});
|
|
253
322
|
}
|
|
254
323
|
|
|
324
|
+
function recordStorageServe(
|
|
325
|
+
inspect: DevInspectStore,
|
|
326
|
+
key: string,
|
|
327
|
+
ok: boolean,
|
|
328
|
+
error?: string,
|
|
329
|
+
): void {
|
|
330
|
+
inspect.record({ kind: "storage", op: "serve", key, ok, error });
|
|
331
|
+
}
|
|
332
|
+
|
|
255
333
|
async function serveLocalStorage(
|
|
256
334
|
request: FastifyRequest,
|
|
257
335
|
reply: FastifyReply,
|
|
@@ -259,12 +337,13 @@ async function serveLocalStorage(
|
|
|
259
337
|
getRuntime: () => DevRuntime | null,
|
|
260
338
|
pathname: string,
|
|
261
339
|
): Promise<void> {
|
|
340
|
+
const encodedKey = pathname.slice("/storage/objects/".length);
|
|
262
341
|
const runtime = getRuntime();
|
|
263
342
|
if (!runtime) {
|
|
264
343
|
await reply.code(503).send({ error: "not_ready" });
|
|
344
|
+
recordStorageServe(options.inspect, encodedKey, false, "not_ready");
|
|
265
345
|
return;
|
|
266
346
|
}
|
|
267
|
-
const encodedKey = pathname.slice("/storage/objects/".length);
|
|
268
347
|
let key: string;
|
|
269
348
|
try {
|
|
270
349
|
key = decodeURIComponent(encodedKey);
|
|
@@ -273,9 +352,12 @@ async function serveLocalStorage(
|
|
|
273
352
|
const mapped = codedErrorBody(error);
|
|
274
353
|
if (mapped) {
|
|
275
354
|
await reply.code(mapped.status).send(mapped.body);
|
|
355
|
+
const mappedError = typeof mapped.body.error === "string" ? mapped.body.error : "invalid_key";
|
|
356
|
+
recordStorageServe(options.inspect, encodedKey, false, mappedError);
|
|
276
357
|
return;
|
|
277
358
|
}
|
|
278
359
|
await reply.code(400).send({ error: "invalid_key" });
|
|
360
|
+
recordStorageServe(options.inspect, encodedKey, false, "invalid_key");
|
|
279
361
|
return;
|
|
280
362
|
}
|
|
281
363
|
|
|
@@ -286,6 +368,7 @@ async function serveLocalStorage(
|
|
|
286
368
|
});
|
|
287
369
|
if (!resolved) {
|
|
288
370
|
await reply.code(404).send({ error: "not_found" });
|
|
371
|
+
recordStorageServe(options.inspect, key, false, "not_found");
|
|
289
372
|
return;
|
|
290
373
|
}
|
|
291
374
|
|
|
@@ -301,16 +384,18 @@ async function serveLocalStorage(
|
|
|
301
384
|
);
|
|
302
385
|
if (!resolved.row.public && !user && !signed) {
|
|
303
386
|
await reply.code(404).send({ error: "not_found" });
|
|
387
|
+
recordStorageServe(options.inspect, key, false, "not_found");
|
|
304
388
|
return;
|
|
305
389
|
}
|
|
306
390
|
|
|
307
391
|
await reply.type(resolved.row.content_type).send(resolved.body);
|
|
392
|
+
recordStorageServe(options.inspect, key, true);
|
|
308
393
|
}
|
|
309
394
|
|
|
310
395
|
/**
|
|
311
396
|
* A single-project HTTP host. Unlike the hosted project host there is no `Host` lookup
|
|
312
397
|
* and no static SPA serving (local Vite does that). Fastify stays `logger: false`;
|
|
313
|
-
* project/auth/docs requests are recorded on the process-local
|
|
398
|
+
* project/auth/docs requests are recorded on the process-local dashboard after reply.
|
|
314
399
|
* Routes, validation, and OpenAPI all come from `@robodev-ai/runtime`.
|
|
315
400
|
*/
|
|
316
401
|
export async function startDevServer(
|
|
@@ -377,8 +462,19 @@ export async function startDevServer(
|
|
|
377
462
|
const method = request.method.toLowerCase();
|
|
378
463
|
|
|
379
464
|
if (isInspectPath(pathname)) {
|
|
380
|
-
if (pathname === "/_robodev/logs
|
|
381
|
-
return reply.
|
|
465
|
+
if (pathname === "/_robodev/logs") {
|
|
466
|
+
return reply.redirect("/_robodev");
|
|
467
|
+
}
|
|
468
|
+
if (pathname === "/_robodev/state.json" || pathname === "/_robodev/logs.json") {
|
|
469
|
+
return reply.send(
|
|
470
|
+
await buildDashboardState({
|
|
471
|
+
inspect: options.inspect,
|
|
472
|
+
runtime: getRuntime(),
|
|
473
|
+
projectId: options.projectId,
|
|
474
|
+
googleClientId: options.googleClientId,
|
|
475
|
+
googleClientSecret: options.googleClientSecret,
|
|
476
|
+
}),
|
|
477
|
+
);
|
|
382
478
|
}
|
|
383
479
|
return reply.type("text/html; charset=utf-8").send(options.inspect.html());
|
|
384
480
|
}
|
|
@@ -389,15 +485,20 @@ export async function startDevServer(
|
|
|
389
485
|
} finally {
|
|
390
486
|
const status = reply.statusCode;
|
|
391
487
|
const durationMs = Date.now() - started;
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
488
|
+
if (!isInspectPath(pathname)) {
|
|
489
|
+
options.inspect.record({
|
|
490
|
+
kind: "request",
|
|
491
|
+
method: request.method,
|
|
492
|
+
path: pathname,
|
|
493
|
+
status,
|
|
494
|
+
durationMs,
|
|
495
|
+
});
|
|
496
|
+
// Print here (not via `log`) so request lines stay off compile/reload logs.
|
|
497
|
+
console.log(`${request.method} ${pathname} ${status} ${durationMs}ms`);
|
|
498
|
+
if (shouldCountRequestHistogram(pathname, getRuntime(), request.method)) {
|
|
499
|
+
options.inspect.incrementRequestHistogram(status);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
401
502
|
}
|
|
402
503
|
return reply;
|
|
403
504
|
|
package/src/dev.ts
CHANGED
|
@@ -192,6 +192,16 @@ export async function main(args: string[]): Promise<void> {
|
|
|
192
192
|
projectId: DEV_PROJECT_ID,
|
|
193
193
|
jwtSecret: DEV_JWT_SECRET,
|
|
194
194
|
}),
|
|
195
|
+
onJobFinished: (event) => {
|
|
196
|
+
inspect.record({
|
|
197
|
+
kind: "job_run",
|
|
198
|
+
name: event.name,
|
|
199
|
+
status: event.outcome,
|
|
200
|
+
payload: event.payload,
|
|
201
|
+
error: event.error ?? undefined,
|
|
202
|
+
});
|
|
203
|
+
inspect.incrementJobHistogram(event.outcome);
|
|
204
|
+
},
|
|
195
205
|
});
|
|
196
206
|
|
|
197
207
|
async function compileAndActivate(input: { syncSchema: boolean }): Promise<void> {
|
|
@@ -323,7 +333,7 @@ export async function main(args: string[]): Promise<void> {
|
|
|
323
333
|
console.log(`Storage ${relative(project.root, storageDir) || storageDir}`);
|
|
324
334
|
console.log(`API ${apiUrl}`);
|
|
325
335
|
console.log(`Swagger ${apiUrl}/_robodev/docs`);
|
|
326
|
-
console.log(`
|
|
336
|
+
console.log(`Dashboard ${apiUrl}/_robodev`);
|
|
327
337
|
console.log(
|
|
328
338
|
`Project ${relative(process.cwd(), project.backendDir) || "."} (${project.layout})`,
|
|
329
339
|
);
|