UNPKG

onilib

Version:

A modular Node.js library for real-time online integration in games and web applications

1,001 lines (844 loc) โ€ข 27.2 kB
#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const { spawn } = require('child_process'); const { promisify } = require('util'); const writeFile = promisify(fs.writeFile); const mkdir = promisify(fs.mkdir); const access = promisify(fs.access); class NoiCLI { constructor() { this.commands = { init: this.init.bind(this), start: this.start.bind(this), test: this.test.bind(this), build: this.build.bind(this), help: this.help.bind(this) }; } async run() { const args = process.argv.slice(2); const command = args[0] || 'help'; const commandArgs = args.slice(1); if (!this.commands[command]) { console.error(`โŒ Unknown command: ${command}`); this.help(); process.exit(1); } try { await this.commands[command](commandArgs); } catch (error) { console.error(`โŒ Error executing command '${command}':`, error.message); process.exit(1); } } async init(args) { const projectName = args[0] || path.basename(process.cwd()); const projectPath = process.cwd(); console.log(`๐Ÿš€ Initializing ONILib project: ${projectName}`); // Check if already initialized try { await access(path.join(projectPath, 'noi.config.js')); console.log('โš ๏ธ Project already initialized (noi.config.js exists)'); return; } catch { // File doesn't exist, continue with initialization } // Create directory structure const directories = [ 'src', 'src/modules', 'test', 'examples', 'data' ]; for (const dir of directories) { const dirPath = path.join(projectPath, dir); try { await mkdir(dirPath, { recursive: true }); console.log(`๐Ÿ“ Created directory: ${dir}`); } catch (error) { if (error.code !== 'EEXIST') { throw error; } } } // Create configuration file await this.createConfigFile(projectPath, projectName); // Create main application file await this.createMainFile(projectPath); // Create example client await this.createExampleClient(projectPath); // Create package.json if it doesn't exist await this.createPackageJson(projectPath, projectName); // Create README await this.createReadme(projectPath, projectName); // Create .gitignore await this.createGitignore(projectPath); console.log('โœ… Project initialized successfully!'); console.log('\n๐Ÿ“‹ Next steps:'); console.log(' 1. npm install'); console.log(' 2. onilib start'); console.log(' 3. Open examples/client.html in your browser'); } async createConfigFile(projectPath, projectName) { const configContent = `// ONILib Configuration module.exports = { // Application settings name: '${projectName}', environment: process.env.NODE_ENV || 'development', // Server configuration port: process.env.PORT || 3000, host: process.env.HOST || '0.0.0.0', // Authentication auth: { jwtSecret: process.env.JWT_SECRET || 'your-secret-key-change-in-production', jwtExpiresIn: '24h', apiKeys: [ process.env.API_KEY || 'noi_default_api_key_change_me' ] }, // WebSocket/Realtime realtime: { port: process.env.WS_PORT || 8080, maxConnections: 1000, heartbeatInterval: 30000, authRequired: true }, // Storage storage: { type: process.env.STORAGE_TYPE || 'sqlite', path: process.env.STORAGE_PATH || './data/app.db' }, // Matchmaking matchmaking: { maxPlayersPerMatch: 2, matchTimeout: 30000, queueTimeout: 300000, enableSkillMatching: false }, // P2P/WebRTC p2p: { enableRelay: false, maxPeersPerRoom: 8, iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' } ] }, // Admin panel admin: { port: process.env.ADMIN_PORT || 3001, authRequired: true, enableCors: true } }; `; await writeFile(path.join(projectPath, 'noi.config.js'), configContent); console.log('๐Ÿ“„ Created noi.config.js'); } async createMainFile(projectPath) { const mainContent = `// Main application entry point const { NodeOnlineIntegration } = require('onilib'); const fs = require('fs'); const path = require('path'); // Try to load config file, fallback to default config let config; try { config = require('./noi.config.js'); } catch (error) { console.log('โš ๏ธ Config file not found, using default configuration'); config = { name: 'onilib-project', environment: 'development', port: 3000, auth: { jwtSecret: 'default-secret-change-in-production', jwtExpiresIn: '24h', apiKeys: ['default-api-key'] }, realtime: { port: 8080, maxConnections: 1000, heartbeatInterval: 30000, authRequired: false }, storage: { type: 'sqlite', path: './data/app.db' }, matchmaking: { maxPlayersPerMatch: 2, matchTimeout: 30000, queueTimeout: 300000 }, p2p: { enableRelay: false, maxPeersPerRoom: 8, iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }, admin: { port: 3001, authRequired: false, enableCors: true } }; } async function main() { // Create NOI instance const noi = new NodeOnlineIntegration(config); // Start the server await noi.start(); // Get modules for custom logic const realtime = noi.getModule('realtime'); const matchmaking = noi.getModule('matchmaking'); const p2p = noi.getModule('p2p'); // Custom message handlers realtime.registerHandler('custom_message', (client, message) => { console.log('Custom message received:', message); // Echo back to sender realtime.sendToClient(client, { type: 'custom_response', data: { echo: message.data, timestamp: Date.now() } }); }); // Matchmaking events matchmaking.on('match:created', (match) => { console.log('Match created:', match.id); }); matchmaking.on('match:started', (match) => { console.log('Match started:', match.id); // Send game start data to all players for (const player of match.players) { realtime.sendToClient(player.client, { type: 'game_start', data: { matchId: match.id, gameMode: 'default', players: match.players.map(p => ({ id: p.id, skill: p.skill })) } }); } }); // P2P events p2p.on('peer:joined_room', ({ peer, roomId }) => { console.log('Peer joined P2P room:', peer.id, roomId); }); // Graceful shutdown process.on('SIGINT', async () => { console.log('๐Ÿ›‘ Shutting down...'); await noi.stop(); process.exit(0); }); process.on('SIGTERM', async () => { console.log('๐Ÿ›‘ Shutting down...'); await noi.stop(); process.exit(0); }); } // Start the application if (require.main === module) { main().catch(console.error); } module.exports = { main }; `; await writeFile(path.join(projectPath, 'src', 'index.js'), mainContent); console.log('๐Ÿ“„ Created src/index.js'); } async createExampleClient(projectPath) { const clientContent = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ONILib Example Client</title> <style> body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background-color: #f5f5f5; } .container { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .status { padding: 10px; margin: 10px 0; border-radius: 4px; font-weight: bold; } .connected { background-color: #d4edda; color: #155724; } .disconnected { background-color: #f8d7da; color: #721c24; } .authenticated { background-color: #cce7ff; color: #004085; } button { background-color: #007bff; color: white; border: none; padding: 10px 20px; margin: 5px; border-radius: 4px; cursor: pointer; } button:hover { background-color: #0056b3; } button:disabled { background-color: #6c757d; cursor: not-allowed; } .log { background-color: #f8f9fa; border: 1px solid #dee2e6; padding: 10px; height: 300px; overflow-y: auto; font-family: monospace; font-size: 12px; } input, select { padding: 8px; margin: 5px; border: 1px solid #ddd; border-radius: 4px; } .section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 4px; } </style> </head> <body> <div class="container"> <h1>๐Ÿš€ ONILib - Example Client</h1> <div class="section"> <h3>Connection</h3> <div id="status" class="status disconnected">Disconnected</div> <input type="text" id="wsUrl" value="ws://localhost:8080" placeholder="WebSocket URL"> <input type="text" id="apiKey" value="noi_default_api_key_change_me" placeholder="API Key"> <button id="connectBtn" onclick="connect()">Connect</button> <button id="disconnectBtn" onclick="disconnect()" disabled>Disconnect</button> </div> <div class="section"> <h3>Rooms</h3> <input type="text" id="roomId" value="test-room" placeholder="Room ID"> <button id="joinRoomBtn" onclick="joinRoom()" disabled>Join Room</button> <button id="leaveRoomBtn" onclick="leaveRoom()" disabled>Leave Room</button> </div> <div class="section"> <h3>Matchmaking</h3> <select id="queueType"> <option value="default">Default Queue</option> <option value="ranked">Ranked Queue</option> </select> <input type="number" id="skillLevel" value="1000" placeholder="Skill Level"> <button id="joinQueueBtn" onclick="joinQueue()" disabled>Join Queue</button> <button id="leaveQueueBtn" onclick="leaveQueue()" disabled>Leave Queue</button> </div> <div class="section"> <h3>P2P</h3> <input type="text" id="p2pRoomId" value="p2p-room" placeholder="P2P Room ID"> <button id="joinP2PBtn" onclick="joinP2PRoom()" disabled>Join P2P Room</button> <button id="leaveP2PBtn" onclick="leaveP2PRoom()" disabled>Leave P2P Room</button> </div> <div class="section"> <h3>Messages</h3> <input type="text" id="messageInput" placeholder="Type a message..."> <button onclick="sendMessage()" disabled id="sendBtn">Send Message</button> </div> <div class="section"> <h3>Log</h3> <div id="log" class="log"></div> <button onclick="clearLog()">Clear Log</button> </div> </div> <script> let ws = null; let connected = false; let authenticated = false; let currentRoom = null; let currentQueue = null; let currentP2PRoom = null; function log(message) { const logDiv = document.getElementById('log'); const timestamp = new Date().toLocaleTimeString(); logDiv.innerHTML += \`[\${timestamp}] \${message}\n\`; logDiv.scrollTop = logDiv.scrollHeight; } function updateStatus() { const statusDiv = document.getElementById('status'); const connectBtn = document.getElementById('connectBtn'); const disconnectBtn = document.getElementById('disconnectBtn'); const buttons = ['joinRoomBtn', 'leaveRoomBtn', 'joinQueueBtn', 'leaveQueueBtn', 'joinP2PBtn', 'leaveP2PBtn', 'sendBtn']; if (connected && authenticated) { statusDiv.textContent = 'Connected & Authenticated'; statusDiv.className = 'status authenticated'; connectBtn.disabled = true; disconnectBtn.disabled = false; buttons.forEach(id => document.getElementById(id).disabled = false); } else if (connected) { statusDiv.textContent = 'Connected (Not Authenticated)'; statusDiv.className = 'status connected'; connectBtn.disabled = true; disconnectBtn.disabled = false; buttons.forEach(id => document.getElementById(id).disabled = true); } else { statusDiv.textContent = 'Disconnected'; statusDiv.className = 'status disconnected'; connectBtn.disabled = false; disconnectBtn.disabled = true; buttons.forEach(id => document.getElementById(id).disabled = true); } } function connect() { const url = document.getElementById('wsUrl').value; const apiKey = document.getElementById('apiKey').value; ws = new WebSocket(url); ws.onopen = () => { connected = true; log('๐Ÿ”— Connected to WebSocket'); updateStatus(); // Authenticate with API key ws.send(JSON.stringify({ type: 'auth', data: { strategy: 'apikey', credentials: { apiKey } } })); }; ws.onmessage = (event) => { const message = JSON.parse(event.data); handleMessage(message); }; ws.onclose = () => { connected = false; authenticated = false; log('โŒ Disconnected from WebSocket'); updateStatus(); }; ws.onerror = (error) => { log('โŒ WebSocket error: ' + error); }; } function disconnect() { if (ws) { ws.close(); } } function handleMessage(message) { log('๐Ÿ“จ Received: ' + JSON.stringify(message, null, 2)); switch (message.type) { case 'auth_success': authenticated = true; log('โœ… Authentication successful'); updateStatus(); break; case 'error': log('โŒ Error: ' + message.error); break; case 'room_joined': currentRoom = message.data.roomId; log('๐Ÿ  Joined room: ' + currentRoom); break; case 'room_left': log('๐Ÿšช Left room: ' + currentRoom); currentRoom = null; break; case 'queue_joined': currentQueue = message.data.queueType; log('โณ Joined queue: ' + currentQueue); break; case 'queue_left': log('๐Ÿšช Left queue: ' + currentQueue); currentQueue = null; break; case 'match_found': log('๐ŸŽฏ Match found! Match ID: ' + message.data.matchId); // Auto-accept match for demo setTimeout(() => { ws.send(JSON.stringify({ type: 'accept_match', data: { matchId: message.data.matchId } })); }, 1000); break; case 'match_started': log('๐ŸŽฎ Match started! Room: ' + message.data.roomId); break; case 'p2p_room_joined': currentP2PRoom = message.data.roomId; log('๐Ÿ”— Joined P2P room: ' + currentP2PRoom); break; case 'p2p_room_left': log('๐Ÿšช Left P2P room: ' + currentP2PRoom); currentP2PRoom = null; break; } } function joinRoom() { const roomId = document.getElementById('roomId').value; ws.send(JSON.stringify({ type: 'join_room', data: { roomId } })); } function leaveRoom() { if (currentRoom) { ws.send(JSON.stringify({ type: 'leave_room', data: { roomId: currentRoom } })); } } function joinQueue() { const queueType = document.getElementById('queueType').value; const skill = parseInt(document.getElementById('skillLevel').value); ws.send(JSON.stringify({ type: 'join_queue', data: { queueType, criteria: { skill } } })); } function leaveQueue() { if (currentQueue) { ws.send(JSON.stringify({ type: 'leave_queue', data: { queueType: currentQueue } })); } } function joinP2PRoom() { const roomId = document.getElementById('p2pRoomId').value; ws.send(JSON.stringify({ type: 'p2p_join_room', data: { roomId } })); } function leaveP2PRoom() { if (currentP2PRoom) { ws.send(JSON.stringify({ type: 'p2p_leave_room', data: { roomId: currentP2PRoom } })); } } function sendMessage() { const input = document.getElementById('messageInput'); const message = input.value.trim(); if (message) { ws.send(JSON.stringify({ type: 'custom_message', data: { text: message } })); input.value = ''; } } function clearLog() { document.getElementById('log').innerHTML = ''; } // Allow Enter key to send messages document.getElementById('messageInput').addEventListener('keypress', (e) => { if (e.key === 'Enter') { sendMessage(); } }); // Initialize updateStatus(); log('๐Ÿš€ ONILib Example Client loaded'); </script> </body> </html> `; await writeFile(path.join(projectPath, 'examples', 'client.html'), clientContent); console.log('๐Ÿ“„ Created examples/client.html'); } async createPackageJson(projectPath, projectName) { const packageJsonPath = path.join(projectPath, 'package.json'); try { await access(packageJsonPath); console.log('๐Ÿ“ฆ package.json already exists, skipping...'); return; } catch { // File doesn't exist, create it } const packageContent = { name: projectName, version: '1.0.0', description: 'ONILib project', main: 'src/index.js', scripts: { start: 'node src/index.js', dev: 'nodemon src/index.js', test: 'jest', build: 'echo "Build completed"' }, dependencies: { 'onilib': '^1.0.0' }, devDependencies: { nodemon: '^3.0.1', jest: '^29.7.0' }, keywords: ['websocket', 'webrtc', 'realtime', 'gaming'], author: '', license: 'MIT' }; await writeFile(packageJsonPath, JSON.stringify(packageContent, null, 2)); console.log('๐Ÿ“ฆ Created package.json'); } async createReadme(projectPath, projectName) { const readmeContent = `# ${projectName} An ONILib project for real-time applications. ## Features - ๐Ÿ” **Authentication**: JWT and API Key support - ๐Ÿ”„ **Real-time Communication**: WebSocket server with rooms - ๐ŸŽฎ **Matchmaking**: Queue-based player matching - ๐Ÿ”— **P2P Support**: WebRTC signaling for peer-to-peer connections - ๐Ÿ’พ **Storage**: Flexible storage adapters (Memory, SQLite, PostgreSQL) - ๐Ÿ“Š **Admin Panel**: REST API for monitoring and management ## Quick Start 1. **Install dependencies**: \`\`\`bash npm install \`\`\` 2. **Start the server**: \`\`\`bash npm start # or for development with auto-reload: npm run dev \`\`\` 3. **Test the client**: Open \`examples/client.html\` in your browser and connect to the WebSocket server. ## Configuration Edit \`noi.config.js\` to customize your application settings: - **Authentication**: Configure JWT secrets and API keys - **WebSocket**: Set ports and connection limits - **Storage**: Choose between memory, SQLite, or PostgreSQL - **Matchmaking**: Configure queue settings and match criteria - **P2P**: Set up WebRTC STUN/TURN servers ## API Endpoints ### WebSocket Messages - \`auth\` - Authenticate with API key or JWT - \`join_room\` - Join a chat room - \`leave_room\` - Leave a chat room - \`join_queue\` - Join matchmaking queue - \`leave_queue\` - Leave matchmaking queue - \`p2p_join_room\` - Join P2P room for WebRTC ### Admin REST API Access the admin panel at \`http://localhost:3001\`: - \`GET /health\` - Health check - \`GET /api/system/stats\` - System statistics - \`GET /api/realtime/clients\` - Connected clients - \`GET /api/matchmaking/queues\` - Active queues - \`GET /api/p2p/rooms\` - P2P rooms ## Development ### Running Tests \`\`\`bash npm test \`\`\` ### CLI Commands - \`onilib init\` - Initialize a new project - \`onilib start\` - Start the server - \`onilib test\` - Run tests - \`onilib build\` - Build for production ## Environment Variables - \`NODE_ENV\` - Environment (development/production) - \`PORT\` - HTTP server port - \`WS_PORT\` - WebSocket server port - \`JWT_SECRET\` - JWT signing secret - \`API_KEY\` - Default API key - \`STORAGE_TYPE\` - Storage adapter (memory/sqlite/postgres) - \`STORAGE_PATH\` - Database file path (for SQLite) ## License MIT `; await writeFile(path.join(projectPath, 'README.md'), readmeContent); console.log('๐Ÿ“„ Created README.md'); } async createGitignore(projectPath) { const gitignoreContent = `# Dependencies node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Coverage directory used by tools like istanbul coverage/ # nyc test coverage .nyc_output # Grunt intermediate storage .grunt # Bower dependency directory bower_components # node-waf configuration .lock-wscript # Compiled binary addons build/Release # Dependency directories node_modules/ jspm_packages/ # Optional npm cache directory .npm # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test .env.production .env.local # parcel-bundler cache .cache .parcel-cache # next.js build output .next # nuxt.js build output .nuxt # vuepress build output .vuepress/dist # Serverless directories .serverless # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Data directory data/ *.db *.sqlite *.sqlite3 # Logs logs/ *.log # IDE .vscode/ .idea/ *.swp *.swo *~ # OS .DS_Store .DS_Store? ._* .Spotlight-V100 .Trashes ehthumbs.db Thumbs.db `; await writeFile(path.join(projectPath, '.gitignore'), gitignoreContent); console.log('๐Ÿ“„ Created .gitignore'); } async start(args) { console.log('๐Ÿš€ Starting ONILib server...'); const isDev = args.includes('--dev') || args.includes('-d'); const command = isDev ? 'npm run dev' : 'npm start'; const child = spawn(command, [], { stdio: 'inherit', shell: true, cwd: process.cwd() }); child.on('error', (error) => { console.error('โŒ Failed to start server:', error.message); process.exit(1); }); child.on('exit', (code) => { if (code !== 0) { console.error(`โŒ Server exited with code ${code}`); process.exit(code); } }); } async test(args) { console.log('๐Ÿงช Running tests...'); const testType = args[0]; let command = 'npm test'; if (testType === 'p2p') { const nodes = args.find(arg => arg.startsWith('--nodes'))?.split('=')[1] || '2'; const duration = args.find(arg => arg.startsWith('--duration'))?.split('=')[1] || '30s'; console.log(`๐Ÿ”— Running P2P test with ${nodes} nodes for ${duration}`); command = `node test/p2p-test.js --nodes=${nodes} --duration=${duration}`; } const child = spawn(command, [], { stdio: 'inherit', shell: true, cwd: process.cwd() }); child.on('error', (error) => { console.error('โŒ Failed to run tests:', error.message); process.exit(1); }); child.on('exit', (code) => { if (code === 0) { console.log('โœ… All tests passed!'); } else { console.error(`โŒ Tests failed with code ${code}`); process.exit(code); } }); } async build(args) { console.log('๐Ÿ”จ Building project...'); const child = spawn('npm run build', [], { stdio: 'inherit', shell: true, cwd: process.cwd() }); child.on('error', (error) => { console.error('โŒ Build failed:', error.message); process.exit(1); }); child.on('exit', (code) => { if (code === 0) { console.log('โœ… Build completed successfully!'); } else { console.error(`โŒ Build failed with code ${code}`); process.exit(code); } }); } help() { console.log(` ๐Ÿš€ ONILib CLI (onilib) Usage: onilib <command> [options] Commands: init [name] Initialize a new ONILib project start [--dev] Start the server (--dev for development mode) test [type] Run tests test p2p --nodes=N --duration=Xs Run P2P stress test build Build the project for production help Show this help message Examples: onilib init my-game-server onilib start --dev onilib test onilib test p2p --nodes=10 --duration=60s onilib build For more information, visit: https://github.com/yourusername/onilib `); } } // Run CLI if called directly if (require.main === module) { const cli = new NoiCLI(); cli.run(); } module.exports = { NoiCLI };