create-whop-kit 0.7.0 → 0.9.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.
@@ -0,0 +1,504 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ exec,
4
+ execInteractive,
5
+ execWithStdin,
6
+ hasCommand
7
+ } from "./chunk-42L7PRMT.js";
8
+
9
+ // src/deploy/index.ts
10
+ import * as p3 from "@clack/prompts";
11
+ import pc3 from "picocolors";
12
+
13
+ // src/deploy/vercel.ts
14
+ import * as p from "@clack/prompts";
15
+ import pc from "picocolors";
16
+ function isVercelInstalled() {
17
+ return hasCommand("vercel");
18
+ }
19
+ async function installOrUpdateVercel() {
20
+ const s = p.spinner();
21
+ if (isVercelInstalled()) {
22
+ const versionResult = exec("vercel --version");
23
+ const currentVersion = versionResult.stdout.replace(/[^0-9.]/g, "");
24
+ s.start("Checking for Vercel CLI updates...");
25
+ const updateResult = exec("npm install -g vercel@latest", void 0, 6e4);
26
+ if (updateResult.success) {
27
+ const newVersion = exec("vercel --version");
28
+ const newVer = newVersion.stdout.replace(/[^0-9.]/g, "");
29
+ if (newVer !== currentVersion) {
30
+ s.stop(`Vercel CLI updated: ${currentVersion} \u2192 ${newVer}`);
31
+ } else {
32
+ s.stop(`Vercel CLI up to date (v${currentVersion})`);
33
+ }
34
+ } else {
35
+ s.stop(`Vercel CLI v${currentVersion} (update check failed, continuing)`);
36
+ }
37
+ return true;
38
+ }
39
+ s.start("Installing Vercel CLI...");
40
+ const result = exec("npm install -g vercel@latest");
41
+ if (result.success) {
42
+ s.stop("Vercel CLI installed");
43
+ return true;
44
+ }
45
+ s.stop("Failed to install Vercel CLI");
46
+ p.log.error(`Install manually: ${pc.bold("npm install -g vercel@latest")}`);
47
+ return false;
48
+ }
49
+ function isVercelAuthenticated() {
50
+ const result = exec("vercel whoami");
51
+ return result.success;
52
+ }
53
+ function getVercelUser() {
54
+ const result = exec("vercel whoami");
55
+ return result.success ? result.stdout.trim() : null;
56
+ }
57
+ async function vercelLogin() {
58
+ p.log.info("You'll be redirected to Vercel to sign in (or create an account).");
59
+ p.log.info(pc.dim("This is needed to deploy your app."));
60
+ console.log("");
61
+ const ok = execInteractive("vercel login");
62
+ console.log("");
63
+ return ok;
64
+ }
65
+ async function vercelLink(projectDir) {
66
+ p.log.step("Vercel: linking project...");
67
+ console.log("");
68
+ const ok = execInteractive("vercel link --yes", projectDir);
69
+ console.log("");
70
+ return ok;
71
+ }
72
+ async function vercelDeploy(projectDir) {
73
+ const s = p.spinner();
74
+ s.start("Vercel: deploying to production (this may take a few minutes)...");
75
+ const result = exec("vercel deploy --prod --yes", projectDir, 3e5);
76
+ if (!result.success) {
77
+ s.stop("Vercel deployment failed");
78
+ const errorOutput = result.stderr || result.stdout;
79
+ if (errorOutput) {
80
+ p.log.error("Build output:");
81
+ const trimmed = errorOutput.length > 600 ? "..." + errorOutput.slice(-600) : errorOutput;
82
+ console.log(pc.dim(trimmed));
83
+ }
84
+ return null;
85
+ }
86
+ const lines = result.stdout.split("\n");
87
+ let url = "";
88
+ for (const line of lines) {
89
+ if (line.includes("Aliased:") || line.includes("Production:")) {
90
+ const match = line.match(/https:\/\/[^\s\[\]]+/);
91
+ if (match) {
92
+ url = match[0];
93
+ if (line.includes("Aliased:")) break;
94
+ }
95
+ }
96
+ }
97
+ if (!url) {
98
+ const urls = [];
99
+ for (const line of lines) {
100
+ const match = line.match(/https:\/\/[^\s\[\]]+\.vercel\.app/);
101
+ if (match) urls.push(match[0]);
102
+ }
103
+ if (urls.length > 0) {
104
+ urls.sort((a, b) => a.length - b.length);
105
+ url = urls[0];
106
+ }
107
+ }
108
+ if (url) {
109
+ s.stop(`Deployed to ${pc.cyan(url)}`);
110
+ return url;
111
+ }
112
+ for (const line of lines) {
113
+ const match = line.match(/https:\/\/[^\s\[\]]+/);
114
+ if (match && !match[0].includes("github.com") && !match[0].includes("vercel.com/")) {
115
+ s.stop(`Deployed to ${pc.cyan(match[0])}`);
116
+ return match[0];
117
+ }
118
+ }
119
+ s.stop("Deployed but could not extract URL");
120
+ const manual = await p.text({
121
+ message: "Paste your Vercel production URL",
122
+ placeholder: "https://your-app.vercel.app",
123
+ validate: (v) => {
124
+ if (!v?.startsWith("https://")) return "Must be a https:// URL";
125
+ }
126
+ });
127
+ if (p.isCancel(manual)) return null;
128
+ return manual;
129
+ }
130
+ function vercelEnvSet(key, value, environment = "production", projectDir) {
131
+ const result = execWithStdin(
132
+ `vercel env add ${key} ${environment} --force`,
133
+ value,
134
+ projectDir
135
+ );
136
+ return result.success;
137
+ }
138
+
139
+ // src/deploy/github.ts
140
+ import * as p2 from "@clack/prompts";
141
+ import pc2 from "picocolors";
142
+ function isGhInstalled() {
143
+ return hasCommand("gh");
144
+ }
145
+ function isGhAuthenticated() {
146
+ const result = exec("gh auth status");
147
+ return result.success;
148
+ }
149
+ async function installGh() {
150
+ const s = p2.spinner();
151
+ s.start("Installing GitHub CLI...");
152
+ const result = exec("npm install -g gh", void 0, 6e4);
153
+ if (result.success && hasCommand("gh")) {
154
+ s.stop("GitHub CLI installed");
155
+ return true;
156
+ }
157
+ s.stop("Could not auto-install GitHub CLI");
158
+ p2.log.info("Install manually:");
159
+ p2.log.info(pc2.bold(" https://cli.github.com"));
160
+ return false;
161
+ }
162
+ async function ghLogin() {
163
+ p2.log.info("You'll be redirected to GitHub to sign in.");
164
+ console.log("");
165
+ const ok = execInteractive("gh auth login --web");
166
+ console.log("");
167
+ return ok;
168
+ }
169
+ async function createGitHubRepo(projectDir, projectName) {
170
+ const s = p2.spinner();
171
+ s.start("Creating private GitHub repository...");
172
+ const result = exec(
173
+ `gh repo create ${projectName} --private --source=. --push`,
174
+ projectDir,
175
+ 6e4
176
+ );
177
+ if (!result.success) {
178
+ s.stop("Could not create repo");
179
+ const stderr = result.stderr || result.stdout;
180
+ if (stderr.includes("already exists")) {
181
+ p2.log.warning(`Repository "${projectName}" already exists on GitHub.`);
182
+ exec(`git remote add origin https://github.com/$(gh api user --jq .login)/${projectName}.git`, projectDir);
183
+ const pushResult = exec("git push -u origin main", projectDir, 3e4);
184
+ if (pushResult.success) {
185
+ const remote = exec("gh repo view --json url --jq .url", projectDir);
186
+ return remote.success ? remote.stdout.trim() : null;
187
+ }
188
+ }
189
+ return null;
190
+ }
191
+ const repoUrl = exec("gh repo view --json url --jq .url", projectDir);
192
+ if (repoUrl.success) {
193
+ s.stop(`GitHub repo created: ${pc2.cyan(repoUrl.stdout.trim())}`);
194
+ return repoUrl.stdout.trim();
195
+ }
196
+ s.stop("GitHub repo created");
197
+ return `https://github.com/${projectName}`;
198
+ }
199
+ function getGitHubOrgs() {
200
+ const result = exec("gh api user/orgs --jq '.[].login'");
201
+ if (!result.success || !result.stdout.trim()) return [];
202
+ return result.stdout.trim().split("\n").filter(Boolean);
203
+ }
204
+
205
+ // src/deploy/whop-api.ts
206
+ var WHOP_API = "https://api.whop.com/api/v1";
207
+ function headers(apiKey) {
208
+ return {
209
+ Authorization: `Bearer ${apiKey}`,
210
+ "Content-Type": "application/json"
211
+ };
212
+ }
213
+ async function validateApiKey(apiKey) {
214
+ try {
215
+ const res = await fetch(`${WHOP_API}/apps`, {
216
+ headers: headers(apiKey)
217
+ });
218
+ return res.ok;
219
+ } catch {
220
+ return false;
221
+ }
222
+ }
223
+ async function createWhopApp(apiKey, name, redirectUris) {
224
+ try {
225
+ const res = await fetch(`${WHOP_API}/apps`, {
226
+ method: "POST",
227
+ headers: headers(apiKey),
228
+ body: JSON.stringify({
229
+ name,
230
+ redirect_uris: redirectUris
231
+ })
232
+ });
233
+ if (!res.ok) {
234
+ const err = await res.text().catch(() => "");
235
+ console.error(`[Whop API] Create app failed (${res.status}): ${err}`);
236
+ return null;
237
+ }
238
+ const data = await res.json();
239
+ return {
240
+ id: data.id,
241
+ client_secret: data.client_secret
242
+ };
243
+ } catch (err) {
244
+ console.error("[Whop API] Create app error:", err);
245
+ return null;
246
+ }
247
+ }
248
+ async function createWhopWebhook(apiKey, url, events) {
249
+ try {
250
+ const res = await fetch(`${WHOP_API}/webhooks`, {
251
+ method: "POST",
252
+ headers: headers(apiKey),
253
+ body: JSON.stringify({
254
+ url,
255
+ events
256
+ })
257
+ });
258
+ if (!res.ok) {
259
+ const err = await res.text().catch(() => "");
260
+ console.error(`[Whop API] Create webhook failed (${res.status}): ${err}`);
261
+ return null;
262
+ }
263
+ const data = await res.json();
264
+ return {
265
+ id: data.id,
266
+ secret: data.secret || data.signing_secret || data.webhook_secret || ""
267
+ };
268
+ } catch (err) {
269
+ console.error("[Whop API] Create webhook error:", err);
270
+ return null;
271
+ }
272
+ }
273
+
274
+ // src/deploy/index.ts
275
+ var WEBHOOK_EVENTS = [
276
+ "membership.activated",
277
+ "membership.deactivated",
278
+ "membership.cancel_at_period_end_changed",
279
+ "payment.succeeded",
280
+ "payment.failed",
281
+ "refund.created"
282
+ ];
283
+ function openUrl(url) {
284
+ const platform = process.platform;
285
+ if (platform === "darwin") exec(`open "${url}"`);
286
+ else if (platform === "win32") exec(`start "" "${url}"`);
287
+ else exec(`xdg-open "${url}"`);
288
+ }
289
+ async function runDeployPipeline(options) {
290
+ const { projectDir, projectName, databaseUrl, framework } = options;
291
+ const setupMode = await p3.select({
292
+ message: "How would you like to deploy?",
293
+ options: [
294
+ {
295
+ value: "github-vercel",
296
+ label: "GitHub + Vercel (recommended)",
297
+ hint: "Private repo, auto-deploy on every push"
298
+ },
299
+ {
300
+ value: "github-only",
301
+ label: "GitHub only",
302
+ hint: "Push code to GitHub, deploy later"
303
+ },
304
+ {
305
+ value: "vercel-only",
306
+ label: "Vercel only",
307
+ hint: "Deploy without GitHub (no auto-deploy on push)"
308
+ }
309
+ ]
310
+ });
311
+ if (p3.isCancel(setupMode)) return null;
312
+ const useGithub = setupMode === "github-vercel" || setupMode === "github-only";
313
+ const useVercel = setupMode === "github-vercel" || setupMode === "vercel-only";
314
+ let githubRepoUrl = null;
315
+ let productionUrl = null;
316
+ if (useGithub) {
317
+ p3.log.info(pc3.bold("\n\u2500\u2500 GitHub \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
318
+ if (!isGhInstalled()) {
319
+ p3.log.info("The GitHub CLI (gh) is needed to create your repo.");
320
+ const installed = await installGh();
321
+ if (!installed) {
322
+ p3.log.warning("Skipping GitHub \u2014 install gh manually: https://cli.github.com");
323
+ }
324
+ }
325
+ if (isGhInstalled()) {
326
+ if (!isGhAuthenticated()) {
327
+ const loginOk = await ghLogin();
328
+ if (!loginOk) {
329
+ p3.log.warning("GitHub auth failed. Skipping repo creation.");
330
+ }
331
+ }
332
+ if (isGhAuthenticated()) {
333
+ const orgs = getGitHubOrgs();
334
+ let repoFullName = projectName;
335
+ if (orgs.length > 0) {
336
+ const orgChoice = await p3.select({
337
+ message: "Which GitHub account?",
338
+ options: [
339
+ { value: "", label: "Personal account", hint: "your personal GitHub" },
340
+ ...orgs.map((org) => ({
341
+ value: org,
342
+ label: org,
343
+ hint: "organization"
344
+ }))
345
+ ]
346
+ });
347
+ if (!p3.isCancel(orgChoice) && orgChoice) {
348
+ repoFullName = `${orgChoice}/${projectName}`;
349
+ }
350
+ }
351
+ githubRepoUrl = await createGitHubRepo(projectDir, repoFullName);
352
+ if (githubRepoUrl) {
353
+ p3.log.success(`Code pushed to ${pc3.cyan(githubRepoUrl)}`);
354
+ }
355
+ }
356
+ }
357
+ }
358
+ if (useVercel) {
359
+ p3.log.info(pc3.bold("\n\u2500\u2500 Vercel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
360
+ const vercelOk = await installOrUpdateVercel();
361
+ if (!vercelOk) {
362
+ p3.log.error("Could not set up Vercel CLI.");
363
+ return githubRepoUrl ? { productionUrl: "", githubUrl: githubRepoUrl } : null;
364
+ }
365
+ if (!isVercelAuthenticated()) {
366
+ const loginOk = await vercelLogin();
367
+ if (!loginOk) {
368
+ p3.log.error("Vercel auth failed. Run " + pc3.bold("whop-kit deploy") + " later.");
369
+ return githubRepoUrl ? { productionUrl: "", githubUrl: githubRepoUrl } : null;
370
+ }
371
+ }
372
+ const vercelUser = getVercelUser();
373
+ p3.log.success(`Signed in${vercelUser ? ` as ${pc3.bold(vercelUser)}` : ""}`);
374
+ await vercelLink(projectDir);
375
+ if (githubRepoUrl) {
376
+ const s = p3.spinner();
377
+ s.start("Connecting GitHub to Vercel (auto-deploy on push)...");
378
+ const connectResult = exec(`vercel git connect ${githubRepoUrl}`, projectDir, 3e4);
379
+ if (connectResult.success) {
380
+ s.stop("Connected \u2014 every git push will auto-deploy");
381
+ } else {
382
+ s.stop("Auto-connect failed (connect manually in Vercel dashboard \u2192 Git)");
383
+ }
384
+ }
385
+ if (databaseUrl) {
386
+ const s = p3.spinner();
387
+ s.start("Setting DATABASE_URL \u2192 production...");
388
+ vercelEnvSet("DATABASE_URL", databaseUrl, "production", projectDir);
389
+ s.message("Setting DATABASE_URL \u2192 preview...");
390
+ vercelEnvSet("DATABASE_URL", databaseUrl, "preview", projectDir);
391
+ s.message("Setting DATABASE_URL \u2192 development...");
392
+ vercelEnvSet("DATABASE_URL", databaseUrl, "development", projectDir);
393
+ s.stop("DATABASE_URL configured (all environments)");
394
+ }
395
+ productionUrl = await vercelDeploy(projectDir);
396
+ if (!productionUrl) {
397
+ p3.log.error("Deploy failed. Try: " + pc3.bold(`cd ${projectName} && vercel deploy --prod`));
398
+ }
399
+ }
400
+ if (productionUrl) {
401
+ p3.log.info(pc3.bold("\n\u2500\u2500 Whop \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
402
+ const connectWhop = await p3.confirm({
403
+ message: "Connect to Whop? (creates OAuth app + webhooks automatically)",
404
+ initialValue: true
405
+ });
406
+ if (!p3.isCancel(connectWhop) && connectWhop) {
407
+ p3.note(
408
+ [
409
+ `${pc3.bold("1.")} Go to the Whop Developer Dashboard`,
410
+ ` ${pc3.cyan("https://whop.com/dashboard/developer")}`,
411
+ "",
412
+ `${pc3.bold("2.")} Click ${pc3.bold('"Create"')} under "Company API Keys"`,
413
+ "",
414
+ `${pc3.bold("3.")} Name it anything (e.g. "${projectName}")`,
415
+ "",
416
+ `${pc3.bold("4.")} Select these permissions:`,
417
+ ` ${pc3.green("\u2022")} developer:create_app`,
418
+ ` ${pc3.green("\u2022")} developer:manage_api_key`,
419
+ ` ${pc3.green("\u2022")} developer:manage_webhook`,
420
+ "",
421
+ `${pc3.bold("5.")} Create the key and paste it below`
422
+ ].join("\n"),
423
+ "Create a Company API Key"
424
+ );
425
+ openUrl("https://whop.com/dashboard/developer");
426
+ let apiKey = options.whopCompanyKey ?? "";
427
+ if (!apiKey) {
428
+ const result = await p3.text({
429
+ message: "Paste your Company API key",
430
+ placeholder: "paste the key here...",
431
+ validate: (v) => !v ? "API key is required" : void 0
432
+ });
433
+ if (p3.isCancel(result)) {
434
+ return { productionUrl, githubUrl: githubRepoUrl ?? void 0 };
435
+ }
436
+ apiKey = result;
437
+ }
438
+ const s = p3.spinner();
439
+ s.start("Validating API key...");
440
+ const keyValid = await validateApiKey(apiKey);
441
+ if (!keyValid) {
442
+ s.stop("Invalid API key");
443
+ p3.log.error("Check permissions: developer:create_app, developer:manage_api_key, developer:manage_webhook");
444
+ return { productionUrl, githubUrl: githubRepoUrl ?? void 0 };
445
+ }
446
+ s.stop("API key valid");
447
+ const redirectUris = [
448
+ "http://localhost:3000/api/auth/callback",
449
+ `${productionUrl}/api/auth/callback`
450
+ ];
451
+ s.start("Creating Whop OAuth app...");
452
+ const app = await createWhopApp(apiKey, projectName, redirectUris);
453
+ if (!app) {
454
+ s.stop("Failed");
455
+ p3.log.error("Create manually: " + pc3.cyan("https://whop.com/dashboard/developer"));
456
+ return { productionUrl, githubUrl: githubRepoUrl ?? void 0 };
457
+ }
458
+ s.stop(`OAuth app created: ${pc3.bold(app.id)}`);
459
+ s.start("Creating webhook...");
460
+ const webhook = await createWhopWebhook(apiKey, `${productionUrl}/api/webhooks/whop`, WEBHOOK_EVENTS);
461
+ if (!webhook) {
462
+ s.stop("Failed (create manually in Whop dashboard)");
463
+ } else {
464
+ s.stop("Webhook created");
465
+ }
466
+ if (useVercel) {
467
+ const envVars = {};
468
+ if (framework === "nextjs") {
469
+ envVars["NEXT_PUBLIC_WHOP_APP_ID"] = app.id;
470
+ } else {
471
+ envVars["WHOP_APP_ID"] = app.id;
472
+ }
473
+ envVars["WHOP_API_KEY"] = app.client_secret;
474
+ if (webhook?.secret) envVars["WHOP_WEBHOOK_SECRET"] = webhook.secret;
475
+ s.start("Pushing credentials to Vercel...");
476
+ for (const [key] of Object.entries(envVars)) {
477
+ s.message(`Pushing ${key}...`);
478
+ vercelEnvSet(key, envVars[key], "production", projectDir);
479
+ vercelEnvSet(key, envVars[key], "preview", projectDir);
480
+ vercelEnvSet(key, envVars[key], "development", projectDir);
481
+ }
482
+ s.stop("All credentials pushed to Vercel");
483
+ s.start("Redeploying with full configuration...");
484
+ const redeploy = exec("vercel deploy --prod --yes", projectDir, 3e5);
485
+ s.stop(redeploy.success ? "Redeployed" : "Redeploy pending \u2014 will apply on next git push");
486
+ }
487
+ return {
488
+ productionUrl,
489
+ githubUrl: githubRepoUrl ?? void 0,
490
+ whopAppId: app.id,
491
+ whopApiKey: app.client_secret,
492
+ webhookSecret: webhook?.secret
493
+ };
494
+ }
495
+ }
496
+ return {
497
+ productionUrl: productionUrl ?? "",
498
+ githubUrl: githubRepoUrl ?? void 0
499
+ };
500
+ }
501
+
502
+ export {
503
+ runDeployPipeline
504
+ };
@@ -675,7 +675,7 @@ var init_default = defineCommand({
675
675
  })();
676
676
  if (shouldDeploy) {
677
677
  deployAttempted = true;
678
- const { runDeployPipeline } = await import("./deploy-GDDFTPNG.js");
678
+ const { runDeployPipeline } = await import("./deploy-5PE2UQV4.js");
679
679
  deployResult = await runDeployPipeline({
680
680
  projectDir,
681
681
  projectName,
@@ -690,23 +690,23 @@ var init_default = defineCommand({
690
690
  if (deployResult?.productionUrl) {
691
691
  if (dbUrl) summary += `${pc5.green("\u2713")} Database connected
692
692
  `;
693
- summary += `${pc5.green("\u2713")} Deployed to Vercel
693
+ if (deployResult.githubUrl) summary += `${pc5.green("\u2713")} GitHub: ${pc5.dim(deployResult.githubUrl)}
694
+ `;
695
+ summary += `${pc5.green("\u2713")} Vercel: ${pc5.cyan(deployResult.productionUrl)}
694
696
  `;
695
697
  if (deployResult.whopAppId) summary += `${pc5.green("\u2713")} Whop app: ${deployResult.whopAppId}
696
698
  `;
697
699
  if (deployResult.webhookSecret) summary += `${pc5.green("\u2713")} Webhooks configured
698
700
  `;
699
701
  summary += `
700
- `;
701
- summary += ` ${pc5.bold("Production:")} ${pc5.cyan(deployResult.productionUrl)}
702
- `;
703
- summary += ` ${pc5.bold("Local dev:")} ${pc5.cyan("http://localhost:3000")}
704
- `;
705
- summary += `
706
702
  `;
707
703
  summary += ` ${pc5.bold("cd")} ${basename2(projectName)}
708
704
  `;
709
- summary += ` ${pc5.bold(`${pm} run dev`)} ${pc5.dim("# start local dev server")}`;
705
+ summary += ` ${pc5.bold(`${pm} run dev`)} ${pc5.dim("# local development at localhost:3000")}
706
+ `;
707
+ if (deployResult.githubUrl) {
708
+ summary += ` ${pc5.bold("git push")} ${pc5.dim("# auto-deploys to Vercel")}`;
709
+ }
710
710
  } else if (deployFailed) {
711
711
  if (dbUrl) summary += `${pc5.green("\u2713")} Database configured
712
712
  `;
package/dist/cli-kit.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  } from "./chunk-HOQ5QQ2M.js";
10
10
  import {
11
11
  runDeployPipeline
12
- } from "./chunk-6EDYLQLE.js";
12
+ } from "./chunk-AR3CVSFT.js";
13
13
  import {
14
14
  detectPackageManager,
15
15
  exec
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runDeployPipeline
4
- } from "./chunk-6EDYLQLE.js";
4
+ } from "./chunk-AR3CVSFT.js";
5
5
  import "./chunk-42L7PRMT.js";
6
6
  export {
7
7
  runDeployPipeline
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-whop-kit",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Scaffold and manage Whop-powered apps with whop-kit",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,372 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- exec,
4
- execInteractive,
5
- execWithStdin,
6
- hasCommand
7
- } from "./chunk-42L7PRMT.js";
8
-
9
- // src/deploy/index.ts
10
- import * as p2 from "@clack/prompts";
11
- import pc2 from "picocolors";
12
-
13
- // src/deploy/vercel.ts
14
- import * as p from "@clack/prompts";
15
- import pc from "picocolors";
16
- function isVercelInstalled() {
17
- return hasCommand("vercel");
18
- }
19
- async function installOrUpdateVercel() {
20
- const s = p.spinner();
21
- if (isVercelInstalled()) {
22
- const versionResult = exec("vercel --version");
23
- const currentVersion = versionResult.stdout.replace(/[^0-9.]/g, "");
24
- s.start("Checking for Vercel CLI updates...");
25
- const updateResult = exec("npm install -g vercel@latest", void 0, 6e4);
26
- if (updateResult.success) {
27
- const newVersion = exec("vercel --version");
28
- const newVer = newVersion.stdout.replace(/[^0-9.]/g, "");
29
- if (newVer !== currentVersion) {
30
- s.stop(`Vercel CLI updated: ${currentVersion} \u2192 ${newVer}`);
31
- } else {
32
- s.stop(`Vercel CLI up to date (v${currentVersion})`);
33
- }
34
- } else {
35
- s.stop(`Vercel CLI v${currentVersion} (update check failed, continuing)`);
36
- }
37
- return true;
38
- }
39
- s.start("Installing Vercel CLI...");
40
- const result = exec("npm install -g vercel@latest");
41
- if (result.success) {
42
- s.stop("Vercel CLI installed");
43
- return true;
44
- }
45
- s.stop("Failed to install Vercel CLI");
46
- p.log.error(`Install manually: ${pc.bold("npm install -g vercel@latest")}`);
47
- return false;
48
- }
49
- function isVercelAuthenticated() {
50
- const result = exec("vercel whoami");
51
- return result.success;
52
- }
53
- function getVercelUser() {
54
- const result = exec("vercel whoami");
55
- return result.success ? result.stdout.trim() : null;
56
- }
57
- async function vercelLogin() {
58
- p.log.info("You'll be redirected to Vercel to sign in (or create an account).");
59
- p.log.info(pc.dim("This is needed to deploy your app."));
60
- console.log("");
61
- const ok = execInteractive("vercel login");
62
- console.log("");
63
- return ok;
64
- }
65
- async function vercelLink(projectDir) {
66
- p.log.step("Vercel: linking project...");
67
- console.log("");
68
- const ok = execInteractive("vercel link --yes", projectDir);
69
- console.log("");
70
- return ok;
71
- }
72
- async function vercelDeploy(projectDir) {
73
- const s = p.spinner();
74
- s.start("Vercel: deploying to production (this may take a few minutes)...");
75
- const result = exec("vercel deploy --prod --yes", projectDir, 3e5);
76
- if (!result.success) {
77
- s.stop("Vercel deployment failed");
78
- const errorOutput = result.stderr || result.stdout;
79
- if (errorOutput) {
80
- p.log.error("Build output:");
81
- const trimmed = errorOutput.length > 600 ? "..." + errorOutput.slice(-600) : errorOutput;
82
- console.log(pc.dim(trimmed));
83
- }
84
- return null;
85
- }
86
- const lines = result.stdout.split("\n");
87
- let url = "";
88
- for (const line of lines) {
89
- if (line.includes("Aliased:") || line.includes("Production:")) {
90
- const match = line.match(/https:\/\/[^\s\[\]]+/);
91
- if (match) {
92
- url = match[0];
93
- if (line.includes("Aliased:")) break;
94
- }
95
- }
96
- }
97
- if (!url) {
98
- const urls = [];
99
- for (const line of lines) {
100
- const match = line.match(/https:\/\/[^\s\[\]]+\.vercel\.app/);
101
- if (match) urls.push(match[0]);
102
- }
103
- if (urls.length > 0) {
104
- urls.sort((a, b) => a.length - b.length);
105
- url = urls[0];
106
- }
107
- }
108
- if (url) {
109
- s.stop(`Deployed to ${pc.cyan(url)}`);
110
- return url;
111
- }
112
- for (const line of lines) {
113
- const match = line.match(/https:\/\/[^\s\[\]]+/);
114
- if (match && !match[0].includes("github.com") && !match[0].includes("vercel.com/")) {
115
- s.stop(`Deployed to ${pc.cyan(match[0])}`);
116
- return match[0];
117
- }
118
- }
119
- s.stop("Deployed but could not extract URL");
120
- const manual = await p.text({
121
- message: "Paste your Vercel production URL",
122
- placeholder: "https://your-app.vercel.app",
123
- validate: (v) => {
124
- if (!v?.startsWith("https://")) return "Must be a https:// URL";
125
- }
126
- });
127
- if (p.isCancel(manual)) return null;
128
- return manual;
129
- }
130
- function vercelEnvSet(key, value, environment = "production", projectDir) {
131
- const result = execWithStdin(
132
- `vercel env add ${key} ${environment} --force`,
133
- value,
134
- projectDir
135
- );
136
- return result.success;
137
- }
138
- function vercelEnvSetBatch(vars, projectDir) {
139
- const success = [];
140
- const failed = [];
141
- for (const [key, value] of Object.entries(vars)) {
142
- if (!value) continue;
143
- const ok = vercelEnvSet(key, value, "production", projectDir) && vercelEnvSet(key, value, "preview", projectDir) && vercelEnvSet(key, value, "development", projectDir);
144
- if (ok) success.push(key);
145
- else failed.push(key);
146
- }
147
- return { success, failed };
148
- }
149
-
150
- // src/deploy/whop-api.ts
151
- var WHOP_API = "https://api.whop.com/api/v1";
152
- function headers(apiKey) {
153
- return {
154
- Authorization: `Bearer ${apiKey}`,
155
- "Content-Type": "application/json"
156
- };
157
- }
158
- async function validateApiKey(apiKey) {
159
- try {
160
- const res = await fetch(`${WHOP_API}/apps`, {
161
- headers: headers(apiKey)
162
- });
163
- return res.ok;
164
- } catch {
165
- return false;
166
- }
167
- }
168
- async function createWhopApp(apiKey, name, redirectUris) {
169
- try {
170
- const res = await fetch(`${WHOP_API}/apps`, {
171
- method: "POST",
172
- headers: headers(apiKey),
173
- body: JSON.stringify({
174
- name,
175
- redirect_uris: redirectUris
176
- })
177
- });
178
- if (!res.ok) {
179
- const err = await res.text().catch(() => "");
180
- console.error(`[Whop API] Create app failed (${res.status}): ${err}`);
181
- return null;
182
- }
183
- const data = await res.json();
184
- return {
185
- id: data.id,
186
- client_secret: data.client_secret
187
- };
188
- } catch (err) {
189
- console.error("[Whop API] Create app error:", err);
190
- return null;
191
- }
192
- }
193
- async function createWhopWebhook(apiKey, url, events) {
194
- try {
195
- const res = await fetch(`${WHOP_API}/webhooks`, {
196
- method: "POST",
197
- headers: headers(apiKey),
198
- body: JSON.stringify({
199
- url,
200
- events
201
- })
202
- });
203
- if (!res.ok) {
204
- const err = await res.text().catch(() => "");
205
- console.error(`[Whop API] Create webhook failed (${res.status}): ${err}`);
206
- return null;
207
- }
208
- const data = await res.json();
209
- return {
210
- id: data.id,
211
- secret: data.secret || data.signing_secret || data.webhook_secret || ""
212
- };
213
- } catch (err) {
214
- console.error("[Whop API] Create webhook error:", err);
215
- return null;
216
- }
217
- }
218
-
219
- // src/deploy/index.ts
220
- var WEBHOOK_EVENTS = [
221
- "membership.activated",
222
- "membership.deactivated",
223
- "membership.cancel_at_period_end_changed",
224
- "payment.succeeded",
225
- "payment.failed",
226
- "refund.created"
227
- ];
228
- function openUrl(url) {
229
- const platform = process.platform;
230
- if (platform === "darwin") exec(`open "${url}"`);
231
- else if (platform === "win32") exec(`start "" "${url}"`);
232
- else exec(`xdg-open "${url}"`);
233
- }
234
- async function runDeployPipeline(options) {
235
- const { projectDir, projectName, databaseUrl, framework } = options;
236
- const ok = await installOrUpdateVercel();
237
- if (!ok) return null;
238
- if (!isVercelAuthenticated()) {
239
- const loginOk = await vercelLogin();
240
- if (!loginOk) {
241
- p2.log.error("Vercel authentication failed. Deploy later with: " + pc2.bold("whop-kit deploy"));
242
- return null;
243
- }
244
- }
245
- const user = getVercelUser();
246
- p2.log.success(`Vercel authenticated${user ? ` as ${pc2.bold(user)}` : ""}`);
247
- const linkOk = await vercelLink(projectDir);
248
- if (!linkOk) {
249
- p2.log.warning("Could not link project. Will try deploying directly.");
250
- }
251
- if (databaseUrl) {
252
- const s2 = p2.spinner();
253
- s2.start("Vercel: setting DATABASE_URL...");
254
- vercelEnvSet("DATABASE_URL", databaseUrl, "production", projectDir);
255
- vercelEnvSet("DATABASE_URL", databaseUrl, "preview", projectDir);
256
- vercelEnvSet("DATABASE_URL", databaseUrl, "development", projectDir);
257
- s2.stop("DATABASE_URL set on Vercel");
258
- }
259
- const productionUrl = await vercelDeploy(projectDir);
260
- if (!productionUrl) {
261
- p2.log.error("Vercel deployment failed. Try deploying manually:");
262
- p2.log.info(pc2.bold(` cd ${projectName} && vercel deploy --prod`));
263
- return null;
264
- }
265
- const connectWhop = await p2.confirm({
266
- message: "Connect to Whop? (creates OAuth app + webhooks automatically)",
267
- initialValue: true
268
- });
269
- if (p2.isCancel(connectWhop) || !connectWhop) {
270
- return { productionUrl };
271
- }
272
- p2.log.info("");
273
- p2.note(
274
- [
275
- `${pc2.bold("1.")} Go to the Whop Developer Dashboard`,
276
- ` ${pc2.cyan("https://whop.com/dashboard/developer")}`,
277
- "",
278
- `${pc2.bold("2.")} Click ${pc2.bold('"Create"')} under "Company API Keys"`,
279
- "",
280
- `${pc2.bold("3.")} Name it anything (e.g. "${projectName}")`,
281
- "",
282
- `${pc2.bold("4.")} Select these permissions:`,
283
- ` ${pc2.green("\u2022")} developer:create_app`,
284
- ` ${pc2.green("\u2022")} developer:manage_api_key`,
285
- ` ${pc2.green("\u2022")} developer:manage_webhook`,
286
- "",
287
- `${pc2.bold("5.")} Create the key and paste it below`
288
- ].join("\n"),
289
- "Create a Company API Key"
290
- );
291
- openUrl("https://whop.com/dashboard/developer");
292
- let apiKey = options.whopCompanyKey ?? "";
293
- if (!apiKey) {
294
- const result = await p2.text({
295
- message: "Paste your Company API key",
296
- placeholder: "paste the key here...",
297
- validate: (v) => !v ? "API key is required" : void 0
298
- });
299
- if (p2.isCancel(result)) return { productionUrl };
300
- apiKey = result;
301
- }
302
- const s = p2.spinner();
303
- s.start("Validating API key...");
304
- const keyValid = await validateApiKey(apiKey);
305
- if (!keyValid) {
306
- s.stop("Invalid API key");
307
- p2.log.error("The key was rejected. Check that it has the required permissions:");
308
- p2.log.info(" developer:create_app, developer:manage_api_key, developer:manage_webhook");
309
- p2.log.info(` Dashboard: ${pc2.cyan("https://whop.com/dashboard/developer")}`);
310
- return { productionUrl };
311
- }
312
- s.stop("API key valid");
313
- const callbackPath = framework === "astro" ? "/api/auth/callback" : "/api/auth/callback";
314
- const redirectUris = [
315
- `http://localhost:3000${callbackPath}`,
316
- `${productionUrl}${callbackPath}`
317
- ];
318
- s.start("Creating Whop OAuth app...");
319
- const app = await createWhopApp(apiKey, projectName, redirectUris);
320
- if (!app) {
321
- s.stop("Failed to create Whop app");
322
- p2.log.error("Create it manually in the Whop dashboard.");
323
- return { productionUrl };
324
- }
325
- s.stop(`Whop app created: ${pc2.bold(app.id)}`);
326
- const webhookUrl = `${productionUrl}/api/webhooks/whop`;
327
- s.start("Creating webhook endpoint...");
328
- const webhook = await createWhopWebhook(apiKey, webhookUrl, WEBHOOK_EVENTS);
329
- if (!webhook) {
330
- s.stop("Failed to create webhook");
331
- p2.log.warning("Create it manually in the Whop dashboard.");
332
- } else {
333
- s.stop("Webhook endpoint created");
334
- }
335
- const envVars = {};
336
- if (framework === "nextjs") {
337
- envVars["NEXT_PUBLIC_WHOP_APP_ID"] = app.id;
338
- } else {
339
- envVars["WHOP_APP_ID"] = app.id;
340
- }
341
- envVars["WHOP_API_KEY"] = app.client_secret;
342
- if (webhook?.secret) {
343
- envVars["WHOP_WEBHOOK_SECRET"] = webhook.secret;
344
- }
345
- s.start("Pushing credentials to Vercel...");
346
- const { success, failed } = vercelEnvSetBatch(envVars, projectDir);
347
- if (failed.length > 0) {
348
- s.stop(`Pushed ${success.length} vars, ${failed.length} failed`);
349
- p2.log.warning(`Failed to push: ${failed.join(", ")}. Add them manually in Vercel dashboard.`);
350
- } else {
351
- s.stop(`${success.length} environment variables pushed`);
352
- }
353
- p2.log.step("Vercel: redeploying with full configuration...");
354
- console.log("");
355
- const redeployOk = execInteractive("vercel deploy --prod --yes", projectDir);
356
- console.log("");
357
- if (redeployOk) {
358
- p2.log.success("Redeployed with full configuration");
359
- } else {
360
- p2.log.warning("Redeploy failed \u2014 env vars will apply on next deploy/push");
361
- }
362
- return {
363
- productionUrl,
364
- whopAppId: app.id,
365
- whopApiKey: app.client_secret,
366
- webhookSecret: webhook?.secret
367
- };
368
- }
369
-
370
- export {
371
- runDeployPipeline
372
- };