@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.
- package/dist/__tests__/sail-installer.test.js +25 -25
- package/dist/sail-installer.js +174 -174
- package/dist/scaffold.js +68 -68
- package/package.json +58 -58
- package/sails/_template/addon.json +20 -20
- package/sails/_template/install.ts +402 -402
- package/sails/admin-dashboard/README.md +117 -117
- package/sails/admin-dashboard/addon.json +28 -28
- package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -34
- package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -243
- package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -40
- package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -240
- package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -149
- package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -173
- package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -203
- package/sails/admin-dashboard/install.ts +305 -305
- package/sails/analytics/README.md +178 -178
- package/sails/analytics/addon.json +27 -27
- package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -58
- package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -64
- package/sails/analytics/files/frontend/lib/analytics.ts +103 -103
- package/sails/analytics/install.ts +297 -297
- package/sails/file-uploads/addon.json +30 -30
- package/sails/file-uploads/files/backend/routes/files.ts +198 -198
- package/sails/file-uploads/files/backend/schema/files.ts +36 -36
- package/sails/file-uploads/files/backend/services/file-storage.ts +128 -128
- package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -248
- package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -147
- package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -106
- package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -118
- package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -37
- package/sails/file-uploads/install.ts +466 -466
- package/sails/gdpr/README.md +174 -174
- package/sails/gdpr/addon.json +27 -27
- package/sails/gdpr/files/backend/routes/gdpr.ts +140 -140
- package/sails/gdpr/files/backend/services/gdpr.ts +293 -293
- package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -97
- package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -192
- package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -75
- package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -186
- package/sails/gdpr/install.ts +756 -756
- package/sails/google-oauth/README.md +121 -121
- package/sails/google-oauth/addon.json +22 -22
- package/sails/google-oauth/files/GoogleButton.tsx +50 -50
- package/sails/google-oauth/install.ts +252 -252
- package/sails/i18n/README.md +193 -193
- package/sails/i18n/addon.json +30 -30
- package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -108
- package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -31
- package/sails/i18n/files/frontend/lib/i18n.ts +32 -32
- package/sails/i18n/files/frontend/locales/de/common.json +44 -44
- package/sails/i18n/files/frontend/locales/en/common.json +44 -44
- package/sails/i18n/install.ts +407 -407
- package/sails/push-notifications/README.md +163 -163
- package/sails/push-notifications/addon.json +31 -31
- package/sails/push-notifications/files/backend/routes/notifications.ts +153 -153
- package/sails/push-notifications/files/backend/schema/notifications.ts +31 -31
- package/sails/push-notifications/files/backend/services/notifications.ts +117 -117
- package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -12
- package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -154
- package/sails/push-notifications/install.ts +384 -384
- package/sails/r2-storage/addon.json +29 -29
- package/sails/r2-storage/files/backend/services/storage.ts +71 -71
- package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -167
- package/sails/r2-storage/install.ts +412 -412
- package/sails/rate-limiting/addon.json +20 -20
- package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -104
- package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -137
- package/sails/rate-limiting/install.ts +300 -300
- package/sails/registry.json +107 -107
- package/sails/stripe/README.md +214 -214
- package/sails/stripe/addon.json +24 -24
- package/sails/stripe/files/backend/routes/stripe.ts +154 -154
- package/sails/stripe/files/backend/schema/stripe.ts +74 -74
- package/sails/stripe/files/backend/services/stripe.ts +224 -224
- package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -135
- package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -86
- package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -116
- package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -226
- 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
|
+
});
|