create-whop-kit 0.2.0 → 0.4.0

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/cli-create.js +376 -98
  2. package/package.json +1 -1
@@ -11,44 +11,68 @@ import { runMain } from "citty";
11
11
 
12
12
  // src/commands/init.ts
13
13
  import { resolve, basename as basename2 } from "path";
14
- import { existsSync as existsSync4 } from "fs";
15
- import * as p2 from "@clack/prompts";
16
- import pc2 from "picocolors";
14
+ import { existsSync as existsSync5 } from "fs";
15
+ import * as p5 from "@clack/prompts";
16
+ import pc5 from "picocolors";
17
17
  import { defineCommand } from "citty";
18
18
 
19
19
  // src/templates.ts
20
- var TEMPLATES = {
20
+ var FRAMEWORKS = {
21
21
  nextjs: {
22
22
  name: "Next.js",
23
23
  description: "Full-stack React with App Router, SSR, and API routes",
24
- repo: "colinmcdermott/whop-saas-starter-v2",
25
24
  available: true
26
25
  },
27
26
  astro: {
28
27
  name: "Astro",
29
28
  description: "Content-focused with islands architecture",
30
- repo: "colinmcdermott/whop-astro-starter",
31
29
  available: true
32
30
  },
33
31
  tanstack: {
34
32
  name: "TanStack Start",
35
33
  description: "Full-stack React with TanStack Router",
36
- repo: "",
37
34
  available: false
38
35
  },
39
36
  vite: {
40
37
  name: "Vite + React",
41
38
  description: "Lightweight SPA with Vite bundler",
42
- repo: "",
43
39
  available: false
44
40
  }
45
41
  };
42
+ var TEMPLATES = {
43
+ "saas:nextjs": {
44
+ name: "Next.js SaaS",
45
+ description: "Full SaaS with dashboard, pricing, billing, and docs",
46
+ repo: "colinmcdermott/whop-saas-starter-v2",
47
+ available: true
48
+ },
49
+ "saas:astro": {
50
+ name: "Astro SaaS",
51
+ description: "SaaS with auth, payments, and webhooks",
52
+ repo: "colinmcdermott/whop-astro-starter",
53
+ available: true
54
+ },
55
+ "blank:nextjs": {
56
+ name: "Next.js Blank",
57
+ description: "Just auth + webhooks \u2014 build anything",
58
+ repo: "colinmcdermott/whop-blank-starter",
59
+ available: true
60
+ }
61
+ };
62
+ function getTemplate(appType, framework) {
63
+ return TEMPLATES[`${appType}:${framework}`] ?? null;
64
+ }
46
65
  var APP_TYPES = {
47
66
  saas: {
48
67
  name: "SaaS",
49
68
  description: "Subscription tiers, dashboard, billing portal",
50
69
  available: true
51
70
  },
71
+ blank: {
72
+ name: "Blank",
73
+ description: "Just auth + payments, you build the rest",
74
+ available: true
75
+ },
52
76
  course: {
53
77
  name: "Course",
54
78
  description: "Lessons, progress tracking, drip content",
@@ -58,52 +82,269 @@ var APP_TYPES = {
58
82
  name: "Community",
59
83
  description: "Member feeds, gated content, roles",
60
84
  available: false
85
+ }
86
+ };
87
+
88
+ // src/providers/neon.ts
89
+ import * as p from "@clack/prompts";
90
+ import pc from "picocolors";
91
+ var neonProvider = {
92
+ name: "Neon",
93
+ description: "Serverless Postgres \u2014 free tier, scales to zero",
94
+ isInstalled() {
95
+ return hasCommand("neonctl") || hasCommand("neon");
61
96
  },
62
- blank: {
63
- name: "Blank",
64
- description: "Just auth + payments, you build the rest",
65
- available: false
97
+ async install() {
98
+ const s = p.spinner();
99
+ s.start("Installing neonctl...");
100
+ const result = exec("npm install -g neonctl");
101
+ if (result.success) {
102
+ s.stop("neonctl installed");
103
+ return true;
104
+ }
105
+ s.stop("Failed to install neonctl");
106
+ p.log.error(`Install manually: ${pc.bold("npm install -g neonctl")}`);
107
+ return false;
108
+ },
109
+ async provision(projectName) {
110
+ const cli = hasCommand("neonctl") ? "neonctl" : "neon";
111
+ const whoami = exec(`${cli} me`);
112
+ if (!whoami.success) {
113
+ p.log.info("You need to authenticate with Neon.");
114
+ p.log.info(`Running ${pc.bold(`${cli} auth`)} \u2014 this will open your browser.`);
115
+ const authResult = exec(`${cli} auth`);
116
+ if (!authResult.success) {
117
+ p.log.error("Neon authentication failed. Try running manually:");
118
+ p.log.info(pc.bold(` ${cli} auth`));
119
+ return null;
120
+ }
121
+ }
122
+ const s = p.spinner();
123
+ s.start(`Creating Neon project "${projectName}"...`);
124
+ const createResult = exec(
125
+ `${cli} projects create --name "${projectName}" --set-context --output json`
126
+ );
127
+ if (!createResult.success) {
128
+ s.stop("Failed to create Neon project");
129
+ p.log.error("Try creating manually at https://console.neon.tech");
130
+ return null;
131
+ }
132
+ s.stop("Neon project created");
133
+ s.start("Getting connection string...");
134
+ const connResult = exec(`${cli} connection-string --prisma`);
135
+ if (!connResult.success) {
136
+ s.stop("Could not retrieve connection string");
137
+ p.log.error("Get it from: https://console.neon.tech");
138
+ return null;
139
+ }
140
+ s.stop("Connection string retrieved");
141
+ return {
142
+ connectionString: connResult.stdout,
143
+ provider: "neon"
144
+ };
66
145
  }
67
146
  };
68
- var DB_OPTIONS = {
69
- neon: {
70
- name: "Neon",
71
- description: "Serverless Postgres (recommended)",
72
- envVarHint: "postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/dbname?sslmode=require"
147
+
148
+ // src/providers/supabase.ts
149
+ import * as p2 from "@clack/prompts";
150
+ import pc2 from "picocolors";
151
+ var supabaseProvider = {
152
+ name: "Supabase",
153
+ description: "Open-source Firebase alternative with Postgres",
154
+ isInstalled() {
155
+ return hasCommand("supabase");
73
156
  },
74
- supabase: {
75
- name: "Supabase",
76
- description: "Open-source Firebase alternative",
77
- envVarHint: "postgresql://postgres.xxx:pass@aws-0-us-east-1.pooler.supabase.com:6543/postgres"
157
+ async install() {
158
+ const s = p2.spinner();
159
+ s.start("Checking Supabase CLI...");
160
+ if (hasCommand("brew")) {
161
+ s.message("Installing via Homebrew...");
162
+ const result = exec("brew install supabase/tap/supabase");
163
+ if (result.success) {
164
+ s.stop("Supabase CLI installed");
165
+ return true;
166
+ }
167
+ }
168
+ s.stop("Supabase CLI not found");
169
+ p2.log.info("Install the Supabase CLI:");
170
+ p2.log.info(pc2.bold(" brew install supabase/tap/supabase"));
171
+ p2.log.info(pc2.dim(" or use npx supabase <command>"));
172
+ const useNpx = await p2.confirm({
173
+ message: "Continue with npx supabase? (slower but works without install)",
174
+ initialValue: true
175
+ });
176
+ if (p2.isCancel(useNpx) || !useNpx) return false;
177
+ return true;
78
178
  },
79
- local: {
80
- name: "Local PostgreSQL",
81
- description: "Your own Postgres instance",
82
- envVarHint: "postgresql://postgres:postgres@localhost:5432/myapp"
179
+ async provision(projectName) {
180
+ const cli = hasCommand("supabase") ? "supabase" : "npx supabase";
181
+ const projectsList = exec(`${cli} projects list`);
182
+ if (!projectsList.success) {
183
+ p2.log.info("You need to authenticate with Supabase.");
184
+ p2.log.info(`Running ${pc2.bold(`${cli} login`)} \u2014 this will open your browser.`);
185
+ const authResult = exec(`${cli} login`);
186
+ if (!authResult.success) {
187
+ p2.log.error("Supabase authentication failed. Try:");
188
+ p2.log.info(pc2.bold(` ${cli} login`));
189
+ return null;
190
+ }
191
+ }
192
+ const orgsResult = exec(`${cli} orgs list`);
193
+ let orgId = "";
194
+ if (orgsResult.success && orgsResult.stdout) {
195
+ const lines = orgsResult.stdout.split("\n").filter((l) => l.trim());
196
+ if (lines.length > 1) {
197
+ const orgInput = await p2.text({
198
+ message: "Supabase Organization ID",
199
+ placeholder: "Find in dashboard: supabase.com/dashboard",
200
+ validate: (v) => !v ? "Required" : void 0
201
+ });
202
+ if (p2.isCancel(orgInput)) return null;
203
+ orgId = orgInput;
204
+ }
205
+ }
206
+ const password = Array.from(crypto.getRandomValues(new Uint8Array(16))).map((b) => b.toString(16).padStart(2, "0")).join("");
207
+ const s = p2.spinner();
208
+ s.start(`Creating Supabase project "${projectName}"...`);
209
+ const createCmd = orgId ? `${cli} projects create "${projectName}" --org-id "${orgId}" --db-password "${password}" --region us-east-1` : `${cli} projects create "${projectName}" --db-password "${password}" --region us-east-1`;
210
+ const createResult = exec(createCmd);
211
+ if (!createResult.success) {
212
+ s.stop("Failed to create Supabase project");
213
+ p2.log.error("Create manually at: https://supabase.com/dashboard");
214
+ return null;
215
+ }
216
+ s.stop("Supabase project created");
217
+ p2.log.info("");
218
+ p2.log.info(pc2.bold("Get your connection string:"));
219
+ p2.log.info(" 1. Go to https://supabase.com/dashboard");
220
+ p2.log.info(" 2. Select your new project");
221
+ p2.log.info(' 3. Click "Connect" \u2192 "Session pooler"');
222
+ p2.log.info(" 4. Copy the connection string");
223
+ p2.log.info("");
224
+ const connString = await p2.text({
225
+ message: "Paste your Supabase connection string",
226
+ placeholder: "postgresql://postgres.[ref]:[password]@...",
227
+ validate: (v) => {
228
+ if (!v) return "Required";
229
+ if (!v.startsWith("postgres")) return "Must be a PostgreSQL connection string";
230
+ }
231
+ });
232
+ if (p2.isCancel(connString)) return null;
233
+ return {
234
+ connectionString: connString,
235
+ provider: "supabase"
236
+ };
237
+ }
238
+ };
239
+
240
+ // src/providers/prisma-postgres.ts
241
+ import * as p3 from "@clack/prompts";
242
+ import pc3 from "picocolors";
243
+ import { readFileSync, existsSync } from "fs";
244
+ var prismaPostgresProvider = {
245
+ name: "Prisma Postgres",
246
+ description: "Instant Postgres \u2014 no auth needed, free tier",
247
+ isInstalled() {
248
+ return true;
83
249
  },
84
- later: {
85
- name: "Configure later",
86
- description: "Skip database setup for now",
87
- envVarHint: ""
250
+ async install() {
251
+ return true;
252
+ },
253
+ async provision(projectName) {
254
+ const region = await p3.select({
255
+ message: "Prisma Postgres region",
256
+ options: [
257
+ { value: "us-east-1", label: "US East (Virginia)", hint: "default" },
258
+ { value: "us-west-1", label: "US West (Oregon)" },
259
+ { value: "eu-central-1", label: "EU Central (Frankfurt)" },
260
+ { value: "eu-west-3", label: "EU West (Paris)" },
261
+ { value: "ap-southeast-1", label: "Asia Pacific (Singapore)" },
262
+ { value: "ap-northeast-1", label: "Asia Pacific (Tokyo)" }
263
+ ]
264
+ });
265
+ if (p3.isCancel(region)) return null;
266
+ const s = p3.spinner();
267
+ s.start("Creating Prisma Postgres database...");
268
+ const result = exec(`npx create-db@latest --region ${region} --json`);
269
+ if (!result.success) {
270
+ const fallback = exec(`npx -y create-db@latest --region ${region}`);
271
+ if (!fallback.success) {
272
+ s.stop("Failed to create database");
273
+ p3.log.error("Try manually: " + pc3.bold("npx create-db@latest"));
274
+ return null;
275
+ }
276
+ const match = fallback.stdout.match(/postgresql:\/\/[^\s"]+/);
277
+ if (match) {
278
+ s.stop("Prisma Postgres database created");
279
+ return {
280
+ connectionString: match[0],
281
+ provider: "prisma-postgres",
282
+ note: "This is a temporary database (24h). Claim it via the link in the output to make it permanent."
283
+ };
284
+ }
285
+ }
286
+ try {
287
+ const data = JSON.parse(result.stdout);
288
+ if (data.connectionString || data.url) {
289
+ s.stop("Prisma Postgres database created");
290
+ return {
291
+ connectionString: data.connectionString || data.url,
292
+ provider: "prisma-postgres",
293
+ note: "This is a temporary database (24h). Claim it to make it permanent."
294
+ };
295
+ }
296
+ } catch {
297
+ const match = result.stdout.match(/postgresql:\/\/[^\s"]+/);
298
+ if (match) {
299
+ s.stop("Prisma Postgres database created");
300
+ return {
301
+ connectionString: match[0],
302
+ provider: "prisma-postgres",
303
+ note: "This is a temporary database (24h). Claim it to make it permanent."
304
+ };
305
+ }
306
+ }
307
+ if (existsSync(".env")) {
308
+ const envContent = readFileSync(".env", "utf-8");
309
+ const match = envContent.match(/DATABASE_URL="?(postgresql:\/\/[^\s"]+)"?/);
310
+ if (match) {
311
+ s.stop("Prisma Postgres database created");
312
+ return {
313
+ connectionString: match[1],
314
+ provider: "prisma-postgres",
315
+ note: "This is a temporary database (24h). Claim it to make it permanent."
316
+ };
317
+ }
318
+ }
319
+ s.stop("Database created but could not extract connection string");
320
+ p3.log.warning("Check the output above for your DATABASE_URL, or visit https://console.prisma.io");
321
+ return null;
88
322
  }
89
323
  };
90
324
 
325
+ // src/providers/index.ts
326
+ var DB_PROVIDERS = {
327
+ neon: neonProvider,
328
+ supabase: supabaseProvider,
329
+ "prisma-postgres": prismaPostgresProvider
330
+ };
331
+
91
332
  // src/utils/checks.ts
92
- import * as p from "@clack/prompts";
93
- import pc from "picocolors";
333
+ import * as p4 from "@clack/prompts";
334
+ import pc4 from "picocolors";
94
335
  function checkNodeVersion(minimum = 18) {
95
336
  const major = parseInt(process.versions.node.split(".")[0], 10);
96
337
  if (major < minimum) {
97
- p.log.error(
98
- `Node.js ${pc.bold(`v${minimum}+`)} is required. You have ${pc.bold(`v${process.versions.node}`)}.`
338
+ p4.log.error(
339
+ `Node.js ${pc4.bold(`v${minimum}+`)} is required. You have ${pc4.bold(`v${process.versions.node}`)}.`
99
340
  );
100
341
  process.exit(1);
101
342
  }
102
343
  }
103
344
  function checkGit() {
104
345
  if (!hasCommand("git")) {
105
- p.log.error(
106
- `${pc.bold("git")} is required but not found. Install it from ${pc.cyan("https://git-scm.com")}`
346
+ p4.log.error(
347
+ `${pc4.bold("git")} is required but not found. Install it from ${pc4.cyan("https://git-scm.com")}`
107
348
  );
108
349
  process.exit(1);
109
350
  }
@@ -122,9 +363,9 @@ function validateWhopAppId(id) {
122
363
  }
123
364
 
124
365
  // src/utils/cleanup.ts
125
- import { rmSync, existsSync } from "fs";
366
+ import { rmSync, existsSync as existsSync2 } from "fs";
126
367
  function cleanupDir(dir) {
127
- if (existsSync(dir)) {
368
+ if (existsSync2(dir)) {
128
369
  try {
129
370
  rmSync(dir, { recursive: true, force: true });
130
371
  } catch {
@@ -133,25 +374,25 @@ function cleanupDir(dir) {
133
374
  }
134
375
 
135
376
  // src/scaffolding/clone.ts
136
- import { existsSync as existsSync2, readFileSync, writeFileSync, rmSync as rmSync2 } from "fs";
377
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, rmSync as rmSync2 } from "fs";
137
378
  import { join, basename } from "path";
138
379
  function cloneTemplate(repo, projectDir) {
139
380
  const result = exec(
140
381
  `git clone --depth 1 https://github.com/${repo}.git "${projectDir}"`
141
382
  );
142
- if (!result.success || !existsSync2(projectDir)) {
383
+ if (!result.success || !existsSync3(projectDir)) {
143
384
  return false;
144
385
  }
145
386
  const gitDir = join(projectDir, ".git");
146
- if (existsSync2(gitDir)) {
387
+ if (existsSync3(gitDir)) {
147
388
  rmSync2(gitDir, { recursive: true, force: true });
148
389
  }
149
390
  return true;
150
391
  }
151
392
  function updatePackageName(projectDir, name) {
152
393
  const pkgPath = join(projectDir, "package.json");
153
- if (existsSync2(pkgPath)) {
154
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
394
+ if (existsSync3(pkgPath)) {
395
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
155
396
  pkg.name = basename(name);
156
397
  writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
157
398
  }
@@ -163,7 +404,7 @@ function initGit(projectDir) {
163
404
  }
164
405
 
165
406
  // src/scaffolding/env-file.ts
166
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3 } from "fs";
407
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync4 } from "fs";
167
408
  import { join as join2 } from "path";
168
409
  function writeEnvFile(projectDir, values) {
169
410
  const examplePath = join2(projectDir, ".env.example");
@@ -171,8 +412,8 @@ function writeEnvFile(projectDir, values) {
171
412
  const filled = Object.fromEntries(
172
413
  Object.entries(values).filter(([, v]) => v)
173
414
  );
174
- if (existsSync3(examplePath)) {
175
- let content = readFileSync2(examplePath, "utf-8");
415
+ if (existsSync4(examplePath)) {
416
+ let content = readFileSync3(examplePath, "utf-8");
176
417
  for (const [key, value] of Object.entries(filled)) {
177
418
  const pattern = new RegExp(
178
419
  `^(#\\s*)?${escapeRegex(key)}=.*$`,
@@ -196,7 +437,7 @@ function escapeRegex(str) {
196
437
 
197
438
  // src/commands/init.ts
198
439
  function isCancelled(value) {
199
- return p2.isCancel(value);
440
+ return p5.isCancel(value);
200
441
  }
201
442
  var init_default = defineCommand({
202
443
  meta: {
@@ -260,94 +501,127 @@ var init_default = defineCommand({
260
501
  checkNodeVersion(18);
261
502
  checkGit();
262
503
  console.log("");
263
- p2.intro(`${pc2.bgCyan(pc2.black(" create-whop-kit "))} Create a Whop-powered app`);
504
+ p5.intro(`${pc5.bgCyan(pc5.black(" create-whop-kit "))} Create a Whop-powered app`);
264
505
  const isNonInteractive = !!(args.framework && args.db);
265
506
  let projectName = args.name;
266
507
  if (!projectName) {
267
- const result = await p2.text({
508
+ const result = await p5.text({
268
509
  message: "Project name",
269
510
  placeholder: "my-whop-app",
270
511
  validate: (v) => {
271
512
  if (!v) return "Project name is required";
272
- if (existsSync4(resolve(v))) return `Directory "${v}" already exists`;
513
+ if (existsSync5(resolve(v))) return `Directory "${v}" already exists`;
273
514
  }
274
515
  });
275
516
  if (isCancelled(result)) {
276
- p2.cancel("Cancelled.");
517
+ p5.cancel("Cancelled.");
277
518
  process.exit(0);
278
519
  }
279
520
  projectName = result;
280
- } else if (existsSync4(resolve(projectName))) {
281
- p2.log.error(`Directory "${projectName}" already exists`);
521
+ } else if (existsSync5(resolve(projectName))) {
522
+ p5.log.error(`Directory "${projectName}" already exists`);
282
523
  process.exit(1);
283
524
  }
284
525
  let appType = args.type;
285
526
  if (!isNonInteractive && !args.yes) {
286
- const result = await p2.select({
527
+ const result = await p5.select({
287
528
  message: "What are you building?",
288
529
  options: Object.entries(APP_TYPES).map(([value, t]) => ({
289
530
  value,
290
- label: t.available ? t.name : `${t.name} ${pc2.dim("(coming soon)")}`,
531
+ label: t.available ? t.name : `${t.name} ${pc5.dim("(coming soon)")}`,
291
532
  hint: t.description,
292
533
  disabled: !t.available
293
534
  }))
294
535
  });
295
536
  if (isCancelled(result)) {
296
- p2.cancel("Cancelled.");
537
+ p5.cancel("Cancelled.");
297
538
  process.exit(0);
298
539
  }
299
540
  appType = result;
300
541
  }
301
542
  let framework = args.framework;
302
543
  if (!framework) {
303
- const result = await p2.select({
544
+ const result = await p5.select({
304
545
  message: "Which framework?",
305
- options: Object.entries(TEMPLATES).map(([value, t]) => ({
546
+ options: Object.entries(FRAMEWORKS).map(([value, f]) => ({
306
547
  value,
307
- label: t.available ? t.name : `${t.name} ${pc2.dim("(coming soon)")}`,
308
- hint: t.description,
309
- disabled: !t.available
548
+ label: f.available ? f.name : `${f.name} ${pc5.dim("(coming soon)")}`,
549
+ hint: f.description,
550
+ disabled: !f.available
310
551
  }))
311
552
  });
312
553
  if (isCancelled(result)) {
313
- p2.cancel("Cancelled.");
554
+ p5.cancel("Cancelled.");
314
555
  process.exit(0);
315
556
  }
316
557
  framework = result;
317
558
  }
318
- const template = TEMPLATES[framework];
559
+ const template = getTemplate(appType, framework);
319
560
  if (!template || !template.available) {
320
- p2.log.error(`Framework "${framework}" is not available. Options: ${Object.keys(TEMPLATES).filter((k) => TEMPLATES[k].available).join(", ")}`);
561
+ p5.log.error(`No template available for ${appType} + ${framework}. Try a different combination.`);
321
562
  process.exit(1);
322
563
  }
323
564
  let database = args.db;
324
565
  if (!database) {
325
- const result = await p2.select({
566
+ const result = await p5.select({
326
567
  message: "Which database?",
327
- options: Object.entries(DB_OPTIONS).map(([value, d]) => ({
328
- value,
329
- label: d.name,
330
- hint: d.description
331
- }))
568
+ options: [
569
+ { value: "neon", label: "Neon", hint: "Serverless Postgres \u2014 auto-provisioned (recommended)" },
570
+ { value: "prisma-postgres", label: "Prisma Postgres", hint: "Instant database \u2014 no account needed" },
571
+ { value: "supabase", label: "Supabase", hint: "Open-source Firebase alternative" },
572
+ { value: "manual", label: "I have a connection string", hint: "Paste an existing PostgreSQL URL" },
573
+ { value: "later", label: "Configure later", hint: "Skip database setup for now" }
574
+ ]
332
575
  });
333
576
  if (isCancelled(result)) {
334
- p2.cancel("Cancelled.");
577
+ p5.cancel("Cancelled.");
335
578
  process.exit(0);
336
579
  }
337
580
  database = result;
338
581
  }
339
582
  let dbUrl = args["db-url"] ?? "";
340
- if (database !== "later" && !dbUrl) {
341
- const result = await p2.text({
583
+ let dbNote = "";
584
+ if (database !== "later" && database !== "manual" && !dbUrl) {
585
+ const provider = DB_PROVIDERS[database];
586
+ if (provider) {
587
+ if (!provider.isInstalled()) {
588
+ const install = await p5.confirm({
589
+ message: `${provider.name} CLI not found. Install it now?`,
590
+ initialValue: true
591
+ });
592
+ if (isCancelled(install) || !install) {
593
+ p5.log.warning("Skipping database provisioning. You can configure it later.");
594
+ database = "later";
595
+ } else {
596
+ const installed = await provider.install();
597
+ if (!installed) {
598
+ p5.log.warning("Skipping database provisioning.");
599
+ database = "later";
600
+ }
601
+ }
602
+ }
603
+ if (database !== "later") {
604
+ const result = await provider.provision(projectName);
605
+ if (result) {
606
+ dbUrl = result.connectionString;
607
+ if (result.note) dbNote = result.note;
608
+ p5.log.success(`${pc5.bold(provider.name)} database ready`);
609
+ } else {
610
+ p5.log.warning("Database provisioning skipped. Configure manually later.");
611
+ }
612
+ }
613
+ }
614
+ } else if (database === "manual" && !dbUrl) {
615
+ const result = await p5.text({
342
616
  message: "Database URL",
343
- placeholder: DB_OPTIONS[database]?.envVarHint ?? "postgresql://...",
617
+ placeholder: "postgresql://user:pass@host:5432/dbname",
344
618
  validate: (v) => {
345
619
  if (!v) return "Required (choose 'Configure later' to skip)";
346
620
  return validateDatabaseUrl(v);
347
621
  }
348
622
  });
349
623
  if (isCancelled(result)) {
350
- p2.cancel("Cancelled.");
624
+ p5.cancel("Cancelled.");
351
625
  process.exit(0);
352
626
  }
353
627
  dbUrl = result;
@@ -356,13 +630,13 @@ var init_default = defineCommand({
356
630
  let apiKey = args["api-key"] ?? "";
357
631
  let webhookSecret = args["webhook-secret"] ?? "";
358
632
  if (!isNonInteractive && !args.yes) {
359
- const setupWhop = await p2.confirm({
633
+ const setupWhop = await p5.confirm({
360
634
  message: "Configure Whop credentials now? (you can do this later via the setup wizard)",
361
635
  initialValue: false
362
636
  });
363
637
  if (!isCancelled(setupWhop) && setupWhop) {
364
638
  if (!appId) {
365
- const result = await p2.text({
639
+ const result = await p5.text({
366
640
  message: "Whop App ID",
367
641
  placeholder: "app_xxxxxxxxx",
368
642
  validate: (v) => v ? validateWhopAppId(v) : void 0
@@ -370,14 +644,14 @@ var init_default = defineCommand({
370
644
  if (!isCancelled(result)) appId = result ?? "";
371
645
  }
372
646
  if (!apiKey) {
373
- const result = await p2.text({
647
+ const result = await p5.text({
374
648
  message: "Whop API Key",
375
649
  placeholder: "apik_xxxxxxxxx (optional, press Enter to skip)"
376
650
  });
377
651
  if (!isCancelled(result)) apiKey = result ?? "";
378
652
  }
379
653
  if (!webhookSecret) {
380
- const result = await p2.text({
654
+ const result = await p5.text({
381
655
  message: "Whop Webhook Secret",
382
656
  placeholder: "optional, press Enter to skip"
383
657
  });
@@ -386,25 +660,25 @@ var init_default = defineCommand({
386
660
  }
387
661
  }
388
662
  if (args["dry-run"]) {
389
- p2.log.info(pc2.dim("Dry run \u2014 showing what would be created:\n"));
390
- console.log(` ${pc2.bold("Project:")} ${projectName}`);
391
- console.log(` ${pc2.bold("Framework:")} ${template.name}`);
392
- console.log(` ${pc2.bold("App type:")} ${APP_TYPES[appType]?.name ?? appType}`);
393
- console.log(` ${pc2.bold("Database:")} ${DB_OPTIONS[database]?.name ?? database}`);
394
- console.log(` ${pc2.bold("Template:")} github.com/${template.repo}`);
395
- if (dbUrl) console.log(` ${pc2.bold("DB URL:")} ${dbUrl.substring(0, 30)}...`);
396
- if (appId) console.log(` ${pc2.bold("Whop App:")} ${appId}`);
663
+ p5.log.info(pc5.dim("Dry run \u2014 showing what would be created:\n"));
664
+ console.log(` ${pc5.bold("Project:")} ${projectName}`);
665
+ console.log(` ${pc5.bold("Framework:")} ${template.name}`);
666
+ console.log(` ${pc5.bold("App type:")} ${APP_TYPES[appType]?.name ?? appType}`);
667
+ console.log(` ${pc5.bold("Database:")} ${database}`);
668
+ console.log(` ${pc5.bold("Template:")} github.com/${template.repo}`);
669
+ if (dbUrl) console.log(` ${pc5.bold("DB URL:")} ${dbUrl.substring(0, 30)}...`);
670
+ if (appId) console.log(` ${pc5.bold("Whop App:")} ${appId}`);
397
671
  console.log("");
398
- p2.outro("No files were created.");
672
+ p5.outro("No files were created.");
399
673
  return;
400
674
  }
401
675
  const projectDir = resolve(projectName);
402
- const s = p2.spinner();
676
+ const s = p5.spinner();
403
677
  s.start(`Cloning ${template.name} template...`);
404
678
  const cloned = cloneTemplate(template.repo, projectDir);
405
679
  if (!cloned) {
406
680
  s.stop("Failed to clone template");
407
- p2.log.error(`Could not clone github.com/${template.repo}. Check your internet connection.`);
681
+ p5.log.error(`Could not clone github.com/${template.repo}. Check your internet connection.`);
408
682
  cleanupDir(projectDir);
409
683
  process.exit(1);
410
684
  }
@@ -436,7 +710,7 @@ var init_default = defineCommand({
436
710
  const installResult = exec(`${pm} install`, projectDir);
437
711
  if (!installResult.success) {
438
712
  s.stop(`${pm} install failed`);
439
- p2.log.warning("Dependency installation failed. Run it manually after setup.");
713
+ p5.log.warning("Dependency installation failed. Run it manually after setup.");
440
714
  } else {
441
715
  s.stop("Dependencies installed");
442
716
  }
@@ -453,26 +727,30 @@ var init_default = defineCommand({
453
727
  else missing.push("Webhook Secret");
454
728
  let summary = "";
455
729
  if (configured.length > 0) {
456
- summary += `${pc2.green("\u2713")} ${configured.join(", ")}
730
+ summary += `${pc5.green("\u2713")} ${configured.join(", ")}
457
731
  `;
458
732
  }
459
733
  if (missing.length > 0) {
460
- summary += `${pc2.yellow("\u25CB")} Missing: ${missing.join(", ")}
734
+ summary += `${pc5.yellow("\u25CB")} Missing: ${missing.join(", ")}
461
735
  `;
462
- summary += ` ${pc2.dim("Configure via the setup wizard or .env.local")}
736
+ summary += ` ${pc5.dim("Configure via the setup wizard or .env.local")}
737
+ `;
738
+ }
739
+ if (dbNote) {
740
+ summary += `${pc5.yellow("!")} ${dbNote}
463
741
  `;
464
742
  }
465
743
  summary += `
466
744
  `;
467
- summary += ` ${pc2.bold("cd")} ${basename2(projectName)}
745
+ summary += ` ${pc5.bold("cd")} ${basename2(projectName)}
468
746
  `;
469
747
  if (dbUrl) {
470
- summary += ` ${pc2.bold(`${pm} run db:push`)}
748
+ summary += ` ${pc5.bold(`${pm} run db:push`)}
471
749
  `;
472
750
  }
473
- summary += ` ${pc2.bold(`${pm} run dev`)}`;
474
- p2.note(summary, "Your app is ready");
475
- p2.outro(`${pc2.green("Happy building!")} ${pc2.dim("\u2014 whop-kit")}`);
751
+ summary += ` ${pc5.bold(`${pm} run dev`)}`;
752
+ p5.note(summary, "Your app is ready");
753
+ p5.outro(`${pc5.green("Happy building!")} ${pc5.dim("\u2014 whop-kit")}`);
476
754
  }
477
755
  });
478
756
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-whop-kit",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Scaffold and manage Whop-powered apps with whop-kit",
5
5
  "type": "module",
6
6
  "license": "MIT",