create-whop-kit 0.4.0 → 0.5.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/README.md CHANGED
@@ -1,41 +1,104 @@
1
1
  # create-whop-kit
2
2
 
3
- Scaffold a new [Whop](https://whop.com)-powered app with [whop-kit](https://www.npmjs.com/package/whop-kit).
3
+ Scaffold and manage [Whop](https://whop.com)-powered apps with [whop-kit](https://www.npmjs.com/package/whop-kit).
4
4
 
5
- ## Usage
5
+ ## Create a new project
6
6
 
7
7
  ```bash
8
8
  npx create-whop-kit my-app
9
9
  ```
10
10
 
11
- You'll be prompted to choose:
11
+ Interactive prompts guide you through:
12
12
 
13
- 1. **What you're building** — SaaS, Course, Community, or Blank
14
- 2. **Framework** — Next.js (more coming soon)
15
- 3. **Database** — Neon, Supabase, Local PostgreSQL, or configure later
13
+ 1. **What are you building?** — SaaS (full dashboard + billing) or Blank (just auth + webhooks)
14
+ 2. **Which framework?** — Next.js or Astro
15
+ 3. **Which database?** — Neon (auto-provisioned), Prisma Postgres (instant), Supabase, manual URL, or skip
16
+ 4. **Whop credentials** — App ID, API key, webhook secret (optional, can use setup wizard later)
16
17
 
17
- The CLI clones the template, installs dependencies, configures your `.env.local`, and initializes a git repo.
18
+ The CLI clones a template, provisions your database, writes `.env.local`, installs dependencies, and initializes git.
18
19
 
19
- ## Options
20
+ ### Non-interactive mode
20
21
 
21
22
  ```bash
22
- # Provide the project name as an argument
23
- npx create-whop-kit my-app
23
+ # Skip all prompts
24
+ npx create-whop-kit my-app --framework nextjs --type saas --db neon --yes
25
+
26
+ # With credentials
27
+ npx create-whop-kit my-app --framework nextjs --db later --app-id "app_xxx" --api-key "apik_xxx"
24
28
 
25
- # Or run interactively
26
- npx create-whop-kit
29
+ # Preview without creating files
30
+ npx create-whop-kit my-app --framework nextjs --db later --dry-run
27
31
  ```
28
32
 
33
+ ### All flags
34
+
35
+ | Flag | Description |
36
+ |------|-------------|
37
+ | `--framework` | `nextjs` or `astro` |
38
+ | `--type` | `saas` or `blank` (default: `saas`) |
39
+ | `--db` | `neon`, `prisma-postgres`, `supabase`, `manual`, `later` |
40
+ | `--db-url` | PostgreSQL connection URL (skips DB provisioning) |
41
+ | `--app-id` | Whop App ID |
42
+ | `--api-key` | Whop API Key |
43
+ | `--webhook-secret` | Whop webhook secret |
44
+ | `-y, --yes` | Skip optional prompts |
45
+ | `--dry-run` | Show what would be created |
46
+ | `--verbose` | Detailed output |
47
+
48
+ ## Manage your project
49
+
50
+ After creating a project, use `whop-kit` to add features and check status:
51
+
52
+ ```bash
53
+ # Check project health
54
+ npx whop-kit status
55
+
56
+ # Add email (Resend or SendGrid)
57
+ npx whop-kit add email
58
+
59
+ # Add analytics (PostHog, Google Analytics, or Plausible)
60
+ npx whop-kit add analytics
61
+
62
+ # Add a webhook event handler
63
+ npx whop-kit add webhook-event
64
+
65
+ # Open provider dashboards
66
+ npx whop-kit open whop
67
+ npx whop-kit open neon
68
+ npx whop-kit open vercel
69
+
70
+ # Update whop-kit to latest
71
+ npx whop-kit upgrade
72
+ ```
73
+
74
+ ## Database provisioning
75
+
76
+ The CLI can provision databases automatically — no need to leave the terminal:
77
+
78
+ | Provider | How it works |
79
+ |----------|-------------|
80
+ | **Neon** | Installs `neonctl` → authenticates (browser) → creates project → gets connection string |
81
+ | **Prisma Postgres** | Runs `npx create-db` → instant database, no account needed |
82
+ | **Supabase** | Installs CLI → authenticates → creates project → guides you to get connection string |
83
+
29
84
  ## Templates
30
85
 
31
- | Template | Framework | Status |
32
- |----------|-----------|--------|
33
- | SaaS | Next.js | Available |
34
- | SaaS | Astro | Coming soon |
35
- | SaaS | TanStack Start | Coming soon |
36
- | Course | — | Coming soon |
37
- | Community | — | Coming soon |
38
- | Blank | — | Coming soon |
86
+ | App Type | Framework | Template | Status |
87
+ |----------|-----------|----------|--------|
88
+ | SaaS | Next.js | Full dashboard, pricing, billing, docs | Available |
89
+ | SaaS | Astro | Auth, payments, webhooks | Available |
90
+ | Blank | Next.js | Just auth + webhooks — build anything | Available |
91
+ | Course | — | — | Coming soon |
92
+ | Community | — | — | Coming soon |
93
+
94
+ ## How it works
95
+
96
+ 1. **Template** — clones a starter repo from GitHub
97
+ 2. **Database** — optionally provisions via provider CLI
98
+ 3. **Environment** — writes `.env.local` from the template's `.env.example`
99
+ 4. **Manifest** — creates `.whop/config.json` tracking your project state
100
+ 5. **Dependencies** — installs with your preferred package manager
101
+ 6. **Git** — initializes a fresh repo
39
102
 
40
103
  ## License
41
104
 
@@ -0,0 +1,370 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/templates.ts
4
+ var FRAMEWORKS = {
5
+ nextjs: {
6
+ name: "Next.js",
7
+ description: "Full-stack React with App Router, SSR, and API routes",
8
+ available: true
9
+ },
10
+ astro: {
11
+ name: "Astro",
12
+ description: "Content-focused with islands architecture",
13
+ available: true
14
+ },
15
+ tanstack: {
16
+ name: "TanStack Start",
17
+ description: "Full-stack React with TanStack Router",
18
+ available: false
19
+ },
20
+ vite: {
21
+ name: "Vite + React",
22
+ description: "Lightweight SPA with Vite bundler",
23
+ available: false
24
+ }
25
+ };
26
+ var TEMPLATES = {
27
+ "saas:nextjs": {
28
+ name: "Next.js SaaS",
29
+ description: "Full SaaS with dashboard, pricing, billing, and docs",
30
+ repo: "colinmcdermott/whop-saas-starter-v2",
31
+ available: true
32
+ },
33
+ "saas:astro": {
34
+ name: "Astro SaaS",
35
+ description: "SaaS with auth, payments, and webhooks",
36
+ repo: "colinmcdermott/whop-astro-starter",
37
+ available: true
38
+ },
39
+ "blank:nextjs": {
40
+ name: "Next.js Blank",
41
+ description: "Just auth + webhooks \u2014 build anything",
42
+ repo: "colinmcdermott/whop-blank-starter",
43
+ available: true
44
+ }
45
+ };
46
+ function getTemplate(appType, framework) {
47
+ return TEMPLATES[`${appType}:${framework}`] ?? null;
48
+ }
49
+ var APP_TYPES = {
50
+ saas: {
51
+ name: "SaaS",
52
+ description: "Subscription tiers, dashboard, billing portal",
53
+ available: true
54
+ },
55
+ blank: {
56
+ name: "Blank",
57
+ description: "Just auth + payments, you build the rest",
58
+ available: true
59
+ },
60
+ course: {
61
+ name: "Course",
62
+ description: "Lessons, progress tracking, drip content",
63
+ available: false
64
+ },
65
+ community: {
66
+ name: "Community",
67
+ description: "Member feeds, gated content, roles",
68
+ available: false
69
+ }
70
+ };
71
+
72
+ // src/utils/exec.ts
73
+ import { execSync } from "child_process";
74
+ function exec(cmd, cwd) {
75
+ try {
76
+ const stdout = execSync(cmd, {
77
+ cwd,
78
+ stdio: "pipe",
79
+ encoding: "utf-8",
80
+ timeout: 12e4
81
+ }).trim();
82
+ return { stdout, success: true };
83
+ } catch {
84
+ return { stdout: "", success: false };
85
+ }
86
+ }
87
+ function execInteractive(cmd, cwd) {
88
+ try {
89
+ execSync(cmd, { cwd, stdio: "inherit", timeout: 3e5 });
90
+ return true;
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+ function hasCommand(cmd) {
96
+ return exec(`which ${cmd}`).success;
97
+ }
98
+ function detectPackageManager() {
99
+ if (hasCommand("pnpm")) return "pnpm";
100
+ if (hasCommand("yarn")) return "yarn";
101
+ if (hasCommand("bun")) return "bun";
102
+ return "npm";
103
+ }
104
+
105
+ // src/scaffolding/manifest.ts
106
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
107
+ import { join } from "path";
108
+ var MANIFEST_DIR = ".whop";
109
+ var MANIFEST_FILE = "config.json";
110
+ function getManifestPath(projectDir) {
111
+ return join(projectDir, MANIFEST_DIR, MANIFEST_FILE);
112
+ }
113
+ function createManifest(projectDir, data) {
114
+ const dir = join(projectDir, MANIFEST_DIR);
115
+ if (!existsSync(dir)) {
116
+ mkdirSync(dir, { recursive: true });
117
+ }
118
+ const manifest = {
119
+ version: 1,
120
+ ...data,
121
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
122
+ };
123
+ writeFileSync(getManifestPath(projectDir), JSON.stringify(manifest, null, 2) + "\n");
124
+ }
125
+ function readManifest(projectDir) {
126
+ const path = getManifestPath(projectDir);
127
+ if (!existsSync(path)) return null;
128
+ try {
129
+ return JSON.parse(readFileSync(path, "utf-8"));
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
134
+ function addFeatureToManifest(projectDir, feature) {
135
+ const manifest = readManifest(projectDir);
136
+ if (!manifest) return;
137
+ if (!manifest.features.includes(feature)) {
138
+ manifest.features.push(feature);
139
+ }
140
+ writeFileSync(getManifestPath(projectDir), JSON.stringify(manifest, null, 2) + "\n");
141
+ }
142
+
143
+ // src/scaffolding/skills.ts
144
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
145
+ import { join as join2 } from "path";
146
+ var PROVIDER_SKILLS = {
147
+ neon: [
148
+ { repo: "https://github.com/neondatabase/agent-skills", skill: "neon-postgres" },
149
+ { repo: "https://github.com/neondatabase/ai-rules", skill: "neon-serverless" }
150
+ ],
151
+ supabase: [
152
+ { repo: "https://github.com/supabase/agent-skills", skill: "supabase-postgres-best-practices" }
153
+ ],
154
+ "prisma-postgres": [
155
+ // Prisma skill is typically bundled in the template already
156
+ ]
157
+ };
158
+ function installProviderSkills(projectDir, provider) {
159
+ const skills = PROVIDER_SKILLS[provider];
160
+ if (!skills || skills.length === 0) return;
161
+ for (const { repo, skill } of skills) {
162
+ const result = exec(
163
+ `npx -y skills add ${repo} --skill ${skill}`,
164
+ projectDir
165
+ );
166
+ if (!result.success) {
167
+ console.log(` (Could not install ${skill} skill \u2014 you can add it later)`);
168
+ }
169
+ }
170
+ }
171
+ function writeProjectContext(projectDir, manifest, envVars) {
172
+ const dir = join2(projectDir, ".whop");
173
+ if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
174
+ const lines = [
175
+ "# Project Context",
176
+ "",
177
+ "Auto-generated by create-whop-kit. Helps AI coding assistants understand",
178
+ "your project. Regenerated by `whop-kit status`. Do not edit manually.",
179
+ "",
180
+ "## Project",
181
+ "",
182
+ `- **Framework:** ${manifest.framework}`,
183
+ `- **App Type:** ${manifest.appType}`,
184
+ `- **Database:** ${manifest.database}`,
185
+ `- **Template Version:** ${manifest.templateVersion}`,
186
+ `- **Created:** ${manifest.createdAt}`,
187
+ "",
188
+ "## Configuration Status",
189
+ ""
190
+ ];
191
+ const checks = [
192
+ ["DATABASE_URL", "Database", true],
193
+ ["NEXT_PUBLIC_WHOP_APP_ID", "Whop App ID", true],
194
+ ["WHOP_API_KEY", "Whop API Key", true],
195
+ ["WHOP_WEBHOOK_SECRET", "Webhook Secret", true],
196
+ ["EMAIL_PROVIDER", "Email", false],
197
+ ["ANALYTICS_PROVIDER", "Analytics", false]
198
+ ];
199
+ for (const [key, label, required] of checks) {
200
+ const set = envVars[key] ?? false;
201
+ const icon = set ? "\u2713" : required ? "\u2717" : "\u25CB";
202
+ lines.push(`- ${icon} ${label} (\`${key}\`)`);
203
+ }
204
+ if (manifest.features.length > 0) {
205
+ lines.push("");
206
+ lines.push("## Installed Features");
207
+ lines.push("");
208
+ for (const feature of manifest.features) {
209
+ lines.push(`- ${feature}`);
210
+ }
211
+ }
212
+ lines.push("");
213
+ lines.push("## Key Files");
214
+ lines.push("");
215
+ if (manifest.framework === "nextjs") {
216
+ lines.push("- `lib/auth.ts` \u2014 session management (whop-kit/auth)");
217
+ lines.push("- `lib/adapters/next.ts` \u2014 Next.js cookie adapter");
218
+ lines.push("- `lib/adapters/prisma.ts` \u2014 Prisma DB + config adapters");
219
+ lines.push("- `app/api/webhooks/whop/route.ts` \u2014 webhook handler");
220
+ lines.push("- `app/api/auth/` \u2014 OAuth login/callback/logout");
221
+ lines.push("- `db/schema.prisma` \u2014 database schema");
222
+ } else if (manifest.framework === "astro") {
223
+ lines.push("- `src/lib/auth.ts` \u2014 session management (whop-kit/auth)");
224
+ lines.push("- `src/lib/adapters/astro.ts` \u2014 Astro cookie adapter");
225
+ lines.push("- `src/lib/adapters/prisma.ts` \u2014 Prisma DB adapter");
226
+ lines.push("- `src/pages/api/webhooks/whop.ts` \u2014 webhook handler");
227
+ lines.push("- `src/pages/api/auth/` \u2014 OAuth login/callback/logout");
228
+ }
229
+ lines.push("");
230
+ lines.push("## CLI Commands");
231
+ lines.push("");
232
+ lines.push("```bash");
233
+ lines.push("npx whop-kit status # check project health");
234
+ lines.push("npx whop-kit add email # add email provider");
235
+ lines.push("npx whop-kit add analytics # add analytics");
236
+ lines.push("npx whop-kit catalog # list available services");
237
+ lines.push("npx whop-kit open whop # open Whop dashboard");
238
+ lines.push("npx whop-kit upgrade # update whop-kit");
239
+ lines.push("```");
240
+ lines.push("");
241
+ writeFileSync2(join2(dir, "project-context.md"), lines.join("\n"));
242
+ }
243
+ function writeFeatureSkill(projectDir, featureKey, provider) {
244
+ const dir = join2(projectDir, ".agents", "skills", "whop-kit-features");
245
+ if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
246
+ const content = generateFeatureSkill(featureKey, provider);
247
+ if (content) {
248
+ writeFileSync2(join2(dir, `${featureKey}-${provider}.md`), content);
249
+ }
250
+ }
251
+ function generateFeatureSkill(featureKey, provider) {
252
+ const key = `${featureKey}:${provider}`;
253
+ const skills = {
254
+ "email:resend": `---
255
+ name: resend-email
256
+ description: Resend transactional email integration. Use when working with email sending, templates, or email configuration.
257
+ ---
258
+
259
+ # Resend Email
260
+
261
+ This project uses Resend for transactional email via whop-kit/email.
262
+
263
+ ## Usage
264
+ \`\`\`typescript
265
+ import { sendEmail } from "@/lib/email";
266
+ await sendEmail({ to: "user@example.com", subject: "Hello", html: "<p>Hi!</p>" });
267
+ \`\`\`
268
+
269
+ ## Config
270
+ - \`EMAIL_PROVIDER=resend\`
271
+ - \`EMAIL_API_KEY\` \u2014 Resend API key
272
+ - \`EMAIL_FROM_ADDRESS\` \u2014 verified sender
273
+
274
+ ## Templates
275
+ Edit \`lib/email-templates.ts\`. Each function returns \`{ subject, html }\`.
276
+
277
+ ## Dashboard
278
+ https://resend.com/overview
279
+ `,
280
+ "email:sendgrid": `---
281
+ name: sendgrid-email
282
+ description: SendGrid transactional email integration. Use when working with email sending, templates, or email configuration.
283
+ ---
284
+
285
+ # SendGrid Email
286
+
287
+ This project uses SendGrid for transactional email via whop-kit/email.
288
+
289
+ ## Usage
290
+ \`\`\`typescript
291
+ import { sendEmail } from "@/lib/email";
292
+ await sendEmail({ to: "user@example.com", subject: "Hello", html: "<p>Hi!</p>" });
293
+ \`\`\`
294
+
295
+ ## Config
296
+ - \`EMAIL_PROVIDER=sendgrid\`
297
+ - \`EMAIL_API_KEY\` \u2014 SendGrid API key
298
+ - \`EMAIL_FROM_ADDRESS\` \u2014 verified sender
299
+
300
+ ## Dashboard
301
+ https://app.sendgrid.com
302
+ `,
303
+ "analytics:posthog": `---
304
+ name: posthog-analytics
305
+ description: PostHog product analytics integration. Use when working with analytics, events, or feature flags.
306
+ ---
307
+
308
+ # PostHog Analytics
309
+
310
+ Injected via whop-kit/analytics \`getAnalyticsScript()\`. No client SDK needed.
311
+
312
+ ## Config
313
+ - \`ANALYTICS_PROVIDER=posthog\`
314
+ - \`ANALYTICS_ID\` \u2014 project API key (phc_xxx)
315
+
316
+ ## Dashboard
317
+ https://us.posthog.com
318
+ `,
319
+ "analytics:google": `---
320
+ name: google-analytics
321
+ description: Google Analytics 4 integration.
322
+ ---
323
+
324
+ # Google Analytics 4
325
+
326
+ Injected via whop-kit/analytics \`getAnalyticsScript()\`.
327
+
328
+ ## Config
329
+ - \`ANALYTICS_PROVIDER=google\`
330
+ - \`ANALYTICS_ID\` \u2014 measurement ID (G-XXXXXXXXXX)
331
+
332
+ ## Dashboard
333
+ https://analytics.google.com
334
+ `,
335
+ "analytics:plausible": `---
336
+ name: plausible-analytics
337
+ description: Plausible privacy-friendly analytics integration.
338
+ ---
339
+
340
+ # Plausible Analytics
341
+
342
+ Injected via whop-kit/analytics \`getAnalyticsScript()\`. No cookies, GDPR compliant.
343
+
344
+ ## Config
345
+ - \`ANALYTICS_PROVIDER=plausible\`
346
+ - \`ANALYTICS_ID\` \u2014 your domain
347
+
348
+ ## Dashboard
349
+ https://plausible.io/sites
350
+ `
351
+ };
352
+ return skills[key] ?? null;
353
+ }
354
+
355
+ export {
356
+ FRAMEWORKS,
357
+ TEMPLATES,
358
+ getTemplate,
359
+ APP_TYPES,
360
+ exec,
361
+ execInteractive,
362
+ hasCommand,
363
+ detectPackageManager,
364
+ createManifest,
365
+ readManifest,
366
+ addFeatureToManifest,
367
+ installProviderSkills,
368
+ writeProjectContext,
369
+ writeFeatureSkill
370
+ };
@@ -1,10 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ APP_TYPES,
4
+ FRAMEWORKS,
3
5
  createManifest,
4
6
  detectPackageManager,
5
7
  exec,
6
- hasCommand
7
- } from "./chunk-M4AXERQP.js";
8
+ execInteractive,
9
+ getTemplate,
10
+ hasCommand,
11
+ installProviderSkills,
12
+ writeProjectContext
13
+ } from "./chunk-BR467LBM.js";
8
14
 
9
15
  // src/cli-create.ts
10
16
  import { runMain } from "citty";
@@ -16,75 +22,6 @@ import * as p5 from "@clack/prompts";
16
22
  import pc5 from "picocolors";
17
23
  import { defineCommand } from "citty";
18
24
 
19
- // src/templates.ts
20
- var FRAMEWORKS = {
21
- nextjs: {
22
- name: "Next.js",
23
- description: "Full-stack React with App Router, SSR, and API routes",
24
- available: true
25
- },
26
- astro: {
27
- name: "Astro",
28
- description: "Content-focused with islands architecture",
29
- available: true
30
- },
31
- tanstack: {
32
- name: "TanStack Start",
33
- description: "Full-stack React with TanStack Router",
34
- available: false
35
- },
36
- vite: {
37
- name: "Vite + React",
38
- description: "Lightweight SPA with Vite bundler",
39
- available: false
40
- }
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
- }
65
- var APP_TYPES = {
66
- saas: {
67
- name: "SaaS",
68
- description: "Subscription tiers, dashboard, billing portal",
69
- available: true
70
- },
71
- blank: {
72
- name: "Blank",
73
- description: "Just auth + payments, you build the rest",
74
- available: true
75
- },
76
- course: {
77
- name: "Course",
78
- description: "Lessons, progress tracking, drip content",
79
- available: false
80
- },
81
- community: {
82
- name: "Community",
83
- description: "Member feeds, gated content, roles",
84
- available: false
85
- }
86
- };
87
-
88
25
  // src/providers/neon.ts
89
26
  import * as p from "@clack/prompts";
90
27
  import pc from "picocolors";
@@ -108,38 +45,72 @@ var neonProvider = {
108
45
  },
109
46
  async provision(projectName) {
110
47
  const cli = hasCommand("neonctl") ? "neonctl" : "neon";
111
- const whoami = exec(`${cli} me`);
48
+ const whoami = exec(`${cli} me --output json`);
112
49
  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) {
50
+ p.log.info("You need to authenticate with Neon. This will open your browser.");
51
+ console.log("");
52
+ const authOk = execInteractive(`${cli} auth`);
53
+ if (!authOk) {
117
54
  p.log.error("Neon authentication failed. Try running manually:");
118
55
  p.log.info(pc.bold(` ${cli} auth`));
119
56
  return null;
120
57
  }
58
+ console.log("");
121
59
  }
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`
60
+ p.log.info(`Creating Neon project "${projectName}"...`);
61
+ console.log("");
62
+ const createOk = execInteractive(
63
+ `${cli} projects create --name "${projectName}" --set-context`
126
64
  );
127
- if (!createResult.success) {
128
- s.stop("Failed to create Neon project");
129
- p.log.error("Try creating manually at https://console.neon.tech");
65
+ if (!createOk) {
66
+ p.log.error("Failed to create Neon project. Try manually at https://console.neon.tech");
130
67
  return null;
131
68
  }
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");
69
+ console.log("");
70
+ p.log.success("Neon project created");
71
+ let connString = "";
72
+ const connResult = exec(`${cli} connection-string --prisma --output json`);
73
+ if (connResult.success) {
74
+ try {
75
+ const parsed = JSON.parse(connResult.stdout);
76
+ connString = parsed.connection_string || parsed.connectionString || connResult.stdout;
77
+ } catch {
78
+ connString = connResult.stdout.trim();
79
+ }
80
+ }
81
+ if (!connString) {
82
+ const fallback = exec(`${cli} connection-string --prisma`);
83
+ if (fallback.success && fallback.stdout.startsWith("postgres")) {
84
+ connString = fallback.stdout.trim();
85
+ }
86
+ }
87
+ if (!connString) {
88
+ const raw = exec(`${cli} connection-string`);
89
+ if (raw.success && raw.stdout.startsWith("postgres")) {
90
+ connString = raw.stdout.trim();
91
+ }
92
+ }
93
+ if (!connString) {
94
+ p.log.warning("Could not extract connection string automatically.");
95
+ console.log("");
96
+ execInteractive(`${cli} connection-string`);
97
+ console.log("");
98
+ const manual = await p.text({
99
+ message: "Paste the connection string shown above",
100
+ placeholder: "postgresql://...",
101
+ validate: (v) => {
102
+ if (!v?.startsWith("postgres")) return "Must be a PostgreSQL connection string";
103
+ }
104
+ });
105
+ if (p.isCancel(manual)) return null;
106
+ connString = manual;
107
+ }
108
+ if (!connString) {
109
+ p.log.error("Could not get connection string. Get it from: https://console.neon.tech");
138
110
  return null;
139
111
  }
140
- s.stop("Connection string retrieved");
141
112
  return {
142
- connectionString: connResult.stdout,
113
+ connectionString: connString,
143
114
  provider: "neon"
144
115
  };
145
116
  }
@@ -705,6 +676,28 @@ var init_default = defineCommand({
705
676
  features: [],
706
677
  templateVersion: "0.2.0"
707
678
  });
679
+ if (database !== "later" && database !== "manual") {
680
+ s.start("Installing provider skills for AI assistants...");
681
+ installProviderSkills(projectDir, database);
682
+ s.stop("Provider skills installed");
683
+ }
684
+ const envStatus = {};
685
+ if (dbUrl) envStatus["DATABASE_URL"] = true;
686
+ if (appId) {
687
+ envStatus["NEXT_PUBLIC_WHOP_APP_ID"] = true;
688
+ envStatus["WHOP_APP_ID"] = true;
689
+ }
690
+ if (apiKey) envStatus["WHOP_API_KEY"] = true;
691
+ if (webhookSecret) envStatus["WHOP_WEBHOOK_SECRET"] = true;
692
+ const manifest = {
693
+ framework,
694
+ appType,
695
+ database,
696
+ features: [],
697
+ templateVersion: "0.4.0",
698
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
699
+ };
700
+ writeProjectContext(projectDir, { version: 1, ...manifest }, envStatus);
708
701
  const pm = detectPackageManager();
709
702
  s.start(`Installing dependencies with ${pm}...`);
710
703
  const installResult = exec(`${pm} install`, projectDir);
@@ -733,7 +726,7 @@ var init_default = defineCommand({
733
726
  if (missing.length > 0) {
734
727
  summary += `${pc5.yellow("\u25CB")} Missing: ${missing.join(", ")}
735
728
  `;
736
- summary += ` ${pc5.dim("Configure via the setup wizard or .env.local")}
729
+ summary += ` ${pc5.dim("The setup wizard at http://localhost:3000 will guide you through it")}
737
730
  `;
738
731
  }
739
732
  if (dbNote) {
@@ -745,10 +738,14 @@ var init_default = defineCommand({
745
738
  summary += ` ${pc5.bold("cd")} ${basename2(projectName)}
746
739
  `;
747
740
  if (dbUrl) {
748
- summary += ` ${pc5.bold(`${pm} run db:push`)}
741
+ summary += ` ${pc5.bold(`${pm} run db:push`)} ${pc5.dim("# push schema to database")}
749
742
  `;
750
743
  }
751
- summary += ` ${pc5.bold(`${pm} run dev`)}`;
744
+ summary += ` ${pc5.bold(`${pm} run dev`)} ${pc5.dim("# start dev server")}
745
+ `;
746
+ summary += `
747
+ `;
748
+ summary += ` ${pc5.dim("Then open http://localhost:3000")}`;
752
749
  p5.note(summary, "Your app is ready");
753
750
  p5.outro(`${pc5.green("Happy building!")} ${pc5.dim("\u2014 whop-kit")}`);
754
751
  }
package/dist/cli-kit.js CHANGED
@@ -1,13 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ APP_TYPES,
4
+ FRAMEWORKS,
5
+ TEMPLATES,
3
6
  addFeatureToManifest,
4
7
  detectPackageManager,
5
8
  exec,
6
- readManifest
7
- } from "./chunk-M4AXERQP.js";
9
+ readManifest,
10
+ writeFeatureSkill
11
+ } from "./chunk-BR467LBM.js";
8
12
 
9
13
  // src/cli-kit.ts
10
- import { defineCommand as defineCommand5, runMain } from "citty";
14
+ import { defineCommand as defineCommand7, runMain } from "citty";
11
15
 
12
16
  // src/commands/add.ts
13
17
  import * as p4 from "@clack/prompts";
@@ -78,6 +82,7 @@ var emailFeature = {
78
82
  if (fromAddress) {
79
83
  appendEnvVar(projectDir, "EMAIL_FROM_ADDRESS", fromAddress);
80
84
  }
85
+ writeFeatureSkill(projectDir, "email", provider);
81
86
  }
82
87
  };
83
88
 
@@ -116,6 +121,7 @@ var analyticsFeature = {
116
121
  }
117
122
  appendEnvVar(projectDir, "ANALYTICS_PROVIDER", provider);
118
123
  appendEnvVar(projectDir, "ANALYTICS_ID", id);
124
+ writeFeatureSkill(projectDir, "analytics", provider);
119
125
  }
120
126
  };
121
127
 
@@ -304,10 +310,152 @@ var status_default = defineCommand2({
304
310
  }
305
311
  });
306
312
 
307
- // src/commands/open.ts
313
+ // src/commands/env.ts
314
+ import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
315
+ import { join as join3 } from "path";
308
316
  import * as p6 from "@clack/prompts";
309
317
  import pc4 from "picocolors";
310
318
  import { defineCommand as defineCommand3 } from "citty";
319
+ function parseEnvFile(projectDir) {
320
+ for (const name of [".env.local", ".env"]) {
321
+ const path = join3(projectDir, name);
322
+ if (existsSync3(path)) {
323
+ const content = readFileSync3(path, "utf-8");
324
+ const vars = {};
325
+ for (const line of content.split("\n")) {
326
+ const trimmed = line.trim();
327
+ if (!trimmed || trimmed.startsWith("#")) continue;
328
+ const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)=["']?(.*)["']?$/);
329
+ if (match) vars[match[1]] = match[2].replace(/["']$/, "");
330
+ }
331
+ return vars;
332
+ }
333
+ }
334
+ return {};
335
+ }
336
+ function maskValue(value) {
337
+ if (value.length <= 8) return "****";
338
+ return value.substring(0, 8) + "..." + "*".repeat(4);
339
+ }
340
+ var env_default = defineCommand3({
341
+ meta: {
342
+ name: "env",
343
+ description: "View environment variables (masked by default)"
344
+ },
345
+ args: {
346
+ reveal: {
347
+ type: "boolean",
348
+ description: "Show actual values instead of masked",
349
+ default: false
350
+ }
351
+ },
352
+ async run({ args }) {
353
+ console.log("");
354
+ p6.intro(`${pc4.bgCyan(pc4.black(" whop-kit env "))}`);
355
+ const manifest = readManifest(".");
356
+ if (!manifest) {
357
+ p6.log.error("No .whop/config.json found. Are you in a whop-kit project?");
358
+ process.exit(1);
359
+ }
360
+ const vars = parseEnvFile(".");
361
+ if (Object.keys(vars).length === 0) {
362
+ p6.log.warning("No environment variables found. Create .env.local with your configuration.");
363
+ p6.outro("");
364
+ return;
365
+ }
366
+ if (args.reveal) {
367
+ p6.log.warning("Revealing secret values:");
368
+ console.log("");
369
+ }
370
+ for (const [key, value] of Object.entries(vars)) {
371
+ const displayValue = args.reveal ? pc4.cyan(value) : pc4.dim(maskValue(value));
372
+ console.log(` ${pc4.bold(key.padEnd(40))} ${displayValue}`);
373
+ }
374
+ console.log("");
375
+ if (!args.reveal) {
376
+ p6.log.info(`Use ${pc4.bold("whop-kit env --reveal")} to show actual values`);
377
+ }
378
+ p6.outro("");
379
+ }
380
+ });
381
+
382
+ // src/commands/catalog.ts
383
+ import * as p7 from "@clack/prompts";
384
+ import pc5 from "picocolors";
385
+ import { defineCommand as defineCommand4 } from "citty";
386
+ var catalog_default = defineCommand4({
387
+ meta: {
388
+ name: "catalog",
389
+ description: "List available templates, frameworks, databases, and features"
390
+ },
391
+ async run() {
392
+ console.log("");
393
+ p7.intro(`${pc5.bgCyan(pc5.black(" whop-kit catalog "))} Available services`);
394
+ console.log(`
395
+ ${pc5.bold(pc5.underline("App Types"))}
396
+ `);
397
+ for (const [key, type] of Object.entries(APP_TYPES)) {
398
+ const status = type.available ? pc5.green("available") : pc5.dim("coming soon");
399
+ console.log(` ${pc5.bold(key.padEnd(15))} ${type.description.padEnd(45)} ${status}`);
400
+ }
401
+ console.log(`
402
+ ${pc5.bold(pc5.underline("Frameworks"))}
403
+ `);
404
+ for (const [key, fw] of Object.entries(FRAMEWORKS)) {
405
+ const status = fw.available ? pc5.green("available") : pc5.dim("coming soon");
406
+ console.log(` ${pc5.bold(key.padEnd(15))} ${fw.description.padEnd(45)} ${status}`);
407
+ }
408
+ console.log(`
409
+ ${pc5.bold(pc5.underline("Templates"))}
410
+ `);
411
+ for (const [key, tmpl] of Object.entries(TEMPLATES)) {
412
+ const status = tmpl.available ? pc5.green("available") : pc5.dim("coming soon");
413
+ console.log(` ${pc5.bold(key.padEnd(20))} ${tmpl.description.padEnd(40)} ${status}`);
414
+ }
415
+ console.log(`
416
+ ${pc5.bold(pc5.underline("Database Providers"))}
417
+ `);
418
+ const dbProviders = [
419
+ { key: "neon", name: "Neon", desc: "Serverless Postgres, auto-provisioned", status: "auto" },
420
+ { key: "prisma-postgres", name: "Prisma Postgres", desc: "Instant database, no auth needed", status: "auto" },
421
+ { key: "supabase", name: "Supabase", desc: "Open-source Firebase alternative", status: "guided" },
422
+ { key: "manual", name: "Manual", desc: "Paste any PostgreSQL connection string", status: "manual" }
423
+ ];
424
+ for (const db of dbProviders) {
425
+ const badge = db.status === "auto" ? pc5.green("auto-provision") : db.status === "guided" ? pc5.yellow("guided setup") : pc5.dim("manual");
426
+ console.log(` ${pc5.bold(db.key.padEnd(20))} ${db.desc.padEnd(40)} ${badge}`);
427
+ }
428
+ console.log(`
429
+ ${pc5.bold(pc5.underline("Features (whop-kit add)"))}
430
+ `);
431
+ const features = [
432
+ { key: "email", desc: "Transactional email", providers: "Resend, SendGrid" },
433
+ { key: "analytics", desc: "Product analytics", providers: "PostHog, Google Analytics, Plausible" },
434
+ { key: "webhook-event", desc: "Add webhook event handler", providers: "\u2014" }
435
+ ];
436
+ for (const feat of features) {
437
+ console.log(` ${pc5.bold(feat.key.padEnd(20))} ${feat.desc.padEnd(30)} ${pc5.dim(feat.providers)}`);
438
+ }
439
+ console.log(`
440
+ ${pc5.bold(pc5.underline("Agent Skills (auto-installed)"))}
441
+ `);
442
+ const skills = [
443
+ { provider: "Neon", skills: "neon-postgres, neon-serverless" },
444
+ { provider: "Supabase", skills: "supabase-postgres-best-practices" },
445
+ { provider: "Whop", skills: "whop-saas-starter, whop-dev" }
446
+ ];
447
+ for (const s of skills) {
448
+ console.log(` ${pc5.bold(s.provider.padEnd(20))} ${pc5.dim(s.skills)}`);
449
+ }
450
+ console.log("");
451
+ p7.outro(`Run ${pc5.bold("npx create-whop-kit")} to get started`);
452
+ }
453
+ });
454
+
455
+ // src/commands/open.ts
456
+ import * as p8 from "@clack/prompts";
457
+ import pc6 from "picocolors";
458
+ import { defineCommand as defineCommand5 } from "citty";
311
459
  var DASHBOARDS = {
312
460
  whop: { name: "Whop Developer Dashboard", url: "https://whop.com/dashboard/developer" },
313
461
  neon: { name: "Neon Console", url: "https://console.neon.tech" },
@@ -320,7 +468,7 @@ function openUrl(url) {
320
468
  else if (platform === "win32") exec(`start "${url}"`);
321
469
  else exec(`xdg-open "${url}"`);
322
470
  }
323
- var open_default = defineCommand3({
471
+ var open_default = defineCommand5({
324
472
  meta: {
325
473
  name: "open",
326
474
  description: "Open a provider dashboard in your browser"
@@ -335,7 +483,7 @@ var open_default = defineCommand3({
335
483
  async run({ args }) {
336
484
  let target = args.target;
337
485
  if (!target) {
338
- const result = await p6.select({
486
+ const result = await p8.select({
339
487
  message: "Which dashboard?",
340
488
  options: Object.entries(DASHBOARDS).map(([value, d]) => ({
341
489
  value,
@@ -343,43 +491,43 @@ var open_default = defineCommand3({
343
491
  hint: d.url
344
492
  }))
345
493
  });
346
- if (p6.isCancel(result)) {
347
- p6.cancel("Cancelled.");
494
+ if (p8.isCancel(result)) {
495
+ p8.cancel("Cancelled.");
348
496
  process.exit(0);
349
497
  }
350
498
  target = result;
351
499
  }
352
500
  const dashboard = DASHBOARDS[target];
353
501
  if (!dashboard) {
354
- p6.log.error(`Unknown dashboard "${target}". Options: ${Object.keys(DASHBOARDS).join(", ")}`);
502
+ p8.log.error(`Unknown dashboard "${target}". Options: ${Object.keys(DASHBOARDS).join(", ")}`);
355
503
  process.exit(1);
356
504
  }
357
505
  openUrl(dashboard.url);
358
506
  console.log(`
359
- Opening ${pc4.bold(dashboard.name)} \u2192 ${pc4.cyan(dashboard.url)}
507
+ Opening ${pc6.bold(dashboard.name)} \u2192 ${pc6.cyan(dashboard.url)}
360
508
  `);
361
509
  }
362
510
  });
363
511
 
364
512
  // src/commands/upgrade.ts
365
- import * as p7 from "@clack/prompts";
366
- import pc5 from "picocolors";
367
- import { defineCommand as defineCommand4 } from "citty";
368
- var upgrade_default = defineCommand4({
513
+ import * as p9 from "@clack/prompts";
514
+ import pc7 from "picocolors";
515
+ import { defineCommand as defineCommand6 } from "citty";
516
+ var upgrade_default = defineCommand6({
369
517
  meta: {
370
518
  name: "upgrade",
371
519
  description: "Update whop-kit to the latest version in your project"
372
520
  },
373
521
  async run() {
374
522
  console.log("");
375
- p7.intro(`${pc5.bgCyan(pc5.black(" whop-kit upgrade "))}`);
523
+ p9.intro(`${pc7.bgCyan(pc7.black(" whop-kit upgrade "))}`);
376
524
  const manifest = readManifest(".");
377
525
  if (!manifest) {
378
- p7.log.error("No .whop/config.json found. Are you in a whop-kit project?");
526
+ p9.log.error("No .whop/config.json found. Are you in a whop-kit project?");
379
527
  process.exit(1);
380
528
  }
381
529
  const pm = detectPackageManager();
382
- const s = p7.spinner();
530
+ const s = p9.spinner();
383
531
  s.start("Checking for updates...");
384
532
  const latest = exec("npm view whop-kit version");
385
533
  s.stop(latest.success ? `Latest: whop-kit@${latest.stdout}` : "Could not check latest version");
@@ -387,25 +535,27 @@ var upgrade_default = defineCommand4({
387
535
  const cmd = pm === "npm" ? "npm install whop-kit@latest" : pm === "yarn" ? "yarn add whop-kit@latest" : pm === "bun" ? "bun add whop-kit@latest" : "pnpm add whop-kit@latest";
388
536
  const result = exec(cmd);
389
537
  if (result.success) {
390
- s.stop(pc5.green("whop-kit upgraded"));
538
+ s.stop(pc7.green("whop-kit upgraded"));
391
539
  } else {
392
- s.stop(pc5.red("Upgrade failed"));
393
- p7.log.error("Try running manually: " + pc5.bold(cmd));
540
+ s.stop(pc7.red("Upgrade failed"));
541
+ p9.log.error("Try running manually: " + pc7.bold(cmd));
394
542
  }
395
- p7.outro("Done");
543
+ p9.outro("Done");
396
544
  }
397
545
  });
398
546
 
399
547
  // src/cli-kit.ts
400
- var main = defineCommand5({
548
+ var main = defineCommand7({
401
549
  meta: {
402
550
  name: "whop-kit",
403
- version: "0.2.0",
551
+ version: "0.5.0",
404
552
  description: "Manage your Whop project"
405
553
  },
406
554
  subCommands: {
407
555
  add: add_default,
408
556
  status: status_default,
557
+ env: env_default,
558
+ catalog: catalog_default,
409
559
  open: open_default,
410
560
  upgrade: upgrade_default
411
561
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-whop-kit",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Scaffold and manage Whop-powered apps with whop-kit",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,73 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/utils/exec.ts
4
- import { execSync } from "child_process";
5
- function exec(cmd, cwd) {
6
- try {
7
- const stdout = execSync(cmd, {
8
- cwd,
9
- stdio: "pipe",
10
- encoding: "utf-8",
11
- timeout: 12e4
12
- }).trim();
13
- return { stdout, success: true };
14
- } catch {
15
- return { stdout: "", success: false };
16
- }
17
- }
18
- function hasCommand(cmd) {
19
- return exec(`which ${cmd}`).success;
20
- }
21
- function detectPackageManager() {
22
- if (hasCommand("pnpm")) return "pnpm";
23
- if (hasCommand("yarn")) return "yarn";
24
- if (hasCommand("bun")) return "bun";
25
- return "npm";
26
- }
27
-
28
- // src/scaffolding/manifest.ts
29
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
30
- import { join } from "path";
31
- var MANIFEST_DIR = ".whop";
32
- var MANIFEST_FILE = "config.json";
33
- function getManifestPath(projectDir) {
34
- return join(projectDir, MANIFEST_DIR, MANIFEST_FILE);
35
- }
36
- function createManifest(projectDir, data) {
37
- const dir = join(projectDir, MANIFEST_DIR);
38
- if (!existsSync(dir)) {
39
- mkdirSync(dir, { recursive: true });
40
- }
41
- const manifest = {
42
- version: 1,
43
- ...data,
44
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
45
- };
46
- writeFileSync(getManifestPath(projectDir), JSON.stringify(manifest, null, 2) + "\n");
47
- }
48
- function readManifest(projectDir) {
49
- const path = getManifestPath(projectDir);
50
- if (!existsSync(path)) return null;
51
- try {
52
- return JSON.parse(readFileSync(path, "utf-8"));
53
- } catch {
54
- return null;
55
- }
56
- }
57
- function addFeatureToManifest(projectDir, feature) {
58
- const manifest = readManifest(projectDir);
59
- if (!manifest) return;
60
- if (!manifest.features.includes(feature)) {
61
- manifest.features.push(feature);
62
- }
63
- writeFileSync(getManifestPath(projectDir), JSON.stringify(manifest, null, 2) + "\n");
64
- }
65
-
66
- export {
67
- exec,
68
- hasCommand,
69
- detectPackageManager,
70
- createManifest,
71
- readManifest,
72
- addFeatureToManifest
73
- };