@whop/cli 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/{api-3MQ6MQGV.js → api-A6NYSDKW.js} +4 -4
- package/dist/{chunk-ET6QSHSA.js → chunk-4WNH276N.js} +778 -131
- package/dist/chunk-4WNH276N.js.map +1 -0
- package/dist/index.js +1290 -477
- package/dist/index.js.map +1 -1
- package/dist/vite.d.ts +5 -0
- package/dist/vite.js +811 -0
- package/dist/vite.js.map +1 -0
- package/package.json +12 -3
- package/dist/chunk-ET6QSHSA.js.map +0 -1
- /package/dist/{api-3MQ6MQGV.js.map → api-A6NYSDKW.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
PromptCancelledError,
|
|
10
10
|
apiBaseUrl,
|
|
11
11
|
buildTarget,
|
|
12
|
+
buildVersion,
|
|
12
13
|
chooseAuthMethod,
|
|
13
14
|
configDir,
|
|
14
15
|
createWhopFetch,
|
|
@@ -25,13 +26,14 @@ import {
|
|
|
25
26
|
readConfig,
|
|
26
27
|
removeAllProfiles,
|
|
27
28
|
removeProfile,
|
|
28
|
-
|
|
29
|
+
resolveActiveAccountId,
|
|
29
30
|
selectProfile,
|
|
31
|
+
setActiveProfileAccount,
|
|
30
32
|
startSpinner,
|
|
31
33
|
switchProfile,
|
|
32
34
|
upsertProfile,
|
|
33
35
|
validateApiKey
|
|
34
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-4WNH276N.js";
|
|
35
37
|
import {
|
|
36
38
|
external_exports
|
|
37
39
|
} from "./chunk-KFCNNWPI.js";
|
|
@@ -128,7 +130,7 @@ var groups_default = [
|
|
|
128
130
|
var loginOptions = external_exports.object({
|
|
129
131
|
apiKey: external_exports.string().optional().describe("API key for non-interactive login (skips the prompt)"),
|
|
130
132
|
profile: external_exports.string().optional().describe("Name for this profile (defaults to the account route)"),
|
|
131
|
-
|
|
133
|
+
accountId: external_exports.string().optional().describe(
|
|
132
134
|
"Business account to act on behalf of (biz_xxx) \u2014 for OAuth users who manage more than one"
|
|
133
135
|
)
|
|
134
136
|
});
|
|
@@ -193,7 +195,7 @@ async function performLogin(input) {
|
|
|
193
195
|
const { profileName, identity: identity2, existed: existed2 } = await performOAuthLogin({
|
|
194
196
|
profile: input.profile,
|
|
195
197
|
agent: input.agent,
|
|
196
|
-
|
|
198
|
+
accountId: input.accountId
|
|
197
199
|
});
|
|
198
200
|
return {
|
|
199
201
|
ok: true,
|
|
@@ -262,7 +264,7 @@ async function loginAdapter(c2) {
|
|
|
262
264
|
agent: c2.agent,
|
|
263
265
|
apiKey: c2.options.apiKey ?? c2.env.WHOP_API_KEY,
|
|
264
266
|
profile: c2.options.profile,
|
|
265
|
-
|
|
267
|
+
accountId: c2.options.accountId
|
|
266
268
|
});
|
|
267
269
|
} catch (error) {
|
|
268
270
|
if (error instanceof PromptCancelledError) {
|
|
@@ -287,14 +289,14 @@ async function loginAdapter(c2) {
|
|
|
287
289
|
}
|
|
288
290
|
]
|
|
289
291
|
};
|
|
290
|
-
} else if (result.code === "
|
|
292
|
+
} else if (result.code === "MULTIPLE_ACCOUNTS" || result.code === "ACCOUNT_NOT_FOUND") {
|
|
291
293
|
cta = {
|
|
292
294
|
description: "Choose which business account to use:",
|
|
293
295
|
commands: [
|
|
294
296
|
{
|
|
295
297
|
command: "auth login",
|
|
296
|
-
options: {
|
|
297
|
-
description: "Pass --
|
|
298
|
+
options: { accountId: true },
|
|
299
|
+
description: "Pass --account-id biz_xxx"
|
|
298
300
|
}
|
|
299
301
|
]
|
|
300
302
|
};
|
|
@@ -307,14 +309,14 @@ async function loginAdapter(c2) {
|
|
|
307
309
|
});
|
|
308
310
|
}
|
|
309
311
|
let firstRun = !result.existed;
|
|
310
|
-
const
|
|
311
|
-
if (
|
|
312
|
+
const accountId = getActiveProfile()?.accountId ?? "";
|
|
313
|
+
if (accountId) {
|
|
312
314
|
try {
|
|
313
|
-
const { createWhopFetch: createWhopFetch2 } = await import("./api-
|
|
315
|
+
const { createWhopFetch: createWhopFetch2 } = await import("./api-A6NYSDKW.js");
|
|
314
316
|
const fetch3 = createWhopFetch2();
|
|
315
317
|
const res = await fetch3(
|
|
316
318
|
new Request(
|
|
317
|
-
`https://api.whop.com/products?company_id=${
|
|
319
|
+
`https://api.whop.com/products?company_id=${accountId}&first=1`
|
|
318
320
|
)
|
|
319
321
|
);
|
|
320
322
|
if (res.ok) {
|
|
@@ -324,14 +326,14 @@ async function loginAdapter(c2) {
|
|
|
324
326
|
} catch {
|
|
325
327
|
}
|
|
326
328
|
}
|
|
327
|
-
if (!
|
|
329
|
+
if (!accountId) {
|
|
328
330
|
return c2.ok(result.output, {
|
|
329
331
|
cta: {
|
|
330
|
-
description: "You're logged in, but
|
|
332
|
+
description: "You're logged in, but you don't have a business account yet. Create one and start selling in one command:",
|
|
331
333
|
commands: [
|
|
332
334
|
{
|
|
333
|
-
command: "
|
|
334
|
-
description: "
|
|
335
|
+
command: "quickstart",
|
|
336
|
+
description: "Create your business account, product, and checkout link"
|
|
335
337
|
}
|
|
336
338
|
]
|
|
337
339
|
}
|
|
@@ -589,10 +591,15 @@ function buildAuthGroup() {
|
|
|
589
591
|
return auth;
|
|
590
592
|
}
|
|
591
593
|
|
|
592
|
-
// src/
|
|
593
|
-
import {
|
|
594
|
+
// src/apps/commands.ts
|
|
595
|
+
import { isCancel, select, text } from "@clack/prompts";
|
|
596
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
597
|
+
import { createHash } from "crypto";
|
|
598
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
|
|
599
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
600
|
+
import chalk from "chalk";
|
|
594
601
|
|
|
595
|
-
// src/
|
|
602
|
+
// src/journey/client.ts
|
|
596
603
|
var PLACEHOLDER = "https://api.whop.com";
|
|
597
604
|
var whopFetch = createWhopFetch();
|
|
598
605
|
async function makeWhopRequest(method, path3, body) {
|
|
@@ -607,473 +614,1145 @@ async function makeWhopRequest(method, path3, body) {
|
|
|
607
614
|
return text3 ? JSON.parse(text3) : { success: true };
|
|
608
615
|
}
|
|
609
616
|
|
|
610
|
-
// src/
|
|
611
|
-
function
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
after: external_exports.string().optional().describe("Pagination cursor")
|
|
622
|
-
};
|
|
623
|
-
if (config.filter) {
|
|
624
|
-
shape[config.filter.option] = external_exports.string().optional().describe(config.filter.describe);
|
|
625
|
-
}
|
|
626
|
-
group.command("list", {
|
|
627
|
-
description: config.description,
|
|
628
|
-
options: external_exports.object(shape),
|
|
629
|
-
run: (c2) => {
|
|
630
|
-
const options = c2.options;
|
|
631
|
-
const params = new URLSearchParams();
|
|
632
|
-
if (config.filter && options[config.filter.option]) {
|
|
633
|
-
params.set(config.filter.param, String(options[config.filter.option]));
|
|
634
|
-
}
|
|
635
|
-
if (options.first) params.set("first", String(options.first));
|
|
636
|
-
if (options.after) params.set("after", String(options.after));
|
|
637
|
-
const qs = params.toString() ? `?${params}` : "";
|
|
638
|
-
return makeWhopRequest("GET", `${config.path}${qs}`);
|
|
639
|
-
}
|
|
617
|
+
// src/apps/api.ts
|
|
618
|
+
async function createApp(input) {
|
|
619
|
+
const company_id = input.company_id ?? await resolveActiveAccountId();
|
|
620
|
+
return makeWhopRequest("POST", "/apps", { ...input, company_id });
|
|
621
|
+
}
|
|
622
|
+
var DEV_TOKEN_TTL_MS = (3 * 60 - 1) * 60 * 1e3;
|
|
623
|
+
async function createDevAccessToken() {
|
|
624
|
+
const company_id = await resolveActiveAccountId();
|
|
625
|
+
return makeWhopRequest("POST", "/access_tokens", {
|
|
626
|
+
...company_id ? { company_id } : {},
|
|
627
|
+
expires_at: new Date(Date.now() + DEV_TOKEN_TTL_MS).toISOString()
|
|
640
628
|
});
|
|
641
629
|
}
|
|
642
|
-
function
|
|
643
|
-
const
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
630
|
+
async function listApps() {
|
|
631
|
+
const companyId = await resolveActiveAccountId();
|
|
632
|
+
const query = companyId ? `?company_id=${encodeURIComponent(companyId)}` : "";
|
|
633
|
+
const response = await makeWhopRequest(
|
|
634
|
+
"GET",
|
|
635
|
+
`/apps${query}`
|
|
636
|
+
);
|
|
637
|
+
return response.data ?? [];
|
|
638
|
+
}
|
|
639
|
+
async function getApp(appId) {
|
|
640
|
+
return makeWhopRequest("GET", `/apps/${appId}`);
|
|
641
|
+
}
|
|
642
|
+
async function updateAppRoute(appId, route) {
|
|
643
|
+
return makeWhopRequest("PATCH", `/apps/${appId}`, { route });
|
|
644
|
+
}
|
|
645
|
+
async function createAppBuild(input) {
|
|
646
|
+
return makeWhopRequest("POST", "/app_builds", {
|
|
647
|
+
app_id: input.app_id,
|
|
648
|
+
platform: "web",
|
|
649
|
+
checksum: input.checksum,
|
|
650
|
+
attachment: { id: input.file_id }
|
|
650
651
|
});
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
652
|
+
}
|
|
653
|
+
async function getAppBuild(buildId) {
|
|
654
|
+
return makeWhopRequest("GET", `/app_builds/${buildId}`);
|
|
655
|
+
}
|
|
656
|
+
async function listAppBuilds(appId) {
|
|
657
|
+
const response = await makeWhopRequest(
|
|
658
|
+
"GET",
|
|
659
|
+
`/app_builds?app_id=${encodeURIComponent(appId)}&platform=web`
|
|
660
|
+
);
|
|
661
|
+
return response.data ?? [];
|
|
662
|
+
}
|
|
663
|
+
async function promoteAppBuild(buildId) {
|
|
664
|
+
return makeWhopRequest(
|
|
665
|
+
"POST",
|
|
666
|
+
`/app_builds/${buildId}/promote`,
|
|
667
|
+
{}
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
async function uploadBuildArchive(zip, filename) {
|
|
671
|
+
const file = await makeWhopRequest("POST", "/files", {
|
|
672
|
+
filename,
|
|
673
|
+
visibility: "public"
|
|
667
674
|
});
|
|
675
|
+
if (!file.upload_url) {
|
|
676
|
+
throw new Error("File creation did not return an upload URL.");
|
|
677
|
+
}
|
|
678
|
+
const body = new Uint8Array(zip).buffer;
|
|
679
|
+
const uploadResponse = await fetch(file.upload_url, {
|
|
680
|
+
method: "PUT",
|
|
681
|
+
headers: file.upload_headers ?? { "Content-Type": "application/zip" },
|
|
682
|
+
body
|
|
683
|
+
});
|
|
684
|
+
if (!uploadResponse.ok) {
|
|
685
|
+
throw new Error(
|
|
686
|
+
`Archive upload failed: ${uploadResponse.status} ${await uploadResponse.text()}`
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
return file.id;
|
|
690
|
+
}
|
|
691
|
+
async function waitForFileReady(fileId, timeoutMs = 6e4) {
|
|
692
|
+
const deadline = Date.now() + timeoutMs;
|
|
693
|
+
while (Date.now() < deadline) {
|
|
694
|
+
const file = await makeWhopRequest("GET", `/files/${fileId}`);
|
|
695
|
+
if (file.upload_status === "ready") return;
|
|
696
|
+
await sleep(1e3);
|
|
697
|
+
}
|
|
698
|
+
throw new Error(
|
|
699
|
+
"Timed out waiting for the build archive to finish processing."
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
async function waitForBuildPromotion(buildId, timeoutMs = 3e4) {
|
|
703
|
+
const deadline = Date.now() + timeoutMs;
|
|
704
|
+
let latest = await getAppBuild(buildId);
|
|
705
|
+
while (Date.now() < deadline) {
|
|
706
|
+
if (latest.is_production) return latest;
|
|
707
|
+
if (latest.status === "rejected") {
|
|
708
|
+
throw new Error("Build was rejected and cannot be promoted.");
|
|
709
|
+
}
|
|
710
|
+
await sleep(750);
|
|
711
|
+
latest = await getAppBuild(buildId);
|
|
712
|
+
}
|
|
713
|
+
return latest;
|
|
714
|
+
}
|
|
715
|
+
function sleep(ms) {
|
|
716
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
668
717
|
}
|
|
669
718
|
|
|
670
|
-
// src/
|
|
671
|
-
import
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
719
|
+
// src/apps/config.ts
|
|
720
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
721
|
+
import { join, resolve } from "path";
|
|
722
|
+
var APP_CONFIG_FILENAME = "whop.app.json";
|
|
723
|
+
function findProjectDir(startDir) {
|
|
724
|
+
let dir = resolve(startDir ?? process.cwd());
|
|
725
|
+
for (let i = 0; i < 25; i++) {
|
|
726
|
+
if (existsSync(join(dir, APP_CONFIG_FILENAME))) return dir;
|
|
727
|
+
const parent = resolve(dir, "..");
|
|
728
|
+
if (parent === dir) break;
|
|
729
|
+
dir = parent;
|
|
730
|
+
}
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
function readAppConfig(projectDir) {
|
|
734
|
+
const path3 = join(projectDir, APP_CONFIG_FILENAME);
|
|
735
|
+
if (!existsSync(path3)) return null;
|
|
736
|
+
try {
|
|
737
|
+
const parsed = JSON.parse(
|
|
738
|
+
readFileSync(path3, "utf-8")
|
|
739
|
+
);
|
|
740
|
+
if (typeof parsed.app_id === "string" && typeof parsed.name === "string" && typeof parsed.route === "string") {
|
|
741
|
+
return { app_id: parsed.app_id, name: parsed.name, route: parsed.route };
|
|
742
|
+
}
|
|
743
|
+
return null;
|
|
744
|
+
} catch {
|
|
745
|
+
return null;
|
|
683
746
|
}
|
|
684
|
-
|
|
685
|
-
|
|
747
|
+
}
|
|
748
|
+
function writeAppConfig(projectDir, config) {
|
|
749
|
+
writeFileSync(
|
|
750
|
+
join(projectDir, APP_CONFIG_FILENAME),
|
|
751
|
+
`${JSON.stringify(config, null, " ")}
|
|
752
|
+
`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
function slugify(name) {
|
|
756
|
+
return name.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
|
|
686
757
|
}
|
|
687
758
|
|
|
688
|
-
// src/
|
|
689
|
-
|
|
759
|
+
// src/apps/scaffold.ts
|
|
760
|
+
import { spawnSync } from "child_process";
|
|
761
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
762
|
+
import { basename, join as join2 } from "path";
|
|
763
|
+
var SPAWN_VIA_SHELL = process.platform === "win32";
|
|
764
|
+
var DLX_RUNNERS = {
|
|
765
|
+
npm: ["npx", "-y"],
|
|
766
|
+
pnpm: ["pnpm", "dlx"],
|
|
767
|
+
bun: ["bunx"],
|
|
768
|
+
yarn: ["yarn", "dlx"]
|
|
769
|
+
};
|
|
770
|
+
var PM_PRIORITY = ["bun", "pnpm", "npm"];
|
|
771
|
+
function isPmInstalled(pm) {
|
|
772
|
+
const result = spawnSync(pm, ["--version"], {
|
|
773
|
+
stdio: "ignore",
|
|
774
|
+
shell: SPAWN_VIA_SHELL
|
|
775
|
+
});
|
|
776
|
+
return !result.error && result.status === 0;
|
|
777
|
+
}
|
|
778
|
+
function detectPackageManager() {
|
|
779
|
+
return PM_PRIORITY.find(isPmInstalled) ?? null;
|
|
780
|
+
}
|
|
781
|
+
function pmInstallHint(pm) {
|
|
782
|
+
const win = process.platform === "win32";
|
|
783
|
+
switch (pm) {
|
|
784
|
+
case "bun":
|
|
785
|
+
return win ? 'powershell -c "irm bun.sh/install.ps1 | iex"' : "curl -fsSL https://bun.sh/install | bash";
|
|
786
|
+
case "pnpm":
|
|
787
|
+
return win ? 'powershell -c "irm get.pnpm.io/install.ps1 | iex"' : "curl -fsSL https://get.pnpm.io/install.sh | sh -";
|
|
788
|
+
case "npm":
|
|
789
|
+
return "install Node.js (includes npm): https://nodejs.org";
|
|
790
|
+
case "yarn":
|
|
791
|
+
return "npm install -g yarn";
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
function runTanstackCreate(options) {
|
|
795
|
+
const [runner, ...runnerArgs] = DLX_RUNNERS[options.pm];
|
|
796
|
+
const result = spawnSync(
|
|
797
|
+
runner,
|
|
798
|
+
[
|
|
799
|
+
...runnerArgs,
|
|
800
|
+
"@tanstack/cli@latest",
|
|
801
|
+
"create",
|
|
802
|
+
options.projectName,
|
|
803
|
+
"--target-dir",
|
|
804
|
+
options.targetDir,
|
|
805
|
+
"--framework",
|
|
806
|
+
"React",
|
|
807
|
+
"--deployment",
|
|
808
|
+
"cloudflare",
|
|
809
|
+
"--no-git",
|
|
810
|
+
"--no-install",
|
|
811
|
+
"--no-intent",
|
|
812
|
+
"--no-toolchain",
|
|
813
|
+
"--no-examples",
|
|
814
|
+
"--non-interactive"
|
|
815
|
+
],
|
|
816
|
+
{
|
|
817
|
+
stdio: options.quiet ? "ignore" : "inherit",
|
|
818
|
+
shell: SPAWN_VIA_SHELL
|
|
819
|
+
}
|
|
820
|
+
);
|
|
821
|
+
if (result.error) throw result.error;
|
|
822
|
+
if (result.status !== 0) {
|
|
823
|
+
throw new Error(
|
|
824
|
+
`\`${options.pm === "npm" ? "npx" : options.pm} @tanstack/cli create\` failed (exit ${result.status}). Check your network connection and try again.`
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
var VITE_CONFIG_NAMES = [
|
|
829
|
+
"vite.config.ts",
|
|
830
|
+
"vite.config.mts",
|
|
831
|
+
"vite.config.js",
|
|
832
|
+
"vite.config.mjs"
|
|
833
|
+
];
|
|
834
|
+
var WRANGLER_CONFIG_NAMES = ["wrangler.jsonc", "wrangler.json"];
|
|
835
|
+
function cliDependencyVersion() {
|
|
836
|
+
return buildVersion ? `^${buildVersion}` : "latest";
|
|
837
|
+
}
|
|
838
|
+
function whopifyProject(options) {
|
|
839
|
+
const manualSteps = [];
|
|
840
|
+
patchPackageJson(options.targetDir, manualSteps);
|
|
841
|
+
patchViteConfig(options.targetDir, manualSteps);
|
|
842
|
+
patchWranglerConfig(options.targetDir, options.route, manualSteps);
|
|
843
|
+
return { manualSteps };
|
|
844
|
+
}
|
|
845
|
+
function patchPackageJson(dir, manualSteps) {
|
|
846
|
+
const file = join2(dir, "package.json");
|
|
847
|
+
let pkg;
|
|
848
|
+
try {
|
|
849
|
+
pkg = JSON.parse(readFileSync2(file, "utf-8"));
|
|
850
|
+
} catch {
|
|
851
|
+
manualSteps.push(
|
|
852
|
+
`package.json could not be parsed. Add a "deploy": "whop apps deploy" script, a "typecheck" script, and a devDependency "@whop/cli": "${cliDependencyVersion()}".`
|
|
853
|
+
);
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
pkg.scripts = {
|
|
857
|
+
...pkg.scripts,
|
|
858
|
+
deploy: "whop apps deploy",
|
|
859
|
+
typecheck: pkg.scripts?.typecheck ?? "tsc --noEmit"
|
|
860
|
+
};
|
|
861
|
+
pkg.devDependencies = {
|
|
862
|
+
...pkg.devDependencies,
|
|
863
|
+
// The project imports the whop() vite plugin from @whop/cli/vite.
|
|
864
|
+
"@whop/cli": pkg.devDependencies?.["@whop/cli"] ?? cliDependencyVersion()
|
|
865
|
+
};
|
|
866
|
+
writeFileSync2(file, `${JSON.stringify(pkg, null, 2)}
|
|
867
|
+
`);
|
|
868
|
+
}
|
|
869
|
+
function patchViteConfig(dir, manualSteps) {
|
|
870
|
+
const file = VITE_CONFIG_NAMES.map((name) => join2(dir, name)).find(
|
|
871
|
+
(path3) => existsSync2(path3)
|
|
872
|
+
);
|
|
873
|
+
if (!file) {
|
|
874
|
+
manualSteps.push(
|
|
875
|
+
"No vite config found (looked for vite.config.ts/.mts/.js/.mjs). Whop hosting serves Vite builds \u2014 create a vite config that builds with @cloudflare/vite-plugin (emitting dist/client and, for SSR, dist/server) and includes the whop() plugin from @whop/cli/vite in its plugins array."
|
|
876
|
+
);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
let content = readFileSync2(file, "utf-8");
|
|
880
|
+
const hasWhopPlugin = content.includes("@whop/cli/vite");
|
|
881
|
+
if (!hasWhopPlugin) {
|
|
882
|
+
const pluginsOpen = /plugins:\s*\[/;
|
|
883
|
+
if (!pluginsOpen.test(content)) {
|
|
884
|
+
manualSteps.push(
|
|
885
|
+
`Could not find a \`plugins: [...]\` array in ${basename(file)}. Add \`import { whop } from '@whop/cli/vite'\` and include \`whop()\` in the config's plugins so builds pack dist/whop-build.zip.`
|
|
886
|
+
);
|
|
887
|
+
} else {
|
|
888
|
+
content = `import { whop } from '@whop/cli/vite'
|
|
889
|
+
${content}`;
|
|
890
|
+
content = content.replace(
|
|
891
|
+
pluginsOpen,
|
|
892
|
+
(match) => `${match}
|
|
893
|
+
whop(),`
|
|
894
|
+
);
|
|
895
|
+
writeFileSync2(file, content);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (!content.includes("@cloudflare/vite-plugin")) {
|
|
899
|
+
manualSteps.push(
|
|
900
|
+
`${basename(file)} does not use @cloudflare/vite-plugin. Whop hosting runs builds on Cloudflare Workers: install @cloudflare/vite-plugin and wrangler, add \`cloudflare({ viteEnvironment: { name: 'ssr' } })\` to the plugins array (before your framework plugin), and make sure \`vite build\` emits dist/client (static assets) and dist/server (worker modules with an index.js entry).`
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
function patchWranglerConfig(dir, route, manualSteps) {
|
|
905
|
+
const file = WRANGLER_CONFIG_NAMES.map((name) => join2(dir, name)).find(
|
|
906
|
+
(path3) => existsSync2(path3)
|
|
907
|
+
);
|
|
908
|
+
if (!file) {
|
|
909
|
+
manualSteps.push(
|
|
910
|
+
`No wrangler config found. Create wrangler.jsonc with: { "name": "${route}", "compatibility_date": "<today>", "compatibility_flags": ["nodejs_compat"], "main": "<your framework's server entry>" } (for TanStack Start the main is "@tanstack/react-start/server-entry").`
|
|
911
|
+
);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const content = readFileSync2(file, "utf-8");
|
|
915
|
+
const updated = replaceTopLevelName(content, route);
|
|
916
|
+
if (updated) {
|
|
917
|
+
writeFileSync2(file, updated);
|
|
918
|
+
} else {
|
|
919
|
+
manualSteps.push(
|
|
920
|
+
`Set the top-level "name" in ${basename(file)} to "${route}" so the worker is named after the app's route.`
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
function replaceTopLevelName(content, route) {
|
|
925
|
+
let depth = 0;
|
|
926
|
+
let i = 0;
|
|
927
|
+
while (i < content.length) {
|
|
928
|
+
const ch = content[i];
|
|
929
|
+
if (ch === '"') {
|
|
930
|
+
let j = i + 1;
|
|
931
|
+
while (j < content.length && content[j] !== '"') {
|
|
932
|
+
j += content[j] === "\\" ? 2 : 1;
|
|
933
|
+
}
|
|
934
|
+
if (depth === 1 && content.slice(i + 1, j) === "name") {
|
|
935
|
+
const after = content.slice(j + 1);
|
|
936
|
+
const value = after.match(/^(\s*:\s*)"(?:[^"\\]|\\.)*"/);
|
|
937
|
+
if (value) {
|
|
938
|
+
const start = j + 1 + value[1].length;
|
|
939
|
+
const end = j + 1 + value[0].length;
|
|
940
|
+
return `${content.slice(0, start)}"${route}"${content.slice(end)}`;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
i = j + 1;
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (ch === "/" && content[i + 1] === "/") {
|
|
947
|
+
const newline = content.indexOf("\n", i);
|
|
948
|
+
i = newline === -1 ? content.length : newline + 1;
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
if (ch === "/" && content[i + 1] === "*") {
|
|
952
|
+
const close = content.indexOf("*/", i + 2);
|
|
953
|
+
i = close === -1 ? content.length : close + 2;
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
if (ch === "{" || ch === "[") depth++;
|
|
957
|
+
if (ch === "}" || ch === "]") depth--;
|
|
958
|
+
i++;
|
|
959
|
+
}
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// src/apps/commands.ts
|
|
964
|
+
var BUILD_ARCHIVE = "dist/whop-build.zip";
|
|
965
|
+
var AppSchema = external_exports.object({
|
|
690
966
|
id: external_exports.string(),
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
member_count: external_exports.number().optional(),
|
|
697
|
-
created_at: external_exports.number().optional()
|
|
967
|
+
name: external_exports.string(),
|
|
968
|
+
route: external_exports.string().nullable().optional(),
|
|
969
|
+
url: external_exports.string().optional(),
|
|
970
|
+
directory: external_exports.string().optional(),
|
|
971
|
+
install_failed: external_exports.boolean().optional()
|
|
698
972
|
});
|
|
699
|
-
var
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
)
|
|
704
|
-
visibility: external_exports.enum(["visible", "hidden", "archived"]).optional().describe("Who can see this product"),
|
|
705
|
-
headline: external_exports.string().optional().describe("Short tagline shown on the store page"),
|
|
706
|
-
description: external_exports.string().optional().describe("Full description"),
|
|
707
|
-
route: external_exports.string().optional().describe("Custom URL slug (e.g. my-course)"),
|
|
708
|
-
redirect_purchase_url: external_exports.string().optional().describe("Redirect buyers here after purchase")
|
|
973
|
+
var BuildSchema = external_exports.object({
|
|
974
|
+
id: external_exports.string(),
|
|
975
|
+
status: external_exports.string(),
|
|
976
|
+
is_production: external_exports.boolean().optional(),
|
|
977
|
+
url: external_exports.string().optional()
|
|
709
978
|
});
|
|
710
|
-
function
|
|
711
|
-
|
|
712
|
-
|
|
979
|
+
function detectPackageManager2(projectDir) {
|
|
980
|
+
if (existsSync3(join3(projectDir, "bun.lock")) || existsSync3(join3(projectDir, "bun.lockb")))
|
|
981
|
+
return "bun";
|
|
982
|
+
if (existsSync3(join3(projectDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
983
|
+
if (existsSync3(join3(projectDir, "yarn.lock"))) return "yarn";
|
|
984
|
+
return "npm";
|
|
985
|
+
}
|
|
986
|
+
function hasScript(projectDir, script) {
|
|
987
|
+
try {
|
|
988
|
+
const pkg = JSON.parse(
|
|
989
|
+
readFileSync3(join3(projectDir, "package.json"), "utf-8")
|
|
990
|
+
);
|
|
991
|
+
return Boolean(pkg.scripts?.[script]);
|
|
992
|
+
} catch {
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
var SPAWN_VIA_SHELL2 = process.platform === "win32";
|
|
997
|
+
function runScript(projectDir, script, extraEnv = {}) {
|
|
998
|
+
const pm = detectPackageManager2(projectDir);
|
|
999
|
+
const result = spawnSync2(pm, ["run", script], {
|
|
1000
|
+
cwd: projectDir,
|
|
1001
|
+
stdio: "inherit",
|
|
1002
|
+
shell: SPAWN_VIA_SHELL2,
|
|
1003
|
+
env: { ...process.env, ...extraEnv }
|
|
1004
|
+
});
|
|
1005
|
+
if (result.error) {
|
|
1006
|
+
throw new Error(
|
|
1007
|
+
`Could not run \`${pm} run ${script}\`: ${result.error.message}`
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
if (result.status !== 0) {
|
|
1011
|
+
throw new Error(
|
|
1012
|
+
`\`${pm} run ${script}\` failed with exit code ${result.status ?? "unknown"}`
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
function log(c2, message) {
|
|
1017
|
+
if (!c2.agent) console.log(message);
|
|
1018
|
+
}
|
|
1019
|
+
function loadProject(dir) {
|
|
1020
|
+
const projectDir = dir ? resolve2(dir) : findProjectDir();
|
|
1021
|
+
if (!projectDir) {
|
|
1022
|
+
return {
|
|
1023
|
+
ok: false,
|
|
1024
|
+
error: {
|
|
1025
|
+
code: "NO_PROJECT",
|
|
1026
|
+
message: `No ${APP_CONFIG_FILENAME} found in this directory or any parent. Run \`whop apps create --existing\` to link this project to a Whop app.`,
|
|
1027
|
+
cta: {
|
|
1028
|
+
commands: [
|
|
1029
|
+
{
|
|
1030
|
+
command: "apps create --existing",
|
|
1031
|
+
description: "Link this project to a Whop app"
|
|
1032
|
+
}
|
|
1033
|
+
]
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
const config = readAppConfig(projectDir);
|
|
1039
|
+
if (!config) {
|
|
1040
|
+
return {
|
|
1041
|
+
ok: false,
|
|
1042
|
+
error: {
|
|
1043
|
+
code: "INVALID_CONFIG",
|
|
1044
|
+
message: `${join3(projectDir, APP_CONFIG_FILENAME)} is missing or malformed. Run \`whop apps create --existing\` to regenerate it.`
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
return { ok: true, projectDir, config };
|
|
1049
|
+
}
|
|
1050
|
+
async function promptText(message, placeholder, initialValue) {
|
|
1051
|
+
const value = await text({ message, placeholder, initialValue });
|
|
1052
|
+
if (isCancel(value)) return null;
|
|
1053
|
+
return String(value);
|
|
1054
|
+
}
|
|
1055
|
+
function formatManualSteps(steps) {
|
|
1056
|
+
return steps.map((step, i) => ` ${i + 1}. ${step}`).join("\n");
|
|
1057
|
+
}
|
|
1058
|
+
async function runRegisterFlow(c2, options) {
|
|
1059
|
+
const projectDir = resolve2(options.dir ?? ".");
|
|
1060
|
+
if (!existsSync3(join3(projectDir, "package.json"))) {
|
|
1061
|
+
return c2.error({
|
|
1062
|
+
code: "NO_PACKAGE_JSON",
|
|
1063
|
+
message: `No package.json in ${projectDir} \u2014 --existing wires up a project that's already here. Drop --existing to scaffold a new one.`
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
const existing = readAppConfig(projectDir);
|
|
1067
|
+
if (existing) {
|
|
1068
|
+
const { manualSteps: manualSteps2 } = whopifyProject({
|
|
1069
|
+
targetDir: projectDir,
|
|
1070
|
+
route: existing.route
|
|
1071
|
+
});
|
|
1072
|
+
if (manualSteps2.length > 0) {
|
|
1073
|
+
return c2.error({
|
|
1074
|
+
code: "WHOPIFY_INCOMPLETE",
|
|
1075
|
+
message: `This project is already linked to ${existing.app_id} (${existing.route}), but it isn't fully wired for Whop hosting yet. Complete these steps:
|
|
1076
|
+
|
|
1077
|
+
${formatManualSteps(manualSteps2)}
|
|
1078
|
+
|
|
1079
|
+
Then run \`whop apps deploy\`. Re-running \`whop apps create --existing\` re-checks the wiring.`
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
if (!c2.agent && !c2.formatExplicit) {
|
|
1083
|
+
console.log(
|
|
1084
|
+
`${chalk.green.bold(`\u2713 Already linked to ${existing.name}`)} ${chalk.dim(`(${existing.app_id})`)} \u2014 wiring verified. Delete ${APP_CONFIG_FILENAME} to relink.`
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
return c2.ok({
|
|
1088
|
+
id: existing.app_id,
|
|
1089
|
+
name: existing.name,
|
|
1090
|
+
route: existing.route,
|
|
1091
|
+
directory: projectDir
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
let linked = null;
|
|
1095
|
+
if (options.link) {
|
|
1096
|
+
const apps = await listApps();
|
|
1097
|
+
linked = apps.find((a) => a.id === options.link) ?? null;
|
|
1098
|
+
if (!linked) {
|
|
1099
|
+
return c2.error({
|
|
1100
|
+
code: "APP_NOT_FOUND",
|
|
1101
|
+
message: `No app ${options.link} found on your account.`
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
let name = options.name ?? linked?.name;
|
|
1106
|
+
if (!name) {
|
|
1107
|
+
if (c2.agent) {
|
|
1108
|
+
return c2.error({
|
|
1109
|
+
code: "NAME_REQUIRED",
|
|
1110
|
+
message: 'Pass a name: `whop apps create "My Site"`.',
|
|
1111
|
+
retryable: true
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
const value = await promptText("What is your app called?", "My Site");
|
|
1115
|
+
if (value === null)
|
|
1116
|
+
return c2.error({
|
|
1117
|
+
code: "CANCELLED",
|
|
1118
|
+
message: "Cancelled.",
|
|
1119
|
+
exitCode: 130
|
|
1120
|
+
});
|
|
1121
|
+
name = value;
|
|
1122
|
+
}
|
|
1123
|
+
if (!linked) {
|
|
1124
|
+
const apps = await listApps();
|
|
1125
|
+
const slug = slugify(name);
|
|
1126
|
+
const similar = apps.filter(
|
|
1127
|
+
(a) => slugify(a.name) === slug || a.route === (options.route ?? slug)
|
|
1128
|
+
);
|
|
1129
|
+
if (similar.length > 0 && !c2.agent) {
|
|
1130
|
+
const choice = await select({
|
|
1131
|
+
message: `Found existing app${similar.length > 1 ? "s" : ""} with a similar name \u2014 link one or create new?`,
|
|
1132
|
+
options: [
|
|
1133
|
+
...similar.map((a) => ({
|
|
1134
|
+
value: a.id,
|
|
1135
|
+
label: `Link ${a.name} (${a.id}${a.route ? `, ${a.route}` : ""})`
|
|
1136
|
+
})),
|
|
1137
|
+
{ value: "__new__", label: `Create a new app called "${name}"` }
|
|
1138
|
+
]
|
|
1139
|
+
});
|
|
1140
|
+
if (isCancel(choice))
|
|
1141
|
+
return c2.error({
|
|
1142
|
+
code: "CANCELLED",
|
|
1143
|
+
message: "Cancelled.",
|
|
1144
|
+
exitCode: 130
|
|
1145
|
+
});
|
|
1146
|
+
if (choice !== "__new__") {
|
|
1147
|
+
linked = similar.find((a) => a.id === choice) ?? null;
|
|
1148
|
+
}
|
|
1149
|
+
} else if (similar.length > 0 && c2.agent) {
|
|
1150
|
+
return c2.error({
|
|
1151
|
+
code: "SIMILAR_APPS_FOUND",
|
|
1152
|
+
message: `Existing app(s) with a similar name: ${similar.map((a) => `${a.name} (${a.id})`).join(
|
|
1153
|
+
", "
|
|
1154
|
+
)}. Pass --link <app_id> to link one, or --name with a distinct name to create new.`,
|
|
1155
|
+
retryable: true
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
let route = options.route ?? slugify(name);
|
|
1160
|
+
if (!options.route && !linked && !c2.agent) {
|
|
1161
|
+
const value = await promptText(
|
|
1162
|
+
"Which route should it live at? (your-route.whop.app)",
|
|
1163
|
+
slugify(name),
|
|
1164
|
+
slugify(name)
|
|
1165
|
+
);
|
|
1166
|
+
if (value === null)
|
|
1167
|
+
return c2.error({
|
|
1168
|
+
code: "CANCELLED",
|
|
1169
|
+
message: "Cancelled.",
|
|
1170
|
+
exitCode: 130
|
|
1171
|
+
});
|
|
1172
|
+
route = slugify(value);
|
|
1173
|
+
}
|
|
1174
|
+
if (!route && !linked?.route) {
|
|
1175
|
+
return c2.error({
|
|
1176
|
+
code: "ROUTE_REQUIRED",
|
|
1177
|
+
message: `Couldn't derive a route from "${name}" \u2014 pass --route with letters and numbers (e.g. --route my-site).`,
|
|
1178
|
+
retryable: true
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
let whopApp;
|
|
1182
|
+
try {
|
|
1183
|
+
if (linked) {
|
|
1184
|
+
whopApp = linked;
|
|
1185
|
+
route = options.route ?? linked.route ?? slugify(name);
|
|
1186
|
+
if (!linked.route || options.route && options.route !== linked.route) {
|
|
1187
|
+
whopApp = await updateAppRoute(linked.id, route);
|
|
1188
|
+
}
|
|
1189
|
+
} else {
|
|
1190
|
+
whopApp = await createApp({
|
|
1191
|
+
name,
|
|
1192
|
+
route,
|
|
1193
|
+
...options.companyId ? { company_id: options.companyId } : {}
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
} catch (err) {
|
|
1197
|
+
return c2.error({
|
|
1198
|
+
code: "CREATE_FAILED",
|
|
1199
|
+
message: err instanceof Error ? err.message : "Failed to register the app",
|
|
1200
|
+
retryable: true
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
writeAppConfig(projectDir, { app_id: whopApp.id, name, route });
|
|
1204
|
+
const { manualSteps } = whopifyProject({ targetDir: projectDir, route });
|
|
1205
|
+
if (manualSteps.length > 0) {
|
|
1206
|
+
return c2.error({
|
|
1207
|
+
code: "WHOPIFY_INCOMPLETE",
|
|
1208
|
+
message: `${name} is registered on Whop (${whopApp.id}) and ${APP_CONFIG_FILENAME} is written, but the project isn't fully wired for hosting yet. Complete these steps:
|
|
1209
|
+
|
|
1210
|
+
${formatManualSteps(manualSteps)}
|
|
1211
|
+
|
|
1212
|
+
Then run \`whop apps deploy\`. Re-running \`whop apps create --existing\` re-checks the wiring and applies anything it can.`
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
const url = whopApp.hosted_url ?? void 0;
|
|
1216
|
+
if (!c2.agent && !c2.formatExplicit) {
|
|
1217
|
+
console.log(
|
|
1218
|
+
[
|
|
1219
|
+
"",
|
|
1220
|
+
chalk.green.bold(`\u2713 ${linked ? "Linked" : "Created"} ${name}`),
|
|
1221
|
+
` ${chalk.dim(whopApp.id)}${url ? ` \u2192 ${chalk.cyan.underline(url)}` : ""}`,
|
|
1222
|
+
"",
|
|
1223
|
+
`${chalk.bold("Next")} ${chalk.cyan.bold("whop apps deploy")}`,
|
|
1224
|
+
""
|
|
1225
|
+
].join("\n")
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
return c2.ok({ id: whopApp.id, name, route, url, directory: projectDir });
|
|
1229
|
+
}
|
|
1230
|
+
function buildAppGroup() {
|
|
1231
|
+
const app = Cli_exports.create("apps", {
|
|
1232
|
+
description: "Build and deploy fully-hosted web apps on Whop (*.whop.app)"
|
|
713
1233
|
});
|
|
714
|
-
|
|
715
|
-
description: "Create a
|
|
716
|
-
hint: "
|
|
717
|
-
|
|
718
|
-
|
|
1234
|
+
app.command("create", {
|
|
1235
|
+
description: "Create a Whop app: scaffold a new project (default) or wire up an existing one",
|
|
1236
|
+
hint: "Registers the app on Whop, writes whop.app.json, and wires the whop() deploy plugin into the vite config. By default it scaffolds the latest TanStack Start template into ./<route>. Pass --existing to register + whopify the Vite project in the current directory (or --dir) instead \u2014 safe to re-run, already-linked projects just get their wiring re-checked. --link reuses an existing app record in either mode.",
|
|
1237
|
+
args: external_exports.object({
|
|
1238
|
+
name: external_exports.string().optional().describe("App name (e.g. 'My Site'; when linking, defaults to the linked app's name)")
|
|
1239
|
+
}),
|
|
1240
|
+
options: external_exports.object({
|
|
1241
|
+
route: external_exports.string().optional().describe("Subdomain route to claim (e.g. my-site \u2192 my-site.whop.app)"),
|
|
1242
|
+
dir: external_exports.string().optional().describe(
|
|
1243
|
+
"Directory to scaffold into (defaults to ./<route>); with --existing, the project directory (defaults to the current directory)"
|
|
1244
|
+
),
|
|
1245
|
+
existing: external_exports.boolean().optional().describe(
|
|
1246
|
+
"Wire up the existing Vite project here instead of scaffolding a new one"
|
|
1247
|
+
),
|
|
1248
|
+
link: external_exports.string().optional().describe(
|
|
1249
|
+
"Reuse an existing app by id (app_xxx) instead of creating a new one \u2014 works when scaffolding and with --existing"
|
|
1250
|
+
),
|
|
1251
|
+
company_id: external_exports.string().optional().describe(
|
|
1252
|
+
"Business account to create the app under (defaults to your active account)"
|
|
1253
|
+
),
|
|
1254
|
+
install: external_exports.boolean().optional().describe(
|
|
1255
|
+
"Install dependencies after scaffolding fresh (default: true)"
|
|
1256
|
+
),
|
|
1257
|
+
pm: external_exports.enum(["npm", "pnpm", "bun", "yarn"]).optional().describe(
|
|
1258
|
+
"Package manager to use (default: first of bun, pnpm, npm found on your PATH)"
|
|
1259
|
+
)
|
|
1260
|
+
}),
|
|
1261
|
+
output: AppSchema,
|
|
719
1262
|
outputPolicy: "agent-only",
|
|
720
1263
|
examples: [
|
|
721
1264
|
{
|
|
722
|
-
|
|
723
|
-
|
|
1265
|
+
args: { name: "My Site" },
|
|
1266
|
+
options: { route: "my-site" },
|
|
1267
|
+
description: "Scaffold 'My Site' into ./my-site, live at my-site.whop.app"
|
|
724
1268
|
},
|
|
725
1269
|
{
|
|
726
|
-
|
|
727
|
-
|
|
1270
|
+
args: { name: "My Site" },
|
|
1271
|
+
options: { existing: true },
|
|
1272
|
+
description: "Register + wire up the Vite project in the current directory"
|
|
728
1273
|
}
|
|
729
1274
|
],
|
|
730
1275
|
run: async (c2) => {
|
|
731
|
-
|
|
732
|
-
if (
|
|
1276
|
+
const explicitDir = c2.options.dir ? resolve2(c2.options.dir) : void 0;
|
|
1277
|
+
if (c2.options.existing) {
|
|
1278
|
+
const linkedDir = explicitDir ?? findProjectDir();
|
|
1279
|
+
const projectDir = linkedDir && readAppConfig(linkedDir) ? linkedDir : explicitDir ?? process.cwd();
|
|
1280
|
+
return runRegisterFlow(c2, {
|
|
1281
|
+
dir: projectDir,
|
|
1282
|
+
name: c2.args.name,
|
|
1283
|
+
route: c2.options.route,
|
|
1284
|
+
link: c2.options.link,
|
|
1285
|
+
companyId: c2.options.company_id
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
if (!explicitDir && existsSync3(join3(process.cwd(), "package.json"))) {
|
|
1289
|
+
log(
|
|
1290
|
+
c2,
|
|
1291
|
+
chalk.dim(
|
|
1292
|
+
"Detected an existing project here \u2014 scaffolding a new app in a subdirectory. Pass --existing to wire up this project instead."
|
|
1293
|
+
)
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
let linked = null;
|
|
1297
|
+
if (c2.options.link) {
|
|
1298
|
+
const apps = await listApps();
|
|
1299
|
+
linked = apps.find((a) => a.id === c2.options.link) ?? null;
|
|
1300
|
+
if (!linked) {
|
|
1301
|
+
return c2.error({
|
|
1302
|
+
code: "APP_NOT_FOUND",
|
|
1303
|
+
message: `No app ${c2.options.link} found on your account.`
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
let name = c2.args.name ?? linked?.name;
|
|
1308
|
+
if (!name) {
|
|
733
1309
|
if (c2.agent) {
|
|
734
1310
|
return c2.error({
|
|
735
|
-
code: "
|
|
736
|
-
message:
|
|
1311
|
+
code: "NAME_REQUIRED",
|
|
1312
|
+
message: 'Pass a name: `whop apps create "My Site"`.',
|
|
737
1313
|
retryable: true
|
|
738
1314
|
});
|
|
739
1315
|
}
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
placeholder: "e.g. My Online Course, Trading Community, Fitness Program"
|
|
743
|
-
});
|
|
744
|
-
if (isCancel(t))
|
|
1316
|
+
const value = await promptText("What is your app called?", "My Site");
|
|
1317
|
+
if (value === null)
|
|
745
1318
|
return c2.error({
|
|
746
1319
|
code: "CANCELLED",
|
|
747
1320
|
message: "Cancelled.",
|
|
748
1321
|
exitCode: 130
|
|
749
1322
|
});
|
|
750
|
-
|
|
1323
|
+
name = value;
|
|
751
1324
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
body
|
|
1325
|
+
let route = c2.options.route ?? linked?.route ?? slugify(name);
|
|
1326
|
+
if (!c2.options.route && !linked?.route && !c2.agent) {
|
|
1327
|
+
const value = await promptText(
|
|
1328
|
+
"Which route should it live at? (your-route.whop.app)",
|
|
1329
|
+
slugify(name),
|
|
1330
|
+
slugify(name)
|
|
759
1331
|
);
|
|
760
|
-
|
|
1332
|
+
if (value === null)
|
|
1333
|
+
return c2.error({
|
|
1334
|
+
code: "CANCELLED",
|
|
1335
|
+
message: "Cancelled.",
|
|
1336
|
+
exitCode: 130
|
|
1337
|
+
});
|
|
1338
|
+
route = slugify(value);
|
|
1339
|
+
}
|
|
1340
|
+
if (!route) {
|
|
761
1341
|
return c2.error({
|
|
762
|
-
code: "
|
|
763
|
-
message:
|
|
1342
|
+
code: "ROUTE_REQUIRED",
|
|
1343
|
+
message: `Couldn't derive a route from "${name}" \u2014 pass --route with letters and numbers (e.g. --route my-site).`,
|
|
764
1344
|
retryable: true
|
|
765
1345
|
});
|
|
766
1346
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
1347
|
+
let pm = c2.options.pm;
|
|
1348
|
+
if (pm && !isPmInstalled(pm)) {
|
|
1349
|
+
return c2.error({
|
|
1350
|
+
code: "PM_NOT_INSTALLED",
|
|
1351
|
+
message: `${pm} is not installed (or not on your PATH). Install it: ${pmInstallHint(pm)}`
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
if (!pm) pm = detectPackageManager() ?? void 0;
|
|
1355
|
+
if (!pm) {
|
|
1356
|
+
if (c2.agent) {
|
|
1357
|
+
return c2.error({
|
|
1358
|
+
code: "NO_PACKAGE_MANAGER",
|
|
1359
|
+
message: `No package manager found (looked for bun, pnpm, npm). Install bun (recommended): ${pmInstallHint("bun")} \u2014 then re-run \`whop apps create\`.`
|
|
1360
|
+
});
|
|
773
1361
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
{
|
|
780
|
-
command: "plans create",
|
|
781
|
-
options: { product_id: product.id },
|
|
782
|
-
description: "Create a pricing plan (required before checkout)"
|
|
783
|
-
}
|
|
1362
|
+
const choice = await select({
|
|
1363
|
+
message: "No package manager found (bun, pnpm, or npm). Which one do you want to install?",
|
|
1364
|
+
options: [
|
|
1365
|
+
{ value: "bun", label: "bun", hint: "recommended" },
|
|
1366
|
+
{ value: "pnpm", label: "pnpm" },
|
|
1367
|
+
{ value: "npm", label: "npm", hint: "comes with Node.js" }
|
|
784
1368
|
]
|
|
1369
|
+
});
|
|
1370
|
+
if (isCancel(choice)) {
|
|
1371
|
+
return c2.error({
|
|
1372
|
+
code: "CANCELLED",
|
|
1373
|
+
message: "Cancelled.",
|
|
1374
|
+
exitCode: 130
|
|
1375
|
+
});
|
|
785
1376
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
registerListCommand(products, {
|
|
790
|
-
path: "/products",
|
|
791
|
-
description: "List products"
|
|
792
|
-
});
|
|
793
|
-
registerItemCommands(products, {
|
|
794
|
-
path: "/products",
|
|
795
|
-
noun: "product",
|
|
796
|
-
idLabel: "Product ID (prod_xxx)",
|
|
797
|
-
schema: ProductSchema,
|
|
798
|
-
updateOptions: createOptions.partial()
|
|
799
|
-
});
|
|
800
|
-
return products;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
// src/commerce/plans.ts
|
|
804
|
-
import { select, isCancel as isCancel2, spinner } from "@clack/prompts";
|
|
1377
|
+
return c2.error({
|
|
1378
|
+
code: "NO_PACKAGE_MANAGER",
|
|
1379
|
+
message: `Install ${String(choice)} with:
|
|
805
1380
|
|
|
806
|
-
|
|
807
|
-
var BILLING_PERIOD_DAYS = {
|
|
808
|
-
daily: 1,
|
|
809
|
-
weekly: 7,
|
|
810
|
-
monthly: 30,
|
|
811
|
-
quarterly: 90,
|
|
812
|
-
annually: 365,
|
|
813
|
-
every_two_years: 730
|
|
814
|
-
};
|
|
1381
|
+
${pmInstallHint(choice)}
|
|
815
1382
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
id: external_exports.string(),
|
|
819
|
-
title: external_exports.string().nullable().optional(),
|
|
820
|
-
plan_type: external_exports.string().optional(),
|
|
821
|
-
billing_period: external_exports.string().nullable().optional(),
|
|
822
|
-
initial_price: external_exports.number().nullable().optional(),
|
|
823
|
-
renewal_price: external_exports.number().nullable().optional(),
|
|
824
|
-
currency: external_exports.string().optional(),
|
|
825
|
-
purchase_url: external_exports.string().nullable().optional(),
|
|
826
|
-
visibility: external_exports.string().optional(),
|
|
827
|
-
created_at: external_exports.number().optional()
|
|
828
|
-
});
|
|
829
|
-
var createOptions2 = external_exports.object({
|
|
830
|
-
product_id: external_exports.string().optional().describe("Product this plan belongs to (prod_xxx)"),
|
|
831
|
-
title: external_exports.string().optional().describe("Display name for this plan"),
|
|
832
|
-
plan_type: external_exports.enum(["one_time", "renewal", "free"]).optional().describe("Billing model \u2014 one_time, renewal, or free"),
|
|
833
|
-
billing_period: external_exports.enum(["daily", "weekly", "monthly", "quarterly", "annually", "every_two_years"]).optional().describe("How often to charge (for renewal plans)"),
|
|
834
|
-
initial_price: external_exports.coerce.number().optional().describe("Price in the plan's currency (e.g. 9.99 for $9.99)"),
|
|
835
|
-
renewal_price: external_exports.coerce.number().optional().describe("Recurring price for renewal plans (defaults to initial_price)"),
|
|
836
|
-
currency: external_exports.string().optional().describe("3-letter currency code (default: USD)"),
|
|
837
|
-
trial_period_days: external_exports.coerce.number().optional().describe("Free trial length in days"),
|
|
838
|
-
visibility: external_exports.enum(["visible", "hidden", "archived"]).optional().describe("Who can see this plan")
|
|
839
|
-
});
|
|
840
|
-
function normalizePlanBody(body) {
|
|
841
|
-
if (typeof body.billing_period === "string") {
|
|
842
|
-
body.billing_period = BILLING_PERIOD_DAYS[body.billing_period];
|
|
843
|
-
}
|
|
844
|
-
if (body.plan_type === "renewal" && body.renewal_price == null && body.initial_price != null) {
|
|
845
|
-
body.renewal_price = body.initial_price;
|
|
846
|
-
}
|
|
847
|
-
return body;
|
|
848
|
-
}
|
|
849
|
-
async function resolveProductId(providedId, isAgent) {
|
|
850
|
-
if (providedId) return { ok: true, productId: providedId, autoResolved: false };
|
|
851
|
-
const profile = getActiveProfile();
|
|
852
|
-
const params = new URLSearchParams({ first: "100" });
|
|
853
|
-
if (profile?.companyId) params.set("company_id", profile.companyId);
|
|
854
|
-
let data;
|
|
855
|
-
try {
|
|
856
|
-
const res = await makeWhopRequest("GET", `/products?${params}`);
|
|
857
|
-
data = res.data ?? [];
|
|
858
|
-
} catch {
|
|
859
|
-
return { ok: false, code: "API_ERROR", message: "Failed to fetch products" };
|
|
860
|
-
}
|
|
861
|
-
if (data.length === 0) {
|
|
862
|
-
return {
|
|
863
|
-
ok: false,
|
|
864
|
-
code: "NO_PRODUCTS",
|
|
865
|
-
message: "No products found. Create one first."
|
|
866
|
-
};
|
|
867
|
-
}
|
|
868
|
-
if (data.length === 1) {
|
|
869
|
-
return { ok: true, productId: data[0].id, autoResolved: true };
|
|
870
|
-
}
|
|
871
|
-
if (isAgent) {
|
|
872
|
-
return { ok: false, code: "PRODUCT_REQUIRED", message: "Multiple products found. Pass --product_id.", candidates: data };
|
|
873
|
-
}
|
|
874
|
-
const chosen = await select({
|
|
875
|
-
message: "Which product is this plan for?",
|
|
876
|
-
options: data.map((p) => ({ value: p.id, label: p.title, hint: p.id }))
|
|
877
|
-
});
|
|
878
|
-
if (isCancel2(chosen)) return { ok: false, code: "CANCELLED", message: "Cancelled." };
|
|
879
|
-
return { ok: true, productId: chosen, autoResolved: false };
|
|
880
|
-
}
|
|
881
|
-
function buildPlansGroup() {
|
|
882
|
-
const plans = Cli_exports.create("plans", {
|
|
883
|
-
description: "Plans \u2014 pricing for your products"
|
|
884
|
-
});
|
|
885
|
-
plans.command("create", {
|
|
886
|
-
description: "Create a pricing plan",
|
|
887
|
-
hint: "A plan sets the price and billing interval. After creating a plan, your checkout link is ready to share.",
|
|
888
|
-
options: createOptions2,
|
|
889
|
-
output: PlanSchema,
|
|
890
|
-
outputPolicy: "agent-only",
|
|
891
|
-
examples: [
|
|
892
|
-
{
|
|
893
|
-
options: { product_id: "prod_xxx", initial_price: 9.99, plan_type: "one_time" },
|
|
894
|
-
description: "One-time payment of $9.99"
|
|
895
|
-
},
|
|
896
|
-
{
|
|
897
|
-
options: {
|
|
898
|
-
product_id: "prod_xxx",
|
|
899
|
-
initial_price: 19.99,
|
|
900
|
-
plan_type: "renewal",
|
|
901
|
-
billing_period: "monthly"
|
|
902
|
-
},
|
|
903
|
-
description: "Monthly subscription at $19.99"
|
|
1383
|
+
then re-run \`whop apps create\`.`
|
|
1384
|
+
});
|
|
904
1385
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
const s = c2.agent ? null : spinner();
|
|
908
|
-
if (s) s.start("Resolving product\u2026");
|
|
909
|
-
const resolved = await resolveProductId(c2.options.product_id, c2.agent);
|
|
910
|
-
if (s) s.stop("");
|
|
911
|
-
if (!resolved.ok) {
|
|
1386
|
+
const targetDir = resolve2(c2.options.dir ?? `./${route}`);
|
|
1387
|
+
if (existsSync3(targetDir) && readdirSync(targetDir).length > 0) {
|
|
912
1388
|
return c2.error({
|
|
913
|
-
code:
|
|
914
|
-
message:
|
|
915
|
-
retryable: resolved.code !== "CANCELLED",
|
|
916
|
-
cta: resolved.code === "NO_PRODUCTS" ? {
|
|
917
|
-
description: "Create a product first:",
|
|
918
|
-
commands: [{ command: "products create", description: "Create a product" }]
|
|
919
|
-
} : resolved.candidates ? {
|
|
920
|
-
description: "Available products:",
|
|
921
|
-
commands: resolved.candidates.map((p) => ({
|
|
922
|
-
command: "plans create",
|
|
923
|
-
options: { product_id: p.id },
|
|
924
|
-
description: p.title
|
|
925
|
-
}))
|
|
926
|
-
} : void 0
|
|
1389
|
+
code: "TARGET_NOT_EMPTY",
|
|
1390
|
+
message: `Target directory is not empty: ${targetDir}. Choose an empty directory with --dir, or a different route.`
|
|
927
1391
|
});
|
|
928
1392
|
}
|
|
929
|
-
|
|
930
|
-
console.error(
|
|
931
|
-
`Note: using product ${resolved.productId} \u2014 pass --product_id to override`
|
|
932
|
-
);
|
|
933
|
-
}
|
|
934
|
-
const body = normalizePlanBody({
|
|
935
|
-
...buildBody(c2.options),
|
|
936
|
-
product_id: resolved.productId
|
|
937
|
-
});
|
|
938
|
-
let plan;
|
|
1393
|
+
let whopApp;
|
|
939
1394
|
try {
|
|
940
|
-
|
|
1395
|
+
if (linked) {
|
|
1396
|
+
whopApp = linked;
|
|
1397
|
+
if (!linked.route || c2.options.route && c2.options.route !== linked.route) {
|
|
1398
|
+
whopApp = await updateAppRoute(linked.id, route);
|
|
1399
|
+
}
|
|
1400
|
+
} else {
|
|
1401
|
+
whopApp = await createApp({
|
|
1402
|
+
name,
|
|
1403
|
+
route,
|
|
1404
|
+
...c2.options.company_id ? { company_id: c2.options.company_id } : {}
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
941
1407
|
} catch (err) {
|
|
942
1408
|
return c2.error({
|
|
943
|
-
code: "
|
|
944
|
-
message: err instanceof Error ? err.message : "Failed to create
|
|
1409
|
+
code: "CREATE_FAILED",
|
|
1410
|
+
message: err instanceof Error ? err.message : "Failed to create the app on Whop",
|
|
945
1411
|
retryable: true
|
|
946
1412
|
});
|
|
947
1413
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1414
|
+
let whopifySteps = [];
|
|
1415
|
+
try {
|
|
1416
|
+
log(
|
|
1417
|
+
c2,
|
|
1418
|
+
chalk.dim("\nScaffolding with the latest TanStack Start template...")
|
|
1419
|
+
);
|
|
1420
|
+
runTanstackCreate({
|
|
1421
|
+
targetDir,
|
|
1422
|
+
projectName: route,
|
|
1423
|
+
pm,
|
|
1424
|
+
quiet: Boolean(c2.agent)
|
|
1425
|
+
});
|
|
1426
|
+
whopifySteps = whopifyProject({ targetDir, route }).manualSteps;
|
|
1427
|
+
} catch (err) {
|
|
1428
|
+
return c2.error({
|
|
1429
|
+
code: "SCAFFOLD_FAILED",
|
|
1430
|
+
message: err instanceof Error ? err.message : "Failed to scaffold the project"
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
writeAppConfig(targetDir, { app_id: whopApp.id, name, route });
|
|
1434
|
+
if (whopifySteps.length > 0) {
|
|
1435
|
+
return c2.error({
|
|
1436
|
+
code: "WHOPIFY_INCOMPLETE",
|
|
1437
|
+
message: `Scaffolded ${targetDir} and registered ${whopApp.id}, but the project needs manual wiring:
|
|
1438
|
+
|
|
1439
|
+
${formatManualSteps(whopifySteps)}
|
|
1440
|
+
|
|
1441
|
+
Then run \`whop apps deploy\`. Re-running \`whop apps create --existing\` in the project re-checks the wiring.`
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
let installFailed = false;
|
|
1445
|
+
if (c2.options.install !== false) {
|
|
1446
|
+
log(c2, chalk.dim(`
|
|
1447
|
+
Installing dependencies with ${pm}...`));
|
|
1448
|
+
const result = spawnSync2(pm, ["install"], {
|
|
1449
|
+
cwd: targetDir,
|
|
1450
|
+
stdio: c2.agent ? "ignore" : "inherit",
|
|
1451
|
+
shell: SPAWN_VIA_SHELL2
|
|
1452
|
+
});
|
|
1453
|
+
installFailed = Boolean(result.error) || result.status !== 0;
|
|
1454
|
+
if (installFailed) {
|
|
1455
|
+
log(
|
|
1456
|
+
c2,
|
|
1457
|
+
chalk.yellow(
|
|
1458
|
+
`
|
|
1459
|
+
${pm} install failed \u2014 run it manually inside ${targetDir}.`
|
|
1460
|
+
)
|
|
1461
|
+
);
|
|
970
1462
|
}
|
|
1463
|
+
}
|
|
1464
|
+
const url = whopApp.hosted_url ?? void 0;
|
|
1465
|
+
if (!c2.agent && !c2.formatExplicit) {
|
|
1466
|
+
console.log(
|
|
1467
|
+
[
|
|
1468
|
+
"",
|
|
1469
|
+
chalk.green.bold(`\u2713 ${name} is ready`),
|
|
1470
|
+
` ${chalk.dim(whopApp.id)}`,
|
|
1471
|
+
"",
|
|
1472
|
+
` ${chalk.bold("Directory")} ${targetDir}`,
|
|
1473
|
+
...url ? [
|
|
1474
|
+
` ${chalk.bold("URL")} ${chalk.cyan.underline(url)} ${chalk.dim("(after first deploy)")}`
|
|
1475
|
+
] : [],
|
|
1476
|
+
"",
|
|
1477
|
+
`${chalk.bold("Next")}`,
|
|
1478
|
+
` cd ${targetDir}`,
|
|
1479
|
+
` ${chalk.cyan.bold("whop apps dev")} ${chalk.dim("\u2192 local dev server")}`,
|
|
1480
|
+
` ${chalk.cyan.bold("whop apps deploy")} ${chalk.dim("\u2192 ship it live")}`,
|
|
1481
|
+
""
|
|
1482
|
+
].join("\n")
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
return c2.ok({
|
|
1486
|
+
id: whopApp.id,
|
|
1487
|
+
name,
|
|
1488
|
+
route,
|
|
1489
|
+
url,
|
|
1490
|
+
directory: targetDir,
|
|
1491
|
+
install_failed: installFailed
|
|
971
1492
|
});
|
|
972
1493
|
}
|
|
973
1494
|
});
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1495
|
+
app.command("list", {
|
|
1496
|
+
description: "List the apps on your business account",
|
|
1497
|
+
output: external_exports.array(AppSchema),
|
|
1498
|
+
outputPolicy: "agent-only",
|
|
1499
|
+
run: async (c2) => {
|
|
1500
|
+
const apps = await listApps();
|
|
1501
|
+
return c2.ok(
|
|
1502
|
+
apps.map((a) => ({
|
|
1503
|
+
id: a.id,
|
|
1504
|
+
name: a.name,
|
|
1505
|
+
route: a.route,
|
|
1506
|
+
url: a.hosted_url ?? void 0
|
|
1507
|
+
}))
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
986
1510
|
});
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1511
|
+
app.command("dev", {
|
|
1512
|
+
description: "Run the local dev server for this app",
|
|
1513
|
+
hint: "Starts the project's dev script with WHOP_APP_ID set and a short-lived access token injected as WHOP_API_KEY (minted from your CLI credential), so server-side SDK calls work locally without env setup. An explicitly exported WHOP_API_KEY is used as-is.",
|
|
1514
|
+
options: external_exports.object({
|
|
1515
|
+
dir: external_exports.string().optional().describe("Project directory (defaults to the current directory)")
|
|
1516
|
+
}),
|
|
1517
|
+
run: async (c2) => {
|
|
1518
|
+
const project = loadProject(c2.options.dir);
|
|
1519
|
+
if (!project.ok) return c2.error(project.error);
|
|
1520
|
+
const injected = {
|
|
1521
|
+
WHOP_APP_ID: project.config.app_id
|
|
1522
|
+
};
|
|
1523
|
+
if (process.env.WHOP_API_KEY) {
|
|
1524
|
+
log(
|
|
1525
|
+
c2,
|
|
1526
|
+
chalk.dim(
|
|
1527
|
+
"Using the WHOP_API_KEY from your environment for server-side calls."
|
|
1528
|
+
)
|
|
1529
|
+
);
|
|
1530
|
+
} else {
|
|
1531
|
+
try {
|
|
1532
|
+
const minted = await createDevAccessToken();
|
|
1533
|
+
injected.WHOP_API_KEY = minted.token;
|
|
1534
|
+
const expiresAt = typeof minted.expires_at === "number" ? new Date(minted.expires_at * 1e3) : minted.expires_at ? new Date(minted.expires_at) : null;
|
|
1535
|
+
const expiry = expiresAt ? ` (expires ${expiresAt.toLocaleTimeString()})` : "";
|
|
1536
|
+
log(
|
|
1537
|
+
c2,
|
|
1538
|
+
chalk.dim(
|
|
1539
|
+
`Injected a temporary access token as WHOP_API_KEY${expiry} \u2014 server-side SDK calls just work. Restart \`whop apps dev\` to refresh it.`
|
|
1540
|
+
)
|
|
1541
|
+
);
|
|
1542
|
+
} catch (err) {
|
|
1543
|
+
log(
|
|
1544
|
+
c2,
|
|
1545
|
+
chalk.yellow(
|
|
1546
|
+
`Couldn't mint a temporary access token (${err instanceof Error ? err.message : "unknown error"}) \u2014 set WHOP_API_KEY manually for server-side calls.`
|
|
1547
|
+
)
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
log(c2, chalk.dim(`Starting dev server for ${project.config.name}...`));
|
|
1552
|
+
try {
|
|
1553
|
+
runScript(project.projectDir, "dev", injected);
|
|
1554
|
+
} catch (err) {
|
|
1555
|
+
return c2.error({
|
|
1556
|
+
code: "DEV_FAILED",
|
|
1557
|
+
message: err instanceof Error ? err.message : "Dev server exited with an error"
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
return c2.ok({ success: true });
|
|
1561
|
+
}
|
|
1010
1562
|
});
|
|
1011
|
-
|
|
1012
|
-
description: "
|
|
1013
|
-
hint: "
|
|
1014
|
-
options:
|
|
1015
|
-
|
|
1563
|
+
app.command("deploy", {
|
|
1564
|
+
description: "Build, upload and ship this app live",
|
|
1565
|
+
hint: "Typechecks, builds with Vite, uploads the build to Whop, and promotes it to production. Use --no-promote to upload a preview-only build.",
|
|
1566
|
+
options: external_exports.object({
|
|
1567
|
+
dir: external_exports.string().optional().describe("Project directory (defaults to the current directory)"),
|
|
1568
|
+
promote: external_exports.boolean().optional().describe(
|
|
1569
|
+
"Promote this build to production after upload (default: true)"
|
|
1570
|
+
),
|
|
1571
|
+
skip_typecheck: external_exports.boolean().optional().describe("Skip the typecheck step"),
|
|
1572
|
+
skip_build: external_exports.boolean().optional().describe("Skip the build step and upload the existing dist/ output")
|
|
1573
|
+
}),
|
|
1574
|
+
output: BuildSchema,
|
|
1016
1575
|
outputPolicy: "agent-only",
|
|
1017
1576
|
examples: [
|
|
1577
|
+
{ description: "Build and ship to production" },
|
|
1018
1578
|
{
|
|
1019
|
-
options: {
|
|
1020
|
-
description: "
|
|
1021
|
-
},
|
|
1022
|
-
{
|
|
1023
|
-
options: { plan_id: "plan_xxx", redirect_url: "https://mysite.com/thank-you" },
|
|
1024
|
-
description: "Redirect buyers to a custom thank-you page"
|
|
1579
|
+
options: { promote: false },
|
|
1580
|
+
description: "Upload a preview build without promoting"
|
|
1025
1581
|
}
|
|
1026
1582
|
],
|
|
1027
1583
|
run: async (c2) => {
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1584
|
+
const project = loadProject(c2.options.dir);
|
|
1585
|
+
if (!project.ok) return c2.error(project.error);
|
|
1586
|
+
const { projectDir, config } = project;
|
|
1030
1587
|
try {
|
|
1031
|
-
|
|
1032
|
-
"
|
|
1033
|
-
"
|
|
1034
|
-
|
|
1588
|
+
if (!c2.options.skip_build) {
|
|
1589
|
+
log(c2, chalk.dim("[1/3] Building..."));
|
|
1590
|
+
runScript(projectDir, "build", { WHOP_APP_ID: config.app_id });
|
|
1591
|
+
if (!c2.options.skip_typecheck && hasScript(projectDir, "typecheck")) {
|
|
1592
|
+
log(c2, chalk.dim("[2/3] Typechecking..."));
|
|
1593
|
+
runScript(projectDir, "typecheck");
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
const archivePath = join3(projectDir, BUILD_ARCHIVE);
|
|
1597
|
+
if (!existsSync3(archivePath)) {
|
|
1598
|
+
return c2.error({
|
|
1599
|
+
code: "NO_BUILD_ARCHIVE",
|
|
1600
|
+
message: `No build archive at ${BUILD_ARCHIVE}. Add the whop() plugin from @whop/cli/vite to your vite config, then build. See https://whop.com/docs/apps/hosting.`
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
log(c2, chalk.dim("[3/3] Uploading + creating build..."));
|
|
1604
|
+
const zip = readFileSync3(archivePath);
|
|
1605
|
+
const checksum = createHash("sha256").update(zip).digest("hex");
|
|
1606
|
+
const fileId = await uploadBuildArchive(
|
|
1607
|
+
new Uint8Array(zip),
|
|
1608
|
+
`${config.route}-build.zip`
|
|
1035
1609
|
);
|
|
1610
|
+
await waitForFileReady(fileId);
|
|
1611
|
+
let build = await createAppBuild({
|
|
1612
|
+
app_id: config.app_id,
|
|
1613
|
+
checksum,
|
|
1614
|
+
file_id: fileId
|
|
1615
|
+
});
|
|
1616
|
+
if (c2.options.promote !== false) {
|
|
1617
|
+
await promoteAppBuild(build.id);
|
|
1618
|
+
build = await waitForBuildPromotion(build.id);
|
|
1619
|
+
}
|
|
1620
|
+
const promoted = build.is_production === true;
|
|
1621
|
+
const productionUrl = promoted ? (await getApp(config.app_id)).hosted_url ?? void 0 : void 0;
|
|
1622
|
+
if (!c2.agent && !c2.formatExplicit) {
|
|
1623
|
+
console.log(
|
|
1624
|
+
[
|
|
1625
|
+
"",
|
|
1626
|
+
chalk.green.bold(
|
|
1627
|
+
promoted ? "\u2713 Deployed to production" : "\u2713 Build uploaded"
|
|
1628
|
+
),
|
|
1629
|
+
` ${chalk.dim(build.id)}`,
|
|
1630
|
+
"",
|
|
1631
|
+
...promoted && productionUrl ? [
|
|
1632
|
+
` ${chalk.bold("Live")} ${chalk.cyan.underline(productionUrl)}`
|
|
1633
|
+
] : [
|
|
1634
|
+
`${chalk.bold("Promote when ready")} ${chalk.cyan.bold(`whop apps promote ${build.id}`)}`
|
|
1635
|
+
],
|
|
1636
|
+
""
|
|
1637
|
+
].join("\n")
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
return c2.ok({
|
|
1641
|
+
id: build.id,
|
|
1642
|
+
status: build.status,
|
|
1643
|
+
is_production: promoted,
|
|
1644
|
+
url: productionUrl
|
|
1645
|
+
});
|
|
1036
1646
|
} catch (err) {
|
|
1037
1647
|
return c2.error({
|
|
1038
|
-
code: "
|
|
1039
|
-
message: err instanceof Error ? err.message : "
|
|
1648
|
+
code: "DEPLOY_FAILED",
|
|
1649
|
+
message: err instanceof Error ? err.message : "Deploy failed",
|
|
1040
1650
|
retryable: true
|
|
1041
1651
|
});
|
|
1042
1652
|
}
|
|
1043
|
-
const purchaseUrl = config.purchase_url;
|
|
1044
|
-
printCreateSuccess(c2, {
|
|
1045
|
-
title: "Checkout configured",
|
|
1046
|
-
id: config.id,
|
|
1047
|
-
checkoutUrl: purchaseUrl,
|
|
1048
|
-
next: { command: "quickstart", hint: "See your full setup status" }
|
|
1049
|
-
});
|
|
1050
|
-
return c2.ok(config, {
|
|
1051
|
-
cta: {
|
|
1052
|
-
description: purchaseUrl ? `Checkout configured. Share your link: ${purchaseUrl}` : "Checkout configured.",
|
|
1053
|
-
commands: [
|
|
1054
|
-
{ command: "quickstart", description: "See your full setup status" }
|
|
1055
|
-
]
|
|
1056
|
-
}
|
|
1057
|
-
});
|
|
1058
1653
|
}
|
|
1059
1654
|
});
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1655
|
+
app.command("promote", {
|
|
1656
|
+
description: "Promote a previously uploaded build to production",
|
|
1657
|
+
args: external_exports.object({
|
|
1658
|
+
build_id: external_exports.string().describe("The build to promote (apbu_xxx)")
|
|
1659
|
+
}),
|
|
1660
|
+
options: external_exports.object({
|
|
1661
|
+
dir: external_exports.string().optional().describe("Project directory (defaults to the current directory)")
|
|
1662
|
+
}),
|
|
1663
|
+
output: BuildSchema,
|
|
1664
|
+
outputPolicy: "agent-only",
|
|
1665
|
+
run: async (c2) => {
|
|
1666
|
+
const project = loadProject(c2.options.dir);
|
|
1667
|
+
if (!project.ok) return c2.error(project.error);
|
|
1668
|
+
try {
|
|
1669
|
+
await promoteAppBuild(c2.args.build_id);
|
|
1670
|
+
const build = await waitForBuildPromotion(c2.args.build_id);
|
|
1671
|
+
if (!build.is_production) {
|
|
1672
|
+
return c2.error({
|
|
1673
|
+
code: "PROMOTE_PENDING",
|
|
1674
|
+
message: `Build ${build.id} was accepted but is still processing (status: ${build.status}). It will go live once approved \u2014 re-run \`whop apps promote ${build.id}\` to confirm.`,
|
|
1675
|
+
retryable: true
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
const url = (await getApp(project.config.app_id)).hosted_url ?? void 0;
|
|
1679
|
+
if (!c2.agent && !c2.formatExplicit) {
|
|
1680
|
+
console.log(
|
|
1681
|
+
[
|
|
1682
|
+
"",
|
|
1683
|
+
chalk.green.bold("\u2713 Promoted to production"),
|
|
1684
|
+
` ${chalk.dim(build.id)}`,
|
|
1685
|
+
...url ? [` ${chalk.bold("Live")} ${chalk.cyan.underline(url)}`] : [],
|
|
1686
|
+
""
|
|
1687
|
+
].join("\n")
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
return c2.ok({
|
|
1691
|
+
id: build.id,
|
|
1692
|
+
status: build.status,
|
|
1693
|
+
is_production: build.is_production,
|
|
1694
|
+
url
|
|
1695
|
+
});
|
|
1696
|
+
} catch (err) {
|
|
1697
|
+
return c2.error({
|
|
1698
|
+
code: "PROMOTE_FAILED",
|
|
1699
|
+
message: err instanceof Error ? err.message : "Promote failed",
|
|
1700
|
+
retryable: true
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1064
1704
|
});
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1705
|
+
app.command("builds", {
|
|
1706
|
+
description: "List builds for this app",
|
|
1707
|
+
options: external_exports.object({
|
|
1708
|
+
dir: external_exports.string().optional().describe("Project directory (defaults to the current directory)")
|
|
1709
|
+
}),
|
|
1710
|
+
output: external_exports.object({ builds: external_exports.array(BuildSchema) }),
|
|
1711
|
+
outputPolicy: "agent-only",
|
|
1712
|
+
run: async (c2) => {
|
|
1713
|
+
const project = loadProject(c2.options.dir);
|
|
1714
|
+
if (!project.ok) return c2.error(project.error);
|
|
1715
|
+
try {
|
|
1716
|
+
const builds = await listAppBuilds(project.config.app_id);
|
|
1717
|
+
const rows = builds.map((build) => ({
|
|
1718
|
+
id: build.id,
|
|
1719
|
+
status: build.status,
|
|
1720
|
+
is_production: build.is_production
|
|
1721
|
+
}));
|
|
1722
|
+
if (!c2.agent && !c2.formatExplicit) {
|
|
1723
|
+
console.log("");
|
|
1724
|
+
for (const row of rows) {
|
|
1725
|
+
const marker = row.is_production ? chalk.green("\u25CF production") : chalk.dim(`\u25CB ${row.status}`);
|
|
1726
|
+
console.log(` ${row.id} ${marker}`);
|
|
1727
|
+
}
|
|
1728
|
+
console.log("");
|
|
1729
|
+
}
|
|
1730
|
+
return c2.ok({ builds: rows });
|
|
1731
|
+
} catch (err) {
|
|
1732
|
+
return c2.error({
|
|
1733
|
+
code: "LIST_FAILED",
|
|
1734
|
+
message: err instanceof Error ? err.message : "Failed to list builds",
|
|
1735
|
+
retryable: true
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1070
1739
|
});
|
|
1071
|
-
return
|
|
1740
|
+
return app;
|
|
1072
1741
|
}
|
|
1073
1742
|
|
|
1074
1743
|
// src/journey/quickstart.ts
|
|
1075
1744
|
import chalk3 from "chalk";
|
|
1076
|
-
import { text as text2, select as select2, isCancel as
|
|
1745
|
+
import { text as text2, select as select2, isCancel as isCancel2 } from "@clack/prompts";
|
|
1746
|
+
|
|
1747
|
+
// src/journey/billing.ts
|
|
1748
|
+
var BILLING_PERIOD_DAYS = {
|
|
1749
|
+
daily: 1,
|
|
1750
|
+
weekly: 7,
|
|
1751
|
+
monthly: 30,
|
|
1752
|
+
quarterly: 90,
|
|
1753
|
+
annually: 365,
|
|
1754
|
+
every_two_years: 730
|
|
1755
|
+
};
|
|
1077
1756
|
|
|
1078
1757
|
// src/lib/colors.ts
|
|
1079
1758
|
import chalk2 from "chalk";
|
|
@@ -1121,7 +1800,10 @@ var quickstartOptions = external_exports.object({
|
|
|
1121
1800
|
"every_two_years",
|
|
1122
1801
|
"one_time"
|
|
1123
1802
|
]).optional().describe("Billing interval \u2014 use 'one_time' for a single payment"),
|
|
1124
|
-
|
|
1803
|
+
business_name: external_exports.string().optional().describe(
|
|
1804
|
+
"Name for a new business account \u2014 quickstart creates it and sets up your product under it (user logins only; ignored with API-key auth, which already belongs to a business)"
|
|
1805
|
+
),
|
|
1806
|
+
account_id: external_exports.string().optional().describe(
|
|
1125
1807
|
"Business account to act on behalf of (biz_xxx); defaults to your active account"
|
|
1126
1808
|
)
|
|
1127
1809
|
});
|
|
@@ -1131,7 +1813,11 @@ var quickstartOutput = external_exports.object({
|
|
|
1131
1813
|
checkoutUrl: external_exports.string().nullable().describe(
|
|
1132
1814
|
"The live checkout link to share \u2014 the journey's payoff, present once a plan exists"
|
|
1133
1815
|
),
|
|
1134
|
-
created: external_exports.object({
|
|
1816
|
+
created: external_exports.object({
|
|
1817
|
+
accountId: external_exports.string().optional(),
|
|
1818
|
+
productId: external_exports.string().optional(),
|
|
1819
|
+
planId: external_exports.string().optional()
|
|
1820
|
+
}).nullable().describe(
|
|
1135
1821
|
"What this run created, if anything \u2014 null for a read-only status check"
|
|
1136
1822
|
),
|
|
1137
1823
|
steps: external_exports.array(StepSchema).describe("The path to your first sale"),
|
|
@@ -1170,36 +1856,87 @@ ${chalk3.yellow("!")} Agent setup skipped: ${message}`);
|
|
|
1170
1856
|
);
|
|
1171
1857
|
}
|
|
1172
1858
|
}
|
|
1173
|
-
const
|
|
1859
|
+
const accountId = c2.options.account_id ?? await resolveActiveAccountId() ?? "";
|
|
1174
1860
|
const accountName = profile?.accountTitle ?? profile?.accountRoute ?? profile?.name ?? "your account";
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1861
|
+
const interactive = !c2.agent && !c2.formatExplicit;
|
|
1862
|
+
const canCreateBusiness = profile?.method === "oauth";
|
|
1863
|
+
let newBusinessName = canCreateBusiness ? c2.options.business_name : void 0;
|
|
1864
|
+
if (!canCreateBusiness && c2.options.business_name !== void 0 && interactive) {
|
|
1865
|
+
console.log(
|
|
1866
|
+
`
|
|
1867
|
+
${chalk3.dim("Ignoring --business_name \u2014 this API key already belongs to a business.")}`
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
if (!accountId && newBusinessName === void 0) {
|
|
1871
|
+
if (!canCreateBusiness) {
|
|
1872
|
+
return c2.error({
|
|
1873
|
+
code: "NO_ACCOUNT",
|
|
1874
|
+
message: "Couldn't resolve the business account for this API key. Check the key, or log in again.",
|
|
1875
|
+
retryable: true,
|
|
1876
|
+
cta: {
|
|
1877
|
+
commands: [{ command: "auth login", description: "Log in to Whop" }]
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
if (!interactive) {
|
|
1882
|
+
return c2.error({
|
|
1883
|
+
code: "NO_ACCOUNT",
|
|
1884
|
+
message: "No business account is linked to this login. Pass --business_name to create one and continue.",
|
|
1885
|
+
retryable: true,
|
|
1886
|
+
cta: {
|
|
1887
|
+
description: "Create your business account and product in one run:",
|
|
1888
|
+
commands: [
|
|
1889
|
+
{
|
|
1890
|
+
command: "quickstart",
|
|
1891
|
+
options: { business_name: "My Business" },
|
|
1892
|
+
description: "Create a business account, then your product"
|
|
1893
|
+
}
|
|
1894
|
+
]
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
console.log(
|
|
1899
|
+
`
|
|
1900
|
+
${chalk3.bold("You don't have a business account yet \u2014 let's create one.")}`
|
|
1901
|
+
);
|
|
1902
|
+
const name = await text2({
|
|
1903
|
+
message: "What's your business called?",
|
|
1904
|
+
placeholder: "My Business"
|
|
1189
1905
|
});
|
|
1906
|
+
if (isCancel2(name)) {
|
|
1907
|
+
return c2.error({
|
|
1908
|
+
code: "CANCELLED",
|
|
1909
|
+
message: "Setup cancelled.",
|
|
1910
|
+
exitCode: 130
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
newBusinessName = name;
|
|
1914
|
+
}
|
|
1915
|
+
if (newBusinessName !== void 0) {
|
|
1916
|
+
try {
|
|
1917
|
+
return await runCreate(c2, {
|
|
1918
|
+
accountId: "",
|
|
1919
|
+
accountName,
|
|
1920
|
+
products: [],
|
|
1921
|
+
newBusinessName
|
|
1922
|
+
});
|
|
1923
|
+
} catch (err) {
|
|
1924
|
+
if (err instanceof QuickstartError) return c2.error(err.envelope);
|
|
1925
|
+
throw err;
|
|
1926
|
+
}
|
|
1190
1927
|
}
|
|
1191
1928
|
const [productsRes, plansRes, checkoutRes] = await Promise.allSettled([
|
|
1192
1929
|
makeWhopRequest(
|
|
1193
1930
|
"GET",
|
|
1194
|
-
`/products?company_id=${
|
|
1931
|
+
`/products?company_id=${accountId}&first=100`
|
|
1195
1932
|
),
|
|
1196
1933
|
makeWhopRequest(
|
|
1197
1934
|
"GET",
|
|
1198
|
-
`/plans?account_id=${
|
|
1935
|
+
`/plans?account_id=${accountId}&first=100`
|
|
1199
1936
|
),
|
|
1200
1937
|
makeWhopRequest(
|
|
1201
1938
|
"GET",
|
|
1202
|
-
`/checkout_configurations?company_id=${
|
|
1939
|
+
`/checkout_configurations?company_id=${accountId}&first=100`
|
|
1203
1940
|
)
|
|
1204
1941
|
]);
|
|
1205
1942
|
const critical = [productsRes, plansRes].find((r) => r.status === "rejected");
|
|
@@ -1216,11 +1953,17 @@ ${chalk3.yellow("!")} Agent setup skipped: ${message}`);
|
|
|
1216
1953
|
const checkouts = checkoutRes.status === "fulfilled" ? checkoutRes.value.data ?? [] : [];
|
|
1217
1954
|
const ready = products.length > 0 && plans.length > 0;
|
|
1218
1955
|
const hasCreationFlags = c2.options.title !== void 0 || c2.options.price !== void 0 || c2.options.billing_period !== void 0;
|
|
1219
|
-
const
|
|
1220
|
-
const shouldCreate = hasCreationFlags || !ready && canMutateInteractively;
|
|
1956
|
+
const shouldCreate = hasCreationFlags || !ready && interactive;
|
|
1221
1957
|
if (shouldCreate) {
|
|
1222
1958
|
try {
|
|
1223
|
-
return await runCreate(c2, {
|
|
1959
|
+
return await runCreate(c2, {
|
|
1960
|
+
accountId,
|
|
1961
|
+
accountName,
|
|
1962
|
+
products,
|
|
1963
|
+
// Only offer the new-business choice in the fully-guided flow —
|
|
1964
|
+
// flags mean the user already said what they want.
|
|
1965
|
+
offerNewBusiness: interactive && !hasCreationFlags && c2.options.account_id === void 0 && canCreateBusiness
|
|
1966
|
+
});
|
|
1224
1967
|
} catch (err) {
|
|
1225
1968
|
if (err instanceof QuickstartError) return c2.error(err.envelope);
|
|
1226
1969
|
throw err;
|
|
@@ -1251,7 +1994,7 @@ async function resolveOrPrompt(c2, provided, missing, prompt) {
|
|
|
1251
1994
|
});
|
|
1252
1995
|
}
|
|
1253
1996
|
const value = await prompt();
|
|
1254
|
-
if (
|
|
1997
|
+
if (isCancel2(value))
|
|
1255
1998
|
throw new QuickstartError({
|
|
1256
1999
|
code: "CANCELLED",
|
|
1257
2000
|
message: "Setup cancelled.",
|
|
@@ -1266,7 +2009,39 @@ async function runCreate(c2, state) {
|
|
|
1266
2009
|
${chalk3.bold("Let's get you set up to sell.")}
|
|
1267
2010
|
`);
|
|
1268
2011
|
}
|
|
1269
|
-
|
|
2012
|
+
let { accountId, accountName, products, newBusinessName } = state;
|
|
2013
|
+
if (state.offerNewBusiness) {
|
|
2014
|
+
const choice = await select2({
|
|
2015
|
+
message: "Which business account is this for?",
|
|
2016
|
+
options: [
|
|
2017
|
+
{ value: "current", label: accountName, hint: accountId },
|
|
2018
|
+
{ value: "new", label: "Create a new business account" }
|
|
2019
|
+
]
|
|
2020
|
+
});
|
|
2021
|
+
if (isCancel2(choice)) {
|
|
2022
|
+
throw new QuickstartError({
|
|
2023
|
+
code: "CANCELLED",
|
|
2024
|
+
message: "Setup cancelled.",
|
|
2025
|
+
exitCode: 130
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
if (choice === "new") {
|
|
2029
|
+
const name = await text2({
|
|
2030
|
+
message: "What's your new business called?",
|
|
2031
|
+
placeholder: "My Business"
|
|
2032
|
+
});
|
|
2033
|
+
if (isCancel2(name)) {
|
|
2034
|
+
throw new QuickstartError({
|
|
2035
|
+
code: "CANCELLED",
|
|
2036
|
+
message: "Setup cancelled.",
|
|
2037
|
+
exitCode: 130
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
newBusinessName = name;
|
|
2041
|
+
products = [];
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
const needsNewProduct = products.length === 0 || c2.options.title !== void 0;
|
|
1270
2045
|
const newTitle = needsNewProduct ? await resolveOrPrompt(
|
|
1271
2046
|
c2,
|
|
1272
2047
|
c2.options.title,
|
|
@@ -1280,7 +2055,7 @@ ${chalk3.bold("Let's get you set up to sell.")}
|
|
|
1280
2055
|
placeholder: "My Online Course"
|
|
1281
2056
|
})
|
|
1282
2057
|
) : void 0;
|
|
1283
|
-
const existing = needsNewProduct ? void 0 : await resolveExistingProduct(c2,
|
|
2058
|
+
const existing = needsNewProduct ? void 0 : await resolveExistingProduct(c2, products);
|
|
1284
2059
|
const price = await resolveOrPrompt(
|
|
1285
2060
|
c2,
|
|
1286
2061
|
c2.options.price,
|
|
@@ -1295,7 +2070,7 @@ ${chalk3.bold("Let's get you set up to sell.")}
|
|
|
1295
2070
|
placeholder: "9.99",
|
|
1296
2071
|
validate: (v) => isNaN(Number(v)) ? "Enter a number" : void 0
|
|
1297
2072
|
});
|
|
1298
|
-
return
|
|
2073
|
+
return isCancel2(p) ? p : Number(p);
|
|
1299
2074
|
}
|
|
1300
2075
|
);
|
|
1301
2076
|
const billingPeriod = await resolveOrPrompt(
|
|
@@ -1316,6 +2091,33 @@ ${chalk3.bold("Let's get you set up to sell.")}
|
|
|
1316
2091
|
]
|
|
1317
2092
|
})
|
|
1318
2093
|
);
|
|
2094
|
+
let accountCreated = false;
|
|
2095
|
+
if (newBusinessName !== void 0) {
|
|
2096
|
+
let account;
|
|
2097
|
+
try {
|
|
2098
|
+
account = await makeWhopRequest("POST", "/accounts", { title: newBusinessName });
|
|
2099
|
+
} catch (err) {
|
|
2100
|
+
return c2.error({
|
|
2101
|
+
code: "ACCOUNT_FAILED",
|
|
2102
|
+
message: `Failed to create the business account: ${err instanceof Error ? err.message : "unknown error"}`,
|
|
2103
|
+
retryable: true
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
accountId = account.id;
|
|
2107
|
+
accountName = account.title ?? newBusinessName;
|
|
2108
|
+
accountCreated = true;
|
|
2109
|
+
setActiveProfileAccount({
|
|
2110
|
+
id: account.id,
|
|
2111
|
+
title: account.title ?? void 0,
|
|
2112
|
+
route: account.route ?? void 0
|
|
2113
|
+
});
|
|
2114
|
+
if (!c2.agent) {
|
|
2115
|
+
console.log(
|
|
2116
|
+
`
|
|
2117
|
+
${chalk3.green("\u2713")} Created business account "${accountName}" ${chalk3.dim(`(${accountId})`)} \u2014 the CLI now acts on behalf of it.`
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
1319
2121
|
let productId;
|
|
1320
2122
|
let productTitle;
|
|
1321
2123
|
let productCreated = false;
|
|
@@ -1330,7 +2132,7 @@ ${chalk3.bold("Let's get you set up to sell.")}
|
|
|
1330
2132
|
"/products",
|
|
1331
2133
|
{
|
|
1332
2134
|
title: newTitle,
|
|
1333
|
-
company_id:
|
|
2135
|
+
company_id: accountId
|
|
1334
2136
|
}
|
|
1335
2137
|
);
|
|
1336
2138
|
} catch (err) {
|
|
@@ -1347,7 +2149,7 @@ ${chalk3.bold("Let's get you set up to sell.")}
|
|
|
1347
2149
|
const isRenewal = billingPeriod !== "one_time";
|
|
1348
2150
|
const planBody = {
|
|
1349
2151
|
product_id: productId,
|
|
1350
|
-
account_id:
|
|
2152
|
+
account_id: accountId,
|
|
1351
2153
|
initial_price: price,
|
|
1352
2154
|
plan_type: isRenewal ? "renewal" : "one_time"
|
|
1353
2155
|
};
|
|
@@ -1383,6 +2185,15 @@ ${chalk3.bold("Let's get you set up to sell.")}
|
|
|
1383
2185
|
const checkoutUrl = plan.purchase_url ?? null;
|
|
1384
2186
|
const steps = [
|
|
1385
2187
|
{ id: "auth", label: "Log in", complete: true },
|
|
2188
|
+
...accountCreated ? [
|
|
2189
|
+
{
|
|
2190
|
+
id: "account",
|
|
2191
|
+
label: "Create a business account",
|
|
2192
|
+
complete: true,
|
|
2193
|
+
firstId: accountId,
|
|
2194
|
+
firstTitle: accountName
|
|
2195
|
+
}
|
|
2196
|
+
] : [],
|
|
1386
2197
|
{
|
|
1387
2198
|
id: "products",
|
|
1388
2199
|
label: "Create a product",
|
|
@@ -1405,20 +2216,17 @@ ${chalk3.bold("Let's get you set up to sell.")}
|
|
|
1405
2216
|
false
|
|
1406
2217
|
);
|
|
1407
2218
|
if (!c2.agent && !c2.formatExplicit) {
|
|
1408
|
-
printSummary(
|
|
1409
|
-
state.accountName,
|
|
1410
|
-
steps,
|
|
1411
|
-
true,
|
|
1412
|
-
checkoutUrl,
|
|
1413
|
-
nextStep,
|
|
1414
|
-
productTitle
|
|
1415
|
-
);
|
|
2219
|
+
printSummary(accountName, steps, true, checkoutUrl, nextStep, productTitle);
|
|
1416
2220
|
}
|
|
1417
2221
|
return c2.ok({
|
|
1418
|
-
account:
|
|
2222
|
+
account: accountName,
|
|
1419
2223
|
ready: true,
|
|
1420
2224
|
checkoutUrl,
|
|
1421
|
-
created: {
|
|
2225
|
+
created: {
|
|
2226
|
+
...accountCreated ? { accountId } : {},
|
|
2227
|
+
productId,
|
|
2228
|
+
planId: plan.id
|
|
2229
|
+
},
|
|
1422
2230
|
steps,
|
|
1423
2231
|
nextStep
|
|
1424
2232
|
});
|
|
@@ -1448,7 +2256,7 @@ async function resolveExistingProduct(c2, products) {
|
|
|
1448
2256
|
hint: p.id
|
|
1449
2257
|
}))
|
|
1450
2258
|
});
|
|
1451
|
-
if (
|
|
2259
|
+
if (isCancel2(chosen))
|
|
1452
2260
|
throw new QuickstartError({
|
|
1453
2261
|
code: "CANCELLED",
|
|
1454
2262
|
message: "Setup cancelled.",
|
|
@@ -1548,7 +2356,7 @@ ${c.success("\u2713 Your store is fully set up.")}`);
|
|
|
1548
2356
|
// package.json
|
|
1549
2357
|
var package_default = {
|
|
1550
2358
|
name: "@whop/cli",
|
|
1551
|
-
version: "0.
|
|
2359
|
+
version: "0.3.0",
|
|
1552
2360
|
description: "The Whop CLI \u2014 build and manage Whop apps from your terminal. Human and agent friendly.",
|
|
1553
2361
|
keywords: [
|
|
1554
2362
|
"agent",
|
|
@@ -1572,6 +2380,13 @@ var package_default = {
|
|
|
1572
2380
|
"dist"
|
|
1573
2381
|
],
|
|
1574
2382
|
type: "module",
|
|
2383
|
+
exports: {
|
|
2384
|
+
".": "./dist/index.js",
|
|
2385
|
+
"./vite": {
|
|
2386
|
+
types: "./dist/vite.d.ts",
|
|
2387
|
+
import: "./dist/vite.js"
|
|
2388
|
+
}
|
|
2389
|
+
},
|
|
1575
2390
|
publishConfig: {
|
|
1576
2391
|
access: "public"
|
|
1577
2392
|
},
|
|
@@ -1598,10 +2413,12 @@ var package_default = {
|
|
|
1598
2413
|
},
|
|
1599
2414
|
devDependencies: {
|
|
1600
2415
|
"@types/node": "25.3.5",
|
|
1601
|
-
|
|
2416
|
+
fflate: "0.8.2",
|
|
2417
|
+
incur: "github:whopio/incur#4b9ca91dc02472db944fb103984ffc4a13251b6f",
|
|
1602
2418
|
tsup: "8.5.0",
|
|
1603
2419
|
tsx: "4.19.4",
|
|
1604
|
-
typescript: "5.9.3"
|
|
2420
|
+
typescript: "5.9.3",
|
|
2421
|
+
vite: "7.3.6"
|
|
1605
2422
|
},
|
|
1606
2423
|
engines: {
|
|
1607
2424
|
node: ">=22"
|
|
@@ -2026,7 +2843,7 @@ var HANDWRITTEN_GROUPS = [
|
|
|
2026
2843
|
section: "get-started",
|
|
2027
2844
|
register: (cli2) => cli2.command("quickstart", {
|
|
2028
2845
|
description: "Start selling \u2014 check your status and create what's missing",
|
|
2029
|
-
hint: "In a terminal it guides you to your first sale. Agents: a read-only status check unless you pass --title/--price/--billing_period.",
|
|
2846
|
+
hint: "In a terminal it guides you to your first sale \u2014 including creating a business account if you don't have one. Agents: a read-only status check unless you pass --business_name/--title/--price/--billing_period.",
|
|
2030
2847
|
options: quickstartOptions,
|
|
2031
2848
|
output: quickstartOutput,
|
|
2032
2849
|
examples: [
|
|
@@ -2038,6 +2855,15 @@ var HANDWRITTEN_GROUPS = [
|
|
|
2038
2855
|
billing_period: "monthly"
|
|
2039
2856
|
},
|
|
2040
2857
|
description: "Create a $9.99/mo product non-interactively"
|
|
2858
|
+
},
|
|
2859
|
+
{
|
|
2860
|
+
options: {
|
|
2861
|
+
business_name: "My Business",
|
|
2862
|
+
title: "My Course",
|
|
2863
|
+
price: 9.99,
|
|
2864
|
+
billing_period: "one_time"
|
|
2865
|
+
},
|
|
2866
|
+
description: "No business account yet? Create it and the product in one run"
|
|
2041
2867
|
}
|
|
2042
2868
|
],
|
|
2043
2869
|
outputPolicy: "agent-only",
|
|
@@ -2045,22 +2871,10 @@ var HANDWRITTEN_GROUPS = [
|
|
|
2045
2871
|
})
|
|
2046
2872
|
},
|
|
2047
2873
|
{
|
|
2048
|
-
name: "
|
|
2049
|
-
description: "
|
|
2050
|
-
section: "
|
|
2051
|
-
register: (cli2) => cli2.command(
|
|
2052
|
-
},
|
|
2053
|
-
{
|
|
2054
|
-
name: "plans",
|
|
2055
|
-
description: "Pricing for your products",
|
|
2056
|
-
section: "commerce",
|
|
2057
|
-
register: (cli2) => cli2.command(buildPlansGroup())
|
|
2058
|
-
},
|
|
2059
|
-
{
|
|
2060
|
-
name: "checkout-configurations",
|
|
2061
|
-
description: "Optional / advanced: customize the checkout page",
|
|
2062
|
-
section: "commerce",
|
|
2063
|
-
register: (cli2) => cli2.command(buildCheckoutGroup())
|
|
2874
|
+
name: "apps",
|
|
2875
|
+
description: "Build and deploy fully-hosted web apps (*.whop.app)",
|
|
2876
|
+
section: "get-started",
|
|
2877
|
+
register: (cli2) => cli2.command(buildAppGroup())
|
|
2064
2878
|
},
|
|
2065
2879
|
{
|
|
2066
2880
|
name: "upgrade",
|
|
@@ -2334,9 +3148,9 @@ var tag_descriptions_default = {
|
|
|
2334
3148
|
Deposits: "Add funds to your balance",
|
|
2335
3149
|
Swaps: "Convert between currencies",
|
|
2336
3150
|
Verifications: "Identity verification status",
|
|
2337
|
-
Products: "
|
|
2338
|
-
Plans: "
|
|
2339
|
-
"Checkout Configurations": "
|
|
3151
|
+
Products: "What you're selling",
|
|
3152
|
+
Plans: "Pricing for your products",
|
|
3153
|
+
"Checkout Configurations": "Optional / advanced: customize the checkout page",
|
|
2340
3154
|
Referrals: "Referral programs",
|
|
2341
3155
|
Ads: "Ad creatives",
|
|
2342
3156
|
"Ad Campaigns": "Top-level campaign organization",
|
|
@@ -2350,7 +3164,6 @@ var descriptions = tag_descriptions_default;
|
|
|
2350
3164
|
var toCliName = (tag) => tag.toLowerCase().replace(/\s+/g, "-");
|
|
2351
3165
|
var SECTION_LABELS = {
|
|
2352
3166
|
"get-started": "Get Started",
|
|
2353
|
-
commerce: "Commerce",
|
|
2354
3167
|
auth: "Auth"
|
|
2355
3168
|
};
|
|
2356
3169
|
var INTEGRATIONS = [
|
|
@@ -2393,16 +3206,16 @@ function buildGroupedHelp(version, description) {
|
|
|
2393
3206
|
description: descriptions[tag] ?? ""
|
|
2394
3207
|
}))
|
|
2395
3208
|
})).filter((section) => section.commands.length > 0);
|
|
2396
|
-
const
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
const
|
|
3209
|
+
const commerceSections = apiSections.filter(
|
|
3210
|
+
(section) => section.label.toLowerCase() === "commerce"
|
|
3211
|
+
);
|
|
3212
|
+
const otherApiSections = apiSections.filter(
|
|
2400
3213
|
(section) => section.label.toLowerCase() !== "commerce"
|
|
2401
3214
|
);
|
|
2402
3215
|
const allSections = [
|
|
2403
3216
|
handwrittenSection("get-started"),
|
|
2404
|
-
|
|
2405
|
-
...
|
|
3217
|
+
...commerceSections,
|
|
3218
|
+
...otherApiSections,
|
|
2406
3219
|
handwrittenSection("auth")
|
|
2407
3220
|
].filter((section) => section.commands.length > 0);
|
|
2408
3221
|
const maxCmdLen = Math.max(
|