@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.
- package/dist/index.js +137 -12
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/template/.cursor/rules/composition-patterns.mdc +186 -0
- package/template/.cursor/rules/data-access.mdc +29 -0
- package/template/.cursor/rules/project-structure.mdc +34 -0
- package/template/.cursor/rules/security.mdc +25 -0
- package/template/.cursor/rules/testing.mdc +24 -0
- package/template/.cursor/rules/ui-conventions.mdc +29 -0
- package/template/.cursor/skills/add-api-route/SKILL.md +122 -0
- package/template/.cursor/skills/add-audit-log/SKILL.md +375 -0
- package/template/.cursor/skills/add-blog/SKILL.md +447 -0
- package/template/.cursor/skills/add-command-palette/SKILL.md +438 -0
- package/template/.cursor/skills/add-component/SKILL.md +158 -0
- package/template/.cursor/skills/add-crud-routes/SKILL.md +221 -0
- package/template/.cursor/skills/add-e2e-test/SKILL.md +227 -0
- package/template/.cursor/skills/add-error-boundary/SKILL.md +472 -0
- package/template/.cursor/skills/add-feature/SKILL.md +174 -0
- package/template/.cursor/skills/add-middleware/SKILL.md +135 -0
- package/template/.cursor/skills/add-page/SKILL.md +151 -0
- package/template/.cursor/skills/add-prisma-model/SKILL.md +148 -0
- package/template/.cursor/skills/add-protected-resource/SKILL.md +192 -0
- package/template/.cursor/skills/add-role/SKILL.md +156 -0
- package/template/.cursor/skills/add-server-action/SKILL.md +167 -0
- package/template/.cursor/skills/add-webhook/SKILL.md +192 -0
- package/template/.cursor/skills/build-complete-feature/SKILL.md +227 -0
- package/template/.cursor/skills/build-dashboard/SKILL.md +211 -0
- package/template/.cursor/skills/build-data-table/SKILL.md +283 -0
- package/template/.cursor/skills/build-form/SKILL.md +231 -0
- package/template/.cursor/skills/build-landing-page/SKILL.md +248 -0
- package/template/.cursor/skills/configure-ai/SKILL.md +617 -0
- package/template/.cursor/skills/configure-analytics/SKILL.md +413 -0
- package/template/.cursor/skills/configure-dark-mode/SKILL.md +309 -0
- package/template/.cursor/skills/configure-email/SKILL.md +170 -0
- package/template/.cursor/skills/configure-email-verification/SKILL.md +333 -0
- package/template/.cursor/skills/configure-feature-flags/SKILL.md +361 -0
- package/template/.cursor/skills/configure-i18n/SKILL.md +518 -0
- package/template/.cursor/skills/configure-jobs/SKILL.md +500 -0
- package/template/.cursor/skills/configure-magic-links/SKILL.md +385 -0
- package/template/.cursor/skills/configure-multi-tenancy/SKILL.md +611 -0
- package/template/.cursor/skills/configure-notifications/SKILL.md +569 -0
- package/template/.cursor/skills/configure-oauth/SKILL.md +217 -0
- package/template/.cursor/skills/configure-onboarding/SKILL.md +483 -0
- package/template/.cursor/skills/configure-payments/SKILL.md +243 -0
- package/template/.cursor/skills/configure-realtime/SKILL.md +733 -0
- package/template/.cursor/skills/configure-search/SKILL.md +581 -0
- package/template/.cursor/skills/configure-storage/SKILL.md +273 -0
- package/template/.cursor/skills/configure-two-factor/SKILL.md +518 -0
- package/template/.cursor/skills/create-execution-plan/SKILL.md +204 -0
- package/template/.cursor/skills/create-seed/SKILL.md +191 -0
- package/template/.cursor/skills/deploy-to-vercel/SKILL.md +300 -0
- package/template/.cursor/skills/design-tokens/SKILL.md +138 -0
- package/template/.cursor/skills/mars-capture-conversation-context/SKILL.md +119 -0
- package/template/.cursor/skills/setup-billing/SKILL.md +322 -0
- package/template/.cursor/skills/setup-project/SKILL.md +104 -0
- package/template/.cursor/skills/setup-teams/SKILL.md +682 -0
- package/template/.cursor/skills/test-api-route/SKILL.md +219 -0
- package/template/.cursor/skills/update-architecture-docs/SKILL.md +99 -0
- package/template/AGENTS.md +104 -0
- package/template/ARCHITECTURE.md +102 -0
- package/template/docs/QUALITY_SCORE.md +20 -0
- package/template/docs/design-docs/conversation-as-system-record.md +70 -0
- package/template/docs/design-docs/core-beliefs.md +43 -0
- package/template/docs/design-docs/index.md +8 -0
- package/template/docs/exec-plans/active/.gitkeep +0 -0
- package/template/docs/exec-plans/completed/.gitkeep +0 -0
- package/template/docs/exec-plans/tech-debt.md +7 -0
- package/template/docs/generated/.gitkeep +0 -0
- package/template/docs/product-specs/index.md +7 -0
- package/template/docs/references/index.md +18 -0
- package/template/e2e/api.spec.ts +20 -0
- package/template/e2e/auth.spec.ts +24 -0
- package/template/e2e/public.spec.ts +25 -0
- package/template/eslint.config.mjs +24 -0
- package/template/next-env.d.ts +6 -0
- package/template/next.config.ts +45 -0
- package/template/package.json +80 -0
- package/template/playwright.config.ts +31 -0
- package/template/postcss.config.mjs +8 -0
- package/template/prisma/generated/prisma/browser.ts +49 -0
- package/template/prisma/generated/prisma/client.ts +73 -0
- package/template/prisma/generated/prisma/commonInputTypes.ts +406 -0
- package/template/prisma/generated/prisma/enums.ts +15 -0
- package/template/prisma/generated/prisma/internal/class.ts +254 -0
- package/template/prisma/generated/prisma/internal/prismaNamespace.ts +1240 -0
- package/template/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts +190 -0
- package/template/prisma/generated/prisma/models/Account.ts +1543 -0
- package/template/prisma/generated/prisma/models/File.ts +1529 -0
- package/template/prisma/generated/prisma/models/Session.ts +1415 -0
- package/template/prisma/generated/prisma/models/Subscription.ts +1455 -0
- package/template/prisma/generated/prisma/models/User.ts +2235 -0
- package/template/prisma/generated/prisma/models/VerificationToken.ts +1099 -0
- package/template/prisma/generated/prisma/models.ts +17 -0
- package/template/prisma/schema/auth.prisma +69 -0
- package/template/prisma/schema/base.prisma +8 -0
- package/template/prisma/schema/file.prisma +15 -0
- package/template/prisma/schema/subscription.prisma +17 -0
- package/template/prisma.config.ts +13 -0
- package/template/scripts/check-architecture.ts +221 -0
- package/template/scripts/check-doc-freshness.ts +242 -0
- package/template/scripts/ensure-db.mjs +291 -0
- package/template/scripts/generate-docs.ts +143 -0
- package/template/scripts/generate-env-example.ts +89 -0
- package/template/scripts/seed.ts +56 -0
- package/template/scripts/update-quality-score.ts +263 -0
- package/template/src/__tests__/architecture.test.ts +114 -0
- package/template/src/app/(auth)/forgotten-password/page.tsx +92 -0
- package/template/src/app/(auth)/layout.tsx +11 -0
- package/template/src/app/(auth)/register/page.tsx +162 -0
- package/template/src/app/(auth)/reset-password/page.tsx +109 -0
- package/template/src/app/(auth)/sign-in/page.tsx +122 -0
- package/template/src/app/(auth)/verify/[token]/page.tsx +87 -0
- package/template/src/app/(auth)/verify/page.tsx +56 -0
- package/template/src/app/(protected)/admin/page.tsx +108 -0
- package/template/src/app/(protected)/dashboard/loading.tsx +20 -0
- package/template/src/app/(protected)/dashboard/page.tsx +22 -0
- package/template/src/app/(protected)/layout.tsx +262 -0
- package/template/src/app/(protected)/settings/page.tsx +370 -0
- package/template/src/app/api/auth/forgot/route.ts +63 -0
- package/template/src/app/api/auth/login/route.ts +121 -0
- package/template/src/app/api/auth/logout/route.ts +19 -0
- package/template/src/app/api/auth/me/route.ts +30 -0
- package/template/src/app/api/auth/reset/route.ts +45 -0
- package/template/src/app/api/auth/signup/route.ts +85 -0
- package/template/src/app/api/auth/verify/route.ts +46 -0
- package/template/src/app/api/csrf/route.ts +12 -0
- package/template/src/app/api/health/route.ts +10 -0
- package/template/src/app/api/protected/admin/users/route.ts +24 -0
- package/template/src/app/api/protected/billing/checkout/route.ts +83 -0
- package/template/src/app/api/protected/billing/portal/route.ts +39 -0
- package/template/src/app/api/protected/files/[fileId]/route.ts +86 -0
- package/template/src/app/api/protected/files/upload/route.ts +64 -0
- package/template/src/app/api/protected/user/password/route.ts +63 -0
- package/template/src/app/api/protected/user/profile/route.ts +35 -0
- package/template/src/app/api/protected/user/sessions/[sessionId]/route.ts +33 -0
- package/template/src/app/api/protected/user/sessions/route.ts +22 -0
- package/template/src/app/api/readiness/route.ts +15 -0
- package/template/src/app/api/webhooks/stripe/route.ts +166 -0
- package/template/src/app/error.tsx +33 -0
- package/template/src/app/layout.tsx +29 -0
- package/template/src/app/not-found.tsx +20 -0
- package/template/src/app/page.tsx +136 -0
- package/template/src/app/privacy/page.tsx +178 -0
- package/template/src/app/providers.tsx +8 -0
- package/template/src/app/terms/page.tsx +139 -0
- package/template/src/config/app.config.ts +70 -0
- package/template/src/config/routes.ts +17 -0
- package/template/src/features/admin/index.ts +11 -0
- package/template/src/features/admin/permissions.ts +64 -0
- package/template/src/features/auth/context/AuthContext.tsx +96 -0
- package/template/src/features/auth/context/index.ts +2 -0
- package/template/src/features/auth/index.ts +3 -0
- package/template/src/features/auth/server/consent.ts +66 -0
- package/template/src/features/auth/server/session-revocation.ts +20 -0
- package/template/src/features/auth/server/sessions.ts +66 -0
- package/template/src/features/auth/server/user.ts +166 -0
- package/template/src/features/auth/types.ts +19 -0
- package/template/src/features/auth/validators.ts +29 -0
- package/template/src/features/billing/server/index.ts +66 -0
- package/template/src/features/billing/types.ts +43 -0
- package/template/src/features/uploads/server/index.ts +49 -0
- package/template/src/features/uploads/types.ts +26 -0
- package/template/src/lib/core/email/templates/base-layout.ts +122 -0
- package/template/src/lib/core/email/templates/index.ts +4 -0
- package/template/src/lib/core/email/templates/password-reset-email.ts +42 -0
- package/template/src/lib/core/email/templates/verification-email.ts +41 -0
- package/template/src/lib/core/email/templates/welcome-email.ts +40 -0
- package/template/src/lib/mars.ts +56 -0
- package/template/src/lib/prisma.ts +19 -0
- package/template/src/proxy.ts +92 -0
- package/template/src/styles/brand.css +15 -0
- package/template/src/styles/globals.css +7 -0
- package/template/tsconfig.json +59 -0
- package/template/vitest.config.ts +41 -0
- 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
|
|
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": "
|
|
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
|