create-kofi-stack 2.0.16 → 2.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +171 -10
  2. package/package.json +12 -12
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) || "none"
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 uploads provider?",
122
+ message: "File storage provider?",
116
123
  options: [
117
- { value: "none", label: "None", hint: "Skip file uploads" },
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: "none"
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: "Arcjet protection"
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("Uploads:")} ${uploads}`,
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>", "Upload provider: none, uploadthing, s3, vercel-blob").option("--rate-limiting", "Add rate limiting with Arcjet").option("--monitoring", "Add monitoring with Sentry").option("-y, --yes", "Skip prompts and use defaults").action(async (projectName, options) => {
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.16",
3
+ "version": "2.0.17",
4
4
  "description": "Scaffold opinionated full-stack projects with Next.js, Convex, Better-Auth, and more",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,22 +9,16 @@
9
9
  "files": [
10
10
  "dist"
11
11
  ],
12
- "scripts": {
13
- "build": "tsup src/index.ts --format esm --dts",
14
- "dev": "tsup src/index.ts --format esm --watch",
15
- "start": "node dist/index.js",
16
- "typecheck": "tsc --noEmit"
17
- },
18
12
  "dependencies": {
19
- "kofi-stack-template-generator": "^2.0.3",
20
- "kofi-stack-types": "^2.0.3",
21
13
  "@clack/prompts": "^0.7.0",
22
14
  "commander": "^12.0.0",
23
15
  "execa": "^8.0.0",
24
16
  "fs-extra": "^11.2.0",
25
17
  "gradient-string": "^2.0.2",
26
18
  "ora": "^8.0.0",
27
- "picocolors": "^1.0.0"
19
+ "picocolors": "^1.0.0",
20
+ "kofi-stack-template-generator": "2.0.17",
21
+ "kofi-stack-types": "2.0.17"
28
22
  },
29
23
  "devDependencies": {
30
24
  "@types/fs-extra": "^11.0.4",
@@ -56,5 +50,11 @@
56
50
  "bugs": {
57
51
  "url": "https://github.com/theodenanyoh11/create-kofi-stack/issues"
58
52
  },
59
- "homepage": "https://github.com/theodenanyoh11/create-kofi-stack#readme"
60
- }
53
+ "homepage": "https://github.com/theodenanyoh11/create-kofi-stack#readme",
54
+ "scripts": {
55
+ "build": "tsup src/index.ts --format esm --dts",
56
+ "dev": "tsup src/index.ts --format esm --watch",
57
+ "start": "node dist/index.js",
58
+ "typecheck": "tsc --noEmit"
59
+ }
60
+ }