robodev 0.12.0 → 0.14.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 (157) hide show
  1. package/package.json +1 -1
  2. package/src/emit-starter.test.ts +14 -0
  3. package/src/index.ts +54 -45
  4. package/src/parse-create-args.test.ts +26 -0
  5. package/src/parse-create-args.ts +63 -0
  6. package/templates/auth-chat/.config/local.spa.template.yml +27 -0
  7. package/templates/auth-chat/.oxlint-base.json +29 -0
  8. package/templates/auth-chat/.oxlintrc.json +78 -0
  9. package/templates/auth-chat/.rulesync/rules/frontend-api-boundary.md +36 -0
  10. package/templates/auth-chat/.rulesync/rules/frontend-component-structure.md +38 -0
  11. package/templates/auth-chat/.rulesync/rules/frontend-forms.md +32 -0
  12. package/templates/auth-chat/.rulesync/rules/frontend-notifications.md +24 -0
  13. package/templates/auth-chat/.rulesync/rules/frontend-povio-components.md +42 -0
  14. package/templates/auth-chat/.rulesync/rules/frontend-povio-ui.md +45 -0
  15. package/templates/auth-chat/.rulesync/rules/frontend-query-autocomplete.md +29 -0
  16. package/templates/auth-chat/.rulesync/rules/frontend-tables-and-lists.md +34 -0
  17. package/templates/auth-chat/.rulesync/rules/frontend-translations.md +26 -0
  18. package/templates/auth-chat/.rulesync/rules/project-overview.md +32 -0
  19. package/templates/auth-chat/.rulesync/rules/robodev-api.md +30 -0
  20. package/templates/auth-chat/.rulesync/rules/robodev-auth.md +40 -0
  21. package/templates/auth-chat/.rulesync/rules/robodev-database.md +22 -0
  22. package/templates/auth-chat/.rulesync/rules/role-based-app-structure.md +36 -0
  23. package/templates/auth-chat/.rulesync/skills/media-feature/SKILL.md +21 -0
  24. package/templates/auth-chat/.rulesync/skills/povio-ui-styling/SKILL.md +208 -0
  25. package/templates/auth-chat/.rulesync/skills/povio-ui-styling/agents/openai.yaml +4 -0
  26. package/templates/auth-chat/.rulesync/skills/robodev-api-route/SKILL.md +13 -0
  27. package/templates/auth-chat/.rulesync/skills/robodev-table/SKILL.md +12 -0
  28. package/templates/auth-chat/README.md +26 -7
  29. package/templates/auth-chat/api/invite.ts +43 -0
  30. package/templates/auth-chat/apps/fe/index.html +24 -0
  31. package/templates/auth-chat/apps/fe/openapi-codegen.config.ts +37 -0
  32. package/templates/auth-chat/apps/fe/package.json +82 -0
  33. package/templates/auth-chat/apps/fe/public/apple-touch-icon.png +0 -0
  34. package/templates/auth-chat/apps/fe/public/favicon-96x96.png +0 -0
  35. package/templates/auth-chat/apps/fe/public/favicon.ico +0 -0
  36. package/templates/auth-chat/apps/fe/public/favicon.svg +3 -0
  37. package/templates/auth-chat/apps/fe/public/site.webmanifest +21 -0
  38. package/templates/auth-chat/apps/fe/public/web-app-manifest-192x192.png +0 -0
  39. package/templates/auth-chat/apps/fe/public/web-app-manifest-512x512.png +0 -0
  40. package/templates/auth-chat/apps/fe/src/assets/fonts/GeneralSans-Bold.otf +0 -0
  41. package/templates/auth-chat/apps/fe/src/assets/fonts/GeneralSans-Medium.otf +0 -0
  42. package/templates/auth-chat/apps/fe/src/assets/fonts/GeneralSans-Regular.otf +0 -0
  43. package/templates/auth-chat/apps/fe/src/assets/fonts/GeneralSans-Semibold.otf +0 -0
  44. package/templates/auth-chat/apps/fe/src/assets/locales/en/translation.json +202 -0
  45. package/templates/auth-chat/apps/fe/src/assets/locales/sl/translation.json +202 -0
  46. package/templates/auth-chat/apps/fe/src/clients/app-rest-client.ts +15 -0
  47. package/templates/auth-chat/apps/fe/src/clients/auth-token-store.ts +101 -0
  48. package/templates/auth-chat/apps/fe/src/clients/rest/app-error-handler.ts +31 -0
  49. package/templates/auth-chat/apps/fe/src/clients/rest/interceptors/authorization-header.interceptor.ts +15 -0
  50. package/templates/auth-chat/apps/fe/src/clients/rest/interceptors/refresh-token.interceptor.ts +37 -0
  51. package/templates/auth-chat/apps/fe/src/clients/rest/interceptors/response.interceptor.ts +17 -0
  52. package/templates/auth-chat/apps/fe/src/components/404.tsx +18 -0
  53. package/templates/auth-chat/apps/fe/src/components/features/auth/AuthBrandPanel.tsx +60 -0
  54. package/templates/auth-chat/apps/fe/src/components/features/auth/AuthLayout.tsx +23 -0
  55. package/templates/auth-chat/apps/fe/src/components/features/auth/LoginPage.tsx +111 -0
  56. package/templates/auth-chat/apps/fe/src/components/features/auth/RegisterPage.tsx +143 -0
  57. package/templates/auth-chat/apps/fe/src/components/features/chat/ChatPage.tsx +105 -0
  58. package/templates/auth-chat/apps/fe/src/components/features/chat/InviteForm.tsx +84 -0
  59. package/templates/auth-chat/apps/fe/src/components/features/profile/ProfilePage.tsx +96 -0
  60. package/templates/auth-chat/apps/fe/src/components/googleAnalytics/GoogleAnalytics.tsx +35 -0
  61. package/templates/auth-chat/apps/fe/src/components/layout/AppLayout.tsx +19 -0
  62. package/templates/auth-chat/apps/fe/src/components/layout/app-header/AppHeader.tsx +91 -0
  63. package/templates/auth-chat/apps/fe/src/components/layout/app-header/MobileNavigation.tsx +51 -0
  64. package/templates/auth-chat/apps/fe/src/components/layout/app-header/NavLink.tsx +29 -0
  65. package/templates/auth-chat/apps/fe/src/components/shared/branding/BrandLogo.tsx +13 -0
  66. package/templates/auth-chat/apps/fe/src/components/shared/error/ErrorFallback.tsx +43 -0
  67. package/templates/auth-chat/apps/fe/src/components/shared/error/ErrorText.tsx +20 -0
  68. package/templates/auth-chat/apps/fe/src/components/shared/error/NotFound.tsx +36 -0
  69. package/templates/auth-chat/apps/fe/src/components/shared/forms/RequiredLabel.tsx +21 -0
  70. package/templates/auth-chat/apps/fe/src/components/shared/forms/RowInputWrapper.tsx +50 -0
  71. package/templates/auth-chat/apps/fe/src/components/shared/head/AppHead.tsx +32 -0
  72. package/templates/auth-chat/apps/fe/src/components/shared/head/DefaultAppHead.tsx +34 -0
  73. package/templates/auth-chat/apps/fe/src/components/shared/layout/LoadingState.tsx +9 -0
  74. package/templates/auth-chat/apps/fe/src/components/shared/layout/ThinPageWrapper.tsx +5 -0
  75. package/templates/auth-chat/apps/fe/src/components/shared/page/PageHeader.tsx +69 -0
  76. package/templates/auth-chat/apps/fe/src/components/shared/ui/Card.tsx +60 -0
  77. package/templates/auth-chat/apps/fe/src/components/shared/ui/GoogleLoginButton.tsx +19 -0
  78. package/templates/auth-chat/apps/fe/src/components/shared/ui/RequiredLabel.tsx +22 -0
  79. package/templates/auth-chat/apps/fe/src/components/shared/ui/TableActions.tsx +22 -0
  80. package/templates/auth-chat/apps/fe/src/config/app.config.ts +35 -0
  81. package/templates/auth-chat/apps/fe/src/config/i18n.ts +43 -0
  82. package/templates/auth-chat/apps/fe/src/config/inits/a11y.ts +14 -0
  83. package/templates/auth-chat/apps/fe/src/config/inits/logger.ts +7 -0
  84. package/templates/auth-chat/apps/fe/src/config/inits/sentry.ts +21 -0
  85. package/templates/auth-chat/apps/fe/src/config/jwt.config.ts +2 -0
  86. package/templates/auth-chat/apps/fe/src/config/query.config.ts +20 -0
  87. package/templates/auth-chat/apps/fe/src/hooks/useAuth.ts +5 -0
  88. package/templates/auth-chat/apps/fe/src/main.tsx +52 -0
  89. package/templates/auth-chat/apps/fe/src/pages/(guest)/login.tsx +23 -0
  90. package/templates/auth-chat/apps/fe/src/pages/(guest)/register.tsx +23 -0
  91. package/templates/auth-chat/apps/fe/src/pages/(guest)/route.tsx +18 -0
  92. package/templates/auth-chat/apps/fe/src/pages/(private)/index.tsx +23 -0
  93. package/templates/auth-chat/apps/fe/src/pages/(private)/profile.tsx +23 -0
  94. package/templates/auth-chat/apps/fe/src/pages/(private)/route.tsx +18 -0
  95. package/templates/auth-chat/apps/fe/src/pages/(public)/auth.tsx +37 -0
  96. package/templates/auth-chat/apps/fe/src/pages/(public)/route.tsx +9 -0
  97. package/templates/auth-chat/apps/fe/src/pages/__root.tsx +164 -0
  98. package/templates/auth-chat/apps/fe/src/providers/AppErrorBoundary.tsx +10 -0
  99. package/templates/auth-chat/apps/fe/src/providers/OpenApiRuntimeProvider.tsx +31 -0
  100. package/templates/auth-chat/apps/fe/src/providers/index.tsx +44 -0
  101. package/templates/auth-chat/apps/fe/src/providers/jwt.provider.tsx +95 -0
  102. package/templates/auth-chat/apps/fe/src/routeTree.gen.ts +225 -0
  103. package/templates/auth-chat/apps/fe/src/styles/base.css +103 -0
  104. package/templates/auth-chat/apps/fe/src/styles/fonts/fonts.tsx +10 -0
  105. package/templates/auth-chat/apps/fe/src/styles/fonts/general-sans.css +31 -0
  106. package/templates/auth-chat/apps/fe/src/styles/globals.css +28 -0
  107. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/button.override.ts +536 -0
  108. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/checkbox.override.ts +71 -0
  109. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/input.override.ts +252 -0
  110. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/label.override.ts +91 -0
  111. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/modal.override.ts +57 -0
  112. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/radio.override.ts +46 -0
  113. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/table.override.ts +104 -0
  114. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/tag.override.ts +66 -0
  115. package/templates/auth-chat/apps/fe/src/styles/overrides/defaults/typography.override.ts +115 -0
  116. package/templates/auth-chat/apps/fe/src/styles/overrides/outline.clsx.ts +10 -0
  117. package/templates/auth-chat/apps/fe/src/styles/overrides/uiOverrides.override.ts +60 -0
  118. package/templates/auth-chat/apps/fe/src/styles/theme.css +2177 -0
  119. package/templates/auth-chat/apps/fe/src/types/i18next.d.ts +12 -0
  120. package/templates/auth-chat/apps/fe/src/types/table.d.ts +17 -0
  121. package/templates/auth-chat/apps/fe/src/types/ui.d.ts +26 -0
  122. package/templates/auth-chat/apps/fe/src/types/vite-env.d.ts +20 -0
  123. package/templates/auth-chat/apps/fe/src/utils/date.utils.ts +17 -0
  124. package/templates/auth-chat/apps/fe/src/utils/number.utils.ts +12 -0
  125. package/templates/auth-chat/apps/fe/src/utils/string.utils.ts +5 -0
  126. package/templates/auth-chat/apps/fe/src/vite-env.d.ts +1 -0
  127. package/templates/auth-chat/apps/fe/tsconfig.app.json +32 -0
  128. package/templates/auth-chat/apps/fe/tsconfig.json +9 -0
  129. package/templates/auth-chat/apps/fe/tsconfig.node.json +31 -0
  130. package/templates/auth-chat/apps/fe/vite.config.ts +94 -0
  131. package/templates/auth-chat/openapi.json +1016 -0
  132. package/templates/auth-chat/oxfmt.config.js +23 -0
  133. package/templates/auth-chat/package.json +6 -13
  134. package/templates/auth-chat/rulesync.jsonc +18 -0
  135. package/templates/auth-chat/tsconfig.json +2 -4
  136. package/templates/backend/README.md +2 -2
  137. package/templates/backend/api/_lib.ts +359 -0
  138. package/templates/backend/api/aliens/labels.ts +19 -0
  139. package/templates/backend/api/files/presigned-url.ts +48 -0
  140. package/templates/backend/api/files/upload.ts +29 -0
  141. package/templates/backend/api/planets/[id]/like.ts +36 -0
  142. package/templates/backend/api/planets/[id].ts +56 -13
  143. package/templates/backend/api/planets/paginate.ts +36 -0
  144. package/templates/backend/api/planets.ts +52 -44
  145. package/templates/backend/database.ts +33 -13
  146. package/templates/backend/package.json +1 -1
  147. package/templates/catalog.json +2 -2
  148. package/templates/empty/package.json +1 -1
  149. package/templates/space/package.json +1 -1
  150. package/templates/auth-chat/index.html +0 -201
  151. package/templates/auth-chat/src/main.tsx +0 -282
  152. package/templates/auth-chat/vite.config.ts +0 -7
  153. package/templates/backend/api/health.ts +0 -7
  154. package/templates/backend/api/launch.ts +0 -29
  155. package/templates/backend/api/me.ts +0 -14
  156. package/templates/backend/api/motto.ts +0 -9
  157. package/templates/backend/api/rockets.ts +0 -43
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robodev",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "CLI for Robodev Starbase — create, auth, link, and deploy hosted apps",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -38,6 +38,20 @@ test("emitStarter rewrites root package.json name only", async () => {
38
38
  }
39
39
  });
40
40
 
41
+ test("collectFiles for auth-chat starter is Tiny APIs without apps/fe", async () => {
42
+ const files = await collectFiles(join(cliRoot(), "..", "starters", "auth-chat"));
43
+ const paths = new Set(files.map((file) => file.path));
44
+ assert.ok(paths.has("database.ts"));
45
+ assert.ok(paths.has("api/messages.ts"));
46
+ assert.ok(paths.has("api/invite.ts"));
47
+ assert.ok(paths.has("api/health.ts"));
48
+ assert.ok(paths.has("api/me.ts"));
49
+ assert.ok(!paths.has("index.html"));
50
+ assert.ok(!paths.has("src/main.tsx"));
51
+ assert.ok([...paths].every((path) => !path.startsWith("apps/")));
52
+ assert.ok(files.length < 200);
53
+ });
54
+
41
55
  test("collectFiles for space starter is Tiny APIs without apps/fe", async () => {
42
56
  const files = await collectFiles(join(cliRoot(), "..", "starters", "space"));
43
57
  const paths = new Set(files.map((file) => file.path));
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  resolveStarterTree,
26
26
  type Starter,
27
27
  } from "./emit-starter.js";
28
+ import { parseCreateArgs } from "./parse-create-args.js";
28
29
 
29
30
  const execFileAsync = promisify(execFile);
30
31
 
@@ -38,7 +39,7 @@ Commands:
38
39
  logout Remove stored credentials
39
40
  whoami Show the signed-in user
40
41
  projects List your projects
41
- create [path] [--starter <id>] [--empty] Create a project, scaffold a starter, and deploy
42
+ create [path] [--starter <id>] [--empty] [--org <id>] Create a project, scaffold a starter, and deploy
42
43
  link [projectId] Write .robodev in the current folder
43
44
  deploy [--force] Deploy database.ts, api/*.ts, jobs, and frontend files (no Vite / npm run build)
44
45
  `);
@@ -279,48 +280,47 @@ function validIdsMessage(starters: Starter[]): string {
279
280
  return `Valid ids: ${starters.map((starter) => starter.id).join(", ")}`;
280
281
  }
281
282
 
282
- function parseCreateArgs(args: string[]): { path?: string; starterId?: string } {
283
- let path: string | undefined;
284
- let starterId: string | undefined;
285
- let empty = false;
286
- for (let i = 0; i < args.length; i++) {
287
- const arg = args[i];
288
- if (arg === "--empty") {
289
- empty = true;
290
- continue;
291
- }
292
- if (arg === "--starter") {
293
- const value = args[i + 1];
294
- if (!value || value.startsWith("-")) {
295
- throw new Error("Missing value for --starter");
296
- }
297
- starterId = value;
298
- i += 1;
299
- continue;
300
- }
301
- if (arg.startsWith("--starter=")) {
302
- const value = arg.slice("--starter=".length);
303
- if (!value) {
304
- throw new Error("Missing value for --starter");
305
- }
306
- starterId = value;
307
- continue;
308
- }
309
- if (arg.startsWith("-")) {
310
- throw new Error(`Unknown flag ${arg}`);
311
- }
312
- if (path !== undefined) {
313
- throw new Error(`Unexpected argument ${arg}`);
314
- }
315
- path = arg;
283
+ type Organization = {
284
+ id: string;
285
+ name: string;
286
+ plan: string;
287
+ createdAt: string;
288
+ role: "admin" | "developer";
289
+ };
290
+
291
+ function validOrgIdsMessage(orgs: Organization[]): string {
292
+ return `Valid ids: ${orgs.map((org) => org.id).join(", ")}`;
293
+ }
294
+
295
+ async function resolveOrganization(flagId: string | undefined): Promise<string | undefined> {
296
+ const { organizations } = await authed<{ organizations: Organization[] }>("/v1/organizations");
297
+ const adminOrgs = organizations.filter((org) => org.role === "admin");
298
+ if (flagId !== undefined) {
299
+ return flagId;
316
300
  }
317
- if (empty && starterId !== undefined) {
318
- throw new Error("Use either --empty or --starter, not both.");
301
+ if (adminOrgs.length === 0) {
302
+ throw new Error("You need an admin role in an organization to create a project.");
319
303
  }
320
- if (empty) {
321
- starterId = "empty";
304
+ if (adminOrgs.length === 1 || !stdin.isTTY) {
305
+ return undefined;
322
306
  }
323
- return { path, starterId };
307
+ console.log("Organizations:");
308
+ adminOrgs.forEach((org, i) => {
309
+ console.log(` ${i + 1}. ${org.name} (${org.id})`);
310
+ });
311
+ const answer = (await prompt("Organization number or id: ")).trim();
312
+ if (!answer) {
313
+ throw new Error(`Unknown organization "". ${validOrgIdsMessage(adminOrgs)}`);
314
+ }
315
+ const asIndex = Number(answer);
316
+ if (Number.isInteger(asIndex) && asIndex >= 1 && asIndex <= adminOrgs.length) {
317
+ return adminOrgs[asIndex - 1].id;
318
+ }
319
+ const found = adminOrgs.find((org) => org.id === answer);
320
+ if (!found) {
321
+ throw new Error(`Unknown organization "${answer}". ${validOrgIdsMessage(adminOrgs)}`);
322
+ }
323
+ return found.id;
324
324
  }
325
325
 
326
326
  async function resolveStarter(flagId: string | undefined): Promise<Starter> {
@@ -406,7 +406,15 @@ async function bunAvailable(): Promise<boolean> {
406
406
  }
407
407
  }
408
408
 
409
- function printSpaceNextSteps(displayPath: string): void {
409
+ async function hasAppsFe(root: string): Promise<boolean> {
410
+ try {
411
+ return (await stat(join(root, "apps/fe"))).isDirectory();
412
+ } catch {
413
+ return false;
414
+ }
415
+ }
416
+
417
+ function printFeNextSteps(displayPath: string): void {
410
418
  if (displayPath !== ".") {
411
419
  console.log(` cd ${displayPath}`);
412
420
  }
@@ -430,9 +438,10 @@ async function runCreate(args: string[]): Promise<void> {
430
438
  await assertCreatable(root);
431
439
 
432
440
  const name = projectNameFromPath(root);
441
+ const organizationId = await resolveOrganization(parsed.orgId);
433
442
  const project = await authed<Project>("/v1/projects", {
434
443
  method: "POST",
435
- body: JSON.stringify({ name }),
444
+ body: JSON.stringify(organizationId ? { name, organizationId } : { name }),
436
445
  });
437
446
 
438
447
  const { treeRoot } = await resolveStarterTree();
@@ -464,7 +473,7 @@ async function runCreate(args: string[]): Promise<void> {
464
473
  "npm install failed. Copy the API URL printed above into Lovable, Bolt, or v0 as VITE_API_URL.",
465
474
  );
466
475
  console.log("https://robodev.povio.dev/docs/frontend");
467
- } else if (starter.id === "space") {
476
+ } else if (await hasAppsFe(root)) {
468
477
  console.log("Run `npm install` in the project folder.");
469
478
  console.log("bun is required for apps/fe. APIs are already deployed.");
470
479
  } else {
@@ -476,11 +485,11 @@ async function runCreate(args: string[]): Promise<void> {
476
485
  const displayPath = relative(process.cwd(), root) || ".";
477
486
  console.log("");
478
487
  console.log("Next:");
479
- if (starter.id === "space") {
488
+ if (await hasAppsFe(root)) {
480
489
  if (!(await bunAvailable())) {
481
490
  console.log("bun is required for apps/fe. APIs are already deployed.");
482
491
  }
483
- printSpaceNextSteps(displayPath);
492
+ printFeNextSteps(displayPath);
484
493
  } else if (starter.kind === "backend") {
485
494
  if (displayPath !== ".") {
486
495
  console.log(` cd ${displayPath}`);
@@ -0,0 +1,26 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { parseCreateArgs } from "./parse-create-args.js";
4
+
5
+ test("parseCreateArgs accepts --org with starter flags", () => {
6
+ assert.deepEqual(parseCreateArgs(["app", "--starter", "space", "--org", "org_1"]), {
7
+ path: "app",
8
+ starterId: "space",
9
+ orgId: "org_1",
10
+ });
11
+ assert.deepEqual(parseCreateArgs(["--empty", "--org=org_2"]), {
12
+ path: undefined,
13
+ starterId: "empty",
14
+ orgId: "org_2",
15
+ });
16
+ });
17
+
18
+ test("parseCreateArgs rejects a missing --org value", () => {
19
+ assert.throws(() => parseCreateArgs(["--org"]), /Missing value for --org/);
20
+ assert.throws(() => parseCreateArgs(["--org="]), /Missing value for --org/);
21
+ assert.throws(() => parseCreateArgs(["--org", "--empty"]), /Missing value for --org/);
22
+ });
23
+
24
+ test("parseCreateArgs rejects unknown flags including --organization", () => {
25
+ assert.throws(() => parseCreateArgs(["--organization", "org_1"]), /Unknown flag --organization/);
26
+ });
@@ -0,0 +1,63 @@
1
+ export type CreateArgs = { path?: string; starterId?: string; orgId?: string };
2
+
3
+ export function parseCreateArgs(args: string[]): CreateArgs {
4
+ let path: string | undefined;
5
+ let starterId: string | undefined;
6
+ let orgId: string | undefined;
7
+ let empty = false;
8
+ for (let i = 0; i < args.length; i++) {
9
+ const arg = args[i];
10
+ if (arg === "--empty") {
11
+ empty = true;
12
+ continue;
13
+ }
14
+ if (arg === "--starter") {
15
+ const value = args[i + 1];
16
+ if (!value || value.startsWith("-")) {
17
+ throw new Error("Missing value for --starter");
18
+ }
19
+ starterId = value;
20
+ i += 1;
21
+ continue;
22
+ }
23
+ if (arg.startsWith("--starter=")) {
24
+ const value = arg.slice("--starter=".length);
25
+ if (!value) {
26
+ throw new Error("Missing value for --starter");
27
+ }
28
+ starterId = value;
29
+ continue;
30
+ }
31
+ if (arg === "--org") {
32
+ const value = args[i + 1];
33
+ if (!value || value.startsWith("-")) {
34
+ throw new Error("Missing value for --org");
35
+ }
36
+ orgId = value;
37
+ i += 1;
38
+ continue;
39
+ }
40
+ if (arg.startsWith("--org=")) {
41
+ const value = arg.slice("--org=".length);
42
+ if (!value) {
43
+ throw new Error("Missing value for --org");
44
+ }
45
+ orgId = value;
46
+ continue;
47
+ }
48
+ if (arg.startsWith("-")) {
49
+ throw new Error(`Unknown flag ${arg}`);
50
+ }
51
+ if (path !== undefined) {
52
+ throw new Error(`Unexpected argument ${arg}`);
53
+ }
54
+ path = arg;
55
+ }
56
+ if (empty && starterId !== undefined) {
57
+ throw new Error("Use either --empty or --starter, not both.");
58
+ }
59
+ if (empty) {
60
+ starterId = "empty";
61
+ }
62
+ return { path, starterId, orgId };
63
+ }
@@ -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
+ }
@@ -0,0 +1,78 @@
1
+ {
2
+ "$schema": "apps/fe/node_modules/oxlint/configuration_schema.json",
3
+ "extends": [".oxlint-base.json"],
4
+ "plugins": [
5
+ "eslint",
6
+ "typescript",
7
+ "unicorn",
8
+ "import",
9
+ "vitest",
10
+ "react",
11
+ "react-perf",
12
+ "jsx-a11y"
13
+ ],
14
+ "categories": {
15
+ "style": "error"
16
+ },
17
+ "ignorePatterns": [
18
+ "openapi-codegen.config.ts",
19
+ "postcss.config.js",
20
+ "out",
21
+ "dist",
22
+ "public",
23
+ "scripts",
24
+ ".vscode",
25
+ ".turbo",
26
+ "node_modules",
27
+ "apps/fe/src/routeTree.gen.ts",
28
+ "apps/fe/src/openapi",
29
+ "apps/fe/public"
30
+ ],
31
+ "overrides": [
32
+ {
33
+ "files": ["vite-plugin-*.ts", "apps/fe/vite-plugin-*.ts"],
34
+ "rules": {
35
+ "import/no-nodejs-modules": "off"
36
+ }
37
+ }
38
+ ],
39
+ "rules": {
40
+ "eslint/no-implicit-coercion": "off",
41
+ "no-map-spread": "off",
42
+ "eslint/sort-keys": "off",
43
+ "eslint/sort-imports": "off",
44
+ "eslint/no-ternary": "off",
45
+ "eslint/id-length": "off",
46
+ "eslint/no-magic-numbers": "off",
47
+ "eslint/arrow-body-style": "off",
48
+ "eslint/max-params": "off",
49
+ "eslint/func-style": "off",
50
+ "eslint/new-cap": "off",
51
+ "eslint/no-continue": "off",
52
+ "eslint/max-statements": "off",
53
+ "eslint/no-duplicate-imports": [
54
+ "error",
55
+ {
56
+ "allowSeparateTypeImports": true
57
+ }
58
+ ],
59
+ "react/exhaustive-deps": "off",
60
+ "react/jsx-props-no-spreading": "off",
61
+ "react/jsx-max-depth": "off",
62
+ "react-perf/jsx-no-new-object-as-prop": "off",
63
+ "react-perf/jsx-no-new-function-as-prop": "off",
64
+ "react-perf/jsx-no-new-array-as-prop": "off",
65
+ "react-perf/jsx-no-jsx-as-prop": "off",
66
+ "typescript/no-empty-interface": "off",
67
+ "import/no-named-export": "off",
68
+ "import/group-exports": "off",
69
+ "import/prefer-default-export": "off",
70
+ "import/exports-last": "off",
71
+ "import/consistent-type-specifier-style": "off",
72
+ "jest/require-hook": "off",
73
+ "unicorn/filename-case": "off",
74
+ "unicorn/prefer-global-this": "off",
75
+ "unicorn/no-await-expression-member": "off",
76
+ "unicorn/no-null": "off"
77
+ }
78
+ }
@@ -0,0 +1,36 @@
1
+ ---
2
+ root: false
3
+ targets: ["claudecode", "codexcli", "cursor"]
4
+ description: "Frontend must consume generated OpenAPI queries and models, not backend internals."
5
+ globs: ["apps/fe/src/**/*.{ts,tsx}"]
6
+ cursor:
7
+ alwaysApply: false
8
+ description: "Apply when editing frontend components, hooks, providers, pages, or clients that call the API."
9
+ globs: ["apps/fe/src/**/*.{ts,tsx}"]
10
+ ---
11
+
12
+ # Frontend API Boundary
13
+
14
+ Frontend application code must consume the API through generated files in `apps/fe/src/openapi`.
15
+
16
+ Use generated modules such as:
17
+
18
+ - `@/openapi/<domain>/<domain>.queries`
19
+ - `@/openapi/<domain>/<domain>.models`
20
+ - `@/openapi/queryModules`
21
+
22
+ For generated mutations, pass invalidation through the mutation options instead of importing `queryClient` or calling `invalidateQueries` with generated query keys. The current module is invalidated by default through `invalidateCurrentModule: true`; invalidate dependent modules explicitly with `invalidateModules: [QueryModule.someModule]`.
23
+
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
+
26
+ Do not import `@robodev-ai/sdk`, `database.ts`, Drizzle tables, or API helper modules into frontend features.
27
+
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
+
30
+ When API contracts change:
31
+
32
+ 1. Update `api/` handlers and `openapi.json` if the documented contract changed.
33
+ 2. Run `bun openapi:gen` from `apps/fe`.
34
+ 3. Update frontend code to use the regenerated queries and models.
35
+
36
+ Point `VITE_PUBLIC_API_URL` at the Starbase project host origin only. Do not append `/api`.
@@ -0,0 +1,38 @@
1
+ ---
2
+ root: false
3
+ targets: ["claudecode", "codexcli", "cursor"]
4
+ description: "Frontend component extraction and feature subcomponent structure conventions."
5
+ globs: ["apps/fe/src/**/*.{ts,tsx}"]
6
+ cursor:
7
+ alwaysApply: false
8
+ description: "Apply when organizing frontend feature components, page sections, mapped items, or reusable UI."
9
+ globs: ["apps/fe/src/**/*.{ts,tsx}"]
10
+ ---
11
+
12
+ # Frontend Component Structure
13
+
14
+ Extract repeatable UI into subcomponents. Every named React component should live in its own file, even when it is currently used only once. Keep the component file near the feature, layout, page, or shared UI area that owns it.
15
+
16
+ Extract large or complex sections of pages into named components so route and page components stay readable. Anything mapped from an array should usually be a component, such as cards, table row actions, repeated sections, or list items.
17
+
18
+ The planets feature is the reference example while the template is still a prototype:
19
+
20
+ - `PlanetCard` for mapped grid cards.
21
+ - `PlanetsFilters` for shared filter controls.
22
+ - `PlanetsTableActions` for repeated row actions.
23
+ - `PlanetsTable` and `PlanetsTableInfinite` for table wrappers.
24
+ - `PlanetDetailsPage` and `PlanetEditPage` for large route-owned views.
25
+
26
+ When implementing a real app, remove the planets and aliens links from app navigation such as `apps/fe/src/components/layout/AppHeader.tsx` so these examples are not directly accessible in the product UI, but keep the example implementation available as a reference unless the team intentionally deletes the demo layer.
27
+
28
+ Route files should own routing, document metadata, and route data boundaries. Feature components should own the page UI and interaction details.
29
+
30
+ Exception: route-local wrappers such as `PageComponent` or layout guard components may stay in route files when they only connect routing primitives to feature components.
31
+
32
+ ## Shared Utilities
33
+
34
+ Keep generic formatting, parsing, calculation, and data-shaping helpers in `apps/fe/src/utils/*.utils.ts` files instead of defining them inside component files. Export helpers through namespaces such as `DateUtils.formatDate(...)` or `NumberUtils.formatInteger(...)` so call sites stay explicit and related helpers stay grouped.
35
+
36
+ Component files may keep UI-local glue helpers only when the logic is tightly coupled to that component's JSX, such as adapting one component's prop shape or handling a local event. If the helper describes dates, numbers, arrays, strings, IDs, or other domain-neutral data, move it into a shared utility file.
37
+
38
+ For apps with multiple user roles that require distinct UI or business workflows, also apply the role-based app structure rule.
@@ -0,0 +1,32 @@
1
+ ---
2
+ root: false
3
+ targets: ["claudecode", "codexcli", "cursor"]
4
+ description: "Frontend form patterns with Povio UI useForm, generated schemas, and formControl wiring."
5
+ globs: ["apps/fe/src/**/*.{ts,tsx}"]
6
+ cursor:
7
+ alwaysApply: false
8
+ description: "Apply when building or changing frontend forms, modals, edit pages, or input components."
9
+ globs: ["apps/fe/src/**/*.{ts,tsx}"]
10
+ ---
11
+
12
+ # Frontend Forms
13
+
14
+ Use `useForm` and form-aware inputs from `@povio/ui/tanstack` with generated OpenAPI Zod schemas for API-backed forms. The planets feature is the reference example while the template is still a prototype:
15
+
16
+ - Create form: `apps/fe/src/components/features/planets/list/PlanetCreateModal.tsx`
17
+ - Edit form: `apps/fe/src/components/features/planets/details/PlanetEditPage.tsx`
18
+
19
+ Prefer this shape:
20
+
21
+ ```tsx
22
+ const form = useForm({
23
+ zodSchema: PlanetsModels.PlanetsCreateInputSchema,
24
+ defaultValues: { name: "" },
25
+ });
26
+ ```
27
+
28
+ Pass form state into Povio UI inputs with `field={{ form, name: "fieldName" }}`. Use `form.handleSubmit`, `form.reset`, and `form.setFieldValue` for form actions, and `useFormValue(form, selector)` for reactive field reads.
29
+
30
+ Avoid local `useState`, custom `value`, and custom `onChange` plumbing for fields that belong to the form. Use TanStack Form's `form.Field` adapter only when a component cannot accept the Povio UI `field` binding.
31
+
32
+ Use generated OpenAPI queries and mutations for submit handlers. Keep toast feedback and navigation close to the feature interaction that owns them.
@@ -0,0 +1,24 @@
1
+ ---
2
+ root: false
3
+ targets: ["claudecode", "codexcli", "cursor"]
4
+ description: "Frontend notification inbox, preferences, push permission, service worker, and token registration boundaries."
5
+ globs: ["apps/fe/src/**/*.{ts,tsx}", "apps/fe/public/**/*.{js,ts}"]
6
+ cursor:
7
+ alwaysApply: false
8
+ description: "Apply when frontend work handles notifications, push permission, service workers, push tokens, unread counts, or notification preferences."
9
+ globs: ["apps/fe/src/**/*.{ts,tsx}", "apps/fe/public/**/*.{js,ts}"]
10
+ ---
11
+
12
+ # Frontend Notifications
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.
15
+
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
+
18
+ Register the service worker and provider subscription through a small browser integration, then persist the token/subscription through the generated authenticated user API. Clean up or detach the current device registration on logout when required by the security model.
19
+
20
+ Keep push payloads minimal and privacy-safe. Validate internal deep links before navigation. Never depend on push as the only record of an important event; provide a paginated in-app notification list and unread state.
21
+
22
+ Use generated mutation invalidation options for unread counts, mark-read actions, preferences, and token changes. Do not access the query client directly for generated API state.
23
+
24
+ Use `@povio/ui` for permission prompts, notification lists, menus/popovers, tags, forms, confirmations, and toasts. Use `povio-ui-styling` for styling-layer decisions.
@@ -0,0 +1,42 @@
1
+ ---
2
+ root: false
3
+ targets: ["claudecode", "codexcli", "cursor"]
4
+ description: "Frontend interactive controls and typography must use Povio UI primitives instead of native replacements."
5
+ globs: ["apps/fe/src/**/*.{ts,tsx}"]
6
+ cursor:
7
+ alwaysApply: false
8
+ description: "Apply when creating or editing React components, controls, forms, tables, overlays, or user-facing text."
9
+ globs: ["apps/fe/src/**/*.{ts,tsx}"]
10
+ ---
11
+
12
+ # Use Povio UI Components
13
+
14
+ Use a component from `@povio/ui` whenever it provides the required primitive. Do not recreate an available Povio component with a native element plus Tailwind or custom CSS.
15
+
16
+ Required replacements include:
17
+
18
+ - `Button`, `TextButton`, or the appropriate Povio action component instead of a styled `<button>`.
19
+ - `TextInput`, `PasswordInput`, `TextArea`, checkbox, radio, select, autocomplete, and other Povio form controls instead of native form controls.
20
+ - `Typography` instead of directly styling user-facing headings, paragraphs, labels, or spans.
21
+ - `Table` or `InfiniteTable` instead of a hand-built native data table.
22
+ - `Modal`, `Confirmation`, `Drawer`, `BottomSheet`, `Menu`, `Tooltip`, or `ResponsivePopover` instead of custom overlay primitives.
23
+ - `FileUpload` instead of a directly exposed file input.
24
+ - Povio hooks such as `useForm` and `useToast` instead of parallel custom infrastructure.
25
+
26
+ Before creating a control:
27
+
28
+ 1. Search `@povio/ui` usage in the repository.
29
+ 2. Inspect existing feature components for the same primitive.
30
+ 3. Use the existing Povio props and variants.
31
+ 4. Apply the `povio-ui-styling` skill when choosing between local Tailwind, `UIConfig`, `UIOverrides`, a shared wrapper, or scoped CSS.
32
+
33
+ Native semantic and structural elements such as `<main>`, `<section>`, `<article>`, `<nav>`, `<form>`, `<div>`, and list elements remain appropriate for document structure and layout. Use TanStack Router navigation primitives for routing. This rule prohibits native replacements for available Povio UI behavior; it does not prohibit semantic HTML.
34
+
35
+ If Povio UI has no suitable primitive:
36
+
37
+ - Compose existing Povio components first.
38
+ - Put a reusable project-specific primitive under `apps/fe/src/components/shared/ui`.
39
+ - Preserve keyboard behavior, focus visibility, accessible names, disabled state, validation state, and loading state.
40
+ - Keep the exception narrow; do not introduce another general-purpose UI library.
41
+
42
+ When touching an existing native control that has a Povio equivalent, migrate it when the change is safely within scope. Do not expand a narrowly requested fix into a broad unrelated rewrite.
@@ -0,0 +1,45 @@
1
+ ---
2
+ root: false
3
+ targets: ["claudecode", "codexcli", "cursor"]
4
+ description: "Frontend UI uses Povio UI primitives first and semantic Tailwind tokens."
5
+ globs: ["apps/fe/src/**/*.{ts,tsx,css}"]
6
+ cursor:
7
+ alwaysApply: false
8
+ description: "Apply when editing frontend UI, styling, layout, components, or CSS."
9
+ globs: ["apps/fe/src/**/*.{ts,tsx,css}"]
10
+ ---
11
+
12
+ # Frontend UI
13
+
14
+ ## Use `@povio/ui` First
15
+
16
+ Always use components and hooks from `@povio/ui` when a suitable primitive exists, including `Button`, `Typography`, `Table`, `Modal`, `Confirmation`, `TextButton`, `TextInput`, `TextArea`, `PasswordInput`, `FileUpload`, `useForm`, `useToast`, and `Tag`.
17
+
18
+ Create project-specific UI only when `@povio/ui` does not provide the needed primitive. Shared custom primitives live under `apps/fe/src/components/shared/ui`.
19
+
20
+ Feature components should compose existing layout and shared primitives such as `PageHeader`, `BackHeader`, `LoadingState`, `ErrorText`, and `Card` before introducing new wrappers.
21
+
22
+ When implementing a real app, remove the demo planets and aliens links from app navigation such as `apps/fe/src/components/layout/app-header/AppHeader.tsx` so they are not directly accessible in the product UI, but keep the example code available as a reference unless the team intentionally deletes the demo layer.
23
+
24
+ ## Tailwind Tokens
25
+
26
+ The default Tailwind color palette is removed in `apps/fe/src/styles/base.css`. Do not use default color utilities such as:
27
+
28
+ - `text-red-500`
29
+ - `bg-blue-600`
30
+ - `border-gray-200`
31
+ - `ring-emerald-400`
32
+
33
+ Use semantic tokens exported from Figma in `apps/fe/src/styles/theme.css` and exposed through Tailwind, for example:
34
+
35
+ - Surface, fill, and outline: `bg-elevation-fill-default-1`, `border-elevation-outline-default-1`
36
+ - Text: `text-text-default-1`, `text-text-default-2`, `text-text-error-1`
37
+ - Interactive: `bg-interactive-contained-primary-idle`, `hover:bg-interactive-contained-primary-hover`
38
+
39
+ Do not manually edit `apps/fe/src/styles/theme.css`; it is exported from Figma. If a hand-written semantic token or Tailwind theme mapping is needed, add it only in `apps/fe/src/styles/base.css` before referencing it from JSX or CSS.
40
+
41
+ ## Forms And API Data
42
+
43
+ For forms backed by API requests, prefer generated OpenAPI model schemas with `@povio/ui` `useForm`, for example `useForm({ zodSchema: UserModels.UpdateProfileBodySchema })`.
44
+
45
+ Use generated OpenAPI queries and mutations for all API state. Keep API side effects, toast feedback, loading states, and query invalidation close to the feature that owns the interaction. For mutation invalidation, pass generated mutation options such as `invalidateModules`; do not import `queryClient` into feature code for generated API queries.