@whop/cli 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/{api-U7WU3JXH.js → api-7L74UEG4.js} +4 -2
- package/dist/{chunk-2Y4EKBOD.js → chunk-2K4WBZA5.js} +4886 -168
- package/dist/chunk-2K4WBZA5.js.map +1 -0
- package/dist/index.js +981 -445
- 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-2Y4EKBOD.js.map +0 -1
- /package/dist/{api-U7WU3JXH.js.map → api-7L74UEG4.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -5,10 +5,13 @@ import {
|
|
|
5
5
|
Cli_exports,
|
|
6
6
|
Identity,
|
|
7
7
|
OAuthError,
|
|
8
|
+
Openapi_exports,
|
|
8
9
|
Profile,
|
|
9
10
|
PromptCancelledError,
|
|
10
11
|
apiBaseUrl,
|
|
12
|
+
apps_spec_default,
|
|
11
13
|
buildTarget,
|
|
14
|
+
buildVersion,
|
|
12
15
|
chooseAuthMethod,
|
|
13
16
|
configDir,
|
|
14
17
|
createWhopFetch,
|
|
@@ -32,7 +35,7 @@ import {
|
|
|
32
35
|
switchProfile,
|
|
33
36
|
upsertProfile,
|
|
34
37
|
validateApiKey
|
|
35
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-2K4WBZA5.js";
|
|
36
39
|
import {
|
|
37
40
|
external_exports
|
|
38
41
|
} from "./chunk-KFCNNWPI.js";
|
|
@@ -311,7 +314,7 @@ async function loginAdapter(c2) {
|
|
|
311
314
|
const accountId = getActiveProfile()?.accountId ?? "";
|
|
312
315
|
if (accountId) {
|
|
313
316
|
try {
|
|
314
|
-
const { createWhopFetch: createWhopFetch2 } = await import("./api-
|
|
317
|
+
const { createWhopFetch: createWhopFetch2 } = await import("./api-7L74UEG4.js");
|
|
315
318
|
const fetch3 = createWhopFetch2();
|
|
316
319
|
const res = await fetch3(
|
|
317
320
|
new Request(
|
|
@@ -590,10 +593,15 @@ function buildAuthGroup() {
|
|
|
590
593
|
return auth;
|
|
591
594
|
}
|
|
592
595
|
|
|
593
|
-
// src/
|
|
594
|
-
import {
|
|
596
|
+
// src/apps/commands.ts
|
|
597
|
+
import { isCancel, select, spinner, text } from "@clack/prompts";
|
|
598
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
599
|
+
import { createHash } from "crypto";
|
|
600
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
|
|
601
|
+
import { join as join3, relative, resolve as resolve2 } from "path";
|
|
602
|
+
import chalk from "chalk";
|
|
595
603
|
|
|
596
|
-
// src/
|
|
604
|
+
// src/journey/client.ts
|
|
597
605
|
var PLACEHOLDER = "https://api.whop.com";
|
|
598
606
|
var whopFetch = createWhopFetch();
|
|
599
607
|
async function makeWhopRequest(method, path3, body) {
|
|
@@ -608,473 +616,1005 @@ async function makeWhopRequest(method, path3, body) {
|
|
|
608
616
|
return text3 ? JSON.parse(text3) : { success: true };
|
|
609
617
|
}
|
|
610
618
|
|
|
611
|
-
// src/
|
|
612
|
-
function
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
after: external_exports.string().optional().describe("Pagination cursor")
|
|
623
|
-
};
|
|
624
|
-
if (config.filter) {
|
|
625
|
-
shape[config.filter.option] = external_exports.string().optional().describe(config.filter.describe);
|
|
626
|
-
}
|
|
627
|
-
group.command("list", {
|
|
628
|
-
description: config.description,
|
|
629
|
-
options: external_exports.object(shape),
|
|
630
|
-
run: (c2) => {
|
|
631
|
-
const options = c2.options;
|
|
632
|
-
const params = new URLSearchParams();
|
|
633
|
-
if (config.filter && options[config.filter.option]) {
|
|
634
|
-
params.set(config.filter.param, String(options[config.filter.option]));
|
|
635
|
-
}
|
|
636
|
-
if (options.first) params.set("first", String(options.first));
|
|
637
|
-
if (options.after) params.set("after", String(options.after));
|
|
638
|
-
const qs = params.toString() ? `?${params}` : "";
|
|
639
|
-
return makeWhopRequest("GET", `${config.path}${qs}`);
|
|
640
|
-
}
|
|
619
|
+
// src/apps/api.ts
|
|
620
|
+
async function createApp(input) {
|
|
621
|
+
const company_id = input.company_id ?? await resolveActiveAccountId();
|
|
622
|
+
return makeWhopRequest("POST", "/apps", { ...input, company_id });
|
|
623
|
+
}
|
|
624
|
+
var DEV_TOKEN_TTL_MS = (3 * 60 - 1) * 60 * 1e3;
|
|
625
|
+
async function createDevAccessToken() {
|
|
626
|
+
const company_id = await resolveActiveAccountId();
|
|
627
|
+
return makeWhopRequest("POST", "/access_tokens", {
|
|
628
|
+
...company_id ? { company_id } : {},
|
|
629
|
+
expires_at: new Date(Date.now() + DEV_TOKEN_TTL_MS).toISOString()
|
|
641
630
|
});
|
|
642
631
|
}
|
|
643
|
-
function
|
|
644
|
-
const
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
632
|
+
async function listApps() {
|
|
633
|
+
const companyId = await resolveActiveAccountId();
|
|
634
|
+
const query = companyId ? `?company_id=${encodeURIComponent(companyId)}` : "";
|
|
635
|
+
const response = await makeWhopRequest(
|
|
636
|
+
"GET",
|
|
637
|
+
`/apps${query}`
|
|
638
|
+
);
|
|
639
|
+
return response.data ?? [];
|
|
640
|
+
}
|
|
641
|
+
async function getApp(appId) {
|
|
642
|
+
return makeWhopRequest("GET", `/apps/${appId}`);
|
|
643
|
+
}
|
|
644
|
+
async function updateAppRoute(appId, route) {
|
|
645
|
+
return makeWhopRequest("PATCH", `/apps/${appId}`, { route });
|
|
646
|
+
}
|
|
647
|
+
async function createAppBuild(input) {
|
|
648
|
+
return makeWhopRequest("POST", "/app_builds", {
|
|
649
|
+
app_id: input.app_id,
|
|
650
|
+
platform: "web",
|
|
651
|
+
checksum: input.checksum,
|
|
652
|
+
attachment: { id: input.file_id }
|
|
651
653
|
});
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
654
|
+
}
|
|
655
|
+
async function getAppBuild(buildId) {
|
|
656
|
+
return makeWhopRequest("GET", `/app_builds/${buildId}`);
|
|
657
|
+
}
|
|
658
|
+
async function promoteAppBuild(buildId) {
|
|
659
|
+
return makeWhopRequest(
|
|
660
|
+
"POST",
|
|
661
|
+
`/app_builds/${buildId}/promote`,
|
|
662
|
+
{}
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
async function uploadBuildArchive(zip, filename) {
|
|
666
|
+
const file = await makeWhopRequest("POST", "/files", {
|
|
667
|
+
filename,
|
|
668
|
+
visibility: "public"
|
|
669
|
+
});
|
|
670
|
+
if (!file.upload_url) {
|
|
671
|
+
throw new Error("File creation did not return an upload URL.");
|
|
672
|
+
}
|
|
673
|
+
const body = new Uint8Array(zip).buffer;
|
|
674
|
+
const uploadResponse = await fetch(file.upload_url, {
|
|
675
|
+
method: "PUT",
|
|
676
|
+
headers: file.upload_headers ?? { "Content-Type": "application/zip" },
|
|
677
|
+
body
|
|
668
678
|
});
|
|
679
|
+
if (!uploadResponse.ok) {
|
|
680
|
+
throw new Error(
|
|
681
|
+
`Archive upload failed: ${uploadResponse.status} ${await uploadResponse.text()}`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
return file.id;
|
|
685
|
+
}
|
|
686
|
+
async function waitForFileReady(fileId, timeoutMs = 6e4) {
|
|
687
|
+
const deadline = Date.now() + timeoutMs;
|
|
688
|
+
while (Date.now() < deadline) {
|
|
689
|
+
const file = await makeWhopRequest("GET", `/files/${fileId}`);
|
|
690
|
+
if (file.upload_status === "ready") return;
|
|
691
|
+
await sleep(1e3);
|
|
692
|
+
}
|
|
693
|
+
throw new Error(
|
|
694
|
+
"Timed out waiting for the build archive to finish processing."
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
async function waitForBuildPromotion(buildId, timeoutMs = 3e4) {
|
|
698
|
+
const deadline = Date.now() + timeoutMs;
|
|
699
|
+
let latest = await getAppBuild(buildId);
|
|
700
|
+
while (Date.now() < deadline) {
|
|
701
|
+
if (latest.is_production) return latest;
|
|
702
|
+
if (latest.status === "rejected") {
|
|
703
|
+
throw new Error("Build was rejected and cannot be promoted.");
|
|
704
|
+
}
|
|
705
|
+
await sleep(750);
|
|
706
|
+
latest = await getAppBuild(buildId);
|
|
707
|
+
}
|
|
708
|
+
return latest;
|
|
709
|
+
}
|
|
710
|
+
function sleep(ms) {
|
|
711
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
669
712
|
}
|
|
670
713
|
|
|
671
|
-
// src/
|
|
672
|
-
import
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
lines.push("", `${chalk.bold("Next")} ${chalk.cyan.bold(`whop ${opts.next.command}`)}`);
|
|
683
|
-
if (opts.next.hint) lines.push(` ${chalk.dim(`\u2192 ${opts.next.hint}`)}`);
|
|
714
|
+
// src/apps/config.ts
|
|
715
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
716
|
+
import { join, resolve } from "path";
|
|
717
|
+
var APP_CONFIG_FILENAME = "whop.app.json";
|
|
718
|
+
function findProjectDir(startDir) {
|
|
719
|
+
let dir = resolve(startDir ?? process.cwd());
|
|
720
|
+
for (let i = 0; i < 25; i++) {
|
|
721
|
+
if (existsSync(join(dir, APP_CONFIG_FILENAME))) return dir;
|
|
722
|
+
const parent = resolve(dir, "..");
|
|
723
|
+
if (parent === dir) break;
|
|
724
|
+
dir = parent;
|
|
684
725
|
}
|
|
685
|
-
|
|
686
|
-
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
function readAppConfig(projectDir) {
|
|
729
|
+
const path3 = join(projectDir, APP_CONFIG_FILENAME);
|
|
730
|
+
if (!existsSync(path3)) return null;
|
|
731
|
+
try {
|
|
732
|
+
const parsed = JSON.parse(
|
|
733
|
+
readFileSync(path3, "utf-8")
|
|
734
|
+
);
|
|
735
|
+
if (typeof parsed.app_id === "string" && typeof parsed.name === "string" && typeof parsed.route === "string") {
|
|
736
|
+
return { app_id: parsed.app_id, name: parsed.name, route: parsed.route };
|
|
737
|
+
}
|
|
738
|
+
return null;
|
|
739
|
+
} catch {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function writeAppConfig(projectDir, config) {
|
|
744
|
+
writeFileSync(
|
|
745
|
+
join(projectDir, APP_CONFIG_FILENAME),
|
|
746
|
+
`${JSON.stringify(config, null, " ")}
|
|
747
|
+
`
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
function slugify(name) {
|
|
751
|
+
return name.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
|
|
687
752
|
}
|
|
688
753
|
|
|
689
|
-
// src/
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
}
|
|
700
|
-
var
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
redirect_purchase_url: external_exports.string().optional().describe("Redirect buyers here after purchase")
|
|
710
|
-
});
|
|
711
|
-
function buildProductsGroup() {
|
|
712
|
-
const products = Cli_exports.create("products", {
|
|
713
|
-
description: "Products \u2014 what you're selling on Whop"
|
|
754
|
+
// src/apps/scaffold.ts
|
|
755
|
+
import { spawnSync } from "child_process";
|
|
756
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
757
|
+
import { basename, join as join2 } from "path";
|
|
758
|
+
var SPAWN_VIA_SHELL = process.platform === "win32";
|
|
759
|
+
var DLX_RUNNERS = {
|
|
760
|
+
npm: ["npx", "-y"],
|
|
761
|
+
pnpm: ["pnpm", "dlx"],
|
|
762
|
+
bun: ["bunx"],
|
|
763
|
+
yarn: ["yarn", "dlx"]
|
|
764
|
+
};
|
|
765
|
+
var PM_PRIORITY = [
|
|
766
|
+
"bun",
|
|
767
|
+
"pnpm",
|
|
768
|
+
"npm"
|
|
769
|
+
];
|
|
770
|
+
function isPmInstalled(pm) {
|
|
771
|
+
const result = spawnSync(pm, ["--version"], {
|
|
772
|
+
stdio: "ignore",
|
|
773
|
+
shell: SPAWN_VIA_SHELL
|
|
714
774
|
});
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
775
|
+
return !result.error && result.status === 0;
|
|
776
|
+
}
|
|
777
|
+
function detectPackageManager() {
|
|
778
|
+
return PM_PRIORITY.find(isPmInstalled) ?? null;
|
|
779
|
+
}
|
|
780
|
+
function pmInstallHint(pm) {
|
|
781
|
+
const win = process.platform === "win32";
|
|
782
|
+
switch (pm) {
|
|
783
|
+
case "bun":
|
|
784
|
+
return win ? 'powershell -c "irm bun.sh/install.ps1 | iex"' : "curl -fsSL https://bun.sh/install | bash";
|
|
785
|
+
case "pnpm":
|
|
786
|
+
return win ? 'powershell -c "irm get.pnpm.io/install.ps1 | iex"' : "curl -fsSL https://get.pnpm.io/install.sh | sh -";
|
|
787
|
+
case "npm":
|
|
788
|
+
return "install Node.js (includes npm): https://nodejs.org";
|
|
789
|
+
case "yarn":
|
|
790
|
+
return "npm install -g yarn";
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
var TANSTACK_CREATE_FLAGS = [
|
|
794
|
+
"--framework",
|
|
795
|
+
"React",
|
|
796
|
+
"--deployment",
|
|
797
|
+
"cloudflare",
|
|
798
|
+
"--no-git",
|
|
799
|
+
"--no-install",
|
|
800
|
+
"--no-intent",
|
|
801
|
+
"--no-toolchain",
|
|
802
|
+
"--no-examples",
|
|
803
|
+
"--non-interactive"
|
|
804
|
+
];
|
|
805
|
+
function tanstackCreateCommand(pm, projectName) {
|
|
806
|
+
return [
|
|
807
|
+
...DLX_RUNNERS[pm],
|
|
808
|
+
"@tanstack/cli@latest",
|
|
809
|
+
"create",
|
|
810
|
+
projectName,
|
|
811
|
+
...TANSTACK_CREATE_FLAGS
|
|
812
|
+
].join(" ");
|
|
813
|
+
}
|
|
814
|
+
function outputTail(result) {
|
|
815
|
+
const combined = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
816
|
+
if (!combined) return "";
|
|
817
|
+
return `
|
|
818
|
+
|
|
819
|
+
${combined.split("\n").slice(-15).join("\n")}`;
|
|
820
|
+
}
|
|
821
|
+
function runTanstackCreate(options) {
|
|
822
|
+
const [runner, ...runnerArgs] = DLX_RUNNERS[options.pm];
|
|
823
|
+
const result = spawnSync(
|
|
824
|
+
runner,
|
|
825
|
+
[
|
|
826
|
+
...runnerArgs,
|
|
827
|
+
"@tanstack/cli@latest",
|
|
828
|
+
"create",
|
|
829
|
+
options.projectName,
|
|
830
|
+
"--target-dir",
|
|
831
|
+
options.targetDir,
|
|
832
|
+
...TANSTACK_CREATE_FLAGS
|
|
730
833
|
],
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
834
|
+
{
|
|
835
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
836
|
+
encoding: "utf-8",
|
|
837
|
+
shell: SPAWN_VIA_SHELL
|
|
838
|
+
}
|
|
839
|
+
);
|
|
840
|
+
if (result.error) throw result.error;
|
|
841
|
+
if (result.status !== 0) {
|
|
842
|
+
throw new Error(
|
|
843
|
+
`\`${options.pm === "npm" ? "npx" : options.pm} @tanstack/cli create\` failed (exit ${result.status}). Check your network connection and try again.${outputTail(result)}`
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
var VITE_CONFIG_NAMES = [
|
|
848
|
+
"vite.config.ts",
|
|
849
|
+
"vite.config.mts",
|
|
850
|
+
"vite.config.js",
|
|
851
|
+
"vite.config.mjs"
|
|
852
|
+
];
|
|
853
|
+
function findViteConfig(dir) {
|
|
854
|
+
return VITE_CONFIG_NAMES.map((name) => join2(dir, name)).find(
|
|
855
|
+
(path3) => existsSync2(path3)
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
var WRANGLER_CONFIG_NAMES = ["wrangler.jsonc", "wrangler.json"];
|
|
859
|
+
function cliDependencyVersion() {
|
|
860
|
+
return buildVersion ? `^${buildVersion}` : "latest";
|
|
861
|
+
}
|
|
862
|
+
function whopifyProject(options) {
|
|
863
|
+
const manualSteps = [];
|
|
864
|
+
patchPackageJson(options.targetDir, manualSteps);
|
|
865
|
+
patchViteConfig(options.targetDir, manualSteps);
|
|
866
|
+
patchWranglerConfig(options.targetDir, options.route, manualSteps);
|
|
867
|
+
return { manualSteps };
|
|
868
|
+
}
|
|
869
|
+
function patchPackageJson(dir, manualSteps) {
|
|
870
|
+
const file = join2(dir, "package.json");
|
|
871
|
+
let pkg;
|
|
872
|
+
try {
|
|
873
|
+
pkg = JSON.parse(readFileSync2(file, "utf-8"));
|
|
874
|
+
} catch {
|
|
875
|
+
manualSteps.push(
|
|
876
|
+
`package.json could not be parsed. Add a "deploy": "whop apps deploy" script, a "typecheck" script, and a devDependency "@whop/cli": "${cliDependencyVersion()}".`
|
|
877
|
+
);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
pkg.scripts = {
|
|
881
|
+
...pkg.scripts,
|
|
882
|
+
deploy: "whop apps deploy",
|
|
883
|
+
typecheck: pkg.scripts?.typecheck ?? "tsc --noEmit"
|
|
884
|
+
};
|
|
885
|
+
pkg.devDependencies = {
|
|
886
|
+
...pkg.devDependencies,
|
|
887
|
+
// The project imports the whop() vite plugin from @whop/cli/vite.
|
|
888
|
+
"@whop/cli": pkg.devDependencies?.["@whop/cli"] ?? cliDependencyVersion()
|
|
889
|
+
};
|
|
890
|
+
writeFileSync2(file, `${JSON.stringify(pkg, null, 2)}
|
|
891
|
+
`);
|
|
892
|
+
}
|
|
893
|
+
function patchViteConfig(dir, manualSteps) {
|
|
894
|
+
const file = findViteConfig(dir);
|
|
895
|
+
if (!file) {
|
|
896
|
+
manualSteps.push(
|
|
897
|
+
"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."
|
|
898
|
+
);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
let content = readFileSync2(file, "utf-8");
|
|
902
|
+
const hasWhopPlugin = content.includes("@whop/cli/vite");
|
|
903
|
+
if (!hasWhopPlugin) {
|
|
904
|
+
const pluginsOpen = /plugins:\s*\[/;
|
|
905
|
+
if (!pluginsOpen.test(content)) {
|
|
906
|
+
manualSteps.push(
|
|
907
|
+
`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.`
|
|
908
|
+
);
|
|
909
|
+
} else {
|
|
910
|
+
content = `import { whop } from '@whop/cli/vite'
|
|
911
|
+
${content}`;
|
|
912
|
+
content = content.replace(
|
|
913
|
+
pluginsOpen,
|
|
914
|
+
(match) => `${match}
|
|
915
|
+
whop(),`
|
|
916
|
+
);
|
|
917
|
+
writeFileSync2(file, content);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (!content.includes("@cloudflare/vite-plugin")) {
|
|
921
|
+
manualSteps.push(
|
|
922
|
+
`${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).`
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
function patchWranglerConfig(dir, route, manualSteps) {
|
|
927
|
+
const file = WRANGLER_CONFIG_NAMES.map((name) => join2(dir, name)).find(
|
|
928
|
+
(path3) => existsSync2(path3)
|
|
929
|
+
);
|
|
930
|
+
if (!file) {
|
|
931
|
+
manualSteps.push(
|
|
932
|
+
`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").`
|
|
933
|
+
);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
const content = readFileSync2(file, "utf-8");
|
|
937
|
+
const updated = replaceTopLevelName(content, route);
|
|
938
|
+
if (updated) {
|
|
939
|
+
writeFileSync2(file, updated);
|
|
940
|
+
} else {
|
|
941
|
+
manualSteps.push(
|
|
942
|
+
`Set the top-level "name" in ${basename(file)} to "${route}" so the worker is named after the app's route.`
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
function replaceTopLevelName(content, route) {
|
|
947
|
+
let depth = 0;
|
|
948
|
+
let i = 0;
|
|
949
|
+
while (i < content.length) {
|
|
950
|
+
const ch = content[i];
|
|
951
|
+
if (ch === '"') {
|
|
952
|
+
let j = i + 1;
|
|
953
|
+
while (j < content.length && content[j] !== '"') {
|
|
954
|
+
j += content[j] === "\\" ? 2 : 1;
|
|
767
955
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
});
|
|
776
|
-
return c2.ok(product, {
|
|
777
|
-
cta: {
|
|
778
|
-
description: "Add pricing to your product:",
|
|
779
|
-
commands: [
|
|
780
|
-
{
|
|
781
|
-
command: "plans create",
|
|
782
|
-
options: { product_id: product.id },
|
|
783
|
-
description: "Create a pricing plan (required before checkout)"
|
|
784
|
-
}
|
|
785
|
-
]
|
|
956
|
+
if (depth === 1 && content.slice(i + 1, j) === "name") {
|
|
957
|
+
const after = content.slice(j + 1);
|
|
958
|
+
const value = after.match(/^(\s*:\s*)"(?:[^"\\]|\\.)*"/);
|
|
959
|
+
if (value) {
|
|
960
|
+
const start = j + 1 + value[1].length;
|
|
961
|
+
const end = j + 1 + value[0].length;
|
|
962
|
+
return `${content.slice(0, start)}"${route}"${content.slice(end)}`;
|
|
786
963
|
}
|
|
787
|
-
}
|
|
964
|
+
}
|
|
965
|
+
i = j + 1;
|
|
966
|
+
continue;
|
|
788
967
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
968
|
+
if (ch === "/" && content[i + 1] === "/") {
|
|
969
|
+
const newline = content.indexOf("\n", i);
|
|
970
|
+
i = newline === -1 ? content.length : newline + 1;
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
if (ch === "/" && content[i + 1] === "*") {
|
|
974
|
+
const close = content.indexOf("*/", i + 2);
|
|
975
|
+
i = close === -1 ? content.length : close + 2;
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
if (ch === "{" || ch === "[") depth++;
|
|
979
|
+
if (ch === "}" || ch === "]") depth--;
|
|
980
|
+
i++;
|
|
981
|
+
}
|
|
982
|
+
return null;
|
|
802
983
|
}
|
|
803
984
|
|
|
804
|
-
// src/
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
// src/commerce/billing.ts
|
|
808
|
-
var BILLING_PERIOD_DAYS = {
|
|
809
|
-
daily: 1,
|
|
810
|
-
weekly: 7,
|
|
811
|
-
monthly: 30,
|
|
812
|
-
quarterly: 90,
|
|
813
|
-
annually: 365,
|
|
814
|
-
every_two_years: 730
|
|
815
|
-
};
|
|
816
|
-
|
|
817
|
-
// src/commerce/plans.ts
|
|
818
|
-
var PlanSchema = external_exports.object({
|
|
985
|
+
// src/apps/commands.ts
|
|
986
|
+
var BUILD_ARCHIVE = "dist/whop-build.zip";
|
|
987
|
+
var BuildSchema = external_exports.object({
|
|
819
988
|
id: external_exports.string(),
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
initial_price: external_exports.number().nullable().optional(),
|
|
824
|
-
renewal_price: external_exports.number().nullable().optional(),
|
|
825
|
-
currency: external_exports.string().optional(),
|
|
826
|
-
purchase_url: external_exports.string().nullable().optional(),
|
|
827
|
-
visibility: external_exports.string().optional(),
|
|
828
|
-
created_at: external_exports.number().optional()
|
|
989
|
+
status: external_exports.string(),
|
|
990
|
+
is_production: external_exports.boolean().optional(),
|
|
991
|
+
url: external_exports.string().optional()
|
|
829
992
|
});
|
|
830
|
-
var
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
billing_period: external_exports.enum(["daily", "weekly", "monthly", "quarterly", "annually", "every_two_years"]).optional().describe("How often to charge (for renewal plans)"),
|
|
835
|
-
initial_price: external_exports.coerce.number().optional().describe("Price in the plan's currency (e.g. 9.99 for $9.99)"),
|
|
836
|
-
renewal_price: external_exports.coerce.number().optional().describe("Recurring price for renewal plans (defaults to initial_price)"),
|
|
837
|
-
currency: external_exports.string().optional().describe("3-letter currency code (default: USD)"),
|
|
838
|
-
trial_period_days: external_exports.coerce.number().optional().describe("Free trial length in days"),
|
|
839
|
-
visibility: external_exports.enum(["visible", "hidden", "archived"]).optional().describe("Who can see this plan")
|
|
840
|
-
});
|
|
841
|
-
function normalizePlanBody(body) {
|
|
842
|
-
if (typeof body.billing_period === "string") {
|
|
843
|
-
body.billing_period = BILLING_PERIOD_DAYS[body.billing_period];
|
|
844
|
-
}
|
|
845
|
-
if (body.plan_type === "renewal" && body.renewal_price == null && body.initial_price != null) {
|
|
846
|
-
body.renewal_price = body.initial_price;
|
|
993
|
+
var DeployAbort = class extends Error {
|
|
994
|
+
constructor(options) {
|
|
995
|
+
super(options.message);
|
|
996
|
+
this.options = options;
|
|
847
997
|
}
|
|
848
|
-
|
|
998
|
+
};
|
|
999
|
+
function detectProjectPackageManager(projectDir) {
|
|
1000
|
+
if (existsSync3(join3(projectDir, "bun.lock")) || existsSync3(join3(projectDir, "bun.lockb")))
|
|
1001
|
+
return "bun";
|
|
1002
|
+
if (existsSync3(join3(projectDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
1003
|
+
if (existsSync3(join3(projectDir, "yarn.lock"))) return "yarn";
|
|
1004
|
+
if (existsSync3(join3(projectDir, "package-lock.json"))) return "npm";
|
|
1005
|
+
return detectPackageManager() ?? "npm";
|
|
849
1006
|
}
|
|
850
|
-
|
|
851
|
-
if (providedId) return { ok: true, productId: providedId, autoResolved: false };
|
|
852
|
-
const profile = getActiveProfile();
|
|
853
|
-
const params = new URLSearchParams({ first: "100" });
|
|
854
|
-
if (profile?.accountId) params.set("company_id", profile.accountId);
|
|
855
|
-
let data;
|
|
1007
|
+
function hasScript(projectDir, script) {
|
|
856
1008
|
try {
|
|
857
|
-
const
|
|
858
|
-
|
|
1009
|
+
const pkg = JSON.parse(
|
|
1010
|
+
readFileSync3(join3(projectDir, "package.json"), "utf-8")
|
|
1011
|
+
);
|
|
1012
|
+
return Boolean(pkg.scripts?.[script]);
|
|
859
1013
|
} catch {
|
|
860
|
-
return
|
|
1014
|
+
return false;
|
|
861
1015
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1016
|
+
}
|
|
1017
|
+
var SPAWN_VIA_SHELL2 = process.platform === "win32";
|
|
1018
|
+
function runScript(projectDir, script, extraEnv = {}) {
|
|
1019
|
+
const pm = detectProjectPackageManager(projectDir);
|
|
1020
|
+
const result = spawnSync2(pm, ["run", script], {
|
|
1021
|
+
cwd: projectDir,
|
|
1022
|
+
stdio: "inherit",
|
|
1023
|
+
shell: SPAWN_VIA_SHELL2,
|
|
1024
|
+
env: { ...process.env, ...extraEnv }
|
|
1025
|
+
});
|
|
1026
|
+
if (result.error) {
|
|
1027
|
+
throw new Error(
|
|
1028
|
+
`Could not run \`${pm} run ${script}\`: ${result.error.message}`
|
|
1029
|
+
);
|
|
868
1030
|
}
|
|
869
|
-
if (
|
|
870
|
-
|
|
1031
|
+
if (result.status !== 0) {
|
|
1032
|
+
throw new Error(
|
|
1033
|
+
`\`${pm} run ${script}\` failed with exit code ${result.status ?? "unknown"}`
|
|
1034
|
+
);
|
|
871
1035
|
}
|
|
872
|
-
|
|
873
|
-
|
|
1036
|
+
}
|
|
1037
|
+
function runInstall(c2, projectDir) {
|
|
1038
|
+
const pm = detectProjectPackageManager(projectDir);
|
|
1039
|
+
const spin = c2.agent ? null : spinner();
|
|
1040
|
+
spin?.start(`Installing dependencies with ${pm}`);
|
|
1041
|
+
const result = spawnSync2(pm, ["install"], {
|
|
1042
|
+
cwd: projectDir,
|
|
1043
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1044
|
+
encoding: "utf-8",
|
|
1045
|
+
shell: SPAWN_VIA_SHELL2
|
|
1046
|
+
});
|
|
1047
|
+
if (result.error || result.status !== 0) {
|
|
1048
|
+
spin?.error(`${pm} install failed`);
|
|
1049
|
+
throw new Error(
|
|
1050
|
+
`\`${pm} install\` failed${result.status != null ? ` with exit code ${result.status}` : ""} \u2014 run it manually inside ${projectDir}, then re-run \`whop apps deploy\`.${outputTail(result)}`
|
|
1051
|
+
);
|
|
874
1052
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1053
|
+
spin?.stop(`Installed dependencies with ${pm}`);
|
|
1054
|
+
}
|
|
1055
|
+
function log(c2, message) {
|
|
1056
|
+
if (!c2.agent) console.log(message);
|
|
1057
|
+
}
|
|
1058
|
+
async function promptText(message, placeholder, initialValue) {
|
|
1059
|
+
const value = await text({ message, placeholder, initialValue });
|
|
1060
|
+
if (isCancel(value)) return null;
|
|
1061
|
+
return String(value);
|
|
1062
|
+
}
|
|
1063
|
+
function cancelled() {
|
|
1064
|
+
throw new DeployAbort({
|
|
1065
|
+
code: "CANCELLED",
|
|
1066
|
+
message: "Cancelled.",
|
|
1067
|
+
exitCode: 130
|
|
878
1068
|
});
|
|
879
|
-
if (isCancel2(chosen)) return { ok: false, code: "CANCELLED", message: "Cancelled." };
|
|
880
|
-
return { ok: true, productId: chosen, autoResolved: false };
|
|
881
1069
|
}
|
|
882
|
-
function
|
|
883
|
-
|
|
884
|
-
|
|
1070
|
+
function formatManualSteps(steps) {
|
|
1071
|
+
return steps.map((step, i) => ` ${i + 1}. ${step}`).join("\n");
|
|
1072
|
+
}
|
|
1073
|
+
function formatAppList(apps) {
|
|
1074
|
+
const recent = [...apps].sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0)).slice(0, 10);
|
|
1075
|
+
return recent.map(
|
|
1076
|
+
(a) => ` ${a.id} ${a.name}${a.route ? ` (${a.route}.whop.app)` : ""}`
|
|
1077
|
+
).join("\n");
|
|
1078
|
+
}
|
|
1079
|
+
async function registerSpecCommands(group, tag) {
|
|
1080
|
+
const spec2 = filterSpecByTag(apps_spec_default, tag);
|
|
1081
|
+
const generated = await Openapi_exports.generateCommands(
|
|
1082
|
+
spec2,
|
|
1083
|
+
createWhopFetch()
|
|
1084
|
+
);
|
|
1085
|
+
for (const [name, entry] of generated) {
|
|
1086
|
+
if ("run" in entry) group.command(name, entry);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
function ensurePackageManager() {
|
|
1090
|
+
const pm = detectPackageManager();
|
|
1091
|
+
if (pm) return pm;
|
|
1092
|
+
throw new DeployAbort({
|
|
1093
|
+
code: "NO_PACKAGE_MANAGER",
|
|
1094
|
+
message: `No package manager found (looked for bun, pnpm, npm). Install one and re-run \`whop apps deploy\`:
|
|
1095
|
+
|
|
1096
|
+
bun (recommended): ${pmInstallHint("bun")}
|
|
1097
|
+
pnpm: ${pmInstallHint("pnpm")}
|
|
1098
|
+
npm: ${pmInstallHint("npm")}`
|
|
885
1099
|
});
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1100
|
+
}
|
|
1101
|
+
async function ensureViteApp(c2, pm, dirOption) {
|
|
1102
|
+
const projectDir = dirOption ? resolve2(dirOption) : findProjectDir() ?? process.cwd();
|
|
1103
|
+
const isViteApp = existsSync3(join3(projectDir, "package.json")) && findViteConfig(projectDir) !== void 0;
|
|
1104
|
+
if (isViteApp) return { projectDir, freshlyScaffolded: false };
|
|
1105
|
+
if (c2.agent) {
|
|
1106
|
+
throw new DeployAbort({
|
|
1107
|
+
code: "NOT_A_VITE_APP",
|
|
1108
|
+
message: [
|
|
1109
|
+
`${projectDir} doesn't look like a Vite app (missing package.json or vite config).`,
|
|
1110
|
+
"",
|
|
1111
|
+
"Ask the user whether to scaffold a new app here, or locate their existing Vite app.",
|
|
1112
|
+
"",
|
|
1113
|
+
"To scaffold a new app (TanStack Start), run:",
|
|
1114
|
+
` ${tanstackCreateCommand(pm, "<app-name>")}`,
|
|
1115
|
+
` cd <app-name> && ${pm} install`,
|
|
1116
|
+
"",
|
|
1117
|
+
"Then re-run `whop apps deploy` from inside the app directory.",
|
|
1118
|
+
"Or, if a Vite app already exists somewhere else, re-run `whop apps deploy --dir <path-to-app>`."
|
|
1119
|
+
].join("\n"),
|
|
1120
|
+
retryable: true,
|
|
1121
|
+
cta: {
|
|
1122
|
+
commands: [
|
|
1123
|
+
{
|
|
1124
|
+
command: "apps deploy",
|
|
1125
|
+
description: "Re-run from inside a Vite app directory"
|
|
1126
|
+
}
|
|
1127
|
+
]
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
const choice = await select({
|
|
1132
|
+
message: `${projectDir} doesn't look like a Vite app. What do you want to do?`,
|
|
1133
|
+
options: [
|
|
893
1134
|
{
|
|
894
|
-
|
|
895
|
-
|
|
1135
|
+
value: "scaffold",
|
|
1136
|
+
label: "Scaffold a new Whop app here (TanStack Start)"
|
|
896
1137
|
},
|
|
897
|
-
{
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1138
|
+
{ value: "cancel", label: "Cancel \u2014 I'll cd into my app directory" }
|
|
1139
|
+
]
|
|
1140
|
+
});
|
|
1141
|
+
if (isCancel(choice) || choice === "cancel") cancelled();
|
|
1142
|
+
const scaffolded = await scaffoldNewApp(c2, pm);
|
|
1143
|
+
return { projectDir: scaffolded, freshlyScaffolded: true };
|
|
1144
|
+
}
|
|
1145
|
+
async function scaffoldNewApp(c2, pm) {
|
|
1146
|
+
const name = await promptText("What is your app called?", "My Site");
|
|
1147
|
+
if (name === null) cancelled();
|
|
1148
|
+
const slug = slugify(name);
|
|
1149
|
+
const routeInput = await promptText(
|
|
1150
|
+
"Which route should it live at? (your-route.whop.app)",
|
|
1151
|
+
slug,
|
|
1152
|
+
slug
|
|
1153
|
+
);
|
|
1154
|
+
if (routeInput === null) cancelled();
|
|
1155
|
+
const route = slugify(routeInput);
|
|
1156
|
+
if (!route) {
|
|
1157
|
+
throw new DeployAbort({
|
|
1158
|
+
code: "ROUTE_REQUIRED",
|
|
1159
|
+
message: `Couldn't derive a route from "${routeInput}" \u2014 use letters and numbers (e.g. my-site).`,
|
|
1160
|
+
retryable: true
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
const targetDir = resolve2(`./${route}`);
|
|
1164
|
+
if (existsSync3(targetDir) && readdirSync(targetDir).length > 0) {
|
|
1165
|
+
throw new DeployAbort({
|
|
1166
|
+
code: "TARGET_NOT_EMPTY",
|
|
1167
|
+
message: `Target directory is not empty: ${targetDir}. Pick a different route, or run \`whop apps deploy\` from an empty directory.`
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
let app;
|
|
1171
|
+
try {
|
|
1172
|
+
app = await createApp({ name, route });
|
|
1173
|
+
} catch (err) {
|
|
1174
|
+
throw new DeployAbort({
|
|
1175
|
+
code: "CREATE_FAILED",
|
|
1176
|
+
message: err instanceof Error ? err.message : "Failed to register the app",
|
|
1177
|
+
retryable: true
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
const spin = c2.agent ? null : spinner();
|
|
1181
|
+
spin?.start("Scaffolding the latest TanStack Start template");
|
|
1182
|
+
try {
|
|
1183
|
+
runTanstackCreate({ targetDir, projectName: route, pm });
|
|
1184
|
+
spin?.stop(`Scaffolded ${route} from the latest TanStack Start template`);
|
|
1185
|
+
} catch (err) {
|
|
1186
|
+
spin?.error("Scaffold failed");
|
|
1187
|
+
throw new DeployAbort({
|
|
1188
|
+
code: "SCAFFOLD_FAILED",
|
|
1189
|
+
message: `${err instanceof Error ? err.message : "Failed to scaffold the project"}
|
|
1190
|
+
|
|
1191
|
+
The app ${app.id} is already registered \u2014 scaffold into a fresh directory and re-run \`whop apps deploy --app ${app.id}\` there.`
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
writeAppConfig(targetDir, { app_id: app.id, name, route });
|
|
1195
|
+
return targetDir;
|
|
1196
|
+
}
|
|
1197
|
+
async function ensureAppLinked(c2, projectDir, appOption) {
|
|
1198
|
+
if (appOption) {
|
|
1199
|
+
let app;
|
|
1200
|
+
try {
|
|
1201
|
+
app = await getApp(appOption);
|
|
1202
|
+
} catch (err) {
|
|
1203
|
+
throw new DeployAbort({
|
|
1204
|
+
code: "APP_NOT_FOUND",
|
|
1205
|
+
message: `Could not load app ${appOption}: ${err instanceof Error ? err.message : "unknown error"}`,
|
|
1206
|
+
retryable: true
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
const config2 = await withRoute(c2, app);
|
|
1210
|
+
writeAppConfig(projectDir, config2);
|
|
1211
|
+
log(
|
|
1212
|
+
c2,
|
|
1213
|
+
chalk.dim(
|
|
1214
|
+
`Linked ${config2.name} (${config2.app_id}) \u2192 ${APP_CONFIG_FILENAME}`
|
|
1215
|
+
)
|
|
1216
|
+
);
|
|
1217
|
+
return config2;
|
|
1218
|
+
}
|
|
1219
|
+
const existing = readAppConfig(projectDir);
|
|
1220
|
+
if (existing) return existing;
|
|
1221
|
+
const apps = await listApps();
|
|
1222
|
+
if (c2.agent) {
|
|
1223
|
+
const reuse = apps.length > 0 ? `This account has existing apps \u2014 ask the user whether to reuse one:
|
|
1224
|
+
${formatAppList(apps)}
|
|
1225
|
+
|
|
1226
|
+
To reuse one: whop apps deploy --app <app_id>
|
|
1227
|
+
|
|
1228
|
+
` : "This account has no apps yet, so a new one must be created.\n\n";
|
|
1229
|
+
throw new DeployAbort({
|
|
1230
|
+
code: "NO_APP_LINKED",
|
|
1231
|
+
message: [
|
|
1232
|
+
`No ${APP_CONFIG_FILENAME} here \u2014 this project isn't linked to a Whop app yet.`,
|
|
1233
|
+
"",
|
|
1234
|
+
`${reuse}To create a new app, ask the user for a name and route (the route becomes <route>.whop.app), then run:`,
|
|
1235
|
+
' whop apps create --name "My Site" --route my-site',
|
|
1236
|
+
"",
|
|
1237
|
+
"Then deploy with the returned app id:",
|
|
1238
|
+
" whop apps deploy --app <app_id>"
|
|
1239
|
+
].join("\n"),
|
|
1240
|
+
retryable: true,
|
|
1241
|
+
cta: {
|
|
1242
|
+
commands: [
|
|
1243
|
+
{
|
|
1244
|
+
command: "apps create",
|
|
1245
|
+
description: "Create a new app (pass --name and --route)"
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
command: "apps deploy",
|
|
1249
|
+
description: "Re-run with --app <app_id> to link and deploy"
|
|
1250
|
+
}
|
|
1251
|
+
]
|
|
905
1252
|
}
|
|
906
|
-
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
let selected = null;
|
|
1256
|
+
if (apps.length > 0) {
|
|
1257
|
+
const recent = [...apps].sort(
|
|
1258
|
+
(a, b) => (b.created_at ?? 0) - (a.created_at ?? 0)
|
|
1259
|
+
);
|
|
1260
|
+
const choice = await select({
|
|
1261
|
+
message: "Which app should this project deploy to?",
|
|
1262
|
+
options: [
|
|
1263
|
+
...recent.map((a) => ({
|
|
1264
|
+
value: a.id,
|
|
1265
|
+
label: `${a.name} (${a.id}${a.route ? `, ${a.route}.whop.app` : ""})`
|
|
1266
|
+
})),
|
|
1267
|
+
{ value: "__new__", label: "Create a new app" }
|
|
1268
|
+
]
|
|
1269
|
+
});
|
|
1270
|
+
if (isCancel(choice)) cancelled();
|
|
1271
|
+
if (choice !== "__new__") {
|
|
1272
|
+
selected = recent.find((a) => a.id === choice) ?? null;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (!selected) {
|
|
1276
|
+
const name = await promptText("What is your app called?", "My Site");
|
|
1277
|
+
if (name === null) cancelled();
|
|
1278
|
+
const slug = slugify(name);
|
|
1279
|
+
const routeInput = await promptText(
|
|
1280
|
+
"Which route should it live at? (your-route.whop.app)",
|
|
1281
|
+
slug,
|
|
1282
|
+
slug
|
|
1283
|
+
);
|
|
1284
|
+
if (routeInput === null) cancelled();
|
|
1285
|
+
const route = slugify(routeInput);
|
|
1286
|
+
if (!route) {
|
|
1287
|
+
throw new DeployAbort({
|
|
1288
|
+
code: "ROUTE_REQUIRED",
|
|
1289
|
+
message: `Couldn't derive a route from "${routeInput}" \u2014 use letters and numbers (e.g. my-site).`,
|
|
1290
|
+
retryable: true
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
try {
|
|
1294
|
+
selected = await createApp({ name, route });
|
|
1295
|
+
} catch (err) {
|
|
1296
|
+
throw new DeployAbort({
|
|
1297
|
+
code: "CREATE_FAILED",
|
|
1298
|
+
message: err instanceof Error ? err.message : "Failed to register the app",
|
|
1299
|
+
retryable: true
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
const config = await withRoute(c2, selected);
|
|
1304
|
+
writeAppConfig(projectDir, config);
|
|
1305
|
+
log(
|
|
1306
|
+
c2,
|
|
1307
|
+
chalk.dim(
|
|
1308
|
+
`Linked ${config.name} (${config.app_id}) \u2192 ${APP_CONFIG_FILENAME}`
|
|
1309
|
+
)
|
|
1310
|
+
);
|
|
1311
|
+
return config;
|
|
1312
|
+
}
|
|
1313
|
+
async function withRoute(c2, app) {
|
|
1314
|
+
if (app.route) return { app_id: app.id, name: app.name, route: app.route };
|
|
1315
|
+
if (c2.agent) {
|
|
1316
|
+
throw new DeployAbort({
|
|
1317
|
+
code: "ROUTE_REQUIRED",
|
|
1318
|
+
message: `App ${app.id} (${app.name}) has no route \u2014 hosted apps are served from <route>.whop.app. Ask the user which route to claim, then run:
|
|
1319
|
+
whop apps update ${app.id} --route <route>
|
|
1320
|
+
whop apps deploy --app ${app.id}`,
|
|
1321
|
+
retryable: true,
|
|
1322
|
+
cta: {
|
|
1323
|
+
commands: [
|
|
1324
|
+
{
|
|
1325
|
+
command: `apps update ${app.id}`,
|
|
1326
|
+
description: "Set the route (pass --route)"
|
|
1327
|
+
}
|
|
1328
|
+
]
|
|
1329
|
+
}
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
const slug = slugify(app.name);
|
|
1333
|
+
const routeInput = await promptText(
|
|
1334
|
+
`${app.name} has no route yet. Which route should it live at? (your-route.whop.app)`,
|
|
1335
|
+
slug,
|
|
1336
|
+
slug
|
|
1337
|
+
);
|
|
1338
|
+
if (routeInput === null) cancelled();
|
|
1339
|
+
const route = slugify(routeInput);
|
|
1340
|
+
if (!route) {
|
|
1341
|
+
throw new DeployAbort({
|
|
1342
|
+
code: "ROUTE_REQUIRED",
|
|
1343
|
+
message: `Couldn't derive a route from "${routeInput}" \u2014 use letters and numbers (e.g. my-site).`,
|
|
1344
|
+
retryable: true
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
try {
|
|
1348
|
+
const updated = await updateAppRoute(app.id, route);
|
|
1349
|
+
return {
|
|
1350
|
+
app_id: updated.id,
|
|
1351
|
+
name: updated.name,
|
|
1352
|
+
route: updated.route ?? route
|
|
1353
|
+
};
|
|
1354
|
+
} catch (err) {
|
|
1355
|
+
throw new DeployAbort({
|
|
1356
|
+
code: "UPDATE_FAILED",
|
|
1357
|
+
message: err instanceof Error ? err.message : "Failed to set the route",
|
|
1358
|
+
retryable: true
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
async function buildAppGroup() {
|
|
1363
|
+
const app = Cli_exports.create("apps", {
|
|
1364
|
+
description: "Build and deploy fully-hosted web apps on Whop (*.whop.app)"
|
|
1365
|
+
});
|
|
1366
|
+
await registerSpecCommands(app, "Apps");
|
|
1367
|
+
const builds = Cli_exports.create("builds", {
|
|
1368
|
+
description: "Manage app builds (list, inspect, promote)"
|
|
1369
|
+
});
|
|
1370
|
+
await registerSpecCommands(builds, "App builds");
|
|
1371
|
+
app.command(builds);
|
|
1372
|
+
app.command("dev", {
|
|
1373
|
+
description: "Run the local dev server for this app",
|
|
1374
|
+
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.",
|
|
1375
|
+
options: external_exports.object({
|
|
1376
|
+
dir: external_exports.string().optional().describe("Project directory (defaults to the current directory)")
|
|
1377
|
+
}),
|
|
907
1378
|
run: async (c2) => {
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
if (s) s.stop("");
|
|
912
|
-
if (!resolved.ok) {
|
|
1379
|
+
const projectDir = c2.options.dir ? resolve2(c2.options.dir) : findProjectDir();
|
|
1380
|
+
const config = projectDir ? readAppConfig(projectDir) : null;
|
|
1381
|
+
if (!projectDir || !config) {
|
|
913
1382
|
return c2.error({
|
|
914
|
-
code:
|
|
915
|
-
message:
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
options: { product_id: p.id },
|
|
925
|
-
description: p.title
|
|
926
|
-
}))
|
|
927
|
-
} : void 0
|
|
1383
|
+
code: "NO_PROJECT",
|
|
1384
|
+
message: `No ${APP_CONFIG_FILENAME} found in this directory or any parent. Run \`whop apps deploy\` to link this project to a Whop app.`,
|
|
1385
|
+
cta: {
|
|
1386
|
+
commands: [
|
|
1387
|
+
{
|
|
1388
|
+
command: "apps deploy",
|
|
1389
|
+
description: "Link this project to a Whop app and ship it"
|
|
1390
|
+
}
|
|
1391
|
+
]
|
|
1392
|
+
}
|
|
928
1393
|
});
|
|
929
1394
|
}
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1395
|
+
const injected = {
|
|
1396
|
+
WHOP_APP_ID: config.app_id
|
|
1397
|
+
};
|
|
1398
|
+
if (process.env.WHOP_API_KEY) {
|
|
1399
|
+
log(
|
|
1400
|
+
c2,
|
|
1401
|
+
chalk.dim(
|
|
1402
|
+
"Using the WHOP_API_KEY from your environment for server-side calls."
|
|
1403
|
+
)
|
|
933
1404
|
);
|
|
1405
|
+
} else {
|
|
1406
|
+
try {
|
|
1407
|
+
const minted = await createDevAccessToken();
|
|
1408
|
+
injected.WHOP_API_KEY = minted.token;
|
|
1409
|
+
const expiresAt = typeof minted.expires_at === "number" ? new Date(minted.expires_at * 1e3) : minted.expires_at ? new Date(minted.expires_at) : null;
|
|
1410
|
+
const expiry = expiresAt ? ` (expires ${expiresAt.toLocaleTimeString()})` : "";
|
|
1411
|
+
log(
|
|
1412
|
+
c2,
|
|
1413
|
+
chalk.dim(
|
|
1414
|
+
`Injected a temporary access token as WHOP_API_KEY${expiry} \u2014 server-side SDK calls just work. Restart \`whop apps dev\` to refresh it.`
|
|
1415
|
+
)
|
|
1416
|
+
);
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
log(
|
|
1419
|
+
c2,
|
|
1420
|
+
chalk.yellow(
|
|
1421
|
+
`Couldn't mint a temporary access token (${err instanceof Error ? err.message : "unknown error"}) \u2014 set WHOP_API_KEY manually for server-side calls.`
|
|
1422
|
+
)
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
934
1425
|
}
|
|
935
|
-
|
|
936
|
-
...buildBody(c2.options),
|
|
937
|
-
product_id: resolved.productId
|
|
938
|
-
});
|
|
939
|
-
let plan;
|
|
1426
|
+
log(c2, chalk.dim(`Starting dev server for ${config.name}...`));
|
|
940
1427
|
try {
|
|
941
|
-
|
|
1428
|
+
runScript(projectDir, "dev", injected);
|
|
942
1429
|
} catch (err) {
|
|
943
1430
|
return c2.error({
|
|
944
|
-
code: "
|
|
945
|
-
message: err instanceof Error ? err.message : "
|
|
946
|
-
retryable: true
|
|
1431
|
+
code: "DEV_FAILED",
|
|
1432
|
+
message: err instanceof Error ? err.message : "Dev server exited with an error"
|
|
947
1433
|
});
|
|
948
1434
|
}
|
|
949
|
-
|
|
950
|
-
const ctaDescription = purchaseUrl ? `\u{1F389} You can sell now. Share your checkout link: ${purchaseUrl}` : "Plan created.";
|
|
951
|
-
printCreateSuccess(c2, {
|
|
952
|
-
title: purchaseUrl ? "Plan created \u2014 you can sell now" : "Plan created",
|
|
953
|
-
id: plan.id,
|
|
954
|
-
checkoutUrl: purchaseUrl,
|
|
955
|
-
next: {
|
|
956
|
-
command: "checkout-configurations create",
|
|
957
|
-
hint: "Optional: customize the checkout page (currency, redirect, payment mode)"
|
|
958
|
-
}
|
|
959
|
-
});
|
|
960
|
-
return c2.ok(plan, {
|
|
961
|
-
cta: {
|
|
962
|
-
description: ctaDescription,
|
|
963
|
-
commands: [
|
|
964
|
-
{ command: "quickstart", description: "See your setup status and what's next" },
|
|
965
|
-
{
|
|
966
|
-
command: "checkout-configurations create",
|
|
967
|
-
options: { plan_id: plan.id },
|
|
968
|
-
description: "Optional: customize the checkout page"
|
|
969
|
-
}
|
|
970
|
-
]
|
|
971
|
-
}
|
|
972
|
-
});
|
|
1435
|
+
return c2.ok({ success: true });
|
|
973
1436
|
}
|
|
974
1437
|
});
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
// src/commerce/checkout.ts
|
|
992
|
-
var CheckoutConfigSchema = external_exports.object({
|
|
993
|
-
id: external_exports.string(),
|
|
994
|
-
mode: external_exports.enum(["payment", "setup"]).optional(),
|
|
995
|
-
currency: external_exports.string().nullable().optional(),
|
|
996
|
-
purchase_url: external_exports.string().nullable().optional(),
|
|
997
|
-
redirect_url: external_exports.string().nullable().optional(),
|
|
998
|
-
company_id: external_exports.string().optional(),
|
|
999
|
-
created_at: external_exports.number().optional()
|
|
1000
|
-
});
|
|
1001
|
-
var createOptions3 = external_exports.object({
|
|
1002
|
-
plan_id: external_exports.string().optional().describe("Plan to attach this checkout to (plan_xxx)"),
|
|
1003
|
-
mode: external_exports.enum(["payment", "setup"]).optional().describe("payment = charge immediately; setup = save payment method only"),
|
|
1004
|
-
currency: external_exports.string().optional().describe("3-letter currency code override"),
|
|
1005
|
-
redirect_url: external_exports.string().optional().describe("URL to redirect after successful checkout"),
|
|
1006
|
-
affiliate_code: external_exports.string().optional().describe("Affiliate code to apply")
|
|
1007
|
-
});
|
|
1008
|
-
function buildCheckoutGroup() {
|
|
1009
|
-
const checkout = Cli_exports.create("checkout-configurations", {
|
|
1010
|
-
description: "Checkout configurations \u2014 customize your checkout page"
|
|
1011
|
-
});
|
|
1012
|
-
checkout.command("create", {
|
|
1013
|
-
description: "Create a checkout configuration",
|
|
1014
|
-
hint: "Customize the currency, redirect URL, and payment mode for a plan's checkout page.",
|
|
1015
|
-
options: createOptions3,
|
|
1016
|
-
output: CheckoutConfigSchema,
|
|
1438
|
+
app.command("deploy", {
|
|
1439
|
+
description: "Build, upload and ship this app live",
|
|
1440
|
+
hint: "The one-stop deploy: verifies the project is a Whop-ready Vite app (offering to scaffold or link one if not), builds, typechecks, uploads the build, and promotes it to production. Use --skip_promote to upload a preview-only build, then promote it later with `whop apps builds promote <build_id>`.",
|
|
1441
|
+
options: external_exports.object({
|
|
1442
|
+
dir: external_exports.string().optional().describe("Project directory (defaults to the current directory)"),
|
|
1443
|
+
app: external_exports.string().optional().describe(
|
|
1444
|
+
"Link this project to an app (app_xxx) before deploying \u2014 writes whop.app.json, replacing any existing link"
|
|
1445
|
+
),
|
|
1446
|
+
skip_promote: external_exports.boolean().optional().describe(
|
|
1447
|
+
"Upload the build without promoting it to production"
|
|
1448
|
+
),
|
|
1449
|
+
skip_typecheck: external_exports.boolean().optional().describe("Skip the typecheck step"),
|
|
1450
|
+
skip_build: external_exports.boolean().optional().describe("Skip the build step and upload the existing dist/ output")
|
|
1451
|
+
}),
|
|
1452
|
+
output: BuildSchema,
|
|
1017
1453
|
outputPolicy: "agent-only",
|
|
1018
1454
|
examples: [
|
|
1455
|
+
{ description: "Build and ship to production" },
|
|
1019
1456
|
{
|
|
1020
|
-
options: {
|
|
1021
|
-
description: "
|
|
1457
|
+
options: { app: "app_xxxxxxxx" },
|
|
1458
|
+
description: "Link this project to an existing app, then deploy"
|
|
1022
1459
|
},
|
|
1023
1460
|
{
|
|
1024
|
-
options: {
|
|
1025
|
-
description: "
|
|
1461
|
+
options: { skip_promote: true },
|
|
1462
|
+
description: "Upload a preview build without promoting"
|
|
1026
1463
|
}
|
|
1027
1464
|
],
|
|
1028
1465
|
run: async (c2) => {
|
|
1029
|
-
const body = buildBody(c2.options);
|
|
1030
|
-
let config;
|
|
1031
1466
|
try {
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1467
|
+
const pm = ensurePackageManager();
|
|
1468
|
+
const { projectDir, freshlyScaffolded } = await ensureViteApp(
|
|
1469
|
+
c2,
|
|
1470
|
+
pm,
|
|
1471
|
+
c2.options.dir
|
|
1472
|
+
);
|
|
1473
|
+
const config = await ensureAppLinked(c2, projectDir, c2.options.app);
|
|
1474
|
+
const { manualSteps } = whopifyProject({
|
|
1475
|
+
targetDir: projectDir,
|
|
1476
|
+
route: config.route
|
|
1477
|
+
});
|
|
1478
|
+
if (manualSteps.length > 0) {
|
|
1479
|
+
return c2.error({
|
|
1480
|
+
code: "WHOPIFY_INCOMPLETE",
|
|
1481
|
+
message: `${config.name} is linked (${config.app_id}), but the project isn't fully wired for Whop hosting yet. Complete these steps:
|
|
1482
|
+
|
|
1483
|
+
${formatManualSteps(manualSteps)}
|
|
1484
|
+
|
|
1485
|
+
Then re-run \`whop apps deploy\` \u2014 it re-checks the wiring and applies anything it can.`
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
if (!c2.options.skip_build) {
|
|
1489
|
+
if (freshlyScaffolded || !existsSync3(join3(projectDir, "node_modules", "@whop", "cli"))) {
|
|
1490
|
+
runInstall(c2, projectDir);
|
|
1491
|
+
}
|
|
1492
|
+
log(c2, chalk.dim("[1/4] Building..."));
|
|
1493
|
+
runScript(projectDir, "build", { WHOP_APP_ID: config.app_id });
|
|
1494
|
+
if (!c2.options.skip_typecheck && hasScript(projectDir, "typecheck")) {
|
|
1495
|
+
log(c2, chalk.dim("[2/4] Typechecking..."));
|
|
1496
|
+
runScript(projectDir, "typecheck");
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
const archivePath = join3(projectDir, BUILD_ARCHIVE);
|
|
1500
|
+
if (!existsSync3(archivePath)) {
|
|
1501
|
+
return c2.error({
|
|
1502
|
+
code: "NO_BUILD_ARCHIVE",
|
|
1503
|
+
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.`
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
const zip = readFileSync3(archivePath);
|
|
1507
|
+
const sizeMb = (zip.byteLength / 1024 / 1024).toFixed(1);
|
|
1508
|
+
const checksum = createHash("sha256").update(zip).digest("hex");
|
|
1509
|
+
const spin = c2.agent ? null : spinner();
|
|
1510
|
+
spin?.start(`[3/4] Uploading build archive (${sizeMb} MB)`);
|
|
1511
|
+
let build;
|
|
1512
|
+
try {
|
|
1513
|
+
const fileId = await uploadBuildArchive(
|
|
1514
|
+
new Uint8Array(zip),
|
|
1515
|
+
`${config.route}-build.zip`
|
|
1516
|
+
);
|
|
1517
|
+
spin?.message("[3/4] Processing archive");
|
|
1518
|
+
await waitForFileReady(fileId);
|
|
1519
|
+
spin?.message("[3/4] Creating build");
|
|
1520
|
+
build = await createAppBuild({
|
|
1521
|
+
app_id: config.app_id,
|
|
1522
|
+
checksum,
|
|
1523
|
+
file_id: fileId
|
|
1524
|
+
});
|
|
1525
|
+
spin?.stop(`[3/4] Build uploaded (${build.id})`);
|
|
1526
|
+
} catch (err) {
|
|
1527
|
+
spin?.error("[3/4] Upload failed");
|
|
1528
|
+
throw err;
|
|
1529
|
+
}
|
|
1530
|
+
if (!c2.options.skip_promote) {
|
|
1531
|
+
const promoteSpin = c2.agent ? null : spinner();
|
|
1532
|
+
promoteSpin?.start("[4/4] Promoting to production");
|
|
1533
|
+
try {
|
|
1534
|
+
await promoteAppBuild(build.id);
|
|
1535
|
+
build = await waitForBuildPromotion(build.id);
|
|
1536
|
+
promoteSpin?.stop(
|
|
1537
|
+
build.is_production ? "[4/4] Promoted to production" : `[4/4] Promotion pending (status: ${build.status})`
|
|
1538
|
+
);
|
|
1539
|
+
} catch (err) {
|
|
1540
|
+
promoteSpin?.error("[4/4] Promote failed");
|
|
1541
|
+
throw err;
|
|
1542
|
+
}
|
|
1543
|
+
} else {
|
|
1544
|
+
log(c2, chalk.dim("[4/4] Skipping promotion (--skip_promote)"));
|
|
1545
|
+
}
|
|
1546
|
+
const promoted = build.is_production === true;
|
|
1547
|
+
const productionUrl = promoted ? (await getApp(config.app_id)).hosted_url ?? void 0 : void 0;
|
|
1548
|
+
if (!c2.agent && !c2.formatExplicit) {
|
|
1549
|
+
const cdTarget = relative(process.cwd(), projectDir) || ".";
|
|
1550
|
+
console.log(
|
|
1551
|
+
[
|
|
1552
|
+
"",
|
|
1553
|
+
chalk.green.bold(
|
|
1554
|
+
promoted ? "\u2713 Deployed to production" : "\u2713 Build uploaded"
|
|
1555
|
+
),
|
|
1556
|
+
` ${chalk.dim(build.id)}`,
|
|
1557
|
+
"",
|
|
1558
|
+
...promoted && productionUrl ? [
|
|
1559
|
+
` ${chalk.bold("Live")} ${chalk.cyan.underline(productionUrl)}`
|
|
1560
|
+
] : [
|
|
1561
|
+
`${chalk.bold("Promote when ready")} ${chalk.cyan.bold(`whop apps builds promote ${build.id}`)}`
|
|
1562
|
+
],
|
|
1563
|
+
...freshlyScaffolded ? [
|
|
1564
|
+
"",
|
|
1565
|
+
chalk.bold("Next"),
|
|
1566
|
+
...cdTarget !== "." ? [` cd ${cdTarget}`] : [],
|
|
1567
|
+
` ${chalk.cyan.bold("whop apps dev")} ${chalk.dim("\u2192 local dev server")}`,
|
|
1568
|
+
` ${chalk.cyan.bold("whop apps deploy")} ${chalk.dim("\u2192 ship your changes")}`
|
|
1569
|
+
] : [],
|
|
1570
|
+
""
|
|
1571
|
+
].join("\n")
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
return c2.ok(
|
|
1575
|
+
{
|
|
1576
|
+
id: build.id,
|
|
1577
|
+
status: build.status,
|
|
1578
|
+
is_production: promoted,
|
|
1579
|
+
url: productionUrl
|
|
1580
|
+
},
|
|
1581
|
+
promoted ? void 0 : {
|
|
1582
|
+
cta: {
|
|
1583
|
+
commands: [
|
|
1584
|
+
{
|
|
1585
|
+
command: `apps builds promote ${build.id}`,
|
|
1586
|
+
description: "Promote this build to production"
|
|
1587
|
+
}
|
|
1588
|
+
]
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1036
1591
|
);
|
|
1037
1592
|
} catch (err) {
|
|
1593
|
+
if (err instanceof DeployAbort) return c2.error(err.options);
|
|
1038
1594
|
return c2.error({
|
|
1039
|
-
code: "
|
|
1040
|
-
message: err instanceof Error ? err.message : "
|
|
1595
|
+
code: "DEPLOY_FAILED",
|
|
1596
|
+
message: err instanceof Error ? err.message : "Deploy failed",
|
|
1041
1597
|
retryable: true
|
|
1042
1598
|
});
|
|
1043
1599
|
}
|
|
1044
|
-
const purchaseUrl = config.purchase_url;
|
|
1045
|
-
printCreateSuccess(c2, {
|
|
1046
|
-
title: "Checkout configured",
|
|
1047
|
-
id: config.id,
|
|
1048
|
-
checkoutUrl: purchaseUrl,
|
|
1049
|
-
next: { command: "quickstart", hint: "See your full setup status" }
|
|
1050
|
-
});
|
|
1051
|
-
return c2.ok(config, {
|
|
1052
|
-
cta: {
|
|
1053
|
-
description: purchaseUrl ? `Checkout configured. Share your link: ${purchaseUrl}` : "Checkout configured.",
|
|
1054
|
-
commands: [
|
|
1055
|
-
{ command: "quickstart", description: "See your full setup status" }
|
|
1056
|
-
]
|
|
1057
|
-
}
|
|
1058
|
-
});
|
|
1059
1600
|
}
|
|
1060
1601
|
});
|
|
1061
|
-
|
|
1062
|
-
path: "/checkout_configurations",
|
|
1063
|
-
description: "List checkout configurations",
|
|
1064
|
-
filter: { option: "plan_id", param: "plan_id", describe: "Filter by plan" }
|
|
1065
|
-
});
|
|
1066
|
-
registerItemCommands(checkout, {
|
|
1067
|
-
path: "/checkout_configurations",
|
|
1068
|
-
noun: "checkout configuration",
|
|
1069
|
-
idLabel: "Checkout configuration ID",
|
|
1070
|
-
schema: CheckoutConfigSchema
|
|
1071
|
-
});
|
|
1072
|
-
return checkout;
|
|
1602
|
+
return app;
|
|
1073
1603
|
}
|
|
1074
1604
|
|
|
1075
1605
|
// src/journey/quickstart.ts
|
|
1076
1606
|
import chalk3 from "chalk";
|
|
1077
|
-
import { text as text2, select as select2, isCancel as
|
|
1607
|
+
import { text as text2, select as select2, isCancel as isCancel2 } from "@clack/prompts";
|
|
1608
|
+
|
|
1609
|
+
// src/journey/billing.ts
|
|
1610
|
+
var BILLING_PERIOD_DAYS = {
|
|
1611
|
+
daily: 1,
|
|
1612
|
+
weekly: 7,
|
|
1613
|
+
monthly: 30,
|
|
1614
|
+
quarterly: 90,
|
|
1615
|
+
annually: 365,
|
|
1616
|
+
every_two_years: 730
|
|
1617
|
+
};
|
|
1078
1618
|
|
|
1079
1619
|
// src/lib/colors.ts
|
|
1080
1620
|
import chalk2 from "chalk";
|
|
@@ -1225,7 +1765,7 @@ ${chalk3.bold("You don't have a business account yet \u2014 let's create one.")}
|
|
|
1225
1765
|
message: "What's your business called?",
|
|
1226
1766
|
placeholder: "My Business"
|
|
1227
1767
|
});
|
|
1228
|
-
if (
|
|
1768
|
+
if (isCancel2(name)) {
|
|
1229
1769
|
return c2.error({
|
|
1230
1770
|
code: "CANCELLED",
|
|
1231
1771
|
message: "Setup cancelled.",
|
|
@@ -1316,7 +1856,7 @@ async function resolveOrPrompt(c2, provided, missing, prompt) {
|
|
|
1316
1856
|
});
|
|
1317
1857
|
}
|
|
1318
1858
|
const value = await prompt();
|
|
1319
|
-
if (
|
|
1859
|
+
if (isCancel2(value))
|
|
1320
1860
|
throw new QuickstartError({
|
|
1321
1861
|
code: "CANCELLED",
|
|
1322
1862
|
message: "Setup cancelled.",
|
|
@@ -1340,7 +1880,7 @@ ${chalk3.bold("Let's get you set up to sell.")}
|
|
|
1340
1880
|
{ value: "new", label: "Create a new business account" }
|
|
1341
1881
|
]
|
|
1342
1882
|
});
|
|
1343
|
-
if (
|
|
1883
|
+
if (isCancel2(choice)) {
|
|
1344
1884
|
throw new QuickstartError({
|
|
1345
1885
|
code: "CANCELLED",
|
|
1346
1886
|
message: "Setup cancelled.",
|
|
@@ -1352,7 +1892,7 @@ ${chalk3.bold("Let's get you set up to sell.")}
|
|
|
1352
1892
|
message: "What's your new business called?",
|
|
1353
1893
|
placeholder: "My Business"
|
|
1354
1894
|
});
|
|
1355
|
-
if (
|
|
1895
|
+
if (isCancel2(name)) {
|
|
1356
1896
|
throw new QuickstartError({
|
|
1357
1897
|
code: "CANCELLED",
|
|
1358
1898
|
message: "Setup cancelled.",
|
|
@@ -1392,7 +1932,7 @@ ${chalk3.bold("Let's get you set up to sell.")}
|
|
|
1392
1932
|
placeholder: "9.99",
|
|
1393
1933
|
validate: (v) => isNaN(Number(v)) ? "Enter a number" : void 0
|
|
1394
1934
|
});
|
|
1395
|
-
return
|
|
1935
|
+
return isCancel2(p) ? p : Number(p);
|
|
1396
1936
|
}
|
|
1397
1937
|
);
|
|
1398
1938
|
const billingPeriod = await resolveOrPrompt(
|
|
@@ -1578,7 +2118,7 @@ async function resolveExistingProduct(c2, products) {
|
|
|
1578
2118
|
hint: p.id
|
|
1579
2119
|
}))
|
|
1580
2120
|
});
|
|
1581
|
-
if (
|
|
2121
|
+
if (isCancel2(chosen))
|
|
1582
2122
|
throw new QuickstartError({
|
|
1583
2123
|
code: "CANCELLED",
|
|
1584
2124
|
message: "Setup cancelled.",
|
|
@@ -1678,7 +2218,7 @@ ${c.success("\u2713 Your store is fully set up.")}`);
|
|
|
1678
2218
|
// package.json
|
|
1679
2219
|
var package_default = {
|
|
1680
2220
|
name: "@whop/cli",
|
|
1681
|
-
version: "0.
|
|
2221
|
+
version: "0.4.0",
|
|
1682
2222
|
description: "The Whop CLI \u2014 build and manage Whop apps from your terminal. Human and agent friendly.",
|
|
1683
2223
|
keywords: [
|
|
1684
2224
|
"agent",
|
|
@@ -1702,6 +2242,13 @@ var package_default = {
|
|
|
1702
2242
|
"dist"
|
|
1703
2243
|
],
|
|
1704
2244
|
type: "module",
|
|
2245
|
+
exports: {
|
|
2246
|
+
".": "./dist/index.js",
|
|
2247
|
+
"./vite": {
|
|
2248
|
+
types: "./dist/vite.d.ts",
|
|
2249
|
+
import: "./dist/vite.js"
|
|
2250
|
+
}
|
|
2251
|
+
},
|
|
1705
2252
|
publishConfig: {
|
|
1706
2253
|
access: "public"
|
|
1707
2254
|
},
|
|
@@ -1728,10 +2275,12 @@ var package_default = {
|
|
|
1728
2275
|
},
|
|
1729
2276
|
devDependencies: {
|
|
1730
2277
|
"@types/node": "25.3.5",
|
|
1731
|
-
|
|
2278
|
+
fflate: "0.8.2",
|
|
2279
|
+
incur: "github:whopio/incur#4b9ca91dc02472db944fb103984ffc4a13251b6f",
|
|
1732
2280
|
tsup: "8.5.0",
|
|
1733
2281
|
tsx: "4.19.4",
|
|
1734
|
-
typescript: "5.9.3"
|
|
2282
|
+
typescript: "5.9.3",
|
|
2283
|
+
vite: "7.3.6"
|
|
1735
2284
|
},
|
|
1736
2285
|
engines: {
|
|
1737
2286
|
node: ">=22"
|
|
@@ -1817,7 +2366,7 @@ var DEFAULT_MANIFEST_URL = "https://github.com/whopio/whop-public-cli/releases/l
|
|
|
1817
2366
|
function manifestUrl(env = process.env) {
|
|
1818
2367
|
return env.WHOP_CLI_MANIFEST_URL?.trim() || DEFAULT_MANIFEST_URL;
|
|
1819
2368
|
}
|
|
1820
|
-
var CHECK_INTERVAL_MS =
|
|
2369
|
+
var CHECK_INTERVAL_MS = 60 * 60 * 1e3;
|
|
1821
2370
|
function cachePath(env) {
|
|
1822
2371
|
return path.join(configDir(env), "update-check.json");
|
|
1823
2372
|
}
|
|
@@ -2184,22 +2733,10 @@ var HANDWRITTEN_GROUPS = [
|
|
|
2184
2733
|
})
|
|
2185
2734
|
},
|
|
2186
2735
|
{
|
|
2187
|
-
name: "
|
|
2188
|
-
description: "
|
|
2189
|
-
section: "
|
|
2190
|
-
register: (cli2) => cli2.command(
|
|
2191
|
-
},
|
|
2192
|
-
{
|
|
2193
|
-
name: "plans",
|
|
2194
|
-
description: "Pricing for your products",
|
|
2195
|
-
section: "commerce",
|
|
2196
|
-
register: (cli2) => cli2.command(buildPlansGroup())
|
|
2197
|
-
},
|
|
2198
|
-
{
|
|
2199
|
-
name: "checkout-configurations",
|
|
2200
|
-
description: "Optional / advanced: customize the checkout page",
|
|
2201
|
-
section: "commerce",
|
|
2202
|
-
register: (cli2) => cli2.command(buildCheckoutGroup())
|
|
2736
|
+
name: "apps",
|
|
2737
|
+
description: "Build and deploy fully-hosted web apps (*.whop.app)",
|
|
2738
|
+
section: "get-started",
|
|
2739
|
+
register: async (cli2) => cli2.command(await buildAppGroup())
|
|
2203
2740
|
},
|
|
2204
2741
|
{
|
|
2205
2742
|
name: "upgrade",
|
|
@@ -2256,8 +2793,8 @@ var HANDWRITTEN_GROUP_NAMES = new Set(
|
|
|
2256
2793
|
);
|
|
2257
2794
|
var API_GROUPS = groups_default.map(([name, tag]) => ({ name, tag })).filter(({ name }) => !HANDWRITTEN_GROUP_NAMES.has(name));
|
|
2258
2795
|
var COMMAND_GROUPS = [...API_GROUPS, ...HANDWRITTEN_GROUPS];
|
|
2259
|
-
function registerHandwrittenGroups(cli2) {
|
|
2260
|
-
for (const group of HANDWRITTEN_GROUPS) group.register(cli2);
|
|
2796
|
+
async function registerHandwrittenGroups(cli2) {
|
|
2797
|
+
for (const group of HANDWRITTEN_GROUPS) await group.register(cli2);
|
|
2261
2798
|
}
|
|
2262
2799
|
async function setupAgents(cli2) {
|
|
2263
2800
|
for (const argv2 of [
|
|
@@ -2398,7 +2935,7 @@ cli.use(async (c2, next) => {
|
|
|
2398
2935
|
}
|
|
2399
2936
|
return next();
|
|
2400
2937
|
});
|
|
2401
|
-
registerHandwrittenGroups(cli);
|
|
2938
|
+
await registerHandwrittenGroups(cli);
|
|
2402
2939
|
for (const { name, tag } of API_GROUPS) {
|
|
2403
2940
|
cli.command(name, { fetch: fetch2, openapi: spec(tag) });
|
|
2404
2941
|
}
|
|
@@ -2473,9 +3010,9 @@ var tag_descriptions_default = {
|
|
|
2473
3010
|
Deposits: "Add funds to your balance",
|
|
2474
3011
|
Swaps: "Convert between currencies",
|
|
2475
3012
|
Verifications: "Identity verification status",
|
|
2476
|
-
Products: "
|
|
2477
|
-
Plans: "
|
|
2478
|
-
"Checkout Configurations": "
|
|
3013
|
+
Products: "What you're selling",
|
|
3014
|
+
Plans: "Pricing for your products",
|
|
3015
|
+
"Checkout Configurations": "Optional / advanced: customize the checkout page",
|
|
2479
3016
|
Referrals: "Referral programs",
|
|
2480
3017
|
Ads: "Ad creatives",
|
|
2481
3018
|
"Ad Campaigns": "Top-level campaign organization",
|
|
@@ -2489,7 +3026,6 @@ var descriptions = tag_descriptions_default;
|
|
|
2489
3026
|
var toCliName = (tag) => tag.toLowerCase().replace(/\s+/g, "-");
|
|
2490
3027
|
var SECTION_LABELS = {
|
|
2491
3028
|
"get-started": "Get Started",
|
|
2492
|
-
commerce: "Commerce",
|
|
2493
3029
|
auth: "Auth"
|
|
2494
3030
|
};
|
|
2495
3031
|
var INTEGRATIONS = [
|
|
@@ -2532,16 +3068,16 @@ function buildGroupedHelp(version, description) {
|
|
|
2532
3068
|
description: descriptions[tag] ?? ""
|
|
2533
3069
|
}))
|
|
2534
3070
|
})).filter((section) => section.commands.length > 0);
|
|
2535
|
-
const
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
const
|
|
3071
|
+
const commerceSections = apiSections.filter(
|
|
3072
|
+
(section) => section.label.toLowerCase() === "commerce"
|
|
3073
|
+
);
|
|
3074
|
+
const otherApiSections = apiSections.filter(
|
|
2539
3075
|
(section) => section.label.toLowerCase() !== "commerce"
|
|
2540
3076
|
);
|
|
2541
3077
|
const allSections = [
|
|
2542
3078
|
handwrittenSection("get-started"),
|
|
2543
|
-
|
|
2544
|
-
...
|
|
3079
|
+
...commerceSections,
|
|
3080
|
+
...otherApiSections,
|
|
2545
3081
|
handwrittenSection("auth")
|
|
2546
3082
|
].filter((section) => section.commands.length > 0);
|
|
2547
3083
|
const maxCmdLen = Math.max(
|