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.
- package/dist/cli-create.js +376 -98
- package/package.json +1 -1
package/dist/cli-create.js
CHANGED
|
@@ -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
|
|
15
|
-
import * as
|
|
16
|
-
import
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
93
|
-
import
|
|
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
|
-
|
|
98
|
-
`Node.js ${
|
|
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
|
-
|
|
106
|
-
`${
|
|
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 (
|
|
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
|
|
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 || !
|
|
383
|
+
if (!result.success || !existsSync3(projectDir)) {
|
|
143
384
|
return false;
|
|
144
385
|
}
|
|
145
386
|
const gitDir = join(projectDir, ".git");
|
|
146
|
-
if (
|
|
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 (
|
|
154
|
-
const pkg = JSON.parse(
|
|
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
|
|
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 (
|
|
175
|
-
let content =
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
513
|
+
if (existsSync5(resolve(v))) return `Directory "${v}" already exists`;
|
|
273
514
|
}
|
|
274
515
|
});
|
|
275
516
|
if (isCancelled(result)) {
|
|
276
|
-
|
|
517
|
+
p5.cancel("Cancelled.");
|
|
277
518
|
process.exit(0);
|
|
278
519
|
}
|
|
279
520
|
projectName = result;
|
|
280
|
-
} else if (
|
|
281
|
-
|
|
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
|
|
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} ${
|
|
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
|
-
|
|
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
|
|
544
|
+
const result = await p5.select({
|
|
304
545
|
message: "Which framework?",
|
|
305
|
-
options: Object.entries(
|
|
546
|
+
options: Object.entries(FRAMEWORKS).map(([value, f]) => ({
|
|
306
547
|
value,
|
|
307
|
-
label:
|
|
308
|
-
hint:
|
|
309
|
-
disabled: !
|
|
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
|
-
|
|
554
|
+
p5.cancel("Cancelled.");
|
|
314
555
|
process.exit(0);
|
|
315
556
|
}
|
|
316
557
|
framework = result;
|
|
317
558
|
}
|
|
318
|
-
const template =
|
|
559
|
+
const template = getTemplate(appType, framework);
|
|
319
560
|
if (!template || !template.available) {
|
|
320
|
-
|
|
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
|
|
566
|
+
const result = await p5.select({
|
|
326
567
|
message: "Which database?",
|
|
327
|
-
options:
|
|
328
|
-
value,
|
|
329
|
-
label:
|
|
330
|
-
hint:
|
|
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
|
-
|
|
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
|
-
|
|
341
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
390
|
-
console.log(` ${
|
|
391
|
-
console.log(` ${
|
|
392
|
-
console.log(` ${
|
|
393
|
-
console.log(` ${
|
|
394
|
-
console.log(` ${
|
|
395
|
-
if (dbUrl) console.log(` ${
|
|
396
|
-
if (appId) console.log(` ${
|
|
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
|
-
|
|
672
|
+
p5.outro("No files were created.");
|
|
399
673
|
return;
|
|
400
674
|
}
|
|
401
675
|
const projectDir = resolve(projectName);
|
|
402
|
-
const s =
|
|
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
|
-
|
|
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
|
-
|
|
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 += `${
|
|
730
|
+
summary += `${pc5.green("\u2713")} ${configured.join(", ")}
|
|
457
731
|
`;
|
|
458
732
|
}
|
|
459
733
|
if (missing.length > 0) {
|
|
460
|
-
summary += `${
|
|
734
|
+
summary += `${pc5.yellow("\u25CB")} Missing: ${missing.join(", ")}
|
|
461
735
|
`;
|
|
462
|
-
summary += ` ${
|
|
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 += ` ${
|
|
745
|
+
summary += ` ${pc5.bold("cd")} ${basename2(projectName)}
|
|
468
746
|
`;
|
|
469
747
|
if (dbUrl) {
|
|
470
|
-
summary += ` ${
|
|
748
|
+
summary += ` ${pc5.bold(`${pm} run db:push`)}
|
|
471
749
|
`;
|
|
472
750
|
}
|
|
473
|
-
summary += ` ${
|
|
474
|
-
|
|
475
|
-
|
|
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
|
|