UNPKG

hytopia

Version:

The HYTOPIA SDK makes it easy for developers to create massively multiplayer games using JavaScript or TypeScript.

398 lines (330 loc) 10.9 kB
#!/usr/bin/env node import { execSync } from 'child_process'; import archiver from 'archiver'; import fs from 'fs'; import path from 'path'; import readline from 'readline'; import { fileURLToPath } from 'url'; // Store command-line flags const flags = {}; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Main function to handle command execution (async () => { const command = process.argv[2]; // Parse command-line flags parseCommandLineFlags(); // Execute the appropriate command const commandHandlers = { 'init': init, 'init-mcp': initMcp, 'package': packageProject }; const handler = commandHandlers[command]; if (handler) { handler(); } else { displayAvailableCommands(command); } })(); /** * Parses command-line flags in the format --flag value */ function parseCommandLineFlags() { for (let i = 3; i < process.argv.length; i += 2) { if (i % 2 === 1) { // Odd indices are flags let flag = process.argv[i].replace('--', ''); let value = process.argv[i + 1]; if (flag.includes('=')) { [ flag, value ] = flag.split('='); } flags[flag] = value; } } } /** * Displays available commands when an unknown command is entered */ function displayAvailableCommands(command) { console.log('Unknown command: ' + command); console.log('Supported commands: init, init-mcp, package'); } /** * Creates a readline interface for user input */ function createReadlineInterface() { return readline.createInterface({ input: process.stdin, output: process.stdout }); } /** * Init command * * Initializes a new HYTOPIA project. Accepting an optional * project name as an argument. * * @example * `bunx hytopia init my-project-name` */ function init() { const destDir = process.cwd(); // Install dependencies installProjectDependencies(); // Initialize project with latest HYTOPIA SDK console.log('🔧 Initializing project with latest HYTOPIA SDK...'); if (flags.template) { initFromTemplate(destDir); } else { initFromBoilerplate(destDir); } // Copy assets into project, not overwriting existing files copyAssets(destDir); // Display success message displayInitSuccessMessage(); // Prompt for MCP setup promptForMcpSetup(); return; } /** * Installs required dependencies for a new project */ function installProjectDependencies() { execSync('bun init --yes'); execSync('bun add hytopia@latest --force'); execSync('bun add @hytopia.com/assets --force'); execSync('bun pm trust mediasoup'); execSync('bun pm trust sharp'); } /** * Initializes a project from a template */ function initFromTemplate(destDir) { console.log(`🖨️ Initializing project with examples template "${flags.template}"...`); execSync('bun add @hytopia.com/examples@latest --force'); const templateDir = path.join(destDir, 'node_modules', '@hytopia.com', 'examples', flags.template); if (!fs.existsSync(templateDir)) { console.error(`❌ Examples template ${flags.template} does not exist in the @hytopia.com/examples package, could not initialize project!`); console.error(` Tried directory: ${templateDir}`); return; } fs.cpSync(templateDir, destDir, { recursive: true }); execSync('bun install'); } /** * Initializes a project from the default boilerplate */ function initFromBoilerplate(destDir) { console.log('🧑‍💻 Initializing project with boilerplate...'); const srcDir = path.join(__dirname, '..', 'boilerplate'); fs.cpSync(srcDir, destDir, { recursive: true }); } /** * Copies assets to the project directory */ function copyAssets(destDir) { fs.cpSync( path.join(destDir, 'node_modules', '@hytopia.com', 'assets'), path.join(destDir, 'assets'), { recursive: true, force: false } ); } /** * Displays success message after project initialization */ function displayInitSuccessMessage() { logDivider(); console.log('✅ HYTOPIA PROJECT INITIALIZED SUCCESSFULLY!'); console.log(' '); console.log('💡 1. Start your development server with: bun --watch index.ts'); console.log('🎮 2. Play your game by opening: https://hytopia.com/play/?join=localhost:8080'); logDivider(); } /** * Prompts the user to set up MCP */ function promptForMcpSetup() { console.log('📋 OPTIONAL: HYTOPIA MCP SETUP'); console.log(' '); console.log('The HYTOPIA MCP enables Cursor and Claude Code editors to access'); console.log('HYTOPIA-specific capabilities, providing significantly better AI'); console.log('assistance and development experience for this HYTOPIA project.'); console.log(' '); const rl = createReadlineInterface(); rl.question('Would you like to initialize the HYTOPIA MCP for this project? (y/n): ', (answer) => { rl.close(); if (answer.trim().toLowerCase() === 'y') { initMcp(); } else { logDivider(); console.log('🎉 You\'re all set! Your HYTOPIA project is ready to use.'); logDivider(); } }); } /** * Initializes the MCP for the selected editors */ function initMcp() { const rl = createReadlineInterface(); logDivider(); console.log('🤖 HYTOPIA MCP SETUP'); console.log('Please select your code editor:'); console.log(' 1. Cursor'); console.log(' 2. Claude Code'); console.log(' 3. Both'); console.log(' 4. None / Cancel'); rl.question('Enter your selection (1-4): ', (answer) => { const selection = parseInt(answer.trim()); if (isNaN(selection) || selection < 1 || selection > 4) { console.log('❌ Invalid selection. Please run `bunx hytopia init-mcp` again and select a number between 1 and 4.'); rl.close(); return; } if ([1, 2, 3].includes(selection)) { logDivider(); } if (selection === 1 || selection === 3) { initEditorMcp('Cursor', 'cursor'); } if (selection === 2 || selection === 3) { initEditorMcp('Claude Code', 'claude'); } rl.close(); if ([1, 2, 3].includes(selection)) { console.log('🎉 You\'re all set! Your HYTOPIA project is ready to use.'); logDivider(); } }); } /** * Initializes MCP for a specific editor */ function initEditorMcp(editorName, editorFlag) { console.log(`🔧 Initializing HYTOPIA MCP for ${editorName}...`); execSync(`bunx topia-mcp@latest init ${editorFlag}`); console.log(`✅ ${editorName} MCP initialized successfully!`); logDivider(); } /** * Package command * * Creates a zip file of the project directory, excluding node_modules, * package-lock.json, bun.lock, and bun.lockb files. * * @example * `bunx hytopia package` */ function packageProject() { const sourceDir = process.cwd(); const projectName = path.basename(sourceDir); const packageJsonPath = path.join(sourceDir, 'package.json'); // Check if package.json exists if (!fs.existsSync(packageJsonPath)) { console.error('❌ Error: package.json not found. This directory does not appear to be a HYTOPIA project.'); console.error(' Please run this command in a valid HYTOPIA project directory.'); return; } // Check if package.json contains "hytopia" try { const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8'); if (!packageJsonContent.includes('hytopia')) { console.error('❌ Error: This directory does not appear to be a HYTOPIA project.'); console.error(' The package.json file does not contain a reference to HYTOPIA.'); return; } } catch (err) { console.error('❌ Error: Could not read package.json file:', err.message); return; } // Make sure assets exist and the model optimizer has been ran const assetsDir = path.join(sourceDir, 'assets'); let hasOptimizedDir = false; if (fs.existsSync(assetsDir)) { // Function to recursively check for .optimized directories const checkForOptimizedDir = (dir) => { const items = fs.readdirSync(dir); for (const item of items) { const itemPath = path.join(dir, item); const stats = fs.statSync(itemPath); if (stats.isDirectory()) { if (item === '.optimized') { hasOptimizedDir = true; return true; } // Check subdirectories if (checkForOptimizedDir(itemPath)) { return true; } } } return false; }; checkForOptimizedDir(assetsDir); if (!hasOptimizedDir) { console.warn('❌ Error: No .optimized directories found in the assets folder.'); console.warn(' Make sure your server has ran the optimizer for your models.'); console.warn(' This can be done by running your server with: bun --watch index.ts'); return; } } else { console.warn('❌ Error: No assets directory found in the project.'); return; } // Prepare to package const outputFile = path.join(sourceDir, `${projectName}.zip`); console.log(`📦 Packaging project "${projectName}"...`); // Create a file to stream archive data to const output = fs.createWriteStream(outputFile); const archive = archiver('zip', { zlib: { level: 9 } // Sets the compression level }); // Listen for all archive data to be written output.on('close', function() { console.log(`✅ Project packaged successfully! (${(archive.pointer() / 1024 / 1024).toFixed(2)} MB)`); console.log(`📁 Package saved to: ${outputFile}`); }); // Good practice to catch warnings (ie stat failures and other non-blocking errors) archive.on('warning', function(err) { if (err.code === 'ENOENT') { console.warn('⚠️ Warning:', err); } else { throw err; } }); // Catch errors archive.on('error', function(err) { console.error('❌ Error during packaging:', err); throw err; }); // Pipe archive data to the file archive.pipe(output); // Get all files and directories in the source directory const items = fs.readdirSync(sourceDir); // Files/directories to exclude const excludeItems = [ '.git', 'node_modules', 'package-lock.json', 'bun.lock', 'bun.lockb', `${projectName}.zip` // Exclude the output file itself ]; // Add each item to the archive, excluding the ones in the exclude list items.forEach(item => { const itemPath = path.join(sourceDir, item); if (!excludeItems.includes(item)) { const stats = fs.statSync(itemPath); if (stats.isDirectory()) { archive.directory(itemPath, item); } else { archive.file(itemPath, { name: item }); } } }); // Finalize the archive archive.finalize(); } /** * Prints a divider line for better console output readability */ function logDivider() { console.log('--------------------------------'); }