From 46cc565ab70aeedb2742474b448b5b10aef43feb Mon Sep 17 00:00:00 2001 From: John Yeo Date: Mon, 6 Oct 2025 09:23:20 +0100 Subject: [PATCH] feat: add dynamic port detection for multiple dev instances MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .gitignore | 1 - package.json | 7 +- scripts/detect-ports.js | 156 +++++++++++++++++++++++++++++++ setup.js => scripts/setup.js | 0 setupci.js => scripts/setupci.js | 0 scripts/start-dev.js | 60 ++++++++++++ server/src/index.ts | 37 +++++--- server/src/utils/auth.ts | 49 ++++++---- vite/package.json | 2 +- vite/vite.config.ts | 6 +- 10 files changed, 278 insertions(+), 40 deletions(-) create mode 100644 scripts/detect-ports.js rename setup.js => scripts/setup.js (100%) rename setupci.js => scripts/setupci.js (100%) create mode 100644 scripts/start-dev.js diff --git a/.gitignore b/.gitignore index 59e2780ea..300a7e0b4 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,6 @@ tests/ **/test.nnb **/scripts.nnb -**/scripts server/test.nnb server/src/test.nnb diff --git a/package.json b/package.json index 2bff0ed1f..38e2df495 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ ], "type": "module", "scripts": { - "dev": "concurrently \"cd server && bun dev\" \"cd server && bun workers:dev\" \"cd vite && bun dev\" \"cd shared && bun dev\"", + "dev": "node scripts/start-dev.js", + "dev:simple": "concurrently \"cd server && bun dev\" \"cd server && bun workers:dev\" \"cd vite && bun dev\" \"cd shared && bun dev\"", "vite:build": "bun -F @autumn/shared build && bun -F @autumn/vite build:bun", "vite:start": "bun -F @autumn/vite start:bun", "shared": "bun -F @autumn/shared build", @@ -18,8 +19,8 @@ "check": "bun -F @autumn/shared build && bun -F @autumn/server check", "server:cron": "pnpm -F server cron:start", "server:check": "NODE_ENV=production pnpm -F server check", - "setup": "node setup.js", - "setupci": "node setupci", + "setup": "node scripts/setup.js", + "setupci": "node scripts/setupci.js", "db:push": " bun -F @autumn/shared db:push", "db:generate": "bun -F @autumn/shared db:generate", "db:migrate": " bun -F @autumn/shared db:migrate", diff --git a/scripts/detect-ports.js b/scripts/detect-ports.js new file mode 100644 index 000000000..91874cd2f --- /dev/null +++ b/scripts/detect-ports.js @@ -0,0 +1,156 @@ +import net from 'node:net'; +import fs from 'node:fs'; +import path from 'node:path'; + +const DEFAULT_VITE_PORT = 3000; +const DEFAULT_SERVER_PORT = 8080; + +/** + * Check if a port is available + */ +async function isPortAvailable(port) { + return new Promise((resolve) => { + const server = net.createServer(); + + server.once('error', (err) => { + if (err.code === 'EADDRINUSE') { + resolve(false); + } else { + resolve(false); + } + }); + + server.once('listening', () => { + server.close(); + resolve(true); + }); + + server.listen(port); + }); +} + +/** + * Find the next available port starting from the given port + */ +async function findAvailablePort(startPort, maxAttempts = 10) { + for (let i = 0; i < maxAttempts; i++) { + const port = startPort + i; + if (await isPortAvailable(port)) { + return port; + } + } + throw new Error(`Could not find available port starting from ${startPort}`); +} + +/** + * Update or create .env file with the detected ports + */ +function updateEnvFile(filePath, updates) { + let content = ''; + + if (fs.existsSync(filePath)) { + content = fs.readFileSync(filePath, 'utf-8'); + } + + // Parse existing env file + const lines = content.split('\n'); + const envMap = new Map(); + + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('#')) { + const [key, ...valueParts] = trimmed.split('='); + if (key) { + envMap.set(key.trim(), valueParts.join('=')); + } + } + } + + // Update with new values + for (const [key, value] of Object.entries(updates)) { + envMap.set(key, value); + } + + // Rebuild content preserving comments and empty lines + const newLines = []; + const processedKeys = new Set(); + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) { + newLines.push(line); + continue; + } + + const [key] = trimmed.split('='); + if (key && envMap.has(key.trim())) { + processedKeys.add(key.trim()); + newLines.push(`${key.trim()}=${envMap.get(key.trim())}`); + } else { + newLines.push(line); + } + } + + // Add new keys that weren't in the original file + for (const [key, value] of envMap.entries()) { + if (!processedKeys.has(key)) { + newLines.push(`${key}=${value}`); + } + } + + fs.writeFileSync(filePath, newLines.join('\n')); +} + +async function detectAndSetPorts() { + const vitePort = await findAvailablePort(DEFAULT_VITE_PORT); + const serverPort = await findAvailablePort(DEFAULT_SERVER_PORT); + + console.log(`šŸ” Detected available ports:`); + console.log(` Frontend: ${vitePort}`); + console.log(` Backend: ${serverPort}`); + + // Get root directory (parent of scripts folder) + const rootDir = path.dirname(new URL(import.meta.url).pathname); + const projectRoot = path.join(rootDir, '..'); + + // Update vite .env + const viteEnvPath = path.join(projectRoot, 'vite', '.env'); + updateEnvFile(viteEnvPath, { + VITE_FRONTEND_URL: `http://localhost:${vitePort}`, + VITE_BACKEND_URL: `http://localhost:${serverPort}`, + }); + + // Update server .env + const serverEnvPath = path.join(projectRoot, 'server', '.env'); + updateEnvFile(serverEnvPath, { + BETTER_AUTH_URL: `http://localhost:${serverPort}`, + CLIENT_URL: `http://localhost:${vitePort}`, + }); + + // Set environment variables for current process + process.env.VITE_PORT = vitePort.toString(); + process.env.SERVER_PORT = serverPort.toString(); + process.env.VITE_FRONTEND_URL = `http://localhost:${vitePort}`; + process.env.VITE_BACKEND_URL = `http://localhost:${serverPort}`; + process.env.BETTER_AUTH_URL = `http://localhost:${serverPort}`; + process.env.CLIENT_URL = `http://localhost:${vitePort}`; + + console.log(`āœ… Environment variables updated`); + + return { vitePort, serverPort }; +} + +// Only run if called directly +if (import.meta.url === `file://${process.argv[1]}`) { + detectAndSetPorts() + .then(({ vitePort, serverPort }) => { + console.log(`\nšŸš€ Ready to start development servers`); + process.exit(0); + }) + .catch((error) => { + console.error('Error detecting ports:', error); + process.exit(1); + }); +} + +export { detectAndSetPorts, findAvailablePort, isPortAvailable }; diff --git a/setup.js b/scripts/setup.js similarity index 100% rename from setup.js rename to scripts/setup.js diff --git a/setupci.js b/scripts/setupci.js similarity index 100% rename from setupci.js rename to scripts/setupci.js diff --git a/scripts/start-dev.js b/scripts/start-dev.js new file mode 100644 index 000000000..2810a2e3d --- /dev/null +++ b/scripts/start-dev.js @@ -0,0 +1,60 @@ +import { spawn } from 'node:child_process'; +import { detectAndSetPorts } from './detect-ports.js'; + +async function startDev() { + try { + // Detect and set ports + const { vitePort, serverPort } = await detectAndSetPorts(); + + console.log('\nšŸš€ Starting development servers...\n'); + + // Start concurrently with the detected ports + const concurrentlyCmd = spawn( + 'bunx', + [ + 'concurrently', + `"cd server && SERVER_PORT=${serverPort} bun dev"`, + `"cd server && bun workers:dev"`, + `"cd vite && VITE_PORT=${vitePort} bun dev"`, + `"cd shared && bun dev"`, + ], + { + stdio: 'inherit', + shell: true, + env: { + ...process.env, + VITE_PORT: vitePort.toString(), + SERVER_PORT: serverPort.toString(), + }, + } + ); + + concurrentlyCmd.on('error', (error) => { + console.error('Failed to start development servers:', error); + process.exit(1); + }); + + concurrentlyCmd.on('exit', (code) => { + if (code !== 0) { + console.error(`Development servers exited with code ${code}`); + } + process.exit(code); + }); + + // Handle termination signals + process.on('SIGINT', () => { + console.log('\n\nšŸ›‘ Shutting down development servers...'); + concurrentlyCmd.kill('SIGINT'); + }); + + process.on('SIGTERM', () => { + console.log('\n\nšŸ›‘ Shutting down development servers...'); + concurrentlyCmd.kill('SIGTERM'); + }); + } catch (error) { + console.error('Error starting development servers:', error); + process.exit(1); + } +} + +startDev(); diff --git a/server/src/index.ts b/server/src/index.ts index ff9fc5388..1811aa9f8 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -39,20 +39,31 @@ const init = async () => { app.use(redirectToHono()); // Check if this blocks API calls... + const allowedOrigins = [ + "http://localhost:3000", + "http://localhost:5173", + "http://localhost:5174", + "https://app.useautumn.com", + "https://staging.useautumn.com", + "https://*.useautumn.com", + "https://localhost:8080", + "https://www.alphalog.ai", + "https://*.alphalog.ai", + process.env.CLIENT_URL || "", + ]; + + // Add dynamic port origins in development + if (process.env.NODE_ENV === "development") { + // Add ports 3000-3010 and 8080-8090 for multiple instances + for (let i = 0; i <= 10; i++) { + allowedOrigins.push(`http://localhost:${3000 + i}`); + allowedOrigins.push(`http://localhost:${8080 + i}`); + } + } + app.use( cors({ - origin: [ - "http://localhost:3000", - "http://localhost:5173", - "http://localhost:5174", - "https://app.useautumn.com", - "https://staging.useautumn.com", - "https://*.useautumn.com", - "https://localhost:8080", - "https://www.alphalog.ai", - "https://*.alphalog.ai", - process.env.CLIENT_URL || "", - ], + origin: allowedOrigins, credentials: true, allowedHeaders: [ "app_env", @@ -160,7 +171,7 @@ const init = async () => { app.use(mainRouter); app.use("/v1", apiRouter); - const PORT = 8080; + const PORT = process.env.SERVER_PORT ? Number.parseInt(process.env.SERVER_PORT) : 8080; server.listen(PORT, () => { console.log(`Server running on port ${PORT}`); diff --git a/server/src/utils/auth.ts b/server/src/utils/auth.ts index 334064c46..1fdd25b64 100644 --- a/server/src/utils/auth.ts +++ b/server/src/utils/auth.ts @@ -1,22 +1,22 @@ import "dotenv/config"; -import { db } from "@/db/initDrizzle.js"; -import sendOTPEmail from "@/internal/emails/sendOTPEmail.js"; -import { drizzleAdapter } from "better-auth/adapters/drizzle"; -import { sendInvitationEmail } from "@/internal/emails/sendInvitationEmail.js"; -import { beforeSessionCreated } from "./authUtils/beforeSessionCreated.js"; -import { betterAuth } from "better-auth"; -import { emailOTP, admin, organization } from "better-auth/plugins"; - -import { sendOnboardingEmail } from "@/internal/emails/sendOnboardingEmail.js"; -import { ADMIN_USER_IDs } from "./constants.js"; -import { afterOrgCreated } from "./authUtils/afterOrgCreated.js"; -import { createLoopsContact } from "@/external/resend/loopsUtils.js"; import { invitation } from "@autumn/shared"; +import { betterAuth } from "better-auth"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { admin, emailOTP, organization } from "better-auth/plugins"; import { eq } from "drizzle-orm"; +import { db } from "@/db/initDrizzle.js"; import { logger } from "@/external/logtail/logtailUtils.js"; +import { createLoopsContact } from "@/external/resend/loopsUtils.js"; +import { sendInvitationEmail } from "@/internal/emails/sendInvitationEmail.js"; +import { sendOnboardingEmail } from "@/internal/emails/sendOnboardingEmail.js"; +import sendOTPEmail from "@/internal/emails/sendOTPEmail.js"; +import { afterOrgCreated } from "./authUtils/afterOrgCreated.js"; +import { beforeSessionCreated } from "./authUtils/beforeSessionCreated.js"; +import { ADMIN_USER_IDs } from "./constants.js"; export const auth = betterAuth({ + baseURL: process.env.BETTER_AUTH_URL, telemetry: { enabled: false, }, @@ -52,13 +52,24 @@ export const auth = betterAuth({ }, }, }, - trustedOrigins: [ - "http://localhost:3000", - "https://app.useautumn.com", - "https://staging.useautumn.com", - "https://*.useautumn.com", - // process.env.CLIENT_URL!, - ], + trustedOrigins: (() => { + const origins = [ + "http://localhost:3000", + "https://app.useautumn.com", + "https://staging.useautumn.com", + "https://*.useautumn.com", + ]; + + // Add dynamic port origins in development + if (process.env.NODE_ENV === "development") { + // Add ports 3000-3010 for multiple instances + for (let i = 0; i <= 10; i++) { + origins.push(`http://localhost:${3000 + i}`); + } + } + + return origins; + })(), emailAndPassword: { enabled: true, disableSignUp: false, diff --git a/vite/package.json b/vite/package.json index 0eca64b3e..c0daf0f62 100644 --- a/vite/package.json +++ b/vite/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite --port 3000 --host", + "dev": "vite --host", "build": "tsc && vite build", "start": "serve -s dist", "lint": "eslint .", diff --git a/vite/vite.config.ts b/vite/vite.config.ts index 300f49d49..498932e5e 100644 --- a/vite/vite.config.ts +++ b/vite/vite.config.ts @@ -13,8 +13,8 @@ export default defineConfig({ }, server: { host: "0.0.0.0", // Required for Docker - port: 3000, - strictPort: true, + port: process.env.VITE_PORT ? Number.parseInt(process.env.VITE_PORT) : 3000, + strictPort: false, // Allow fallback to next available port allowedHosts: [ "dev.useautumn.com", "client.dev.useautumn.com", @@ -25,7 +25,7 @@ export default defineConfig({ interval: 1000, }, hmr: { - port: 3000, + port: process.env.VITE_PORT ? Number.parseInt(process.env.VITE_PORT) : 3000, }, }, });