@vibecodemax/cli 0.1.7 → 0.1.9

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.
Files changed (2) hide show
  1. package/dist/cli.js +637 -2
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import { spawnSync } from "node:child_process";
4
+ import { spawn, spawnSync } from "node:child_process";
5
+ import { randomBytes } from "node:crypto";
5
6
  import { checkS3Context, setupS3Storage } from "./storageS3.js";
6
7
  const SETUP_STATE_PATH = path.join(".vibecodemax", "setup-state.json");
7
8
  const SETUP_CONFIG_PATH = path.join(".vibecodemax", "setup-config.json");
@@ -23,6 +24,30 @@ const STORAGE_MIME_TYPES_BY_CATEGORY = {
23
24
  video: ["video/mp4", "video/webm", "video/quicktime"],
24
25
  };
25
26
  const STORAGE_DEFAULT_MIME_CATEGORIES = ["images"];
27
+ const STRIPE_EVENTS = [
28
+ "checkout.session.completed",
29
+ "customer.subscription.created",
30
+ "customer.subscription.updated",
31
+ "customer.subscription.deleted",
32
+ "invoice.payment_succeeded",
33
+ "invoice.payment_failed",
34
+ ];
35
+ const CONCURRENTLY_VERSION = "^9.2.1";
36
+ const TRIGGER_DEV_ALL_SCRIPT = 'concurrently -k -n app,jobs -c cyan,yellow "npm run dev" "npm run trigger:dev"';
37
+ const TRIGGER_STRIPE_DEV_ALL_SCRIPT = 'concurrently -k -n app,jobs,payments -c cyan,yellow,magenta "npm run dev" "npm run trigger:dev" "npm run stripe:listen"';
38
+ const STRIPE_ONLY_DEV_ALL_SCRIPT = 'concurrently -k -n app,payments -c cyan,magenta "npm run dev" "npm run stripe:listen"';
39
+ const LEMON_EVENTS = [
40
+ "order_created",
41
+ "subscription_created",
42
+ "subscription_updated",
43
+ "subscription_cancelled",
44
+ "subscription_resumed",
45
+ "subscription_expired",
46
+ "subscription_paused",
47
+ "subscription_unpaused",
48
+ "subscription_payment_failed",
49
+ "subscription_payment_success",
50
+ ];
26
51
  function printJson(value) {
27
52
  process.stdout.write(`${JSON.stringify(value)}\n`);
28
53
  }
@@ -48,7 +73,7 @@ function parseArgs(argv) {
48
73
  let subcommand;
49
74
  const flags = {};
50
75
  let index = 0;
51
- if ((command === "admin" || command === "storage") && rest[0] && !rest[0].startsWith("--")) {
76
+ if ((command === "admin" || command === "storage" || command === "payments") && rest[0] && !rest[0].startsWith("--")) {
52
77
  subcommand = rest[0];
53
78
  index = 1;
54
79
  }
@@ -630,6 +655,13 @@ function getSupabaseRunner(dependencyManager) {
630
655
  return "yarn supabase";
631
656
  return "npx supabase";
632
657
  }
658
+ function getDependencyInstallCommand(dependencyManager, packageName) {
659
+ if (dependencyManager === "pnpm")
660
+ return `pnpm add -D ${packageName}`;
661
+ if (dependencyManager === "yarn")
662
+ return `yarn add -D ${packageName}`;
663
+ return `npm install --save-dev ${packageName}`;
664
+ }
633
665
  function runShellCommand(command, cwd) {
634
666
  const result = spawnSync(process.env.SHELL || "/bin/zsh", ["-lc", command], {
635
667
  cwd,
@@ -658,6 +690,17 @@ function runLinkedSupabaseCommand(command, cwd, failureCode, fallbackMessage) {
658
690
  }
659
691
  return typeof result.stdout === "string" ? result.stdout.trim() : "";
660
692
  }
693
+ function ensureStripeCliInstalled(cwd) {
694
+ const result = spawnSync(process.env.SHELL || "/bin/zsh", ["-lc", "stripe --version"], {
695
+ cwd,
696
+ env: process.env,
697
+ encoding: "utf8",
698
+ });
699
+ if (result.status !== 0) {
700
+ fail("STRIPE_CLI_MISSING", "Stripe CLI is not installed. Install it from https://docs.stripe.com/stripe-cli/install before continuing Stripe localhost setup.");
701
+ }
702
+ return typeof result.stdout === "string" ? result.stdout.trim() : "";
703
+ }
661
704
  function isMigrationOrderingConflict(message) {
662
705
  return /Found local migration files to be inserted before the last migration on remote database\./i.test(message);
663
706
  }
@@ -973,6 +1016,571 @@ async function setupSupabaseStorage(flags) {
973
1016
  envWritten: ["SUPABASE_PUBLIC_BUCKET", "SUPABASE_PRIVATE_BUCKET"],
974
1017
  });
975
1018
  }
1019
+ function normalizePaymentsMode(value) {
1020
+ return value === "production" ? "production" : "test";
1021
+ }
1022
+ function isStripePublishableKey(value) {
1023
+ return isNonEmptyString(value) && /^pk_(test|live)_[A-Za-z0-9_]+$/.test(value.trim());
1024
+ }
1025
+ function isStripeSecretKey(value) {
1026
+ return isNonEmptyString(value) && /^sk_(test|live)_[A-Za-z0-9_]+$/.test(value.trim());
1027
+ }
1028
+ function isStripeWebhookSecret(value) {
1029
+ return isNonEmptyString(value) && /^whsec_[A-Za-z0-9]+$/.test(value.trim());
1030
+ }
1031
+ function isNumericStoreId(value) {
1032
+ return isNonEmptyString(value) && /^\d+$/.test(value.trim());
1033
+ }
1034
+ function requireWebhookUrl(value, expectedPath, provider) {
1035
+ const trimmed = value.trim();
1036
+ if (!trimmed) {
1037
+ fail("INVALID_WEBHOOK_URL", `${provider} webhook URL is required.`);
1038
+ }
1039
+ try {
1040
+ const parsed = new URL(trimmed);
1041
+ const isHttps = parsed.protocol === "https:";
1042
+ const isLocalhost = parsed.protocol === "http:" && parsed.hostname === "localhost";
1043
+ if ((!isHttps && !isLocalhost) || parsed.pathname !== expectedPath) {
1044
+ fail("INVALID_WEBHOOK_URL", `${provider} webhook URL must be an https URL ending in ${expectedPath}, or http://localhost ending in ${expectedPath}.`);
1045
+ }
1046
+ }
1047
+ catch {
1048
+ fail("INVALID_WEBHOOK_URL", `${provider} webhook URL must be a valid https URL ending in ${expectedPath}, or http://localhost ending in ${expectedPath}.`);
1049
+ }
1050
+ return trimmed;
1051
+ }
1052
+ function requirePackageJson(cwd) {
1053
+ const packageJsonPath = path.join(cwd, "package.json");
1054
+ if (!fs.existsSync(packageJsonPath)) {
1055
+ fail("MISSING_PACKAGE_JSON", "package.json is missing in the project root.");
1056
+ }
1057
+ try {
1058
+ const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
1059
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1060
+ fail("INVALID_PACKAGE_JSON", "package.json must contain a JSON object.");
1061
+ }
1062
+ return { packageJsonPath, packageJson: parsed };
1063
+ }
1064
+ catch {
1065
+ fail("INVALID_PACKAGE_JSON", "package.json is not valid JSON.");
1066
+ }
1067
+ }
1068
+ function writePackageJson(packageJsonPath, packageJson) {
1069
+ fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf8");
1070
+ }
1071
+ function buildStripeListenScript(localhostUrl) {
1072
+ return `stripe listen --events ${STRIPE_EVENTS.join(",")} --forward-to ${localhostUrl}/api/webhooks/stripe`;
1073
+ }
1074
+ function ensureStripeLocalScripts(cwd, dependencyManager, localhostUrl) {
1075
+ const { packageJsonPath, packageJson } = requirePackageJson(cwd);
1076
+ const scripts = packageJson.scripts && typeof packageJson.scripts === "object" && !Array.isArray(packageJson.scripts)
1077
+ ? { ...packageJson.scripts }
1078
+ : {};
1079
+ const devDependencies = packageJson.devDependencies && typeof packageJson.devDependencies === "object" && !Array.isArray(packageJson.devDependencies)
1080
+ ? { ...packageJson.devDependencies }
1081
+ : {};
1082
+ const dependencies = packageJson.dependencies && typeof packageJson.dependencies === "object" && !Array.isArray(packageJson.dependencies)
1083
+ ? packageJson.dependencies
1084
+ : {};
1085
+ const stripeListenScript = buildStripeListenScript(localhostUrl);
1086
+ const changedScripts = [];
1087
+ let installConcurrently = false;
1088
+ if (scripts["stripe:listen"] !== stripeListenScript) {
1089
+ scripts["stripe:listen"] = stripeListenScript;
1090
+ changedScripts.push("stripe:listen");
1091
+ }
1092
+ const currentDevAll = typeof scripts["dev:all"] === "string" ? scripts["dev:all"] : "";
1093
+ if (!currentDevAll) {
1094
+ scripts["dev:all"] = STRIPE_ONLY_DEV_ALL_SCRIPT;
1095
+ changedScripts.push("dev:all");
1096
+ if (!isNonEmptyString(devDependencies.concurrently) && !isNonEmptyString(dependencies.concurrently)) {
1097
+ devDependencies.concurrently = CONCURRENTLY_VERSION;
1098
+ installConcurrently = true;
1099
+ }
1100
+ }
1101
+ else if (currentDevAll === TRIGGER_DEV_ALL_SCRIPT) {
1102
+ scripts["dev:all"] = TRIGGER_STRIPE_DEV_ALL_SCRIPT;
1103
+ changedScripts.push("dev:all");
1104
+ }
1105
+ else if (currentDevAll === STRIPE_ONLY_DEV_ALL_SCRIPT || currentDevAll === TRIGGER_STRIPE_DEV_ALL_SCRIPT) {
1106
+ // already configured
1107
+ }
1108
+ else if (currentDevAll.includes("npm run stripe:listen")) {
1109
+ // preserve user-updated script if Stripe listener is already included
1110
+ }
1111
+ else {
1112
+ fail("DEV_ALL_CONFLICT", "package.json already defines dev:all in an unexpected shape. Update it manually to include npm run stripe:listen, or reset it to the generated Trigger pattern before retrying.");
1113
+ }
1114
+ packageJson.scripts = scripts;
1115
+ if (Object.keys(devDependencies).length > 0) {
1116
+ packageJson.devDependencies = devDependencies;
1117
+ }
1118
+ writePackageJson(packageJsonPath, packageJson);
1119
+ if (installConcurrently) {
1120
+ runShellCommand(getDependencyInstallCommand(dependencyManager, "concurrently"), cwd);
1121
+ }
1122
+ return {
1123
+ changedScripts,
1124
+ installConcurrently,
1125
+ scripts,
1126
+ };
1127
+ }
1128
+ function waitForStripeListenSecret(command, cwd) {
1129
+ return new Promise((resolve, reject) => {
1130
+ const child = spawn(process.env.SHELL || "/bin/zsh", ["-lc", command], {
1131
+ cwd,
1132
+ env: process.env,
1133
+ stdio: ["ignore", "pipe", "pipe"],
1134
+ detached: true,
1135
+ });
1136
+ let buffer = "";
1137
+ let settled = false;
1138
+ const timeoutId = setTimeout(() => {
1139
+ if (!settled) {
1140
+ finish(new Error("Timed out waiting for Stripe CLI to print a webhook signing secret."));
1141
+ }
1142
+ }, 15000);
1143
+ const finish = (error, secret = "") => {
1144
+ if (settled)
1145
+ return;
1146
+ settled = true;
1147
+ clearTimeout(timeoutId);
1148
+ try {
1149
+ process.kill(-child.pid, "SIGTERM");
1150
+ }
1151
+ catch { }
1152
+ if (error)
1153
+ reject(error);
1154
+ else
1155
+ resolve(secret);
1156
+ };
1157
+ const inspectChunk = (chunk) => {
1158
+ buffer += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
1159
+ const match = buffer.match(/whsec_[A-Za-z0-9]+/);
1160
+ if (match) {
1161
+ finish(null, match[0]);
1162
+ }
1163
+ };
1164
+ child.stdout?.on("data", inspectChunk);
1165
+ child.stderr?.on("data", inspectChunk);
1166
+ child.on("error", (error) => finish(error));
1167
+ child.on("close", (code) => {
1168
+ if (!settled) {
1169
+ const message = buffer.trim() || `stripe listen exited with code ${String(code ?? "")}`.trim();
1170
+ finish(new Error(message || "Failed to capture Stripe webhook signing secret from stripe listen."));
1171
+ }
1172
+ });
1173
+ });
1174
+ }
1175
+ function formEncode(value, prefix = "") {
1176
+ const entries = [];
1177
+ if (Array.isArray(value)) {
1178
+ value.forEach((item, index) => {
1179
+ entries.push(...formEncode(item, `${prefix}[${index}]`));
1180
+ });
1181
+ return entries;
1182
+ }
1183
+ if (value && typeof value === "object") {
1184
+ for (const [key, nested] of Object.entries(value)) {
1185
+ entries.push(...formEncode(nested, prefix ? `${prefix}[${key}]` : key));
1186
+ }
1187
+ return entries;
1188
+ }
1189
+ if (value === null || value === undefined)
1190
+ return entries;
1191
+ entries.push([prefix, String(value)]);
1192
+ return entries;
1193
+ }
1194
+ async function stripeRequest(params) {
1195
+ const url = new URL(`https://api.stripe.com${params.path}`);
1196
+ if (params.query) {
1197
+ for (const [key, value] of formEncode(params.query)) {
1198
+ url.searchParams.append(key, value);
1199
+ }
1200
+ }
1201
+ const headers = {
1202
+ Authorization: `Bearer ${params.secretKey}`,
1203
+ };
1204
+ let body;
1205
+ if (params.body) {
1206
+ headers["Content-Type"] = "application/x-www-form-urlencoded";
1207
+ body = new URLSearchParams(formEncode(params.body));
1208
+ }
1209
+ const response = await fetch(url.toString(), {
1210
+ method: params.method,
1211
+ headers,
1212
+ body,
1213
+ });
1214
+ const json = await response.json().catch(() => null);
1215
+ if (!response.ok) {
1216
+ const message = extractErrorMessage(json?.error) || extractErrorMessage(json) || `Stripe returned ${response.status}`;
1217
+ fail("STRIPE_API_ERROR", message, 1, { status: response.status });
1218
+ }
1219
+ return json;
1220
+ }
1221
+ async function lemonRequest(params) {
1222
+ const url = new URL(`https://api.lemonsqueezy.com/v1${params.path}`);
1223
+ if (params.query) {
1224
+ for (const [key, value] of Object.entries(params.query)) {
1225
+ url.searchParams.set(key, value);
1226
+ }
1227
+ }
1228
+ const response = await fetch(url.toString(), {
1229
+ method: params.method,
1230
+ headers: {
1231
+ Accept: "application/vnd.api+json",
1232
+ Authorization: `Bearer ${params.apiKey}`,
1233
+ ...(params.body ? { "Content-Type": "application/vnd.api+json" } : {}),
1234
+ },
1235
+ body: params.body ? JSON.stringify(params.body) : undefined,
1236
+ });
1237
+ const json = await response.json().catch(() => null);
1238
+ if (!response.ok) {
1239
+ const detail = Array.isArray(json?.errors)
1240
+ ? (json.errors[0]?.detail)
1241
+ : null;
1242
+ const message = isNonEmptyString(detail) ? detail : (extractErrorMessage(json) || `Lemon Squeezy returned ${response.status}`);
1243
+ fail("LEMONSQUEEZY_API_ERROR", message, 1, { status: response.status });
1244
+ }
1245
+ return json;
1246
+ }
1247
+ function checkStripeKeys(flags) {
1248
+ const { values } = loadLocalEnv();
1249
+ const mode = normalizePaymentsMode(readStringFlag(flags, "mode"));
1250
+ const publishableKey = values.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;
1251
+ const secretKey = values.STRIPE_SECRET_KEY;
1252
+ const expectedPublishablePrefix = mode === "test" ? "pk_test_" : "pk_live_";
1253
+ const expectedSecretPrefix = mode === "test" ? "sk_test_" : "sk_live_";
1254
+ const missingKeys = [];
1255
+ const invalidKeys = [];
1256
+ const mistakes = [];
1257
+ if (!isNonEmptyString(publishableKey)) {
1258
+ missingKeys.push("NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY");
1259
+ }
1260
+ else if (!isStripePublishableKey(publishableKey)) {
1261
+ invalidKeys.push("NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY");
1262
+ }
1263
+ else if (!publishableKey.startsWith(expectedPublishablePrefix)) {
1264
+ mistakes.push(`NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY does not match ${mode} mode.`);
1265
+ }
1266
+ if (!isNonEmptyString(secretKey)) {
1267
+ missingKeys.push("STRIPE_SECRET_KEY");
1268
+ }
1269
+ else if (!isStripeSecretKey(secretKey)) {
1270
+ invalidKeys.push("STRIPE_SECRET_KEY");
1271
+ }
1272
+ else if (!secretKey.startsWith(expectedSecretPrefix)) {
1273
+ mistakes.push(`STRIPE_SECRET_KEY does not match ${mode} mode.`);
1274
+ }
1275
+ if (isNonEmptyString(publishableKey) && isNonEmptyString(secretKey)) {
1276
+ if (publishableKey.startsWith("pk_live_") && secretKey.startsWith("sk_test_")) {
1277
+ mistakes.push("Publishable and secret keys are from different Stripe modes (pk_live with sk_test).");
1278
+ }
1279
+ if (publishableKey.startsWith("pk_test_") && secretKey.startsWith("sk_live_")) {
1280
+ mistakes.push("Publishable and secret keys are from different Stripe modes (pk_test with sk_live).");
1281
+ }
1282
+ }
1283
+ if (missingKeys.length > 0 || invalidKeys.length > 0 || mistakes.length > 0) {
1284
+ fail("INVALID_PAYMENTS_ENV", "Stripe keys are missing, malformed, or inconsistent with the selected mode.", 1, { missingKeys, invalidKeys, mistakes });
1285
+ }
1286
+ printJson({
1287
+ ok: true,
1288
+ command: "payments check-stripe-keys",
1289
+ mode,
1290
+ verified: true,
1291
+ presentKeys: ["NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY", "STRIPE_SECRET_KEY"],
1292
+ checks: ["presence", "format", "mode_consistency"],
1293
+ });
1294
+ }
1295
+ function checkStripeCli() {
1296
+ const version = ensureStripeCliInstalled(process.cwd());
1297
+ printJson({
1298
+ ok: true,
1299
+ command: "payments check-stripe-cli",
1300
+ ready: true,
1301
+ version,
1302
+ });
1303
+ }
1304
+ function loginStripeCli(flags) {
1305
+ const { values } = loadLocalEnv();
1306
+ ensureStripeCliInstalled(process.cwd());
1307
+ const authMode = readStringFlag(flags, "auth-mode") === "api_key" ? "api_key" : "tty";
1308
+ const command = authMode === "api_key"
1309
+ ? `stripe login --api-key ${shellQuote(requireEnvValue(values, "STRIPE_SECRET_KEY", ".env.local"))}`
1310
+ : "stripe login";
1311
+ runShellCommand(command, process.cwd());
1312
+ printJson({
1313
+ ok: true,
1314
+ command: "payments login-stripe-cli",
1315
+ authMode,
1316
+ authenticated: true,
1317
+ });
1318
+ }
1319
+ function checkLemonKeys(flags) {
1320
+ const { values } = loadLocalEnv();
1321
+ const mode = normalizePaymentsMode(readStringFlag(flags, "mode"));
1322
+ const apiKey = values.LEMONSQUEEZY_API_KEY;
1323
+ const storeId = values.LEMONSQUEEZY_STORE_ID;
1324
+ const missingKeys = [];
1325
+ const invalidKeys = [];
1326
+ if (!isNonEmptyString(apiKey)) {
1327
+ missingKeys.push("LEMONSQUEEZY_API_KEY");
1328
+ }
1329
+ if (!isNonEmptyString(storeId)) {
1330
+ missingKeys.push("LEMONSQUEEZY_STORE_ID");
1331
+ }
1332
+ else if (!isNumericStoreId(storeId)) {
1333
+ invalidKeys.push("LEMONSQUEEZY_STORE_ID");
1334
+ }
1335
+ if (missingKeys.length > 0 || invalidKeys.length > 0) {
1336
+ fail("INVALID_PAYMENTS_ENV", "Lemon Squeezy API key or store ID is missing or malformed.", 1, { missingKeys, invalidKeys });
1337
+ }
1338
+ printJson({
1339
+ ok: true,
1340
+ command: "payments check-lemonsqueezy-keys",
1341
+ mode,
1342
+ verified: true,
1343
+ presentKeys: ["LEMONSQUEEZY_API_KEY", "LEMONSQUEEZY_STORE_ID"],
1344
+ checks: ["presence", "store_id_format"],
1345
+ });
1346
+ }
1347
+ async function createStripeWebhook(flags) {
1348
+ const { envLocalPath, values } = loadLocalEnv();
1349
+ const mode = normalizePaymentsMode(readStringFlag(flags, "mode"));
1350
+ const webhookUrl = requireWebhookUrl(readStringFlag(flags, "webhook-url"), "/api/webhooks/stripe", "Stripe");
1351
+ const secretKey = values.STRIPE_SECRET_KEY;
1352
+ if (!isStripeSecretKey(secretKey)) {
1353
+ fail("INVALID_PAYMENTS_ENV", "STRIPE_SECRET_KEY is missing or malformed in .env.local.");
1354
+ }
1355
+ const listed = await stripeRequest({
1356
+ secretKey,
1357
+ method: "GET",
1358
+ path: "/v1/webhook_endpoints",
1359
+ query: { limit: 100 },
1360
+ });
1361
+ const existing = Array.isArray(listed.data)
1362
+ ? listed.data.find((endpoint) => {
1363
+ const url = typeof endpoint.url === "string" ? endpoint.url : "";
1364
+ const events = Array.isArray(endpoint.enabled_events) ? endpoint.enabled_events : [];
1365
+ return url === webhookUrl && JSON.stringify([...events].sort()) === JSON.stringify([...STRIPE_EVENTS].sort());
1366
+ })
1367
+ : null;
1368
+ if (existing && isStripeWebhookSecret(values.STRIPE_WEBHOOK_SECRET)) {
1369
+ printJson({
1370
+ ok: true,
1371
+ command: "payments create-stripe-webhook",
1372
+ mode,
1373
+ created: false,
1374
+ reused: true,
1375
+ endpointId: typeof existing.id === "string" ? existing.id : null,
1376
+ webhookUrl,
1377
+ enabledEvents: STRIPE_EVENTS,
1378
+ envWritten: [],
1379
+ });
1380
+ return;
1381
+ }
1382
+ const created = await stripeRequest({
1383
+ secretKey,
1384
+ method: "POST",
1385
+ path: "/v1/webhook_endpoints",
1386
+ body: {
1387
+ url: webhookUrl,
1388
+ enabled_events: STRIPE_EVENTS,
1389
+ description: "VibeCodeMax payments webhook",
1390
+ },
1391
+ });
1392
+ if (!isStripeWebhookSecret(created.secret)) {
1393
+ fail("STRIPE_WEBHOOK_CREATE_FAILED", "Stripe did not return a webhook signing secret.");
1394
+ }
1395
+ mergeEnvFile(envLocalPath, {
1396
+ STRIPE_WEBHOOK_SECRET: created.secret.trim(),
1397
+ });
1398
+ printJson({
1399
+ ok: true,
1400
+ command: "payments create-stripe-webhook",
1401
+ mode,
1402
+ created: true,
1403
+ reused: false,
1404
+ endpointId: typeof created.id === "string" ? created.id : null,
1405
+ webhookUrl: typeof created.url === "string" ? created.url : webhookUrl,
1406
+ enabledEvents: STRIPE_EVENTS,
1407
+ envWritten: ["STRIPE_WEBHOOK_SECRET"],
1408
+ });
1409
+ }
1410
+ async function setupStripeLocalhost(flags) {
1411
+ const cwd = process.cwd();
1412
+ const dependencyManager = detectDependencyManager(cwd, flags);
1413
+ const localhostUrl = normalizeLocalUrl(readStringFlag(flags, "localhost-url"));
1414
+ const { envLocalPath, values } = loadLocalEnv(cwd);
1415
+ const secretKey = values.STRIPE_SECRET_KEY;
1416
+ if (!isStripeSecretKey(secretKey) || !secretKey.startsWith("sk_test_")) {
1417
+ fail("INVALID_PAYMENTS_ENV", "STRIPE_SECRET_KEY must be a Stripe test secret key in .env.local for localhost Stripe setup.");
1418
+ }
1419
+ ensureStripeCliInstalled(cwd);
1420
+ const scriptResult = ensureStripeLocalScripts(cwd, dependencyManager, localhostUrl);
1421
+ const listenCommand = `stripe listen --api-key ${shellQuote(secretKey.trim())} --events ${STRIPE_EVENTS.join(",")} --forward-to ${localhostUrl}/api/webhooks/stripe`;
1422
+ let signingSecret = "";
1423
+ try {
1424
+ signingSecret = await waitForStripeListenSecret(listenCommand, cwd);
1425
+ }
1426
+ catch (error) {
1427
+ fail("STRIPE_LOCALHOST_SETUP_FAILED", error instanceof Error ? error.message : "Failed to capture Stripe localhost webhook signing secret.");
1428
+ }
1429
+ mergeEnvFile(envLocalPath, {
1430
+ STRIPE_WEBHOOK_SECRET: signingSecret,
1431
+ });
1432
+ printJson({
1433
+ ok: true,
1434
+ command: "payments setup-stripe-localhost",
1435
+ localhostUrl,
1436
+ webhookUrl: `${localhostUrl}/api/webhooks/stripe`,
1437
+ scriptsChanged: scriptResult.changedScripts,
1438
+ installConcurrently: scriptResult.installConcurrently,
1439
+ devAllScript: typeof scriptResult.scripts["dev:all"] === "string" ? scriptResult.scripts["dev:all"] : null,
1440
+ stripeListenScript: typeof scriptResult.scripts["stripe:listen"] === "string" ? scriptResult.scripts["stripe:listen"] : null,
1441
+ envWritten: ["STRIPE_WEBHOOK_SECRET"],
1442
+ });
1443
+ }
1444
+ function checkStripeWebhookSecret(flags) {
1445
+ const { values } = loadLocalEnv();
1446
+ const mode = normalizePaymentsMode(readStringFlag(flags, "mode"));
1447
+ const webhookUrl = requireWebhookUrl(readStringFlag(flags, "webhook-url"), "/api/webhooks/stripe", "Stripe");
1448
+ const missingKeys = [];
1449
+ const invalidKeys = [];
1450
+ if (!isNonEmptyString(values.STRIPE_WEBHOOK_SECRET)) {
1451
+ missingKeys.push("STRIPE_WEBHOOK_SECRET");
1452
+ }
1453
+ else if (!isStripeWebhookSecret(values.STRIPE_WEBHOOK_SECRET)) {
1454
+ invalidKeys.push("STRIPE_WEBHOOK_SECRET");
1455
+ }
1456
+ if (missingKeys.length > 0 || invalidKeys.length > 0) {
1457
+ fail("INVALID_PAYMENTS_ENV", "Stripe webhook secret is missing or malformed in .env.local.", 1, { missingKeys, invalidKeys });
1458
+ }
1459
+ printJson({
1460
+ ok: true,
1461
+ command: "payments check-stripe-webhook-secret",
1462
+ mode,
1463
+ verified: true,
1464
+ webhookUrl,
1465
+ presentKeys: ["STRIPE_WEBHOOK_SECRET"],
1466
+ checks: ["presence", "format", "webhook_url_format"],
1467
+ });
1468
+ }
1469
+ async function createLemonWebhook(flags) {
1470
+ const { envLocalPath, values } = loadLocalEnv();
1471
+ const mode = normalizePaymentsMode(readStringFlag(flags, "mode"));
1472
+ const webhookUrl = requireWebhookUrl(readStringFlag(flags, "webhook-url"), "/api/webhooks/lemonsqueezy", "Lemon Squeezy");
1473
+ const apiKey = values.LEMONSQUEEZY_API_KEY;
1474
+ const storeId = values.LEMONSQUEEZY_STORE_ID;
1475
+ if (!isNonEmptyString(apiKey)) {
1476
+ fail("INVALID_PAYMENTS_ENV", "LEMONSQUEEZY_API_KEY is missing in .env.local.");
1477
+ }
1478
+ if (!isNumericStoreId(storeId)) {
1479
+ fail("INVALID_PAYMENTS_ENV", "LEMONSQUEEZY_STORE_ID is missing or malformed in .env.local.");
1480
+ }
1481
+ const listed = await lemonRequest({
1482
+ apiKey,
1483
+ method: "GET",
1484
+ path: "/webhooks",
1485
+ query: {
1486
+ "filter[store_id]": storeId.trim(),
1487
+ "page[size]": "100",
1488
+ },
1489
+ });
1490
+ const existing = Array.isArray(listed.data)
1491
+ ? listed.data.find((endpoint) => {
1492
+ const attrs = endpoint.attributes && typeof endpoint.attributes === "object"
1493
+ ? endpoint.attributes
1494
+ : {};
1495
+ const relationships = endpoint.relationships && typeof endpoint.relationships === "object"
1496
+ ? endpoint.relationships
1497
+ : {};
1498
+ const endpointStoreId = typeof relationships.store?.data?.id === "string"
1499
+ ? (relationships.store.data.id)
1500
+ : "";
1501
+ const events = Array.isArray(attrs.events) ? attrs.events : [];
1502
+ return String(attrs.url || "") === webhookUrl
1503
+ && endpointStoreId === storeId.trim()
1504
+ && JSON.stringify([...events].sort()) === JSON.stringify([...LEMON_EVENTS].sort());
1505
+ })
1506
+ : null;
1507
+ if (existing && isNonEmptyString(values.LEMONSQUEEZY_WEBHOOK_SECRET)) {
1508
+ printJson({
1509
+ ok: true,
1510
+ command: "payments create-lemonsqueezy-webhook",
1511
+ mode,
1512
+ created: false,
1513
+ reused: true,
1514
+ endpointId: typeof existing.id === "string" ? existing.id : null,
1515
+ webhookUrl,
1516
+ storeId: storeId.trim(),
1517
+ enabledEvents: LEMON_EVENTS,
1518
+ envWritten: [],
1519
+ });
1520
+ return;
1521
+ }
1522
+ const generatedSecret = randomBytes(24).toString("hex");
1523
+ const created = await lemonRequest({
1524
+ apiKey,
1525
+ method: "POST",
1526
+ path: "/webhooks",
1527
+ body: {
1528
+ data: {
1529
+ type: "webhooks",
1530
+ attributes: {
1531
+ url: webhookUrl,
1532
+ events: LEMON_EVENTS,
1533
+ secret: generatedSecret,
1534
+ test_mode: mode === "test",
1535
+ },
1536
+ relationships: {
1537
+ store: {
1538
+ data: {
1539
+ type: "stores",
1540
+ id: storeId.trim(),
1541
+ },
1542
+ },
1543
+ },
1544
+ },
1545
+ },
1546
+ });
1547
+ mergeEnvFile(envLocalPath, {
1548
+ LEMONSQUEEZY_WEBHOOK_SECRET: generatedSecret,
1549
+ });
1550
+ printJson({
1551
+ ok: true,
1552
+ command: "payments create-lemonsqueezy-webhook",
1553
+ mode,
1554
+ created: true,
1555
+ reused: false,
1556
+ endpointId: typeof created.data?.id === "string" ? created.data.id : null,
1557
+ webhookUrl: typeof created.data?.attributes?.url === "string" ? created.data.attributes.url : webhookUrl,
1558
+ storeId: storeId.trim(),
1559
+ enabledEvents: LEMON_EVENTS,
1560
+ envWritten: ["LEMONSQUEEZY_WEBHOOK_SECRET"],
1561
+ });
1562
+ }
1563
+ function checkLemonWebhookSecret(flags) {
1564
+ const { values } = loadLocalEnv();
1565
+ const mode = normalizePaymentsMode(readStringFlag(flags, "mode"));
1566
+ const webhookUrl = requireWebhookUrl(readStringFlag(flags, "webhook-url"), "/api/webhooks/lemonsqueezy", "Lemon Squeezy");
1567
+ const missingKeys = [];
1568
+ if (!isNonEmptyString(values.LEMONSQUEEZY_WEBHOOK_SECRET)) {
1569
+ missingKeys.push("LEMONSQUEEZY_WEBHOOK_SECRET");
1570
+ }
1571
+ if (missingKeys.length > 0) {
1572
+ fail("INVALID_PAYMENTS_ENV", "Lemon Squeezy webhook secret is missing in .env.local.", 1, { missingKeys });
1573
+ }
1574
+ printJson({
1575
+ ok: true,
1576
+ command: "payments check-lemonsqueezy-webhook-secret",
1577
+ mode,
1578
+ verified: true,
1579
+ webhookUrl,
1580
+ presentKeys: ["LEMONSQUEEZY_WEBHOOK_SECRET"],
1581
+ checks: ["presence", "webhook_url_format"],
1582
+ });
1583
+ }
976
1584
  async function main() {
977
1585
  const { command, subcommand, flags } = parseArgs(process.argv.slice(2));
978
1586
  if (!command || command === "--help" || command === "help") {
@@ -985,6 +1593,15 @@ async function main() {
985
1593
  "storage setup-supabase",
986
1594
  "storage check-s3-context",
987
1595
  "storage setup-s3",
1596
+ "payments check-stripe-keys",
1597
+ "payments check-stripe-cli",
1598
+ "payments login-stripe-cli",
1599
+ "payments setup-stripe-localhost",
1600
+ "payments create-stripe-webhook",
1601
+ "payments check-stripe-webhook-secret",
1602
+ "payments check-lemonsqueezy-keys",
1603
+ "payments create-lemonsqueezy-webhook",
1604
+ "payments check-lemonsqueezy-webhook-secret",
988
1605
  "configure-site-redirects",
989
1606
  "configure-email-password",
990
1607
  "enable-google-provider",
@@ -1011,6 +1628,24 @@ async function main() {
1011
1628
  return checkS3Context(flags);
1012
1629
  if (command === "storage" && subcommand === "setup-s3")
1013
1630
  return setupS3Storage(flags);
1631
+ if (command === "payments" && subcommand === "check-stripe-keys")
1632
+ return checkStripeKeys(flags);
1633
+ if (command === "payments" && subcommand === "check-stripe-cli")
1634
+ return checkStripeCli();
1635
+ if (command === "payments" && subcommand === "login-stripe-cli")
1636
+ return loginStripeCli(flags);
1637
+ if (command === "payments" && subcommand === "setup-stripe-localhost")
1638
+ return setupStripeLocalhost(flags);
1639
+ if (command === "payments" && subcommand === "create-stripe-webhook")
1640
+ return createStripeWebhook(flags);
1641
+ if (command === "payments" && subcommand === "check-stripe-webhook-secret")
1642
+ return checkStripeWebhookSecret(flags);
1643
+ if (command === "payments" && subcommand === "check-lemonsqueezy-keys")
1644
+ return checkLemonKeys(flags);
1645
+ if (command === "payments" && subcommand === "create-lemonsqueezy-webhook")
1646
+ return createLemonWebhook(flags);
1647
+ if (command === "payments" && subcommand === "check-lemonsqueezy-webhook-secret")
1648
+ return checkLemonWebhookSecret(flags);
1014
1649
  if (command === "configure-site-redirects")
1015
1650
  return configureSiteRedirects(flags);
1016
1651
  if (command === "configure-email-password")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecodemax/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "VibeCodeMax CLI — local provider setup for bootstrap and project configuration",
5
5
  "type": "module",
6
6
  "bin": {