@vibecodemax/cli 0.1.4 → 0.1.6
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 +75 -48
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
|
-
import * as os from "node:os";
|
|
4
3
|
import * as path from "node:path";
|
|
5
4
|
import { spawnSync } from "node:child_process";
|
|
6
5
|
import { checkS3Context, setupS3Storage, smokeTestS3 } from "./storageS3.js";
|
|
@@ -41,6 +40,19 @@ function fail(code, message, exitCode = 1, extra = {}) {
|
|
|
41
40
|
printJson({ ok: false, code, message, ...extra });
|
|
42
41
|
process.exit(exitCode);
|
|
43
42
|
}
|
|
43
|
+
/** Extract a human-readable error message from a Supabase/provider JSON response. */
|
|
44
|
+
function extractErrorMessage(json) {
|
|
45
|
+
if (!json || typeof json !== "object")
|
|
46
|
+
return null;
|
|
47
|
+
const obj = json;
|
|
48
|
+
if (isNonEmptyString(obj.error))
|
|
49
|
+
return obj.error;
|
|
50
|
+
if (isNonEmptyString(obj.message))
|
|
51
|
+
return obj.message;
|
|
52
|
+
if (isNonEmptyString(obj.msg))
|
|
53
|
+
return obj.msg;
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
44
56
|
function parseArgs(argv) {
|
|
45
57
|
const [command, ...rest] = argv;
|
|
46
58
|
let subcommand;
|
|
@@ -492,31 +504,44 @@ async function createAdminUser(supabaseUrl, serviceRoleKey, email, temporaryPass
|
|
|
492
504
|
}
|
|
493
505
|
return userId;
|
|
494
506
|
}
|
|
495
|
-
async function
|
|
507
|
+
async function findAdminMembership(supabaseUrl, serviceRoleKey, userId) {
|
|
508
|
+
const result = await supabaseAdminRequest({
|
|
509
|
+
supabaseUrl,
|
|
510
|
+
serviceRoleKey,
|
|
511
|
+
method: "GET",
|
|
512
|
+
endpoint: `/rest/v1/admin_users?user_id=eq.${userId}&select=user_id,role`,
|
|
513
|
+
});
|
|
514
|
+
const rows = Array.isArray(result) ? result : [];
|
|
515
|
+
const row = rows.find((candidate) => {
|
|
516
|
+
if (!candidate || typeof candidate !== "object")
|
|
517
|
+
return false;
|
|
518
|
+
return candidate.user_id === userId;
|
|
519
|
+
});
|
|
520
|
+
return {
|
|
521
|
+
exists: Boolean(row),
|
|
522
|
+
role: typeof row?.role === "string" ? row.role : null,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
async function promoteAdminUser(supabaseUrl, serviceRoleKey, userId) {
|
|
526
|
+
const existing = await findAdminMembership(supabaseUrl, serviceRoleKey, userId);
|
|
527
|
+
if (existing.exists)
|
|
528
|
+
return;
|
|
496
529
|
await supabaseAdminRequest({
|
|
497
530
|
supabaseUrl,
|
|
498
531
|
serviceRoleKey,
|
|
499
532
|
method: "POST",
|
|
500
|
-
endpoint: "/rest/v1/
|
|
533
|
+
endpoint: "/rest/v1/admin_users",
|
|
501
534
|
body: [
|
|
502
535
|
{
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
name: "Admin User",
|
|
506
|
-
role: "admin",
|
|
536
|
+
user_id: userId,
|
|
537
|
+
role: "super",
|
|
507
538
|
},
|
|
508
539
|
],
|
|
509
540
|
});
|
|
510
541
|
}
|
|
511
|
-
async function
|
|
512
|
-
const
|
|
513
|
-
|
|
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");
|
|
542
|
+
async function verifyAdminUser(supabaseUrl, serviceRoleKey, userId) {
|
|
543
|
+
const membership = await findAdminMembership(supabaseUrl, serviceRoleKey, userId);
|
|
544
|
+
return membership.exists;
|
|
520
545
|
}
|
|
521
546
|
async function ensureAdmin(flags) {
|
|
522
547
|
const { values } = loadLocalEnv();
|
|
@@ -538,8 +563,8 @@ async function ensureAdmin(flags) {
|
|
|
538
563
|
if (!userId) {
|
|
539
564
|
fail("INVALID_RESPONSE", "Resolved admin user ID is missing after ensure-admin.");
|
|
540
565
|
}
|
|
541
|
-
await
|
|
542
|
-
const verified = await
|
|
566
|
+
await promoteAdminUser(supabaseUrl, serviceRoleKey, userId);
|
|
567
|
+
const verified = await verifyAdminUser(supabaseUrl, serviceRoleKey, userId);
|
|
543
568
|
if (!verified) {
|
|
544
569
|
fail("ADMIN_VERIFY_FAILED", "The admin role could not be verified after promotion.");
|
|
545
570
|
}
|
|
@@ -553,7 +578,7 @@ async function ensureAdmin(flags) {
|
|
|
553
578
|
promoted: true,
|
|
554
579
|
verified: true,
|
|
555
580
|
defaultName: "Admin User",
|
|
556
|
-
applied: ["auth_user", "
|
|
581
|
+
applied: ["auth_user", "admin_users"],
|
|
557
582
|
};
|
|
558
583
|
if (temporaryPassword) {
|
|
559
584
|
result.temporaryPassword = temporaryPassword;
|
|
@@ -617,6 +642,20 @@ function runShellCommand(command, cwd) {
|
|
|
617
642
|
}
|
|
618
643
|
return typeof result.stdout === "string" ? result.stdout.trim() : "";
|
|
619
644
|
}
|
|
645
|
+
function runLinkedSupabaseCommand(command, cwd, failureCode, fallbackMessage) {
|
|
646
|
+
const result = spawnSync(process.env.SHELL || "/bin/zsh", ["-lc", command], {
|
|
647
|
+
cwd,
|
|
648
|
+
env: process.env,
|
|
649
|
+
encoding: "utf8",
|
|
650
|
+
});
|
|
651
|
+
if (result.status !== 0) {
|
|
652
|
+
const message = [result.stderr, result.stdout]
|
|
653
|
+
.map((value) => (typeof value === "string" ? value.trim() : ""))
|
|
654
|
+
.find(Boolean) || fallbackMessage;
|
|
655
|
+
fail(failureCode, message);
|
|
656
|
+
}
|
|
657
|
+
return typeof result.stdout === "string" ? result.stdout.trim() : "";
|
|
658
|
+
}
|
|
620
659
|
function normalizeStorageMimeCategories(rawValue) {
|
|
621
660
|
const requested = rawValue
|
|
622
661
|
.split(",")
|
|
@@ -695,13 +734,7 @@ async function storageRequest(params) {
|
|
|
695
734
|
json = null;
|
|
696
735
|
}
|
|
697
736
|
if (!response.ok) {
|
|
698
|
-
const message =
|
|
699
|
-
? json.error
|
|
700
|
-
: isNonEmptyString(json?.message)
|
|
701
|
-
? json.message
|
|
702
|
-
: isNonEmptyString(json?.msg)
|
|
703
|
-
? json.msg
|
|
704
|
-
: `Supabase returned ${response.status}`;
|
|
737
|
+
const message = extractErrorMessage(json) || `Supabase returned ${response.status}`;
|
|
705
738
|
fail("SUPABASE_STORAGE_ERROR", message, 1, { status: response.status });
|
|
706
739
|
}
|
|
707
740
|
return json;
|
|
@@ -751,14 +784,14 @@ async function readStorageBucket(supabaseUrl, serviceRoleKey, bucketId) {
|
|
|
751
784
|
}
|
|
752
785
|
if (response.status === 404)
|
|
753
786
|
return null;
|
|
787
|
+
// Supabase may return 400 (not 404) for non-existent buckets in some API versions
|
|
788
|
+
if (response.status === 400) {
|
|
789
|
+
const extracted = extractErrorMessage(json);
|
|
790
|
+
if (extracted && /bucket\s*not\s*found/i.test(extracted))
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
754
793
|
if (!response.ok) {
|
|
755
|
-
const message =
|
|
756
|
-
? json.error
|
|
757
|
-
: isNonEmptyString(json?.message)
|
|
758
|
-
? json.message
|
|
759
|
-
: isNonEmptyString(json?.msg)
|
|
760
|
-
? json.msg
|
|
761
|
-
: `Supabase returned ${response.status}`;
|
|
794
|
+
const message = extractErrorMessage(json) || `Supabase returned ${response.status}`;
|
|
762
795
|
fail("SUPABASE_STORAGE_ERROR", message, 1, { status: response.status });
|
|
763
796
|
}
|
|
764
797
|
return json && typeof json === "object" ? json : {};
|
|
@@ -809,13 +842,6 @@ async function ensureStorageBucket(supabaseUrl, serviceRoleKey, bucketId, isPubl
|
|
|
809
842
|
verified: true,
|
|
810
843
|
};
|
|
811
844
|
}
|
|
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
845
|
function buildStorageHealthcheckObjectPath(runId) {
|
|
820
846
|
return `${STORAGE_HEALTHCHECK_PREFIX}/${runId}/upload.txt`;
|
|
821
847
|
}
|
|
@@ -856,20 +882,14 @@ async function setupSupabaseStorage(flags) {
|
|
|
856
882
|
const supabaseRunner = getSupabaseRunner(dependencyManager);
|
|
857
883
|
const mimeCategories = resolveStorageMimeCategories(flags);
|
|
858
884
|
const allowedMimeTypes = expandStorageMimeCategories(mimeCategories);
|
|
885
|
+
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.");
|
|
859
887
|
const publicBucket = await ensureStorageBucket(supabaseUrl, serviceRoleKey, STORAGE_PUBLIC_BUCKET, true, allowedMimeTypes);
|
|
860
888
|
const privateBucket = await ensureStorageBucket(supabaseUrl, serviceRoleKey, STORAGE_PRIVATE_BUCKET, false, allowedMimeTypes);
|
|
861
889
|
mergeEnvFile(envLocalPath, {
|
|
862
890
|
SUPABASE_PUBLIC_BUCKET: STORAGE_PUBLIC_BUCKET,
|
|
863
891
|
SUPABASE_PRIVATE_BUCKET: STORAGE_PRIVATE_BUCKET,
|
|
864
892
|
});
|
|
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
893
|
printJson({
|
|
874
894
|
ok: true,
|
|
875
895
|
command: "storage setup-supabase",
|
|
@@ -885,6 +905,7 @@ async function setupSupabaseStorage(flags) {
|
|
|
885
905
|
policyMigrationsDiscovered: true,
|
|
886
906
|
policyMigrationsApplied: true,
|
|
887
907
|
policiesVerified: true,
|
|
908
|
+
policyVerificationMethod: "linked_db_push",
|
|
888
909
|
envWritten: ["SUPABASE_PUBLIC_BUCKET", "SUPABASE_PRIVATE_BUCKET"],
|
|
889
910
|
});
|
|
890
911
|
}
|
|
@@ -954,6 +975,12 @@ async function main() {
|
|
|
954
975
|
});
|
|
955
976
|
return;
|
|
956
977
|
}
|
|
978
|
+
// Handle --help for any resolved command before dispatching (avoids live API calls)
|
|
979
|
+
if (flags.help || flags.h) {
|
|
980
|
+
const resolvedCommand = subcommand ? `${command} ${subcommand}` : command;
|
|
981
|
+
printJson({ ok: true, command: resolvedCommand, help: true, message: `Usage: npx @vibecodemax/cli ${resolvedCommand} [options]` });
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
957
984
|
if (command === "read-setup-state")
|
|
958
985
|
return readSetupState();
|
|
959
986
|
if (command === "admin" && subcommand === "ensure-admin")
|