Skip to main content
Glama
web-server.tsβ€’6.18 kB
import { spawn, ChildProcess } from 'child_process'; import { existsSync } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { logger } from '@core/logger/index.js'; export interface WebServerConfig { port: number; host?: string; apiUrl?: string; wsUrl?: string; } export class WebServerManager { private config: WebServerConfig; private process: ChildProcess | null = null; private uiPath: string; constructor(config: WebServerConfig) { this.config = config; // Resolve UI path relative to this file const currentFileUrl = import.meta.url; const currentFilePath = fileURLToPath(currentFileUrl); // Check if we're running from dist (compiled) or src (development) const isCompiledVersion = currentFilePath.includes('/dist/'); if (isCompiledVersion) { // When running from dist/src/app/index.cjs, UI is at dist/src/app/ui // The bundled code is at dist/src/app/, so UI is in the same directory this.uiPath = path.resolve(path.dirname(currentFilePath), 'ui'); } else { // When running from src/app/web/web-server.ts, UI is at src/app/ui this.uiPath = path.resolve(path.dirname(currentFilePath), '../ui'); } } async start(): Promise<void> { logger.info( `Starting Web UI server on ${this.config.host || 'localhost'}:${this.config.port}`, null, 'green' ); // Check if UI directory exists if (!existsSync(this.uiPath)) { throw new Error(`UI directory not found at ${this.uiPath}`); } // Check if package.json exists in UI directory const packageJsonPath = path.join(this.uiPath, 'package.json'); if (!existsSync(packageJsonPath)) { throw new Error(`UI package.json not found at ${packageJsonPath}`); } // Check if .next/standalone exists (production build) const standalonePath = path.join(this.uiPath, '.next', 'standalone'); const standaloneServerPath = path.join(standalonePath, 'server.js'); if (existsSync(standaloneServerPath)) { // Use production build await this.startProduction(); } else { // Use development mode await this.startDevelopment(); } } private async startProduction(): Promise<void> { const standalonePath = path.join(this.uiPath, '.next', 'standalone'); const serverPath = path.join(standalonePath, 'server.js'); logger.info('Using production build (standalone)', null, 'cyan'); // Set environment variables const env = { ...process.env, PORT: this.config.port.toString(), HOSTNAME: this.config.host || 'localhost', ...(this.config.apiUrl && { API_URL: this.config.apiUrl }), ...(this.config.wsUrl && { NEXT_PUBLIC_WS_URL: this.config.wsUrl }), // Extract port from API URL for Next.js rewrite rules ...(this.config.apiUrl && { API_PORT: new URL(this.config.apiUrl).port || '3001' }), }; this.process = spawn('node', [serverPath], { cwd: standalonePath, env, stdio: ['pipe', 'pipe', 'pipe'], }); this.setupProcessHandlers(); } private async startDevelopment(): Promise<void> { logger.info('Using development mode', null, 'cyan'); // Check if node_modules exists, install if needed const nodeModulesPath = path.join(this.uiPath, 'node_modules'); if (!existsSync(nodeModulesPath)) { logger.info('Installing UI dependencies...', null, 'yellow'); await this.installDependencies(); } // Set environment variables const env = { ...process.env, PORT: this.config.port.toString(), ...(this.config.apiUrl && { API_URL: this.config.apiUrl }), ...(this.config.wsUrl && { NEXT_PUBLIC_WS_URL: this.config.wsUrl }), // Extract port from API URL for Next.js rewrite rules ...(this.config.apiUrl && { API_PORT: new URL(this.config.apiUrl).port || '3001' }), }; // Start development server with pnpm if available, otherwise npm const packageManager = this.detectPackageManager(); this.process = spawn(packageManager, ['run', 'dev'], { cwd: this.uiPath, env, stdio: ['pipe', 'pipe', 'pipe'], }); this.setupProcessHandlers(); } private detectPackageManager(): string { // Check if pnpm-lock.yaml exists if (existsSync(path.join(this.uiPath, 'pnpm-lock.yaml'))) { return 'pnpm'; } // Check if yarn.lock exists if (existsSync(path.join(this.uiPath, 'yarn.lock'))) { return 'yarn'; } // Default to npm return 'npm'; } private installDependencies(): Promise<void> { const packageManager = this.detectPackageManager(); return new Promise((resolve, reject) => { const installProcess = spawn(packageManager, ['install'], { cwd: this.uiPath, stdio: ['pipe', 'pipe', 'pipe'], }); installProcess.stdout?.on('data', data => { logger.debug(`${packageManager} install: ${data}`); }); installProcess.stderr?.on('data', data => { logger.debug(`${packageManager} install error: ${data}`); }); installProcess.on('close', code => { if (code === 0) { logger.info('UI dependencies installed successfully', null, 'green'); resolve(); } else { reject(new Error(`${packageManager} install failed with code ${code}`)); } }); installProcess.on('error', error => { reject(new Error(`Failed to install dependencies: ${error.message}`)); }); }); } private setupProcessHandlers(): void { if (!this.process) return; this.process.stdout?.on('data', data => { const output = data.toString().trim(); if (output) { logger.info(`[UI] ${output}`, null, 'cyan'); } }); this.process.stderr?.on('data', data => { const output = data.toString().trim(); if (output && !output.includes('warn')) { logger.warn(`[UI] ${output}`, null, 'yellow'); } }); this.process.on('close', code => { if (code !== 0) { logger.error(`Web UI server exited with code ${code}`); } else { logger.info('Web UI server stopped', null, 'gray'); } }); this.process.on('error', error => { logger.error(`Web UI server error: ${error.message}`); }); } stop(): void { if (this.process) { logger.info('Stopping Web UI server...', null, 'yellow'); this.process.kill('SIGTERM'); this.process = null; } } isRunning(): boolean { return this.process !== null && !this.process.killed; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/campfirein/cipher'

If you have feedback or need assistance with the MCP directory API, please join our Discord server