@whop/cli 0.2.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-U7WU3JXH.js → api-A6NYSDKW.js} +2 -2
- package/dist/{chunk-2Y4EKBOD.js → chunk-4WNH276N.js} +210 -40
- package/dist/chunk-4WNH276N.js.map +1 -0
- package/dist/index.js +1102 -428
- 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-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,
|
|
@@ -32,7 +33,7 @@ import {
|
|
|
32
33
|
switchProfile,
|
|
33
34
|
upsertProfile,
|
|
34
35
|
validateApiKey
|
|
35
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-4WNH276N.js";
|
|
36
37
|
import {
|
|
37
38
|
external_exports
|
|
38
39
|
} from "./chunk-KFCNNWPI.js";
|
|
@@ -311,7 +312,7 @@ async function loginAdapter(c2) {
|
|
|
311
312
|
const accountId = getActiveProfile()?.accountId ?? "";
|
|
312
313
|
if (accountId) {
|
|
313
314
|
try {
|
|
314
|
-
const { createWhopFetch: createWhopFetch2 } = await import("./api-
|
|
315
|
+
const { createWhopFetch: createWhopFetch2 } = await import("./api-A6NYSDKW.js");
|
|
315
316
|
const fetch3 = createWhopFetch2();
|
|
316
317
|
const res = await fetch3(
|
|
317
318
|
new Request(
|
|
@@ -590,10 +591,15 @@ function buildAuthGroup() {
|
|
|
590
591
|
return auth;
|
|
591
592
|
}
|
|
592
593
|
|
|
593
|
-
// src/
|
|
594
|
-
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";
|
|
595
601
|
|
|
596
|
-
// src/
|
|
602
|
+
// src/journey/client.ts
|
|
597
603
|
var PLACEHOLDER = "https://api.whop.com";
|
|
598
604
|
var whopFetch = createWhopFetch();
|
|
599
605
|
async function makeWhopRequest(method, path3, body) {
|
|
@@ -608,473 +614,1145 @@ async function makeWhopRequest(method, path3, body) {
|
|
|
608
614
|
return text3 ? JSON.parse(text3) : { success: true };
|
|
609
615
|
}
|
|
610
616
|
|
|
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
|
-
}
|
|
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()
|
|
641
628
|
});
|
|
642
629
|
}
|
|
643
|
-
function
|
|
644
|
-
const
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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 }
|
|
651
651
|
});
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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"
|
|
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
|
|
668
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));
|
|
669
717
|
}
|
|
670
718
|
|
|
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}`)}`);
|
|
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;
|
|
684
730
|
}
|
|
685
|
-
|
|
686
|
-
|
|
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;
|
|
746
|
+
}
|
|
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);
|
|
687
757
|
}
|
|
688
758
|
|
|
689
|
-
// src/
|
|
690
|
-
|
|
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({
|
|
691
966
|
id: external_exports.string(),
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
member_count: external_exports.number().optional(),
|
|
698
|
-
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()
|
|
699
972
|
});
|
|
700
|
-
var
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
)
|
|
705
|
-
visibility: external_exports.enum(["visible", "hidden", "archived"]).optional().describe("Who can see this product"),
|
|
706
|
-
headline: external_exports.string().optional().describe("Short tagline shown on the store page"),
|
|
707
|
-
description: external_exports.string().optional().describe("Full description"),
|
|
708
|
-
route: external_exports.string().optional().describe("Custom URL slug (e.g. my-course)"),
|
|
709
|
-
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()
|
|
710
978
|
});
|
|
711
|
-
function
|
|
712
|
-
|
|
713
|
-
|
|
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)"
|
|
714
1233
|
});
|
|
715
|
-
|
|
716
|
-
description: "Create a
|
|
717
|
-
hint: "
|
|
718
|
-
|
|
719
|
-
|
|
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,
|
|
720
1262
|
outputPolicy: "agent-only",
|
|
721
1263
|
examples: [
|
|
722
1264
|
{
|
|
723
|
-
|
|
724
|
-
|
|
1265
|
+
args: { name: "My Site" },
|
|
1266
|
+
options: { route: "my-site" },
|
|
1267
|
+
description: "Scaffold 'My Site' into ./my-site, live at my-site.whop.app"
|
|
725
1268
|
},
|
|
726
1269
|
{
|
|
727
|
-
|
|
728
|
-
|
|
1270
|
+
args: { name: "My Site" },
|
|
1271
|
+
options: { existing: true },
|
|
1272
|
+
description: "Register + wire up the Vite project in the current directory"
|
|
729
1273
|
}
|
|
730
1274
|
],
|
|
731
1275
|
run: async (c2) => {
|
|
732
|
-
|
|
733
|
-
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) {
|
|
734
1309
|
if (c2.agent) {
|
|
735
1310
|
return c2.error({
|
|
736
|
-
code: "
|
|
737
|
-
message:
|
|
1311
|
+
code: "NAME_REQUIRED",
|
|
1312
|
+
message: 'Pass a name: `whop apps create "My Site"`.',
|
|
738
1313
|
retryable: true
|
|
739
1314
|
});
|
|
740
1315
|
}
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
placeholder: "e.g. My Online Course, Trading Community, Fitness Program"
|
|
744
|
-
});
|
|
745
|
-
if (isCancel(t))
|
|
1316
|
+
const value = await promptText("What is your app called?", "My Site");
|
|
1317
|
+
if (value === null)
|
|
746
1318
|
return c2.error({
|
|
747
1319
|
code: "CANCELLED",
|
|
748
1320
|
message: "Cancelled.",
|
|
749
1321
|
exitCode: 130
|
|
750
1322
|
});
|
|
751
|
-
|
|
1323
|
+
name = value;
|
|
752
1324
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
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)
|
|
760
1331
|
);
|
|
761
|
-
|
|
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) {
|
|
762
1341
|
return c2.error({
|
|
763
|
-
code: "
|
|
764
|
-
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).`,
|
|
765
1344
|
retryable: true
|
|
766
1345
|
});
|
|
767
1346
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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
|
+
});
|
|
774
1361
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
{
|
|
781
|
-
command: "plans create",
|
|
782
|
-
options: { product_id: product.id },
|
|
783
|
-
description: "Create a pricing plan (required before checkout)"
|
|
784
|
-
}
|
|
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" }
|
|
785
1368
|
]
|
|
1369
|
+
});
|
|
1370
|
+
if (isCancel(choice)) {
|
|
1371
|
+
return c2.error({
|
|
1372
|
+
code: "CANCELLED",
|
|
1373
|
+
message: "Cancelled.",
|
|
1374
|
+
exitCode: 130
|
|
1375
|
+
});
|
|
786
1376
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
registerListCommand(products, {
|
|
791
|
-
path: "/products",
|
|
792
|
-
description: "List products"
|
|
793
|
-
});
|
|
794
|
-
registerItemCommands(products, {
|
|
795
|
-
path: "/products",
|
|
796
|
-
noun: "product",
|
|
797
|
-
idLabel: "Product ID (prod_xxx)",
|
|
798
|
-
schema: ProductSchema,
|
|
799
|
-
updateOptions: createOptions.partial()
|
|
800
|
-
});
|
|
801
|
-
return products;
|
|
802
|
-
}
|
|
1377
|
+
return c2.error({
|
|
1378
|
+
code: "NO_PACKAGE_MANAGER",
|
|
1379
|
+
message: `Install ${String(choice)} with:
|
|
803
1380
|
|
|
804
|
-
|
|
805
|
-
import { select, isCancel as isCancel2, spinner } from "@clack/prompts";
|
|
1381
|
+
${pmInstallHint(choice)}
|
|
806
1382
|
|
|
807
|
-
|
|
808
|
-
|
|
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({
|
|
819
|
-
id: external_exports.string(),
|
|
820
|
-
title: external_exports.string().nullable().optional(),
|
|
821
|
-
plan_type: external_exports.string().optional(),
|
|
822
|
-
billing_period: external_exports.string().nullable().optional(),
|
|
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()
|
|
829
|
-
});
|
|
830
|
-
var createOptions2 = external_exports.object({
|
|
831
|
-
product_id: external_exports.string().optional().describe("Product this plan belongs to (prod_xxx)"),
|
|
832
|
-
title: external_exports.string().optional().describe("Display name for this plan"),
|
|
833
|
-
plan_type: external_exports.enum(["one_time", "renewal", "free"]).optional().describe("Billing model \u2014 one_time, renewal, or free"),
|
|
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;
|
|
847
|
-
}
|
|
848
|
-
return body;
|
|
849
|
-
}
|
|
850
|
-
async function resolveProductId(providedId, isAgent) {
|
|
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;
|
|
856
|
-
try {
|
|
857
|
-
const res = await makeWhopRequest("GET", `/products?${params}`);
|
|
858
|
-
data = res.data ?? [];
|
|
859
|
-
} catch {
|
|
860
|
-
return { ok: false, code: "API_ERROR", message: "Failed to fetch products" };
|
|
861
|
-
}
|
|
862
|
-
if (data.length === 0) {
|
|
863
|
-
return {
|
|
864
|
-
ok: false,
|
|
865
|
-
code: "NO_PRODUCTS",
|
|
866
|
-
message: "No products found. Create one first."
|
|
867
|
-
};
|
|
868
|
-
}
|
|
869
|
-
if (data.length === 1) {
|
|
870
|
-
return { ok: true, productId: data[0].id, autoResolved: true };
|
|
871
|
-
}
|
|
872
|
-
if (isAgent) {
|
|
873
|
-
return { ok: false, code: "PRODUCT_REQUIRED", message: "Multiple products found. Pass --product_id.", candidates: data };
|
|
874
|
-
}
|
|
875
|
-
const chosen = await select({
|
|
876
|
-
message: "Which product is this plan for?",
|
|
877
|
-
options: data.map((p) => ({ value: p.id, label: p.title, hint: p.id }))
|
|
878
|
-
});
|
|
879
|
-
if (isCancel2(chosen)) return { ok: false, code: "CANCELLED", message: "Cancelled." };
|
|
880
|
-
return { ok: true, productId: chosen, autoResolved: false };
|
|
881
|
-
}
|
|
882
|
-
function buildPlansGroup() {
|
|
883
|
-
const plans = Cli_exports.create("plans", {
|
|
884
|
-
description: "Plans \u2014 pricing for your products"
|
|
885
|
-
});
|
|
886
|
-
plans.command("create", {
|
|
887
|
-
description: "Create a pricing plan",
|
|
888
|
-
hint: "A plan sets the price and billing interval. After creating a plan, your checkout link is ready to share.",
|
|
889
|
-
options: createOptions2,
|
|
890
|
-
output: PlanSchema,
|
|
891
|
-
outputPolicy: "agent-only",
|
|
892
|
-
examples: [
|
|
893
|
-
{
|
|
894
|
-
options: { product_id: "prod_xxx", initial_price: 9.99, plan_type: "one_time" },
|
|
895
|
-
description: "One-time payment of $9.99"
|
|
896
|
-
},
|
|
897
|
-
{
|
|
898
|
-
options: {
|
|
899
|
-
product_id: "prod_xxx",
|
|
900
|
-
initial_price: 19.99,
|
|
901
|
-
plan_type: "renewal",
|
|
902
|
-
billing_period: "monthly"
|
|
903
|
-
},
|
|
904
|
-
description: "Monthly subscription at $19.99"
|
|
1383
|
+
then re-run \`whop apps create\`.`
|
|
1384
|
+
});
|
|
905
1385
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
const s = c2.agent ? null : spinner();
|
|
909
|
-
if (s) s.start("Resolving product\u2026");
|
|
910
|
-
const resolved = await resolveProductId(c2.options.product_id, c2.agent);
|
|
911
|
-
if (s) s.stop("");
|
|
912
|
-
if (!resolved.ok) {
|
|
1386
|
+
const targetDir = resolve2(c2.options.dir ?? `./${route}`);
|
|
1387
|
+
if (existsSync3(targetDir) && readdirSync(targetDir).length > 0) {
|
|
913
1388
|
return c2.error({
|
|
914
|
-
code:
|
|
915
|
-
message:
|
|
916
|
-
retryable: resolved.code !== "CANCELLED",
|
|
917
|
-
cta: resolved.code === "NO_PRODUCTS" ? {
|
|
918
|
-
description: "Create a product first:",
|
|
919
|
-
commands: [{ command: "products create", description: "Create a product" }]
|
|
920
|
-
} : resolved.candidates ? {
|
|
921
|
-
description: "Available products:",
|
|
922
|
-
commands: resolved.candidates.map((p) => ({
|
|
923
|
-
command: "plans create",
|
|
924
|
-
options: { product_id: p.id },
|
|
925
|
-
description: p.title
|
|
926
|
-
}))
|
|
927
|
-
} : 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.`
|
|
928
1391
|
});
|
|
929
1392
|
}
|
|
930
|
-
|
|
931
|
-
console.error(
|
|
932
|
-
`Note: using product ${resolved.productId} \u2014 pass --product_id to override`
|
|
933
|
-
);
|
|
934
|
-
}
|
|
935
|
-
const body = normalizePlanBody({
|
|
936
|
-
...buildBody(c2.options),
|
|
937
|
-
product_id: resolved.productId
|
|
938
|
-
});
|
|
939
|
-
let plan;
|
|
1393
|
+
let whopApp;
|
|
940
1394
|
try {
|
|
941
|
-
|
|
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
|
+
}
|
|
942
1407
|
} catch (err) {
|
|
943
1408
|
return c2.error({
|
|
944
|
-
code: "
|
|
945
|
-
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",
|
|
946
1411
|
retryable: true
|
|
947
1412
|
});
|
|
948
1413
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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
|
+
);
|
|
971
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
|
|
972
1492
|
});
|
|
973
1493
|
}
|
|
974
1494
|
});
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
+
}
|
|
987
1510
|
});
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
+
}
|
|
1011
1562
|
});
|
|
1012
|
-
|
|
1013
|
-
description: "
|
|
1014
|
-
hint: "
|
|
1015
|
-
options:
|
|
1016
|
-
|
|
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,
|
|
1017
1575
|
outputPolicy: "agent-only",
|
|
1018
1576
|
examples: [
|
|
1577
|
+
{ description: "Build and ship to production" },
|
|
1019
1578
|
{
|
|
1020
|
-
options: {
|
|
1021
|
-
description: "
|
|
1022
|
-
},
|
|
1023
|
-
{
|
|
1024
|
-
options: { plan_id: "plan_xxx", redirect_url: "https://mysite.com/thank-you" },
|
|
1025
|
-
description: "Redirect buyers to a custom thank-you page"
|
|
1579
|
+
options: { promote: false },
|
|
1580
|
+
description: "Upload a preview build without promoting"
|
|
1026
1581
|
}
|
|
1027
1582
|
],
|
|
1028
1583
|
run: async (c2) => {
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1584
|
+
const project = loadProject(c2.options.dir);
|
|
1585
|
+
if (!project.ok) return c2.error(project.error);
|
|
1586
|
+
const { projectDir, config } = project;
|
|
1031
1587
|
try {
|
|
1032
|
-
|
|
1033
|
-
"
|
|
1034
|
-
"
|
|
1035
|
-
|
|
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`
|
|
1036
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
|
+
});
|
|
1037
1646
|
} catch (err) {
|
|
1038
1647
|
return c2.error({
|
|
1039
|
-
code: "
|
|
1040
|
-
message: err instanceof Error ? err.message : "
|
|
1648
|
+
code: "DEPLOY_FAILED",
|
|
1649
|
+
message: err instanceof Error ? err.message : "Deploy failed",
|
|
1041
1650
|
retryable: true
|
|
1042
1651
|
});
|
|
1043
1652
|
}
|
|
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
1653
|
}
|
|
1060
1654
|
});
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
+
}
|
|
1065
1704
|
});
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
+
}
|
|
1071
1739
|
});
|
|
1072
|
-
return
|
|
1740
|
+
return app;
|
|
1073
1741
|
}
|
|
1074
1742
|
|
|
1075
1743
|
// src/journey/quickstart.ts
|
|
1076
1744
|
import chalk3 from "chalk";
|
|
1077
|
-
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
|
+
};
|
|
1078
1756
|
|
|
1079
1757
|
// src/lib/colors.ts
|
|
1080
1758
|
import chalk2 from "chalk";
|
|
@@ -1225,7 +1903,7 @@ ${chalk3.bold("You don't have a business account yet \u2014 let's create one.")}
|
|
|
1225
1903
|
message: "What's your business called?",
|
|
1226
1904
|
placeholder: "My Business"
|
|
1227
1905
|
});
|
|
1228
|
-
if (
|
|
1906
|
+
if (isCancel2(name)) {
|
|
1229
1907
|
return c2.error({
|
|
1230
1908
|
code: "CANCELLED",
|
|
1231
1909
|
message: "Setup cancelled.",
|
|
@@ -1316,7 +1994,7 @@ async function resolveOrPrompt(c2, provided, missing, prompt) {
|
|
|
1316
1994
|
});
|
|
1317
1995
|
}
|
|
1318
1996
|
const value = await prompt();
|
|
1319
|
-
if (
|
|
1997
|
+
if (isCancel2(value))
|
|
1320
1998
|
throw new QuickstartError({
|
|
1321
1999
|
code: "CANCELLED",
|
|
1322
2000
|
message: "Setup cancelled.",
|
|
@@ -1340,7 +2018,7 @@ ${chalk3.bold("Let's get you set up to sell.")}
|
|
|
1340
2018
|
{ value: "new", label: "Create a new business account" }
|
|
1341
2019
|
]
|
|
1342
2020
|
});
|
|
1343
|
-
if (
|
|
2021
|
+
if (isCancel2(choice)) {
|
|
1344
2022
|
throw new QuickstartError({
|
|
1345
2023
|
code: "CANCELLED",
|
|
1346
2024
|
message: "Setup cancelled.",
|
|
@@ -1352,7 +2030,7 @@ ${chalk3.bold("Let's get you set up to sell.")}
|
|
|
1352
2030
|
message: "What's your new business called?",
|
|
1353
2031
|
placeholder: "My Business"
|
|
1354
2032
|
});
|
|
1355
|
-
if (
|
|
2033
|
+
if (isCancel2(name)) {
|
|
1356
2034
|
throw new QuickstartError({
|
|
1357
2035
|
code: "CANCELLED",
|
|
1358
2036
|
message: "Setup cancelled.",
|
|
@@ -1392,7 +2070,7 @@ ${chalk3.bold("Let's get you set up to sell.")}
|
|
|
1392
2070
|
placeholder: "9.99",
|
|
1393
2071
|
validate: (v) => isNaN(Number(v)) ? "Enter a number" : void 0
|
|
1394
2072
|
});
|
|
1395
|
-
return
|
|
2073
|
+
return isCancel2(p) ? p : Number(p);
|
|
1396
2074
|
}
|
|
1397
2075
|
);
|
|
1398
2076
|
const billingPeriod = await resolveOrPrompt(
|
|
@@ -1578,7 +2256,7 @@ async function resolveExistingProduct(c2, products) {
|
|
|
1578
2256
|
hint: p.id
|
|
1579
2257
|
}))
|
|
1580
2258
|
});
|
|
1581
|
-
if (
|
|
2259
|
+
if (isCancel2(chosen))
|
|
1582
2260
|
throw new QuickstartError({
|
|
1583
2261
|
code: "CANCELLED",
|
|
1584
2262
|
message: "Setup cancelled.",
|
|
@@ -1678,7 +2356,7 @@ ${c.success("\u2713 Your store is fully set up.")}`);
|
|
|
1678
2356
|
// package.json
|
|
1679
2357
|
var package_default = {
|
|
1680
2358
|
name: "@whop/cli",
|
|
1681
|
-
version: "0.
|
|
2359
|
+
version: "0.3.0",
|
|
1682
2360
|
description: "The Whop CLI \u2014 build and manage Whop apps from your terminal. Human and agent friendly.",
|
|
1683
2361
|
keywords: [
|
|
1684
2362
|
"agent",
|
|
@@ -1702,6 +2380,13 @@ var package_default = {
|
|
|
1702
2380
|
"dist"
|
|
1703
2381
|
],
|
|
1704
2382
|
type: "module",
|
|
2383
|
+
exports: {
|
|
2384
|
+
".": "./dist/index.js",
|
|
2385
|
+
"./vite": {
|
|
2386
|
+
types: "./dist/vite.d.ts",
|
|
2387
|
+
import: "./dist/vite.js"
|
|
2388
|
+
}
|
|
2389
|
+
},
|
|
1705
2390
|
publishConfig: {
|
|
1706
2391
|
access: "public"
|
|
1707
2392
|
},
|
|
@@ -1728,10 +2413,12 @@ var package_default = {
|
|
|
1728
2413
|
},
|
|
1729
2414
|
devDependencies: {
|
|
1730
2415
|
"@types/node": "25.3.5",
|
|
1731
|
-
|
|
2416
|
+
fflate: "0.8.2",
|
|
2417
|
+
incur: "github:whopio/incur#4b9ca91dc02472db944fb103984ffc4a13251b6f",
|
|
1732
2418
|
tsup: "8.5.0",
|
|
1733
2419
|
tsx: "4.19.4",
|
|
1734
|
-
typescript: "5.9.3"
|
|
2420
|
+
typescript: "5.9.3",
|
|
2421
|
+
vite: "7.3.6"
|
|
1735
2422
|
},
|
|
1736
2423
|
engines: {
|
|
1737
2424
|
node: ">=22"
|
|
@@ -2184,22 +2871,10 @@ var HANDWRITTEN_GROUPS = [
|
|
|
2184
2871
|
})
|
|
2185
2872
|
},
|
|
2186
2873
|
{
|
|
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())
|
|
2874
|
+
name: "apps",
|
|
2875
|
+
description: "Build and deploy fully-hosted web apps (*.whop.app)",
|
|
2876
|
+
section: "get-started",
|
|
2877
|
+
register: (cli2) => cli2.command(buildAppGroup())
|
|
2203
2878
|
},
|
|
2204
2879
|
{
|
|
2205
2880
|
name: "upgrade",
|
|
@@ -2473,9 +3148,9 @@ var tag_descriptions_default = {
|
|
|
2473
3148
|
Deposits: "Add funds to your balance",
|
|
2474
3149
|
Swaps: "Convert between currencies",
|
|
2475
3150
|
Verifications: "Identity verification status",
|
|
2476
|
-
Products: "
|
|
2477
|
-
Plans: "
|
|
2478
|
-
"Checkout Configurations": "
|
|
3151
|
+
Products: "What you're selling",
|
|
3152
|
+
Plans: "Pricing for your products",
|
|
3153
|
+
"Checkout Configurations": "Optional / advanced: customize the checkout page",
|
|
2479
3154
|
Referrals: "Referral programs",
|
|
2480
3155
|
Ads: "Ad creatives",
|
|
2481
3156
|
"Ad Campaigns": "Top-level campaign organization",
|
|
@@ -2489,7 +3164,6 @@ var descriptions = tag_descriptions_default;
|
|
|
2489
3164
|
var toCliName = (tag) => tag.toLowerCase().replace(/\s+/g, "-");
|
|
2490
3165
|
var SECTION_LABELS = {
|
|
2491
3166
|
"get-started": "Get Started",
|
|
2492
|
-
commerce: "Commerce",
|
|
2493
3167
|
auth: "Auth"
|
|
2494
3168
|
};
|
|
2495
3169
|
var INTEGRATIONS = [
|
|
@@ -2532,16 +3206,16 @@ function buildGroupedHelp(version, description) {
|
|
|
2532
3206
|
description: descriptions[tag] ?? ""
|
|
2533
3207
|
}))
|
|
2534
3208
|
})).filter((section) => section.commands.length > 0);
|
|
2535
|
-
const
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
const
|
|
3209
|
+
const commerceSections = apiSections.filter(
|
|
3210
|
+
(section) => section.label.toLowerCase() === "commerce"
|
|
3211
|
+
);
|
|
3212
|
+
const otherApiSections = apiSections.filter(
|
|
2539
3213
|
(section) => section.label.toLowerCase() !== "commerce"
|
|
2540
3214
|
);
|
|
2541
3215
|
const allSections = [
|
|
2542
3216
|
handwrittenSection("get-started"),
|
|
2543
|
-
|
|
2544
|
-
...
|
|
3217
|
+
...commerceSections,
|
|
3218
|
+
...otherApiSections,
|
|
2545
3219
|
handwrittenSection("auth")
|
|
2546
3220
|
].filter((section) => section.commands.length > 0);
|
|
2547
3221
|
const maxCmdLen = Math.max(
|