robodev 0.27.0 → 0.28.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.
Files changed (102) hide show
  1. package/package.json +1 -1
  2. package/src/collect-files.test.ts +62 -0
  3. package/src/collect-files.ts +58 -10
  4. package/src/config.test.ts +22 -2
  5. package/src/config.ts +33 -15
  6. package/src/emit-starter.test.ts +18 -18
  7. package/src/index.ts +32 -4
  8. package/templates/auth-chat/.rulesync/rules/frontend-api-boundary.md +1 -1
  9. package/templates/auth-chat/.rulesync/rules/frontend-notifications.md +1 -1
  10. package/templates/auth-chat/.rulesync/rules/project-overview.md +3 -3
  11. package/templates/auth-chat/.rulesync/rules/robodev-api.md +7 -7
  12. package/templates/auth-chat/.rulesync/rules/robodev-auth.md +2 -2
  13. package/templates/auth-chat/.rulesync/rules/robodev-database.md +5 -5
  14. package/templates/auth-chat/.rulesync/rules/role-based-app-structure.md +2 -2
  15. package/templates/auth-chat/.rulesync/skills/media-feature/SKILL.md +1 -1
  16. package/templates/auth-chat/.rulesync/skills/robodev-api-route/SKILL.md +3 -3
  17. package/templates/auth-chat/.rulesync/skills/robodev-table/SKILL.md +2 -2
  18. package/templates/auth-chat/README.md +1 -1
  19. package/templates/auth-chat/apps/fe/package.json +4 -4
  20. package/templates/auth-chat/package.json +1 -1
  21. package/templates/auth-chat/robodev/package.json +4 -0
  22. package/templates/auth-chat/tsconfig.json +1 -1
  23. package/templates/backend/README.md +1 -1
  24. package/templates/backend/package.json +1 -1
  25. package/templates/backend/robodev/package.json +4 -0
  26. package/templates/backend/tsconfig.json +1 -1
  27. package/templates/empty/README.md +2 -2
  28. package/templates/empty/package.json +1 -1
  29. package/templates/empty/robodev/package.json +4 -0
  30. package/templates/empty/tsconfig.json +1 -1
  31. package/templates/marketplace/.rulesync/rules/frontend-api-boundary.md +1 -1
  32. package/templates/marketplace/.rulesync/rules/frontend-notifications.md +1 -1
  33. package/templates/marketplace/.rulesync/rules/project-overview.md +2 -2
  34. package/templates/marketplace/.rulesync/rules/robodev-api.md +7 -7
  35. package/templates/marketplace/.rulesync/rules/robodev-auth.md +2 -2
  36. package/templates/marketplace/.rulesync/rules/robodev-database.md +5 -5
  37. package/templates/marketplace/.rulesync/rules/role-based-app-structure.md +2 -2
  38. package/templates/marketplace/.rulesync/skills/media-feature/SKILL.md +1 -1
  39. package/templates/marketplace/.rulesync/skills/robodev-api-route/SKILL.md +3 -3
  40. package/templates/marketplace/.rulesync/skills/robodev-table/SKILL.md +2 -2
  41. package/templates/marketplace/README.md +2 -2
  42. package/templates/marketplace/apps/fe/package.json +4 -4
  43. package/templates/marketplace/package.json +1 -1
  44. package/templates/marketplace/robodev/package.json +4 -0
  45. package/templates/marketplace/tsconfig.json +1 -1
  46. package/templates/space/.rulesync/rules/frontend-api-boundary.md +1 -1
  47. package/templates/space/.rulesync/rules/frontend-notifications.md +1 -1
  48. package/templates/space/.rulesync/rules/project-overview.md +2 -2
  49. package/templates/space/.rulesync/rules/robodev-api.md +7 -7
  50. package/templates/space/.rulesync/rules/robodev-auth.md +2 -2
  51. package/templates/space/.rulesync/rules/robodev-database.md +5 -5
  52. package/templates/space/.rulesync/rules/role-based-app-structure.md +2 -2
  53. package/templates/space/.rulesync/skills/media-feature/SKILL.md +1 -1
  54. package/templates/space/.rulesync/skills/robodev-api-route/SKILL.md +3 -3
  55. package/templates/space/.rulesync/skills/robodev-table/SKILL.md +2 -2
  56. package/templates/space/README.md +2 -2
  57. package/templates/space/apps/fe/package.json +4 -4
  58. package/templates/space/package.json +1 -1
  59. package/templates/space/robodev/package.json +4 -0
  60. package/templates/space/tsconfig.json +1 -1
  61. /package/templates/auth-chat/{api → robodev/api}/health.ts +0 -0
  62. /package/templates/auth-chat/{api → robodev/api}/invite.ts +0 -0
  63. /package/templates/auth-chat/{api → robodev/api}/me.ts +0 -0
  64. /package/templates/auth-chat/{api → robodev/api}/messages.ts +0 -0
  65. /package/templates/auth-chat/{database.ts → robodev/database.ts} +0 -0
  66. /package/templates/auth-chat/{hooks → robodev/hooks}/auth.ts +0 -0
  67. /package/templates/backend/{api → robodev/api}/_lib.ts +0 -0
  68. /package/templates/backend/{api → robodev/api}/aliens/labels.ts +0 -0
  69. /package/templates/backend/{api → robodev/api}/planets/[id]/like.ts +0 -0
  70. /package/templates/backend/{api → robodev/api}/planets/[id].ts +0 -0
  71. /package/templates/backend/{api → robodev/api}/planets/paginate.ts +0 -0
  72. /package/templates/backend/{api → robodev/api}/planets.ts +0 -0
  73. /package/templates/backend/{database.ts → robodev/database.ts} +0 -0
  74. /package/templates/empty/{api → robodev/api}/health.ts +0 -0
  75. /package/templates/empty/{database.ts → robodev/database.ts} +0 -0
  76. /package/templates/marketplace/{api → robodev/api}/_lib.seed.test.ts +0 -0
  77. /package/templates/marketplace/{api → robodev/api}/_lib.ts +0 -0
  78. /package/templates/marketplace/{api → robodev/api}/cart/[id].ts +0 -0
  79. /package/templates/marketplace/{api → robodev/api}/cart.ts +0 -0
  80. /package/templates/marketplace/{api → robodev/api}/categories.ts +0 -0
  81. /package/templates/marketplace/{api → robodev/api}/checkout.ts +0 -0
  82. /package/templates/marketplace/{api → robodev/api}/health.ts +0 -0
  83. /package/templates/marketplace/{api → robodev/api}/listings/[id].ts +0 -0
  84. /package/templates/marketplace/{api → robodev/api}/listings/paginate.ts +0 -0
  85. /package/templates/marketplace/{api → robodev/api}/listings.ts +0 -0
  86. /package/templates/marketplace/{api → robodev/api}/me.ts +0 -0
  87. /package/templates/marketplace/{api → robodev/api}/orders/[id]/complete.ts +0 -0
  88. /package/templates/marketplace/{api → robodev/api}/orders/[id]/ship.ts +0 -0
  89. /package/templates/marketplace/{api → robodev/api}/orders/[id].ts +0 -0
  90. /package/templates/marketplace/{api → robodev/api}/purchases.ts +0 -0
  91. /package/templates/marketplace/{api → robodev/api}/reviews.ts +0 -0
  92. /package/templates/marketplace/{api → robodev/api}/sales.ts +0 -0
  93. /package/templates/marketplace/{api → robodev/api}/watches/[listingId].ts +0 -0
  94. /package/templates/marketplace/{api → robodev/api}/watches.ts +0 -0
  95. /package/templates/marketplace/{database.ts → robodev/database.ts} +0 -0
  96. /package/templates/space/{api → robodev/api}/_lib.ts +0 -0
  97. /package/templates/space/{api → robodev/api}/aliens/labels.ts +0 -0
  98. /package/templates/space/{api → robodev/api}/planets/[id]/like.ts +0 -0
  99. /package/templates/space/{api → robodev/api}/planets/[id].ts +0 -0
  100. /package/templates/space/{api → robodev/api}/planets/paginate.ts +0 -0
  101. /package/templates/space/{api → robodev/api}/planets.ts +0 -0
  102. /package/templates/space/{database.ts → robodev/database.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robodev",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "CLI for Robodev Starbase — create, auth, link, and deploy hosted apps",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -7,6 +7,54 @@ 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/hooks"), { recursive: true });
15
+ await writeFile(join(root, "robodev/database.ts"), "export default {}");
16
+ await writeFile(join(root, "robodev/package.json"), '{"private":true,"dependencies":{}}');
17
+ await writeFile(join(root, "robodev/.robodev"), "nope");
18
+ await writeFile(join(root, "index.html"), "<html></html>");
19
+ await mkdir(join(root, "src"), { recursive: true });
20
+ await writeFile(join(root, "src/main.tsx"), "export {}");
21
+ await writeFile(join(root, "robodev/api/x.ts"), "export {}");
22
+ await writeFile(join(root, "robodev/api/invoices/[id].ts"), "export {}");
23
+ await writeFile(join(root, "robodev/jobs/x.ts"), "export {}");
24
+ await writeFile(join(root, "robodev/hooks/auth.ts"), "export {}");
25
+ await writeFile(join(root, "robodev/hooks/lib.ts"), "export {}");
26
+ await writeFile(join(root, "package.json"), "{}");
27
+ await mkdir(join(root, "node_modules"), { recursive: true });
28
+ await writeFile(join(root, "node_modules/x.js"), "nope");
29
+ await mkdir(join(root, "dist"), { recursive: true });
30
+ await writeFile(join(root, "dist/x.js"), "nope");
31
+ await writeFile(join(root, ".env"), "SECRET=1");
32
+ await mkdir(join(root, ".robodev"), { recursive: true });
33
+ await writeFile(join(root, ".robodev/x"), "nope");
34
+
35
+ const files = await collectFiles(root);
36
+ const paths = new Set(files.map((file) => file.path));
37
+ assert.ok(paths.has("index.html"));
38
+ assert.ok(paths.has("src/main.tsx"));
39
+ assert.ok(paths.has("robodev/database.ts"));
40
+ assert.ok(paths.has("robodev/package.json"));
41
+ assert.ok(paths.has("robodev/api/x.ts"));
42
+ assert.ok(paths.has("robodev/api/invoices/[id].ts"));
43
+ assert.ok(paths.has("robodev/jobs/x.ts"));
44
+ assert.ok(paths.has("robodev/hooks/auth.ts"));
45
+ assert.ok(paths.has("robodev/hooks/lib.ts"));
46
+ assert.ok(paths.has("package.json"));
47
+ assert.ok(!paths.has("node_modules/x.js"));
48
+ assert.ok(!paths.has("dist/x.js"));
49
+ assert.ok(!paths.has(".env"));
50
+ assert.ok(!paths.has(".robodev/x"));
51
+ assert.ok(!paths.has("robodev/.robodev"));
52
+ } finally {
53
+ await rm(root, { recursive: true, force: true });
54
+ }
55
+ });
56
+
57
+ test("collectFiles still accepts a legacy root database.ts tree", async () => {
10
58
  const root = await mkdtemp(join(tmpdir(), "rd-cli-"));
11
59
  try {
12
60
  await writeFile(join(root, "database.ts"), "export default {}");
@@ -78,3 +126,17 @@ test("collectFiles skips nested apps/fe sources", async () => {
78
126
  await rm(root, { recursive: true, force: true });
79
127
  }
80
128
  });
129
+
130
+ test("collectFiles rejects mixed backend layouts", async () => {
131
+ const root = await mkdtemp(join(tmpdir(), "rd-cli-mix-"));
132
+ try {
133
+ await mkdir(join(root, "robodev"), { recursive: true });
134
+ await mkdir(join(root, "api"), { recursive: true });
135
+ await writeFile(join(root, "robodev/database.ts"), "export default {}");
136
+ await writeFile(join(root, "database.ts"), "export default {}");
137
+ await writeFile(join(root, "api/x.ts"), "export {}");
138
+ await assert.rejects(() => collectFiles(root), /mix robodev/);
139
+ } finally {
140
+ await rm(root, { recursive: true, force: true });
141
+ }
142
+ });
@@ -85,22 +85,41 @@ 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 (inner.startsWith("api/") || inner.startsWith("jobs/") || inner.startsWith("hooks/")) {
115
+ return inner.endsWith(".ts");
116
+ }
117
+ return path === "robodev/package.json";
118
+ }
119
+
99
120
  function isAllowed(path: string): boolean {
100
- if (path === "database.ts") return true;
101
- if (path.startsWith("api/")) return path.endsWith(".ts");
102
- if (path.startsWith("jobs/")) return path.endsWith(".ts");
103
- if (path.startsWith("hooks/")) return path.endsWith(".ts");
121
+ if (isBackendPath(path)) return true;
122
+ if (path.startsWith("robodev/")) return false;
104
123
  if (path === "index.html") return true;
105
124
  if (path.startsWith("src/")) return SRC_EXTENSIONS.has(extname(path));
106
125
  if (path.startsWith("public/")) return PUBLIC_EXTENSIONS.has(extname(path));
@@ -110,6 +129,23 @@ function isAllowed(path: string): boolean {
110
129
  return false;
111
130
  }
112
131
 
132
+ export function mixedBackendLayoutError(paths: Iterable<string>): string | null {
133
+ const list = [...paths].map(normalize);
134
+ const hasNewDb = list.includes("robodev/database.ts");
135
+ const hasLegacyDb = list.includes("database.ts");
136
+ const hasLegacyApiJobs = list.some((path) => path.startsWith("api/") || path.startsWith("jobs/"));
137
+ const hasNewApiJobs = list.some(
138
+ (path) => path.startsWith("robodev/api/") || path.startsWith("robodev/jobs/"),
139
+ );
140
+ if (hasNewDb && (hasLegacyDb || hasLegacyApiJobs)) {
141
+ return "Do not mix robodev/ backend files with root database.ts, api/, or jobs/";
142
+ }
143
+ if (hasLegacyDb && (hasNewDb || hasNewApiJobs)) {
144
+ return "Do not mix robodev/ backend files with root database.ts, api/, or jobs/";
145
+ }
146
+ return null;
147
+ }
148
+
113
149
  export function shouldCollectPath(path: string): boolean {
114
150
  const normalized = normalize(path);
115
151
  return !isUnsafe(normalized) && !isDenied(normalized) && isAllowed(normalized);
@@ -176,10 +212,10 @@ export function mergeCollectedFiles(
176
212
  }
177
213
 
178
214
  export async function collectFiles(root: string): Promise<CollectedFile[]> {
179
- try {
180
- await stat(join(root, "database.ts"));
181
- } catch {
182
- throw new Error("database.ts not found in the current directory");
215
+ const hasNew = await fileExists(join(root, "robodev/database.ts"));
216
+ const hasLegacy = await fileExists(join(root, "database.ts"));
217
+ if (!hasNew && !hasLegacy) {
218
+ throw new Error("database.ts not found (looked in robodev/database.ts and ./database.ts)");
183
219
  }
184
220
 
185
221
  const files: CollectedFile[] = [];
@@ -223,8 +259,20 @@ export async function collectFiles(root: string): Promise<CollectedFile[]> {
223
259
  }
224
260
 
225
261
  await walk(root);
226
- if (!files.some((file) => file.path === "database.ts")) {
227
- throw new Error("database.ts not found in the current directory");
262
+ const mixed = mixedBackendLayoutError(files.map((file) => file.path));
263
+ if (mixed) {
264
+ throw new Error(mixed);
265
+ }
266
+ if (!files.some((file) => file.path === "robodev/database.ts" || file.path === "database.ts")) {
267
+ throw new Error("database.ts not found (looked in robodev/database.ts and ./database.ts)");
228
268
  }
229
269
  return files;
230
270
  }
271
+
272
+ async function fileExists(path: string): Promise<boolean> {
273
+ try {
274
+ return (await stat(path)).isFile();
275
+ } catch {
276
+ return false;
277
+ }
278
+ }
@@ -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, ".robodev"), "utf8")) as {
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, ".robodev"), "utf8")) as {
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
- /** Project link file written by `create` / `link`. */
48
- export function linkPath(cwd = process.cwd()): string {
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
- await writeFile(linkPath(cwd), `${JSON.stringify(next, null, 2)}\n`);
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;
@@ -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 .robodev in the current folder
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 .robodev file. Run `robodev link` or `robodev create` first.");
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 ["package.json", "database.ts", ".robodev"]) {
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 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.
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` at the project root. Default-export `defineDatabase` with Drizzle `pgTable` definitions. `robodev deploy` from this root creates or syncs the named Postgres database. There are no migration files.
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`.
@@ -29,7 +29,7 @@ npx robodev deploy
29
29
 
30
30
  Dashboard create deploys schema + APIs only. The Tiny FE tree is copied by `robodev create`; a later CLI deploy hosts the SPA.
31
31
 
32
- `GET /api/me`, `GET/POST /api/messages`, and `POST /api/invite` require a Robodev Auth Bearer token. `GET /api/health` stays public. Invites send one email; posting a chat message does not. Auth stays reserved at `/api/user/*`. Do not add `api/user/**`.
32
+ `GET /api/me`, `GET/POST /api/messages`, and `POST /api/invite` require a Robodev Auth Bearer token. `GET /api/health` stays public. Invites send one email; posting a chat message does not. Auth stays reserved at `/api/user/*`. Do not add `robodev/api/user/**`.
33
33
 
34
34
  Regenerate AI rules after editing `.rulesync/`:
35
35
 
@@ -8,10 +8,10 @@
8
8
  "dev": "bunx --bun vite",
9
9
  "build": "bun run openapi:gen && bunx --bun vite build",
10
10
  "clean": "rimraf dist src/openapi",
11
- "lint": "oxlint --type-aware . ../../api ../../database.ts --fix",
12
- "lint:check": "oxlint --type-aware . ../../api ../../database.ts --quiet",
13
- "format": "oxfmt -c \"../../oxfmt.config.js\" . ../../api ../../database.ts",
14
- "format:check": "oxfmt -c \"../../oxfmt.config.js\" --check . ../../api ../../database.ts",
11
+ "lint": "oxlint --type-aware . ../../robodev/api ../../robodev/database.ts --fix",
12
+ "lint:check": "oxlint --type-aware . ../../robodev/api ../../robodev/database.ts --quiet",
13
+ "format": "oxfmt -c \"../../oxfmt.config.js\" . ../../robodev/api ../../robodev/database.ts",
14
+ "format:check": "oxfmt -c \"../../oxfmt.config.js\" --check . ../../robodev/api ../../robodev/database.ts",
15
15
  "ts:check": "tsc -b --noEmit",
16
16
  "test": "vitest run",
17
17
  "check": "bun ts:check && bun lint:check && bun format:check",