@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,466 +1,466 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File Uploads Sail Installer
|
|
3
|
-
*
|
|
4
|
-
* Adds a generic file upload system with S3-compatible storage.
|
|
5
|
-
* Supports Cloudflare R2, AWS S3, MinIO, and other S3-compatible providers.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* npx tsx sails/file-uploads/install.ts
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
readFileSync,
|
|
13
|
-
writeFileSync,
|
|
14
|
-
copyFileSync,
|
|
15
|
-
existsSync,
|
|
16
|
-
mkdirSync,
|
|
17
|
-
} from "node:fs";
|
|
18
|
-
import { resolve, dirname, join } from "node:path";
|
|
19
|
-
import { execSync } from "node:child_process";
|
|
20
|
-
import { input, confirm, select } from "@inquirer/prompts";
|
|
21
|
-
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Paths
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
const SAIL_DIR = dirname(new URL(import.meta.url).pathname);
|
|
27
|
-
const PROJECT_ROOT = resolve(SAIL_DIR, "../..");
|
|
28
|
-
const BACKEND_ROOT = join(PROJECT_ROOT, "packages/backend");
|
|
29
|
-
const FRONTEND_ROOT = join(PROJECT_ROOT, "packages/frontend");
|
|
30
|
-
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// Helpers
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
interface SailManifest {
|
|
36
|
-
name: string;
|
|
37
|
-
displayName: string;
|
|
38
|
-
version: string;
|
|
39
|
-
requiredEnvVars: { key: string; description: string }[];
|
|
40
|
-
dependencies: { backend: Record<string, string>; frontend: Record<string, string> };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function loadManifest(): SailManifest {
|
|
44
|
-
return JSON.parse(readFileSync(join(SAIL_DIR, "addon.json"), "utf-8"));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function insertAtMarker(filePath: string, marker: string, code: string): void {
|
|
48
|
-
if (!existsSync(filePath)) {
|
|
49
|
-
console.warn(` Warning: File not found: ${filePath}`);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
let content = readFileSync(filePath, "utf-8");
|
|
53
|
-
if (!content.includes(marker)) {
|
|
54
|
-
console.warn(` Warning: Marker "${marker}" not found in ${filePath}`);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
if (content.includes(code.trim())) {
|
|
58
|
-
console.log(` Skipped (already present) -> ${filePath}`);
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
content = content.replace(marker, `${marker}\n${code}`);
|
|
62
|
-
writeFileSync(filePath, content, "utf-8");
|
|
63
|
-
console.log(` Modified -> ${filePath}`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function copyFile(src: string, dest: string, label: string): void {
|
|
67
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
68
|
-
copyFileSync(src, dest);
|
|
69
|
-
console.log(` Copied -> ${label}`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function appendToEnvFiles(entries: Record<string, string>, section: string): void {
|
|
73
|
-
for (const envFile of [".env.example", ".env"]) {
|
|
74
|
-
const envPath = join(PROJECT_ROOT, envFile);
|
|
75
|
-
if (!existsSync(envPath)) continue;
|
|
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# ${section}\n${lines.join("\n")}\n`;
|
|
83
|
-
writeFileSync(envPath, content, "utf-8");
|
|
84
|
-
console.log(` Updated ${envFile}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function installDeps(deps: Record<string, string>, workspace: string): void {
|
|
90
|
-
const entries = Object.entries(deps);
|
|
91
|
-
if (entries.length === 0) return;
|
|
92
|
-
const packages = entries.map(([n, v]) => `${n}@${v}`).join(" ");
|
|
93
|
-
const cmd = `npm install ${packages} --workspace=${workspace}`;
|
|
94
|
-
console.log(` Running: ${cmd}`);
|
|
95
|
-
execSync(cmd, { cwd: PROJECT_ROOT, stdio: "inherit" });
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
// Main
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
|
|
102
|
-
async function main(): Promise<void> {
|
|
103
|
-
const manifest = loadManifest();
|
|
104
|
-
|
|
105
|
-
// -- Step 1: Welcome -------------------------------------------------------
|
|
106
|
-
console.log("\n------------------------------------------------------------");
|
|
107
|
-
console.log(` File Uploads Sail Installer (v${manifest.version})`);
|
|
108
|
-
console.log("------------------------------------------------------------");
|
|
109
|
-
console.log();
|
|
110
|
-
console.log(" This sail adds a complete file upload system:");
|
|
111
|
-
console.log(" - S3-compatible storage (R2, S3, MinIO)");
|
|
112
|
-
console.log(" - Presigned URL uploads (files go directly to storage)");
|
|
113
|
-
console.log(" - File management API (list, download, delete)");
|
|
114
|
-
console.log(" - Database tracking of uploaded files");
|
|
115
|
-
console.log(" - React hooks for upload and file management");
|
|
116
|
-
console.log(" - File browser page with drag-and-drop upload");
|
|
117
|
-
console.log();
|
|
118
|
-
|
|
119
|
-
const pkgPath = join(PROJECT_ROOT, "package.json");
|
|
120
|
-
if (existsSync(pkgPath)) {
|
|
121
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
122
|
-
console.log(` Template version: ${pkg.version ?? "unknown"}`);
|
|
123
|
-
console.log();
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// -- Step 2: Choose storage provider ---------------------------------------
|
|
127
|
-
const provider = await select({
|
|
128
|
-
message: "Which S3-compatible storage provider will you use?",
|
|
129
|
-
choices: [
|
|
130
|
-
{ name: "Cloudflare R2", value: "r2" },
|
|
131
|
-
{ name: "AWS S3", value: "s3" },
|
|
132
|
-
{ name: "MinIO (self-hosted)", value: "minio" },
|
|
133
|
-
{ name: "Other S3-compatible", value: "other" },
|
|
134
|
-
],
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// -- Provider-specific guidance -------------------------------------------
|
|
138
|
-
if (provider === "r2") {
|
|
139
|
-
console.log();
|
|
140
|
-
console.log(" Cloudflare R2 setup:");
|
|
141
|
-
console.log(" 1. Go to https://dash.cloudflare.com/?to=/:account/r2/new");
|
|
142
|
-
console.log(" 2. Create a bucket");
|
|
143
|
-
console.log(" 3. Go to R2 > Overview > Manage R2 API Tokens");
|
|
144
|
-
console.log(" 4. Create a token with Object Read & Write permissions");
|
|
145
|
-
console.log(" 5. Note your Account ID, Access Key ID, and Secret Access Key");
|
|
146
|
-
console.log();
|
|
147
|
-
} else if (provider === "s3") {
|
|
148
|
-
console.log();
|
|
149
|
-
console.log(" AWS S3 setup:");
|
|
150
|
-
console.log(" 1. Go to https://s3.console.aws.amazon.com/s3/buckets");
|
|
151
|
-
console.log(" 2. Create a bucket");
|
|
152
|
-
console.log(" 3. Create an IAM user with S3 access");
|
|
153
|
-
console.log(" 4. Note the Access Key ID and Secret Access Key");
|
|
154
|
-
console.log(" 5. Note your bucket's region (e.g., us-east-1)");
|
|
155
|
-
console.log();
|
|
156
|
-
} else if (provider === "minio") {
|
|
157
|
-
console.log();
|
|
158
|
-
console.log(" MinIO setup:");
|
|
159
|
-
console.log(" 1. Start MinIO: docker run -p 9000:9000 -p 9001:9001 minio/minio server /data --console-address ':9001'");
|
|
160
|
-
console.log(" 2. Open the console at http://localhost:9001");
|
|
161
|
-
console.log(" 3. Create a bucket");
|
|
162
|
-
console.log(" 4. Default credentials: minioadmin / minioadmin");
|
|
163
|
-
console.log();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const hasCredentials = await confirm({
|
|
167
|
-
message: "Do you have your storage credentials ready?",
|
|
168
|
-
default: false,
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
if (!hasCredentials) {
|
|
172
|
-
console.log();
|
|
173
|
-
console.log(" Please set up your storage provider and obtain credentials,");
|
|
174
|
-
console.log(" then run this installer again.");
|
|
175
|
-
console.log();
|
|
176
|
-
|
|
177
|
-
await confirm({
|
|
178
|
-
message: "I have my credentials ready now, continue",
|
|
179
|
-
default: false,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// -- Step 3: Collect credentials -------------------------------------------
|
|
184
|
-
console.log();
|
|
185
|
-
console.log(" Enter your storage credentials:");
|
|
186
|
-
console.log();
|
|
187
|
-
|
|
188
|
-
// Compute defaults based on provider
|
|
189
|
-
const defaultEndpoint =
|
|
190
|
-
provider === "r2"
|
|
191
|
-
? "https://<account-id>.r2.cloudflarestorage.com"
|
|
192
|
-
: provider === "s3"
|
|
193
|
-
? "https://s3.<region>.amazonaws.com"
|
|
194
|
-
: provider === "minio"
|
|
195
|
-
? "http://localhost:9000"
|
|
196
|
-
: "";
|
|
197
|
-
|
|
198
|
-
const defaultRegion =
|
|
199
|
-
provider === "r2" ? "auto" : provider === "s3" ? "us-east-1" : "auto";
|
|
200
|
-
|
|
201
|
-
const s3Endpoint = await input({
|
|
202
|
-
message: "S3 Endpoint URL:",
|
|
203
|
-
default: defaultEndpoint,
|
|
204
|
-
validate: (value) => {
|
|
205
|
-
if (!value || value.trim().length === 0) return "Endpoint is required.";
|
|
206
|
-
if (!value.startsWith("http")) return "Endpoint should start with http:// or https://";
|
|
207
|
-
return true;
|
|
208
|
-
},
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
const s3AccessKeyId = await input({
|
|
212
|
-
message: "S3 Access Key ID:",
|
|
213
|
-
validate: (value) => {
|
|
214
|
-
if (!value || value.trim().length === 0) return "Access Key ID is required.";
|
|
215
|
-
return true;
|
|
216
|
-
},
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
const s3SecretAccessKey = await input({
|
|
220
|
-
message: "S3 Secret Access Key:",
|
|
221
|
-
validate: (value) => {
|
|
222
|
-
if (!value || value.trim().length === 0) return "Secret Access Key is required.";
|
|
223
|
-
return true;
|
|
224
|
-
},
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
const s3BucketName = await input({
|
|
228
|
-
message: "S3 Bucket Name:",
|
|
229
|
-
default: "uploads",
|
|
230
|
-
validate: (value) => {
|
|
231
|
-
if (!value || value.trim().length === 0) return "Bucket name is required.";
|
|
232
|
-
return true;
|
|
233
|
-
},
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
const s3PublicUrl = await input({
|
|
237
|
-
message: "S3 Public URL for serving files (leave blank if not public):",
|
|
238
|
-
default: "",
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
const s3Region = await input({
|
|
242
|
-
message: "S3 Region:",
|
|
243
|
-
default: defaultRegion,
|
|
244
|
-
validate: (value) => {
|
|
245
|
-
if (!value || value.trim().length === 0) return "Region is required.";
|
|
246
|
-
return true;
|
|
247
|
-
},
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
// -- Step 4: Max file size -------------------------------------------------
|
|
251
|
-
console.log();
|
|
252
|
-
const maxSizeInput = await input({
|
|
253
|
-
message: "Maximum upload file size in MB:",
|
|
254
|
-
default: "50",
|
|
255
|
-
validate: (value) => {
|
|
256
|
-
const n = Number(value);
|
|
257
|
-
if (isNaN(n) || n <= 0) return "Please enter a positive number.";
|
|
258
|
-
return true;
|
|
259
|
-
},
|
|
260
|
-
});
|
|
261
|
-
const maxSizeMB = Number(maxSizeInput);
|
|
262
|
-
|
|
263
|
-
// -- Step 5: Summary -------------------------------------------------------
|
|
264
|
-
console.log();
|
|
265
|
-
console.log(" Summary of changes:");
|
|
266
|
-
console.log(" -------------------");
|
|
267
|
-
console.log(" Files to create (backend):");
|
|
268
|
-
console.log(" + packages/backend/src/services/file-storage.ts");
|
|
269
|
-
console.log(" + packages/backend/src/routes/files.ts");
|
|
270
|
-
console.log(" + packages/backend/src/db/schema/files.ts");
|
|
271
|
-
console.log();
|
|
272
|
-
console.log(" Files to create (frontend):");
|
|
273
|
-
console.log(" + packages/frontend/src/hooks/useFileUpload.ts");
|
|
274
|
-
console.log(" + packages/frontend/src/hooks/useFiles.ts");
|
|
275
|
-
console.log(" + packages/frontend/src/components/files/FileUploadButton.tsx");
|
|
276
|
-
console.log(" + packages/frontend/src/components/files/FileList.tsx");
|
|
277
|
-
console.log(" + packages/frontend/src/pages/Files.tsx");
|
|
278
|
-
console.log();
|
|
279
|
-
console.log(" Files to modify:");
|
|
280
|
-
console.log(" ~ packages/backend/src/index.ts (import + mount routes)");
|
|
281
|
-
console.log(" ~ packages/backend/src/db/schema/index.ts (export schema)");
|
|
282
|
-
console.log(" ~ packages/backend/src/env.ts (add env vars)");
|
|
283
|
-
console.log(" ~ packages/frontend/src/router.tsx (add /files route)");
|
|
284
|
-
console.log(" ~ .env.example / .env");
|
|
285
|
-
console.log();
|
|
286
|
-
console.log(" Environment variables:");
|
|
287
|
-
console.log(` S3_ENDPOINT=${s3Endpoint.slice(0, 30)}...`);
|
|
288
|
-
console.log(` S3_ACCESS_KEY_ID=${s3AccessKeyId.slice(0, 8)}...`);
|
|
289
|
-
console.log(` S3_SECRET_ACCESS_KEY=${s3SecretAccessKey.slice(0, 8)}...`);
|
|
290
|
-
console.log(` S3_BUCKET_NAME=${s3BucketName}`);
|
|
291
|
-
console.log(` S3_PUBLIC_URL=${s3PublicUrl || "(not set)"}`);
|
|
292
|
-
console.log(` S3_REGION=${s3Region}`);
|
|
293
|
-
console.log();
|
|
294
|
-
console.log(` Max file size: ${maxSizeMB} MB`);
|
|
295
|
-
console.log();
|
|
296
|
-
|
|
297
|
-
// -- Step 6: Confirm -------------------------------------------------------
|
|
298
|
-
const proceed = await confirm({ message: "Proceed with installation?", default: true });
|
|
299
|
-
if (!proceed) {
|
|
300
|
-
console.log("\n Installation cancelled.\n");
|
|
301
|
-
process.exit(0);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// -- Step 7: Execute -------------------------------------------------------
|
|
305
|
-
console.log();
|
|
306
|
-
console.log(" Installing...");
|
|
307
|
-
console.log();
|
|
308
|
-
|
|
309
|
-
// -- Copy backend files ---
|
|
310
|
-
console.log(" Copying backend files...");
|
|
311
|
-
const backendFiles = [
|
|
312
|
-
{ src: "backend/services/file-storage.ts", dest: "src/services/file-storage.ts" },
|
|
313
|
-
{ src: "backend/routes/files.ts", dest: "src/routes/files.ts" },
|
|
314
|
-
{ src: "backend/schema/files.ts", dest: "src/db/schema/files.ts" },
|
|
315
|
-
];
|
|
316
|
-
for (const f of backendFiles) {
|
|
317
|
-
copyFile(join(SAIL_DIR, "files", f.src), join(BACKEND_ROOT, f.dest), f.dest);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
console.log();
|
|
321
|
-
console.log(" Copying frontend files...");
|
|
322
|
-
const frontendFiles = [
|
|
323
|
-
{ src: "frontend/hooks/useFileUpload.ts", dest: "src/hooks/useFileUpload.ts" },
|
|
324
|
-
{ src: "frontend/hooks/useFiles.ts", dest: "src/hooks/useFiles.ts" },
|
|
325
|
-
{ src: "frontend/components/FileUploadButton.tsx", dest: "src/components/files/FileUploadButton.tsx" },
|
|
326
|
-
{ src: "frontend/components/FileList.tsx", dest: "src/components/files/FileList.tsx" },
|
|
327
|
-
{ src: "frontend/pages/Files.tsx", dest: "src/pages/Files.tsx" },
|
|
328
|
-
];
|
|
329
|
-
for (const f of frontendFiles) {
|
|
330
|
-
copyFile(join(SAIL_DIR, "files", f.src), join(FRONTEND_ROOT, f.dest), f.dest);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// -- Modify backend markers ---
|
|
334
|
-
console.log();
|
|
335
|
-
console.log(" Modifying backend files...");
|
|
336
|
-
|
|
337
|
-
insertAtMarker(
|
|
338
|
-
join(BACKEND_ROOT, "src/db/schema/index.ts"),
|
|
339
|
-
"// [SAIL_SCHEMA]",
|
|
340
|
-
'export * from "./files.js";',
|
|
341
|
-
);
|
|
342
|
-
|
|
343
|
-
insertAtMarker(
|
|
344
|
-
join(BACKEND_ROOT, "src/index.ts"),
|
|
345
|
-
"// [SAIL_IMPORTS]",
|
|
346
|
-
'import { filesRouter } from "./routes/files.js";',
|
|
347
|
-
);
|
|
348
|
-
|
|
349
|
-
insertAtMarker(
|
|
350
|
-
join(BACKEND_ROOT, "src/index.ts"),
|
|
351
|
-
"// [SAIL_ROUTES]",
|
|
352
|
-
'app.use("/api/files", filesRouter);',
|
|
353
|
-
);
|
|
354
|
-
|
|
355
|
-
insertAtMarker(
|
|
356
|
-
join(BACKEND_ROOT, "src/env.ts"),
|
|
357
|
-
"// [SAIL_ENV_VARS]",
|
|
358
|
-
` S3_ENDPOINT: z.string().min(1, "S3_ENDPOINT is required"),\n S3_ACCESS_KEY_ID: z.string().min(1, "S3_ACCESS_KEY_ID is required"),\n S3_SECRET_ACCESS_KEY: z.string().min(1, "S3_SECRET_ACCESS_KEY is required"),\n S3_BUCKET_NAME: z.string().min(1, "S3_BUCKET_NAME is required"),\n S3_PUBLIC_URL: z.string().default(""),\n S3_REGION: z.string().default("auto"),`,
|
|
359
|
-
);
|
|
360
|
-
|
|
361
|
-
// -- Modify frontend router ---
|
|
362
|
-
console.log();
|
|
363
|
-
console.log(" Modifying frontend files...");
|
|
364
|
-
|
|
365
|
-
const routerPath = join(FRONTEND_ROOT, "src/router.tsx");
|
|
366
|
-
insertAtMarker(
|
|
367
|
-
routerPath,
|
|
368
|
-
"// [SAIL_IMPORTS]",
|
|
369
|
-
'import { FilesPage } from "./pages/Files";',
|
|
370
|
-
);
|
|
371
|
-
|
|
372
|
-
// The frontend marker uses JSX comment syntax
|
|
373
|
-
insertAtMarker(
|
|
374
|
-
routerPath,
|
|
375
|
-
"{/* [SAIL_ROUTES] */}",
|
|
376
|
-
` {\n path: "/files",\n element: <FilesPage />,\n },`,
|
|
377
|
-
);
|
|
378
|
-
|
|
379
|
-
// -- Install dependencies ---
|
|
380
|
-
console.log();
|
|
381
|
-
console.log(" Installing dependencies...");
|
|
382
|
-
installDeps(manifest.dependencies.backend, "packages/backend");
|
|
383
|
-
installDeps(manifest.dependencies.frontend, "packages/frontend");
|
|
384
|
-
|
|
385
|
-
// -- Generate migrations ---
|
|
386
|
-
console.log();
|
|
387
|
-
console.log(" Generating database migrations...");
|
|
388
|
-
try {
|
|
389
|
-
execSync("npx drizzle-kit generate", { cwd: BACKEND_ROOT, stdio: "inherit" });
|
|
390
|
-
} catch {
|
|
391
|
-
console.warn(" Warning: Could not generate migrations. Run manually:");
|
|
392
|
-
console.warn(" cd packages/backend && npx drizzle-kit generate");
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// -- Update env files ---
|
|
396
|
-
console.log();
|
|
397
|
-
console.log(" Updating environment files...");
|
|
398
|
-
appendToEnvFiles(
|
|
399
|
-
{
|
|
400
|
-
S3_ENDPOINT: s3Endpoint,
|
|
401
|
-
S3_ACCESS_KEY_ID: s3AccessKeyId,
|
|
402
|
-
S3_SECRET_ACCESS_KEY: s3SecretAccessKey,
|
|
403
|
-
S3_BUCKET_NAME: s3BucketName,
|
|
404
|
-
S3_PUBLIC_URL: s3PublicUrl,
|
|
405
|
-
S3_REGION: s3Region,
|
|
406
|
-
},
|
|
407
|
-
"File Uploads (S3-compatible storage)",
|
|
408
|
-
);
|
|
409
|
-
|
|
410
|
-
// -- Step 8: Next steps ----------------------------------------------------
|
|
411
|
-
console.log();
|
|
412
|
-
console.log("------------------------------------------------------------");
|
|
413
|
-
console.log(" File Uploads installed successfully!");
|
|
414
|
-
console.log("------------------------------------------------------------");
|
|
415
|
-
console.log();
|
|
416
|
-
console.log(" Next steps:");
|
|
417
|
-
console.log();
|
|
418
|
-
console.log(" 1. Run database migrations:");
|
|
419
|
-
console.log(" npm run db:migrate");
|
|
420
|
-
console.log();
|
|
421
|
-
console.log(" 2. Configure CORS on your storage bucket.");
|
|
422
|
-
console.log(" Your bucket must allow PUT requests from your frontend origin.");
|
|
423
|
-
console.log();
|
|
424
|
-
|
|
425
|
-
if (provider === "r2") {
|
|
426
|
-
console.log(" Cloudflare R2 CORS policy (bucket settings):");
|
|
427
|
-
console.log(" [");
|
|
428
|
-
console.log(" {");
|
|
429
|
-
console.log(' "AllowedOrigins": ["http://localhost:5173", "https://yourdomain.com"],');
|
|
430
|
-
console.log(' "AllowedMethods": ["GET", "PUT"],');
|
|
431
|
-
console.log(' "AllowedHeaders": ["Content-Type"],');
|
|
432
|
-
console.log(' "MaxAgeSeconds": 3600');
|
|
433
|
-
console.log(" }");
|
|
434
|
-
console.log(" ]");
|
|
435
|
-
} else if (provider === "s3") {
|
|
436
|
-
console.log(" AWS S3 CORS configuration (bucket permissions):");
|
|
437
|
-
console.log(" [");
|
|
438
|
-
console.log(" {");
|
|
439
|
-
console.log(' "AllowedHeaders": ["Content-Type"],');
|
|
440
|
-
console.log(' "AllowedMethods": ["GET", "PUT"],');
|
|
441
|
-
console.log(' "AllowedOrigins": ["http://localhost:5173", "https://yourdomain.com"],');
|
|
442
|
-
console.log(' "MaxAgeSeconds": 3600');
|
|
443
|
-
console.log(" }");
|
|
444
|
-
console.log(" ]");
|
|
445
|
-
} else if (provider === "minio") {
|
|
446
|
-
console.log(" MinIO: Set the bucket policy to allow public read");
|
|
447
|
-
console.log(" or configure CORS via mc admin:");
|
|
448
|
-
console.log(" mc admin config set local api cors_allow_origin=http://localhost:5173");
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
console.log();
|
|
452
|
-
console.log(" 3. Start your dev server:");
|
|
453
|
-
console.log(" npm run dev");
|
|
454
|
-
console.log();
|
|
455
|
-
console.log(" 4. Navigate to /files to test the file upload system.");
|
|
456
|
-
console.log();
|
|
457
|
-
console.log(` Max upload size is set to ${maxSizeMB} MB.`);
|
|
458
|
-
console.log(" To change it, update the maxSize prop on <FileUploadButton />");
|
|
459
|
-
console.log(" in packages/frontend/src/pages/Files.tsx.");
|
|
460
|
-
console.log();
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
main().catch((err) => {
|
|
464
|
-
console.error("Installation failed:", err);
|
|
465
|
-
process.exit(1);
|
|
466
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* File Uploads Sail Installer
|
|
3
|
+
*
|
|
4
|
+
* Adds a generic file upload system with S3-compatible storage.
|
|
5
|
+
* Supports Cloudflare R2, AWS S3, MinIO, and other S3-compatible providers.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npx tsx sails/file-uploads/install.ts
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
readFileSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
copyFileSync,
|
|
15
|
+
existsSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
} from "node:fs";
|
|
18
|
+
import { resolve, dirname, join } from "node:path";
|
|
19
|
+
import { execSync } from "node:child_process";
|
|
20
|
+
import { input, confirm, select } from "@inquirer/prompts";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Paths
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
const SAIL_DIR = dirname(new URL(import.meta.url).pathname);
|
|
27
|
+
const PROJECT_ROOT = resolve(SAIL_DIR, "../..");
|
|
28
|
+
const BACKEND_ROOT = join(PROJECT_ROOT, "packages/backend");
|
|
29
|
+
const FRONTEND_ROOT = join(PROJECT_ROOT, "packages/frontend");
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Helpers
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
interface SailManifest {
|
|
36
|
+
name: string;
|
|
37
|
+
displayName: string;
|
|
38
|
+
version: string;
|
|
39
|
+
requiredEnvVars: { key: string; description: string }[];
|
|
40
|
+
dependencies: { backend: Record<string, string>; frontend: Record<string, string> };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function loadManifest(): SailManifest {
|
|
44
|
+
return JSON.parse(readFileSync(join(SAIL_DIR, "addon.json"), "utf-8"));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function insertAtMarker(filePath: string, marker: string, code: string): void {
|
|
48
|
+
if (!existsSync(filePath)) {
|
|
49
|
+
console.warn(` Warning: File not found: ${filePath}`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
let content = readFileSync(filePath, "utf-8");
|
|
53
|
+
if (!content.includes(marker)) {
|
|
54
|
+
console.warn(` Warning: Marker "${marker}" not found in ${filePath}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (content.includes(code.trim())) {
|
|
58
|
+
console.log(` Skipped (already present) -> ${filePath}`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
content = content.replace(marker, `${marker}\n${code}`);
|
|
62
|
+
writeFileSync(filePath, content, "utf-8");
|
|
63
|
+
console.log(` Modified -> ${filePath}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function copyFile(src: string, dest: string, label: string): void {
|
|
67
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
68
|
+
copyFileSync(src, dest);
|
|
69
|
+
console.log(` Copied -> ${label}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function appendToEnvFiles(entries: Record<string, string>, section: string): void {
|
|
73
|
+
for (const envFile of [".env.example", ".env"]) {
|
|
74
|
+
const envPath = join(PROJECT_ROOT, envFile);
|
|
75
|
+
if (!existsSync(envPath)) continue;
|
|
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# ${section}\n${lines.join("\n")}\n`;
|
|
83
|
+
writeFileSync(envPath, content, "utf-8");
|
|
84
|
+
console.log(` Updated ${envFile}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function installDeps(deps: Record<string, string>, workspace: string): void {
|
|
90
|
+
const entries = Object.entries(deps);
|
|
91
|
+
if (entries.length === 0) return;
|
|
92
|
+
const packages = entries.map(([n, v]) => `${n}@${v}`).join(" ");
|
|
93
|
+
const cmd = `npm install ${packages} --workspace=${workspace}`;
|
|
94
|
+
console.log(` Running: ${cmd}`);
|
|
95
|
+
execSync(cmd, { cwd: PROJECT_ROOT, stdio: "inherit" });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Main
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
async function main(): Promise<void> {
|
|
103
|
+
const manifest = loadManifest();
|
|
104
|
+
|
|
105
|
+
// -- Step 1: Welcome -------------------------------------------------------
|
|
106
|
+
console.log("\n------------------------------------------------------------");
|
|
107
|
+
console.log(` File Uploads Sail Installer (v${manifest.version})`);
|
|
108
|
+
console.log("------------------------------------------------------------");
|
|
109
|
+
console.log();
|
|
110
|
+
console.log(" This sail adds a complete file upload system:");
|
|
111
|
+
console.log(" - S3-compatible storage (R2, S3, MinIO)");
|
|
112
|
+
console.log(" - Presigned URL uploads (files go directly to storage)");
|
|
113
|
+
console.log(" - File management API (list, download, delete)");
|
|
114
|
+
console.log(" - Database tracking of uploaded files");
|
|
115
|
+
console.log(" - React hooks for upload and file management");
|
|
116
|
+
console.log(" - File browser page with drag-and-drop upload");
|
|
117
|
+
console.log();
|
|
118
|
+
|
|
119
|
+
const pkgPath = join(PROJECT_ROOT, "package.json");
|
|
120
|
+
if (existsSync(pkgPath)) {
|
|
121
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
122
|
+
console.log(` Template version: ${pkg.version ?? "unknown"}`);
|
|
123
|
+
console.log();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// -- Step 2: Choose storage provider ---------------------------------------
|
|
127
|
+
const provider = await select({
|
|
128
|
+
message: "Which S3-compatible storage provider will you use?",
|
|
129
|
+
choices: [
|
|
130
|
+
{ name: "Cloudflare R2", value: "r2" },
|
|
131
|
+
{ name: "AWS S3", value: "s3" },
|
|
132
|
+
{ name: "MinIO (self-hosted)", value: "minio" },
|
|
133
|
+
{ name: "Other S3-compatible", value: "other" },
|
|
134
|
+
],
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// -- Provider-specific guidance -------------------------------------------
|
|
138
|
+
if (provider === "r2") {
|
|
139
|
+
console.log();
|
|
140
|
+
console.log(" Cloudflare R2 setup:");
|
|
141
|
+
console.log(" 1. Go to https://dash.cloudflare.com/?to=/:account/r2/new");
|
|
142
|
+
console.log(" 2. Create a bucket");
|
|
143
|
+
console.log(" 3. Go to R2 > Overview > Manage R2 API Tokens");
|
|
144
|
+
console.log(" 4. Create a token with Object Read & Write permissions");
|
|
145
|
+
console.log(" 5. Note your Account ID, Access Key ID, and Secret Access Key");
|
|
146
|
+
console.log();
|
|
147
|
+
} else if (provider === "s3") {
|
|
148
|
+
console.log();
|
|
149
|
+
console.log(" AWS S3 setup:");
|
|
150
|
+
console.log(" 1. Go to https://s3.console.aws.amazon.com/s3/buckets");
|
|
151
|
+
console.log(" 2. Create a bucket");
|
|
152
|
+
console.log(" 3. Create an IAM user with S3 access");
|
|
153
|
+
console.log(" 4. Note the Access Key ID and Secret Access Key");
|
|
154
|
+
console.log(" 5. Note your bucket's region (e.g., us-east-1)");
|
|
155
|
+
console.log();
|
|
156
|
+
} else if (provider === "minio") {
|
|
157
|
+
console.log();
|
|
158
|
+
console.log(" MinIO setup:");
|
|
159
|
+
console.log(" 1. Start MinIO: docker run -p 9000:9000 -p 9001:9001 minio/minio server /data --console-address ':9001'");
|
|
160
|
+
console.log(" 2. Open the console at http://localhost:9001");
|
|
161
|
+
console.log(" 3. Create a bucket");
|
|
162
|
+
console.log(" 4. Default credentials: minioadmin / minioadmin");
|
|
163
|
+
console.log();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const hasCredentials = await confirm({
|
|
167
|
+
message: "Do you have your storage credentials ready?",
|
|
168
|
+
default: false,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (!hasCredentials) {
|
|
172
|
+
console.log();
|
|
173
|
+
console.log(" Please set up your storage provider and obtain credentials,");
|
|
174
|
+
console.log(" then run this installer again.");
|
|
175
|
+
console.log();
|
|
176
|
+
|
|
177
|
+
await confirm({
|
|
178
|
+
message: "I have my credentials ready now, continue",
|
|
179
|
+
default: false,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// -- Step 3: Collect credentials -------------------------------------------
|
|
184
|
+
console.log();
|
|
185
|
+
console.log(" Enter your storage credentials:");
|
|
186
|
+
console.log();
|
|
187
|
+
|
|
188
|
+
// Compute defaults based on provider
|
|
189
|
+
const defaultEndpoint =
|
|
190
|
+
provider === "r2"
|
|
191
|
+
? "https://<account-id>.r2.cloudflarestorage.com"
|
|
192
|
+
: provider === "s3"
|
|
193
|
+
? "https://s3.<region>.amazonaws.com"
|
|
194
|
+
: provider === "minio"
|
|
195
|
+
? "http://localhost:9000"
|
|
196
|
+
: "";
|
|
197
|
+
|
|
198
|
+
const defaultRegion =
|
|
199
|
+
provider === "r2" ? "auto" : provider === "s3" ? "us-east-1" : "auto";
|
|
200
|
+
|
|
201
|
+
const s3Endpoint = await input({
|
|
202
|
+
message: "S3 Endpoint URL:",
|
|
203
|
+
default: defaultEndpoint,
|
|
204
|
+
validate: (value) => {
|
|
205
|
+
if (!value || value.trim().length === 0) return "Endpoint is required.";
|
|
206
|
+
if (!value.startsWith("http")) return "Endpoint should start with http:// or https://";
|
|
207
|
+
return true;
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const s3AccessKeyId = await input({
|
|
212
|
+
message: "S3 Access Key ID:",
|
|
213
|
+
validate: (value) => {
|
|
214
|
+
if (!value || value.trim().length === 0) return "Access Key ID is required.";
|
|
215
|
+
return true;
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const s3SecretAccessKey = await input({
|
|
220
|
+
message: "S3 Secret Access Key:",
|
|
221
|
+
validate: (value) => {
|
|
222
|
+
if (!value || value.trim().length === 0) return "Secret Access Key is required.";
|
|
223
|
+
return true;
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const s3BucketName = await input({
|
|
228
|
+
message: "S3 Bucket Name:",
|
|
229
|
+
default: "uploads",
|
|
230
|
+
validate: (value) => {
|
|
231
|
+
if (!value || value.trim().length === 0) return "Bucket name is required.";
|
|
232
|
+
return true;
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const s3PublicUrl = await input({
|
|
237
|
+
message: "S3 Public URL for serving files (leave blank if not public):",
|
|
238
|
+
default: "",
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const s3Region = await input({
|
|
242
|
+
message: "S3 Region:",
|
|
243
|
+
default: defaultRegion,
|
|
244
|
+
validate: (value) => {
|
|
245
|
+
if (!value || value.trim().length === 0) return "Region is required.";
|
|
246
|
+
return true;
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// -- Step 4: Max file size -------------------------------------------------
|
|
251
|
+
console.log();
|
|
252
|
+
const maxSizeInput = await input({
|
|
253
|
+
message: "Maximum upload file size in MB:",
|
|
254
|
+
default: "50",
|
|
255
|
+
validate: (value) => {
|
|
256
|
+
const n = Number(value);
|
|
257
|
+
if (isNaN(n) || n <= 0) return "Please enter a positive number.";
|
|
258
|
+
return true;
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
const maxSizeMB = Number(maxSizeInput);
|
|
262
|
+
|
|
263
|
+
// -- Step 5: Summary -------------------------------------------------------
|
|
264
|
+
console.log();
|
|
265
|
+
console.log(" Summary of changes:");
|
|
266
|
+
console.log(" -------------------");
|
|
267
|
+
console.log(" Files to create (backend):");
|
|
268
|
+
console.log(" + packages/backend/src/services/file-storage.ts");
|
|
269
|
+
console.log(" + packages/backend/src/routes/files.ts");
|
|
270
|
+
console.log(" + packages/backend/src/db/schema/files.ts");
|
|
271
|
+
console.log();
|
|
272
|
+
console.log(" Files to create (frontend):");
|
|
273
|
+
console.log(" + packages/frontend/src/hooks/useFileUpload.ts");
|
|
274
|
+
console.log(" + packages/frontend/src/hooks/useFiles.ts");
|
|
275
|
+
console.log(" + packages/frontend/src/components/files/FileUploadButton.tsx");
|
|
276
|
+
console.log(" + packages/frontend/src/components/files/FileList.tsx");
|
|
277
|
+
console.log(" + packages/frontend/src/pages/Files.tsx");
|
|
278
|
+
console.log();
|
|
279
|
+
console.log(" Files to modify:");
|
|
280
|
+
console.log(" ~ packages/backend/src/index.ts (import + mount routes)");
|
|
281
|
+
console.log(" ~ packages/backend/src/db/schema/index.ts (export schema)");
|
|
282
|
+
console.log(" ~ packages/backend/src/env.ts (add env vars)");
|
|
283
|
+
console.log(" ~ packages/frontend/src/router.tsx (add /files route)");
|
|
284
|
+
console.log(" ~ .env.example / .env");
|
|
285
|
+
console.log();
|
|
286
|
+
console.log(" Environment variables:");
|
|
287
|
+
console.log(` S3_ENDPOINT=${s3Endpoint.slice(0, 30)}...`);
|
|
288
|
+
console.log(` S3_ACCESS_KEY_ID=${s3AccessKeyId.slice(0, 8)}...`);
|
|
289
|
+
console.log(` S3_SECRET_ACCESS_KEY=${s3SecretAccessKey.slice(0, 8)}...`);
|
|
290
|
+
console.log(` S3_BUCKET_NAME=${s3BucketName}`);
|
|
291
|
+
console.log(` S3_PUBLIC_URL=${s3PublicUrl || "(not set)"}`);
|
|
292
|
+
console.log(` S3_REGION=${s3Region}`);
|
|
293
|
+
console.log();
|
|
294
|
+
console.log(` Max file size: ${maxSizeMB} MB`);
|
|
295
|
+
console.log();
|
|
296
|
+
|
|
297
|
+
// -- Step 6: Confirm -------------------------------------------------------
|
|
298
|
+
const proceed = await confirm({ message: "Proceed with installation?", default: true });
|
|
299
|
+
if (!proceed) {
|
|
300
|
+
console.log("\n Installation cancelled.\n");
|
|
301
|
+
process.exit(0);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// -- Step 7: Execute -------------------------------------------------------
|
|
305
|
+
console.log();
|
|
306
|
+
console.log(" Installing...");
|
|
307
|
+
console.log();
|
|
308
|
+
|
|
309
|
+
// -- Copy backend files ---
|
|
310
|
+
console.log(" Copying backend files...");
|
|
311
|
+
const backendFiles = [
|
|
312
|
+
{ src: "backend/services/file-storage.ts", dest: "src/services/file-storage.ts" },
|
|
313
|
+
{ src: "backend/routes/files.ts", dest: "src/routes/files.ts" },
|
|
314
|
+
{ src: "backend/schema/files.ts", dest: "src/db/schema/files.ts" },
|
|
315
|
+
];
|
|
316
|
+
for (const f of backendFiles) {
|
|
317
|
+
copyFile(join(SAIL_DIR, "files", f.src), join(BACKEND_ROOT, f.dest), f.dest);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
console.log();
|
|
321
|
+
console.log(" Copying frontend files...");
|
|
322
|
+
const frontendFiles = [
|
|
323
|
+
{ src: "frontend/hooks/useFileUpload.ts", dest: "src/hooks/useFileUpload.ts" },
|
|
324
|
+
{ src: "frontend/hooks/useFiles.ts", dest: "src/hooks/useFiles.ts" },
|
|
325
|
+
{ src: "frontend/components/FileUploadButton.tsx", dest: "src/components/files/FileUploadButton.tsx" },
|
|
326
|
+
{ src: "frontend/components/FileList.tsx", dest: "src/components/files/FileList.tsx" },
|
|
327
|
+
{ src: "frontend/pages/Files.tsx", dest: "src/pages/Files.tsx" },
|
|
328
|
+
];
|
|
329
|
+
for (const f of frontendFiles) {
|
|
330
|
+
copyFile(join(SAIL_DIR, "files", f.src), join(FRONTEND_ROOT, f.dest), f.dest);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// -- Modify backend markers ---
|
|
334
|
+
console.log();
|
|
335
|
+
console.log(" Modifying backend files...");
|
|
336
|
+
|
|
337
|
+
insertAtMarker(
|
|
338
|
+
join(BACKEND_ROOT, "src/db/schema/index.ts"),
|
|
339
|
+
"// [SAIL_SCHEMA]",
|
|
340
|
+
'export * from "./files.js";',
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
insertAtMarker(
|
|
344
|
+
join(BACKEND_ROOT, "src/index.ts"),
|
|
345
|
+
"// [SAIL_IMPORTS]",
|
|
346
|
+
'import { filesRouter } from "./routes/files.js";',
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
insertAtMarker(
|
|
350
|
+
join(BACKEND_ROOT, "src/index.ts"),
|
|
351
|
+
"// [SAIL_ROUTES]",
|
|
352
|
+
'app.use("/api/files", filesRouter);',
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
insertAtMarker(
|
|
356
|
+
join(BACKEND_ROOT, "src/env.ts"),
|
|
357
|
+
"// [SAIL_ENV_VARS]",
|
|
358
|
+
` S3_ENDPOINT: z.string().min(1, "S3_ENDPOINT is required"),\n S3_ACCESS_KEY_ID: z.string().min(1, "S3_ACCESS_KEY_ID is required"),\n S3_SECRET_ACCESS_KEY: z.string().min(1, "S3_SECRET_ACCESS_KEY is required"),\n S3_BUCKET_NAME: z.string().min(1, "S3_BUCKET_NAME is required"),\n S3_PUBLIC_URL: z.string().default(""),\n S3_REGION: z.string().default("auto"),`,
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
// -- Modify frontend router ---
|
|
362
|
+
console.log();
|
|
363
|
+
console.log(" Modifying frontend files...");
|
|
364
|
+
|
|
365
|
+
const routerPath = join(FRONTEND_ROOT, "src/router.tsx");
|
|
366
|
+
insertAtMarker(
|
|
367
|
+
routerPath,
|
|
368
|
+
"// [SAIL_IMPORTS]",
|
|
369
|
+
'import { FilesPage } from "./pages/Files";',
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
// The frontend marker uses JSX comment syntax
|
|
373
|
+
insertAtMarker(
|
|
374
|
+
routerPath,
|
|
375
|
+
"{/* [SAIL_ROUTES] */}",
|
|
376
|
+
` {\n path: "/files",\n element: <FilesPage />,\n },`,
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
// -- Install dependencies ---
|
|
380
|
+
console.log();
|
|
381
|
+
console.log(" Installing dependencies...");
|
|
382
|
+
installDeps(manifest.dependencies.backend, "packages/backend");
|
|
383
|
+
installDeps(manifest.dependencies.frontend, "packages/frontend");
|
|
384
|
+
|
|
385
|
+
// -- Generate migrations ---
|
|
386
|
+
console.log();
|
|
387
|
+
console.log(" Generating database migrations...");
|
|
388
|
+
try {
|
|
389
|
+
execSync("npx drizzle-kit generate", { cwd: BACKEND_ROOT, stdio: "inherit" });
|
|
390
|
+
} catch {
|
|
391
|
+
console.warn(" Warning: Could not generate migrations. Run manually:");
|
|
392
|
+
console.warn(" cd packages/backend && npx drizzle-kit generate");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// -- Update env files ---
|
|
396
|
+
console.log();
|
|
397
|
+
console.log(" Updating environment files...");
|
|
398
|
+
appendToEnvFiles(
|
|
399
|
+
{
|
|
400
|
+
S3_ENDPOINT: s3Endpoint,
|
|
401
|
+
S3_ACCESS_KEY_ID: s3AccessKeyId,
|
|
402
|
+
S3_SECRET_ACCESS_KEY: s3SecretAccessKey,
|
|
403
|
+
S3_BUCKET_NAME: s3BucketName,
|
|
404
|
+
S3_PUBLIC_URL: s3PublicUrl,
|
|
405
|
+
S3_REGION: s3Region,
|
|
406
|
+
},
|
|
407
|
+
"File Uploads (S3-compatible storage)",
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
// -- Step 8: Next steps ----------------------------------------------------
|
|
411
|
+
console.log();
|
|
412
|
+
console.log("------------------------------------------------------------");
|
|
413
|
+
console.log(" File Uploads installed successfully!");
|
|
414
|
+
console.log("------------------------------------------------------------");
|
|
415
|
+
console.log();
|
|
416
|
+
console.log(" Next steps:");
|
|
417
|
+
console.log();
|
|
418
|
+
console.log(" 1. Run database migrations:");
|
|
419
|
+
console.log(" npm run db:migrate");
|
|
420
|
+
console.log();
|
|
421
|
+
console.log(" 2. Configure CORS on your storage bucket.");
|
|
422
|
+
console.log(" Your bucket must allow PUT requests from your frontend origin.");
|
|
423
|
+
console.log();
|
|
424
|
+
|
|
425
|
+
if (provider === "r2") {
|
|
426
|
+
console.log(" Cloudflare R2 CORS policy (bucket settings):");
|
|
427
|
+
console.log(" [");
|
|
428
|
+
console.log(" {");
|
|
429
|
+
console.log(' "AllowedOrigins": ["http://localhost:5173", "https://yourdomain.com"],');
|
|
430
|
+
console.log(' "AllowedMethods": ["GET", "PUT"],');
|
|
431
|
+
console.log(' "AllowedHeaders": ["Content-Type"],');
|
|
432
|
+
console.log(' "MaxAgeSeconds": 3600');
|
|
433
|
+
console.log(" }");
|
|
434
|
+
console.log(" ]");
|
|
435
|
+
} else if (provider === "s3") {
|
|
436
|
+
console.log(" AWS S3 CORS configuration (bucket permissions):");
|
|
437
|
+
console.log(" [");
|
|
438
|
+
console.log(" {");
|
|
439
|
+
console.log(' "AllowedHeaders": ["Content-Type"],');
|
|
440
|
+
console.log(' "AllowedMethods": ["GET", "PUT"],');
|
|
441
|
+
console.log(' "AllowedOrigins": ["http://localhost:5173", "https://yourdomain.com"],');
|
|
442
|
+
console.log(' "MaxAgeSeconds": 3600');
|
|
443
|
+
console.log(" }");
|
|
444
|
+
console.log(" ]");
|
|
445
|
+
} else if (provider === "minio") {
|
|
446
|
+
console.log(" MinIO: Set the bucket policy to allow public read");
|
|
447
|
+
console.log(" or configure CORS via mc admin:");
|
|
448
|
+
console.log(" mc admin config set local api cors_allow_origin=http://localhost:5173");
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
console.log();
|
|
452
|
+
console.log(" 3. Start your dev server:");
|
|
453
|
+
console.log(" npm run dev");
|
|
454
|
+
console.log();
|
|
455
|
+
console.log(" 4. Navigate to /files to test the file upload system.");
|
|
456
|
+
console.log();
|
|
457
|
+
console.log(` Max upload size is set to ${maxSizeMB} MB.`);
|
|
458
|
+
console.log(" To change it, update the maxSize prop on <FileUploadButton />");
|
|
459
|
+
console.log(" in packages/frontend/src/pages/Files.tsx.");
|
|
460
|
+
console.log();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
main().catch((err) => {
|
|
464
|
+
console.error("Installation failed:", err);
|
|
465
|
+
process.exit(1);
|
|
466
|
+
});
|