@shipindays/shipindays 0.1.13 → 0.1.15

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
@@ -42,12 +42,26 @@ const EMAIL_PROVIDERS = {
42
42
  },
43
43
  };
44
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",
54
+ },
55
+ };
56
+
45
57
  const ENV_VARS = {
46
58
  base: {
47
59
  "# App": [
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,12 +79,10 @@ 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)": [
@@ -83,13 +95,42 @@ const ENV_VARS = {
83
95
  "MAILGUN_DOMAIN=",
84
96
  ],
85
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
+
86
125
  },
87
126
  };
88
127
 
89
- // Recursively copy a directory, skipping unwanted folders.
90
- // We do this manually instead of fs.copy(filter) because fs-extra's filter
91
- // callback skips the root directory itself on some platforms when the
92
- // target doesn't exist yet, resulting in nothing being copied.
128
+ /**
129
+ * RECURSIVE DIRECTORY MERGE
130
+ * * This function walks through the source directory and copies files to the destination.
131
+ * If a directory exists in both places, it dives deeper to merge contents rather
132
+ * than replacing the entire folder.
133
+ */
93
134
  async function copyDir(src, dest, skipNames = []) {
94
135
  await fs.ensureDir(dest);
95
136
  const entries = await fs.readdir(src, { withFileTypes: true });
@@ -101,71 +142,68 @@ async function copyDir(src, dest, skipNames = []) {
101
142
  const destPath = path.join(dest, entry.name);
102
143
 
103
144
  if (entry.isDirectory()) {
104
- // Create sub-directory in destination and recurse
105
- await fs.ensureDir(destPath);
145
+ // If it's a directory, recurse into it to ensure we don't
146
+ // overwrite existing folders in the target, but merge into them.
106
147
  await copyDir(srcPath, destPath, skipNames);
107
148
  } else {
108
- // Copy the file
149
+ // If it's a file, copy/overwrite it into the target.
109
150
  await fs.copy(srcPath, destPath, { overwrite: true });
110
151
  }
111
152
  }
112
153
  }
113
154
 
114
- // Each block lives at templates/blocks/<feature>/<provider>/
115
- // Its src/ folder is copied on top of the project's src/.
116
- // To add a new provider: create the folder, match exported function names,
117
- // add a package.json with extra deps, register it in the provider map above,
118
- // and add env vars below.
155
+ /**
156
+ * BLOCK INJECTION LOGIC
157
+ * * Improved to handle deep nesting. It takes everything inside the provider folder
158
+ * (except package.json and node_modules) and merges it into the project root.
159
+ */
119
160
  async function injectBlock(feature, provider, targetPath) {
120
161
  const blockRoot = path.join(BLOCKS_DIR, feature, provider);
121
- const blockSrcDir = path.join(blockRoot, "src");
122
162
 
123
163
  if (!await fs.pathExists(blockRoot)) {
124
- throw new Error(`Block not found: ${blockRoot}`);
125
- }
126
-
127
- // 1. Copy everything inside block/src/ to target/src/
128
- if (await fs.pathExists(blockSrcDir)) {
129
- // We skip node_modules and .next if they accidentally exist in the block
130
- await copyDir(blockSrcDir, path.join(targetPath, "src"), ["node_modules", ".next"]);
164
+ throw new Error(`Block folder missing: ${blockRoot}`);
131
165
  }
132
166
 
133
- // 2. Handle non-src files (like public/ or drizzle/ if the block has them)
134
- // Check if there are other folders in the block root that aren't 'src' or 'package.json'
135
167
  const blockEntries = await fs.readdir(blockRoot, { withFileTypes: true });
168
+
136
169
  for (const entry of blockEntries) {
137
- if (entry.isDirectory() && entry.name !== "src" && entry.name !== "node_modules") {
138
- await copyDir(
139
- path.join(blockRoot, entry.name),
140
- path.join(targetPath, entry.name)
141
- );
170
+ // 1. Skip package.json (handled by mergePackageJson)
171
+ // 2. Skip node_modules or .next if they exist in the template
172
+ if (
173
+ entry.name === "package.json" ||
174
+ entry.name === "node_modules" ||
175
+ entry.name === ".next"
176
+ ) continue;
177
+
178
+ const srcPath = path.join(blockRoot, entry.name);
179
+ const destPath = path.join(targetPath, entry.name);
180
+
181
+ if (entry.isDirectory()) {
182
+ // This will now correctly merge 'src', 'public', 'hooks', etc.
183
+ // because copyDir is recursive.
184
+ await copyDir(srcPath, destPath, ["node_modules", ".next"]);
185
+ } else {
186
+ await fs.copy(srcPath, destPath, { overwrite: true });
142
187
  }
143
188
  }
144
189
  }
145
190
 
146
- // Reads the block's package.json and merges its deps into the project's package.json.
147
- // The block's package.json is never copied as a file — only read for merging.
191
+ /**
192
+ * PACKAGE.JSON MERGER
193
+ * * Deep merges dependencies and devDependencies so the final project
194
+ * has all required libraries from every selected block.
195
+ */
148
196
  async function mergePackageJson(targetPath, feature, provider) {
149
197
  const blockPkgPath = path.join(BLOCKS_DIR, feature, provider, "package.json");
150
198
  const targetPkgPath = path.join(targetPath, "package.json");
151
199
 
152
- if (!await fs.pathExists(blockPkgPath)) return;
153
- if (!await fs.pathExists(targetPkgPath)) return;
200
+ if (!await fs.pathExists(blockPkgPath) || !await fs.pathExists(targetPkgPath)) return;
154
201
 
155
202
  const targetPkg = await fs.readJson(targetPkgPath);
156
203
  const blockPkg = await fs.readJson(blockPkgPath);
157
204
 
158
- // Merge dependencies
159
- targetPkg.dependencies = {
160
- ...(targetPkg.dependencies ?? {}),
161
- ...(blockPkg.dependencies ?? {}),
162
- };
163
-
164
- // Merge devDependencies
165
- targetPkg.devDependencies = {
166
- ...(targetPkg.devDependencies ?? {}),
167
- ...(blockPkg.devDependencies ?? {}),
168
- };
205
+ targetPkg.dependencies = { ...(targetPkg.dependencies ?? {}), ...(blockPkg.dependencies ?? {}) };
206
+ targetPkg.devDependencies = { ...(targetPkg.devDependencies ?? {}), ...(blockPkg.devDependencies ?? {}) };
169
207
 
170
208
  await fs.writeJson(targetPkgPath, targetPkg, { spaces: 2 });
171
209
  }
@@ -322,7 +360,14 @@ async function main() {
322
360
  });
323
361
  if (p.isCancel(emailProvider)) { p.cancel("Cancelled."); process.exit(0); }
324
362
 
325
- const choices = { auth: authProvider, email: emailProvider };
363
+ const paymentProvider = await p.select({
364
+ message: "Payment provider",
365
+ options: Object.entries(PAYMENT_PROVIDERS).map(([value, { label, hint }]) => ({
366
+ value, label, hint,
367
+ })),
368
+ });
369
+
370
+ const choices = { auth: authProvider, email: emailProvider, payment: paymentProvider };
326
371
 
327
372
  // 4. Git + install preferences
328
373
  const initGit = await p.confirm({
@@ -342,24 +387,21 @@ async function main() {
342
387
 
343
388
  // 5. Copy base template
344
389
  spin.start("Copying base template...");
345
- if (!await fs.pathExists(BASE_DIR)) {
346
- spin.stop(chalk.red(`Base template not found: ${BASE_DIR}`));
347
- process.exit(1);
348
- }
349
390
  await copyDir(BASE_DIR, targetPath, ["node_modules", ".next", ".turbo"]);
350
391
  spin.stop("Base template copied.");
351
392
 
352
- // 6. Inject auth block
353
- spin.start(`Injecting auth: ${choices.auth}...`);
354
- await injectBlock("auth", choices.auth, targetPath);
355
- await mergePackageJson(targetPath, "auth", choices.auth);
356
- spin.stop(`Auth: ${choices.auth} ✓`);
357
-
358
- // 7. Inject email block
359
- spin.start(`Injecting email: ${choices.email}...`);
360
- await injectBlock("email", choices.email, targetPath);
361
- await mergePackageJson(targetPath, "email", choices.email);
362
- spin.stop(`Email: ${choices.email} ✓`);
393
+ // 6. Inject Blocks
394
+ // The injectBlock function now handles the internal recursion correctly.
395
+ const features = ["auth", "email", "payments"];
396
+ for (const feature of features) {
397
+ const provider = choices[feature];
398
+ if (provider) {
399
+ spin.start(`Injecting ${feature}: ${provider}...`);
400
+ await injectBlock(feature, provider, targetPath);
401
+ await mergePackageJson(targetPath, feature, provider);
402
+ spin.stop(`${feature} injected ✓`);
403
+ }
404
+ }
363
405
 
364
406
  // 8. Write .env.example
365
407
  spin.start("Writing .env.example...");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipindays/shipindays",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
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,5 @@
1
+ {
2
+ "dependencies": {
3
+ "dodopayments": "^latest"
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "dependencies": {
3
+ "stripe": "^14.21.0"
4
+ }
5
+ }