create-loadout 1.0.1 → 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/README.md +76 -9
- package/dist/cli.js +12 -79
- package/dist/create-next.js +1 -1
- package/dist/engine.d.ts +9 -0
- package/dist/engine.js +84 -0
- package/dist/index.js +156 -1
- package/dist/integrations/index.d.ts +1 -0
- package/dist/integrations/index.js +1 -1
- package/dist/integrations/stripe.js +0 -14
- package/dist/mcp-server.d.ts +2 -0
- package/dist/mcp-server.js +161 -0
- package/dist/metadata.d.ts +18 -0
- package/dist/metadata.js +28 -0
- package/dist/prompts.js +3 -6
- package/dist/templates/firecrawl.js +1 -1
- package/dist/templates/stripe.d.ts +0 -3
- package/dist/templates/stripe.js +0 -130
- package/dist/validate.d.ts +14 -0
- package/dist/validate.js +66 -0
- package/package.json +10 -5
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ your-app/
|
|
|
80
80
|
| 🔥 | **Firecrawl** | Web scraping service |
|
|
81
81
|
| ⏰ | **Inngest** | Background jobs |
|
|
82
82
|
| 📁 | **UploadThing** | File uploads |
|
|
83
|
-
| 💳 | **Stripe** |
|
|
83
|
+
| 💳 | **Stripe** | Payment service with checkout + billing |
|
|
84
84
|
| 📊 | **PostHog** | Product analytics |
|
|
85
85
|
| 🐛 | **Sentry** | Error tracking |
|
|
86
86
|
|
|
@@ -90,27 +90,94 @@ your-app/
|
|
|
90
90
|
|
|
91
91
|
## How It Works
|
|
92
92
|
|
|
93
|
-
###
|
|
93
|
+
### Interactive Mode
|
|
94
94
|
|
|
95
95
|
```bash
|
|
96
96
|
npx create-loadout
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
Answer the prompts — project name, integrations, AI provider — and you're done.
|
|
100
100
|
|
|
101
|
-
-
|
|
102
|
-
- Which integrations you need
|
|
103
|
-
- AI provider (if using AI SDK)
|
|
101
|
+
### Non-Interactive Mode
|
|
104
102
|
|
|
105
|
-
|
|
103
|
+
Skip the prompts entirely with CLI flags:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npx create-loadout my-app --clerk --neon-drizzle --stripe
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
All available flags:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
--clerk Clerk authentication
|
|
113
|
+
--neon-drizzle Neon + Drizzle database
|
|
114
|
+
--ai-sdk Vercel AI SDK
|
|
115
|
+
--ai-provider <p> AI provider (openai, anthropic, google)
|
|
116
|
+
--resend Resend email
|
|
117
|
+
--postmark Postmark email
|
|
118
|
+
--firecrawl Firecrawl web scraping
|
|
119
|
+
--inngest Inngest background jobs
|
|
120
|
+
--uploadthing UploadThing file uploads
|
|
121
|
+
--stripe Stripe payments
|
|
122
|
+
--posthog PostHog analytics
|
|
123
|
+
--sentry Sentry error tracking
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Add integrations to an existing project:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
npx create-loadout --add --posthog --sentry
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Use a config file:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npx create-loadout --config loadout.json
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"name": "my-app",
|
|
141
|
+
"integrations": ["clerk", "neon-drizzle", "stripe"],
|
|
142
|
+
"aiProvider": "anthropic"
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
List all integrations as JSON:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npx create-loadout --list
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Start Building
|
|
106
153
|
|
|
107
154
|
```bash
|
|
108
155
|
cd your-app
|
|
109
|
-
npm install
|
|
110
156
|
npm run dev
|
|
111
157
|
```
|
|
112
158
|
|
|
113
|
-
|
|
159
|
+
Fill in `.env.local` and you're live.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## MCP Server for Claude Code
|
|
164
|
+
|
|
165
|
+
Loadout ships an MCP server so Claude Code agents can scaffold and extend projects programmatically.
|
|
166
|
+
|
|
167
|
+
### Register
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
claude mcp add create-loadout -- npx -y create-loadout-mcp
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Available Tools
|
|
174
|
+
|
|
175
|
+
| Tool | Description |
|
|
176
|
+
|------|-------------|
|
|
177
|
+
| `list_integrations` | List all integrations with metadata, env vars, and constraints |
|
|
178
|
+
| `create_project` | Scaffold a new Next.js project with selected integrations |
|
|
179
|
+
| `add_integrations` | Add integrations to an existing project |
|
|
180
|
+
| `detect_project` | Check if a directory is a Next.js project, list installed/available integrations |
|
|
114
181
|
|
|
115
182
|
---
|
|
116
183
|
|
package/dist/cli.js
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
|
-
import fs from 'fs/promises';
|
|
4
3
|
import path from 'path';
|
|
5
4
|
import { getProjectConfig, getAddIntegrationConfig } from './prompts.js';
|
|
6
|
-
import {
|
|
7
|
-
import { setupShadcn } from './setup-shadcn.js';
|
|
8
|
-
import { installIntegrations } from './integrations/index.js';
|
|
9
|
-
import { generateClaudeMd, appendClaudeMd } from './claude-md.js';
|
|
10
|
-
import { generateEnvFiles, appendEnvFiles } from './env.js';
|
|
11
|
-
import { generateConfig, appendConfig } from './config.js';
|
|
12
|
-
import { generateReadme, generateGitignore } from './generate-readme.js';
|
|
13
|
-
import { generateInstrumentationClient, generateInstrumentation, } from './instrumentation.js';
|
|
14
|
-
import { zustandTemplates } from './templates/zustand.js';
|
|
15
|
-
import { generateLandingPage } from './landing-page.js';
|
|
5
|
+
import { createProject, addIntegrations } from './engine.js';
|
|
16
6
|
import { isExistingProject, getInstalledIntegrations, getAvailableIntegrations, } from './detect.js';
|
|
7
|
+
function oraReporter() {
|
|
8
|
+
const spinner = ora();
|
|
9
|
+
const callback = (step) => {
|
|
10
|
+
spinner.start(step);
|
|
11
|
+
};
|
|
12
|
+
return { spinner, callback };
|
|
13
|
+
}
|
|
17
14
|
export async function main() {
|
|
18
15
|
console.log();
|
|
19
16
|
console.log(chalk.bold.cyan(' create-loadout'));
|
|
@@ -37,36 +34,8 @@ export async function main() {
|
|
|
37
34
|
async function newProjectFlow() {
|
|
38
35
|
const config = await getProjectConfig();
|
|
39
36
|
console.log();
|
|
40
|
-
const spinner
|
|
41
|
-
const
|
|
42
|
-
spinner.succeed('Next.js app created');
|
|
43
|
-
spinner.start('Setting up shadcn/ui...');
|
|
44
|
-
await setupShadcn(projectPath);
|
|
45
|
-
spinner.succeed('shadcn/ui configured');
|
|
46
|
-
await extendUtils(projectPath);
|
|
47
|
-
spinner.start('Installing base packages...');
|
|
48
|
-
const { execa } = await import('execa');
|
|
49
|
-
await execa('npm', ['install', 'zod@^3.24', 'zustand@^5', 'luxon@^3'], { cwd: projectPath });
|
|
50
|
-
await execa('npm', ['install', '-D', '@types/luxon'], { cwd: projectPath });
|
|
51
|
-
spinner.succeed('Base packages installed (zod, zustand, luxon)');
|
|
52
|
-
await fs.mkdir(path.join(projectPath, 'lib/stores'), { recursive: true });
|
|
53
|
-
await fs.writeFile(path.join(projectPath, 'lib/stores/counter.store.ts'), zustandTemplates.exampleStore);
|
|
54
|
-
if (config.integrations.length > 0) {
|
|
55
|
-
spinner.start(`Setting up ${config.integrations.length} integration(s)...`);
|
|
56
|
-
await installIntegrations(projectPath, config);
|
|
57
|
-
spinner.succeed('Integrations configured');
|
|
58
|
-
}
|
|
59
|
-
await generateInstrumentationClient(projectPath, config);
|
|
60
|
-
await generateInstrumentation(projectPath, config);
|
|
61
|
-
spinner.start('Generating config and environment files...');
|
|
62
|
-
await generateConfig(projectPath, config);
|
|
63
|
-
await generateEnvFiles(projectPath, config);
|
|
64
|
-
spinner.succeed('Config and environment files created');
|
|
65
|
-
spinner.start('Generating project files...');
|
|
66
|
-
await generateLandingPage(projectPath, config);
|
|
67
|
-
await generateGitignore(projectPath);
|
|
68
|
-
await generateReadme(projectPath, config);
|
|
69
|
-
await generateClaudeMd(projectPath, config);
|
|
37
|
+
const { spinner, callback } = oraReporter();
|
|
38
|
+
const result = await createProject(config, callback);
|
|
70
39
|
spinner.succeed('Project files created');
|
|
71
40
|
console.log();
|
|
72
41
|
console.log(chalk.green.bold(' Success!') + ' Created ' + chalk.cyan(config.name));
|
|
@@ -117,21 +86,14 @@ async function addIntegrationFlow(projectPath) {
|
|
|
117
86
|
return;
|
|
118
87
|
}
|
|
119
88
|
console.log();
|
|
120
|
-
const spinner = ora(`Installing ${addConfig.integrations.length} integration(s)...`).start();
|
|
121
89
|
const config = {
|
|
122
90
|
name: path.basename(projectPath),
|
|
123
91
|
integrations: addConfig.integrations,
|
|
124
92
|
aiProvider: addConfig.aiProvider,
|
|
125
93
|
};
|
|
126
|
-
|
|
94
|
+
const { spinner, callback } = oraReporter();
|
|
95
|
+
const result = await addIntegrations(projectPath, config, callback);
|
|
127
96
|
spinner.succeed('Integrations installed');
|
|
128
|
-
spinner.start('Updating config and environment files...');
|
|
129
|
-
await appendConfig(projectPath, addConfig.integrations, addConfig.aiProvider);
|
|
130
|
-
await appendEnvFiles(projectPath, addConfig.integrations, addConfig.aiProvider);
|
|
131
|
-
spinner.succeed('Config and environment files updated');
|
|
132
|
-
spinner.start('Updating CLAUDE.md...');
|
|
133
|
-
await appendClaudeMd(projectPath, addConfig.integrations);
|
|
134
|
-
spinner.succeed('CLAUDE.md updated');
|
|
135
97
|
console.log();
|
|
136
98
|
console.log(chalk.green.bold(' Success!') + ' Added integrations:');
|
|
137
99
|
addConfig.integrations.forEach((integration) => {
|
|
@@ -155,32 +117,3 @@ async function addIntegrationFlow(projectPath) {
|
|
|
155
117
|
console.log();
|
|
156
118
|
}
|
|
157
119
|
}
|
|
158
|
-
async function extendUtils(projectPath) {
|
|
159
|
-
const utilsPath = path.join(projectPath, 'lib/utils.ts');
|
|
160
|
-
const existingContent = await fs.readFile(utilsPath, 'utf-8');
|
|
161
|
-
const additionalUtils = `
|
|
162
|
-
import { DateTime } from 'luxon';
|
|
163
|
-
|
|
164
|
-
export function formatDate(date: Date | string, format = 'LLL d, yyyy'): string {
|
|
165
|
-
const dt = typeof date === 'string' ? DateTime.fromISO(date) : DateTime.fromJSDate(date);
|
|
166
|
-
return dt.toFormat(format);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export function formatRelative(date: Date | string): string {
|
|
170
|
-
const dt = typeof date === 'string' ? DateTime.fromISO(date) : DateTime.fromJSDate(date);
|
|
171
|
-
return dt.toRelative() ?? dt.toFormat('LLL d, yyyy');
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export function debounce<P extends unknown[], R>(
|
|
175
|
-
func: (...args: P) => R,
|
|
176
|
-
wait: number
|
|
177
|
-
): (...args: P) => void {
|
|
178
|
-
let timeout: ReturnType<typeof setTimeout>;
|
|
179
|
-
return (...args: P) => {
|
|
180
|
-
clearTimeout(timeout);
|
|
181
|
-
timeout = setTimeout(() => func(...args), wait);
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
`;
|
|
185
|
-
await fs.writeFile(utilsPath, existingContent + additionalUtils);
|
|
186
|
-
}
|
package/dist/create-next.js
CHANGED
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ProjectConfig, IntegrationId } from './types.js';
|
|
2
|
+
export type ProgressCallback = (step: string) => void;
|
|
3
|
+
export interface EngineResult {
|
|
4
|
+
projectPath: string;
|
|
5
|
+
integrations: IntegrationId[];
|
|
6
|
+
envVarsNeeded: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function createProject(config: ProjectConfig, onProgress?: ProgressCallback): Promise<EngineResult>;
|
|
9
|
+
export declare function addIntegrations(projectPath: string, config: ProjectConfig, onProgress?: ProgressCallback): Promise<EngineResult>;
|
package/dist/engine.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createNextApp } from './create-next.js';
|
|
4
|
+
import { setupShadcn } from './setup-shadcn.js';
|
|
5
|
+
import { installIntegrations, getEnvVars } from './integrations/index.js';
|
|
6
|
+
import { generateClaudeMd, appendClaudeMd } from './claude-md.js';
|
|
7
|
+
import { generateEnvFiles, appendEnvFiles } from './env.js';
|
|
8
|
+
import { generateConfig, appendConfig } from './config.js';
|
|
9
|
+
import { generateReadme, generateGitignore } from './generate-readme.js';
|
|
10
|
+
import { generateInstrumentationClient, generateInstrumentation, } from './instrumentation.js';
|
|
11
|
+
import { zustandTemplates } from './templates/zustand.js';
|
|
12
|
+
import { generateLandingPage } from './landing-page.js';
|
|
13
|
+
export async function createProject(config, onProgress) {
|
|
14
|
+
onProgress?.('Creating Next.js app...');
|
|
15
|
+
const projectPath = await createNextApp(config.name);
|
|
16
|
+
onProgress?.('Setting up shadcn/ui...');
|
|
17
|
+
await setupShadcn(projectPath);
|
|
18
|
+
await extendUtils(projectPath);
|
|
19
|
+
onProgress?.('Installing base packages...');
|
|
20
|
+
const { execa } = await import('execa');
|
|
21
|
+
await execa('npm', ['install', 'zod@^3.24', 'zustand@^5', 'luxon@^3'], { cwd: projectPath });
|
|
22
|
+
await execa('npm', ['install', '-D', '@types/luxon'], { cwd: projectPath });
|
|
23
|
+
await fs.mkdir(path.join(projectPath, 'lib/stores'), { recursive: true });
|
|
24
|
+
await fs.writeFile(path.join(projectPath, 'lib/stores/counter.store.ts'), zustandTemplates.exampleStore);
|
|
25
|
+
if (config.integrations.length > 0) {
|
|
26
|
+
onProgress?.(`Setting up ${config.integrations.length} integration(s)...`);
|
|
27
|
+
await installIntegrations(projectPath, config);
|
|
28
|
+
}
|
|
29
|
+
await generateInstrumentationClient(projectPath, config);
|
|
30
|
+
await generateInstrumentation(projectPath, config);
|
|
31
|
+
onProgress?.('Generating config and environment files...');
|
|
32
|
+
await generateConfig(projectPath, config);
|
|
33
|
+
await generateEnvFiles(projectPath, config);
|
|
34
|
+
onProgress?.('Generating project files...');
|
|
35
|
+
await generateLandingPage(projectPath, config);
|
|
36
|
+
await generateGitignore(projectPath);
|
|
37
|
+
await generateReadme(projectPath, config);
|
|
38
|
+
await generateClaudeMd(projectPath, config);
|
|
39
|
+
const envVarsNeeded = collectEnvVars(config);
|
|
40
|
+
return { projectPath, integrations: config.integrations, envVarsNeeded };
|
|
41
|
+
}
|
|
42
|
+
export async function addIntegrations(projectPath, config, onProgress) {
|
|
43
|
+
onProgress?.(`Installing ${config.integrations.length} integration(s)...`);
|
|
44
|
+
await installIntegrations(projectPath, config);
|
|
45
|
+
onProgress?.('Updating config and environment files...');
|
|
46
|
+
await appendConfig(projectPath, config.integrations, config.aiProvider);
|
|
47
|
+
await appendEnvFiles(projectPath, config.integrations, config.aiProvider);
|
|
48
|
+
onProgress?.('Updating CLAUDE.md...');
|
|
49
|
+
await appendClaudeMd(projectPath, config.integrations);
|
|
50
|
+
const envVarsNeeded = collectEnvVars(config);
|
|
51
|
+
return { projectPath, integrations: config.integrations, envVarsNeeded };
|
|
52
|
+
}
|
|
53
|
+
async function extendUtils(projectPath) {
|
|
54
|
+
const utilsPath = path.join(projectPath, 'lib/utils.ts');
|
|
55
|
+
const existingContent = await fs.readFile(utilsPath, 'utf-8');
|
|
56
|
+
const additionalUtils = `
|
|
57
|
+
import { DateTime } from 'luxon';
|
|
58
|
+
|
|
59
|
+
export function formatDate(date: Date | string, format = 'LLL d, yyyy'): string {
|
|
60
|
+
const dt = typeof date === 'string' ? DateTime.fromISO(date) : DateTime.fromJSDate(date);
|
|
61
|
+
return dt.toFormat(format);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function formatRelative(date: Date | string): string {
|
|
65
|
+
const dt = typeof date === 'string' ? DateTime.fromISO(date) : DateTime.fromJSDate(date);
|
|
66
|
+
return dt.toRelative() ?? dt.toFormat('LLL d, yyyy');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function debounce<P extends unknown[], R>(
|
|
70
|
+
func: (...args: P) => R,
|
|
71
|
+
wait: number
|
|
72
|
+
): (...args: P) => void {
|
|
73
|
+
let timeout: ReturnType<typeof setTimeout>;
|
|
74
|
+
return (...args: P) => {
|
|
75
|
+
clearTimeout(timeout);
|
|
76
|
+
timeout = setTimeout(() => func(...args), wait);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
await fs.writeFile(utilsPath, existingContent + additionalUtils);
|
|
81
|
+
}
|
|
82
|
+
function collectEnvVars(config) {
|
|
83
|
+
return getEnvVars(config).map((v) => v.key);
|
|
84
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,158 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
2
3
|
import { main } from './cli.js';
|
|
3
|
-
|
|
4
|
+
import { createProject, addIntegrations } from './engine.js';
|
|
5
|
+
import { validateProjectConfig, validateIntegrationSelection, ALL_INTEGRATION_IDS } from './validate.js';
|
|
6
|
+
import { listIntegrations } from './metadata.js';
|
|
7
|
+
import { isExistingProject, getInstalledIntegrations } from './detect.js';
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
const program = new Command();
|
|
11
|
+
program
|
|
12
|
+
.name('create-loadout')
|
|
13
|
+
.description('Custom Next.js scaffolding with SaaS integrations')
|
|
14
|
+
.version('1.0.1')
|
|
15
|
+
.argument('[name]', 'Project name')
|
|
16
|
+
.option('--clerk', 'Add Clerk authentication')
|
|
17
|
+
.option('--neon-drizzle', 'Add Neon + Drizzle database')
|
|
18
|
+
.option('--ai-sdk', 'Add Vercel AI SDK')
|
|
19
|
+
.option('--ai-provider <provider>', 'AI provider (openai, anthropic, google)')
|
|
20
|
+
.option('--resend', 'Add Resend email')
|
|
21
|
+
.option('--postmark', 'Add Postmark email')
|
|
22
|
+
.option('--firecrawl', 'Add Firecrawl web scraping')
|
|
23
|
+
.option('--inngest', 'Add Inngest background jobs')
|
|
24
|
+
.option('--uploadthing', 'Add UploadThing file uploads')
|
|
25
|
+
.option('--stripe', 'Add Stripe payments')
|
|
26
|
+
.option('--posthog', 'Add PostHog analytics')
|
|
27
|
+
.option('--sentry', 'Add Sentry error tracking')
|
|
28
|
+
.option('--config <path>', 'Path to loadout.json config file')
|
|
29
|
+
.option('--add', 'Add integrations to existing project')
|
|
30
|
+
.option('--list', 'List all available integrations as JSON')
|
|
31
|
+
.action(async (name, opts) => {
|
|
32
|
+
// --list: dump integration metadata and exit
|
|
33
|
+
if (opts.list) {
|
|
34
|
+
console.log(JSON.stringify(listIntegrations(), null, 2));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// Detect if running non-interactive
|
|
38
|
+
const hasConfig = typeof opts.config === 'string';
|
|
39
|
+
const hasFlags = name || hasConfig || opts.add || integrationFlagsPresent(opts);
|
|
40
|
+
if (!hasFlags) {
|
|
41
|
+
// No flags → interactive mode (existing behavior)
|
|
42
|
+
await main();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Non-interactive mode
|
|
46
|
+
try {
|
|
47
|
+
if (hasConfig) {
|
|
48
|
+
await runFromConfigFile(opts.config);
|
|
49
|
+
}
|
|
50
|
+
else if (opts.add) {
|
|
51
|
+
await runAddMode(opts);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
await runCreateMode(name, opts);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
function integrationFlagsPresent(opts) {
|
|
63
|
+
return ALL_INTEGRATION_IDS.some((id) => opts[camelCase(id)] === true);
|
|
64
|
+
}
|
|
65
|
+
function camelCase(s) {
|
|
66
|
+
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
67
|
+
}
|
|
68
|
+
function collectIntegrations(opts) {
|
|
69
|
+
const integrations = [];
|
|
70
|
+
for (const id of ALL_INTEGRATION_IDS) {
|
|
71
|
+
if (opts[camelCase(id)] === true) {
|
|
72
|
+
integrations.push(id);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return integrations;
|
|
76
|
+
}
|
|
77
|
+
async function runCreateMode(name, opts) {
|
|
78
|
+
const integrations = collectIntegrations(opts);
|
|
79
|
+
const aiProvider = opts.aiProvider || undefined;
|
|
80
|
+
const config = { name, integrations, aiProvider };
|
|
81
|
+
const errors = validateProjectConfig(config);
|
|
82
|
+
if (errors.length > 0) {
|
|
83
|
+
console.error('Validation errors:');
|
|
84
|
+
errors.forEach((e) => console.error(` - ${e.field}: ${e.message}`));
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
const result = await createProject(config, (step) => console.log(step));
|
|
88
|
+
console.log();
|
|
89
|
+
console.log(`Success! Created ${config.name} at ${result.projectPath}`);
|
|
90
|
+
if (result.integrations.length > 0) {
|
|
91
|
+
console.log(`Integrations: ${result.integrations.join(', ')}`);
|
|
92
|
+
}
|
|
93
|
+
if (result.envVarsNeeded.length > 0) {
|
|
94
|
+
console.log(`Environment variables to configure: ${result.envVarsNeeded.join(', ')}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function runAddMode(opts) {
|
|
98
|
+
const cwd = process.cwd();
|
|
99
|
+
if (!(await isExistingProject(cwd))) {
|
|
100
|
+
console.error('Error: Not in a Next.js project directory. --add requires an existing project.');
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
const integrations = collectIntegrations(opts);
|
|
104
|
+
if (integrations.length === 0) {
|
|
105
|
+
console.error('Error: --add requires at least one integration flag (e.g. --clerk --stripe)');
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
const valErrors = validateIntegrationSelection(integrations);
|
|
109
|
+
if (valErrors.length > 0) {
|
|
110
|
+
console.error('Validation errors:');
|
|
111
|
+
valErrors.forEach((e) => console.error(` - ${e.field}: ${e.message}`));
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
const installed = await getInstalledIntegrations(cwd);
|
|
115
|
+
const alreadyInstalled = integrations.filter((id) => installed.includes(id));
|
|
116
|
+
if (alreadyInstalled.length > 0) {
|
|
117
|
+
console.error(`Error: Already installed: ${alreadyInstalled.join(', ')}`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
const aiProvider = opts.aiProvider || undefined;
|
|
121
|
+
const config = {
|
|
122
|
+
name: path.basename(cwd),
|
|
123
|
+
integrations,
|
|
124
|
+
aiProvider,
|
|
125
|
+
};
|
|
126
|
+
const result = await addIntegrations(cwd, config, (step) => console.log(step));
|
|
127
|
+
console.log();
|
|
128
|
+
console.log(`Success! Added integrations: ${result.integrations.join(', ')}`);
|
|
129
|
+
if (result.envVarsNeeded.length > 0) {
|
|
130
|
+
console.log(`Environment variables to configure: ${result.envVarsNeeded.join(', ')}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function runFromConfigFile(configPath) {
|
|
134
|
+
const resolved = path.resolve(configPath);
|
|
135
|
+
const raw = await fs.readFile(resolved, 'utf-8');
|
|
136
|
+
const parsed = JSON.parse(raw);
|
|
137
|
+
const config = {
|
|
138
|
+
name: parsed.name,
|
|
139
|
+
integrations: parsed.integrations || [],
|
|
140
|
+
aiProvider: parsed.aiProvider,
|
|
141
|
+
};
|
|
142
|
+
const errors = validateProjectConfig(config);
|
|
143
|
+
if (errors.length > 0) {
|
|
144
|
+
console.error('Config validation errors:');
|
|
145
|
+
errors.forEach((e) => console.error(` - ${e.field}: ${e.message}`));
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
const result = await createProject(config, (step) => console.log(step));
|
|
149
|
+
console.log();
|
|
150
|
+
console.log(`Success! Created ${config.name} at ${result.projectPath}`);
|
|
151
|
+
if (result.integrations.length > 0) {
|
|
152
|
+
console.log(`Integrations: ${result.integrations.join(', ')}`);
|
|
153
|
+
}
|
|
154
|
+
if (result.envVarsNeeded.length > 0) {
|
|
155
|
+
console.log(`Environment variables to configure: ${result.envVarsNeeded.join(', ')}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
program.parse();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Integration, IntegrationId, EnvVar, ProjectConfig } from '../types.js';
|
|
2
|
+
export declare function getIntegration(id: IntegrationId, config: ProjectConfig): Integration;
|
|
2
3
|
export declare function installIntegrations(projectPath: string, config: ProjectConfig): Promise<void>;
|
|
3
4
|
export declare function getEnvVars(config: ProjectConfig): EnvVar[];
|
|
4
5
|
export declare const integrations: Partial<Record<IntegrationId, Integration>>;
|
|
@@ -24,7 +24,7 @@ const staticIntegrations = {
|
|
|
24
24
|
sentry: sentryIntegration,
|
|
25
25
|
};
|
|
26
26
|
// Get integration, with dynamic ones using config
|
|
27
|
-
function getIntegration(id, config) {
|
|
27
|
+
export function getIntegration(id, config) {
|
|
28
28
|
if (id === 'ai-sdk') {
|
|
29
29
|
return createAiSdkIntegration(config.aiProvider ?? 'openai');
|
|
30
30
|
}
|
|
@@ -25,21 +25,7 @@ export const stripeIntegration = {
|
|
|
25
25
|
},
|
|
26
26
|
],
|
|
27
27
|
setup: async (projectPath) => {
|
|
28
|
-
// Create payment service
|
|
29
28
|
await fs.mkdir(path.join(projectPath, 'services'), { recursive: true });
|
|
30
29
|
await fs.writeFile(path.join(projectPath, 'services/payment.service.ts'), stripeTemplates.paymentService);
|
|
31
|
-
// Create API routes
|
|
32
|
-
await fs.mkdir(path.join(projectPath, 'app/api/stripe/checkout'), {
|
|
33
|
-
recursive: true,
|
|
34
|
-
});
|
|
35
|
-
await fs.mkdir(path.join(projectPath, 'app/api/stripe/webhooks'), {
|
|
36
|
-
recursive: true,
|
|
37
|
-
});
|
|
38
|
-
await fs.mkdir(path.join(projectPath, 'app/api/stripe/portal'), {
|
|
39
|
-
recursive: true,
|
|
40
|
-
});
|
|
41
|
-
await fs.writeFile(path.join(projectPath, 'app/api/stripe/checkout/route.ts'), stripeTemplates.checkoutRoute);
|
|
42
|
-
await fs.writeFile(path.join(projectPath, 'app/api/stripe/webhooks/route.ts'), stripeTemplates.webhooksRoute);
|
|
43
|
-
await fs.writeFile(path.join(projectPath, 'app/api/stripe/portal/route.ts'), stripeTemplates.portalRoute);
|
|
44
30
|
},
|
|
45
31
|
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { createProject, addIntegrations } from './engine.js';
|
|
7
|
+
import { validateProjectConfig, validateIntegrationSelection, } from './validate.js';
|
|
8
|
+
import { listIntegrations } from './metadata.js';
|
|
9
|
+
import { isExistingProject, getInstalledIntegrations, getAvailableIntegrations, } from './detect.js';
|
|
10
|
+
const server = new McpServer({
|
|
11
|
+
name: 'create-loadout',
|
|
12
|
+
version: '1.0.1',
|
|
13
|
+
});
|
|
14
|
+
// Tool: list_integrations
|
|
15
|
+
server.tool('list_integrations', 'List all available integrations with metadata, env vars, and constraints', {}, async () => {
|
|
16
|
+
const integrations = listIntegrations();
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: 'text', text: JSON.stringify(integrations, null, 2) }],
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
// Tool: create_project
|
|
22
|
+
server.tool('create_project', 'Scaffold a new Next.js project with selected integrations', {
|
|
23
|
+
name: z.string().describe('Project name (lowercase, numbers, hyphens only)'),
|
|
24
|
+
integrations: z.array(z.string()).default([]).describe('Integration IDs to install'),
|
|
25
|
+
aiProvider: z.enum(['openai', 'anthropic', 'google']).optional().describe('AI provider (required if ai-sdk selected)'),
|
|
26
|
+
}, async ({ name, integrations, aiProvider }) => {
|
|
27
|
+
const config = {
|
|
28
|
+
name,
|
|
29
|
+
integrations: integrations,
|
|
30
|
+
aiProvider: aiProvider,
|
|
31
|
+
};
|
|
32
|
+
const errors = validateProjectConfig(config);
|
|
33
|
+
if (errors.length > 0) {
|
|
34
|
+
return {
|
|
35
|
+
content: [{
|
|
36
|
+
type: 'text',
|
|
37
|
+
text: JSON.stringify({ error: 'Validation failed', details: errors }),
|
|
38
|
+
}],
|
|
39
|
+
isError: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const log = [];
|
|
43
|
+
const result = await createProject(config, (step) => log.push(step));
|
|
44
|
+
return {
|
|
45
|
+
content: [{
|
|
46
|
+
type: 'text',
|
|
47
|
+
text: JSON.stringify({
|
|
48
|
+
success: true,
|
|
49
|
+
projectPath: result.projectPath,
|
|
50
|
+
integrations: result.integrations,
|
|
51
|
+
envVarsNeeded: result.envVarsNeeded,
|
|
52
|
+
log,
|
|
53
|
+
}, null, 2),
|
|
54
|
+
}],
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
// Tool: add_integrations
|
|
58
|
+
server.tool('add_integrations', 'Add integrations to an existing Next.js project', {
|
|
59
|
+
projectPath: z.string().describe('Absolute path to the existing Next.js project'),
|
|
60
|
+
integrations: z.array(z.string()).describe('Integration IDs to add'),
|
|
61
|
+
aiProvider: z.enum(['openai', 'anthropic', 'google']).optional().describe('AI provider (required if ai-sdk selected)'),
|
|
62
|
+
}, async ({ projectPath, integrations, aiProvider }) => {
|
|
63
|
+
const resolved = path.resolve(projectPath);
|
|
64
|
+
if (!(await isExistingProject(resolved))) {
|
|
65
|
+
return {
|
|
66
|
+
content: [{
|
|
67
|
+
type: 'text',
|
|
68
|
+
text: JSON.stringify({ error: 'Not a Next.js project', path: resolved }),
|
|
69
|
+
}],
|
|
70
|
+
isError: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const valErrors = validateIntegrationSelection(integrations);
|
|
74
|
+
if (valErrors.length > 0) {
|
|
75
|
+
return {
|
|
76
|
+
content: [{
|
|
77
|
+
type: 'text',
|
|
78
|
+
text: JSON.stringify({ error: 'Validation failed', details: valErrors }),
|
|
79
|
+
}],
|
|
80
|
+
isError: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const installed = await getInstalledIntegrations(resolved);
|
|
84
|
+
const alreadyInstalled = integrations.filter((id) => installed.includes(id));
|
|
85
|
+
if (alreadyInstalled.length > 0) {
|
|
86
|
+
return {
|
|
87
|
+
content: [{
|
|
88
|
+
type: 'text',
|
|
89
|
+
text: JSON.stringify({ error: 'Already installed', integrations: alreadyInstalled }),
|
|
90
|
+
}],
|
|
91
|
+
isError: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (integrations.includes('ai-sdk') && !aiProvider) {
|
|
95
|
+
return {
|
|
96
|
+
content: [{
|
|
97
|
+
type: 'text',
|
|
98
|
+
text: JSON.stringify({ error: 'aiProvider required when ai-sdk is selected' }),
|
|
99
|
+
}],
|
|
100
|
+
isError: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const config = {
|
|
104
|
+
name: path.basename(resolved),
|
|
105
|
+
integrations: integrations,
|
|
106
|
+
aiProvider: aiProvider,
|
|
107
|
+
};
|
|
108
|
+
const log = [];
|
|
109
|
+
const result = await addIntegrations(resolved, config, (step) => log.push(step));
|
|
110
|
+
return {
|
|
111
|
+
content: [{
|
|
112
|
+
type: 'text',
|
|
113
|
+
text: JSON.stringify({
|
|
114
|
+
success: true,
|
|
115
|
+
projectPath: result.projectPath,
|
|
116
|
+
integrations: result.integrations,
|
|
117
|
+
envVarsNeeded: result.envVarsNeeded,
|
|
118
|
+
log,
|
|
119
|
+
}, null, 2),
|
|
120
|
+
}],
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
// Tool: detect_project
|
|
124
|
+
server.tool('detect_project', 'Check if a directory is a Next.js project and list installed/available integrations', {
|
|
125
|
+
projectPath: z.string().optional().describe('Path to check (defaults to cwd)'),
|
|
126
|
+
}, async ({ projectPath }) => {
|
|
127
|
+
const resolved = path.resolve(projectPath || process.cwd());
|
|
128
|
+
const isProject = await isExistingProject(resolved);
|
|
129
|
+
if (!isProject) {
|
|
130
|
+
return {
|
|
131
|
+
content: [{
|
|
132
|
+
type: 'text',
|
|
133
|
+
text: JSON.stringify({
|
|
134
|
+
isNextJsProject: false,
|
|
135
|
+
path: resolved,
|
|
136
|
+
}, null, 2),
|
|
137
|
+
}],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const installed = await getInstalledIntegrations(resolved);
|
|
141
|
+
const available = getAvailableIntegrations(installed);
|
|
142
|
+
return {
|
|
143
|
+
content: [{
|
|
144
|
+
type: 'text',
|
|
145
|
+
text: JSON.stringify({
|
|
146
|
+
isNextJsProject: true,
|
|
147
|
+
path: resolved,
|
|
148
|
+
installedIntegrations: installed,
|
|
149
|
+
availableIntegrations: available,
|
|
150
|
+
}, null, 2),
|
|
151
|
+
}],
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
async function run() {
|
|
155
|
+
const transport = new StdioServerTransport();
|
|
156
|
+
await server.connect(transport);
|
|
157
|
+
}
|
|
158
|
+
run().catch((error) => {
|
|
159
|
+
console.error('MCP server error:', error);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IntegrationId } from './types.js';
|
|
2
|
+
export interface IntegrationInfo {
|
|
3
|
+
id: IntegrationId;
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
packages: string[];
|
|
7
|
+
envVars: {
|
|
8
|
+
key: string;
|
|
9
|
+
description: string;
|
|
10
|
+
example: string;
|
|
11
|
+
}[];
|
|
12
|
+
mutuallyExclusiveWith?: IntegrationId[];
|
|
13
|
+
requiresOption?: {
|
|
14
|
+
field: string;
|
|
15
|
+
values: string[];
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export declare function listIntegrations(): IntegrationInfo[];
|
package/dist/metadata.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getIntegration } from './integrations/index.js';
|
|
2
|
+
import { ALL_INTEGRATION_IDS, AI_PROVIDERS } from './validate.js';
|
|
3
|
+
const EMAIL_PROVIDERS = ['resend', 'postmark'];
|
|
4
|
+
export function listIntegrations() {
|
|
5
|
+
return ALL_INTEGRATION_IDS.map((id) => {
|
|
6
|
+
// Use a dummy config to get integration metadata
|
|
7
|
+
const config = { name: 'dummy', integrations: [id], aiProvider: 'openai' };
|
|
8
|
+
const integration = getIntegration(id, config);
|
|
9
|
+
const info = {
|
|
10
|
+
id: integration.id,
|
|
11
|
+
name: integration.name,
|
|
12
|
+
description: integration.description,
|
|
13
|
+
packages: integration.packages,
|
|
14
|
+
envVars: integration.envVars.map((v) => ({
|
|
15
|
+
key: v.key,
|
|
16
|
+
description: v.description,
|
|
17
|
+
example: v.example,
|
|
18
|
+
})),
|
|
19
|
+
};
|
|
20
|
+
if (EMAIL_PROVIDERS.includes(id)) {
|
|
21
|
+
info.mutuallyExclusiveWith = EMAIL_PROVIDERS.filter((e) => e !== id);
|
|
22
|
+
}
|
|
23
|
+
if (id === 'ai-sdk') {
|
|
24
|
+
info.requiresOption = { field: 'aiProvider', values: [...AI_PROVIDERS] };
|
|
25
|
+
}
|
|
26
|
+
return info;
|
|
27
|
+
});
|
|
28
|
+
}
|
package/dist/prompts.js
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import { input, confirm, select } from '@inquirer/prompts';
|
|
2
|
+
import { validateProjectName } from './validate.js';
|
|
2
3
|
export async function getProjectConfig() {
|
|
3
4
|
const name = await input({
|
|
4
5
|
message: 'Project name:',
|
|
5
6
|
default: 'my-app',
|
|
6
7
|
validate: (value) => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (!/^[a-z0-9-]+$/.test(value)) {
|
|
10
|
-
return 'Project name can only contain lowercase letters, numbers, and hyphens';
|
|
11
|
-
}
|
|
12
|
-
return true;
|
|
8
|
+
const errors = validateProjectName(value);
|
|
9
|
+
return errors.length > 0 ? errors[0].message : true;
|
|
13
10
|
},
|
|
14
11
|
});
|
|
15
12
|
const integrations = [];
|
package/dist/templates/stripe.js
CHANGED
|
@@ -81,135 +81,5 @@ export class PaymentService {
|
|
|
81
81
|
export const paymentService = new PaymentService(STRIPE_SECRET_KEY);
|
|
82
82
|
|
|
83
83
|
export type { Stripe };
|
|
84
|
-
`,
|
|
85
|
-
checkoutRoute: `import { NextResponse } from 'next/server';
|
|
86
|
-
import { paymentService } from '@/services/payment.service';
|
|
87
|
-
import { z } from 'zod';
|
|
88
|
-
|
|
89
|
-
const checkoutSchema = z.object({
|
|
90
|
-
priceId: z.string(),
|
|
91
|
-
customerId: z.string().optional(),
|
|
92
|
-
successUrl: z.url(),
|
|
93
|
-
cancelUrl: z.url(),
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
export async function POST(req: Request) {
|
|
97
|
-
try {
|
|
98
|
-
const body = await req.json();
|
|
99
|
-
const { priceId, customerId, successUrl, cancelUrl } = checkoutSchema.parse(body);
|
|
100
|
-
|
|
101
|
-
const session = await paymentService.createCheckoutSession({
|
|
102
|
-
priceId,
|
|
103
|
-
customerId,
|
|
104
|
-
successUrl,
|
|
105
|
-
cancelUrl,
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
return NextResponse.json({ url: session.url });
|
|
109
|
-
} catch (error) {
|
|
110
|
-
if (error instanceof z.ZodError) {
|
|
111
|
-
return NextResponse.json(
|
|
112
|
-
{ error: 'Invalid request', details: error.errors },
|
|
113
|
-
{ status: 400 }
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
return NextResponse.json(
|
|
117
|
-
{ error: error instanceof Error ? error.message : 'Failed to create checkout' },
|
|
118
|
-
{ status: 500 }
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
`,
|
|
123
|
-
webhooksRoute: `import { headers } from 'next/headers';
|
|
124
|
-
import { NextResponse } from 'next/server';
|
|
125
|
-
import { paymentService, type Stripe } from '@/services/payment.service';
|
|
126
|
-
import { STRIPE_WEBHOOK_SECRET } from '@/lib/config';
|
|
127
|
-
|
|
128
|
-
export async function POST(req: Request) {
|
|
129
|
-
const body = await req.text();
|
|
130
|
-
const headersList = await headers();
|
|
131
|
-
const signature = headersList.get('stripe-signature')!;
|
|
132
|
-
|
|
133
|
-
let event: Stripe.Event;
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
event = paymentService.constructWebhookEvent(body, signature, STRIPE_WEBHOOK_SECRET);
|
|
137
|
-
} catch (error) {
|
|
138
|
-
console.error('Webhook signature verification failed:', error);
|
|
139
|
-
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
switch (event.type) {
|
|
143
|
-
case 'checkout.session.completed': {
|
|
144
|
-
const session = event.data.object as Stripe.Checkout.Session;
|
|
145
|
-
console.log('Checkout completed:', session.id);
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
case 'customer.subscription.created':
|
|
150
|
-
case 'customer.subscription.updated': {
|
|
151
|
-
const subscription = event.data.object as Stripe.Subscription;
|
|
152
|
-
console.log('Subscription updated:', subscription.id, subscription.status);
|
|
153
|
-
break;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
case 'customer.subscription.deleted': {
|
|
157
|
-
const subscription = event.data.object as Stripe.Subscription;
|
|
158
|
-
console.log('Subscription cancelled:', subscription.id);
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
case 'invoice.payment_succeeded': {
|
|
163
|
-
const invoice = event.data.object as Stripe.Invoice;
|
|
164
|
-
console.log('Payment succeeded:', invoice.id);
|
|
165
|
-
break;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
case 'invoice.payment_failed': {
|
|
169
|
-
const invoice = event.data.object as Stripe.Invoice;
|
|
170
|
-
console.log('Payment failed:', invoice.id);
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
default:
|
|
175
|
-
console.log('Unhandled event type:', event.type);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return NextResponse.json({ received: true });
|
|
179
|
-
}
|
|
180
|
-
`,
|
|
181
|
-
portalRoute: `import { NextResponse } from 'next/server';
|
|
182
|
-
import { paymentService } from '@/services/payment.service';
|
|
183
|
-
import { z } from 'zod';
|
|
184
|
-
|
|
185
|
-
const portalSchema = z.object({
|
|
186
|
-
customerId: z.string(),
|
|
187
|
-
returnUrl: z.url(),
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
export async function POST(req: Request) {
|
|
191
|
-
try {
|
|
192
|
-
const body = await req.json();
|
|
193
|
-
const { customerId, returnUrl } = portalSchema.parse(body);
|
|
194
|
-
|
|
195
|
-
const session = await paymentService.createPortalSession({
|
|
196
|
-
customerId,
|
|
197
|
-
returnUrl,
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
return NextResponse.json({ url: session.url });
|
|
201
|
-
} catch (error) {
|
|
202
|
-
if (error instanceof z.ZodError) {
|
|
203
|
-
return NextResponse.json(
|
|
204
|
-
{ error: 'Invalid request', details: error.errors },
|
|
205
|
-
{ status: 400 }
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
return NextResponse.json(
|
|
209
|
-
{ error: error instanceof Error ? error.message : 'Failed to create portal session' },
|
|
210
|
-
{ status: 500 }
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
84
|
`,
|
|
215
85
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IntegrationId, AIProviderChoice } from './types.js';
|
|
2
|
+
export declare const ALL_INTEGRATION_IDS: IntegrationId[];
|
|
3
|
+
export declare const AI_PROVIDERS: AIProviderChoice[];
|
|
4
|
+
export interface ValidationError {
|
|
5
|
+
field: string;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function validateProjectName(name: string): ValidationError[];
|
|
9
|
+
export declare function validateIntegrationSelection(ids: string[]): ValidationError[];
|
|
10
|
+
export declare function validateProjectConfig(config: {
|
|
11
|
+
name: string;
|
|
12
|
+
integrations: string[];
|
|
13
|
+
aiProvider?: string;
|
|
14
|
+
}): ValidationError[];
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const ALL_INTEGRATION_IDS = [
|
|
2
|
+
'clerk',
|
|
3
|
+
'neon-drizzle',
|
|
4
|
+
'ai-sdk',
|
|
5
|
+
'resend',
|
|
6
|
+
'postmark',
|
|
7
|
+
'firecrawl',
|
|
8
|
+
'inngest',
|
|
9
|
+
'uploadthing',
|
|
10
|
+
'stripe',
|
|
11
|
+
'posthog',
|
|
12
|
+
'sentry',
|
|
13
|
+
];
|
|
14
|
+
export const AI_PROVIDERS = ['openai', 'anthropic', 'google'];
|
|
15
|
+
const EMAIL_PROVIDERS = ['resend', 'postmark'];
|
|
16
|
+
const PROJECT_NAME_REGEX = /^[a-z0-9-]+$/;
|
|
17
|
+
export function validateProjectName(name) {
|
|
18
|
+
const errors = [];
|
|
19
|
+
if (!name.trim()) {
|
|
20
|
+
errors.push({ field: 'name', message: 'Project name is required' });
|
|
21
|
+
}
|
|
22
|
+
else if (!PROJECT_NAME_REGEX.test(name)) {
|
|
23
|
+
errors.push({
|
|
24
|
+
field: 'name',
|
|
25
|
+
message: 'Project name can only contain lowercase letters, numbers, and hyphens',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return errors;
|
|
29
|
+
}
|
|
30
|
+
export function validateIntegrationSelection(ids) {
|
|
31
|
+
const errors = [];
|
|
32
|
+
for (const id of ids) {
|
|
33
|
+
if (!ALL_INTEGRATION_IDS.includes(id)) {
|
|
34
|
+
errors.push({
|
|
35
|
+
field: 'integrations',
|
|
36
|
+
message: `Unknown integration: "${id}". Valid: ${ALL_INTEGRATION_IDS.join(', ')}`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const selectedEmail = ids.filter((id) => EMAIL_PROVIDERS.includes(id));
|
|
41
|
+
if (selectedEmail.length > 1) {
|
|
42
|
+
errors.push({
|
|
43
|
+
field: 'integrations',
|
|
44
|
+
message: 'Only one email provider allowed (resend or postmark)',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return errors;
|
|
48
|
+
}
|
|
49
|
+
export function validateProjectConfig(config) {
|
|
50
|
+
const errors = [];
|
|
51
|
+
errors.push(...validateProjectName(config.name));
|
|
52
|
+
errors.push(...validateIntegrationSelection(config.integrations));
|
|
53
|
+
if (config.integrations.includes('ai-sdk') && !config.aiProvider) {
|
|
54
|
+
errors.push({
|
|
55
|
+
field: 'aiProvider',
|
|
56
|
+
message: 'aiProvider is required when ai-sdk is selected (openai, anthropic, or google)',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (config.aiProvider && !AI_PROVIDERS.includes(config.aiProvider)) {
|
|
60
|
+
errors.push({
|
|
61
|
+
field: 'aiProvider',
|
|
62
|
+
message: `Invalid AI provider: "${config.aiProvider}". Valid: ${AI_PROVIDERS.join(', ')}`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return errors;
|
|
66
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-loadout",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Custom Next.js scaffolding CLI with optional SaaS integrations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"create-loadout": "./dist/index.js"
|
|
7
|
+
"create-loadout": "./dist/index.js",
|
|
8
|
+
"create-loadout-mcp": "./dist/mcp-server.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"dist"
|
|
@@ -12,6 +13,7 @@
|
|
|
12
13
|
"scripts": {
|
|
13
14
|
"build": "tsc",
|
|
14
15
|
"dev": "tsx src/index.ts",
|
|
16
|
+
"create": "tsx src/index.ts",
|
|
15
17
|
"prepublishOnly": "npm run build"
|
|
16
18
|
},
|
|
17
19
|
"keywords": [
|
|
@@ -31,14 +33,17 @@
|
|
|
31
33
|
"license": "MIT",
|
|
32
34
|
"dependencies": {
|
|
33
35
|
"@inquirer/prompts": "^7.0.0",
|
|
36
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
34
37
|
"chalk": "^5.0.0",
|
|
38
|
+
"commander": "^13.1.0",
|
|
39
|
+
"execa": "^9.0.0",
|
|
35
40
|
"ora": "^8.0.0",
|
|
36
|
-
"
|
|
41
|
+
"zod": "^4.3.6"
|
|
37
42
|
},
|
|
38
43
|
"devDependencies": {
|
|
39
44
|
"@types/node": "^22.0.0",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
45
|
+
"tsx": "^4.0.0",
|
|
46
|
+
"typescript": "^5.0.0"
|
|
42
47
|
},
|
|
43
48
|
"engines": {
|
|
44
49
|
"node": ">=18.0.0"
|