robodev 0.10.0 → 0.12.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 (197) hide show
  1. package/package.json +1 -1
  2. package/src/collect-files.test.ts +19 -0
  3. package/src/collect-files.ts +1 -0
  4. package/src/config.test.ts +26 -0
  5. package/src/config.ts +34 -5
  6. package/src/emit-starter.test.ts +54 -0
  7. package/src/emit-starter.ts +47 -13
  8. package/src/index.ts +37 -4
  9. package/templates/auth-chat/README.md +1 -1
  10. package/templates/auth-chat/api/health.ts +1 -1
  11. package/templates/auth-chat/api/me.ts +1 -1
  12. package/templates/auth-chat/api/messages.ts +1 -1
  13. package/templates/auth-chat/package.json +1 -1
  14. package/templates/auth-chat/src/main.tsx +5 -5
  15. package/templates/backend/README.md +1 -1
  16. package/templates/backend/api/health.ts +1 -1
  17. package/templates/backend/api/launch.ts +1 -1
  18. package/templates/backend/api/me.ts +1 -1
  19. package/templates/backend/api/motto.ts +1 -1
  20. package/templates/backend/api/planets/[id].ts +1 -1
  21. package/templates/backend/api/planets.ts +1 -1
  22. package/templates/backend/api/rockets.ts +1 -1
  23. package/templates/backend/package.json +1 -1
  24. package/templates/catalog.json +2 -2
  25. package/templates/empty/README.md +1 -1
  26. package/templates/empty/api/health.ts +1 -1
  27. package/templates/empty/package.json +1 -1
  28. package/templates/space/.config/local.spa.template.yml +27 -0
  29. package/templates/space/.oxlint-base.json +29 -0
  30. package/templates/space/.oxlintrc.json +78 -0
  31. package/templates/space/.rulesync/rules/frontend-api-boundary.md +36 -0
  32. package/templates/space/.rulesync/rules/frontend-component-structure.md +38 -0
  33. package/templates/space/.rulesync/rules/frontend-forms.md +32 -0
  34. package/templates/space/.rulesync/rules/frontend-notifications.md +24 -0
  35. package/templates/space/.rulesync/rules/frontend-povio-components.md +42 -0
  36. package/templates/space/.rulesync/rules/frontend-povio-ui.md +45 -0
  37. package/templates/space/.rulesync/rules/frontend-query-autocomplete.md +29 -0
  38. package/templates/space/.rulesync/rules/frontend-tables-and-lists.md +34 -0
  39. package/templates/space/.rulesync/rules/frontend-translations.md +26 -0
  40. package/templates/space/.rulesync/rules/project-overview.md +32 -0
  41. package/templates/space/.rulesync/rules/robodev-api.md +30 -0
  42. package/templates/space/.rulesync/rules/robodev-auth.md +40 -0
  43. package/templates/space/.rulesync/rules/robodev-database.md +22 -0
  44. package/templates/space/.rulesync/rules/role-based-app-structure.md +36 -0
  45. package/templates/space/.rulesync/skills/media-feature/SKILL.md +21 -0
  46. package/templates/space/.rulesync/skills/povio-ui-styling/SKILL.md +208 -0
  47. package/templates/space/.rulesync/skills/povio-ui-styling/agents/openai.yaml +4 -0
  48. package/templates/space/.rulesync/skills/robodev-api-route/SKILL.md +13 -0
  49. package/templates/space/.rulesync/skills/robodev-table/SKILL.md +12 -0
  50. package/templates/space/README.md +26 -7
  51. package/templates/space/api/_lib.ts +359 -0
  52. package/templates/space/api/aliens/labels.ts +19 -0
  53. package/templates/space/api/files/presigned-url.ts +48 -0
  54. package/templates/space/api/files/upload.ts +29 -0
  55. package/templates/space/api/planets/[id]/like.ts +36 -0
  56. package/templates/space/api/planets/[id].ts +56 -13
  57. package/templates/space/api/planets/paginate.ts +36 -0
  58. package/templates/space/api/planets.ts +52 -44
  59. package/templates/space/apps/fe/index.html +24 -0
  60. package/templates/space/apps/fe/openapi-codegen.config.ts +37 -0
  61. package/templates/space/apps/fe/package.json +82 -0
  62. package/templates/space/apps/fe/public/apple-touch-icon.png +0 -0
  63. package/templates/space/apps/fe/public/favicon-96x96.png +0 -0
  64. package/templates/space/apps/fe/public/favicon.ico +0 -0
  65. package/templates/space/apps/fe/public/favicon.svg +3 -0
  66. package/templates/space/apps/fe/public/site.webmanifest +21 -0
  67. package/templates/space/apps/fe/public/web-app-manifest-192x192.png +0 -0
  68. package/templates/space/apps/fe/public/web-app-manifest-512x512.png +0 -0
  69. package/templates/space/apps/fe/src/assets/fonts/GeneralSans-Bold.otf +0 -0
  70. package/templates/space/apps/fe/src/assets/fonts/GeneralSans-Medium.otf +0 -0
  71. package/templates/space/apps/fe/src/assets/fonts/GeneralSans-Regular.otf +0 -0
  72. package/templates/space/apps/fe/src/assets/fonts/GeneralSans-Semibold.otf +0 -0
  73. package/templates/space/apps/fe/src/assets/locales/en/translation.json +301 -0
  74. package/templates/space/apps/fe/src/assets/locales/sl/translation.json +301 -0
  75. package/templates/space/apps/fe/src/clients/app-rest-client.ts +15 -0
  76. package/templates/space/apps/fe/src/clients/auth-token-store.ts +101 -0
  77. package/templates/space/apps/fe/src/clients/rest/app-error-handler.ts +31 -0
  78. package/templates/space/apps/fe/src/clients/rest/interceptors/authorization-header.interceptor.ts +15 -0
  79. package/templates/space/apps/fe/src/clients/rest/interceptors/refresh-token.interceptor.ts +37 -0
  80. package/templates/space/apps/fe/src/clients/rest/interceptors/response.interceptor.ts +17 -0
  81. package/templates/space/apps/fe/src/components/404.tsx +18 -0
  82. package/templates/space/apps/fe/src/components/features/auth/AuthBrandPanel.tsx +60 -0
  83. package/templates/space/apps/fe/src/components/features/auth/AuthLayout.tsx +23 -0
  84. package/templates/space/apps/fe/src/components/features/auth/LoginPage.tsx +111 -0
  85. package/templates/space/apps/fe/src/components/features/auth/RegisterPage.tsx +143 -0
  86. package/templates/space/apps/fe/src/components/features/home/HomePage.tsx +21 -0
  87. package/templates/space/apps/fe/src/components/features/planets/details/PlanetDetailsPage.tsx +139 -0
  88. package/templates/space/apps/fe/src/components/features/planets/details/PlanetEditPage.tsx +266 -0
  89. package/templates/space/apps/fe/src/components/features/planets/feed/PlanetFeedCard.tsx +131 -0
  90. package/templates/space/apps/fe/src/components/features/planets/feed/PlanetsFeedPage.tsx +119 -0
  91. package/templates/space/apps/fe/src/components/features/planets/list/PlanetCard.tsx +89 -0
  92. package/templates/space/apps/fe/src/components/features/planets/list/PlanetCreateModal.tsx +179 -0
  93. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsFilters.css +3 -0
  94. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsFilters.tsx +73 -0
  95. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsGridPage.tsx +51 -0
  96. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsInfiniteTablePage.tsx +79 -0
  97. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsPage.tsx +65 -0
  98. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsTablePage.tsx +72 -0
  99. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsTableSummary.tsx +29 -0
  100. package/templates/space/apps/fe/src/components/features/planets/list/PlanetsViewTabs.tsx +79 -0
  101. package/templates/space/apps/fe/src/components/features/planets/list/table/PlanetsTable.tsx +30 -0
  102. package/templates/space/apps/fe/src/components/features/planets/list/table/PlanetsTableActions.tsx +64 -0
  103. package/templates/space/apps/fe/src/components/features/planets/list/table/PlanetsTableInfinite.tsx +36 -0
  104. package/templates/space/apps/fe/src/components/features/planets/list/table/planetsTableColumns.tsx +72 -0
  105. package/templates/space/apps/fe/src/components/features/planets/list/table/usePlanetTableFilters.tsx +102 -0
  106. package/templates/space/apps/fe/src/components/features/profile/ProfilePage.tsx +96 -0
  107. package/templates/space/apps/fe/src/components/googleAnalytics/GoogleAnalytics.tsx +35 -0
  108. package/templates/space/apps/fe/src/components/layout/AppLayout.tsx +19 -0
  109. package/templates/space/apps/fe/src/components/layout/app-header/AppHeader.tsx +93 -0
  110. package/templates/space/apps/fe/src/components/layout/app-header/MobileNavigation.tsx +53 -0
  111. package/templates/space/apps/fe/src/components/layout/app-header/NavLink.tsx +29 -0
  112. package/templates/space/apps/fe/src/components/shared/branding/BrandLogo.tsx +13 -0
  113. package/templates/space/apps/fe/src/components/shared/error/ErrorFallback.tsx +43 -0
  114. package/templates/space/apps/fe/src/components/shared/error/ErrorText.tsx +20 -0
  115. package/templates/space/apps/fe/src/components/shared/error/NotFound.tsx +36 -0
  116. package/templates/space/apps/fe/src/components/shared/forms/RequiredLabel.tsx +21 -0
  117. package/templates/space/apps/fe/src/components/shared/forms/RowInputWrapper.tsx +50 -0
  118. package/templates/space/apps/fe/src/components/shared/head/AppHead.tsx +32 -0
  119. package/templates/space/apps/fe/src/components/shared/head/DefaultAppHead.tsx +34 -0
  120. package/templates/space/apps/fe/src/components/shared/layout/LoadingState.tsx +9 -0
  121. package/templates/space/apps/fe/src/components/shared/layout/ThinPageWrapper.tsx +5 -0
  122. package/templates/space/apps/fe/src/components/shared/page/PageHeader.tsx +69 -0
  123. package/templates/space/apps/fe/src/components/shared/ui/Card.tsx +60 -0
  124. package/templates/space/apps/fe/src/components/shared/ui/GoogleLoginButton.tsx +19 -0
  125. package/templates/space/apps/fe/src/components/shared/ui/RequiredLabel.tsx +22 -0
  126. package/templates/space/apps/fe/src/components/shared/ui/TableActions.tsx +22 -0
  127. package/templates/space/apps/fe/src/config/app.config.ts +35 -0
  128. package/templates/space/apps/fe/src/config/i18n.ts +43 -0
  129. package/templates/space/apps/fe/src/config/inits/a11y.ts +14 -0
  130. package/templates/space/apps/fe/src/config/inits/logger.ts +7 -0
  131. package/templates/space/apps/fe/src/config/inits/sentry.ts +21 -0
  132. package/templates/space/apps/fe/src/config/jwt.config.ts +2 -0
  133. package/templates/space/apps/fe/src/config/query.config.ts +20 -0
  134. package/templates/space/apps/fe/src/hooks/useAuth.ts +5 -0
  135. package/templates/space/apps/fe/src/main.tsx +52 -0
  136. package/templates/space/apps/fe/src/pages/(guest)/login.tsx +23 -0
  137. package/templates/space/apps/fe/src/pages/(guest)/register.tsx +23 -0
  138. package/templates/space/apps/fe/src/pages/(guest)/route.tsx +18 -0
  139. package/templates/space/apps/fe/src/pages/(private)/index.tsx +23 -0
  140. package/templates/space/apps/fe/src/pages/(private)/planets/$id/edit.tsx +53 -0
  141. package/templates/space/apps/fe/src/pages/(private)/planets/$id/index.tsx +41 -0
  142. package/templates/space/apps/fe/src/pages/(private)/planets/index.tsx +24 -0
  143. package/templates/space/apps/fe/src/pages/(private)/planets-feed.tsx +23 -0
  144. package/templates/space/apps/fe/src/pages/(private)/profile.tsx +23 -0
  145. package/templates/space/apps/fe/src/pages/(private)/route.tsx +18 -0
  146. package/templates/space/apps/fe/src/pages/(public)/auth.tsx +37 -0
  147. package/templates/space/apps/fe/src/pages/(public)/route.tsx +9 -0
  148. package/templates/space/apps/fe/src/pages/__root.tsx +164 -0
  149. package/templates/space/apps/fe/src/providers/AppErrorBoundary.tsx +10 -0
  150. package/templates/space/apps/fe/src/providers/OpenApiRuntimeProvider.tsx +31 -0
  151. package/templates/space/apps/fe/src/providers/index.tsx +44 -0
  152. package/templates/space/apps/fe/src/providers/jwt.provider.tsx +95 -0
  153. package/templates/space/apps/fe/src/routeTree.gen.ts +309 -0
  154. package/templates/space/apps/fe/src/styles/base.css +103 -0
  155. package/templates/space/apps/fe/src/styles/fonts/fonts.tsx +10 -0
  156. package/templates/space/apps/fe/src/styles/fonts/general-sans.css +31 -0
  157. package/templates/space/apps/fe/src/styles/globals.css +28 -0
  158. package/templates/space/apps/fe/src/styles/overrides/defaults/button.override.ts +536 -0
  159. package/templates/space/apps/fe/src/styles/overrides/defaults/checkbox.override.ts +71 -0
  160. package/templates/space/apps/fe/src/styles/overrides/defaults/input.override.ts +252 -0
  161. package/templates/space/apps/fe/src/styles/overrides/defaults/label.override.ts +91 -0
  162. package/templates/space/apps/fe/src/styles/overrides/defaults/modal.override.ts +57 -0
  163. package/templates/space/apps/fe/src/styles/overrides/defaults/radio.override.ts +46 -0
  164. package/templates/space/apps/fe/src/styles/overrides/defaults/table.override.ts +104 -0
  165. package/templates/space/apps/fe/src/styles/overrides/defaults/tag.override.ts +66 -0
  166. package/templates/space/apps/fe/src/styles/overrides/defaults/typography.override.ts +115 -0
  167. package/templates/space/apps/fe/src/styles/overrides/outline.clsx.ts +10 -0
  168. package/templates/space/apps/fe/src/styles/overrides/uiOverrides.override.ts +60 -0
  169. package/templates/space/apps/fe/src/styles/theme.css +2177 -0
  170. package/templates/space/apps/fe/src/types/i18next.d.ts +12 -0
  171. package/templates/space/apps/fe/src/types/table.d.ts +17 -0
  172. package/templates/space/apps/fe/src/types/ui.d.ts +26 -0
  173. package/templates/space/apps/fe/src/types/vite-env.d.ts +20 -0
  174. package/templates/space/apps/fe/src/utils/date.utils.ts +17 -0
  175. package/templates/space/apps/fe/src/utils/image-fallback.ts +9 -0
  176. package/templates/space/apps/fe/src/utils/media-upload.ts +63 -0
  177. package/templates/space/apps/fe/src/utils/number.utils.ts +12 -0
  178. package/templates/space/apps/fe/src/utils/string.utils.ts +5 -0
  179. package/templates/space/apps/fe/src/vite-env.d.ts +1 -0
  180. package/templates/space/apps/fe/tsconfig.app.json +32 -0
  181. package/templates/space/apps/fe/tsconfig.json +9 -0
  182. package/templates/space/apps/fe/tsconfig.node.json +31 -0
  183. package/templates/space/apps/fe/vite.config.ts +94 -0
  184. package/templates/space/database.ts +33 -13
  185. package/templates/space/openapi.json +1819 -0
  186. package/templates/space/oxfmt.config.js +23 -0
  187. package/templates/space/package.json +6 -13
  188. package/templates/space/rulesync.jsonc +18 -0
  189. package/templates/space/tsconfig.json +2 -4
  190. package/templates/space/api/health.ts +0 -7
  191. package/templates/space/api/launch.ts +0 -29
  192. package/templates/space/api/me.ts +0 -14
  193. package/templates/space/api/motto.ts +0 -9
  194. package/templates/space/api/rockets.ts +0 -43
  195. package/templates/space/index.html +0 -138
  196. package/templates/space/src/main.tsx +0 -229
  197. package/templates/space/vite.config.ts +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robodev",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "CLI for Robodev Starbase — create, auth, link, and deploy hosted apps",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,3 +45,22 @@ test("collectFiles includes hosted sources and skips denied paths", async () =>
45
45
  await rm(root, { recursive: true, force: true });
46
46
  }
47
47
  });
48
+
49
+ test("collectFiles skips nested apps/fe sources", async () => {
50
+ const root = await mkdtemp(join(tmpdir(), "rd-cli-"));
51
+ try {
52
+ await writeFile(join(root, "database.ts"), "export default {}");
53
+ await mkdir(join(root, "apps/fe/src"), { recursive: true });
54
+ await writeFile(join(root, "apps/fe/src/main.tsx"), "export {}");
55
+ await writeFile(join(root, "apps/fe/index.html"), "<html></html>");
56
+ await writeFile(join(root, "apps/fe/package.json"), "{}");
57
+ const files = await collectFiles(root);
58
+ const paths = new Set(files.map((file) => file.path));
59
+ assert.ok(paths.has("database.ts"));
60
+ assert.ok(!paths.has("apps/fe/src/main.tsx"));
61
+ assert.ok(!paths.has("apps/fe/index.html"));
62
+ assert.ok(!paths.has("apps/fe/package.json"));
63
+ } finally {
64
+ await rm(root, { recursive: true, force: true });
65
+ }
66
+ });
@@ -86,6 +86,7 @@ function isUnsafe(path: string): boolean {
86
86
  function isDenied(path: string): boolean {
87
87
  const top = path.split("/")[0];
88
88
  if (SKIP_DIRS.has(top)) return true;
89
+ if (path === "apps" || path.startsWith("apps/")) return true;
89
90
  if (path === ".env" || path.startsWith(".env.")) return true;
90
91
  const name = basename(path);
91
92
  if (name.startsWith(".") && name !== ".gitignore") return true;
@@ -0,0 +1,26 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { test } from "node:test";
6
+ import { writeViteApiUrl } from "./config.js";
7
+
8
+ test("writeViteApiUrl sets Vite and Povio public API URLs", async () => {
9
+ const root = await mkdtemp(join(tmpdir(), "rd-env-"));
10
+ try {
11
+ await mkdir(join(root, ".config"), { recursive: true });
12
+ await writeFile(
13
+ join(root, ".config/local.spa.template.yml"),
14
+ `APP_PUBLIC_API_URL: &APP_PUBLIC_API_URL "http://localhost:4000"\nVITE_PUBLIC_API_URL: *APP_PUBLIC_API_URL\n`,
15
+ );
16
+ await writeViteApiUrl("https://prj.example.test", root);
17
+ const env = await readFile(join(root, ".env"), "utf8");
18
+ assert.match(env, /^VITE_API_URL=https:\/\/prj\.example\.test$/m);
19
+ assert.match(env, /^VITE_PUBLIC_API_URL=https:\/\/prj\.example\.test$/m);
20
+ assert.match(env, /^APP_PUBLIC_API_URL=https:\/\/prj\.example\.test$/m);
21
+ const spa = await readFile(join(root, ".config/local.spa.yml"), "utf8");
22
+ assert.match(spa, /APP_PUBLIC_API_URL: &APP_PUBLIC_API_URL "https:\/\/prj\.example\.test"/);
23
+ } finally {
24
+ await rm(root, { recursive: true, force: true });
25
+ }
26
+ });
package/src/config.ts CHANGED
@@ -65,7 +65,17 @@ export async function writeLink(link: ProjectLink, cwd = process.cwd()): Promise
65
65
  await writeFile(linkPath(cwd), `${JSON.stringify(link, null, 2)}\n`);
66
66
  }
67
67
 
68
- /** Sets `VITE_API_URL` so the starter UI talks to the deployed project host. */
68
+ const PROJECT_API_ENV_KEYS = ["VITE_API_URL", "VITE_PUBLIC_API_URL", "APP_PUBLIC_API_URL"] as const;
69
+
70
+ function upsertEnvKey(content: string, key: string, value: string): string {
71
+ const line = `${key}=${value}`;
72
+ const pattern = new RegExp(`^${key}=.*$`, "m");
73
+ if (pattern.test(content)) return content.replace(pattern, line);
74
+ const trimmed = content.replace(/\s*$/, "");
75
+ return `${trimmed}${trimmed ? "\n" : ""}${line}\n`;
76
+ }
77
+
78
+ /** Sets Vite / Povio API URLs so starter UIs talk to the deployed project host. */
69
79
  export async function writeViteApiUrl(projectApiUrl: string, cwd = process.cwd()): Promise<void> {
70
80
  const envPath = join(cwd, ".env");
71
81
  let content = "";
@@ -74,10 +84,29 @@ export async function writeViteApiUrl(projectApiUrl: string, cwd = process.cwd()
74
84
  } catch {
75
85
  content = "";
76
86
  }
77
- if (/^VITE_API_URL=/m.test(content)) {
78
- content = content.replace(/^VITE_API_URL=.*$/m, `VITE_API_URL=${projectApiUrl}`);
79
- } else {
80
- content = `${content.replace(/\s*$/, "")}${content ? "\n" : ""}VITE_API_URL=${projectApiUrl}\n`;
87
+ for (const key of PROJECT_API_ENV_KEYS) {
88
+ content = upsertEnvKey(content, key, projectApiUrl);
81
89
  }
82
90
  await writeFile(envPath, content.endsWith("\n") ? content : `${content}\n`);
91
+ await rewriteLocalSpaConfig(projectApiUrl, cwd);
92
+ }
93
+
94
+ async function rewriteLocalSpaConfig(projectApiUrl: string, cwd: string): Promise<void> {
95
+ const configDir = join(cwd, ".config");
96
+ const templatePath = join(configDir, "local.spa.template.yml");
97
+ const destPath = join(configDir, "local.spa.yml");
98
+ let source: string;
99
+ try {
100
+ source = await readFile(templatePath, "utf8");
101
+ } catch {
102
+ try {
103
+ source = await readFile(destPath, "utf8");
104
+ } catch {
105
+ return;
106
+ }
107
+ }
108
+ const quoted = JSON.stringify(projectApiUrl);
109
+ const next = source.replace(/^(APP_PUBLIC_API_URL:\s*&APP_PUBLIC_API_URL\s+).+$/m, `$1${quoted}`);
110
+ await mkdir(configDir, { recursive: true });
111
+ await writeFile(destPath, next.endsWith("\n") ? next : `${next}\n`);
83
112
  }
@@ -0,0 +1,54 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { test } from "node:test";
6
+ import { collectFiles } from "./collect-files.js";
7
+ import { cliRoot, emitStarter } from "./emit-starter.js";
8
+
9
+ const versions = { robodev: "0.12.0", sdk: "0.12.0", client: "0.12.0" };
10
+
11
+ test("emitStarter rewrites root package.json name only", async () => {
12
+ const dest = await mkdtemp(join(tmpdir(), "rd-emit-"));
13
+ try {
14
+ await emitStarter(join(cliRoot(), "..", "starters", "space"), dest, {
15
+ name: "Orbit",
16
+ packageName: "orbit-app",
17
+ title: "Space",
18
+ versions,
19
+ });
20
+ const rootPkg = JSON.parse(await readFile(join(dest, "package.json"), "utf8")) as {
21
+ name: string;
22
+ };
23
+ const fePkg = JSON.parse(await readFile(join(dest, "apps/fe/package.json"), "utf8")) as {
24
+ name: string;
25
+ dependencies?: Record<string, string>;
26
+ devDependencies?: Record<string, string>;
27
+ };
28
+ assert.equal(rootPkg.name, "orbit-app");
29
+ assert.equal(fePkg.name, "fe");
30
+ assert.equal(fePkg.dependencies?.robodev, undefined);
31
+ assert.equal(fePkg.devDependencies?.robodev, undefined);
32
+ const overview = await readFile(join(dest, ".rulesync/rules/project-overview.md"), "utf8");
33
+ assert.match(overview, /^# Space starter AI rules/m);
34
+ const readme = await readFile(join(dest, "README.md"), "utf8");
35
+ assert.match(readme, /^# Orbit/m);
36
+ } finally {
37
+ await rm(dest, { recursive: true, force: true });
38
+ }
39
+ });
40
+
41
+ test("collectFiles for space starter is Tiny APIs without apps/fe", async () => {
42
+ const files = await collectFiles(join(cliRoot(), "..", "starters", "space"));
43
+ const paths = new Set(files.map((file) => file.path));
44
+ assert.ok(paths.has("database.ts"));
45
+ assert.ok(paths.has("api/planets.ts"));
46
+ assert.ok(paths.has("api/planets/paginate.ts"));
47
+ assert.ok(paths.has("api/aliens/labels.ts"));
48
+ assert.ok(paths.has("api/files/presigned-url.ts"));
49
+ assert.ok(paths.has("api/files/upload.ts"));
50
+ assert.ok(!paths.has("index.html"));
51
+ assert.ok(!paths.has("src/main.tsx"));
52
+ assert.ok([...paths].every((path) => !path.startsWith("apps/")));
53
+ assert.ok(files.length < 200);
54
+ });
@@ -1,5 +1,5 @@
1
- import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
1
+ import { copyFile, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import { dirname, extname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
5
5
  export type Starter = {
@@ -23,6 +23,20 @@ export type EmitContext = {
23
23
  };
24
24
 
25
25
  const SKIP_DIRS = new Set(["node_modules", ".robodev", ".git"]);
26
+ const BINARY_COPY_EXTENSIONS = new Set([
27
+ ".png",
28
+ ".jpg",
29
+ ".jpeg",
30
+ ".gif",
31
+ ".webp",
32
+ ".ico",
33
+ ".bmp",
34
+ ".woff",
35
+ ".woff2",
36
+ ".ttf",
37
+ ".eot",
38
+ ".otf",
39
+ ]);
26
40
 
27
41
  type DepMap = Record<string, string>;
28
42
 
@@ -103,22 +117,35 @@ export async function loadPackageVersions(): Promise<PackageVersions> {
103
117
  }
104
118
 
105
119
  /** Copies a starter tree and applies package + display rewrites used by create and prepack. */
106
- export async function emitStarter(src: string, dest: string, ctx: EmitContext): Promise<void> {
120
+ export async function emitStarter(
121
+ src: string,
122
+ dest: string,
123
+ ctx: EmitContext,
124
+ isRoot = true,
125
+ ): Promise<void> {
107
126
  await mkdir(dest, { recursive: true });
108
127
  const entries = await readdir(src, { withFileTypes: true });
109
128
  for (const entry of entries) {
110
129
  if (entry.isDirectory()) {
111
130
  if (SKIP_DIRS.has(entry.name)) continue;
112
- await emitStarter(join(src, entry.name), join(dest, entry.name), ctx);
131
+ await emitStarter(join(src, entry.name), join(dest, entry.name), ctx, false);
113
132
  continue;
114
133
  }
115
134
  if (!entry.isFile() || entry.name === ".env") continue;
116
- const content = await readFile(join(src, entry.name), "utf8");
135
+ const srcPath = join(src, entry.name);
136
+ const destPath = join(dest, entry.name);
137
+ if (BINARY_COPY_EXTENSIONS.has(extname(entry.name).toLowerCase())) {
138
+ await copyFile(srcPath, destPath);
139
+ continue;
140
+ }
141
+ const content = await readFile(srcPath, "utf8");
117
142
  const next =
118
143
  entry.name === "package.json"
119
- ? rewritePackageJson(content, ctx)
120
- : rewriteDisplay(content, ctx);
121
- await writeFile(join(dest, entry.name), next);
144
+ ? rewritePackageJson(content, ctx, isRoot)
145
+ : shouldRewriteDisplay(entry.name, isRoot)
146
+ ? rewriteDisplay(content, ctx)
147
+ : content;
148
+ await writeFile(destPath, next);
122
149
  }
123
150
  }
124
151
 
@@ -154,9 +181,14 @@ async function readPackageVersion(path: string, fallback?: string): Promise<stri
154
181
  throw new Error(`Missing version in ${path}`);
155
182
  }
156
183
 
157
- function rewritePackageJson(raw: string, ctx: EmitContext): string {
184
+ function shouldRewriteDisplay(fileName: string, isRoot: boolean): boolean {
185
+ if (!isRoot) return false;
186
+ return fileName === "README.md" || fileName.endsWith(".html");
187
+ }
188
+
189
+ function rewritePackageJson(raw: string, ctx: EmitContext, isRoot: boolean): string {
158
190
  const pkg = JSON.parse(raw) as PackageJson;
159
- pkg.name = ctx.packageName;
191
+ if (isRoot) pkg.name = ctx.packageName;
160
192
 
161
193
  const pins: Record<string, string> = {
162
194
  robodev: `^${ctx.versions.robodev}`,
@@ -172,9 +204,11 @@ function rewritePackageJson(raw: string, ctx: EmitContext): string {
172
204
  }
173
205
 
174
206
  const moved = pkg.dependencies?.robodev;
175
- if (pkg.dependencies) delete pkg.dependencies.robodev;
176
- pkg.devDependencies = pkg.devDependencies ?? {};
177
- pkg.devDependencies.robodev = pkg.devDependencies.robodev ?? moved ?? pins.robodev;
207
+ if (isRoot) {
208
+ if (pkg.dependencies) delete pkg.dependencies.robodev;
209
+ pkg.devDependencies = pkg.devDependencies ?? {};
210
+ pkg.devDependencies.robodev = pkg.devDependencies.robodev ?? moved ?? pins.robodev;
211
+ }
178
212
 
179
213
  return `${JSON.stringify(pkg, null, 2)}\n`;
180
214
  }
package/src/index.ts CHANGED
@@ -397,6 +397,28 @@ async function npmInstall(root: string): Promise<void> {
397
397
  });
398
398
  }
399
399
 
400
+ async function bunAvailable(): Promise<boolean> {
401
+ try {
402
+ await execFileAsync("bun", ["--version"]);
403
+ return true;
404
+ } catch {
405
+ return false;
406
+ }
407
+ }
408
+
409
+ function printSpaceNextSteps(displayPath: string): void {
410
+ if (displayPath !== ".") {
411
+ console.log(` cd ${displayPath}`);
412
+ }
413
+ console.log(" cd apps/fe");
414
+ console.log(" bun install");
415
+ console.log(" bun openapi:gen");
416
+ console.log(" bun dev");
417
+ console.log(
418
+ " Open the Vite URL (default http://localhost:3000). The project App URL is APIs and Swagger, not this UI.",
419
+ );
420
+ }
421
+
400
422
  /** Creates a Starbase project, copies the chosen starter, links, deploys, and installs deps. */
401
423
  async function runCreate(args: string[]): Promise<void> {
402
424
  const parsed = parseCreateArgs(args);
@@ -442,6 +464,9 @@ async function runCreate(args: string[]): Promise<void> {
442
464
  "npm install failed. Copy the API URL printed above into Lovable, Bolt, or v0 as VITE_API_URL.",
443
465
  );
444
466
  console.log("https://robodev.povio.dev/docs/frontend");
467
+ } else if (starter.id === "space") {
468
+ console.log("Run `npm install` in the project folder.");
469
+ console.log("bun is required for apps/fe. APIs are already deployed.");
445
470
  } else {
446
471
  console.log("Run `npm install` in the project folder, then `npm run dev`.");
447
472
  }
@@ -451,14 +476,22 @@ async function runCreate(args: string[]): Promise<void> {
451
476
  const displayPath = relative(process.cwd(), root) || ".";
452
477
  console.log("");
453
478
  console.log("Next:");
454
- if (displayPath !== ".") {
455
- console.log(` cd ${displayPath}`);
456
- }
457
- if (starter.kind === "backend") {
479
+ if (starter.id === "space") {
480
+ if (!(await bunAvailable())) {
481
+ console.log("bun is required for apps/fe. APIs are already deployed.");
482
+ }
483
+ printSpaceNextSteps(displayPath);
484
+ } else if (starter.kind === "backend") {
485
+ if (displayPath !== ".") {
486
+ console.log(` cd ${displayPath}`);
487
+ }
458
488
  console.log(" Copy the API URL printed above into Lovable, Bolt, or v0 as VITE_API_URL.");
459
489
  console.log(" Do not run robodev deploy or npm run build inside those tools.");
460
490
  console.log(" https://robodev.povio.dev/docs/frontend");
461
491
  } else {
492
+ if (displayPath !== ".") {
493
+ console.log(` cd ${displayPath}`);
494
+ }
462
495
  console.log(" npm run dev");
463
496
  }
464
497
  }
@@ -12,6 +12,6 @@ Local Vite reads `VITE_API_URL` from `.env` (written by `robodev create` / `robo
12
12
 
13
13
  Docs: https://robodev.povio.dev/docs/starter
14
14
 
15
- `GET /me` and `GET/POST /messages` (`api/me.ts`, `api/messages.ts`) require a Robodev Auth Bearer token. `GET /health` stays public. Email auth works without Google. If Google is not configured on Starbase, the button shows a 503 message. Google OAuth may not complete inside the dashboard preview iframe — use Open app.
15
+ `GET /api/me` and `GET/POST /api/messages` (`api/me.ts`, `api/messages.ts`) require a Robodev Auth Bearer token. `GET /api/health` stays public. Email auth works without Google. If Google is not configured on Starbase, the button shows a 503 message. Google OAuth may not complete inside the dashboard preview iframe — use Open app.
16
16
 
17
17
  Edit files in the dashboard Code page, or here, then Save / `npx robodev deploy`.
@@ -1,4 +1,4 @@
1
- /** GET /health — liveness check for the deployed project host. */
1
+ /** GET /api/health — liveness check for the deployed project host. */
2
2
  import { defineApi, z } from "@robodev-ai/sdk";
3
3
 
4
4
  export const get = defineApi({
@@ -1,4 +1,4 @@
1
- /** GET /me — current project user (Robodev Auth required). */
1
+ /** GET /api/me — current project user (Robodev Auth required). */
2
2
  import { defineApi, z } from "@robodev-ai/sdk";
3
3
 
4
4
  export const get = defineApi({
@@ -1,4 +1,4 @@
1
- /** GET/POST /messages — signed-in users read and post in the shared room. */
1
+ /** GET/POST /api/messages — signed-in users read and post in the shared room. */
2
2
  import { messages } from "../database";
3
3
  import { defineApi, desc, z } from "@robodev-ai/sdk";
4
4
 
@@ -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.10.0"
23
+ "robodev": "^0.12.0"
24
24
  }
25
25
  }
@@ -40,7 +40,7 @@ function App() {
40
40
  const listRef = useRef<HTMLDivElement>(null);
41
41
 
42
42
  async function refreshMessages() {
43
- const res = await api.fetch("/messages");
43
+ const res = await api.fetch("/api/messages");
44
44
  if (!res.ok) throw new Error(await res.text());
45
45
  setMessages((await res.json()) as Message[]);
46
46
  }
@@ -74,7 +74,7 @@ function App() {
74
74
  }, [messages]);
75
75
 
76
76
  useEffect(() => {
77
- void fetch(`${apiUrl}/messages`).then((res) => {
77
+ void fetch(`${apiUrl}/api/messages`).then((res) => {
78
78
  setUnauthStatus(
79
79
  `${res.status} ${res.status === 401 ? "unauthorized (expected)" : res.statusText}`,
80
80
  );
@@ -134,7 +134,7 @@ function App() {
134
134
  setPending(true);
135
135
  setError(null);
136
136
  try {
137
- const res = await api.fetch("/messages", {
137
+ const res = await api.fetch("/api/messages", {
138
138
  method: "POST",
139
139
  body: JSON.stringify({ body: draft.trim() }),
140
140
  });
@@ -160,10 +160,10 @@ function App() {
160
160
  return (
161
161
  <main className="narrow">
162
162
  <p className="eyebrow">Robodev Auth</p>
163
- <h1>{{NAME}}</h1>
163
+ <h1>Auth chat</h1>
164
164
  <p className="lede">
165
165
  Sign in with email or Google. Protected APIs live at {apiUrl}. Unauthenticated GET
166
- /messages: {unauthStatus ?? "checking…"}.
166
+ /api/messages: {unauthStatus ?? "checking…"}.
167
167
  </p>
168
168
  {error ? <p className="error">{error}</p> : null}
169
169
 
@@ -14,4 +14,4 @@ Do not run `npm run dev` or `npm run build` in this folder. Do not run `robodev`
14
14
 
15
15
  Frontend how-to: https://robodev.povio.dev/docs/frontend
16
16
 
17
- `GET /me` (`api/me.ts`) requires a Robodev Auth Bearer token. Planet and rocket routes stay public.
17
+ `GET /api/me` (`api/me.ts`) requires a Robodev Auth Bearer token. Planet and rocket routes stay public.
@@ -1,4 +1,4 @@
1
- /** GET /health — liveness check for the deployed project host. */
1
+ /** GET /api/health — liveness check for the deployed project host. */
2
2
  import { defineApi, z } from "@robodev-ai/sdk";
3
3
 
4
4
  export const get = defineApi({
@@ -1,4 +1,4 @@
1
- /** POST /launch — marks a rocket as launched. */
1
+ /** POST /api/launch — marks a rocket as launched. */
2
2
  import { rockets } from "../database";
3
3
  import { defineApi, eq, z } from "@robodev-ai/sdk";
4
4
 
@@ -1,4 +1,4 @@
1
- /** GET /me — current project user (Robodev Auth required). */
1
+ /** GET /api/me — current project user (Robodev Auth required). */
2
2
  import { defineApi, z } from "@robodev-ai/sdk";
3
3
 
4
4
  export const get = defineApi({
@@ -1,4 +1,4 @@
1
- /** GET /motto — plain-text envelope response. */
1
+ /** GET /api/motto — plain-text envelope response. */
2
2
  import { defineApi } from "@robodev-ai/sdk";
3
3
 
4
4
  export const get = defineApi({
@@ -1,4 +1,4 @@
1
- /** GET /planets/:id — one planet by path param. */
1
+ /** GET /api/planets/:id — one planet by path param. */
2
2
  import { defineApi, eq, z } from "@robodev-ai/sdk";
3
3
  import { planets } from "../../database";
4
4
 
@@ -1,4 +1,4 @@
1
- /** GET/POST /planets — list (seeds sample rows on first GET) and create planets. */
1
+ /** GET/POST /api/planets — list (seeds sample rows on first GET) and create planets. */
2
2
  import { planets, rockets } from "../database";
3
3
  import { defineApi, z } from "@robodev-ai/sdk";
4
4
 
@@ -1,4 +1,4 @@
1
- /** GET/POST /rockets — list (optional `?destinationId=`) and create rockets. */
1
+ /** GET/POST /api/rockets — list (optional `?destinationId=`) and create rockets. */
2
2
  import { rockets } from "../database";
3
3
  import { defineApi, eq, z } from "@robodev-ai/sdk";
4
4
 
@@ -10,7 +10,7 @@
10
10
  "@robodev-ai/sdk": "^0.6.0"
11
11
  },
12
12
  "devDependencies": {
13
- "robodev": "^0.10.0",
13
+ "robodev": "^0.12.0",
14
14
  "typescript": "^5.9.2"
15
15
  }
16
16
  }
@@ -3,7 +3,7 @@
3
3
  {
4
4
  "id": "space",
5
5
  "title": "Space",
6
- "description": "Planets, rockets, and a small React UI"
6
+ "description": "Tiny Template planets and aliens APIs plus a local Povio UI in apps/fe"
7
7
  },
8
8
  {
9
9
  "id": "auth-chat",
@@ -19,7 +19,7 @@
19
19
  {
20
20
  "id": "empty",
21
21
  "title": "Empty",
22
- "description": "Blank database.ts and GET /health — no sample schema or UI. For Lovable, Bolt, or v0",
22
+ "description": "Blank database.ts and GET /api/health — no sample schema or UI. For Lovable, Bolt, or v0",
23
23
  "kind": "backend"
24
24
  }
25
25
  ]
@@ -1,6 +1,6 @@
1
1
  # {{NAME}}
2
2
 
3
- Blank `database.ts` and `GET /health` — no sample schema or UI. For Lovable, Bolt, or v0.
3
+ Blank `database.ts` and `GET /api/health` — no sample schema or UI. For Lovable, Bolt, or v0.
4
4
 
5
5
  Deploy `database.ts` and `api/` from a real terminal:
6
6
 
@@ -1,4 +1,4 @@
1
- /** GET /health — liveness check for the deployed project host. */
1
+ /** GET /api/health — liveness check for the deployed project host. */
2
2
  import { defineApi, z } from "@robodev-ai/sdk";
3
3
 
4
4
  export const get = defineApi({
@@ -10,7 +10,7 @@
10
10
  "@robodev-ai/sdk": "^0.6.0"
11
11
  },
12
12
  "devDependencies": {
13
- "robodev": "^0.10.0",
13
+ "robodev": "^0.12.0",
14
14
  "typescript": "^5.9.2"
15
15
  }
16
16
  }
@@ -0,0 +1,27 @@
1
+ STAGE: "local"
2
+ OUTPUT: "export"
3
+
4
+ ANALYZE: "false"
5
+ NEXT_TELEMETRY_DISABLED: "1"
6
+
7
+ VITE_PUBLIC_RELEASE: ${env:RELEASE}
8
+ VITE_PUBLIC_STAGE: ${func:stage}
9
+
10
+ APP_PUBLIC_API_URL: &APP_PUBLIC_API_URL "http://localhost:4000"
11
+ APP_PUBLIC_API_MODE: &APP_PUBLIC_API_MODE "real"
12
+ VITE_PUBLIC_API_URL: *APP_PUBLIC_API_URL
13
+ VITE_PUBLIC_API_MODE: *APP_PUBLIC_API_MODE
14
+
15
+ VITE_PUBLIC_LOG_LEVEL: "trace"
16
+
17
+ VITE_PUBLIC_SENTRY_DSN: ""
18
+ VITE_PUBLIC_SENTRY_ENVIRONMENT: ${func:stage}
19
+ VITE_PUBLIC_SENTRY_TRACES_SAMPLE_RATE: "0"
20
+ VITE_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE: "0"
21
+ VITE_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE: "0"
22
+
23
+ VITE_PUBLIC_AUTH_CUSTOM_JWT_ENABLE_MAGIC_LINK: "true"
24
+
25
+ VITE_PUBLIC_GOOGLE_ANALYTICS_MEASUREMENT_ID: ""
26
+
27
+ VITE_DEV_PORT: "3000"
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "apps/fe/node_modules/oxlint/configuration_schema.json",
3
+ "plugins": ["eslint", "typescript", "unicorn", "import", "oxc", "promise", "vitest"],
4
+ "categories": {
5
+ "correctness": "error",
6
+ "perf": "error",
7
+ "pedantic": "off",
8
+ "style": "off",
9
+ "restriction": "off",
10
+ "suspicious": "off",
11
+ "nursery": "off"
12
+ },
13
+ "env": {
14
+ "builtin": true,
15
+ "node": true,
16
+ "shared-node-browser": true
17
+ },
18
+ "rules": {
19
+ "eslint/eqeqeq": [
20
+ "error",
21
+ "always",
22
+ {
23
+ "null": "ignore"
24
+ }
25
+ ],
26
+ "typescript/no-floating-promises": "off",
27
+ "eslint/capitalized-comments": "off"
28
+ }
29
+ }