@whop/cli 0.3.0 → 0.5.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 +8 -0
- package/dist/{api-A6NYSDKW.js → api-E6Y2WAEG.js} +4 -2
- package/dist/{chunk-4WNH276N.js → chunk-X34EUMEM.js} +5362 -355
- package/dist/chunk-X34EUMEM.js.map +1 -0
- package/dist/index.js +591 -601
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-4WNH276N.js.map +0 -1
- /package/dist/{api-A6NYSDKW.js.map → api-E6Y2WAEG.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -5,9 +5,11 @@ import {
|
|
|
5
5
|
Cli_exports,
|
|
6
6
|
Identity,
|
|
7
7
|
OAuthError,
|
|
8
|
+
Openapi_exports,
|
|
8
9
|
Profile,
|
|
9
10
|
PromptCancelledError,
|
|
10
11
|
apiBaseUrl,
|
|
12
|
+
apps_spec_default,
|
|
11
13
|
buildTarget,
|
|
12
14
|
buildVersion,
|
|
13
15
|
chooseAuthMethod,
|
|
@@ -33,7 +35,7 @@ import {
|
|
|
33
35
|
switchProfile,
|
|
34
36
|
upsertProfile,
|
|
35
37
|
validateApiKey
|
|
36
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-X34EUMEM.js";
|
|
37
39
|
import {
|
|
38
40
|
external_exports
|
|
39
41
|
} from "./chunk-KFCNNWPI.js";
|
|
@@ -120,6 +122,10 @@ var groups_default = [
|
|
|
120
122
|
"audiences",
|
|
121
123
|
"Audiences"
|
|
122
124
|
],
|
|
125
|
+
[
|
|
126
|
+
"media",
|
|
127
|
+
"Media"
|
|
128
|
+
],
|
|
123
129
|
[
|
|
124
130
|
"social-accounts",
|
|
125
131
|
"Social Accounts"
|
|
@@ -312,7 +318,7 @@ async function loginAdapter(c2) {
|
|
|
312
318
|
const accountId = getActiveProfile()?.accountId ?? "";
|
|
313
319
|
if (accountId) {
|
|
314
320
|
try {
|
|
315
|
-
const { createWhopFetch: createWhopFetch2 } = await import("./api-
|
|
321
|
+
const { createWhopFetch: createWhopFetch2 } = await import("./api-E6Y2WAEG.js");
|
|
316
322
|
const fetch3 = createWhopFetch2();
|
|
317
323
|
const res = await fetch3(
|
|
318
324
|
new Request(
|
|
@@ -592,11 +598,11 @@ function buildAuthGroup() {
|
|
|
592
598
|
}
|
|
593
599
|
|
|
594
600
|
// src/apps/commands.ts
|
|
595
|
-
import { isCancel, select, text } from "@clack/prompts";
|
|
601
|
+
import { isCancel, select, spinner, text } from "@clack/prompts";
|
|
596
602
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
597
603
|
import { createHash } from "crypto";
|
|
598
604
|
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
|
|
599
|
-
import { join as join3, resolve as resolve2 } from "path";
|
|
605
|
+
import { join as join3, relative, resolve as resolve2 } from "path";
|
|
600
606
|
import chalk from "chalk";
|
|
601
607
|
|
|
602
608
|
// src/journey/client.ts
|
|
@@ -642,6 +648,9 @@ async function getApp(appId) {
|
|
|
642
648
|
async function updateAppRoute(appId, route) {
|
|
643
649
|
return makeWhopRequest("PATCH", `/apps/${appId}`, { route });
|
|
644
650
|
}
|
|
651
|
+
async function updateAppSecrets(appId, secrets) {
|
|
652
|
+
return makeWhopRequest("PATCH", `/apps/${appId}`, { secrets });
|
|
653
|
+
}
|
|
645
654
|
async function createAppBuild(input) {
|
|
646
655
|
return makeWhopRequest("POST", "/app_builds", {
|
|
647
656
|
app_id: input.app_id,
|
|
@@ -653,13 +662,6 @@ async function createAppBuild(input) {
|
|
|
653
662
|
async function getAppBuild(buildId) {
|
|
654
663
|
return makeWhopRequest("GET", `/app_builds/${buildId}`);
|
|
655
664
|
}
|
|
656
|
-
async function listAppBuilds(appId) {
|
|
657
|
-
const response = await makeWhopRequest(
|
|
658
|
-
"GET",
|
|
659
|
-
`/app_builds?app_id=${encodeURIComponent(appId)}&platform=web`
|
|
660
|
-
);
|
|
661
|
-
return response.data ?? [];
|
|
662
|
-
}
|
|
663
665
|
async function promoteAppBuild(buildId) {
|
|
664
666
|
return makeWhopRequest(
|
|
665
667
|
"POST",
|
|
@@ -767,7 +769,11 @@ var DLX_RUNNERS = {
|
|
|
767
769
|
bun: ["bunx"],
|
|
768
770
|
yarn: ["yarn", "dlx"]
|
|
769
771
|
};
|
|
770
|
-
var PM_PRIORITY = [
|
|
772
|
+
var PM_PRIORITY = [
|
|
773
|
+
"bun",
|
|
774
|
+
"pnpm",
|
|
775
|
+
"npm"
|
|
776
|
+
];
|
|
771
777
|
function isPmInstalled(pm) {
|
|
772
778
|
const result = spawnSync(pm, ["--version"], {
|
|
773
779
|
stdio: "ignore",
|
|
@@ -791,6 +797,34 @@ function pmInstallHint(pm) {
|
|
|
791
797
|
return "npm install -g yarn";
|
|
792
798
|
}
|
|
793
799
|
}
|
|
800
|
+
var TANSTACK_CREATE_FLAGS = [
|
|
801
|
+
"--framework",
|
|
802
|
+
"React",
|
|
803
|
+
"--deployment",
|
|
804
|
+
"cloudflare",
|
|
805
|
+
"--no-git",
|
|
806
|
+
"--no-install",
|
|
807
|
+
"--no-intent",
|
|
808
|
+
"--no-toolchain",
|
|
809
|
+
"--no-examples",
|
|
810
|
+
"--non-interactive"
|
|
811
|
+
];
|
|
812
|
+
function tanstackCreateCommand(pm, projectName) {
|
|
813
|
+
return [
|
|
814
|
+
...DLX_RUNNERS[pm],
|
|
815
|
+
"@tanstack/cli@latest",
|
|
816
|
+
"create",
|
|
817
|
+
projectName,
|
|
818
|
+
...TANSTACK_CREATE_FLAGS
|
|
819
|
+
].join(" ");
|
|
820
|
+
}
|
|
821
|
+
function outputTail(result) {
|
|
822
|
+
const combined = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
823
|
+
if (!combined) return "";
|
|
824
|
+
return `
|
|
825
|
+
|
|
826
|
+
${combined.split("\n").slice(-15).join("\n")}`;
|
|
827
|
+
}
|
|
794
828
|
function runTanstackCreate(options) {
|
|
795
829
|
const [runner, ...runnerArgs] = DLX_RUNNERS[options.pm];
|
|
796
830
|
const result = spawnSync(
|
|
@@ -802,26 +836,18 @@ function runTanstackCreate(options) {
|
|
|
802
836
|
options.projectName,
|
|
803
837
|
"--target-dir",
|
|
804
838
|
options.targetDir,
|
|
805
|
-
|
|
806
|
-
"React",
|
|
807
|
-
"--deployment",
|
|
808
|
-
"cloudflare",
|
|
809
|
-
"--no-git",
|
|
810
|
-
"--no-install",
|
|
811
|
-
"--no-intent",
|
|
812
|
-
"--no-toolchain",
|
|
813
|
-
"--no-examples",
|
|
814
|
-
"--non-interactive"
|
|
839
|
+
...TANSTACK_CREATE_FLAGS
|
|
815
840
|
],
|
|
816
841
|
{
|
|
817
|
-
stdio:
|
|
842
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
843
|
+
encoding: "utf-8",
|
|
818
844
|
shell: SPAWN_VIA_SHELL
|
|
819
845
|
}
|
|
820
846
|
);
|
|
821
847
|
if (result.error) throw result.error;
|
|
822
848
|
if (result.status !== 0) {
|
|
823
849
|
throw new Error(
|
|
824
|
-
`\`${options.pm === "npm" ? "npx" : options.pm} @tanstack/cli create\` failed (exit ${result.status}). Check your network connection and try again
|
|
850
|
+
`\`${options.pm === "npm" ? "npx" : options.pm} @tanstack/cli create\` failed (exit ${result.status}). Check your network connection and try again.${outputTail(result)}`
|
|
825
851
|
);
|
|
826
852
|
}
|
|
827
853
|
}
|
|
@@ -831,6 +857,11 @@ var VITE_CONFIG_NAMES = [
|
|
|
831
857
|
"vite.config.js",
|
|
832
858
|
"vite.config.mjs"
|
|
833
859
|
];
|
|
860
|
+
function findViteConfig(dir) {
|
|
861
|
+
return VITE_CONFIG_NAMES.map((name) => join2(dir, name)).find(
|
|
862
|
+
(path3) => existsSync2(path3)
|
|
863
|
+
);
|
|
864
|
+
}
|
|
834
865
|
var WRANGLER_CONFIG_NAMES = ["wrangler.jsonc", "wrangler.json"];
|
|
835
866
|
function cliDependencyVersion() {
|
|
836
867
|
return buildVersion ? `^${buildVersion}` : "latest";
|
|
@@ -867,9 +898,7 @@ function patchPackageJson(dir, manualSteps) {
|
|
|
867
898
|
`);
|
|
868
899
|
}
|
|
869
900
|
function patchViteConfig(dir, manualSteps) {
|
|
870
|
-
const file =
|
|
871
|
-
(path3) => existsSync2(path3)
|
|
872
|
-
);
|
|
901
|
+
const file = findViteConfig(dir);
|
|
873
902
|
if (!file) {
|
|
874
903
|
manualSteps.push(
|
|
875
904
|
"No vite config found (looked for vite.config.ts/.mts/.js/.mjs). Whop hosting serves Vite builds \u2014 create a vite config that builds with @cloudflare/vite-plugin (emitting dist/client and, for SSR, dist/server) and includes the whop() plugin from @whop/cli/vite in its plugins array."
|
|
@@ -962,26 +991,36 @@ function replaceTopLevelName(content, route) {
|
|
|
962
991
|
|
|
963
992
|
// src/apps/commands.ts
|
|
964
993
|
var BUILD_ARCHIVE = "dist/whop-build.zip";
|
|
965
|
-
var
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
994
|
+
var UNSAFE_DEV_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
995
|
+
"PATH",
|
|
996
|
+
"NODE_OPTIONS",
|
|
997
|
+
"NODE_EXTRA_CA_CERTS",
|
|
998
|
+
"NODE_REPL_EXTERNAL_MODULE"
|
|
999
|
+
]);
|
|
1000
|
+
var UNSAFE_DEV_ENV_PREFIXES = ["NPM_", "PNPM_", "YARN_", "BUN_", "LD_", "DYLD_"];
|
|
1001
|
+
function isUnsafeDevEnvKey(key) {
|
|
1002
|
+
const upper = key.toUpperCase();
|
|
1003
|
+
return UNSAFE_DEV_ENV_KEYS.has(upper) || UNSAFE_DEV_ENV_PREFIXES.some((prefix) => upper.startsWith(prefix));
|
|
1004
|
+
}
|
|
973
1005
|
var BuildSchema = external_exports.object({
|
|
974
1006
|
id: external_exports.string(),
|
|
975
1007
|
status: external_exports.string(),
|
|
976
1008
|
is_production: external_exports.boolean().optional(),
|
|
977
1009
|
url: external_exports.string().optional()
|
|
978
1010
|
});
|
|
979
|
-
|
|
1011
|
+
var DeployAbort = class extends Error {
|
|
1012
|
+
constructor(options) {
|
|
1013
|
+
super(options.message);
|
|
1014
|
+
this.options = options;
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
function detectProjectPackageManager(projectDir) {
|
|
980
1018
|
if (existsSync3(join3(projectDir, "bun.lock")) || existsSync3(join3(projectDir, "bun.lockb")))
|
|
981
1019
|
return "bun";
|
|
982
1020
|
if (existsSync3(join3(projectDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
983
1021
|
if (existsSync3(join3(projectDir, "yarn.lock"))) return "yarn";
|
|
984
|
-
return "npm";
|
|
1022
|
+
if (existsSync3(join3(projectDir, "package-lock.json"))) return "npm";
|
|
1023
|
+
return detectPackageManager() ?? "npm";
|
|
985
1024
|
}
|
|
986
1025
|
function hasScript(projectDir, script) {
|
|
987
1026
|
try {
|
|
@@ -995,7 +1034,7 @@ function hasScript(projectDir, script) {
|
|
|
995
1034
|
}
|
|
996
1035
|
var SPAWN_VIA_SHELL2 = process.platform === "win32";
|
|
997
1036
|
function runScript(projectDir, script, extraEnv = {}) {
|
|
998
|
-
const pm =
|
|
1037
|
+
const pm = detectProjectPackageManager(projectDir);
|
|
999
1038
|
const result = spawnSync2(pm, ["run", script], {
|
|
1000
1039
|
cwd: projectDir,
|
|
1001
1040
|
stdio: "inherit",
|
|
@@ -1013,501 +1052,413 @@ function runScript(projectDir, script, extraEnv = {}) {
|
|
|
1013
1052
|
);
|
|
1014
1053
|
}
|
|
1015
1054
|
}
|
|
1055
|
+
function runInstall(c2, projectDir) {
|
|
1056
|
+
const pm = detectProjectPackageManager(projectDir);
|
|
1057
|
+
const spin = c2.agent ? null : spinner();
|
|
1058
|
+
spin?.start(`Installing dependencies with ${pm}`);
|
|
1059
|
+
const result = spawnSync2(pm, ["install"], {
|
|
1060
|
+
cwd: projectDir,
|
|
1061
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1062
|
+
encoding: "utf-8",
|
|
1063
|
+
shell: SPAWN_VIA_SHELL2
|
|
1064
|
+
});
|
|
1065
|
+
if (result.error || result.status !== 0) {
|
|
1066
|
+
spin?.error(`${pm} install failed`);
|
|
1067
|
+
throw new Error(
|
|
1068
|
+
`\`${pm} install\` failed${result.status != null ? ` with exit code ${result.status}` : ""} \u2014 run it manually inside ${projectDir}, then re-run \`whop apps deploy\`.${outputTail(result)}`
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
spin?.stop(`Installed dependencies with ${pm}`);
|
|
1072
|
+
}
|
|
1016
1073
|
function log(c2, message) {
|
|
1017
1074
|
if (!c2.agent) console.log(message);
|
|
1018
1075
|
}
|
|
1019
|
-
function loadProject(dir) {
|
|
1020
|
-
const projectDir = dir ? resolve2(dir) : findProjectDir();
|
|
1021
|
-
if (!projectDir) {
|
|
1022
|
-
return {
|
|
1023
|
-
ok: false,
|
|
1024
|
-
error: {
|
|
1025
|
-
code: "NO_PROJECT",
|
|
1026
|
-
message: `No ${APP_CONFIG_FILENAME} found in this directory or any parent. Run \`whop apps create --existing\` to link this project to a Whop app.`,
|
|
1027
|
-
cta: {
|
|
1028
|
-
commands: [
|
|
1029
|
-
{
|
|
1030
|
-
command: "apps create --existing",
|
|
1031
|
-
description: "Link this project to a Whop app"
|
|
1032
|
-
}
|
|
1033
|
-
]
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
};
|
|
1037
|
-
}
|
|
1038
|
-
const config = readAppConfig(projectDir);
|
|
1039
|
-
if (!config) {
|
|
1040
|
-
return {
|
|
1041
|
-
ok: false,
|
|
1042
|
-
error: {
|
|
1043
|
-
code: "INVALID_CONFIG",
|
|
1044
|
-
message: `${join3(projectDir, APP_CONFIG_FILENAME)} is missing or malformed. Run \`whop apps create --existing\` to regenerate it.`
|
|
1045
|
-
}
|
|
1046
|
-
};
|
|
1047
|
-
}
|
|
1048
|
-
return { ok: true, projectDir, config };
|
|
1049
|
-
}
|
|
1050
1076
|
async function promptText(message, placeholder, initialValue) {
|
|
1051
1077
|
const value = await text({ message, placeholder, initialValue });
|
|
1052
1078
|
if (isCancel(value)) return null;
|
|
1053
1079
|
return String(value);
|
|
1054
1080
|
}
|
|
1081
|
+
function cancelled() {
|
|
1082
|
+
throw new DeployAbort({
|
|
1083
|
+
code: "CANCELLED",
|
|
1084
|
+
message: "Cancelled.",
|
|
1085
|
+
exitCode: 130
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1055
1088
|
function formatManualSteps(steps) {
|
|
1056
1089
|
return steps.map((step, i) => ` ${i + 1}. ${step}`).join("\n");
|
|
1057
1090
|
}
|
|
1058
|
-
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1091
|
+
function formatAppList(apps) {
|
|
1092
|
+
const recent = [...apps].sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0)).slice(0, 10);
|
|
1093
|
+
return recent.map(
|
|
1094
|
+
(a) => ` ${a.id} ${a.name}${a.route ? ` (${a.route}.whop.app)` : ""}`
|
|
1095
|
+
).join("\n");
|
|
1096
|
+
}
|
|
1097
|
+
async function registerSpecCommands(group, tag) {
|
|
1098
|
+
const spec2 = filterSpecByTag(apps_spec_default, tag);
|
|
1099
|
+
const generated = await Openapi_exports.generateCommands(
|
|
1100
|
+
spec2,
|
|
1101
|
+
createWhopFetch()
|
|
1102
|
+
);
|
|
1103
|
+
for (const [name, entry] of generated) {
|
|
1104
|
+
if ("run" in entry) group.command(name, entry);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
function ensurePackageManager() {
|
|
1108
|
+
const pm = detectPackageManager();
|
|
1109
|
+
if (pm) return pm;
|
|
1110
|
+
throw new DeployAbort({
|
|
1111
|
+
code: "NO_PACKAGE_MANAGER",
|
|
1112
|
+
message: `No package manager found (looked for bun, pnpm, npm). Install one and re-run \`whop apps deploy\`:
|
|
1113
|
+
|
|
1114
|
+
bun (recommended): ${pmInstallHint("bun")}
|
|
1115
|
+
pnpm: ${pmInstallHint("pnpm")}
|
|
1116
|
+
npm: ${pmInstallHint("npm")}`
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
async function ensureViteApp(c2, pm, dirOption) {
|
|
1120
|
+
const projectDir = dirOption ? resolve2(dirOption) : findProjectDir() ?? process.cwd();
|
|
1121
|
+
const isViteApp = existsSync3(join3(projectDir, "package.json")) && findViteConfig(projectDir) !== void 0;
|
|
1122
|
+
if (isViteApp) return { projectDir, freshlyScaffolded: false };
|
|
1123
|
+
if (c2.agent) {
|
|
1124
|
+
throw new DeployAbort({
|
|
1125
|
+
code: "NOT_A_VITE_APP",
|
|
1126
|
+
message: [
|
|
1127
|
+
`${projectDir} doesn't look like a Vite app (missing package.json or vite config).`,
|
|
1128
|
+
"",
|
|
1129
|
+
"Ask the user whether to scaffold a new app here, or locate their existing Vite app.",
|
|
1130
|
+
"",
|
|
1131
|
+
"To scaffold a new app (TanStack Start), run:",
|
|
1132
|
+
` ${tanstackCreateCommand(pm, "<app-name>")}`,
|
|
1133
|
+
` cd <app-name> && ${pm} install`,
|
|
1134
|
+
"",
|
|
1135
|
+
"Then re-run `whop apps deploy` from inside the app directory.",
|
|
1136
|
+
"Or, if a Vite app already exists somewhere else, re-run `whop apps deploy --dir <path-to-app>`."
|
|
1137
|
+
].join("\n"),
|
|
1138
|
+
retryable: true,
|
|
1139
|
+
cta: {
|
|
1140
|
+
commands: [
|
|
1141
|
+
{
|
|
1142
|
+
command: "apps deploy",
|
|
1143
|
+
description: "Re-run from inside a Vite app directory"
|
|
1144
|
+
}
|
|
1145
|
+
]
|
|
1146
|
+
}
|
|
1064
1147
|
});
|
|
1065
1148
|
}
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1149
|
+
const choice = await select({
|
|
1150
|
+
message: `${projectDir} doesn't look like a Vite app. What do you want to do?`,
|
|
1151
|
+
options: [
|
|
1152
|
+
{
|
|
1153
|
+
value: "scaffold",
|
|
1154
|
+
label: "Scaffold a new Whop app here (TanStack Start)"
|
|
1155
|
+
},
|
|
1156
|
+
{ value: "cancel", label: "Cancel \u2014 I'll cd into my app directory" }
|
|
1157
|
+
]
|
|
1158
|
+
});
|
|
1159
|
+
if (isCancel(choice) || choice === "cancel") cancelled();
|
|
1160
|
+
const scaffolded = await scaffoldNewApp(c2, pm);
|
|
1161
|
+
return { projectDir: scaffolded, freshlyScaffolded: true };
|
|
1162
|
+
}
|
|
1163
|
+
async function scaffoldNewApp(c2, pm) {
|
|
1164
|
+
const name = await promptText("What is your app called?", "My Site");
|
|
1165
|
+
if (name === null) cancelled();
|
|
1166
|
+
const slug = slugify(name);
|
|
1167
|
+
const routeInput = await promptText(
|
|
1168
|
+
"Which route should it live at? (your-route.whop.app)",
|
|
1169
|
+
slug,
|
|
1170
|
+
slug
|
|
1171
|
+
);
|
|
1172
|
+
if (routeInput === null) cancelled();
|
|
1173
|
+
const route = slugify(routeInput);
|
|
1174
|
+
if (!route) {
|
|
1175
|
+
throw new DeployAbort({
|
|
1176
|
+
code: "ROUTE_REQUIRED",
|
|
1177
|
+
message: `Couldn't derive a route from "${routeInput}" \u2014 use letters and numbers (e.g. my-site).`,
|
|
1178
|
+
retryable: true
|
|
1071
1179
|
});
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
${
|
|
1180
|
+
}
|
|
1181
|
+
const targetDir = resolve2(`./${route}`);
|
|
1182
|
+
if (existsSync3(targetDir) && readdirSync(targetDir).length > 0) {
|
|
1183
|
+
throw new DeployAbort({
|
|
1184
|
+
code: "TARGET_NOT_EMPTY",
|
|
1185
|
+
message: `Target directory is not empty: ${targetDir}. Pick a different route, or run \`whop apps deploy\` from an empty directory.`
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
let app;
|
|
1189
|
+
try {
|
|
1190
|
+
app = await createApp({ name, route });
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
throw new DeployAbort({
|
|
1193
|
+
code: "CREATE_FAILED",
|
|
1194
|
+
message: err instanceof Error ? err.message : "Failed to register the app",
|
|
1195
|
+
retryable: true
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
const spin = c2.agent ? null : spinner();
|
|
1199
|
+
spin?.start("Scaffolding the latest TanStack Start template");
|
|
1200
|
+
try {
|
|
1201
|
+
runTanstackCreate({ targetDir, projectName: route, pm });
|
|
1202
|
+
spin?.stop(`Scaffolded ${route} from the latest TanStack Start template`);
|
|
1203
|
+
} catch (err) {
|
|
1204
|
+
spin?.error("Scaffold failed");
|
|
1205
|
+
throw new DeployAbort({
|
|
1206
|
+
code: "SCAFFOLD_FAILED",
|
|
1207
|
+
message: `${err instanceof Error ? err.message : "Failed to scaffold the project"}
|
|
1078
1208
|
|
|
1079
|
-
|
|
1080
|
-
});
|
|
1081
|
-
}
|
|
1082
|
-
if (!c2.agent && !c2.formatExplicit) {
|
|
1083
|
-
console.log(
|
|
1084
|
-
`${chalk.green.bold(`\u2713 Already linked to ${existing.name}`)} ${chalk.dim(`(${existing.app_id})`)} \u2014 wiring verified. Delete ${APP_CONFIG_FILENAME} to relink.`
|
|
1085
|
-
);
|
|
1086
|
-
}
|
|
1087
|
-
return c2.ok({
|
|
1088
|
-
id: existing.app_id,
|
|
1089
|
-
name: existing.name,
|
|
1090
|
-
route: existing.route,
|
|
1091
|
-
directory: projectDir
|
|
1209
|
+
The app ${app.id} is already registered \u2014 scaffold into a fresh directory and re-run \`whop apps deploy --app ${app.id}\` there.`
|
|
1092
1210
|
});
|
|
1093
1211
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1212
|
+
writeAppConfig(targetDir, { app_id: app.id, name, route });
|
|
1213
|
+
return targetDir;
|
|
1214
|
+
}
|
|
1215
|
+
async function ensureAppLinked(c2, projectDir, appOption) {
|
|
1216
|
+
if (appOption) {
|
|
1217
|
+
let app;
|
|
1218
|
+
try {
|
|
1219
|
+
app = await getApp(appOption);
|
|
1220
|
+
} catch (err) {
|
|
1221
|
+
throw new DeployAbort({
|
|
1100
1222
|
code: "APP_NOT_FOUND",
|
|
1101
|
-
message: `
|
|
1223
|
+
message: `Could not load app ${appOption}: ${err instanceof Error ? err.message : "unknown error"}`,
|
|
1224
|
+
retryable: true
|
|
1102
1225
|
});
|
|
1103
1226
|
}
|
|
1227
|
+
const config2 = await withRoute(c2, app);
|
|
1228
|
+
writeAppConfig(projectDir, config2);
|
|
1229
|
+
log(
|
|
1230
|
+
c2,
|
|
1231
|
+
chalk.dim(
|
|
1232
|
+
`Linked ${config2.name} (${config2.app_id}) \u2192 ${APP_CONFIG_FILENAME}`
|
|
1233
|
+
)
|
|
1234
|
+
);
|
|
1235
|
+
return config2;
|
|
1104
1236
|
}
|
|
1105
|
-
|
|
1106
|
-
if (
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1237
|
+
const existing = readAppConfig(projectDir);
|
|
1238
|
+
if (existing) return existing;
|
|
1239
|
+
const apps = await listApps();
|
|
1240
|
+
if (c2.agent) {
|
|
1241
|
+
const reuse = apps.length > 0 ? `This account has existing apps \u2014 ask the user whether to reuse one:
|
|
1242
|
+
${formatAppList(apps)}
|
|
1243
|
+
|
|
1244
|
+
To reuse one: whop apps deploy --app <app_id>
|
|
1245
|
+
|
|
1246
|
+
` : "This account has no apps yet, so a new one must be created.\n\n";
|
|
1247
|
+
throw new DeployAbort({
|
|
1248
|
+
code: "NO_APP_LINKED",
|
|
1249
|
+
message: [
|
|
1250
|
+
`No ${APP_CONFIG_FILENAME} here \u2014 this project isn't linked to a Whop app yet.`,
|
|
1251
|
+
"",
|
|
1252
|
+
`${reuse}To create a new app, ask the user for a name and route (the route becomes <route>.whop.app), then run:`,
|
|
1253
|
+
' whop apps create --name "My Site" --route my-site',
|
|
1254
|
+
"",
|
|
1255
|
+
"Then deploy with the returned app id:",
|
|
1256
|
+
" whop apps deploy --app <app_id>"
|
|
1257
|
+
].join("\n"),
|
|
1258
|
+
retryable: true,
|
|
1259
|
+
cta: {
|
|
1260
|
+
commands: [
|
|
1261
|
+
{
|
|
1262
|
+
command: "apps create",
|
|
1263
|
+
description: "Create a new app (pass --name and --route)"
|
|
1264
|
+
},
|
|
1265
|
+
{
|
|
1266
|
+
command: "apps deploy",
|
|
1267
|
+
description: "Re-run with --app <app_id> to link and deploy"
|
|
1268
|
+
}
|
|
1269
|
+
]
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
let selected = null;
|
|
1274
|
+
if (apps.length > 0) {
|
|
1275
|
+
const recent = [...apps].sort(
|
|
1276
|
+
(a, b) => (b.created_at ?? 0) - (a.created_at ?? 0)
|
|
1277
|
+
);
|
|
1278
|
+
const choice = await select({
|
|
1279
|
+
message: "Which app should this project deploy to?",
|
|
1280
|
+
options: [
|
|
1281
|
+
...recent.map((a) => ({
|
|
1282
|
+
value: a.id,
|
|
1283
|
+
label: `${a.name} (${a.id}${a.route ? `, ${a.route}.whop.app` : ""})`
|
|
1284
|
+
})),
|
|
1285
|
+
{ value: "__new__", label: "Create a new app" }
|
|
1286
|
+
]
|
|
1287
|
+
});
|
|
1288
|
+
if (isCancel(choice)) cancelled();
|
|
1289
|
+
if (choice !== "__new__") {
|
|
1290
|
+
selected = recent.find((a) => a.id === choice) ?? null;
|
|
1113
1291
|
}
|
|
1114
|
-
const value = await promptText("What is your app called?", "My Site");
|
|
1115
|
-
if (value === null)
|
|
1116
|
-
return c2.error({
|
|
1117
|
-
code: "CANCELLED",
|
|
1118
|
-
message: "Cancelled.",
|
|
1119
|
-
exitCode: 130
|
|
1120
|
-
});
|
|
1121
|
-
name = value;
|
|
1122
1292
|
}
|
|
1123
|
-
if (!
|
|
1124
|
-
const
|
|
1293
|
+
if (!selected) {
|
|
1294
|
+
const name = await promptText("What is your app called?", "My Site");
|
|
1295
|
+
if (name === null) cancelled();
|
|
1125
1296
|
const slug = slugify(name);
|
|
1126
|
-
const
|
|
1127
|
-
|
|
1297
|
+
const routeInput = await promptText(
|
|
1298
|
+
"Which route should it live at? (your-route.whop.app)",
|
|
1299
|
+
slug,
|
|
1300
|
+
slug
|
|
1128
1301
|
);
|
|
1129
|
-
if (
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
})),
|
|
1137
|
-
{ value: "__new__", label: `Create a new app called "${name}"` }
|
|
1138
|
-
]
|
|
1302
|
+
if (routeInput === null) cancelled();
|
|
1303
|
+
const route = slugify(routeInput);
|
|
1304
|
+
if (!route) {
|
|
1305
|
+
throw new DeployAbort({
|
|
1306
|
+
code: "ROUTE_REQUIRED",
|
|
1307
|
+
message: `Couldn't derive a route from "${routeInput}" \u2014 use letters and numbers (e.g. my-site).`,
|
|
1308
|
+
retryable: true
|
|
1139
1309
|
});
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
linked = similar.find((a) => a.id === choice) ?? null;
|
|
1148
|
-
}
|
|
1149
|
-
} else if (similar.length > 0 && c2.agent) {
|
|
1150
|
-
return c2.error({
|
|
1151
|
-
code: "SIMILAR_APPS_FOUND",
|
|
1152
|
-
message: `Existing app(s) with a similar name: ${similar.map((a) => `${a.name} (${a.id})`).join(
|
|
1153
|
-
", "
|
|
1154
|
-
)}. Pass --link <app_id> to link one, or --name with a distinct name to create new.`,
|
|
1310
|
+
}
|
|
1311
|
+
try {
|
|
1312
|
+
selected = await createApp({ name, route });
|
|
1313
|
+
} catch (err) {
|
|
1314
|
+
throw new DeployAbort({
|
|
1315
|
+
code: "CREATE_FAILED",
|
|
1316
|
+
message: err instanceof Error ? err.message : "Failed to register the app",
|
|
1155
1317
|
retryable: true
|
|
1156
1318
|
});
|
|
1157
1319
|
}
|
|
1158
1320
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
)
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1321
|
+
const config = await withRoute(c2, selected);
|
|
1322
|
+
writeAppConfig(projectDir, config);
|
|
1323
|
+
log(
|
|
1324
|
+
c2,
|
|
1325
|
+
chalk.dim(
|
|
1326
|
+
`Linked ${config.name} (${config.app_id}) \u2192 ${APP_CONFIG_FILENAME}`
|
|
1327
|
+
)
|
|
1328
|
+
);
|
|
1329
|
+
return config;
|
|
1330
|
+
}
|
|
1331
|
+
async function withRoute(c2, app) {
|
|
1332
|
+
if (app.route) return { app_id: app.id, name: app.name, route: app.route };
|
|
1333
|
+
if (c2.agent) {
|
|
1334
|
+
throw new DeployAbort({
|
|
1335
|
+
code: "ROUTE_REQUIRED",
|
|
1336
|
+
message: `App ${app.id} (${app.name}) has no route \u2014 hosted apps are served from <route>.whop.app. Ask the user which route to claim, then run:
|
|
1337
|
+
whop apps update ${app.id} --route <route>
|
|
1338
|
+
whop apps deploy --app ${app.id}`,
|
|
1339
|
+
retryable: true,
|
|
1340
|
+
cta: {
|
|
1341
|
+
commands: [
|
|
1342
|
+
{
|
|
1343
|
+
command: `apps update ${app.id}`,
|
|
1344
|
+
description: "Set the route (pass --route)"
|
|
1345
|
+
}
|
|
1346
|
+
]
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1173
1349
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1350
|
+
const slug = slugify(app.name);
|
|
1351
|
+
const routeInput = await promptText(
|
|
1352
|
+
`${app.name} has no route yet. Which route should it live at? (your-route.whop.app)`,
|
|
1353
|
+
slug,
|
|
1354
|
+
slug
|
|
1355
|
+
);
|
|
1356
|
+
if (routeInput === null) cancelled();
|
|
1357
|
+
const route = slugify(routeInput);
|
|
1358
|
+
if (!route) {
|
|
1359
|
+
throw new DeployAbort({
|
|
1176
1360
|
code: "ROUTE_REQUIRED",
|
|
1177
|
-
message: `Couldn't derive a route from "${
|
|
1361
|
+
message: `Couldn't derive a route from "${routeInput}" \u2014 use letters and numbers (e.g. my-site).`,
|
|
1178
1362
|
retryable: true
|
|
1179
1363
|
});
|
|
1180
1364
|
}
|
|
1181
|
-
let whopApp;
|
|
1182
1365
|
try {
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
} else {
|
|
1190
|
-
whopApp = await createApp({
|
|
1191
|
-
name,
|
|
1192
|
-
route,
|
|
1193
|
-
...options.companyId ? { company_id: options.companyId } : {}
|
|
1194
|
-
});
|
|
1195
|
-
}
|
|
1366
|
+
const updated = await updateAppRoute(app.id, route);
|
|
1367
|
+
return {
|
|
1368
|
+
app_id: updated.id,
|
|
1369
|
+
name: updated.name,
|
|
1370
|
+
route: updated.route ?? route
|
|
1371
|
+
};
|
|
1196
1372
|
} catch (err) {
|
|
1197
|
-
|
|
1198
|
-
code: "
|
|
1199
|
-
message: err instanceof Error ? err.message : "Failed to
|
|
1373
|
+
throw new DeployAbort({
|
|
1374
|
+
code: "UPDATE_FAILED",
|
|
1375
|
+
message: err instanceof Error ? err.message : "Failed to set the route",
|
|
1200
1376
|
retryable: true
|
|
1201
1377
|
});
|
|
1202
1378
|
}
|
|
1203
|
-
writeAppConfig(projectDir, { app_id: whopApp.id, name, route });
|
|
1204
|
-
const { manualSteps } = whopifyProject({ targetDir: projectDir, route });
|
|
1205
|
-
if (manualSteps.length > 0) {
|
|
1206
|
-
return c2.error({
|
|
1207
|
-
code: "WHOPIFY_INCOMPLETE",
|
|
1208
|
-
message: `${name} is registered on Whop (${whopApp.id}) and ${APP_CONFIG_FILENAME} is written, but the project isn't fully wired for hosting yet. Complete these steps:
|
|
1209
|
-
|
|
1210
|
-
${formatManualSteps(manualSteps)}
|
|
1211
|
-
|
|
1212
|
-
Then run \`whop apps deploy\`. Re-running \`whop apps create --existing\` re-checks the wiring and applies anything it can.`
|
|
1213
|
-
});
|
|
1214
|
-
}
|
|
1215
|
-
const url = whopApp.hosted_url ?? void 0;
|
|
1216
|
-
if (!c2.agent && !c2.formatExplicit) {
|
|
1217
|
-
console.log(
|
|
1218
|
-
[
|
|
1219
|
-
"",
|
|
1220
|
-
chalk.green.bold(`\u2713 ${linked ? "Linked" : "Created"} ${name}`),
|
|
1221
|
-
` ${chalk.dim(whopApp.id)}${url ? ` \u2192 ${chalk.cyan.underline(url)}` : ""}`,
|
|
1222
|
-
"",
|
|
1223
|
-
`${chalk.bold("Next")} ${chalk.cyan.bold("whop apps deploy")}`,
|
|
1224
|
-
""
|
|
1225
|
-
].join("\n")
|
|
1226
|
-
);
|
|
1227
|
-
}
|
|
1228
|
-
return c2.ok({ id: whopApp.id, name, route, url, directory: projectDir });
|
|
1229
1379
|
}
|
|
1230
|
-
function
|
|
1231
|
-
|
|
1232
|
-
|
|
1380
|
+
function resolveSecretsAppId(appOption) {
|
|
1381
|
+
if (appOption) return appOption;
|
|
1382
|
+
const projectDir = findProjectDir();
|
|
1383
|
+
const config = projectDir ? readAppConfig(projectDir) : null;
|
|
1384
|
+
if (config) return config.app_id;
|
|
1385
|
+
throw new Error(
|
|
1386
|
+
`No app specified. Pass --app app_xxx or run inside a project with ${APP_CONFIG_FILENAME}.`
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
function buildSecretsGroup() {
|
|
1390
|
+
const secrets = Cli_exports.create("secrets", {
|
|
1391
|
+
description: "Manage app secrets \u2014 encrypted at rest, injected as env bindings into the hosted runtime and `whop apps dev`"
|
|
1233
1392
|
});
|
|
1234
|
-
|
|
1235
|
-
description: "
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
name: external_exports.string().optional().describe("App name (e.g. 'My Site'; when linking, defaults to the linked app's name)")
|
|
1393
|
+
secrets.command("list", {
|
|
1394
|
+
description: "Show the app's secrets",
|
|
1395
|
+
options: external_exports.object({
|
|
1396
|
+
app: external_exports.string().optional().describe("App id (defaults to the project's linked app)")
|
|
1239
1397
|
}),
|
|
1398
|
+
run: async (c2) => {
|
|
1399
|
+
const remote = await getApp(resolveSecretsAppId(c2.options.app));
|
|
1400
|
+
return c2.ok({ app_id: remote.id, secrets: remote.secrets ?? {} });
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
secrets.command("set", {
|
|
1404
|
+
description: "Add or overwrite secrets",
|
|
1240
1405
|
options: external_exports.object({
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
"Directory to scaffold into (defaults to ./<route>); with --existing, the project directory (defaults to the current directory)"
|
|
1244
|
-
),
|
|
1245
|
-
existing: external_exports.boolean().optional().describe(
|
|
1246
|
-
"Wire up the existing Vite project here instead of scaffolding a new one"
|
|
1247
|
-
),
|
|
1248
|
-
link: external_exports.string().optional().describe(
|
|
1249
|
-
"Reuse an existing app by id (app_xxx) instead of creating a new one \u2014 works when scaffolding and with --existing"
|
|
1250
|
-
),
|
|
1251
|
-
company_id: external_exports.string().optional().describe(
|
|
1252
|
-
"Business account to create the app under (defaults to your active account)"
|
|
1253
|
-
),
|
|
1254
|
-
install: external_exports.boolean().optional().describe(
|
|
1255
|
-
"Install dependencies after scaffolding fresh (default: true)"
|
|
1256
|
-
),
|
|
1257
|
-
pm: external_exports.enum(["npm", "pnpm", "bun", "yarn"]).optional().describe(
|
|
1258
|
-
"Package manager to use (default: first of bun, pnpm, npm found on your PATH)"
|
|
1259
|
-
)
|
|
1406
|
+
secret: external_exports.array(external_exports.string()).min(1).describe("KEY=VALUE pair (repeatable)"),
|
|
1407
|
+
app: external_exports.string().optional().describe("App id (defaults to the project's linked app)")
|
|
1260
1408
|
}),
|
|
1261
|
-
output: AppSchema,
|
|
1262
|
-
outputPolicy: "agent-only",
|
|
1263
1409
|
examples: [
|
|
1264
|
-
{
|
|
1265
|
-
|
|
1266
|
-
options: { route: "my-site" },
|
|
1267
|
-
description: "Scaffold 'My Site' into ./my-site, live at my-site.whop.app"
|
|
1268
|
-
},
|
|
1269
|
-
{
|
|
1270
|
-
args: { name: "My Site" },
|
|
1271
|
-
options: { existing: true },
|
|
1272
|
-
description: "Register + wire up the Vite project in the current directory"
|
|
1273
|
-
}
|
|
1410
|
+
{ options: { secret: ["MAIL_API_KEY=mail-key-123"] } },
|
|
1411
|
+
{ options: { secret: ["A=1", "B=2"], app: "app_xxxxxxxx" } }
|
|
1274
1412
|
],
|
|
1275
1413
|
run: async (c2) => {
|
|
1276
|
-
const
|
|
1277
|
-
|
|
1278
|
-
const
|
|
1279
|
-
|
|
1280
|
-
return runRegisterFlow(c2, {
|
|
1281
|
-
dir: projectDir,
|
|
1282
|
-
name: c2.args.name,
|
|
1283
|
-
route: c2.options.route,
|
|
1284
|
-
link: c2.options.link,
|
|
1285
|
-
companyId: c2.options.company_id
|
|
1286
|
-
});
|
|
1287
|
-
}
|
|
1288
|
-
if (!explicitDir && existsSync3(join3(process.cwd(), "package.json"))) {
|
|
1289
|
-
log(
|
|
1290
|
-
c2,
|
|
1291
|
-
chalk.dim(
|
|
1292
|
-
"Detected an existing project here \u2014 scaffolding a new app in a subdirectory. Pass --existing to wire up this project instead."
|
|
1293
|
-
)
|
|
1294
|
-
);
|
|
1295
|
-
}
|
|
1296
|
-
let linked = null;
|
|
1297
|
-
if (c2.options.link) {
|
|
1298
|
-
const apps = await listApps();
|
|
1299
|
-
linked = apps.find((a) => a.id === c2.options.link) ?? null;
|
|
1300
|
-
if (!linked) {
|
|
1301
|
-
return c2.error({
|
|
1302
|
-
code: "APP_NOT_FOUND",
|
|
1303
|
-
message: `No app ${c2.options.link} found on your account.`
|
|
1304
|
-
});
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
let name = c2.args.name ?? linked?.name;
|
|
1308
|
-
if (!name) {
|
|
1309
|
-
if (c2.agent) {
|
|
1414
|
+
const updates = {};
|
|
1415
|
+
for (const pair of c2.options.secret) {
|
|
1416
|
+
const eq = pair.indexOf("=");
|
|
1417
|
+
if (eq <= 0) {
|
|
1310
1418
|
return c2.error({
|
|
1311
|
-
code: "
|
|
1312
|
-
message:
|
|
1313
|
-
retryable: true
|
|
1314
|
-
});
|
|
1315
|
-
}
|
|
1316
|
-
const value = await promptText("What is your app called?", "My Site");
|
|
1317
|
-
if (value === null)
|
|
1318
|
-
return c2.error({
|
|
1319
|
-
code: "CANCELLED",
|
|
1320
|
-
message: "Cancelled.",
|
|
1321
|
-
exitCode: 130
|
|
1322
|
-
});
|
|
1323
|
-
name = value;
|
|
1324
|
-
}
|
|
1325
|
-
let route = c2.options.route ?? linked?.route ?? slugify(name);
|
|
1326
|
-
if (!c2.options.route && !linked?.route && !c2.agent) {
|
|
1327
|
-
const value = await promptText(
|
|
1328
|
-
"Which route should it live at? (your-route.whop.app)",
|
|
1329
|
-
slugify(name),
|
|
1330
|
-
slugify(name)
|
|
1331
|
-
);
|
|
1332
|
-
if (value === null)
|
|
1333
|
-
return c2.error({
|
|
1334
|
-
code: "CANCELLED",
|
|
1335
|
-
message: "Cancelled.",
|
|
1336
|
-
exitCode: 130
|
|
1337
|
-
});
|
|
1338
|
-
route = slugify(value);
|
|
1339
|
-
}
|
|
1340
|
-
if (!route) {
|
|
1341
|
-
return c2.error({
|
|
1342
|
-
code: "ROUTE_REQUIRED",
|
|
1343
|
-
message: `Couldn't derive a route from "${name}" \u2014 pass --route with letters and numbers (e.g. --route my-site).`,
|
|
1344
|
-
retryable: true
|
|
1345
|
-
});
|
|
1346
|
-
}
|
|
1347
|
-
let pm = c2.options.pm;
|
|
1348
|
-
if (pm && !isPmInstalled(pm)) {
|
|
1349
|
-
return c2.error({
|
|
1350
|
-
code: "PM_NOT_INSTALLED",
|
|
1351
|
-
message: `${pm} is not installed (or not on your PATH). Install it: ${pmInstallHint(pm)}`
|
|
1352
|
-
});
|
|
1353
|
-
}
|
|
1354
|
-
if (!pm) pm = detectPackageManager() ?? void 0;
|
|
1355
|
-
if (!pm) {
|
|
1356
|
-
if (c2.agent) {
|
|
1357
|
-
return c2.error({
|
|
1358
|
-
code: "NO_PACKAGE_MANAGER",
|
|
1359
|
-
message: `No package manager found (looked for bun, pnpm, npm). Install bun (recommended): ${pmInstallHint("bun")} \u2014 then re-run \`whop apps create\`.`
|
|
1360
|
-
});
|
|
1361
|
-
}
|
|
1362
|
-
const choice = await select({
|
|
1363
|
-
message: "No package manager found (bun, pnpm, or npm). Which one do you want to install?",
|
|
1364
|
-
options: [
|
|
1365
|
-
{ value: "bun", label: "bun", hint: "recommended" },
|
|
1366
|
-
{ value: "pnpm", label: "pnpm" },
|
|
1367
|
-
{ value: "npm", label: "npm", hint: "comes with Node.js" }
|
|
1368
|
-
]
|
|
1369
|
-
});
|
|
1370
|
-
if (isCancel(choice)) {
|
|
1371
|
-
return c2.error({
|
|
1372
|
-
code: "CANCELLED",
|
|
1373
|
-
message: "Cancelled.",
|
|
1374
|
-
exitCode: 130
|
|
1375
|
-
});
|
|
1376
|
-
}
|
|
1377
|
-
return c2.error({
|
|
1378
|
-
code: "NO_PACKAGE_MANAGER",
|
|
1379
|
-
message: `Install ${String(choice)} with:
|
|
1380
|
-
|
|
1381
|
-
${pmInstallHint(choice)}
|
|
1382
|
-
|
|
1383
|
-
then re-run \`whop apps create\`.`
|
|
1384
|
-
});
|
|
1385
|
-
}
|
|
1386
|
-
const targetDir = resolve2(c2.options.dir ?? `./${route}`);
|
|
1387
|
-
if (existsSync3(targetDir) && readdirSync(targetDir).length > 0) {
|
|
1388
|
-
return c2.error({
|
|
1389
|
-
code: "TARGET_NOT_EMPTY",
|
|
1390
|
-
message: `Target directory is not empty: ${targetDir}. Choose an empty directory with --dir, or a different route.`
|
|
1391
|
-
});
|
|
1392
|
-
}
|
|
1393
|
-
let whopApp;
|
|
1394
|
-
try {
|
|
1395
|
-
if (linked) {
|
|
1396
|
-
whopApp = linked;
|
|
1397
|
-
if (!linked.route || c2.options.route && c2.options.route !== linked.route) {
|
|
1398
|
-
whopApp = await updateAppRoute(linked.id, route);
|
|
1399
|
-
}
|
|
1400
|
-
} else {
|
|
1401
|
-
whopApp = await createApp({
|
|
1402
|
-
name,
|
|
1403
|
-
route,
|
|
1404
|
-
...c2.options.company_id ? { company_id: c2.options.company_id } : {}
|
|
1419
|
+
code: "INVALID_SECRET",
|
|
1420
|
+
message: `"${pair}" is not KEY=VALUE. Use --secret NAME=value (an empty value deletes the secret).`
|
|
1405
1421
|
});
|
|
1406
1422
|
}
|
|
1407
|
-
|
|
1408
|
-
return c2.error({
|
|
1409
|
-
code: "CREATE_FAILED",
|
|
1410
|
-
message: err instanceof Error ? err.message : "Failed to create the app on Whop",
|
|
1411
|
-
retryable: true
|
|
1412
|
-
});
|
|
1413
|
-
}
|
|
1414
|
-
let whopifySteps = [];
|
|
1415
|
-
try {
|
|
1416
|
-
log(
|
|
1417
|
-
c2,
|
|
1418
|
-
chalk.dim("\nScaffolding with the latest TanStack Start template...")
|
|
1419
|
-
);
|
|
1420
|
-
runTanstackCreate({
|
|
1421
|
-
targetDir,
|
|
1422
|
-
projectName: route,
|
|
1423
|
-
pm,
|
|
1424
|
-
quiet: Boolean(c2.agent)
|
|
1425
|
-
});
|
|
1426
|
-
whopifySteps = whopifyProject({ targetDir, route }).manualSteps;
|
|
1427
|
-
} catch (err) {
|
|
1428
|
-
return c2.error({
|
|
1429
|
-
code: "SCAFFOLD_FAILED",
|
|
1430
|
-
message: err instanceof Error ? err.message : "Failed to scaffold the project"
|
|
1431
|
-
});
|
|
1432
|
-
}
|
|
1433
|
-
writeAppConfig(targetDir, { app_id: whopApp.id, name, route });
|
|
1434
|
-
if (whopifySteps.length > 0) {
|
|
1435
|
-
return c2.error({
|
|
1436
|
-
code: "WHOPIFY_INCOMPLETE",
|
|
1437
|
-
message: `Scaffolded ${targetDir} and registered ${whopApp.id}, but the project needs manual wiring:
|
|
1438
|
-
|
|
1439
|
-
${formatManualSteps(whopifySteps)}
|
|
1440
|
-
|
|
1441
|
-
Then run \`whop apps deploy\`. Re-running \`whop apps create --existing\` in the project re-checks the wiring.`
|
|
1442
|
-
});
|
|
1423
|
+
updates[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
1443
1424
|
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
cwd: targetDir,
|
|
1450
|
-
stdio: c2.agent ? "ignore" : "inherit",
|
|
1451
|
-
shell: SPAWN_VIA_SHELL2
|
|
1452
|
-
});
|
|
1453
|
-
installFailed = Boolean(result.error) || result.status !== 0;
|
|
1454
|
-
if (installFailed) {
|
|
1455
|
-
log(
|
|
1456
|
-
c2,
|
|
1457
|
-
chalk.yellow(
|
|
1458
|
-
`
|
|
1459
|
-
${pm} install failed \u2014 run it manually inside ${targetDir}.`
|
|
1460
|
-
)
|
|
1461
|
-
);
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
const url = whopApp.hosted_url ?? void 0;
|
|
1465
|
-
if (!c2.agent && !c2.formatExplicit) {
|
|
1466
|
-
console.log(
|
|
1467
|
-
[
|
|
1468
|
-
"",
|
|
1469
|
-
chalk.green.bold(`\u2713 ${name} is ready`),
|
|
1470
|
-
` ${chalk.dim(whopApp.id)}`,
|
|
1471
|
-
"",
|
|
1472
|
-
` ${chalk.bold("Directory")} ${targetDir}`,
|
|
1473
|
-
...url ? [
|
|
1474
|
-
` ${chalk.bold("URL")} ${chalk.cyan.underline(url)} ${chalk.dim("(after first deploy)")}`
|
|
1475
|
-
] : [],
|
|
1476
|
-
"",
|
|
1477
|
-
`${chalk.bold("Next")}`,
|
|
1478
|
-
` cd ${targetDir}`,
|
|
1479
|
-
` ${chalk.cyan.bold("whop apps dev")} ${chalk.dim("\u2192 local dev server")}`,
|
|
1480
|
-
` ${chalk.cyan.bold("whop apps deploy")} ${chalk.dim("\u2192 ship it live")}`,
|
|
1481
|
-
""
|
|
1482
|
-
].join("\n")
|
|
1483
|
-
);
|
|
1484
|
-
}
|
|
1485
|
-
return c2.ok({
|
|
1486
|
-
id: whopApp.id,
|
|
1487
|
-
name,
|
|
1488
|
-
route,
|
|
1489
|
-
url,
|
|
1490
|
-
directory: targetDir,
|
|
1491
|
-
install_failed: installFailed
|
|
1492
|
-
});
|
|
1425
|
+
const remote = await updateAppSecrets(
|
|
1426
|
+
resolveSecretsAppId(c2.options.app),
|
|
1427
|
+
updates
|
|
1428
|
+
);
|
|
1429
|
+
return c2.ok({ app_id: remote.id, secrets: remote.secrets ?? {} });
|
|
1493
1430
|
}
|
|
1494
1431
|
});
|
|
1495
|
-
|
|
1496
|
-
description: "
|
|
1497
|
-
|
|
1498
|
-
|
|
1432
|
+
secrets.command("unset", {
|
|
1433
|
+
description: "Delete secrets by name",
|
|
1434
|
+
options: external_exports.object({
|
|
1435
|
+
key: external_exports.array(external_exports.string()).min(1).describe("Secret name (repeatable)"),
|
|
1436
|
+
app: external_exports.string().optional().describe("App id (defaults to the project's linked app)")
|
|
1437
|
+
}),
|
|
1438
|
+
examples: [{ options: { key: ["MAIL_API_KEY"] } }],
|
|
1499
1439
|
run: async (c2) => {
|
|
1500
|
-
const
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
route: a.route,
|
|
1506
|
-
url: a.hosted_url ?? void 0
|
|
1507
|
-
}))
|
|
1440
|
+
const updates = {};
|
|
1441
|
+
for (const key of c2.options.key) updates[key] = null;
|
|
1442
|
+
const remote = await updateAppSecrets(
|
|
1443
|
+
resolveSecretsAppId(c2.options.app),
|
|
1444
|
+
updates
|
|
1508
1445
|
);
|
|
1446
|
+
return c2.ok({ app_id: remote.id, secrets: remote.secrets ?? {} });
|
|
1509
1447
|
}
|
|
1510
1448
|
});
|
|
1449
|
+
return secrets;
|
|
1450
|
+
}
|
|
1451
|
+
async function buildAppGroup() {
|
|
1452
|
+
const app = Cli_exports.create("apps", {
|
|
1453
|
+
description: "Build and deploy fully-hosted web apps on Whop (*.whop.app)"
|
|
1454
|
+
});
|
|
1455
|
+
await registerSpecCommands(app, "Apps");
|
|
1456
|
+
const builds = Cli_exports.create("builds", {
|
|
1457
|
+
description: "Manage app builds (list, inspect, promote)"
|
|
1458
|
+
});
|
|
1459
|
+
await registerSpecCommands(builds, "App builds");
|
|
1460
|
+
app.command(builds);
|
|
1461
|
+
app.command(buildSecretsGroup());
|
|
1511
1462
|
app.command("dev", {
|
|
1512
1463
|
description: "Run the local dev server for this app",
|
|
1513
1464
|
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.",
|
|
@@ -1515,10 +1466,24 @@ ${pm} install failed \u2014 run it manually inside ${targetDir}.`
|
|
|
1515
1466
|
dir: external_exports.string().optional().describe("Project directory (defaults to the current directory)")
|
|
1516
1467
|
}),
|
|
1517
1468
|
run: async (c2) => {
|
|
1518
|
-
const
|
|
1519
|
-
|
|
1469
|
+
const projectDir = c2.options.dir ? resolve2(c2.options.dir) : findProjectDir();
|
|
1470
|
+
const config = projectDir ? readAppConfig(projectDir) : null;
|
|
1471
|
+
if (!projectDir || !config) {
|
|
1472
|
+
return c2.error({
|
|
1473
|
+
code: "NO_PROJECT",
|
|
1474
|
+
message: `No ${APP_CONFIG_FILENAME} found in this directory or any parent. Run \`whop apps deploy\` to link this project to a Whop app.`,
|
|
1475
|
+
cta: {
|
|
1476
|
+
commands: [
|
|
1477
|
+
{
|
|
1478
|
+
command: "apps deploy",
|
|
1479
|
+
description: "Link this project to a Whop app and ship it"
|
|
1480
|
+
}
|
|
1481
|
+
]
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1520
1485
|
const injected = {
|
|
1521
|
-
WHOP_APP_ID:
|
|
1486
|
+
WHOP_APP_ID: config.app_id
|
|
1522
1487
|
};
|
|
1523
1488
|
if (process.env.WHOP_API_KEY) {
|
|
1524
1489
|
log(
|
|
@@ -1548,9 +1513,43 @@ ${pm} install failed \u2014 run it manually inside ${targetDir}.`
|
|
|
1548
1513
|
);
|
|
1549
1514
|
}
|
|
1550
1515
|
}
|
|
1551
|
-
log(c2, chalk.dim(`Starting dev server for ${project.config.name}...`));
|
|
1552
1516
|
try {
|
|
1553
|
-
|
|
1517
|
+
const remote = await getApp(config.app_id);
|
|
1518
|
+
const secretEntries = Object.entries(remote.secrets ?? {}).filter(
|
|
1519
|
+
([key]) => !(key in process.env) && !(key in injected)
|
|
1520
|
+
);
|
|
1521
|
+
const unsafe = secretEntries.filter(([key]) => isUnsafeDevEnvKey(key));
|
|
1522
|
+
for (const [key, value] of secretEntries) {
|
|
1523
|
+
if (!isUnsafeDevEnvKey(key)) injected[key] = value;
|
|
1524
|
+
}
|
|
1525
|
+
if (unsafe.length > 0) {
|
|
1526
|
+
log(
|
|
1527
|
+
c2,
|
|
1528
|
+
chalk.yellow(
|
|
1529
|
+
`Skipped app secret${unsafe.length === 1 ? "" : "s"} ${unsafe.map(([key]) => key).join(", ")} \u2014 these names control the local runtime and are never injected by \`whop apps dev\`.`
|
|
1530
|
+
)
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
const injectedCount = secretEntries.length - unsafe.length;
|
|
1534
|
+
if (injectedCount > 0) {
|
|
1535
|
+
log(
|
|
1536
|
+
c2,
|
|
1537
|
+
chalk.dim(
|
|
1538
|
+
`Injected ${injectedCount} app secret${injectedCount === 1 ? "" : "s"} into the environment (${secretEntries.filter(([key]) => !isUnsafeDevEnvKey(key)).map(([key]) => key).join(", ")}).`
|
|
1539
|
+
)
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
} catch (err) {
|
|
1543
|
+
log(
|
|
1544
|
+
c2,
|
|
1545
|
+
chalk.yellow(
|
|
1546
|
+
`Couldn't fetch app secrets (${err instanceof Error ? err.message : "unknown error"}) \u2014 continuing without them.`
|
|
1547
|
+
)
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
log(c2, chalk.dim(`Starting dev server for ${config.name}...`));
|
|
1551
|
+
try {
|
|
1552
|
+
runScript(projectDir, "dev", injected);
|
|
1554
1553
|
} catch (err) {
|
|
1555
1554
|
return c2.error({
|
|
1556
1555
|
code: "DEV_FAILED",
|
|
@@ -1562,12 +1561,13 @@ ${pm} install failed \u2014 run it manually inside ${targetDir}.`
|
|
|
1562
1561
|
});
|
|
1563
1562
|
app.command("deploy", {
|
|
1564
1563
|
description: "Build, upload and ship this app live",
|
|
1565
|
-
hint: "
|
|
1564
|
+
hint: "The one-stop deploy: verifies the project is a Whop-ready Vite app (offering to scaffold or link one if not), builds, typechecks, uploads the build, and promotes it to production. Use --skip_promote to upload a preview-only build, then promote it later with `whop apps builds promote <build_id>`.",
|
|
1566
1565
|
options: external_exports.object({
|
|
1567
1566
|
dir: external_exports.string().optional().describe("Project directory (defaults to the current directory)"),
|
|
1568
|
-
|
|
1569
|
-
"
|
|
1567
|
+
app: external_exports.string().optional().describe(
|
|
1568
|
+
"Link this project to an app (app_xxx) before deploying \u2014 writes whop.app.json, replacing any existing link"
|
|
1570
1569
|
),
|
|
1570
|
+
skip_promote: external_exports.boolean().optional().describe("Upload the build without promoting it to production"),
|
|
1571
1571
|
skip_typecheck: external_exports.boolean().optional().describe("Skip the typecheck step"),
|
|
1572
1572
|
skip_build: external_exports.boolean().optional().describe("Skip the build step and upload the existing dist/ output")
|
|
1573
1573
|
}),
|
|
@@ -1576,20 +1576,45 @@ ${pm} install failed \u2014 run it manually inside ${targetDir}.`
|
|
|
1576
1576
|
examples: [
|
|
1577
1577
|
{ description: "Build and ship to production" },
|
|
1578
1578
|
{
|
|
1579
|
-
options: {
|
|
1579
|
+
options: { app: "app_xxxxxxxx" },
|
|
1580
|
+
description: "Link this project to an existing app, then deploy"
|
|
1581
|
+
},
|
|
1582
|
+
{
|
|
1583
|
+
options: { skip_promote: true },
|
|
1580
1584
|
description: "Upload a preview build without promoting"
|
|
1581
1585
|
}
|
|
1582
1586
|
],
|
|
1583
1587
|
run: async (c2) => {
|
|
1584
|
-
const project = loadProject(c2.options.dir);
|
|
1585
|
-
if (!project.ok) return c2.error(project.error);
|
|
1586
|
-
const { projectDir, config } = project;
|
|
1587
1588
|
try {
|
|
1589
|
+
const pm = ensurePackageManager();
|
|
1590
|
+
const { projectDir, freshlyScaffolded } = await ensureViteApp(
|
|
1591
|
+
c2,
|
|
1592
|
+
pm,
|
|
1593
|
+
c2.options.dir
|
|
1594
|
+
);
|
|
1595
|
+
const config = await ensureAppLinked(c2, projectDir, c2.options.app);
|
|
1596
|
+
const { manualSteps } = whopifyProject({
|
|
1597
|
+
targetDir: projectDir,
|
|
1598
|
+
route: config.route
|
|
1599
|
+
});
|
|
1600
|
+
if (manualSteps.length > 0) {
|
|
1601
|
+
return c2.error({
|
|
1602
|
+
code: "WHOPIFY_INCOMPLETE",
|
|
1603
|
+
message: `${config.name} is linked (${config.app_id}), but the project isn't fully wired for Whop hosting yet. Complete these steps:
|
|
1604
|
+
|
|
1605
|
+
${formatManualSteps(manualSteps)}
|
|
1606
|
+
|
|
1607
|
+
Then re-run \`whop apps deploy\` \u2014 it re-checks the wiring and applies anything it can.`
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1588
1610
|
if (!c2.options.skip_build) {
|
|
1589
|
-
|
|
1611
|
+
if (freshlyScaffolded || !existsSync3(join3(projectDir, "node_modules", "@whop", "cli"))) {
|
|
1612
|
+
runInstall(c2, projectDir);
|
|
1613
|
+
}
|
|
1614
|
+
log(c2, chalk.dim("[1/4] Building..."));
|
|
1590
1615
|
runScript(projectDir, "build", { WHOP_APP_ID: config.app_id });
|
|
1591
1616
|
if (!c2.options.skip_typecheck && hasScript(projectDir, "typecheck")) {
|
|
1592
|
-
log(c2, chalk.dim("[2/
|
|
1617
|
+
log(c2, chalk.dim("[2/4] Typechecking..."));
|
|
1593
1618
|
runScript(projectDir, "typecheck");
|
|
1594
1619
|
}
|
|
1595
1620
|
}
|
|
@@ -1600,26 +1625,50 @@ ${pm} install failed \u2014 run it manually inside ${targetDir}.`
|
|
|
1600
1625
|
message: `No build archive at ${BUILD_ARCHIVE}. Add the whop() plugin from @whop/cli/vite to your vite config, then build. See https://whop.com/docs/apps/hosting.`
|
|
1601
1626
|
});
|
|
1602
1627
|
}
|
|
1603
|
-
log(c2, chalk.dim("[3/3] Uploading + creating build..."));
|
|
1604
1628
|
const zip = readFileSync3(archivePath);
|
|
1629
|
+
const sizeMb = (zip.byteLength / 1024 / 1024).toFixed(1);
|
|
1605
1630
|
const checksum = createHash("sha256").update(zip).digest("hex");
|
|
1606
|
-
const
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
await
|
|
1618
|
-
|
|
1631
|
+
const spin = c2.agent ? null : spinner();
|
|
1632
|
+
spin?.start(`[3/4] Uploading build archive (${sizeMb} MB)`);
|
|
1633
|
+
let build;
|
|
1634
|
+
try {
|
|
1635
|
+
const fileId = await uploadBuildArchive(
|
|
1636
|
+
new Uint8Array(zip),
|
|
1637
|
+
`${config.route}-build.zip`
|
|
1638
|
+
);
|
|
1639
|
+
spin?.message("[3/4] Processing archive");
|
|
1640
|
+
await waitForFileReady(fileId);
|
|
1641
|
+
spin?.message("[3/4] Creating build");
|
|
1642
|
+
build = await createAppBuild({
|
|
1643
|
+
app_id: config.app_id,
|
|
1644
|
+
checksum,
|
|
1645
|
+
file_id: fileId
|
|
1646
|
+
});
|
|
1647
|
+
spin?.stop(`[3/4] Build uploaded (${build.id})`);
|
|
1648
|
+
} catch (err) {
|
|
1649
|
+
spin?.error("[3/4] Upload failed");
|
|
1650
|
+
throw err;
|
|
1651
|
+
}
|
|
1652
|
+
if (!c2.options.skip_promote) {
|
|
1653
|
+
const promoteSpin = c2.agent ? null : spinner();
|
|
1654
|
+
promoteSpin?.start("[4/4] Promoting to production");
|
|
1655
|
+
try {
|
|
1656
|
+
await promoteAppBuild(build.id);
|
|
1657
|
+
build = await waitForBuildPromotion(build.id);
|
|
1658
|
+
promoteSpin?.stop(
|
|
1659
|
+
build.is_production ? "[4/4] Promoted to production" : `[4/4] Promotion pending (status: ${build.status})`
|
|
1660
|
+
);
|
|
1661
|
+
} catch (err) {
|
|
1662
|
+
promoteSpin?.error("[4/4] Promote failed");
|
|
1663
|
+
throw err;
|
|
1664
|
+
}
|
|
1665
|
+
} else {
|
|
1666
|
+
log(c2, chalk.dim("[4/4] Skipping promotion (--skip_promote)"));
|
|
1619
1667
|
}
|
|
1620
1668
|
const promoted = build.is_production === true;
|
|
1621
1669
|
const productionUrl = promoted ? (await getApp(config.app_id)).hosted_url ?? void 0 : void 0;
|
|
1622
1670
|
if (!c2.agent && !c2.formatExplicit) {
|
|
1671
|
+
const cdTarget = relative(process.cwd(), projectDir) || ".";
|
|
1623
1672
|
console.log(
|
|
1624
1673
|
[
|
|
1625
1674
|
"",
|
|
@@ -1631,19 +1680,39 @@ ${pm} install failed \u2014 run it manually inside ${targetDir}.`
|
|
|
1631
1680
|
...promoted && productionUrl ? [
|
|
1632
1681
|
` ${chalk.bold("Live")} ${chalk.cyan.underline(productionUrl)}`
|
|
1633
1682
|
] : [
|
|
1634
|
-
`${chalk.bold("Promote when ready")} ${chalk.cyan.bold(`whop apps promote ${build.id}`)}`
|
|
1683
|
+
`${chalk.bold("Promote when ready")} ${chalk.cyan.bold(`whop apps builds promote ${build.id}`)}`
|
|
1635
1684
|
],
|
|
1685
|
+
...freshlyScaffolded ? [
|
|
1686
|
+
"",
|
|
1687
|
+
chalk.bold("Next"),
|
|
1688
|
+
...cdTarget !== "." ? [` cd ${cdTarget}`] : [],
|
|
1689
|
+
` ${chalk.cyan.bold("whop apps dev")} ${chalk.dim("\u2192 local dev server")}`,
|
|
1690
|
+
` ${chalk.cyan.bold("whop apps deploy")} ${chalk.dim("\u2192 ship your changes")}`
|
|
1691
|
+
] : [],
|
|
1636
1692
|
""
|
|
1637
1693
|
].join("\n")
|
|
1638
1694
|
);
|
|
1639
1695
|
}
|
|
1640
|
-
return c2.ok(
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1696
|
+
return c2.ok(
|
|
1697
|
+
{
|
|
1698
|
+
id: build.id,
|
|
1699
|
+
status: build.status,
|
|
1700
|
+
is_production: promoted,
|
|
1701
|
+
url: productionUrl
|
|
1702
|
+
},
|
|
1703
|
+
promoted ? void 0 : {
|
|
1704
|
+
cta: {
|
|
1705
|
+
commands: [
|
|
1706
|
+
{
|
|
1707
|
+
command: `apps builds promote ${build.id}`,
|
|
1708
|
+
description: "Promote this build to production"
|
|
1709
|
+
}
|
|
1710
|
+
]
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
);
|
|
1646
1714
|
} catch (err) {
|
|
1715
|
+
if (err instanceof DeployAbort) return c2.error(err.options);
|
|
1647
1716
|
return c2.error({
|
|
1648
1717
|
code: "DEPLOY_FAILED",
|
|
1649
1718
|
message: err instanceof Error ? err.message : "Deploy failed",
|
|
@@ -1652,91 +1721,6 @@ ${pm} install failed \u2014 run it manually inside ${targetDir}.`
|
|
|
1652
1721
|
}
|
|
1653
1722
|
}
|
|
1654
1723
|
});
|
|
1655
|
-
app.command("promote", {
|
|
1656
|
-
description: "Promote a previously uploaded build to production",
|
|
1657
|
-
args: external_exports.object({
|
|
1658
|
-
build_id: external_exports.string().describe("The build to promote (apbu_xxx)")
|
|
1659
|
-
}),
|
|
1660
|
-
options: external_exports.object({
|
|
1661
|
-
dir: external_exports.string().optional().describe("Project directory (defaults to the current directory)")
|
|
1662
|
-
}),
|
|
1663
|
-
output: BuildSchema,
|
|
1664
|
-
outputPolicy: "agent-only",
|
|
1665
|
-
run: async (c2) => {
|
|
1666
|
-
const project = loadProject(c2.options.dir);
|
|
1667
|
-
if (!project.ok) return c2.error(project.error);
|
|
1668
|
-
try {
|
|
1669
|
-
await promoteAppBuild(c2.args.build_id);
|
|
1670
|
-
const build = await waitForBuildPromotion(c2.args.build_id);
|
|
1671
|
-
if (!build.is_production) {
|
|
1672
|
-
return c2.error({
|
|
1673
|
-
code: "PROMOTE_PENDING",
|
|
1674
|
-
message: `Build ${build.id} was accepted but is still processing (status: ${build.status}). It will go live once approved \u2014 re-run \`whop apps promote ${build.id}\` to confirm.`,
|
|
1675
|
-
retryable: true
|
|
1676
|
-
});
|
|
1677
|
-
}
|
|
1678
|
-
const url = (await getApp(project.config.app_id)).hosted_url ?? void 0;
|
|
1679
|
-
if (!c2.agent && !c2.formatExplicit) {
|
|
1680
|
-
console.log(
|
|
1681
|
-
[
|
|
1682
|
-
"",
|
|
1683
|
-
chalk.green.bold("\u2713 Promoted to production"),
|
|
1684
|
-
` ${chalk.dim(build.id)}`,
|
|
1685
|
-
...url ? [` ${chalk.bold("Live")} ${chalk.cyan.underline(url)}`] : [],
|
|
1686
|
-
""
|
|
1687
|
-
].join("\n")
|
|
1688
|
-
);
|
|
1689
|
-
}
|
|
1690
|
-
return c2.ok({
|
|
1691
|
-
id: build.id,
|
|
1692
|
-
status: build.status,
|
|
1693
|
-
is_production: build.is_production,
|
|
1694
|
-
url
|
|
1695
|
-
});
|
|
1696
|
-
} catch (err) {
|
|
1697
|
-
return c2.error({
|
|
1698
|
-
code: "PROMOTE_FAILED",
|
|
1699
|
-
message: err instanceof Error ? err.message : "Promote failed",
|
|
1700
|
-
retryable: true
|
|
1701
|
-
});
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
});
|
|
1705
|
-
app.command("builds", {
|
|
1706
|
-
description: "List builds for this app",
|
|
1707
|
-
options: external_exports.object({
|
|
1708
|
-
dir: external_exports.string().optional().describe("Project directory (defaults to the current directory)")
|
|
1709
|
-
}),
|
|
1710
|
-
output: external_exports.object({ builds: external_exports.array(BuildSchema) }),
|
|
1711
|
-
outputPolicy: "agent-only",
|
|
1712
|
-
run: async (c2) => {
|
|
1713
|
-
const project = loadProject(c2.options.dir);
|
|
1714
|
-
if (!project.ok) return c2.error(project.error);
|
|
1715
|
-
try {
|
|
1716
|
-
const builds = await listAppBuilds(project.config.app_id);
|
|
1717
|
-
const rows = builds.map((build) => ({
|
|
1718
|
-
id: build.id,
|
|
1719
|
-
status: build.status,
|
|
1720
|
-
is_production: build.is_production
|
|
1721
|
-
}));
|
|
1722
|
-
if (!c2.agent && !c2.formatExplicit) {
|
|
1723
|
-
console.log("");
|
|
1724
|
-
for (const row of rows) {
|
|
1725
|
-
const marker = row.is_production ? chalk.green("\u25CF production") : chalk.dim(`\u25CB ${row.status}`);
|
|
1726
|
-
console.log(` ${row.id} ${marker}`);
|
|
1727
|
-
}
|
|
1728
|
-
console.log("");
|
|
1729
|
-
}
|
|
1730
|
-
return c2.ok({ builds: rows });
|
|
1731
|
-
} catch (err) {
|
|
1732
|
-
return c2.error({
|
|
1733
|
-
code: "LIST_FAILED",
|
|
1734
|
-
message: err instanceof Error ? err.message : "Failed to list builds",
|
|
1735
|
-
retryable: true
|
|
1736
|
-
});
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
});
|
|
1740
1724
|
return app;
|
|
1741
1725
|
}
|
|
1742
1726
|
|
|
@@ -2356,7 +2340,7 @@ ${c.success("\u2713 Your store is fully set up.")}`);
|
|
|
2356
2340
|
// package.json
|
|
2357
2341
|
var package_default = {
|
|
2358
2342
|
name: "@whop/cli",
|
|
2359
|
-
version: "0.
|
|
2343
|
+
version: "0.5.0",
|
|
2360
2344
|
description: "The Whop CLI \u2014 build and manage Whop apps from your terminal. Human and agent friendly.",
|
|
2361
2345
|
keywords: [
|
|
2362
2346
|
"agent",
|
|
@@ -2504,7 +2488,7 @@ var DEFAULT_MANIFEST_URL = "https://github.com/whopio/whop-public-cli/releases/l
|
|
|
2504
2488
|
function manifestUrl(env = process.env) {
|
|
2505
2489
|
return env.WHOP_CLI_MANIFEST_URL?.trim() || DEFAULT_MANIFEST_URL;
|
|
2506
2490
|
}
|
|
2507
|
-
var CHECK_INTERVAL_MS =
|
|
2491
|
+
var CHECK_INTERVAL_MS = 60 * 60 * 1e3;
|
|
2508
2492
|
function cachePath(env) {
|
|
2509
2493
|
return path.join(configDir(env), "update-check.json");
|
|
2510
2494
|
}
|
|
@@ -2874,7 +2858,7 @@ var HANDWRITTEN_GROUPS = [
|
|
|
2874
2858
|
name: "apps",
|
|
2875
2859
|
description: "Build and deploy fully-hosted web apps (*.whop.app)",
|
|
2876
2860
|
section: "get-started",
|
|
2877
|
-
register: (cli2) => cli2.command(buildAppGroup())
|
|
2861
|
+
register: async (cli2) => cli2.command(await buildAppGroup())
|
|
2878
2862
|
},
|
|
2879
2863
|
{
|
|
2880
2864
|
name: "upgrade",
|
|
@@ -2931,8 +2915,8 @@ var HANDWRITTEN_GROUP_NAMES = new Set(
|
|
|
2931
2915
|
);
|
|
2932
2916
|
var API_GROUPS = groups_default.map(([name, tag]) => ({ name, tag })).filter(({ name }) => !HANDWRITTEN_GROUP_NAMES.has(name));
|
|
2933
2917
|
var COMMAND_GROUPS = [...API_GROUPS, ...HANDWRITTEN_GROUPS];
|
|
2934
|
-
function registerHandwrittenGroups(cli2) {
|
|
2935
|
-
for (const group of HANDWRITTEN_GROUPS) group.register(cli2);
|
|
2918
|
+
async function registerHandwrittenGroups(cli2) {
|
|
2919
|
+
for (const group of HANDWRITTEN_GROUPS) await group.register(cli2);
|
|
2936
2920
|
}
|
|
2937
2921
|
async function setupAgents(cli2) {
|
|
2938
2922
|
for (const argv2 of [
|
|
@@ -3073,7 +3057,7 @@ cli.use(async (c2, next) => {
|
|
|
3073
3057
|
}
|
|
3074
3058
|
return next();
|
|
3075
3059
|
});
|
|
3076
|
-
registerHandwrittenGroups(cli);
|
|
3060
|
+
await registerHandwrittenGroups(cli);
|
|
3077
3061
|
for (const { name, tag } of API_GROUPS) {
|
|
3078
3062
|
cli.command(name, { fetch: fetch2, openapi: spec(tag) });
|
|
3079
3063
|
}
|
|
@@ -3128,6 +3112,12 @@ var api_structure_default = [
|
|
|
3128
3112
|
"Audiences"
|
|
3129
3113
|
]
|
|
3130
3114
|
},
|
|
3115
|
+
{
|
|
3116
|
+
group: "Media",
|
|
3117
|
+
tags: [
|
|
3118
|
+
"Media"
|
|
3119
|
+
]
|
|
3120
|
+
},
|
|
3131
3121
|
{
|
|
3132
3122
|
group: "Identity",
|
|
3133
3123
|
tags: [
|