@vibecodemax/cli 0.1.6 → 0.1.7

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
@@ -2,14 +2,14 @@
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
- import { checkS3Context, setupS3Storage, smokeTestS3 } from "./storageS3.js";
5
+ import { checkS3Context, setupS3Storage } from "./storageS3.js";
6
6
  const SETUP_STATE_PATH = path.join(".vibecodemax", "setup-state.json");
7
+ const SETUP_CONFIG_PATH = path.join(".vibecodemax", "setup-config.json");
7
8
  const MANAGEMENT_API_BASE = "https://api.supabase.com";
8
9
  const DEFAULT_LOCALHOST_URL = "http://localhost:3000";
9
10
  const STORAGE_PUBLIC_BUCKET = "public-assets";
10
11
  const STORAGE_PRIVATE_BUCKET = "private-uploads";
11
12
  const STORAGE_REQUIRED_FILE_SIZE_LIMIT = 10 * 1024 * 1024;
12
- const STORAGE_HEALTHCHECK_PREFIX = "_vibecodemax/healthcheck/default";
13
13
  const STORAGE_MIME_TYPES_BY_CATEGORY = {
14
14
  images: ["image/jpeg", "image/png", "image/webp", "image/gif"],
15
15
  documents: [
@@ -23,16 +23,6 @@ const STORAGE_MIME_TYPES_BY_CATEGORY = {
23
23
  video: ["video/mp4", "video/webm", "video/quicktime"],
24
24
  };
25
25
  const STORAGE_DEFAULT_MIME_CATEGORIES = ["images"];
26
- const STORAGE_REQUIRED_POLICY_NAMES = [
27
- "public_assets_select_own",
28
- "public_assets_insert_own",
29
- "public_assets_update_own",
30
- "public_assets_delete_own",
31
- "private_uploads_select_own",
32
- "private_uploads_insert_own",
33
- "private_uploads_update_own",
34
- "private_uploads_delete_own",
35
- ];
36
26
  function printJson(value) {
37
27
  process.stdout.write(`${JSON.stringify(value)}\n`);
38
28
  }
@@ -114,6 +104,18 @@ function loadLocalEnv(cwd = process.cwd()) {
114
104
  },
115
105
  };
116
106
  }
107
+ function readSetupConfig(cwd = process.cwd()) {
108
+ const raw = readFileIfExists(path.join(cwd, SETUP_CONFIG_PATH)).trim();
109
+ if (!raw)
110
+ return {};
111
+ try {
112
+ const parsed = JSON.parse(raw);
113
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
114
+ }
115
+ catch {
116
+ return {};
117
+ }
118
+ }
117
119
  function isNonEmptyString(value) {
118
120
  return typeof value === "string" && value.trim().length > 0;
119
121
  }
@@ -656,6 +658,44 @@ function runLinkedSupabaseCommand(command, cwd, failureCode, fallbackMessage) {
656
658
  }
657
659
  return typeof result.stdout === "string" ? result.stdout.trim() : "";
658
660
  }
661
+ function isMigrationOrderingConflict(message) {
662
+ return /Found local migration files to be inserted before the last migration on remote database\./i.test(message);
663
+ }
664
+ function runStorageMigrationPush(commandBase, cwd) {
665
+ const firstResult = spawnSync(process.env.SHELL || "/bin/zsh", ["-lc", commandBase], {
666
+ cwd,
667
+ env: process.env,
668
+ encoding: "utf8",
669
+ });
670
+ if (firstResult.status === 0) {
671
+ return {
672
+ stdout: typeof firstResult.stdout === "string" ? firstResult.stdout.trim() : "",
673
+ includeAll: false,
674
+ };
675
+ }
676
+ const firstMessage = [firstResult.stderr, firstResult.stdout]
677
+ .map((value) => (typeof value === "string" ? value.trim() : ""))
678
+ .find(Boolean) || "Failed to apply storage migrations to the linked Supabase project.";
679
+ if (!isMigrationOrderingConflict(firstMessage)) {
680
+ fail("STORAGE_POLICY_MIGRATION_APPLY_FAILED", firstMessage);
681
+ }
682
+ const retryCommand = `${commandBase} --include-all`;
683
+ const retryResult = spawnSync(process.env.SHELL || "/bin/zsh", ["-lc", retryCommand], {
684
+ cwd,
685
+ env: process.env,
686
+ encoding: "utf8",
687
+ });
688
+ if (retryResult.status !== 0) {
689
+ const retryMessage = [retryResult.stderr, retryResult.stdout]
690
+ .map((value) => (typeof value === "string" ? value.trim() : ""))
691
+ .find(Boolean) || "Failed to apply storage migrations to the linked Supabase project.";
692
+ fail("STORAGE_POLICY_MIGRATION_APPLY_FAILED", retryMessage);
693
+ }
694
+ return {
695
+ stdout: typeof retryResult.stdout === "string" ? retryResult.stdout.trim() : "",
696
+ includeAll: true,
697
+ };
698
+ }
659
699
  function normalizeStorageMimeCategories(rawValue) {
660
700
  const requested = rawValue
661
701
  .split(",")
@@ -664,11 +704,18 @@ function normalizeStorageMimeCategories(rawValue) {
664
704
  const selected = requested.filter((value) => Object.prototype.hasOwnProperty.call(STORAGE_MIME_TYPES_BY_CATEGORY, value));
665
705
  return selected.length > 0 ? [...new Set(selected)] : [...STORAGE_DEFAULT_MIME_CATEGORIES];
666
706
  }
667
- function resolveStorageMimeCategories(flags) {
668
- const raw = readStringFlag(flags, "mime-categories");
669
- if (!raw)
707
+ function resolveStorageMimeCategories(cwd) {
708
+ const setupConfig = readSetupConfig(cwd);
709
+ const storageConfig = setupConfig.storage && typeof setupConfig.storage === "object"
710
+ ? setupConfig.storage
711
+ : {};
712
+ const rawCategories = Array.isArray(storageConfig.mimeCategories)
713
+ ? storageConfig.mimeCategories.filter((value) => isNonEmptyString(value))
714
+ : [];
715
+ if (rawCategories.length === 0) {
670
716
  return [...STORAGE_DEFAULT_MIME_CATEGORIES];
671
- return normalizeStorageMimeCategories(raw);
717
+ }
718
+ return normalizeStorageMimeCategories(rawCategories.join(","));
672
719
  }
673
720
  function expandStorageMimeCategories(categories) {
674
721
  const mimeTypes = [];
@@ -694,28 +741,47 @@ function parseStorageMigrationFilename(filename) {
694
741
  return null;
695
742
  }
696
743
  function discoverStorageMigrationFiles(cwd) {
697
- const migrationsRoot = path.join(cwd, "supabase", "migrations");
698
- if (!fs.existsSync(migrationsRoot)) {
744
+ const storageMigrationsRoot = path.join(cwd, "supabase", "storage-migrations");
745
+ const activeMigrationsRoot = path.join(cwd, "supabase", "migrations");
746
+ if (!fs.existsSync(activeMigrationsRoot)) {
699
747
  fail("MISSING_STORAGE_MIGRATIONS", "supabase/migrations is missing. Run bootstrap.base first so the local Supabase project is initialized.");
700
748
  }
701
- const selected = fs.readdirSync(migrationsRoot)
749
+ if (!fs.existsSync(storageMigrationsRoot)) {
750
+ fail("MISSING_STORAGE_MIGRATIONS", "supabase/storage-migrations is missing. Regenerate the project so storage-owned migrations are available locally.");
751
+ }
752
+ const policyFiles = fs.readdirSync(storageMigrationsRoot)
702
753
  .map((filename) => parseStorageMigrationFilename(filename))
703
- .filter((entry) => entry !== null && entry.scope.startsWith("storage_"))
754
+ .filter((entry) => entry !== null && /(^|_)storage_policies$/.test(entry.scope))
704
755
  .sort((a, b) => a.filename.localeCompare(b.filename));
705
- if (selected.length === 0) {
706
- fail("MISSING_STORAGE_MIGRATIONS", "No storage migration files were found. Add a migration matching YYYYMMDDHHMMSS_storage_*.sql in supabase/migrations.");
707
- }
708
- const policyFiles = selected
709
- .filter((entry) => /(^|_)storage_policies\.sql$/.test(entry.filename))
710
- .map((entry) => entry.filename);
711
756
  if (policyFiles.length === 0) {
712
- fail("MISSING_STORAGE_POLICY_MIGRATION", "No storage policy migration file was found. Add a migration ending in storage_policies.sql in supabase/migrations.");
757
+ fail("MISSING_STORAGE_POLICY_MIGRATION", "No storage policy migration file was found in supabase/storage-migrations. This directory is for storage policy SQL only. Buckets are created by storage bootstrap through the Supabase Storage API. Regenerate the project or add a file ending in _storage_policies.sql.");
713
758
  }
714
759
  return {
715
- files: selected.map((entry) => entry.filename),
716
- policyFiles,
760
+ files: policyFiles.map((entry) => entry.filename),
761
+ policyFiles: policyFiles.map((entry) => entry.filename),
717
762
  };
718
763
  }
764
+ function materializeStorageMigrationFiles(cwd, files) {
765
+ const storageMigrationsRoot = path.join(cwd, "supabase", "storage-migrations");
766
+ const activeMigrationsRoot = path.join(cwd, "supabase", "migrations");
767
+ const preparedFiles = [];
768
+ for (const filename of files) {
769
+ const sourcePath = path.join(storageMigrationsRoot, filename);
770
+ const targetPath = path.join(activeMigrationsRoot, filename);
771
+ const sourceSql = fs.readFileSync(sourcePath, "utf8");
772
+ if (fs.existsSync(targetPath)) {
773
+ const existingSql = fs.readFileSync(targetPath, "utf8");
774
+ if (existingSql !== sourceSql) {
775
+ fail("STORAGE_MIGRATION_CONFLICT", `Active migration ${filename} does not match supabase/storage-migrations/${filename}.`);
776
+ }
777
+ }
778
+ else {
779
+ fs.copyFileSync(sourcePath, targetPath);
780
+ }
781
+ preparedFiles.push(filename);
782
+ }
783
+ return preparedFiles;
784
+ }
719
785
  async function storageRequest(params) {
720
786
  const response = await fetch(`${params.supabaseUrl}${params.endpoint}`, {
721
787
  method: params.method,
@@ -842,9 +908,6 @@ async function ensureStorageBucket(supabaseUrl, serviceRoleKey, bucketId, isPubl
842
908
  verified: true,
843
909
  };
844
910
  }
845
- function buildStorageHealthcheckObjectPath(runId) {
846
- return `${STORAGE_HEALTHCHECK_PREFIX}/${runId}/upload.txt`;
847
- }
848
911
  async function checkSupabaseContext() {
849
912
  const { values, envLocalPath } = loadLocalEnv();
850
913
  const missingKeys = [];
@@ -880,10 +943,11 @@ async function setupSupabaseStorage(flags) {
880
943
  const serviceRoleKey = requireServiceRoleKey(values);
881
944
  const dependencyManager = detectDependencyManager(cwd, flags);
882
945
  const supabaseRunner = getSupabaseRunner(dependencyManager);
883
- const mimeCategories = resolveStorageMimeCategories(flags);
946
+ const mimeCategories = resolveStorageMimeCategories(cwd);
884
947
  const allowedMimeTypes = expandStorageMimeCategories(mimeCategories);
885
948
  const migrations = discoverStorageMigrationFiles(cwd);
886
- runLinkedSupabaseCommand(`${supabaseRunner} db push --linked`, cwd, "STORAGE_POLICY_MIGRATION_APPLY_FAILED", "Failed to apply storage migrations to the linked Supabase project.");
949
+ const preparedMigrationFiles = materializeStorageMigrationFiles(cwd, migrations.files);
950
+ const migrationPush = runStorageMigrationPush(`${supabaseRunner} db push --linked`, cwd);
887
951
  const publicBucket = await ensureStorageBucket(supabaseUrl, serviceRoleKey, STORAGE_PUBLIC_BUCKET, true, allowedMimeTypes);
888
952
  const privateBucket = await ensureStorageBucket(supabaseUrl, serviceRoleKey, STORAGE_PRIVATE_BUCKET, false, allowedMimeTypes);
889
953
  mergeEnvFile(envLocalPath, {
@@ -902,57 +966,13 @@ async function setupSupabaseStorage(flags) {
902
966
  fileSizeLimit: STORAGE_REQUIRED_FILE_SIZE_LIMIT,
903
967
  migrationFiles: migrations.files,
904
968
  policyFiles: migrations.policyFiles,
969
+ preparedMigrationFiles,
905
970
  policyMigrationsDiscovered: true,
906
971
  policyMigrationsApplied: true,
907
- policiesVerified: true,
908
- policyVerificationMethod: "linked_db_push",
972
+ migrationPushIncludeAll: migrationPush.includeAll,
909
973
  envWritten: ["SUPABASE_PUBLIC_BUCKET", "SUPABASE_PRIVATE_BUCKET"],
910
974
  });
911
975
  }
912
- async function smokeTestSupabase() {
913
- const { values } = loadLocalEnv();
914
- const supabaseUrl = requireSupabaseUrl(values);
915
- const serviceRoleKey = requireServiceRoleKey(values);
916
- const runId = `run_${Date.now()}`;
917
- const objectPath = buildStorageHealthcheckObjectPath(runId);
918
- const prefix = `${STORAGE_HEALTHCHECK_PREFIX}/${runId}/`;
919
- await storageRequest({
920
- supabaseUrl,
921
- serviceRoleKey,
922
- method: "POST",
923
- endpoint: `/storage/v1/object/${STORAGE_PUBLIC_BUCKET}/${objectPath}`,
924
- rawBody: "healthcheck probe",
925
- contentType: "text/plain",
926
- });
927
- const listResult = await storageRequest({
928
- supabaseUrl,
929
- serviceRoleKey,
930
- method: "POST",
931
- endpoint: `/storage/v1/object/list/${STORAGE_PUBLIC_BUCKET}`,
932
- body: { prefix },
933
- contentType: "application/json",
934
- });
935
- await storageRequest({
936
- supabaseUrl,
937
- serviceRoleKey,
938
- method: "DELETE",
939
- endpoint: `/storage/v1/object/${STORAGE_PUBLIC_BUCKET}`,
940
- body: { prefixes: [objectPath] },
941
- contentType: "application/json",
942
- });
943
- printJson({
944
- ok: true,
945
- command: "storage smoke-test-supabase",
946
- runId,
947
- bucketId: STORAGE_PUBLIC_BUCKET,
948
- objectPath,
949
- prefix,
950
- upload: true,
951
- list: true,
952
- delete: true,
953
- listedItems: Array.isArray(listResult) ? listResult.length : 0,
954
- });
955
- }
956
976
  async function main() {
957
977
  const { command, subcommand, flags } = parseArgs(process.argv.slice(2));
958
978
  if (!command || command === "--help" || command === "help") {
@@ -963,10 +983,8 @@ async function main() {
963
983
  "admin ensure-admin",
964
984
  "storage check-supabase-context",
965
985
  "storage setup-supabase",
966
- "storage smoke-test-supabase",
967
986
  "storage check-s3-context",
968
987
  "storage setup-s3",
969
- "storage smoke-test-s3",
970
988
  "configure-site-redirects",
971
989
  "configure-email-password",
972
990
  "enable-google-provider",
@@ -989,14 +1007,10 @@ async function main() {
989
1007
  return checkSupabaseContext();
990
1008
  if (command === "storage" && subcommand === "setup-supabase")
991
1009
  return setupSupabaseStorage(flags);
992
- if (command === "storage" && subcommand === "smoke-test-supabase")
993
- return smokeTestSupabase();
994
1010
  if (command === "storage" && subcommand === "check-s3-context")
995
1011
  return checkS3Context(flags);
996
1012
  if (command === "storage" && subcommand === "setup-s3")
997
1013
  return setupS3Storage(flags);
998
- if (command === "storage" && subcommand === "smoke-test-s3")
999
- return smokeTestS3(flags);
1000
1014
  if (command === "configure-site-redirects")
1001
1015
  return configureSiteRedirects(flags);
1002
1016
  if (command === "configure-email-password")
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, PutObjectCommand, ListObjectsV2Command, DeleteObjectCommand, GetObjectCommand, } from "@aws-sdk/client-s3";
4
+ import { S3Client, HeadBucketCommand, CreateBucketCommand, PutPublicAccessBlockCommand, PutBucketEncryptionCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketCorsCommand, 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 = {
@@ -10,7 +10,6 @@ const DEFAULT_BUCKETS = {
10
10
  };
11
11
  const DEFAULT_PROJECT_SLUG = "vibecodemax";
12
12
  const DEFAULT_REGION = "us-east-1";
13
- const HEALTHCHECK_PREFIX = "_vibecodemax/healthcheck/default";
14
13
  const PUBLIC_ACCESS_BLOCK_PRIVATE = {
15
14
  BlockPublicAcls: true,
16
15
  IgnorePublicAcls: true,
@@ -482,55 +481,6 @@ async function verifyTwoBuckets(region, credentials, buckets) {
482
481
  });
483
482
  }
484
483
  }
485
- function buildObjectUrl(bucket, region, key) {
486
- const encoded = key.split("/").map((segment) => encodeURIComponent(segment)).join("/");
487
- if (region === "us-east-1")
488
- return `https://${bucket}.s3.amazonaws.com/${encoded}`;
489
- return `https://${bucket}.s3.${region}.amazonaws.com/${encoded}`;
490
- }
491
- async function smokeTestBuckets(region, credentials, buckets) {
492
- const { s3Client } = createAwsClients(region, credentials);
493
- const runId = `run_${Date.now()}`;
494
- const prefix = `${HEALTHCHECK_PREFIX}/${runId}`;
495
- const publicKey = `${prefix}/public-probe.txt`;
496
- const privateKey = `${prefix}/private-probe.txt`;
497
- await s3Client.send(new PutObjectCommand({ Bucket: buckets.public, Key: publicKey, Body: "public healthcheck probe", ContentType: "text/plain" }));
498
- await s3Client.send(new PutObjectCommand({ Bucket: buckets.private, Key: privateKey, Body: "private healthcheck probe", ContentType: "text/plain" }));
499
- const publicList = await s3Client.send(new ListObjectsV2Command({ Bucket: buckets.public, Prefix: prefix }));
500
- const privateList = await s3Client.send(new ListObjectsV2Command({ Bucket: buckets.private, Prefix: prefix }));
501
- const listPassed = Number(publicList.KeyCount || 0) > 0 && Number(privateList.KeyCount || 0) > 0;
502
- if (!listPassed) {
503
- fail("AWS_SMOKE_TEST_FAILED", "AWS S3 smoke-test list operation did not return the uploaded healthcheck objects.", 1, { prefix });
504
- }
505
- const publicUrl = buildObjectUrl(buckets.public, region, publicKey);
506
- const privateUrl = buildObjectUrl(buckets.private, region, privateKey);
507
- const publicReadResponse = await fetch(publicUrl);
508
- const privateReadResponse = await fetch(privateUrl);
509
- const publicRead = publicReadResponse.ok;
510
- const privateReadBlocked = !privateReadResponse.ok;
511
- if (!publicRead || !privateReadBlocked) {
512
- fail("AWS_SMOKE_TEST_FAILED", "AWS S3 smoke-test public/private read checks failed.", 1, {
513
- publicReadStatus: publicReadResponse.status,
514
- privateReadStatus: privateReadResponse.status,
515
- });
516
- }
517
- await s3Client.send(new GetObjectCommand({ Bucket: buckets.private, Key: privateKey }));
518
- await s3Client.send(new DeleteObjectCommand({ Bucket: buckets.public, Key: publicKey }));
519
- await s3Client.send(new DeleteObjectCommand({ Bucket: buckets.private, Key: privateKey }));
520
- return {
521
- ok: true,
522
- command: "storage smoke-test-s3",
523
- runId,
524
- prefix,
525
- buckets,
526
- upload: true,
527
- list: true,
528
- publicRead: true,
529
- privateRead: true,
530
- delete: true,
531
- publicObjectUrl: publicUrl,
532
- };
533
- }
534
484
  export async function checkS3Context(flags) {
535
485
  const context = readAwsContext(flags);
536
486
  if (context.missingKeys.length > 0) {
@@ -603,28 +553,6 @@ export async function setupS3Storage(flags) {
603
553
  envWritten: ["AWS_REGION", "AWS_S3_PUBLIC_BUCKET", "AWS_S3_PRIVATE_BUCKET"],
604
554
  });
605
555
  }
606
- export async function smokeTestS3(flags) {
607
- const context = readAwsContext(flags);
608
- const publicBucket = context.localEnv.values.AWS_S3_PUBLIC_BUCKET || "";
609
- const privateBucket = context.localEnv.values.AWS_S3_PRIVATE_BUCKET || "";
610
- if (context.missingKeys.length > 0) {
611
- fail("MISSING_ENV", `Missing required AWS values: ${context.missingKeys.join(", ")}. Add them to .env.bootstrap.local.`);
612
- }
613
- if (!isNonEmptyString(publicBucket) || !isNonEmptyString(privateBucket)) {
614
- fail("MISSING_ENV", "AWS_S3_PUBLIC_BUCKET or AWS_S3_PRIVATE_BUCKET is missing. Run storage setup first so .env.local is populated.");
615
- }
616
- const credentials = {
617
- accessKeyId: context.accessKeyId,
618
- secretAccessKey: context.secretAccessKey,
619
- ...(context.sessionToken ? { sessionToken: context.sessionToken } : {}),
620
- };
621
- await validateCredentials(context.region, credentials);
622
- const result = await smokeTestBuckets(context.region, credentials, {
623
- public: publicBucket.trim(),
624
- private: privateBucket.trim(),
625
- });
626
- printJson(result);
627
- }
628
556
  export function __testOnlyDeterministicBuckets(projectSlug, accountId) {
629
557
  return deterministicBuckets({ projectSlug, accountId });
630
558
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecodemax/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "VibeCodeMax CLI — local provider setup for bootstrap and project configuration",
5
5
  "type": "module",
6
6
  "bin": {