@vibecodemax/cli 0.1.10 → 0.1.12

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/README.md CHANGED
@@ -46,7 +46,7 @@ npx @vibecodemax/cli admin ensure-admin --email admin@example.com
46
46
 
47
47
  This command reads `SUPABASE_SERVICE_ROLE_KEY` and `SUPABASE_URL` or `NEXT_PUBLIC_SUPABASE_URL` from local env files, performs lookup/create/promote/verify locally, and prints JSON only.
48
48
 
49
- If a new user is created, the CLI output includes `temporaryPassword`. Show that password to the user directly and do not forward it into MCP payloads.
49
+ If a new user is created, the CLI output does not expose a password. The result indicates that password setup still needs a user-facing reset or invite flow.
50
50
 
51
51
  ### `configure-site-redirects`
52
52
 
@@ -73,18 +73,6 @@ Enables Google OAuth using credentials from `.env.bootstrap.local`. The Supabase
73
73
  npx @vibecodemax/cli enable-google-provider
74
74
  ```
75
75
 
76
- ### `apply-auth-templates`
77
-
78
- Applies custom auth email templates from local project files. The Supabase personal access token must be stored as `SUPABASE_ACCESS_TOKEN`.
79
-
80
- ```bash
81
- npx @vibecodemax/cli apply-auth-templates \
82
- --confirm-email-enabled true \
83
- --confirm-email-path src/email/confirm-email.html \
84
- --reset-password-enabled true \
85
- --reset-password-path src/email/reset-password.html
86
- ```
87
-
88
76
  ## Output
89
77
 
90
78
  All commands print structured JSON to stdout and exit with code 0 on success, non-zero on failure.
@@ -99,4 +87,4 @@ All commands print structured JSON to stdout and exit with code 0 on success, no
99
87
 
100
88
  ## Security
101
89
 
102
- Credentials are read from local env files and sent only to the relevant provider APIs. No secret values appear in CLI output.
90
+ Credentials are read from local env files and sent only to the relevant provider APIs. No secret values appear in CLI output. Secret-bearing local commands avoid putting credentials on child-process argv, and `read-setup-state` requires `.vibecodemax/setup-state.json` to remain secret-free.
package/dist/cli.js CHANGED
@@ -48,6 +48,21 @@ const LEMON_EVENTS = [
48
48
  "subscription_payment_failed",
49
49
  "subscription_payment_success",
50
50
  ];
51
+ const SECRET_LIKE_KEY_PATTERN = /(access[_-]?token|refresh[_-]?token|service[_-]?role|webhook[_-]?secret|api[_-]?key|secret|password)/i;
52
+ const SECRET_LIKE_VALUE_PATTERN = /(sb_secret_[A-Za-z0-9_-]+|sk_(?:test|live)_[A-Za-z0-9]+|whsec_[A-Za-z0-9]+|SUPABASE_SERVICE_ROLE_KEY|SUPABASE_ACCESS_TOKEN)/;
53
+ const CHILD_PROCESS_ENV_KEYS = [
54
+ "PATH",
55
+ "HOME",
56
+ "USERPROFILE",
57
+ "APPDATA",
58
+ "LOCALAPPDATA",
59
+ "TMPDIR",
60
+ "TMP",
61
+ "TEMP",
62
+ "SYSTEMROOT",
63
+ "COMSPEC",
64
+ "SHELL",
65
+ ];
51
66
  function printJson(value) {
52
67
  process.stdout.write(`${JSON.stringify(value)}\n`);
53
68
  }
@@ -73,7 +88,7 @@ function parseArgs(argv) {
73
88
  let subcommand;
74
89
  const flags = {};
75
90
  let index = 0;
76
- if ((command === "admin" || command === "storage" || command === "payments") && rest[0] && !rest[0].startsWith("--")) {
91
+ if ((command === "base" || command === "admin" || command === "storage" || command === "payments") && rest[0] && !rest[0].startsWith("--")) {
77
92
  subcommand = rest[0];
78
93
  index = 1;
79
94
  }
@@ -231,12 +246,43 @@ function requireServiceRoleKey(envValues) {
231
246
  return requireEnvValue(envValues, "SUPABASE_SERVICE_ROLE_KEY", ".env.local");
232
247
  }
233
248
  function mergeEnvFile(filePath, nextValues) {
249
+ for (const [key, value] of Object.entries(nextValues)) {
250
+ if (/[\r\n]/.test(value)) {
251
+ fail("INVALID_ENV_VALUE", `${key} contains a newline and cannot be written safely to ${path.basename(filePath)}.`);
252
+ }
253
+ }
234
254
  const existing = parseDotEnv(readFileIfExists(filePath));
235
255
  const merged = { ...existing, ...nextValues };
236
256
  const keys = Object.keys(merged).sort();
237
257
  const content = `${keys.map((key) => `${key}=${merged[key]}`).join("\n")}\n`;
238
258
  fs.writeFileSync(filePath, content, "utf8");
239
259
  }
260
+ function detectSecretLikeEntry(value, keyPath = "setupState") {
261
+ if (Array.isArray(value)) {
262
+ for (let index = 0; index < value.length; index += 1) {
263
+ const nested = detectSecretLikeEntry(value[index], `${keyPath}[${index}]`);
264
+ if (nested)
265
+ return nested;
266
+ }
267
+ return null;
268
+ }
269
+ if (value && typeof value === "object") {
270
+ for (const [key, nestedValue] of Object.entries(value)) {
271
+ const nestedPath = `${keyPath}.${key}`;
272
+ if (SECRET_LIKE_KEY_PATTERN.test(key)) {
273
+ return nestedPath;
274
+ }
275
+ const nested = detectSecretLikeEntry(nestedValue, nestedPath);
276
+ if (nested)
277
+ return nested;
278
+ }
279
+ return null;
280
+ }
281
+ if (typeof value === "string" && SECRET_LIKE_VALUE_PATTERN.test(value)) {
282
+ return keyPath;
283
+ }
284
+ return null;
285
+ }
240
286
  function readSetupState() {
241
287
  const setupStatePath = path.join(process.cwd(), SETUP_STATE_PATH);
242
288
  const content = readFileIfExists(setupStatePath).trim();
@@ -254,11 +300,19 @@ function readSetupState() {
254
300
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
255
301
  fail("INVALID_SETUP_STATE", `${SETUP_STATE_PATH} must contain a JSON object.`);
256
302
  }
303
+ const secretPath = detectSecretLikeEntry(parsed);
304
+ if (secretPath) {
305
+ fail("SECRET_IN_SETUP_STATE", `${SETUP_STATE_PATH} must remain secret-free. Remove secret-like data before reading it through the CLI.`, 1, {
306
+ keyPath: secretPath,
307
+ });
308
+ }
257
309
  printJson({ ok: true, setupState: parsed });
258
310
  }
259
- async function managementRequest({ method, projectRef, token, body }) {
260
- const endpoint = `${MANAGEMENT_API_BASE}/v1/projects/${projectRef}/config/auth`;
261
- const response = await fetch(endpoint, {
311
+ async function managementRequest({ method, projectRef, token, body, endpoint }) {
312
+ const resolvedEndpoint = endpoint
313
+ ? `${MANAGEMENT_API_BASE}${endpoint}`
314
+ : `${MANAGEMENT_API_BASE}/v1/projects/${projectRef}/config/auth`;
315
+ const response = await fetch(resolvedEndpoint, {
262
316
  method,
263
317
  headers: {
264
318
  Accept: "application/json",
@@ -294,49 +348,6 @@ function readAuthConfigEnvelope(responseJson) {
294
348
  return envelope.config.auth;
295
349
  return responseJson;
296
350
  }
297
- function normalizeTemplateText(value) {
298
- if (!isNonEmptyString(value))
299
- return "";
300
- return value.replace(/\r\n/g, "\n").trim();
301
- }
302
- function buildTemplateDesiredFields(options) {
303
- const desired = {};
304
- if (options.confirmEmailEnabled) {
305
- const confirmHtml = normalizeTemplateText(readFileIfExists(path.resolve(process.cwd(), options.confirmEmailPath || "")));
306
- if (!confirmHtml) {
307
- fail("MISSING_TEMPLATE_FILE", `Confirm email template content is missing. Check ${options.confirmEmailPath}.`);
308
- }
309
- desired.mailer_subjects_confirmation = isNonEmptyString(options.confirmEmailSubject)
310
- ? options.confirmEmailSubject.trim()
311
- : "Confirm your signup";
312
- desired.mailer_templates_confirmation_content = confirmHtml;
313
- }
314
- if (options.resetPasswordEnabled) {
315
- const resetHtml = normalizeTemplateText(readFileIfExists(path.resolve(process.cwd(), options.resetPasswordPath || "")));
316
- if (!resetHtml) {
317
- fail("MISSING_TEMPLATE_FILE", `Reset password template content is missing. Check ${options.resetPasswordPath}.`);
318
- }
319
- desired.mailer_subjects_recovery = isNonEmptyString(options.resetPasswordSubject)
320
- ? options.resetPasswordSubject.trim()
321
- : "Reset your password";
322
- desired.mailer_templates_recovery_content = resetHtml;
323
- }
324
- return desired;
325
- }
326
- function diffTemplateFields(currentAuthConfig = {}, desiredFields) {
327
- const patch = {};
328
- const changedKeys = [];
329
- for (const [key, desiredValue] of Object.entries(desiredFields)) {
330
- const currentValue = currentAuthConfig[key];
331
- const normalizedCurrent = typeof currentValue === "string" ? normalizeTemplateText(currentValue) : currentValue;
332
- const normalizedDesired = typeof desiredValue === "string" ? normalizeTemplateText(desiredValue) : desiredValue;
333
- if (normalizedCurrent !== normalizedDesired) {
334
- patch[key] = desiredValue;
335
- changedKeys.push(key);
336
- }
337
- }
338
- return { patch, changedKeys, noChanges: changedKeys.length === 0 };
339
- }
340
351
  async function configureSiteRedirects(flags) {
341
352
  const { envLocalPath, envBootstrapPath, values } = loadLocalEnv();
342
353
  const projectRef = resolveProjectRef(flags, values);
@@ -427,34 +438,427 @@ async function enableGoogleProvider(flags) {
427
438
  applied: ["external_google_enabled", "external_google_client_id", "external_google_secret"],
428
439
  });
429
440
  }
430
- async function applyAuthTemplates(flags) {
431
- const { envBootstrapPath, values } = loadLocalEnv();
432
- const projectRef = resolveProjectRef(flags, values);
433
- if (!projectRef) {
434
- fail("MISSING_PROJECT_REF", "SUPABASE_PROJECT_REF is missing. Add it to .env.bootstrap.local or ensure NEXT_PUBLIC_SUPABASE_URL is present in .env.local.");
441
+ function readObject(value) {
442
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
443
+ }
444
+ function readIdentifier(value) {
445
+ if (typeof value === "number" && Number.isFinite(value))
446
+ return String(value);
447
+ return isNonEmptyString(value) ? value.trim() : "";
448
+ }
449
+ function normalizeDependencyManagerFlag(flags) {
450
+ const dependencyManager = readStringFlag(flags, "dependency-manager");
451
+ if (dependencyManager === "pnpm")
452
+ return "pnpm";
453
+ if (dependencyManager === "yarn")
454
+ return "yarn";
455
+ return "npm";
456
+ }
457
+ function getSupabaseRunnerCommand(dependencyManager) {
458
+ if (dependencyManager === "pnpm") {
459
+ return { command: "pnpm", args: ["exec", "supabase"] };
460
+ }
461
+ if (dependencyManager === "yarn") {
462
+ return { command: "yarn", args: ["supabase"] };
463
+ }
464
+ return { command: "npx", args: ["supabase"] };
465
+ }
466
+ function runSupabaseCommand(args, dependencyManager) {
467
+ const runner = getSupabaseRunnerCommand(dependencyManager);
468
+ const result = spawnSync(runner.command, [...runner.args, ...args], {
469
+ cwd: process.cwd(),
470
+ encoding: "utf8",
471
+ });
472
+ if (result.status !== 0) {
473
+ fail("SUPABASE_CLI_ERROR", [result.stderr, result.stdout].find(isNonEmptyString) || `Supabase CLI ${args.join(" ")} failed.`, 1, { exitCode: result.status ?? 1 });
474
+ }
475
+ }
476
+ function runSupabaseCommandCapture(args, dependencyManager) {
477
+ const runner = getSupabaseRunnerCommand(dependencyManager);
478
+ const result = spawnSync(runner.command, [...runner.args, ...args], {
479
+ cwd: process.cwd(),
480
+ encoding: "utf8",
481
+ });
482
+ if (result.status !== 0) {
483
+ fail("SUPABASE_CLI_ERROR", [result.stderr, result.stdout].find(isNonEmptyString) || `Supabase CLI ${args.join(" ")} failed.`, 1, { exitCode: result.status ?? 1 });
484
+ }
485
+ return result.stdout || "";
486
+ }
487
+ function extractMigrationVersion(value) {
488
+ if (!isNonEmptyString(value))
489
+ return "";
490
+ const match = value.trim().match(/^(\d{14})/);
491
+ return match ? match[1] : "";
492
+ }
493
+ function listLocalMigrationVersions(cwd = process.cwd()) {
494
+ const migrationsDir = path.join(cwd, "supabase", "migrations");
495
+ let entries = [];
496
+ try {
497
+ entries = fs.readdirSync(migrationsDir);
498
+ }
499
+ catch {
500
+ return [];
501
+ }
502
+ return [...new Set(entries.map((entry) => extractMigrationVersion(entry)).filter(Boolean))].sort();
503
+ }
504
+ function collectVersionsFromRows(rows, keyCandidates) {
505
+ const values = new Set();
506
+ for (const row of rows) {
507
+ if (!row || typeof row !== "object")
508
+ continue;
509
+ const record = row;
510
+ for (const key of keyCandidates) {
511
+ const version = extractMigrationVersion(record[key]);
512
+ if (version) {
513
+ values.add(version);
514
+ }
515
+ }
516
+ }
517
+ return Array.from(values).sort();
518
+ }
519
+ function parseMigrationListOutput(raw) {
520
+ const trimmed = raw.trim();
521
+ if (!trimmed) {
522
+ return { localVersions: [], remoteVersions: [] };
523
+ }
524
+ try {
525
+ const parsed = JSON.parse(trimmed);
526
+ const rows = Array.isArray(parsed)
527
+ ? parsed
528
+ : Array.isArray(parsed?.migrations)
529
+ ? (parsed.migrations)
530
+ : Array.isArray(parsed?.data)
531
+ ? (parsed.data)
532
+ : null;
533
+ if (!rows) {
534
+ fail("INVALID_MIGRATION_LIST_OUTPUT", "Supabase migration list JSON output did not contain a rows array.");
535
+ }
536
+ const localVersions = collectVersionsFromRows(rows, [
537
+ "local",
538
+ "local_version",
539
+ "localVersion",
540
+ "localMigration",
541
+ "local_migration",
542
+ ]);
543
+ const remoteVersions = collectVersionsFromRows(rows, [
544
+ "remote",
545
+ "remote_version",
546
+ "remoteVersion",
547
+ "remoteMigration",
548
+ "remote_migration",
549
+ ]);
550
+ if (localVersions.length === 0 && remoteVersions.length === 0) {
551
+ fail("INVALID_MIGRATION_LIST_OUTPUT", "Supabase migration list JSON output did not include recognizable local or remote migration version fields.");
552
+ }
553
+ return { localVersions, remoteVersions };
554
+ }
555
+ catch (error) {
556
+ if (error instanceof SyntaxError) {
557
+ fail("INVALID_MIGRATION_LIST_OUTPUT", "Supabase migration list output was not valid JSON. Upgrade the local Supabase CLI if it does not support --output json for migration list.");
558
+ }
559
+ throw error;
560
+ }
561
+ }
562
+ async function supabaseManagementApiRequest(params) {
563
+ const response = await fetch(`${MANAGEMENT_API_BASE}${params.endpoint}`, {
564
+ method: params.method,
565
+ headers: {
566
+ Accept: "application/json",
567
+ "Content-Type": "application/json",
568
+ Authorization: `Bearer ${params.token}`,
569
+ },
570
+ body: params.body === undefined ? undefined : JSON.stringify(params.body),
571
+ });
572
+ let json = null;
573
+ try {
574
+ json = await response.json();
575
+ }
576
+ catch {
577
+ json = null;
578
+ }
579
+ if (!response.ok) {
580
+ const message = extractErrorMessage(json) || `Supabase returned ${response.status}`;
581
+ fail("MANAGEMENT_API_ERROR", message, 1, { status: response.status, endpoint: params.endpoint });
582
+ }
583
+ return json;
584
+ }
585
+ function readArrayResponse(value) {
586
+ if (Array.isArray(value))
587
+ return value;
588
+ const record = readObject(value);
589
+ for (const key of ["organizations", "projects", "api_keys", "apiKeys", "data"]) {
590
+ if (Array.isArray(record[key]))
591
+ return record[key];
592
+ }
593
+ return [];
594
+ }
595
+ function parseOrganizationRecord(value) {
596
+ const record = readObject(value);
597
+ const id = readIdentifier(record.id);
598
+ const name = readIdentifier(record.name || record.organization_name || record.slug || record.ref);
599
+ if (!id || !name)
600
+ return null;
601
+ return { id, name };
602
+ }
603
+ function parseProjectRecord(value) {
604
+ const record = readObject(value);
605
+ const ref = readIdentifier(record.id || record.ref || record.project_ref || record.projectRef);
606
+ const name = readIdentifier(record.name || record.project_name || record.projectName);
607
+ if (!ref || !name)
608
+ return null;
609
+ return {
610
+ ref,
611
+ name,
612
+ status: readIdentifier(record.status || record.project_status || record.state),
613
+ region: readIdentifier(record.region || record.region_code || record.aws_region || record.location),
614
+ };
615
+ }
616
+ function formatOrganizationsForPrompt(orgs) {
617
+ if (orgs.length === 0)
618
+ return "No Supabase organizations were found for this account.";
619
+ return orgs.map((org) => `- ${org.name} (${org.id})`).join("\n");
620
+ }
621
+ function formatProjectsForPrompt(projects) {
622
+ if (projects.length === 0)
623
+ return "No existing Supabase projects were found for this account.";
624
+ return projects.map((project) => {
625
+ const details = [project.ref, project.region].filter(Boolean).join(" • ");
626
+ return details ? `- ${project.name} (${details})` : `- ${project.name} (${project.ref})`;
627
+ }).join("\n");
628
+ }
629
+ async function listSupabaseOrganizations(token) {
630
+ const json = await supabaseManagementApiRequest({ method: "GET", token, endpoint: "/v1/organizations" });
631
+ return readArrayResponse(json)
632
+ .map(parseOrganizationRecord)
633
+ .filter((entry) => Boolean(entry));
634
+ }
635
+ async function createSupabaseOrganization(token, name) {
636
+ const json = await supabaseManagementApiRequest({
637
+ method: "POST",
638
+ token,
639
+ endpoint: "/v1/organizations",
640
+ body: { name },
641
+ });
642
+ const direct = parseOrganizationRecord(json);
643
+ if (direct)
644
+ return direct;
645
+ const nested = parseOrganizationRecord(readObject(json).organization || readObject(json).data);
646
+ if (nested)
647
+ return nested;
648
+ fail("INVALID_MANAGEMENT_RESPONSE", "Supabase organization creation did not return an organization identifier.");
649
+ }
650
+ async function listSupabaseProjects(token) {
651
+ const json = await supabaseManagementApiRequest({ method: "GET", token, endpoint: "/v1/projects" });
652
+ return readArrayResponse(json)
653
+ .map(parseProjectRecord)
654
+ .filter((entry) => Boolean(entry));
655
+ }
656
+ function generateDbPassword() {
657
+ return randomBytes(24).toString("base64url");
658
+ }
659
+ async function createSupabaseProject(token, params) {
660
+ const json = await supabaseManagementApiRequest({
661
+ method: "POST",
662
+ token,
663
+ endpoint: "/v1/projects",
664
+ body: {
665
+ organization_id: params.organizationId,
666
+ name: params.projectName,
667
+ region: params.region,
668
+ db_pass: params.dbPassword,
669
+ },
670
+ });
671
+ const direct = parseProjectRecord(json);
672
+ if (direct)
673
+ return direct;
674
+ const nested = parseProjectRecord(readObject(json).project || readObject(json).data);
675
+ if (nested)
676
+ return nested;
677
+ fail("INVALID_MANAGEMENT_RESPONSE", "Supabase project creation did not return a project ref.");
678
+ }
679
+ async function waitForSupabaseProjectReady(token, projectRef) {
680
+ const readyStatuses = new Set(["ACTIVE_HEALTHY", "ACTIVE", "HEALTHY", "READY"]);
681
+ const pendingStatuses = new Set(["INACTIVE", "CREATING", "COMING_UP", "INIT", "UNKNOWN"]);
682
+ for (let attempt = 0; attempt < 36; attempt += 1) {
683
+ const json = await supabaseManagementApiRequest({ method: "GET", token, endpoint: `/v1/projects/${projectRef}` });
684
+ const project = parseProjectRecord(json) || parseProjectRecord(readObject(json).project || readObject(json).data);
685
+ const status = readIdentifier(project?.status || readObject(json).status || readObject(json).project_status).toUpperCase();
686
+ if (!status || readyStatuses.has(status))
687
+ return;
688
+ if (!pendingStatuses.has(status))
689
+ return;
690
+ await new Promise((resolve) => setTimeout(resolve, 5000));
691
+ }
692
+ fail("PROJECT_NOT_READY", `Supabase project ${projectRef} did not become ready in time.`);
693
+ }
694
+ async function fetchSupabaseProjectKeys(token, projectRef) {
695
+ const json = await supabaseManagementApiRequest({
696
+ method: "GET",
697
+ token,
698
+ endpoint: `/v1/projects/${projectRef}/api-keys?reveal=true`,
699
+ });
700
+ const records = readArrayResponse(json).map(readObject);
701
+ const values = records
702
+ .map((record) => [record.api_key, record.apiKey, record.key, record.value].find(isNonEmptyString))
703
+ .filter((value) => isNonEmptyString(value));
704
+ const publishableKey = values.find((value) => value.startsWith("sb_publishable_")) || "";
705
+ const serviceRoleKey = values.find((value) => value.startsWith("sb_secret_")) || "";
706
+ if (!publishableKey || !serviceRoleKey) {
707
+ fail("MISSING_PROJECT_KEYS", `Supabase did not return publishable and secret API keys for project ${projectRef}.`);
435
708
  }
709
+ return {
710
+ projectUrl: `https://${projectRef}.supabase.co`,
711
+ publishableKey,
712
+ serviceRoleKey,
713
+ };
714
+ }
715
+ function writeSupabaseProjectEnv(envLocalPath, envBootstrapPath, values) {
716
+ mergeEnvFile(envLocalPath, {
717
+ NEXT_PUBLIC_SUPABASE_URL: values.projectUrl,
718
+ SUPABASE_URL: values.projectUrl,
719
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: values.publishableKey,
720
+ SUPABASE_SERVICE_ROLE_KEY: values.serviceRoleKey,
721
+ });
722
+ const bootstrapValues = {
723
+ SUPABASE_PROJECT_REF: values.projectRef,
724
+ };
725
+ if (isNonEmptyString(values.dbPassword)) {
726
+ bootstrapValues.SUPABASE_DB_PASSWORD = values.dbPassword;
727
+ }
728
+ mergeEnvFile(envBootstrapPath, bootstrapValues);
729
+ return {
730
+ envKeysWritten: [
731
+ "NEXT_PUBLIC_SUPABASE_URL",
732
+ "SUPABASE_URL",
733
+ "NEXT_PUBLIC_SUPABASE_ANON_KEY",
734
+ "SUPABASE_SERVICE_ROLE_KEY",
735
+ ],
736
+ bootstrapKeysWritten: Object.keys(bootstrapValues),
737
+ };
738
+ }
739
+ async function checkBaseSupabaseAccessToken() {
740
+ const { envBootstrapPath, values } = loadLocalEnv();
436
741
  const accessToken = requireAuthAccessToken(values, path.basename(envBootstrapPath));
437
- const desiredFields = buildTemplateDesiredFields({
438
- confirmEmailEnabled: readStringFlag(flags, "confirm-email-enabled") === "true",
439
- confirmEmailSubject: readStringFlag(flags, "confirm-email-subject"),
440
- confirmEmailPath: readStringFlag(flags, "confirm-email-path"),
441
- resetPasswordEnabled: readStringFlag(flags, "reset-password-enabled") === "true",
442
- resetPasswordSubject: readStringFlag(flags, "reset-password-subject"),
443
- resetPasswordPath: readStringFlag(flags, "reset-password-path"),
742
+ const [organizations, projects] = await Promise.all([
743
+ listSupabaseOrganizations(accessToken),
744
+ listSupabaseProjects(accessToken),
745
+ ]);
746
+ printJson({
747
+ ok: true,
748
+ command: "base check-supabase-access-token",
749
+ hasExistingProjects: projects.length > 0,
750
+ projectCount: projects.length,
751
+ projectList: formatProjectsForPrompt(projects),
752
+ projects: projects.map((project) => ({ ref: project.ref, name: project.name, region: project.region })),
753
+ hasOrganizations: organizations.length > 0,
754
+ organizationCount: organizations.length,
755
+ organizationList: formatOrganizationsForPrompt(organizations),
756
+ organizations: organizations,
444
757
  });
445
- const current = await managementRequest({ method: "GET", projectRef, token: accessToken });
446
- const currentAuthConfig = readAuthConfigEnvelope(current);
447
- const diff = diffTemplateFields(currentAuthConfig, desiredFields);
448
- if (!diff.noChanges) {
449
- await managementRequest({ method: "PATCH", projectRef, token: accessToken, body: diff.patch });
758
+ }
759
+ async function createBaseSupabaseProject(flags) {
760
+ const { envLocalPath, envBootstrapPath, values } = loadLocalEnv();
761
+ const accessToken = requireAuthAccessToken(values, path.basename(envBootstrapPath));
762
+ const dependencyManager = normalizeDependencyManagerFlag(flags);
763
+ const selection = readStringFlag(flags, "organization-selection");
764
+ if (!selection) {
765
+ fail("MISSING_ORGANIZATION_SELECTION", "--organization-selection is required.");
766
+ }
767
+ const projectName = readStringFlag(flags, "project-name");
768
+ if (!projectName) {
769
+ fail("MISSING_PROJECT_NAME", "--project-name is required.");
770
+ }
771
+ const region = readStringFlag(flags, "region");
772
+ if (!region) {
773
+ fail("MISSING_PROJECT_REGION", "--region is required.");
774
+ }
775
+ let organizationId = selection;
776
+ let createdOrganization = false;
777
+ if (selection === "create") {
778
+ const organizationName = readStringFlag(flags, "organization-name");
779
+ if (!organizationName) {
780
+ fail("MISSING_ORGANIZATION_NAME", "--organization-name is required when --organization-selection create is used.");
781
+ }
782
+ const organization = await createSupabaseOrganization(accessToken, organizationName);
783
+ organizationId = organization.id;
784
+ createdOrganization = true;
785
+ }
786
+ const existingDbPassword = readIdentifier(values.SUPABASE_DB_PASSWORD);
787
+ const dbPassword = existingDbPassword || generateDbPassword();
788
+ const project = await createSupabaseProject(accessToken, {
789
+ organizationId,
790
+ projectName,
791
+ region,
792
+ dbPassword,
793
+ });
794
+ await waitForSupabaseProjectReady(accessToken, project.ref);
795
+ runSupabaseCommand(["link", "--project-ref", project.ref], dependencyManager);
796
+ const keys = await fetchSupabaseProjectKeys(accessToken, project.ref);
797
+ const writes = writeSupabaseProjectEnv(envLocalPath, envBootstrapPath, {
798
+ projectRef: project.ref,
799
+ projectUrl: keys.projectUrl,
800
+ publishableKey: keys.publishableKey,
801
+ serviceRoleKey: keys.serviceRoleKey,
802
+ dbPassword,
803
+ });
804
+ printJson({
805
+ ok: true,
806
+ command: "base create-supabase-project",
807
+ projectRef: project.ref,
808
+ projectUrl: keys.projectUrl,
809
+ envKeysWritten: writes.envKeysWritten,
810
+ bootstrapKeysWritten: writes.bootstrapKeysWritten,
811
+ dbPasswordGenerated: true,
812
+ dbPasswordReused: Boolean(existingDbPassword),
813
+ createdOrganization,
814
+ });
815
+ }
816
+ async function linkBaseSupabaseProject(flags) {
817
+ const { envLocalPath, envBootstrapPath, values } = loadLocalEnv();
818
+ const accessToken = requireAuthAccessToken(values, path.basename(envBootstrapPath));
819
+ const dependencyManager = normalizeDependencyManagerFlag(flags);
820
+ const projectRef = readStringFlag(flags, "project-ref");
821
+ if (!projectRef) {
822
+ fail("MISSING_PROJECT_REF", "--project-ref is required.");
450
823
  }
824
+ runSupabaseCommand(["link", "--project-ref", projectRef], dependencyManager);
825
+ const keys = await fetchSupabaseProjectKeys(accessToken, projectRef);
826
+ const writes = writeSupabaseProjectEnv(envLocalPath, envBootstrapPath, {
827
+ projectRef,
828
+ projectUrl: keys.projectUrl,
829
+ publishableKey: keys.publishableKey,
830
+ serviceRoleKey: keys.serviceRoleKey,
831
+ });
451
832
  printJson({
452
833
  ok: true,
453
- command: "apply-auth-templates",
834
+ command: "base link-supabase-project",
454
835
  projectRef,
455
- changedKeys: diff.changedKeys,
456
- appliedKeys: diff.changedKeys,
457
- noChanges: diff.noChanges,
836
+ projectUrl: keys.projectUrl,
837
+ envKeysWritten: writes.envKeysWritten,
838
+ bootstrapKeysWritten: writes.bootstrapKeysWritten,
839
+ });
840
+ }
841
+ function preflightBaseMigrationState(flags) {
842
+ const dependencyManager = normalizeDependencyManagerFlag(flags);
843
+ const localVersions = listLocalMigrationVersions();
844
+ const raw = runSupabaseCommandCapture(["migration", "list", "--linked", "--output", "json"], dependencyManager);
845
+ const parsed = parseMigrationListOutput(raw);
846
+ const localSet = new Set(localVersions);
847
+ const remoteSet = new Set(parsed.remoteVersions);
848
+ const localOnlyVersions = localVersions.filter((version) => !remoteSet.has(version));
849
+ const remoteOnlyVersions = parsed.remoteVersions.filter((version) => !localSet.has(version));
850
+ const blocked = remoteOnlyVersions.length > 0;
851
+ printJson({
852
+ ok: true,
853
+ command: "base preflight-migration-state",
854
+ localVersions,
855
+ remoteVersions: parsed.remoteVersions,
856
+ localOnlyVersions,
857
+ remoteOnlyVersions,
858
+ blocked,
859
+ requiresUserDecision: blocked,
860
+ decisionOptions: blocked ? ["reset", "repair", "relink"] : ["continue"],
861
+ status: blocked ? "diverged" : "aligned_or_recoverable",
458
862
  });
459
863
  }
460
864
  async function supabaseAdminRequest(params) {
@@ -662,6 +1066,24 @@ function getDependencyInstallCommand(dependencyManager, packageName) {
662
1066
  return `yarn add -D ${packageName}`;
663
1067
  return `npm install --save-dev ${packageName}`;
664
1068
  }
1069
+ function buildChildProcessEnv(overrides = {}) {
1070
+ const env = {};
1071
+ for (const key of CHILD_PROCESS_ENV_KEYS) {
1072
+ const value = process.env[key];
1073
+ if (isNonEmptyString(value)) {
1074
+ env[key] = value;
1075
+ }
1076
+ }
1077
+ for (const [key, value] of Object.entries(process.env)) {
1078
+ if ((key.startsWith("FAKE_") || key.startsWith("MOCK_")) && isNonEmptyString(value)) {
1079
+ env[key] = value;
1080
+ }
1081
+ }
1082
+ return {
1083
+ ...env,
1084
+ ...overrides,
1085
+ };
1086
+ }
665
1087
  function runShellCommand(command, cwd) {
666
1088
  const result = spawnSync(process.env.SHELL || "/bin/zsh", ["-lc", command], {
667
1089
  cwd,
@@ -676,6 +1098,20 @@ function runShellCommand(command, cwd) {
676
1098
  }
677
1099
  return typeof result.stdout === "string" ? result.stdout.trim() : "";
678
1100
  }
1101
+ function runDirectCommand(executable, args, cwd, options) {
1102
+ const result = spawnSync(executable, args, {
1103
+ cwd,
1104
+ env: options.env,
1105
+ encoding: "utf8",
1106
+ });
1107
+ if (result.status !== 0) {
1108
+ const message = [result.stderr, result.stdout]
1109
+ .map((value) => (typeof value === "string" ? value.trim() : ""))
1110
+ .find(Boolean) || options.fallbackMessage;
1111
+ fail(options.failureCode, message);
1112
+ }
1113
+ return typeof result.stdout === "string" ? result.stdout.trim() : "";
1114
+ }
679
1115
  function runLinkedSupabaseCommand(command, cwd, failureCode, fallbackMessage) {
680
1116
  const result = spawnSync(process.env.SHELL || "/bin/zsh", ["-lc", command], {
681
1117
  cwd,
@@ -1125,11 +1561,17 @@ function ensureStripeLocalScripts(cwd, dependencyManager, localhostUrl) {
1125
1561
  scripts,
1126
1562
  };
1127
1563
  }
1128
- function waitForStripeListenSecret(command, cwd) {
1564
+ function waitForStripeListenSecret(secretKey, localhostUrl, cwd) {
1129
1565
  return new Promise((resolve, reject) => {
1130
- const child = spawn(process.env.SHELL || "/bin/zsh", ["-lc", command], {
1566
+ const child = spawn("stripe", [
1567
+ "listen",
1568
+ "--events",
1569
+ STRIPE_EVENTS.join(","),
1570
+ "--forward-to",
1571
+ `${localhostUrl}/api/webhooks/stripe`,
1572
+ ], {
1131
1573
  cwd,
1132
- env: process.env,
1574
+ env: buildChildProcessEnv({ STRIPE_API_KEY: secretKey }),
1133
1575
  stdio: ["ignore", "pipe", "pipe"],
1134
1576
  detached: true,
1135
1577
  });
@@ -1307,12 +1749,27 @@ function checkStripeCli() {
1307
1749
  }
1308
1750
  function loginStripeCli(flags) {
1309
1751
  const { values } = loadLocalEnv();
1310
- ensureStripeCliInstalled(process.cwd());
1752
+ const cwd = process.cwd();
1753
+ ensureStripeCliInstalled(cwd);
1311
1754
  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());
1755
+ if (authMode === "api_key") {
1756
+ const secretKey = requireEnvValue(values, "STRIPE_SECRET_KEY", ".env.local");
1757
+ if (!isStripeSecretKey(secretKey)) {
1758
+ fail("INVALID_PAYMENTS_ENV", "STRIPE_SECRET_KEY is missing or malformed in .env.local.");
1759
+ }
1760
+ runDirectCommand("stripe", ["get", "/v1/account"], cwd, {
1761
+ env: buildChildProcessEnv({ STRIPE_API_KEY: secretKey }),
1762
+ failureCode: "LOCAL_COMMAND_FAILED",
1763
+ fallbackMessage: "Stripe CLI could not authenticate using STRIPE_SECRET_KEY.",
1764
+ });
1765
+ }
1766
+ else {
1767
+ runDirectCommand("stripe", ["login"], cwd, {
1768
+ env: buildChildProcessEnv(),
1769
+ failureCode: "LOCAL_COMMAND_FAILED",
1770
+ fallbackMessage: "Stripe CLI login failed.",
1771
+ });
1772
+ }
1316
1773
  printJson({
1317
1774
  ok: true,
1318
1775
  command: "payments login-stripe-cli",
@@ -1432,10 +1889,9 @@ async function setupStripeLocalhost(flags) {
1432
1889
  }
1433
1890
  ensureStripeCliInstalled(cwd);
1434
1891
  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
1892
  let signingSecret = "";
1437
1893
  try {
1438
- signingSecret = await waitForStripeListenSecret(listenCommand, cwd);
1894
+ signingSecret = await waitForStripeListenSecret(secretKey.trim(), localhostUrl, cwd);
1439
1895
  }
1440
1896
  catch (error) {
1441
1897
  fail("STRIPE_LOCALHOST_SETUP_FAILED", error instanceof Error ? error.message : "Failed to capture Stripe localhost webhook signing secret.");
@@ -1602,6 +2058,10 @@ async function main() {
1602
2058
  ok: true,
1603
2059
  commands: [
1604
2060
  "read-setup-state",
2061
+ "base check-supabase-access-token",
2062
+ "base create-supabase-project",
2063
+ "base link-supabase-project",
2064
+ "base preflight-migration-state",
1605
2065
  "admin ensure-admin",
1606
2066
  "storage check-supabase-context",
1607
2067
  "storage setup-supabase",
@@ -1619,7 +2079,6 @@ async function main() {
1619
2079
  "configure-site-redirects",
1620
2080
  "configure-email-password",
1621
2081
  "enable-google-provider",
1622
- "apply-auth-templates",
1623
2082
  ],
1624
2083
  });
1625
2084
  return;
@@ -1632,6 +2091,14 @@ async function main() {
1632
2091
  }
1633
2092
  if (command === "read-setup-state")
1634
2093
  return readSetupState();
2094
+ if (command === "base" && subcommand === "check-supabase-access-token")
2095
+ return checkBaseSupabaseAccessToken();
2096
+ if (command === "base" && subcommand === "create-supabase-project")
2097
+ return createBaseSupabaseProject(flags);
2098
+ if (command === "base" && subcommand === "link-supabase-project")
2099
+ return linkBaseSupabaseProject(flags);
2100
+ if (command === "base" && subcommand === "preflight-migration-state")
2101
+ return preflightBaseMigrationState(flags);
1635
2102
  if (command === "admin" && subcommand === "ensure-admin")
1636
2103
  return ensureAdmin(flags);
1637
2104
  if (command === "storage" && subcommand === "check-supabase-context")
@@ -1666,8 +2133,6 @@ async function main() {
1666
2133
  return configureEmailPassword(flags);
1667
2134
  if (command === "enable-google-provider")
1668
2135
  return enableGoogleProvider(flags);
1669
- if (command === "apply-auth-templates")
1670
- return applyAuthTemplates(flags);
1671
2136
  fail("UNKNOWN_COMMAND", `Unknown command: ${command}`);
1672
2137
  }
1673
2138
  main().catch((error) => {
package/dist/storageS3.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import * as crypto from "node:crypto";
4
- import { S3Client, HeadBucketCommand, CreateBucketCommand, PutPublicAccessBlockCommand, PutBucketEncryptionCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketCorsCommand, GetBucketLocationCommand, GetPublicAccessBlockCommand, GetBucketEncryptionCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketCorsCommand, } from "@aws-sdk/client-s3";
4
+ import { S3Client, HeadBucketCommand, CreateBucketCommand, PutPublicAccessBlockCommand, PutBucketEncryptionCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketCorsCommand, DeleteBucketCorsCommand, GetBucketLocationCommand, GetPublicAccessBlockCommand, GetBucketEncryptionCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketCorsCommand, } from "@aws-sdk/client-s3";
5
5
  import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
6
6
  const SETUP_CONFIG_PATH = path.join(".vibecodemax", "setup-config.json");
7
7
  const DEFAULT_BUCKETS = {
@@ -98,6 +98,11 @@ function readStringFlag(flags, key) {
98
98
  return typeof value === "string" ? value.trim() : "";
99
99
  }
100
100
  function mergeEnvFile(filePath, nextValues) {
101
+ for (const [key, value] of Object.entries(nextValues)) {
102
+ if (/[\r\n]/.test(value)) {
103
+ fail("INVALID_ENV_VALUE", `${key} contains a newline and cannot be written safely to ${path.basename(filePath)}.`);
104
+ }
105
+ }
101
106
  const existing = parseDotEnv(readFileIfExists(filePath));
102
107
  const merged = { ...existing, ...nextValues };
103
108
  const keys = Object.keys(merged).sort();
@@ -287,6 +292,11 @@ function isRegionMismatchError(error) {
287
292
  const status = getAwsStatus(error);
288
293
  return status === 301 || code === "PermanentRedirect" || code === "AuthorizationHeaderMalformed" || code === "IncorrectEndpoint" || code === "IllegalLocationConstraintException";
289
294
  }
295
+ function isMissingCorsConfiguration(error) {
296
+ const code = getAwsCode(error);
297
+ const status = getAwsStatus(error);
298
+ return code === "NoSuchCORSConfiguration" || status === 404;
299
+ }
290
300
  async function ensureSingleBucket(s3Client, bucket, region) {
291
301
  let created = false;
292
302
  try {
@@ -366,16 +376,23 @@ async function applyBucketSafety(region, credentials, buckets) {
366
376
  }
367
377
  async function applyBucketCors(region, credentials, buckets) {
368
378
  const { s3Client } = createAwsClients(region, credentials);
369
- for (const bucket of [buckets.public, buckets.private]) {
370
- try {
371
- await s3Client.send(new PutBucketCorsCommand({
372
- Bucket: bucket,
373
- CORSConfiguration: BUCKET_CORS_CONFIGURATION,
374
- }));
375
- }
376
- catch (error) {
377
- const message = error instanceof Error ? error.message : `Failed to apply CORS to ${bucket}.`;
378
- fail("AWS_POLICY_APPLY_FAILED", message, 1, { bucket, awsCode: getAwsCode(error), awsStatus: getAwsStatus(error) });
379
+ try {
380
+ await s3Client.send(new PutBucketCorsCommand({
381
+ Bucket: buckets.public,
382
+ CORSConfiguration: BUCKET_CORS_CONFIGURATION,
383
+ }));
384
+ }
385
+ catch (error) {
386
+ const message = error instanceof Error ? error.message : `Failed to apply CORS to ${buckets.public}.`;
387
+ fail("AWS_POLICY_APPLY_FAILED", message, 1, { bucket: buckets.public, awsCode: getAwsCode(error), awsStatus: getAwsStatus(error) });
388
+ }
389
+ try {
390
+ await s3Client.send(new DeleteBucketCorsCommand({ Bucket: buckets.private }));
391
+ }
392
+ catch (error) {
393
+ if (!isMissingCorsConfiguration(error)) {
394
+ const message = error instanceof Error ? error.message : `Failed to remove CORS from ${buckets.private}.`;
395
+ fail("AWS_POLICY_APPLY_FAILED", message, 1, { bucket: buckets.private, awsCode: getAwsCode(error), awsStatus: getAwsStatus(error) });
379
396
  }
380
397
  }
381
398
  }
@@ -438,7 +455,16 @@ async function getBucketEvidence(s3Client, bucket, regionHint) {
438
455
  const encryptionResponse = await s3Client.send(new GetBucketEncryptionCommand({ Bucket: bucket }));
439
456
  const ownershipResponse = await s3Client.send(new GetBucketOwnershipControlsCommand({ Bucket: bucket }));
440
457
  const policyResponse = await s3Client.send(new GetBucketPolicyCommand({ Bucket: bucket }));
441
- const corsResponse = await s3Client.send(new GetBucketCorsCommand({ Bucket: bucket }));
458
+ let corsRules = [];
459
+ try {
460
+ const corsResponse = await s3Client.send(new GetBucketCorsCommand({ Bucket: bucket }));
461
+ corsRules = corsResponse.CORSRules || [];
462
+ }
463
+ catch (error) {
464
+ if (!isMissingCorsConfiguration(error)) {
465
+ throw error;
466
+ }
467
+ }
442
468
  const policyText = typeof policyResponse.Policy === "string" ? policyResponse.Policy : "";
443
469
  return {
444
470
  bucketRegion,
@@ -447,7 +473,7 @@ async function getBucketEvidence(s3Client, bucket, regionHint) {
447
473
  ownershipControls: ownershipResponse.OwnershipControls?.Rules?.[0]?.ObjectOwnership || null,
448
474
  policyApplied: Boolean(policyText),
449
475
  hasPublicReadPolicy: policyText.includes("\"Action\":\"s3:GetObject\"") || policyText.includes("\"Action\":[\"s3:GetObject\"]"),
450
- corsRules: corsResponse.CORSRules || [],
476
+ corsRules,
451
477
  };
452
478
  }
453
479
  async function verifyTwoBuckets(region, credentials, buckets) {
@@ -473,7 +499,7 @@ async function verifyTwoBuckets(region, credentials, buckets) {
473
499
  Boolean(privateEvidence.publicAccessBlock?.RestrictPublicBuckets) &&
474
500
  privateEvidence.policyApplied === true &&
475
501
  privateEvidence.hasPublicReadPolicy === false &&
476
- Array.isArray(privateEvidence.corsRules) && privateEvidence.corsRules.length > 0;
502
+ Array.isArray(privateEvidence.corsRules) && privateEvidence.corsRules.length === 0;
477
503
  if (!publicOk || !privateOk) {
478
504
  fail("AWS_VERIFY_FAILED", "AWS S3 bucket configuration verification failed.", 1, {
479
505
  publicBucket: buckets.public,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecodemax/cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "VibeCodeMax CLI — local provider setup for bootstrap and project configuration",
5
5
  "type": "module",
6
6
  "bin": {