@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 +90 -27
- package/package.json +1 -1
- package/templates/blocks/email/mailgun/package.json +6 -0
- package/templates/blocks/payments/dodopayments/package.json +5 -0
- package/templates/blocks/payments/stripe/package.json +5 -0
- package/templates/blocks/email/nodemailer/index.ts +0 -62
- package/templates/blocks/email/nodemailer/package.json +0 -8
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
|
-
|
|
40
|
-
label: "
|
|
41
|
-
hint: "
|
|
39
|
+
mailgun: {
|
|
40
|
+
label: "Mailgun",
|
|
41
|
+
hint: "mailgun.com — powerful 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
|
-
|
|
81
|
-
"#
|
|
82
|
-
"
|
|
83
|
-
"
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
|
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,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
|
-
}
|