#!/usr/bin/env node /* eslint-disable no-console */ const { spawn, spawnSync } = require('child_process'); const net = require('net'); function getRedisConfig(env = process.env) { return { containerName: env.REDIS_CONTAINER_NAME || 'orangetv-redis-dev', image: env.REDIS_IMAGE || 'redis:alpine', port: env.REDIS_PORT || '6379', }; } function buildRedisUrl(port) { return `redis://localhost:${port}`; } function resolveRedisAction(state) { if (!state.exists) return 'create'; if (!state.running) return 'start'; return 'none'; } function isPortAvailable(port) { return new Promise((resolve) => { const server = net.createServer(); server.once('error', () => { resolve(false); }); server.once('listening', () => { server.close(() => resolve(true)); }); server.listen(port); }); } async function assertDevPortsAvailable(ports = [3000]) { const checks = await Promise.all( ports.map(async (port) => ({ port, available: await isPortAvailable(port), })) ); const blocked = checks.filter((check) => !check.available); if (blocked.length > 0) { throw new Error( `Dev port${blocked.length > 1 ? 's are' : ' is'} already in use: ${blocked .map((check) => check.port) .join(', ')}.\nStop the existing OrangeTV dev server or free those ports, then retry.` ); } } function runDocker(args, options = {}) { return spawnSync('docker', args, { encoding: 'utf8', stdio: options.stdio || 'pipe', }); } function assertDockerAvailable() { const result = runDocker(['info']); if (result.status !== 0) { const detail = result.stderr?.trim() || result.stdout?.trim(); throw new Error( `Docker is not available. Start Docker Desktop and retry.\n${detail || ''}` ); } } function getContainerState(containerName) { const result = runDocker([ 'inspect', '-f', '{{.State.Running}}', containerName, ]); if (result.status !== 0) { return { exists: false, running: false }; } return { exists: true, running: result.stdout.trim() === 'true', }; } function ensureRedis(config = getRedisConfig()) { assertDockerAvailable(); const state = getContainerState(config.containerName); const action = resolveRedisAction(state); if (action === 'none') { console.log(`Redis container "${config.containerName}" is already running.`); return; } if (action === 'start') { console.log(`Starting Redis container "${config.containerName}"...`); const result = runDocker(['start', config.containerName], { stdio: 'inherit', }); if (result.status !== 0) { throw new Error(`Failed to start Redis container "${config.containerName}".`); } return; } console.log( `Creating Redis container "${config.containerName}" on localhost:${config.port}...` ); const result = runDocker( [ 'run', '--name', config.containerName, '-p', `${config.port}:6379`, '-d', config.image, ], { stdio: 'inherit' } ); if (result.status !== 0) { throw new Error( `Failed to create Redis container. If port ${config.port} is already in use, retry with REDIS_PORT=6380 pnpm dev:redis.` ); } } function startFastifyDev(config = getRedisConfig()) { const redisUrl = buildRedisUrl(config.port); const env = { ...process.env, VITE_STORAGE_TYPE: 'redis', REDIS_URL: redisUrl, }; console.log(`Using Redis storage: ${redisUrl}`); console.log('Starting OrangeTV Fastify/Vite dev server...'); const child = spawn('pnpm', ['dev'], { env, stdio: 'inherit', }); process.on('SIGINT', () => { if (!child.killed) child.kill('SIGINT'); }); process.on('SIGTERM', () => { if (!child.killed) child.kill('SIGTERM'); }); child.on('exit', (code, signal) => { if (signal) { process.exit(1); } process.exit(code || 0); }); } async function main() { try { const config = getRedisConfig(); await assertDevPortsAvailable(); ensureRedis(config); startFastifyDev(config); } catch (error) { console.error(error.message); process.exit(1); } } if (require.main === module) { main(); } module.exports = { assertDevPortsAvailable, buildRedisUrl, ensureRedis, getRedisConfig, isPortAvailable, resolveRedisAction, startFastifyDev, };