robodev 0.34.0 → 0.35.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 +30 -26
- package/src/dev-server.ts +268 -11
- package/src/dev.ts +32 -5
- 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.35.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.0",
|
|
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,25 +49,30 @@ 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 {
|
|
@@ -88,10 +95,7 @@ export function devRouteClients(input: {
|
|
|
88
95
|
}
|
|
89
96
|
},
|
|
90
97
|
},
|
|
91
|
-
sockets:
|
|
92
|
-
send: () => unavailable("sockets"),
|
|
93
|
-
broadcast: () => unavailable("sockets"),
|
|
94
|
-
},
|
|
98
|
+
sockets: createProjectSocketsClient(input.projectId),
|
|
95
99
|
env: input.env,
|
|
96
100
|
};
|
|
97
101
|
}
|
package/src/dev-server.ts
CHANGED
|
@@ -1,19 +1,35 @@
|
|
|
1
1
|
import { Readable } from "node:stream";
|
|
2
2
|
import cors from "@fastify/cors";
|
|
3
|
-
import
|
|
3
|
+
import websocket from "@fastify/websocket";
|
|
4
|
+
import Fastify, { type FastifyInstance, type FastifyReply, type FastifyRequest } from "fastify";
|
|
5
|
+
import type { WebSocket } from "ws";
|
|
4
6
|
import {
|
|
7
|
+
SOCKET_CLOSE_POLICY,
|
|
8
|
+
SOCKET_IDLE_PING_MS,
|
|
9
|
+
bindSocketSession,
|
|
5
10
|
buildOpenApi,
|
|
6
11
|
handleLocalAuth,
|
|
7
12
|
isLocalAuthPath,
|
|
13
|
+
isReservedStoragePath,
|
|
8
14
|
matchProjectRoute,
|
|
15
|
+
matchSocketRoute,
|
|
16
|
+
parseLogicalKey,
|
|
17
|
+
resolveLocalObject,
|
|
9
18
|
resolveLocalUser,
|
|
10
19
|
runRoute,
|
|
11
20
|
scalarHtml,
|
|
12
21
|
swaggerHtml,
|
|
22
|
+
verifyStorageServeToken,
|
|
13
23
|
type AuthQueryable,
|
|
14
24
|
type RuntimeRoute,
|
|
25
|
+
type RuntimeSocket,
|
|
15
26
|
} from "@robodev-ai/runtime";
|
|
16
|
-
import type {
|
|
27
|
+
import type {
|
|
28
|
+
JobEnqueueInput,
|
|
29
|
+
RobodevDb,
|
|
30
|
+
SocketDefinition,
|
|
31
|
+
SocketHandlerContext,
|
|
32
|
+
} from "@robodev-ai/sdk";
|
|
17
33
|
import type { RuntimeJob } from "@robodev-ai/runtime";
|
|
18
34
|
import { devRouteClients } from "./dev-clients.js";
|
|
19
35
|
import { isInspectPath, type DevInspectStore } from "./dev-inspect.js";
|
|
@@ -24,6 +40,7 @@ export type DevRuntime = {
|
|
|
24
40
|
pool: AuthQueryable & { end: () => Promise<void> };
|
|
25
41
|
routes: RuntimeRoute[];
|
|
26
42
|
jobs: RuntimeJob[];
|
|
43
|
+
sockets: RuntimeSocket[];
|
|
27
44
|
openapi: Record<string, unknown>;
|
|
28
45
|
};
|
|
29
46
|
|
|
@@ -39,6 +56,7 @@ export type DevServerOptions = {
|
|
|
39
56
|
brokerBaseUrl: string;
|
|
40
57
|
googleClientId?: string;
|
|
41
58
|
googleClientSecret?: string;
|
|
59
|
+
storageDir: string;
|
|
42
60
|
};
|
|
43
61
|
|
|
44
62
|
const SWAGGER_PATHS = new Set(["/_robodev/docs", "/_robodev/swagger", "/docs", "/swagger"]);
|
|
@@ -57,6 +75,238 @@ function rawBodyOf(request: FastifyRequest): Buffer | undefined {
|
|
|
57
75
|
return (request as FastifyRequest & { rawBody?: Buffer }).rawBody;
|
|
58
76
|
}
|
|
59
77
|
|
|
78
|
+
function queryRecord(query: unknown): Record<string, string> {
|
|
79
|
+
if (!query || typeof query !== "object") return {};
|
|
80
|
+
const out: Record<string, string> = {};
|
|
81
|
+
for (const [key, value] of Object.entries(query as Record<string, unknown>)) {
|
|
82
|
+
if (typeof value === "string") out[key] = value;
|
|
83
|
+
else if (Array.isArray(value) && typeof value[0] === "string") out[key] = value[0];
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function socketAuthorization(request: FastifyRequest): string | undefined {
|
|
89
|
+
const header = request.headers.authorization;
|
|
90
|
+
if (typeof header === "string" && header.length > 0) return header;
|
|
91
|
+
const token = queryRecord(request.query).accessToken;
|
|
92
|
+
if (token) return `Bearer ${token}`;
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function codedErrorBody(error: unknown): { status: number; body: Record<string, unknown> } | null {
|
|
97
|
+
if (!error || typeof error !== "object" || !("statusCode" in error)) return null;
|
|
98
|
+
const statusCode = (error as { statusCode: unknown }).statusCode;
|
|
99
|
+
if (typeof statusCode !== "number") return null;
|
|
100
|
+
const code = (error as { code?: unknown }).code;
|
|
101
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
102
|
+
return {
|
|
103
|
+
status: statusCode,
|
|
104
|
+
body: {
|
|
105
|
+
error: typeof code === "string" ? code : message,
|
|
106
|
+
message,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function routeClients(options: DevServerOptions, getRuntime: () => DevRuntime | null) {
|
|
112
|
+
return devRouteClients({
|
|
113
|
+
env: options.env,
|
|
114
|
+
log: options.log,
|
|
115
|
+
enqueue: options.enqueue,
|
|
116
|
+
inspect: options.inspect,
|
|
117
|
+
query: (sql, values) => {
|
|
118
|
+
const runtime = getRuntime();
|
|
119
|
+
if (!runtime) throw new Error("dev runtime not ready");
|
|
120
|
+
return runtime.pool.query(sql, values);
|
|
121
|
+
},
|
|
122
|
+
objectsDir: options.storageDir,
|
|
123
|
+
publicBaseUrl: options.listenOrigin,
|
|
124
|
+
projectId: options.projectId,
|
|
125
|
+
jwtSecret: options.jwtSecret,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function resolveSocketUser(
|
|
130
|
+
options: DevServerOptions,
|
|
131
|
+
def: SocketDefinition,
|
|
132
|
+
request: FastifyRequest,
|
|
133
|
+
): Promise<{ ok: true; user: SocketHandlerContext["user"] } | { ok: false }> {
|
|
134
|
+
const auth = def.auth ?? false;
|
|
135
|
+
if (auth === false) return { ok: true, user: null };
|
|
136
|
+
if (typeof auth === "function") {
|
|
137
|
+
try {
|
|
138
|
+
const user = await auth({ headers: request.headers });
|
|
139
|
+
if (!user) return { ok: false };
|
|
140
|
+
return { ok: true, user };
|
|
141
|
+
} catch {
|
|
142
|
+
return { ok: false };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (auth !== "required" && auth !== "optional") return { ok: false };
|
|
146
|
+
const user = await resolveLocalUser(
|
|
147
|
+
{ projectId: options.projectId, jwtSecret: options.jwtSecret },
|
|
148
|
+
socketAuthorization(request),
|
|
149
|
+
);
|
|
150
|
+
if (auth === "required" && !user) return { ok: false };
|
|
151
|
+
return { ok: true, user };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function handleLocalSocket(
|
|
155
|
+
socket: WebSocket,
|
|
156
|
+
request: FastifyRequest,
|
|
157
|
+
options: DevServerOptions,
|
|
158
|
+
getRuntime: () => DevRuntime | null,
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
const runtime = getRuntime();
|
|
161
|
+
if (!runtime) {
|
|
162
|
+
socket.close(SOCKET_CLOSE_POLICY, "not_found");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const pathname = pathnameOf(request.url);
|
|
166
|
+
const matched = matchSocketRoute(pathname, runtime.sockets);
|
|
167
|
+
if (!matched) {
|
|
168
|
+
socket.close(SOCKET_CLOSE_POLICY, "not_found");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const auth = await resolveSocketUser(options, matched.socket.def, request);
|
|
173
|
+
if (!auth.ok) {
|
|
174
|
+
socket.close(SOCKET_CLOSE_POLICY, "unauthorized");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const clients = routeClients(options, getRuntime);
|
|
179
|
+
const session = await bindSocketSession({
|
|
180
|
+
projectId: options.projectId,
|
|
181
|
+
name: matched.socket.name,
|
|
182
|
+
socket: {
|
|
183
|
+
send: (data) => socket.send(data),
|
|
184
|
+
close: (code, reason) => socket.close(code, reason),
|
|
185
|
+
get readyState() {
|
|
186
|
+
return socket.readyState;
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
timeoutMs: matched.socket.def.timeoutMs,
|
|
190
|
+
createContext: ({ send, close, room }) => ({
|
|
191
|
+
db: runtime.db,
|
|
192
|
+
email: clients.email,
|
|
193
|
+
llm: clients.llm,
|
|
194
|
+
agent: clients.agent,
|
|
195
|
+
storage: clients.storage,
|
|
196
|
+
env: clients.env,
|
|
197
|
+
params: matched.params,
|
|
198
|
+
query: queryRecord(request.query),
|
|
199
|
+
headers: request.headers,
|
|
200
|
+
user: auth.user,
|
|
201
|
+
push: clients.push,
|
|
202
|
+
jobs: clients.jobs,
|
|
203
|
+
sockets: clients.sockets,
|
|
204
|
+
send,
|
|
205
|
+
close,
|
|
206
|
+
room,
|
|
207
|
+
}),
|
|
208
|
+
onConnect: matched.socket.def.onConnect,
|
|
209
|
+
onMessage: matched.socket.def.onMessage,
|
|
210
|
+
onClose: matched.socket.def.onClose,
|
|
211
|
+
});
|
|
212
|
+
if (!session.ok) {
|
|
213
|
+
if (socket.readyState === socket.OPEN) {
|
|
214
|
+
socket.close(session.code, session.reason);
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let alive = true;
|
|
220
|
+
const pingTimer = setInterval(() => {
|
|
221
|
+
if (socket.readyState !== socket.OPEN) {
|
|
222
|
+
clearInterval(pingTimer);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (!alive) {
|
|
226
|
+
socket.close(SOCKET_CLOSE_POLICY, "idle");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
alive = false;
|
|
230
|
+
try {
|
|
231
|
+
socket.ping();
|
|
232
|
+
} catch {
|
|
233
|
+
socket.close(SOCKET_CLOSE_POLICY, "idle");
|
|
234
|
+
}
|
|
235
|
+
}, SOCKET_IDLE_PING_MS);
|
|
236
|
+
pingTimer.unref?.();
|
|
237
|
+
|
|
238
|
+
socket.on("pong", () => {
|
|
239
|
+
alive = true;
|
|
240
|
+
});
|
|
241
|
+
socket.on("message", (data, isBinary) => {
|
|
242
|
+
if (isBinary) {
|
|
243
|
+
session.onBinary();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const text = typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8");
|
|
247
|
+
session.onText(text);
|
|
248
|
+
});
|
|
249
|
+
socket.on("close", () => {
|
|
250
|
+
clearInterval(pingTimer);
|
|
251
|
+
session.onClose();
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function serveLocalStorage(
|
|
256
|
+
request: FastifyRequest,
|
|
257
|
+
reply: FastifyReply,
|
|
258
|
+
options: DevServerOptions,
|
|
259
|
+
getRuntime: () => DevRuntime | null,
|
|
260
|
+
pathname: string,
|
|
261
|
+
): Promise<void> {
|
|
262
|
+
const runtime = getRuntime();
|
|
263
|
+
if (!runtime) {
|
|
264
|
+
await reply.code(503).send({ error: "not_ready" });
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const encodedKey = pathname.slice("/storage/objects/".length);
|
|
268
|
+
let key: string;
|
|
269
|
+
try {
|
|
270
|
+
key = decodeURIComponent(encodedKey);
|
|
271
|
+
parseLogicalKey(key);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
const mapped = codedErrorBody(error);
|
|
274
|
+
if (mapped) {
|
|
275
|
+
await reply.code(mapped.status).send(mapped.body);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
await reply.code(400).send({ error: "invalid_key" });
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const resolved = await resolveLocalObject({
|
|
283
|
+
query: (sql, values) => runtime.pool.query(sql, values),
|
|
284
|
+
objectsDir: options.storageDir,
|
|
285
|
+
key,
|
|
286
|
+
});
|
|
287
|
+
if (!resolved) {
|
|
288
|
+
await reply.code(404).send({ error: "not_found" });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const user = await resolveLocalUser(
|
|
293
|
+
{ projectId: options.projectId, jwtSecret: options.jwtSecret },
|
|
294
|
+
request.headers.authorization,
|
|
295
|
+
);
|
|
296
|
+
const signed = verifyStorageServeToken(
|
|
297
|
+
options.jwtSecret,
|
|
298
|
+
options.projectId,
|
|
299
|
+
key,
|
|
300
|
+
request.query as { exp?: unknown; sig?: unknown },
|
|
301
|
+
);
|
|
302
|
+
if (!resolved.row.public && !user && !signed) {
|
|
303
|
+
await reply.code(404).send({ error: "not_found" });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
await reply.type(resolved.row.content_type).send(resolved.body);
|
|
308
|
+
}
|
|
309
|
+
|
|
60
310
|
/**
|
|
61
311
|
* A single-project HTTP host. Unlike the hosted project host there is no `Host` lookup
|
|
62
312
|
* and no static SPA serving (local Vite does that). Fastify stays `logger: false`;
|
|
@@ -74,6 +324,10 @@ export async function startDevServer(
|
|
|
74
324
|
credentials: true,
|
|
75
325
|
methods: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
76
326
|
});
|
|
327
|
+
await app.register(websocket);
|
|
328
|
+
app.get("/sockets/*", { websocket: true }, (socket, request) => {
|
|
329
|
+
void handleLocalSocket(socket, request, options, getRuntime);
|
|
330
|
+
});
|
|
77
331
|
|
|
78
332
|
app.addContentTypeParser("application/json", { parseAs: "string" }, (_request, body, done) => {
|
|
79
333
|
if (!body) {
|
|
@@ -103,13 +357,16 @@ export async function startDevServer(
|
|
|
103
357
|
);
|
|
104
358
|
|
|
105
359
|
// `rawBody` handlers and multipart parsing need the untouched bytes.
|
|
106
|
-
app.addHook("preParsing", async (
|
|
360
|
+
app.addHook("preParsing", async (request, _reply, payload) => {
|
|
361
|
+
if (String(request.headers.upgrade ?? "").toLowerCase() === "websocket") {
|
|
362
|
+
return payload;
|
|
363
|
+
}
|
|
107
364
|
const chunks: Buffer[] = [];
|
|
108
365
|
for await (const chunk of payload) {
|
|
109
366
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
110
367
|
}
|
|
111
368
|
const raw = Buffer.concat(chunks);
|
|
112
|
-
(
|
|
369
|
+
(request as FastifyRequest & { rawBody?: Buffer }).rawBody = raw;
|
|
113
370
|
return Readable.from(raw);
|
|
114
371
|
});
|
|
115
372
|
|
|
@@ -214,6 +471,11 @@ export async function startDevServer(
|
|
|
214
471
|
return;
|
|
215
472
|
}
|
|
216
473
|
|
|
474
|
+
if (request.method.toUpperCase() === "GET" && isReservedStoragePath(pathname)) {
|
|
475
|
+
await serveLocalStorage(request, reply, options, getRuntime, pathname);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
217
479
|
if (!matchProjectRoute(pathname, method, runtime.routes)) {
|
|
218
480
|
await reply.code(404).send({
|
|
219
481
|
error: "not_found",
|
|
@@ -238,13 +500,8 @@ export async function startDevServer(
|
|
|
238
500
|
{ projectId: options.projectId, jwtSecret: options.jwtSecret },
|
|
239
501
|
authorization,
|
|
240
502
|
),
|
|
241
|
-
clients: () =>
|
|
242
|
-
|
|
243
|
-
env: options.env,
|
|
244
|
-
log: options.log,
|
|
245
|
-
enqueue: options.enqueue,
|
|
246
|
-
inspect: options.inspect,
|
|
247
|
-
}),
|
|
503
|
+
clients: () => routeClients(options, getRuntime),
|
|
504
|
+
mapError: (error) => codedErrorBody(error),
|
|
248
505
|
});
|
|
249
506
|
|
|
250
507
|
if (outcome.kind === "error") {
|
package/src/dev.ts
CHANGED
|
@@ -9,9 +9,13 @@ import {
|
|
|
9
9
|
assertJobCrons,
|
|
10
10
|
buildPlan,
|
|
11
11
|
compileApiTree,
|
|
12
|
+
closeProjectSockets,
|
|
12
13
|
createLocalJobsEngine,
|
|
13
14
|
ensureAuthSchema,
|
|
14
15
|
ensureJobsSchema,
|
|
16
|
+
ensureStorageSchema,
|
|
17
|
+
setProjectSocketNames,
|
|
18
|
+
SOCKET_CLOSE_RESTART,
|
|
15
19
|
isEsbuildFailure,
|
|
16
20
|
loadModules,
|
|
17
21
|
mapEsbuildErrors,
|
|
@@ -154,6 +158,8 @@ export async function main(args: string[]): Promise<void> {
|
|
|
154
158
|
const postgres: DevPostgres = parsed.databaseUrl
|
|
155
159
|
? externalDevPostgres(parsed.databaseUrl)
|
|
156
160
|
: await startDevPostgres({ dataDir: join(devDir, "pg"), port: parsed.dbPort, quiet: true });
|
|
161
|
+
const storageDir = join(devDir, "storage");
|
|
162
|
+
await mkdir(storageDir, { recursive: true });
|
|
157
163
|
|
|
158
164
|
let current: DevRuntime | null = null;
|
|
159
165
|
let lastSchemaSource = "";
|
|
@@ -172,7 +178,20 @@ export async function main(args: string[]): Promise<void> {
|
|
|
172
178
|
getGeneration: () => current,
|
|
173
179
|
log,
|
|
174
180
|
clients: () =>
|
|
175
|
-
devRouteClients({
|
|
181
|
+
devRouteClients({
|
|
182
|
+
env,
|
|
183
|
+
log,
|
|
184
|
+
inspect,
|
|
185
|
+
enqueue: (input) => engine.enqueue(input),
|
|
186
|
+
query: (sql, values) => {
|
|
187
|
+
if (!current) throw new Error("dev runtime not ready");
|
|
188
|
+
return current.pool.query(sql, values);
|
|
189
|
+
},
|
|
190
|
+
objectsDir: storageDir,
|
|
191
|
+
publicBaseUrl: `http://localhost:${parsed.port}`,
|
|
192
|
+
projectId: DEV_PROJECT_ID,
|
|
193
|
+
jwtSecret: DEV_JWT_SECRET,
|
|
194
|
+
}),
|
|
176
195
|
});
|
|
177
196
|
|
|
178
197
|
async function compileAndActivate(input: { syncSchema: boolean }): Promise<void> {
|
|
@@ -222,15 +241,22 @@ export async function main(args: string[]): Promise<void> {
|
|
|
222
241
|
await pool.query("SELECT 1");
|
|
223
242
|
await ensureAuthSchema(pool);
|
|
224
243
|
await ensureJobsSchema(pool);
|
|
244
|
+
await ensureStorageSchema(pool);
|
|
225
245
|
|
|
246
|
+
closeProjectSockets(DEV_PROJECT_ID, SOCKET_CLOSE_RESTART, "deployed");
|
|
226
247
|
const previous = current;
|
|
227
248
|
current = {
|
|
228
249
|
db,
|
|
229
250
|
pool,
|
|
230
251
|
routes: loaded.routes,
|
|
231
252
|
jobs: loaded.jobs,
|
|
253
|
+
sockets: loaded.sockets,
|
|
232
254
|
openapi: buildDevOpenApi(loaded.database.name, loaded.routes),
|
|
233
255
|
};
|
|
256
|
+
setProjectSocketNames(
|
|
257
|
+
DEV_PROJECT_ID,
|
|
258
|
+
loaded.sockets.map((socket) => socket.name),
|
|
259
|
+
);
|
|
234
260
|
await engine.reconcile(loaded.jobs);
|
|
235
261
|
|
|
236
262
|
for (const route of loaded.routes) {
|
|
@@ -243,10 +269,8 @@ export async function main(args: string[]): Promise<void> {
|
|
|
243
269
|
log(` job ${job.name}`);
|
|
244
270
|
}
|
|
245
271
|
}
|
|
246
|
-
|
|
247
|
-
log(
|
|
248
|
-
` ${loaded.sockets.length} socket(s) compiled but not served — sockets need hosted Starbase.`,
|
|
249
|
-
);
|
|
272
|
+
for (const socket of loaded.sockets) {
|
|
273
|
+
log(` WS ${socket.path}`);
|
|
250
274
|
}
|
|
251
275
|
if (previous) {
|
|
252
276
|
await previous.pool.end().catch(() => undefined);
|
|
@@ -275,6 +299,7 @@ export async function main(args: string[]): Promise<void> {
|
|
|
275
299
|
brokerBaseUrl: starbaseUrl(),
|
|
276
300
|
googleClientId: env.GOOGLE_CLIENT_ID,
|
|
277
301
|
googleClientSecret: env.GOOGLE_CLIENT_SECRET,
|
|
302
|
+
storageDir,
|
|
278
303
|
},
|
|
279
304
|
() => current,
|
|
280
305
|
);
|
|
@@ -295,6 +320,7 @@ export async function main(args: string[]): Promise<void> {
|
|
|
295
320
|
`Postgres ${postgres.host}:${postgres.port}/${postgres.database} (--database-url)`,
|
|
296
321
|
);
|
|
297
322
|
}
|
|
323
|
+
console.log(`Storage ${relative(project.root, storageDir) || storageDir}`);
|
|
298
324
|
console.log(`API ${apiUrl}`);
|
|
299
325
|
console.log(`Swagger ${apiUrl}/_robodev/docs`);
|
|
300
326
|
console.log(`Inspect ${apiUrl}/_robodev/logs`);
|
|
@@ -352,6 +378,7 @@ export async function main(args: string[]): Promise<void> {
|
|
|
352
378
|
async function shutdown(code: number): Promise<void> {
|
|
353
379
|
if (closing) return;
|
|
354
380
|
closing = true;
|
|
381
|
+
closeProjectSockets(DEV_PROJECT_ID);
|
|
355
382
|
await engine.stop();
|
|
356
383
|
for (const watcher of watchers) watcher.close();
|
|
357
384
|
fe?.kill("SIGTERM");
|