@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.
- package/dist/cli.js +238 -10
- 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
|
-
|
|
1020
|
-
|
|
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
|
-
|
|
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
|
|
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")
|