feat: add dynamic port detection for multiple dev instances
Added automatic port detection to support running multiple instances of the repo simultaneously on local machines. The system detects available ports and automatically configures environment variables. Changes: - Created scripts/detect-ports.js for automatic port detection - Created scripts/start-dev.js to orchestrate port detection and service startup - Updated dev command to use new port detection system - Modified server to use dynamic SERVER_PORT environment variable - Modified vite config to use dynamic VITE_PORT environment variable - Added dynamic CORS/trusted origins for ports 3000-3010 in development - Organized setup scripts into scripts/ folder 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
320
scripts/setup.js
Normal file
320
scripts/setup.js
Normal file
@@ -0,0 +1,320 @@
|
||||
#!/usr/bin/env node
|
||||
import { randomBytes } from "crypto";
|
||||
import { writeFileSync, copyFileSync, readFileSync } from "fs";
|
||||
import inquirer from "inquirer";
|
||||
import { spawnSync } from "child_process";
|
||||
import chalk from "chalk";
|
||||
|
||||
const genUrlSafeBase64 = (bytes) => {
|
||||
return randomBytes(bytes)
|
||||
.toString("base64")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/g, "");
|
||||
};
|
||||
|
||||
const genRandomSubdomain = (length = 10) => {
|
||||
const chars = "abcdefghijklmnopqrstuvwxyz";
|
||||
let result = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const genAlphanumericPassword = (length = 24) => {
|
||||
const chars =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
let result = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// Helper to get or create a Supabase org
|
||||
const getOrCreateSupabaseOrg = async () => {
|
||||
// List orgs (table output)
|
||||
const orgListResult = spawnSync("npx", ["supabase", "orgs", "list"], {
|
||||
encoding: "utf-8",
|
||||
shell: true,
|
||||
});
|
||||
if (orgListResult.status !== 0) {
|
||||
console.error(chalk.red("❌ Failed to list Supabase orgs."));
|
||||
process.exit(1);
|
||||
}
|
||||
const lines = orgListResult.stdout
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
// Find separator line (e.g., "------|------")
|
||||
const sepIdx = lines.findIndex(
|
||||
(line) => line.includes("|") && line.includes("-"),
|
||||
);
|
||||
|
||||
// If only header and separator, no orgs exist
|
||||
if (lines.length <= sepIdx + 1) {
|
||||
console.log(chalk.yellowBright("No Supabase organizations found."));
|
||||
const { createOrg } = await inquirer.prompt([
|
||||
{
|
||||
type: "confirm",
|
||||
name: "createOrg",
|
||||
message: chalk.cyan(
|
||||
"Would you like to create a new Supabase organization now?",
|
||||
),
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
if (!createOrg) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
"❌ Cannot continue without a Supabase organization. Exiting.",
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const { orgName } = await inquirer.prompt([
|
||||
{
|
||||
type: "input",
|
||||
name: "orgName",
|
||||
message: chalk.cyan("Enter a name for your new Supabase organization:"),
|
||||
validate: (input) => input && input.length > 2,
|
||||
},
|
||||
]);
|
||||
console.log(
|
||||
chalk.blueBright(`\nCreating Supabase organization '${orgName}'...`),
|
||||
);
|
||||
const createOrgResult = spawnSync(
|
||||
"npx",
|
||||
["supabase", "orgs", "create", orgName],
|
||||
{ stdio: "inherit", encoding: "utf-8", shell: true },
|
||||
);
|
||||
if (createOrgResult.status !== 0) {
|
||||
console.error(chalk.red("❌ Failed to create Supabase organization."));
|
||||
process.exit(1);
|
||||
}
|
||||
// Done! Org created, return
|
||||
return;
|
||||
}
|
||||
|
||||
// At least one org exists, just proceed
|
||||
return;
|
||||
};
|
||||
|
||||
const generateSupabaseDatabaseUrl = async () => {
|
||||
// Step 3: Check if already logged in, otherwise run supabase login
|
||||
console.log(
|
||||
chalk.magentaBright("\n================ Supabase Setup ================\n"),
|
||||
);
|
||||
|
||||
// Check if user is already logged in by trying to list orgs
|
||||
console.log(chalk.blueBright("\nChecking Supabase authentication..."));
|
||||
const authCheckResult = spawnSync("npx", ["supabase", "orgs", "list"], {
|
||||
encoding: "utf-8",
|
||||
shell: true,
|
||||
stdio: "pipe", // Capture output instead of showing it
|
||||
});
|
||||
|
||||
if (authCheckResult.status !== 0) {
|
||||
// User is not logged in
|
||||
console.log(chalk.yellowBright("Not logged in to Supabase."));
|
||||
console.log(chalk.blueBright("Launching Supabase login..."));
|
||||
spawnSync("npx", ["supabase", "login"], { stdio: "inherit", shell: true });
|
||||
} else {
|
||||
console.log(chalk.greenBright("✅ Already logged in to Supabase."));
|
||||
}
|
||||
|
||||
// Step 3.5: Ensure org exists and select one
|
||||
await getOrCreateSupabaseOrg();
|
||||
|
||||
// Step 4: Prompt for project name and region
|
||||
const projectName = "autumn-oss-db";
|
||||
|
||||
// Step 5: Generate DB password (alphanumeric only)
|
||||
const dbPassword = genAlphanumericPassword(24); // 24 chars, alphanumeric
|
||||
|
||||
// Step 6: Create Supabase project
|
||||
console.log(
|
||||
chalk.blueBright(`\nCreating Supabase project '${projectName}'...`),
|
||||
);
|
||||
const createResult = spawnSync(
|
||||
"npx",
|
||||
[
|
||||
"supabase",
|
||||
"projects",
|
||||
"create",
|
||||
projectName,
|
||||
"--db-password",
|
||||
dbPassword,
|
||||
],
|
||||
{ stdio: "inherit", encoding: "utf-8", shell: true },
|
||||
);
|
||||
|
||||
if (createResult.status !== 0) {
|
||||
console.error("❌ Failed to create Supabase project.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// List supabase projects
|
||||
const listResult = spawnSync(
|
||||
"npx",
|
||||
["supabase", "projects", "list", "--output", "json"],
|
||||
{ encoding: "utf-8", shell: true },
|
||||
);
|
||||
|
||||
if (listResult.status !== 0) {
|
||||
console.error("❌ Failed to list Supabase projects.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const projects = JSON.parse(listResult.stdout);
|
||||
const found = projects.find((p) => p.name === projectName);
|
||||
if (!found) {
|
||||
console.error(chalk.red("❌ Failed to find new Supabase project."));
|
||||
process.exit(1);
|
||||
}
|
||||
const projectId = found.id;
|
||||
const region = found.region;
|
||||
|
||||
// Step 8: Construct DATABASE_URL
|
||||
const databaseUrl = `postgresql://postgres.${projectId}:${dbPassword}@aws-1-${region}.pooler.supabase.com:5432/postgres`;
|
||||
console.log(chalk.greenBright(`\nGenerated DB password: ${dbPassword}\n`));
|
||||
console.log(chalk.greenBright(`\nYour DATABASE_URL is:\n${databaseUrl}\n`));
|
||||
console.log(chalk.yellow("--------------------------------"));
|
||||
|
||||
// Step 9: Prompt to run docker compose up
|
||||
console.log(chalk.magentaBright("\nNext steps:"));
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Run "docker compose -f docker-compose.dev.yml up" to start Autumn',
|
||||
),
|
||||
);
|
||||
|
||||
return databaseUrl;
|
||||
};
|
||||
|
||||
const handleDatabaseSetup = async () => {
|
||||
// Step 2: Ask user what to do for DB
|
||||
console.log(
|
||||
chalk.magentaBright(
|
||||
"\n================ Autumn Database Setup ================\n",
|
||||
),
|
||||
);
|
||||
const { dbOption } = await inquirer.prompt([
|
||||
{
|
||||
type: "list",
|
||||
name: "dbOption",
|
||||
message: chalk.cyan("How do you want to set up your database?"),
|
||||
choices: [
|
||||
{ name: "Set up Supabase (Cloud) for Autumn", value: "supabase" },
|
||||
{ name: "Paste in your own DATABASE_URL", value: "paste" },
|
||||
{ name: "Paste in your own DATABASE_URL later", value: "later" },
|
||||
],
|
||||
default: "supabase",
|
||||
},
|
||||
]);
|
||||
|
||||
let databaseUrl = "";
|
||||
|
||||
if (dbOption === "supabase") {
|
||||
databaseUrl = await generateSupabaseDatabaseUrl();
|
||||
} else if (dbOption === "paste") {
|
||||
const res = await inquirer.prompt([
|
||||
{
|
||||
type: "input",
|
||||
name: "databaseUrl",
|
||||
message: "Paste in your DATABASE_URL:",
|
||||
validate: (input) => input && input.length > 5,
|
||||
},
|
||||
]);
|
||||
databaseUrl = res.databaseUrl;
|
||||
} else if (dbOption === "later") {
|
||||
databaseUrl = "";
|
||||
}
|
||||
|
||||
return databaseUrl;
|
||||
};
|
||||
|
||||
async function main() {
|
||||
// Step 1: Generate secrets
|
||||
console.log(
|
||||
chalk.magentaBright("\n================ Autumn Setup ================\n"),
|
||||
);
|
||||
const localtunnelReservedKey = genRandomSubdomain(32);
|
||||
const secrets = {
|
||||
BETTER_AUTH_SECRET: genUrlSafeBase64(64),
|
||||
ENCRYPTION_IV: genUrlSafeBase64(16),
|
||||
ENCRYPTION_PASSWORD: genUrlSafeBase64(64),
|
||||
BETTER_AUTH_URL: "http://localhost:8080",
|
||||
CLIENT_URL: "http://localhost:3000",
|
||||
LOCALTUNNEL_RESERVED_KEY: localtunnelReservedKey,
|
||||
STRIPE_WEBHOOK_URL: `https://${localtunnelReservedKey}.loca.lt`,
|
||||
};
|
||||
|
||||
let databaseUrl = "";
|
||||
let stripeWebhookVars = [];
|
||||
|
||||
databaseUrl = await handleDatabaseSetup();
|
||||
// stripeWebhookVars = await handleLocalRunSetup();
|
||||
|
||||
// Step 11: Write to server/.env
|
||||
console.log(
|
||||
chalk.magentaBright("\n================ Writing .env ================\n"),
|
||||
);
|
||||
const envSections = [];
|
||||
|
||||
// Autumn Auth section
|
||||
envSections.push(
|
||||
"# Auth",
|
||||
`BETTER_AUTH_SECRET=${secrets.BETTER_AUTH_SECRET}`,
|
||||
`BETTER_AUTH_URL=${secrets.BETTER_AUTH_URL}`,
|
||||
`CLIENT_URL=${secrets.CLIENT_URL}`,
|
||||
"",
|
||||
);
|
||||
|
||||
// Stripe required section
|
||||
envSections.push(
|
||||
"# Stripe",
|
||||
`LOCALTUNNEL_RESERVED_KEY=${secrets.LOCALTUNNEL_RESERVED_KEY}`,
|
||||
`ENCRYPTION_IV=${secrets.ENCRYPTION_IV}`,
|
||||
`ENCRYPTION_PASSWORD=${secrets.ENCRYPTION_PASSWORD}`,
|
||||
`STRIPE_WEBHOOK_URL=${secrets.STRIPE_WEBHOOK_URL}`,
|
||||
"",
|
||||
);
|
||||
|
||||
// Database section
|
||||
if (databaseUrl) {
|
||||
envSections.push("# Database", `DATABASE_URL=${databaseUrl}`, "");
|
||||
}
|
||||
|
||||
// Stripe Webhooks section
|
||||
if (stripeWebhookVars.length > 0) {
|
||||
envSections.push("# Stripe Webhooks");
|
||||
envSections.push(...stripeWebhookVars);
|
||||
envSections.push("");
|
||||
}
|
||||
|
||||
const envVars = envSections.join("\n");
|
||||
|
||||
writeFileSync("server/.env", envVars);
|
||||
try {
|
||||
copyFileSync("vite/.env.example", "vite/.env");
|
||||
} catch (error) {
|
||||
console.log(chalk.red("❌ Failed to copy vite/.env.example to vite/.env"));
|
||||
console.log(chalk.red("❌ Please copy the file manually"));
|
||||
}
|
||||
|
||||
console.log(chalk.greenBright("🎉 Setup complete! 🎉"));
|
||||
console.log(chalk.cyan("You can find your env variables in server/.env"));
|
||||
|
||||
console.log(chalk.cyan("\nNext steps:\n"));
|
||||
console.log(chalk.cyan("1. Set up your database tables:\n"));
|
||||
console.log(chalk.whiteBright(" bun db:generate && bun db:migrate\n"));
|
||||
console.log(chalk.cyan("2. Start Autumn:\n"));
|
||||
console.log(
|
||||
chalk.whiteBright(" docker compose -f docker-compose.dev.yml up\n"),
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user