robodev 0.31.0 → 0.32.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robodev",
3
- "version": "0.31.0",
3
+ "version": "0.32.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",
@@ -25,8 +25,8 @@
25
25
  "embedded-postgres": "17.10.0-beta.17",
26
26
  "fastify": "^5.12.1",
27
27
  "tsx": "^4.20.5",
28
- "@robodev-ai/runtime": "0.1.0",
29
- "@robodev-ai/sdk": "0.10.0"
28
+ "@robodev-ai/runtime": "0.2.0",
29
+ "@robodev-ai/sdk": "0.11.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^24.3.0",
@@ -1,10 +1,10 @@
1
1
  import type { RouteClients } from "@robodev-ai/runtime";
2
- import type { EmailClient, LlmClient } from "@robodev-ai/sdk";
2
+ import type { EmailClient, JobEnqueueInput, LlmClient } from "@robodev-ai/sdk";
3
3
 
4
4
  /**
5
5
  * The `ctx.*` clients `robodev dev` can offer offline. `email` prints to the terminal;
6
- * everything that needs Starbase infrastructure fails with a message that says so rather
7
- * than silently doing nothing.
6
+ * `jobs` enqueues onto the local engine; everything else that needs Starbase
7
+ * infrastructure fails with a message that says so rather than silently doing nothing.
8
8
  */
9
9
 
10
10
  function unavailable(feature: string): never {
@@ -36,6 +36,7 @@ function offlineLlmClient(): LlmClient {
36
36
  export function devRouteClients(input: {
37
37
  env: Record<string, string>;
38
38
  log: (line: string) => void;
39
+ enqueue: (job: JobEnqueueInput) => Promise<{ id: string }>;
39
40
  }): RouteClients {
40
41
  return {
41
42
  email: consoleEmailClient(input.log),
@@ -55,7 +56,7 @@ export function devRouteClients(input: {
55
56
  list: () => unavailable("storage"),
56
57
  },
57
58
  push: { send: () => unavailable("push") },
58
- jobs: { enqueue: () => unavailable("jobs") },
59
+ jobs: { enqueue: input.enqueue },
59
60
  sockets: {
60
61
  send: () => unavailable("sockets"),
61
62
  broadcast: () => unavailable("sockets"),
package/src/dev-server.ts CHANGED
@@ -13,7 +13,8 @@ import {
13
13
  type AuthQueryable,
14
14
  type RuntimeRoute,
15
15
  } from "@robodev-ai/runtime";
16
- import type { RobodevDb } from "@robodev-ai/sdk";
16
+ import type { JobEnqueueInput, RobodevDb } from "@robodev-ai/sdk";
17
+ import type { RuntimeJob } from "@robodev-ai/runtime";
17
18
  import { devRouteClients } from "./dev-clients.js";
18
19
 
19
20
  /** One compiled generation of the project. Swapped wholesale on reload. */
@@ -21,6 +22,7 @@ export type DevRuntime = {
21
22
  db: RobodevDb;
22
23
  pool: AuthQueryable & { end: () => Promise<void> };
23
24
  routes: RuntimeRoute[];
25
+ jobs: RuntimeJob[];
24
26
  openapi: Record<string, unknown>;
25
27
  };
26
28
 
@@ -30,6 +32,7 @@ export type DevServerOptions = {
30
32
  jwtSecret: string;
31
33
  env: Record<string, string>;
32
34
  log: (line: string) => void;
35
+ enqueue: (job: JobEnqueueInput) => Promise<{ id: string }>;
33
36
  };
34
37
 
35
38
  const SWAGGER_PATHS = new Set(["/_robodev/docs", "/_robodev/swagger", "/docs", "/swagger"]);
@@ -181,7 +184,8 @@ export async function startDevServer(
181
184
  { projectId: options.projectId, jwtSecret: options.jwtSecret },
182
185
  authorization,
183
186
  ),
184
- clients: () => devRouteClients({ env: options.env, log: options.log }),
187
+ clients: () =>
188
+ devRouteClients({ env: options.env, log: options.log, enqueue: options.enqueue }),
185
189
  });
186
190
 
187
191
  if (outcome.kind === "error") {
package/src/dev.ts CHANGED
@@ -6,9 +6,12 @@ import { spawn, type ChildProcess } from "node:child_process";
6
6
  import {
7
7
  apiBundleExternals,
8
8
  applyPlan,
9
+ assertJobCrons,
9
10
  buildPlan,
10
11
  compileApiTree,
12
+ createLocalJobsEngine,
11
13
  ensureAuthSchema,
14
+ ensureJobsSchema,
12
15
  isEsbuildFailure,
13
16
  loadModules,
14
17
  mapEsbuildErrors,
@@ -26,6 +29,7 @@ import {
26
29
  type DevProject,
27
30
  } from "./dev-project.js";
28
31
  import { externalDevPostgres, startDevPostgres, type DevPostgres } from "./dev-postgres.js";
32
+ import { devRouteClients } from "./dev-clients.js";
29
33
  import { buildDevOpenApi, startDevServer, type DevRuntime } from "./dev-server.js";
30
34
 
31
35
  export type DevArgs = {
@@ -154,6 +158,15 @@ export async function main(args: string[]): Promise<void> {
154
158
  let closing = false;
155
159
 
156
160
  const log = (line: string) => console.log(line);
161
+ const engine = createLocalJobsEngine({
162
+ query: (sql, values) => {
163
+ if (!current) throw new Error("jobs engine not ready");
164
+ return current.pool.query(sql, values);
165
+ },
166
+ getGeneration: () => current,
167
+ log,
168
+ clients: () => devRouteClients({ env, log, enqueue: (input) => engine.enqueue(input) }),
169
+ });
157
170
 
158
171
  async function compileAndActivate(input: { syncSchema: boolean }): Promise<void> {
159
172
  const files = await collectBackendFiles(project.backendDir);
@@ -179,6 +192,7 @@ export async function main(args: string[]): Promise<void> {
179
192
  }
180
193
 
181
194
  const loaded = await loadModules(distDir, files);
195
+ assertJobCrons(loaded.jobs);
182
196
  // Any edit to database.ts can change columns, not just tables, so compare its source.
183
197
  const schemaSource = files.find((file) => file.path === "database.ts")?.content ?? "";
184
198
 
@@ -200,31 +214,36 @@ export async function main(args: string[]): Promise<void> {
200
214
  const { db, pool } = createDb(postgres.connectionString, loaded.database.tables);
201
215
  await pool.query("SELECT 1");
202
216
  await ensureAuthSchema(pool);
217
+ await ensureJobsSchema(pool);
203
218
 
204
219
  const previous = current;
205
220
  current = {
206
221
  db,
207
222
  pool,
208
223
  routes: loaded.routes,
224
+ jobs: loaded.jobs,
209
225
  openapi: buildDevOpenApi(loaded.database.name, loaded.routes),
210
226
  };
211
- if (previous) {
212
- await previous.pool.end().catch(() => undefined);
213
- }
227
+ await engine.reconcile(loaded.jobs);
214
228
 
215
229
  for (const route of loaded.routes) {
216
230
  console.log(` ${route.method.toUpperCase()} ${route.path}`);
217
231
  }
218
- if (loaded.jobs.length > 0) {
219
- console.log(
220
- ` ${loaded.jobs.length} job(s) compiled but not run — jobs need hosted Starbase.`,
221
- );
232
+ for (const job of loaded.jobs) {
233
+ if (job.def.cron) {
234
+ console.log(` job ${job.name} (cron ${job.def.cron})`);
235
+ } else {
236
+ console.log(` job ${job.name}`);
237
+ }
222
238
  }
223
239
  if (loaded.sockets.length > 0) {
224
240
  console.log(
225
241
  ` ${loaded.sockets.length} socket(s) compiled but not served — sockets need hosted Starbase.`,
226
242
  );
227
243
  }
244
+ if (previous) {
245
+ await previous.pool.end().catch(() => undefined);
246
+ }
228
247
  }
229
248
 
230
249
  async function releaseDatabase(): Promise<void> {
@@ -235,8 +254,16 @@ export async function main(args: string[]): Promise<void> {
235
254
  let server: Awaited<ReturnType<typeof startDevServer>>;
236
255
  try {
237
256
  await compileAndActivate({ syncSchema: true });
257
+ await engine.start();
238
258
  server = await startDevServer(
239
- { port: parsed.port, projectId: DEV_PROJECT_ID, jwtSecret: DEV_JWT_SECRET, env, log },
259
+ {
260
+ port: parsed.port,
261
+ projectId: DEV_PROJECT_ID,
262
+ jwtSecret: DEV_JWT_SECRET,
263
+ env,
264
+ log,
265
+ enqueue: (input) => engine.enqueue(input),
266
+ },
240
267
  () => current,
241
268
  );
242
269
  } catch (error) {
@@ -312,6 +339,7 @@ export async function main(args: string[]): Promise<void> {
312
339
  async function shutdown(code: number): Promise<void> {
313
340
  if (closing) return;
314
341
  closing = true;
342
+ await engine.stop();
315
343
  for (const watcher of watchers) watcher.close();
316
344
  fe?.kill("SIGTERM");
317
345
  await server.close().catch(() => undefined);
@@ -9,10 +9,10 @@
9
9
  "rules:gen": "rulesync generate"
10
10
  },
11
11
  "dependencies": {
12
- "@robodev-ai/sdk": "^0.10.0"
12
+ "@robodev-ai/sdk": "^0.11.0"
13
13
  },
14
14
  "devDependencies": {
15
- "robodev": "^0.31.0",
15
+ "robodev": "^0.32.0",
16
16
  "rulesync": "^8.18.0",
17
17
  "typescript": "^5.9.2"
18
18
  }
@@ -8,10 +8,10 @@
8
8
  "deploy": "robodev deploy"
9
9
  },
10
10
  "dependencies": {
11
- "@robodev-ai/sdk": "^0.10.0"
11
+ "@robodev-ai/sdk": "^0.11.0"
12
12
  },
13
13
  "devDependencies": {
14
- "robodev": "^0.31.0",
14
+ "robodev": "^0.32.0",
15
15
  "typescript": "^5.9.2"
16
16
  }
17
17
  }
@@ -8,10 +8,10 @@
8
8
  "deploy": "robodev deploy"
9
9
  },
10
10
  "dependencies": {
11
- "@robodev-ai/sdk": "^0.10.0"
11
+ "@robodev-ai/sdk": "^0.11.0"
12
12
  },
13
13
  "devDependencies": {
14
- "robodev": "^0.31.0",
14
+ "robodev": "^0.32.0",
15
15
  "typescript": "^5.9.2"
16
16
  }
17
17
  }
@@ -9,10 +9,10 @@
9
9
  "rules:gen": "rulesync generate"
10
10
  },
11
11
  "dependencies": {
12
- "@robodev-ai/sdk": "^0.10.0"
12
+ "@robodev-ai/sdk": "^0.11.0"
13
13
  },
14
14
  "devDependencies": {
15
- "robodev": "^0.31.0",
15
+ "robodev": "^0.32.0",
16
16
  "rulesync": "^8.18.0",
17
17
  "typescript": "^5.9.2"
18
18
  }
@@ -9,10 +9,10 @@
9
9
  "rules:gen": "rulesync generate"
10
10
  },
11
11
  "dependencies": {
12
- "@robodev-ai/sdk": "^0.10.0"
12
+ "@robodev-ai/sdk": "^0.11.0"
13
13
  },
14
14
  "devDependencies": {
15
- "robodev": "^0.31.0",
15
+ "robodev": "^0.32.0",
16
16
  "rulesync": "^8.18.0",
17
17
  "typescript": "^5.9.2"
18
18
  }