@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/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-2Y4EKBOD.js";
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-U7WU3JXH.js");
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/commerce/products.ts
594
- import { text, isCancel } from "@clack/prompts";
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/commerce/client.ts
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/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
- }
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 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}`)
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
- 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}`)
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/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}`)}`);
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
- lines.push("");
686
- console.log(lines.join("\n"));
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/commerce/products.ts
690
- var ProductSchema = external_exports.object({
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
- 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()
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 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")
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 buildProductsGroup() {
712
- const products = Cli_exports.create("products", {
713
- description: "Products \u2014 what you're selling on Whop"
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
- 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,
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
- options: { title: "My Online Course" },
724
- description: "Create a product"
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
- options: { title: "My Course", visibility: "hidden" },
728
- description: "Create a hidden product"
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
- let title = c2.options.title;
733
- if (!title) {
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: "TITLE_REQUIRED",
737
- message: "Pass --title to set the product name.",
1311
+ code: "NAME_REQUIRED",
1312
+ message: 'Pass a name: `whop apps create "My Site"`.',
738
1313
  retryable: true
739
1314
  });
740
1315
  }
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))
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
- title = t;
1323
+ name = value;
752
1324
  }
753
- const body = { ...buildBody(c2.options), title };
754
- let product;
755
- try {
756
- product = await makeWhopRequest(
757
- "POST",
758
- "/products",
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
- } catch (err) {
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: "API_ERROR",
764
- message: err instanceof Error ? err.message : "Failed to create product",
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
- 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"
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
- 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
- }
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
- // src/commerce/plans.ts
805
- import { select, isCancel as isCancel2, spinner } from "@clack/prompts";
1381
+ ${pmInstallHint(choice)}
806
1382
 
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({
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
- 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) {
1386
+ const targetDir = resolve2(c2.options.dir ?? `./${route}`);
1387
+ if (existsSync3(targetDir) && readdirSync(targetDir).length > 0) {
913
1388
  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
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
- if (resolved.autoResolved && !c2.agent) {
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
- plan = await makeWhopRequest("POST", "/plans", body);
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: "API_ERROR",
945
- message: err instanceof Error ? err.message : "Failed to create plan",
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
- 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
- ]
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
- 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
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
- 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"
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
- 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,
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: { plan_id: "plan_xxx" },
1021
- description: "Basic checkout configuration for a plan"
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 body = buildBody(c2.options);
1030
- let config;
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
- config = await makeWhopRequest(
1033
- "POST",
1034
- "/checkout_configurations",
1035
- body
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: "API_ERROR",
1040
- message: err instanceof Error ? err.message : "Failed to create checkout configuration",
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
- registerListCommand(checkout, {
1062
- path: "/checkout_configurations",
1063
- description: "List checkout configurations",
1064
- filter: { option: "plan_id", param: "plan_id", describe: "Filter by plan" }
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
- registerItemCommands(checkout, {
1067
- path: "/checkout_configurations",
1068
- noun: "checkout configuration",
1069
- idLabel: "Checkout configuration ID",
1070
- schema: CheckoutConfigSchema
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 checkout;
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 isCancel3 } from "@clack/prompts";
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 (isCancel3(name)) {
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 (isCancel3(value))
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 (isCancel3(choice)) {
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 (isCancel3(name)) {
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 isCancel3(p) ? p : Number(p);
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 (isCancel3(chosen))
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.2.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
- incur: "github:whopio/incur#5ca60d5",
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: "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())
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: "Description of what you sell",
2477
- Plans: "Description of your pricing",
2478
- "Checkout Configurations": "What your checkout page looks like",
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 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(
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
- commerceSection,
2544
- ...filteredApiSections,
3217
+ ...commerceSections,
3218
+ ...otherApiSections,
2545
3219
  handwrittenSection("auth")
2546
3220
  ].filter((section) => section.commands.length > 0);
2547
3221
  const maxCmdLen = Math.max(