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