create-whop-kit 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-M4AXERQP.js +73 -0
- package/dist/cli-create.js +761 -0
- package/dist/cli-kit.js +413 -0
- package/package.json +8 -8
- package/dist/index.js +0 -209
package/dist/cli-kit.js
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
addFeatureToManifest,
|
|
4
|
+
detectPackageManager,
|
|
5
|
+
exec,
|
|
6
|
+
readManifest
|
|
7
|
+
} from "./chunk-M4AXERQP.js";
|
|
8
|
+
|
|
9
|
+
// src/cli-kit.ts
|
|
10
|
+
import { defineCommand as defineCommand5, runMain } from "citty";
|
|
11
|
+
|
|
12
|
+
// src/commands/add.ts
|
|
13
|
+
import * as p4 from "@clack/prompts";
|
|
14
|
+
import pc2 from "picocolors";
|
|
15
|
+
import { defineCommand } from "citty";
|
|
16
|
+
|
|
17
|
+
// src/features/email.ts
|
|
18
|
+
import * as p from "@clack/prompts";
|
|
19
|
+
|
|
20
|
+
// src/features/helpers.ts
|
|
21
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
22
|
+
import { join } from "path";
|
|
23
|
+
function appendEnvVar(projectDir, key, value) {
|
|
24
|
+
const envPath = join(projectDir, ".env.local");
|
|
25
|
+
if (!existsSync(envPath)) {
|
|
26
|
+
writeFileSync(envPath, `${key}="${value}"
|
|
27
|
+
`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
let content = readFileSync(envPath, "utf-8");
|
|
31
|
+
const pattern = new RegExp(`^(#\\s*)?${key}=.*$`, "m");
|
|
32
|
+
if (pattern.test(content)) {
|
|
33
|
+
content = content.replace(pattern, `${key}="${value}"`);
|
|
34
|
+
} else {
|
|
35
|
+
content = content.trimEnd() + `
|
|
36
|
+
${key}="${value}"
|
|
37
|
+
`;
|
|
38
|
+
}
|
|
39
|
+
writeFileSync(envPath, content);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/features/email.ts
|
|
43
|
+
var emailFeature = {
|
|
44
|
+
name: "Email",
|
|
45
|
+
description: "Transactional email via Resend or SendGrid",
|
|
46
|
+
configKey: "email",
|
|
47
|
+
async run(projectDir) {
|
|
48
|
+
const provider = await p.select({
|
|
49
|
+
message: "Email provider",
|
|
50
|
+
options: [
|
|
51
|
+
{ value: "resend", label: "Resend", hint: "Modern email API" },
|
|
52
|
+
{ value: "sendgrid", label: "SendGrid", hint: "Established platform" }
|
|
53
|
+
]
|
|
54
|
+
});
|
|
55
|
+
if (p.isCancel(provider)) {
|
|
56
|
+
p.cancel("Cancelled.");
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
const apiKey = await p.text({
|
|
60
|
+
message: `${provider === "resend" ? "Resend" : "SendGrid"} API key`,
|
|
61
|
+
placeholder: provider === "resend" ? "re_xxxxxxxxx" : "SG.xxxxxxxxx",
|
|
62
|
+
validate: (v) => !v ? "API key is required" : void 0
|
|
63
|
+
});
|
|
64
|
+
if (p.isCancel(apiKey)) {
|
|
65
|
+
p.cancel("Cancelled.");
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
const fromAddress = await p.text({
|
|
69
|
+
message: "From email address",
|
|
70
|
+
placeholder: "noreply@yourdomain.com"
|
|
71
|
+
});
|
|
72
|
+
if (p.isCancel(fromAddress)) {
|
|
73
|
+
p.cancel("Cancelled.");
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
appendEnvVar(projectDir, "EMAIL_PROVIDER", provider);
|
|
77
|
+
appendEnvVar(projectDir, "EMAIL_API_KEY", apiKey);
|
|
78
|
+
if (fromAddress) {
|
|
79
|
+
appendEnvVar(projectDir, "EMAIL_FROM_ADDRESS", fromAddress);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// src/features/analytics.ts
|
|
85
|
+
import * as p2 from "@clack/prompts";
|
|
86
|
+
var analyticsFeature = {
|
|
87
|
+
name: "Analytics",
|
|
88
|
+
description: "Product analytics via PostHog, Google Analytics, or Plausible",
|
|
89
|
+
configKey: "analytics",
|
|
90
|
+
async run(projectDir) {
|
|
91
|
+
const provider = await p2.select({
|
|
92
|
+
message: "Analytics provider",
|
|
93
|
+
options: [
|
|
94
|
+
{ value: "posthog", label: "PostHog", hint: "Open-source product analytics" },
|
|
95
|
+
{ value: "google", label: "Google Analytics", hint: "GA4" },
|
|
96
|
+
{ value: "plausible", label: "Plausible", hint: "Privacy-friendly analytics" }
|
|
97
|
+
]
|
|
98
|
+
});
|
|
99
|
+
if (p2.isCancel(provider)) {
|
|
100
|
+
p2.cancel("Cancelled.");
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
const placeholders = {
|
|
104
|
+
posthog: "phc_xxxxxxxxx",
|
|
105
|
+
google: "G-XXXXXXXXXX",
|
|
106
|
+
plausible: "yourdomain.com"
|
|
107
|
+
};
|
|
108
|
+
const id = await p2.text({
|
|
109
|
+
message: `${provider === "google" ? "Measurement" : provider === "posthog" ? "Project API" : "Site"} ID`,
|
|
110
|
+
placeholder: placeholders[provider] ?? "",
|
|
111
|
+
validate: (v) => !v ? "ID is required" : void 0
|
|
112
|
+
});
|
|
113
|
+
if (p2.isCancel(id)) {
|
|
114
|
+
p2.cancel("Cancelled.");
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
appendEnvVar(projectDir, "ANALYTICS_PROVIDER", provider);
|
|
118
|
+
appendEnvVar(projectDir, "ANALYTICS_ID", id);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// src/features/webhook-event.ts
|
|
123
|
+
import * as p3 from "@clack/prompts";
|
|
124
|
+
import pc from "picocolors";
|
|
125
|
+
var webhookEventFeature = {
|
|
126
|
+
name: "Webhook Event",
|
|
127
|
+
description: "Add a new webhook event handler",
|
|
128
|
+
configKey: "webhook-event",
|
|
129
|
+
async run() {
|
|
130
|
+
const eventName = await p3.text({
|
|
131
|
+
message: "Event name",
|
|
132
|
+
placeholder: "payment_succeeded",
|
|
133
|
+
validate: (v) => !v ? "Event name is required" : void 0
|
|
134
|
+
});
|
|
135
|
+
if (p3.isCancel(eventName)) {
|
|
136
|
+
p3.cancel("Cancelled.");
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
const code = `
|
|
140
|
+
${eventName}: async (data) => {
|
|
141
|
+
const userId = data.user_id as string | undefined;
|
|
142
|
+
if (!userId) return;
|
|
143
|
+
// TODO: Handle ${eventName}
|
|
144
|
+
console.log(\`[Webhook] ${eventName} for \${userId}\`);
|
|
145
|
+
},`;
|
|
146
|
+
p3.note(
|
|
147
|
+
`Add this to the ${pc.bold("on")} object in your webhook route:
|
|
148
|
+
|
|
149
|
+
${pc.cyan(code)}`,
|
|
150
|
+
"Add to your webhook handler"
|
|
151
|
+
);
|
|
152
|
+
p3.log.info(
|
|
153
|
+
`File: ${pc.dim("app/api/webhooks/whop/route.ts")} (Next.js) or ${pc.dim("src/pages/api/webhooks/whop.ts")} (Astro)`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// src/commands/add.ts
|
|
159
|
+
var FEATURES = {
|
|
160
|
+
email: emailFeature,
|
|
161
|
+
analytics: analyticsFeature,
|
|
162
|
+
"webhook-event": webhookEventFeature
|
|
163
|
+
};
|
|
164
|
+
var add_default = defineCommand({
|
|
165
|
+
meta: {
|
|
166
|
+
name: "add",
|
|
167
|
+
description: "Add a feature to your Whop project"
|
|
168
|
+
},
|
|
169
|
+
args: {
|
|
170
|
+
feature: {
|
|
171
|
+
type: "positional",
|
|
172
|
+
description: `Feature to add: ${Object.keys(FEATURES).join(", ")}`,
|
|
173
|
+
required: false
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
async run({ args }) {
|
|
177
|
+
console.log("");
|
|
178
|
+
p4.intro(`${pc2.bgCyan(pc2.black(" whop-kit add "))} Add a feature`);
|
|
179
|
+
const manifest = readManifest(".");
|
|
180
|
+
if (!manifest) {
|
|
181
|
+
p4.log.error(
|
|
182
|
+
"No .whop/config.json found. Run this command from a project created with create-whop-kit."
|
|
183
|
+
);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
let featureKey = args.feature;
|
|
187
|
+
if (!featureKey) {
|
|
188
|
+
const result = await p4.select({
|
|
189
|
+
message: "What would you like to add?",
|
|
190
|
+
options: Object.entries(FEATURES).map(([value, f]) => {
|
|
191
|
+
const installed = manifest.features.includes(f.configKey);
|
|
192
|
+
return {
|
|
193
|
+
value,
|
|
194
|
+
label: installed ? `${f.name} ${pc2.green("\u2713 configured")}` : f.name,
|
|
195
|
+
hint: f.description
|
|
196
|
+
};
|
|
197
|
+
})
|
|
198
|
+
});
|
|
199
|
+
if (p4.isCancel(result)) {
|
|
200
|
+
p4.cancel("Cancelled.");
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
203
|
+
featureKey = result;
|
|
204
|
+
}
|
|
205
|
+
const feature = FEATURES[featureKey];
|
|
206
|
+
if (!feature) {
|
|
207
|
+
p4.log.error(
|
|
208
|
+
`Unknown feature "${featureKey}". Available: ${Object.keys(FEATURES).join(", ")}`
|
|
209
|
+
);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
if (manifest.features.includes(feature.configKey)) {
|
|
213
|
+
const proceed = await p4.confirm({
|
|
214
|
+
message: `${feature.name} is already configured. Reconfigure?`,
|
|
215
|
+
initialValue: false
|
|
216
|
+
});
|
|
217
|
+
if (p4.isCancel(proceed) || !proceed) {
|
|
218
|
+
p4.cancel("Cancelled.");
|
|
219
|
+
process.exit(0);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
await feature.run(".");
|
|
223
|
+
addFeatureToManifest(".", feature.configKey);
|
|
224
|
+
p4.outro(`${pc2.green("\u2713")} ${feature.name} configured`);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// src/commands/status.ts
|
|
229
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
230
|
+
import { join as join2 } from "path";
|
|
231
|
+
import * as p5 from "@clack/prompts";
|
|
232
|
+
import pc3 from "picocolors";
|
|
233
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
234
|
+
var ENV_CHECKS = [
|
|
235
|
+
{ key: "DATABASE_URL", label: "Database", required: true },
|
|
236
|
+
{ key: "NEXT_PUBLIC_WHOP_APP_ID", label: "Whop App ID", required: true },
|
|
237
|
+
{ key: "WHOP_API_KEY", label: "Whop API Key", required: true },
|
|
238
|
+
{ key: "WHOP_WEBHOOK_SECRET", label: "Webhook Secret", required: true },
|
|
239
|
+
{ key: "EMAIL_PROVIDER", label: "Email Provider", required: false },
|
|
240
|
+
{ key: "EMAIL_API_KEY", label: "Email API Key", required: false },
|
|
241
|
+
{ key: "ANALYTICS_PROVIDER", label: "Analytics Provider", required: false },
|
|
242
|
+
{ key: "ANALYTICS_ID", label: "Analytics ID", required: false }
|
|
243
|
+
];
|
|
244
|
+
function readEnvFile(projectDir) {
|
|
245
|
+
const envPath = join2(projectDir, ".env.local");
|
|
246
|
+
if (!existsSync2(envPath)) return {};
|
|
247
|
+
const content = readFileSync2(envPath, "utf-8");
|
|
248
|
+
const vars = {};
|
|
249
|
+
for (const line of content.split("\n")) {
|
|
250
|
+
const trimmed = line.trim();
|
|
251
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
252
|
+
const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)=["']?(.*)["']?$/);
|
|
253
|
+
if (match) {
|
|
254
|
+
vars[match[1]] = match[2].replace(/["']$/, "");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return vars;
|
|
258
|
+
}
|
|
259
|
+
var status_default = defineCommand2({
|
|
260
|
+
meta: {
|
|
261
|
+
name: "status",
|
|
262
|
+
description: "Show your project's configuration status"
|
|
263
|
+
},
|
|
264
|
+
async run() {
|
|
265
|
+
console.log("");
|
|
266
|
+
p5.intro(`${pc3.bgCyan(pc3.black(" whop-kit status "))} Project health`);
|
|
267
|
+
const manifest = readManifest(".");
|
|
268
|
+
if (!manifest) {
|
|
269
|
+
p5.log.error(
|
|
270
|
+
"No .whop/config.json found. Are you in a project created with create-whop-kit?"
|
|
271
|
+
);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
const envVars = readEnvFile(".");
|
|
275
|
+
if (!envVars["NEXT_PUBLIC_WHOP_APP_ID"] && envVars["WHOP_APP_ID"]) {
|
|
276
|
+
envVars["NEXT_PUBLIC_WHOP_APP_ID"] = envVars["WHOP_APP_ID"];
|
|
277
|
+
}
|
|
278
|
+
console.log(` ${pc3.bold("Framework:")} ${manifest.framework}`);
|
|
279
|
+
console.log(` ${pc3.bold("App type:")} ${manifest.appType}`);
|
|
280
|
+
console.log(` ${pc3.bold("Database:")} ${manifest.database}`);
|
|
281
|
+
console.log(` ${pc3.bold("Created:")} ${new Date(manifest.createdAt).toLocaleDateString()}`);
|
|
282
|
+
console.log("");
|
|
283
|
+
console.log(` ${pc3.bold("Configuration:")}`);
|
|
284
|
+
let allRequired = true;
|
|
285
|
+
for (const check of ENV_CHECKS) {
|
|
286
|
+
const value = envVars[check.key];
|
|
287
|
+
const isSet = !!value;
|
|
288
|
+
if (check.required && !isSet) allRequired = false;
|
|
289
|
+
const icon = isSet ? pc3.green("\u2713") : check.required ? pc3.red("\u2717") : pc3.yellow("\u25CB");
|
|
290
|
+
const maskedValue = isSet ? pc3.dim(value.substring(0, 8) + "...") : check.required ? pc3.red("not set") : pc3.dim("not set (optional)");
|
|
291
|
+
console.log(` ${icon} ${check.label.padEnd(20)} ${maskedValue}`);
|
|
292
|
+
}
|
|
293
|
+
console.log("");
|
|
294
|
+
if (manifest.features.length > 0) {
|
|
295
|
+
console.log(` ${pc3.bold("Features:")} ${manifest.features.join(", ")}`);
|
|
296
|
+
}
|
|
297
|
+
if (allRequired) {
|
|
298
|
+
p5.outro(pc3.green("All required configuration is set. Ready to run!"));
|
|
299
|
+
} else {
|
|
300
|
+
p5.outro(
|
|
301
|
+
`${pc3.yellow("Some required config is missing.")} Run ${pc3.bold("whop-kit add")} or edit ${pc3.dim(".env.local")}`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// src/commands/open.ts
|
|
308
|
+
import * as p6 from "@clack/prompts";
|
|
309
|
+
import pc4 from "picocolors";
|
|
310
|
+
import { defineCommand as defineCommand3 } from "citty";
|
|
311
|
+
var DASHBOARDS = {
|
|
312
|
+
whop: { name: "Whop Developer Dashboard", url: "https://whop.com/dashboard/developer" },
|
|
313
|
+
neon: { name: "Neon Console", url: "https://console.neon.tech" },
|
|
314
|
+
supabase: { name: "Supabase Dashboard", url: "https://supabase.com/dashboard" },
|
|
315
|
+
vercel: { name: "Vercel Dashboard", url: "https://vercel.com/dashboard" }
|
|
316
|
+
};
|
|
317
|
+
function openUrl(url) {
|
|
318
|
+
const platform = process.platform;
|
|
319
|
+
if (platform === "darwin") exec(`open "${url}"`);
|
|
320
|
+
else if (platform === "win32") exec(`start "${url}"`);
|
|
321
|
+
else exec(`xdg-open "${url}"`);
|
|
322
|
+
}
|
|
323
|
+
var open_default = defineCommand3({
|
|
324
|
+
meta: {
|
|
325
|
+
name: "open",
|
|
326
|
+
description: "Open a provider dashboard in your browser"
|
|
327
|
+
},
|
|
328
|
+
args: {
|
|
329
|
+
target: {
|
|
330
|
+
type: "positional",
|
|
331
|
+
description: `Dashboard to open: ${Object.keys(DASHBOARDS).join(", ")}`,
|
|
332
|
+
required: false
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
async run({ args }) {
|
|
336
|
+
let target = args.target;
|
|
337
|
+
if (!target) {
|
|
338
|
+
const result = await p6.select({
|
|
339
|
+
message: "Which dashboard?",
|
|
340
|
+
options: Object.entries(DASHBOARDS).map(([value, d]) => ({
|
|
341
|
+
value,
|
|
342
|
+
label: d.name,
|
|
343
|
+
hint: d.url
|
|
344
|
+
}))
|
|
345
|
+
});
|
|
346
|
+
if (p6.isCancel(result)) {
|
|
347
|
+
p6.cancel("Cancelled.");
|
|
348
|
+
process.exit(0);
|
|
349
|
+
}
|
|
350
|
+
target = result;
|
|
351
|
+
}
|
|
352
|
+
const dashboard = DASHBOARDS[target];
|
|
353
|
+
if (!dashboard) {
|
|
354
|
+
p6.log.error(`Unknown dashboard "${target}". Options: ${Object.keys(DASHBOARDS).join(", ")}`);
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
openUrl(dashboard.url);
|
|
358
|
+
console.log(`
|
|
359
|
+
Opening ${pc4.bold(dashboard.name)} \u2192 ${pc4.cyan(dashboard.url)}
|
|
360
|
+
`);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// 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({
|
|
369
|
+
meta: {
|
|
370
|
+
name: "upgrade",
|
|
371
|
+
description: "Update whop-kit to the latest version in your project"
|
|
372
|
+
},
|
|
373
|
+
async run() {
|
|
374
|
+
console.log("");
|
|
375
|
+
p7.intro(`${pc5.bgCyan(pc5.black(" whop-kit upgrade "))}`);
|
|
376
|
+
const manifest = readManifest(".");
|
|
377
|
+
if (!manifest) {
|
|
378
|
+
p7.log.error("No .whop/config.json found. Are you in a whop-kit project?");
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
const pm = detectPackageManager();
|
|
382
|
+
const s = p7.spinner();
|
|
383
|
+
s.start("Checking for updates...");
|
|
384
|
+
const latest = exec("npm view whop-kit version");
|
|
385
|
+
s.stop(latest.success ? `Latest: whop-kit@${latest.stdout}` : "Could not check latest version");
|
|
386
|
+
s.start(`Upgrading whop-kit with ${pm}...`);
|
|
387
|
+
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
|
+
const result = exec(cmd);
|
|
389
|
+
if (result.success) {
|
|
390
|
+
s.stop(pc5.green("whop-kit upgraded"));
|
|
391
|
+
} else {
|
|
392
|
+
s.stop(pc5.red("Upgrade failed"));
|
|
393
|
+
p7.log.error("Try running manually: " + pc5.bold(cmd));
|
|
394
|
+
}
|
|
395
|
+
p7.outro("Done");
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// src/cli-kit.ts
|
|
400
|
+
var main = defineCommand5({
|
|
401
|
+
meta: {
|
|
402
|
+
name: "whop-kit",
|
|
403
|
+
version: "0.2.0",
|
|
404
|
+
description: "Manage your Whop project"
|
|
405
|
+
},
|
|
406
|
+
subCommands: {
|
|
407
|
+
add: add_default,
|
|
408
|
+
status: status_default,
|
|
409
|
+
open: open_default,
|
|
410
|
+
upgrade: upgrade_default
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
runMain(main);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-whop-kit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Scaffold
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Scaffold and manage Whop-powered apps with whop-kit",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Colin McDermott",
|
|
@@ -10,22 +10,22 @@
|
|
|
10
10
|
"url": "https://github.com/colinmcdermott/create-whop-kit"
|
|
11
11
|
},
|
|
12
12
|
"bin": {
|
|
13
|
-
"create-whop-kit": "./dist/
|
|
13
|
+
"create-whop-kit": "./dist/cli-create.js",
|
|
14
|
+
"whop-kit": "./dist/cli-kit.js"
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
|
16
17
|
"dist",
|
|
17
|
-
"
|
|
18
|
-
"README.md",
|
|
19
|
-
"LICENSE"
|
|
18
|
+
"README.md"
|
|
20
19
|
],
|
|
21
20
|
"scripts": {
|
|
22
21
|
"build": "tsup",
|
|
23
22
|
"dev": "tsup --watch",
|
|
24
|
-
"start": "node dist/index.js",
|
|
25
23
|
"prepublishOnly": "npm run build"
|
|
26
24
|
},
|
|
27
25
|
"dependencies": {
|
|
28
|
-
"@clack/prompts": "^0.10.0"
|
|
26
|
+
"@clack/prompts": "^0.10.0",
|
|
27
|
+
"citty": "^0.2.2",
|
|
28
|
+
"picocolors": "^1.1.1"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"tsup": "^8.4.0",
|
package/dist/index.js
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/index.ts
|
|
4
|
-
import * as p from "@clack/prompts";
|
|
5
|
-
import { execSync } from "child_process";
|
|
6
|
-
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
7
|
-
import { resolve, join, basename } from "path";
|
|
8
|
-
import { fileURLToPath } from "url";
|
|
9
|
-
var __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
10
|
-
var TEMPLATES_DIR = resolve(__dirname, "..", "templates");
|
|
11
|
-
var TEMPLATES = {
|
|
12
|
-
nextjs: {
|
|
13
|
-
name: "Next.js",
|
|
14
|
-
description: "Full-stack React with App Router, SSR, and API routes",
|
|
15
|
-
repo: "colinmcdermott/whop-saas-starter-v2",
|
|
16
|
-
available: true
|
|
17
|
-
},
|
|
18
|
-
astro: {
|
|
19
|
-
name: "Astro",
|
|
20
|
-
description: "Content-focused with islands architecture",
|
|
21
|
-
repo: "",
|
|
22
|
-
available: false
|
|
23
|
-
},
|
|
24
|
-
tanstack: {
|
|
25
|
-
name: "TanStack Start",
|
|
26
|
-
description: "Full-stack React with TanStack Router",
|
|
27
|
-
repo: "",
|
|
28
|
-
available: false
|
|
29
|
-
},
|
|
30
|
-
vite: {
|
|
31
|
-
name: "Vite + React",
|
|
32
|
-
description: "Lightweight SPA with Vite bundler",
|
|
33
|
-
repo: "",
|
|
34
|
-
available: false
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
var APP_TYPES = {
|
|
38
|
-
saas: {
|
|
39
|
-
name: "SaaS",
|
|
40
|
-
description: "Subscription tiers, dashboard, billing portal",
|
|
41
|
-
available: true
|
|
42
|
-
},
|
|
43
|
-
course: {
|
|
44
|
-
name: "Course",
|
|
45
|
-
description: "Lessons, progress tracking, drip content",
|
|
46
|
-
available: false
|
|
47
|
-
},
|
|
48
|
-
community: {
|
|
49
|
-
name: "Community",
|
|
50
|
-
description: "Member feeds, gated content, roles",
|
|
51
|
-
available: false
|
|
52
|
-
},
|
|
53
|
-
blank: {
|
|
54
|
-
name: "Blank",
|
|
55
|
-
description: "Just auth + payments, you build the rest",
|
|
56
|
-
available: false
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
var DB_OPTIONS = {
|
|
60
|
-
neon: {
|
|
61
|
-
name: "Neon",
|
|
62
|
-
description: "Serverless Postgres (recommended)",
|
|
63
|
-
envVarHint: "postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/dbname?sslmode=require"
|
|
64
|
-
},
|
|
65
|
-
supabase: {
|
|
66
|
-
name: "Supabase",
|
|
67
|
-
description: "Open-source Firebase alternative",
|
|
68
|
-
envVarHint: "postgresql://postgres.xxx:pass@aws-0-us-east-1.pooler.supabase.com:6543/postgres"
|
|
69
|
-
},
|
|
70
|
-
local: {
|
|
71
|
-
name: "Local PostgreSQL",
|
|
72
|
-
description: "Your own Postgres instance",
|
|
73
|
-
envVarHint: "postgresql://postgres:postgres@localhost:5432/myapp"
|
|
74
|
-
},
|
|
75
|
-
later: {
|
|
76
|
-
name: "Configure later",
|
|
77
|
-
description: "Skip database setup for now",
|
|
78
|
-
envVarHint: ""
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
function run(cmd, cwd) {
|
|
82
|
-
try {
|
|
83
|
-
return execSync(cmd, { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
|
|
84
|
-
} catch {
|
|
85
|
-
return "";
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
function hasCommand(cmd) {
|
|
89
|
-
return run(`which ${cmd}`) !== "";
|
|
90
|
-
}
|
|
91
|
-
async function main() {
|
|
92
|
-
const args = process.argv.slice(2);
|
|
93
|
-
const projectName = args[0];
|
|
94
|
-
console.log("");
|
|
95
|
-
p.intro("Create Whop Kit App");
|
|
96
|
-
const name = projectName ?? await p.text({
|
|
97
|
-
message: "Project name",
|
|
98
|
-
placeholder: "my-whop-app",
|
|
99
|
-
validate: (v) => {
|
|
100
|
-
if (!v) return "Project name is required";
|
|
101
|
-
if (existsSync(resolve(v))) return `Directory "${v}" already exists`;
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
if (p.isCancel(name)) {
|
|
105
|
-
p.cancel("Cancelled.");
|
|
106
|
-
process.exit(0);
|
|
107
|
-
}
|
|
108
|
-
const appType = await p.select({
|
|
109
|
-
message: "What are you building?",
|
|
110
|
-
options: Object.entries(APP_TYPES).map(([value, { name: name2, description, available }]) => ({
|
|
111
|
-
value,
|
|
112
|
-
label: available ? name2 : `${name2} (coming soon)`,
|
|
113
|
-
hint: description,
|
|
114
|
-
disabled: !available
|
|
115
|
-
}))
|
|
116
|
-
});
|
|
117
|
-
if (p.isCancel(appType)) {
|
|
118
|
-
p.cancel("Cancelled.");
|
|
119
|
-
process.exit(0);
|
|
120
|
-
}
|
|
121
|
-
const framework = await p.select({
|
|
122
|
-
message: "Which framework?",
|
|
123
|
-
options: Object.entries(TEMPLATES).map(([value, { name: name2, description, available }]) => ({
|
|
124
|
-
value,
|
|
125
|
-
label: available ? name2 : `${name2} (coming soon)`,
|
|
126
|
-
hint: description,
|
|
127
|
-
disabled: !available
|
|
128
|
-
}))
|
|
129
|
-
});
|
|
130
|
-
if (p.isCancel(framework)) {
|
|
131
|
-
p.cancel("Cancelled.");
|
|
132
|
-
process.exit(0);
|
|
133
|
-
}
|
|
134
|
-
const database = await p.select({
|
|
135
|
-
message: "Which database?",
|
|
136
|
-
options: Object.entries(DB_OPTIONS).map(([value, { name: name2, description }]) => ({
|
|
137
|
-
value,
|
|
138
|
-
label: name2,
|
|
139
|
-
hint: description
|
|
140
|
-
}))
|
|
141
|
-
});
|
|
142
|
-
if (p.isCancel(database)) {
|
|
143
|
-
p.cancel("Cancelled.");
|
|
144
|
-
process.exit(0);
|
|
145
|
-
}
|
|
146
|
-
let databaseUrl = "";
|
|
147
|
-
if (database !== "later") {
|
|
148
|
-
const dbUrl = await p.text({
|
|
149
|
-
message: "Database URL",
|
|
150
|
-
placeholder: DB_OPTIONS[database].envVarHint,
|
|
151
|
-
validate: (v) => {
|
|
152
|
-
if (!v) return "Database URL is required (or go back and choose 'Configure later')";
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
if (p.isCancel(dbUrl)) {
|
|
156
|
-
p.cancel("Cancelled.");
|
|
157
|
-
process.exit(0);
|
|
158
|
-
}
|
|
159
|
-
databaseUrl = dbUrl;
|
|
160
|
-
}
|
|
161
|
-
const template = TEMPLATES[framework];
|
|
162
|
-
const projectDir = resolve(name);
|
|
163
|
-
const s = p.spinner();
|
|
164
|
-
s.start("Cloning template...");
|
|
165
|
-
const cloneResult = run(
|
|
166
|
-
`git clone --depth 1 https://github.com/${template.repo}.git "${projectDir}" 2>&1`
|
|
167
|
-
);
|
|
168
|
-
if (!existsSync(projectDir)) {
|
|
169
|
-
s.stop("Failed to clone template");
|
|
170
|
-
p.log.error(cloneResult || "Git clone failed. Make sure git is installed.");
|
|
171
|
-
process.exit(1);
|
|
172
|
-
}
|
|
173
|
-
run(`rm -rf "${join(projectDir, ".git")}"`);
|
|
174
|
-
const pkgPath = join(projectDir, "package.json");
|
|
175
|
-
if (existsSync(pkgPath)) {
|
|
176
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
177
|
-
pkg.name = basename(name);
|
|
178
|
-
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
179
|
-
}
|
|
180
|
-
s.stop("Template cloned");
|
|
181
|
-
if (databaseUrl) {
|
|
182
|
-
s.start("Configuring environment...");
|
|
183
|
-
const envContent = `DATABASE_URL="${databaseUrl}"
|
|
184
|
-
`;
|
|
185
|
-
writeFileSync(join(projectDir, ".env.local"), envContent);
|
|
186
|
-
s.stop("Environment configured");
|
|
187
|
-
}
|
|
188
|
-
const packageManager = hasCommand("pnpm") ? "pnpm" : hasCommand("yarn") ? "yarn" : "npm";
|
|
189
|
-
s.start(`Installing dependencies with ${packageManager}...`);
|
|
190
|
-
run(`${packageManager} install`, projectDir);
|
|
191
|
-
s.stop("Dependencies installed");
|
|
192
|
-
run("git init", projectDir);
|
|
193
|
-
run("git add -A", projectDir);
|
|
194
|
-
run('git commit -m "initial: scaffolded with create-whop-kit"', projectDir);
|
|
195
|
-
const relativePath = name;
|
|
196
|
-
p.note(
|
|
197
|
-
[
|
|
198
|
-
`cd ${relativePath}`,
|
|
199
|
-
databaseUrl ? `${packageManager} run db:push` : `# Add DATABASE_URL to .env.local first`,
|
|
200
|
-
`${packageManager} run dev`
|
|
201
|
-
].join("\n"),
|
|
202
|
-
"Next steps"
|
|
203
|
-
);
|
|
204
|
-
p.outro("Happy building!");
|
|
205
|
-
}
|
|
206
|
-
main().catch((err) => {
|
|
207
|
-
console.error(err);
|
|
208
|
-
process.exit(1);
|
|
209
|
-
});
|