robodev 0.29.0 → 0.31.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.
@@ -32,27 +32,54 @@ test("resolveFrontendPaths keeps outDir inside the project", () => {
32
32
  );
33
33
  });
34
34
 
35
- test("mapFrontendOutDir remaps text files and skips binaries", async () => {
35
+ test("mapFrontendOutDir remaps text files and includes public binaries", async () => {
36
36
  const out = await mkdtemp(join(tmpdir(), "rd-out-"));
37
37
  try {
38
38
  await writeFile(join(out, "index.html"), "<html><body>app</body></html>");
39
39
  await mkdir(join(out, "assets"), { recursive: true });
40
40
  await writeFile(join(out, "assets/index-abc.js"), "console.log(1)");
41
41
  await writeFile(join(out, "assets/index-abc.css"), "body{color:red}");
42
- await writeFile(join(out, "assets/font.woff2"), "binary-font");
43
- await writeFile(join(out, "icon.png"), "binary-png");
44
- const { files, skippedBinaries } = await mapFrontendOutDir(out);
42
+ await writeFile(join(out, "assets/font.woff2"), Buffer.from("binary-font"));
43
+ await writeFile(join(out, "icon.png"), Buffer.from("binary-png"));
44
+ const files = await mapFrontendOutDir(out);
45
45
  const paths = new Set(files.map((file) => file.path));
46
46
  assert.ok(paths.has("index.html"));
47
47
  assert.ok(paths.has("public/assets/index-abc.js"));
48
48
  assert.ok(paths.has("public/assets/index-abc.css"));
49
- assert.ok(!paths.has("public/assets/font.woff2"));
50
- assert.ok(!paths.has("public/icon.png"));
51
- assert.equal(skippedBinaries, true);
49
+ assert.ok(paths.has("public/assets/font.woff2"));
50
+ assert.ok(paths.has("public/icon.png"));
52
51
  assert.equal(
53
52
  files.find((file) => file.path === "index.html")?.content,
54
53
  "<html><body>app</body></html>",
55
54
  );
55
+ assert.equal(
56
+ files.find((file) => file.path === "public/assets/font.woff2")?.content,
57
+ Buffer.from("binary-font").toString("base64"),
58
+ );
59
+ assert.equal(
60
+ files.find((file) => file.path === "public/icon.png")?.content,
61
+ Buffer.from("binary-png").toString("base64"),
62
+ );
63
+ } finally {
64
+ await rm(out, { recursive: true, force: true });
65
+ }
66
+ });
67
+
68
+ test("mapFrontendOutDir throws when dist still has denied files", async () => {
69
+ const out = await mkdtemp(join(tmpdir(), "rd-out-skip-"));
70
+ try {
71
+ await writeFile(join(out, "index.html"), "<html></html>");
72
+ await mkdir(join(out, "assets"), { recursive: true });
73
+ await writeFile(join(out, "assets/app.wasm"), Buffer.from("wasm"));
74
+ await writeFile(join(out, "clip.mp4"), Buffer.from("mp4"));
75
+ await assert.rejects(
76
+ () => mapFrontendOutDir(out),
77
+ (err: unknown) =>
78
+ err instanceof Error &&
79
+ /cannot be uploaded/.test(err.message) &&
80
+ err.message.includes("public/assets/app.wasm") &&
81
+ err.message.includes("public/clip.mp4"),
82
+ );
56
83
  } finally {
57
84
  await rm(out, { recursive: true, force: true });
58
85
  }
@@ -2,7 +2,12 @@ import { execFile, spawn } from "node:child_process";
2
2
  import { readdir, readFile, stat } from "node:fs/promises";
3
3
  import { isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { promisify } from "node:util";
5
- import { assertCollectedFile, type CollectedFile, shouldCollectPath } from "./collect-files.js";
5
+ import {
6
+ assertCollectedFile,
7
+ type CollectedFile,
8
+ readCollectedContent,
9
+ shouldCollectPath,
10
+ } from "./collect-files.js";
6
11
  import { isFrontendConfig, normalizeFrontendConfig, type FrontendConfig } from "./config.js";
7
12
 
8
13
  const execFileAsync = promisify(execFile);
@@ -132,12 +137,9 @@ function mappedDeployPath(relFromOutDir: string): string {
132
137
  return `public/${rel}`;
133
138
  }
134
139
 
135
- export async function mapFrontendOutDir(outDirAbs: string): Promise<{
136
- files: CollectedFile[];
137
- skippedBinaries: boolean;
138
- }> {
140
+ export async function mapFrontendOutDir(outDirAbs: string): Promise<CollectedFile[]> {
139
141
  const files: CollectedFile[] = [];
140
- let skippedBinaries = false;
142
+ const skipped: string[] = [];
141
143
 
142
144
  async function walk(dir: string): Promise<void> {
143
145
  let entries;
@@ -156,10 +158,10 @@ export async function mapFrontendOutDir(outDirAbs: string): Promise<{
156
158
  const rel = relative(outDirAbs, abs).replaceAll("\\", "/");
157
159
  const deployPath = mappedDeployPath(rel);
158
160
  if (!shouldCollectPath(deployPath)) {
159
- skippedBinaries = true;
161
+ skipped.push(deployPath);
160
162
  continue;
161
163
  }
162
- const content = await readFile(abs, "utf8");
164
+ const content = await readCollectedContent(abs, deployPath);
163
165
  const file = { path: deployPath, content };
164
166
  assertCollectedFile(file);
165
167
  files.push(file);
@@ -167,7 +169,24 @@ export async function mapFrontendOutDir(outDirAbs: string): Promise<{
167
169
  }
168
170
 
169
171
  await walk(outDirAbs);
170
- return { files, skippedBinaries };
172
+ if (skipped.length > 0) {
173
+ throw new Error(
174
+ `Frontend build produced files that cannot be uploaded: ${skipped.sort().join(", ")}`,
175
+ );
176
+ }
177
+ return files;
178
+ }
179
+
180
+ export async function frontendOutIndexExists(
181
+ projectRoot: string,
182
+ frontend: FrontendConfig,
183
+ ): Promise<boolean> {
184
+ try {
185
+ const { outAbs } = resolveFrontendPaths(projectRoot, frontend);
186
+ return (await stat(join(outAbs, "index.html"))).isFile();
187
+ } catch {
188
+ return false;
189
+ }
171
190
  }
172
191
 
173
192
  export async function buildAndCollectFrontend(
@@ -199,14 +218,9 @@ export async function buildAndCollectFrontend(
199
218
  throw new Error(`Frontend build did not produce ${join(outAbs, "index.html")}`);
200
219
  }
201
220
 
202
- const { files, skippedBinaries } = await mapFrontendOutDir(outAbs);
221
+ const files = await mapFrontendOutDir(outAbs);
203
222
  if (!files.some((file) => file.path === "index.html")) {
204
223
  throw new Error(`Frontend build did not produce ${join(outAbs, "index.html")}`);
205
224
  }
206
- if (skippedBinaries) {
207
- console.log(
208
- "Skipped binary frontend assets (fonts/images). Hosted UI may use system font fallback.",
209
- );
210
- }
211
225
  return files;
212
226
  }
package/src/index.ts CHANGED
@@ -23,7 +23,11 @@ import {
23
23
  } from "./config.js";
24
24
  import { waitUntilLive } from "./wait-until-live.js";
25
25
  import { collectFiles, mergeCollectedFiles, type CollectedFile } from "./collect-files.js";
26
- import { buildAndCollectFrontend, commandAvailable } from "./frontend-build.js";
26
+ import {
27
+ buildAndCollectFrontend,
28
+ commandAvailable,
29
+ frontendOutIndexExists,
30
+ } from "./frontend-build.js";
27
31
  import {
28
32
  emitStarter,
29
33
  loadCatalog,
@@ -49,6 +53,15 @@ Commands:
49
53
  create [path] [--starter <id>] [--empty] [--org <id>] Create a project, scaffold a starter, and deploy
50
54
  link [projectId] Write robodev/.robodev in the current folder
51
55
  deploy [--force] Deploy schema, APIs, jobs, and frontend (local Vite build when .robodev frontend is set)
56
+ dev [options] Run this project's APIs locally on an embedded Postgres, no Starbase needed
57
+
58
+ dev options:
59
+ --port <n> API port (default 4000)
60
+ --db-port <n> Postgres port (default 0 = pick a free one)
61
+ --database-url <url> Use an existing Postgres instead of the embedded one
62
+ --force Apply destructive schema changes
63
+ --no-watch Compile once and serve, without watching files
64
+ --with-fe Also start apps/fe with VITE_API_URL pointed at the local API
52
65
  `);
53
66
  process.exit(1);
54
67
  }
@@ -538,6 +551,9 @@ async function runCreate(args: string[]): Promise<void> {
538
551
  });
539
552
  feHosted = true;
540
553
  } catch (error) {
554
+ if (await frontendOutIndexExists(root, defaultFrontendConfig())) {
555
+ throw error;
556
+ }
541
557
  console.log(error instanceof Error ? error.message : error);
542
558
  console.log("Frontend build failed. Deploying schema and APIs only.");
543
559
  skipFrontendBuild = true;
@@ -618,6 +634,11 @@ async function main(): Promise<void> {
618
634
  case "deploy":
619
635
  await runDeploy(args.includes("--force"));
620
636
  break;
637
+ case "dev": {
638
+ const { main: runDev } = await import("./dev.js");
639
+ await runDev(args);
640
+ break;
641
+ }
621
642
  default:
622
643
  usage();
623
644
  }
@@ -13,7 +13,7 @@ cursor:
13
13
 
14
14
  This project is a Vite React frontend in `apps/fe` plus a Robodev backend under **`robodev/`** (`robodev/database.ts` + `robodev/api/**/*.ts` + `robodev/hooks/auth.ts` using `@robodev-ai/sdk`). There is no Expo / mobile app in this starter. End-user auth is reserved on the project host at `/api/user/*`. There is no in-browser fake backend.
15
15
 
16
- `robodev deploy` from this root uploads schema and APIs. It does not host the Povio SPA. Run the UI with bun in `apps/fe`.
16
+ `robodev deploy` from this root uploads schema, APIs, and (when `.robodev` has a `frontend` block) the local Vite build as a hosted static SPA. Use bun in `apps/fe` to iterate locally.
17
17
 
18
18
  Shared AI rules and skills are authored in `.rulesync/`. Generated files for Codex, Claude Code, and Cursor are local outputs and should be regenerated with `bun rules:gen`.
19
19
 
@@ -5,7 +5,7 @@
5
5
  "description": "Auth chat starter frontend",
6
6
  "type": "module",
7
7
  "scripts": {
8
- "dev": "bunx --bun vite",
8
+ "dev": "vite --host",
9
9
  "build": "bun run openapi:gen && bunx --bun vite build",
10
10
  "clean": "rimraf dist src/openapi",
11
11
  "lint": "oxlint --type-aware . ../../robodev/api ../../robodev/database.ts --fix",
@@ -4,14 +4,15 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
+ "dev": "robodev dev --with-fe",
7
8
  "deploy": "robodev deploy",
8
9
  "rules:gen": "rulesync generate"
9
10
  },
10
11
  "dependencies": {
11
- "@robodev-ai/sdk": "^0.8.0"
12
+ "@robodev-ai/sdk": "^0.10.0"
12
13
  },
13
14
  "devDependencies": {
14
- "robodev": "^0.29.0",
15
+ "robodev": "^0.31.0",
15
16
  "rulesync": "^8.18.0",
16
17
  "typescript": "^5.9.2"
17
18
  }
@@ -4,13 +4,14 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
+ "dev": "robodev dev",
7
8
  "deploy": "robodev deploy"
8
9
  },
9
10
  "dependencies": {
10
- "@robodev-ai/sdk": "^0.8.0"
11
+ "@robodev-ai/sdk": "^0.10.0"
11
12
  },
12
13
  "devDependencies": {
13
- "robodev": "^0.29.0",
14
+ "robodev": "^0.31.0",
14
15
  "typescript": "^5.9.2"
15
16
  }
16
17
  }
@@ -4,13 +4,14 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
+ "dev": "robodev dev",
7
8
  "deploy": "robodev deploy"
8
9
  },
9
10
  "dependencies": {
10
- "@robodev-ai/sdk": "^0.8.0"
11
+ "@robodev-ai/sdk": "^0.10.0"
11
12
  },
12
13
  "devDependencies": {
13
- "robodev": "^0.29.0",
14
+ "robodev": "^0.31.0",
14
15
  "typescript": "^5.9.2"
15
16
  }
16
17
  }
@@ -13,7 +13,7 @@ cursor:
13
13
 
14
14
  This project is a Vite React frontend in `apps/fe` plus a Robodev backend under **`robodev/`** (`robodev/database.ts` + `robodev/api/**/*.ts` using `@robodev-ai/sdk`). There is no Expo / mobile app in this starter. End-user auth is reserved on the project host at `/api/user/*`. There is no in-browser fake backend.
15
15
 
16
- `robodev deploy` from this root uploads schema and APIs. It does not host the Povio SPA. Run the UI with bun in `apps/fe`.
16
+ `robodev deploy` from this root uploads schema, APIs, and (when `.robodev` has a `frontend` block) the local Vite build as a hosted static SPA. Use bun in `apps/fe` to iterate locally.
17
17
 
18
18
  Shared AI rules and skills are authored in `.rulesync/`. Generated files for Codex, Claude Code, and Cursor are local outputs and should be regenerated with `bun rules:gen`.
19
19
 
@@ -5,7 +5,7 @@
5
5
  "description": "Marketplace starter frontend",
6
6
  "type": "module",
7
7
  "scripts": {
8
- "dev": "bunx --bun vite",
8
+ "dev": "vite --host",
9
9
  "build": "bun run openapi:gen && bunx --bun vite build",
10
10
  "clean": "rimraf dist src/openapi",
11
11
  "lint": "oxlint --type-aware . ../../robodev/api ../../robodev/database.ts --fix",
@@ -4,14 +4,15 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
+ "dev": "robodev dev --with-fe",
7
8
  "deploy": "robodev deploy",
8
9
  "rules:gen": "rulesync generate"
9
10
  },
10
11
  "dependencies": {
11
- "@robodev-ai/sdk": "^0.8.0"
12
+ "@robodev-ai/sdk": "^0.10.0"
12
13
  },
13
14
  "devDependencies": {
14
- "robodev": "^0.29.0",
15
+ "robodev": "^0.31.0",
15
16
  "rulesync": "^8.18.0",
16
17
  "typescript": "^5.9.2"
17
18
  }
@@ -13,7 +13,7 @@ cursor:
13
13
 
14
14
  This project is a Vite React frontend in `apps/fe` plus a Robodev backend under **`robodev/`** (`robodev/database.ts` + `robodev/api/**/*.ts` using `@robodev-ai/sdk`). There is no Expo / mobile app in this starter. End-user auth is reserved on the project host at `/api/user/*`. There is no in-browser fake backend.
15
15
 
16
- `robodev deploy` from this root uploads schema and APIs. It does not host the Povio SPA. Run the UI with bun in `apps/fe`.
16
+ `robodev deploy` from this root uploads schema, APIs, and (when `.robodev` has a `frontend` block) the local Vite build as a hosted static SPA. Use bun in `apps/fe` to iterate locally.
17
17
 
18
18
  Shared AI rules and skills are authored in `.rulesync/`. Generated files for Codex, Claude Code, and Cursor are local outputs and should be regenerated with `bun rules:gen`.
19
19
 
@@ -5,7 +5,7 @@
5
5
  "description": "Space starter frontend",
6
6
  "type": "module",
7
7
  "scripts": {
8
- "dev": "bunx --bun vite",
8
+ "dev": "vite --host",
9
9
  "build": "bun run openapi:gen && bunx --bun vite build",
10
10
  "clean": "rimraf dist src/openapi",
11
11
  "lint": "oxlint --type-aware . ../../robodev/api ../../robodev/database.ts --fix",
@@ -4,14 +4,15 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
+ "dev": "robodev dev --with-fe",
7
8
  "deploy": "robodev deploy",
8
9
  "rules:gen": "rulesync generate"
9
10
  },
10
11
  "dependencies": {
11
- "@robodev-ai/sdk": "^0.8.0"
12
+ "@robodev-ai/sdk": "^0.10.0"
12
13
  },
13
14
  "devDependencies": {
14
- "robodev": "^0.29.0",
15
+ "robodev": "^0.31.0",
15
16
  "rulesync": "^8.18.0",
16
17
  "typescript": "^5.9.2"
17
18
  }