create-whop-kit 0.2.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/cli-create.js +348 -67
- package/package.json +1 -1
package/dist/cli-create.js
CHANGED
|
@@ -11,9 +11,9 @@ 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
|
|
@@ -88,22 +88,266 @@ var DB_OPTIONS = {
|
|
|
88
88
|
}
|
|
89
89
|
};
|
|
90
90
|
|
|
91
|
-
// src/
|
|
91
|
+
// src/providers/neon.ts
|
|
92
92
|
import * as p from "@clack/prompts";
|
|
93
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";
|
|
94
338
|
function checkNodeVersion(minimum = 18) {
|
|
95
339
|
const major = parseInt(process.versions.node.split(".")[0], 10);
|
|
96
340
|
if (major < minimum) {
|
|
97
|
-
|
|
98
|
-
`Node.js ${
|
|
341
|
+
p4.log.error(
|
|
342
|
+
`Node.js ${pc4.bold(`v${minimum}+`)} is required. You have ${pc4.bold(`v${process.versions.node}`)}.`
|
|
99
343
|
);
|
|
100
344
|
process.exit(1);
|
|
101
345
|
}
|
|
102
346
|
}
|
|
103
347
|
function checkGit() {
|
|
104
348
|
if (!hasCommand("git")) {
|
|
105
|
-
|
|
106
|
-
`${
|
|
349
|
+
p4.log.error(
|
|
350
|
+
`${pc4.bold("git")} is required but not found. Install it from ${pc4.cyan("https://git-scm.com")}`
|
|
107
351
|
);
|
|
108
352
|
process.exit(1);
|
|
109
353
|
}
|
|
@@ -122,9 +366,9 @@ function validateWhopAppId(id) {
|
|
|
122
366
|
}
|
|
123
367
|
|
|
124
368
|
// src/utils/cleanup.ts
|
|
125
|
-
import { rmSync, existsSync } from "fs";
|
|
369
|
+
import { rmSync, existsSync as existsSync2 } from "fs";
|
|
126
370
|
function cleanupDir(dir) {
|
|
127
|
-
if (
|
|
371
|
+
if (existsSync2(dir)) {
|
|
128
372
|
try {
|
|
129
373
|
rmSync(dir, { recursive: true, force: true });
|
|
130
374
|
} catch {
|
|
@@ -133,25 +377,25 @@ function cleanupDir(dir) {
|
|
|
133
377
|
}
|
|
134
378
|
|
|
135
379
|
// src/scaffolding/clone.ts
|
|
136
|
-
import { existsSync as
|
|
380
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, rmSync as rmSync2 } from "fs";
|
|
137
381
|
import { join, basename } from "path";
|
|
138
382
|
function cloneTemplate(repo, projectDir) {
|
|
139
383
|
const result = exec(
|
|
140
384
|
`git clone --depth 1 https://github.com/${repo}.git "${projectDir}"`
|
|
141
385
|
);
|
|
142
|
-
if (!result.success || !
|
|
386
|
+
if (!result.success || !existsSync3(projectDir)) {
|
|
143
387
|
return false;
|
|
144
388
|
}
|
|
145
389
|
const gitDir = join(projectDir, ".git");
|
|
146
|
-
if (
|
|
390
|
+
if (existsSync3(gitDir)) {
|
|
147
391
|
rmSync2(gitDir, { recursive: true, force: true });
|
|
148
392
|
}
|
|
149
393
|
return true;
|
|
150
394
|
}
|
|
151
395
|
function updatePackageName(projectDir, name) {
|
|
152
396
|
const pkgPath = join(projectDir, "package.json");
|
|
153
|
-
if (
|
|
154
|
-
const pkg = JSON.parse(
|
|
397
|
+
if (existsSync3(pkgPath)) {
|
|
398
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
155
399
|
pkg.name = basename(name);
|
|
156
400
|
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
157
401
|
}
|
|
@@ -163,7 +407,7 @@ function initGit(projectDir) {
|
|
|
163
407
|
}
|
|
164
408
|
|
|
165
409
|
// src/scaffolding/env-file.ts
|
|
166
|
-
import { readFileSync as
|
|
410
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync4 } from "fs";
|
|
167
411
|
import { join as join2 } from "path";
|
|
168
412
|
function writeEnvFile(projectDir, values) {
|
|
169
413
|
const examplePath = join2(projectDir, ".env.example");
|
|
@@ -171,8 +415,8 @@ function writeEnvFile(projectDir, values) {
|
|
|
171
415
|
const filled = Object.fromEntries(
|
|
172
416
|
Object.entries(values).filter(([, v]) => v)
|
|
173
417
|
);
|
|
174
|
-
if (
|
|
175
|
-
let content =
|
|
418
|
+
if (existsSync4(examplePath)) {
|
|
419
|
+
let content = readFileSync3(examplePath, "utf-8");
|
|
176
420
|
for (const [key, value] of Object.entries(filled)) {
|
|
177
421
|
const pattern = new RegExp(
|
|
178
422
|
`^(#\\s*)?${escapeRegex(key)}=.*$`,
|
|
@@ -196,7 +440,7 @@ function escapeRegex(str) {
|
|
|
196
440
|
|
|
197
441
|
// src/commands/init.ts
|
|
198
442
|
function isCancelled(value) {
|
|
199
|
-
return
|
|
443
|
+
return p5.isCancel(value);
|
|
200
444
|
}
|
|
201
445
|
var init_default = defineCommand({
|
|
202
446
|
meta: {
|
|
@@ -260,94 +504,127 @@ var init_default = defineCommand({
|
|
|
260
504
|
checkNodeVersion(18);
|
|
261
505
|
checkGit();
|
|
262
506
|
console.log("");
|
|
263
|
-
|
|
507
|
+
p5.intro(`${pc5.bgCyan(pc5.black(" create-whop-kit "))} Create a Whop-powered app`);
|
|
264
508
|
const isNonInteractive = !!(args.framework && args.db);
|
|
265
509
|
let projectName = args.name;
|
|
266
510
|
if (!projectName) {
|
|
267
|
-
const result = await
|
|
511
|
+
const result = await p5.text({
|
|
268
512
|
message: "Project name",
|
|
269
513
|
placeholder: "my-whop-app",
|
|
270
514
|
validate: (v) => {
|
|
271
515
|
if (!v) return "Project name is required";
|
|
272
|
-
if (
|
|
516
|
+
if (existsSync5(resolve(v))) return `Directory "${v}" already exists`;
|
|
273
517
|
}
|
|
274
518
|
});
|
|
275
519
|
if (isCancelled(result)) {
|
|
276
|
-
|
|
520
|
+
p5.cancel("Cancelled.");
|
|
277
521
|
process.exit(0);
|
|
278
522
|
}
|
|
279
523
|
projectName = result;
|
|
280
|
-
} else if (
|
|
281
|
-
|
|
524
|
+
} else if (existsSync5(resolve(projectName))) {
|
|
525
|
+
p5.log.error(`Directory "${projectName}" already exists`);
|
|
282
526
|
process.exit(1);
|
|
283
527
|
}
|
|
284
528
|
let appType = args.type;
|
|
285
529
|
if (!isNonInteractive && !args.yes) {
|
|
286
|
-
const result = await
|
|
530
|
+
const result = await p5.select({
|
|
287
531
|
message: "What are you building?",
|
|
288
532
|
options: Object.entries(APP_TYPES).map(([value, t]) => ({
|
|
289
533
|
value,
|
|
290
|
-
label: t.available ? t.name : `${t.name} ${
|
|
534
|
+
label: t.available ? t.name : `${t.name} ${pc5.dim("(coming soon)")}`,
|
|
291
535
|
hint: t.description,
|
|
292
536
|
disabled: !t.available
|
|
293
537
|
}))
|
|
294
538
|
});
|
|
295
539
|
if (isCancelled(result)) {
|
|
296
|
-
|
|
540
|
+
p5.cancel("Cancelled.");
|
|
297
541
|
process.exit(0);
|
|
298
542
|
}
|
|
299
543
|
appType = result;
|
|
300
544
|
}
|
|
301
545
|
let framework = args.framework;
|
|
302
546
|
if (!framework) {
|
|
303
|
-
const result = await
|
|
547
|
+
const result = await p5.select({
|
|
304
548
|
message: "Which framework?",
|
|
305
549
|
options: Object.entries(TEMPLATES).map(([value, t]) => ({
|
|
306
550
|
value,
|
|
307
|
-
label: t.available ? t.name : `${t.name} ${
|
|
551
|
+
label: t.available ? t.name : `${t.name} ${pc5.dim("(coming soon)")}`,
|
|
308
552
|
hint: t.description,
|
|
309
553
|
disabled: !t.available
|
|
310
554
|
}))
|
|
311
555
|
});
|
|
312
556
|
if (isCancelled(result)) {
|
|
313
|
-
|
|
557
|
+
p5.cancel("Cancelled.");
|
|
314
558
|
process.exit(0);
|
|
315
559
|
}
|
|
316
560
|
framework = result;
|
|
317
561
|
}
|
|
318
562
|
const template = TEMPLATES[framework];
|
|
319
563
|
if (!template || !template.available) {
|
|
320
|
-
|
|
564
|
+
p5.log.error(`Framework "${framework}" is not available. Options: ${Object.keys(TEMPLATES).filter((k) => TEMPLATES[k].available).join(", ")}`);
|
|
321
565
|
process.exit(1);
|
|
322
566
|
}
|
|
323
567
|
let database = args.db;
|
|
324
568
|
if (!database) {
|
|
325
|
-
const result = await
|
|
569
|
+
const result = await p5.select({
|
|
326
570
|
message: "Which database?",
|
|
327
|
-
options:
|
|
328
|
-
value,
|
|
329
|
-
label:
|
|
330
|
-
hint:
|
|
331
|
-
|
|
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
|
+
]
|
|
332
578
|
});
|
|
333
579
|
if (isCancelled(result)) {
|
|
334
|
-
|
|
580
|
+
p5.cancel("Cancelled.");
|
|
335
581
|
process.exit(0);
|
|
336
582
|
}
|
|
337
583
|
database = result;
|
|
338
584
|
}
|
|
339
585
|
let dbUrl = args["db-url"] ?? "";
|
|
340
|
-
|
|
341
|
-
|
|
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({
|
|
342
619
|
message: "Database URL",
|
|
343
|
-
placeholder:
|
|
620
|
+
placeholder: "postgresql://user:pass@host:5432/dbname",
|
|
344
621
|
validate: (v) => {
|
|
345
622
|
if (!v) return "Required (choose 'Configure later' to skip)";
|
|
346
623
|
return validateDatabaseUrl(v);
|
|
347
624
|
}
|
|
348
625
|
});
|
|
349
626
|
if (isCancelled(result)) {
|
|
350
|
-
|
|
627
|
+
p5.cancel("Cancelled.");
|
|
351
628
|
process.exit(0);
|
|
352
629
|
}
|
|
353
630
|
dbUrl = result;
|
|
@@ -356,13 +633,13 @@ var init_default = defineCommand({
|
|
|
356
633
|
let apiKey = args["api-key"] ?? "";
|
|
357
634
|
let webhookSecret = args["webhook-secret"] ?? "";
|
|
358
635
|
if (!isNonInteractive && !args.yes) {
|
|
359
|
-
const setupWhop = await
|
|
636
|
+
const setupWhop = await p5.confirm({
|
|
360
637
|
message: "Configure Whop credentials now? (you can do this later via the setup wizard)",
|
|
361
638
|
initialValue: false
|
|
362
639
|
});
|
|
363
640
|
if (!isCancelled(setupWhop) && setupWhop) {
|
|
364
641
|
if (!appId) {
|
|
365
|
-
const result = await
|
|
642
|
+
const result = await p5.text({
|
|
366
643
|
message: "Whop App ID",
|
|
367
644
|
placeholder: "app_xxxxxxxxx",
|
|
368
645
|
validate: (v) => v ? validateWhopAppId(v) : void 0
|
|
@@ -370,14 +647,14 @@ var init_default = defineCommand({
|
|
|
370
647
|
if (!isCancelled(result)) appId = result ?? "";
|
|
371
648
|
}
|
|
372
649
|
if (!apiKey) {
|
|
373
|
-
const result = await
|
|
650
|
+
const result = await p5.text({
|
|
374
651
|
message: "Whop API Key",
|
|
375
652
|
placeholder: "apik_xxxxxxxxx (optional, press Enter to skip)"
|
|
376
653
|
});
|
|
377
654
|
if (!isCancelled(result)) apiKey = result ?? "";
|
|
378
655
|
}
|
|
379
656
|
if (!webhookSecret) {
|
|
380
|
-
const result = await
|
|
657
|
+
const result = await p5.text({
|
|
381
658
|
message: "Whop Webhook Secret",
|
|
382
659
|
placeholder: "optional, press Enter to skip"
|
|
383
660
|
});
|
|
@@ -386,25 +663,25 @@ var init_default = defineCommand({
|
|
|
386
663
|
}
|
|
387
664
|
}
|
|
388
665
|
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(` ${
|
|
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}`);
|
|
397
674
|
console.log("");
|
|
398
|
-
|
|
675
|
+
p5.outro("No files were created.");
|
|
399
676
|
return;
|
|
400
677
|
}
|
|
401
678
|
const projectDir = resolve(projectName);
|
|
402
|
-
const s =
|
|
679
|
+
const s = p5.spinner();
|
|
403
680
|
s.start(`Cloning ${template.name} template...`);
|
|
404
681
|
const cloned = cloneTemplate(template.repo, projectDir);
|
|
405
682
|
if (!cloned) {
|
|
406
683
|
s.stop("Failed to clone template");
|
|
407
|
-
|
|
684
|
+
p5.log.error(`Could not clone github.com/${template.repo}. Check your internet connection.`);
|
|
408
685
|
cleanupDir(projectDir);
|
|
409
686
|
process.exit(1);
|
|
410
687
|
}
|
|
@@ -436,7 +713,7 @@ var init_default = defineCommand({
|
|
|
436
713
|
const installResult = exec(`${pm} install`, projectDir);
|
|
437
714
|
if (!installResult.success) {
|
|
438
715
|
s.stop(`${pm} install failed`);
|
|
439
|
-
|
|
716
|
+
p5.log.warning("Dependency installation failed. Run it manually after setup.");
|
|
440
717
|
} else {
|
|
441
718
|
s.stop("Dependencies installed");
|
|
442
719
|
}
|
|
@@ -453,26 +730,30 @@ var init_default = defineCommand({
|
|
|
453
730
|
else missing.push("Webhook Secret");
|
|
454
731
|
let summary = "";
|
|
455
732
|
if (configured.length > 0) {
|
|
456
|
-
summary += `${
|
|
733
|
+
summary += `${pc5.green("\u2713")} ${configured.join(", ")}
|
|
457
734
|
`;
|
|
458
735
|
}
|
|
459
736
|
if (missing.length > 0) {
|
|
460
|
-
summary += `${
|
|
737
|
+
summary += `${pc5.yellow("\u25CB")} Missing: ${missing.join(", ")}
|
|
738
|
+
`;
|
|
739
|
+
summary += ` ${pc5.dim("Configure via the setup wizard or .env.local")}
|
|
461
740
|
`;
|
|
462
|
-
|
|
741
|
+
}
|
|
742
|
+
if (dbNote) {
|
|
743
|
+
summary += `${pc5.yellow("!")} ${dbNote}
|
|
463
744
|
`;
|
|
464
745
|
}
|
|
465
746
|
summary += `
|
|
466
747
|
`;
|
|
467
|
-
summary += ` ${
|
|
748
|
+
summary += ` ${pc5.bold("cd")} ${basename2(projectName)}
|
|
468
749
|
`;
|
|
469
750
|
if (dbUrl) {
|
|
470
|
-
summary += ` ${
|
|
751
|
+
summary += ` ${pc5.bold(`${pm} run db:push`)}
|
|
471
752
|
`;
|
|
472
753
|
}
|
|
473
|
-
summary += ` ${
|
|
474
|
-
|
|
475
|
-
|
|
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")}`);
|
|
476
757
|
}
|
|
477
758
|
});
|
|
478
759
|
|