@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/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-2Y4EKBOD.js";
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-U7WU3JXH.js");
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/commerce/products.ts
594
- import { text, isCancel } from "@clack/prompts";
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/commerce/client.ts
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/commerce/crud.ts
612
- function buildBody(options) {
613
- const body = {};
614
- for (const [key, value] of Object.entries(options)) {
615
- if (value !== void 0) body[key] = value;
616
- }
617
- return body;
618
- }
619
- function registerListCommand(group, config) {
620
- const shape = {
621
- first: external_exports.coerce.number().optional().describe("Number of results"),
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 registerItemCommands(group, config) {
644
- const { path: path3, noun, idLabel, schema, updateOptions, transformBody } = config;
645
- const idArg = external_exports.object({ id: external_exports.string().describe(idLabel) });
646
- group.command("get", {
647
- description: `Get a ${noun} by ID`,
648
- args: idArg,
649
- output: schema,
650
- run: (c2) => makeWhopRequest(`GET`, `${path3}/${c2.args.id}`)
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
- if (updateOptions) {
653
- group.command("update", {
654
- description: `Update a ${noun}`,
655
- args: idArg,
656
- options: updateOptions,
657
- output: schema,
658
- run: (c2) => {
659
- const body = buildBody(c2.options);
660
- return makeWhopRequest("PATCH", `${path3}/${c2.args.id}`, transformBody ? transformBody(body) : body);
661
- }
662
- });
663
- }
664
- group.command("delete", {
665
- description: `Delete a ${noun}`,
666
- args: idArg,
667
- run: (c2) => makeWhopRequest("DELETE", `${path3}/${c2.args.id}`)
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/commerce/output.ts
672
- import chalk from "chalk";
673
- function printCreateSuccess(c2, opts) {
674
- if (c2.agent || c2.formatExplicit) return;
675
- const lines = ["", chalk.green.bold(`\u2713 ${opts.title}`)];
676
- if (opts.id) lines.push(` ${chalk.dim(opts.id)}`);
677
- if (opts.checkoutUrl) {
678
- lines.push("", chalk.bold("\u{1F389} You can sell now. Share your checkout link:"));
679
- lines.push(` ${chalk.cyan.underline(opts.checkoutUrl)}`);
680
- }
681
- if (opts.next) {
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
- lines.push("");
686
- console.log(lines.join("\n"));
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/commerce/products.ts
690
- var ProductSchema = external_exports.object({
691
- id: external_exports.string(),
692
- title: external_exports.string(),
693
- visibility: external_exports.string().optional(),
694
- route: external_exports.string().optional(),
695
- headline: external_exports.string().nullable().optional(),
696
- description: external_exports.string().nullable().optional(),
697
- member_count: external_exports.number().optional(),
698
- created_at: external_exports.number().optional()
699
- });
700
- var createOptions = external_exports.object({
701
- title: external_exports.string().optional().describe("Product display name (max 80 characters)"),
702
- company_id: external_exports.string().optional().describe(
703
- "Business account to create under (defaults to your active account)"
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")
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
- products.command("create", {
716
- description: "Create a product",
717
- hint: "A product is the top-level container for what you sell. Add a pricing plan next.",
718
- options: createOptions,
719
- output: ProductSchema,
720
- outputPolicy: "agent-only",
721
- examples: [
722
- {
723
- options: { title: "My Online Course" },
724
- description: "Create a product"
725
- },
726
- {
727
- options: { title: "My Course", visibility: "hidden" },
728
- description: "Create a hidden product"
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
- run: async (c2) => {
732
- let title = c2.options.title;
733
- if (!title) {
734
- if (c2.agent) {
735
- return c2.error({
736
- code: "TITLE_REQUIRED",
737
- message: "Pass --title to set the product name.",
738
- retryable: true
739
- });
740
- }
741
- const t = await text({
742
- message: "What are you selling?",
743
- placeholder: "e.g. My Online Course, Trading Community, Fitness Program"
744
- });
745
- if (isCancel(t))
746
- return c2.error({
747
- code: "CANCELLED",
748
- message: "Cancelled.",
749
- exitCode: 130
750
- });
751
- title = t;
752
- }
753
- const body = { ...buildBody(c2.options), title };
754
- let product;
755
- try {
756
- product = await makeWhopRequest(
757
- "POST",
758
- "/products",
759
- body
760
- );
761
- } catch (err) {
762
- return c2.error({
763
- code: "API_ERROR",
764
- message: err instanceof Error ? err.message : "Failed to create product",
765
- retryable: true
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
- printCreateSuccess(c2, {
769
- title: `Product "${product.title}" created`,
770
- id: product.id,
771
- next: {
772
- command: `plans create --product_id ${product.id}`,
773
- hint: "Add pricing \u2014 required before you can take a checkout"
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
- 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;
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/commerce/plans.ts
805
- import { select, isCancel as isCancel2, spinner } from "@clack/prompts";
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
- 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()
989
+ status: external_exports.string(),
990
+ is_production: external_exports.boolean().optional(),
991
+ url: external_exports.string().optional()
829
992
  });
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;
993
+ var DeployAbort = class extends Error {
994
+ constructor(options) {
995
+ super(options.message);
996
+ this.options = options;
847
997
  }
848
- return body;
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
- 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;
1007
+ function hasScript(projectDir, script) {
856
1008
  try {
857
- const res = await makeWhopRequest("GET", `/products?${params}`);
858
- data = res.data ?? [];
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 { ok: false, code: "API_ERROR", message: "Failed to fetch products" };
1014
+ return false;
861
1015
  }
862
- if (data.length === 0) {
863
- return {
864
- ok: false,
865
- code: "NO_PRODUCTS",
866
- message: "No products found. Create one first."
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 (data.length === 1) {
870
- return { ok: true, productId: data[0].id, autoResolved: true };
1031
+ if (result.status !== 0) {
1032
+ throw new Error(
1033
+ `\`${pm} run ${script}\` failed with exit code ${result.status ?? "unknown"}`
1034
+ );
871
1035
  }
872
- if (isAgent) {
873
- return { ok: false, code: "PRODUCT_REQUIRED", message: "Multiple products found. Pass --product_id.", candidates: data };
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
- 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 }))
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 buildPlansGroup() {
883
- const plans = Cli_exports.create("plans", {
884
- description: "Plans \u2014 pricing for your products"
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
- 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: [
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
- options: { product_id: "prod_xxx", initial_price: 9.99, plan_type: "one_time" },
895
- description: "One-time payment of $9.99"
1135
+ value: "scaffold",
1136
+ label: "Scaffold a new Whop app here (TanStack Start)"
896
1137
  },
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"
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 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) {
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: resolved.code,
915
- message: resolved.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
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
- if (resolved.autoResolved && !c2.agent) {
931
- console.error(
932
- `Note: using product ${resolved.productId} \u2014 pass --product_id to override`
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
- const body = normalizePlanBody({
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
- plan = await makeWhopRequest("POST", "/plans", body);
1428
+ runScript(projectDir, "dev", injected);
942
1429
  } catch (err) {
943
1430
  return c2.error({
944
- code: "API_ERROR",
945
- message: err instanceof Error ? err.message : "Failed to create plan",
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
- const purchaseUrl = plan.purchase_url;
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
- registerListCommand(plans, {
976
- path: "/plans",
977
- description: "List plans",
978
- filter: { option: "product_id", param: "product_ids", describe: "Filter by product" }
979
- });
980
- registerItemCommands(plans, {
981
- path: "/plans",
982
- noun: "plan",
983
- idLabel: "Plan ID (plan_xxx)",
984
- schema: PlanSchema,
985
- updateOptions: createOptions2.omit({ product_id: true }).partial(),
986
- transformBody: normalizePlanBody
987
- });
988
- return plans;
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: { plan_id: "plan_xxx" },
1021
- description: "Basic checkout configuration for a plan"
1457
+ options: { app: "app_xxxxxxxx" },
1458
+ description: "Link this project to an existing app, then deploy"
1022
1459
  },
1023
1460
  {
1024
- options: { plan_id: "plan_xxx", redirect_url: "https://mysite.com/thank-you" },
1025
- description: "Redirect buyers to a custom thank-you page"
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
- config = await makeWhopRequest(
1033
- "POST",
1034
- "/checkout_configurations",
1035
- body
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: "API_ERROR",
1040
- message: err instanceof Error ? err.message : "Failed to create checkout configuration",
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
- registerListCommand(checkout, {
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 isCancel3 } from "@clack/prompts";
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 (isCancel3(name)) {
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 (isCancel3(value))
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 (isCancel3(choice)) {
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 (isCancel3(name)) {
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 isCancel3(p) ? p : Number(p);
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 (isCancel3(chosen))
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.2.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
- incur: "github:whopio/incur#5ca60d5",
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 = 24 * 60 * 60 * 1e3;
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: "products",
2188
- description: "What you're selling",
2189
- section: "commerce",
2190
- register: (cli2) => cli2.command(buildProductsGroup())
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: "Description of what you sell",
2477
- Plans: "Description of your pricing",
2478
- "Checkout Configurations": "What your checkout page looks like",
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 extraCommerce = apiSections.filter((section) => section.label.toLowerCase() === "commerce").flatMap((section) => section.commands);
2536
- const commerceSection = handwrittenSection("commerce");
2537
- commerceSection.commands = [...commerceSection.commands, ...extraCommerce];
2538
- const filteredApiSections = apiSections.filter(
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
- commerceSection,
2544
- ...filteredApiSections,
3079
+ ...commerceSections,
3080
+ ...otherApiSections,
2545
3081
  handwrittenSection("auth")
2546
3082
  ].filter((section) => section.commands.length > 0);
2547
3083
  const maxCmdLen = Math.max(