Some checks failed
✅ Check / 🔬 ESLint (push) Failing after 21s
✅ Check / 🏷️ Typecheck (push) Failing after 11s
✅ Check / 🔎 Test (push) Failing after 11s
✅ Check / e2e-test (push) Failing after 11s
✅ Check / 🔨 Build (push) Failing after 11s
🚀 Deploy / 🚀 Deploy (doc) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (demo) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (supabase) (push) Failing after 18s
341 lines
7.9 KiB
JavaScript
341 lines
7.9 KiB
JavaScript
import { input, select } from "@inquirer/prompts";
|
|
import { execa } from "execa";
|
|
import fs from "node:fs";
|
|
|
|
(async () => {
|
|
await loginToSupabase();
|
|
const projectName = await input({
|
|
message: "Enter the name of the project:",
|
|
default: "Atomic CRM",
|
|
});
|
|
const databasePassword = await input({
|
|
message: "Enter a database password:",
|
|
default: generatePassword(16),
|
|
});
|
|
|
|
const organizationId = await selectOrganization();
|
|
|
|
const region = await selectRegion();
|
|
|
|
const projectRef = await createProject({
|
|
projectName,
|
|
databasePassword,
|
|
organizationId,
|
|
region,
|
|
});
|
|
await waitForProjectToBeReady({ projectRef });
|
|
|
|
// This also ensures the project is ready
|
|
const { publishableKey } = await fetchApiKeys({
|
|
projectRef,
|
|
});
|
|
|
|
await linkProject({
|
|
projectRef,
|
|
databasePassword,
|
|
});
|
|
|
|
await setupDatabase({
|
|
databasePassword,
|
|
});
|
|
|
|
await setupSupabaseSecrets({
|
|
projectRef,
|
|
publishableKey,
|
|
});
|
|
|
|
await persistSupabaseEnv({
|
|
projectRef,
|
|
publishableKey,
|
|
});
|
|
})();
|
|
|
|
async function loginToSupabase() {
|
|
await execa("npx", ["supabase", "login"], { stdio: "inherit" });
|
|
}
|
|
|
|
async function createProject({
|
|
projectName,
|
|
databasePassword,
|
|
organizationId,
|
|
region,
|
|
}) {
|
|
const { stdout } = await execa(
|
|
"npx",
|
|
[
|
|
"supabase",
|
|
"projects",
|
|
"create",
|
|
"--output",
|
|
"json",
|
|
"--db-password",
|
|
databasePassword,
|
|
"--region",
|
|
region,
|
|
...(organizationId ? ["--org-id", organizationId] : []),
|
|
projectName,
|
|
],
|
|
{
|
|
stdin: "pipe",
|
|
stdout: "pipe",
|
|
},
|
|
);
|
|
|
|
try {
|
|
const matchJSON = stdout.match(new RegExp("{.*}", "s"));
|
|
if (!matchJSON) {
|
|
throw new Error("Invalid JSON output");
|
|
}
|
|
const jsonOuput = JSON.parse(matchJSON[0]);
|
|
return jsonOuput.id;
|
|
} catch (e) {
|
|
console.error("Failed to create project");
|
|
console.error(e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async function selectOrganization() {
|
|
const { stdout: organizationsJson } = await execa(
|
|
"npx",
|
|
["supabase", "orgs", "list", "--output", "json"],
|
|
{
|
|
stdout: "pipe",
|
|
},
|
|
);
|
|
|
|
const organizations = JSON.parse(organizationsJson);
|
|
|
|
if (organizations.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
if (organizations.length === 1) {
|
|
return organizations[0].id;
|
|
}
|
|
|
|
const selectedOrganizationId = await select({
|
|
message: "Select an organization to create the project in:",
|
|
choices: organizations.map((org) => ({
|
|
name: org.name,
|
|
value: org.id,
|
|
})),
|
|
});
|
|
|
|
return selectedOrganizationId;
|
|
}
|
|
|
|
const regions = [
|
|
{ value: "us-west-1", name: "West US (North California)" },
|
|
{ value: "us-west-2", name: "West US (Oregon)" },
|
|
{ value: "us-east-1", name: "East US (North Virginia)" },
|
|
{ value: "us-east-2", name: "East US (Ohio)" },
|
|
{ value: "ca-central-1", name: "Canada (Central)" },
|
|
{ value: "eu-west-1", name: "West EU (Ireland)" },
|
|
{ value: "eu-west-2", name: "West Europe (London)" },
|
|
{ value: "eu-west-3", name: "West EU (Paris)" },
|
|
{ value: "eu-central-1", name: "Central EU (Frankfurt)" },
|
|
{ value: "eu-central-2", name: "Central Europe (Zurich)" },
|
|
{ value: "eu-north-1", name: "North EU (Stockholm)" },
|
|
{ value: "ap-south-1", name: "South Asia (Mumbai)" },
|
|
{ value: "ap-southeast-1", name: "Southeast Asia (Singapore)" },
|
|
{ value: "ap-northeast-1", name: "Northeast Asia (Tokyo)" },
|
|
{ value: "ap-northeast-2", name: "Northeast Asia (Seoul)" },
|
|
{ value: "ap-southeast-2", name: "Oceania (Sydney)" },
|
|
{ value: "sa-east-1", name: "South America (São Paulo)" },
|
|
];
|
|
|
|
async function selectRegion() {
|
|
const selectedRegion = await select({
|
|
message:
|
|
"Select a region for the project (close to you for best performance):",
|
|
choices: regions.map((region) => ({
|
|
name: region.name,
|
|
value: region.value,
|
|
})),
|
|
});
|
|
|
|
return selectedRegion;
|
|
}
|
|
|
|
async function waitForProjectToBeReady({ projectRef }) {
|
|
// eslint-disable-next-line no-console
|
|
console.log("Waiting for project to be ready...");
|
|
const { stdout } = await execa(
|
|
"npx",
|
|
["supabase", "projects", "list", "--output", "json"],
|
|
{
|
|
stdout: "pipe",
|
|
},
|
|
);
|
|
|
|
try {
|
|
// The response is an Array of objects or null if there are no projects
|
|
const matchJSON =
|
|
stdout === null ? "[]" : stdout.match(new RegExp("\\[.*\\]", "s"));
|
|
if (!matchJSON) {
|
|
throw new Error("Invalid JSON output");
|
|
}
|
|
const jsonOuput = JSON.parse(matchJSON[0]);
|
|
|
|
const project = jsonOuput.find((project) => project.id === projectRef);
|
|
if (project?.status !== "ACTIVE_HEALTHY") {
|
|
await sleep(1000);
|
|
return waitForProjectToBeReady({ projectRef });
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to create project");
|
|
console.error(e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
let retry = 0;
|
|
async function linkProject({ projectRef, databasePassword }) {
|
|
await execa(
|
|
"npx",
|
|
[
|
|
"supabase",
|
|
"link",
|
|
"--project-ref",
|
|
projectRef,
|
|
"--password",
|
|
databasePassword,
|
|
],
|
|
{
|
|
stdout: "ignore",
|
|
stderr: "ignore",
|
|
},
|
|
).catch(() => {
|
|
retry++;
|
|
if (retry === 1) {
|
|
// eslint-disable-next-line no-console
|
|
console.log("Waiting for project to be ready...");
|
|
}
|
|
return sleep(1000).then(() =>
|
|
linkProject({ projectRef, databasePassword }),
|
|
);
|
|
});
|
|
}
|
|
|
|
async function setupDatabase({ databasePassword }) {
|
|
await execa(
|
|
"npx",
|
|
[
|
|
"supabase",
|
|
"db",
|
|
"push",
|
|
"--linked",
|
|
"--include-roles",
|
|
"--include-seed",
|
|
"--password",
|
|
databasePassword,
|
|
],
|
|
{
|
|
stdio: "inherit",
|
|
},
|
|
);
|
|
}
|
|
|
|
async function fetchApiKeys({ projectRef }) {
|
|
let publishableKey = "";
|
|
try {
|
|
const { stdout, exitCode } = await execa(
|
|
"npx",
|
|
[
|
|
"supabase",
|
|
"projects",
|
|
"api-keys",
|
|
"--output",
|
|
"json",
|
|
"--project-ref",
|
|
projectRef,
|
|
],
|
|
{
|
|
stdout: "pipe",
|
|
stderr: "ignore",
|
|
},
|
|
);
|
|
// If the exitCode is not 0, the command failed most probably because the project is not ready
|
|
if (exitCode === 0) {
|
|
// The response is an Array of objects
|
|
const matchJSON = stdout.match(new RegExp("\\[.*\\]", "s"));
|
|
if (!matchJSON) {
|
|
throw new Error("Invalid JSON output");
|
|
}
|
|
const jsonOutput = JSON.parse(matchJSON[0]);
|
|
|
|
// Prioritize the default publishable key, but any publishable key will work.
|
|
publishableKey = jsonOutput.find(
|
|
(key) => key.type === "publishable" && key.name === "default",
|
|
)?.api_key;
|
|
if (!publishableKey) {
|
|
publishableKey = jsonOutput.find(
|
|
(key) => key.type === "publishable",
|
|
)?.api_key;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to fetch API keys");
|
|
console.error(e);
|
|
throw e;
|
|
}
|
|
|
|
if (publishableKey === "") {
|
|
await sleep(1000);
|
|
return fetchApiKeys({ projectRef });
|
|
}
|
|
|
|
return { publishableKey };
|
|
}
|
|
|
|
async function setupSupabaseSecrets({ projectRef, publishableKey }) {
|
|
await execa(
|
|
"npx",
|
|
[
|
|
"supabase",
|
|
"secrets",
|
|
"set",
|
|
`SB_PUBLISHABLE_KEY=${publishableKey}`,
|
|
"--project-ref",
|
|
projectRef,
|
|
],
|
|
{
|
|
stdio: "inherit",
|
|
},
|
|
);
|
|
}
|
|
|
|
async function persistSupabaseEnv({ projectRef, publishableKey }) {
|
|
fs.writeFileSync(
|
|
`${process.cwd()}/.env.production.local`,
|
|
`
|
|
VITE_SUPABASE_URL=https://${projectRef}.supabase.co
|
|
VITE_SB_PUBLISHABLE_KEY=${publishableKey}`,
|
|
{ flag: "a" },
|
|
);
|
|
}
|
|
|
|
function generatePassword(length) {
|
|
const password = crypto
|
|
.getRandomValues(new BigUint64Array(4))
|
|
.reduce(
|
|
(prev, curr, index) =>
|
|
(!index ? prev : prev.toString(36)) +
|
|
(index % 2 ? curr.toString(36).toUpperCase() : curr.toString(36)),
|
|
)
|
|
.split("")
|
|
.sort(() => 128 - crypto.getRandomValues(new Uint8Array(1))[0])
|
|
.join("");
|
|
|
|
if (length) {
|
|
return password.slice(0, length);
|
|
}
|
|
|
|
return password;
|
|
}
|
|
|
|
function sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|