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