robodev 0.33.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 +280 -11
- package/src/dev.ts +37 -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
|
|
|
@@ -35,6 +52,11 @@ export type DevServerOptions = {
|
|
|
35
52
|
log: (line: string) => void;
|
|
36
53
|
enqueue: (job: JobEnqueueInput) => Promise<{ id: string }>;
|
|
37
54
|
inspect: DevInspectStore;
|
|
55
|
+
listenOrigin: string;
|
|
56
|
+
brokerBaseUrl: string;
|
|
57
|
+
googleClientId?: string;
|
|
58
|
+
googleClientSecret?: string;
|
|
59
|
+
storageDir: string;
|
|
38
60
|
};
|
|
39
61
|
|
|
40
62
|
const SWAGGER_PATHS = new Set(["/_robodev/docs", "/_robodev/swagger", "/docs", "/swagger"]);
|
|
@@ -53,6 +75,238 @@ function rawBodyOf(request: FastifyRequest): Buffer | undefined {
|
|
|
53
75
|
return (request as FastifyRequest & { rawBody?: Buffer }).rawBody;
|
|
54
76
|
}
|
|
55
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
|
+
|
|
56
310
|
/**
|
|
57
311
|
* A single-project HTTP host. Unlike the hosted project host there is no `Host` lookup
|
|
58
312
|
* and no static SPA serving (local Vite does that). Fastify stays `logger: false`;
|
|
@@ -70,6 +324,10 @@ export async function startDevServer(
|
|
|
70
324
|
credentials: true,
|
|
71
325
|
methods: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
72
326
|
});
|
|
327
|
+
await app.register(websocket);
|
|
328
|
+
app.get("/sockets/*", { websocket: true }, (socket, request) => {
|
|
329
|
+
void handleLocalSocket(socket, request, options, getRuntime);
|
|
330
|
+
});
|
|
73
331
|
|
|
74
332
|
app.addContentTypeParser("application/json", { parseAs: "string" }, (_request, body, done) => {
|
|
75
333
|
if (!body) {
|
|
@@ -99,13 +357,16 @@ export async function startDevServer(
|
|
|
99
357
|
);
|
|
100
358
|
|
|
101
359
|
// `rawBody` handlers and multipart parsing need the untouched bytes.
|
|
102
|
-
app.addHook("preParsing", async (
|
|
360
|
+
app.addHook("preParsing", async (request, _reply, payload) => {
|
|
361
|
+
if (String(request.headers.upgrade ?? "").toLowerCase() === "websocket") {
|
|
362
|
+
return payload;
|
|
363
|
+
}
|
|
103
364
|
const chunks: Buffer[] = [];
|
|
104
365
|
for await (const chunk of payload) {
|
|
105
366
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
106
367
|
}
|
|
107
368
|
const raw = Buffer.concat(chunks);
|
|
108
|
-
(
|
|
369
|
+
(request as FastifyRequest & { rawBody?: Buffer }).rawBody = raw;
|
|
109
370
|
return Readable.from(raw);
|
|
110
371
|
});
|
|
111
372
|
|
|
@@ -196,12 +457,25 @@ export async function startDevServer(
|
|
|
196
457
|
options.inspect.record({ kind: "auth_code", type, to, code });
|
|
197
458
|
options.log(`${type} code for ${to}: ${code}`);
|
|
198
459
|
},
|
|
460
|
+
listenOrigin: options.listenOrigin,
|
|
461
|
+
brokerBaseUrl: options.brokerBaseUrl,
|
|
462
|
+
googleClientId: options.googleClientId,
|
|
463
|
+
googleClientSecret: options.googleClientSecret,
|
|
199
464
|
},
|
|
200
465
|
);
|
|
466
|
+
if (result.headers?.Location) {
|
|
467
|
+
await reply.redirect(result.headers.Location);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
201
470
|
await reply.code(result.status).send(result.body);
|
|
202
471
|
return;
|
|
203
472
|
}
|
|
204
473
|
|
|
474
|
+
if (request.method.toUpperCase() === "GET" && isReservedStoragePath(pathname)) {
|
|
475
|
+
await serveLocalStorage(request, reply, options, getRuntime, pathname);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
205
479
|
if (!matchProjectRoute(pathname, method, runtime.routes)) {
|
|
206
480
|
await reply.code(404).send({
|
|
207
481
|
error: "not_found",
|
|
@@ -226,13 +500,8 @@ export async function startDevServer(
|
|
|
226
500
|
{ projectId: options.projectId, jwtSecret: options.jwtSecret },
|
|
227
501
|
authorization,
|
|
228
502
|
),
|
|
229
|
-
clients: () =>
|
|
230
|
-
|
|
231
|
-
env: options.env,
|
|
232
|
-
log: options.log,
|
|
233
|
-
enqueue: options.enqueue,
|
|
234
|
-
inspect: options.inspect,
|
|
235
|
-
}),
|
|
503
|
+
clients: () => routeClients(options, getRuntime),
|
|
504
|
+
mapError: (error) => codedErrorBody(error),
|
|
236
505
|
});
|
|
237
506
|
|
|
238
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,
|
|
@@ -29,6 +33,7 @@ import {
|
|
|
29
33
|
type DevProject,
|
|
30
34
|
} from "./dev-project.js";
|
|
31
35
|
import { externalDevPostgres, startDevPostgres, type DevPostgres } from "./dev-postgres.js";
|
|
36
|
+
import { starbaseUrl } from "./config.js";
|
|
32
37
|
import { devRouteClients } from "./dev-clients.js";
|
|
33
38
|
import { createDevInspect } from "./dev-inspect.js";
|
|
34
39
|
import { buildDevOpenApi, startDevServer, type DevRuntime } from "./dev-server.js";
|
|
@@ -153,6 +158,8 @@ export async function main(args: string[]): Promise<void> {
|
|
|
153
158
|
const postgres: DevPostgres = parsed.databaseUrl
|
|
154
159
|
? externalDevPostgres(parsed.databaseUrl)
|
|
155
160
|
: await startDevPostgres({ dataDir: join(devDir, "pg"), port: parsed.dbPort, quiet: true });
|
|
161
|
+
const storageDir = join(devDir, "storage");
|
|
162
|
+
await mkdir(storageDir, { recursive: true });
|
|
156
163
|
|
|
157
164
|
let current: DevRuntime | null = null;
|
|
158
165
|
let lastSchemaSource = "";
|
|
@@ -171,7 +178,20 @@ export async function main(args: string[]): Promise<void> {
|
|
|
171
178
|
getGeneration: () => current,
|
|
172
179
|
log,
|
|
173
180
|
clients: () =>
|
|
174
|
-
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
|
+
}),
|
|
175
195
|
});
|
|
176
196
|
|
|
177
197
|
async function compileAndActivate(input: { syncSchema: boolean }): Promise<void> {
|
|
@@ -221,15 +241,22 @@ export async function main(args: string[]): Promise<void> {
|
|
|
221
241
|
await pool.query("SELECT 1");
|
|
222
242
|
await ensureAuthSchema(pool);
|
|
223
243
|
await ensureJobsSchema(pool);
|
|
244
|
+
await ensureStorageSchema(pool);
|
|
224
245
|
|
|
246
|
+
closeProjectSockets(DEV_PROJECT_ID, SOCKET_CLOSE_RESTART, "deployed");
|
|
225
247
|
const previous = current;
|
|
226
248
|
current = {
|
|
227
249
|
db,
|
|
228
250
|
pool,
|
|
229
251
|
routes: loaded.routes,
|
|
230
252
|
jobs: loaded.jobs,
|
|
253
|
+
sockets: loaded.sockets,
|
|
231
254
|
openapi: buildDevOpenApi(loaded.database.name, loaded.routes),
|
|
232
255
|
};
|
|
256
|
+
setProjectSocketNames(
|
|
257
|
+
DEV_PROJECT_ID,
|
|
258
|
+
loaded.sockets.map((socket) => socket.name),
|
|
259
|
+
);
|
|
233
260
|
await engine.reconcile(loaded.jobs);
|
|
234
261
|
|
|
235
262
|
for (const route of loaded.routes) {
|
|
@@ -242,10 +269,8 @@ export async function main(args: string[]): Promise<void> {
|
|
|
242
269
|
log(` job ${job.name}`);
|
|
243
270
|
}
|
|
244
271
|
}
|
|
245
|
-
|
|
246
|
-
log(
|
|
247
|
-
` ${loaded.sockets.length} socket(s) compiled but not served — sockets need hosted Starbase.`,
|
|
248
|
-
);
|
|
272
|
+
for (const socket of loaded.sockets) {
|
|
273
|
+
log(` WS ${socket.path}`);
|
|
249
274
|
}
|
|
250
275
|
if (previous) {
|
|
251
276
|
await previous.pool.end().catch(() => undefined);
|
|
@@ -270,6 +295,11 @@ export async function main(args: string[]): Promise<void> {
|
|
|
270
295
|
log,
|
|
271
296
|
inspect,
|
|
272
297
|
enqueue: (input) => engine.enqueue(input),
|
|
298
|
+
listenOrigin: `http://localhost:${parsed.port}`,
|
|
299
|
+
brokerBaseUrl: starbaseUrl(),
|
|
300
|
+
googleClientId: env.GOOGLE_CLIENT_ID,
|
|
301
|
+
googleClientSecret: env.GOOGLE_CLIENT_SECRET,
|
|
302
|
+
storageDir,
|
|
273
303
|
},
|
|
274
304
|
() => current,
|
|
275
305
|
);
|
|
@@ -290,6 +320,7 @@ export async function main(args: string[]): Promise<void> {
|
|
|
290
320
|
`Postgres ${postgres.host}:${postgres.port}/${postgres.database} (--database-url)`,
|
|
291
321
|
);
|
|
292
322
|
}
|
|
323
|
+
console.log(`Storage ${relative(project.root, storageDir) || storageDir}`);
|
|
293
324
|
console.log(`API ${apiUrl}`);
|
|
294
325
|
console.log(`Swagger ${apiUrl}/_robodev/docs`);
|
|
295
326
|
console.log(`Inspect ${apiUrl}/_robodev/logs`);
|
|
@@ -347,6 +378,7 @@ export async function main(args: string[]): Promise<void> {
|
|
|
347
378
|
async function shutdown(code: number): Promise<void> {
|
|
348
379
|
if (closing) return;
|
|
349
380
|
closing = true;
|
|
381
|
+
closeProjectSockets(DEV_PROJECT_ID);
|
|
350
382
|
await engine.stop();
|
|
351
383
|
for (const watcher of watchers) watcher.close();
|
|
352
384
|
fe?.kill("SIGTERM");
|