import { randomBytes } from 'crypto'; const c = (colour) => { if(colour === 'reset') { return '\x1b[0m'; } return Bun.color(colour, 'ansi'); }; const log = (message) => console.log(message); const logInfo = (message) => log(c('blue') + '[INFO] ' + c('reset') + message); const logSuccess = (message) => log(c('green') + '[SUCCESS] ' + c('reset') + message); const logWarn = (message) => log(c('yellow') + '[WARN] ' + c('reset') + message); const logError = (message) => log(c('red') + '[ERROR] ' + c('reset') + message); const ENV_FILE = '.env'; const ENV_EXAMPLE_FILE = '.env.example'; const DOCKER_COMPOSE_FILE = 'docker-compose.yml'; const DOCKER_COMPOSE_EXAMPLE_FILE = 'docker-compose-example.yml'; async function generateSecureRandomString(bytes = 32) { return randomBytes(bytes).toString('hex'); } async function ensureFileFromExample(targetPath, examplePath) { logInfo(`Checking for ${targetPath}...`); const targetExists = await Bun.file(targetPath).exists(); if (!targetExists) { logWarn(`${targetPath} not found. Copying from ${examplePath}...`); const exampleExists = await Bun.file(examplePath).exists(); if (!exampleExists) { logError(`${examplePath} not found. Cannot create ${targetPath}. Please ensure ${examplePath} exists.`); // Don't exit immediately, maybe other checks can proceed. // Consider if this should be a fatal error depending on project needs. return { copied: false, error: true }; } try { const exampleContent = await Bun.file(examplePath).text(); await Bun.write(targetPath, exampleContent); logSuccess(`Copied ${examplePath} to ${targetPath}.`); return { copied: true, error: false }; // Indicates file was copied } catch (error) { logError(`Failed to copy ${examplePath} to ${targetPath}: ${error.message}`); return { copied: false, error: true }; // Indicate error } } else { logInfo(`${targetPath} already exists.`); return { copied: false, error: false }; // Indicates file already existed } } const secrets = new Map(); async function replacePlaceholders(filePath) { logInfo(`Checking for placeholders in ${filePath}...`); try { let content = await Bun.file(filePath).text(); const placeholderRegex = /{{\s*([A-Z0-9_]+)\s*}}/g; const placeholders = [...content.matchAll(placeholderRegex)]; const uniquePlaceholderNames = [...new Set(placeholders.map(match => match[1]))]; if (uniquePlaceholderNames.length === 0) { logInfo(`No placeholders found in ${filePath}.`); return true; // Indicate success (no action needed) } logInfo(`Found placeholders: ${uniquePlaceholderNames.join(', ')}. Generating secrets...`); for (const name of uniquePlaceholderNames) { // Reuse existing secret if already generated for another file in this run if (!secrets.has(name)) { secrets.set(name, await generateSecureRandomString()); } } let replacementsMade = false; content = content.replace(placeholderRegex, (match, name) => { const secret = secrets.get(name); if (secret) { replacementsMade = true; return secret; } return match; // Return original match if name not found (shouldn't happen with current logic) }); if (replacementsMade) { await Bun.write(filePath, content); logSuccess(`Replaced placeholders in ${filePath} with generated secrets.`); } else { logInfo(`No placeholder values needed replacement in ${filePath}.`); } return true; // Indicate success } catch (error) { logError(`Failed to process placeholders in ${filePath}: ${error.message}`); return false; // Indicate failure } } async function ensureDockerComposeRunning() { logInfo('Ensuring Docker Compose services are running...'); // Check if docker-compose.yml exists first const composeFileExists = await Bun.file(DOCKER_COMPOSE_FILE).exists(); if (!composeFileExists) { logWarn(`Skipping Docker Compose setup because ${DOCKER_COMPOSE_FILE} does not exist.`); return true; // Not an error, just skipping } try { logInfo(`Running docker compose -f ${DOCKER_COMPOSE_FILE} up -d...`); const proc = Bun.spawn({ cmd: ['docker', 'compose', '-f', DOCKER_COMPOSE_FILE, 'up', '-d'], stdout: 'inherit', // Pipe output to the setup script's stdout stderr: 'inherit', // Pipe errors to the setup script's stderr }); const exitCode = await proc.exited; if (exitCode === 0) { logSuccess('Docker Compose services started successfully (or were already running).'); return true; // Indicate success } else { logError(`Docker Compose command failed with exit code ${exitCode}.`); return false; // Indicate failure } } catch (error) { logError(`Failed to run Docker Compose: ${error.message}`); logError('Please ensure Docker is installed, running, and you have permissions to use it.'); return false; // Indicate failure } } async function main() { let overallSuccess = true; console.log(c('aqua') +` _____ _ _ _____ _ _ / ____| | | | | __ \\ (_) | | | (___ | |_ _ _| | ___| |__) |__ _ _ __ | |_ \\___ \\| __| | | | |/ _ \\ ___/ _ \\| | '_ \\| __| ____) | |_| |_| | | __/ | | (_) | | | | | |_ |_____/ \\__|\\__, |_|\\___|_| \\___/|_|_| |_|\\__| __/ | |___/ ` + c('reset')); logInfo('Starting project setup validation...'); // Ensure .env file exists and replace placeholders if copied const envResult = await ensureFileFromExample(ENV_FILE, ENV_EXAMPLE_FILE); if (envResult.error) { overallSuccess = false; } else if (envResult.copied) { if (!await replacePlaceholders(ENV_FILE)) { overallSuccess = false; } } // Ensure docker-compose.yml exists and replace placeholders if copied const composeResult = await ensureFileFromExample(DOCKER_COMPOSE_FILE, DOCKER_COMPOSE_EXAMPLE_FILE); if (composeResult.error) { overallSuccess = false; } else if (composeResult.copied) { if (!await replacePlaceholders(DOCKER_COMPOSE_FILE)) { overallSuccess = false; } } // Only attempt to run docker compose if the previous steps were generally successful // and the compose file actually exists now. if (overallSuccess && await Bun.file(DOCKER_COMPOSE_FILE).exists()) { if (!await ensureDockerComposeRunning()) { overallSuccess = false; } } else if (!await Bun.file(DOCKER_COMPOSE_FILE).exists()) { logWarn(`Skipping Docker Compose execution as ${DOCKER_COMPOSE_FILE} is missing.`); // Not necessarily a failure if the example file was also missing. } if (overallSuccess) { logSuccess('Project setup validation completed successfully.'); } else { logError('Project setup validation failed. Please check the logs above.'); process.exit(1); // Exit with error code if any step failed } } main().catch(err => { logError(`Unhandled error during setup: ${err.message}\n${err.stack}`); process.exit(1); });