start-vibing-stacks 2.3.0 → 2.4.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.
- package/dist/detector.js +19 -5
- package/dist/index.js +15 -3
- package/dist/scanner.js +23 -2
- package/dist/setup.js +17 -1
- package/dist/types.d.ts +4 -0
- package/dist/ui.js +6 -5
- package/package.json +1 -1
- package/stacks/_shared/config/security-rules.json +27 -5
- package/stacks/frontend/react/skills/preline-ui/SKILL.md +31 -35
- package/stacks/frontend/react/skills/react-standards/SKILL.md +20 -20
- package/stacks/frontend/react/skills/react-ui-patterns/SKILL.md +78 -42
- package/stacks/frontend/react/skills/tailwind-patterns/SKILL.md +1 -1
- package/stacks/frontend/react/skills/zod-validation/SKILL.md +84 -18
- package/stacks/nodejs/skills/nextjs-app-router/SKILL.md +101 -0
- package/stacks/nodejs/stack.json +43 -121
- package/templates/CLAUDE-nodejs.md +323 -0
- package/templates/CLAUDE-php.md +131 -10
|
@@ -138,7 +138,7 @@ function UserList({ users }: { users: User[] }) {
|
|
|
138
138
|
icon={<Users className="h-8 w-8" />}
|
|
139
139
|
title="No users yet"
|
|
140
140
|
description="Invite your first team member"
|
|
141
|
-
action={{ label: 'Invite User', onClick: () =>
|
|
141
|
+
action={{ label: 'Invite User', onClick: () => window.location.assign('/users/invite') }}
|
|
142
142
|
/>
|
|
143
143
|
);
|
|
144
144
|
}
|
|
@@ -191,57 +191,76 @@ function EmptyState({ icon, title, description, action }: {
|
|
|
191
191
|
</button>
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
-
## Form Pattern (
|
|
194
|
+
## Form Pattern (React Hook Form + Zod)
|
|
195
195
|
|
|
196
196
|
```tsx
|
|
197
|
-
import { useForm } from '
|
|
197
|
+
import { useForm } from 'react-hook-form';
|
|
198
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
199
|
+
import { useMutation } from '@tanstack/react-query';
|
|
200
|
+
import { z } from 'zod';
|
|
201
|
+
import { toast } from 'sonner';
|
|
202
|
+
|
|
203
|
+
const CreateUserSchema = z.object({
|
|
204
|
+
name: z.string().min(2, 'Name is required'),
|
|
205
|
+
email: z.string().email('Invalid email'),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
type CreateUserForm = z.infer<typeof CreateUserSchema>;
|
|
198
209
|
|
|
199
210
|
export default function CreateUser() {
|
|
200
|
-
const {
|
|
201
|
-
|
|
202
|
-
|
|
211
|
+
const {
|
|
212
|
+
register,
|
|
213
|
+
handleSubmit,
|
|
214
|
+
reset,
|
|
215
|
+
formState: { errors },
|
|
216
|
+
} = useForm<CreateUserForm>({
|
|
217
|
+
resolver: zodResolver(CreateUserSchema),
|
|
203
218
|
});
|
|
204
219
|
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
220
|
+
const mutation = useMutation({
|
|
221
|
+
mutationFn: (data: CreateUserForm) =>
|
|
222
|
+
fetch('/api/users', {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
headers: { 'Content-Type': 'application/json' },
|
|
225
|
+
body: JSON.stringify(data),
|
|
226
|
+
}).then((res) => {
|
|
227
|
+
if (!res.ok) throw new Error('Failed to create user');
|
|
228
|
+
return res.json();
|
|
229
|
+
}),
|
|
230
|
+
onSuccess: () => {
|
|
231
|
+
toast.success('User created!');
|
|
232
|
+
reset();
|
|
233
|
+
},
|
|
234
|
+
onError: () => toast.error('Failed to create user'),
|
|
235
|
+
});
|
|
215
236
|
|
|
216
237
|
return (
|
|
217
|
-
<form onSubmit={
|
|
238
|
+
<form onSubmit={handleSubmit((data) => mutation.mutate(data))} className="space-y-4">
|
|
218
239
|
<div>
|
|
219
240
|
<label className="block text-sm font-medium text-foreground mb-1">Name</label>
|
|
220
241
|
<input
|
|
221
|
-
|
|
222
|
-
onChange={e => setData('name', e.target.value)}
|
|
242
|
+
{...register('name')}
|
|
223
243
|
className="w-full h-10 px-3 rounded-md border border-border bg-background text-foreground"
|
|
224
244
|
/>
|
|
225
|
-
{errors.name && <p className="mt-1 text-sm text-destructive">{errors.name}</p>}
|
|
245
|
+
{errors.name && <p className="mt-1 text-sm text-destructive">{errors.name.message}</p>}
|
|
226
246
|
</div>
|
|
227
247
|
|
|
228
248
|
<div>
|
|
229
249
|
<label className="block text-sm font-medium text-foreground mb-1">Email</label>
|
|
230
250
|
<input
|
|
231
251
|
type="email"
|
|
232
|
-
|
|
233
|
-
onChange={e => setData('email', e.target.value)}
|
|
252
|
+
{...register('email')}
|
|
234
253
|
className="w-full h-10 px-3 rounded-md border border-border bg-background text-foreground"
|
|
235
254
|
/>
|
|
236
|
-
{errors.email && <p className="mt-1 text-sm text-destructive">{errors.email}</p>}
|
|
255
|
+
{errors.email && <p className="mt-1 text-sm text-destructive">{errors.email.message}</p>}
|
|
237
256
|
</div>
|
|
238
257
|
|
|
239
258
|
<button
|
|
240
259
|
type="submit"
|
|
241
|
-
disabled={
|
|
260
|
+
disabled={mutation.isPending}
|
|
242
261
|
className="bg-primary text-primary-foreground px-6 py-2 rounded-lg hover:bg-primary-hover disabled:opacity-50 transition-colors"
|
|
243
262
|
>
|
|
244
|
-
{
|
|
263
|
+
{mutation.isPending ? (
|
|
245
264
|
<span className="flex items-center gap-2">
|
|
246
265
|
<Loader className="h-4 w-4 animate-spin" /> Creating...
|
|
247
266
|
</span>
|
|
@@ -252,27 +271,44 @@ export default function CreateUser() {
|
|
|
252
271
|
}
|
|
253
272
|
```
|
|
254
273
|
|
|
255
|
-
## Optimistic Updates
|
|
274
|
+
## Optimistic Updates (TanStack Query)
|
|
256
275
|
|
|
257
276
|
```tsx
|
|
258
|
-
|
|
277
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
278
|
+
|
|
259
279
|
function ToggleFavorite({ item }: { item: Item }) {
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
280
|
+
const queryClient = useQueryClient();
|
|
281
|
+
|
|
282
|
+
const mutation = useMutation({
|
|
283
|
+
mutationFn: () =>
|
|
284
|
+
fetch(`/api/items/${item.id}/favorite`, { method: 'POST' }).then((res) => {
|
|
285
|
+
if (!res.ok) throw new Error('Failed');
|
|
286
|
+
return res.json();
|
|
287
|
+
}),
|
|
288
|
+
onMutate: async () => {
|
|
289
|
+
await queryClient.cancelQueries({ queryKey: ['items'] });
|
|
290
|
+
const previous = queryClient.getQueryData<Item[]>(['items']);
|
|
291
|
+
|
|
292
|
+
queryClient.setQueryData<Item[]>(['items'], (old) =>
|
|
293
|
+
old?.map((i) =>
|
|
294
|
+
i.id === item.id ? { ...i, isFavorite: !i.isFavorite } : i
|
|
295
|
+
)
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
return { previous };
|
|
299
|
+
},
|
|
300
|
+
onError: (_err, _vars, context) => {
|
|
301
|
+
queryClient.setQueryData(['items'], context?.previous);
|
|
302
|
+
toast.error('Failed to update');
|
|
303
|
+
},
|
|
304
|
+
onSettled: () => {
|
|
305
|
+
queryClient.invalidateQueries({ queryKey: ['items'] });
|
|
306
|
+
},
|
|
307
|
+
});
|
|
272
308
|
|
|
273
309
|
return (
|
|
274
|
-
<button onClick={
|
|
275
|
-
{
|
|
310
|
+
<button onClick={() => mutation.mutate()} className="text-xl">
|
|
311
|
+
{item.isFavorite ? '❤️' : '🤍'}
|
|
276
312
|
</button>
|
|
277
313
|
);
|
|
278
314
|
}
|
|
@@ -294,5 +330,5 @@ function ToggleFavorite({ item }: { item: Item }) {
|
|
|
294
330
|
1. **`if (loading) return <Spinner />`** — check `loading && !data` instead
|
|
295
331
|
2. **Silent catch** — always toast/display errors to user
|
|
296
332
|
3. **No empty state** — every list needs one
|
|
297
|
-
4. **Clickable button during submit** — always `disabled={
|
|
333
|
+
4. **Clickable button during submit** — always `disabled={isPending}` or `disabled={isSubmitting}`
|
|
298
334
|
5. **Console.log-only errors** — user must see feedback
|
|
@@ -113,18 +113,38 @@ if (result.success) {
|
|
|
113
113
|
|
|
114
114
|
### Environment Variables
|
|
115
115
|
|
|
116
|
+
> **Split server and client env schemas.** `NEXT_PUBLIC_*` is embedded in the browser bundle — NEVER put secrets there.
|
|
117
|
+
|
|
116
118
|
```tsx
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
NEXT_PUBLIC_STRIPE_KEY: z.string().startsWith('pk_'),
|
|
119
|
+
// lib/env.server.ts — Server-only secrets (NEVER import from client components)
|
|
120
|
+
const ServerEnvSchema = z.object({
|
|
120
121
|
DATABASE_URL: z.string().min(1),
|
|
122
|
+
OPENAI_KEY: z.string().startsWith('sk-'),
|
|
123
|
+
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
|
|
121
124
|
NODE_ENV: z.enum(['development', 'production', 'test']),
|
|
122
125
|
});
|
|
123
126
|
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
export const serverEnv = ServerEnvSchema.parse({
|
|
128
|
+
DATABASE_URL: process.env['DATABASE_URL'],
|
|
129
|
+
OPENAI_KEY: process.env['OPENAI_KEY'],
|
|
130
|
+
STRIPE_SECRET_KEY: process.env['STRIPE_SECRET_KEY'],
|
|
131
|
+
NODE_ENV: process.env['NODE_ENV'],
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// lib/env.client.ts — Public vars only (safe for browser)
|
|
135
|
+
const ClientEnvSchema = z.object({
|
|
136
|
+
NEXT_PUBLIC_APP_URL: z.string().url(),
|
|
137
|
+
NEXT_PUBLIC_STRIPE_KEY: z.string().startsWith('pk_'),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
export const clientEnv = ClientEnvSchema.parse({
|
|
141
|
+
NEXT_PUBLIC_APP_URL: process.env['NEXT_PUBLIC_APP_URL'],
|
|
142
|
+
NEXT_PUBLIC_STRIPE_KEY: process.env['NEXT_PUBLIC_STRIPE_KEY'],
|
|
143
|
+
});
|
|
126
144
|
```
|
|
127
145
|
|
|
146
|
+
**Rule:** If a variable contains a key, secret, token, or password, it MUST be in `ServerEnvSchema` without `NEXT_PUBLIC_` prefix.
|
|
147
|
+
|
|
128
148
|
### Reusable Schemas
|
|
129
149
|
|
|
130
150
|
```tsx
|
|
@@ -160,26 +180,72 @@ const ContactSchema = z.object({
|
|
|
160
180
|
});
|
|
161
181
|
```
|
|
162
182
|
|
|
163
|
-
## Integration with
|
|
183
|
+
## Integration with Next.js Server Actions
|
|
164
184
|
|
|
165
185
|
```tsx
|
|
166
|
-
|
|
167
|
-
|
|
186
|
+
'use server';
|
|
187
|
+
|
|
188
|
+
import { z } from 'zod';
|
|
189
|
+
|
|
190
|
+
const CreateLeadSchema = z.object({
|
|
168
191
|
name: z.string().min(2),
|
|
169
|
-
email:
|
|
170
|
-
|
|
192
|
+
email: z.string().email().toLowerCase().trim(),
|
|
193
|
+
domainId: z.string().uuid(),
|
|
171
194
|
});
|
|
172
195
|
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
196
|
+
export async function createLead(formData: FormData) {
|
|
197
|
+
const result = CreateLeadSchema.safeParse({
|
|
198
|
+
name: formData.get('name'),
|
|
199
|
+
email: formData.get('email'),
|
|
200
|
+
domainId: formData.get('domainId'),
|
|
201
|
+
});
|
|
202
|
+
|
|
176
203
|
if (!result.success) {
|
|
177
|
-
|
|
178
|
-
return;
|
|
204
|
+
return { errors: result.error.flatten().fieldErrors };
|
|
179
205
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
};
|
|
206
|
+
|
|
207
|
+
await db.lead.create({ data: result.data });
|
|
208
|
+
return { success: true };
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Client-Side with React Hook Form
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
'use client';
|
|
216
|
+
|
|
217
|
+
import { useForm } from 'react-hook-form';
|
|
218
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
219
|
+
|
|
220
|
+
const CreateLeadSchema = z.object({
|
|
221
|
+
name: z.string().min(2),
|
|
222
|
+
email: z.string().email(),
|
|
223
|
+
domainId: z.string().uuid(),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
type CreateLeadForm = z.infer<typeof CreateLeadSchema>;
|
|
227
|
+
|
|
228
|
+
export function LeadForm() {
|
|
229
|
+
const { register, handleSubmit, formState: { errors } } = useForm<CreateLeadForm>({
|
|
230
|
+
resolver: zodResolver(CreateLeadSchema),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const onSubmit = async (data: CreateLeadForm) => {
|
|
234
|
+
await fetch('/api/leads', {
|
|
235
|
+
method: 'POST',
|
|
236
|
+
headers: { 'Content-Type': 'application/json' },
|
|
237
|
+
body: JSON.stringify(data),
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
243
|
+
<input {...register('name')} />
|
|
244
|
+
{errors.name && <span>{errors.name.message}</span>}
|
|
245
|
+
{/* ... */}
|
|
246
|
+
</form>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
183
249
|
```
|
|
184
250
|
|
|
185
251
|
## FORBIDDEN
|
|
@@ -114,6 +114,104 @@ export async function generateMetadata({ params }: { params: { id: string } }) {
|
|
|
114
114
|
}
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
## Environment Variables & API Security (MANDATORY)
|
|
118
|
+
|
|
119
|
+
> **NEXT_PUBLIC_ vars are embedded in the browser JS bundle.** Anyone can see them in DevTools.
|
|
120
|
+
|
|
121
|
+
### Server vs Client Environment
|
|
122
|
+
|
|
123
|
+
| Prefix | Accessible from | Safe for |
|
|
124
|
+
|--------|----------------|----------|
|
|
125
|
+
| No prefix | Server Components, Route Handlers, Server Actions | API keys, secrets, tokens, DB URLs |
|
|
126
|
+
| `NEXT_PUBLIC_*` | Server + Browser (embedded in JS) | Public URLs, analytics IDs, publishable keys |
|
|
127
|
+
|
|
128
|
+
### FORBIDDEN Environment Patterns
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# NEVER DO THIS — secret exposed in browser bundle
|
|
132
|
+
NEXT_PUBLIC_OPENAI_KEY=sk-abc123
|
|
133
|
+
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_abc
|
|
134
|
+
NEXT_PUBLIC_DATABASE_URL=postgresql://user:pass@host/db
|
|
135
|
+
|
|
136
|
+
# CORRECT — server-only (no NEXT_PUBLIC_ prefix)
|
|
137
|
+
OPENAI_KEY=sk-abc123
|
|
138
|
+
STRIPE_SECRET_KEY=sk_live_abc
|
|
139
|
+
DATABASE_URL=postgresql://user:pass@host/db
|
|
140
|
+
|
|
141
|
+
# OK as NEXT_PUBLIC_ — no secret value
|
|
142
|
+
NEXT_PUBLIC_APP_URL=https://myapp.com
|
|
143
|
+
NEXT_PUBLIC_STRIPE_KEY=pk_live_abc
|
|
144
|
+
NEXT_PUBLIC_GA_ID=G-XXXXX
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### API Proxy Pattern (MANDATORY)
|
|
148
|
+
|
|
149
|
+
External API calls with secrets MUST go through server-side Route Handlers:
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
// app/api/ai/route.ts — Secret stays on server
|
|
153
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
154
|
+
|
|
155
|
+
export async function POST(req: NextRequest) {
|
|
156
|
+
const { prompt } = await req.json();
|
|
157
|
+
|
|
158
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: {
|
|
161
|
+
'Content-Type': 'application/json',
|
|
162
|
+
Authorization: `Bearer ${process.env['OPENAI_KEY']}`,
|
|
163
|
+
},
|
|
164
|
+
body: JSON.stringify({
|
|
165
|
+
model: 'gpt-4',
|
|
166
|
+
messages: [{ role: 'user', content: prompt }],
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
return NextResponse.json({ error: 'AI request failed' }, { status: 502 });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return NextResponse.json(await response.json());
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
// components/chat.tsx — Client calls YOUR route, not external API
|
|
180
|
+
'use client';
|
|
181
|
+
|
|
182
|
+
async function sendMessage(prompt: string) {
|
|
183
|
+
const res = await fetch('/api/ai', {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
headers: { 'Content-Type': 'application/json' },
|
|
186
|
+
body: JSON.stringify({ prompt }),
|
|
187
|
+
});
|
|
188
|
+
return res.json();
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Server Actions for Mutations
|
|
193
|
+
|
|
194
|
+
Server Actions also keep secrets server-side:
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
// app/actions/payment.ts
|
|
198
|
+
'use server';
|
|
199
|
+
|
|
200
|
+
import Stripe from 'stripe';
|
|
201
|
+
|
|
202
|
+
const stripe = new Stripe(process.env['STRIPE_SECRET_KEY']!);
|
|
203
|
+
|
|
204
|
+
export async function createCheckout(priceId: string) {
|
|
205
|
+
const session = await stripe.checkout.sessions.create({
|
|
206
|
+
mode: 'payment',
|
|
207
|
+
line_items: [{ price: priceId, quantity: 1 }],
|
|
208
|
+
success_url: `${process.env['APP_URL']}/success`,
|
|
209
|
+
cancel_url: `${process.env['APP_URL']}/cancel`,
|
|
210
|
+
});
|
|
211
|
+
return { url: session.url };
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
117
215
|
## FORBIDDEN
|
|
118
216
|
|
|
119
217
|
1. **`'use client'` on server-capable components** — default to server
|
|
@@ -121,3 +219,6 @@ export async function generateMetadata({ params }: { params: { id: string } }) {
|
|
|
121
219
|
3. **`getServerSideProps` / `getStaticProps`** — App Router uses async components
|
|
122
220
|
4. **API routes for server-only data** — use server components directly
|
|
123
221
|
5. **Prop drilling through layouts** — use parallel routes or context
|
|
222
|
+
6. **`NEXT_PUBLIC_` with API keys, secrets, or tokens** — secrets leak to browser bundle
|
|
223
|
+
7. **Calling external APIs from client components** — use Route Handlers as proxy
|
|
224
|
+
8. **`process.env['SECRET']` in `'use client'` files** — only `NEXT_PUBLIC_*` vars work client-side
|
package/stacks/nodejs/stack.json
CHANGED
|
@@ -1,29 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "nodejs",
|
|
3
3
|
"name": "Node.js / TypeScript",
|
|
4
|
-
"icon": "
|
|
4
|
+
"icon": "📦",
|
|
5
5
|
"runtime": "Bun / Node.js 20+",
|
|
6
6
|
"minVersion": "20.0.0",
|
|
7
7
|
"packageManager": "bun|npm|pnpm",
|
|
8
|
-
"extensions": [
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
".js",
|
|
12
|
-
".jsx",
|
|
13
|
-
".mjs",
|
|
14
|
-
".cjs"
|
|
15
|
-
],
|
|
16
|
-
"testExtensions": [
|
|
17
|
-
"*.test.ts",
|
|
18
|
-
"*.spec.ts",
|
|
19
|
-
"*.test.tsx"
|
|
20
|
-
],
|
|
21
|
-
"detectFiles": [
|
|
22
|
-
"package.json",
|
|
23
|
-
"tsconfig.json",
|
|
24
|
-
"bun.lockb",
|
|
25
|
-
"next.config.js"
|
|
26
|
-
],
|
|
8
|
+
"extensions": [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
|
|
9
|
+
"testExtensions": ["*.test.ts", "*.spec.ts", "*.test.tsx"],
|
|
10
|
+
"detectFiles": ["package.json", "tsconfig.json", "bun.lockb", "bun.lock", "next.config.js"],
|
|
27
11
|
"commands": {
|
|
28
12
|
"test": "bun run test",
|
|
29
13
|
"lint": "bun run lint",
|
|
@@ -33,146 +17,95 @@
|
|
|
33
17
|
"typecheck": "bun run typecheck"
|
|
34
18
|
},
|
|
35
19
|
"qualityGates": [
|
|
36
|
-
{
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"order": 1
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
"name": "Lint",
|
|
44
|
-
"command": "bun run lint",
|
|
45
|
-
"required": true,
|
|
46
|
-
"order": 2
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
"name": "Tests",
|
|
50
|
-
"command": "bun run test",
|
|
51
|
-
"required": true,
|
|
52
|
-
"order": 3
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"name": "Build",
|
|
56
|
-
"command": "bun run build",
|
|
57
|
-
"required": true,
|
|
58
|
-
"order": 4
|
|
59
|
-
}
|
|
20
|
+
{ "name": "TypeCheck", "command": "bun run typecheck", "required": true, "order": 1 },
|
|
21
|
+
{ "name": "Lint", "command": "bun run lint", "required": true, "order": 2 },
|
|
22
|
+
{ "name": "Tests", "command": "bun run test", "required": true, "order": 3 },
|
|
23
|
+
{ "name": "Build", "command": "bun run build", "required": true, "order": 4 }
|
|
60
24
|
],
|
|
61
25
|
"frameworks": [
|
|
62
26
|
{
|
|
63
27
|
"id": "nextjs",
|
|
64
28
|
"name": "Next.js (App Router)",
|
|
65
|
-
"icon": "
|
|
66
|
-
"detectFiles": [
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
"next.config.mjs"
|
|
70
|
-
]
|
|
29
|
+
"icon": "▲",
|
|
30
|
+
"detectFiles": ["next.config.js", "next.config.ts", "next.config.mjs"],
|
|
31
|
+
"default": true,
|
|
32
|
+
"skills": ["nextjs-app-router"]
|
|
71
33
|
},
|
|
72
34
|
{
|
|
73
35
|
"id": "nuxt",
|
|
74
36
|
"name": "Nuxt",
|
|
75
|
-
"icon": "
|
|
76
|
-
"detectFiles": [
|
|
77
|
-
|
|
78
|
-
]
|
|
37
|
+
"icon": "💚",
|
|
38
|
+
"detectFiles": ["nuxt.config.ts"],
|
|
39
|
+
"skills": []
|
|
79
40
|
},
|
|
80
41
|
{
|
|
81
42
|
"id": "astro",
|
|
82
43
|
"name": "Astro",
|
|
83
|
-
"icon": "
|
|
84
|
-
"detectFiles": [
|
|
85
|
-
|
|
86
|
-
]
|
|
44
|
+
"icon": "🚀",
|
|
45
|
+
"detectFiles": ["astro.config.mjs"],
|
|
46
|
+
"skills": []
|
|
87
47
|
},
|
|
88
48
|
{
|
|
89
49
|
"id": "express",
|
|
90
50
|
"name": "Express",
|
|
91
|
-
"icon": "
|
|
51
|
+
"icon": "⚡",
|
|
52
|
+
"skills": ["trpc-api"]
|
|
92
53
|
},
|
|
93
54
|
{
|
|
94
55
|
"id": "fastify",
|
|
95
56
|
"name": "Fastify",
|
|
96
|
-
"icon": "
|
|
57
|
+
"icon": "🏎️",
|
|
58
|
+
"skills": ["trpc-api"]
|
|
97
59
|
},
|
|
98
60
|
{
|
|
99
61
|
"id": "vanilla",
|
|
100
62
|
"name": "Vanilla Node.js",
|
|
101
|
-
"icon": "
|
|
63
|
+
"icon": "📄",
|
|
64
|
+
"skills": []
|
|
102
65
|
}
|
|
103
66
|
],
|
|
104
67
|
"databases": [
|
|
105
|
-
{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
"id": "postgresql",
|
|
112
|
-
"name": "PostgreSQL",
|
|
113
|
-
"icon": "\ud83d\udc18"
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
"id": "mysql",
|
|
117
|
-
"name": "MySQL / MariaDB",
|
|
118
|
-
"icon": "\ud83d\udc2c"
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
"id": "sqlite",
|
|
122
|
-
"name": "SQLite (Turso / libSQL)",
|
|
123
|
-
"icon": "\ud83d\udcc1"
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
"id": "redis",
|
|
127
|
-
"name": "Redis (Upstash)",
|
|
128
|
-
"icon": "\ud83d\udd34"
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
"id": "none",
|
|
132
|
-
"name": "None",
|
|
133
|
-
"icon": "\u274c"
|
|
134
|
-
}
|
|
68
|
+
{ "id": "mongodb", "name": "MongoDB", "icon": "🍃" },
|
|
69
|
+
{ "id": "postgresql", "name": "PostgreSQL", "icon": "🐘" },
|
|
70
|
+
{ "id": "mysql", "name": "MySQL / MariaDB", "icon": "🐬" },
|
|
71
|
+
{ "id": "sqlite", "name": "SQLite (Turso / libSQL)", "icon": "📁" },
|
|
72
|
+
{ "id": "redis", "name": "Redis (Upstash)", "icon": "🔴" },
|
|
73
|
+
{ "id": "none", "name": "None", "icon": "❌" }
|
|
135
74
|
],
|
|
136
75
|
"frontendOptions": [
|
|
137
76
|
{
|
|
138
77
|
"id": "react-tailwind",
|
|
139
78
|
"name": "React 19+ / TailwindCSS 4+",
|
|
140
|
-
"icon": "
|
|
79
|
+
"icon": "⚛️",
|
|
80
|
+
"skillsDir": "react",
|
|
81
|
+
"default": true,
|
|
82
|
+
"frameworks": ["nextjs", "express", "fastify", "vanilla"]
|
|
141
83
|
},
|
|
142
84
|
{
|
|
143
85
|
"id": "vue",
|
|
144
86
|
"name": "Vue.js / Nuxt",
|
|
145
|
-
"icon": "
|
|
87
|
+
"icon": "💚",
|
|
88
|
+
"frameworks": ["nuxt"]
|
|
146
89
|
},
|
|
147
90
|
{
|
|
148
91
|
"id": "svelte",
|
|
149
92
|
"name": "Svelte / SvelteKit",
|
|
150
|
-
"icon": "
|
|
151
|
-
|
|
152
|
-
{
|
|
153
|
-
"id": "shadcn",
|
|
154
|
-
"name": "shadcn/ui + Tailwind",
|
|
155
|
-
"icon": "\ud83c\udfa8"
|
|
93
|
+
"icon": "🔥",
|
|
94
|
+
"frameworks": ["astro", "vanilla"]
|
|
156
95
|
},
|
|
157
96
|
{
|
|
158
97
|
"id": "none",
|
|
159
|
-
"name": "API only
|
|
160
|
-
"icon": "
|
|
98
|
+
"name": "API only — no frontend",
|
|
99
|
+
"icon": "❌"
|
|
161
100
|
}
|
|
162
101
|
],
|
|
163
102
|
"deployTargets": [
|
|
164
|
-
{
|
|
165
|
-
"id": "github",
|
|
166
|
-
"name": "GitHub (git push)",
|
|
167
|
-
"icon": "\ud83d\udc19"
|
|
168
|
-
}
|
|
103
|
+
{ "id": "github", "name": "GitHub (git push)", "icon": "🐙" }
|
|
169
104
|
],
|
|
170
105
|
"skills": [
|
|
171
106
|
"typescript-strict",
|
|
172
|
-
"
|
|
173
|
-
"
|
|
174
|
-
"zod-validation",
|
|
175
|
-
"vitest-testing"
|
|
107
|
+
"bun-runtime",
|
|
108
|
+
"zod-validation"
|
|
176
109
|
],
|
|
177
110
|
"requirements": [
|
|
178
111
|
{
|
|
@@ -185,17 +118,6 @@
|
|
|
185
118
|
"linux": "curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt install -y nodejs"
|
|
186
119
|
},
|
|
187
120
|
"versionRegex": "v?(\\d+\\.\\d+\\.\\d+)"
|
|
188
|
-
},
|
|
189
|
-
{
|
|
190
|
-
"name": "Bun",
|
|
191
|
-
"command": "bun",
|
|
192
|
-
"versionFlag": "--version",
|
|
193
|
-
"minVersion": "1.0.0",
|
|
194
|
-
"installCommand": {
|
|
195
|
-
"macos": "brew install oven-sh/bun/bun",
|
|
196
|
-
"linux": "curl -fsSL https://bun.sh/install | bash"
|
|
197
|
-
},
|
|
198
|
-
"versionRegex": "(\\d+\\.\\d+\\.\\d+)"
|
|
199
121
|
}
|
|
200
122
|
]
|
|
201
123
|
}
|