accs-cli
Version:
ACCS CLI — Full-featured developer tool for scaffolding, running, building, and managing multi-language projects
266 lines (236 loc) • 7.93 kB
JavaScript
/**
* Serve command - Local development server
*/
import express from 'express';
import path from 'path';
import chalk from 'chalk';
import { logger } from '../utils/logger.js';
import { FileUtils } from '../utils/file-utils.js';
import { configManager } from '../config/config-manager.js';
import { createServer } from 'http';
import os from 'os';
export function serveCommand(program) {
program
.command('serve')
.option('-p, --port <port>', 'Port to serve on', configManager.get('defaultPort', 3000))
.option('-d, --dir <directory>', 'Directory to serve', 'dist')
.option('-h, --host <host>', 'Host to serve on (e.g., 0.0.0.0 for all interfaces)', 'localhost')
.option('-o, --open', 'Open in browser', true)
.option('-v, --verbose', 'Verbose logging')
.description('Serve the built project locally')
.action(async (options) => {
try {
await serveProject(options);
} catch (error) {
logger.error('Failed to start server:', error.message);
process.exit(1);
}
});
}
async function serveProject(options) {
const projectRoot = FileUtils.getProjectRoot();
const serveDir = path.join(projectRoot, options.dir);
const port = options.port;
const host = options.host;
// Validate port
if (isNaN(port) || port < 1 || port > 65535) {
throw new Error('Invalid port number');
}
// Check if directory exists
if (!FileUtils.exists(serveDir)) {
logger.warn(`Directory "${options.dir}" doesn't exist. Creating it...`);
await FileUtils.createDir(serveDir);
// Create a simple index.html if directory is empty
const indexPath = path.join(serveDir, 'index.html');
const indexContent = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ACCS Server</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
max-width: 800px;
margin: 2rem auto;
padding: 2rem;
background: #f5f5f5;
}
.container {
background: white;
padding: 3rem;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
text-align: center;
}
h1 { color: #333; margin-bottom: 1rem; }
p { color: #666; line-height: 1.6; }
.code { background: #f8f9fa; padding: 0.5rem; border-radius: 4px; font-family: monospace; }
</style>
</head>
<body>
<div class="container">
<h1>🚀 ACCS Development Server</h1>
<p>Your server is running successfully!</p>
<p>To get started, add your files to the <span class="code">${options.dir}</span> directory.</p>
<p>Run <span class="code">accs build</span> to build your project.</p>
</div>
</body>
</html>`;
await import('fs').then(fs =>
fs.promises.writeFile(indexPath, indexContent, 'utf8')
);
}
const app = express();
// Middleware for logging
if (options.verbose) {
app.use((req, res, next) => {
const timestamp = new Date().toLocaleTimeString();
logger.info(`[${timestamp}] ${req.method} ${req.url}`);
next();
});
}
// Serve static files
app.use(express.static(serveDir, {
extensions: ['html', 'htm'],
index: ['index.html', 'index.htm']
}));
// API endpoint for server info
app.get('/__server_info__', (req, res) => {
res.json({
name: 'ACCS Development Server',
version: '1.0.0',
directory: serveDir,
port: port,
uptime: process.uptime()
});
});
// 404 handler
app.use((req, res) => {
const html404 = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 - Not Found</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
background: #f5f5f5;
}
.error-container {
text-align: center;
background: white;
padding: 3rem;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
h1 { color: #e74c3c; margin-bottom: 1rem; }
p { color: #666; }
a { color: #3498db; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="error-container">
<h1>404 - File Not Found</h1>
<p>The requested file <code>${req.url}</code> could not be found.</p>
<p><a href="/">← Back to Home</a></p>
</div>
</body>
</html>`;
res.status(404).send(html404);
});
// Error handler
app.use((err, req, res) => {
logger.error('Server error:', err.message);
res.status(500).json({ error: 'Internal Server Error' });
});
// Start server
const server = app.listen(port, host, () => {
let displayHost = host;
if (host === '0.0.0.0') {
const networkInterfaces = os.networkInterfaces();
const addresses = [];
for (const interfaceName in networkInterfaces) {
for (const iface of networkInterfaces[interfaceName]) {
if (iface.family === 'IPv4' && !iface.internal) {
addresses.push(iface.address);
}
}
}
if (addresses.length > 0) {
logger.success(`Server running on all network interfaces. Access via:`);
addresses.forEach(addr => {
logger.success(` ${chalk.cyan(`http://${addr}:${port}`)}`);
});
displayHost = addresses[0]; // Use the first address for opening in browser
} else {
logger.warn(`Server running on 0.0.0.0 but no network interfaces found. Access via: ${chalk.cyan(`http://localhost:${port}`)}`);
displayHost = 'localhost';
}
} else {
logger.success(`Server running at ${chalk.cyan(`http://${displayHost}:${port}`)}`);
}
logger.info(`Serving directory: ${chalk.yellow(path.relative(process.cwd(), serveDir))}`);
logger.separator();
logger.info('Press Ctrl+C to stop the server');
// Open browser if requested
if (options.open) {
openBrowser(`http://${displayHost}:${port}`);
}
});
// Graceful shutdown
const shutdown = (signal) => {
logger.info(`\nReceived ${signal}, shutting down gracefully...`);
server.close(() => {
logger.success('Server closed');
process.exit(0);
});
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
// Handle server errors
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
logger.error(`Port ${port} is already in use`);
suggestAlternativePort(port);
} else {
logger.error('Server error:', err.message);
}
process.exit(1);
});
}
async function openBrowser(url) {
try {
const { default: open } = await import('open');
await open(url);
logger.success('Browser opened automatically');
} catch (error) {
logger.warn('Could not open browser automatically');
logger.info(`Manual: ${url}`);
}
}
async function suggestAlternativePort(currentPort) {
logger.info('Trying to find an available port...');
for (let port = currentPort + 1; port <= currentPort + 10; port++) {
if (await isPortAvailable(port)) {
logger.info(`Try using port ${port}: ${chalk.cyan(`accs serve --port ${port}`)}`);
break;
}
}
}
function isPortAvailable(port) {
return new Promise((resolve) => {
const server = createServer();
server.listen(port, () => {
server.close(() => resolve(true));
});
server.on('error', () => resolve(false));
});
}