create-loadout 1.0.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/README.md +154 -0
- package/dist/claude-md.d.ts +3 -0
- package/dist/claude-md.js +494 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +186 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +98 -0
- package/dist/create-next.d.ts +1 -0
- package/dist/create-next.js +17 -0
- package/dist/detect.d.ts +4 -0
- package/dist/detect.js +60 -0
- package/dist/env.d.ts +3 -0
- package/dist/env.js +183 -0
- package/dist/generate-readme.d.ts +3 -0
- package/dist/generate-readme.js +160 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/instrumentation.d.ts +3 -0
- package/dist/instrumentation.js +95 -0
- package/dist/integrations/ai-sdk.d.ts +3 -0
- package/dist/integrations/ai-sdk.js +20 -0
- package/dist/integrations/clerk.d.ts +2 -0
- package/dist/integrations/clerk.js +50 -0
- package/dist/integrations/firecrawl.d.ts +2 -0
- package/dist/integrations/firecrawl.js +26 -0
- package/dist/integrations/index.d.ts +4 -0
- package/dist/integrations/index.js +64 -0
- package/dist/integrations/inngest.d.ts +2 -0
- package/dist/integrations/inngest.js +45 -0
- package/dist/integrations/neon-drizzle.d.ts +2 -0
- package/dist/integrations/neon-drizzle.js +56 -0
- package/dist/integrations/posthog.d.ts +2 -0
- package/dist/integrations/posthog.js +25 -0
- package/dist/integrations/resend.d.ts +2 -0
- package/dist/integrations/resend.js +34 -0
- package/dist/integrations/sentry.d.ts +2 -0
- package/dist/integrations/sentry.js +47 -0
- package/dist/integrations/stripe.d.ts +2 -0
- package/dist/integrations/stripe.js +45 -0
- package/dist/integrations/uploadthing.d.ts +2 -0
- package/dist/integrations/uploadthing.js +34 -0
- package/dist/landing-page.d.ts +2 -0
- package/dist/landing-page.js +97 -0
- package/dist/prompts.d.ts +7 -0
- package/dist/prompts.js +99 -0
- package/dist/setup-shadcn.d.ts +1 -0
- package/dist/setup-shadcn.js +27 -0
- package/dist/templates/ai-sdk.d.ts +12 -0
- package/dist/templates/ai-sdk.js +96 -0
- package/dist/templates/clerk.d.ts +6 -0
- package/dist/templates/clerk.js +96 -0
- package/dist/templates/firecrawl.d.ts +4 -0
- package/dist/templates/firecrawl.js +106 -0
- package/dist/templates/inngest.d.ts +6 -0
- package/dist/templates/inngest.js +91 -0
- package/dist/templates/neon-drizzle.d.ts +16 -0
- package/dist/templates/neon-drizzle.js +343 -0
- package/dist/templates/posthog.d.ts +3 -0
- package/dist/templates/posthog.js +10 -0
- package/dist/templates/resend.d.ts +5 -0
- package/dist/templates/resend.js +102 -0
- package/dist/templates/sentry.d.ts +8 -0
- package/dist/templates/sentry.js +145 -0
- package/dist/templates/stripe.d.ts +6 -0
- package/dist/templates/stripe.js +215 -0
- package/dist/templates/uploadthing.d.ts +7 -0
- package/dist/templates/uploadthing.js +150 -0
- package/dist/templates/zustand.d.ts +3 -0
- package/dist/templates/zustand.js +26 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.js +1 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
```txt
|
|
4
|
+
_ _ _
|
|
5
|
+
| | ___ __ _ __| |___ _ _| |_
|
|
6
|
+
| |__/ _ \/ _` / _` / _ \ || | _|
|
|
7
|
+
|____\___/\__,_\__,_\___/\_,_|\__|
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**Stop copy-pasting boilerplate. Start building.**
|
|
12
|
+
|
|
13
|
+
**An opinionated Next.js scaffold with the integrations you probably need.**
|
|
14
|
+
|
|
15
|
+
[](https://www.npmjs.com/package/create-loadout)
|
|
16
|
+
[](https://www.npmjs.com/package/create-loadout)
|
|
17
|
+
[](https://github.com/KylerD/loadout)
|
|
18
|
+
[](LICENSE)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx create-loadout
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Works on Mac, Windows, and Linux.
|
|
25
|
+
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Why I Built This
|
|
31
|
+
|
|
32
|
+
Every new SaaS project starts the same way. Create the Next.js app. Add Tailwind. Set up shadcn. Copy your auth config from the last project. Wire up the database. Add Stripe. Configure error tracking. Set up analytics.
|
|
33
|
+
|
|
34
|
+
It's the same 2-4 hours every time. And every time, you're copy-pasting from old projects, fixing the inevitable drift, and wondering if you remembered everything.
|
|
35
|
+
|
|
36
|
+
**Loadout gives you a fully-wired Next.js app in under a minute.**
|
|
37
|
+
|
|
38
|
+
You pick your integrations. It scaffolds everything — services, API routes, typed env vars, even a `CLAUDE.md` so your AI assistant knows how the project is structured.
|
|
39
|
+
|
|
40
|
+
No more boilerplate archaeology. Just start building.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## What You Get
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
your-app/
|
|
48
|
+
├── app/ # Next.js App Router
|
|
49
|
+
├── components/ # React components + shadcn/ui
|
|
50
|
+
├── actions/ # Server actions
|
|
51
|
+
├── services/ # Business logic (DI-ready)
|
|
52
|
+
├── dao/ # Data access layer (Drizzle ORM)
|
|
53
|
+
├── models/ # DTOs, views, schemas, state
|
|
54
|
+
├── lib/
|
|
55
|
+
│ ├── config.ts # Type-safe env vars
|
|
56
|
+
│ └── db/ # Database client + schema
|
|
57
|
+
├── CLAUDE.md # AI assistant context
|
|
58
|
+
├── .env.example # Documented env template
|
|
59
|
+
└── .env.local # Your secrets (gitignored)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Every integration follows the same pattern:**
|
|
63
|
+
|
|
64
|
+
- Services for business logic
|
|
65
|
+
- Type-safe configuration
|
|
66
|
+
- Ready-to-use API routes where needed
|
|
67
|
+
- Zero magic — just clean, readable code
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Available Integrations
|
|
72
|
+
|
|
73
|
+
| | Integration | What You Get |
|
|
74
|
+
| :-: | ------------------ | ------------------------------------------ |
|
|
75
|
+
| 🔐 | **Clerk** | Authentication + user service |
|
|
76
|
+
| 🗄️ | **Neon + Drizzle** | Serverless Postgres with full CRUD example |
|
|
77
|
+
| 🤖 | **AI SDK** | OpenAI / Anthropic / Google |
|
|
78
|
+
| 📧 | **Resend** | Email service + React email templates |
|
|
79
|
+
| 🔥 | **Firecrawl** | Web scraping service |
|
|
80
|
+
| ⏰ | **Inngest** | Background jobs |
|
|
81
|
+
| 📁 | **UploadThing** | File uploads |
|
|
82
|
+
| 💳 | **Stripe** | Checkout, webhooks, customer portal |
|
|
83
|
+
| 📊 | **PostHog** | Product analytics |
|
|
84
|
+
| 🐛 | **Sentry** | Error tracking |
|
|
85
|
+
|
|
86
|
+
**Always included:** TypeScript, Tailwind, shadcn/ui, Zod, Zustand, Luxon
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## How It Works
|
|
91
|
+
|
|
92
|
+
### 1. Run the CLI
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npx create-loadout
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 2. Answer the Prompts
|
|
99
|
+
|
|
100
|
+
- Project name
|
|
101
|
+
- Which integrations you need
|
|
102
|
+
- AI provider (if using AI SDK)
|
|
103
|
+
|
|
104
|
+
### 3. Start Building
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
cd your-app
|
|
108
|
+
npm install
|
|
109
|
+
npm run dev
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
That's it. Fill in `.env.local` and you're live.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Architecture
|
|
117
|
+
|
|
118
|
+
Generated projects follow a **layered architecture**:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
UI Components (app/, components/)
|
|
122
|
+
↓
|
|
123
|
+
Server Actions (actions/*.actions.ts)
|
|
124
|
+
↓
|
|
125
|
+
Services (services/*.service.ts)
|
|
126
|
+
↓
|
|
127
|
+
DAOs (dao/*.dao.ts + Drizzle ORM)
|
|
128
|
+
↓
|
|
129
|
+
Neon (Serverless Postgres)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Services use constructor-based dependency injection with singleton exports — optimized for Next.js serverless.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Development
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
git clone https://github.com/KylerD/loadout.git
|
|
140
|
+
cd loadout
|
|
141
|
+
npm install
|
|
142
|
+
npm run dev
|
|
143
|
+
|
|
144
|
+
# Build and test locally
|
|
145
|
+
npm run build
|
|
146
|
+
npm link
|
|
147
|
+
create-loadout
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const stackSections = [
|
|
4
|
+
{
|
|
5
|
+
id: 'core',
|
|
6
|
+
name: 'Core',
|
|
7
|
+
items: [
|
|
8
|
+
{ name: 'Next.js', url: 'https://nextjs.org/docs', description: 'React framework with App Router' },
|
|
9
|
+
{ name: 'TypeScript', url: 'https://www.typescriptlang.org/docs/', description: 'Type safety' },
|
|
10
|
+
{ name: 'Tailwind CSS', url: 'https://tailwindcss.com/docs', description: 'Utility-first CSS' },
|
|
11
|
+
{ name: 'shadcn/ui', url: 'https://ui.shadcn.com/docs', description: 'UI components' },
|
|
12
|
+
{ name: 'Zod', url: 'https://zod.dev/', description: 'Schema validation' },
|
|
13
|
+
{ name: 'Zustand', url: 'https://zustand.docs.pmnd.rs/', description: 'Client state management' },
|
|
14
|
+
{ name: 'Luxon', url: 'https://moment.github.io/luxon/', description: 'Date/time manipulation' },
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'clerk',
|
|
19
|
+
name: 'Authentication',
|
|
20
|
+
items: [
|
|
21
|
+
{ name: 'Clerk', url: 'https://clerk.com/docs', description: 'Authentication and user management' },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'neon-drizzle',
|
|
26
|
+
name: 'Database',
|
|
27
|
+
items: [
|
|
28
|
+
{ name: 'Neon', url: 'https://neon.tech/docs', description: 'Serverless Postgres' },
|
|
29
|
+
{ name: 'Drizzle ORM', url: 'https://orm.drizzle.team/docs/overview', description: 'TypeScript ORM' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'ai-sdk',
|
|
34
|
+
name: 'AI',
|
|
35
|
+
items: [
|
|
36
|
+
{ name: 'Vercel AI SDK', url: 'https://sdk.vercel.ai/docs', description: 'AI integration' },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'resend',
|
|
41
|
+
name: 'Email',
|
|
42
|
+
items: [
|
|
43
|
+
{ name: 'Resend', url: 'https://resend.com/docs', description: 'Email API' },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'firecrawl',
|
|
48
|
+
name: 'Scraping',
|
|
49
|
+
items: [
|
|
50
|
+
{ name: 'Firecrawl', url: 'https://docs.firecrawl.dev/', description: 'Web scraping' },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'inngest',
|
|
55
|
+
name: 'Background Jobs',
|
|
56
|
+
items: [
|
|
57
|
+
{ name: 'Inngest', url: 'https://www.inngest.com/docs', description: 'Background jobs and workflows' },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'uploadthing',
|
|
62
|
+
name: 'File Uploads',
|
|
63
|
+
items: [
|
|
64
|
+
{ name: 'UploadThing', url: 'https://docs.uploadthing.com/', description: 'File uploads' },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'stripe',
|
|
69
|
+
name: 'Payments',
|
|
70
|
+
items: [
|
|
71
|
+
{ name: 'Stripe', url: 'https://docs.stripe.com/', description: 'Payments and subscriptions' },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'posthog',
|
|
76
|
+
name: 'Analytics',
|
|
77
|
+
items: [
|
|
78
|
+
{ name: 'PostHog', url: 'https://posthog.com/docs', description: 'Product analytics' },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'sentry',
|
|
83
|
+
name: 'Error Tracking',
|
|
84
|
+
items: [
|
|
85
|
+
{ name: 'Sentry', url: 'https://docs.sentry.io/platforms/javascript/guides/nextjs/', description: 'Error monitoring' },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
export async function generateClaudeMd(projectPath, config) {
|
|
90
|
+
const sections = stackSections.filter((section) => section.id === 'core' || config.integrations.includes(section.id));
|
|
91
|
+
const hasDb = config.integrations.includes('neon-drizzle');
|
|
92
|
+
const hasPostHog = config.integrations.includes('posthog');
|
|
93
|
+
const hasSentry = config.integrations.includes('sentry');
|
|
94
|
+
const hasClerk = config.integrations.includes('clerk');
|
|
95
|
+
let content = `# ${config.name}
|
|
96
|
+
|
|
97
|
+
## Tech Stack
|
|
98
|
+
|
|
99
|
+
`;
|
|
100
|
+
for (const section of sections) {
|
|
101
|
+
content += `### ${section.name}\n`;
|
|
102
|
+
for (const item of section.items) {
|
|
103
|
+
content += `- [${item.name}](${item.url}) - ${item.description}\n`;
|
|
104
|
+
}
|
|
105
|
+
content += '\n';
|
|
106
|
+
}
|
|
107
|
+
content += `## Development Commands
|
|
108
|
+
|
|
109
|
+
\`\`\`bash
|
|
110
|
+
npm run dev # Start development server
|
|
111
|
+
npm run build # Build for production
|
|
112
|
+
npm run start # Start production server
|
|
113
|
+
npm run lint # Run ESLint
|
|
114
|
+
\`\`\`
|
|
115
|
+
`;
|
|
116
|
+
if (hasDb) {
|
|
117
|
+
content += `
|
|
118
|
+
### Database Commands
|
|
119
|
+
|
|
120
|
+
\`\`\`bash
|
|
121
|
+
npm run db:generate # Generate migrations from schema changes
|
|
122
|
+
npm run db:migrate # Apply migrations to database
|
|
123
|
+
npm run db:studio # Open Drizzle Studio to browse data
|
|
124
|
+
\`\`\`
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
if (config.integrations.includes('inngest')) {
|
|
128
|
+
content += `
|
|
129
|
+
### Background Jobs
|
|
130
|
+
|
|
131
|
+
\`\`\`bash
|
|
132
|
+
npm run inngest:dev # Start Inngest dev server for local testing
|
|
133
|
+
\`\`\`
|
|
134
|
+
`;
|
|
135
|
+
}
|
|
136
|
+
content += `
|
|
137
|
+
## Architecture
|
|
138
|
+
|
|
139
|
+
### Directory Structure
|
|
140
|
+
|
|
141
|
+
\`\`\`
|
|
142
|
+
├── app/ # Next.js App Router pages and API routes
|
|
143
|
+
├── components/ # React components (including shadcn/ui)
|
|
144
|
+
`;
|
|
145
|
+
if (config.integrations.includes('resend')) {
|
|
146
|
+
content += `│ └── emails/ # React Email templates
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
if (hasPostHog || hasSentry) {
|
|
150
|
+
content += `├── instrumentation-client.ts # Client-side init
|
|
151
|
+
`;
|
|
152
|
+
}
|
|
153
|
+
if (hasSentry) {
|
|
154
|
+
content += `├── instrumentation.ts # Server-side Sentry registration
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
157
|
+
if (hasDb) {
|
|
158
|
+
content += `├── actions/ # Server actions (form submissions)
|
|
159
|
+
│ └── *.actions.ts
|
|
160
|
+
├── services/ # Business logic orchestration
|
|
161
|
+
│ └── *.service.ts
|
|
162
|
+
├── dao/ # Data access objects (database queries)
|
|
163
|
+
│ └── *.dao.ts
|
|
164
|
+
├── mappers/ # Data transformation
|
|
165
|
+
│ └── *.mapper.ts
|
|
166
|
+
├── models/ # Type definitions
|
|
167
|
+
│ ├── *.dto.ts # Database types (InferSelectModel)
|
|
168
|
+
│ ├── *.view.ts # View models for UI
|
|
169
|
+
│ ├── *.schema.ts # Zod validation + ServiceRequest/Result
|
|
170
|
+
│ ├── *.state.ts # Action state objects
|
|
171
|
+
│ └── *ServiceError.enum.ts # Service error enums
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
content += `├── services/ # Business logic services
|
|
176
|
+
│ └── *.service.ts
|
|
177
|
+
`;
|
|
178
|
+
}
|
|
179
|
+
content += `├── lib/
|
|
180
|
+
│ ├── config.ts # Centralized environment variables
|
|
181
|
+
│ ├── stores/ # Zustand stores for client state
|
|
182
|
+
│ │ └── *.store.ts
|
|
183
|
+
`;
|
|
184
|
+
if (hasDb) {
|
|
185
|
+
content += `│ └── db/ # Database client and schema
|
|
186
|
+
`;
|
|
187
|
+
}
|
|
188
|
+
content += `└── public/ # Static assets
|
|
189
|
+
\`\`\`
|
|
190
|
+
`;
|
|
191
|
+
if (hasDb) {
|
|
192
|
+
content += `
|
|
193
|
+
### Layered Architecture
|
|
194
|
+
|
|
195
|
+
The application follows a strict 4-layer architecture:
|
|
196
|
+
|
|
197
|
+
\`\`\`
|
|
198
|
+
UI Components (app/, components/)
|
|
199
|
+
↓
|
|
200
|
+
Server Actions (actions/*.actions.ts)
|
|
201
|
+
↓
|
|
202
|
+
Services (services/*.service.ts)
|
|
203
|
+
↓
|
|
204
|
+
DAOs (dao/*.dao.ts)
|
|
205
|
+
↓
|
|
206
|
+
Database (Drizzle ORM)
|
|
207
|
+
\`\`\`
|
|
208
|
+
|
|
209
|
+
**Layer responsibilities:**
|
|
210
|
+
|
|
211
|
+
- **Actions** - Handle form submissions, validate with Zod, check auth, call services, revalidate cache
|
|
212
|
+
- **Services** - Orchestrate business logic, coordinate multiple DAOs
|
|
213
|
+
- **DAOs** - Encapsulate all database queries using Drizzle ORM
|
|
214
|
+
- **Mappers** - Transform between DTOs, service requests, and view models
|
|
215
|
+
|
|
216
|
+
### Model File Naming Conventions
|
|
217
|
+
|
|
218
|
+
Files in \`models/\` follow strict naming:
|
|
219
|
+
|
|
220
|
+
| Pattern | Purpose | Example |
|
|
221
|
+
|---------|---------|---------|
|
|
222
|
+
| \`*.dto.ts\` | Database types from Drizzle | \`UserDto\`, \`UserInsertDto\` |
|
|
223
|
+
| \`*.view.ts\` | View models for UI | \`UserView\` |
|
|
224
|
+
| \`*.schema.ts\` | Zod schemas + service types | \`UserCreateFormSchema\`, \`UserCreateServiceRequest\` |
|
|
225
|
+
| \`*.state.ts\` | Action state objects | \`UserCreateState\` |
|
|
226
|
+
| \`*ServiceError.enum.ts\` | Service error enums | \`UserServiceError\` |
|
|
227
|
+
|
|
228
|
+
### Action File Organization
|
|
229
|
+
|
|
230
|
+
One action file per domain entity: \`actions/{entity}.action.ts\`. Do NOT split by operation type.
|
|
231
|
+
|
|
232
|
+
\`\`\`
|
|
233
|
+
actions/
|
|
234
|
+
project.action.ts # createProject, updateProject, deleteProject, searchProjects
|
|
235
|
+
task.action.ts # createTask, updateTask, deleteTask, searchTasks
|
|
236
|
+
comment.action.ts # createComment, deleteComment
|
|
237
|
+
settings.action.ts # updateSettings
|
|
238
|
+
\`\`\`
|
|
239
|
+
|
|
240
|
+
Do NOT create separate files like \`project.create.action.ts\` or \`task.search.action.ts\`.
|
|
241
|
+
|
|
242
|
+
### Server Action Pattern
|
|
243
|
+
|
|
244
|
+
\`\`\`typescript
|
|
245
|
+
'use server';
|
|
246
|
+
|
|
247
|
+
export async function createEntity(
|
|
248
|
+
state: EntityCreateState,
|
|
249
|
+
formData: FormData
|
|
250
|
+
): Promise<EntityCreateState> {
|
|
251
|
+
const user = await currentUser();
|
|
252
|
+
if (!user) {
|
|
253
|
+
return { success: false, error: 'Not authenticated', data: null };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const rawData = Object.fromEntries(formData);
|
|
257
|
+
const validated = EntityCreateSchema.safeParse(rawData);
|
|
258
|
+
|
|
259
|
+
if (!validated.success) {
|
|
260
|
+
return { success: false, error: z.prettifyError(validated.error), data: null };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const result = await entityService.createEntity(validated.data);
|
|
265
|
+
revalidatePath('/entities');
|
|
266
|
+
return { success: true, error: null, data: result };
|
|
267
|
+
} catch (error) {
|
|
268
|
+
return { success: false, error: 'Failed to create entity', data: null };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
\`\`\`
|
|
272
|
+
|
|
273
|
+
### DAO Pattern
|
|
274
|
+
|
|
275
|
+
\`\`\`typescript
|
|
276
|
+
export class EntityDAO {
|
|
277
|
+
async create(dto: EntityInsertDto): Promise<EntityDto | undefined> {
|
|
278
|
+
const [created] = await db.insert(entities).values(dto).returning();
|
|
279
|
+
return created;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async getById(id: string): Promise<EntityDto | undefined> {
|
|
283
|
+
return await db.query.entities.findFirst({
|
|
284
|
+
where: eq(entities.id, id),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export const entityDAO = new EntityDAO();
|
|
290
|
+
\`\`\`
|
|
291
|
+
|
|
292
|
+
### Service Error Enums
|
|
293
|
+
|
|
294
|
+
Each service class has a corresponding error enum in \`models/{serviceName}ServiceError.enum.ts\`. Services throw errors using enum values, actions catch and translate to user-friendly messages.
|
|
295
|
+
|
|
296
|
+
\`\`\`typescript
|
|
297
|
+
// models/performanceServiceError.enum.ts
|
|
298
|
+
export enum PerformanceServiceError {
|
|
299
|
+
NotFound = "PERFORMANCE_NOT_FOUND",
|
|
300
|
+
NotOwned = "PERFORMANCE_NOT_OWNED",
|
|
301
|
+
DuplicateTime = "PERFORMANCE_DUPLICATE_TIME",
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// services/performance.service.ts
|
|
305
|
+
import { PerformanceServiceError } from "@/models/performanceServiceError.enum";
|
|
306
|
+
|
|
307
|
+
if (conflict) {
|
|
308
|
+
throw new Error(PerformanceServiceError.DuplicateTime);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// actions/performance.action.ts
|
|
312
|
+
import { PerformanceServiceError } from "@/models/performanceServiceError.enum";
|
|
313
|
+
|
|
314
|
+
catch (error) {
|
|
315
|
+
if (error instanceof Error) {
|
|
316
|
+
switch (error.message) {
|
|
317
|
+
case PerformanceServiceError.DuplicateTime:
|
|
318
|
+
return { success: false, error: 'A performance already exists at this time', data: null };
|
|
319
|
+
case PerformanceServiceError.NotFound:
|
|
320
|
+
return { success: false, error: 'Performance not found', data: null };
|
|
321
|
+
case PerformanceServiceError.NotOwned:
|
|
322
|
+
return { success: false, error: 'You do not have permission to modify this performance', data: null };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return { success: false, error: 'Failed to update performance', data: null };
|
|
326
|
+
}
|
|
327
|
+
\`\`\`
|
|
328
|
+
`;
|
|
329
|
+
}
|
|
330
|
+
content += `
|
|
331
|
+
## Client State Management (Zustand)
|
|
332
|
+
|
|
333
|
+
For complex multi-step forms or flows, use Zustand stores.
|
|
334
|
+
|
|
335
|
+
**Store location**: \`lib/stores/*.store.ts\`
|
|
336
|
+
|
|
337
|
+
### Store Pattern
|
|
338
|
+
|
|
339
|
+
\`\`\`typescript
|
|
340
|
+
import { createStore, useStore } from 'zustand';
|
|
341
|
+
|
|
342
|
+
interface FormState {
|
|
343
|
+
title: string;
|
|
344
|
+
description: string;
|
|
345
|
+
setTitle: (title: string) => void;
|
|
346
|
+
setDescription: (description: string) => void;
|
|
347
|
+
reset: () => void;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const initialState = {
|
|
351
|
+
title: '',
|
|
352
|
+
description: '',
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
export const formStore = createStore<FormState>()((set) => ({
|
|
356
|
+
...initialState,
|
|
357
|
+
setTitle: (title) => set({ title }),
|
|
358
|
+
setDescription: (description) => set({ description }),
|
|
359
|
+
reset: () => set(initialState),
|
|
360
|
+
}));
|
|
361
|
+
|
|
362
|
+
export const useFormStore = <T>(selector: (state: FormState) => T): T => {
|
|
363
|
+
return useStore(formStore, selector);
|
|
364
|
+
};
|
|
365
|
+
\`\`\`
|
|
366
|
+
|
|
367
|
+
### Usage in Components
|
|
368
|
+
|
|
369
|
+
\`\`\`tsx
|
|
370
|
+
'use client';
|
|
371
|
+
|
|
372
|
+
import { Input } from '@/components/ui/input';
|
|
373
|
+
import { Label } from '@/components/ui/label';
|
|
374
|
+
|
|
375
|
+
function TitleStep() {
|
|
376
|
+
const title = useFormStore((state) => state.title);
|
|
377
|
+
const setTitle = useFormStore((state) => state.setTitle);
|
|
378
|
+
|
|
379
|
+
return (
|
|
380
|
+
<div className="space-y-2">
|
|
381
|
+
<Label htmlFor="title">Title</Label>
|
|
382
|
+
<Input
|
|
383
|
+
id="title"
|
|
384
|
+
value={title}
|
|
385
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
386
|
+
placeholder="Enter a title"
|
|
387
|
+
/>
|
|
388
|
+
</div>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
\`\`\`
|
|
392
|
+
|
|
393
|
+
## Code Style
|
|
394
|
+
|
|
395
|
+
### No Comments
|
|
396
|
+
|
|
397
|
+
Do not add comments to the codebase. Code should be self-documenting through:
|
|
398
|
+
- Clear, descriptive variable and function names
|
|
399
|
+
- Proper TypeScript types
|
|
400
|
+
- Logical code structure
|
|
401
|
+
- Small, focused functions
|
|
402
|
+
|
|
403
|
+
### Closure Variable Naming
|
|
404
|
+
|
|
405
|
+
Always use verbose singular names in closures (\`.map()\`, \`.filter()\`, etc.):
|
|
406
|
+
|
|
407
|
+
\`\`\`typescript
|
|
408
|
+
// ✅ Correct
|
|
409
|
+
users.map((user) => user.email)
|
|
410
|
+
items.filter((item) => item.isActive)
|
|
411
|
+
|
|
412
|
+
// ❌ Wrong
|
|
413
|
+
users.map((u) => u.email)
|
|
414
|
+
items.filter((i) => i.isActive)
|
|
415
|
+
\`\`\`
|
|
416
|
+
|
|
417
|
+
## Utility Functions
|
|
418
|
+
|
|
419
|
+
Import from \`@/lib/utils\`:
|
|
420
|
+
|
|
421
|
+
\`\`\`typescript
|
|
422
|
+
import { cn, formatDate, formatRelative, debounce } from '@/lib/utils';
|
|
423
|
+
|
|
424
|
+
// Class name merging (shadcn/ui)
|
|
425
|
+
cn('text-sm', isActive && 'text-blue-500')
|
|
426
|
+
|
|
427
|
+
// Date formatting with Luxon
|
|
428
|
+
formatDate(new Date()) // "Jan 15, 2024"
|
|
429
|
+
formatDate('2024-01-15', 'yyyy-MM-dd') // "2024-01-15"
|
|
430
|
+
formatRelative(new Date()) // "2 hours ago"
|
|
431
|
+
|
|
432
|
+
// Debounce function calls
|
|
433
|
+
const debouncedSearch = debounce((query: string) => {
|
|
434
|
+
// search logic
|
|
435
|
+
}, 300);
|
|
436
|
+
\`\`\`
|
|
437
|
+
|
|
438
|
+
## Environment Variables
|
|
439
|
+
|
|
440
|
+
Copy \`.env.example\` to \`.env.local\` and fill in your API keys.
|
|
441
|
+
|
|
442
|
+
**Important:** Import environment variables from \`@/lib/config\`:
|
|
443
|
+
|
|
444
|
+
\`\`\`typescript
|
|
445
|
+
// ✅ Good
|
|
446
|
+
import { STRIPE_SECRET_KEY } from '@/lib/config';
|
|
447
|
+
|
|
448
|
+
// ❌ Avoid
|
|
449
|
+
const key = process.env.STRIPE_SECRET_KEY;
|
|
450
|
+
\`\`\`
|
|
451
|
+
`;
|
|
452
|
+
if (hasClerk) {
|
|
453
|
+
content += `
|
|
454
|
+
## Authentication
|
|
455
|
+
|
|
456
|
+
**Provider:** Clerk
|
|
457
|
+
|
|
458
|
+
- Route protection via \`proxy.ts\` (Next.js 16+)
|
|
459
|
+
- Use \`currentUser()\` in Server Components and Actions for auth checks
|
|
460
|
+
- Client-side: Use Clerk hooks (\`useUser()\`, \`useAuth()\`, \`<SignedIn>\`, \`<SignedOut>\`)
|
|
461
|
+
|
|
462
|
+
\`\`\`typescript
|
|
463
|
+
// Server-side auth check
|
|
464
|
+
const user = await currentUser();
|
|
465
|
+
if (!user) {
|
|
466
|
+
return redirect('/');
|
|
467
|
+
}
|
|
468
|
+
\`\`\`
|
|
469
|
+
`;
|
|
470
|
+
}
|
|
471
|
+
await fs.writeFile(path.join(projectPath, 'CLAUDE.md'), content);
|
|
472
|
+
}
|
|
473
|
+
export async function appendClaudeMd(projectPath, integrations) {
|
|
474
|
+
const claudeMdPath = path.join(projectPath, 'CLAUDE.md');
|
|
475
|
+
let existing = '';
|
|
476
|
+
try {
|
|
477
|
+
existing = await fs.readFile(claudeMdPath, 'utf-8');
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const newSections = stackSections.filter((section) => integrations.includes(section.id));
|
|
483
|
+
if (newSections.length === 0)
|
|
484
|
+
return;
|
|
485
|
+
let content = '\n## Added Integrations\n\n';
|
|
486
|
+
for (const section of newSections) {
|
|
487
|
+
content += `### ${section.name}\n`;
|
|
488
|
+
for (const item of section.items) {
|
|
489
|
+
content += `- [${item.name}](${item.url}) - ${item.description}\n`;
|
|
490
|
+
}
|
|
491
|
+
content += '\n';
|
|
492
|
+
}
|
|
493
|
+
await fs.writeFile(claudeMdPath, existing.trimEnd() + '\n' + content);
|
|
494
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(): Promise<void>;
|