stripe-no-webhooks 0.0.2 → 0.0.4

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/bin/cli.js CHANGED
@@ -5,6 +5,10 @@ const readline = require("readline");
5
5
  const fs = require("fs");
6
6
  const path = require("path");
7
7
 
8
+ // Load environment variables from .env files in the user's project directory
9
+ require("dotenv").config({ path: path.join(process.cwd(), ".env.local") });
10
+ require("dotenv").config({ path: path.join(process.cwd(), ".env") });
11
+
8
12
  const args = process.argv.slice(2);
9
13
  const command = args[0];
10
14
  const databaseUrl = args[1];
@@ -25,12 +29,21 @@ function question(rl, query, defaultValue = "") {
25
29
  });
26
30
  }
27
31
 
28
- function questionHidden(rl, query) {
32
+ function maskSecretKey(key) {
33
+ if (!key || key.length < 8) return "*****";
34
+ return key.slice(0, 3) + "*****" + key.slice(-4);
35
+ }
36
+
37
+ function questionHidden(rl, query, defaultValue = "") {
29
38
  return new Promise((resolve) => {
30
39
  const stdin = process.stdin;
31
40
  const stdout = process.stdout;
32
41
 
33
- stdout.write(`${query}: `);
42
+ const maskedDefault = defaultValue ? maskSecretKey(defaultValue) : "";
43
+ const prompt = maskedDefault
44
+ ? `${query} (${maskedDefault}): `
45
+ : `${query}: `;
46
+ stdout.write(prompt);
34
47
 
35
48
  stdin.setRawMode(true);
36
49
  stdin.resume();
@@ -43,7 +56,7 @@ function questionHidden(rl, query) {
43
56
  stdin.pause();
44
57
  stdin.removeListener("data", onData);
45
58
  stdout.write("\n");
46
- resolve(input);
59
+ resolve(input || defaultValue);
47
60
  } else if (char === "\u0003") {
48
61
  // Ctrl+C
49
62
  process.exit();
@@ -55,7 +68,7 @@ function questionHidden(rl, query) {
55
68
  }
56
69
  } else {
57
70
  input += char;
58
- stdout.write("**************"); // Show asterisk for each character
71
+ stdout.write("*"); // Show asterisk for each character
59
72
  }
60
73
  };
61
74
 
@@ -80,6 +93,13 @@ async function migrate(databaseUrl) {
80
93
  logger: console,
81
94
  });
82
95
  console.log("āœ… Migrations completed successfully!");
96
+
97
+ // Save DATABASE_URL to env files
98
+ const envVars = [{ key: "DATABASE_URL", value: databaseUrl }];
99
+ const updatedFiles = saveToEnvFiles(envVars);
100
+ if (updatedFiles.length > 0) {
101
+ console.log(`šŸ“ Updated ${updatedFiles.join(", ")} with DATABASE_URL`);
102
+ }
83
103
  } catch (error) {
84
104
  console.error("āŒ Migration failed:");
85
105
  console.error(error);
@@ -87,6 +107,126 @@ async function migrate(databaseUrl) {
87
107
  }
88
108
  }
89
109
 
110
+ function saveToEnvFiles(envVars) {
111
+ const envFiles = [
112
+ ".env",
113
+ ".env.local",
114
+ ".env.development",
115
+ ".env.production",
116
+ ];
117
+ const cwd = process.cwd();
118
+ const updatedFiles = [];
119
+
120
+ for (const envFile of envFiles) {
121
+ const envPath = path.join(cwd, envFile);
122
+ if (fs.existsSync(envPath)) {
123
+ let content = fs.readFileSync(envPath, "utf8");
124
+
125
+ for (const { key, value } of envVars) {
126
+ const line = `${key}=${value}`;
127
+ const regex = new RegExp(`^${key}=.*`, "m");
128
+
129
+ if (regex.test(content)) {
130
+ // Replace existing value
131
+ content = content.replace(regex, line);
132
+ } else {
133
+ // Append to file
134
+ const newline = content.endsWith("\n") ? "" : "\n";
135
+ content = content + newline + line + "\n";
136
+ }
137
+ }
138
+
139
+ fs.writeFileSync(envPath, content);
140
+ updatedFiles.push(envFile);
141
+ }
142
+ }
143
+
144
+ return updatedFiles;
145
+ }
146
+
147
+ function getTemplatesDir() {
148
+ return path.join(__dirname, "..", "src", "templates");
149
+ }
150
+
151
+ function getAppRouterTemplate() {
152
+ const templatePath = path.join(getTemplatesDir(), "app-router.ts");
153
+ return fs.readFileSync(templatePath, "utf8");
154
+ }
155
+
156
+ function getPagesRouterTemplate() {
157
+ const templatePath = path.join(getTemplatesDir(), "pages-router.ts");
158
+ return fs.readFileSync(templatePath, "utf8");
159
+ }
160
+
161
+ function detectRouterType() {
162
+ const cwd = process.cwd();
163
+ const hasAppDir = fs.existsSync(path.join(cwd, "app"));
164
+ const hasPagesDir = fs.existsSync(path.join(cwd, "pages"));
165
+
166
+ // Also check for src/app and src/pages (common Next.js structure)
167
+ const hasSrcAppDir = fs.existsSync(path.join(cwd, "src", "app"));
168
+ const hasSrcPagesDir = fs.existsSync(path.join(cwd, "src", "pages"));
169
+
170
+ // Prefer App Router if app directory exists
171
+ if (hasAppDir || hasSrcAppDir) {
172
+ return { type: "app", useSrc: hasSrcAppDir && !hasAppDir };
173
+ }
174
+
175
+ if (hasPagesDir || hasSrcPagesDir) {
176
+ return { type: "pages", useSrc: hasSrcPagesDir && !hasPagesDir };
177
+ }
178
+
179
+ // Default to App Router if no directories found
180
+ return { type: "app", useSrc: false };
181
+ }
182
+
183
+ function createApiRoute(routerType, useSrc) {
184
+ const cwd = process.cwd();
185
+ const baseDir = useSrc ? path.join(cwd, "src") : cwd;
186
+
187
+ if (routerType === "app") {
188
+ // App Router: app/api/stripe/[...all]/route.ts
189
+ const routeDir = path.join(baseDir, "app", "api", "stripe", "[...all]");
190
+ const routeFile = path.join(routeDir, "route.ts");
191
+
192
+ // Create directories if they don't exist
193
+ fs.mkdirSync(routeDir, { recursive: true });
194
+
195
+ // Get template content (remove the comment with file path)
196
+ let template = getAppRouterTemplate();
197
+ template = template.replace(
198
+ /^\/\/ app\/api\/stripe\/\[\.\.\.all\]\/route\.ts\n/,
199
+ ""
200
+ );
201
+
202
+ // Write the file
203
+ fs.writeFileSync(routeFile, template);
204
+
205
+ const prefix = useSrc ? "src/" : "";
206
+ return `${prefix}app/api/stripe/[...all]/route.ts`;
207
+ } else {
208
+ // Pages Router: pages/api/stripe/[...all].ts
209
+ const routeDir = path.join(baseDir, "pages", "api", "stripe");
210
+ const routeFile = path.join(routeDir, "[...all].ts");
211
+
212
+ // Create directories if they don't exist
213
+ fs.mkdirSync(routeDir, { recursive: true });
214
+
215
+ // Get template content (remove the comment with file path)
216
+ let template = getPagesRouterTemplate();
217
+ template = template.replace(
218
+ /^\/\/ pages\/api\/stripe\/\[\.\.\.all\]\.ts\n/,
219
+ ""
220
+ );
221
+
222
+ // Write the file
223
+ fs.writeFileSync(routeFile, template);
224
+
225
+ const prefix = useSrc ? "src/" : "";
226
+ return `${prefix}pages/api/stripe/[...all].ts`;
227
+ }
228
+ }
229
+
90
230
  async function config() {
91
231
  let Stripe;
92
232
  try {
@@ -97,15 +237,20 @@ async function config() {
97
237
  process.exit(1);
98
238
  }
99
239
 
100
- const rl = createPrompt();
101
-
102
240
  console.log("\nšŸ”§ Stripe Webhook Configuration\n");
103
241
 
242
+ // Detect router type from folder structure
243
+ const { type: routerType, useSrc } = detectRouterType();
244
+ const routerLabel = routerType === "app" ? "App Router" : "Pages Router";
245
+ const srcLabel = useSrc ? " (src/)" : "";
246
+ console.log(`šŸ“‚ Detected: ${routerLabel}${srcLabel}\n`);
247
+
104
248
  // Get Stripe API key (hidden input)
105
- rl.close(); // Close readline for hidden input
249
+ const existingStripeKey = process.env.STRIPE_SECRET_KEY || "";
106
250
  const stripeSecretKey = await questionHidden(
107
251
  null,
108
- "Enter your Stripe Secret Key (sk_...)"
252
+ "Enter your Stripe Secret Key (sk_...)",
253
+ existingStripeKey
109
254
  );
110
255
 
111
256
  if (!stripeSecretKey || !stripeSecretKey.startsWith("sk_")) {
@@ -113,16 +258,16 @@ async function config() {
113
258
  process.exit(1);
114
259
  }
115
260
 
116
- // Reopen readline for remaining questions
117
- const rl2 = createPrompt();
261
+ // Create readline for site URL question
262
+ const rl = createPrompt();
118
263
 
119
264
  // Get site URL with default from env
120
265
  const defaultSiteUrl = process.env.NEXT_PUBLIC_SITE_URL || "";
121
- const siteUrl = await question(rl2, "Enter your site URL", defaultSiteUrl);
266
+ const siteUrl = await question(rl, "Enter your site URL", defaultSiteUrl);
122
267
 
123
268
  if (!siteUrl) {
124
269
  console.error("āŒ Site URL is required");
125
- rl2.close();
270
+ rl.close();
126
271
  process.exit(1);
127
272
  }
128
273
 
@@ -133,67 +278,105 @@ async function config() {
133
278
  webhookUrl = `${url.origin}/api/stripe/webhook`;
134
279
  } catch (e) {
135
280
  console.error("āŒ Invalid URL format");
136
- rl2.close();
281
+ rl.close();
137
282
  process.exit(1);
138
283
  }
139
284
 
140
- rl2.close();
285
+ // Get DATABASE_URL (optional) - skip if already set in env
286
+ let databaseUrlInput = "";
287
+ if (process.env.DATABASE_URL) {
288
+ console.log("āœ“ DATABASE_URL already set in environment");
289
+ databaseUrlInput = process.env.DATABASE_URL;
290
+ } else {
291
+ databaseUrlInput = await question(
292
+ rl,
293
+ "Enter your DATABASE_URL (optional, press Enter to skip)",
294
+ ""
295
+ );
296
+ }
141
297
 
142
- console.log(`\nšŸ“” Creating webhook endpoint: ${webhookUrl}\n`);
298
+ rl.close();
299
+
300
+ // Create the API route
301
+ console.log(`šŸ“ Creating API route...`);
302
+ try {
303
+ const createdFile = createApiRoute(routerType, useSrc);
304
+ console.log(`āœ… Created ${createdFile}`);
305
+ } catch (error) {
306
+ console.error("āŒ Failed to create API route:", error.message);
307
+ process.exit(1);
308
+ }
309
+
310
+ // Copy billing.config.ts to root
311
+ console.log(`šŸ“ Creating billing.config.ts...`);
312
+ try {
313
+ const billingConfigPath = path.join(process.cwd(), "billing.config.ts");
314
+ if (!fs.existsSync(billingConfigPath)) {
315
+ const templatePath = path.join(getTemplatesDir(), "billing.config.ts");
316
+ const template = fs.readFileSync(templatePath, "utf8");
317
+ fs.writeFileSync(billingConfigPath, template);
318
+ console.log(`āœ… Created billing.config.ts\n`);
319
+ } else {
320
+ console.log(`āœ“ billing.config.ts already exists\n`);
321
+ }
322
+ } catch (error) {
323
+ console.error("āŒ Failed to create billing.config.ts:", error.message);
324
+ process.exit(1);
325
+ }
326
+
327
+ console.log(`šŸ“” Creating webhook endpoint: ${webhookUrl}\n`);
143
328
 
144
329
  const stripe = new Stripe(stripeSecretKey);
145
330
 
146
331
  try {
147
- // Get all available event types
332
+ // Check if a webhook with the same URL already exists
333
+ const existingWebhooks = await stripe.webhookEndpoints.list({ limit: 100 });
334
+ const existingWebhook = existingWebhooks.data.find(
335
+ (wh) => wh.url === webhookUrl
336
+ );
337
+
338
+ if (existingWebhook) {
339
+ console.log(`šŸ”„ Found existing webhook with same URL, deleting it...`);
340
+ await stripe.webhookEndpoints.del(existingWebhook.id);
341
+ console.log(`āœ… Deleted existing webhook (${existingWebhook.id})\n`);
342
+ }
343
+
344
+ // Create webhook endpoint
345
+ console.log(`šŸ”„ Creating new webhook endpoint...`);
148
346
  const webhook = await stripe.webhookEndpoints.create({
149
347
  url: webhookUrl,
150
348
  enabled_events: ["*"], // Listen to all events
151
349
  description: "Created by stripe-no-webhooks CLI",
152
350
  });
153
-
154
351
  console.log("āœ… Webhook created successfully!\n");
155
352
 
156
- // Try to add webhook secret to env files
157
- const envFiles = [
158
- ".env",
159
- ".env.local",
160
- ".env.development",
161
- ".env.production",
353
+ // Build list of env vars to update
354
+ const envVars = [
355
+ { key: "STRIPE_SECRET_KEY", value: stripeSecretKey },
356
+ { key: "STRIPE_WEBHOOK_SECRET", value: webhook.secret },
357
+ { key: "NEXT_PUBLIC_SITE_URL", value: siteUrl },
162
358
  ];
163
- const cwd = process.cwd();
164
- const updatedFiles = [];
165
-
166
- for (const envFile of envFiles) {
167
- const envPath = path.join(cwd, envFile);
168
- if (fs.existsSync(envPath)) {
169
- let content = fs.readFileSync(envPath, "utf8");
170
- const secretLine = `STRIPE_WEBHOOK_SECRET=${webhook.secret}`;
171
-
172
- if (content.includes("STRIPE_WEBHOOK_SECRET=")) {
173
- // Replace existing value
174
- content = content.replace(/STRIPE_WEBHOOK_SECRET=.*/, secretLine);
175
- } else {
176
- // Append to file
177
- const newline = content.endsWith("\n") ? "" : "\n";
178
- content = content + newline + secretLine + "\n";
179
- }
180
-
181
- fs.writeFileSync(envPath, content);
182
- updatedFiles.push(envFile);
183
- }
359
+ if (databaseUrlInput) {
360
+ envVars.push({ key: "DATABASE_URL", value: databaseUrlInput });
184
361
  }
185
362
 
363
+ // Save to env files
364
+ const updatedFiles = saveToEnvFiles(envVars);
365
+
366
+ const envVarNames = envVars.map((v) => v.key).join(", ");
186
367
  if (updatedFiles.length > 0) {
368
+ console.log(`šŸ“ Updated ${updatedFiles.join(", ")} with ${envVarNames}`);
187
369
  console.log(
188
- `šŸ“ Updated ${updatedFiles.join(
189
- ", "
190
- )} with STRIPE_WEBHOOK_SECRET\nREMEMBER: Update the enviroment variable in Vercel too\nSTRIPE_WEBHOOK_SECRET=${
191
- webhook.secret
192
- }`
370
+ "\nREMEMBER: Update the environment variables in Vercel too:"
193
371
  );
372
+ for (const { key, value } of envVars) {
373
+ console.log(`${key}=${value}`);
374
+ }
194
375
  } else {
195
- console.log("Add this to your environment variables:\n");
196
- console.log(`STRIPE_WEBHOOK_SECRET=${webhook.secret}\n`);
376
+ console.log("Add these to your environment variables:\n");
377
+ for (const { key, value } of envVars) {
378
+ console.log(`${key}=${value}`);
379
+ }
197
380
  }
198
381
 
199
382
  console.log("─".repeat(50));
@@ -217,6 +400,298 @@ async function config() {
217
400
  }
218
401
  }
219
402
 
403
+ function findMatchingBrace(content, startIndex) {
404
+ // Find the matching closing brace for an opening brace
405
+ let depth = 0;
406
+ for (let i = startIndex; i < content.length; i++) {
407
+ if (content[i] === "{" || content[i] === "[") depth++;
408
+ else if (content[i] === "}" || content[i] === "]") {
409
+ depth--;
410
+ if (depth === 0) return i;
411
+ }
412
+ }
413
+ return -1;
414
+ }
415
+
416
+ function parseBillingConfig(content) {
417
+ // Extract the plans array from the file content
418
+ const plansStartMatch = content.match(/plans\s*:\s*\[/);
419
+ if (!plansStartMatch) {
420
+ return [];
421
+ }
422
+
423
+ const plansStart = plansStartMatch.index + plansStartMatch[0].length - 1;
424
+ const plansEnd = findMatchingBrace(content, plansStart);
425
+ if (plansEnd === -1) return [];
426
+
427
+ const plansContent = content.substring(plansStart + 1, plansEnd);
428
+ const plans = [];
429
+
430
+ // Find each plan object by looking for opening braces at the top level
431
+ let depth = 0;
432
+ let planStart = -1;
433
+
434
+ for (let i = 0; i < plansContent.length; i++) {
435
+ const char = plansContent[i];
436
+ if (char === "{") {
437
+ if (depth === 0) planStart = i;
438
+ depth++;
439
+ } else if (char === "}") {
440
+ depth--;
441
+ if (depth === 0 && planStart !== -1) {
442
+ const planRaw = plansContent.substring(planStart, i + 1);
443
+ const plan = parsePlanObject(planRaw);
444
+ if (plan.name) {
445
+ plans.push({
446
+ plan,
447
+ raw: planRaw,
448
+ startIndex:
449
+ plansStartMatch.index + plansStartMatch[0].length + planStart,
450
+ });
451
+ }
452
+ planStart = -1;
453
+ }
454
+ }
455
+ }
456
+
457
+ return plans;
458
+ }
459
+
460
+ function parsePlanObject(planContent) {
461
+ const plan = {};
462
+
463
+ // Extract id if present (product id)
464
+ const idMatch = planContent.match(/^\s*\{\s*id\s*:\s*["']([^"']+)["']/);
465
+ if (idMatch) plan.id = idMatch[1];
466
+
467
+ // Also try to find id not at start
468
+ if (!plan.id) {
469
+ const idMatch2 = planContent.match(/[,{]\s*id\s*:\s*["']([^"']+)["']/);
470
+ if (idMatch2) plan.id = idMatch2[1];
471
+ }
472
+
473
+ // Extract name
474
+ const nameMatch = planContent.match(/name\s*:\s*["']([^"']+)["']/);
475
+ if (nameMatch) plan.name = nameMatch[1];
476
+
477
+ // Extract description
478
+ const descMatch = planContent.match(/description\s*:\s*["']([^"']+)["']/);
479
+ if (descMatch) plan.description = descMatch[1];
480
+
481
+ // Extract price array
482
+ const priceStartMatch = planContent.match(/price\s*:\s*\[/);
483
+ if (priceStartMatch) {
484
+ const priceStart = priceStartMatch.index + priceStartMatch[0].length - 1;
485
+ const priceEnd = findMatchingBrace(planContent, priceStart);
486
+ if (priceEnd !== -1) {
487
+ const priceArrayContent = planContent.substring(priceStart + 1, priceEnd);
488
+ plan.prices = parsePriceArray(priceArrayContent);
489
+ plan.priceArrayStart = priceStart;
490
+ plan.priceArrayEnd = priceEnd;
491
+ }
492
+ }
493
+
494
+ return plan;
495
+ }
496
+
497
+ function parsePriceArray(priceArrayContent) {
498
+ const prices = [];
499
+ let depth = 0;
500
+ let priceStart = -1;
501
+
502
+ for (let i = 0; i < priceArrayContent.length; i++) {
503
+ const char = priceArrayContent[i];
504
+ if (char === "{") {
505
+ if (depth === 0) priceStart = i;
506
+ depth++;
507
+ } else if (char === "}") {
508
+ depth--;
509
+ if (depth === 0 && priceStart !== -1) {
510
+ const priceRaw = priceArrayContent.substring(priceStart, i + 1);
511
+ const price = parsePriceObject(priceRaw);
512
+ prices.push({ price, raw: priceRaw, localStart: priceStart });
513
+ priceStart = -1;
514
+ }
515
+ }
516
+ }
517
+
518
+ return prices;
519
+ }
520
+
521
+ function parsePriceObject(priceContent) {
522
+ const price = {};
523
+
524
+ // Extract id if present (price id)
525
+ const idMatch = priceContent.match(/id\s*:\s*["']([^"']+)["']/);
526
+ if (idMatch) price.id = idMatch[1];
527
+
528
+ // Extract amount
529
+ const amountMatch = priceContent.match(/amount\s*:\s*(\d+)/);
530
+ if (amountMatch) price.amount = parseInt(amountMatch[1], 10);
531
+
532
+ // Extract currency
533
+ const currencyMatch = priceContent.match(/currency\s*:\s*["']([^"']+)["']/);
534
+ if (currencyMatch) price.currency = currencyMatch[1];
535
+
536
+ // Extract interval
537
+ const intervalMatch = priceContent.match(/interval\s*:\s*["']([^"']+)["']/);
538
+ if (intervalMatch) price.interval = intervalMatch[1];
539
+
540
+ return price;
541
+ }
542
+
543
+ async function push() {
544
+ const billingConfigPath = path.join(process.cwd(), "billing.config.ts");
545
+
546
+ if (!fs.existsSync(billingConfigPath)) {
547
+ console.error("āŒ billing.config.ts not found in project root.");
548
+ console.log("Run 'npx stripe-no-webhooks config' first to create it.");
549
+ process.exit(1);
550
+ }
551
+
552
+ let Stripe;
553
+ try {
554
+ Stripe = require("stripe").default || require("stripe");
555
+ } catch (e) {
556
+ console.error("āŒ Stripe package not found.");
557
+ console.log("Please install it first: npm install stripe");
558
+ process.exit(1);
559
+ }
560
+
561
+ // Get Stripe API key from env or prompt
562
+ let stripeSecretKey = process.env.STRIPE_SECRET_KEY;
563
+ if (!stripeSecretKey) {
564
+ stripeSecretKey = await questionHidden(
565
+ null,
566
+ "Enter your Stripe Secret Key (sk_...)"
567
+ );
568
+ }
569
+
570
+ if (!stripeSecretKey || !stripeSecretKey.startsWith("sk_")) {
571
+ console.error("āŒ Invalid Stripe Secret Key. It should start with 'sk_'");
572
+ process.exit(1);
573
+ }
574
+
575
+ const stripe = new Stripe(stripeSecretKey);
576
+
577
+ console.log("\nšŸ“¤ Pushing billing plans to Stripe...\n");
578
+
579
+ let content = fs.readFileSync(billingConfigPath, "utf8");
580
+ const parsedPlans = parseBillingConfig(content);
581
+
582
+ if (parsedPlans.length === 0) {
583
+ console.log("No plans found in billing.config.ts");
584
+ console.log("Add plans to the config file and run this command again.");
585
+ process.exit(0);
586
+ }
587
+
588
+ let updatedContent = content;
589
+ let productsCreated = 0;
590
+ let pricesCreated = 0;
591
+ let skippedProducts = 0;
592
+ let skippedPrices = 0;
593
+
594
+ for (const { plan, raw } of parsedPlans) {
595
+ let productId = plan.id;
596
+ let updatedPlanRaw = raw;
597
+
598
+ // Create product if needed
599
+ if (!productId) {
600
+ try {
601
+ console.log(`šŸ”„ Creating product "${plan.name}" in Stripe...`);
602
+
603
+ const product = await stripe.products.create({
604
+ name: plan.name,
605
+ description: plan.description || undefined,
606
+ });
607
+
608
+ productId = product.id;
609
+ console.log(`āœ… Created product "${plan.name}" (${productId})`);
610
+
611
+ // Add product id to plan
612
+ updatedPlanRaw = updatedPlanRaw.replace(
613
+ /\{/,
614
+ `{\n id: "${productId}",`
615
+ );
616
+ productsCreated++;
617
+ } catch (error) {
618
+ console.error(
619
+ `āŒ Failed to create product "${plan.name}":`,
620
+ error.message
621
+ );
622
+ continue;
623
+ }
624
+ } else {
625
+ console.log(`ā­ļø Product "${plan.name}" already exists (${productId})`);
626
+ skippedProducts++;
627
+ }
628
+
629
+ // Create prices if needed
630
+ if (plan.prices && plan.prices.length > 0) {
631
+ for (const { price, raw: priceRaw } of plan.prices) {
632
+ if (price.id) {
633
+ console.log(
634
+ ` ā­ļø Price ${price.interval}/${price.currency} already exists (${price.id})`
635
+ );
636
+ skippedPrices++;
637
+ continue;
638
+ }
639
+
640
+ try {
641
+ console.log(
642
+ ` šŸ”„ Creating price ${price.amount / 100} ${price.currency}/${
643
+ price.interval
644
+ }...`
645
+ );
646
+
647
+ const stripePrice = await stripe.prices.create({
648
+ product: productId,
649
+ unit_amount: price.amount,
650
+ currency: price.currency.toLowerCase(),
651
+ recurring: {
652
+ interval: price.interval,
653
+ },
654
+ });
655
+
656
+ console.log(` āœ… Created price (${stripePrice.id})`);
657
+
658
+ // Add price id to price object
659
+ const updatedPriceRaw = priceRaw.replace(
660
+ /\{/,
661
+ `{\n id: "${stripePrice.id}",`
662
+ );
663
+ updatedPlanRaw = updatedPlanRaw.replace(priceRaw, updatedPriceRaw);
664
+ pricesCreated++;
665
+ } catch (error) {
666
+ console.error(
667
+ ` āŒ Failed to create price ${price.interval}/${price.currency}:`,
668
+ error.message
669
+ );
670
+ }
671
+ }
672
+ }
673
+
674
+ // Update content with modified plan
675
+ if (updatedPlanRaw !== raw) {
676
+ updatedContent = updatedContent.replace(raw, updatedPlanRaw);
677
+ }
678
+ }
679
+
680
+ // Write updated content back to file
681
+ if (productsCreated > 0 || pricesCreated > 0) {
682
+ fs.writeFileSync(billingConfigPath, updatedContent);
683
+ console.log(
684
+ `\nšŸ“ Updated billing.config.ts with ${productsCreated} product(s) and ${pricesCreated} price(s)`
685
+ );
686
+ }
687
+
688
+ console.log(`\nāœ… Done!`);
689
+ console.log(
690
+ ` Products: ${productsCreated} created, ${skippedProducts} skipped`
691
+ );
692
+ console.log(` Prices: ${pricesCreated} created, ${skippedPrices} skipped`);
693
+ }
694
+
220
695
  async function main() {
221
696
  switch (command) {
222
697
  case "migrate":
@@ -227,10 +702,15 @@ async function main() {
227
702
  await config();
228
703
  break;
229
704
 
705
+ case "push":
706
+ await push();
707
+ break;
708
+
230
709
  default:
231
710
  console.log("Usage:");
232
711
  console.log(" npx stripe-no-webhooks migrate <connection_string>");
233
712
  console.log(" npx stripe-no-webhooks config");
713
+ console.log(" npx stripe-no-webhooks push");
234
714
  process.exit(1);
235
715
  }
236
716
  }