@whop/cli 0.5.0 → 0.7.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
@@ -21,6 +21,7 @@ import {
21
21
  getActiveProfile,
22
22
  getSecret,
23
23
  getValidAccessToken,
24
+ listOAuthAccounts,
24
25
  listProfiles,
25
26
  native_spec_default,
26
27
  performOAuthLogin,
@@ -29,107 +30,158 @@ import {
29
30
  removeAllProfiles,
30
31
  removeProfile,
31
32
  resolveActiveAccountId,
33
+ selectAccount,
32
34
  selectProfile,
33
35
  setActiveProfileAccount,
34
36
  startSpinner,
35
37
  switchProfile,
36
38
  upsertProfile,
37
39
  validateApiKey
38
- } from "./chunk-X34EUMEM.js";
40
+ } from "./chunk-BSCOWFVU.js";
39
41
  import {
40
42
  external_exports
41
43
  } from "./chunk-KFCNNWPI.js";
42
44
 
43
45
  // src/api/groups.json
44
46
  var groups_default = [
45
- [
46
- "accounts",
47
- "Accounts"
48
- ],
49
- [
50
- "users",
51
- "Users"
52
- ],
53
- [
54
- "stats",
55
- "Stats"
56
- ],
57
- [
58
- "ledgers",
59
- "Ledgers"
60
- ],
61
- [
62
- "payouts",
63
- "Payouts"
64
- ],
65
- [
66
- "cards",
67
- "Cards"
68
- ],
69
- [
70
- "transfers",
71
- "Transfers"
72
- ],
73
- [
74
- "deposits",
75
- "Deposits"
76
- ],
77
- [
78
- "swaps",
79
- "Swaps"
80
- ],
81
- [
82
- "verifications",
83
- "Verifications"
84
- ],
85
- [
86
- "products",
87
- "Products"
88
- ],
89
- [
90
- "plans",
91
- "Plans"
92
- ],
93
- [
94
- "checkout-configurations",
95
- "Checkout Configurations"
96
- ],
97
- [
98
- "referrals",
99
- "Referrals"
100
- ],
101
- [
102
- "people",
103
- "People"
104
- ],
105
- [
106
- "events",
107
- "Events"
108
- ],
109
- [
110
- "ads",
111
- "Ads"
112
- ],
113
- [
114
- "ad-campaigns",
115
- "Ad Campaigns"
116
- ],
117
- [
118
- "ad-groups",
119
- "Ad Groups"
120
- ],
121
- [
122
- "audiences",
123
- "Audiences"
124
- ],
125
- [
126
- "media",
127
- "Media"
128
- ],
129
- [
130
- "social-accounts",
131
- "Social Accounts"
132
- ]
47
+ {
48
+ name: "accounts",
49
+ tag: "Accounts",
50
+ description: "A business on Whop: profile, wallet, capabilities, settings.",
51
+ group: "Core Resources"
52
+ },
53
+ {
54
+ name: "users",
55
+ tag: "Users",
56
+ description: "A person on Whop: profile and connected identities.",
57
+ group: "Core Resources"
58
+ },
59
+ {
60
+ name: "stats",
61
+ tag: "Stats",
62
+ description: "Aggregated financial time series for reporting.",
63
+ group: "Core Resources"
64
+ },
65
+ {
66
+ name: "ledgers",
67
+ tag: "Ledgers",
68
+ description: "The activity feed behind an account or user's balance.",
69
+ group: "Money"
70
+ },
71
+ {
72
+ name: "payouts",
73
+ tag: "Payouts",
74
+ description: "Send money from a balance to a bank or wallet.",
75
+ group: "Money"
76
+ },
77
+ {
78
+ name: "cards",
79
+ tag: "Cards",
80
+ description: "Issue cards that spend from a balance.",
81
+ group: "Money"
82
+ },
83
+ {
84
+ name: "transfers",
85
+ tag: "Transfers",
86
+ description: "Move funds between Whop accounts and users.",
87
+ group: "Money"
88
+ },
89
+ {
90
+ name: "deposits",
91
+ tag: "Deposits",
92
+ description: "Add funds to a balance.",
93
+ group: "Money"
94
+ },
95
+ {
96
+ name: "swaps",
97
+ tag: "Swaps",
98
+ description: "Convert a balance between currencies.",
99
+ group: "Money"
100
+ },
101
+ {
102
+ name: "verifications",
103
+ tag: "Verifications",
104
+ description: "Identity review required before payouts and card issuing.",
105
+ group: "Money"
106
+ },
107
+ {
108
+ name: "products",
109
+ tag: "Products",
110
+ description: "The things you sell. Each owns plans and a store page.",
111
+ group: "Commerce"
112
+ },
113
+ {
114
+ name: "plans",
115
+ tag: "Plans",
116
+ description: "Pricing for a product: one-time, recurring, trials, stock.",
117
+ group: "Commerce"
118
+ },
119
+ {
120
+ name: "checkout-configurations",
121
+ tag: "Checkout Configurations",
122
+ description: "Turn a plan into a shareable, prefilled checkout link.",
123
+ group: "Commerce"
124
+ },
125
+ {
126
+ name: "referrals",
127
+ tag: "Referrals",
128
+ description: "Track who referred a purchase and what they earn.",
129
+ group: "Commerce"
130
+ },
131
+ {
132
+ name: "people",
133
+ tag: "People",
134
+ description: "Visitors and customers of an account, aggregated from pixel events.",
135
+ group: "Tracking"
136
+ },
137
+ {
138
+ name: "events",
139
+ tag: "Events",
140
+ description: "Conversion and engagement events tracked for attribution.",
141
+ group: "Tracking"
142
+ },
143
+ {
144
+ name: "ads",
145
+ tag: "Ads",
146
+ description: "The creative: copy, assets, and destination URL.",
147
+ group: "Ads"
148
+ },
149
+ {
150
+ name: "ad-campaigns",
151
+ tag: "Ad Campaigns",
152
+ description: "Platform, objective, and budget for a set of ads.",
153
+ group: "Ads"
154
+ },
155
+ {
156
+ name: "ad-groups",
157
+ tag: "Ad Groups",
158
+ description: "Audience, placements, and schedule within a campaign.",
159
+ group: "Ads"
160
+ },
161
+ {
162
+ name: "audiences",
163
+ tag: "Audiences",
164
+ description: "Reusable targeting lists for ad groups.",
165
+ group: "Ads"
166
+ },
167
+ {
168
+ name: "media",
169
+ tag: "Media",
170
+ description: "AI-generated assets, billed from a balance, attachable wherever files are accepted.",
171
+ group: "Media"
172
+ },
173
+ {
174
+ name: "social-accounts",
175
+ tag: "Social Accounts",
176
+ description: "Connected Facebook and Instagram accounts that run ads.",
177
+ group: "Identity"
178
+ },
179
+ {
180
+ name: "apps",
181
+ tag: "Apps",
182
+ description: "Apps you build on Whop: metadata, hosted builds, runtime logs.",
183
+ group: "Developer"
184
+ }
133
185
  ];
134
186
 
135
187
  // src/auth/commands.ts
@@ -181,6 +233,29 @@ var switchArgs = external_exports.object({
181
233
  var switchOutput = external_exports.object({
182
234
  active: external_exports.string().describe("The newly active profile")
183
235
  });
236
+ var accountArgs = external_exports.object({
237
+ id: external_exports.string().optional().describe(
238
+ "Business account to make the default \u2014 id (biz_xxx) or route (interactive picker if omitted)"
239
+ )
240
+ });
241
+ var accountOptions = external_exports.object({
242
+ list: external_exports.boolean().optional().describe(
243
+ "List the business accounts you can manage without changing the default"
244
+ )
245
+ });
246
+ var accountListItem = external_exports.object({
247
+ id: external_exports.string(),
248
+ title: external_exports.string(),
249
+ route: external_exports.string(),
250
+ active: external_exports.boolean().describe("Whether this is the profile's default business account")
251
+ });
252
+ var accountOutput = external_exports.object({
253
+ profile: external_exports.string().describe("The active profile"),
254
+ account: Identity.nullable().describe(
255
+ "The default business account passed to scoped commands"
256
+ ),
257
+ accounts: external_exports.array(accountListItem).describe("Business accounts this user can manage")
258
+ });
184
259
  var loginExamples = [
185
260
  { description: "Interactive login" },
186
261
  {
@@ -318,7 +393,7 @@ async function loginAdapter(c2) {
318
393
  const accountId = getActiveProfile()?.accountId ?? "";
319
394
  if (accountId) {
320
395
  try {
321
- const { createWhopFetch: createWhopFetch2 } = await import("./api-E6Y2WAEG.js");
396
+ const { createWhopFetch: createWhopFetch2 } = await import("./api-YDHSDKDV.js");
322
397
  const fetch3 = createWhopFetch2();
323
398
  const res = await fetch3(
324
399
  new Request(
@@ -524,6 +599,196 @@ function buildAuthGroup() {
524
599
  );
525
600
  }
526
601
  });
602
+ auth.command("account", {
603
+ description: "List your business accounts or set the default one for the active profile",
604
+ aliases: ["accounts"],
605
+ args: accountArgs,
606
+ options: accountOptions,
607
+ output: accountOutput,
608
+ examples: [
609
+ { description: "Pick the default business account interactively" },
610
+ {
611
+ options: { list: true },
612
+ description: "List the business accounts you can manage"
613
+ },
614
+ {
615
+ args: { id: "biz_xxx" },
616
+ description: "Set the default business account"
617
+ }
618
+ ],
619
+ hint: "OAuth profiles only \u2014 API key profiles are tied to a single business account.",
620
+ run: async (c2) => {
621
+ const profile = getActiveProfile();
622
+ if (!profile) {
623
+ return c2.error({
624
+ code: "NO_ACTIVE_PROFILE",
625
+ message: "No active profile \u2014 log in first.",
626
+ retryable: true,
627
+ cta: {
628
+ description: "To authenticate:",
629
+ commands: [{ command: "auth login", description: "Log in to Whop" }]
630
+ }
631
+ });
632
+ }
633
+ if (profile.method !== "oauth") {
634
+ return c2.error({
635
+ code: "API_KEY_PROFILE",
636
+ message: "API key profiles are tied to a single business account. Log in with Whop (OAuth) to switch between the businesses you manage.",
637
+ retryable: false,
638
+ cta: {
639
+ description: "To use a different business account:",
640
+ commands: [
641
+ {
642
+ command: "auth login",
643
+ description: 'Choose "Log in with Whop"'
644
+ }
645
+ ]
646
+ }
647
+ });
648
+ }
649
+ let token;
650
+ try {
651
+ token = await getValidAccessToken(profile);
652
+ } catch {
653
+ token = null;
654
+ }
655
+ if (!token) {
656
+ return c2.error({
657
+ code: "SESSION_EXPIRED",
658
+ message: "Your session is no longer valid \u2014 log in again.",
659
+ retryable: false,
660
+ cta: {
661
+ description: "To authenticate:",
662
+ commands: [{ command: "auth login", description: "Log in to Whop" }]
663
+ }
664
+ });
665
+ }
666
+ const s = c2.agent ? null : startSpinner("Loading your business accounts\u2026");
667
+ let accounts;
668
+ try {
669
+ accounts = await listOAuthAccounts(token);
670
+ s?.stop(
671
+ `Found ${accounts.length} business account${accounts.length === 1 ? "" : "s"}.`
672
+ );
673
+ } catch {
674
+ s?.stop("Couldn't load your business accounts.");
675
+ return c2.error({
676
+ code: "ACCOUNT_LOOKUP_FAILED",
677
+ message: "Couldn't load your business accounts. Please try again.",
678
+ retryable: true
679
+ });
680
+ }
681
+ if (accounts.length === 0) {
682
+ return c2.error({
683
+ code: "NO_ACCOUNTS",
684
+ message: "You don't have a business account yet.",
685
+ retryable: false,
686
+ cta: {
687
+ description: "Create one and start selling in one command:",
688
+ commands: [
689
+ {
690
+ command: "quickstart",
691
+ description: "Create your business account, product, and checkout link"
692
+ }
693
+ ]
694
+ }
695
+ });
696
+ }
697
+ const currentAccount = profile.accountId || profile.accountTitle || profile.accountRoute ? {
698
+ id: profile.accountId,
699
+ title: profile.accountTitle,
700
+ route: profile.accountRoute
701
+ } : null;
702
+ if (c2.options.list) {
703
+ return c2.ok(
704
+ {
705
+ profile: profile.name,
706
+ account: currentAccount,
707
+ accounts: accounts.map((account) => ({
708
+ ...account,
709
+ active: account.id === profile.accountId
710
+ }))
711
+ },
712
+ {
713
+ cta: {
714
+ description: "Change the default business account:",
715
+ commands: [
716
+ {
717
+ command: "auth account",
718
+ args: { id: true },
719
+ description: "Pass a business account id or route"
720
+ }
721
+ ]
722
+ }
723
+ }
724
+ );
725
+ }
726
+ let requested = c2.args.id;
727
+ if (!requested) {
728
+ if (c2.agent) {
729
+ return c2.error({
730
+ code: "ACCOUNT_REQUIRED",
731
+ message: `Specify a business account. Available: ${accounts.map((account) => `${account.id} (${account.title})`).join(", ")}.`,
732
+ retryable: true
733
+ });
734
+ }
735
+ try {
736
+ requested = (await selectAccount(accounts, profile.accountId)).id;
737
+ } catch (error) {
738
+ if (error instanceof PromptCancelledError) {
739
+ return c2.error({
740
+ code: "CANCELLED",
741
+ message: "Switch cancelled.",
742
+ exitCode: 130
743
+ });
744
+ }
745
+ throw error;
746
+ }
747
+ }
748
+ const match = accounts.find((account) => account.id === requested) ?? accounts.find((account) => account.route === requested);
749
+ if (!match) {
750
+ return c2.error({
751
+ code: "ACCOUNT_NOT_FOUND",
752
+ message: `No business account "${requested}" for this user. Available: ${accounts.map((account) => `${account.id} (${account.title})`).join(", ")}.`,
753
+ retryable: true
754
+ });
755
+ }
756
+ if (!setActiveProfileAccount(match, profile.name)) {
757
+ return c2.error({
758
+ code: "PROFILE_REMOVED",
759
+ message: `Profile "${profile.name}" was removed while switching \u2014 log in again.`,
760
+ retryable: false,
761
+ cta: {
762
+ description: "To authenticate:",
763
+ commands: [
764
+ { command: "auth login", description: "Log in to Whop" }
765
+ ]
766
+ }
767
+ });
768
+ }
769
+ return c2.ok(
770
+ {
771
+ profile: profile.name,
772
+ account: match,
773
+ accounts: accounts.map((account) => ({
774
+ ...account,
775
+ active: account.id === match.id
776
+ }))
777
+ },
778
+ {
779
+ cta: {
780
+ description: `Now acting on behalf of ${match.title}.`,
781
+ commands: [
782
+ {
783
+ command: "auth status",
784
+ description: "Show the active identity"
785
+ }
786
+ ]
787
+ }
788
+ }
789
+ );
790
+ }
791
+ });
527
792
  auth.command("status", {
528
793
  description: "Show the active identity (whoami)",
529
794
  aliases: ["whoami"],
@@ -651,6 +916,14 @@ async function updateAppRoute(appId, route) {
651
916
  async function updateAppSecrets(appId, secrets) {
652
917
  return makeWhopRequest("PATCH", `/apps/${appId}`, { secrets });
653
918
  }
919
+ async function listAppLogs(appId, params) {
920
+ const query = new URLSearchParams();
921
+ for (const [key, value] of Object.entries(params)) {
922
+ if (value !== void 0 && value !== "") query.set(key, String(value));
923
+ }
924
+ const qs = query.size > 0 ? `?${query}` : "";
925
+ return makeWhopRequest("GET", `/apps/${appId}/logs${qs}`);
926
+ }
654
927
  async function createAppBuild(input) {
655
928
  return makeWhopRequest("POST", "/app_builds", {
656
929
  app_id: input.app_id,
@@ -1377,7 +1650,7 @@ async function withRoute(c2, app) {
1377
1650
  });
1378
1651
  }
1379
1652
  }
1380
- function resolveSecretsAppId(appOption) {
1653
+ function resolveLinkedAppId(appOption) {
1381
1654
  if (appOption) return appOption;
1382
1655
  const projectDir = findProjectDir();
1383
1656
  const config = projectDir ? readAppConfig(projectDir) : null;
@@ -1386,6 +1659,33 @@ function resolveSecretsAppId(appOption) {
1386
1659
  `No app specified. Pass --app app_xxx or run inside a project with ${APP_CONFIG_FILENAME}.`
1387
1660
  );
1388
1661
  }
1662
+ var DURATION_MS = {
1663
+ s: 1e3,
1664
+ m: 6e4,
1665
+ h: 36e5,
1666
+ d: 864e5
1667
+ };
1668
+ function parseTimeOption(value) {
1669
+ if (!value) return void 0;
1670
+ const match = /^(\d+)([smhd])$/.exec(value);
1671
+ if (!match) return value;
1672
+ return new Date(
1673
+ Date.now() - Number(match[1]) * DURATION_MS[match[2]]
1674
+ ).toISOString();
1675
+ }
1676
+ var LOG_LEVEL_COLORS = {
1677
+ error: chalk.red,
1678
+ warn: chalk.yellow
1679
+ };
1680
+ function printLogLine(row) {
1681
+ const time = row.created_at.replace("T", " ").replace("Z", "");
1682
+ const color = LOG_LEVEL_COLORS[row.level] ?? chalk.dim;
1683
+ const request = row.request_method ? chalk.dim(` ${row.request_method} ${row.request_path ?? ""}`) : "";
1684
+ console.log(
1685
+ `${chalk.dim(time)} ${color(row.level.padEnd(5))}${request} ${row.message}`
1686
+ );
1687
+ if (row.stack) console.log(chalk.dim(row.stack));
1688
+ }
1389
1689
  function buildSecretsGroup() {
1390
1690
  const secrets = Cli_exports.create("secrets", {
1391
1691
  description: "Manage app secrets \u2014 encrypted at rest, injected as env bindings into the hosted runtime and `whop apps dev`"
@@ -1396,7 +1696,7 @@ function buildSecretsGroup() {
1396
1696
  app: external_exports.string().optional().describe("App id (defaults to the project's linked app)")
1397
1697
  }),
1398
1698
  run: async (c2) => {
1399
- const remote = await getApp(resolveSecretsAppId(c2.options.app));
1699
+ const remote = await getApp(resolveLinkedAppId(c2.options.app));
1400
1700
  return c2.ok({ app_id: remote.id, secrets: remote.secrets ?? {} });
1401
1701
  }
1402
1702
  });
@@ -1423,7 +1723,7 @@ function buildSecretsGroup() {
1423
1723
  updates[pair.slice(0, eq)] = pair.slice(eq + 1);
1424
1724
  }
1425
1725
  const remote = await updateAppSecrets(
1426
- resolveSecretsAppId(c2.options.app),
1726
+ resolveLinkedAppId(c2.options.app),
1427
1727
  updates
1428
1728
  );
1429
1729
  return c2.ok({ app_id: remote.id, secrets: remote.secrets ?? {} });
@@ -1440,7 +1740,7 @@ function buildSecretsGroup() {
1440
1740
  const updates = {};
1441
1741
  for (const key of c2.options.key) updates[key] = null;
1442
1742
  const remote = await updateAppSecrets(
1443
- resolveSecretsAppId(c2.options.app),
1743
+ resolveLinkedAppId(c2.options.app),
1444
1744
  updates
1445
1745
  );
1446
1746
  return c2.ok({ app_id: remote.id, secrets: remote.secrets ?? {} });
@@ -1459,6 +1759,54 @@ async function buildAppGroup() {
1459
1759
  await registerSpecCommands(builds, "App builds");
1460
1760
  app.command(builds);
1461
1761
  app.command(buildSecretsGroup());
1762
+ app.command("logs", {
1763
+ description: "Read the app's server runtime logs \u2014 console output, uncaught exceptions, failed requests (kept 7 days)",
1764
+ hint: "Newest first in the structured output; the terminal view prints oldest to newest. Deploy, hit your app, then read its logs to debug server code.",
1765
+ options: external_exports.object({
1766
+ app: external_exports.string().optional().describe("App id (defaults to the project's linked app)"),
1767
+ level: external_exports.enum(["log", "debug", "info", "warn", "error"]).optional().describe("Only console lines of this level"),
1768
+ query: external_exports.string().optional().describe("Only logs whose message contains this text"),
1769
+ build: external_exports.string().optional().describe("Only logs from this build (abld_xxx)"),
1770
+ since: external_exports.string().optional().describe(
1771
+ 'Start of the window \u2014 a duration like "10m"/"2h"/"1d" or an ISO 8601 timestamp'
1772
+ ),
1773
+ until: external_exports.string().optional().describe("End of the window \u2014 a duration or ISO 8601 timestamp"),
1774
+ first: external_exports.number().optional().describe("Number of log lines to return (max 500)"),
1775
+ after: external_exports.string().optional().describe("Cursor from a previous page's page_info")
1776
+ }),
1777
+ examples: [
1778
+ { description: "Recent logs for the linked app" },
1779
+ {
1780
+ options: { level: "error", since: "1h" },
1781
+ description: "Errors from the last hour"
1782
+ },
1783
+ {
1784
+ options: { query: "checkout", app: "app_xxxxxxxx" },
1785
+ description: "Search another app's logs by message text"
1786
+ }
1787
+ ],
1788
+ outputPolicy: "agent-only",
1789
+ run: async (c2) => {
1790
+ const appId = resolveLinkedAppId(c2.options.app);
1791
+ const page = await listAppLogs(appId, {
1792
+ level: c2.options.level,
1793
+ query: c2.options.query,
1794
+ app_build_id: c2.options.build,
1795
+ created_after: parseTimeOption(c2.options.since),
1796
+ created_before: parseTimeOption(c2.options.until),
1797
+ first: c2.options.first,
1798
+ after: c2.options.after
1799
+ });
1800
+ const logs = page.data ?? [];
1801
+ if (!c2.agent && !c2.formatExplicit) {
1802
+ if (logs.length === 0) {
1803
+ console.log(chalk.dim("No logs in this window."));
1804
+ }
1805
+ for (const row of [...logs].reverse()) printLogLine(row);
1806
+ }
1807
+ return c2.ok({ app_id: appId, logs, page_info: page.page_info });
1808
+ }
1809
+ });
1462
1810
  app.command("dev", {
1463
1811
  description: "Run the local dev server for this app",
1464
1812
  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.",
@@ -2340,7 +2688,7 @@ ${c.success("\u2713 Your store is fully set up.")}`);
2340
2688
  // package.json
2341
2689
  var package_default = {
2342
2690
  name: "@whop/cli",
2343
- version: "0.5.0",
2691
+ version: "0.7.0",
2344
2692
  description: "The Whop CLI \u2014 build and manage Whop apps from your terminal. Human and agent friendly.",
2345
2693
  keywords: [
2346
2694
  "agent",
@@ -2361,7 +2709,8 @@ var package_default = {
2361
2709
  "whop-cli": "dist/index.js"
2362
2710
  },
2363
2711
  files: [
2364
- "dist"
2712
+ "dist",
2713
+ "skills"
2365
2714
  ],
2366
2715
  type: "module",
2367
2716
  exports: {
@@ -2398,7 +2747,7 @@ var package_default = {
2398
2747
  devDependencies: {
2399
2748
  "@types/node": "25.3.5",
2400
2749
  fflate: "0.8.2",
2401
- incur: "github:whopio/incur#4b9ca91dc02472db944fb103984ffc4a13251b6f",
2750
+ incur: "github:whopio/incur#fbe3f4dbf48f1bc780cb1bca476f7dade1fe12d3",
2402
2751
  tsup: "8.5.0",
2403
2752
  tsx: "4.19.4",
2404
2753
  typescript: "5.9.3",
@@ -2913,7 +3262,9 @@ var HANDWRITTEN_GROUPS = [
2913
3262
  var HANDWRITTEN_GROUP_NAMES = new Set(
2914
3263
  HANDWRITTEN_GROUPS.map(({ name }) => name)
2915
3264
  );
2916
- var API_GROUPS = groups_default.map(([name, tag]) => ({ name, tag })).filter(({ name }) => !HANDWRITTEN_GROUP_NAMES.has(name));
3265
+ var API_GROUPS = groups_default.filter(
3266
+ ({ name }) => !HANDWRITTEN_GROUP_NAMES.has(name)
3267
+ );
2917
3268
  var COMMAND_GROUPS = [...API_GROUPS, ...HANDWRITTEN_GROUPS];
2918
3269
  async function registerHandwrittenGroups(cli2) {
2919
3270
  for (const group of HANDWRITTEN_GROUPS) await group.register(cli2);
@@ -2934,6 +3285,155 @@ async function setupAgents(cli2) {
2934
3285
  }
2935
3286
  }
2936
3287
 
3288
+ // src/media/commands.ts
3289
+ import { spinner as spinner2 } from "@clack/prompts";
3290
+ var POLL_INTERVAL_MS = 3e3;
3291
+ var whopFetch2 = createWhopFetch();
3292
+ async function buildMediaGroup(description) {
3293
+ const media = Cli_exports.create("media", { description });
3294
+ const spec2 = filterSpecByTag(native_spec_default, "Media");
3295
+ const generated = await Openapi_exports.generateCommands(
3296
+ spec2,
3297
+ whopFetch2
3298
+ );
3299
+ for (const [name, entry] of generated) {
3300
+ if (!("run" in entry)) continue;
3301
+ if (name === "generate") {
3302
+ media.command(name, buildGenerateCommand(entry));
3303
+ continue;
3304
+ }
3305
+ media.command(name, entry);
3306
+ }
3307
+ return media;
3308
+ }
3309
+ function buildGenerateCommand(entry) {
3310
+ const options = (entry.options ?? external_exports.object({})).extend({
3311
+ wait: external_exports.boolean().optional().describe(
3312
+ "Block until generation finishes and return the terminal asset (with file.id when ready)"
3313
+ ),
3314
+ timeout: external_exports.coerce.number().default(600).describe("Max seconds to wait with --wait before giving up")
3315
+ });
3316
+ return {
3317
+ description: entry.description,
3318
+ options,
3319
+ examples: [
3320
+ {
3321
+ options: {
3322
+ type: "image",
3323
+ prompt: "A running club jogging across a bridge at sunrise",
3324
+ wait: true
3325
+ },
3326
+ description: "Generate an image and wait for the finished file"
3327
+ },
3328
+ {
3329
+ options: {
3330
+ type: "video",
3331
+ prompt: "Sneaker product spin on a white background",
3332
+ duration_seconds: 5,
3333
+ wait: true
3334
+ },
3335
+ description: "Generate a 5s video and wait"
3336
+ }
3337
+ ],
3338
+ run: async (c2) => {
3339
+ const { wait, timeout, ...body } = c2.options;
3340
+ const request = Object.fromEntries(
3341
+ Object.entries(body).filter(([, value]) => value !== void 0)
3342
+ );
3343
+ const asset = await createGeneration(c2, request);
3344
+ if (!wait) return asset;
3345
+ return awaitGeneration(c2, asset, Number(timeout));
3346
+ }
3347
+ };
3348
+ }
3349
+ async function createGeneration(c2, body) {
3350
+ const res = await whopFetch2(
3351
+ new Request("https://api.whop.com/media/generate", {
3352
+ method: "POST",
3353
+ body: JSON.stringify(body)
3354
+ })
3355
+ );
3356
+ const payload = await res.json().catch(() => null);
3357
+ if (res.ok && payload) return payload;
3358
+ if (res.status === 402) {
3359
+ const depositUrl = payload?.error?.deposit_url;
3360
+ return c2.error({
3361
+ code: "INSUFFICIENT_BALANCE",
3362
+ message: `${payload?.message ?? "Insufficient balance to fund this generation."}${depositUrl ? ` Deposit at ${depositUrl}` : ""}`,
3363
+ retryable: true,
3364
+ cta: {
3365
+ description: "Fund your balance, then retry:",
3366
+ commands: [
3367
+ { command: "deposits create", description: "Add funds to your balance" }
3368
+ ]
3369
+ }
3370
+ });
3371
+ }
3372
+ return c2.error({
3373
+ code: "API_ERROR",
3374
+ message: payload?.message ?? `Generation request failed (HTTP ${res.status})`,
3375
+ retryable: true
3376
+ });
3377
+ }
3378
+ async function awaitGeneration(c2, created, timeoutSeconds) {
3379
+ const interactive = !c2.agent && process.stdout.isTTY;
3380
+ const progress = interactive ? spinner2() : null;
3381
+ progress?.start(`Generating ${created.media_type ?? "asset"}\u2026`);
3382
+ const startedAt = Date.now();
3383
+ let asset = created;
3384
+ while (asset.status === "processing") {
3385
+ if (Date.now() - startedAt > timeoutSeconds * 1e3) {
3386
+ progress?.stop(`Still processing after ${timeoutSeconds}s.`);
3387
+ return c2.error({
3388
+ code: "WAIT_TIMEOUT",
3389
+ message: `Generation ${asset.id} is still processing after ${timeoutSeconds}s. It keeps running server-side \u2014 check on it later.`,
3390
+ retryable: true,
3391
+ cta: {
3392
+ description: "Check the asset:",
3393
+ commands: [
3394
+ { command: `media get ${asset.id}`, description: "Poll the asset status" }
3395
+ ]
3396
+ }
3397
+ });
3398
+ }
3399
+ await sleep2(POLL_INTERVAL_MS);
3400
+ progress?.message(
3401
+ `Generating ${created.media_type ?? "asset"}\u2026 ${Math.round((Date.now() - startedAt) / 1e3)}s`
3402
+ );
3403
+ asset = await fetchAsset(c2, asset.id, progress);
3404
+ }
3405
+ if (asset.status === "failed") {
3406
+ progress?.stop("Generation failed.");
3407
+ return c2.error({
3408
+ code: "GENERATION_FAILED",
3409
+ message: `${asset.error_message ?? "Generation failed."} The charge was refunded \u2014 adjust the prompt and retry.`,
3410
+ retryable: true
3411
+ });
3412
+ }
3413
+ progress?.stop(
3414
+ `Generated ${asset.file?.id ?? asset.id} in ${Math.round((Date.now() - startedAt) / 1e3)}s.`
3415
+ );
3416
+ return asset;
3417
+ }
3418
+ async function fetchAsset(c2, id, progress) {
3419
+ const res = await whopFetch2(
3420
+ new Request(`https://api.whop.com/media/${id}`, { method: "GET" })
3421
+ );
3422
+ const payload = await res.json().catch(() => null);
3423
+ if (!res.ok || !payload) {
3424
+ progress?.stop("Polling failed.");
3425
+ return c2.error({
3426
+ code: "API_ERROR",
3427
+ message: payload?.message ?? `Couldn't poll media asset ${id} (HTTP ${res.status})`,
3428
+ retryable: true
3429
+ });
3430
+ }
3431
+ return payload;
3432
+ }
3433
+ function sleep2(ms) {
3434
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
3435
+ }
3436
+
2937
3437
  // src/lib/renderer.ts
2938
3438
  var MAX_CELL_WIDTH = 40;
2939
3439
  function isPlainObject(value) {
@@ -2990,13 +3490,91 @@ function renderRows(rows) {
2990
3490
  ${c.muted(`& ${hiddenCount} more columns \u2014 use --format json to see all`)}` : "";
2991
3491
  return [header, separator, ...body].join("\n") + overflowHint;
2992
3492
  }
3493
+ function pruneEmpty(value) {
3494
+ if (value == null) return { kept: void 0, hidden: 1 };
3495
+ if (Array.isArray(value)) {
3496
+ if (value.length === 0) return { kept: void 0, hidden: 1 };
3497
+ if (!value.every(isPlainObject)) return { kept: value, hidden: 0 };
3498
+ let hidden = 0;
3499
+ const items = [];
3500
+ for (const item of value) {
3501
+ const pruned = pruneEmpty(item);
3502
+ hidden += pruned.hidden;
3503
+ if (pruned.kept !== void 0) items.push(pruned.kept);
3504
+ }
3505
+ if (items.length === 0) return { kept: void 0, hidden };
3506
+ return { kept: items, hidden };
3507
+ }
3508
+ if (isPlainObject(value)) {
3509
+ let hidden = 0;
3510
+ const out = {};
3511
+ for (const [key, entry] of Object.entries(value)) {
3512
+ const pruned = pruneEmpty(entry);
3513
+ hidden += pruned.hidden;
3514
+ if (pruned.kept !== void 0) out[key] = pruned.kept;
3515
+ }
3516
+ if (Object.keys(out).length === 0) return { kept: void 0, hidden: Math.max(hidden, 1) };
3517
+ return { kept: out, hidden };
3518
+ }
3519
+ return { kept: value, hidden: 0 };
3520
+ }
3521
+ function formatScalar(value) {
3522
+ const text3 = String(value);
3523
+ if (typeof value === "boolean") return value ? c.success(text3) : c.error(text3);
3524
+ if (typeof value === "number") return c.number(text3);
3525
+ if (typeof value === "string") {
3526
+ if (WHOP_TAG.test(value)) return c.id(text3);
3527
+ if (/^https?:\/\//.test(value)) return c.link(text3);
3528
+ }
3529
+ return text3;
3530
+ }
3531
+ function renderEntries(obj, depth) {
3532
+ const pad = " ".repeat(depth);
3533
+ const lines = [];
3534
+ for (const [key, value] of Object.entries(obj)) {
3535
+ const label = c.muted(`${key}:`);
3536
+ if (isPlainObject(value)) {
3537
+ lines.push(`${pad}${label}`);
3538
+ lines.push(...renderEntries(value, depth + 1));
3539
+ } else if (isObjectArray(value)) {
3540
+ lines.push(`${pad}${label}`);
3541
+ for (const item of value) {
3542
+ const itemLines = renderEntries(item, depth + 1);
3543
+ itemLines[0] = `${pad}- ${itemLines[0].slice(pad.length + 2)}`;
3544
+ lines.push(...itemLines);
3545
+ }
3546
+ } else if (Array.isArray(value)) {
3547
+ lines.push(`${pad}${label} ${value.map(formatScalar).join(", ")}`);
3548
+ } else {
3549
+ lines.push(`${pad}${label} ${formatScalar(value)}`);
3550
+ }
3551
+ }
3552
+ return lines;
3553
+ }
3554
+ function renderObject(data) {
3555
+ const { kept, hidden } = pruneEmpty(data);
3556
+ const lines = kept === void 0 ? [c.muted("(all fields empty)")] : renderEntries(kept, 0);
3557
+ const hint = hidden > 0 ? `
3558
+ ${c.muted(`${hidden} empty ${hidden === 1 ? "field" : "fields"} hidden \u2014 use --format json to see all`)}` : "";
3559
+ return lines.join("\n") + hint;
3560
+ }
2993
3561
  function render(data) {
2994
- if (isObjectArray(data)) return renderRows(data);
3562
+ if (Array.isArray(data)) {
3563
+ if (data.length === 0) return c.muted("(no results)");
3564
+ return isObjectArray(data) ? renderRows(data) : null;
3565
+ }
2995
3566
  if (isPlainObject(data)) {
2996
- const arrayFields = Object.values(data).filter(isObjectArray);
2997
- if (arrayFields.length === 1) {
2998
- return renderRows(arrayFields[0]);
3567
+ const values = Object.values(data);
3568
+ const hasScalarField = values.some((v) => v !== null && typeof v !== "object");
3569
+ if (!hasScalarField) {
3570
+ const objectArrays = values.filter(isObjectArray);
3571
+ if (objectArrays.length === 1) return renderRows(objectArrays[0]);
3572
+ const arrays = values.filter(Array.isArray);
3573
+ if (objectArrays.length === 0 && arrays.length === 1 && arrays[0].length === 0) {
3574
+ return c.muted("(no results)");
3575
+ }
2999
3576
  }
3577
+ return renderObject(data);
3000
3578
  }
3001
3579
  return null;
3002
3580
  }
@@ -3007,6 +3585,11 @@ var cli = Cli_exports.create("whop", {
3007
3585
  description: "Build and manage Whop apps from your terminal.",
3008
3586
  renderer: render,
3009
3587
  sync: {
3588
+ // Shipped skill files generated by scripts/sync-spec.ts from the
3589
+ // spec's own tag copy (x-whop-summary + description, authored in
3590
+ // backend/openapi/v1/tags.yml). A matching directory name overrides
3591
+ // the auto-generated skill for that command group.
3592
+ include: ["skills/*"],
3010
3593
  suggestions: [
3011
3594
  "start selling on Whop",
3012
3595
  "show my Whop business accounts",
@@ -3058,100 +3641,16 @@ cli.use(async (c2, next) => {
3058
3641
  return next();
3059
3642
  });
3060
3643
  await registerHandwrittenGroups(cli);
3061
- for (const { name, tag } of API_GROUPS) {
3062
- cli.command(name, { fetch: fetch2, openapi: spec(tag) });
3644
+ for (const { name, tag, description } of API_GROUPS) {
3645
+ if (name === "media") {
3646
+ cli.command(await buildMediaGroup(description));
3647
+ continue;
3648
+ }
3649
+ cli.command(name, { description, fetch: fetch2, openapi: spec(tag) });
3063
3650
  }
3064
3651
  var cli_default = cli;
3065
3652
 
3066
- // src/api/api-structure.json
3067
- var api_structure_default = [
3068
- {
3069
- group: "Core Resources",
3070
- tags: [
3071
- "Accounts",
3072
- "Users",
3073
- "Webhooks",
3074
- "Stats"
3075
- ]
3076
- },
3077
- {
3078
- group: "Money",
3079
- tags: [
3080
- "Ledgers",
3081
- "Payouts",
3082
- "Cards",
3083
- "Transfers",
3084
- "Deposits",
3085
- "Swaps",
3086
- "Payout Methods",
3087
- "Verifications"
3088
- ]
3089
- },
3090
- {
3091
- group: "Commerce",
3092
- tags: [
3093
- "Products",
3094
- "Plans",
3095
- "Checkout Configurations",
3096
- "Referrals"
3097
- ]
3098
- },
3099
- {
3100
- group: "Tracking",
3101
- tags: [
3102
- "People",
3103
- "Events"
3104
- ]
3105
- },
3106
- {
3107
- group: "Ads",
3108
- tags: [
3109
- "Ads",
3110
- "Ad Campaigns",
3111
- "Ad Groups",
3112
- "Audiences"
3113
- ]
3114
- },
3115
- {
3116
- group: "Media",
3117
- tags: [
3118
- "Media"
3119
- ]
3120
- },
3121
- {
3122
- group: "Identity",
3123
- tags: [
3124
- "Social Accounts"
3125
- ]
3126
- }
3127
- ];
3128
-
3129
- // src/api/tag-descriptions.json
3130
- var tag_descriptions_default = {
3131
- Accounts: "Your Whop account",
3132
- Users: "Users on your platform",
3133
- Stats: "Platform-wide statistics",
3134
- Ledgers: "Transaction history",
3135
- Payouts: "Withdraw earnings",
3136
- Cards: "Issued virtual cards",
3137
- Transfers: "Move funds between accounts",
3138
- Deposits: "Add funds to your balance",
3139
- Swaps: "Convert between currencies",
3140
- Verifications: "Identity verification status",
3141
- Products: "What you're selling",
3142
- Plans: "Pricing for your products",
3143
- "Checkout Configurations": "Optional / advanced: customize the checkout page",
3144
- Referrals: "Referral programs",
3145
- Ads: "Ad creatives",
3146
- "Ad Campaigns": "Top-level campaign organization",
3147
- "Ad Groups": "Sets of related ads",
3148
- Audiences: "Who sees your ads",
3149
- "Social Accounts": "Linked social profiles"
3150
- };
3151
-
3152
3653
  // src/lib/grouped-help.ts
3153
- var descriptions = tag_descriptions_default;
3154
- var toCliName = (tag) => tag.toLowerCase().replace(/\s+/g, "-");
3155
3654
  var SECTION_LABELS = {
3156
3655
  "get-started": "Get Started",
3157
3656
  auth: "Auth"
@@ -3188,14 +3687,12 @@ var handwrittenSection = (section) => ({
3188
3687
  });
3189
3688
  function buildGroupedHelp(version, description) {
3190
3689
  const lines = [];
3191
- const presentCommands = new Set(API_GROUPS.map(({ name }) => name));
3192
- const apiSections = api_structure_default.map(({ group, tags }) => ({
3193
- label: group,
3194
- commands: tags.filter((tag) => presentCommands.has(toCliName(tag))).map((tag) => ({
3195
- name: toCliName(tag),
3196
- description: descriptions[tag] ?? ""
3197
- }))
3198
- })).filter((section) => section.commands.length > 0);
3690
+ const apiSections = [];
3691
+ for (const { name, description: description2, group } of API_GROUPS) {
3692
+ let section = apiSections.find((s) => s.label === group);
3693
+ if (!section) apiSections.push(section = { label: group, commands: [] });
3694
+ section.commands.push({ name, description: description2 });
3695
+ }
3199
3696
  const commerceSections = apiSections.filter(
3200
3697
  (section) => section.label.toLowerCase() === "commerce"
3201
3698
  );