@codaijs/keel 0.2.2 → 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,402 +1,402 @@
1
- /**
2
- * Template Sail Installer
3
- *
4
- * This file serves as a reference implementation for creating sail installers.
5
- * Each sail should have its own install.ts that follows this wizard pattern.
6
- *
7
- * The installer is executed by the CLI tool or can be run standalone:
8
- * npx tsx sails/<sail-name>/install.ts
9
- *
10
- * ============================================================================
11
- * WIZARD PATTERN
12
- * ============================================================================
13
- *
14
- * Every sail installer should follow this 8-step wizard flow:
15
- *
16
- * Step 1: Welcome - Explain what the sail does
17
- * Step 2: Prerequisites - Check if the user has required accounts/setup
18
- * If not, provide step-by-step guidance and wait
19
- * Step 3: Credentials - Collect required API keys and secrets
20
- * Validate format where possible (prefixes, etc.)
21
- * Step 4: Summary - Show all files that will be created/modified
22
- * Show collected env vars (masked)
23
- * Step 5: Confirm - Ask user to confirm before making changes
24
- * Step 6: Execute - Copy files, modify markers, update env
25
- * Step 7: Dependencies - Install npm packages, run migrations
26
- * Step 8: Next steps - Print what to do next and how to test
27
- *
28
- * Use @inquirer/prompts for all interactive prompts:
29
- * - input() for text/credential entry with validation
30
- * - confirm() for yes/no decisions
31
- * - select() for choosing one option from a list
32
- * - checkbox() for selecting multiple options
33
- *
34
- * ============================================================================
35
- */
36
-
37
- import { readFileSync, writeFileSync, copyFileSync, existsSync, mkdirSync } from "node:fs";
38
- import { resolve, dirname, join } from "node:path";
39
- import { execSync } from "node:child_process";
40
- import { input, confirm, select } from "@inquirer/prompts";
41
-
42
- // ---------------------------------------------------------------------------
43
- // Types
44
- // ---------------------------------------------------------------------------
45
-
46
- interface EnvVar {
47
- key: string;
48
- description: string;
49
- }
50
-
51
- interface SailManifest {
52
- name: string;
53
- displayName: string;
54
- description: string;
55
- version: string;
56
- compatibility: string;
57
- requiredEnvVars: EnvVar[];
58
- dependencies: {
59
- backend: Record<string, string>;
60
- frontend: Record<string, string>;
61
- };
62
- modifies: {
63
- backend: string[];
64
- frontend: string[];
65
- };
66
- adds: {
67
- backend: string[];
68
- frontend: string[];
69
- };
70
- }
71
-
72
- // ---------------------------------------------------------------------------
73
- // Helpers
74
- // ---------------------------------------------------------------------------
75
-
76
- const SAIL_DIR = dirname(new URL(import.meta.url).pathname);
77
- const PROJECT_ROOT = resolve(SAIL_DIR, "../..");
78
-
79
- function loadManifest(): SailManifest {
80
- const raw = readFileSync(join(SAIL_DIR, "addon.json"), "utf-8");
81
- return JSON.parse(raw) as SailManifest;
82
- }
83
-
84
- // ---------------------------------------------------------------------------
85
- // Step 1 -- Welcome message
86
- // ---------------------------------------------------------------------------
87
-
88
- function printWelcome(manifest: SailManifest): void {
89
- console.log("\n------------------------------------------------------------");
90
- console.log(` ${manifest.displayName} Installer (v${manifest.version})`);
91
- console.log("------------------------------------------------------------");
92
- console.log();
93
- console.log(` ${manifest.description}`);
94
- console.log();
95
-
96
- const pkgPath = join(PROJECT_ROOT, "package.json");
97
- if (existsSync(pkgPath)) {
98
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
99
- console.log(` Template version: ${pkg.version ?? "unknown"}`);
100
- console.log(` Required compatibility: ${manifest.compatibility}`);
101
- console.log();
102
- }
103
- }
104
-
105
- // ---------------------------------------------------------------------------
106
- // Step 2 -- Prerequisites check
107
- // ---------------------------------------------------------------------------
108
-
109
- async function checkPrerequisites(): Promise<void> {
110
- const hasPrereqs = await confirm({
111
- message: "Do you have the required external service account set up?",
112
- default: false,
113
- });
114
-
115
- if (!hasPrereqs) {
116
- console.log();
117
- console.log(" Follow these steps to set up the required service:");
118
- console.log();
119
- console.log(" 1. Go to <service dashboard URL>");
120
- console.log(" 2. Create an account or sign in");
121
- console.log(" 3. Create the required resources (API keys, projects, etc.)");
122
- console.log(" 4. Note down the credentials you will need");
123
- console.log();
124
-
125
- await confirm({
126
- message: "I have completed the setup and have my credentials ready",
127
- default: false,
128
- });
129
- }
130
- }
131
-
132
- // ---------------------------------------------------------------------------
133
- // Step 3 -- Collect credentials
134
- // ---------------------------------------------------------------------------
135
-
136
- async function collectCredentials(
137
- envVars: EnvVar[]
138
- ): Promise<Record<string, string>> {
139
- const values: Record<string, string> = {};
140
-
141
- console.log();
142
- for (const v of envVars) {
143
- const value = await input({
144
- message: `${v.description}:`,
145
- validate: (val) => {
146
- if (!val || val.trim().length === 0) {
147
- return `${v.key} is required.`;
148
- }
149
- return true;
150
- },
151
- });
152
- values[v.key] = value.trim();
153
- }
154
-
155
- return values;
156
- }
157
-
158
- // ---------------------------------------------------------------------------
159
- // Step 4 -- Show summary
160
- // ---------------------------------------------------------------------------
161
-
162
- function showSummary(
163
- manifest: SailManifest,
164
- envValues: Record<string, string>
165
- ): void {
166
- console.log();
167
- console.log(" Summary of changes:");
168
- console.log(" -------------------");
169
-
170
- if (manifest.adds.backend.length > 0 || manifest.adds.frontend.length > 0) {
171
- console.log(" Files to create:");
172
- for (const f of manifest.adds.backend) {
173
- console.log(` + packages/backend/${f}`);
174
- }
175
- for (const f of manifest.adds.frontend) {
176
- console.log(` + packages/frontend/${f}`);
177
- }
178
- console.log();
179
- }
180
-
181
- if (manifest.modifies.backend.length > 0 || manifest.modifies.frontend.length > 0) {
182
- console.log(" Files to modify:");
183
- for (const f of manifest.modifies.backend) {
184
- console.log(` ~ packages/backend/${f}`);
185
- }
186
- for (const f of manifest.modifies.frontend) {
187
- console.log(` ~ packages/frontend/${f}`);
188
- }
189
- console.log();
190
- }
191
-
192
- console.log(" Environment variables:");
193
- for (const [key, val] of Object.entries(envValues)) {
194
- const masked = val.length > 8 ? val.slice(0, 8) + "..." : val;
195
- console.log(` ${key}=${masked}`);
196
- }
197
- console.log();
198
- }
199
-
200
- // ---------------------------------------------------------------------------
201
- // Step 5 -- Confirm
202
- // ---------------------------------------------------------------------------
203
-
204
- async function confirmInstallation(): Promise<void> {
205
- const proceed = await confirm({
206
- message: "Proceed with installation?",
207
- default: true,
208
- });
209
-
210
- if (!proceed) {
211
- console.log("\n Installation cancelled.\n");
212
- process.exit(0);
213
- }
214
- }
215
-
216
- // ---------------------------------------------------------------------------
217
- // Step 6 -- Execute
218
- // ---------------------------------------------------------------------------
219
-
220
- function copySailFiles(manifest: SailManifest): void {
221
- const filesDir = join(SAIL_DIR, "files");
222
- if (!existsSync(filesDir)) {
223
- console.log(" No files/ directory -- skipping file copy.");
224
- return;
225
- }
226
-
227
- console.log(" Copying files...");
228
-
229
- for (const file of manifest.adds.backend) {
230
- const src = join(filesDir, "backend", file.replace(/^src\//, ""));
231
- const dest = join(PROJECT_ROOT, "packages/backend", file);
232
- if (existsSync(src)) {
233
- mkdirSync(dirname(dest), { recursive: true });
234
- copyFileSync(src, dest);
235
- console.log(` Copied -> ${file}`);
236
- }
237
- }
238
-
239
- for (const file of manifest.adds.frontend) {
240
- const src = join(filesDir, "frontend", file.replace(/^src\//, ""));
241
- const dest = join(PROJECT_ROOT, "packages/frontend", file);
242
- if (existsSync(src)) {
243
- mkdirSync(dirname(dest), { recursive: true });
244
- copyFileSync(src, dest);
245
- console.log(` Copied -> ${file}`);
246
- }
247
- }
248
- }
249
-
250
- function insertAtMarker(
251
- filePath: string,
252
- marker: string,
253
- insertion: string
254
- ): void {
255
- if (!existsSync(filePath)) {
256
- console.warn(` Warning: File not found: ${filePath} -- skipping.`);
257
- return;
258
- }
259
-
260
- const content = readFileSync(filePath, "utf-8");
261
-
262
- if (!content.includes(marker)) {
263
- console.warn(` Warning: Marker "${marker}" not found in ${filePath}.`);
264
- return;
265
- }
266
-
267
- if (content.includes(insertion.trim())) {
268
- console.log(` Skipped (already present) -> ${filePath}`);
269
- return;
270
- }
271
-
272
- const updated = content.replace(marker, `${marker}\n${insertion}`);
273
- writeFileSync(filePath, updated, "utf-8");
274
- console.log(` Inserted at ${marker} -> ${filePath}`);
275
- }
276
-
277
- function updateEnvFiles(
278
- sectionName: string,
279
- envVars: Record<string, string>
280
- ): void {
281
- console.log(" Updating environment files...");
282
-
283
- for (const envFile of [".env.example", ".env"]) {
284
- const envPath = join(PROJECT_ROOT, envFile);
285
- if (!existsSync(envPath)) continue;
286
-
287
- let content = readFileSync(envPath, "utf-8");
288
- const additions: string[] = [];
289
- for (const [key, value] of Object.entries(envVars)) {
290
- if (!content.includes(key)) {
291
- additions.push(`${key}=${value}`);
292
- }
293
- }
294
-
295
- if (additions.length > 0) {
296
- content += `\n# ${sectionName}\n${additions.join("\n")}\n`;
297
- writeFileSync(envPath, content, "utf-8");
298
- console.log(` Updated ${envFile}`);
299
- }
300
- }
301
- }
302
-
303
- // ---------------------------------------------------------------------------
304
- // Step 7 -- Install dependencies and run migrations
305
- // ---------------------------------------------------------------------------
306
-
307
- function installDependencies(
308
- deps: Record<string, string>,
309
- workspace: string
310
- ): void {
311
- const entries = Object.entries(deps);
312
- if (entries.length === 0) return;
313
-
314
- const packages = entries.map(([name, version]) => `${name}@${version}`).join(" ");
315
- const cmd = `npm install ${packages} --workspace=${workspace}`;
316
- console.log(` Running: ${cmd}`);
317
- execSync(cmd, { cwd: PROJECT_ROOT, stdio: "inherit" });
318
- }
319
-
320
- function generateMigrations(): void {
321
- console.log(" Running: npx drizzle-kit generate");
322
- try {
323
- execSync("npx drizzle-kit generate", {
324
- cwd: join(PROJECT_ROOT, "packages/backend"),
325
- stdio: "inherit",
326
- });
327
- } catch {
328
- console.warn(" Warning: Could not generate migrations. Run manually:");
329
- console.warn(" cd packages/backend && npx drizzle-kit generate");
330
- }
331
- }
332
-
333
- // ---------------------------------------------------------------------------
334
- // Step 8 -- Print next steps
335
- // ---------------------------------------------------------------------------
336
-
337
- function printNextSteps(manifest: SailManifest): void {
338
- console.log();
339
- console.log("------------------------------------------------------------");
340
- console.log(` ${manifest.displayName} installed successfully!`);
341
- console.log("------------------------------------------------------------");
342
- console.log();
343
- console.log(" Next steps:");
344
- console.log(" 1. Fill in any placeholder env vars in .env");
345
- console.log(" 2. Run database migrations: npm run db:migrate");
346
- console.log(" 3. Review the modified files listed in addon.json");
347
- console.log(" 4. Read the sail README for provider-specific setup");
348
- console.log();
349
- console.log(" Testing:");
350
- console.log(" 1. Start your dev server: npm run dev");
351
- console.log(" 2. Test the new functionality in the browser");
352
- console.log(" 3. Check server logs for any configuration errors");
353
- console.log();
354
- }
355
-
356
- // ---------------------------------------------------------------------------
357
- // Main
358
- // ---------------------------------------------------------------------------
359
-
360
- async function main(): Promise<void> {
361
- const manifest = loadManifest();
362
-
363
- printWelcome(manifest);
364
- await checkPrerequisites();
365
- const envValues = await collectCredentials(manifest.requiredEnvVars);
366
- showSummary(manifest, envValues);
367
- await confirmInstallation();
368
-
369
- console.log();
370
- console.log(" Installing...");
371
- console.log();
372
- copySailFiles(manifest);
373
-
374
- console.log();
375
- console.log(" Modifying existing files...");
376
- // Sail specific marker insertions go here. Example:
377
- // insertAtMarker(
378
- // join(PROJECT_ROOT, "packages/backend/src/index.ts"),
379
- // "// [SAIL_IMPORTS]",
380
- // 'import { myRouter } from "./routes/my-route";'
381
- // );
382
-
383
- updateEnvFiles(manifest.displayName, envValues);
384
-
385
- console.log();
386
- console.log(" Installing dependencies...");
387
- installDependencies(manifest.dependencies.backend, "packages/backend");
388
- installDependencies(manifest.dependencies.frontend, "packages/frontend");
389
-
390
- if (manifest.adds.backend.some((f) => f.includes("schema"))) {
391
- console.log();
392
- console.log(" Generating database migrations...");
393
- generateMigrations();
394
- }
395
-
396
- printNextSteps(manifest);
397
- }
398
-
399
- main().catch((err) => {
400
- console.error("Installation failed:", err);
401
- process.exit(1);
402
- });
1
+ /**
2
+ * Template Sail Installer
3
+ *
4
+ * This file serves as a reference implementation for creating sail installers.
5
+ * Each sail should have its own install.ts that follows this wizard pattern.
6
+ *
7
+ * The installer is executed by the CLI tool or can be run standalone:
8
+ * npx tsx sails/<sail-name>/install.ts
9
+ *
10
+ * ============================================================================
11
+ * WIZARD PATTERN
12
+ * ============================================================================
13
+ *
14
+ * Every sail installer should follow this 8-step wizard flow:
15
+ *
16
+ * Step 1: Welcome - Explain what the sail does
17
+ * Step 2: Prerequisites - Check if the user has required accounts/setup
18
+ * If not, provide step-by-step guidance and wait
19
+ * Step 3: Credentials - Collect required API keys and secrets
20
+ * Validate format where possible (prefixes, etc.)
21
+ * Step 4: Summary - Show all files that will be created/modified
22
+ * Show collected env vars (masked)
23
+ * Step 5: Confirm - Ask user to confirm before making changes
24
+ * Step 6: Execute - Copy files, modify markers, update env
25
+ * Step 7: Dependencies - Install npm packages, run migrations
26
+ * Step 8: Next steps - Print what to do next and how to test
27
+ *
28
+ * Use @inquirer/prompts for all interactive prompts:
29
+ * - input() for text/credential entry with validation
30
+ * - confirm() for yes/no decisions
31
+ * - select() for choosing one option from a list
32
+ * - checkbox() for selecting multiple options
33
+ *
34
+ * ============================================================================
35
+ */
36
+
37
+ import { readFileSync, writeFileSync, copyFileSync, existsSync, mkdirSync } from "node:fs";
38
+ import { resolve, dirname, join } from "node:path";
39
+ import { execSync } from "node:child_process";
40
+ import { input, confirm, select } from "@inquirer/prompts";
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Types
44
+ // ---------------------------------------------------------------------------
45
+
46
+ interface EnvVar {
47
+ key: string;
48
+ description: string;
49
+ }
50
+
51
+ interface SailManifest {
52
+ name: string;
53
+ displayName: string;
54
+ description: string;
55
+ version: string;
56
+ compatibility: string;
57
+ requiredEnvVars: EnvVar[];
58
+ dependencies: {
59
+ backend: Record<string, string>;
60
+ frontend: Record<string, string>;
61
+ };
62
+ modifies: {
63
+ backend: string[];
64
+ frontend: string[];
65
+ };
66
+ adds: {
67
+ backend: string[];
68
+ frontend: string[];
69
+ };
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Helpers
74
+ // ---------------------------------------------------------------------------
75
+
76
+ const SAIL_DIR = dirname(new URL(import.meta.url).pathname);
77
+ const PROJECT_ROOT = resolve(SAIL_DIR, "../..");
78
+
79
+ function loadManifest(): SailManifest {
80
+ const raw = readFileSync(join(SAIL_DIR, "addon.json"), "utf-8");
81
+ return JSON.parse(raw) as SailManifest;
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Step 1 -- Welcome message
86
+ // ---------------------------------------------------------------------------
87
+
88
+ function printWelcome(manifest: SailManifest): void {
89
+ console.log("\n------------------------------------------------------------");
90
+ console.log(` ${manifest.displayName} Installer (v${manifest.version})`);
91
+ console.log("------------------------------------------------------------");
92
+ console.log();
93
+ console.log(` ${manifest.description}`);
94
+ console.log();
95
+
96
+ const pkgPath = join(PROJECT_ROOT, "package.json");
97
+ if (existsSync(pkgPath)) {
98
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
99
+ console.log(` Template version: ${pkg.version ?? "unknown"}`);
100
+ console.log(` Required compatibility: ${manifest.compatibility}`);
101
+ console.log();
102
+ }
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Step 2 -- Prerequisites check
107
+ // ---------------------------------------------------------------------------
108
+
109
+ async function checkPrerequisites(): Promise<void> {
110
+ const hasPrereqs = await confirm({
111
+ message: "Do you have the required external service account set up?",
112
+ default: false,
113
+ });
114
+
115
+ if (!hasPrereqs) {
116
+ console.log();
117
+ console.log(" Follow these steps to set up the required service:");
118
+ console.log();
119
+ console.log(" 1. Go to <service dashboard URL>");
120
+ console.log(" 2. Create an account or sign in");
121
+ console.log(" 3. Create the required resources (API keys, projects, etc.)");
122
+ console.log(" 4. Note down the credentials you will need");
123
+ console.log();
124
+
125
+ await confirm({
126
+ message: "I have completed the setup and have my credentials ready",
127
+ default: false,
128
+ });
129
+ }
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Step 3 -- Collect credentials
134
+ // ---------------------------------------------------------------------------
135
+
136
+ async function collectCredentials(
137
+ envVars: EnvVar[]
138
+ ): Promise<Record<string, string>> {
139
+ const values: Record<string, string> = {};
140
+
141
+ console.log();
142
+ for (const v of envVars) {
143
+ const value = await input({
144
+ message: `${v.description}:`,
145
+ validate: (val) => {
146
+ if (!val || val.trim().length === 0) {
147
+ return `${v.key} is required.`;
148
+ }
149
+ return true;
150
+ },
151
+ });
152
+ values[v.key] = value.trim();
153
+ }
154
+
155
+ return values;
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // Step 4 -- Show summary
160
+ // ---------------------------------------------------------------------------
161
+
162
+ function showSummary(
163
+ manifest: SailManifest,
164
+ envValues: Record<string, string>
165
+ ): void {
166
+ console.log();
167
+ console.log(" Summary of changes:");
168
+ console.log(" -------------------");
169
+
170
+ if (manifest.adds.backend.length > 0 || manifest.adds.frontend.length > 0) {
171
+ console.log(" Files to create:");
172
+ for (const f of manifest.adds.backend) {
173
+ console.log(` + packages/backend/${f}`);
174
+ }
175
+ for (const f of manifest.adds.frontend) {
176
+ console.log(` + packages/frontend/${f}`);
177
+ }
178
+ console.log();
179
+ }
180
+
181
+ if (manifest.modifies.backend.length > 0 || manifest.modifies.frontend.length > 0) {
182
+ console.log(" Files to modify:");
183
+ for (const f of manifest.modifies.backend) {
184
+ console.log(` ~ packages/backend/${f}`);
185
+ }
186
+ for (const f of manifest.modifies.frontend) {
187
+ console.log(` ~ packages/frontend/${f}`);
188
+ }
189
+ console.log();
190
+ }
191
+
192
+ console.log(" Environment variables:");
193
+ for (const [key, val] of Object.entries(envValues)) {
194
+ const masked = val.length > 8 ? val.slice(0, 8) + "..." : val;
195
+ console.log(` ${key}=${masked}`);
196
+ }
197
+ console.log();
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Step 5 -- Confirm
202
+ // ---------------------------------------------------------------------------
203
+
204
+ async function confirmInstallation(): Promise<void> {
205
+ const proceed = await confirm({
206
+ message: "Proceed with installation?",
207
+ default: true,
208
+ });
209
+
210
+ if (!proceed) {
211
+ console.log("\n Installation cancelled.\n");
212
+ process.exit(0);
213
+ }
214
+ }
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // Step 6 -- Execute
218
+ // ---------------------------------------------------------------------------
219
+
220
+ function copySailFiles(manifest: SailManifest): void {
221
+ const filesDir = join(SAIL_DIR, "files");
222
+ if (!existsSync(filesDir)) {
223
+ console.log(" No files/ directory -- skipping file copy.");
224
+ return;
225
+ }
226
+
227
+ console.log(" Copying files...");
228
+
229
+ for (const file of manifest.adds.backend) {
230
+ const src = join(filesDir, "backend", file.replace(/^src\//, ""));
231
+ const dest = join(PROJECT_ROOT, "packages/backend", file);
232
+ if (existsSync(src)) {
233
+ mkdirSync(dirname(dest), { recursive: true });
234
+ copyFileSync(src, dest);
235
+ console.log(` Copied -> ${file}`);
236
+ }
237
+ }
238
+
239
+ for (const file of manifest.adds.frontend) {
240
+ const src = join(filesDir, "frontend", file.replace(/^src\//, ""));
241
+ const dest = join(PROJECT_ROOT, "packages/frontend", file);
242
+ if (existsSync(src)) {
243
+ mkdirSync(dirname(dest), { recursive: true });
244
+ copyFileSync(src, dest);
245
+ console.log(` Copied -> ${file}`);
246
+ }
247
+ }
248
+ }
249
+
250
+ function insertAtMarker(
251
+ filePath: string,
252
+ marker: string,
253
+ insertion: string
254
+ ): void {
255
+ if (!existsSync(filePath)) {
256
+ console.warn(` Warning: File not found: ${filePath} -- skipping.`);
257
+ return;
258
+ }
259
+
260
+ const content = readFileSync(filePath, "utf-8");
261
+
262
+ if (!content.includes(marker)) {
263
+ console.warn(` Warning: Marker "${marker}" not found in ${filePath}.`);
264
+ return;
265
+ }
266
+
267
+ if (content.includes(insertion.trim())) {
268
+ console.log(` Skipped (already present) -> ${filePath}`);
269
+ return;
270
+ }
271
+
272
+ const updated = content.replace(marker, `${marker}\n${insertion}`);
273
+ writeFileSync(filePath, updated, "utf-8");
274
+ console.log(` Inserted at ${marker} -> ${filePath}`);
275
+ }
276
+
277
+ function updateEnvFiles(
278
+ sectionName: string,
279
+ envVars: Record<string, string>
280
+ ): void {
281
+ console.log(" Updating environment files...");
282
+
283
+ for (const envFile of [".env.example", ".env"]) {
284
+ const envPath = join(PROJECT_ROOT, envFile);
285
+ if (!existsSync(envPath)) continue;
286
+
287
+ let content = readFileSync(envPath, "utf-8");
288
+ const additions: string[] = [];
289
+ for (const [key, value] of Object.entries(envVars)) {
290
+ if (!content.includes(key)) {
291
+ additions.push(`${key}=${value}`);
292
+ }
293
+ }
294
+
295
+ if (additions.length > 0) {
296
+ content += `\n# ${sectionName}\n${additions.join("\n")}\n`;
297
+ writeFileSync(envPath, content, "utf-8");
298
+ console.log(` Updated ${envFile}`);
299
+ }
300
+ }
301
+ }
302
+
303
+ // ---------------------------------------------------------------------------
304
+ // Step 7 -- Install dependencies and run migrations
305
+ // ---------------------------------------------------------------------------
306
+
307
+ function installDependencies(
308
+ deps: Record<string, string>,
309
+ workspace: string
310
+ ): void {
311
+ const entries = Object.entries(deps);
312
+ if (entries.length === 0) return;
313
+
314
+ const packages = entries.map(([name, version]) => `${name}@${version}`).join(" ");
315
+ const cmd = `npm install ${packages} --workspace=${workspace}`;
316
+ console.log(` Running: ${cmd}`);
317
+ execSync(cmd, { cwd: PROJECT_ROOT, stdio: "inherit" });
318
+ }
319
+
320
+ function generateMigrations(): void {
321
+ console.log(" Running: npx drizzle-kit generate");
322
+ try {
323
+ execSync("npx drizzle-kit generate", {
324
+ cwd: join(PROJECT_ROOT, "packages/backend"),
325
+ stdio: "inherit",
326
+ });
327
+ } catch {
328
+ console.warn(" Warning: Could not generate migrations. Run manually:");
329
+ console.warn(" cd packages/backend && npx drizzle-kit generate");
330
+ }
331
+ }
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // Step 8 -- Print next steps
335
+ // ---------------------------------------------------------------------------
336
+
337
+ function printNextSteps(manifest: SailManifest): void {
338
+ console.log();
339
+ console.log("------------------------------------------------------------");
340
+ console.log(` ${manifest.displayName} installed successfully!`);
341
+ console.log("------------------------------------------------------------");
342
+ console.log();
343
+ console.log(" Next steps:");
344
+ console.log(" 1. Fill in any placeholder env vars in .env");
345
+ console.log(" 2. Run database migrations: npm run db:migrate");
346
+ console.log(" 3. Review the modified files listed in addon.json");
347
+ console.log(" 4. Read the sail README for provider-specific setup");
348
+ console.log();
349
+ console.log(" Testing:");
350
+ console.log(" 1. Start your dev server: npm run dev");
351
+ console.log(" 2. Test the new functionality in the browser");
352
+ console.log(" 3. Check server logs for any configuration errors");
353
+ console.log();
354
+ }
355
+
356
+ // ---------------------------------------------------------------------------
357
+ // Main
358
+ // ---------------------------------------------------------------------------
359
+
360
+ async function main(): Promise<void> {
361
+ const manifest = loadManifest();
362
+
363
+ printWelcome(manifest);
364
+ await checkPrerequisites();
365
+ const envValues = await collectCredentials(manifest.requiredEnvVars);
366
+ showSummary(manifest, envValues);
367
+ await confirmInstallation();
368
+
369
+ console.log();
370
+ console.log(" Installing...");
371
+ console.log();
372
+ copySailFiles(manifest);
373
+
374
+ console.log();
375
+ console.log(" Modifying existing files...");
376
+ // Sail specific marker insertions go here. Example:
377
+ // insertAtMarker(
378
+ // join(PROJECT_ROOT, "packages/backend/src/index.ts"),
379
+ // "// [SAIL_IMPORTS]",
380
+ // 'import { myRouter } from "./routes/my-route";'
381
+ // );
382
+
383
+ updateEnvFiles(manifest.displayName, envValues);
384
+
385
+ console.log();
386
+ console.log(" Installing dependencies...");
387
+ installDependencies(manifest.dependencies.backend, "packages/backend");
388
+ installDependencies(manifest.dependencies.frontend, "packages/frontend");
389
+
390
+ if (manifest.adds.backend.some((f) => f.includes("schema"))) {
391
+ console.log();
392
+ console.log(" Generating database migrations...");
393
+ generateMigrations();
394
+ }
395
+
396
+ printNextSteps(manifest);
397
+ }
398
+
399
+ main().catch((err) => {
400
+ console.error("Installation failed:", err);
401
+ process.exit(1);
402
+ });