robodev 0.26.0 → 0.27.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 +5 -0
- package/src/collect-files.ts +1 -0
- package/src/emit-starter.test.ts +1 -0
- package/templates/auth-chat/.rulesync/rules/project-overview.md +2 -2
- package/templates/auth-chat/.rulesync/rules/robodev-auth.md +1 -0
- package/templates/auth-chat/api/invite.ts +14 -4
- package/templates/auth-chat/database.ts +12 -2
- package/templates/auth-chat/hooks/auth.ts +17 -0
- 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/package.json +2 -2
- package/templates/space/package.json +2 -2
package/package.json
CHANGED
|
@@ -19,6 +19,9 @@ test("collectFiles includes hosted sources and skips denied paths", async () =>
|
|
|
19
19
|
await writeFile(join(root, "api/invoices/[id].ts"), "export {}");
|
|
20
20
|
await mkdir(join(root, "jobs"), { recursive: true });
|
|
21
21
|
await writeFile(join(root, "jobs/x.ts"), "export {}");
|
|
22
|
+
await mkdir(join(root, "hooks"), { recursive: true });
|
|
23
|
+
await writeFile(join(root, "hooks/auth.ts"), "export {}");
|
|
24
|
+
await writeFile(join(root, "hooks/lib.ts"), "export {}");
|
|
22
25
|
await writeFile(join(root, "package.json"), "{}");
|
|
23
26
|
await mkdir(join(root, "node_modules"), { recursive: true });
|
|
24
27
|
await writeFile(join(root, "node_modules/x.js"), "nope");
|
|
@@ -36,6 +39,8 @@ test("collectFiles includes hosted sources and skips denied paths", async () =>
|
|
|
36
39
|
assert.ok(paths.has("api/x.ts"));
|
|
37
40
|
assert.ok(paths.has("api/invoices/[id].ts"));
|
|
38
41
|
assert.ok(paths.has("jobs/x.ts"));
|
|
42
|
+
assert.ok(paths.has("hooks/auth.ts"));
|
|
43
|
+
assert.ok(paths.has("hooks/lib.ts"));
|
|
39
44
|
assert.ok(paths.has("package.json"));
|
|
40
45
|
assert.ok(!paths.has("node_modules/x.js"));
|
|
41
46
|
assert.ok(!paths.has("dist/x.js"));
|
package/src/collect-files.ts
CHANGED
|
@@ -100,6 +100,7 @@ function isAllowed(path: string): boolean {
|
|
|
100
100
|
if (path === "database.ts") return true;
|
|
101
101
|
if (path.startsWith("api/")) return path.endsWith(".ts");
|
|
102
102
|
if (path.startsWith("jobs/")) return path.endsWith(".ts");
|
|
103
|
+
if (path.startsWith("hooks/")) return path.endsWith(".ts");
|
|
103
104
|
if (path === "index.html") return true;
|
|
104
105
|
if (path.startsWith("src/")) return SRC_EXTENSIONS.has(extname(path));
|
|
105
106
|
if (path.startsWith("public/")) return PUBLIC_EXTENSIONS.has(extname(path));
|
package/src/emit-starter.test.ts
CHANGED
|
@@ -71,6 +71,7 @@ test("collectFiles for auth-chat starter is Tiny APIs without apps/fe", async ()
|
|
|
71
71
|
assert.ok(paths.has("api/invite.ts"));
|
|
72
72
|
assert.ok(paths.has("api/health.ts"));
|
|
73
73
|
assert.ok(paths.has("api/me.ts"));
|
|
74
|
+
assert.ok(paths.has("hooks/auth.ts"));
|
|
74
75
|
assert.ok(!paths.has("index.html"));
|
|
75
76
|
assert.ok(!paths.has("src/main.tsx"));
|
|
76
77
|
assert.ok([...paths].every((path) => !path.startsWith("apps/")));
|
|
@@ -11,13 +11,13 @@ cursor:
|
|
|
11
11
|
|
|
12
12
|
# Space starter AI rules
|
|
13
13
|
|
|
14
|
-
This project is a Vite React frontend in `apps/fe` plus a Robodev backend at the **repository root** (`database.ts` + `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.
|
|
14
|
+
This project is a Vite React frontend in `apps/fe` plus a Robodev backend at the **repository root** (`database.ts` + `api/**/*.ts` + `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
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`.
|
|
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
|
|
|
20
|
-
The demo is a shared chat room plus an authenticated invite-email form. Signed-in users read and post on `GET/POST /api/messages`. Invites go through `POST /api/invite` (`auth: "required"`)
|
|
20
|
+
The demo is a shared chat room plus an authenticated invite-email form. Signed-in users read and post on `GET/POST /api/messages`. Invites go through `POST /api/invite` (`auth: "required"`), which persists an `invites` row then sends email. `hooks/auth.ts` claims matching pending invites on `user.created`. Posting a chat message does not send email. Do not add `api/user/**`. Do not add a User table — store `userId` as text matching `ctx.user.id`.
|
|
21
21
|
|
|
22
22
|
When an app has multiple user roles with distinct UI or business workflows, use the conditional role-based app structure rule. Do not apply role splitting to simple single-role apps or small permission differences.
|
|
23
23
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
/** POST /api/invite — signed-in users
|
|
1
|
+
/** POST /api/invite — signed-in users persist an invite, then send email. */
|
|
2
|
+
import { invites } from "../database";
|
|
2
3
|
import { defineApi, z } from "@robodev-ai/sdk";
|
|
3
4
|
|
|
4
5
|
function headerValue(
|
|
@@ -28,16 +29,25 @@ export const post = defineApi({
|
|
|
28
29
|
auth: "required",
|
|
29
30
|
body: z.object({ to: z.string().email() }),
|
|
30
31
|
response: z.object({ id: z.string(), status: z.literal("sent") }),
|
|
31
|
-
handler: async ({ email, body, user, env, headers }) => {
|
|
32
|
+
handler: async ({ db, email, body, user, env, headers }) => {
|
|
33
|
+
const inviteEmail = body.to.toLowerCase();
|
|
34
|
+
const [row] = await db
|
|
35
|
+
.insert(invites)
|
|
36
|
+
.values({
|
|
37
|
+
email: inviteEmail,
|
|
38
|
+
invitedByUserId: user!.id,
|
|
39
|
+
})
|
|
40
|
+
.returning();
|
|
32
41
|
const url = resolveProjectUrl(env, headers);
|
|
33
42
|
const who = user?.name?.trim() || user?.email || "Someone";
|
|
34
43
|
const project = url ? ` Project URL: ${url}` : "";
|
|
35
|
-
|
|
36
|
-
to:
|
|
44
|
+
const sent = await email.send({
|
|
45
|
+
to: inviteEmail,
|
|
37
46
|
subject: "You're invited to the chat",
|
|
38
47
|
text:
|
|
39
48
|
`${who} invited you to a shared chat on Robodev. ` +
|
|
40
49
|
`Open the App URL and sign in to join.${project}`,
|
|
41
50
|
});
|
|
51
|
+
return { id: row?.id ?? sent.id, status: "sent" as const };
|
|
42
52
|
},
|
|
43
53
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared chat room. `robodev_auth.users` is platform-owned — store only a
|
|
3
|
-
* snapshot of the author on each message.
|
|
3
|
+
* snapshot of the author on each message. Invite `userId` columns are text
|
|
4
|
+
* matching Auth `ctx.user.id`.
|
|
4
5
|
*/
|
|
5
6
|
import { defineDatabase, pgTable, text, timestamp, uuid } from "@robodev-ai/sdk";
|
|
6
7
|
|
|
@@ -13,7 +14,16 @@ export const messages = pgTable("messages", {
|
|
|
13
14
|
createdAt: timestamp("createdAt", { withTimezone: true }).defaultNow(),
|
|
14
15
|
});
|
|
15
16
|
|
|
17
|
+
export const invites = pgTable("invites", {
|
|
18
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
19
|
+
email: text("email").notNull(),
|
|
20
|
+
invitedByUserId: text("invitedByUserId").notNull(),
|
|
21
|
+
createdAt: timestamp("createdAt", { withTimezone: true }).defaultNow(),
|
|
22
|
+
claimedAt: timestamp("claimedAt", { withTimezone: true }),
|
|
23
|
+
claimedByUserId: text("claimedByUserId"),
|
|
24
|
+
});
|
|
25
|
+
|
|
16
26
|
export default defineDatabase({
|
|
17
27
|
name: "chat",
|
|
18
|
-
tables: { messages },
|
|
28
|
+
tables: { messages, invites },
|
|
19
29
|
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { and, defineAuthHook, eq, isNull } from "@robodev-ai/sdk";
|
|
2
|
+
import { invites } from "../database";
|
|
3
|
+
|
|
4
|
+
export default defineAuthHook({
|
|
5
|
+
onUserCreated: async ({ db, event }) => {
|
|
6
|
+
const email = event.user?.email.toLowerCase();
|
|
7
|
+
const userId = event.user?.id;
|
|
8
|
+
if (!email || !userId) return;
|
|
9
|
+
await db
|
|
10
|
+
.update(invites)
|
|
11
|
+
.set({
|
|
12
|
+
claimedAt: new Date(),
|
|
13
|
+
claimedByUserId: userId,
|
|
14
|
+
})
|
|
15
|
+
.where(and(eq(invites.email, email), isNull(invites.claimedAt)));
|
|
16
|
+
},
|
|
17
|
+
});
|
|
@@ -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.7.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"robodev": "^0.
|
|
14
|
+
"robodev": "^0.27.0",
|
|
15
15
|
"rulesync": "^8.18.0",
|
|
16
16
|
"typescript": "^5.9.2"
|
|
17
17
|
}
|
|
@@ -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.7.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"robodev": "^0.
|
|
14
|
+
"robodev": "^0.27.0",
|
|
15
15
|
"rulesync": "^8.18.0",
|
|
16
16
|
"typescript": "^5.9.2"
|
|
17
17
|
}
|
|
@@ -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.7.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"robodev": "^0.
|
|
14
|
+
"robodev": "^0.27.0",
|
|
15
15
|
"rulesync": "^8.18.0",
|
|
16
16
|
"typescript": "^5.9.2"
|
|
17
17
|
}
|