@mars-stack/cli 0.2.0 → 1.0.2

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 (175) hide show
  1. package/dist/index.js +137 -12
  2. package/dist/index.js.map +1 -1
  3. package/package.json +4 -3
  4. package/template/.cursor/rules/composition-patterns.mdc +186 -0
  5. package/template/.cursor/rules/data-access.mdc +29 -0
  6. package/template/.cursor/rules/project-structure.mdc +34 -0
  7. package/template/.cursor/rules/security.mdc +25 -0
  8. package/template/.cursor/rules/testing.mdc +24 -0
  9. package/template/.cursor/rules/ui-conventions.mdc +29 -0
  10. package/template/.cursor/skills/add-api-route/SKILL.md +122 -0
  11. package/template/.cursor/skills/add-audit-log/SKILL.md +375 -0
  12. package/template/.cursor/skills/add-blog/SKILL.md +447 -0
  13. package/template/.cursor/skills/add-command-palette/SKILL.md +438 -0
  14. package/template/.cursor/skills/add-component/SKILL.md +158 -0
  15. package/template/.cursor/skills/add-crud-routes/SKILL.md +221 -0
  16. package/template/.cursor/skills/add-e2e-test/SKILL.md +227 -0
  17. package/template/.cursor/skills/add-error-boundary/SKILL.md +472 -0
  18. package/template/.cursor/skills/add-feature/SKILL.md +174 -0
  19. package/template/.cursor/skills/add-middleware/SKILL.md +135 -0
  20. package/template/.cursor/skills/add-page/SKILL.md +151 -0
  21. package/template/.cursor/skills/add-prisma-model/SKILL.md +148 -0
  22. package/template/.cursor/skills/add-protected-resource/SKILL.md +192 -0
  23. package/template/.cursor/skills/add-role/SKILL.md +156 -0
  24. package/template/.cursor/skills/add-server-action/SKILL.md +167 -0
  25. package/template/.cursor/skills/add-webhook/SKILL.md +192 -0
  26. package/template/.cursor/skills/build-complete-feature/SKILL.md +227 -0
  27. package/template/.cursor/skills/build-dashboard/SKILL.md +211 -0
  28. package/template/.cursor/skills/build-data-table/SKILL.md +283 -0
  29. package/template/.cursor/skills/build-form/SKILL.md +231 -0
  30. package/template/.cursor/skills/build-landing-page/SKILL.md +248 -0
  31. package/template/.cursor/skills/configure-ai/SKILL.md +617 -0
  32. package/template/.cursor/skills/configure-analytics/SKILL.md +413 -0
  33. package/template/.cursor/skills/configure-dark-mode/SKILL.md +309 -0
  34. package/template/.cursor/skills/configure-email/SKILL.md +170 -0
  35. package/template/.cursor/skills/configure-email-verification/SKILL.md +333 -0
  36. package/template/.cursor/skills/configure-feature-flags/SKILL.md +361 -0
  37. package/template/.cursor/skills/configure-i18n/SKILL.md +518 -0
  38. package/template/.cursor/skills/configure-jobs/SKILL.md +500 -0
  39. package/template/.cursor/skills/configure-magic-links/SKILL.md +385 -0
  40. package/template/.cursor/skills/configure-multi-tenancy/SKILL.md +611 -0
  41. package/template/.cursor/skills/configure-notifications/SKILL.md +569 -0
  42. package/template/.cursor/skills/configure-oauth/SKILL.md +217 -0
  43. package/template/.cursor/skills/configure-onboarding/SKILL.md +483 -0
  44. package/template/.cursor/skills/configure-payments/SKILL.md +243 -0
  45. package/template/.cursor/skills/configure-realtime/SKILL.md +733 -0
  46. package/template/.cursor/skills/configure-search/SKILL.md +581 -0
  47. package/template/.cursor/skills/configure-storage/SKILL.md +273 -0
  48. package/template/.cursor/skills/configure-two-factor/SKILL.md +518 -0
  49. package/template/.cursor/skills/create-execution-plan/SKILL.md +204 -0
  50. package/template/.cursor/skills/create-seed/SKILL.md +191 -0
  51. package/template/.cursor/skills/deploy-to-vercel/SKILL.md +300 -0
  52. package/template/.cursor/skills/design-tokens/SKILL.md +138 -0
  53. package/template/.cursor/skills/mars-capture-conversation-context/SKILL.md +119 -0
  54. package/template/.cursor/skills/setup-billing/SKILL.md +322 -0
  55. package/template/.cursor/skills/setup-project/SKILL.md +104 -0
  56. package/template/.cursor/skills/setup-teams/SKILL.md +682 -0
  57. package/template/.cursor/skills/test-api-route/SKILL.md +219 -0
  58. package/template/.cursor/skills/update-architecture-docs/SKILL.md +99 -0
  59. package/template/AGENTS.md +104 -0
  60. package/template/ARCHITECTURE.md +102 -0
  61. package/template/docs/QUALITY_SCORE.md +20 -0
  62. package/template/docs/design-docs/conversation-as-system-record.md +70 -0
  63. package/template/docs/design-docs/core-beliefs.md +43 -0
  64. package/template/docs/design-docs/index.md +8 -0
  65. package/template/docs/exec-plans/active/.gitkeep +0 -0
  66. package/template/docs/exec-plans/completed/.gitkeep +0 -0
  67. package/template/docs/exec-plans/tech-debt.md +7 -0
  68. package/template/docs/generated/.gitkeep +0 -0
  69. package/template/docs/product-specs/index.md +7 -0
  70. package/template/docs/references/index.md +18 -0
  71. package/template/e2e/api.spec.ts +20 -0
  72. package/template/e2e/auth.spec.ts +24 -0
  73. package/template/e2e/public.spec.ts +25 -0
  74. package/template/eslint.config.mjs +24 -0
  75. package/template/next-env.d.ts +6 -0
  76. package/template/next.config.ts +45 -0
  77. package/template/package.json +80 -0
  78. package/template/playwright.config.ts +31 -0
  79. package/template/postcss.config.mjs +8 -0
  80. package/template/prisma/generated/prisma/browser.ts +49 -0
  81. package/template/prisma/generated/prisma/client.ts +73 -0
  82. package/template/prisma/generated/prisma/commonInputTypes.ts +406 -0
  83. package/template/prisma/generated/prisma/enums.ts +15 -0
  84. package/template/prisma/generated/prisma/internal/class.ts +254 -0
  85. package/template/prisma/generated/prisma/internal/prismaNamespace.ts +1240 -0
  86. package/template/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts +190 -0
  87. package/template/prisma/generated/prisma/models/Account.ts +1543 -0
  88. package/template/prisma/generated/prisma/models/File.ts +1529 -0
  89. package/template/prisma/generated/prisma/models/Session.ts +1415 -0
  90. package/template/prisma/generated/prisma/models/Subscription.ts +1455 -0
  91. package/template/prisma/generated/prisma/models/User.ts +2235 -0
  92. package/template/prisma/generated/prisma/models/VerificationToken.ts +1099 -0
  93. package/template/prisma/generated/prisma/models.ts +17 -0
  94. package/template/prisma/schema/auth.prisma +69 -0
  95. package/template/prisma/schema/base.prisma +8 -0
  96. package/template/prisma/schema/file.prisma +15 -0
  97. package/template/prisma/schema/subscription.prisma +17 -0
  98. package/template/prisma.config.ts +13 -0
  99. package/template/scripts/check-architecture.ts +221 -0
  100. package/template/scripts/check-doc-freshness.ts +242 -0
  101. package/template/scripts/ensure-db.mjs +291 -0
  102. package/template/scripts/generate-docs.ts +143 -0
  103. package/template/scripts/generate-env-example.ts +89 -0
  104. package/template/scripts/seed.ts +56 -0
  105. package/template/scripts/update-quality-score.ts +263 -0
  106. package/template/src/__tests__/architecture.test.ts +114 -0
  107. package/template/src/app/(auth)/forgotten-password/page.tsx +92 -0
  108. package/template/src/app/(auth)/layout.tsx +11 -0
  109. package/template/src/app/(auth)/register/page.tsx +162 -0
  110. package/template/src/app/(auth)/reset-password/page.tsx +109 -0
  111. package/template/src/app/(auth)/sign-in/page.tsx +122 -0
  112. package/template/src/app/(auth)/verify/[token]/page.tsx +87 -0
  113. package/template/src/app/(auth)/verify/page.tsx +56 -0
  114. package/template/src/app/(protected)/admin/page.tsx +108 -0
  115. package/template/src/app/(protected)/dashboard/loading.tsx +20 -0
  116. package/template/src/app/(protected)/dashboard/page.tsx +22 -0
  117. package/template/src/app/(protected)/layout.tsx +262 -0
  118. package/template/src/app/(protected)/settings/page.tsx +370 -0
  119. package/template/src/app/api/auth/forgot/route.ts +63 -0
  120. package/template/src/app/api/auth/login/route.ts +121 -0
  121. package/template/src/app/api/auth/logout/route.ts +19 -0
  122. package/template/src/app/api/auth/me/route.ts +30 -0
  123. package/template/src/app/api/auth/reset/route.ts +45 -0
  124. package/template/src/app/api/auth/signup/route.ts +85 -0
  125. package/template/src/app/api/auth/verify/route.ts +46 -0
  126. package/template/src/app/api/csrf/route.ts +12 -0
  127. package/template/src/app/api/health/route.ts +10 -0
  128. package/template/src/app/api/protected/admin/users/route.ts +24 -0
  129. package/template/src/app/api/protected/billing/checkout/route.ts +83 -0
  130. package/template/src/app/api/protected/billing/portal/route.ts +39 -0
  131. package/template/src/app/api/protected/files/[fileId]/route.ts +86 -0
  132. package/template/src/app/api/protected/files/upload/route.ts +64 -0
  133. package/template/src/app/api/protected/user/password/route.ts +63 -0
  134. package/template/src/app/api/protected/user/profile/route.ts +35 -0
  135. package/template/src/app/api/protected/user/sessions/[sessionId]/route.ts +33 -0
  136. package/template/src/app/api/protected/user/sessions/route.ts +22 -0
  137. package/template/src/app/api/readiness/route.ts +15 -0
  138. package/template/src/app/api/webhooks/stripe/route.ts +166 -0
  139. package/template/src/app/error.tsx +33 -0
  140. package/template/src/app/layout.tsx +29 -0
  141. package/template/src/app/not-found.tsx +20 -0
  142. package/template/src/app/page.tsx +136 -0
  143. package/template/src/app/privacy/page.tsx +178 -0
  144. package/template/src/app/providers.tsx +8 -0
  145. package/template/src/app/terms/page.tsx +139 -0
  146. package/template/src/config/app.config.ts +70 -0
  147. package/template/src/config/routes.ts +17 -0
  148. package/template/src/features/admin/index.ts +11 -0
  149. package/template/src/features/admin/permissions.ts +64 -0
  150. package/template/src/features/auth/context/AuthContext.tsx +96 -0
  151. package/template/src/features/auth/context/index.ts +2 -0
  152. package/template/src/features/auth/index.ts +3 -0
  153. package/template/src/features/auth/server/consent.ts +66 -0
  154. package/template/src/features/auth/server/session-revocation.ts +20 -0
  155. package/template/src/features/auth/server/sessions.ts +66 -0
  156. package/template/src/features/auth/server/user.ts +166 -0
  157. package/template/src/features/auth/types.ts +19 -0
  158. package/template/src/features/auth/validators.ts +29 -0
  159. package/template/src/features/billing/server/index.ts +66 -0
  160. package/template/src/features/billing/types.ts +43 -0
  161. package/template/src/features/uploads/server/index.ts +49 -0
  162. package/template/src/features/uploads/types.ts +26 -0
  163. package/template/src/lib/core/email/templates/base-layout.ts +122 -0
  164. package/template/src/lib/core/email/templates/index.ts +4 -0
  165. package/template/src/lib/core/email/templates/password-reset-email.ts +42 -0
  166. package/template/src/lib/core/email/templates/verification-email.ts +41 -0
  167. package/template/src/lib/core/email/templates/welcome-email.ts +40 -0
  168. package/template/src/lib/mars.ts +56 -0
  169. package/template/src/lib/prisma.ts +19 -0
  170. package/template/src/proxy.ts +92 -0
  171. package/template/src/styles/brand.css +15 -0
  172. package/template/src/styles/globals.css +7 -0
  173. package/template/tsconfig.json +59 -0
  174. package/template/vitest.config.ts +41 -0
  175. package/template/vitest.setup.ts +24 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mars-stack/cli",
3
- "version": "0.2.0",
3
+ "version": "1.0.2",
4
4
  "description": "MARS CLI: scaffold, configure, and maintain SaaS apps",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,7 +22,7 @@
22
22
  "mars": "./dist/index.js"
23
23
  },
24
24
  "publishConfig": {
25
- "access": "restricted"
25
+ "access": "public"
26
26
  },
27
27
  "files": [
28
28
  "dist",
@@ -34,9 +34,10 @@
34
34
  "start": "node dist/index.js",
35
35
  "test": "vitest run",
36
36
  "test:scaffold": "tsx ../../scripts/test-scaffold-matrix.ts",
37
- "prepublishOnly": "yarn build"
37
+ "prepublishOnly": "yarn build && node scripts/copy-template.mjs"
38
38
  },
39
39
  "dependencies": {
40
+ "@mars-stack/ui": "*",
40
41
  "commander": "^14.0.3",
41
42
  "fs-extra": "^11.0.0",
42
43
  "ora": "^8.0.0",
@@ -0,0 +1,186 @@
1
+ ---
2
+ description: React composition patterns — compound components, state lifting, explicit variants
3
+ globs: **/*.tsx
4
+ alwaysApply: false
5
+ ---
6
+
7
+ ## Composition Over Configuration
8
+
9
+ When building components, prefer composition over boolean props. These patterns apply to all feature components and any new primitives/patterns added to `@mars-stack/ui`.
10
+
11
+ Source: [Vercel Composition Patterns](https://github.com/vercel-labs/agent-skills/tree/main/skills/composition-patterns)
12
+
13
+ ## 1. No Boolean Prop Proliferation (CRITICAL)
14
+
15
+ Don't add boolean props like `isThread`, `isEditing`, `isCompact` to customise component behaviour. Each boolean doubles possible states and creates unmaintainable conditional logic.
16
+
17
+ **Wrong:**
18
+
19
+ ```tsx
20
+ function Composer({ isThread, isEditing, isForwarding, showAttachments }: Props) {
21
+ return (
22
+ <form>
23
+ <Input />
24
+ {isThread ? <ThreadField /> : isEditing ? <EditActions /> : <DefaultActions />}
25
+ {showAttachments && <Attachments />}
26
+ </form>
27
+ );
28
+ }
29
+ ```
30
+
31
+ **Right — create explicit variants that compose shared parts:**
32
+
33
+ ```tsx
34
+ function ThreadComposer({ channelId }: { channelId: string }) {
35
+ return (
36
+ <Composer.Frame>
37
+ <Composer.Input />
38
+ <AlsoSendToChannelField channelId={channelId} />
39
+ <Composer.Footer>
40
+ <Composer.Submit />
41
+ </Composer.Footer>
42
+ </Composer.Frame>
43
+ );
44
+ }
45
+
46
+ function EditComposer() {
47
+ return (
48
+ <Composer.Frame>
49
+ <Composer.Input />
50
+ <Composer.Footer>
51
+ <Composer.CancelEdit />
52
+ <Composer.SaveEdit />
53
+ </Composer.Footer>
54
+ </Composer.Frame>
55
+ );
56
+ }
57
+ ```
58
+
59
+ ## 2. Compound Components with Shared Context (HIGH)
60
+
61
+ Structure complex components as compound components. Subcomponents access shared state via context, not props. Consumers compose the pieces they need.
62
+
63
+ ```tsx
64
+ const ComposerContext = createContext<ComposerContextValue | null>(null);
65
+
66
+ function ComposerFrame({ children }: { children: React.ReactNode }) {
67
+ return <form>{children}</form>;
68
+ }
69
+
70
+ function ComposerInput() {
71
+ const { state, actions: { update } } = use(ComposerContext);
72
+ return <TextInput value={state.input} onChangeText={(text) => update((s) => ({ ...s, input: text }))} />;
73
+ }
74
+
75
+ const Composer = {
76
+ Frame: ComposerFrame,
77
+ Input: ComposerInput,
78
+ Submit: ComposerSubmit,
79
+ // ...
80
+ };
81
+ ```
82
+
83
+ Export compound components as a namespace object, not individual exports.
84
+
85
+ ## 3. Generic Context Interfaces (HIGH)
86
+
87
+ Define context with three parts: `state`, `actions`, `meta`. This interface is a contract that any provider can implement — enabling the same UI to work with different state implementations.
88
+
89
+ ```tsx
90
+ interface ComposerContextValue {
91
+ state: { input: string; attachments: Attachment[]; isSubmitting: boolean };
92
+ actions: { update: (updater: (s: State) => State) => void; submit: () => void };
93
+ meta: { inputRef: React.RefObject<HTMLTextAreaElement> };
94
+ }
95
+ ```
96
+
97
+ UI components consume the interface. Providers implement it. Swap the provider, keep the UI.
98
+
99
+ ## 4. Lift State into Providers (HIGH)
100
+
101
+ Move state into dedicated provider components. This lets sibling components outside the main UI access state without prop drilling.
102
+
103
+ **Wrong — state trapped in component:**
104
+
105
+ ```tsx
106
+ function ForwardDialog() {
107
+ return (
108
+ <Dialog>
109
+ <ForwardComposer />
110
+ <MessagePreview /> {/* Can't access composer state */}
111
+ <ForwardButton /> {/* Can't call submit */}
112
+ </Dialog>
113
+ );
114
+ }
115
+ ```
116
+
117
+ **Right — state in provider, UI siblings can access it:**
118
+
119
+ ```tsx
120
+ function ForwardDialog() {
121
+ return (
122
+ <ForwardProvider>
123
+ <Dialog>
124
+ <ForwardComposer />
125
+ <MessagePreview /> {/* Reads state from context */}
126
+ <ForwardButton /> {/* Calls submit from context */}
127
+ </Dialog>
128
+ </ForwardProvider>
129
+ );
130
+ }
131
+ ```
132
+
133
+ Components that need shared state don't have to be visually nested — they just need to be within the same provider.
134
+
135
+ ## 5. Decouple State from UI (MEDIUM)
136
+
137
+ The provider is the only place that knows how state is managed. UI components consume the context interface — they don't know if state comes from `useState`, Zustand, or a server sync.
138
+
139
+ ```tsx
140
+ // Local state provider
141
+ function ForwardProvider({ children }: { children: React.ReactNode }) {
142
+ const [state, setState] = useState(initialState);
143
+ return <Composer.Provider state={state} actions={{ update: setState, submit }}>{children}</Composer.Provider>;
144
+ }
145
+
146
+ // Global synced state provider — same UI works with both
147
+ function ChannelProvider({ channelId, children }: Props) {
148
+ const { state, update, submit } = useGlobalChannel(channelId);
149
+ return <Composer.Provider state={state} actions={{ update, submit }}>{children}</Composer.Provider>;
150
+ }
151
+ ```
152
+
153
+ ## 6. Children Over Render Props (MEDIUM)
154
+
155
+ Use `children` for composition. Use render props only when the parent needs to pass data back to the child (e.g., list items with index).
156
+
157
+ **Wrong:**
158
+
159
+ ```tsx
160
+ <Composer renderHeader={() => <Header />} renderFooter={() => <Footer />} />
161
+ ```
162
+
163
+ **Right:**
164
+
165
+ ```tsx
166
+ <Composer.Frame>
167
+ <Header />
168
+ <Composer.Input />
169
+ <Footer />
170
+ </Composer.Frame>
171
+ ```
172
+
173
+ ## 7. React 19 APIs
174
+
175
+ This project uses React 19. Follow these conventions:
176
+
177
+ - **No `forwardRef`** — `ref` is a regular prop in React 19.
178
+ - **Use `use()` instead of `useContext()`** — `use()` can be called conditionally.
179
+
180
+ ```tsx
181
+ // Right (React 19)
182
+ function ComposerInput({ ref, ...props }: Props & { ref?: React.Ref<HTMLTextAreaElement> }) {
183
+ const { state } = use(ComposerContext);
184
+ return <textarea ref={ref} value={state.input} {...props} />;
185
+ }
186
+ ```
@@ -0,0 +1,29 @@
1
+ ---
2
+ description: Database and data access patterns
3
+ globs: **/server/**
4
+ alwaysApply: false
5
+ ---
6
+
7
+ ## Prisma (v7)
8
+
9
+ - Import the database client: `import { prisma } from '@/lib/prisma'`.
10
+ - Import Prisma types (models, enums, namespaces): `import type { User } from '@db'` or `import { Prisma } from '@db'`. The `@db` alias maps to `prisma/generated/prisma/client`.
11
+ - The `datasource` URL lives in `prisma.config.ts`, **not** in the schema file.
12
+ - `PrismaClient` requires a driver adapter: `new PrismaClient({ adapter })` with `@prisma/adapter-pg`.
13
+ - After changing the schema: `yarn db:generate` then `yarn db:push` (dev) or `yarn db:migrate` (with migration history). `prisma generate` no longer auto-runs.
14
+ - User-scoped queries MUST include `userId` from the authenticated session in the `where` clause, never from request parameters.
15
+ - Use `$transaction` for multi-step writes that must be atomic.
16
+ - New models go in `prisma/schema/` as separate `.prisma` files (multi-file schema).
17
+
18
+ ## Error Handling
19
+
20
+ - API route catch blocks MUST use `handleApiError(error, { endpoint })` from `@/lib/mars`.
21
+ - `handleApiError` automatically detects Prisma errors and logs actionable diagnostics (connection refused, auth failed, missing tables, SSL issues) to the terminal.
22
+ - Database errors return HTTP 503 to clients. Zod validation errors return 400. Everything else returns 500.
23
+ - Never expose raw database error messages to the client.
24
+
25
+ ## File Handling
26
+
27
+ - File proxy routes MUST stream `response.body` instead of buffering with `arrayBuffer()`.
28
+ - Enforce max file size checks from the DB record before fetching.
29
+ - Sanitize filenames in `Content-Disposition` headers.
@@ -0,0 +1,34 @@
1
+ ---
2
+ description: Project structure and import conventions
3
+ globs:
4
+ alwaysApply: true
5
+ ---
6
+
7
+ ## Directory Layout
8
+
9
+ - `src/lib/` -- Wiring layer: `mars.ts` (re-exports logger, email, api-error, auth middleware, session, CSRF from `@mars-stack/core`) and `prisma.ts` (database client).
10
+ - UI primitives (Button, Input, Select, etc.) and patterns (Card, Modal, FormField) come from the `@mars-stack/ui` package.
11
+ - Shared hooks come from `@mars-stack/ui/hooks`.
12
+ - `src/features/<name>/` -- Feature modules with their own `components/`, `server/`, `hooks/`, `validation/`.
13
+ - `src/config/` -- `app.config.ts` (single source of truth) and `routes.ts`.
14
+ - `src/styles/` -- Design tokens: `primitives.css` → `tokens.css` → `theme.css`.
15
+
16
+ ## Import Rules
17
+
18
+ - Use `@/lib/prisma` for the database client, `@/lib/mars` for core infrastructure (logger, email, api-error, auth wrappers, session, CSRF).
19
+ - Use `@mars-stack/ui` for UI primitives and patterns, `@mars-stack/ui/hooks` for shared hooks.
20
+ - Use `@mars-stack/core/rate-limit` for rate limiting, `@mars-stack/core/test-utils` for test utilities, `@mars-stack/core/auth/hooks` for auth hooks, `@mars-stack/core/auth/validation` for auth validation schemas.
21
+ - `features/` may import from `@/lib/`, `@mars-stack/*`, and `config/`. Features MUST NOT import from other features.
22
+ - `app/` may import from anywhere.
23
+
24
+ ## Naming
25
+
26
+ - Use named exports. Default exports only for Next.js pages/layouts.
27
+ - Prefer explicit TypeScript types. No `any` -- use `unknown` and narrow.
28
+ - Barrel exports (`index.ts`) at the boundary of each module for clean imports.
29
+
30
+ ## Config
31
+
32
+ - `app.config.ts` is the single source of truth for app identity, feature flags, theme, and service providers.
33
+ - Check `appConfig.features.*` before wiring up optional functionality.
34
+ - Environment variables are validated lazily via `src/core/env/`. Add new vars to `buildEnvSchema()`.
@@ -0,0 +1,25 @@
1
+ ---
2
+ description: Security guardrails for all code changes
3
+ globs:
4
+ alwaysApply: true
5
+ ---
6
+
7
+ ## Authentication & Authorization
8
+
9
+ - Every API route under `src/app/api/protected/` MUST use `withAuth`, `withAuthNoParams`, `withRole`, or `withOwnership` from `@/lib/mars`.
10
+ - Admin-only routes MUST use `withRole(['admin'], ...)` which verifies the role against the database on every request. Never trust the JWT role claim alone.
11
+ - Ownership-gated routes MUST use `withOwnership`. Never accept a `userId` parameter from the client for authorization.
12
+ - Public auth endpoints (`login`, `signup`, `forgot`, `reset`, `verify`) MUST call `checkRateLimit` with the appropriate `RATE_LIMITS` config from `@mars-stack/core/rate-limit`.
13
+
14
+ ## Secrets
15
+
16
+ - Use constant-time comparison (`constantTimeEqual`) for all secret/token/signature comparisons. Never use `===` for secrets.
17
+ - CSRF keys are derived from `JWT_SECRET` via HMAC with a domain separator; never reuse `JWT_SECRET` directly.
18
+ - Never log secrets, tokens, or full authorization headers.
19
+ - Server-only modules MUST import `"server-only"` at the top to prevent client-side bundling.
20
+
21
+ ## Serverless Constraints
22
+
23
+ - No `setInterval` / `setTimeout` for background cleanup; use lazy-on-read patterns.
24
+ - No in-memory queues or global mutable state that assumes process persistence.
25
+ - Background processing should fire a separate HTTP request, not rely on fire-and-forget promises.
@@ -0,0 +1,24 @@
1
+ ---
2
+ description: Testing conventions
3
+ globs: **/*.test.ts,**/*.test.tsx
4
+ alwaysApply: false
5
+ ---
6
+
7
+ ## Framework
8
+
9
+ - Unit tests: Vitest (`yarn test`). Config at `vitest.config.ts`.
10
+ - E2E tests: Playwright (`yarn test:e2e`). Config at `playwright.config.ts`.
11
+ - Test files live next to the code they test: `route.test.ts` beside `route.ts`.
12
+
13
+ ## Mocking
14
+
15
+ - Use `@mars-stack/core/test-utils` to mock authenticated sessions in API route tests.
16
+ - Use `@mars-stack/core/test-utils` for consistent test data.
17
+ - Mock Prisma with `vi.mock('@/lib/prisma')` and provide typed mocks for each model method.
18
+
19
+ ## Patterns
20
+
21
+ - Test API routes by calling the exported handler directly, not via HTTP.
22
+ - Assert both the response body and status code.
23
+ - Test error paths: invalid input (Zod), unauthorized, not found, database errors.
24
+ - Unit tests MUST NOT depend on external services (database, Redis, email). Mock everything.
@@ -0,0 +1,29 @@
1
+ ---
2
+ description: UI component and styling conventions
3
+ globs: **/*.tsx
4
+ alwaysApply: false
5
+ ---
6
+
7
+ ## Design Token System
8
+
9
+ MARS uses a three-layer CSS token system. Never use raw Tailwind colour classes (e.g., `text-gray-900`). Always use semantic tokens:
10
+
11
+ - **Surfaces**: `bg-surface-background`, `bg-surface-card`, `bg-surface-elevated`, `bg-surface-input`
12
+ - **Text**: `text-text-primary`, `text-text-secondary`, `text-text-muted`, `text-text-link`, `text-text-error`
13
+ - **Borders**: `border-border-default`, `border-border-input`, `border-border-focus`, `border-border-error`
14
+ - **Brand**: `bg-brand-primary`, `bg-brand-primary-hover`, `text-text-on-brand`
15
+ - **Feedback**: `bg-success-muted`, `text-text-success`, `bg-error-muted`, `text-text-error`
16
+ - **Interactive**: `bg-ghost-hover`, `bg-ghost-active`, `bg-danger-bg`, `focus:ring-ring-focus`
17
+
18
+ ## Component Architecture
19
+
20
+ - **Primitives** (`@mars-stack/ui` (primitives)): Single-responsibility, fully styled via tokens, accept `className` for overrides. Examples: `Button`, `Input`, `Badge`, `Avatar`, `Spinner`.
21
+ - **Patterns** (`@mars-stack/ui` (patterns)): Compose primitives, no business logic. Examples: `Card`, `Modal`, `Toast`, `FormField`, `EmptyState`.
22
+ - **Feature components** (`src/features/<name>/components/`): Contain business logic, compose primitives and patterns.
23
+
24
+ ## Rules
25
+
26
+ - Always import components from barrel exports: `@mars-stack/ui` or `@mars-stack/ui`.
27
+ - Use `clsx` for conditional class composition.
28
+ - New primitives go in `@mars-stack/ui` (primitives) with a barrel re-export.
29
+ - Components MUST support dark mode via the token system (tokens swap automatically with `.dark` class).
@@ -0,0 +1,122 @@
1
+ # Skill: Add an API Route
2
+
3
+ Create a new Next.js API route following MARS conventions for authentication, validation, error handling, and testing.
4
+
5
+ ## When to Use
6
+
7
+ Use this skill when the user asks to create a new endpoint, API route, or backend handler.
8
+
9
+ ## Decision: Public or Protected?
10
+
11
+ | Type | Location | Auth Wrapper | Use Case |
12
+ |------|----------|-------------|----------|
13
+ | Public | `src/app/api/<name>/route.ts` | None (but add rate limiting) | Auth endpoints, webhooks, health checks |
14
+ | Protected | `src/app/api/protected/<name>/route.ts` | `withAuth` / `withRole` / `withOwnership` | User data, settings, admin |
15
+
16
+ ## Protected Route Template
17
+
18
+ ```typescript
19
+ import { handleApiError, withAuthNoParams, type AuthenticatedRequest } from '@/lib/mars';
20
+ import { NextResponse } from 'next/server';
21
+
22
+ export const GET = withAuthNoParams(async (request: AuthenticatedRequest) => {
23
+ try {
24
+ const userId = request.session.userId;
25
+ // ... business logic using userId for scoping
26
+ return NextResponse.json({ data: result });
27
+ } catch (error) {
28
+ return handleApiError(error, { endpoint: '/api/protected/<name>' });
29
+ }
30
+ });
31
+ ```
32
+
33
+ ### Auth Wrapper Reference
34
+
35
+ | Wrapper | Signature | When to Use |
36
+ |---------|-----------|-------------|
37
+ | `withAuth` | `(request, { params })` | Routes with dynamic URL params |
38
+ | `withAuthNoParams` | `(request)` | Routes without URL params |
39
+ | `withAuthSimple` | `()` | Routes that only need session verification |
40
+ | `withRole` | `withRole(['admin'], handler)` | Admin-only routes (verifies role from DB) |
41
+ | `withOwnership` | `withOwnership(getResourceUserId, handler)` | User must own the resource |
42
+
43
+ ## Public Route Template (with Rate Limiting)
44
+
45
+ ```typescript
46
+ import { handleApiError } from '@/lib/mars';
47
+ import {
48
+ checkRateLimit, getClientIP, RATE_LIMITS, rateLimitResponse,
49
+ } from '@mars-stack/core/rate-limit';
50
+ import { NextResponse } from 'next/server';
51
+
52
+ export async function POST(request: Request) {
53
+ const ip = getClientIP(request);
54
+ const rateLimit = await checkRateLimit(ip, RATE_LIMITS.default);
55
+ if (!rateLimit.success) return rateLimitResponse(rateLimit.resetAt);
56
+
57
+ try {
58
+ // ... business logic
59
+ return NextResponse.json({ data: result });
60
+ } catch (error) {
61
+ return handleApiError(error, { endpoint: '/api/<name>' });
62
+ }
63
+ }
64
+ ```
65
+
66
+ ## Input Validation
67
+
68
+ Always validate request bodies with Zod before use:
69
+
70
+ ```typescript
71
+ import { z } from 'zod';
72
+
73
+ const schema = z.object({
74
+ name: z.string().min(1).max(100),
75
+ email: z.string().email(),
76
+ });
77
+
78
+ // Inside the handler:
79
+ const body = schema.parse(await request.json());
80
+ ```
81
+
82
+ `handleApiError` will automatically catch `ZodError` and return a 400 with the first error message.
83
+
84
+ ## Dynamic Routes
85
+
86
+ For routes with URL parameters like `/api/protected/widgets/[id]/route.ts`:
87
+
88
+ ```typescript
89
+ export const GET = withAuth(async (request, context) => {
90
+ try {
91
+ const { id } = await context.params;
92
+ const userId = request.session.userId;
93
+ // ... fetch by id, scoped to userId
94
+ } catch (error) {
95
+ return handleApiError(error, { endpoint: '/api/protected/widgets/[id]' });
96
+ }
97
+ });
98
+ ```
99
+
100
+ ## Error Handling Reference
101
+
102
+ `handleApiError` from `@/lib/mars` handles:
103
+
104
+ | Error Type | HTTP Status | Client Response |
105
+ |-----------|-------------|----------------|
106
+ | `ZodError` | 400 | First validation error message |
107
+ | Prisma / Database | 503 | "Service temporarily unavailable" + detailed terminal diagnostics |
108
+ | All others | 500 | Custom `fallbackMessage` or "An unexpected error occurred" |
109
+
110
+ ## Testing
111
+
112
+ Create `route.test.ts` beside the route file. See the `add-feature` skill for the full testing pattern.
113
+
114
+ ## Checklist
115
+
116
+ - [ ] Correct directory (public vs protected)
117
+ - [ ] Appropriate auth wrapper applied
118
+ - [ ] Rate limiting on public endpoints
119
+ - [ ] Zod validation for all inputs
120
+ - [ ] Queries scoped by `request.session.userId`
121
+ - [ ] `handleApiError` in every catch block
122
+ - [ ] Test file created