@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/README.md +24 -0
- package/dist/{api-P7EWPZKG.js → api-YDHSDKDV.js} +4 -2
- package/dist/{chunk-GKKJXDPK.js → chunk-BSCOWFVU.js} +1649 -167
- package/dist/chunk-BSCOWFVU.js.map +1 -0
- package/dist/index.js +547 -12
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/whop-accounts/SKILL.md +4 -2
- package/skills/whop-ads/SKILL.md +76 -0
- package/skills/whop-apps/SKILL.md +18 -0
- package/skills/whop-events/SKILL.md +19 -0
- package/skills/whop-media/SKILL.md +23 -0
- package/skills/whop-payouts/SKILL.md +1 -0
- package/skills/whop-people/SKILL.md +19 -0
- package/skills/whop-referrals/SKILL.md +1 -1
- package/skills/whop-social-accounts/SKILL.md +26 -0
- package/skills/whop-verifications/SKILL.md +4 -0
- package/dist/chunk-GKKJXDPK.js.map +0 -1
- /package/dist/{api-P7EWPZKG.js.map → api-YDHSDKDV.js.map} +0 -0
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-
|
|
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-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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#
|
|
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 (
|
|
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
|
|
3044
|
-
|
|
3045
|
-
|
|
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;
|