@shipindays/shipindays 0.1.12 → 0.1.14

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/index.js CHANGED
@@ -36,9 +36,21 @@ const EMAIL_PROVIDERS = {
36
36
  label: "Resend",
37
37
  hint: "resend.com — best DX, generous free tier",
38
38
  },
39
- nodemailer: {
40
- label: "Nodemailer",
41
- hint: "SMTPworks with Gmail, Outlook, any mail server",
39
+ mailgun: {
40
+ label: "Mailgun",
41
+ hint: "mailgun.compowerful API, great for scaling",
42
+ },
43
+ };
44
+
45
+ // Add to your CLI constants
46
+ const PAYMENT_PROVIDERS = {
47
+ stripe: {
48
+ label: "Stripe",
49
+ hint: "Subscriptions + One-time payments via Stripe Checkout",
50
+ },
51
+ dodopayments: {
52
+ label: "Dodo Payments",
53
+ hint: "Merchant of Record — simplifies global tax/compliance",
42
54
  },
43
55
  };
44
56
 
@@ -48,6 +60,8 @@ const ENV_VARS = {
48
60
  "NEXT_PUBLIC_APP_URL=http://localhost:3000",
49
61
  ],
50
62
  },
63
+
64
+ // auth env's
51
65
  auth: {
52
66
  supabase: {
53
67
  "# Supabase (supabase.com → project → settings → API)": [
@@ -65,28 +79,49 @@ const ENV_VARS = {
65
79
  "AUTH_GOOGLE_ID=",
66
80
  "AUTH_GOOGLE_SECRET=",
67
81
  ],
68
- "# OAuth — GitHub (github.com → Settings → Developer settings → OAuth Apps)": [
69
- "AUTH_GITHUB_ID=",
70
- "AUTH_GITHUB_SECRET=",
71
- ],
72
82
  },
73
83
  },
84
+
85
+ // emails env's
74
86
  email: {
75
87
  resend: {
76
88
  "# Resend (resend.com → API Keys)": [
77
89
  "RESEND_API_KEY=",
78
90
  ],
79
91
  },
80
- nodemailer: {
81
- "# SMTP / Nodemailer": [
82
- "SMTP_HOST=smtp.gmail.com",
83
- "SMTP_PORT=587",
84
- "SMTP_SECURE=false",
85
- "SMTP_USER=",
86
- "SMTP_PASS=",
87
- "SMTP_FROM=you@yourdomain.com",
92
+ mailgun: {
93
+ "# Mailgun (mailgun.com → Sending → Domains)": [
94
+ "MAILGUN_API_KEY=",
95
+ "MAILGUN_DOMAIN=",
88
96
  ],
89
97
  },
98
+
99
+ // payments env's
100
+ payments: {
101
+ stripe: {
102
+ "# Stripe (dashboard.stripe.com → Developers → API keys)": [
103
+ "STRIPE_SECRET_KEY=",
104
+ "STRIPE_WEBHOOK_SECRET=",
105
+ "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=",
106
+ ],
107
+ "# Stripe Pricing (Create products in Stripe dashboard)": [
108
+ "NEXT_PUBLIC_STRIPE_PRICE_ID_BASIC=",
109
+ "NEXT_PUBLIC_STRIPE_PRICE_ID_PRO=",
110
+ ],
111
+ },
112
+
113
+ dodopayments: {
114
+ "# Dodo Payments (app.dodopayments.com → Developers → API Keys)": [
115
+ "DODO_PAYMENTS_API_KEY=",
116
+ "DODO_PAYMENTS_WEBHOOK_KEY=",
117
+ ],
118
+ "# Dodo Pricing (Create products in Dodo dashboard)": [
119
+ "NEXT_PUBLIC_DODO_PRICE_ID_BASIC=",
120
+ "NEXT_PUBLIC_DODO_PRICE_ID_PRO=",
121
+ ],
122
+ },
123
+ },
124
+
90
125
  },
91
126
  };
92
127
 
@@ -97,13 +132,19 @@ const ENV_VARS = {
97
132
  async function copyDir(src, dest, skipNames = []) {
98
133
  await fs.ensureDir(dest);
99
134
  const entries = await fs.readdir(src, { withFileTypes: true });
135
+
100
136
  for (const entry of entries) {
101
137
  if (skipNames.includes(entry.name)) continue;
138
+
102
139
  const srcPath = path.join(src, entry.name);
103
140
  const destPath = path.join(dest, entry.name);
141
+
104
142
  if (entry.isDirectory()) {
143
+ // Create sub-directory in destination and recurse
144
+ await fs.ensureDir(destPath);
105
145
  await copyDir(srcPath, destPath, skipNames);
106
146
  } else {
147
+ // Copy the file
107
148
  await fs.copy(srcPath, destPath, { overwrite: true });
108
149
  }
109
150
  }
@@ -119,20 +160,26 @@ async function injectBlock(feature, provider, targetPath) {
119
160
  const blockSrcDir = path.join(blockRoot, "src");
120
161
 
121
162
  if (!await fs.pathExists(blockRoot)) {
122
- throw new Error(
123
- `Block not found: ${blockRoot}\n` +
124
- `Make sure templates/blocks/${feature}/${provider}/ exists.`
125
- );
163
+ throw new Error(`Block not found: ${blockRoot}`);
126
164
  }
127
165
 
128
- if (!await fs.pathExists(blockSrcDir)) {
129
- throw new Error(
130
- `Block src/ folder missing: ${blockSrcDir}\n` +
131
- `Structure: templates/blocks/${feature}/${provider}/src/...`
132
- );
166
+ // 1. Copy everything inside block/src/ to target/src/
167
+ if (await fs.pathExists(blockSrcDir)) {
168
+ // We skip node_modules and .next if they accidentally exist in the block
169
+ await copyDir(blockSrcDir, path.join(targetPath, "src"), ["node_modules", ".next"]);
133
170
  }
134
171
 
135
- await copyDir(blockSrcDir, path.join(targetPath, "src"), ["node_modules", ".next"]);
172
+ // 2. Handle non-src files (like public/ or drizzle/ if the block has them)
173
+ // Check if there are other folders in the block root that aren't 'src' or 'package.json'
174
+ const blockEntries = await fs.readdir(blockRoot, { withFileTypes: true });
175
+ for (const entry of blockEntries) {
176
+ if (entry.isDirectory() && entry.name !== "src" && entry.name !== "node_modules") {
177
+ await copyDir(
178
+ path.join(blockRoot, entry.name),
179
+ path.join(targetPath, entry.name)
180
+ );
181
+ }
182
+ }
136
183
  }
137
184
 
138
185
  // Reads the block's package.json and merges its deps into the project's package.json.
@@ -147,10 +194,13 @@ async function mergePackageJson(targetPath, feature, provider) {
147
194
  const targetPkg = await fs.readJson(targetPkgPath);
148
195
  const blockPkg = await fs.readJson(blockPkgPath);
149
196
 
197
+ // Merge dependencies
150
198
  targetPkg.dependencies = {
151
199
  ...(targetPkg.dependencies ?? {}),
152
200
  ...(blockPkg.dependencies ?? {}),
153
201
  };
202
+
203
+ // Merge devDependencies
154
204
  targetPkg.devDependencies = {
155
205
  ...(targetPkg.devDependencies ?? {}),
156
206
  ...(blockPkg.devDependencies ?? {}),
@@ -311,7 +361,14 @@ async function main() {
311
361
  });
312
362
  if (p.isCancel(emailProvider)) { p.cancel("Cancelled."); process.exit(0); }
313
363
 
314
- const choices = { auth: authProvider, email: emailProvider };
364
+ const paymentProvider = await p.select({
365
+ message: "Payment provider",
366
+ options: Object.entries(PAYMENT_PROVIDERS).map(([value, { label, hint }]) => ({
367
+ value, label, hint,
368
+ })),
369
+ });
370
+
371
+ const choices = { auth: authProvider, email: emailProvider, payment: paymentProvider };
315
372
 
316
373
  // 4. Git + install preferences
317
374
  const initGit = await p.confirm({
@@ -350,6 +407,12 @@ async function main() {
350
407
  await mergePackageJson(targetPath, "email", choices.email);
351
408
  spin.stop(`Email: ${choices.email} ✓`);
352
409
 
410
+ // 7. Inject payment block
411
+ spin.start(`Injecting payments: ${choices.payments}...`);
412
+ await injectBlock("payments", choices.payments, targetPath);
413
+ await mergePackageJson(targetPath, "payments", choices.payments);
414
+ spin.stop(`Payments: ${choices.payments} ✓`);
415
+
353
416
  // 8. Write .env.example
354
417
  spin.start("Writing .env.example...");
355
418
  await fs.outputFile(path.join(targetPath, ".env.example"), buildEnvExample(choices));
@@ -377,7 +440,7 @@ async function main() {
377
440
  run(`git commit -m "chore: scaffold from shipindays"`, targetPath);
378
441
  spin.stop("Git initialised.");
379
442
  } catch {
380
- spin.stop(chalk.yellow("Git skipped — run manually."));
443
+ spin.stop(chalk.yellow("Git skipped — please run manually."));
381
444
  }
382
445
  }
383
446
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipindays/shipindays",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Open source CLI to scaffold a Next.js SaaS. Pick your auth, email, and payments wired up and ready to ship.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,6 @@
1
+ {
2
+ "dependencies": {
3
+ "form-data": "^4.0.1",
4
+ "mailgun": "^0.5.0"
5
+ }
6
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "dependencies": {
3
+ "dodopayments": "^latest"
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "dependencies": {
3
+ "stripe": "^14.21.0"
4
+ }
5
+ }
@@ -1,62 +0,0 @@
1
- // FILE: src/lib/email/index.ts
2
- // ROUTE: not a route — imported anywhere that sends email
3
- // ROLE: Nodemailer provider implementation
4
- //
5
- // INJECTED BY CLI when user picks "Nodemailer" as their email provider.
6
- // Replaces templates/base/src/lib/email/index.ts
7
- // ─────────────────────────────────────────────────────────────────────────────
8
-
9
- import nodemailer from "nodemailer";
10
-
11
- // Nodemailer transport — reads SMTP config from env
12
- // Works with Gmail, Outlook, Mailgun SMTP, any SMTP server
13
- const transporter = nodemailer.createTransport({
14
- host: process.env.SMTP_HOST,
15
- port: Number(process.env.SMTP_PORT ?? 587),
16
- secure: process.env.SMTP_SECURE === "true",
17
- auth: {
18
- user: process.env.SMTP_USER,
19
- pass: process.env.SMTP_PASS,
20
- },
21
- });
22
-
23
- const FROM = process.env.SMTP_FROM ?? "you@yourdomain.com";
24
-
25
- // ─── sendWelcomeEmail ─────────────────────────────────────────────────────────
26
- export async function sendWelcomeEmail({ to, name }: { to: string; name: string }) {
27
- await transporter.sendMail({
28
- from: FROM,
29
- to,
30
- subject: "Welcome! 🎉",
31
- html: `
32
- <div style="font-family:sans-serif;max-width:560px;margin:0 auto;padding:40px 20px">
33
- <h1 style="font-size:24px;color:#111">Hey ${name}, welcome aboard!</h1>
34
- <p style="color:#555;line-height:1.7">Your account is ready. Click below to get started.</p>
35
- <a href="${process.env.NEXT_PUBLIC_APP_URL}/dashboard"
36
- style="display:inline-block;margin-top:20px;padding:12px 24px;background:#111;color:#fff;border-radius:6px;text-decoration:none">
37
- Go to Dashboard →
38
- </a>
39
- </div>
40
- `,
41
- });
42
- }
43
-
44
- // ─── sendPasswordResetEmail ───────────────────────────────────────────────────
45
- export async function sendPasswordResetEmail({ to, resetUrl }: { to: string; resetUrl: string }) {
46
- await transporter.sendMail({
47
- from: FROM,
48
- to,
49
- subject: "Reset your password",
50
- html: `
51
- <div style="font-family:sans-serif;max-width:560px;margin:0 auto;padding:40px 20px">
52
- <h1 style="font-size:24px;color:#111">Reset your password</h1>
53
- <p style="color:#555;line-height:1.7">Click below to reset your password. Link expires in 1 hour.</p>
54
- <a href="${resetUrl}"
55
- style="display:inline-block;margin-top:20px;padding:12px 24px;background:#111;color:#fff;border-radius:6px;text-decoration:none">
56
- Reset Password →
57
- </a>
58
- <p style="margin-top:24px;color:#999;font-size:12px">If you didn't request this, ignore this email.</p>
59
- </div>
60
- `,
61
- });
62
- }
@@ -1,8 +0,0 @@
1
- {
2
- "dependencies": {
3
- "nodemailer": "^6.9.9"
4
- },
5
- "devDependencies": {
6
- "@types/nodemailer": "^6.4.16"
7
- }
8
- }