create-kofi-stack 2.0.16 → 2.0.18
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/index.js +171 -10
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
2
8
|
|
|
3
9
|
// src/index.ts
|
|
4
10
|
import { Command } from "commander";
|
|
@@ -35,7 +41,8 @@ async function runPrompts(projectNameArg, options) {
|
|
|
35
41
|
marketingSite: validateMarketingSite(options.marketing) || "none",
|
|
36
42
|
integrations: {
|
|
37
43
|
analytics: validateAnalytics(options.analytics) || "none",
|
|
38
|
-
uploads: validateUploads(options.uploads) || "
|
|
44
|
+
uploads: validateUploads(options.uploads) || "convex-fs",
|
|
45
|
+
payments: validatePayments(options.payments) || "none"
|
|
39
46
|
},
|
|
40
47
|
addons: [
|
|
41
48
|
...options.rateLimiting ? ["rate-limiting"] : [],
|
|
@@ -112,23 +119,35 @@ async function runPrompts(projectNameArg, options) {
|
|
|
112
119
|
});
|
|
113
120
|
if (p.isCancel(analytics)) throw new Error("cancelled");
|
|
114
121
|
const uploads = await p.select({
|
|
115
|
-
message: "File
|
|
122
|
+
message: "File storage provider?",
|
|
116
123
|
options: [
|
|
117
|
-
{ value: "
|
|
124
|
+
{ value: "convex-fs", label: "Convex FS", hint: "Built-in Convex storage (Recommended)" },
|
|
125
|
+
{ value: "r2", label: "Cloudflare R2", hint: "S3-compatible edge storage" },
|
|
118
126
|
{ value: "uploadthing", label: "UploadThing", hint: "Easy file uploads" },
|
|
119
127
|
{ value: "s3", label: "AWS S3", hint: "S3-compatible storage" },
|
|
120
|
-
{ value: "vercel-blob", label: "Vercel Blob", hint: "Vercel storage" }
|
|
128
|
+
{ value: "vercel-blob", label: "Vercel Blob", hint: "Vercel storage" },
|
|
129
|
+
{ value: "none", label: "None", hint: "Skip file storage" }
|
|
121
130
|
],
|
|
122
|
-
initialValue: "
|
|
131
|
+
initialValue: "convex-fs"
|
|
123
132
|
});
|
|
124
133
|
if (p.isCancel(uploads)) throw new Error("cancelled");
|
|
134
|
+
const payments = await p.select({
|
|
135
|
+
message: "Payment provider?",
|
|
136
|
+
options: [
|
|
137
|
+
{ value: "none", label: "None", hint: "Skip payments" },
|
|
138
|
+
{ value: "stripe", label: "Stripe", hint: "Full payment platform" },
|
|
139
|
+
{ value: "polar", label: "Polar", hint: "Open source monetization" }
|
|
140
|
+
],
|
|
141
|
+
initialValue: "none"
|
|
142
|
+
});
|
|
143
|
+
if (p.isCancel(payments)) throw new Error("cancelled");
|
|
125
144
|
const addonsSelected = await p.multiselect({
|
|
126
145
|
message: "Additional features?",
|
|
127
146
|
options: [
|
|
128
147
|
{
|
|
129
148
|
value: "rate-limiting",
|
|
130
149
|
label: "Rate Limiting",
|
|
131
|
-
hint: "
|
|
150
|
+
hint: "Convex Rate Limiter (Recommended)"
|
|
132
151
|
},
|
|
133
152
|
{
|
|
134
153
|
value: "monitoring",
|
|
@@ -147,7 +166,8 @@ async function runPrompts(projectNameArg, options) {
|
|
|
147
166
|
structure === "monorepo" ? `${pc.cyan("Marketing:")} ${marketingSite}` : null,
|
|
148
167
|
`${pc.cyan("Base Color:")} ${baseColor}`,
|
|
149
168
|
`${pc.cyan("Analytics:")} ${analytics}`,
|
|
150
|
-
`${pc.cyan("
|
|
169
|
+
`${pc.cyan("Storage:")} ${uploads}`,
|
|
170
|
+
`${pc.cyan("Payments:")} ${payments}`,
|
|
151
171
|
addonsSelected.length > 0 ? `${pc.cyan("Addons:")} ${addonsSelected.join(", ")}` : null
|
|
152
172
|
].filter(Boolean).join("\n"),
|
|
153
173
|
"Configuration"
|
|
@@ -178,7 +198,8 @@ async function runPrompts(projectNameArg, options) {
|
|
|
178
198
|
},
|
|
179
199
|
integrations: {
|
|
180
200
|
analytics,
|
|
181
|
-
uploads
|
|
201
|
+
uploads,
|
|
202
|
+
payments
|
|
182
203
|
},
|
|
183
204
|
addons: addonsSelected,
|
|
184
205
|
packageManager: "pnpm"
|
|
@@ -200,7 +221,14 @@ function validateAnalytics(value) {
|
|
|
200
221
|
}
|
|
201
222
|
function validateUploads(value) {
|
|
202
223
|
if (!value) return void 0;
|
|
203
|
-
if (["none", "uploadthing", "s3", "vercel-blob"].includes(value)) {
|
|
224
|
+
if (["none", "convex-fs", "r2", "uploadthing", "s3", "vercel-blob"].includes(value)) {
|
|
225
|
+
return value;
|
|
226
|
+
}
|
|
227
|
+
return void 0;
|
|
228
|
+
}
|
|
229
|
+
function validatePayments(value) {
|
|
230
|
+
if (!value) return void 0;
|
|
231
|
+
if (["none", "stripe", "polar"].includes(value)) {
|
|
204
232
|
return value;
|
|
205
233
|
}
|
|
206
234
|
return void 0;
|
|
@@ -212,8 +240,28 @@ import pc2 from "picocolors";
|
|
|
212
240
|
import ora from "ora";
|
|
213
241
|
import fs from "fs-extra";
|
|
214
242
|
import path2 from "path";
|
|
243
|
+
import { execSync } from "child_process";
|
|
215
244
|
import { execa } from "execa";
|
|
216
245
|
import { generateVirtualProject } from "kofi-stack-template-generator";
|
|
246
|
+
function generateSecret() {
|
|
247
|
+
try {
|
|
248
|
+
return execSync("openssl rand -base64 32", { encoding: "utf-8" }).trim();
|
|
249
|
+
} catch {
|
|
250
|
+
const crypto = __require("crypto");
|
|
251
|
+
return crypto.randomBytes(32).toString("base64");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async function updateEnvWithSecrets(envPath, secrets) {
|
|
255
|
+
if (!await fs.pathExists(envPath)) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
let content = await fs.readFile(envPath, "utf-8");
|
|
259
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
260
|
+
const regex = new RegExp(`^(${key}=)(?:""|'')?$`, "m");
|
|
261
|
+
content = content.replace(regex, `$1"${value}"`);
|
|
262
|
+
}
|
|
263
|
+
await fs.writeFile(envPath, content, "utf-8");
|
|
264
|
+
}
|
|
217
265
|
async function generateProject(config, options = {}) {
|
|
218
266
|
const { skipPrompts = false } = options;
|
|
219
267
|
const spinner = ora();
|
|
@@ -236,6 +284,20 @@ async function generateProject(config, options = {}) {
|
|
|
236
284
|
spinner.start("Writing files to disk...");
|
|
237
285
|
await writeNodeToDisk(result.tree.root, config.targetDir);
|
|
238
286
|
spinner.succeed("Files written to disk");
|
|
287
|
+
spinner.start("Generating secrets...");
|
|
288
|
+
const backendEnvPath = config.structure === "monorepo" ? path2.join(config.targetDir, "packages/backend/.env.local") : path2.join(config.targetDir, ".env.local");
|
|
289
|
+
await updateEnvWithSecrets(backendEnvPath, {
|
|
290
|
+
BETTER_AUTH_SECRET: generateSecret()
|
|
291
|
+
});
|
|
292
|
+
if (config.marketingSite === "payload") {
|
|
293
|
+
const marketingEnvPath = path2.join(config.targetDir, "apps/marketing/.env.local");
|
|
294
|
+
await updateEnvWithSecrets(marketingEnvPath, {
|
|
295
|
+
PAYLOAD_SECRET: generateSecret(),
|
|
296
|
+
CRON_SECRET: generateSecret(),
|
|
297
|
+
PREVIEW_SECRET: generateSecret()
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
spinner.succeed("Secrets generated");
|
|
239
301
|
spinner.start("Initializing git repository...");
|
|
240
302
|
try {
|
|
241
303
|
await execa("git", ["init"], { cwd: config.targetDir });
|
|
@@ -284,6 +346,10 @@ async function generateProject(config, options = {}) {
|
|
|
284
346
|
});
|
|
285
347
|
console.log();
|
|
286
348
|
p2.log.success("Convex configured successfully!");
|
|
349
|
+
await setupResend(config, backendEnvPath);
|
|
350
|
+
if (config.marketingSite === "payload") {
|
|
351
|
+
await setupPayload(config);
|
|
352
|
+
}
|
|
287
353
|
} catch {
|
|
288
354
|
console.log();
|
|
289
355
|
p2.log.warn("Convex setup was interrupted. Run pnpm dev:setup to try again.");
|
|
@@ -321,6 +387,101 @@ async function generateProject(config, options = {}) {
|
|
|
321
387
|
throw error;
|
|
322
388
|
}
|
|
323
389
|
}
|
|
390
|
+
async function setupResend(config, backendEnvPath) {
|
|
391
|
+
console.log();
|
|
392
|
+
const setupResendNow = await p2.confirm({
|
|
393
|
+
message: "Would you like to set up Resend email now?",
|
|
394
|
+
initialValue: false
|
|
395
|
+
});
|
|
396
|
+
if (p2.isCancel(setupResendNow) || !setupResendNow) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
console.log();
|
|
400
|
+
p2.log.info(pc2.dim("Get your Resend credentials at https://resend.com"));
|
|
401
|
+
console.log();
|
|
402
|
+
const domain = await p2.text({
|
|
403
|
+
message: "Verified domain name (from https://resend.com/domains):",
|
|
404
|
+
placeholder: "example.com",
|
|
405
|
+
validate: (value) => {
|
|
406
|
+
if (!value) return "Domain is required";
|
|
407
|
+
return void 0;
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
if (p2.isCancel(domain)) return;
|
|
411
|
+
const fromEmail = await p2.text({
|
|
412
|
+
message: "Sending email address:",
|
|
413
|
+
placeholder: `noreply@${domain}`,
|
|
414
|
+
defaultValue: `noreply@${domain}`,
|
|
415
|
+
validate: (value) => {
|
|
416
|
+
if (!value) return "Email is required";
|
|
417
|
+
if (!value.includes("@")) return "Invalid email format";
|
|
418
|
+
return void 0;
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
if (p2.isCancel(fromEmail)) return;
|
|
422
|
+
const apiKey = await p2.password({
|
|
423
|
+
message: "Resend API key (from https://resend.com/api-keys):",
|
|
424
|
+
validate: (value) => {
|
|
425
|
+
if (!value) return "API key is required";
|
|
426
|
+
if (!value.startsWith("re_")) return "Invalid Resend API key format";
|
|
427
|
+
return void 0;
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
if (p2.isCancel(apiKey)) return;
|
|
431
|
+
await updateEnvWithSecrets(backendEnvPath, {
|
|
432
|
+
RESEND_API_KEY: apiKey,
|
|
433
|
+
RESEND_FROM_EMAIL: fromEmail
|
|
434
|
+
});
|
|
435
|
+
p2.log.success("Resend configured successfully!");
|
|
436
|
+
}
|
|
437
|
+
async function setupPayload(config) {
|
|
438
|
+
console.log();
|
|
439
|
+
const setupPayloadNow = await p2.confirm({
|
|
440
|
+
message: "Would you like to set up Payload CMS now?",
|
|
441
|
+
initialValue: true
|
|
442
|
+
});
|
|
443
|
+
if (p2.isCancel(setupPayloadNow) || !setupPayloadNow) {
|
|
444
|
+
console.log();
|
|
445
|
+
p2.log.info(pc2.dim("Run the following when ready:"));
|
|
446
|
+
p2.log.message(` ${pc2.cyan("1. Add DATABASE_URL to apps/marketing/.env.local")}`);
|
|
447
|
+
p2.log.message(` ${pc2.cyan("2. pnpm --filter @repo/marketing payload migrate")}`);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
console.log();
|
|
451
|
+
p2.log.info(pc2.dim("Payload CMS requires a PostgreSQL database."));
|
|
452
|
+
p2.log.message(pc2.dim("You can get a free database at:"));
|
|
453
|
+
p2.log.message(pc2.dim(" - https://neon.tech"));
|
|
454
|
+
p2.log.message(pc2.dim(" - https://supabase.com"));
|
|
455
|
+
p2.log.message(pc2.dim(" - https://railway.app"));
|
|
456
|
+
console.log();
|
|
457
|
+
const databaseUrl = await p2.text({
|
|
458
|
+
message: "PostgreSQL DATABASE_URL:",
|
|
459
|
+
placeholder: "postgresql://user:password@host:5432/database",
|
|
460
|
+
validate: (value) => {
|
|
461
|
+
if (!value) return "DATABASE_URL is required";
|
|
462
|
+
if (!value.startsWith("postgresql://") && !value.startsWith("postgres://")) {
|
|
463
|
+
return "Invalid PostgreSQL connection string";
|
|
464
|
+
}
|
|
465
|
+
return void 0;
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
if (p2.isCancel(databaseUrl)) return;
|
|
469
|
+
const marketingEnvPath = path2.join(config.targetDir, "apps/marketing/.env.local");
|
|
470
|
+
await updateEnvWithSecrets(marketingEnvPath, {
|
|
471
|
+
DATABASE_URL: databaseUrl
|
|
472
|
+
});
|
|
473
|
+
const spinner = ora("Running Payload migrations...").start();
|
|
474
|
+
try {
|
|
475
|
+
await execa("pnpm", ["--filter", "@repo/marketing", "payload", "migrate"], {
|
|
476
|
+
cwd: config.targetDir,
|
|
477
|
+
stdio: "pipe"
|
|
478
|
+
});
|
|
479
|
+
spinner.succeed("Payload migrations completed");
|
|
480
|
+
} catch {
|
|
481
|
+
spinner.warn("Failed to run migrations. Run pnpm --filter @repo/marketing payload migrate manually.");
|
|
482
|
+
}
|
|
483
|
+
p2.log.success("Payload CMS configured successfully!");
|
|
484
|
+
}
|
|
324
485
|
async function writeNodeToDisk(node, targetDir) {
|
|
325
486
|
if (node.type === "file") {
|
|
326
487
|
const filePath = path2.join(targetDir, node.name);
|
|
@@ -375,7 +536,7 @@ function displayBanner() {
|
|
|
375
536
|
console.log();
|
|
376
537
|
}
|
|
377
538
|
var program = new Command();
|
|
378
|
-
program.name("create-kofi-stack").description("Scaffold opinionated full-stack projects").version(VERSION).argument("[project-name]", "Name of your project").option("--standalone", "Create standalone project (default)").option("--monorepo", "Create monorepo with Turborepo").option("--marketing <type>", "Marketing site type: none, nextjs, payload").option("--analytics <type>", "Analytics: none, posthog, vercel").option("--uploads <type>", "
|
|
539
|
+
program.name("create-kofi-stack").description("Scaffold opinionated full-stack projects").version(VERSION).argument("[project-name]", "Name of your project").option("--standalone", "Create standalone project (default)").option("--monorepo", "Create monorepo with Turborepo").option("--marketing <type>", "Marketing site type: none, nextjs, payload").option("--analytics <type>", "Analytics: none, posthog, vercel").option("--uploads <type>", "Storage: convex-fs, r2, uploadthing, s3, vercel-blob, none").option("--payments <type>", "Payments: none, stripe, polar").option("--rate-limiting", "Add rate limiting with Convex Rate Limiter").option("--monitoring", "Add monitoring with Sentry").option("-y, --yes", "Skip prompts and use defaults").action(async (projectName, options) => {
|
|
379
540
|
displayBanner();
|
|
380
541
|
p3.intro(pc3.bgCyan(pc3.black(" create-kofi-stack ")));
|
|
381
542
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-kofi-stack",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.18",
|
|
4
4
|
"description": "Scaffold opinionated full-stack projects with Next.js, Convex, Better-Auth, and more",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"typecheck": "tsc --noEmit"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"kofi-stack-template-generator": "
|
|
20
|
-
"kofi-stack-types": "
|
|
19
|
+
"kofi-stack-template-generator": "workspace:*",
|
|
20
|
+
"kofi-stack-types": "workspace:*",
|
|
21
21
|
"@clack/prompts": "^0.7.0",
|
|
22
22
|
"commander": "^12.0.0",
|
|
23
23
|
"execa": "^8.0.0",
|