robodev 0.6.0 → 0.8.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 +6 -0
- package/src/collect-files.ts +1 -0
- package/src/emit-starter.ts +6 -1
- package/src/index.ts +16 -3
- package/templates/auth-chat/package.json +2 -2
- package/templates/backend/README.md +17 -0
- package/templates/backend/api/health.ts +7 -0
- package/templates/backend/api/launch.ts +29 -0
- package/templates/backend/api/me.ts +14 -0
- package/templates/backend/api/motto.ts +9 -0
- package/templates/backend/api/planets/[id].ts +21 -0
- package/templates/backend/api/planets.ts +60 -0
- package/templates/backend/api/rockets.ts +43 -0
- package/templates/backend/database.ts +27 -0
- package/templates/backend/package.json +16 -0
- package/templates/backend/tsconfig.json +11 -0
- package/templates/catalog.json +6 -0
- package/templates/space/api/motto.ts +9 -0
- package/templates/space/api/planets/[id].ts +21 -0
- package/templates/space/package.json +2 -2
package/package.json
CHANGED
|
@@ -15,6 +15,10 @@ test("collectFiles includes hosted sources and skips denied paths", async () =>
|
|
|
15
15
|
await writeFile(join(root, "src/main.tsx"), "export {}");
|
|
16
16
|
await mkdir(join(root, "api"), { recursive: true });
|
|
17
17
|
await writeFile(join(root, "api/x.ts"), "export {}");
|
|
18
|
+
await mkdir(join(root, "api/invoices"), { recursive: true });
|
|
19
|
+
await writeFile(join(root, "api/invoices/[id].ts"), "export {}");
|
|
20
|
+
await mkdir(join(root, "jobs"), { recursive: true });
|
|
21
|
+
await writeFile(join(root, "jobs/x.ts"), "export {}");
|
|
18
22
|
await writeFile(join(root, "package.json"), "{}");
|
|
19
23
|
await mkdir(join(root, "node_modules"), { recursive: true });
|
|
20
24
|
await writeFile(join(root, "node_modules/x.js"), "nope");
|
|
@@ -30,6 +34,8 @@ test("collectFiles includes hosted sources and skips denied paths", async () =>
|
|
|
30
34
|
assert.ok(paths.has("src/main.tsx"));
|
|
31
35
|
assert.ok(paths.has("database.ts"));
|
|
32
36
|
assert.ok(paths.has("api/x.ts"));
|
|
37
|
+
assert.ok(paths.has("api/invoices/[id].ts"));
|
|
38
|
+
assert.ok(paths.has("jobs/x.ts"));
|
|
33
39
|
assert.ok(paths.has("package.json"));
|
|
34
40
|
assert.ok(!paths.has("node_modules/x.js"));
|
|
35
41
|
assert.ok(!paths.has("dist/x.js"));
|
package/src/collect-files.ts
CHANGED
|
@@ -96,6 +96,7 @@ function isDenied(path: string): boolean {
|
|
|
96
96
|
function isAllowed(path: string): boolean {
|
|
97
97
|
if (path === "database.ts") return true;
|
|
98
98
|
if (path.startsWith("api/")) return path.endsWith(".ts");
|
|
99
|
+
if (path.startsWith("jobs/")) return path.endsWith(".ts");
|
|
99
100
|
if (path === "index.html") return true;
|
|
100
101
|
if (path.startsWith("src/")) return SRC_EXTENSIONS.has(extname(path));
|
|
101
102
|
if (path.startsWith("public/")) return PUBLIC_EXTENSIONS.has(extname(path));
|
package/src/emit-starter.ts
CHANGED
|
@@ -2,7 +2,12 @@ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
|
-
export type Starter = {
|
|
5
|
+
export type Starter = {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
kind?: "app" | "backend";
|
|
10
|
+
};
|
|
6
11
|
|
|
7
12
|
export type PackageVersions = {
|
|
8
13
|
robodev: string;
|
package/src/index.ts
CHANGED
|
@@ -40,7 +40,7 @@ Commands:
|
|
|
40
40
|
projects List your projects
|
|
41
41
|
create [path] [--starter <id>] Create a project, scaffold a starter, and deploy
|
|
42
42
|
link [projectId] Write .robodev in the current folder
|
|
43
|
-
deploy [--force] Deploy
|
|
43
|
+
deploy [--force] Deploy database.ts, api/*.ts, jobs, and frontend files (no Vite / npm run build)
|
|
44
44
|
`);
|
|
45
45
|
process.exit(1);
|
|
46
46
|
}
|
|
@@ -426,7 +426,14 @@ async function runCreate(args: string[]): Promise<void> {
|
|
|
426
426
|
await npmInstall(root);
|
|
427
427
|
} catch (error) {
|
|
428
428
|
console.log(error instanceof Error ? error.message : error);
|
|
429
|
-
|
|
429
|
+
if (starter.kind === "backend") {
|
|
430
|
+
console.log(
|
|
431
|
+
"npm install failed. Copy the API URL printed above into Lovable, Bolt, or v0 as VITE_API_URL.",
|
|
432
|
+
);
|
|
433
|
+
console.log("https://robodev.povio.dev/docs/frontend");
|
|
434
|
+
} else {
|
|
435
|
+
console.log("Run `npm install` in the project folder, then `npm run dev`.");
|
|
436
|
+
}
|
|
430
437
|
return;
|
|
431
438
|
}
|
|
432
439
|
|
|
@@ -436,7 +443,13 @@ async function runCreate(args: string[]): Promise<void> {
|
|
|
436
443
|
if (displayPath !== ".") {
|
|
437
444
|
console.log(` cd ${displayPath}`);
|
|
438
445
|
}
|
|
439
|
-
|
|
446
|
+
if (starter.kind === "backend") {
|
|
447
|
+
console.log(" Copy the API URL printed above into Lovable, Bolt, or v0 as VITE_API_URL.");
|
|
448
|
+
console.log(" Do not run robodev deploy or npm run build inside those tools.");
|
|
449
|
+
console.log(" https://robodev.povio.dev/docs/frontend");
|
|
450
|
+
} else {
|
|
451
|
+
console.log(" npm run dev");
|
|
452
|
+
}
|
|
440
453
|
}
|
|
441
454
|
|
|
442
455
|
async function main(): Promise<void> {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"react": "^18.3.1",
|
|
13
13
|
"react-dom": "^18.3.1",
|
|
14
|
-
"@robodev-ai/sdk": "^0.
|
|
14
|
+
"@robodev-ai/sdk": "^0.6.0",
|
|
15
15
|
"@robodev-ai/client": "^0.2.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
@@ -20,6 +20,6 @@
|
|
|
20
20
|
"@vitejs/plugin-react": "^4.7.0",
|
|
21
21
|
"typescript": "^5.9.2",
|
|
22
22
|
"vite": "^6.3.5",
|
|
23
|
-
"robodev": "^0.
|
|
23
|
+
"robodev": "^0.8.0"
|
|
24
24
|
}
|
|
25
25
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# {{NAME}}
|
|
2
|
+
|
|
3
|
+
Space APIs only — no Vite UI. For Lovable, Bolt, or v0.
|
|
4
|
+
|
|
5
|
+
Deploy `database.ts` and `api/` from a real terminal:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx robodev deploy
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Build the UI elsewhere against `VITE_API_URL` (written to `.env` by `robodev create` / `robodev link`). Paste that URL into the frontend tool.
|
|
12
|
+
|
|
13
|
+
Do not run `npm run dev` or `npm run build` in this folder. Do not run `robodev` (auth, create, deploy) or `npm run build` inside Lovable, Bolt, or v0 — those tools cannot complete loopback OAuth and must not treat this folder as a Vite app.
|
|
14
|
+
|
|
15
|
+
Frontend how-to: https://robodev.povio.dev/docs/frontend
|
|
16
|
+
|
|
17
|
+
`GET /me` (`api/me.ts`) requires a Robodev Auth Bearer token. Planet and rocket routes stay public.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** POST /launch — marks a rocket as launched. */
|
|
2
|
+
import { rockets } from "../database";
|
|
3
|
+
import { defineApi, eq, z } from "@robodev-ai/sdk";
|
|
4
|
+
|
|
5
|
+
const Rocket = z.object({
|
|
6
|
+
id: z.string(),
|
|
7
|
+
name: z.string(),
|
|
8
|
+
destinationId: z.string(),
|
|
9
|
+
launched: z.boolean(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const post = defineApi({
|
|
13
|
+
body: z.object({
|
|
14
|
+
id: z.string().uuid(),
|
|
15
|
+
}),
|
|
16
|
+
response: Rocket,
|
|
17
|
+
handler: async ({ db, body }) => {
|
|
18
|
+
const rows = await db
|
|
19
|
+
.update(rockets)
|
|
20
|
+
.set({ launched: true })
|
|
21
|
+
.where(eq(rockets.id, body.id))
|
|
22
|
+
.returning();
|
|
23
|
+
const row = rows[0];
|
|
24
|
+
if (!row) {
|
|
25
|
+
throw new Error("Rocket not found");
|
|
26
|
+
}
|
|
27
|
+
return Rocket.parse(row);
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** GET /me — current project user (Robodev Auth required). */
|
|
2
|
+
import { defineApi, z } from "@robodev-ai/sdk";
|
|
3
|
+
|
|
4
|
+
export const get = defineApi({
|
|
5
|
+
auth: "required",
|
|
6
|
+
response: z.object({
|
|
7
|
+
user: z.object({
|
|
8
|
+
id: z.string(),
|
|
9
|
+
email: z.string(),
|
|
10
|
+
name: z.string().nullable().optional(),
|
|
11
|
+
}),
|
|
12
|
+
}),
|
|
13
|
+
handler: async ({ user }) => ({ user }),
|
|
14
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** GET /planets/:id — one planet by path param. */
|
|
2
|
+
import { defineApi, eq, z } from "@robodev-ai/sdk";
|
|
3
|
+
import { planets } from "../../database";
|
|
4
|
+
|
|
5
|
+
const Planet = z.object({
|
|
6
|
+
id: z.string(),
|
|
7
|
+
name: z.string(),
|
|
8
|
+
climate: z.string(),
|
|
9
|
+
moons: z.coerce.number(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const get = defineApi({
|
|
13
|
+
response: Planet,
|
|
14
|
+
handler: async ({ db, params }) => {
|
|
15
|
+
const [row] = await db.select().from(planets).where(eq(planets.id, params.id)).limit(1);
|
|
16
|
+
if (!row) {
|
|
17
|
+
return { status: 404, body: { error: "not_found" } };
|
|
18
|
+
}
|
|
19
|
+
return Planet.parse(row);
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** GET/POST /planets — list (seeds sample rows on first GET) and create planets. */
|
|
2
|
+
import { planets, rockets } from "../database";
|
|
3
|
+
import { defineApi, z } from "@robodev-ai/sdk";
|
|
4
|
+
|
|
5
|
+
const EARTH = "11111111-1111-1111-1111-111111111111";
|
|
6
|
+
const MARS = "22222222-2222-2222-2222-222222222222";
|
|
7
|
+
const KEPLER = "33333333-3333-3333-3333-333333333333";
|
|
8
|
+
const VOYAGER = "44444444-4444-4444-4444-444444444444";
|
|
9
|
+
const ODYSSEY = "55555555-5555-5555-5555-555555555555";
|
|
10
|
+
|
|
11
|
+
const Planet = z.object({
|
|
12
|
+
id: z.string(),
|
|
13
|
+
name: z.string(),
|
|
14
|
+
climate: z.string(),
|
|
15
|
+
moons: z.coerce.number(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const get = defineApi({
|
|
19
|
+
response: z.array(Planet),
|
|
20
|
+
handler: async ({ db }) => {
|
|
21
|
+
const existing = await db.select().from(planets).limit(1);
|
|
22
|
+
if (existing.length === 0) {
|
|
23
|
+
try {
|
|
24
|
+
await db.insert(planets).values([
|
|
25
|
+
{ id: EARTH, name: "Earth", climate: "temperate", moons: 1 },
|
|
26
|
+
{ id: MARS, name: "Mars", climate: "arid", moons: 2 },
|
|
27
|
+
{ id: KEPLER, name: "Kepler-452b", climate: "unknown", moons: 0 },
|
|
28
|
+
]);
|
|
29
|
+
await db.insert(rockets).values([
|
|
30
|
+
{ id: VOYAGER, name: "Voyager", destinationId: MARS, launched: false },
|
|
31
|
+
{ id: ODYSSEY, name: "Odyssey", destinationId: KEPLER, launched: true },
|
|
32
|
+
]);
|
|
33
|
+
} catch {
|
|
34
|
+
// First request won the seed race.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const rows = await db.select().from(planets);
|
|
38
|
+
return Planet.array().parse(rows);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export const post = defineApi({
|
|
43
|
+
body: z.object({
|
|
44
|
+
name: z.string().min(1),
|
|
45
|
+
climate: z.string().min(1),
|
|
46
|
+
moons: z.coerce.number().int().min(0).default(0),
|
|
47
|
+
}),
|
|
48
|
+
response: Planet,
|
|
49
|
+
handler: async ({ db, body }) => {
|
|
50
|
+
const [row] = await db
|
|
51
|
+
.insert(planets)
|
|
52
|
+
.values({
|
|
53
|
+
name: body.name,
|
|
54
|
+
climate: body.climate,
|
|
55
|
+
moons: body.moons,
|
|
56
|
+
})
|
|
57
|
+
.returning();
|
|
58
|
+
return Planet.parse(row);
|
|
59
|
+
},
|
|
60
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** GET/POST /rockets — list (optional `?destinationId=`) and create rockets. */
|
|
2
|
+
import { rockets } from "../database";
|
|
3
|
+
import { defineApi, eq, z } from "@robodev-ai/sdk";
|
|
4
|
+
|
|
5
|
+
const Rocket = z.object({
|
|
6
|
+
id: z.string(),
|
|
7
|
+
name: z.string(),
|
|
8
|
+
destinationId: z.string(),
|
|
9
|
+
launched: z.boolean(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const get = defineApi({
|
|
13
|
+
query: z.object({
|
|
14
|
+
destinationId: z.string().optional(),
|
|
15
|
+
}),
|
|
16
|
+
response: z.array(Rocket),
|
|
17
|
+
handler: async ({ db, query }) => {
|
|
18
|
+
const rows = query.destinationId
|
|
19
|
+
? await db.select().from(rockets).where(eq(rockets.destinationId, query.destinationId))
|
|
20
|
+
: await db.select().from(rockets);
|
|
21
|
+
return Rocket.array().parse(rows);
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const post = defineApi({
|
|
26
|
+
body: z.object({
|
|
27
|
+
name: z.string().min(1),
|
|
28
|
+
destinationId: z.string().uuid(),
|
|
29
|
+
launched: z.boolean().optional(),
|
|
30
|
+
}),
|
|
31
|
+
response: Rocket,
|
|
32
|
+
handler: async ({ db, body }) => {
|
|
33
|
+
const [row] = await db
|
|
34
|
+
.insert(rockets)
|
|
35
|
+
.values({
|
|
36
|
+
name: body.name,
|
|
37
|
+
destinationId: body.destinationId,
|
|
38
|
+
launched: body.launched ?? false,
|
|
39
|
+
})
|
|
40
|
+
.returning();
|
|
41
|
+
return Rocket.parse(row);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Starter schema. `robodev deploy` creates the `space` database and these tables
|
|
3
|
+
* (no migration files). Edit here and deploy again to sync.
|
|
4
|
+
*/
|
|
5
|
+
import { boolean, defineDatabase, integer, pgTable, text, timestamp, uuid } from "@robodev-ai/sdk";
|
|
6
|
+
|
|
7
|
+
export const planets = pgTable("planets", {
|
|
8
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
9
|
+
name: text("name").notNull(),
|
|
10
|
+
climate: text("climate").notNull(),
|
|
11
|
+
moons: integer("moons").default(0),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const rockets = pgTable("rockets", {
|
|
15
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
16
|
+
name: text("name").notNull(),
|
|
17
|
+
destinationId: uuid("destinationId")
|
|
18
|
+
.notNull()
|
|
19
|
+
.references(() => planets.id, { onDelete: "cascade" }),
|
|
20
|
+
launched: boolean("launched").default(false),
|
|
21
|
+
createdAt: timestamp("createdAt", { withTimezone: true }).defaultNow(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export default defineDatabase({
|
|
25
|
+
name: "space",
|
|
26
|
+
tables: { planets, rockets },
|
|
27
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PACKAGE_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"deploy": "robodev deploy"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@robodev-ai/sdk": "^0.6.0"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"robodev": "^0.8.0",
|
|
14
|
+
"typescript": "^5.9.2"
|
|
15
|
+
}
|
|
16
|
+
}
|
package/templates/catalog.json
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
"id": "auth-chat",
|
|
10
10
|
"title": "Auth chat",
|
|
11
11
|
"description": "Email + Google sign-in and a shared chat room"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "backend",
|
|
15
|
+
"title": "Backend",
|
|
16
|
+
"description": "Space APIs only — no Vite UI. For Lovable, Bolt, or v0",
|
|
17
|
+
"kind": "backend"
|
|
12
18
|
}
|
|
13
19
|
]
|
|
14
20
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** GET /planets/:id — one planet by path param. */
|
|
2
|
+
import { defineApi, eq, z } from "@robodev-ai/sdk";
|
|
3
|
+
import { planets } from "../../database";
|
|
4
|
+
|
|
5
|
+
const Planet = z.object({
|
|
6
|
+
id: z.string(),
|
|
7
|
+
name: z.string(),
|
|
8
|
+
climate: z.string(),
|
|
9
|
+
moons: z.coerce.number(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const get = defineApi({
|
|
13
|
+
response: Planet,
|
|
14
|
+
handler: async ({ db, params }) => {
|
|
15
|
+
const [row] = await db.select().from(planets).where(eq(planets.id, params.id)).limit(1);
|
|
16
|
+
if (!row) {
|
|
17
|
+
return { status: 404, body: { error: "not_found" } };
|
|
18
|
+
}
|
|
19
|
+
return Planet.parse(row);
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"react": "^18.3.1",
|
|
13
13
|
"react-dom": "^18.3.1",
|
|
14
|
-
"@robodev-ai/sdk": "^0.
|
|
14
|
+
"@robodev-ai/sdk": "^0.6.0",
|
|
15
15
|
"@robodev-ai/client": "^0.2.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
@@ -20,6 +20,6 @@
|
|
|
20
20
|
"@vitejs/plugin-react": "^4.7.0",
|
|
21
21
|
"typescript": "^5.9.2",
|
|
22
22
|
"vite": "^6.3.5",
|
|
23
|
-
"robodev": "^0.
|
|
23
|
+
"robodev": "^0.8.0"
|
|
24
24
|
}
|
|
25
25
|
}
|