@vibecodemax/cli 0.1.8 → 0.1.10

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 +238 -10
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
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
5
  import { randomBytes } from "node:crypto";
6
6
  import { checkS3Context, setupS3Storage } from "./storageS3.js";
7
7
  const SETUP_STATE_PATH = path.join(".vibecodemax", "setup-state.json");
@@ -32,6 +32,10 @@ const STRIPE_EVENTS = [
32
32
  "invoice.payment_succeeded",
33
33
  "invoice.payment_failed",
34
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"';
35
39
  const LEMON_EVENTS = [
36
40
  "order_created",
37
41
  "subscription_created",
@@ -651,6 +655,13 @@ function getSupabaseRunner(dependencyManager) {
651
655
  return "yarn supabase";
652
656
  return "npx supabase";
653
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
+ }
654
665
  function runShellCommand(command, cwd) {
655
666
  const result = spawnSync(process.env.SHELL || "/bin/zsh", ["-lc", command], {
656
667
  cwd,
@@ -679,6 +690,17 @@ function runLinkedSupabaseCommand(command, cwd, failureCode, fallbackMessage) {
679
690
  }
680
691
  return typeof result.stdout === "string" ? result.stdout.trim() : "";
681
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
+ }
682
704
  function isMigrationOrderingConflict(message) {
683
705
  return /Found local migration files to be inserted before the last migration on remote database\./i.test(message);
684
706
  }
@@ -1016,15 +1038,140 @@ function requireWebhookUrl(value, expectedPath, provider) {
1016
1038
  }
1017
1039
  try {
1018
1040
  const parsed = new URL(trimmed);
1019
- if (parsed.protocol !== "https:" || parsed.pathname !== expectedPath) {
1020
- fail("INVALID_WEBHOOK_URL", `${provider} webhook URL must be an https URL ending in ${expectedPath}.`);
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}.`);
1021
1045
  }
1022
1046
  }
1023
1047
  catch {
1024
- fail("INVALID_WEBHOOK_URL", `${provider} webhook URL must be a valid https URL ending in ${expectedPath}.`);
1048
+ fail("INVALID_WEBHOOK_URL", `${provider} webhook URL must be a valid https URL ending in ${expectedPath}, or http://localhost ending in ${expectedPath}.`);
1025
1049
  }
1026
1050
  return trimmed;
1027
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
+ }
1028
1175
  function formEncode(value, prefix = "") {
1029
1176
  const entries = [];
1030
1177
  if (Array.isArray(value)) {
@@ -1045,6 +1192,14 @@ function formEncode(value, prefix = "") {
1045
1192
  return entries;
1046
1193
  }
1047
1194
  async function stripeRequest(params) {
1195
+ const { response, json } = await stripeRequestWithResponse(params);
1196
+ if (!response.ok) {
1197
+ const message = extractErrorMessage(json?.error) || extractErrorMessage(json) || `Stripe returned ${response.status}`;
1198
+ fail("STRIPE_API_ERROR", message, 1, { status: response.status });
1199
+ }
1200
+ return json;
1201
+ }
1202
+ async function stripeRequestWithResponse(params) {
1048
1203
  const url = new URL(`https://api.stripe.com${params.path}`);
1049
1204
  if (params.query) {
1050
1205
  for (const [key, value] of formEncode(params.query)) {
@@ -1065,11 +1220,7 @@ async function stripeRequest(params) {
1065
1220
  body,
1066
1221
  });
1067
1222
  const json = await response.json().catch(() => null);
1068
- if (!response.ok) {
1069
- const message = extractErrorMessage(json?.error) || extractErrorMessage(json) || `Stripe returned ${response.status}`;
1070
- fail("STRIPE_API_ERROR", message, 1, { status: response.status });
1071
- }
1072
- return json;
1223
+ return { response, json };
1073
1224
  }
1074
1225
  async function lemonRequest(params) {
1075
1226
  const url = new URL(`https://api.lemonsqueezy.com/v1${params.path}`);
@@ -1145,6 +1296,30 @@ function checkStripeKeys(flags) {
1145
1296
  checks: ["presence", "format", "mode_consistency"],
1146
1297
  });
1147
1298
  }
1299
+ function checkStripeCli() {
1300
+ const version = ensureStripeCliInstalled(process.cwd());
1301
+ printJson({
1302
+ ok: true,
1303
+ command: "payments check-stripe-cli",
1304
+ ready: true,
1305
+ version,
1306
+ });
1307
+ }
1308
+ function loginStripeCli(flags) {
1309
+ const { values } = loadLocalEnv();
1310
+ ensureStripeCliInstalled(process.cwd());
1311
+ const authMode = readStringFlag(flags, "auth-mode") === "api_key" ? "api_key" : "tty";
1312
+ const command = authMode === "api_key"
1313
+ ? `stripe login --api-key ${shellQuote(requireEnvValue(values, "STRIPE_SECRET_KEY", ".env.local"))}`
1314
+ : "stripe login";
1315
+ runShellCommand(command, process.cwd());
1316
+ printJson({
1317
+ ok: true,
1318
+ command: "payments login-stripe-cli",
1319
+ authMode,
1320
+ authenticated: true,
1321
+ });
1322
+ }
1148
1323
  function checkLemonKeys(flags) {
1149
1324
  const { values } = loadLocalEnv();
1150
1325
  const mode = normalizePaymentsMode(readStringFlag(flags, "mode"));
@@ -1208,7 +1383,7 @@ async function createStripeWebhook(flags) {
1208
1383
  });
1209
1384
  return;
1210
1385
  }
1211
- const created = await stripeRequest({
1386
+ const createdResponse = await stripeRequestWithResponse({
1212
1387
  secretKey,
1213
1388
  method: "POST",
1214
1389
  path: "/v1/webhook_endpoints",
@@ -1218,6 +1393,16 @@ async function createStripeWebhook(flags) {
1218
1393
  description: "VibeCodeMax payments webhook",
1219
1394
  },
1220
1395
  });
1396
+ if (!createdResponse.response.ok) {
1397
+ const errorMessage = extractErrorMessage(createdResponse.json?.error)
1398
+ || extractErrorMessage(createdResponse.json)
1399
+ || `Stripe returned ${createdResponse.response.status}`;
1400
+ if (/activate|activation|business details|submit.*business/i.test(errorMessage)) {
1401
+ fail("STRIPE_ACCOUNT_NOT_ACTIVATED", "Your Stripe account is not activated. Submit your business details in the Stripe dashboard before setting up live webhooks.", 1, { status: createdResponse.response.status });
1402
+ }
1403
+ fail("STRIPE_WEBHOOK_CREATE_FAILED", errorMessage, 1, { status: createdResponse.response.status });
1404
+ }
1405
+ const created = createdResponse.json;
1221
1406
  if (!isStripeWebhookSecret(created.secret)) {
1222
1407
  fail("STRIPE_WEBHOOK_CREATE_FAILED", "Stripe did not return a webhook signing secret.");
1223
1408
  }
@@ -1236,6 +1421,40 @@ async function createStripeWebhook(flags) {
1236
1421
  envWritten: ["STRIPE_WEBHOOK_SECRET"],
1237
1422
  });
1238
1423
  }
1424
+ async function setupStripeLocalhost(flags) {
1425
+ const cwd = process.cwd();
1426
+ const dependencyManager = detectDependencyManager(cwd, flags);
1427
+ const localhostUrl = normalizeLocalUrl(readStringFlag(flags, "localhost-url"));
1428
+ const { envLocalPath, values } = loadLocalEnv(cwd);
1429
+ const secretKey = values.STRIPE_SECRET_KEY;
1430
+ if (!isStripeSecretKey(secretKey) || !secretKey.startsWith("sk_test_")) {
1431
+ fail("INVALID_PAYMENTS_ENV", "STRIPE_SECRET_KEY must be a Stripe test secret key in .env.local for localhost Stripe setup.");
1432
+ }
1433
+ ensureStripeCliInstalled(cwd);
1434
+ const scriptResult = ensureStripeLocalScripts(cwd, dependencyManager, localhostUrl);
1435
+ const listenCommand = `stripe listen --api-key ${shellQuote(secretKey.trim())} --events ${STRIPE_EVENTS.join(",")} --forward-to ${localhostUrl}/api/webhooks/stripe`;
1436
+ let signingSecret = "";
1437
+ try {
1438
+ signingSecret = await waitForStripeListenSecret(listenCommand, cwd);
1439
+ }
1440
+ catch (error) {
1441
+ fail("STRIPE_LOCALHOST_SETUP_FAILED", error instanceof Error ? error.message : "Failed to capture Stripe localhost webhook signing secret.");
1442
+ }
1443
+ mergeEnvFile(envLocalPath, {
1444
+ STRIPE_WEBHOOK_SECRET: signingSecret,
1445
+ });
1446
+ printJson({
1447
+ ok: true,
1448
+ command: "payments setup-stripe-localhost",
1449
+ localhostUrl,
1450
+ webhookUrl: `${localhostUrl}/api/webhooks/stripe`,
1451
+ scriptsChanged: scriptResult.changedScripts,
1452
+ installConcurrently: scriptResult.installConcurrently,
1453
+ devAllScript: typeof scriptResult.scripts["dev:all"] === "string" ? scriptResult.scripts["dev:all"] : null,
1454
+ stripeListenScript: typeof scriptResult.scripts["stripe:listen"] === "string" ? scriptResult.scripts["stripe:listen"] : null,
1455
+ envWritten: ["STRIPE_WEBHOOK_SECRET"],
1456
+ });
1457
+ }
1239
1458
  function checkStripeWebhookSecret(flags) {
1240
1459
  const { values } = loadLocalEnv();
1241
1460
  const mode = normalizePaymentsMode(readStringFlag(flags, "mode"));
@@ -1389,6 +1608,9 @@ async function main() {
1389
1608
  "storage check-s3-context",
1390
1609
  "storage setup-s3",
1391
1610
  "payments check-stripe-keys",
1611
+ "payments check-stripe-cli",
1612
+ "payments login-stripe-cli",
1613
+ "payments setup-stripe-localhost",
1392
1614
  "payments create-stripe-webhook",
1393
1615
  "payments check-stripe-webhook-secret",
1394
1616
  "payments check-lemonsqueezy-keys",
@@ -1422,6 +1644,12 @@ async function main() {
1422
1644
  return setupS3Storage(flags);
1423
1645
  if (command === "payments" && subcommand === "check-stripe-keys")
1424
1646
  return checkStripeKeys(flags);
1647
+ if (command === "payments" && subcommand === "check-stripe-cli")
1648
+ return checkStripeCli();
1649
+ if (command === "payments" && subcommand === "login-stripe-cli")
1650
+ return loginStripeCli(flags);
1651
+ if (command === "payments" && subcommand === "setup-stripe-localhost")
1652
+ return setupStripeLocalhost(flags);
1425
1653
  if (command === "payments" && subcommand === "create-stripe-webhook")
1426
1654
  return createStripeWebhook(flags);
1427
1655
  if (command === "payments" && subcommand === "check-stripe-webhook-secret")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecodemax/cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "VibeCodeMax CLI — local provider setup for bootstrap and project configuration",
5
5
  "type": "module",
6
6
  "bin": {