#!/usr/bin/env bun
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { serve } from 'bun';
/**
* @fileoverview Simple static file server for documentation using Bun.
* Serves the TypeDoc generated documentation with proper MIME types.
*
* @author VyOS MCP Server
* @version 1.0.0
* @since 2025-01-13
*/
const DOCS_DIR = './docs';
const PORT = Number(process.env.PORT) || 8080;
const HOST = process.env.HOST || 'localhost';
// MIME type mapping for common file extensions
const MIME_TYPES: Record<string, string> = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml; charset=utf-8',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject',
'.txt': 'text/plain; charset=utf-8',
'.md': 'text/markdown; charset=utf-8',
};
/**
* Get MIME type for a file extension
*/
function getMimeType(filePath: string): string {
const ext = filePath.toLowerCase().substring(filePath.lastIndexOf('.'));
return MIME_TYPES[ext] || 'text/plain; charset=utf-8';
}
/**
* Resolve file path and handle directory requests
*/
function resolvePath(url: string): string {
let filePath = url === '/' ? '/index.html' : url;
// Remove query parameters
const queryIndex = filePath.indexOf('?');
if (queryIndex !== -1) {
filePath = filePath.substring(0, queryIndex);
}
// Remove fragment
const fragmentIndex = filePath.indexOf('#');
if (fragmentIndex !== -1) {
filePath = filePath.substring(0, fragmentIndex);
}
// Normalize path and prevent directory traversal
filePath = filePath.replace(/\\/g, '/').replace(/\/+/g, '/');
if (filePath.includes('..')) {
return '/index.html';
}
const fullPath = join(DOCS_DIR, filePath);
// If it's a directory, try to serve index.html
if (existsSync(fullPath) && Bun.file(fullPath).type === 'directory') {
return join(filePath, 'index.html');
}
return filePath;
}
/**
* Create 404 error response
*/
function create404Response(): Response {
const html = `
<!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: Arial, sans-serif; margin: 40px; text-align: center; }
.error { color: #e74c3c; }
.back-link { margin-top: 20px; }
a { color: #3498db; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1 class="error">404 - Page Not Found</h1>
<p>The requested documentation page could not be found.</p>
<div class="back-link">
<a href="/">โ Back to Documentation Home</a>
</div>
</body>
</html>`;
return new Response(html, {
status: 404,
headers: {
'Content-Type': 'text/html; charset=utf-8',
},
});
}
/**
* Main server function
*/
const server = serve({
port: PORT,
hostname: HOST,
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
const filePath = resolvePath(url.pathname);
const fullPath = join(DOCS_DIR, filePath);
try {
const file = Bun.file(fullPath);
// Check if file exists
if (!(await file.exists())) {
console.log(`๐ 404: ${url.pathname} (${fullPath})`);
return create404Response();
}
const mimeType = getMimeType(filePath);
const content = await file.arrayBuffer();
console.log(
`๐ 200: ${url.pathname} (${(content.byteLength / 1024).toFixed(1)}KB)`,
);
return new Response(content, {
headers: {
'Content-Type': mimeType,
'Cache-Control': 'public, max-age=3600', // 1 hour cache
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
},
});
} catch (error) {
console.error(`โ Error serving ${url.pathname}:`, error);
return new Response('Internal Server Error', {
status: 500,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
});
}
},
error(error: Error): Response {
console.error('โ Server error:', error);
return new Response('Internal Server Error', {
status: 500,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
});
},
});
// Check if docs directory exists
if (!existsSync(DOCS_DIR)) {
console.error(`โ Documentation directory '${DOCS_DIR}' not found!`);
console.log('๐ก Run "bun run docs" first to generate documentation.');
process.exit(1);
}
console.log(`๐ VyOS MCP Documentation Server`);
console.log(`๐ Server running at: http://${HOST}:${PORT}`);
console.log(`๐ Serving files from: ${DOCS_DIR}`);
console.log(`๐ Press Ctrl+C to stop`);
// Graceful shutdown
process.on('SIGINT', () => {
console.log('\n๐ Shutting down documentation server...');
server.stop();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\n๐ Shutting down documentation server...');
server.stop();
process.exit(0);
});