robodev 0.29.0 → 0.30.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 +1 -1
- package/src/collect-files.test.ts +16 -0
- package/src/collect-files.ts +45 -12
- package/src/frontend-build.test.ts +34 -7
- package/src/frontend-build.ts +29 -15
- package/src/index.ts +8 -1
- package/templates/auth-chat/.rulesync/rules/project-overview.md +1 -1
- package/templates/auth-chat/apps/fe/package.json +1 -1
- package/templates/auth-chat/package.json +2 -2
- package/templates/backend/package.json +2 -2
- package/templates/empty/package.json +2 -2
- package/templates/marketplace/.rulesync/rules/project-overview.md +1 -1
- package/templates/marketplace/apps/fe/package.json +1 -1
- package/templates/marketplace/package.json +2 -2
- package/templates/space/.rulesync/rules/project-overview.md +1 -1
- package/templates/space/apps/fe/package.json +1 -1
- package/templates/space/package.json +2 -2
package/package.json
CHANGED
|
@@ -57,6 +57,22 @@ test("collectFiles includes hosted sources and skips denied paths", async () =>
|
|
|
57
57
|
}
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
test("collectFiles includes public SPA binaries as base64", async () => {
|
|
61
|
+
const root = await mkdtemp(join(tmpdir(), "rd-cli-bin-"));
|
|
62
|
+
try {
|
|
63
|
+
await writeFile(join(root, "database.ts"), "export default {}");
|
|
64
|
+
await mkdir(join(root, "public/assets"), { recursive: true });
|
|
65
|
+
await writeFile(join(root, "public/assets/font.woff2"), Buffer.from("woff2-bytes"));
|
|
66
|
+
await writeFile(join(root, "public/icon.png"), Buffer.from("png-bytes"));
|
|
67
|
+
const files = await collectFiles(root);
|
|
68
|
+
const byPath = Object.fromEntries(files.map((file) => [file.path, file.content]));
|
|
69
|
+
assert.equal(byPath["public/assets/font.woff2"], Buffer.from("woff2-bytes").toString("base64"));
|
|
70
|
+
assert.equal(byPath["public/icon.png"], Buffer.from("png-bytes").toString("base64"));
|
|
71
|
+
} finally {
|
|
72
|
+
await rm(root, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
60
76
|
test("collectFiles still accepts a legacy root database.ts tree", async () => {
|
|
61
77
|
const root = await mkdtemp(join(tmpdir(), "rd-cli-"));
|
|
62
78
|
try {
|
package/src/collect-files.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
3
|
|
|
4
|
-
export const MAX_FILE_COUNT =
|
|
4
|
+
export const MAX_FILE_COUNT = 400;
|
|
5
5
|
export const MAX_FILE_BYTES = 1024 * 1024;
|
|
6
|
-
/** Hashed Vite JS from Tiny can exceed 1MB; keep API/source at 1MB. */
|
|
7
|
-
export const MAX_STATIC_FILE_BYTES =
|
|
8
|
-
export const MAX_TREE_BYTES =
|
|
6
|
+
/** Hashed Vite JS and SPA fonts from Tiny can exceed 1MB; keep API/source at 1MB. */
|
|
7
|
+
export const MAX_STATIC_FILE_BYTES = 8 * 1024 * 1024;
|
|
8
|
+
export const MAX_TREE_BYTES = 25 * 1024 * 1024;
|
|
9
9
|
export const MAX_PATH_LENGTH = 240;
|
|
10
10
|
export const MAX_PATH_DEPTH = 12;
|
|
11
11
|
|
|
@@ -33,6 +33,18 @@ const BINARY_EXTENSIONS = new Set([
|
|
|
33
33
|
".webm",
|
|
34
34
|
".mp3",
|
|
35
35
|
]);
|
|
36
|
+
const PUBLIC_BINARY_EXTENSIONS = new Set([
|
|
37
|
+
".woff",
|
|
38
|
+
".woff2",
|
|
39
|
+
".ttf",
|
|
40
|
+
".otf",
|
|
41
|
+
".png",
|
|
42
|
+
".jpg",
|
|
43
|
+
".jpeg",
|
|
44
|
+
".gif",
|
|
45
|
+
".webp",
|
|
46
|
+
".ico",
|
|
47
|
+
]);
|
|
36
48
|
const SRC_EXTENSIONS = new Set([
|
|
37
49
|
".ts",
|
|
38
50
|
".tsx",
|
|
@@ -102,10 +114,15 @@ function isDenied(path: string): boolean {
|
|
|
102
114
|
if (isRobodevLockfile(path)) return true;
|
|
103
115
|
const name = basename(path);
|
|
104
116
|
if (name.startsWith(".") && name !== ".gitignore") return true;
|
|
105
|
-
if (BINARY_EXTENSIONS.has(extname(path))) return true;
|
|
117
|
+
if (BINARY_EXTENSIONS.has(extname(path)) && !isPublicBinaryCollectedPath(path)) return true;
|
|
106
118
|
return false;
|
|
107
119
|
}
|
|
108
120
|
|
|
121
|
+
export function isPublicBinaryCollectedPath(path: string): boolean {
|
|
122
|
+
const normalized = normalize(path);
|
|
123
|
+
return normalized.startsWith("public/") && PUBLIC_BINARY_EXTENSIONS.has(extname(normalized));
|
|
124
|
+
}
|
|
125
|
+
|
|
109
126
|
function isBackendPath(path: string): boolean {
|
|
110
127
|
const inner = path.startsWith("robodev/") ? path.slice("robodev/".length) : path;
|
|
111
128
|
if (inner === "database.ts" && (path === "database.ts" || path === "robodev/database.ts")) {
|
|
@@ -127,7 +144,9 @@ function isAllowed(path: string): boolean {
|
|
|
127
144
|
if (path.startsWith("robodev/")) return false;
|
|
128
145
|
if (path === "index.html") return true;
|
|
129
146
|
if (path.startsWith("src/")) return SRC_EXTENSIONS.has(extname(path));
|
|
130
|
-
if (path.startsWith("public/"))
|
|
147
|
+
if (path.startsWith("public/")) {
|
|
148
|
+
return PUBLIC_EXTENSIONS.has(extname(path)) || PUBLIC_BINARY_EXTENSIONS.has(extname(path));
|
|
149
|
+
}
|
|
131
150
|
if (CONFIG_FILES.has(path) || CONFIG_FILES.has(basename(path))) return true;
|
|
132
151
|
if (/^tsconfig\..+\.json$/.test(basename(path)) && !path.includes("/")) return true;
|
|
133
152
|
if (!path.includes("/") && ROOT_STATIC_EXTENSIONS.has(extname(path))) return true;
|
|
@@ -177,6 +196,20 @@ function formatMaxBytes(bytes: number): string {
|
|
|
177
196
|
return `${Math.round(bytes / (1024 * 1024))}MB`;
|
|
178
197
|
}
|
|
179
198
|
|
|
199
|
+
export function collectedFileStoredBytes(file: CollectedFile): number {
|
|
200
|
+
if (isPublicBinaryCollectedPath(file.path)) {
|
|
201
|
+
return Buffer.from(file.content, "base64").byteLength;
|
|
202
|
+
}
|
|
203
|
+
return Buffer.byteLength(file.content, "utf8");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function readCollectedContent(abs: string, deployPath: string): Promise<string> {
|
|
207
|
+
if (isPublicBinaryCollectedPath(deployPath)) {
|
|
208
|
+
return (await readFile(abs)).toString("base64");
|
|
209
|
+
}
|
|
210
|
+
return readFile(abs, "utf8");
|
|
211
|
+
}
|
|
212
|
+
|
|
180
213
|
export function assertCollectedFile(file: CollectedFile): void {
|
|
181
214
|
const path = normalize(file.path);
|
|
182
215
|
if (
|
|
@@ -187,7 +220,7 @@ export function assertCollectedFile(file: CollectedFile): void {
|
|
|
187
220
|
) {
|
|
188
221
|
throw new Error(`Invalid file path: ${path}`);
|
|
189
222
|
}
|
|
190
|
-
const bytes =
|
|
223
|
+
const bytes = collectedFileStoredBytes(file);
|
|
191
224
|
const max = maxBytesForCollectedPath(path);
|
|
192
225
|
if (bytes > max) {
|
|
193
226
|
throw new Error(`File must be under ${formatMaxBytes(max)}: ${path}`);
|
|
@@ -203,7 +236,7 @@ export function assertCollectedLimits(files: CollectedFile[]): void {
|
|
|
203
236
|
assertCollectedFile(file);
|
|
204
237
|
totalBytes += Buffer.byteLength(file.content, "utf8");
|
|
205
238
|
if (totalBytes > MAX_TREE_BYTES) {
|
|
206
|
-
throw new Error(
|
|
239
|
+
throw new Error(`Deploy tree must be under ${formatMaxBytes(MAX_TREE_BYTES)}`);
|
|
207
240
|
}
|
|
208
241
|
}
|
|
209
242
|
}
|
|
@@ -251,15 +284,15 @@ export async function collectFiles(root: string): Promise<CollectedFile[]> {
|
|
|
251
284
|
if (path.length > MAX_PATH_LENGTH || path.split("/").length > MAX_PATH_DEPTH) {
|
|
252
285
|
throw new Error(`Invalid file path: ${path}`);
|
|
253
286
|
}
|
|
254
|
-
const content = await
|
|
255
|
-
const bytes =
|
|
287
|
+
const content = await readCollectedContent(abs, path);
|
|
288
|
+
const bytes = collectedFileStoredBytes({ path, content });
|
|
256
289
|
const max = maxBytesForCollectedPath(path);
|
|
257
290
|
if (bytes > max) {
|
|
258
291
|
throw new Error(`File must be under ${formatMaxBytes(max)}: ${path}`);
|
|
259
292
|
}
|
|
260
|
-
totalBytes +=
|
|
293
|
+
totalBytes += Buffer.byteLength(content, "utf8");
|
|
261
294
|
if (totalBytes > MAX_TREE_BYTES) {
|
|
262
|
-
throw new Error(
|
|
295
|
+
throw new Error(`Deploy tree must be under ${formatMaxBytes(MAX_TREE_BYTES)}`);
|
|
263
296
|
}
|
|
264
297
|
files.push({ path, content });
|
|
265
298
|
if (files.length > MAX_FILE_COUNT) {
|
|
@@ -32,27 +32,54 @@ test("resolveFrontendPaths keeps outDir inside the project", () => {
|
|
|
32
32
|
);
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
test("mapFrontendOutDir remaps text files and
|
|
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
|
|
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(
|
|
50
|
-
assert.ok(
|
|
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
|
}
|
package/src/frontend-build.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
161
|
+
skipped.push(deployPath);
|
|
160
162
|
continue;
|
|
161
163
|
}
|
|
162
|
-
const content = await
|
|
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
|
-
|
|
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
|
|
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 {
|
|
26
|
+
import {
|
|
27
|
+
buildAndCollectFrontend,
|
|
28
|
+
commandAvailable,
|
|
29
|
+
frontendOutIndexExists,
|
|
30
|
+
} from "./frontend-build.js";
|
|
27
31
|
import {
|
|
28
32
|
emitStarter,
|
|
29
33
|
loadCatalog,
|
|
@@ -538,6 +542,9 @@ async function runCreate(args: string[]): Promise<void> {
|
|
|
538
542
|
});
|
|
539
543
|
feHosted = true;
|
|
540
544
|
} catch (error) {
|
|
545
|
+
if (await frontendOutIndexExists(root, defaultFrontendConfig())) {
|
|
546
|
+
throw error;
|
|
547
|
+
}
|
|
541
548
|
console.log(error instanceof Error ? error.message : error);
|
|
542
549
|
console.log("Frontend build failed. Deploying schema and APIs only.");
|
|
543
550
|
skipFrontendBuild = true;
|
|
@@ -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
|
|
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": "
|
|
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",
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
"rules:gen": "rulesync generate"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@robodev-ai/sdk": "^0.
|
|
11
|
+
"@robodev-ai/sdk": "^0.10.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"robodev": "^0.
|
|
14
|
+
"robodev": "^0.30.0",
|
|
15
15
|
"rulesync": "^8.18.0",
|
|
16
16
|
"typescript": "^5.9.2"
|
|
17
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
|
|
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": "
|
|
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",
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
"rules:gen": "rulesync generate"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@robodev-ai/sdk": "^0.
|
|
11
|
+
"@robodev-ai/sdk": "^0.10.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"robodev": "^0.
|
|
14
|
+
"robodev": "^0.30.0",
|
|
15
15
|
"rulesync": "^8.18.0",
|
|
16
16
|
"typescript": "^5.9.2"
|
|
17
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
|
|
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": "
|
|
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",
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
"rules:gen": "rulesync generate"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@robodev-ai/sdk": "^0.
|
|
11
|
+
"@robodev-ai/sdk": "^0.10.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"robodev": "^0.
|
|
14
|
+
"robodev": "^0.30.0",
|
|
15
15
|
"rulesync": "^8.18.0",
|
|
16
16
|
"typescript": "^5.9.2"
|
|
17
17
|
}
|