create-loadout 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -76,10 +76,11 @@ your-app/
76
76
  | 🗄️ | **Neon + Drizzle** | Serverless Postgres with full CRUD example |
77
77
  | 🤖 | **AI SDK** | OpenAI / Anthropic / Google |
78
78
  | 📧 | **Resend** | Email service + React email templates |
79
+ | 📬 | **Postmark** | Transactional email with top deliverability |
79
80
  | 🔥 | **Firecrawl** | Web scraping service |
80
81
  | ⏰ | **Inngest** | Background jobs |
81
82
  | 📁 | **UploadThing** | File uploads |
82
- | 💳 | **Stripe** | Checkout, webhooks, customer portal |
83
+ | 💳 | **Stripe** | Payment service with checkout + billing |
83
84
  | 📊 | **PostHog** | Product analytics |
84
85
  | 🐛 | **Sentry** | Error tracking |
85
86
 
@@ -89,27 +90,94 @@ your-app/
89
90
 
90
91
  ## How It Works
91
92
 
92
- ### 1. Run the CLI
93
+ ### Interactive Mode
93
94
 
94
95
  ```bash
95
96
  npx create-loadout
96
97
  ```
97
98
 
98
- ### 2. Answer the Prompts
99
+ Answer the prompts — project name, integrations, AI provider — and you're done.
99
100
 
100
- - Project name
101
- - Which integrations you need
102
- - AI provider (if using AI SDK)
101
+ ### Non-Interactive Mode
103
102
 
104
- ### 3. Start Building
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
105
153
 
106
154
  ```bash
107
155
  cd your-app
108
- npm install
109
156
  npm run dev
110
157
  ```
111
158
 
112
- That's it. Fill in `.env.local` and you're live.
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 |
113
181
 
114
182
  ---
115
183
 
package/dist/claude-md.js CHANGED
@@ -43,6 +43,13 @@ const stackSections = [
43
43
  { name: 'Resend', url: 'https://resend.com/docs', description: 'Email API' },
44
44
  ],
45
45
  },
46
+ {
47
+ id: 'postmark',
48
+ name: 'Email',
49
+ items: [
50
+ { name: 'Postmark', url: 'https://postmarkapp.com/developer', description: 'Transactional email' },
51
+ ],
52
+ },
46
53
  {
47
54
  id: 'firecrawl',
48
55
  name: 'Scraping',
@@ -142,8 +149,8 @@ npm run inngest:dev # Start Inngest dev server for local testing
142
149
  ├── app/ # Next.js App Router pages and API routes
143
150
  ├── components/ # React components (including shadcn/ui)
144
151
  `;
145
- if (config.integrations.includes('resend')) {
146
- content += `│ └── emails/ # React Email templates
152
+ if (config.integrations.includes('resend') || config.integrations.includes('postmark')) {
153
+ content += `│ └── emails/ # Email templates
147
154
  `;
148
155
  }
149
156
  if (hasPostHog || hasSentry) {
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 { createNextApp } from './create-next.js';
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 = ora('Creating Next.js app...').start();
41
- const projectPath = await createNextApp(config.name);
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
- await installIntegrations(projectPath, config);
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/config.js CHANGED
@@ -14,6 +14,10 @@ const staticConfigVars = {
14
14
  { name: 'RESEND_API_KEY', envKey: 'RESEND_API_KEY', isPublic: false },
15
15
  { name: 'RESEND_FROM_EMAIL', envKey: 'RESEND_FROM_EMAIL', isPublic: false, defaultValue: 'onboarding@resend.dev' },
16
16
  ],
17
+ postmark: [
18
+ { name: 'POSTMARK_SERVER_TOKEN', envKey: 'POSTMARK_SERVER_TOKEN', isPublic: false },
19
+ { name: 'POSTMARK_FROM_EMAIL', envKey: 'POSTMARK_FROM_EMAIL', isPublic: false },
20
+ ],
17
21
  firecrawl: [
18
22
  { name: 'FIRECRAWL_API_KEY', envKey: 'FIRECRAWL_API_KEY', isPublic: false },
19
23
  ],
@@ -11,7 +11,7 @@ export async function createNextApp(name) {
11
11
  '--import-alias', '@/*',
12
12
  '--yes',
13
13
  ], {
14
- stdio: 'inherit',
14
+ stdio: 'pipe',
15
15
  });
16
16
  return path.resolve(process.cwd(), name);
17
17
  }
package/dist/detect.js CHANGED
@@ -5,6 +5,7 @@ const integrationPackages = {
5
5
  'neon-drizzle': ['drizzle-orm', '@neondatabase/serverless'],
6
6
  'ai-sdk': ['ai'],
7
7
  resend: ['resend'],
8
+ postmark: ['postmark'],
8
9
  firecrawl: ['@mendable/firecrawl-js'],
9
10
  inngest: ['inngest'],
10
11
  uploadthing: ['uploadthing'],
@@ -49,6 +50,7 @@ export function getAvailableIntegrations(installed) {
49
50
  'neon-drizzle',
50
51
  'ai-sdk',
51
52
  'resend',
53
+ 'postmark',
52
54
  'firecrawl',
53
55
  'inngest',
54
56
  'uploadthing',
@@ -56,5 +58,13 @@ export function getAvailableIntegrations(installed) {
56
58
  'posthog',
57
59
  'sentry',
58
60
  ];
59
- return all.filter((id) => !installed.includes(id));
61
+ const emailProviders = ['resend', 'postmark'];
62
+ const hasEmail = emailProviders.some((id) => installed.includes(id));
63
+ return all.filter((id) => {
64
+ if (installed.includes(id))
65
+ return false;
66
+ if (hasEmail && emailProviders.includes(id))
67
+ return false;
68
+ return true;
69
+ });
60
70
  }
@@ -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/env.js CHANGED
@@ -29,6 +29,14 @@ const staticEnvSections = {
29
29
  { key: 'RESEND_FROM_EMAIL', example: 'onboarding@resend.dev', description: 'Default from email address' },
30
30
  ],
31
31
  },
32
+ postmark: {
33
+ name: 'POSTMARK - Email',
34
+ url: 'https://account.postmarkapp.com/servers',
35
+ vars: [
36
+ { key: 'POSTMARK_SERVER_TOKEN', example: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', description: 'Postmark server token' },
37
+ { key: 'POSTMARK_FROM_EMAIL', example: 'hello@yourdomain.com', description: 'Default from email address' },
38
+ ],
39
+ },
32
40
  firecrawl: {
33
41
  name: 'FIRECRAWL - Scraping',
34
42
  url: 'https://firecrawl.dev',
@@ -5,6 +5,7 @@ const integrationNames = {
5
5
  'neon-drizzle': 'Neon + Drizzle',
6
6
  'ai-sdk': 'Vercel AI SDK',
7
7
  resend: 'Resend',
8
+ postmark: 'Postmark',
8
9
  firecrawl: 'Firecrawl',
9
10
  inngest: 'Inngest',
10
11
  uploadthing: 'UploadThing',
@@ -89,7 +90,7 @@ npm run inngest:dev # Start Inngest dev server
89
90
  ├── lib/ # Utilities and clients
90
91
  ├── services/ # Business logic
91
92
  `;
92
- if (config.integrations.includes('resend')) {
93
+ if (config.integrations.includes('resend') || config.integrations.includes('postmark')) {
93
94
  content += `├── emails/ # Email templates\n`;
94
95
  }
95
96
  content += `└── public/ # Static assets
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
- main();
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>>;
@@ -7,6 +7,7 @@ import { firecrawlIntegration } from './firecrawl.js';
7
7
  import { inngestIntegration } from './inngest.js';
8
8
  import { uploadthingIntegration } from './uploadthing.js';
9
9
  import { stripeIntegration } from './stripe.js';
10
+ import { postmarkIntegration } from './postmark.js';
10
11
  import { posthogIntegration } from './posthog.js';
11
12
  import { sentryIntegration } from './sentry.js';
12
13
  // Static integrations (don't need config)
@@ -18,11 +19,12 @@ const staticIntegrations = {
18
19
  inngest: inngestIntegration,
19
20
  uploadthing: uploadthingIntegration,
20
21
  stripe: stripeIntegration,
22
+ postmark: postmarkIntegration,
21
23
  posthog: posthogIntegration,
22
24
  sentry: sentryIntegration,
23
25
  };
24
26
  // Get integration, with dynamic ones using config
25
- function getIntegration(id, config) {
27
+ export function getIntegration(id, config) {
26
28
  if (id === 'ai-sdk') {
27
29
  return createAiSdkIntegration(config.aiProvider ?? 'openai');
28
30
  }
@@ -0,0 +1,2 @@
1
+ import type { Integration } from '../types.js';
2
+ export declare const postmarkIntegration: Integration;
@@ -0,0 +1,34 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { postmarkTemplates } from '../templates/postmark.js';
4
+ export const postmarkIntegration = {
5
+ id: 'postmark',
6
+ name: 'Postmark',
7
+ description: 'Transactional email',
8
+ packages: ['postmark'],
9
+ envVars: [
10
+ {
11
+ key: 'POSTMARK_SERVER_TOKEN',
12
+ description: 'Postmark server token',
13
+ example: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
14
+ },
15
+ {
16
+ key: 'POSTMARK_FROM_EMAIL',
17
+ description: 'Default from email address',
18
+ example: 'hello@yourdomain.com',
19
+ },
20
+ ],
21
+ setup: async (projectPath) => {
22
+ // Create email service
23
+ await fs.mkdir(path.join(projectPath, 'services'), { recursive: true });
24
+ await fs.writeFile(path.join(projectPath, 'services/email.service.ts'), postmarkTemplates.emailService);
25
+ // Create email templates in components/emails
26
+ await fs.mkdir(path.join(projectPath, 'components/emails'), { recursive: true });
27
+ await fs.writeFile(path.join(projectPath, 'components/emails/welcome.tsx'), postmarkTemplates.welcomeEmail);
28
+ // Create API route
29
+ await fs.mkdir(path.join(projectPath, 'app/api/email/send'), {
30
+ recursive: true,
31
+ });
32
+ await fs.writeFile(path.join(projectPath, 'app/api/email/send/route.ts'), postmarkTemplates.sendRoute);
33
+ },
34
+ };