create-loadout 1.0.0 → 1.0.1

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,6 +76,7 @@ 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 |
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/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
  ],
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
  }
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
@@ -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,6 +19,7 @@ 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
  };
@@ -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
+ };
@@ -13,6 +13,7 @@ const integrationTechnologies = {
13
13
  'neon-drizzle': { name: 'Neon + Drizzle', href: 'https://neon.tech/docs', description: 'Serverless Postgres' },
14
14
  'ai-sdk': { name: 'AI SDK', href: 'https://sdk.vercel.ai/docs', description: 'AI integration' },
15
15
  resend: { name: 'Resend', href: 'https://resend.com/docs', description: 'Email API' },
16
+ postmark: { name: 'Postmark', href: 'https://postmarkapp.com/developer', description: 'Transactional email' },
16
17
  firecrawl: { name: 'Firecrawl', href: 'https://docs.firecrawl.dev', description: 'Web scraping' },
17
18
  inngest: { name: 'Inngest', href: 'https://www.inngest.com/docs', description: 'Background jobs' },
18
19
  uploadthing: { name: 'UploadThing', href: 'https://docs.uploadthing.com', description: 'File uploads' },
package/dist/prompts.js CHANGED
@@ -35,8 +35,15 @@ export async function getProjectConfig() {
35
35
  });
36
36
  }
37
37
  // Email
38
- if (await confirm({ message: 'Add Resend for email?', default: false })) {
39
- integrations.push('resend');
38
+ if (await confirm({ message: 'Add email?', default: false })) {
39
+ const emailProvider = await select({
40
+ message: 'Which email provider?',
41
+ choices: [
42
+ { value: 'resend', name: 'Resend (Modern DX, React Email templates)' },
43
+ { value: 'postmark', name: 'Postmark (Best-in-class deliverability, fast transactional email)' },
44
+ ],
45
+ });
46
+ integrations.push(emailProvider);
40
47
  }
41
48
  // Scraping
42
49
  if (await confirm({ message: 'Add Firecrawl for web scraping?', default: false })) {
@@ -68,7 +75,8 @@ const integrationPrompts = {
68
75
  clerk: 'Add Clerk for authentication?',
69
76
  'neon-drizzle': 'Add Neon + Drizzle for database?',
70
77
  'ai-sdk': 'Add Vercel AI SDK?',
71
- resend: 'Add Resend for email?',
78
+ resend: 'Add email?',
79
+ postmark: 'Add email?',
72
80
  firecrawl: 'Add Firecrawl for web scraping?',
73
81
  inngest: 'Add Inngest for background jobs?',
74
82
  uploadthing: 'Add UploadThing for file uploads?',
@@ -79,7 +87,26 @@ const integrationPrompts = {
79
87
  export async function getAddIntegrationConfig(available) {
80
88
  const integrations = [];
81
89
  let aiProvider;
90
+ const emailProviders = ['resend', 'postmark'];
91
+ let emailPromptShown = false;
82
92
  for (const id of available) {
93
+ // For email providers, show a single confirm + select prompt
94
+ if (emailProviders.includes(id)) {
95
+ if (emailPromptShown)
96
+ continue;
97
+ emailPromptShown = true;
98
+ if (await confirm({ message: 'Add email?', default: false })) {
99
+ const emailProvider = await select({
100
+ message: 'Which email provider?',
101
+ choices: [
102
+ { value: 'resend', name: 'Resend (Modern DX, React Email templates)' },
103
+ { value: 'postmark', name: 'Postmark (Best-in-class deliverability, fast transactional email)' },
104
+ ],
105
+ });
106
+ integrations.push(emailProvider);
107
+ }
108
+ continue;
109
+ }
83
110
  const message = integrationPrompts[id];
84
111
  if (await confirm({ message, default: false })) {
85
112
  integrations.push(id);
@@ -0,0 +1,5 @@
1
+ export declare const postmarkTemplates: {
2
+ emailService: string;
3
+ welcomeEmail: string;
4
+ sendRoute: string;
5
+ };
@@ -0,0 +1,103 @@
1
+ export const postmarkTemplates = {
2
+ emailService: `import { ServerClient } from 'postmark';
3
+ import type { ReactElement } from 'react';
4
+ import { renderToStaticMarkup } from 'react-dom/server';
5
+ import { POSTMARK_SERVER_TOKEN, POSTMARK_FROM_EMAIL } from '@/lib/config';
6
+
7
+ export interface SendEmailOptions {
8
+ to: string | string[];
9
+ subject: string;
10
+ react: ReactElement;
11
+ from?: string;
12
+ replyTo?: string;
13
+ }
14
+
15
+ export class EmailService {
16
+ private client: ServerClient;
17
+ private defaultFrom: string;
18
+
19
+ constructor(serverToken: string, defaultFrom: string) {
20
+ this.client = new ServerClient(serverToken);
21
+ this.defaultFrom = defaultFrom;
22
+ }
23
+
24
+ async send(options: SendEmailOptions) {
25
+ const htmlBody = renderToStaticMarkup(options.react);
26
+ const to = Array.isArray(options.to) ? options.to.join(',') : options.to;
27
+
28
+ const result = await this.client.sendEmail({
29
+ From: options.from || this.defaultFrom,
30
+ To: to,
31
+ Subject: options.subject,
32
+ HtmlBody: htmlBody,
33
+ ReplyTo: options.replyTo,
34
+ MessageStream: 'outbound',
35
+ });
36
+
37
+ return { id: result.MessageID };
38
+ }
39
+
40
+ async sendWelcome(to: string, name: string) {
41
+ const { WelcomeEmail } = await import('@/components/emails/welcome');
42
+ return this.send({
43
+ to,
44
+ subject: 'Welcome!',
45
+ react: WelcomeEmail({ name }),
46
+ });
47
+ }
48
+ }
49
+
50
+ export const emailService = new EmailService(POSTMARK_SERVER_TOKEN, POSTMARK_FROM_EMAIL);
51
+ `,
52
+ welcomeEmail: `import * as React from 'react';
53
+
54
+ interface WelcomeEmailProps {
55
+ name: string;
56
+ }
57
+
58
+ export function WelcomeEmail({ name }: WelcomeEmailProps) {
59
+ return (
60
+ <div style={{ fontFamily: 'sans-serif', padding: '20px' }}>
61
+ <h1 style={{ color: '#333' }}>Welcome, {name}!</h1>
62
+ <p style={{ color: '#666', lineHeight: 1.6 }}>
63
+ Thanks for signing up. We're excited to have you on board.
64
+ </p>
65
+ <p style={{ color: '#666', lineHeight: 1.6 }}>
66
+ If you have any questions, feel free to reply to this email.
67
+ </p>
68
+ </div>
69
+ );
70
+ }
71
+ `,
72
+ sendRoute: `import { NextResponse } from 'next/server';
73
+ import { emailService } from '@/services/email.service';
74
+ import { z } from 'zod';
75
+
76
+ const sendEmailSchema = z.object({
77
+ to: z.email(),
78
+ name: z.string().min(1),
79
+ });
80
+
81
+ export async function POST(req: Request) {
82
+ try {
83
+ const body = await req.json();
84
+ const { to, name } = sendEmailSchema.parse(body);
85
+
86
+ const data = await emailService.sendWelcome(to, name);
87
+
88
+ return NextResponse.json(data);
89
+ } catch (error) {
90
+ if (error instanceof z.ZodError) {
91
+ return NextResponse.json(
92
+ { error: 'Invalid request', details: error.errors },
93
+ { status: 400 }
94
+ );
95
+ }
96
+ return NextResponse.json(
97
+ { error: error instanceof Error ? error.message : 'Failed to send email' },
98
+ { status: 500 }
99
+ );
100
+ }
101
+ }
102
+ `,
103
+ };
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type IntegrationId = 'clerk' | 'neon-drizzle' | 'ai-sdk' | 'resend' | 'firecrawl' | 'inngest' | 'uploadthing' | 'stripe' | 'posthog' | 'sentry';
1
+ export type IntegrationId = 'clerk' | 'neon-drizzle' | 'ai-sdk' | 'resend' | 'firecrawl' | 'inngest' | 'uploadthing' | 'stripe' | 'postmark' | 'posthog' | 'sentry';
2
2
  export interface Integration {
3
3
  id: IntegrationId;
4
4
  name: string;
package/package.json CHANGED
@@ -1,46 +1,46 @@
1
- {
2
- "name": "create-loadout",
3
- "version": "1.0.0",
4
- "description": "Custom Next.js scaffolding CLI with optional SaaS integrations",
5
- "type": "module",
6
- "bin": {
7
- "create-loadout": "./dist/index.js"
8
- },
9
- "files": [
10
- "dist"
11
- ],
12
- "scripts": {
13
- "build": "tsc",
14
- "dev": "tsx src/index.ts",
15
- "prepublishOnly": "npm run build"
16
- },
17
- "keywords": [
18
- "nextjs",
19
- "cli",
20
- "scaffolding",
21
- "saas",
22
- "boilerplate"
23
- ],
24
- "author": {
25
- "name": "Kyle Davidson"
26
- },
27
- "repository": {
28
- "type": "git",
29
- "url": "https://github.com/kylerd/loadout.git"
30
- },
31
- "license": "MIT",
32
- "dependencies": {
33
- "@inquirer/prompts": "^7.0.0",
34
- "chalk": "^5.0.0",
35
- "ora": "^8.0.0",
36
- "execa": "^9.0.0"
37
- },
38
- "devDependencies": {
39
- "@types/node": "^22.0.0",
40
- "typescript": "^5.0.0",
41
- "tsx": "^4.0.0"
42
- },
43
- "engines": {
44
- "node": ">=18.0.0"
45
- }
46
- }
1
+ {
2
+ "name": "create-loadout",
3
+ "version": "1.0.1",
4
+ "description": "Custom Next.js scaffolding CLI with optional SaaS integrations",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-loadout": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "dev": "tsx src/index.ts",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "keywords": [
18
+ "nextjs",
19
+ "cli",
20
+ "scaffolding",
21
+ "saas",
22
+ "boilerplate"
23
+ ],
24
+ "author": {
25
+ "name": "Kyle Davidson"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/kylerd/loadout.git"
30
+ },
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "@inquirer/prompts": "^7.0.0",
34
+ "chalk": "^5.0.0",
35
+ "ora": "^8.0.0",
36
+ "execa": "^9.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22.0.0",
40
+ "typescript": "^5.0.0",
41
+ "tsx": "^4.0.0"
42
+ },
43
+ "engines": {
44
+ "node": ">=18.0.0"
45
+ }
46
+ }