robodev 0.27.0 → 0.29.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 +68 -0
- package/src/collect-files.ts +68 -10
- package/src/config.test.ts +22 -2
- package/src/config.ts +33 -15
- package/src/emit-starter.test.ts +18 -18
- package/src/index.ts +32 -4
- package/templates/auth-chat/.rulesync/rules/frontend-api-boundary.md +1 -1
- package/templates/auth-chat/.rulesync/rules/frontend-notifications.md +1 -1
- package/templates/auth-chat/.rulesync/rules/project-overview.md +3 -3
- package/templates/auth-chat/.rulesync/rules/robodev-api.md +7 -7
- package/templates/auth-chat/.rulesync/rules/robodev-auth.md +2 -2
- package/templates/auth-chat/.rulesync/rules/robodev-database.md +5 -5
- package/templates/auth-chat/.rulesync/rules/role-based-app-structure.md +2 -2
- package/templates/auth-chat/.rulesync/skills/media-feature/SKILL.md +1 -1
- package/templates/auth-chat/.rulesync/skills/robodev-api-route/SKILL.md +3 -3
- package/templates/auth-chat/.rulesync/skills/robodev-table/SKILL.md +2 -2
- package/templates/auth-chat/README.md +1 -1
- package/templates/auth-chat/apps/fe/package.json +4 -4
- package/templates/auth-chat/package.json +2 -2
- package/templates/auth-chat/robodev/package.json +4 -0
- package/templates/auth-chat/tsconfig.json +1 -1
- package/templates/backend/README.md +1 -1
- package/templates/backend/package.json +2 -2
- package/templates/backend/robodev/package.json +4 -0
- package/templates/backend/tsconfig.json +1 -1
- package/templates/empty/README.md +2 -2
- package/templates/empty/package.json +2 -2
- package/templates/empty/robodev/package.json +4 -0
- package/templates/empty/tsconfig.json +1 -1
- package/templates/marketplace/.rulesync/rules/frontend-api-boundary.md +1 -1
- package/templates/marketplace/.rulesync/rules/frontend-notifications.md +1 -1
- package/templates/marketplace/.rulesync/rules/project-overview.md +2 -2
- package/templates/marketplace/.rulesync/rules/robodev-api.md +7 -7
- package/templates/marketplace/.rulesync/rules/robodev-auth.md +2 -2
- package/templates/marketplace/.rulesync/rules/robodev-database.md +5 -5
- package/templates/marketplace/.rulesync/rules/role-based-app-structure.md +2 -2
- package/templates/marketplace/.rulesync/skills/media-feature/SKILL.md +1 -1
- package/templates/marketplace/.rulesync/skills/robodev-api-route/SKILL.md +3 -3
- package/templates/marketplace/.rulesync/skills/robodev-table/SKILL.md +2 -2
- package/templates/marketplace/README.md +2 -2
- package/templates/marketplace/apps/fe/package.json +4 -4
- package/templates/marketplace/package.json +2 -2
- package/templates/marketplace/robodev/package.json +4 -0
- package/templates/marketplace/tsconfig.json +1 -1
- package/templates/space/.rulesync/rules/frontend-api-boundary.md +1 -1
- package/templates/space/.rulesync/rules/frontend-notifications.md +1 -1
- package/templates/space/.rulesync/rules/project-overview.md +2 -2
- package/templates/space/.rulesync/rules/robodev-api.md +7 -7
- package/templates/space/.rulesync/rules/robodev-auth.md +2 -2
- package/templates/space/.rulesync/rules/robodev-database.md +5 -5
- package/templates/space/.rulesync/rules/role-based-app-structure.md +2 -2
- package/templates/space/.rulesync/skills/media-feature/SKILL.md +1 -1
- package/templates/space/.rulesync/skills/robodev-api-route/SKILL.md +3 -3
- package/templates/space/.rulesync/skills/robodev-table/SKILL.md +2 -2
- package/templates/space/README.md +2 -2
- package/templates/space/apps/fe/package.json +4 -4
- package/templates/space/package.json +2 -2
- package/templates/space/robodev/package.json +4 -0
- package/templates/space/tsconfig.json +1 -1
- /package/templates/auth-chat/{api → robodev/api}/health.ts +0 -0
- /package/templates/auth-chat/{api → robodev/api}/invite.ts +0 -0
- /package/templates/auth-chat/{api → robodev/api}/me.ts +0 -0
- /package/templates/auth-chat/{api → robodev/api}/messages.ts +0 -0
- /package/templates/auth-chat/{database.ts → robodev/database.ts} +0 -0
- /package/templates/auth-chat/{hooks → robodev/hooks}/auth.ts +0 -0
- /package/templates/backend/{api → robodev/api}/_lib.ts +0 -0
- /package/templates/backend/{api → robodev/api}/aliens/labels.ts +0 -0
- /package/templates/backend/{api → robodev/api}/planets/[id]/like.ts +0 -0
- /package/templates/backend/{api → robodev/api}/planets/[id].ts +0 -0
- /package/templates/backend/{api → robodev/api}/planets/paginate.ts +0 -0
- /package/templates/backend/{api → robodev/api}/planets.ts +0 -0
- /package/templates/backend/{database.ts → robodev/database.ts} +0 -0
- /package/templates/empty/{api → robodev/api}/health.ts +0 -0
- /package/templates/empty/{database.ts → robodev/database.ts} +0 -0
- /package/templates/marketplace/{api → robodev/api}/_lib.seed.test.ts +0 -0
- /package/templates/marketplace/{api → robodev/api}/_lib.ts +0 -0
- /package/templates/marketplace/{api → robodev/api}/cart/[id].ts +0 -0
- /package/templates/marketplace/{api → robodev/api}/cart.ts +0 -0
- /package/templates/marketplace/{api → robodev/api}/categories.ts +0 -0
- /package/templates/marketplace/{api → robodev/api}/checkout.ts +0 -0
- /package/templates/marketplace/{api → robodev/api}/health.ts +0 -0
- /package/templates/marketplace/{api → robodev/api}/listings/[id].ts +0 -0
- /package/templates/marketplace/{api → robodev/api}/listings/paginate.ts +0 -0
- /package/templates/marketplace/{api → robodev/api}/listings.ts +0 -0
- /package/templates/marketplace/{api → robodev/api}/me.ts +0 -0
- /package/templates/marketplace/{api → robodev/api}/orders/[id]/complete.ts +0 -0
- /package/templates/marketplace/{api → robodev/api}/orders/[id]/ship.ts +0 -0
- /package/templates/marketplace/{api → robodev/api}/orders/[id].ts +0 -0
- /package/templates/marketplace/{api → robodev/api}/purchases.ts +0 -0
- /package/templates/marketplace/{api → robodev/api}/reviews.ts +0 -0
- /package/templates/marketplace/{api → robodev/api}/sales.ts +0 -0
- /package/templates/marketplace/{api → robodev/api}/watches/[listingId].ts +0 -0
- /package/templates/marketplace/{api → robodev/api}/watches.ts +0 -0
- /package/templates/marketplace/{database.ts → robodev/database.ts} +0 -0
- /package/templates/space/{api → robodev/api}/_lib.ts +0 -0
- /package/templates/space/{api → robodev/api}/aliens/labels.ts +0 -0
- /package/templates/space/{api → robodev/api}/planets/[id]/like.ts +0 -0
- /package/templates/space/{api → robodev/api}/planets/[id].ts +0 -0
- /package/templates/space/{api → robodev/api}/planets/paginate.ts +0 -0
- /package/templates/space/{api → robodev/api}/planets.ts +0 -0
- /package/templates/space/{database.ts → robodev/database.ts} +0 -0
package/package.json
CHANGED
|
@@ -7,6 +7,57 @@ import { test } from "node:test";
|
|
|
7
7
|
import { collectFiles, mergeCollectedFiles } from "./collect-files.js";
|
|
8
8
|
|
|
9
9
|
test("collectFiles includes hosted sources and skips denied paths", async () => {
|
|
10
|
+
const root = await mkdtemp(join(tmpdir(), "rd-cli-"));
|
|
11
|
+
try {
|
|
12
|
+
await mkdir(join(root, "robodev/api/invoices"), { recursive: true });
|
|
13
|
+
await mkdir(join(root, "robodev/jobs"), { recursive: true });
|
|
14
|
+
await mkdir(join(root, "robodev/sockets"), { recursive: true });
|
|
15
|
+
await mkdir(join(root, "robodev/hooks"), { recursive: true });
|
|
16
|
+
await writeFile(join(root, "robodev/database.ts"), "export default {}");
|
|
17
|
+
await writeFile(join(root, "robodev/package.json"), '{"private":true,"dependencies":{}}');
|
|
18
|
+
await writeFile(join(root, "robodev/.robodev"), "nope");
|
|
19
|
+
await writeFile(join(root, "index.html"), "<html></html>");
|
|
20
|
+
await mkdir(join(root, "src"), { recursive: true });
|
|
21
|
+
await writeFile(join(root, "src/main.tsx"), "export {}");
|
|
22
|
+
await writeFile(join(root, "robodev/api/x.ts"), "export {}");
|
|
23
|
+
await writeFile(join(root, "robodev/api/invoices/[id].ts"), "export {}");
|
|
24
|
+
await writeFile(join(root, "robodev/jobs/x.ts"), "export {}");
|
|
25
|
+
await writeFile(join(root, "robodev/sockets/chat.ts"), "export {}");
|
|
26
|
+
await writeFile(join(root, "robodev/hooks/auth.ts"), "export {}");
|
|
27
|
+
await writeFile(join(root, "robodev/hooks/lib.ts"), "export {}");
|
|
28
|
+
await writeFile(join(root, "package.json"), "{}");
|
|
29
|
+
await mkdir(join(root, "node_modules"), { recursive: true });
|
|
30
|
+
await writeFile(join(root, "node_modules/x.js"), "nope");
|
|
31
|
+
await mkdir(join(root, "dist"), { recursive: true });
|
|
32
|
+
await writeFile(join(root, "dist/x.js"), "nope");
|
|
33
|
+
await writeFile(join(root, ".env"), "SECRET=1");
|
|
34
|
+
await mkdir(join(root, ".robodev"), { recursive: true });
|
|
35
|
+
await writeFile(join(root, ".robodev/x"), "nope");
|
|
36
|
+
|
|
37
|
+
const files = await collectFiles(root);
|
|
38
|
+
const paths = new Set(files.map((file) => file.path));
|
|
39
|
+
assert.ok(paths.has("index.html"));
|
|
40
|
+
assert.ok(paths.has("src/main.tsx"));
|
|
41
|
+
assert.ok(paths.has("robodev/database.ts"));
|
|
42
|
+
assert.ok(paths.has("robodev/package.json"));
|
|
43
|
+
assert.ok(paths.has("robodev/api/x.ts"));
|
|
44
|
+
assert.ok(paths.has("robodev/api/invoices/[id].ts"));
|
|
45
|
+
assert.ok(paths.has("robodev/jobs/x.ts"));
|
|
46
|
+
assert.ok(paths.has("robodev/sockets/chat.ts"));
|
|
47
|
+
assert.ok(paths.has("robodev/hooks/auth.ts"));
|
|
48
|
+
assert.ok(paths.has("robodev/hooks/lib.ts"));
|
|
49
|
+
assert.ok(paths.has("package.json"));
|
|
50
|
+
assert.ok(!paths.has("node_modules/x.js"));
|
|
51
|
+
assert.ok(!paths.has("dist/x.js"));
|
|
52
|
+
assert.ok(!paths.has(".env"));
|
|
53
|
+
assert.ok(!paths.has(".robodev/x"));
|
|
54
|
+
assert.ok(!paths.has("robodev/.robodev"));
|
|
55
|
+
} finally {
|
|
56
|
+
await rm(root, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("collectFiles still accepts a legacy root database.ts tree", async () => {
|
|
10
61
|
const root = await mkdtemp(join(tmpdir(), "rd-cli-"));
|
|
11
62
|
try {
|
|
12
63
|
await writeFile(join(root, "database.ts"), "export default {}");
|
|
@@ -19,6 +70,8 @@ test("collectFiles includes hosted sources and skips denied paths", async () =>
|
|
|
19
70
|
await writeFile(join(root, "api/invoices/[id].ts"), "export {}");
|
|
20
71
|
await mkdir(join(root, "jobs"), { recursive: true });
|
|
21
72
|
await writeFile(join(root, "jobs/x.ts"), "export {}");
|
|
73
|
+
await mkdir(join(root, "sockets"), { recursive: true });
|
|
74
|
+
await writeFile(join(root, "sockets/chat.ts"), "export {}");
|
|
22
75
|
await mkdir(join(root, "hooks"), { recursive: true });
|
|
23
76
|
await writeFile(join(root, "hooks/auth.ts"), "export {}");
|
|
24
77
|
await writeFile(join(root, "hooks/lib.ts"), "export {}");
|
|
@@ -39,6 +92,7 @@ test("collectFiles includes hosted sources and skips denied paths", async () =>
|
|
|
39
92
|
assert.ok(paths.has("api/x.ts"));
|
|
40
93
|
assert.ok(paths.has("api/invoices/[id].ts"));
|
|
41
94
|
assert.ok(paths.has("jobs/x.ts"));
|
|
95
|
+
assert.ok(paths.has("sockets/chat.ts"));
|
|
42
96
|
assert.ok(paths.has("hooks/auth.ts"));
|
|
43
97
|
assert.ok(paths.has("hooks/lib.ts"));
|
|
44
98
|
assert.ok(paths.has("package.json"));
|
|
@@ -78,3 +132,17 @@ test("collectFiles skips nested apps/fe sources", async () => {
|
|
|
78
132
|
await rm(root, { recursive: true, force: true });
|
|
79
133
|
}
|
|
80
134
|
});
|
|
135
|
+
|
|
136
|
+
test("collectFiles rejects mixed backend layouts", async () => {
|
|
137
|
+
const root = await mkdtemp(join(tmpdir(), "rd-cli-mix-"));
|
|
138
|
+
try {
|
|
139
|
+
await mkdir(join(root, "robodev"), { recursive: true });
|
|
140
|
+
await mkdir(join(root, "api"), { recursive: true });
|
|
141
|
+
await writeFile(join(root, "robodev/database.ts"), "export default {}");
|
|
142
|
+
await writeFile(join(root, "database.ts"), "export default {}");
|
|
143
|
+
await writeFile(join(root, "api/x.ts"), "export {}");
|
|
144
|
+
await assert.rejects(() => collectFiles(root), /mix robodev/);
|
|
145
|
+
} finally {
|
|
146
|
+
await rm(root, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
});
|
package/src/collect-files.ts
CHANGED
|
@@ -85,22 +85,46 @@ function isUnsafe(path: string): boolean {
|
|
|
85
85
|
);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
function isRobodevLockfile(path: string): boolean {
|
|
89
|
+
return (
|
|
90
|
+
path === "robodev/package-lock.json" ||
|
|
91
|
+
path === "robodev/pnpm-lock.yaml" ||
|
|
92
|
+
path === "robodev/yarn.lock" ||
|
|
93
|
+
path === "robodev/npm-shrinkwrap.json"
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
88
97
|
function isDenied(path: string): boolean {
|
|
89
98
|
const top = path.split("/")[0];
|
|
90
99
|
if (SKIP_DIRS.has(top)) return true;
|
|
91
100
|
if (path === "apps" || path.startsWith("apps/")) return true;
|
|
92
101
|
if (path === ".env" || path.startsWith(".env.")) return true;
|
|
102
|
+
if (isRobodevLockfile(path)) return true;
|
|
93
103
|
const name = basename(path);
|
|
94
104
|
if (name.startsWith(".") && name !== ".gitignore") return true;
|
|
95
105
|
if (BINARY_EXTENSIONS.has(extname(path))) return true;
|
|
96
106
|
return false;
|
|
97
107
|
}
|
|
98
108
|
|
|
109
|
+
function isBackendPath(path: string): boolean {
|
|
110
|
+
const inner = path.startsWith("robodev/") ? path.slice("robodev/".length) : path;
|
|
111
|
+
if (inner === "database.ts" && (path === "database.ts" || path === "robodev/database.ts")) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
if (
|
|
115
|
+
inner.startsWith("api/") ||
|
|
116
|
+
inner.startsWith("jobs/") ||
|
|
117
|
+
inner.startsWith("hooks/") ||
|
|
118
|
+
inner.startsWith("sockets/")
|
|
119
|
+
) {
|
|
120
|
+
return inner.endsWith(".ts");
|
|
121
|
+
}
|
|
122
|
+
return path === "robodev/package.json";
|
|
123
|
+
}
|
|
124
|
+
|
|
99
125
|
function isAllowed(path: string): boolean {
|
|
100
|
-
if (path
|
|
101
|
-
if (path.startsWith("
|
|
102
|
-
if (path.startsWith("jobs/")) return path.endsWith(".ts");
|
|
103
|
-
if (path.startsWith("hooks/")) return path.endsWith(".ts");
|
|
126
|
+
if (isBackendPath(path)) return true;
|
|
127
|
+
if (path.startsWith("robodev/")) return false;
|
|
104
128
|
if (path === "index.html") return true;
|
|
105
129
|
if (path.startsWith("src/")) return SRC_EXTENSIONS.has(extname(path));
|
|
106
130
|
if (path.startsWith("public/")) return PUBLIC_EXTENSIONS.has(extname(path));
|
|
@@ -110,6 +134,28 @@ function isAllowed(path: string): boolean {
|
|
|
110
134
|
return false;
|
|
111
135
|
}
|
|
112
136
|
|
|
137
|
+
export function mixedBackendLayoutError(paths: Iterable<string>): string | null {
|
|
138
|
+
const list = [...paths].map(normalize);
|
|
139
|
+
const hasNewDb = list.includes("robodev/database.ts");
|
|
140
|
+
const hasLegacyDb = list.includes("database.ts");
|
|
141
|
+
const hasLegacyApiJobs = list.some(
|
|
142
|
+
(path) => path.startsWith("api/") || path.startsWith("jobs/") || path.startsWith("sockets/"),
|
|
143
|
+
);
|
|
144
|
+
const hasNewApiJobs = list.some(
|
|
145
|
+
(path) =>
|
|
146
|
+
path.startsWith("robodev/api/") ||
|
|
147
|
+
path.startsWith("robodev/jobs/") ||
|
|
148
|
+
path.startsWith("robodev/sockets/"),
|
|
149
|
+
);
|
|
150
|
+
if (hasNewDb && (hasLegacyDb || hasLegacyApiJobs)) {
|
|
151
|
+
return "Do not mix robodev/ backend files with root database.ts, api/, or jobs/";
|
|
152
|
+
}
|
|
153
|
+
if (hasLegacyDb && (hasNewDb || hasNewApiJobs)) {
|
|
154
|
+
return "Do not mix robodev/ backend files with root database.ts, api/, or jobs/";
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
113
159
|
export function shouldCollectPath(path: string): boolean {
|
|
114
160
|
const normalized = normalize(path);
|
|
115
161
|
return !isUnsafe(normalized) && !isDenied(normalized) && isAllowed(normalized);
|
|
@@ -176,10 +222,10 @@ export function mergeCollectedFiles(
|
|
|
176
222
|
}
|
|
177
223
|
|
|
178
224
|
export async function collectFiles(root: string): Promise<CollectedFile[]> {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
throw new Error("database.ts not found in
|
|
225
|
+
const hasNew = await fileExists(join(root, "robodev/database.ts"));
|
|
226
|
+
const hasLegacy = await fileExists(join(root, "database.ts"));
|
|
227
|
+
if (!hasNew && !hasLegacy) {
|
|
228
|
+
throw new Error("database.ts not found (looked in robodev/database.ts and ./database.ts)");
|
|
183
229
|
}
|
|
184
230
|
|
|
185
231
|
const files: CollectedFile[] = [];
|
|
@@ -223,8 +269,20 @@ export async function collectFiles(root: string): Promise<CollectedFile[]> {
|
|
|
223
269
|
}
|
|
224
270
|
|
|
225
271
|
await walk(root);
|
|
226
|
-
|
|
227
|
-
|
|
272
|
+
const mixed = mixedBackendLayoutError(files.map((file) => file.path));
|
|
273
|
+
if (mixed) {
|
|
274
|
+
throw new Error(mixed);
|
|
275
|
+
}
|
|
276
|
+
if (!files.some((file) => file.path === "robodev/database.ts" || file.path === "database.ts")) {
|
|
277
|
+
throw new Error("database.ts not found (looked in robodev/database.ts and ./database.ts)");
|
|
228
278
|
}
|
|
229
279
|
return files;
|
|
230
280
|
}
|
|
281
|
+
|
|
282
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
283
|
+
try {
|
|
284
|
+
return (await stat(path)).isFile();
|
|
285
|
+
} catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
package/src/config.test.ts
CHANGED
|
@@ -121,7 +121,7 @@ test("writeLink merges frontend and unknown keys", async () => {
|
|
|
121
121
|
},
|
|
122
122
|
root,
|
|
123
123
|
);
|
|
124
|
-
const raw = JSON.parse(await readFile(join(root, "
|
|
124
|
+
const raw = JSON.parse(await readFile(join(root, "robodev/.robodev"), "utf8")) as {
|
|
125
125
|
projectId: string;
|
|
126
126
|
apiUrl: string;
|
|
127
127
|
projectApiUrl: string;
|
|
@@ -148,7 +148,7 @@ test("writeLink preserves frontend false opt-out", async () => {
|
|
|
148
148
|
`${JSON.stringify({ projectId: "prj-1", apiUrl: "https://a.example", frontend: false }, null, 2)}\n`,
|
|
149
149
|
);
|
|
150
150
|
await writeLink({ projectId: "prj-1", apiUrl: "https://b.example" }, root);
|
|
151
|
-
const raw = JSON.parse(await readFile(join(root, "
|
|
151
|
+
const raw = JSON.parse(await readFile(join(root, "robodev/.robodev"), "utf8")) as {
|
|
152
152
|
frontend: unknown;
|
|
153
153
|
};
|
|
154
154
|
assert.equal(raw.frontend, false);
|
|
@@ -158,3 +158,23 @@ test("writeLink preserves frontend false opt-out", async () => {
|
|
|
158
158
|
await rm(root, { recursive: true, force: true });
|
|
159
159
|
}
|
|
160
160
|
});
|
|
161
|
+
|
|
162
|
+
test("readLink prefers robodev/.robodev over a legacy root file", async () => {
|
|
163
|
+
const root = await mkdtemp(join(tmpdir(), "rd-link-pref-"));
|
|
164
|
+
try {
|
|
165
|
+
await writeFile(
|
|
166
|
+
join(root, ".robodev"),
|
|
167
|
+
`${JSON.stringify({ projectId: "legacy", apiUrl: "https://legacy.example" }, null, 2)}\n`,
|
|
168
|
+
);
|
|
169
|
+
await mkdir(join(root, "robodev"), { recursive: true });
|
|
170
|
+
await writeFile(
|
|
171
|
+
join(root, "robodev/.robodev"),
|
|
172
|
+
`${JSON.stringify({ projectId: "canonical", apiUrl: "https://canonical.example" }, null, 2)}\n`,
|
|
173
|
+
);
|
|
174
|
+
const link = await readLink(root);
|
|
175
|
+
assert.equal(link?.projectId, "canonical");
|
|
176
|
+
assert.equal(link?.apiUrl, "https://canonical.example");
|
|
177
|
+
} finally {
|
|
178
|
+
await rm(root, { recursive: true, force: true });
|
|
179
|
+
}
|
|
180
|
+
});
|
package/src/config.ts
CHANGED
|
@@ -44,11 +44,39 @@ export function credentialsPath(): string {
|
|
|
44
44
|
return join(homedir(), ".robodev", "credentials.json");
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/**
|
|
48
|
-
export function
|
|
47
|
+
/** Canonical project link file written by `create` / `link`. */
|
|
48
|
+
export function canonicalLinkPath(cwd = process.cwd()): string {
|
|
49
|
+
return join(cwd, "robodev", ".robodev");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Legacy project link file at the project root. */
|
|
53
|
+
export function legacyLinkPath(cwd = process.cwd()): string {
|
|
49
54
|
return join(cwd, ".robodev");
|
|
50
55
|
}
|
|
51
56
|
|
|
57
|
+
/** Project link file written by `create` / `link`. Always the canonical path. */
|
|
58
|
+
export function linkPath(cwd = process.cwd()): string {
|
|
59
|
+
return canonicalLinkPath(cwd);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function readJsonFile(path: string): Promise<Record<string, unknown> | null> {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(await readFile(path, "utf8")) as unknown;
|
|
65
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
66
|
+
return parsed as Record<string, unknown>;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function readLinkJson(cwd = process.cwd()): Promise<Record<string, unknown> | null> {
|
|
75
|
+
const canonical = await readJsonFile(canonicalLinkPath(cwd));
|
|
76
|
+
if (canonical) return canonical;
|
|
77
|
+
return readJsonFile(legacyLinkPath(cwd));
|
|
78
|
+
}
|
|
79
|
+
|
|
52
80
|
export function defaultFrontendConfig(): FrontendConfig {
|
|
53
81
|
return { root: "apps/fe", script: "build", outDir: "dist" };
|
|
54
82
|
}
|
|
@@ -98,18 +126,6 @@ export async function clearCredentials(): Promise<void> {
|
|
|
98
126
|
await rm(credentialsPath(), { force: true });
|
|
99
127
|
}
|
|
100
128
|
|
|
101
|
-
export async function readLinkJson(cwd = process.cwd()): Promise<Record<string, unknown> | null> {
|
|
102
|
-
try {
|
|
103
|
-
const parsed = JSON.parse(await readFile(linkPath(cwd), "utf8")) as unknown;
|
|
104
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
105
|
-
return parsed as Record<string, unknown>;
|
|
106
|
-
}
|
|
107
|
-
return null;
|
|
108
|
-
} catch {
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
129
|
export async function readLink(cwd = process.cwd()): Promise<ProjectLink | null> {
|
|
114
130
|
const raw = await readLinkJson(cwd);
|
|
115
131
|
if (!raw || typeof raw.projectId !== "string" || typeof raw.apiUrl !== "string") {
|
|
@@ -141,7 +157,9 @@ export async function writeLink(link: ProjectLink, cwd = process.cwd()): Promise
|
|
|
141
157
|
if ("frontend" in link) {
|
|
142
158
|
next.frontend = link.frontend;
|
|
143
159
|
}
|
|
144
|
-
|
|
160
|
+
const dest = canonicalLinkPath(cwd);
|
|
161
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
162
|
+
await writeFile(dest, `${JSON.stringify(next, null, 2)}\n`);
|
|
145
163
|
}
|
|
146
164
|
|
|
147
165
|
const PROJECT_API_ENV_KEYS = ["VITE_API_URL", "VITE_PUBLIC_API_URL", "APP_PUBLIC_API_URL"] as const;
|
package/src/emit-starter.test.ts
CHANGED
|
@@ -66,12 +66,12 @@ test("emitStarter auth-chat FE build runs openapi:gen before vite", async () =>
|
|
|
66
66
|
test("collectFiles for auth-chat starter is Tiny APIs without apps/fe", async () => {
|
|
67
67
|
const files = await collectFiles(join(cliRoot(), "..", "starters", "auth-chat"));
|
|
68
68
|
const paths = new Set(files.map((file) => file.path));
|
|
69
|
-
assert.ok(paths.has("database.ts"));
|
|
70
|
-
assert.ok(paths.has("api/messages.ts"));
|
|
71
|
-
assert.ok(paths.has("api/invite.ts"));
|
|
72
|
-
assert.ok(paths.has("api/health.ts"));
|
|
73
|
-
assert.ok(paths.has("api/me.ts"));
|
|
74
|
-
assert.ok(paths.has("hooks/auth.ts"));
|
|
69
|
+
assert.ok(paths.has("robodev/database.ts"));
|
|
70
|
+
assert.ok(paths.has("robodev/api/messages.ts"));
|
|
71
|
+
assert.ok(paths.has("robodev/api/invite.ts"));
|
|
72
|
+
assert.ok(paths.has("robodev/api/health.ts"));
|
|
73
|
+
assert.ok(paths.has("robodev/api/me.ts"));
|
|
74
|
+
assert.ok(paths.has("robodev/hooks/auth.ts"));
|
|
75
75
|
assert.ok(!paths.has("index.html"));
|
|
76
76
|
assert.ok(!paths.has("src/main.tsx"));
|
|
77
77
|
assert.ok([...paths].every((path) => !path.startsWith("apps/")));
|
|
@@ -102,12 +102,12 @@ test("emitStarter marketplace FE build runs openapi:gen before vite", async () =
|
|
|
102
102
|
test("collectFiles for marketplace starter is Tiny APIs without apps/fe", async () => {
|
|
103
103
|
const files = await collectFiles(join(cliRoot(), "..", "starters", "marketplace"));
|
|
104
104
|
const paths = new Set(files.map((file) => file.path));
|
|
105
|
-
assert.ok(paths.has("database.ts"));
|
|
106
|
-
assert.ok(paths.has("api/listings.ts"));
|
|
107
|
-
assert.ok(paths.has("api/cart.ts"));
|
|
108
|
-
assert.ok(paths.has("api/checkout.ts"));
|
|
109
|
-
assert.ok(paths.has("api/health.ts"));
|
|
110
|
-
assert.ok(paths.has("api/me.ts"));
|
|
105
|
+
assert.ok(paths.has("robodev/database.ts"));
|
|
106
|
+
assert.ok(paths.has("robodev/api/listings.ts"));
|
|
107
|
+
assert.ok(paths.has("robodev/api/cart.ts"));
|
|
108
|
+
assert.ok(paths.has("robodev/api/checkout.ts"));
|
|
109
|
+
assert.ok(paths.has("robodev/api/health.ts"));
|
|
110
|
+
assert.ok(paths.has("robodev/api/me.ts"));
|
|
111
111
|
assert.ok(!paths.has("index.html"));
|
|
112
112
|
assert.ok(!paths.has("src/main.tsx"));
|
|
113
113
|
assert.ok([...paths].every((path) => !path.startsWith("apps/")));
|
|
@@ -117,12 +117,12 @@ test("collectFiles for marketplace starter is Tiny APIs without apps/fe", async
|
|
|
117
117
|
test("collectFiles for space starter is Tiny APIs without apps/fe", async () => {
|
|
118
118
|
const files = await collectFiles(join(cliRoot(), "..", "starters", "space"));
|
|
119
119
|
const paths = new Set(files.map((file) => file.path));
|
|
120
|
-
assert.ok(paths.has("database.ts"));
|
|
121
|
-
assert.ok(paths.has("api/planets.ts"));
|
|
122
|
-
assert.ok(paths.has("api/planets/paginate.ts"));
|
|
123
|
-
assert.ok(paths.has("api/aliens/labels.ts"));
|
|
124
|
-
assert.ok(!paths.has("api/files/upload-request.ts"));
|
|
125
|
-
assert.ok(!paths.has("api/files/upload.ts"));
|
|
120
|
+
assert.ok(paths.has("robodev/database.ts"));
|
|
121
|
+
assert.ok(paths.has("robodev/api/planets.ts"));
|
|
122
|
+
assert.ok(paths.has("robodev/api/planets/paginate.ts"));
|
|
123
|
+
assert.ok(paths.has("robodev/api/aliens/labels.ts"));
|
|
124
|
+
assert.ok(!paths.has("robodev/api/files/upload-request.ts"));
|
|
125
|
+
assert.ok(!paths.has("robodev/api/files/upload.ts"));
|
|
126
126
|
assert.ok(!paths.has("index.html"));
|
|
127
127
|
assert.ok(!paths.has("src/main.tsx"));
|
|
128
128
|
assert.ok([...paths].every((path) => !path.startsWith("apps/")));
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
-
import { mkdir, stat } from "node:fs/promises";
|
|
3
|
+
import { mkdir, readFile, stat } from "node:fs/promises";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
5
|
import { stdin, stdout } from "node:process";
|
|
6
6
|
import { basename, join, relative, resolve } from "node:path";
|
|
@@ -47,7 +47,7 @@ Commands:
|
|
|
47
47
|
whoami Show the signed-in user
|
|
48
48
|
projects List your projects
|
|
49
49
|
create [path] [--starter <id>] [--empty] [--org <id>] Create a project, scaffold a starter, and deploy
|
|
50
|
-
link [projectId] Write
|
|
50
|
+
link [projectId] Write robodev/.robodev in the current folder
|
|
51
51
|
deploy [--force] Deploy schema, APIs, jobs, and frontend (local Vite build when .robodev frontend is set)
|
|
52
52
|
`);
|
|
53
53
|
process.exit(1);
|
|
@@ -238,7 +238,7 @@ async function runDeploy(
|
|
|
238
238
|
const root = options.root ?? process.cwd();
|
|
239
239
|
const link = await readLink(root);
|
|
240
240
|
if (!link) {
|
|
241
|
-
throw new Error("No
|
|
241
|
+
throw new Error("No robodev/.robodev file. Run `robodev link` or `robodev create` first.");
|
|
242
242
|
}
|
|
243
243
|
const files = await collectDeployFiles(root, link, options);
|
|
244
244
|
let force = forceFlag;
|
|
@@ -414,7 +414,13 @@ function packageNameFromPath(root: string): string {
|
|
|
414
414
|
}
|
|
415
415
|
|
|
416
416
|
async function assertCreatable(root: string): Promise<void> {
|
|
417
|
-
for (const file of [
|
|
417
|
+
for (const file of [
|
|
418
|
+
"package.json",
|
|
419
|
+
"database.ts",
|
|
420
|
+
".robodev",
|
|
421
|
+
"robodev/database.ts",
|
|
422
|
+
"robodev/.robodev",
|
|
423
|
+
]) {
|
|
418
424
|
try {
|
|
419
425
|
await stat(join(root, file));
|
|
420
426
|
} catch (error) {
|
|
@@ -425,6 +431,27 @@ async function assertCreatable(root: string): Promise<void> {
|
|
|
425
431
|
}
|
|
426
432
|
}
|
|
427
433
|
|
|
434
|
+
async function npmInstallRobodevDeps(root: string): Promise<void> {
|
|
435
|
+
const pkgPath = join(root, "robodev", "package.json");
|
|
436
|
+
let raw: string;
|
|
437
|
+
try {
|
|
438
|
+
raw = await readFile(pkgPath, "utf8");
|
|
439
|
+
} catch {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
let pkg: { dependencies?: Record<string, string> };
|
|
443
|
+
try {
|
|
444
|
+
pkg = JSON.parse(raw) as { dependencies?: Record<string, string> };
|
|
445
|
+
} catch {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const deps = pkg.dependencies ?? {};
|
|
449
|
+
const extra = Object.keys(deps).filter((name) => name !== "@robodev-ai/sdk");
|
|
450
|
+
if (extra.length === 0) return;
|
|
451
|
+
console.log("Installing robodev/ npm packages…");
|
|
452
|
+
await npmInstall(join(root, "robodev"));
|
|
453
|
+
}
|
|
454
|
+
|
|
428
455
|
async function npmInstall(root: string): Promise<void> {
|
|
429
456
|
console.log("Installing npm packages…");
|
|
430
457
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -520,6 +547,7 @@ async function runCreate(args: string[]): Promise<void> {
|
|
|
520
547
|
|
|
521
548
|
try {
|
|
522
549
|
await npmInstall(root);
|
|
550
|
+
await npmInstallRobodevDeps(root);
|
|
523
551
|
} catch (error) {
|
|
524
552
|
console.log(error instanceof Error ? error.message : error);
|
|
525
553
|
if (starter.kind === "backend") {
|
|
@@ -23,7 +23,7 @@ For generated mutations, pass invalidation through the mutation options instead
|
|
|
23
23
|
|
|
24
24
|
After mutations, derive follow-up UI state from canonical API response or query state instead of leaving action controls unchanged. Disable duplicate submissions when the API state already represents a pending or completed action, and surface mutation errors through the default generated-client error handler or a feature-specific message.
|
|
25
25
|
|
|
26
|
-
Do not import `@robodev-ai/sdk`, `database.ts`, Drizzle tables, or API helper modules into frontend features.
|
|
26
|
+
Do not import `@robodev-ai/sdk`, `robodev/database.ts`, Drizzle tables, or API helper modules into frontend features.
|
|
27
27
|
|
|
28
28
|
`apps/fe/src/openapi` is generated by `@povio/openapi-codegen-cli` from root `openapi.json` (or a live project `/openapi.json` when `OPENAPI_LIVE=true`). Do not hand-edit generated OpenAPI client files.
|
|
29
29
|
|
|
@@ -11,7 +11,7 @@ cursor:
|
|
|
11
11
|
|
|
12
12
|
# Frontend Notifications
|
|
13
13
|
|
|
14
|
-
Use generated `@/openapi` queries, mutations, and models for notification history, preferences, and push-token registration. Do not import `@robodev-ai/sdk` or `database.ts` into frontend feature code.
|
|
14
|
+
Use generated `@/openapi` queries, mutations, and models for notification history, preferences, and push-token registration. Do not import `@robodev-ai/sdk` or `robodev/database.ts` into frontend feature code.
|
|
15
15
|
|
|
16
16
|
Request push permission only after an explanatory UI and explicit user gesture. Never request it automatically during initial application load. Handle unsupported, default, denied, granted, registered, expired, and failed-registration states.
|
|
17
17
|
|
|
@@ -11,19 +11,19 @@ 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
|
|
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
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"`), 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`.
|
|
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 `robodev/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
|
|
|
24
24
|
Use the more specific generated rules for:
|
|
25
25
|
|
|
26
|
-
- Robodev `defineDatabase` tables in `database.ts`.
|
|
26
|
+
- Robodev `defineDatabase` tables in `robodev/database.ts`.
|
|
27
27
|
- Robodev `defineApi` routes in `api/`.
|
|
28
28
|
- Reserved `/api/user/*` auth contracts (do not implement `api/user/**` in the project).
|
|
29
29
|
- Frontend API boundaries around generated `@/openapi` queries and models.
|
|
@@ -2,24 +2,24 @@
|
|
|
2
2
|
root: false
|
|
3
3
|
targets: ["claudecode", "codexcli", "cursor"]
|
|
4
4
|
description: "Robodev defineApi file routes and handler conventions."
|
|
5
|
-
globs: ["api/**/*.ts"]
|
|
5
|
+
globs: ["robodev/api/**/*.ts"]
|
|
6
6
|
cursor:
|
|
7
7
|
alwaysApply: false
|
|
8
8
|
description: "Apply when editing Starbase API routes."
|
|
9
|
-
globs: ["api/**/*.ts"]
|
|
9
|
+
globs: ["robodev/api/**/*.ts"]
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# Robodev API
|
|
13
13
|
|
|
14
|
-
Project APIs live in `api/`. `api/planets.ts` is served at `/planets` and `/api/planets`. Prefer the `/api/...` path in OpenAPI and frontend clients.
|
|
14
|
+
Project APIs live in `robodev/api/`. `robodev/api/planets.ts` is served at `/planets` and `/api/planets`. Prefer the `/api/...` path in OpenAPI and frontend clients.
|
|
15
15
|
|
|
16
16
|
## File layout
|
|
17
17
|
|
|
18
|
-
- `api/foo.ts` → `/api/foo`
|
|
19
|
-
- `api/foo/[id].ts` → `/api/foo/:id`
|
|
18
|
+
- `robodev/api/foo.ts` → `/api/foo`
|
|
19
|
+
- `robodev/api/foo/[id].ts` → `/api/foo/:id`
|
|
20
20
|
- Export `get` / `post` / `put` / `patch` / `delete` as `defineApi(...)`.
|
|
21
|
-
- Shared helpers belong in `api/_lib.ts`. Do not export `defineApi` from `_lib.ts`.
|
|
22
|
-
- Do not add `api/user/**`. Those paths are reserved for Robodev Auth.
|
|
21
|
+
- Shared helpers belong in `robodev/api/_lib.ts`. Do not export `defineApi` from `_lib.ts`.
|
|
22
|
+
- Do not add `robodev/api/user/**`. Those paths are reserved for Robodev Auth.
|
|
23
23
|
|
|
24
24
|
## Handlers
|
|
25
25
|
|
|
@@ -8,8 +8,8 @@ cursor:
|
|
|
8
8
|
description: "Apply when touching auth, login, register, or /api/user clients."
|
|
9
9
|
globs:
|
|
10
10
|
[
|
|
11
|
-
"database.ts",
|
|
12
|
-
"api/**/*.ts",
|
|
11
|
+
"robodev/database.ts",
|
|
12
|
+
"robodev/api/**/*.ts",
|
|
13
13
|
"hooks/**/*.ts",
|
|
14
14
|
"apps/fe/src/components/features/auth/**/*.{ts,tsx}",
|
|
15
15
|
"apps/fe/src/clients/**/*.ts",
|
|
@@ -2,21 +2,21 @@
|
|
|
2
2
|
root: false
|
|
3
3
|
targets: ["claudecode", "codexcli", "cursor"]
|
|
4
4
|
description: "Robodev database.ts Drizzle schema and deploy sync."
|
|
5
|
-
globs: ["database.ts"]
|
|
5
|
+
globs: ["robodev/database.ts"]
|
|
6
6
|
cursor:
|
|
7
7
|
alwaysApply: false
|
|
8
8
|
description: "Apply when editing the Starbase database schema."
|
|
9
|
-
globs: ["database.ts"]
|
|
9
|
+
globs: ["robodev/database.ts"]
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# Robodev Database
|
|
13
13
|
|
|
14
|
-
The backend schema lives in `database.ts
|
|
14
|
+
The backend schema lives in `robodev/database.ts`. Default-export `defineDatabase` with Drizzle `pgTable` definitions. `robodev deploy` from this root creates or syncs the named Postgres database. There are no migration files.
|
|
15
15
|
|
|
16
16
|
## Rules
|
|
17
17
|
|
|
18
18
|
- Import table helpers from `@robodev-ai/sdk` (`pgTable`, `uuid`, `text`, `timestamp`, `integer`, and query helpers).
|
|
19
19
|
- Do not recreate User or auth tables. End users live in platform `robodev_auth`. Store `userId` as `text` matching `ctx.user.id`.
|
|
20
20
|
- Prefer scalar columns and foreign keys. Destructive drops need deploy confirmation or `--force`.
|
|
21
|
-
- Export named tables so API files can import them from `../database` or `api/_lib.ts`.
|
|
22
|
-
- Seed demo rows in API handlers on first read (see `api/_lib.ts` `ensureSeed`), not with a separate seed runner.
|
|
21
|
+
- Export named tables so API files can import them from `../database` or `robodev/api/_lib.ts`.
|
|
22
|
+
- Seed demo rows in API handlers on first read (see `robodev/api/_lib.ts` `ensureSeed`), not with a separate seed runner.
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
root: false
|
|
3
3
|
targets: ["claudecode", "codexcli", "cursor"]
|
|
4
4
|
description: "Conditional role-based frontend and API structure for apps with distinct user roles."
|
|
5
|
-
globs: ["apps/fe/src/**/*.{ts,tsx}", "api/**/*.ts"]
|
|
5
|
+
globs: ["apps/fe/src/**/*.{ts,tsx}", "robodev/api/**/*.ts"]
|
|
6
6
|
cursor:
|
|
7
7
|
alwaysApply: false
|
|
8
8
|
description: "Apply only when an app has multiple user roles with distinct UI surfaces or business workflows."
|
|
9
|
-
globs: ["apps/fe/src/**/*.{ts,tsx}", "api/**/*.ts"]
|
|
9
|
+
globs: ["apps/fe/src/**/*.{ts,tsx}", "robodev/api/**/*.ts"]
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# Role-Based App Structure
|
|
@@ -18,4 +18,4 @@ Preserve the upload contract:
|
|
|
18
18
|
|
|
19
19
|
Never use a standalone files API. File bytes stay on the device until submit.
|
|
20
20
|
|
|
21
|
-
Media metadata lives in the `media` table in `database.ts`. Bytes live in Robodev storage. Planet images use resource name `planet-image`.
|
|
21
|
+
Media metadata lives in the `media` table in `robodev/database.ts`. Bytes live in Robodev storage. Planet images use resource name `planet-image`.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: robodev-api-route
|
|
3
|
-
description: Add a defineApi route under api/.
|
|
3
|
+
description: Add a defineApi route under robodev/api/.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Add a Robodev API route
|
|
7
7
|
|
|
8
|
-
1. Create `api/<path>.ts` or `api/<path>/[id].ts`.
|
|
8
|
+
1. Create `robodev/api/<path>.ts` or `robodev/api/<path>/[id].ts`.
|
|
9
9
|
2. Export HTTP methods with `defineApi` from `@robodev-ai/sdk`.
|
|
10
10
|
3. Use `auth: "required"` unless the route is public.
|
|
11
11
|
4. Keep JSON shapes aligned with `openapi.json`.
|
|
12
|
-
5. Do not create `api/user/**` files.
|
|
12
|
+
5. Do not create `robodev/api/user/**` files.
|
|
13
13
|
6. Deploy from the project root (`robodev deploy`), then `bun openapi:gen` in `apps/fe` if the contract changed.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: robodev-table
|
|
3
|
-
description: Add a Drizzle table to database.ts and wire it into defineApi handlers.
|
|
3
|
+
description: Add a Drizzle table to robodev/database.ts and wire it into defineApi handlers.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Add a Robodev table
|
|
7
7
|
|
|
8
|
-
1. Add a `pgTable` to `database.ts` and include it in `defineDatabase({ tables })`.
|
|
8
|
+
1. Add a `pgTable` to `robodev/database.ts` and include it in `defineDatabase({ tables })`.
|
|
9
9
|
2. Do not add a User table. Auth users are platform-managed; store `userId` as text.
|
|
10
10
|
3. Import the table in `api/_lib.ts` or the route file that needs it.
|
|
11
11
|
4. Deploy from the project root with `robodev deploy`. Destructive drops need confirmation or `--force`.
|