@codaijs/keel 0.2.3 → 0.2.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.
Files changed (80) hide show
  1. package/dist/__tests__/sail-installer.test.js +25 -25
  2. package/dist/sail-installer.js +174 -174
  3. package/dist/scaffold.js +68 -68
  4. package/package.json +58 -58
  5. package/sails/_template/addon.json +20 -20
  6. package/sails/_template/install.ts +402 -402
  7. package/sails/admin-dashboard/README.md +117 -117
  8. package/sails/admin-dashboard/addon.json +28 -28
  9. package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -34
  10. package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -243
  11. package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -40
  12. package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -240
  13. package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -149
  14. package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -173
  15. package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -203
  16. package/sails/admin-dashboard/install.ts +305 -305
  17. package/sails/analytics/README.md +178 -178
  18. package/sails/analytics/addon.json +27 -27
  19. package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -58
  20. package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -64
  21. package/sails/analytics/files/frontend/lib/analytics.ts +103 -103
  22. package/sails/analytics/install.ts +297 -297
  23. package/sails/file-uploads/addon.json +30 -30
  24. package/sails/file-uploads/files/backend/routes/files.ts +198 -198
  25. package/sails/file-uploads/files/backend/schema/files.ts +36 -36
  26. package/sails/file-uploads/files/backend/services/file-storage.ts +128 -128
  27. package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -248
  28. package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -147
  29. package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -106
  30. package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -118
  31. package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -37
  32. package/sails/file-uploads/install.ts +466 -466
  33. package/sails/gdpr/README.md +174 -174
  34. package/sails/gdpr/addon.json +27 -27
  35. package/sails/gdpr/files/backend/routes/gdpr.ts +140 -140
  36. package/sails/gdpr/files/backend/services/gdpr.ts +293 -293
  37. package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -97
  38. package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -192
  39. package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -75
  40. package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -186
  41. package/sails/gdpr/install.ts +756 -756
  42. package/sails/google-oauth/README.md +121 -121
  43. package/sails/google-oauth/addon.json +22 -22
  44. package/sails/google-oauth/files/GoogleButton.tsx +50 -50
  45. package/sails/google-oauth/install.ts +252 -252
  46. package/sails/i18n/README.md +193 -193
  47. package/sails/i18n/addon.json +30 -30
  48. package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -108
  49. package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -31
  50. package/sails/i18n/files/frontend/lib/i18n.ts +32 -32
  51. package/sails/i18n/files/frontend/locales/de/common.json +44 -44
  52. package/sails/i18n/files/frontend/locales/en/common.json +44 -44
  53. package/sails/i18n/install.ts +407 -407
  54. package/sails/push-notifications/README.md +163 -163
  55. package/sails/push-notifications/addon.json +31 -31
  56. package/sails/push-notifications/files/backend/routes/notifications.ts +153 -153
  57. package/sails/push-notifications/files/backend/schema/notifications.ts +31 -31
  58. package/sails/push-notifications/files/backend/services/notifications.ts +117 -117
  59. package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -12
  60. package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -154
  61. package/sails/push-notifications/install.ts +384 -384
  62. package/sails/r2-storage/addon.json +29 -29
  63. package/sails/r2-storage/files/backend/services/storage.ts +71 -71
  64. package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -167
  65. package/sails/r2-storage/install.ts +412 -412
  66. package/sails/rate-limiting/addon.json +20 -20
  67. package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -104
  68. package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -137
  69. package/sails/rate-limiting/install.ts +300 -300
  70. package/sails/registry.json +107 -107
  71. package/sails/stripe/README.md +214 -214
  72. package/sails/stripe/addon.json +24 -24
  73. package/sails/stripe/files/backend/routes/stripe.ts +154 -154
  74. package/sails/stripe/files/backend/schema/stripe.ts +74 -74
  75. package/sails/stripe/files/backend/services/stripe.ts +224 -224
  76. package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -135
  77. package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -86
  78. package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -116
  79. package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -226
  80. package/sails/stripe/install.ts +378 -378
@@ -1,378 +1,378 @@
1
- /**
2
- * Stripe Payments Sail Installer
3
- *
4
- * Adds Stripe subscription management with checkout sessions, webhooks,
5
- * customer portal, and subscription status tracking.
6
- * Features a full interactive setup wizard.
7
- *
8
- * Usage:
9
- * npx tsx sails/stripe/install.ts
10
- */
11
-
12
- import {
13
- readFileSync,
14
- writeFileSync,
15
- copyFileSync,
16
- existsSync,
17
- mkdirSync,
18
- } from "node:fs";
19
- import { resolve, dirname, join } from "node:path";
20
- import { execSync } from "node:child_process";
21
- import { input, confirm } from "@inquirer/prompts";
22
-
23
- // ---------------------------------------------------------------------------
24
- // Paths
25
- // ---------------------------------------------------------------------------
26
-
27
- const SAIL_DIR = dirname(new URL(import.meta.url).pathname);
28
- const PROJECT_ROOT = resolve(SAIL_DIR, "../..");
29
- const BACKEND_ROOT = join(PROJECT_ROOT, "packages/backend");
30
- const FRONTEND_ROOT = join(PROJECT_ROOT, "packages/frontend");
31
-
32
- // ---------------------------------------------------------------------------
33
- // Helpers
34
- // ---------------------------------------------------------------------------
35
-
36
- interface SailManifest {
37
- name: string;
38
- displayName: string;
39
- version: string;
40
- requiredEnvVars: { key: string; description: string }[];
41
- dependencies: { backend: Record<string, string>; frontend: Record<string, string> };
42
- }
43
-
44
- function loadManifest(): SailManifest {
45
- return JSON.parse(readFileSync(join(SAIL_DIR, "addon.json"), "utf-8"));
46
- }
47
-
48
- function insertAtMarker(filePath: string, marker: string, code: string): void {
49
- if (!existsSync(filePath)) {
50
- console.warn(` Warning: File not found: ${filePath}`);
51
- return;
52
- }
53
- let content = readFileSync(filePath, "utf-8");
54
- if (!content.includes(marker)) {
55
- console.warn(` Warning: Marker "${marker}" not found in ${filePath}`);
56
- return;
57
- }
58
- if (content.includes(code.trim())) {
59
- console.log(` Skipped (already present) -> ${filePath}`);
60
- return;
61
- }
62
- content = content.replace(marker, `${marker}\n${code}`);
63
- writeFileSync(filePath, content, "utf-8");
64
- console.log(` Modified -> ${filePath}`);
65
- }
66
-
67
- function copyFile(src: string, dest: string, label: string): void {
68
- mkdirSync(dirname(dest), { recursive: true });
69
- copyFileSync(src, dest);
70
- console.log(` Copied -> ${label}`);
71
- }
72
-
73
- function appendToEnvExample(entries: Record<string, string>): void {
74
- const envPath = join(PROJECT_ROOT, ".env.example");
75
- if (!existsSync(envPath)) return;
76
- let content = readFileSync(envPath, "utf-8");
77
- const lines: string[] = [];
78
- for (const [key, val] of Object.entries(entries)) {
79
- if (!content.includes(key)) lines.push(`${key}=${val}`);
80
- }
81
- if (lines.length > 0) {
82
- content += `\n# Stripe Payments\n${lines.join("\n")}\n`;
83
- writeFileSync(envPath, content, "utf-8");
84
- }
85
- }
86
-
87
- function installDeps(deps: Record<string, string>, workspace: string): void {
88
- const entries = Object.entries(deps);
89
- if (entries.length === 0) return;
90
- const packages = entries.map(([n, v]) => `${n}@${v}`).join(" ");
91
- const cmd = `npm install ${packages} --workspace=${workspace}`;
92
- console.log(` Running: ${cmd}`);
93
- execSync(cmd, { cwd: PROJECT_ROOT, stdio: "inherit" });
94
- }
95
-
96
- // ---------------------------------------------------------------------------
97
- // Main
98
- // ---------------------------------------------------------------------------
99
-
100
- async function main(): Promise<void> {
101
- const manifest = loadManifest();
102
-
103
- // -- Step 1: Welcome message -------------------------------------------------
104
- console.log("\n------------------------------------------------------------");
105
- console.log(` Stripe Payments Sail Installer (v${manifest.version})`);
106
- console.log("------------------------------------------------------------");
107
- console.log();
108
- console.log(" This sail integrates Stripe into your project, providing:");
109
- console.log(" - Subscription management with checkout sessions");
110
- console.log(" - Webhook handling for payment events");
111
- console.log(" - Customer portal for self-service billing");
112
- console.log(" - Subscription status tracking in the database");
113
- console.log(" - Pricing page and checkout flow components");
114
- console.log();
115
-
116
- const pkgPath = join(PROJECT_ROOT, "package.json");
117
- if (existsSync(pkgPath)) {
118
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
119
- console.log(` Template version: ${pkg.version ?? "unknown"}`);
120
- console.log();
121
- }
122
-
123
- // -- Step 2: Stripe account check --------------------------------------------
124
- const hasAccount = await confirm({
125
- message: "Do you already have a Stripe account?",
126
- default: true,
127
- });
128
-
129
- if (!hasAccount) {
130
- console.log();
131
- console.log(" Create a Stripe account at:");
132
- console.log(" https://dashboard.stripe.com/register");
133
- console.log();
134
- console.log(" Stripe offers a generous test mode so you can develop");
135
- console.log(" without processing real payments.");
136
- console.log();
137
-
138
- await confirm({
139
- message: "I have created my Stripe account and am ready to continue",
140
- default: false,
141
- });
142
- }
143
-
144
- // -- Step 3: API keys -------------------------------------------------------
145
- console.log();
146
- console.log(" Get your API keys from:");
147
- console.log(" https://dashboard.stripe.com/test/apikeys");
148
- console.log();
149
-
150
- const stripeSecretKey = await input({
151
- message: "Stripe Secret Key:",
152
- validate: (value) => {
153
- if (!value || value.trim().length === 0) return "Secret Key is required.";
154
- if (!value.startsWith("sk_")) return "Secret Key should start with 'sk_' (e.g., sk_test_...).";
155
- return true;
156
- },
157
- });
158
-
159
- const stripePublishableKey = await input({
160
- message: "Stripe Publishable Key:",
161
- validate: (value) => {
162
- if (!value || value.trim().length === 0) return "Publishable Key is required.";
163
- if (!value.startsWith("pk_")) return "Publishable Key should start with 'pk_' (e.g., pk_test_...).";
164
- return true;
165
- },
166
- });
167
-
168
- // -- Step 4: Webhook setup ---------------------------------------------------
169
- console.log();
170
- console.log(" Now set up your webhook endpoint:");
171
- console.log();
172
- console.log(" 1. Go to https://dashboard.stripe.com/test/webhooks");
173
- console.log(' 2. Click "Add endpoint"');
174
- console.log(" 3. Set the URL to:");
175
- console.log(" {BACKEND_URL}/api/stripe/webhook");
176
- console.log(" For local development:");
177
- console.log(" http://localhost:3000/api/stripe/webhook");
178
- console.log(" 4. Select these events:");
179
- console.log(" - checkout.session.completed");
180
- console.log(" - customer.subscription.updated");
181
- console.log(" - customer.subscription.deleted");
182
- console.log(" 5. Copy the signing secret (starts with whsec_)");
183
- console.log();
184
-
185
- const stripeWebhookSecret = await input({
186
- message: "Stripe Webhook Secret:",
187
- validate: (value) => {
188
- if (!value || value.trim().length === 0) return "Webhook Secret is required.";
189
- if (!value.startsWith("whsec_")) return "Webhook Secret should start with 'whsec_'.";
190
- return true;
191
- },
192
- });
193
-
194
- // -- Step 5: Product setup guidance ------------------------------------------
195
- console.log();
196
- const wantProducts = await confirm({
197
- message: "Would you like guidance on creating test products now?",
198
- default: false,
199
- });
200
-
201
- if (wantProducts) {
202
- console.log();
203
- console.log(" Create test products in the Stripe Dashboard:");
204
- console.log();
205
- console.log(" 1. Go to https://dashboard.stripe.com/test/products");
206
- console.log(' 2. Click "Add product"');
207
- console.log(" 3. Set a name (e.g., \"Pro Plan\") and description");
208
- console.log(' 4. Under pricing, select "Recurring"');
209
- console.log(" 5. Set the price (e.g., $19/month)");
210
- console.log(' 6. Click "Save product"');
211
- console.log(" 7. Copy the Price ID (price_...) for use in your Pricing.tsx");
212
- console.log();
213
- console.log(" Repeat for each plan (e.g., Basic, Pro, Enterprise).");
214
- console.log();
215
-
216
- await confirm({ message: "I have finished setting up products (or will do it later)", default: true });
217
- }
218
-
219
- // -- Step 6: Summary --------------------------------------------------------
220
- console.log();
221
- console.log(" Summary of changes:");
222
- console.log(" -------------------");
223
- console.log(" Files to copy (backend):");
224
- console.log(" + packages/backend/src/db/schema/stripe.ts");
225
- console.log(" + packages/backend/src/routes/stripe.ts");
226
- console.log(" + packages/backend/src/services/stripe.ts");
227
- console.log();
228
- console.log(" Files to copy (frontend):");
229
- console.log(" + packages/frontend/src/pages/Pricing.tsx");
230
- console.log(" + packages/frontend/src/pages/Checkout.tsx");
231
- console.log(" + packages/frontend/src/components/stripe/SubscriptionStatus.tsx");
232
- console.log(" + packages/frontend/src/hooks/useSubscription.ts");
233
- console.log();
234
- console.log(" Files to modify:");
235
- console.log(" ~ packages/backend/src/db/schema/index.ts");
236
- console.log(" ~ packages/backend/src/index.ts");
237
- console.log(" ~ packages/backend/src/env.ts");
238
- console.log(" ~ packages/frontend/src/router.tsx");
239
- console.log(" ~ .env.example / .env");
240
- console.log();
241
- console.log(" Environment variables:");
242
- console.log(` STRIPE_SECRET_KEY=${stripeSecretKey.slice(0, 12)}...`);
243
- console.log(` STRIPE_PUBLISHABLE_KEY=${stripePublishableKey.slice(0, 12)}...`);
244
- console.log(` STRIPE_WEBHOOK_SECRET=${stripeWebhookSecret.slice(0, 12)}...`);
245
- console.log();
246
-
247
- // -- Step 7: Confirm and execute ---------------------------------------------
248
- const proceed = await confirm({ message: "Proceed with installation?", default: true });
249
- if (!proceed) { console.log("\n Installation cancelled.\n"); process.exit(0); }
250
-
251
- console.log();
252
- console.log(" Installing...");
253
- console.log();
254
-
255
- console.log(" Copying backend files...");
256
- const backendFiles = [
257
- { src: "backend/schema/stripe.ts", dest: "src/db/schema/stripe.ts" },
258
- { src: "backend/routes/stripe.ts", dest: "src/routes/stripe.ts" },
259
- { src: "backend/services/stripe.ts", dest: "src/services/stripe.ts" },
260
- ];
261
- for (const f of backendFiles) {
262
- copyFile(join(SAIL_DIR, "files", f.src), join(BACKEND_ROOT, f.dest), f.dest);
263
- }
264
-
265
- console.log();
266
- console.log(" Copying frontend files...");
267
- const frontendFiles = [
268
- { src: "frontend/pages/Pricing.tsx", dest: "src/pages/Pricing.tsx" },
269
- { src: "frontend/pages/Checkout.tsx", dest: "src/pages/Checkout.tsx" },
270
- { src: "frontend/components/SubscriptionStatus.tsx", dest: "src/components/stripe/SubscriptionStatus.tsx" },
271
- { src: "frontend/hooks/useSubscription.ts", dest: "src/hooks/useSubscription.ts" },
272
- ];
273
- for (const f of frontendFiles) {
274
- copyFile(join(SAIL_DIR, "files", f.src), join(FRONTEND_ROOT, f.dest), f.dest);
275
- }
276
-
277
- console.log();
278
- console.log(" Modifying backend files...");
279
-
280
- insertAtMarker(join(BACKEND_ROOT, "src/db/schema/index.ts"), "// [SAIL_SCHEMA]", 'export * from "./stripe";');
281
- insertAtMarker(join(BACKEND_ROOT, "src/index.ts"), "// [SAIL_IMPORTS]", 'import { stripeRouter } from "./routes/stripe";');
282
- insertAtMarker(join(BACKEND_ROOT, "src/index.ts"), "// [SAIL_ROUTES]", 'app.use("/api/stripe", stripeRouter);');
283
-
284
- // Stripe webhook needs the raw body — insert express.raw() BEFORE express.json()
285
- const indexPath = join(BACKEND_ROOT, "src/index.ts");
286
- if (existsSync(indexPath)) {
287
- let indexContent = readFileSync(indexPath, "utf-8");
288
- const rawMiddleware = 'app.use("/api/stripe/webhook", express.raw({ type: "application/json" }));';
289
- if (!indexContent.includes(rawMiddleware)) {
290
- indexContent = indexContent.replace(
291
- "app.use(express.json());",
292
- `// Raw body for Stripe webhook signature verification (must be before express.json())\n${rawMiddleware}\n\napp.use(express.json());`,
293
- );
294
- writeFileSync(indexPath, indexContent, "utf-8");
295
- console.log(" Modified -> src/index.ts (added raw body middleware for webhook)");
296
- }
297
- }
298
-
299
- insertAtMarker(
300
- join(BACKEND_ROOT, "src/env.ts"),
301
- "// [SAIL_ENV_VARS]",
302
- ` STRIPE_SECRET_KEY: z.string().min(1, "STRIPE_SECRET_KEY is required"),\n STRIPE_PUBLISHABLE_KEY: z.string().min(1, "STRIPE_PUBLISHABLE_KEY is required"),\n STRIPE_WEBHOOK_SECRET: z.string().min(1, "STRIPE_WEBHOOK_SECRET is required"),`
303
- );
304
-
305
- console.log();
306
- console.log(" Modifying frontend files...");
307
-
308
- const routerPath = join(FRONTEND_ROOT, "src/router.tsx");
309
- insertAtMarker(routerPath, "// [SAIL_IMPORTS]", `import { PricingPage } from "./pages/Pricing";\nimport { CheckoutPage } from "./pages/Checkout";`);
310
- insertAtMarker(
311
- routerPath,
312
- "{/* [SAIL_ROUTES] */}",
313
- ` {\n path: "/pricing",\n element: <PricingPage />,\n },\n {\n path: "/checkout/success",\n element: <CheckoutPage status="success" />,\n },\n {\n path: "/checkout/cancel",\n element: <CheckoutPage status="cancel" />,\n },`
314
- );
315
-
316
- console.log();
317
- console.log(" Installing dependencies...");
318
- installDeps(manifest.dependencies.backend, "packages/backend");
319
- installDeps(manifest.dependencies.frontend, "packages/frontend");
320
-
321
- console.log();
322
- console.log(" Generating database migrations...");
323
- try {
324
- execSync("npx drizzle-kit generate", { cwd: BACKEND_ROOT, stdio: "inherit" });
325
- } catch {
326
- console.warn(" Warning: Could not generate migrations. Run manually: cd packages/backend && npx drizzle-kit generate");
327
- }
328
-
329
- console.log();
330
- console.log(" Updating environment files...");
331
- appendToEnvExample({ STRIPE_SECRET_KEY: stripeSecretKey, STRIPE_PUBLISHABLE_KEY: stripePublishableKey, STRIPE_WEBHOOK_SECRET: stripeWebhookSecret });
332
-
333
- const dotEnvPath = join(PROJECT_ROOT, ".env");
334
- if (existsSync(dotEnvPath)) {
335
- let dotEnv = readFileSync(dotEnvPath, "utf-8");
336
- if (!dotEnv.includes("STRIPE_SECRET_KEY")) {
337
- dotEnv += `\n# Stripe Payments\nSTRIPE_SECRET_KEY=${stripeSecretKey}\nSTRIPE_PUBLISHABLE_KEY=${stripePublishableKey}\nSTRIPE_WEBHOOK_SECRET=${stripeWebhookSecret}\n`;
338
- writeFileSync(dotEnvPath, dotEnv, "utf-8");
339
- console.log(" Updated .env");
340
- }
341
- }
342
-
343
- // -- Step 8: Print test instructions -----------------------------------------
344
- console.log();
345
- console.log("------------------------------------------------------------");
346
- console.log(" Stripe Payments installed successfully!");
347
- console.log("------------------------------------------------------------");
348
- console.log();
349
- console.log(" Next steps:");
350
- console.log();
351
- console.log(" 1. Run database migrations:");
352
- console.log(" npm run db:migrate");
353
- console.log();
354
- console.log(" 2. Create products in Stripe Dashboard:");
355
- console.log(" https://dashboard.stripe.com/test/products");
356
- console.log(" Copy the price IDs (price_...) into your Pricing.tsx");
357
- console.log();
358
- console.log(" 3. Start your dev server:");
359
- console.log(" npm run dev");
360
- console.log();
361
- console.log(" Testing locally with the Stripe CLI:");
362
- console.log(" Install: brew install stripe/stripe-cli/stripe");
363
- console.log(" Login: stripe login");
364
- console.log(" Listen: stripe listen --forward-to localhost:3000/api/stripe/webhook");
365
- console.log();
366
- console.log(" Test card numbers:");
367
- console.log(" Success: 4242 4242 4242 4242");
368
- console.log(" Requires auth: 4000 0025 0000 3155");
369
- console.log(" Declined: 4000 0000 0000 9995");
370
- console.log(" Use any future expiry date, any CVC, and any postal code.");
371
- console.log();
372
- console.log(" Webhook testing:");
373
- console.log(" Trigger events: stripe trigger checkout.session.completed");
374
- console.log(" View events: stripe events list");
375
- console.log();
376
- }
377
-
378
- main().catch((err) => { console.error("Installation failed:", err); process.exit(1); });
1
+ /**
2
+ * Stripe Payments Sail Installer
3
+ *
4
+ * Adds Stripe subscription management with checkout sessions, webhooks,
5
+ * customer portal, and subscription status tracking.
6
+ * Features a full interactive setup wizard.
7
+ *
8
+ * Usage:
9
+ * npx tsx sails/stripe/install.ts
10
+ */
11
+
12
+ import {
13
+ readFileSync,
14
+ writeFileSync,
15
+ copyFileSync,
16
+ existsSync,
17
+ mkdirSync,
18
+ } from "node:fs";
19
+ import { resolve, dirname, join } from "node:path";
20
+ import { execSync } from "node:child_process";
21
+ import { input, confirm } from "@inquirer/prompts";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Paths
25
+ // ---------------------------------------------------------------------------
26
+
27
+ const SAIL_DIR = dirname(new URL(import.meta.url).pathname);
28
+ const PROJECT_ROOT = resolve(SAIL_DIR, "../..");
29
+ const BACKEND_ROOT = join(PROJECT_ROOT, "packages/backend");
30
+ const FRONTEND_ROOT = join(PROJECT_ROOT, "packages/frontend");
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Helpers
34
+ // ---------------------------------------------------------------------------
35
+
36
+ interface SailManifest {
37
+ name: string;
38
+ displayName: string;
39
+ version: string;
40
+ requiredEnvVars: { key: string; description: string }[];
41
+ dependencies: { backend: Record<string, string>; frontend: Record<string, string> };
42
+ }
43
+
44
+ function loadManifest(): SailManifest {
45
+ return JSON.parse(readFileSync(join(SAIL_DIR, "addon.json"), "utf-8"));
46
+ }
47
+
48
+ function insertAtMarker(filePath: string, marker: string, code: string): void {
49
+ if (!existsSync(filePath)) {
50
+ console.warn(` Warning: File not found: ${filePath}`);
51
+ return;
52
+ }
53
+ let content = readFileSync(filePath, "utf-8");
54
+ if (!content.includes(marker)) {
55
+ console.warn(` Warning: Marker "${marker}" not found in ${filePath}`);
56
+ return;
57
+ }
58
+ if (content.includes(code.trim())) {
59
+ console.log(` Skipped (already present) -> ${filePath}`);
60
+ return;
61
+ }
62
+ content = content.replace(marker, `${marker}\n${code}`);
63
+ writeFileSync(filePath, content, "utf-8");
64
+ console.log(` Modified -> ${filePath}`);
65
+ }
66
+
67
+ function copyFile(src: string, dest: string, label: string): void {
68
+ mkdirSync(dirname(dest), { recursive: true });
69
+ copyFileSync(src, dest);
70
+ console.log(` Copied -> ${label}`);
71
+ }
72
+
73
+ function appendToEnvExample(entries: Record<string, string>): void {
74
+ const envPath = join(PROJECT_ROOT, ".env.example");
75
+ if (!existsSync(envPath)) return;
76
+ let content = readFileSync(envPath, "utf-8");
77
+ const lines: string[] = [];
78
+ for (const [key, val] of Object.entries(entries)) {
79
+ if (!content.includes(key)) lines.push(`${key}=${val}`);
80
+ }
81
+ if (lines.length > 0) {
82
+ content += `\n# Stripe Payments\n${lines.join("\n")}\n`;
83
+ writeFileSync(envPath, content, "utf-8");
84
+ }
85
+ }
86
+
87
+ function installDeps(deps: Record<string, string>, workspace: string): void {
88
+ const entries = Object.entries(deps);
89
+ if (entries.length === 0) return;
90
+ const packages = entries.map(([n, v]) => `${n}@${v}`).join(" ");
91
+ const cmd = `npm install ${packages} --workspace=${workspace}`;
92
+ console.log(` Running: ${cmd}`);
93
+ execSync(cmd, { cwd: PROJECT_ROOT, stdio: "inherit" });
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Main
98
+ // ---------------------------------------------------------------------------
99
+
100
+ async function main(): Promise<void> {
101
+ const manifest = loadManifest();
102
+
103
+ // -- Step 1: Welcome message -------------------------------------------------
104
+ console.log("\n------------------------------------------------------------");
105
+ console.log(` Stripe Payments Sail Installer (v${manifest.version})`);
106
+ console.log("------------------------------------------------------------");
107
+ console.log();
108
+ console.log(" This sail integrates Stripe into your project, providing:");
109
+ console.log(" - Subscription management with checkout sessions");
110
+ console.log(" - Webhook handling for payment events");
111
+ console.log(" - Customer portal for self-service billing");
112
+ console.log(" - Subscription status tracking in the database");
113
+ console.log(" - Pricing page and checkout flow components");
114
+ console.log();
115
+
116
+ const pkgPath = join(PROJECT_ROOT, "package.json");
117
+ if (existsSync(pkgPath)) {
118
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
119
+ console.log(` Template version: ${pkg.version ?? "unknown"}`);
120
+ console.log();
121
+ }
122
+
123
+ // -- Step 2: Stripe account check --------------------------------------------
124
+ const hasAccount = await confirm({
125
+ message: "Do you already have a Stripe account?",
126
+ default: true,
127
+ });
128
+
129
+ if (!hasAccount) {
130
+ console.log();
131
+ console.log(" Create a Stripe account at:");
132
+ console.log(" https://dashboard.stripe.com/register");
133
+ console.log();
134
+ console.log(" Stripe offers a generous test mode so you can develop");
135
+ console.log(" without processing real payments.");
136
+ console.log();
137
+
138
+ await confirm({
139
+ message: "I have created my Stripe account and am ready to continue",
140
+ default: false,
141
+ });
142
+ }
143
+
144
+ // -- Step 3: API keys -------------------------------------------------------
145
+ console.log();
146
+ console.log(" Get your API keys from:");
147
+ console.log(" https://dashboard.stripe.com/test/apikeys");
148
+ console.log();
149
+
150
+ const stripeSecretKey = await input({
151
+ message: "Stripe Secret Key:",
152
+ validate: (value) => {
153
+ if (!value || value.trim().length === 0) return "Secret Key is required.";
154
+ if (!value.startsWith("sk_")) return "Secret Key should start with 'sk_' (e.g., sk_test_...).";
155
+ return true;
156
+ },
157
+ });
158
+
159
+ const stripePublishableKey = await input({
160
+ message: "Stripe Publishable Key:",
161
+ validate: (value) => {
162
+ if (!value || value.trim().length === 0) return "Publishable Key is required.";
163
+ if (!value.startsWith("pk_")) return "Publishable Key should start with 'pk_' (e.g., pk_test_...).";
164
+ return true;
165
+ },
166
+ });
167
+
168
+ // -- Step 4: Webhook setup ---------------------------------------------------
169
+ console.log();
170
+ console.log(" Now set up your webhook endpoint:");
171
+ console.log();
172
+ console.log(" 1. Go to https://dashboard.stripe.com/test/webhooks");
173
+ console.log(' 2. Click "Add endpoint"');
174
+ console.log(" 3. Set the URL to:");
175
+ console.log(" {BACKEND_URL}/api/stripe/webhook");
176
+ console.log(" For local development:");
177
+ console.log(" http://localhost:3000/api/stripe/webhook");
178
+ console.log(" 4. Select these events:");
179
+ console.log(" - checkout.session.completed");
180
+ console.log(" - customer.subscription.updated");
181
+ console.log(" - customer.subscription.deleted");
182
+ console.log(" 5. Copy the signing secret (starts with whsec_)");
183
+ console.log();
184
+
185
+ const stripeWebhookSecret = await input({
186
+ message: "Stripe Webhook Secret:",
187
+ validate: (value) => {
188
+ if (!value || value.trim().length === 0) return "Webhook Secret is required.";
189
+ if (!value.startsWith("whsec_")) return "Webhook Secret should start with 'whsec_'.";
190
+ return true;
191
+ },
192
+ });
193
+
194
+ // -- Step 5: Product setup guidance ------------------------------------------
195
+ console.log();
196
+ const wantProducts = await confirm({
197
+ message: "Would you like guidance on creating test products now?",
198
+ default: false,
199
+ });
200
+
201
+ if (wantProducts) {
202
+ console.log();
203
+ console.log(" Create test products in the Stripe Dashboard:");
204
+ console.log();
205
+ console.log(" 1. Go to https://dashboard.stripe.com/test/products");
206
+ console.log(' 2. Click "Add product"');
207
+ console.log(" 3. Set a name (e.g., \"Pro Plan\") and description");
208
+ console.log(' 4. Under pricing, select "Recurring"');
209
+ console.log(" 5. Set the price (e.g., $19/month)");
210
+ console.log(' 6. Click "Save product"');
211
+ console.log(" 7. Copy the Price ID (price_...) for use in your Pricing.tsx");
212
+ console.log();
213
+ console.log(" Repeat for each plan (e.g., Basic, Pro, Enterprise).");
214
+ console.log();
215
+
216
+ await confirm({ message: "I have finished setting up products (or will do it later)", default: true });
217
+ }
218
+
219
+ // -- Step 6: Summary --------------------------------------------------------
220
+ console.log();
221
+ console.log(" Summary of changes:");
222
+ console.log(" -------------------");
223
+ console.log(" Files to copy (backend):");
224
+ console.log(" + packages/backend/src/db/schema/stripe.ts");
225
+ console.log(" + packages/backend/src/routes/stripe.ts");
226
+ console.log(" + packages/backend/src/services/stripe.ts");
227
+ console.log();
228
+ console.log(" Files to copy (frontend):");
229
+ console.log(" + packages/frontend/src/pages/Pricing.tsx");
230
+ console.log(" + packages/frontend/src/pages/Checkout.tsx");
231
+ console.log(" + packages/frontend/src/components/stripe/SubscriptionStatus.tsx");
232
+ console.log(" + packages/frontend/src/hooks/useSubscription.ts");
233
+ console.log();
234
+ console.log(" Files to modify:");
235
+ console.log(" ~ packages/backend/src/db/schema/index.ts");
236
+ console.log(" ~ packages/backend/src/index.ts");
237
+ console.log(" ~ packages/backend/src/env.ts");
238
+ console.log(" ~ packages/frontend/src/router.tsx");
239
+ console.log(" ~ .env.example / .env");
240
+ console.log();
241
+ console.log(" Environment variables:");
242
+ console.log(` STRIPE_SECRET_KEY=${stripeSecretKey.slice(0, 12)}...`);
243
+ console.log(` STRIPE_PUBLISHABLE_KEY=${stripePublishableKey.slice(0, 12)}...`);
244
+ console.log(` STRIPE_WEBHOOK_SECRET=${stripeWebhookSecret.slice(0, 12)}...`);
245
+ console.log();
246
+
247
+ // -- Step 7: Confirm and execute ---------------------------------------------
248
+ const proceed = await confirm({ message: "Proceed with installation?", default: true });
249
+ if (!proceed) { console.log("\n Installation cancelled.\n"); process.exit(0); }
250
+
251
+ console.log();
252
+ console.log(" Installing...");
253
+ console.log();
254
+
255
+ console.log(" Copying backend files...");
256
+ const backendFiles = [
257
+ { src: "backend/schema/stripe.ts", dest: "src/db/schema/stripe.ts" },
258
+ { src: "backend/routes/stripe.ts", dest: "src/routes/stripe.ts" },
259
+ { src: "backend/services/stripe.ts", dest: "src/services/stripe.ts" },
260
+ ];
261
+ for (const f of backendFiles) {
262
+ copyFile(join(SAIL_DIR, "files", f.src), join(BACKEND_ROOT, f.dest), f.dest);
263
+ }
264
+
265
+ console.log();
266
+ console.log(" Copying frontend files...");
267
+ const frontendFiles = [
268
+ { src: "frontend/pages/Pricing.tsx", dest: "src/pages/Pricing.tsx" },
269
+ { src: "frontend/pages/Checkout.tsx", dest: "src/pages/Checkout.tsx" },
270
+ { src: "frontend/components/SubscriptionStatus.tsx", dest: "src/components/stripe/SubscriptionStatus.tsx" },
271
+ { src: "frontend/hooks/useSubscription.ts", dest: "src/hooks/useSubscription.ts" },
272
+ ];
273
+ for (const f of frontendFiles) {
274
+ copyFile(join(SAIL_DIR, "files", f.src), join(FRONTEND_ROOT, f.dest), f.dest);
275
+ }
276
+
277
+ console.log();
278
+ console.log(" Modifying backend files...");
279
+
280
+ insertAtMarker(join(BACKEND_ROOT, "src/db/schema/index.ts"), "// [SAIL_SCHEMA]", 'export * from "./stripe";');
281
+ insertAtMarker(join(BACKEND_ROOT, "src/index.ts"), "// [SAIL_IMPORTS]", 'import { stripeRouter } from "./routes/stripe";');
282
+ insertAtMarker(join(BACKEND_ROOT, "src/index.ts"), "// [SAIL_ROUTES]", 'app.use("/api/stripe", stripeRouter);');
283
+
284
+ // Stripe webhook needs the raw body — insert express.raw() BEFORE express.json()
285
+ const indexPath = join(BACKEND_ROOT, "src/index.ts");
286
+ if (existsSync(indexPath)) {
287
+ let indexContent = readFileSync(indexPath, "utf-8");
288
+ const rawMiddleware = 'app.use("/api/stripe/webhook", express.raw({ type: "application/json" }));';
289
+ if (!indexContent.includes(rawMiddleware)) {
290
+ indexContent = indexContent.replace(
291
+ "app.use(express.json());",
292
+ `// Raw body for Stripe webhook signature verification (must be before express.json())\n${rawMiddleware}\n\napp.use(express.json());`,
293
+ );
294
+ writeFileSync(indexPath, indexContent, "utf-8");
295
+ console.log(" Modified -> src/index.ts (added raw body middleware for webhook)");
296
+ }
297
+ }
298
+
299
+ insertAtMarker(
300
+ join(BACKEND_ROOT, "src/env.ts"),
301
+ "// [SAIL_ENV_VARS]",
302
+ ` STRIPE_SECRET_KEY: z.string().min(1, "STRIPE_SECRET_KEY is required"),\n STRIPE_PUBLISHABLE_KEY: z.string().min(1, "STRIPE_PUBLISHABLE_KEY is required"),\n STRIPE_WEBHOOK_SECRET: z.string().min(1, "STRIPE_WEBHOOK_SECRET is required"),`
303
+ );
304
+
305
+ console.log();
306
+ console.log(" Modifying frontend files...");
307
+
308
+ const routerPath = join(FRONTEND_ROOT, "src/router.tsx");
309
+ insertAtMarker(routerPath, "// [SAIL_IMPORTS]", `import { PricingPage } from "./pages/Pricing";\nimport { CheckoutPage } from "./pages/Checkout";`);
310
+ insertAtMarker(
311
+ routerPath,
312
+ "{/* [SAIL_ROUTES] */}",
313
+ ` {\n path: "/pricing",\n element: <PricingPage />,\n },\n {\n path: "/checkout/success",\n element: <CheckoutPage status="success" />,\n },\n {\n path: "/checkout/cancel",\n element: <CheckoutPage status="cancel" />,\n },`
314
+ );
315
+
316
+ console.log();
317
+ console.log(" Installing dependencies...");
318
+ installDeps(manifest.dependencies.backend, "packages/backend");
319
+ installDeps(manifest.dependencies.frontend, "packages/frontend");
320
+
321
+ console.log();
322
+ console.log(" Generating database migrations...");
323
+ try {
324
+ execSync("npx drizzle-kit generate", { cwd: BACKEND_ROOT, stdio: "inherit" });
325
+ } catch {
326
+ console.warn(" Warning: Could not generate migrations. Run manually: cd packages/backend && npx drizzle-kit generate");
327
+ }
328
+
329
+ console.log();
330
+ console.log(" Updating environment files...");
331
+ appendToEnvExample({ STRIPE_SECRET_KEY: stripeSecretKey, STRIPE_PUBLISHABLE_KEY: stripePublishableKey, STRIPE_WEBHOOK_SECRET: stripeWebhookSecret });
332
+
333
+ const dotEnvPath = join(PROJECT_ROOT, ".env");
334
+ if (existsSync(dotEnvPath)) {
335
+ let dotEnv = readFileSync(dotEnvPath, "utf-8");
336
+ if (!dotEnv.includes("STRIPE_SECRET_KEY")) {
337
+ dotEnv += `\n# Stripe Payments\nSTRIPE_SECRET_KEY=${stripeSecretKey}\nSTRIPE_PUBLISHABLE_KEY=${stripePublishableKey}\nSTRIPE_WEBHOOK_SECRET=${stripeWebhookSecret}\n`;
338
+ writeFileSync(dotEnvPath, dotEnv, "utf-8");
339
+ console.log(" Updated .env");
340
+ }
341
+ }
342
+
343
+ // -- Step 8: Print test instructions -----------------------------------------
344
+ console.log();
345
+ console.log("------------------------------------------------------------");
346
+ console.log(" Stripe Payments installed successfully!");
347
+ console.log("------------------------------------------------------------");
348
+ console.log();
349
+ console.log(" Next steps:");
350
+ console.log();
351
+ console.log(" 1. Run database migrations:");
352
+ console.log(" npm run db:migrate");
353
+ console.log();
354
+ console.log(" 2. Create products in Stripe Dashboard:");
355
+ console.log(" https://dashboard.stripe.com/test/products");
356
+ console.log(" Copy the price IDs (price_...) into your Pricing.tsx");
357
+ console.log();
358
+ console.log(" 3. Start your dev server:");
359
+ console.log(" npm run dev");
360
+ console.log();
361
+ console.log(" Testing locally with the Stripe CLI:");
362
+ console.log(" Install: brew install stripe/stripe-cli/stripe");
363
+ console.log(" Login: stripe login");
364
+ console.log(" Listen: stripe listen --forward-to localhost:3000/api/stripe/webhook");
365
+ console.log();
366
+ console.log(" Test card numbers:");
367
+ console.log(" Success: 4242 4242 4242 4242");
368
+ console.log(" Requires auth: 4000 0025 0000 3155");
369
+ console.log(" Declined: 4000 0000 0000 9995");
370
+ console.log(" Use any future expiry date, any CVC, and any postal code.");
371
+ console.log();
372
+ console.log(" Webhook testing:");
373
+ console.log(" Trigger events: stripe trigger checkout.session.completed");
374
+ console.log(" View events: stripe events list");
375
+ console.log();
376
+ }
377
+
378
+ main().catch((err) => { console.error("Installation failed:", err); process.exit(1); });