@vibecodemax/cli 0.1.2 → 0.1.4

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 CHANGED
@@ -1,8 +1,39 @@
1
1
  #!/usr/bin/env node
2
2
  import * as fs from "node:fs";
3
+ import * as os from "node:os";
3
4
  import * as path from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import { checkS3Context, setupS3Storage, smokeTestS3 } from "./storageS3.js";
7
+ const SETUP_STATE_PATH = path.join(".vibecodemax", "setup-state.json");
4
8
  const MANAGEMENT_API_BASE = "https://api.supabase.com";
5
9
  const DEFAULT_LOCALHOST_URL = "http://localhost:3000";
10
+ const STORAGE_PUBLIC_BUCKET = "public-assets";
11
+ const STORAGE_PRIVATE_BUCKET = "private-uploads";
12
+ const STORAGE_REQUIRED_FILE_SIZE_LIMIT = 10 * 1024 * 1024;
13
+ const STORAGE_HEALTHCHECK_PREFIX = "_vibecodemax/healthcheck/default";
14
+ const STORAGE_MIME_TYPES_BY_CATEGORY = {
15
+ images: ["image/jpeg", "image/png", "image/webp", "image/gif"],
16
+ documents: [
17
+ "application/pdf",
18
+ "text/plain",
19
+ "text/csv",
20
+ "application/json",
21
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
22
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
23
+ ],
24
+ video: ["video/mp4", "video/webm", "video/quicktime"],
25
+ };
26
+ const STORAGE_DEFAULT_MIME_CATEGORIES = ["images"];
27
+ const STORAGE_REQUIRED_POLICY_NAMES = [
28
+ "public_assets_select_own",
29
+ "public_assets_insert_own",
30
+ "public_assets_update_own",
31
+ "public_assets_delete_own",
32
+ "private_uploads_select_own",
33
+ "private_uploads_insert_own",
34
+ "private_uploads_update_own",
35
+ "private_uploads_delete_own",
36
+ ];
6
37
  function printJson(value) {
7
38
  process.stdout.write(`${JSON.stringify(value)}\n`);
8
39
  }
@@ -12,8 +43,14 @@ function fail(code, message, exitCode = 1, extra = {}) {
12
43
  }
13
44
  function parseArgs(argv) {
14
45
  const [command, ...rest] = argv;
46
+ let subcommand;
15
47
  const flags = {};
16
- for (let index = 0; index < rest.length; index += 1) {
48
+ let index = 0;
49
+ if ((command === "admin" || command === "storage") && rest[0] && !rest[0].startsWith("--")) {
50
+ subcommand = rest[0];
51
+ index = 1;
52
+ }
53
+ for (; index < rest.length; index += 1) {
17
54
  const token = rest[index];
18
55
  if (!token.startsWith("--"))
19
56
  continue;
@@ -26,7 +63,7 @@ function parseArgs(argv) {
26
63
  flags[key] = next;
27
64
  index += 1;
28
65
  }
29
- return { command, flags };
66
+ return { command, subcommand, flags };
30
67
  }
31
68
  function readFileIfExists(filePath) {
32
69
  try {
@@ -141,6 +178,19 @@ function requireEnvValue(envValues, key, fileHint) {
141
178
  }
142
179
  return value.trim();
143
180
  }
181
+ function requireAuthAccessToken(envValues, fileHint) {
182
+ return requireEnvValue(envValues, "SUPABASE_ACCESS_TOKEN", fileHint);
183
+ }
184
+ function requireSupabaseUrl(envValues) {
185
+ const value = envValues.SUPABASE_URL || envValues.NEXT_PUBLIC_SUPABASE_URL;
186
+ if (!isNonEmptyString(value)) {
187
+ fail("MISSING_ENV", "SUPABASE_URL or NEXT_PUBLIC_SUPABASE_URL is missing. Add one of them to .env.local.");
188
+ }
189
+ return value.trim();
190
+ }
191
+ function requireServiceRoleKey(envValues) {
192
+ return requireEnvValue(envValues, "SUPABASE_SERVICE_ROLE_KEY", ".env.local");
193
+ }
144
194
  function mergeEnvFile(filePath, nextValues) {
145
195
  const existing = parseDotEnv(readFileIfExists(filePath));
146
196
  const merged = { ...existing, ...nextValues };
@@ -148,6 +198,25 @@ function mergeEnvFile(filePath, nextValues) {
148
198
  const content = `${keys.map((key) => `${key}=${merged[key]}`).join("\n")}\n`;
149
199
  fs.writeFileSync(filePath, content, "utf8");
150
200
  }
201
+ function readSetupState() {
202
+ const setupStatePath = path.join(process.cwd(), SETUP_STATE_PATH);
203
+ const content = readFileIfExists(setupStatePath).trim();
204
+ if (!content) {
205
+ printJson({ ok: true, setupState: {} });
206
+ return;
207
+ }
208
+ let parsed;
209
+ try {
210
+ parsed = JSON.parse(content);
211
+ }
212
+ catch {
213
+ fail("INVALID_SETUP_STATE", `${SETUP_STATE_PATH} is not valid JSON.`);
214
+ }
215
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
216
+ fail("INVALID_SETUP_STATE", `${SETUP_STATE_PATH} must contain a JSON object.`);
217
+ }
218
+ printJson({ ok: true, setupState: parsed });
219
+ }
151
220
  async function managementRequest({ method, projectRef, token, body }) {
152
221
  const endpoint = `${MANAGEMENT_API_BASE}/v1/projects/${projectRef}/config/auth`;
153
222
  const response = await fetch(endpoint, {
@@ -235,7 +304,7 @@ async function configureSiteRedirects(flags) {
235
304
  if (!projectRef) {
236
305
  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.");
237
306
  }
238
- const accessToken = requireEnvValue(values, "SUPABASE_ACCESS_TOKEN", path.basename(envBootstrapPath));
307
+ const accessToken = requireAuthAccessToken(values, path.basename(envBootstrapPath));
239
308
  const mode = readStringFlag(flags, "mode") === "production" ? "production" : "test";
240
309
  const productionDomain = mode === "production" ? normalizeBaseUrl(readStringFlag(flags, "production-domain")) : "";
241
310
  if (mode === "production" && !productionDomain) {
@@ -273,7 +342,7 @@ async function configureEmailPassword(flags) {
273
342
  if (!projectRef) {
274
343
  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.");
275
344
  }
276
- const accessToken = requireEnvValue(values, "SUPABASE_ACCESS_TOKEN", path.basename(envBootstrapPath));
345
+ const accessToken = requireAuthAccessToken(values, path.basename(envBootstrapPath));
277
346
  const emailConfirmation = readStringFlag(flags, "email-confirmation") !== "no";
278
347
  await managementRequest({
279
348
  method: "PATCH",
@@ -298,13 +367,13 @@ async function enableGoogleProvider(flags) {
298
367
  if (!projectRef) {
299
368
  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.");
300
369
  }
301
- const managementToken = requireEnvValue(values, "SUPABASE_MANAGEMENT_TOKEN", path.basename(envBootstrapPath));
370
+ const accessToken = requireAuthAccessToken(values, path.basename(envBootstrapPath));
302
371
  const googleClientId = requireEnvValue(values, "GOOGLE_CLIENT_ID", path.basename(envBootstrapPath));
303
372
  const googleClientSecret = requireEnvValue(values, "GOOGLE_CLIENT_SECRET", path.basename(envBootstrapPath));
304
373
  await managementRequest({
305
374
  method: "PATCH",
306
375
  projectRef,
307
- token: managementToken,
376
+ token: accessToken,
308
377
  body: {
309
378
  external_google_enabled: true,
310
379
  external_google_client_id: googleClientId,
@@ -325,7 +394,7 @@ async function applyAuthTemplates(flags) {
325
394
  if (!projectRef) {
326
395
  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.");
327
396
  }
328
- const managementToken = requireEnvValue(values, "SUPABASE_MANAGEMENT_TOKEN", path.basename(envBootstrapPath));
397
+ const accessToken = requireAuthAccessToken(values, path.basename(envBootstrapPath));
329
398
  const desiredFields = buildTemplateDesiredFields({
330
399
  confirmEmailEnabled: readStringFlag(flags, "confirm-email-enabled") === "true",
331
400
  confirmEmailSubject: readStringFlag(flags, "confirm-email-subject"),
@@ -334,11 +403,11 @@ async function applyAuthTemplates(flags) {
334
403
  resetPasswordSubject: readStringFlag(flags, "reset-password-subject"),
335
404
  resetPasswordPath: readStringFlag(flags, "reset-password-path"),
336
405
  });
337
- const current = await managementRequest({ method: "GET", projectRef, token: managementToken });
406
+ const current = await managementRequest({ method: "GET", projectRef, token: accessToken });
338
407
  const currentAuthConfig = readAuthConfigEnvelope(current);
339
408
  const diff = diffTemplateFields(currentAuthConfig, desiredFields);
340
409
  if (!diff.noChanges) {
341
- await managementRequest({ method: "PATCH", projectRef, token: managementToken, body: diff.patch });
410
+ await managementRequest({ method: "PATCH", projectRef, token: accessToken, body: diff.patch });
342
411
  }
343
412
  printJson({
344
413
  ok: true,
@@ -349,12 +418,534 @@ async function applyAuthTemplates(flags) {
349
418
  noChanges: diff.noChanges,
350
419
  });
351
420
  }
421
+ async function supabaseAdminRequest(params) {
422
+ const response = await fetch(`${params.supabaseUrl}${params.endpoint}`, {
423
+ method: params.method,
424
+ headers: {
425
+ apikey: params.serviceRoleKey,
426
+ Authorization: `Bearer ${params.serviceRoleKey}`,
427
+ "Content-Type": "application/json",
428
+ },
429
+ body: params.body ? JSON.stringify(params.body) : undefined,
430
+ });
431
+ let json = null;
432
+ try {
433
+ json = await response.json();
434
+ }
435
+ catch {
436
+ json = null;
437
+ }
438
+ if (!response.ok) {
439
+ const message = isNonEmptyString(json?.msg)
440
+ ? json.msg
441
+ : isNonEmptyString(json?.message)
442
+ ? json.message
443
+ : isNonEmptyString(json?.error)
444
+ ? json.error
445
+ : `Supabase returned ${response.status}`;
446
+ fail("SUPABASE_ADMIN_ERROR", message, 1, { status: response.status });
447
+ }
448
+ return json;
449
+ }
450
+ async function findUserByEmail(supabaseUrl, serviceRoleKey, email) {
451
+ const result = await supabaseAdminRequest({
452
+ supabaseUrl,
453
+ serviceRoleKey,
454
+ method: "GET",
455
+ endpoint: `/auth/v1/admin/users?email=${encodeURIComponent(email)}`,
456
+ });
457
+ const users = Array.isArray(result.users) ? result.users : [];
458
+ const user = users.find((candidate) => {
459
+ if (!candidate || typeof candidate !== "object")
460
+ return false;
461
+ const candidateEmail = candidate.email;
462
+ return typeof candidateEmail === "string" && candidateEmail.toLowerCase() === email.toLowerCase();
463
+ });
464
+ return {
465
+ exists: Boolean(user),
466
+ userId: user && typeof user.id === "string" ? user.id : null,
467
+ };
468
+ }
469
+ function generateTemporaryPassword() {
470
+ return `VcmAdmin!${Math.random().toString(36).slice(2, 10)}9aA`;
471
+ }
472
+ async function createAdminUser(supabaseUrl, serviceRoleKey, email, temporaryPassword) {
473
+ const result = await supabaseAdminRequest({
474
+ supabaseUrl,
475
+ serviceRoleKey,
476
+ method: "POST",
477
+ endpoint: "/auth/v1/admin/users",
478
+ body: {
479
+ email,
480
+ password: temporaryPassword,
481
+ email_confirm: true,
482
+ user_metadata: { name: "Admin User" },
483
+ },
484
+ });
485
+ const userId = typeof result.user?.id === "string"
486
+ ? result.user.id
487
+ : typeof result.id === "string"
488
+ ? result.id
489
+ : "";
490
+ if (!userId) {
491
+ fail("INVALID_RESPONSE", "Supabase did not return a user ID for the created admin user.");
492
+ }
493
+ return userId;
494
+ }
495
+ async function promoteAdminProfile(supabaseUrl, serviceRoleKey, userId, email) {
496
+ await supabaseAdminRequest({
497
+ supabaseUrl,
498
+ serviceRoleKey,
499
+ method: "POST",
500
+ endpoint: "/rest/v1/profiles?on_conflict=id",
501
+ body: [
502
+ {
503
+ id: userId,
504
+ email,
505
+ name: "Admin User",
506
+ role: "admin",
507
+ },
508
+ ],
509
+ });
510
+ }
511
+ async function verifyAdminProfile(supabaseUrl, serviceRoleKey, userId) {
512
+ const result = await supabaseAdminRequest({
513
+ supabaseUrl,
514
+ serviceRoleKey,
515
+ method: "GET",
516
+ endpoint: `/rest/v1/profiles?id=eq.${userId}&select=id,role`,
517
+ });
518
+ const rows = Array.isArray(result) ? result : [];
519
+ return rows.some((row) => row && typeof row === "object" && row.role === "admin");
520
+ }
521
+ async function ensureAdmin(flags) {
522
+ const { values } = loadLocalEnv();
523
+ const email = readStringFlag(flags, "email");
524
+ if (!email || !/^[^@]+@[^@]+\.[^@]+$/.test(email)) {
525
+ fail("INVALID_EMAIL", "A valid --email value is required.");
526
+ }
527
+ const supabaseUrl = requireSupabaseUrl(values);
528
+ const serviceRoleKey = requireServiceRoleKey(values);
529
+ const lookup = await findUserByEmail(supabaseUrl, serviceRoleKey, email);
530
+ let userId = lookup.userId;
531
+ let temporaryPassword = "";
532
+ let created = false;
533
+ if (!lookup.exists) {
534
+ temporaryPassword = generateTemporaryPassword();
535
+ userId = await createAdminUser(supabaseUrl, serviceRoleKey, email, temporaryPassword);
536
+ created = true;
537
+ }
538
+ if (!userId) {
539
+ fail("INVALID_RESPONSE", "Resolved admin user ID is missing after ensure-admin.");
540
+ }
541
+ await promoteAdminProfile(supabaseUrl, serviceRoleKey, userId, email);
542
+ const verified = await verifyAdminProfile(supabaseUrl, serviceRoleKey, userId);
543
+ if (!verified) {
544
+ fail("ADMIN_VERIFY_FAILED", "The admin role could not be verified after promotion.");
545
+ }
546
+ const result = {
547
+ ok: true,
548
+ command: "admin ensure-admin",
549
+ email,
550
+ userId,
551
+ userExists: lookup.exists,
552
+ created,
553
+ promoted: true,
554
+ verified: true,
555
+ defaultName: "Admin User",
556
+ applied: ["auth_user", "admin_profile"],
557
+ };
558
+ if (temporaryPassword) {
559
+ result.temporaryPassword = temporaryPassword;
560
+ }
561
+ printJson(result);
562
+ }
563
+ function shellQuote(value) {
564
+ const text = String(value);
565
+ return `'${text.replace(/'/g, `"'"'`)}'`;
566
+ }
567
+ function normalizeDependencyManagerName(value) {
568
+ if (value === "pnpm")
569
+ return "pnpm";
570
+ if (value === "yarn")
571
+ return "yarn";
572
+ return "npm";
573
+ }
574
+ function detectDependencyManager(cwd, flags) {
575
+ const explicitRaw = readStringFlag(flags, "dependency-manager");
576
+ if (explicitRaw) {
577
+ return normalizeDependencyManagerName(explicitRaw);
578
+ }
579
+ const packageJsonPath = path.join(cwd, "package.json");
580
+ try {
581
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
582
+ if (isNonEmptyString(packageJson.packageManager)) {
583
+ const manager = packageJson.packageManager.split("@")[0];
584
+ if (manager === "pnpm" || manager === "yarn")
585
+ return manager;
586
+ if (manager === "npm")
587
+ return "npm";
588
+ }
589
+ }
590
+ catch {
591
+ // ignore and fall back to lockfile detection
592
+ }
593
+ if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml")))
594
+ return "pnpm";
595
+ if (fs.existsSync(path.join(cwd, "yarn.lock")))
596
+ return "yarn";
597
+ return "npm";
598
+ }
599
+ function getSupabaseRunner(dependencyManager) {
600
+ if (dependencyManager === "pnpm")
601
+ return "pnpm exec supabase";
602
+ if (dependencyManager === "yarn")
603
+ return "yarn supabase";
604
+ return "npx supabase";
605
+ }
606
+ function runShellCommand(command, cwd) {
607
+ const result = spawnSync(process.env.SHELL || "/bin/zsh", ["-lc", command], {
608
+ cwd,
609
+ env: process.env,
610
+ encoding: "utf8",
611
+ });
612
+ if (result.status !== 0) {
613
+ const message = [result.stderr, result.stdout]
614
+ .map((value) => (typeof value === "string" ? value.trim() : ""))
615
+ .find(Boolean) || `Command failed: ${command}`;
616
+ fail("LOCAL_COMMAND_FAILED", message);
617
+ }
618
+ return typeof result.stdout === "string" ? result.stdout.trim() : "";
619
+ }
620
+ function normalizeStorageMimeCategories(rawValue) {
621
+ const requested = rawValue
622
+ .split(",")
623
+ .map((value) => value.trim())
624
+ .filter(Boolean);
625
+ const selected = requested.filter((value) => Object.prototype.hasOwnProperty.call(STORAGE_MIME_TYPES_BY_CATEGORY, value));
626
+ return selected.length > 0 ? [...new Set(selected)] : [...STORAGE_DEFAULT_MIME_CATEGORIES];
627
+ }
628
+ function resolveStorageMimeCategories(flags) {
629
+ const raw = readStringFlag(flags, "mime-categories");
630
+ if (!raw)
631
+ return [...STORAGE_DEFAULT_MIME_CATEGORIES];
632
+ return normalizeStorageMimeCategories(raw);
633
+ }
634
+ function expandStorageMimeCategories(categories) {
635
+ const mimeTypes = [];
636
+ for (const category of categories) {
637
+ const mapped = STORAGE_MIME_TYPES_BY_CATEGORY[category] || [];
638
+ for (const mimeType of mapped) {
639
+ if (!mimeTypes.includes(mimeType)) {
640
+ mimeTypes.push(mimeType);
641
+ }
642
+ }
643
+ }
644
+ return mimeTypes;
645
+ }
646
+ function parseStorageMigrationFilename(filename) {
647
+ const doubleUnderscore = filename.match(/^(\d{14})__([a-z0-9_]+)\.sql$/);
648
+ if (doubleUnderscore) {
649
+ return { filename, timestamp: doubleUnderscore[1], scope: doubleUnderscore[2] };
650
+ }
651
+ const singleUnderscore = filename.match(/^(\d{14})_([a-z0-9_]+)\.sql$/);
652
+ if (singleUnderscore) {
653
+ return { filename, timestamp: singleUnderscore[1], scope: singleUnderscore[2] };
654
+ }
655
+ return null;
656
+ }
657
+ function discoverStorageMigrationFiles(cwd) {
658
+ const migrationsRoot = path.join(cwd, "supabase", "migrations");
659
+ if (!fs.existsSync(migrationsRoot)) {
660
+ fail("MISSING_STORAGE_MIGRATIONS", "supabase/migrations is missing. Run bootstrap.base first so the local Supabase project is initialized.");
661
+ }
662
+ const selected = fs.readdirSync(migrationsRoot)
663
+ .map((filename) => parseStorageMigrationFilename(filename))
664
+ .filter((entry) => entry !== null && entry.scope.startsWith("storage_"))
665
+ .sort((a, b) => a.filename.localeCompare(b.filename));
666
+ if (selected.length === 0) {
667
+ fail("MISSING_STORAGE_MIGRATIONS", "No storage migration files were found. Add a migration matching YYYYMMDDHHMMSS_storage_*.sql in supabase/migrations.");
668
+ }
669
+ const policyFiles = selected
670
+ .filter((entry) => /(^|_)storage_policies\.sql$/.test(entry.filename))
671
+ .map((entry) => entry.filename);
672
+ if (policyFiles.length === 0) {
673
+ fail("MISSING_STORAGE_POLICY_MIGRATION", "No storage policy migration file was found. Add a migration ending in storage_policies.sql in supabase/migrations.");
674
+ }
675
+ return {
676
+ files: selected.map((entry) => entry.filename),
677
+ policyFiles,
678
+ };
679
+ }
680
+ async function storageRequest(params) {
681
+ const response = await fetch(`${params.supabaseUrl}${params.endpoint}`, {
682
+ method: params.method,
683
+ headers: {
684
+ apikey: params.serviceRoleKey,
685
+ Authorization: `Bearer ${params.serviceRoleKey}`,
686
+ ...(params.contentType ? { "Content-Type": params.contentType } : {}),
687
+ },
688
+ body: params.rawBody ?? (params.body ? JSON.stringify(params.body) : undefined),
689
+ });
690
+ let json = null;
691
+ try {
692
+ json = await response.json();
693
+ }
694
+ catch {
695
+ json = null;
696
+ }
697
+ if (!response.ok) {
698
+ const message = isNonEmptyString(json?.error)
699
+ ? json.error
700
+ : isNonEmptyString(json?.message)
701
+ ? json.message
702
+ : isNonEmptyString(json?.msg)
703
+ ? json.msg
704
+ : `Supabase returned ${response.status}`;
705
+ fail("SUPABASE_STORAGE_ERROR", message, 1, { status: response.status });
706
+ }
707
+ return json;
708
+ }
709
+ function normalizeBucket(data = {}) {
710
+ const allowedMimeTypes = Array.isArray(data.allowed_mime_types)
711
+ ? data.allowed_mime_types
712
+ : Array.isArray(data.allowedMimeTypes)
713
+ ? data.allowedMimeTypes
714
+ : [];
715
+ const fileSizeLimit = Number.isFinite(data.file_size_limit)
716
+ ? Number(data.file_size_limit)
717
+ : Number.isFinite(data.fileSizeLimit)
718
+ ? Number(data.fileSizeLimit)
719
+ : null;
720
+ return {
721
+ id: isNonEmptyString(data.id) ? data.id : isNonEmptyString(data.name) ? String(data.name) : null,
722
+ public: Boolean(data.public),
723
+ allowedMimeTypes: [...allowedMimeTypes].filter((value) => typeof value === "string").sort(),
724
+ fileSizeLimit,
725
+ };
726
+ }
727
+ function compareBucketConfig(observed, expected) {
728
+ const expectedMimes = [...expected.allowedMimeTypes].sort();
729
+ const observedMimes = [...observed.allowedMimeTypes].sort();
730
+ return {
731
+ publicMatches: observed.public === expected.public,
732
+ mimeMatches: JSON.stringify(observedMimes) === JSON.stringify(expectedMimes),
733
+ fileSizeMatches: observed.fileSizeLimit === expected.fileSizeLimit,
734
+ };
735
+ }
736
+ async function readStorageBucket(supabaseUrl, serviceRoleKey, bucketId) {
737
+ const response = await fetch(`${supabaseUrl}/storage/v1/bucket/${bucketId}`, {
738
+ method: "GET",
739
+ headers: {
740
+ apikey: serviceRoleKey,
741
+ Authorization: `Bearer ${serviceRoleKey}`,
742
+ Accept: "application/json",
743
+ },
744
+ });
745
+ let json = null;
746
+ try {
747
+ json = await response.json();
748
+ }
749
+ catch {
750
+ json = null;
751
+ }
752
+ if (response.status === 404)
753
+ return null;
754
+ if (!response.ok) {
755
+ const message = isNonEmptyString(json?.error)
756
+ ? json.error
757
+ : isNonEmptyString(json?.message)
758
+ ? json.message
759
+ : isNonEmptyString(json?.msg)
760
+ ? json.msg
761
+ : `Supabase returned ${response.status}`;
762
+ fail("SUPABASE_STORAGE_ERROR", message, 1, { status: response.status });
763
+ }
764
+ return json && typeof json === "object" ? json : {};
765
+ }
766
+ async function ensureStorageBucket(supabaseUrl, serviceRoleKey, bucketId, isPublic, allowedMimeTypes) {
767
+ const existing = await readStorageBucket(supabaseUrl, serviceRoleKey, bucketId);
768
+ const endpoint = existing ? `/storage/v1/bucket/${bucketId}` : "/storage/v1/bucket";
769
+ const method = existing ? "PUT" : "POST";
770
+ const body = existing
771
+ ? {
772
+ public: isPublic,
773
+ allowed_mime_types: allowedMimeTypes,
774
+ file_size_limit: STORAGE_REQUIRED_FILE_SIZE_LIMIT,
775
+ }
776
+ : {
777
+ id: bucketId,
778
+ name: bucketId,
779
+ public: isPublic,
780
+ allowed_mime_types: allowedMimeTypes,
781
+ file_size_limit: STORAGE_REQUIRED_FILE_SIZE_LIMIT,
782
+ };
783
+ await storageRequest({
784
+ supabaseUrl,
785
+ serviceRoleKey,
786
+ method,
787
+ endpoint,
788
+ body,
789
+ contentType: "application/json",
790
+ });
791
+ const observedRaw = await readStorageBucket(supabaseUrl, serviceRoleKey, bucketId);
792
+ if (!observedRaw) {
793
+ fail("SUPABASE_STORAGE_ERROR", `Bucket ${bucketId} could not be read after configuration.`);
794
+ }
795
+ const observed = normalizeBucket(observedRaw);
796
+ const expected = {
797
+ public: isPublic,
798
+ allowedMimeTypes,
799
+ fileSizeLimit: STORAGE_REQUIRED_FILE_SIZE_LIMIT,
800
+ };
801
+ const comparison = compareBucketConfig(observed, expected);
802
+ if (!comparison.publicMatches || !comparison.mimeMatches || !comparison.fileSizeMatches) {
803
+ fail("SUPABASE_BUCKET_VERIFY_FAILED", `Bucket ${bucketId} does not match the required configuration.`);
804
+ }
805
+ return {
806
+ bucket: bucketId,
807
+ created: !existing,
808
+ updated: Boolean(existing),
809
+ verified: true,
810
+ };
811
+ }
812
+ function verifyRequiredStoragePolicies(dumpContent) {
813
+ const normalized = dumpContent.toLowerCase();
814
+ const missing = STORAGE_REQUIRED_POLICY_NAMES.filter((policyName) => !normalized.includes(`create policy \"${policyName}\"`));
815
+ if (missing.length > 0) {
816
+ fail("STORAGE_POLICY_VERIFY_FAILED", `Missing required storage policies after local Supabase CLI apply: ${missing.join(", ")}.`);
817
+ }
818
+ }
819
+ function buildStorageHealthcheckObjectPath(runId) {
820
+ return `${STORAGE_HEALTHCHECK_PREFIX}/${runId}/upload.txt`;
821
+ }
822
+ async function checkSupabaseContext() {
823
+ const { values, envLocalPath } = loadLocalEnv();
824
+ const missingKeys = [];
825
+ const presentKeys = [];
826
+ const supabaseUrl = values.SUPABASE_URL || values.NEXT_PUBLIC_SUPABASE_URL;
827
+ if (isNonEmptyString(supabaseUrl)) {
828
+ presentKeys.push(isNonEmptyString(values.SUPABASE_URL) ? "SUPABASE_URL" : "NEXT_PUBLIC_SUPABASE_URL");
829
+ }
830
+ else {
831
+ missingKeys.push("NEXT_PUBLIC_SUPABASE_URL");
832
+ }
833
+ if (isNonEmptyString(values.SUPABASE_SERVICE_ROLE_KEY)) {
834
+ presentKeys.push("SUPABASE_SERVICE_ROLE_KEY");
835
+ }
836
+ else {
837
+ missingKeys.push("SUPABASE_SERVICE_ROLE_KEY");
838
+ }
839
+ printJson({
840
+ ok: true,
841
+ command: "storage check-supabase-context",
842
+ ready: missingKeys.length === 0,
843
+ missingKeys,
844
+ presentKeys,
845
+ expectedExisting: true,
846
+ checkedFiles: [path.basename(envLocalPath)],
847
+ projectRef: resolveProjectRef({}, values) || null,
848
+ });
849
+ }
850
+ async function setupSupabaseStorage(flags) {
851
+ const cwd = process.cwd();
852
+ const { envLocalPath, values } = loadLocalEnv();
853
+ const supabaseUrl = requireSupabaseUrl(values);
854
+ const serviceRoleKey = requireServiceRoleKey(values);
855
+ const dependencyManager = detectDependencyManager(cwd, flags);
856
+ const supabaseRunner = getSupabaseRunner(dependencyManager);
857
+ const mimeCategories = resolveStorageMimeCategories(flags);
858
+ const allowedMimeTypes = expandStorageMimeCategories(mimeCategories);
859
+ const publicBucket = await ensureStorageBucket(supabaseUrl, serviceRoleKey, STORAGE_PUBLIC_BUCKET, true, allowedMimeTypes);
860
+ const privateBucket = await ensureStorageBucket(supabaseUrl, serviceRoleKey, STORAGE_PRIVATE_BUCKET, false, allowedMimeTypes);
861
+ mergeEnvFile(envLocalPath, {
862
+ SUPABASE_PUBLIC_BUCKET: STORAGE_PUBLIC_BUCKET,
863
+ SUPABASE_PRIVATE_BUCKET: STORAGE_PRIVATE_BUCKET,
864
+ });
865
+ const migrations = discoverStorageMigrationFiles(cwd);
866
+ runShellCommand(`${supabaseRunner} db push --linked`, cwd);
867
+ const dumpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vibecodemax-storage-dump-"));
868
+ const dumpPath = path.join(dumpDir, "storage.sql");
869
+ runShellCommand(`${supabaseRunner} db dump --linked --schema storage -f ${shellQuote(dumpPath)} >/dev/null`, cwd);
870
+ const dumpContent = readFileIfExists(dumpPath);
871
+ verifyRequiredStoragePolicies(dumpContent);
872
+ fs.rmSync(dumpDir, { recursive: true, force: true });
873
+ printJson({
874
+ ok: true,
875
+ command: "storage setup-supabase",
876
+ dependencyManager,
877
+ publicBucket: STORAGE_PUBLIC_BUCKET,
878
+ privateBucket: STORAGE_PRIVATE_BUCKET,
879
+ bucketsConfigured: [publicBucket, privateBucket],
880
+ mimeCategories,
881
+ allowedMimeTypes,
882
+ fileSizeLimit: STORAGE_REQUIRED_FILE_SIZE_LIMIT,
883
+ migrationFiles: migrations.files,
884
+ policyFiles: migrations.policyFiles,
885
+ policyMigrationsDiscovered: true,
886
+ policyMigrationsApplied: true,
887
+ policiesVerified: true,
888
+ envWritten: ["SUPABASE_PUBLIC_BUCKET", "SUPABASE_PRIVATE_BUCKET"],
889
+ });
890
+ }
891
+ async function smokeTestSupabase() {
892
+ const { values } = loadLocalEnv();
893
+ const supabaseUrl = requireSupabaseUrl(values);
894
+ const serviceRoleKey = requireServiceRoleKey(values);
895
+ const runId = `run_${Date.now()}`;
896
+ const objectPath = buildStorageHealthcheckObjectPath(runId);
897
+ const prefix = `${STORAGE_HEALTHCHECK_PREFIX}/${runId}/`;
898
+ await storageRequest({
899
+ supabaseUrl,
900
+ serviceRoleKey,
901
+ method: "POST",
902
+ endpoint: `/storage/v1/object/${STORAGE_PUBLIC_BUCKET}/${objectPath}`,
903
+ rawBody: "healthcheck probe",
904
+ contentType: "text/plain",
905
+ });
906
+ const listResult = await storageRequest({
907
+ supabaseUrl,
908
+ serviceRoleKey,
909
+ method: "POST",
910
+ endpoint: `/storage/v1/object/list/${STORAGE_PUBLIC_BUCKET}`,
911
+ body: { prefix },
912
+ contentType: "application/json",
913
+ });
914
+ await storageRequest({
915
+ supabaseUrl,
916
+ serviceRoleKey,
917
+ method: "DELETE",
918
+ endpoint: `/storage/v1/object/${STORAGE_PUBLIC_BUCKET}`,
919
+ body: { prefixes: [objectPath] },
920
+ contentType: "application/json",
921
+ });
922
+ printJson({
923
+ ok: true,
924
+ command: "storage smoke-test-supabase",
925
+ runId,
926
+ bucketId: STORAGE_PUBLIC_BUCKET,
927
+ objectPath,
928
+ prefix,
929
+ upload: true,
930
+ list: true,
931
+ delete: true,
932
+ listedItems: Array.isArray(listResult) ? listResult.length : 0,
933
+ });
934
+ }
352
935
  async function main() {
353
- const { command, flags } = parseArgs(process.argv.slice(2));
936
+ const { command, subcommand, flags } = parseArgs(process.argv.slice(2));
354
937
  if (!command || command === "--help" || command === "help") {
355
938
  printJson({
356
939
  ok: true,
357
940
  commands: [
941
+ "read-setup-state",
942
+ "admin ensure-admin",
943
+ "storage check-supabase-context",
944
+ "storage setup-supabase",
945
+ "storage smoke-test-supabase",
946
+ "storage check-s3-context",
947
+ "storage setup-s3",
948
+ "storage smoke-test-s3",
358
949
  "configure-site-redirects",
359
950
  "configure-email-password",
360
951
  "enable-google-provider",
@@ -363,6 +954,22 @@ async function main() {
363
954
  });
364
955
  return;
365
956
  }
957
+ if (command === "read-setup-state")
958
+ return readSetupState();
959
+ if (command === "admin" && subcommand === "ensure-admin")
960
+ return ensureAdmin(flags);
961
+ if (command === "storage" && subcommand === "check-supabase-context")
962
+ return checkSupabaseContext();
963
+ if (command === "storage" && subcommand === "setup-supabase")
964
+ return setupSupabaseStorage(flags);
965
+ if (command === "storage" && subcommand === "smoke-test-supabase")
966
+ return smokeTestSupabase();
967
+ if (command === "storage" && subcommand === "check-s3-context")
968
+ return checkS3Context(flags);
969
+ if (command === "storage" && subcommand === "setup-s3")
970
+ return setupS3Storage(flags);
971
+ if (command === "storage" && subcommand === "smoke-test-s3")
972
+ return smokeTestS3(flags);
366
973
  if (command === "configure-site-redirects")
367
974
  return configureSiteRedirects(flags);
368
975
  if (command === "configure-email-password")