@whop/cli 0.3.0 → 0.4.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
@@ -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-4WNH276N.js";
38
+ } from "./chunk-2K4WBZA5.js";
37
39
  import {
38
40
  external_exports
39
41
  } from "./chunk-KFCNNWPI.js";
@@ -312,7 +314,7 @@ async function loginAdapter(c2) {
312
314
  const accountId = getActiveProfile()?.accountId ?? "";
313
315
  if (accountId) {
314
316
  try {
315
- const { createWhopFetch: createWhopFetch2 } = await import("./api-A6NYSDKW.js");
317
+ const { createWhopFetch: createWhopFetch2 } = await import("./api-7L74UEG4.js");
316
318
  const fetch3 = createWhopFetch2();
317
319
  const res = await fetch3(
318
320
  new Request(
@@ -592,11 +594,11 @@ function buildAuthGroup() {
592
594
  }
593
595
 
594
596
  // src/apps/commands.ts
595
- import { isCancel, select, text } from "@clack/prompts";
597
+ import { isCancel, select, spinner, text } from "@clack/prompts";
596
598
  import { spawnSync as spawnSync2 } from "child_process";
597
599
  import { createHash } from "crypto";
598
600
  import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
599
- import { join as join3, resolve as resolve2 } from "path";
601
+ import { join as join3, relative, resolve as resolve2 } from "path";
600
602
  import chalk from "chalk";
601
603
 
602
604
  // src/journey/client.ts
@@ -653,13 +655,6 @@ async function createAppBuild(input) {
653
655
  async function getAppBuild(buildId) {
654
656
  return makeWhopRequest("GET", `/app_builds/${buildId}`);
655
657
  }
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
658
  async function promoteAppBuild(buildId) {
664
659
  return makeWhopRequest(
665
660
  "POST",
@@ -767,7 +762,11 @@ var DLX_RUNNERS = {
767
762
  bun: ["bunx"],
768
763
  yarn: ["yarn", "dlx"]
769
764
  };
770
- var PM_PRIORITY = ["bun", "pnpm", "npm"];
765
+ var PM_PRIORITY = [
766
+ "bun",
767
+ "pnpm",
768
+ "npm"
769
+ ];
771
770
  function isPmInstalled(pm) {
772
771
  const result = spawnSync(pm, ["--version"], {
773
772
  stdio: "ignore",
@@ -791,6 +790,34 @@ function pmInstallHint(pm) {
791
790
  return "npm install -g yarn";
792
791
  }
793
792
  }
793
+ var TANSTACK_CREATE_FLAGS = [
794
+ "--framework",
795
+ "React",
796
+ "--deployment",
797
+ "cloudflare",
798
+ "--no-git",
799
+ "--no-install",
800
+ "--no-intent",
801
+ "--no-toolchain",
802
+ "--no-examples",
803
+ "--non-interactive"
804
+ ];
805
+ function tanstackCreateCommand(pm, projectName) {
806
+ return [
807
+ ...DLX_RUNNERS[pm],
808
+ "@tanstack/cli@latest",
809
+ "create",
810
+ projectName,
811
+ ...TANSTACK_CREATE_FLAGS
812
+ ].join(" ");
813
+ }
814
+ function outputTail(result) {
815
+ const combined = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
816
+ if (!combined) return "";
817
+ return `
818
+
819
+ ${combined.split("\n").slice(-15).join("\n")}`;
820
+ }
794
821
  function runTanstackCreate(options) {
795
822
  const [runner, ...runnerArgs] = DLX_RUNNERS[options.pm];
796
823
  const result = spawnSync(
@@ -802,26 +829,18 @@ function runTanstackCreate(options) {
802
829
  options.projectName,
803
830
  "--target-dir",
804
831
  options.targetDir,
805
- "--framework",
806
- "React",
807
- "--deployment",
808
- "cloudflare",
809
- "--no-git",
810
- "--no-install",
811
- "--no-intent",
812
- "--no-toolchain",
813
- "--no-examples",
814
- "--non-interactive"
832
+ ...TANSTACK_CREATE_FLAGS
815
833
  ],
816
834
  {
817
- stdio: options.quiet ? "ignore" : "inherit",
835
+ stdio: ["ignore", "pipe", "pipe"],
836
+ encoding: "utf-8",
818
837
  shell: SPAWN_VIA_SHELL
819
838
  }
820
839
  );
821
840
  if (result.error) throw result.error;
822
841
  if (result.status !== 0) {
823
842
  throw new Error(
824
- `\`${options.pm === "npm" ? "npx" : options.pm} @tanstack/cli create\` failed (exit ${result.status}). Check your network connection and try again.`
843
+ `\`${options.pm === "npm" ? "npx" : options.pm} @tanstack/cli create\` failed (exit ${result.status}). Check your network connection and try again.${outputTail(result)}`
825
844
  );
826
845
  }
827
846
  }
@@ -831,6 +850,11 @@ var VITE_CONFIG_NAMES = [
831
850
  "vite.config.js",
832
851
  "vite.config.mjs"
833
852
  ];
853
+ function findViteConfig(dir) {
854
+ return VITE_CONFIG_NAMES.map((name) => join2(dir, name)).find(
855
+ (path3) => existsSync2(path3)
856
+ );
857
+ }
834
858
  var WRANGLER_CONFIG_NAMES = ["wrangler.jsonc", "wrangler.json"];
835
859
  function cliDependencyVersion() {
836
860
  return buildVersion ? `^${buildVersion}` : "latest";
@@ -867,9 +891,7 @@ function patchPackageJson(dir, manualSteps) {
867
891
  `);
868
892
  }
869
893
  function patchViteConfig(dir, manualSteps) {
870
- const file = VITE_CONFIG_NAMES.map((name) => join2(dir, name)).find(
871
- (path3) => existsSync2(path3)
872
- );
894
+ const file = findViteConfig(dir);
873
895
  if (!file) {
874
896
  manualSteps.push(
875
897
  "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 +984,25 @@ function replaceTopLevelName(content, route) {
962
984
 
963
985
  // src/apps/commands.ts
964
986
  var BUILD_ARCHIVE = "dist/whop-build.zip";
965
- var AppSchema = external_exports.object({
966
- id: external_exports.string(),
967
- name: external_exports.string(),
968
- route: external_exports.string().nullable().optional(),
969
- url: external_exports.string().optional(),
970
- directory: external_exports.string().optional(),
971
- install_failed: external_exports.boolean().optional()
972
- });
973
987
  var BuildSchema = external_exports.object({
974
988
  id: external_exports.string(),
975
989
  status: external_exports.string(),
976
990
  is_production: external_exports.boolean().optional(),
977
991
  url: external_exports.string().optional()
978
992
  });
979
- function detectPackageManager2(projectDir) {
993
+ var DeployAbort = class extends Error {
994
+ constructor(options) {
995
+ super(options.message);
996
+ this.options = options;
997
+ }
998
+ };
999
+ function detectProjectPackageManager(projectDir) {
980
1000
  if (existsSync3(join3(projectDir, "bun.lock")) || existsSync3(join3(projectDir, "bun.lockb")))
981
1001
  return "bun";
982
1002
  if (existsSync3(join3(projectDir, "pnpm-lock.yaml"))) return "pnpm";
983
1003
  if (existsSync3(join3(projectDir, "yarn.lock"))) return "yarn";
984
- return "npm";
1004
+ if (existsSync3(join3(projectDir, "package-lock.json"))) return "npm";
1005
+ return detectPackageManager() ?? "npm";
985
1006
  }
986
1007
  function hasScript(projectDir, script) {
987
1008
  try {
@@ -995,7 +1016,7 @@ function hasScript(projectDir, script) {
995
1016
  }
996
1017
  var SPAWN_VIA_SHELL2 = process.platform === "win32";
997
1018
  function runScript(projectDir, script, extraEnv = {}) {
998
- const pm = detectPackageManager2(projectDir);
1019
+ const pm = detectProjectPackageManager(projectDir);
999
1020
  const result = spawnSync2(pm, ["run", script], {
1000
1021
  cwd: projectDir,
1001
1022
  stdio: "inherit",
@@ -1013,501 +1034,341 @@ function runScript(projectDir, script, extraEnv = {}) {
1013
1034
  );
1014
1035
  }
1015
1036
  }
1037
+ function runInstall(c2, projectDir) {
1038
+ const pm = detectProjectPackageManager(projectDir);
1039
+ const spin = c2.agent ? null : spinner();
1040
+ spin?.start(`Installing dependencies with ${pm}`);
1041
+ const result = spawnSync2(pm, ["install"], {
1042
+ cwd: projectDir,
1043
+ stdio: ["ignore", "pipe", "pipe"],
1044
+ encoding: "utf-8",
1045
+ shell: SPAWN_VIA_SHELL2
1046
+ });
1047
+ if (result.error || result.status !== 0) {
1048
+ spin?.error(`${pm} install failed`);
1049
+ throw new Error(
1050
+ `\`${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)}`
1051
+ );
1052
+ }
1053
+ spin?.stop(`Installed dependencies with ${pm}`);
1054
+ }
1016
1055
  function log(c2, message) {
1017
1056
  if (!c2.agent) console.log(message);
1018
1057
  }
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
1058
  async function promptText(message, placeholder, initialValue) {
1051
1059
  const value = await text({ message, placeholder, initialValue });
1052
1060
  if (isCancel(value)) return null;
1053
1061
  return String(value);
1054
1062
  }
1063
+ function cancelled() {
1064
+ throw new DeployAbort({
1065
+ code: "CANCELLED",
1066
+ message: "Cancelled.",
1067
+ exitCode: 130
1068
+ });
1069
+ }
1055
1070
  function formatManualSteps(steps) {
1056
1071
  return steps.map((step, i) => ` ${i + 1}. ${step}`).join("\n");
1057
1072
  }
1058
- async function runRegisterFlow(c2, options) {
1059
- const projectDir = resolve2(options.dir ?? ".");
1060
- if (!existsSync3(join3(projectDir, "package.json"))) {
1061
- return c2.error({
1062
- code: "NO_PACKAGE_JSON",
1063
- message: `No package.json in ${projectDir} \u2014 --existing wires up a project that's already here. Drop --existing to scaffold a new one.`
1073
+ function formatAppList(apps) {
1074
+ const recent = [...apps].sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0)).slice(0, 10);
1075
+ return recent.map(
1076
+ (a) => ` ${a.id} ${a.name}${a.route ? ` (${a.route}.whop.app)` : ""}`
1077
+ ).join("\n");
1078
+ }
1079
+ async function registerSpecCommands(group, tag) {
1080
+ const spec2 = filterSpecByTag(apps_spec_default, tag);
1081
+ const generated = await Openapi_exports.generateCommands(
1082
+ spec2,
1083
+ createWhopFetch()
1084
+ );
1085
+ for (const [name, entry] of generated) {
1086
+ if ("run" in entry) group.command(name, entry);
1087
+ }
1088
+ }
1089
+ function ensurePackageManager() {
1090
+ const pm = detectPackageManager();
1091
+ if (pm) return pm;
1092
+ throw new DeployAbort({
1093
+ code: "NO_PACKAGE_MANAGER",
1094
+ message: `No package manager found (looked for bun, pnpm, npm). Install one and re-run \`whop apps deploy\`:
1095
+
1096
+ bun (recommended): ${pmInstallHint("bun")}
1097
+ pnpm: ${pmInstallHint("pnpm")}
1098
+ npm: ${pmInstallHint("npm")}`
1099
+ });
1100
+ }
1101
+ async function ensureViteApp(c2, pm, dirOption) {
1102
+ const projectDir = dirOption ? resolve2(dirOption) : findProjectDir() ?? process.cwd();
1103
+ const isViteApp = existsSync3(join3(projectDir, "package.json")) && findViteConfig(projectDir) !== void 0;
1104
+ if (isViteApp) return { projectDir, freshlyScaffolded: false };
1105
+ if (c2.agent) {
1106
+ throw new DeployAbort({
1107
+ code: "NOT_A_VITE_APP",
1108
+ message: [
1109
+ `${projectDir} doesn't look like a Vite app (missing package.json or vite config).`,
1110
+ "",
1111
+ "Ask the user whether to scaffold a new app here, or locate their existing Vite app.",
1112
+ "",
1113
+ "To scaffold a new app (TanStack Start), run:",
1114
+ ` ${tanstackCreateCommand(pm, "<app-name>")}`,
1115
+ ` cd <app-name> && ${pm} install`,
1116
+ "",
1117
+ "Then re-run `whop apps deploy` from inside the app directory.",
1118
+ "Or, if a Vite app already exists somewhere else, re-run `whop apps deploy --dir <path-to-app>`."
1119
+ ].join("\n"),
1120
+ retryable: true,
1121
+ cta: {
1122
+ commands: [
1123
+ {
1124
+ command: "apps deploy",
1125
+ description: "Re-run from inside a Vite app directory"
1126
+ }
1127
+ ]
1128
+ }
1064
1129
  });
1065
1130
  }
1066
- const existing = readAppConfig(projectDir);
1067
- if (existing) {
1068
- const { manualSteps: manualSteps2 } = whopifyProject({
1069
- targetDir: projectDir,
1070
- route: existing.route
1131
+ const choice = await select({
1132
+ message: `${projectDir} doesn't look like a Vite app. What do you want to do?`,
1133
+ options: [
1134
+ {
1135
+ value: "scaffold",
1136
+ label: "Scaffold a new Whop app here (TanStack Start)"
1137
+ },
1138
+ { value: "cancel", label: "Cancel \u2014 I'll cd into my app directory" }
1139
+ ]
1140
+ });
1141
+ if (isCancel(choice) || choice === "cancel") cancelled();
1142
+ const scaffolded = await scaffoldNewApp(c2, pm);
1143
+ return { projectDir: scaffolded, freshlyScaffolded: true };
1144
+ }
1145
+ async function scaffoldNewApp(c2, pm) {
1146
+ const name = await promptText("What is your app called?", "My Site");
1147
+ if (name === null) cancelled();
1148
+ const slug = slugify(name);
1149
+ const routeInput = await promptText(
1150
+ "Which route should it live at? (your-route.whop.app)",
1151
+ slug,
1152
+ slug
1153
+ );
1154
+ if (routeInput === null) cancelled();
1155
+ const route = slugify(routeInput);
1156
+ if (!route) {
1157
+ throw new DeployAbort({
1158
+ code: "ROUTE_REQUIRED",
1159
+ message: `Couldn't derive a route from "${routeInput}" \u2014 use letters and numbers (e.g. my-site).`,
1160
+ retryable: true
1071
1161
  });
1072
- if (manualSteps2.length > 0) {
1073
- return c2.error({
1074
- code: "WHOPIFY_INCOMPLETE",
1075
- message: `This project is already linked to ${existing.app_id} (${existing.route}), but it isn't fully wired for Whop hosting yet. Complete these steps:
1076
-
1077
- ${formatManualSteps(manualSteps2)}
1162
+ }
1163
+ const targetDir = resolve2(`./${route}`);
1164
+ if (existsSync3(targetDir) && readdirSync(targetDir).length > 0) {
1165
+ throw new DeployAbort({
1166
+ code: "TARGET_NOT_EMPTY",
1167
+ message: `Target directory is not empty: ${targetDir}. Pick a different route, or run \`whop apps deploy\` from an empty directory.`
1168
+ });
1169
+ }
1170
+ let app;
1171
+ try {
1172
+ app = await createApp({ name, route });
1173
+ } catch (err) {
1174
+ throw new DeployAbort({
1175
+ code: "CREATE_FAILED",
1176
+ message: err instanceof Error ? err.message : "Failed to register the app",
1177
+ retryable: true
1178
+ });
1179
+ }
1180
+ const spin = c2.agent ? null : spinner();
1181
+ spin?.start("Scaffolding the latest TanStack Start template");
1182
+ try {
1183
+ runTanstackCreate({ targetDir, projectName: route, pm });
1184
+ spin?.stop(`Scaffolded ${route} from the latest TanStack Start template`);
1185
+ } catch (err) {
1186
+ spin?.error("Scaffold failed");
1187
+ throw new DeployAbort({
1188
+ code: "SCAFFOLD_FAILED",
1189
+ message: `${err instanceof Error ? err.message : "Failed to scaffold the project"}
1078
1190
 
1079
- Then run \`whop apps deploy\`. Re-running \`whop apps create --existing\` re-checks the wiring.`
1080
- });
1081
- }
1082
- if (!c2.agent && !c2.formatExplicit) {
1083
- console.log(
1084
- `${chalk.green.bold(`\u2713 Already linked to ${existing.name}`)} ${chalk.dim(`(${existing.app_id})`)} \u2014 wiring verified. Delete ${APP_CONFIG_FILENAME} to relink.`
1085
- );
1086
- }
1087
- return c2.ok({
1088
- id: existing.app_id,
1089
- name: existing.name,
1090
- route: existing.route,
1091
- directory: projectDir
1191
+ The app ${app.id} is already registered \u2014 scaffold into a fresh directory and re-run \`whop apps deploy --app ${app.id}\` there.`
1092
1192
  });
1093
1193
  }
1094
- let linked = null;
1095
- if (options.link) {
1096
- const apps = await listApps();
1097
- linked = apps.find((a) => a.id === options.link) ?? null;
1098
- if (!linked) {
1099
- return c2.error({
1194
+ writeAppConfig(targetDir, { app_id: app.id, name, route });
1195
+ return targetDir;
1196
+ }
1197
+ async function ensureAppLinked(c2, projectDir, appOption) {
1198
+ if (appOption) {
1199
+ let app;
1200
+ try {
1201
+ app = await getApp(appOption);
1202
+ } catch (err) {
1203
+ throw new DeployAbort({
1100
1204
  code: "APP_NOT_FOUND",
1101
- message: `No app ${options.link} found on your account.`
1205
+ message: `Could not load app ${appOption}: ${err instanceof Error ? err.message : "unknown error"}`,
1206
+ retryable: true
1102
1207
  });
1103
1208
  }
1209
+ const config2 = await withRoute(c2, app);
1210
+ writeAppConfig(projectDir, config2);
1211
+ log(
1212
+ c2,
1213
+ chalk.dim(
1214
+ `Linked ${config2.name} (${config2.app_id}) \u2192 ${APP_CONFIG_FILENAME}`
1215
+ )
1216
+ );
1217
+ return config2;
1104
1218
  }
1105
- let name = options.name ?? linked?.name;
1106
- if (!name) {
1107
- if (c2.agent) {
1108
- return c2.error({
1109
- code: "NAME_REQUIRED",
1110
- message: 'Pass a name: `whop apps create "My Site"`.',
1111
- retryable: true
1112
- });
1219
+ const existing = readAppConfig(projectDir);
1220
+ if (existing) return existing;
1221
+ const apps = await listApps();
1222
+ if (c2.agent) {
1223
+ const reuse = apps.length > 0 ? `This account has existing apps \u2014 ask the user whether to reuse one:
1224
+ ${formatAppList(apps)}
1225
+
1226
+ To reuse one: whop apps deploy --app <app_id>
1227
+
1228
+ ` : "This account has no apps yet, so a new one must be created.\n\n";
1229
+ throw new DeployAbort({
1230
+ code: "NO_APP_LINKED",
1231
+ message: [
1232
+ `No ${APP_CONFIG_FILENAME} here \u2014 this project isn't linked to a Whop app yet.`,
1233
+ "",
1234
+ `${reuse}To create a new app, ask the user for a name and route (the route becomes <route>.whop.app), then run:`,
1235
+ ' whop apps create --name "My Site" --route my-site',
1236
+ "",
1237
+ "Then deploy with the returned app id:",
1238
+ " whop apps deploy --app <app_id>"
1239
+ ].join("\n"),
1240
+ retryable: true,
1241
+ cta: {
1242
+ commands: [
1243
+ {
1244
+ command: "apps create",
1245
+ description: "Create a new app (pass --name and --route)"
1246
+ },
1247
+ {
1248
+ command: "apps deploy",
1249
+ description: "Re-run with --app <app_id> to link and deploy"
1250
+ }
1251
+ ]
1252
+ }
1253
+ });
1254
+ }
1255
+ let selected = null;
1256
+ if (apps.length > 0) {
1257
+ const recent = [...apps].sort(
1258
+ (a, b) => (b.created_at ?? 0) - (a.created_at ?? 0)
1259
+ );
1260
+ const choice = await select({
1261
+ message: "Which app should this project deploy to?",
1262
+ options: [
1263
+ ...recent.map((a) => ({
1264
+ value: a.id,
1265
+ label: `${a.name} (${a.id}${a.route ? `, ${a.route}.whop.app` : ""})`
1266
+ })),
1267
+ { value: "__new__", label: "Create a new app" }
1268
+ ]
1269
+ });
1270
+ if (isCancel(choice)) cancelled();
1271
+ if (choice !== "__new__") {
1272
+ selected = recent.find((a) => a.id === choice) ?? null;
1113
1273
  }
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
1274
  }
1123
- if (!linked) {
1124
- const apps = await listApps();
1275
+ if (!selected) {
1276
+ const name = await promptText("What is your app called?", "My Site");
1277
+ if (name === null) cancelled();
1125
1278
  const slug = slugify(name);
1126
- const similar = apps.filter(
1127
- (a) => slugify(a.name) === slug || a.route === (options.route ?? slug)
1279
+ const routeInput = await promptText(
1280
+ "Which route should it live at? (your-route.whop.app)",
1281
+ slug,
1282
+ slug
1128
1283
  );
1129
- if (similar.length > 0 && !c2.agent) {
1130
- const choice = await select({
1131
- message: `Found existing app${similar.length > 1 ? "s" : ""} with a similar name \u2014 link one or create new?`,
1132
- options: [
1133
- ...similar.map((a) => ({
1134
- value: a.id,
1135
- label: `Link ${a.name} (${a.id}${a.route ? `, ${a.route}` : ""})`
1136
- })),
1137
- { value: "__new__", label: `Create a new app called "${name}"` }
1138
- ]
1284
+ if (routeInput === null) cancelled();
1285
+ const route = slugify(routeInput);
1286
+ if (!route) {
1287
+ throw new DeployAbort({
1288
+ code: "ROUTE_REQUIRED",
1289
+ message: `Couldn't derive a route from "${routeInput}" \u2014 use letters and numbers (e.g. my-site).`,
1290
+ retryable: true
1139
1291
  });
1140
- if (isCancel(choice))
1141
- return c2.error({
1142
- code: "CANCELLED",
1143
- message: "Cancelled.",
1144
- exitCode: 130
1145
- });
1146
- if (choice !== "__new__") {
1147
- linked = similar.find((a) => a.id === choice) ?? null;
1148
- }
1149
- } else if (similar.length > 0 && c2.agent) {
1150
- return c2.error({
1151
- code: "SIMILAR_APPS_FOUND",
1152
- message: `Existing app(s) with a similar name: ${similar.map((a) => `${a.name} (${a.id})`).join(
1153
- ", "
1154
- )}. Pass --link <app_id> to link one, or --name with a distinct name to create new.`,
1292
+ }
1293
+ try {
1294
+ selected = await createApp({ name, route });
1295
+ } catch (err) {
1296
+ throw new DeployAbort({
1297
+ code: "CREATE_FAILED",
1298
+ message: err instanceof Error ? err.message : "Failed to register the app",
1155
1299
  retryable: true
1156
1300
  });
1157
1301
  }
1158
1302
  }
1159
- let route = options.route ?? slugify(name);
1160
- if (!options.route && !linked && !c2.agent) {
1161
- const value = await promptText(
1162
- "Which route should it live at? (your-route.whop.app)",
1163
- slugify(name),
1164
- slugify(name)
1165
- );
1166
- if (value === null)
1167
- return c2.error({
1168
- code: "CANCELLED",
1169
- message: "Cancelled.",
1170
- exitCode: 130
1171
- });
1172
- route = slugify(value);
1303
+ const config = await withRoute(c2, selected);
1304
+ writeAppConfig(projectDir, config);
1305
+ log(
1306
+ c2,
1307
+ chalk.dim(
1308
+ `Linked ${config.name} (${config.app_id}) \u2192 ${APP_CONFIG_FILENAME}`
1309
+ )
1310
+ );
1311
+ return config;
1312
+ }
1313
+ async function withRoute(c2, app) {
1314
+ if (app.route) return { app_id: app.id, name: app.name, route: app.route };
1315
+ if (c2.agent) {
1316
+ throw new DeployAbort({
1317
+ code: "ROUTE_REQUIRED",
1318
+ 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:
1319
+ whop apps update ${app.id} --route <route>
1320
+ whop apps deploy --app ${app.id}`,
1321
+ retryable: true,
1322
+ cta: {
1323
+ commands: [
1324
+ {
1325
+ command: `apps update ${app.id}`,
1326
+ description: "Set the route (pass --route)"
1327
+ }
1328
+ ]
1329
+ }
1330
+ });
1173
1331
  }
1174
- if (!route && !linked?.route) {
1175
- return c2.error({
1332
+ const slug = slugify(app.name);
1333
+ const routeInput = await promptText(
1334
+ `${app.name} has no route yet. Which route should it live at? (your-route.whop.app)`,
1335
+ slug,
1336
+ slug
1337
+ );
1338
+ if (routeInput === null) cancelled();
1339
+ const route = slugify(routeInput);
1340
+ if (!route) {
1341
+ throw new DeployAbort({
1176
1342
  code: "ROUTE_REQUIRED",
1177
- message: `Couldn't derive a route from "${name}" \u2014 pass --route with letters and numbers (e.g. --route my-site).`,
1343
+ message: `Couldn't derive a route from "${routeInput}" \u2014 use letters and numbers (e.g. my-site).`,
1178
1344
  retryable: true
1179
1345
  });
1180
1346
  }
1181
- let whopApp;
1182
1347
  try {
1183
- if (linked) {
1184
- whopApp = linked;
1185
- route = options.route ?? linked.route ?? slugify(name);
1186
- if (!linked.route || options.route && options.route !== linked.route) {
1187
- whopApp = await updateAppRoute(linked.id, route);
1188
- }
1189
- } else {
1190
- whopApp = await createApp({
1191
- name,
1192
- route,
1193
- ...options.companyId ? { company_id: options.companyId } : {}
1194
- });
1195
- }
1348
+ const updated = await updateAppRoute(app.id, route);
1349
+ return {
1350
+ app_id: updated.id,
1351
+ name: updated.name,
1352
+ route: updated.route ?? route
1353
+ };
1196
1354
  } catch (err) {
1197
- return c2.error({
1198
- code: "CREATE_FAILED",
1199
- message: err instanceof Error ? err.message : "Failed to register the app",
1355
+ throw new DeployAbort({
1356
+ code: "UPDATE_FAILED",
1357
+ message: err instanceof Error ? err.message : "Failed to set the route",
1200
1358
  retryable: true
1201
1359
  });
1202
1360
  }
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
1361
  }
1230
- function buildAppGroup() {
1362
+ async function buildAppGroup() {
1231
1363
  const app = Cli_exports.create("apps", {
1232
1364
  description: "Build and deploy fully-hosted web apps on Whop (*.whop.app)"
1233
1365
  });
1234
- app.command("create", {
1235
- description: "Create a Whop app: scaffold a new project (default) or wire up an existing one",
1236
- hint: "Registers the app on Whop, writes whop.app.json, and wires the whop() deploy plugin into the vite config. By default it scaffolds the latest TanStack Start template into ./<route>. Pass --existing to register + whopify the Vite project in the current directory (or --dir) instead \u2014 safe to re-run, already-linked projects just get their wiring re-checked. --link reuses an existing app record in either mode.",
1237
- args: external_exports.object({
1238
- name: external_exports.string().optional().describe("App name (e.g. 'My Site'; when linking, defaults to the linked app's name)")
1239
- }),
1240
- options: external_exports.object({
1241
- route: external_exports.string().optional().describe("Subdomain route to claim (e.g. my-site \u2192 my-site.whop.app)"),
1242
- dir: external_exports.string().optional().describe(
1243
- "Directory to scaffold into (defaults to ./<route>); with --existing, the project directory (defaults to the current directory)"
1244
- ),
1245
- existing: external_exports.boolean().optional().describe(
1246
- "Wire up the existing Vite project here instead of scaffolding a new one"
1247
- ),
1248
- link: external_exports.string().optional().describe(
1249
- "Reuse an existing app by id (app_xxx) instead of creating a new one \u2014 works when scaffolding and with --existing"
1250
- ),
1251
- company_id: external_exports.string().optional().describe(
1252
- "Business account to create the app under (defaults to your active account)"
1253
- ),
1254
- install: external_exports.boolean().optional().describe(
1255
- "Install dependencies after scaffolding fresh (default: true)"
1256
- ),
1257
- pm: external_exports.enum(["npm", "pnpm", "bun", "yarn"]).optional().describe(
1258
- "Package manager to use (default: first of bun, pnpm, npm found on your PATH)"
1259
- )
1260
- }),
1261
- output: AppSchema,
1262
- outputPolicy: "agent-only",
1263
- examples: [
1264
- {
1265
- args: { name: "My Site" },
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
- }
1274
- ],
1275
- run: async (c2) => {
1276
- const explicitDir = c2.options.dir ? resolve2(c2.options.dir) : void 0;
1277
- if (c2.options.existing) {
1278
- const linkedDir = explicitDir ?? findProjectDir();
1279
- const projectDir = linkedDir && readAppConfig(linkedDir) ? linkedDir : explicitDir ?? process.cwd();
1280
- return runRegisterFlow(c2, {
1281
- dir: projectDir,
1282
- name: c2.args.name,
1283
- route: c2.options.route,
1284
- link: c2.options.link,
1285
- companyId: c2.options.company_id
1286
- });
1287
- }
1288
- if (!explicitDir && existsSync3(join3(process.cwd(), "package.json"))) {
1289
- log(
1290
- c2,
1291
- chalk.dim(
1292
- "Detected an existing project here \u2014 scaffolding a new app in a subdirectory. Pass --existing to wire up this project instead."
1293
- )
1294
- );
1295
- }
1296
- let linked = null;
1297
- if (c2.options.link) {
1298
- const apps = await listApps();
1299
- linked = apps.find((a) => a.id === c2.options.link) ?? null;
1300
- if (!linked) {
1301
- return c2.error({
1302
- code: "APP_NOT_FOUND",
1303
- message: `No app ${c2.options.link} found on your account.`
1304
- });
1305
- }
1306
- }
1307
- let name = c2.args.name ?? linked?.name;
1308
- if (!name) {
1309
- if (c2.agent) {
1310
- return c2.error({
1311
- code: "NAME_REQUIRED",
1312
- message: 'Pass a name: `whop apps create "My Site"`.',
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 } : {}
1405
- });
1406
- }
1407
- } catch (err) {
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
- });
1443
- }
1444
- let installFailed = false;
1445
- if (c2.options.install !== false) {
1446
- log(c2, chalk.dim(`
1447
- Installing dependencies with ${pm}...`));
1448
- const result = spawnSync2(pm, ["install"], {
1449
- cwd: targetDir,
1450
- stdio: c2.agent ? "ignore" : "inherit",
1451
- shell: SPAWN_VIA_SHELL2
1452
- });
1453
- installFailed = Boolean(result.error) || result.status !== 0;
1454
- if (installFailed) {
1455
- log(
1456
- c2,
1457
- chalk.yellow(
1458
- `
1459
- ${pm} install failed \u2014 run it manually inside ${targetDir}.`
1460
- )
1461
- );
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
- });
1493
- }
1494
- });
1495
- app.command("list", {
1496
- description: "List the apps on your business account",
1497
- output: external_exports.array(AppSchema),
1498
- outputPolicy: "agent-only",
1499
- run: async (c2) => {
1500
- const apps = await listApps();
1501
- return c2.ok(
1502
- apps.map((a) => ({
1503
- id: a.id,
1504
- name: a.name,
1505
- route: a.route,
1506
- url: a.hosted_url ?? void 0
1507
- }))
1508
- );
1509
- }
1366
+ await registerSpecCommands(app, "Apps");
1367
+ const builds = Cli_exports.create("builds", {
1368
+ description: "Manage app builds (list, inspect, promote)"
1510
1369
  });
1370
+ await registerSpecCommands(builds, "App builds");
1371
+ app.command(builds);
1511
1372
  app.command("dev", {
1512
1373
  description: "Run the local dev server for this app",
1513
1374
  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 +1376,24 @@ ${pm} install failed \u2014 run it manually inside ${targetDir}.`
1515
1376
  dir: external_exports.string().optional().describe("Project directory (defaults to the current directory)")
1516
1377
  }),
1517
1378
  run: async (c2) => {
1518
- const project = loadProject(c2.options.dir);
1519
- if (!project.ok) return c2.error(project.error);
1379
+ const projectDir = c2.options.dir ? resolve2(c2.options.dir) : findProjectDir();
1380
+ const config = projectDir ? readAppConfig(projectDir) : null;
1381
+ if (!projectDir || !config) {
1382
+ return c2.error({
1383
+ code: "NO_PROJECT",
1384
+ message: `No ${APP_CONFIG_FILENAME} found in this directory or any parent. Run \`whop apps deploy\` to link this project to a Whop app.`,
1385
+ cta: {
1386
+ commands: [
1387
+ {
1388
+ command: "apps deploy",
1389
+ description: "Link this project to a Whop app and ship it"
1390
+ }
1391
+ ]
1392
+ }
1393
+ });
1394
+ }
1520
1395
  const injected = {
1521
- WHOP_APP_ID: project.config.app_id
1396
+ WHOP_APP_ID: config.app_id
1522
1397
  };
1523
1398
  if (process.env.WHOP_API_KEY) {
1524
1399
  log(
@@ -1548,9 +1423,9 @@ ${pm} install failed \u2014 run it manually inside ${targetDir}.`
1548
1423
  );
1549
1424
  }
1550
1425
  }
1551
- log(c2, chalk.dim(`Starting dev server for ${project.config.name}...`));
1426
+ log(c2, chalk.dim(`Starting dev server for ${config.name}...`));
1552
1427
  try {
1553
- runScript(project.projectDir, "dev", injected);
1428
+ runScript(projectDir, "dev", injected);
1554
1429
  } catch (err) {
1555
1430
  return c2.error({
1556
1431
  code: "DEV_FAILED",
@@ -1562,11 +1437,14 @@ ${pm} install failed \u2014 run it manually inside ${targetDir}.`
1562
1437
  });
1563
1438
  app.command("deploy", {
1564
1439
  description: "Build, upload and ship this app live",
1565
- hint: "Typechecks, builds with Vite, uploads the build to Whop, and promotes it to production. Use --no-promote to upload a preview-only build.",
1440
+ 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
1441
  options: external_exports.object({
1567
1442
  dir: external_exports.string().optional().describe("Project directory (defaults to the current directory)"),
1568
- promote: external_exports.boolean().optional().describe(
1569
- "Promote this build to production after upload (default: true)"
1443
+ app: external_exports.string().optional().describe(
1444
+ "Link this project to an app (app_xxx) before deploying \u2014 writes whop.app.json, replacing any existing link"
1445
+ ),
1446
+ skip_promote: external_exports.boolean().optional().describe(
1447
+ "Upload the build without promoting it to production"
1570
1448
  ),
1571
1449
  skip_typecheck: external_exports.boolean().optional().describe("Skip the typecheck step"),
1572
1450
  skip_build: external_exports.boolean().optional().describe("Skip the build step and upload the existing dist/ output")
@@ -1576,20 +1454,45 @@ ${pm} install failed \u2014 run it manually inside ${targetDir}.`
1576
1454
  examples: [
1577
1455
  { description: "Build and ship to production" },
1578
1456
  {
1579
- options: { promote: false },
1457
+ options: { app: "app_xxxxxxxx" },
1458
+ description: "Link this project to an existing app, then deploy"
1459
+ },
1460
+ {
1461
+ options: { skip_promote: true },
1580
1462
  description: "Upload a preview build without promoting"
1581
1463
  }
1582
1464
  ],
1583
1465
  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
1466
  try {
1467
+ const pm = ensurePackageManager();
1468
+ const { projectDir, freshlyScaffolded } = await ensureViteApp(
1469
+ c2,
1470
+ pm,
1471
+ c2.options.dir
1472
+ );
1473
+ const config = await ensureAppLinked(c2, projectDir, c2.options.app);
1474
+ const { manualSteps } = whopifyProject({
1475
+ targetDir: projectDir,
1476
+ route: config.route
1477
+ });
1478
+ if (manualSteps.length > 0) {
1479
+ return c2.error({
1480
+ code: "WHOPIFY_INCOMPLETE",
1481
+ message: `${config.name} is linked (${config.app_id}), but the project isn't fully wired for Whop hosting yet. Complete these steps:
1482
+
1483
+ ${formatManualSteps(manualSteps)}
1484
+
1485
+ Then re-run \`whop apps deploy\` \u2014 it re-checks the wiring and applies anything it can.`
1486
+ });
1487
+ }
1588
1488
  if (!c2.options.skip_build) {
1589
- log(c2, chalk.dim("[1/3] Building..."));
1489
+ if (freshlyScaffolded || !existsSync3(join3(projectDir, "node_modules", "@whop", "cli"))) {
1490
+ runInstall(c2, projectDir);
1491
+ }
1492
+ log(c2, chalk.dim("[1/4] Building..."));
1590
1493
  runScript(projectDir, "build", { WHOP_APP_ID: config.app_id });
1591
1494
  if (!c2.options.skip_typecheck && hasScript(projectDir, "typecheck")) {
1592
- log(c2, chalk.dim("[2/3] Typechecking..."));
1495
+ log(c2, chalk.dim("[2/4] Typechecking..."));
1593
1496
  runScript(projectDir, "typecheck");
1594
1497
  }
1595
1498
  }
@@ -1600,26 +1503,50 @@ ${pm} install failed \u2014 run it manually inside ${targetDir}.`
1600
1503
  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
1504
  });
1602
1505
  }
1603
- log(c2, chalk.dim("[3/3] Uploading + creating build..."));
1604
1506
  const zip = readFileSync3(archivePath);
1507
+ const sizeMb = (zip.byteLength / 1024 / 1024).toFixed(1);
1605
1508
  const checksum = createHash("sha256").update(zip).digest("hex");
1606
- const fileId = await uploadBuildArchive(
1607
- new Uint8Array(zip),
1608
- `${config.route}-build.zip`
1609
- );
1610
- await waitForFileReady(fileId);
1611
- let build = await createAppBuild({
1612
- app_id: config.app_id,
1613
- checksum,
1614
- file_id: fileId
1615
- });
1616
- if (c2.options.promote !== false) {
1617
- await promoteAppBuild(build.id);
1618
- build = await waitForBuildPromotion(build.id);
1509
+ const spin = c2.agent ? null : spinner();
1510
+ spin?.start(`[3/4] Uploading build archive (${sizeMb} MB)`);
1511
+ let build;
1512
+ try {
1513
+ const fileId = await uploadBuildArchive(
1514
+ new Uint8Array(zip),
1515
+ `${config.route}-build.zip`
1516
+ );
1517
+ spin?.message("[3/4] Processing archive");
1518
+ await waitForFileReady(fileId);
1519
+ spin?.message("[3/4] Creating build");
1520
+ build = await createAppBuild({
1521
+ app_id: config.app_id,
1522
+ checksum,
1523
+ file_id: fileId
1524
+ });
1525
+ spin?.stop(`[3/4] Build uploaded (${build.id})`);
1526
+ } catch (err) {
1527
+ spin?.error("[3/4] Upload failed");
1528
+ throw err;
1529
+ }
1530
+ if (!c2.options.skip_promote) {
1531
+ const promoteSpin = c2.agent ? null : spinner();
1532
+ promoteSpin?.start("[4/4] Promoting to production");
1533
+ try {
1534
+ await promoteAppBuild(build.id);
1535
+ build = await waitForBuildPromotion(build.id);
1536
+ promoteSpin?.stop(
1537
+ build.is_production ? "[4/4] Promoted to production" : `[4/4] Promotion pending (status: ${build.status})`
1538
+ );
1539
+ } catch (err) {
1540
+ promoteSpin?.error("[4/4] Promote failed");
1541
+ throw err;
1542
+ }
1543
+ } else {
1544
+ log(c2, chalk.dim("[4/4] Skipping promotion (--skip_promote)"));
1619
1545
  }
1620
1546
  const promoted = build.is_production === true;
1621
1547
  const productionUrl = promoted ? (await getApp(config.app_id)).hosted_url ?? void 0 : void 0;
1622
1548
  if (!c2.agent && !c2.formatExplicit) {
1549
+ const cdTarget = relative(process.cwd(), projectDir) || ".";
1623
1550
  console.log(
1624
1551
  [
1625
1552
  "",
@@ -1631,19 +1558,39 @@ ${pm} install failed \u2014 run it manually inside ${targetDir}.`
1631
1558
  ...promoted && productionUrl ? [
1632
1559
  ` ${chalk.bold("Live")} ${chalk.cyan.underline(productionUrl)}`
1633
1560
  ] : [
1634
- `${chalk.bold("Promote when ready")} ${chalk.cyan.bold(`whop apps promote ${build.id}`)}`
1561
+ `${chalk.bold("Promote when ready")} ${chalk.cyan.bold(`whop apps builds promote ${build.id}`)}`
1635
1562
  ],
1563
+ ...freshlyScaffolded ? [
1564
+ "",
1565
+ chalk.bold("Next"),
1566
+ ...cdTarget !== "." ? [` cd ${cdTarget}`] : [],
1567
+ ` ${chalk.cyan.bold("whop apps dev")} ${chalk.dim("\u2192 local dev server")}`,
1568
+ ` ${chalk.cyan.bold("whop apps deploy")} ${chalk.dim("\u2192 ship your changes")}`
1569
+ ] : [],
1636
1570
  ""
1637
1571
  ].join("\n")
1638
1572
  );
1639
1573
  }
1640
- return c2.ok({
1641
- id: build.id,
1642
- status: build.status,
1643
- is_production: promoted,
1644
- url: productionUrl
1645
- });
1574
+ return c2.ok(
1575
+ {
1576
+ id: build.id,
1577
+ status: build.status,
1578
+ is_production: promoted,
1579
+ url: productionUrl
1580
+ },
1581
+ promoted ? void 0 : {
1582
+ cta: {
1583
+ commands: [
1584
+ {
1585
+ command: `apps builds promote ${build.id}`,
1586
+ description: "Promote this build to production"
1587
+ }
1588
+ ]
1589
+ }
1590
+ }
1591
+ );
1646
1592
  } catch (err) {
1593
+ if (err instanceof DeployAbort) return c2.error(err.options);
1647
1594
  return c2.error({
1648
1595
  code: "DEPLOY_FAILED",
1649
1596
  message: err instanceof Error ? err.message : "Deploy failed",
@@ -1652,91 +1599,6 @@ ${pm} install failed \u2014 run it manually inside ${targetDir}.`
1652
1599
  }
1653
1600
  }
1654
1601
  });
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
1602
  return app;
1741
1603
  }
1742
1604
 
@@ -2356,7 +2218,7 @@ ${c.success("\u2713 Your store is fully set up.")}`);
2356
2218
  // package.json
2357
2219
  var package_default = {
2358
2220
  name: "@whop/cli",
2359
- version: "0.3.0",
2221
+ version: "0.4.0",
2360
2222
  description: "The Whop CLI \u2014 build and manage Whop apps from your terminal. Human and agent friendly.",
2361
2223
  keywords: [
2362
2224
  "agent",
@@ -2504,7 +2366,7 @@ var DEFAULT_MANIFEST_URL = "https://github.com/whopio/whop-public-cli/releases/l
2504
2366
  function manifestUrl(env = process.env) {
2505
2367
  return env.WHOP_CLI_MANIFEST_URL?.trim() || DEFAULT_MANIFEST_URL;
2506
2368
  }
2507
- var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
2369
+ var CHECK_INTERVAL_MS = 60 * 60 * 1e3;
2508
2370
  function cachePath(env) {
2509
2371
  return path.join(configDir(env), "update-check.json");
2510
2372
  }
@@ -2874,7 +2736,7 @@ var HANDWRITTEN_GROUPS = [
2874
2736
  name: "apps",
2875
2737
  description: "Build and deploy fully-hosted web apps (*.whop.app)",
2876
2738
  section: "get-started",
2877
- register: (cli2) => cli2.command(buildAppGroup())
2739
+ register: async (cli2) => cli2.command(await buildAppGroup())
2878
2740
  },
2879
2741
  {
2880
2742
  name: "upgrade",
@@ -2931,8 +2793,8 @@ var HANDWRITTEN_GROUP_NAMES = new Set(
2931
2793
  );
2932
2794
  var API_GROUPS = groups_default.map(([name, tag]) => ({ name, tag })).filter(({ name }) => !HANDWRITTEN_GROUP_NAMES.has(name));
2933
2795
  var COMMAND_GROUPS = [...API_GROUPS, ...HANDWRITTEN_GROUPS];
2934
- function registerHandwrittenGroups(cli2) {
2935
- for (const group of HANDWRITTEN_GROUPS) group.register(cli2);
2796
+ async function registerHandwrittenGroups(cli2) {
2797
+ for (const group of HANDWRITTEN_GROUPS) await group.register(cli2);
2936
2798
  }
2937
2799
  async function setupAgents(cli2) {
2938
2800
  for (const argv2 of [
@@ -3073,7 +2935,7 @@ cli.use(async (c2, next) => {
3073
2935
  }
3074
2936
  return next();
3075
2937
  });
3076
- registerHandwrittenGroups(cli);
2938
+ await registerHandwrittenGroups(cli);
3077
2939
  for (const { name, tag } of API_GROUPS) {
3078
2940
  cli.command(name, { fetch: fetch2, openapi: spec(tag) });
3079
2941
  }