@whop/cli 0.6.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,13 +30,14 @@ 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-GKKJXDPK.js";
40
+ } from "./chunk-BSCOWFVU.js";
39
41
  import {
40
42
  external_exports
41
43
  } from "./chunk-KFCNNWPI.js";
@@ -173,6 +175,12 @@ var groups_default = [
173
175
  tag: "Social Accounts",
174
176
  description: "Connected Facebook and Instagram accounts that run ads.",
175
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"
176
184
  }
177
185
  ];
178
186
 
@@ -225,6 +233,29 @@ var switchArgs = external_exports.object({
225
233
  var switchOutput = external_exports.object({
226
234
  active: external_exports.string().describe("The newly active profile")
227
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
+ });
228
259
  var loginExamples = [
229
260
  { description: "Interactive login" },
230
261
  {
@@ -362,7 +393,7 @@ async function loginAdapter(c2) {
362
393
  const accountId = getActiveProfile()?.accountId ?? "";
363
394
  if (accountId) {
364
395
  try {
365
- const { createWhopFetch: createWhopFetch2 } = await import("./api-P7EWPZKG.js");
396
+ const { createWhopFetch: createWhopFetch2 } = await import("./api-YDHSDKDV.js");
366
397
  const fetch3 = createWhopFetch2();
367
398
  const res = await fetch3(
368
399
  new Request(
@@ -568,6 +599,196 @@ function buildAuthGroup() {
568
599
  );
569
600
  }
570
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
+ });
571
792
  auth.command("status", {
572
793
  description: "Show the active identity (whoami)",
573
794
  aliases: ["whoami"],
@@ -695,6 +916,14 @@ async function updateAppRoute(appId, route) {
695
916
  async function updateAppSecrets(appId, secrets) {
696
917
  return makeWhopRequest("PATCH", `/apps/${appId}`, { secrets });
697
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
+ }
698
927
  async function createAppBuild(input) {
699
928
  return makeWhopRequest("POST", "/app_builds", {
700
929
  app_id: input.app_id,
@@ -1421,7 +1650,7 @@ async function withRoute(c2, app) {
1421
1650
  });
1422
1651
  }
1423
1652
  }
1424
- function resolveSecretsAppId(appOption) {
1653
+ function resolveLinkedAppId(appOption) {
1425
1654
  if (appOption) return appOption;
1426
1655
  const projectDir = findProjectDir();
1427
1656
  const config = projectDir ? readAppConfig(projectDir) : null;
@@ -1430,6 +1659,33 @@ function resolveSecretsAppId(appOption) {
1430
1659
  `No app specified. Pass --app app_xxx or run inside a project with ${APP_CONFIG_FILENAME}.`
1431
1660
  );
1432
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
+ }
1433
1689
  function buildSecretsGroup() {
1434
1690
  const secrets = Cli_exports.create("secrets", {
1435
1691
  description: "Manage app secrets \u2014 encrypted at rest, injected as env bindings into the hosted runtime and `whop apps dev`"
@@ -1440,7 +1696,7 @@ function buildSecretsGroup() {
1440
1696
  app: external_exports.string().optional().describe("App id (defaults to the project's linked app)")
1441
1697
  }),
1442
1698
  run: async (c2) => {
1443
- const remote = await getApp(resolveSecretsAppId(c2.options.app));
1699
+ const remote = await getApp(resolveLinkedAppId(c2.options.app));
1444
1700
  return c2.ok({ app_id: remote.id, secrets: remote.secrets ?? {} });
1445
1701
  }
1446
1702
  });
@@ -1467,7 +1723,7 @@ function buildSecretsGroup() {
1467
1723
  updates[pair.slice(0, eq)] = pair.slice(eq + 1);
1468
1724
  }
1469
1725
  const remote = await updateAppSecrets(
1470
- resolveSecretsAppId(c2.options.app),
1726
+ resolveLinkedAppId(c2.options.app),
1471
1727
  updates
1472
1728
  );
1473
1729
  return c2.ok({ app_id: remote.id, secrets: remote.secrets ?? {} });
@@ -1484,7 +1740,7 @@ function buildSecretsGroup() {
1484
1740
  const updates = {};
1485
1741
  for (const key of c2.options.key) updates[key] = null;
1486
1742
  const remote = await updateAppSecrets(
1487
- resolveSecretsAppId(c2.options.app),
1743
+ resolveLinkedAppId(c2.options.app),
1488
1744
  updates
1489
1745
  );
1490
1746
  return c2.ok({ app_id: remote.id, secrets: remote.secrets ?? {} });
@@ -1503,6 +1759,54 @@ async function buildAppGroup() {
1503
1759
  await registerSpecCommands(builds, "App builds");
1504
1760
  app.command(builds);
1505
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
+ });
1506
1810
  app.command("dev", {
1507
1811
  description: "Run the local dev server for this app",
1508
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.",
@@ -2384,7 +2688,7 @@ ${c.success("\u2713 Your store is fully set up.")}`);
2384
2688
  // package.json
2385
2689
  var package_default = {
2386
2690
  name: "@whop/cli",
2387
- version: "0.6.0",
2691
+ version: "0.7.0",
2388
2692
  description: "The Whop CLI \u2014 build and manage Whop apps from your terminal. Human and agent friendly.",
2389
2693
  keywords: [
2390
2694
  "agent",
@@ -2443,7 +2747,7 @@ var package_default = {
2443
2747
  devDependencies: {
2444
2748
  "@types/node": "25.3.5",
2445
2749
  fflate: "0.8.2",
2446
- incur: "github:whopio/incur#4b9ca91dc02472db944fb103984ffc4a13251b6f",
2750
+ incur: "github:whopio/incur#fbe3f4dbf48f1bc780cb1bca476f7dade1fe12d3",
2447
2751
  tsup: "8.5.0",
2448
2752
  tsx: "4.19.4",
2449
2753
  typescript: "5.9.3",
@@ -2981,6 +3285,155 @@ async function setupAgents(cli2) {
2981
3285
  }
2982
3286
  }
2983
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
+
2984
3437
  // src/lib/renderer.ts
2985
3438
  var MAX_CELL_WIDTH = 40;
2986
3439
  function isPlainObject(value) {
@@ -3037,13 +3490,91 @@ function renderRows(rows) {
3037
3490
  ${c.muted(`& ${hiddenCount} more columns \u2014 use --format json to see all`)}` : "";
3038
3491
  return [header, separator, ...body].join("\n") + overflowHint;
3039
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
+ }
3040
3561
  function render(data) {
3041
- 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
+ }
3042
3566
  if (isPlainObject(data)) {
3043
- const arrayFields = Object.values(data).filter(isObjectArray);
3044
- if (arrayFields.length === 1) {
3045
- 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
+ }
3046
3576
  }
3577
+ return renderObject(data);
3047
3578
  }
3048
3579
  return null;
3049
3580
  }
@@ -3111,6 +3642,10 @@ cli.use(async (c2, next) => {
3111
3642
  });
3112
3643
  await registerHandwrittenGroups(cli);
3113
3644
  for (const { name, tag, description } of API_GROUPS) {
3645
+ if (name === "media") {
3646
+ cli.command(await buildMediaGroup(description));
3647
+ continue;
3648
+ }
3114
3649
  cli.command(name, { description, fetch: fetch2, openapi: spec(tag) });
3115
3650
  }
3116
3651
  var cli_default = cli;