robodev 0.34.0 → 0.36.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 +4 -2
- package/src/dev-clients.ts +31 -26
- package/src/dev-inspect.test.ts +136 -7
- package/src/dev-inspect.ts +685 -106
- package/src/dev-server.ts +301 -24
- package/src/dev.ts +43 -6
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "robodev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"description": "CLI for Robodev Starbase — create, auth, link, deploy, and run a project locally with robodev dev",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,14 +22,16 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@fastify/cors": "^11.3.0",
|
|
25
|
+
"@fastify/websocket": "^11.2.0",
|
|
25
26
|
"embedded-postgres": "17.10.0-beta.17",
|
|
26
27
|
"fastify": "^5.12.1",
|
|
27
28
|
"tsx": "^4.20.5",
|
|
28
|
-
"@robodev-ai/runtime": "0.
|
|
29
|
+
"@robodev-ai/runtime": "0.4.1",
|
|
29
30
|
"@robodev-ai/sdk": "0.11.0"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@types/node": "^24.3.0",
|
|
34
|
+
"@types/ws": "^8.18.1",
|
|
33
35
|
"typescript": "^5.9.2"
|
|
34
36
|
},
|
|
35
37
|
"scripts": {
|
package/src/dev-clients.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createLocalStorageClient,
|
|
3
|
+
createProjectSocketsClient,
|
|
4
|
+
type StorageQueryable,
|
|
5
|
+
} from "@robodev-ai/runtime";
|
|
1
6
|
import type { RouteClients } from "@robodev-ai/runtime";
|
|
2
7
|
import type { EmailClient, JobEnqueueInput, LlmClient } from "@robodev-ai/sdk";
|
|
3
8
|
import type { DevInspectStore } from "./dev-inspect.js";
|
|
4
9
|
|
|
5
10
|
/**
|
|
6
11
|
* The `ctx.*` clients `robodev dev` can offer offline. `email` prints to the terminal;
|
|
7
|
-
* `jobs`
|
|
8
|
-
*
|
|
12
|
+
* `jobs`, `storage`, and `sockets` run locally. llm / agent / push throw hosted-style
|
|
13
|
+
* not-configured codes rather than silently doing nothing.
|
|
9
14
|
*/
|
|
10
15
|
|
|
11
|
-
function
|
|
12
|
-
throw Object.assign(
|
|
13
|
-
new Error(`ctx.${feature} needs hosted Starbase. Run robodev deploy to use it.`),
|
|
14
|
-
{ statusCode: 501 },
|
|
15
|
-
);
|
|
16
|
+
function notConfigured(code: string): never {
|
|
17
|
+
throw Object.assign(new Error(code), { statusCode: 400, code });
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
function errorMessage(error: unknown): string {
|
|
@@ -37,8 +39,8 @@ export function consoleEmailClient(
|
|
|
37
39
|
|
|
38
40
|
function offlineLlmClient(): LlmClient {
|
|
39
41
|
return {
|
|
40
|
-
complete: () =>
|
|
41
|
-
stream: () =>
|
|
42
|
+
complete: () => notConfigured("llm_not_configured"),
|
|
43
|
+
stream: () => notConfigured("llm_not_configured"),
|
|
42
44
|
};
|
|
43
45
|
}
|
|
44
46
|
|
|
@@ -47,29 +49,35 @@ export function devRouteClients(input: {
|
|
|
47
49
|
log: (line: string) => void;
|
|
48
50
|
enqueue: (job: JobEnqueueInput) => Promise<{ id: string }>;
|
|
49
51
|
inspect: DevInspectStore;
|
|
52
|
+
query: StorageQueryable["query"];
|
|
53
|
+
objectsDir: string;
|
|
54
|
+
publicBaseUrl: string;
|
|
55
|
+
projectId: string;
|
|
56
|
+
jwtSecret: string;
|
|
50
57
|
}): RouteClients {
|
|
51
58
|
return {
|
|
52
59
|
email: consoleEmailClient(input.log, input.inspect),
|
|
53
60
|
llm: offlineLlmClient(),
|
|
54
61
|
agent: {
|
|
55
|
-
createSession: () =>
|
|
56
|
-
start: () =>
|
|
57
|
-
events: () =>
|
|
58
|
-
subscribe: () =>
|
|
59
|
-
destroy: () =>
|
|
62
|
+
createSession: () => notConfigured("llm_not_configured"),
|
|
63
|
+
start: () => notConfigured("llm_not_configured"),
|
|
64
|
+
events: () => notConfigured("llm_not_configured"),
|
|
65
|
+
subscribe: () => notConfigured("llm_not_configured"),
|
|
66
|
+
destroy: () => notConfigured("llm_not_configured"),
|
|
60
67
|
},
|
|
61
|
-
storage: {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
},
|
|
68
|
-
push: { send: () =>
|
|
68
|
+
storage: createLocalStorageClient({
|
|
69
|
+
query: input.query,
|
|
70
|
+
objectsDir: input.objectsDir,
|
|
71
|
+
publicBaseUrl: input.publicBaseUrl,
|
|
72
|
+
projectId: input.projectId,
|
|
73
|
+
jwtSecret: input.jwtSecret,
|
|
74
|
+
}),
|
|
75
|
+
push: { send: () => notConfigured("push_not_configured") },
|
|
69
76
|
jobs: {
|
|
70
77
|
enqueue: async (job) => {
|
|
71
78
|
try {
|
|
72
79
|
const result = await input.enqueue(job);
|
|
80
|
+
// Enqueue is not a run; the dashboard Jobs page reads `job_run` from onJobFinished.
|
|
73
81
|
input.inspect.record({
|
|
74
82
|
kind: "job",
|
|
75
83
|
name: job.name,
|
|
@@ -88,10 +96,7 @@ export function devRouteClients(input: {
|
|
|
88
96
|
}
|
|
89
97
|
},
|
|
90
98
|
},
|
|
91
|
-
sockets:
|
|
92
|
-
send: () => unavailable("sockets"),
|
|
93
|
-
broadcast: () => unavailable("sockets"),
|
|
94
|
-
},
|
|
99
|
+
sockets: createProjectSocketsClient(input.projectId),
|
|
95
100
|
env: input.env,
|
|
96
101
|
};
|
|
97
102
|
}
|
package/src/dev-inspect.test.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { test } from "node:test";
|
|
3
3
|
import {
|
|
4
|
+
buildDashboardState,
|
|
5
|
+
callMatchesRoute,
|
|
4
6
|
createDevInspect,
|
|
5
7
|
inspectHtml,
|
|
8
|
+
isHistogramExcludedPath,
|
|
6
9
|
isInspectPath,
|
|
10
|
+
shouldCountRequestHistogram,
|
|
7
11
|
type DevInspectAuthCodeEvent,
|
|
8
12
|
type DevInspectEmailEvent,
|
|
9
13
|
type DevInspectJobEvent,
|
|
@@ -11,9 +15,18 @@ import {
|
|
|
11
15
|
type DevInspectRequestEvent,
|
|
12
16
|
} from "./dev-inspect.js";
|
|
13
17
|
|
|
14
|
-
|
|
18
|
+
const fileRuntime = {
|
|
19
|
+
routes: [{ method: "get", path: "/api/planets" }],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
test("isInspectPath covers dashboard pages and JSON only", () => {
|
|
15
23
|
assert.equal(isInspectPath("/_robodev"), true);
|
|
24
|
+
assert.equal(isInspectPath("/_robodev/apis"), true);
|
|
25
|
+
assert.equal(isInspectPath("/_robodev/jobs"), true);
|
|
26
|
+
assert.equal(isInspectPath("/_robodev/emails"), true);
|
|
27
|
+
assert.equal(isInspectPath("/_robodev/auth"), true);
|
|
16
28
|
assert.equal(isInspectPath("/_robodev/logs"), true);
|
|
29
|
+
assert.equal(isInspectPath("/_robodev/state.json"), true);
|
|
17
30
|
assert.equal(isInspectPath("/_robodev/logs.json"), true);
|
|
18
31
|
assert.equal(isInspectPath("/_robodev/docs"), false);
|
|
19
32
|
assert.equal(isInspectPath("/_robodev/ready"), false);
|
|
@@ -22,6 +35,23 @@ test("isInspectPath excludes only inspect self-routes", () => {
|
|
|
22
35
|
assert.equal(isInspectPath("/api/planets"), false);
|
|
23
36
|
});
|
|
24
37
|
|
|
38
|
+
test("shouldCountRequestHistogram counts Auth and matched file routes only", () => {
|
|
39
|
+
assert.equal(shouldCountRequestHistogram("/api/user/me", fileRuntime, "GET"), true);
|
|
40
|
+
assert.equal(shouldCountRequestHistogram("/api/user/auth/google", fileRuntime, "GET"), true);
|
|
41
|
+
assert.equal(shouldCountRequestHistogram("/api/planets", fileRuntime, "GET"), true);
|
|
42
|
+
assert.equal(shouldCountRequestHistogram("/api/planets", fileRuntime, "POST"), false);
|
|
43
|
+
assert.equal(shouldCountRequestHistogram("/api/unknown", fileRuntime, "GET"), false);
|
|
44
|
+
assert.equal(shouldCountRequestHistogram("/_robodev", fileRuntime, "GET"), false);
|
|
45
|
+
assert.equal(shouldCountRequestHistogram("/_robodev/apis", fileRuntime, "GET"), false);
|
|
46
|
+
assert.equal(shouldCountRequestHistogram("/_robodev/docs", fileRuntime, "GET"), false);
|
|
47
|
+
assert.equal(shouldCountRequestHistogram("/_robodev/scalar", fileRuntime, "GET"), false);
|
|
48
|
+
assert.equal(shouldCountRequestHistogram("/_robodev/ready", fileRuntime, "GET"), false);
|
|
49
|
+
assert.equal(shouldCountRequestHistogram("/openapi.json", fileRuntime, "GET"), false);
|
|
50
|
+
assert.equal(shouldCountRequestHistogram("/docs", fileRuntime, "GET"), false);
|
|
51
|
+
assert.equal(shouldCountRequestHistogram("/swagger", fileRuntime, "GET"), false);
|
|
52
|
+
assert.equal(isHistogramExcludedPath("/scalar"), true);
|
|
53
|
+
});
|
|
54
|
+
|
|
25
55
|
test("ring buffer drops oldest events past max", () => {
|
|
26
56
|
const inspect = createDevInspect({ max: 3 });
|
|
27
57
|
inspect.record({ kind: "log", message: "a" });
|
|
@@ -37,6 +67,32 @@ test("ring buffer drops oldest events past max", () => {
|
|
|
37
67
|
);
|
|
38
68
|
});
|
|
39
69
|
|
|
70
|
+
test("request histogram survives 500-event ring rotation", () => {
|
|
71
|
+
const inspect = createDevInspect({ max: 2 });
|
|
72
|
+
inspect.incrementRequestHistogram(200);
|
|
73
|
+
inspect.incrementRequestHistogram(200);
|
|
74
|
+
inspect.incrementRequestHistogram(404);
|
|
75
|
+
inspect.incrementRequestHistogram(500);
|
|
76
|
+
inspect.record({ kind: "request", method: "GET", path: "/api/a", status: 200, durationMs: 1 });
|
|
77
|
+
inspect.record({ kind: "request", method: "GET", path: "/api/b", status: 200, durationMs: 1 });
|
|
78
|
+
inspect.record({ kind: "request", method: "GET", path: "/api/c", status: 200, durationMs: 1 });
|
|
79
|
+
assert.equal(inspect.list().length, 2);
|
|
80
|
+
|
|
81
|
+
const last = inspect.requestHistogram().at(-1);
|
|
82
|
+
assert.equal(last?.c2xx, 2);
|
|
83
|
+
assert.equal(last?.c4xx, 1);
|
|
84
|
+
assert.equal(last?.c5xx, 1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("3xx does not increment request histogram buckets", () => {
|
|
88
|
+
const inspect = createDevInspect();
|
|
89
|
+
inspect.incrementRequestHistogram(302);
|
|
90
|
+
const last = inspect.requestHistogram().at(-1);
|
|
91
|
+
assert.equal(last?.c2xx, 0);
|
|
92
|
+
assert.equal(last?.c4xx, 0);
|
|
93
|
+
assert.equal(last?.c5xx, 0);
|
|
94
|
+
});
|
|
95
|
+
|
|
40
96
|
test("list is newest last and does not leak the internal buffer", () => {
|
|
41
97
|
const inspect = createDevInspect({ max: 500 });
|
|
42
98
|
inspect.record({ kind: "log", message: "first" });
|
|
@@ -112,12 +168,85 @@ test("record assigns id, at, and the expected fields for each kind", () => {
|
|
|
112
168
|
assert.equal(inspect.list().length, 5);
|
|
113
169
|
});
|
|
114
170
|
|
|
115
|
-
test("
|
|
171
|
+
test("callMatchesRoute uses matchPathParams and requires method", () => {
|
|
172
|
+
const route = { method: "GET", path: "/api/planets/:id" };
|
|
173
|
+
assert.equal(callMatchesRoute({ method: "GET", path: "/api/planets/123" }, route), true);
|
|
174
|
+
assert.equal(callMatchesRoute({ method: "GET", path: "/api/planets" }, route), false);
|
|
175
|
+
assert.equal(callMatchesRoute({ method: "POST", path: "/api/planets/123" }, route), false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("dashboard snapshot lists reserved Auth, filters emails, and ignores enqueue jobs", async () => {
|
|
179
|
+
const inspect = createDevInspect();
|
|
180
|
+
inspect.record({
|
|
181
|
+
kind: "request",
|
|
182
|
+
method: "GET",
|
|
183
|
+
path: "/api/planets",
|
|
184
|
+
status: 200,
|
|
185
|
+
durationMs: 4,
|
|
186
|
+
});
|
|
187
|
+
inspect.record({
|
|
188
|
+
kind: "request",
|
|
189
|
+
method: "GET",
|
|
190
|
+
path: "/_robodev/docs",
|
|
191
|
+
status: 200,
|
|
192
|
+
durationMs: 1,
|
|
193
|
+
});
|
|
194
|
+
inspect.record({ kind: "email", to: "a@b.test", subject: "Hello", text: "Body" });
|
|
195
|
+
inspect.record({ kind: "auth_code", type: "magic", to: "a@b.test", code: "999111" });
|
|
196
|
+
inspect.record({ kind: "job", name: "hourly", payload: { n: 1 }, error: "" });
|
|
197
|
+
inspect.record({
|
|
198
|
+
kind: "job_run",
|
|
199
|
+
name: "hourly",
|
|
200
|
+
status: "failed",
|
|
201
|
+
payload: { n: 1 },
|
|
202
|
+
error: "boom",
|
|
203
|
+
});
|
|
204
|
+
inspect.incrementJobHistogram("failed");
|
|
205
|
+
|
|
206
|
+
const state = await buildDashboardState({
|
|
207
|
+
inspect,
|
|
208
|
+
runtime: {
|
|
209
|
+
routes: [{ method: "get", path: "/api/planets", file: "api/planets.ts" }],
|
|
210
|
+
jobs: [{ name: "hourly", file: "jobs/hourly.ts", def: { cron: "0 * * * *" } }],
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
assert.equal(state.overview.apiCount, 1 + 12);
|
|
215
|
+
assert.equal(state.overview.jobCount, 1);
|
|
216
|
+
assert.equal(state.overview.google, "broker");
|
|
217
|
+
assert.ok(state.routes.some((route) => route.path === "/api/user/auth/google/callback"));
|
|
218
|
+
assert.equal(state.calls.length, 1);
|
|
219
|
+
assert.equal(state.calls[0]?.path, "/api/planets");
|
|
220
|
+
assert.equal(state.emails.length, 1);
|
|
221
|
+
assert.equal(state.emails[0]?.subject, "Hello");
|
|
222
|
+
assert.equal(state.jobRuns.length, 1);
|
|
223
|
+
assert.equal(state.jobRuns[0]?.status, "failed");
|
|
224
|
+
assert.equal(state.overview.jobHistogram.at(-1)?.failed, 1);
|
|
225
|
+
assert.deepEqual(state.auth.users, []);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("incomplete Google credentials are not labeled override", async () => {
|
|
229
|
+
const state = await buildDashboardState({
|
|
230
|
+
inspect: createDevInspect(),
|
|
231
|
+
runtime: { routes: [], jobs: [] },
|
|
232
|
+
googleClientId: "only-id",
|
|
233
|
+
});
|
|
234
|
+
assert.equal(state.overview.google, "incomplete");
|
|
235
|
+
assert.equal(state.auth.google, "incomplete");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("inspect HTML has nav labels and polls state.json", () => {
|
|
116
239
|
const html = inspectHtml();
|
|
117
|
-
assert.match(html,
|
|
118
|
-
assert.match(html,
|
|
119
|
-
assert.match(html,
|
|
120
|
-
assert.match(html,
|
|
121
|
-
assert.match(html,
|
|
240
|
+
assert.match(html, />Overview</);
|
|
241
|
+
assert.match(html, />APIs</);
|
|
242
|
+
assert.match(html, />Jobs</);
|
|
243
|
+
assert.match(html, />Emails</);
|
|
244
|
+
assert.match(html, />Auth</);
|
|
245
|
+
assert.doesNotMatch(html, /<h2>Requests<\/h2>/);
|
|
246
|
+
assert.doesNotMatch(html, /<h2>Emails<\/h2>/);
|
|
247
|
+
assert.doesNotMatch(html, /<h2>Jobs<\/h2>/);
|
|
248
|
+
assert.doesNotMatch(html, /<h2>Logs<\/h2>/);
|
|
249
|
+
assert.match(html, /\/_robodev\/state\.json/);
|
|
250
|
+
assert.doesNotMatch(html, /fetch\("\/_robodev\/logs\.json"\)/);
|
|
122
251
|
assert.match(html, /setInterval\(poll, 1000\)/);
|
|
123
252
|
});
|