create-whop-kit 0.1.0 → 0.2.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 +480 -0
- package/dist/cli-kit.js +413 -0
- package/package.json +8 -8
- package/dist/index.js +0 -209
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createManifest,
|
|
4
|
+
detectPackageManager,
|
|
5
|
+
exec,
|
|
6
|
+
hasCommand
|
|
7
|
+
} from "./chunk-M4AXERQP.js";
|
|
8
|
+
|
|
9
|
+
// src/cli-create.ts
|
|
10
|
+
import { runMain } from "citty";
|
|
11
|
+
|
|
12
|
+
// src/commands/init.ts
|
|
13
|
+
import { resolve, basename as basename2 } from "path";
|
|
14
|
+
import { existsSync as existsSync4 } from "fs";
|
|
15
|
+
import * as p2 from "@clack/prompts";
|
|
16
|
+
import pc2 from "picocolors";
|
|
17
|
+
import { defineCommand } from "citty";
|
|
18
|
+
|
|
19
|
+
// src/templates.ts
|
|
20
|
+
var TEMPLATES = {
|
|
21
|
+
nextjs: {
|
|
22
|
+
name: "Next.js",
|
|
23
|
+
description: "Full-stack React with App Router, SSR, and API routes",
|
|
24
|
+
repo: "colinmcdermott/whop-saas-starter-v2",
|
|
25
|
+
available: true
|
|
26
|
+
},
|
|
27
|
+
astro: {
|
|
28
|
+
name: "Astro",
|
|
29
|
+
description: "Content-focused with islands architecture",
|
|
30
|
+
repo: "colinmcdermott/whop-astro-starter",
|
|
31
|
+
available: true
|
|
32
|
+
},
|
|
33
|
+
tanstack: {
|
|
34
|
+
name: "TanStack Start",
|
|
35
|
+
description: "Full-stack React with TanStack Router",
|
|
36
|
+
repo: "",
|
|
37
|
+
available: false
|
|
38
|
+
},
|
|
39
|
+
vite: {
|
|
40
|
+
name: "Vite + React",
|
|
41
|
+
description: "Lightweight SPA with Vite bundler",
|
|
42
|
+
repo: "",
|
|
43
|
+
available: false
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var APP_TYPES = {
|
|
47
|
+
saas: {
|
|
48
|
+
name: "SaaS",
|
|
49
|
+
description: "Subscription tiers, dashboard, billing portal",
|
|
50
|
+
available: true
|
|
51
|
+
},
|
|
52
|
+
course: {
|
|
53
|
+
name: "Course",
|
|
54
|
+
description: "Lessons, progress tracking, drip content",
|
|
55
|
+
available: false
|
|
56
|
+
},
|
|
57
|
+
community: {
|
|
58
|
+
name: "Community",
|
|
59
|
+
description: "Member feeds, gated content, roles",
|
|
60
|
+
available: false
|
|
61
|
+
},
|
|
62
|
+
blank: {
|
|
63
|
+
name: "Blank",
|
|
64
|
+
description: "Just auth + payments, you build the rest",
|
|
65
|
+
available: false
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var DB_OPTIONS = {
|
|
69
|
+
neon: {
|
|
70
|
+
name: "Neon",
|
|
71
|
+
description: "Serverless Postgres (recommended)",
|
|
72
|
+
envVarHint: "postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/dbname?sslmode=require"
|
|
73
|
+
},
|
|
74
|
+
supabase: {
|
|
75
|
+
name: "Supabase",
|
|
76
|
+
description: "Open-source Firebase alternative",
|
|
77
|
+
envVarHint: "postgresql://postgres.xxx:pass@aws-0-us-east-1.pooler.supabase.com:6543/postgres"
|
|
78
|
+
},
|
|
79
|
+
local: {
|
|
80
|
+
name: "Local PostgreSQL",
|
|
81
|
+
description: "Your own Postgres instance",
|
|
82
|
+
envVarHint: "postgresql://postgres:postgres@localhost:5432/myapp"
|
|
83
|
+
},
|
|
84
|
+
later: {
|
|
85
|
+
name: "Configure later",
|
|
86
|
+
description: "Skip database setup for now",
|
|
87
|
+
envVarHint: ""
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// src/utils/checks.ts
|
|
92
|
+
import * as p from "@clack/prompts";
|
|
93
|
+
import pc from "picocolors";
|
|
94
|
+
function checkNodeVersion(minimum = 18) {
|
|
95
|
+
const major = parseInt(process.versions.node.split(".")[0], 10);
|
|
96
|
+
if (major < minimum) {
|
|
97
|
+
p.log.error(
|
|
98
|
+
`Node.js ${pc.bold(`v${minimum}+`)} is required. You have ${pc.bold(`v${process.versions.node}`)}.`
|
|
99
|
+
);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function checkGit() {
|
|
104
|
+
if (!hasCommand("git")) {
|
|
105
|
+
p.log.error(
|
|
106
|
+
`${pc.bold("git")} is required but not found. Install it from ${pc.cyan("https://git-scm.com")}`
|
|
107
|
+
);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function validateDatabaseUrl(url) {
|
|
112
|
+
if (!url.startsWith("postgres://") && !url.startsWith("postgresql://")) {
|
|
113
|
+
return "Must be a PostgreSQL connection string (starts with postgres:// or postgresql://)";
|
|
114
|
+
}
|
|
115
|
+
return void 0;
|
|
116
|
+
}
|
|
117
|
+
function validateWhopAppId(id) {
|
|
118
|
+
if (id && !id.startsWith("app_")) {
|
|
119
|
+
return 'Whop App IDs start with "app_"';
|
|
120
|
+
}
|
|
121
|
+
return void 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/utils/cleanup.ts
|
|
125
|
+
import { rmSync, existsSync } from "fs";
|
|
126
|
+
function cleanupDir(dir) {
|
|
127
|
+
if (existsSync(dir)) {
|
|
128
|
+
try {
|
|
129
|
+
rmSync(dir, { recursive: true, force: true });
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/scaffolding/clone.ts
|
|
136
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync, rmSync as rmSync2 } from "fs";
|
|
137
|
+
import { join, basename } from "path";
|
|
138
|
+
function cloneTemplate(repo, projectDir) {
|
|
139
|
+
const result = exec(
|
|
140
|
+
`git clone --depth 1 https://github.com/${repo}.git "${projectDir}"`
|
|
141
|
+
);
|
|
142
|
+
if (!result.success || !existsSync2(projectDir)) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
const gitDir = join(projectDir, ".git");
|
|
146
|
+
if (existsSync2(gitDir)) {
|
|
147
|
+
rmSync2(gitDir, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
function updatePackageName(projectDir, name) {
|
|
152
|
+
const pkgPath = join(projectDir, "package.json");
|
|
153
|
+
if (existsSync2(pkgPath)) {
|
|
154
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
155
|
+
pkg.name = basename(name);
|
|
156
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function initGit(projectDir) {
|
|
160
|
+
exec("git init", projectDir);
|
|
161
|
+
exec("git add -A", projectDir);
|
|
162
|
+
exec('git commit -m "initial: scaffolded with create-whop-kit"', projectDir);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/scaffolding/env-file.ts
|
|
166
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3 } from "fs";
|
|
167
|
+
import { join as join2 } from "path";
|
|
168
|
+
function writeEnvFile(projectDir, values) {
|
|
169
|
+
const examplePath = join2(projectDir, ".env.example");
|
|
170
|
+
const envPath = join2(projectDir, ".env.local");
|
|
171
|
+
const filled = Object.fromEntries(
|
|
172
|
+
Object.entries(values).filter(([, v]) => v)
|
|
173
|
+
);
|
|
174
|
+
if (existsSync3(examplePath)) {
|
|
175
|
+
let content = readFileSync2(examplePath, "utf-8");
|
|
176
|
+
for (const [key, value] of Object.entries(filled)) {
|
|
177
|
+
const pattern = new RegExp(
|
|
178
|
+
`^(#\\s*)?${escapeRegex(key)}=.*$`,
|
|
179
|
+
"m"
|
|
180
|
+
);
|
|
181
|
+
if (pattern.test(content)) {
|
|
182
|
+
content = content.replace(pattern, `${key}="${value}"`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
writeFileSync2(envPath, content);
|
|
186
|
+
} else {
|
|
187
|
+
const lines = Object.entries(filled).map(
|
|
188
|
+
([key, value]) => `${key}="${value}"`
|
|
189
|
+
);
|
|
190
|
+
writeFileSync2(envPath, lines.join("\n") + "\n");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function escapeRegex(str) {
|
|
194
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/commands/init.ts
|
|
198
|
+
function isCancelled(value) {
|
|
199
|
+
return p2.isCancel(value);
|
|
200
|
+
}
|
|
201
|
+
var init_default = defineCommand({
|
|
202
|
+
meta: {
|
|
203
|
+
name: "create-whop-kit",
|
|
204
|
+
version: "0.2.0",
|
|
205
|
+
description: "Scaffold a new Whop-powered app with whop-kit"
|
|
206
|
+
},
|
|
207
|
+
args: {
|
|
208
|
+
name: {
|
|
209
|
+
type: "positional",
|
|
210
|
+
description: "Project name",
|
|
211
|
+
required: false
|
|
212
|
+
},
|
|
213
|
+
framework: {
|
|
214
|
+
type: "string",
|
|
215
|
+
description: "Framework: nextjs, astro"
|
|
216
|
+
},
|
|
217
|
+
type: {
|
|
218
|
+
type: "string",
|
|
219
|
+
description: "App type: saas",
|
|
220
|
+
default: "saas"
|
|
221
|
+
},
|
|
222
|
+
db: {
|
|
223
|
+
type: "string",
|
|
224
|
+
description: "Database: neon, supabase, local, later"
|
|
225
|
+
},
|
|
226
|
+
"db-url": {
|
|
227
|
+
type: "string",
|
|
228
|
+
description: "Database connection URL"
|
|
229
|
+
},
|
|
230
|
+
"app-id": {
|
|
231
|
+
type: "string",
|
|
232
|
+
description: "Whop App ID"
|
|
233
|
+
},
|
|
234
|
+
"api-key": {
|
|
235
|
+
type: "string",
|
|
236
|
+
description: "Whop API Key"
|
|
237
|
+
},
|
|
238
|
+
"webhook-secret": {
|
|
239
|
+
type: "string",
|
|
240
|
+
description: "Whop webhook secret"
|
|
241
|
+
},
|
|
242
|
+
yes: {
|
|
243
|
+
type: "boolean",
|
|
244
|
+
alias: "y",
|
|
245
|
+
description: "Skip optional prompts, use defaults",
|
|
246
|
+
default: false
|
|
247
|
+
},
|
|
248
|
+
"dry-run": {
|
|
249
|
+
type: "boolean",
|
|
250
|
+
description: "Show what would be created without doing it",
|
|
251
|
+
default: false
|
|
252
|
+
},
|
|
253
|
+
verbose: {
|
|
254
|
+
type: "boolean",
|
|
255
|
+
description: "Show detailed output",
|
|
256
|
+
default: false
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
async run({ args }) {
|
|
260
|
+
checkNodeVersion(18);
|
|
261
|
+
checkGit();
|
|
262
|
+
console.log("");
|
|
263
|
+
p2.intro(`${pc2.bgCyan(pc2.black(" create-whop-kit "))} Create a Whop-powered app`);
|
|
264
|
+
const isNonInteractive = !!(args.framework && args.db);
|
|
265
|
+
let projectName = args.name;
|
|
266
|
+
if (!projectName) {
|
|
267
|
+
const result = await p2.text({
|
|
268
|
+
message: "Project name",
|
|
269
|
+
placeholder: "my-whop-app",
|
|
270
|
+
validate: (v) => {
|
|
271
|
+
if (!v) return "Project name is required";
|
|
272
|
+
if (existsSync4(resolve(v))) return `Directory "${v}" already exists`;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
if (isCancelled(result)) {
|
|
276
|
+
p2.cancel("Cancelled.");
|
|
277
|
+
process.exit(0);
|
|
278
|
+
}
|
|
279
|
+
projectName = result;
|
|
280
|
+
} else if (existsSync4(resolve(projectName))) {
|
|
281
|
+
p2.log.error(`Directory "${projectName}" already exists`);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
let appType = args.type;
|
|
285
|
+
if (!isNonInteractive && !args.yes) {
|
|
286
|
+
const result = await p2.select({
|
|
287
|
+
message: "What are you building?",
|
|
288
|
+
options: Object.entries(APP_TYPES).map(([value, t]) => ({
|
|
289
|
+
value,
|
|
290
|
+
label: t.available ? t.name : `${t.name} ${pc2.dim("(coming soon)")}`,
|
|
291
|
+
hint: t.description,
|
|
292
|
+
disabled: !t.available
|
|
293
|
+
}))
|
|
294
|
+
});
|
|
295
|
+
if (isCancelled(result)) {
|
|
296
|
+
p2.cancel("Cancelled.");
|
|
297
|
+
process.exit(0);
|
|
298
|
+
}
|
|
299
|
+
appType = result;
|
|
300
|
+
}
|
|
301
|
+
let framework = args.framework;
|
|
302
|
+
if (!framework) {
|
|
303
|
+
const result = await p2.select({
|
|
304
|
+
message: "Which framework?",
|
|
305
|
+
options: Object.entries(TEMPLATES).map(([value, t]) => ({
|
|
306
|
+
value,
|
|
307
|
+
label: t.available ? t.name : `${t.name} ${pc2.dim("(coming soon)")}`,
|
|
308
|
+
hint: t.description,
|
|
309
|
+
disabled: !t.available
|
|
310
|
+
}))
|
|
311
|
+
});
|
|
312
|
+
if (isCancelled(result)) {
|
|
313
|
+
p2.cancel("Cancelled.");
|
|
314
|
+
process.exit(0);
|
|
315
|
+
}
|
|
316
|
+
framework = result;
|
|
317
|
+
}
|
|
318
|
+
const template = TEMPLATES[framework];
|
|
319
|
+
if (!template || !template.available) {
|
|
320
|
+
p2.log.error(`Framework "${framework}" is not available. Options: ${Object.keys(TEMPLATES).filter((k) => TEMPLATES[k].available).join(", ")}`);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
let database = args.db;
|
|
324
|
+
if (!database) {
|
|
325
|
+
const result = await p2.select({
|
|
326
|
+
message: "Which database?",
|
|
327
|
+
options: Object.entries(DB_OPTIONS).map(([value, d]) => ({
|
|
328
|
+
value,
|
|
329
|
+
label: d.name,
|
|
330
|
+
hint: d.description
|
|
331
|
+
}))
|
|
332
|
+
});
|
|
333
|
+
if (isCancelled(result)) {
|
|
334
|
+
p2.cancel("Cancelled.");
|
|
335
|
+
process.exit(0);
|
|
336
|
+
}
|
|
337
|
+
database = result;
|
|
338
|
+
}
|
|
339
|
+
let dbUrl = args["db-url"] ?? "";
|
|
340
|
+
if (database !== "later" && !dbUrl) {
|
|
341
|
+
const result = await p2.text({
|
|
342
|
+
message: "Database URL",
|
|
343
|
+
placeholder: DB_OPTIONS[database]?.envVarHint ?? "postgresql://...",
|
|
344
|
+
validate: (v) => {
|
|
345
|
+
if (!v) return "Required (choose 'Configure later' to skip)";
|
|
346
|
+
return validateDatabaseUrl(v);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
if (isCancelled(result)) {
|
|
350
|
+
p2.cancel("Cancelled.");
|
|
351
|
+
process.exit(0);
|
|
352
|
+
}
|
|
353
|
+
dbUrl = result;
|
|
354
|
+
}
|
|
355
|
+
let appId = args["app-id"] ?? "";
|
|
356
|
+
let apiKey = args["api-key"] ?? "";
|
|
357
|
+
let webhookSecret = args["webhook-secret"] ?? "";
|
|
358
|
+
if (!isNonInteractive && !args.yes) {
|
|
359
|
+
const setupWhop = await p2.confirm({
|
|
360
|
+
message: "Configure Whop credentials now? (you can do this later via the setup wizard)",
|
|
361
|
+
initialValue: false
|
|
362
|
+
});
|
|
363
|
+
if (!isCancelled(setupWhop) && setupWhop) {
|
|
364
|
+
if (!appId) {
|
|
365
|
+
const result = await p2.text({
|
|
366
|
+
message: "Whop App ID",
|
|
367
|
+
placeholder: "app_xxxxxxxxx",
|
|
368
|
+
validate: (v) => v ? validateWhopAppId(v) : void 0
|
|
369
|
+
});
|
|
370
|
+
if (!isCancelled(result)) appId = result ?? "";
|
|
371
|
+
}
|
|
372
|
+
if (!apiKey) {
|
|
373
|
+
const result = await p2.text({
|
|
374
|
+
message: "Whop API Key",
|
|
375
|
+
placeholder: "apik_xxxxxxxxx (optional, press Enter to skip)"
|
|
376
|
+
});
|
|
377
|
+
if (!isCancelled(result)) apiKey = result ?? "";
|
|
378
|
+
}
|
|
379
|
+
if (!webhookSecret) {
|
|
380
|
+
const result = await p2.text({
|
|
381
|
+
message: "Whop Webhook Secret",
|
|
382
|
+
placeholder: "optional, press Enter to skip"
|
|
383
|
+
});
|
|
384
|
+
if (!isCancelled(result)) webhookSecret = result ?? "";
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (args["dry-run"]) {
|
|
389
|
+
p2.log.info(pc2.dim("Dry run \u2014 showing what would be created:\n"));
|
|
390
|
+
console.log(` ${pc2.bold("Project:")} ${projectName}`);
|
|
391
|
+
console.log(` ${pc2.bold("Framework:")} ${template.name}`);
|
|
392
|
+
console.log(` ${pc2.bold("App type:")} ${APP_TYPES[appType]?.name ?? appType}`);
|
|
393
|
+
console.log(` ${pc2.bold("Database:")} ${DB_OPTIONS[database]?.name ?? database}`);
|
|
394
|
+
console.log(` ${pc2.bold("Template:")} github.com/${template.repo}`);
|
|
395
|
+
if (dbUrl) console.log(` ${pc2.bold("DB URL:")} ${dbUrl.substring(0, 30)}...`);
|
|
396
|
+
if (appId) console.log(` ${pc2.bold("Whop App:")} ${appId}`);
|
|
397
|
+
console.log("");
|
|
398
|
+
p2.outro("No files were created.");
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const projectDir = resolve(projectName);
|
|
402
|
+
const s = p2.spinner();
|
|
403
|
+
s.start(`Cloning ${template.name} template...`);
|
|
404
|
+
const cloned = cloneTemplate(template.repo, projectDir);
|
|
405
|
+
if (!cloned) {
|
|
406
|
+
s.stop("Failed to clone template");
|
|
407
|
+
p2.log.error(`Could not clone github.com/${template.repo}. Check your internet connection.`);
|
|
408
|
+
cleanupDir(projectDir);
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
updatePackageName(projectDir, projectName);
|
|
412
|
+
s.stop(`${template.name} template cloned`);
|
|
413
|
+
const envVars = {};
|
|
414
|
+
if (dbUrl) envVars["DATABASE_URL"] = dbUrl;
|
|
415
|
+
if (framework === "nextjs") {
|
|
416
|
+
if (appId) envVars["NEXT_PUBLIC_WHOP_APP_ID"] = appId;
|
|
417
|
+
} else {
|
|
418
|
+
if (appId) envVars["WHOP_APP_ID"] = appId;
|
|
419
|
+
}
|
|
420
|
+
if (apiKey) envVars["WHOP_API_KEY"] = apiKey;
|
|
421
|
+
if (webhookSecret) envVars["WHOP_WEBHOOK_SECRET"] = webhookSecret;
|
|
422
|
+
if (Object.keys(envVars).length > 0) {
|
|
423
|
+
s.start("Configuring environment...");
|
|
424
|
+
writeEnvFile(projectDir, envVars);
|
|
425
|
+
s.stop("Environment configured");
|
|
426
|
+
}
|
|
427
|
+
createManifest(projectDir, {
|
|
428
|
+
framework,
|
|
429
|
+
appType,
|
|
430
|
+
database,
|
|
431
|
+
features: [],
|
|
432
|
+
templateVersion: "0.2.0"
|
|
433
|
+
});
|
|
434
|
+
const pm = detectPackageManager();
|
|
435
|
+
s.start(`Installing dependencies with ${pm}...`);
|
|
436
|
+
const installResult = exec(`${pm} install`, projectDir);
|
|
437
|
+
if (!installResult.success) {
|
|
438
|
+
s.stop(`${pm} install failed`);
|
|
439
|
+
p2.log.warning("Dependency installation failed. Run it manually after setup.");
|
|
440
|
+
} else {
|
|
441
|
+
s.stop("Dependencies installed");
|
|
442
|
+
}
|
|
443
|
+
initGit(projectDir);
|
|
444
|
+
const configured = [];
|
|
445
|
+
const missing = [];
|
|
446
|
+
if (dbUrl) configured.push("Database");
|
|
447
|
+
else missing.push("Database URL");
|
|
448
|
+
if (appId) configured.push("Whop App ID");
|
|
449
|
+
else missing.push("Whop App ID");
|
|
450
|
+
if (apiKey) configured.push("Whop API Key");
|
|
451
|
+
else missing.push("Whop API Key");
|
|
452
|
+
if (webhookSecret) configured.push("Webhook Secret");
|
|
453
|
+
else missing.push("Webhook Secret");
|
|
454
|
+
let summary = "";
|
|
455
|
+
if (configured.length > 0) {
|
|
456
|
+
summary += `${pc2.green("\u2713")} ${configured.join(", ")}
|
|
457
|
+
`;
|
|
458
|
+
}
|
|
459
|
+
if (missing.length > 0) {
|
|
460
|
+
summary += `${pc2.yellow("\u25CB")} Missing: ${missing.join(", ")}
|
|
461
|
+
`;
|
|
462
|
+
summary += ` ${pc2.dim("Configure via the setup wizard or .env.local")}
|
|
463
|
+
`;
|
|
464
|
+
}
|
|
465
|
+
summary += `
|
|
466
|
+
`;
|
|
467
|
+
summary += ` ${pc2.bold("cd")} ${basename2(projectName)}
|
|
468
|
+
`;
|
|
469
|
+
if (dbUrl) {
|
|
470
|
+
summary += ` ${pc2.bold(`${pm} run db:push`)}
|
|
471
|
+
`;
|
|
472
|
+
}
|
|
473
|
+
summary += ` ${pc2.bold(`${pm} run dev`)}`;
|
|
474
|
+
p2.note(summary, "Your app is ready");
|
|
475
|
+
p2.outro(`${pc2.green("Happy building!")} ${pc2.dim("\u2014 whop-kit")}`);
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// src/cli-create.ts
|
|
480
|
+
runMain(init_default);
|
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.2.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
|
-
});
|