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