#!/usr/bin/env node // Wrapper around `ng serve` that resolves the best binding: // 1. https://stella-ops.local (port 443) — if hostname resolves and port is free // 2. https://localhost:10000 — always available fallback // // Additionally binds http://stella-ops.local (port 80) as a redirect to HTTPS // when the hostname resolves and port 80 is available. const { spawn } = require('child_process'); const http = require('http'); const dns = require('dns'); const net = require('net'); const path = require('path'); const HOSTNAME = 'stella-ops.local'; const HTTPS_PORT = 443; const HTTP_PORT = 80; const DEV_PORT = 10000; const SETUP_DOC = 'docs/technical/architecture/port-registry.md'; function isWindows() { return process.platform === 'win32'; } function hostsFilePath() { return isWindows() ? 'C:\\Windows\\System32\\drivers\\etc\\hosts' : '/etc/hosts'; } function resolveHostnameIp(hostname) { return new Promise((resolve) => { dns.lookup(hostname, { family: 4 }, (err, address) => { if (err) return resolve(null); resolve(address); }); }); } function isPortAvailable(port, ip) { return new Promise((resolve) => { const server = net.createServer(); server.once('error', () => resolve(false)); server.once('listening', () => { server.close(() => resolve(true)); }); server.listen(port, ip); }); } function startHttpRedirect(httpsHost, httpsPort, bindIp) { return new Promise((resolve) => { const server = http.createServer((req, res) => { const location = `https://${httpsHost}${httpsPort === 443 ? '' : ':' + httpsPort}${req.url}`; res.writeHead(301, { Location: location }); res.end(); }); server.on('error', (err) => { console.warn(` HTTP redirect on port ${HTTP_PORT} failed: ${err.message}`); resolve(false); }); server.listen(HTTP_PORT, bindIp, () => { console.log(` HTTP redirect active: http://${HOSTNAME} -> https://${httpsHost}${httpsPort === 443 ? '' : ':' + httpsPort}`); console.log(` Bound to ${bindIp}:${HTTP_PORT}`); resolve(true); }); }); } async function main() { const extraArgs = process.argv.slice(2); const resolvedIp = await resolveHostnameIp(HOSTNAME); const hostnameOk = !!resolvedIp; const port443Free = hostnameOk ? await isPortAvailable(HTTPS_PORT, resolvedIp) : false; const port80Free = hostnameOk ? await isPortAvailable(HTTP_PORT, resolvedIp) : false; let host, port; if (hostnameOk && port443Free) { host = HOSTNAME; port = HTTPS_PORT; console.log(''); console.log(` ${HOSTNAME} resolves to ${resolvedIp}; port ${HTTPS_PORT} is available.`); console.log(` Dev server binding to https://${HOSTNAME}`); console.log(` Also accessible at https://localhost:${DEV_PORT}`); if (port80Free) { const redirectOk = await startHttpRedirect(HOSTNAME, HTTPS_PORT, resolvedIp); if (redirectOk) { console.log(` Also accessible at http://${HOSTNAME} (redirects to HTTPS)`); } else { console.warn(` Failed to start HTTP redirect on ${resolvedIp}:${HTTP_PORT}`); } } else { console.warn(` Port ${HTTP_PORT} on ${resolvedIp} is unavailable; skipping http://${HOSTNAME} redirect.`); } console.log(''); } else if (hostnameOk) { host = HOSTNAME; port = DEV_PORT; console.warn(''); console.warn(` ${HOSTNAME} resolves to ${resolvedIp} but port ${HTTPS_PORT} is unavailable`); console.warn(` (requires elevated privileges or is already in use).`); console.warn(` Dev server binding to https://${HOSTNAME}:${DEV_PORT}`); console.warn(''); } else { host = 'localhost'; port = DEV_PORT; console.warn(''); console.warn(` WARNING: ${HOSTNAME} does not resolve.`); console.warn(` Dev server binding to https://localhost:${DEV_PORT}`); console.warn(''); console.warn(` To use https://${HOSTNAME}, add to ${hostsFilePath()}:`); console.warn(''); console.warn(` 127.1.0.1 ${HOSTNAME} # or your preferred IP`); console.warn(''); console.warn(` See ${SETUP_DOC} for the full list of hostnames.`); console.warn(''); } runNgServe(host, port, extraArgs); } function runNgServe(host, port, extraArgs) { const cwd = path.resolve(__dirname, '..'); const ngCli = path.resolve(cwd, 'node_modules', '@angular', 'cli', 'bin', 'ng.js'); // Pass the hostname (not IP) so Vite resolves it for both HTTPS and HMR websocket. // The hostname resolves to a unique loopback IP via the hosts file, so ports // won't collide with other services. const args = ['serve', '--host', host, '--port', String(port), '--ssl', ...extraArgs]; console.log(` ng serve binding to ${host}:${port}`); const child = spawn(process.execPath, [ngCli, ...args], { stdio: 'inherit', cwd, }); child.on('exit', (code) => process.exit(code ?? 1)); } main();