@whop/cli 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  PromptCancelledError,
10
10
  apiBaseUrl,
11
11
  buildTarget,
12
+ buildVersion,
12
13
  chooseAuthMethod,
13
14
  configDir,
14
15
  createWhopFetch,
@@ -25,13 +26,14 @@ import {
25
26
  readConfig,
26
27
  removeAllProfiles,
27
28
  removeProfile,
28
- resolveActiveCompanyId,
29
+ resolveActiveAccountId,
29
30
  selectProfile,
31
+ setActiveProfileAccount,
30
32
  startSpinner,
31
33
  switchProfile,
32
34
  upsertProfile,
33
35
  validateApiKey
34
- } from "./chunk-ET6QSHSA.js";
36
+ } from "./chunk-4WNH276N.js";
35
37
  import {
36
38
  external_exports
37
39
  } from "./chunk-KFCNNWPI.js";
@@ -128,7 +130,7 @@ var groups_default = [
128
130
  var loginOptions = external_exports.object({
129
131
  apiKey: external_exports.string().optional().describe("API key for non-interactive login (skips the prompt)"),
130
132
  profile: external_exports.string().optional().describe("Name for this profile (defaults to the account route)"),
131
- companyId: external_exports.string().optional().describe(
133
+ accountId: external_exports.string().optional().describe(
132
134
  "Business account to act on behalf of (biz_xxx) \u2014 for OAuth users who manage more than one"
133
135
  )
134
136
  });
@@ -193,7 +195,7 @@ async function performLogin(input) {
193
195
  const { profileName, identity: identity2, existed: existed2 } = await performOAuthLogin({
194
196
  profile: input.profile,
195
197
  agent: input.agent,
196
- companyId: input.companyId
198
+ accountId: input.accountId
197
199
  });
198
200
  return {
199
201
  ok: true,
@@ -262,7 +264,7 @@ async function loginAdapter(c2) {
262
264
  agent: c2.agent,
263
265
  apiKey: c2.options.apiKey ?? c2.env.WHOP_API_KEY,
264
266
  profile: c2.options.profile,
265
- companyId: c2.options.companyId
267
+ accountId: c2.options.accountId
266
268
  });
267
269
  } catch (error) {
268
270
  if (error instanceof PromptCancelledError) {
@@ -287,14 +289,14 @@ async function loginAdapter(c2) {
287
289
  }
288
290
  ]
289
291
  };
290
- } else if (result.code === "MULTIPLE_COMPANIES" || result.code === "COMPANY_NOT_FOUND") {
292
+ } else if (result.code === "MULTIPLE_ACCOUNTS" || result.code === "ACCOUNT_NOT_FOUND") {
291
293
  cta = {
292
294
  description: "Choose which business account to use:",
293
295
  commands: [
294
296
  {
295
297
  command: "auth login",
296
- options: { companyId: true },
297
- description: "Pass --company-id biz_xxx"
298
+ options: { accountId: true },
299
+ description: "Pass --account-id biz_xxx"
298
300
  }
299
301
  ]
300
302
  };
@@ -307,14 +309,14 @@ async function loginAdapter(c2) {
307
309
  });
308
310
  }
309
311
  let firstRun = !result.existed;
310
- const companyId = getActiveProfile()?.companyId ?? "";
311
- if (companyId) {
312
+ const accountId = getActiveProfile()?.accountId ?? "";
313
+ if (accountId) {
312
314
  try {
313
- const { createWhopFetch: createWhopFetch2 } = await import("./api-3MQ6MQGV.js");
315
+ const { createWhopFetch: createWhopFetch2 } = await import("./api-A6NYSDKW.js");
314
316
  const fetch3 = createWhopFetch2();
315
317
  const res = await fetch3(
316
318
  new Request(
317
- `https://api.whop.com/products?company_id=${companyId}&first=1`
319
+ `https://api.whop.com/products?company_id=${accountId}&first=1`
318
320
  )
319
321
  );
320
322
  if (res.ok) {
@@ -324,14 +326,14 @@ async function loginAdapter(c2) {
324
326
  } catch {
325
327
  }
326
328
  }
327
- if (!companyId) {
329
+ if (!accountId) {
328
330
  return c2.ok(result.output, {
329
331
  cta: {
330
- description: "You're logged in, but no business account is linked to this account yet. Create a business account on Whop, then log in again:",
332
+ description: "You're logged in, but you don't have a business account yet. Create one and start selling in one command:",
331
333
  commands: [
332
334
  {
333
- command: "auth login",
334
- description: "Re-authenticate once you have a business account"
335
+ command: "quickstart",
336
+ description: "Create your business account, product, and checkout link"
335
337
  }
336
338
  ]
337
339
  }
@@ -589,10 +591,15 @@ function buildAuthGroup() {
589
591
  return auth;
590
592
  }
591
593
 
592
- // src/commerce/products.ts
593
- 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";
594
601
 
595
- // src/commerce/client.ts
602
+ // src/journey/client.ts
596
603
  var PLACEHOLDER = "https://api.whop.com";
597
604
  var whopFetch = createWhopFetch();
598
605
  async function makeWhopRequest(method, path3, body) {
@@ -607,473 +614,1145 @@ async function makeWhopRequest(method, path3, body) {
607
614
  return text3 ? JSON.parse(text3) : { success: true };
608
615
  }
609
616
 
610
- // src/commerce/crud.ts
611
- function buildBody(options) {
612
- const body = {};
613
- for (const [key, value] of Object.entries(options)) {
614
- if (value !== void 0) body[key] = value;
615
- }
616
- return body;
617
- }
618
- function registerListCommand(group, config) {
619
- const shape = {
620
- first: external_exports.coerce.number().optional().describe("Number of results"),
621
- after: external_exports.string().optional().describe("Pagination cursor")
622
- };
623
- if (config.filter) {
624
- shape[config.filter.option] = external_exports.string().optional().describe(config.filter.describe);
625
- }
626
- group.command("list", {
627
- description: config.description,
628
- options: external_exports.object(shape),
629
- run: (c2) => {
630
- const options = c2.options;
631
- const params = new URLSearchParams();
632
- if (config.filter && options[config.filter.option]) {
633
- params.set(config.filter.param, String(options[config.filter.option]));
634
- }
635
- if (options.first) params.set("first", String(options.first));
636
- if (options.after) params.set("after", String(options.after));
637
- const qs = params.toString() ? `?${params}` : "";
638
- return makeWhopRequest("GET", `${config.path}${qs}`);
639
- }
617
+ // src/apps/api.ts
618
+ async function createApp(input) {
619
+ const company_id = input.company_id ?? await resolveActiveAccountId();
620
+ return makeWhopRequest("POST", "/apps", { ...input, company_id });
621
+ }
622
+ var DEV_TOKEN_TTL_MS = (3 * 60 - 1) * 60 * 1e3;
623
+ async function createDevAccessToken() {
624
+ const company_id = await resolveActiveAccountId();
625
+ return makeWhopRequest("POST", "/access_tokens", {
626
+ ...company_id ? { company_id } : {},
627
+ expires_at: new Date(Date.now() + DEV_TOKEN_TTL_MS).toISOString()
640
628
  });
641
629
  }
642
- function registerItemCommands(group, config) {
643
- const { path: path3, noun, idLabel, schema, updateOptions, transformBody } = config;
644
- const idArg = external_exports.object({ id: external_exports.string().describe(idLabel) });
645
- group.command("get", {
646
- description: `Get a ${noun} by ID`,
647
- args: idArg,
648
- output: schema,
649
- 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 }
650
651
  });
651
- if (updateOptions) {
652
- group.command("update", {
653
- description: `Update a ${noun}`,
654
- args: idArg,
655
- options: updateOptions,
656
- output: schema,
657
- run: (c2) => {
658
- const body = buildBody(c2.options);
659
- return makeWhopRequest("PATCH", `${path3}/${c2.args.id}`, transformBody ? transformBody(body) : body);
660
- }
661
- });
662
- }
663
- group.command("delete", {
664
- description: `Delete a ${noun}`,
665
- args: idArg,
666
- 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"
667
674
  });
675
+ if (!file.upload_url) {
676
+ throw new Error("File creation did not return an upload URL.");
677
+ }
678
+ const body = new Uint8Array(zip).buffer;
679
+ const uploadResponse = await fetch(file.upload_url, {
680
+ method: "PUT",
681
+ headers: file.upload_headers ?? { "Content-Type": "application/zip" },
682
+ body
683
+ });
684
+ if (!uploadResponse.ok) {
685
+ throw new Error(
686
+ `Archive upload failed: ${uploadResponse.status} ${await uploadResponse.text()}`
687
+ );
688
+ }
689
+ return file.id;
690
+ }
691
+ async function waitForFileReady(fileId, timeoutMs = 6e4) {
692
+ const deadline = Date.now() + timeoutMs;
693
+ while (Date.now() < deadline) {
694
+ const file = await makeWhopRequest("GET", `/files/${fileId}`);
695
+ if (file.upload_status === "ready") return;
696
+ await sleep(1e3);
697
+ }
698
+ throw new Error(
699
+ "Timed out waiting for the build archive to finish processing."
700
+ );
701
+ }
702
+ async function waitForBuildPromotion(buildId, timeoutMs = 3e4) {
703
+ const deadline = Date.now() + timeoutMs;
704
+ let latest = await getAppBuild(buildId);
705
+ while (Date.now() < deadline) {
706
+ if (latest.is_production) return latest;
707
+ if (latest.status === "rejected") {
708
+ throw new Error("Build was rejected and cannot be promoted.");
709
+ }
710
+ await sleep(750);
711
+ latest = await getAppBuild(buildId);
712
+ }
713
+ return latest;
714
+ }
715
+ function sleep(ms) {
716
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
668
717
  }
669
718
 
670
- // src/commerce/output.ts
671
- import chalk from "chalk";
672
- function printCreateSuccess(c2, opts) {
673
- if (c2.agent || c2.formatExplicit) return;
674
- const lines = ["", chalk.green.bold(`\u2713 ${opts.title}`)];
675
- if (opts.id) lines.push(` ${chalk.dim(opts.id)}`);
676
- if (opts.checkoutUrl) {
677
- lines.push("", chalk.bold("\u{1F389} You can sell now. Share your checkout link:"));
678
- lines.push(` ${chalk.cyan.underline(opts.checkoutUrl)}`);
679
- }
680
- if (opts.next) {
681
- lines.push("", `${chalk.bold("Next")} ${chalk.cyan.bold(`whop ${opts.next.command}`)}`);
682
- 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;
730
+ }
731
+ return null;
732
+ }
733
+ function readAppConfig(projectDir) {
734
+ const path3 = join(projectDir, APP_CONFIG_FILENAME);
735
+ if (!existsSync(path3)) return null;
736
+ try {
737
+ const parsed = JSON.parse(
738
+ readFileSync(path3, "utf-8")
739
+ );
740
+ if (typeof parsed.app_id === "string" && typeof parsed.name === "string" && typeof parsed.route === "string") {
741
+ return { app_id: parsed.app_id, name: parsed.name, route: parsed.route };
742
+ }
743
+ return null;
744
+ } catch {
745
+ return null;
683
746
  }
684
- lines.push("");
685
- console.log(lines.join("\n"));
747
+ }
748
+ function writeAppConfig(projectDir, config) {
749
+ writeFileSync(
750
+ join(projectDir, APP_CONFIG_FILENAME),
751
+ `${JSON.stringify(config, null, " ")}
752
+ `
753
+ );
754
+ }
755
+ function slugify(name) {
756
+ return name.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
686
757
  }
687
758
 
688
- // src/commerce/products.ts
689
- 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({
690
966
  id: external_exports.string(),
691
- title: external_exports.string(),
692
- visibility: external_exports.string().optional(),
693
- route: external_exports.string().optional(),
694
- headline: external_exports.string().nullable().optional(),
695
- description: external_exports.string().nullable().optional(),
696
- member_count: external_exports.number().optional(),
697
- created_at: external_exports.number().optional()
967
+ name: external_exports.string(),
968
+ route: external_exports.string().nullable().optional(),
969
+ url: external_exports.string().optional(),
970
+ directory: external_exports.string().optional(),
971
+ install_failed: external_exports.boolean().optional()
698
972
  });
699
- var createOptions = external_exports.object({
700
- title: external_exports.string().optional().describe("Product display name (max 80 characters)"),
701
- company_id: external_exports.string().optional().describe(
702
- "Business account to create under (defaults to your active account)"
703
- ),
704
- visibility: external_exports.enum(["visible", "hidden", "archived"]).optional().describe("Who can see this product"),
705
- headline: external_exports.string().optional().describe("Short tagline shown on the store page"),
706
- description: external_exports.string().optional().describe("Full description"),
707
- route: external_exports.string().optional().describe("Custom URL slug (e.g. my-course)"),
708
- redirect_purchase_url: external_exports.string().optional().describe("Redirect buyers here after purchase")
973
+ var BuildSchema = external_exports.object({
974
+ id: external_exports.string(),
975
+ status: external_exports.string(),
976
+ is_production: external_exports.boolean().optional(),
977
+ url: external_exports.string().optional()
709
978
  });
710
- function buildProductsGroup() {
711
- const products = Cli_exports.create("products", {
712
- 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)"
713
1233
  });
714
- products.command("create", {
715
- description: "Create a product",
716
- hint: "A product is the top-level container for what you sell. Add a pricing plan next.",
717
- options: createOptions,
718
- 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,
719
1262
  outputPolicy: "agent-only",
720
1263
  examples: [
721
1264
  {
722
- options: { title: "My Online Course" },
723
- 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"
724
1268
  },
725
1269
  {
726
- options: { title: "My Course", visibility: "hidden" },
727
- 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"
728
1273
  }
729
1274
  ],
730
1275
  run: async (c2) => {
731
- let title = c2.options.title;
732
- 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) {
733
1309
  if (c2.agent) {
734
1310
  return c2.error({
735
- code: "TITLE_REQUIRED",
736
- message: "Pass --title to set the product name.",
1311
+ code: "NAME_REQUIRED",
1312
+ message: 'Pass a name: `whop apps create "My Site"`.',
737
1313
  retryable: true
738
1314
  });
739
1315
  }
740
- const t = await text({
741
- message: "What are you selling?",
742
- placeholder: "e.g. My Online Course, Trading Community, Fitness Program"
743
- });
744
- if (isCancel(t))
1316
+ const value = await promptText("What is your app called?", "My Site");
1317
+ if (value === null)
745
1318
  return c2.error({
746
1319
  code: "CANCELLED",
747
1320
  message: "Cancelled.",
748
1321
  exitCode: 130
749
1322
  });
750
- title = t;
1323
+ name = value;
751
1324
  }
752
- const body = { ...buildBody(c2.options), title };
753
- let product;
754
- try {
755
- product = await makeWhopRequest(
756
- "POST",
757
- "/products",
758
- body
1325
+ let route = c2.options.route ?? linked?.route ?? slugify(name);
1326
+ if (!c2.options.route && !linked?.route && !c2.agent) {
1327
+ const value = await promptText(
1328
+ "Which route should it live at? (your-route.whop.app)",
1329
+ slugify(name),
1330
+ slugify(name)
759
1331
  );
760
- } 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) {
761
1341
  return c2.error({
762
- code: "API_ERROR",
763
- 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).`,
764
1344
  retryable: true
765
1345
  });
766
1346
  }
767
- printCreateSuccess(c2, {
768
- title: `Product "${product.title}" created`,
769
- id: product.id,
770
- next: {
771
- command: `plans create --product_id ${product.id}`,
772
- 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
+ });
773
1361
  }
774
- });
775
- return c2.ok(product, {
776
- cta: {
777
- description: "Add pricing to your product:",
778
- commands: [
779
- {
780
- command: "plans create",
781
- options: { product_id: product.id },
782
- description: "Create a pricing plan (required before checkout)"
783
- }
1362
+ const choice = await select({
1363
+ message: "No package manager found (bun, pnpm, or npm). Which one do you want to install?",
1364
+ options: [
1365
+ { value: "bun", label: "bun", hint: "recommended" },
1366
+ { value: "pnpm", label: "pnpm" },
1367
+ { value: "npm", label: "npm", hint: "comes with Node.js" }
784
1368
  ]
1369
+ });
1370
+ if (isCancel(choice)) {
1371
+ return c2.error({
1372
+ code: "CANCELLED",
1373
+ message: "Cancelled.",
1374
+ exitCode: 130
1375
+ });
785
1376
  }
786
- });
787
- }
788
- });
789
- registerListCommand(products, {
790
- path: "/products",
791
- description: "List products"
792
- });
793
- registerItemCommands(products, {
794
- path: "/products",
795
- noun: "product",
796
- idLabel: "Product ID (prod_xxx)",
797
- schema: ProductSchema,
798
- updateOptions: createOptions.partial()
799
- });
800
- return products;
801
- }
802
-
803
- // src/commerce/plans.ts
804
- import { select, isCancel as isCancel2, spinner } from "@clack/prompts";
1377
+ return c2.error({
1378
+ code: "NO_PACKAGE_MANAGER",
1379
+ message: `Install ${String(choice)} with:
805
1380
 
806
- // src/commerce/billing.ts
807
- var BILLING_PERIOD_DAYS = {
808
- daily: 1,
809
- weekly: 7,
810
- monthly: 30,
811
- quarterly: 90,
812
- annually: 365,
813
- every_two_years: 730
814
- };
1381
+ ${pmInstallHint(choice)}
815
1382
 
816
- // src/commerce/plans.ts
817
- var PlanSchema = external_exports.object({
818
- id: external_exports.string(),
819
- title: external_exports.string().nullable().optional(),
820
- plan_type: external_exports.string().optional(),
821
- billing_period: external_exports.string().nullable().optional(),
822
- initial_price: external_exports.number().nullable().optional(),
823
- renewal_price: external_exports.number().nullable().optional(),
824
- currency: external_exports.string().optional(),
825
- purchase_url: external_exports.string().nullable().optional(),
826
- visibility: external_exports.string().optional(),
827
- created_at: external_exports.number().optional()
828
- });
829
- var createOptions2 = external_exports.object({
830
- product_id: external_exports.string().optional().describe("Product this plan belongs to (prod_xxx)"),
831
- title: external_exports.string().optional().describe("Display name for this plan"),
832
- plan_type: external_exports.enum(["one_time", "renewal", "free"]).optional().describe("Billing model \u2014 one_time, renewal, or free"),
833
- billing_period: external_exports.enum(["daily", "weekly", "monthly", "quarterly", "annually", "every_two_years"]).optional().describe("How often to charge (for renewal plans)"),
834
- initial_price: external_exports.coerce.number().optional().describe("Price in the plan's currency (e.g. 9.99 for $9.99)"),
835
- renewal_price: external_exports.coerce.number().optional().describe("Recurring price for renewal plans (defaults to initial_price)"),
836
- currency: external_exports.string().optional().describe("3-letter currency code (default: USD)"),
837
- trial_period_days: external_exports.coerce.number().optional().describe("Free trial length in days"),
838
- visibility: external_exports.enum(["visible", "hidden", "archived"]).optional().describe("Who can see this plan")
839
- });
840
- function normalizePlanBody(body) {
841
- if (typeof body.billing_period === "string") {
842
- body.billing_period = BILLING_PERIOD_DAYS[body.billing_period];
843
- }
844
- if (body.plan_type === "renewal" && body.renewal_price == null && body.initial_price != null) {
845
- body.renewal_price = body.initial_price;
846
- }
847
- return body;
848
- }
849
- async function resolveProductId(providedId, isAgent) {
850
- if (providedId) return { ok: true, productId: providedId, autoResolved: false };
851
- const profile = getActiveProfile();
852
- const params = new URLSearchParams({ first: "100" });
853
- if (profile?.companyId) params.set("company_id", profile.companyId);
854
- let data;
855
- try {
856
- const res = await makeWhopRequest("GET", `/products?${params}`);
857
- data = res.data ?? [];
858
- } catch {
859
- return { ok: false, code: "API_ERROR", message: "Failed to fetch products" };
860
- }
861
- if (data.length === 0) {
862
- return {
863
- ok: false,
864
- code: "NO_PRODUCTS",
865
- message: "No products found. Create one first."
866
- };
867
- }
868
- if (data.length === 1) {
869
- return { ok: true, productId: data[0].id, autoResolved: true };
870
- }
871
- if (isAgent) {
872
- return { ok: false, code: "PRODUCT_REQUIRED", message: "Multiple products found. Pass --product_id.", candidates: data };
873
- }
874
- const chosen = await select({
875
- message: "Which product is this plan for?",
876
- options: data.map((p) => ({ value: p.id, label: p.title, hint: p.id }))
877
- });
878
- if (isCancel2(chosen)) return { ok: false, code: "CANCELLED", message: "Cancelled." };
879
- return { ok: true, productId: chosen, autoResolved: false };
880
- }
881
- function buildPlansGroup() {
882
- const plans = Cli_exports.create("plans", {
883
- description: "Plans \u2014 pricing for your products"
884
- });
885
- plans.command("create", {
886
- description: "Create a pricing plan",
887
- hint: "A plan sets the price and billing interval. After creating a plan, your checkout link is ready to share.",
888
- options: createOptions2,
889
- output: PlanSchema,
890
- outputPolicy: "agent-only",
891
- examples: [
892
- {
893
- options: { product_id: "prod_xxx", initial_price: 9.99, plan_type: "one_time" },
894
- description: "One-time payment of $9.99"
895
- },
896
- {
897
- options: {
898
- product_id: "prod_xxx",
899
- initial_price: 19.99,
900
- plan_type: "renewal",
901
- billing_period: "monthly"
902
- },
903
- description: "Monthly subscription at $19.99"
1383
+ then re-run \`whop apps create\`.`
1384
+ });
904
1385
  }
905
- ],
906
- run: async (c2) => {
907
- const s = c2.agent ? null : spinner();
908
- if (s) s.start("Resolving product\u2026");
909
- const resolved = await resolveProductId(c2.options.product_id, c2.agent);
910
- if (s) s.stop("");
911
- if (!resolved.ok) {
1386
+ const targetDir = resolve2(c2.options.dir ?? `./${route}`);
1387
+ if (existsSync3(targetDir) && readdirSync(targetDir).length > 0) {
912
1388
  return c2.error({
913
- code: resolved.code,
914
- message: resolved.message,
915
- retryable: resolved.code !== "CANCELLED",
916
- cta: resolved.code === "NO_PRODUCTS" ? {
917
- description: "Create a product first:",
918
- commands: [{ command: "products create", description: "Create a product" }]
919
- } : resolved.candidates ? {
920
- description: "Available products:",
921
- commands: resolved.candidates.map((p) => ({
922
- command: "plans create",
923
- options: { product_id: p.id },
924
- description: p.title
925
- }))
926
- } : void 0
1389
+ code: "TARGET_NOT_EMPTY",
1390
+ message: `Target directory is not empty: ${targetDir}. Choose an empty directory with --dir, or a different route.`
927
1391
  });
928
1392
  }
929
- if (resolved.autoResolved && !c2.agent) {
930
- console.error(
931
- `Note: using product ${resolved.productId} \u2014 pass --product_id to override`
932
- );
933
- }
934
- const body = normalizePlanBody({
935
- ...buildBody(c2.options),
936
- product_id: resolved.productId
937
- });
938
- let plan;
1393
+ let whopApp;
939
1394
  try {
940
- 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
+ }
941
1407
  } catch (err) {
942
1408
  return c2.error({
943
- code: "API_ERROR",
944
- 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",
945
1411
  retryable: true
946
1412
  });
947
1413
  }
948
- const purchaseUrl = plan.purchase_url;
949
- const ctaDescription = purchaseUrl ? `\u{1F389} You can sell now. Share your checkout link: ${purchaseUrl}` : "Plan created.";
950
- printCreateSuccess(c2, {
951
- title: purchaseUrl ? "Plan created \u2014 you can sell now" : "Plan created",
952
- id: plan.id,
953
- checkoutUrl: purchaseUrl,
954
- next: {
955
- command: "checkout-configurations create",
956
- hint: "Optional: customize the checkout page (currency, redirect, payment mode)"
957
- }
958
- });
959
- return c2.ok(plan, {
960
- cta: {
961
- description: ctaDescription,
962
- commands: [
963
- { command: "quickstart", description: "See your setup status and what's next" },
964
- {
965
- command: "checkout-configurations create",
966
- options: { plan_id: plan.id },
967
- description: "Optional: customize the checkout page"
968
- }
969
- ]
1414
+ let whopifySteps = [];
1415
+ try {
1416
+ log(
1417
+ c2,
1418
+ chalk.dim("\nScaffolding with the latest TanStack Start template...")
1419
+ );
1420
+ runTanstackCreate({
1421
+ targetDir,
1422
+ projectName: route,
1423
+ pm,
1424
+ quiet: Boolean(c2.agent)
1425
+ });
1426
+ whopifySteps = whopifyProject({ targetDir, route }).manualSteps;
1427
+ } catch (err) {
1428
+ return c2.error({
1429
+ code: "SCAFFOLD_FAILED",
1430
+ message: err instanceof Error ? err.message : "Failed to scaffold the project"
1431
+ });
1432
+ }
1433
+ writeAppConfig(targetDir, { app_id: whopApp.id, name, route });
1434
+ if (whopifySteps.length > 0) {
1435
+ return c2.error({
1436
+ code: "WHOPIFY_INCOMPLETE",
1437
+ message: `Scaffolded ${targetDir} and registered ${whopApp.id}, but the project needs manual wiring:
1438
+
1439
+ ${formatManualSteps(whopifySteps)}
1440
+
1441
+ Then run \`whop apps deploy\`. Re-running \`whop apps create --existing\` in the project re-checks the wiring.`
1442
+ });
1443
+ }
1444
+ let installFailed = false;
1445
+ if (c2.options.install !== false) {
1446
+ log(c2, chalk.dim(`
1447
+ Installing dependencies with ${pm}...`));
1448
+ const result = spawnSync2(pm, ["install"], {
1449
+ cwd: targetDir,
1450
+ stdio: c2.agent ? "ignore" : "inherit",
1451
+ shell: SPAWN_VIA_SHELL2
1452
+ });
1453
+ installFailed = Boolean(result.error) || result.status !== 0;
1454
+ if (installFailed) {
1455
+ log(
1456
+ c2,
1457
+ chalk.yellow(
1458
+ `
1459
+ ${pm} install failed \u2014 run it manually inside ${targetDir}.`
1460
+ )
1461
+ );
970
1462
  }
1463
+ }
1464
+ const url = whopApp.hosted_url ?? void 0;
1465
+ if (!c2.agent && !c2.formatExplicit) {
1466
+ console.log(
1467
+ [
1468
+ "",
1469
+ chalk.green.bold(`\u2713 ${name} is ready`),
1470
+ ` ${chalk.dim(whopApp.id)}`,
1471
+ "",
1472
+ ` ${chalk.bold("Directory")} ${targetDir}`,
1473
+ ...url ? [
1474
+ ` ${chalk.bold("URL")} ${chalk.cyan.underline(url)} ${chalk.dim("(after first deploy)")}`
1475
+ ] : [],
1476
+ "",
1477
+ `${chalk.bold("Next")}`,
1478
+ ` cd ${targetDir}`,
1479
+ ` ${chalk.cyan.bold("whop apps dev")} ${chalk.dim("\u2192 local dev server")}`,
1480
+ ` ${chalk.cyan.bold("whop apps deploy")} ${chalk.dim("\u2192 ship it live")}`,
1481
+ ""
1482
+ ].join("\n")
1483
+ );
1484
+ }
1485
+ return c2.ok({
1486
+ id: whopApp.id,
1487
+ name,
1488
+ route,
1489
+ url,
1490
+ directory: targetDir,
1491
+ install_failed: installFailed
971
1492
  });
972
1493
  }
973
1494
  });
974
- registerListCommand(plans, {
975
- path: "/plans",
976
- description: "List plans",
977
- filter: { option: "product_id", param: "product_ids", describe: "Filter by product" }
978
- });
979
- registerItemCommands(plans, {
980
- path: "/plans",
981
- noun: "plan",
982
- idLabel: "Plan ID (plan_xxx)",
983
- schema: PlanSchema,
984
- updateOptions: createOptions2.omit({ product_id: true }).partial(),
985
- 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
+ }
986
1510
  });
987
- return plans;
988
- }
989
-
990
- // src/commerce/checkout.ts
991
- var CheckoutConfigSchema = external_exports.object({
992
- id: external_exports.string(),
993
- mode: external_exports.enum(["payment", "setup"]).optional(),
994
- currency: external_exports.string().nullable().optional(),
995
- purchase_url: external_exports.string().nullable().optional(),
996
- redirect_url: external_exports.string().nullable().optional(),
997
- company_id: external_exports.string().optional(),
998
- created_at: external_exports.number().optional()
999
- });
1000
- var createOptions3 = external_exports.object({
1001
- plan_id: external_exports.string().optional().describe("Plan to attach this checkout to (plan_xxx)"),
1002
- mode: external_exports.enum(["payment", "setup"]).optional().describe("payment = charge immediately; setup = save payment method only"),
1003
- currency: external_exports.string().optional().describe("3-letter currency code override"),
1004
- redirect_url: external_exports.string().optional().describe("URL to redirect after successful checkout"),
1005
- affiliate_code: external_exports.string().optional().describe("Affiliate code to apply")
1006
- });
1007
- function buildCheckoutGroup() {
1008
- const checkout = Cli_exports.create("checkout-configurations", {
1009
- 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
+ }
1010
1562
  });
1011
- checkout.command("create", {
1012
- description: "Create a checkout configuration",
1013
- hint: "Customize the currency, redirect URL, and payment mode for a plan's checkout page.",
1014
- options: createOptions3,
1015
- 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,
1016
1575
  outputPolicy: "agent-only",
1017
1576
  examples: [
1577
+ { description: "Build and ship to production" },
1018
1578
  {
1019
- options: { plan_id: "plan_xxx" },
1020
- description: "Basic checkout configuration for a plan"
1021
- },
1022
- {
1023
- options: { plan_id: "plan_xxx", redirect_url: "https://mysite.com/thank-you" },
1024
- description: "Redirect buyers to a custom thank-you page"
1579
+ options: { promote: false },
1580
+ description: "Upload a preview build without promoting"
1025
1581
  }
1026
1582
  ],
1027
1583
  run: async (c2) => {
1028
- const body = buildBody(c2.options);
1029
- let config;
1584
+ const project = loadProject(c2.options.dir);
1585
+ if (!project.ok) return c2.error(project.error);
1586
+ const { projectDir, config } = project;
1030
1587
  try {
1031
- config = await makeWhopRequest(
1032
- "POST",
1033
- "/checkout_configurations",
1034
- 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`
1035
1609
  );
1610
+ await waitForFileReady(fileId);
1611
+ let build = await createAppBuild({
1612
+ app_id: config.app_id,
1613
+ checksum,
1614
+ file_id: fileId
1615
+ });
1616
+ if (c2.options.promote !== false) {
1617
+ await promoteAppBuild(build.id);
1618
+ build = await waitForBuildPromotion(build.id);
1619
+ }
1620
+ const promoted = build.is_production === true;
1621
+ const productionUrl = promoted ? (await getApp(config.app_id)).hosted_url ?? void 0 : void 0;
1622
+ if (!c2.agent && !c2.formatExplicit) {
1623
+ console.log(
1624
+ [
1625
+ "",
1626
+ chalk.green.bold(
1627
+ promoted ? "\u2713 Deployed to production" : "\u2713 Build uploaded"
1628
+ ),
1629
+ ` ${chalk.dim(build.id)}`,
1630
+ "",
1631
+ ...promoted && productionUrl ? [
1632
+ ` ${chalk.bold("Live")} ${chalk.cyan.underline(productionUrl)}`
1633
+ ] : [
1634
+ `${chalk.bold("Promote when ready")} ${chalk.cyan.bold(`whop apps promote ${build.id}`)}`
1635
+ ],
1636
+ ""
1637
+ ].join("\n")
1638
+ );
1639
+ }
1640
+ return c2.ok({
1641
+ id: build.id,
1642
+ status: build.status,
1643
+ is_production: promoted,
1644
+ url: productionUrl
1645
+ });
1036
1646
  } catch (err) {
1037
1647
  return c2.error({
1038
- code: "API_ERROR",
1039
- message: err instanceof Error ? err.message : "Failed to create checkout configuration",
1648
+ code: "DEPLOY_FAILED",
1649
+ message: err instanceof Error ? err.message : "Deploy failed",
1040
1650
  retryable: true
1041
1651
  });
1042
1652
  }
1043
- const purchaseUrl = config.purchase_url;
1044
- printCreateSuccess(c2, {
1045
- title: "Checkout configured",
1046
- id: config.id,
1047
- checkoutUrl: purchaseUrl,
1048
- next: { command: "quickstart", hint: "See your full setup status" }
1049
- });
1050
- return c2.ok(config, {
1051
- cta: {
1052
- description: purchaseUrl ? `Checkout configured. Share your link: ${purchaseUrl}` : "Checkout configured.",
1053
- commands: [
1054
- { command: "quickstart", description: "See your full setup status" }
1055
- ]
1056
- }
1057
- });
1058
1653
  }
1059
1654
  });
1060
- registerListCommand(checkout, {
1061
- path: "/checkout_configurations",
1062
- description: "List checkout configurations",
1063
- 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
+ }
1064
1704
  });
1065
- registerItemCommands(checkout, {
1066
- path: "/checkout_configurations",
1067
- noun: "checkout configuration",
1068
- idLabel: "Checkout configuration ID",
1069
- 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
+ }
1070
1739
  });
1071
- return checkout;
1740
+ return app;
1072
1741
  }
1073
1742
 
1074
1743
  // src/journey/quickstart.ts
1075
1744
  import chalk3 from "chalk";
1076
- import { text as text2, select as select2, isCancel as 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
+ };
1077
1756
 
1078
1757
  // src/lib/colors.ts
1079
1758
  import chalk2 from "chalk";
@@ -1121,7 +1800,10 @@ var quickstartOptions = external_exports.object({
1121
1800
  "every_two_years",
1122
1801
  "one_time"
1123
1802
  ]).optional().describe("Billing interval \u2014 use 'one_time' for a single payment"),
1124
- company_id: external_exports.string().optional().describe(
1803
+ business_name: external_exports.string().optional().describe(
1804
+ "Name for a new business account \u2014 quickstart creates it and sets up your product under it (user logins only; ignored with API-key auth, which already belongs to a business)"
1805
+ ),
1806
+ account_id: external_exports.string().optional().describe(
1125
1807
  "Business account to act on behalf of (biz_xxx); defaults to your active account"
1126
1808
  )
1127
1809
  });
@@ -1131,7 +1813,11 @@ var quickstartOutput = external_exports.object({
1131
1813
  checkoutUrl: external_exports.string().nullable().describe(
1132
1814
  "The live checkout link to share \u2014 the journey's payoff, present once a plan exists"
1133
1815
  ),
1134
- created: external_exports.object({ productId: external_exports.string().optional(), planId: external_exports.string().optional() }).nullable().describe(
1816
+ created: external_exports.object({
1817
+ accountId: external_exports.string().optional(),
1818
+ productId: external_exports.string().optional(),
1819
+ planId: external_exports.string().optional()
1820
+ }).nullable().describe(
1135
1821
  "What this run created, if anything \u2014 null for a read-only status check"
1136
1822
  ),
1137
1823
  steps: external_exports.array(StepSchema).describe("The path to your first sale"),
@@ -1170,36 +1856,87 @@ ${chalk3.yellow("!")} Agent setup skipped: ${message}`);
1170
1856
  );
1171
1857
  }
1172
1858
  }
1173
- const companyId = c2.options.company_id ?? await resolveActiveCompanyId() ?? "";
1859
+ const accountId = c2.options.account_id ?? await resolveActiveAccountId() ?? "";
1174
1860
  const accountName = profile?.accountTitle ?? profile?.accountRoute ?? profile?.name ?? "your account";
1175
- if (!companyId) {
1176
- return c2.error({
1177
- code: "NO_COMPANY",
1178
- message: "No business account is linked to this login. Create a business account on Whop, then log in again.",
1179
- retryable: false,
1180
- cta: {
1181
- description: "Create a business account, then re-authenticate:",
1182
- commands: [
1183
- {
1184
- command: "auth login",
1185
- description: "Log in and pick a business account"
1186
- }
1187
- ]
1188
- }
1861
+ const interactive = !c2.agent && !c2.formatExplicit;
1862
+ const canCreateBusiness = profile?.method === "oauth";
1863
+ let newBusinessName = canCreateBusiness ? c2.options.business_name : void 0;
1864
+ if (!canCreateBusiness && c2.options.business_name !== void 0 && interactive) {
1865
+ console.log(
1866
+ `
1867
+ ${chalk3.dim("Ignoring --business_name \u2014 this API key already belongs to a business.")}`
1868
+ );
1869
+ }
1870
+ if (!accountId && newBusinessName === void 0) {
1871
+ if (!canCreateBusiness) {
1872
+ return c2.error({
1873
+ code: "NO_ACCOUNT",
1874
+ message: "Couldn't resolve the business account for this API key. Check the key, or log in again.",
1875
+ retryable: true,
1876
+ cta: {
1877
+ commands: [{ command: "auth login", description: "Log in to Whop" }]
1878
+ }
1879
+ });
1880
+ }
1881
+ if (!interactive) {
1882
+ return c2.error({
1883
+ code: "NO_ACCOUNT",
1884
+ message: "No business account is linked to this login. Pass --business_name to create one and continue.",
1885
+ retryable: true,
1886
+ cta: {
1887
+ description: "Create your business account and product in one run:",
1888
+ commands: [
1889
+ {
1890
+ command: "quickstart",
1891
+ options: { business_name: "My Business" },
1892
+ description: "Create a business account, then your product"
1893
+ }
1894
+ ]
1895
+ }
1896
+ });
1897
+ }
1898
+ console.log(
1899
+ `
1900
+ ${chalk3.bold("You don't have a business account yet \u2014 let's create one.")}`
1901
+ );
1902
+ const name = await text2({
1903
+ message: "What's your business called?",
1904
+ placeholder: "My Business"
1189
1905
  });
1906
+ if (isCancel2(name)) {
1907
+ return c2.error({
1908
+ code: "CANCELLED",
1909
+ message: "Setup cancelled.",
1910
+ exitCode: 130
1911
+ });
1912
+ }
1913
+ newBusinessName = name;
1914
+ }
1915
+ if (newBusinessName !== void 0) {
1916
+ try {
1917
+ return await runCreate(c2, {
1918
+ accountId: "",
1919
+ accountName,
1920
+ products: [],
1921
+ newBusinessName
1922
+ });
1923
+ } catch (err) {
1924
+ if (err instanceof QuickstartError) return c2.error(err.envelope);
1925
+ throw err;
1926
+ }
1190
1927
  }
1191
1928
  const [productsRes, plansRes, checkoutRes] = await Promise.allSettled([
1192
1929
  makeWhopRequest(
1193
1930
  "GET",
1194
- `/products?company_id=${companyId}&first=100`
1931
+ `/products?company_id=${accountId}&first=100`
1195
1932
  ),
1196
1933
  makeWhopRequest(
1197
1934
  "GET",
1198
- `/plans?account_id=${companyId}&first=100`
1935
+ `/plans?account_id=${accountId}&first=100`
1199
1936
  ),
1200
1937
  makeWhopRequest(
1201
1938
  "GET",
1202
- `/checkout_configurations?company_id=${companyId}&first=100`
1939
+ `/checkout_configurations?company_id=${accountId}&first=100`
1203
1940
  )
1204
1941
  ]);
1205
1942
  const critical = [productsRes, plansRes].find((r) => r.status === "rejected");
@@ -1216,11 +1953,17 @@ ${chalk3.yellow("!")} Agent setup skipped: ${message}`);
1216
1953
  const checkouts = checkoutRes.status === "fulfilled" ? checkoutRes.value.data ?? [] : [];
1217
1954
  const ready = products.length > 0 && plans.length > 0;
1218
1955
  const hasCreationFlags = c2.options.title !== void 0 || c2.options.price !== void 0 || c2.options.billing_period !== void 0;
1219
- const canMutateInteractively = !c2.agent && !c2.formatExplicit;
1220
- const shouldCreate = hasCreationFlags || !ready && canMutateInteractively;
1956
+ const shouldCreate = hasCreationFlags || !ready && interactive;
1221
1957
  if (shouldCreate) {
1222
1958
  try {
1223
- return await runCreate(c2, { companyId, accountName, products });
1959
+ return await runCreate(c2, {
1960
+ accountId,
1961
+ accountName,
1962
+ products,
1963
+ // Only offer the new-business choice in the fully-guided flow —
1964
+ // flags mean the user already said what they want.
1965
+ offerNewBusiness: interactive && !hasCreationFlags && c2.options.account_id === void 0 && canCreateBusiness
1966
+ });
1224
1967
  } catch (err) {
1225
1968
  if (err instanceof QuickstartError) return c2.error(err.envelope);
1226
1969
  throw err;
@@ -1251,7 +1994,7 @@ async function resolveOrPrompt(c2, provided, missing, prompt) {
1251
1994
  });
1252
1995
  }
1253
1996
  const value = await prompt();
1254
- if (isCancel3(value))
1997
+ if (isCancel2(value))
1255
1998
  throw new QuickstartError({
1256
1999
  code: "CANCELLED",
1257
2000
  message: "Setup cancelled.",
@@ -1266,7 +2009,39 @@ async function runCreate(c2, state) {
1266
2009
  ${chalk3.bold("Let's get you set up to sell.")}
1267
2010
  `);
1268
2011
  }
1269
- const needsNewProduct = state.products.length === 0 || c2.options.title !== void 0;
2012
+ let { accountId, accountName, products, newBusinessName } = state;
2013
+ if (state.offerNewBusiness) {
2014
+ const choice = await select2({
2015
+ message: "Which business account is this for?",
2016
+ options: [
2017
+ { value: "current", label: accountName, hint: accountId },
2018
+ { value: "new", label: "Create a new business account" }
2019
+ ]
2020
+ });
2021
+ if (isCancel2(choice)) {
2022
+ throw new QuickstartError({
2023
+ code: "CANCELLED",
2024
+ message: "Setup cancelled.",
2025
+ exitCode: 130
2026
+ });
2027
+ }
2028
+ if (choice === "new") {
2029
+ const name = await text2({
2030
+ message: "What's your new business called?",
2031
+ placeholder: "My Business"
2032
+ });
2033
+ if (isCancel2(name)) {
2034
+ throw new QuickstartError({
2035
+ code: "CANCELLED",
2036
+ message: "Setup cancelled.",
2037
+ exitCode: 130
2038
+ });
2039
+ }
2040
+ newBusinessName = name;
2041
+ products = [];
2042
+ }
2043
+ }
2044
+ const needsNewProduct = products.length === 0 || c2.options.title !== void 0;
1270
2045
  const newTitle = needsNewProduct ? await resolveOrPrompt(
1271
2046
  c2,
1272
2047
  c2.options.title,
@@ -1280,7 +2055,7 @@ ${chalk3.bold("Let's get you set up to sell.")}
1280
2055
  placeholder: "My Online Course"
1281
2056
  })
1282
2057
  ) : void 0;
1283
- const existing = needsNewProduct ? void 0 : await resolveExistingProduct(c2, state.products);
2058
+ const existing = needsNewProduct ? void 0 : await resolveExistingProduct(c2, products);
1284
2059
  const price = await resolveOrPrompt(
1285
2060
  c2,
1286
2061
  c2.options.price,
@@ -1295,7 +2070,7 @@ ${chalk3.bold("Let's get you set up to sell.")}
1295
2070
  placeholder: "9.99",
1296
2071
  validate: (v) => isNaN(Number(v)) ? "Enter a number" : void 0
1297
2072
  });
1298
- return isCancel3(p) ? p : Number(p);
2073
+ return isCancel2(p) ? p : Number(p);
1299
2074
  }
1300
2075
  );
1301
2076
  const billingPeriod = await resolveOrPrompt(
@@ -1316,6 +2091,33 @@ ${chalk3.bold("Let's get you set up to sell.")}
1316
2091
  ]
1317
2092
  })
1318
2093
  );
2094
+ let accountCreated = false;
2095
+ if (newBusinessName !== void 0) {
2096
+ let account;
2097
+ try {
2098
+ account = await makeWhopRequest("POST", "/accounts", { title: newBusinessName });
2099
+ } catch (err) {
2100
+ return c2.error({
2101
+ code: "ACCOUNT_FAILED",
2102
+ message: `Failed to create the business account: ${err instanceof Error ? err.message : "unknown error"}`,
2103
+ retryable: true
2104
+ });
2105
+ }
2106
+ accountId = account.id;
2107
+ accountName = account.title ?? newBusinessName;
2108
+ accountCreated = true;
2109
+ setActiveProfileAccount({
2110
+ id: account.id,
2111
+ title: account.title ?? void 0,
2112
+ route: account.route ?? void 0
2113
+ });
2114
+ if (!c2.agent) {
2115
+ console.log(
2116
+ `
2117
+ ${chalk3.green("\u2713")} Created business account "${accountName}" ${chalk3.dim(`(${accountId})`)} \u2014 the CLI now acts on behalf of it.`
2118
+ );
2119
+ }
2120
+ }
1319
2121
  let productId;
1320
2122
  let productTitle;
1321
2123
  let productCreated = false;
@@ -1330,7 +2132,7 @@ ${chalk3.bold("Let's get you set up to sell.")}
1330
2132
  "/products",
1331
2133
  {
1332
2134
  title: newTitle,
1333
- company_id: state.companyId
2135
+ company_id: accountId
1334
2136
  }
1335
2137
  );
1336
2138
  } catch (err) {
@@ -1347,7 +2149,7 @@ ${chalk3.bold("Let's get you set up to sell.")}
1347
2149
  const isRenewal = billingPeriod !== "one_time";
1348
2150
  const planBody = {
1349
2151
  product_id: productId,
1350
- account_id: state.companyId,
2152
+ account_id: accountId,
1351
2153
  initial_price: price,
1352
2154
  plan_type: isRenewal ? "renewal" : "one_time"
1353
2155
  };
@@ -1383,6 +2185,15 @@ ${chalk3.bold("Let's get you set up to sell.")}
1383
2185
  const checkoutUrl = plan.purchase_url ?? null;
1384
2186
  const steps = [
1385
2187
  { id: "auth", label: "Log in", complete: true },
2188
+ ...accountCreated ? [
2189
+ {
2190
+ id: "account",
2191
+ label: "Create a business account",
2192
+ complete: true,
2193
+ firstId: accountId,
2194
+ firstTitle: accountName
2195
+ }
2196
+ ] : [],
1386
2197
  {
1387
2198
  id: "products",
1388
2199
  label: "Create a product",
@@ -1405,20 +2216,17 @@ ${chalk3.bold("Let's get you set up to sell.")}
1405
2216
  false
1406
2217
  );
1407
2218
  if (!c2.agent && !c2.formatExplicit) {
1408
- printSummary(
1409
- state.accountName,
1410
- steps,
1411
- true,
1412
- checkoutUrl,
1413
- nextStep,
1414
- productTitle
1415
- );
2219
+ printSummary(accountName, steps, true, checkoutUrl, nextStep, productTitle);
1416
2220
  }
1417
2221
  return c2.ok({
1418
- account: state.accountName,
2222
+ account: accountName,
1419
2223
  ready: true,
1420
2224
  checkoutUrl,
1421
- created: { productId, planId: plan.id },
2225
+ created: {
2226
+ ...accountCreated ? { accountId } : {},
2227
+ productId,
2228
+ planId: plan.id
2229
+ },
1422
2230
  steps,
1423
2231
  nextStep
1424
2232
  });
@@ -1448,7 +2256,7 @@ async function resolveExistingProduct(c2, products) {
1448
2256
  hint: p.id
1449
2257
  }))
1450
2258
  });
1451
- if (isCancel3(chosen))
2259
+ if (isCancel2(chosen))
1452
2260
  throw new QuickstartError({
1453
2261
  code: "CANCELLED",
1454
2262
  message: "Setup cancelled.",
@@ -1548,7 +2356,7 @@ ${c.success("\u2713 Your store is fully set up.")}`);
1548
2356
  // package.json
1549
2357
  var package_default = {
1550
2358
  name: "@whop/cli",
1551
- version: "0.1.0",
2359
+ version: "0.3.0",
1552
2360
  description: "The Whop CLI \u2014 build and manage Whop apps from your terminal. Human and agent friendly.",
1553
2361
  keywords: [
1554
2362
  "agent",
@@ -1572,6 +2380,13 @@ var package_default = {
1572
2380
  "dist"
1573
2381
  ],
1574
2382
  type: "module",
2383
+ exports: {
2384
+ ".": "./dist/index.js",
2385
+ "./vite": {
2386
+ types: "./dist/vite.d.ts",
2387
+ import: "./dist/vite.js"
2388
+ }
2389
+ },
1575
2390
  publishConfig: {
1576
2391
  access: "public"
1577
2392
  },
@@ -1598,10 +2413,12 @@ var package_default = {
1598
2413
  },
1599
2414
  devDependencies: {
1600
2415
  "@types/node": "25.3.5",
1601
- incur: "github:whopio/incur#5ca60d5",
2416
+ fflate: "0.8.2",
2417
+ incur: "github:whopio/incur#4b9ca91dc02472db944fb103984ffc4a13251b6f",
1602
2418
  tsup: "8.5.0",
1603
2419
  tsx: "4.19.4",
1604
- typescript: "5.9.3"
2420
+ typescript: "5.9.3",
2421
+ vite: "7.3.6"
1605
2422
  },
1606
2423
  engines: {
1607
2424
  node: ">=22"
@@ -2026,7 +2843,7 @@ var HANDWRITTEN_GROUPS = [
2026
2843
  section: "get-started",
2027
2844
  register: (cli2) => cli2.command("quickstart", {
2028
2845
  description: "Start selling \u2014 check your status and create what's missing",
2029
- hint: "In a terminal it guides you to your first sale. Agents: a read-only status check unless you pass --title/--price/--billing_period.",
2846
+ hint: "In a terminal it guides you to your first sale \u2014 including creating a business account if you don't have one. Agents: a read-only status check unless you pass --business_name/--title/--price/--billing_period.",
2030
2847
  options: quickstartOptions,
2031
2848
  output: quickstartOutput,
2032
2849
  examples: [
@@ -2038,6 +2855,15 @@ var HANDWRITTEN_GROUPS = [
2038
2855
  billing_period: "monthly"
2039
2856
  },
2040
2857
  description: "Create a $9.99/mo product non-interactively"
2858
+ },
2859
+ {
2860
+ options: {
2861
+ business_name: "My Business",
2862
+ title: "My Course",
2863
+ price: 9.99,
2864
+ billing_period: "one_time"
2865
+ },
2866
+ description: "No business account yet? Create it and the product in one run"
2041
2867
  }
2042
2868
  ],
2043
2869
  outputPolicy: "agent-only",
@@ -2045,22 +2871,10 @@ var HANDWRITTEN_GROUPS = [
2045
2871
  })
2046
2872
  },
2047
2873
  {
2048
- name: "products",
2049
- description: "What you're selling",
2050
- section: "commerce",
2051
- register: (cli2) => cli2.command(buildProductsGroup())
2052
- },
2053
- {
2054
- name: "plans",
2055
- description: "Pricing for your products",
2056
- section: "commerce",
2057
- register: (cli2) => cli2.command(buildPlansGroup())
2058
- },
2059
- {
2060
- name: "checkout-configurations",
2061
- description: "Optional / advanced: customize the checkout page",
2062
- section: "commerce",
2063
- register: (cli2) => cli2.command(buildCheckoutGroup())
2874
+ name: "apps",
2875
+ description: "Build and deploy fully-hosted web apps (*.whop.app)",
2876
+ section: "get-started",
2877
+ register: (cli2) => cli2.command(buildAppGroup())
2064
2878
  },
2065
2879
  {
2066
2880
  name: "upgrade",
@@ -2334,9 +3148,9 @@ var tag_descriptions_default = {
2334
3148
  Deposits: "Add funds to your balance",
2335
3149
  Swaps: "Convert between currencies",
2336
3150
  Verifications: "Identity verification status",
2337
- Products: "Description of what you sell",
2338
- Plans: "Description of your pricing",
2339
- "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",
2340
3154
  Referrals: "Referral programs",
2341
3155
  Ads: "Ad creatives",
2342
3156
  "Ad Campaigns": "Top-level campaign organization",
@@ -2350,7 +3164,6 @@ var descriptions = tag_descriptions_default;
2350
3164
  var toCliName = (tag) => tag.toLowerCase().replace(/\s+/g, "-");
2351
3165
  var SECTION_LABELS = {
2352
3166
  "get-started": "Get Started",
2353
- commerce: "Commerce",
2354
3167
  auth: "Auth"
2355
3168
  };
2356
3169
  var INTEGRATIONS = [
@@ -2393,16 +3206,16 @@ function buildGroupedHelp(version, description) {
2393
3206
  description: descriptions[tag] ?? ""
2394
3207
  }))
2395
3208
  })).filter((section) => section.commands.length > 0);
2396
- const extraCommerce = apiSections.filter((section) => section.label.toLowerCase() === "commerce").flatMap((section) => section.commands);
2397
- const commerceSection = handwrittenSection("commerce");
2398
- commerceSection.commands = [...commerceSection.commands, ...extraCommerce];
2399
- const filteredApiSections = apiSections.filter(
3209
+ const commerceSections = apiSections.filter(
3210
+ (section) => section.label.toLowerCase() === "commerce"
3211
+ );
3212
+ const otherApiSections = apiSections.filter(
2400
3213
  (section) => section.label.toLowerCase() !== "commerce"
2401
3214
  );
2402
3215
  const allSections = [
2403
3216
  handwrittenSection("get-started"),
2404
- commerceSection,
2405
- ...filteredApiSections,
3217
+ ...commerceSections,
3218
+ ...otherApiSections,
2406
3219
  handwrittenSection("auth")
2407
3220
  ].filter((section) => section.commands.length > 0);
2408
3221
  const maxCmdLen = Math.max(