UNPKG

ursamu-mud

Version:

Ursamu - Modular MUD Engine with sandboxed scripting and plugin system

808 lines • 32.7 kB
/** * Project and configuration commands */ import { readFile, writeFile, mkdir, access } from 'fs/promises'; import { resolve, join } from 'path'; import inquirer from 'inquirer'; import chalk from 'chalk'; import { BaseCommand } from './BaseCommand.js'; import { RestClient } from '../clients/RestClient.js'; const DEFAULT_PROJECT_CONFIG = { name: 'mud-project', version: '1.0.0', description: 'MUD game project', author: 'Lemuel Canady', serverUrl: 'http://localhost:8080', debuggerUrl: 'ws://localhost:9090', scriptsPath: './scripts', templatesPath: './templates', buildPath: './build', dependencies: [] }; export class ProjectCommands extends BaseCommand { restClient; constructor(config) { super(config); this.restClient = new RestClient(config); } register(program) { const project = program .command('project') .description('Project management commands'); // Initialize new project project .command('init') .description('Initialize new MUD project') .argument('[name]', 'project name') .option('-t, --template <type>', 'project template (basic, advanced)', 'basic') .option('--skip-deps', 'skip dependency installation') .option('--force', 'overwrite existing project') .action(async (name, options) => { await this.handleCommand(() => this.initProject(name, options)); }); // Show project info project .command('info') .description('Show project information') .option('-v, --verbose', 'show detailed information') .action(async (options) => { await this.handleCommand(() => this.showProjectInfo(options)); }); // Validate project project .command('validate') .description('Validate project configuration') .option('--fix', 'attempt to fix issues') .action(async (options) => { await this.handleCommand(() => this.validateProject(options)); }); // Build project project .command('build') .description('Build project scripts') .option('-w, --watch', 'watch for changes and rebuild') .option('-o, --output <path>', 'output directory') .option('--minify', 'minify output scripts') .action(async (options) => { await this.handleCommand(() => this.buildProject(options)); }); // Clean project project .command('clean') .description('Clean build artifacts') .option('--all', 'clean all generated files') .action(async (options) => { await this.handleCommand(() => this.cleanProject(options)); }); // Deploy project project .command('deploy') .description('Deploy project to server') .option('-e, --env <environment>', 'deployment environment', 'development') .option('--dry-run', 'simulate deployment without making changes') .action(async (options) => { await this.handleCommand(() => this.deployProject(options)); }); // Configure project project .command('config') .description('Configure project settings') .argument('[key]', 'configuration key') .argument('[value]', 'configuration value') .option('-l, --list', 'list all configuration') .option('-g, --global', 'use global configuration') .action(async (key, value, options) => { await this.handleCommand(() => this.configureProject(key, value, options)); }); // Generate project documentation project .command('docs') .description('Generate project documentation') .option('-o, --output <path>', 'output directory', './docs') .option('-f, --format <type>', 'documentation format (html, md)', 'html') .action(async (options) => { await this.handleCommand(() => this.generateDocs(options)); }); // Health check project .command('health') .description('Check project and server health') .option('-v, --verbose', 'verbose health information') .action(async (options) => { await this.handleCommand(() => this.healthCheck(options)); }); // Backup project project .command('backup') .description('Create project backup') .option('-o, --output <path>', 'backup file path') .option('--include-build', 'include build artifacts') .action(async (options) => { await this.handleCommand(() => this.backupProject(options)); }); // Restore project project .command('restore') .description('Restore project from backup') .argument('<backup-file>', 'backup file path') .option('--force', 'overwrite existing files') .action(async (backupFile, options) => { await this.handleCommand(() => this.restoreProject(backupFile, options)); }); } async initProject(name, options = {}) { const spinner = this.startSpinner('Initializing project...'); try { // Get project details if (!name) { this.infoSpinner(); const answers = await inquirer.prompt([ { type: 'input', name: 'projectName', message: 'Project name:', default: 'mud-project', validate: (input) => { if (!input.trim()) return 'Project name is required'; if (!/^[a-zA-Z0-9_-]+$/.test(input)) return 'Invalid project name format'; return true; } }, { type: 'input', name: 'description', message: 'Project description:', default: 'A new MUD game project' }, { type: 'input', name: 'author', message: 'Author:', default: process.env.USER || 'MUD Developer' }, { type: 'list', name: 'template', message: 'Project template:', choices: ['basic', 'advanced', 'combat-focused', 'social-focused'], default: options.template || 'basic' } ]); name = answers.projectName; options = { ...options, ...answers }; } this.startSpinner('Creating project structure...'); const projectPath = resolve(name); // Check if project already exists try { await access(projectPath); if (!options.force) { this.failSpinner('Project directory already exists. Use --force to overwrite.'); return; } } catch { // Directory doesn't exist, which is what we want } // Create project directory structure await this.createProjectStructure(projectPath, options); this.updateSpinner('Generating configuration files...'); // Create project configuration const projectConfig = { ...DEFAULT_PROJECT_CONFIG, name: name, description: options.description || `${name} MUD project`, author: options.author || process.env.USER || 'Lemuel Canady' }; await this.createConfigFiles(projectPath, projectConfig); this.updateSpinner('Generating sample scripts...'); // Generate initial scripts based on template await this.generateTemplateFiles(projectPath, options.template || 'basic'); // Install dependencies if not skipped if (!options.skipDeps) { this.updateSpinner('Installing dependencies...'); await this.installDependencies(projectPath); } this.succeedSpinner(`Project created: ${chalk.green(name)}`); console.log(chalk.gray(`Location: ${projectPath}`)); console.log(chalk.gray('Next steps:')); console.log(chalk.gray(` cd ${name}`)); console.log(chalk.gray(' mud project info')); console.log(chalk.gray(' mud script list')); } catch (error) { this.failSpinner('Failed to initialize project'); throw error; } } async showProjectInfo(options = {}) { const spinner = this.startSpinner('Loading project information...'); try { const projectConfig = await this.loadProjectConfig(); if (!projectConfig) { this.failSpinner('No project configuration found. Run `mud project init` first.'); return; } this.succeedSpinner('Project information loaded'); console.log(chalk.bold(`šŸ“¦ Project: ${projectConfig.name}`)); console.log(`Version: ${projectConfig.version}`); console.log(`Description: ${projectConfig.description}`); console.log(`Author: ${projectConfig.author}`); if (options.verbose) { console.log(chalk.bold('\nConfiguration:')); console.log(` Server URL: ${projectConfig.serverUrl}`); console.log(` Debugger URL: ${projectConfig.debuggerUrl}`); console.log(` Scripts Path: ${projectConfig.scriptsPath}`); console.log(` Templates Path: ${projectConfig.templatesPath}`); console.log(` Build Path: ${projectConfig.buildPath}`); if (projectConfig.dependencies.length > 0) { console.log(chalk.bold('\nDependencies:')); for (const dep of projectConfig.dependencies) { console.log(` • ${dep}`); } } // Check server connectivity console.log(chalk.bold('\nServer Status:')); try { const isConnected = await this.restClient.checkConnection(); console.log(` Connection: ${isConnected ? chalk.green('āœ“ Connected') : chalk.red('āœ— Disconnected')}`); } catch { console.log(` Connection: ${chalk.red('āœ— Error')}`); } } } catch (error) { this.failSpinner('Failed to load project information'); throw error; } } async validateProject(options = {}) { const spinner = this.startSpinner('Validating project...'); try { const issues = []; const warnings = []; this.updateSpinner('Checking project configuration...'); // Check project config const projectConfig = await this.loadProjectConfig(); if (!projectConfig) { issues.push('Missing project configuration file'); } else { if (!projectConfig.name) issues.push('Missing project name'); if (!projectConfig.version) warnings.push('Missing project version'); if (!projectConfig.author) warnings.push('Missing project author'); } this.updateSpinner('Checking directory structure...'); // Check directory structure const requiredDirs = ['scripts', 'templates', 'build']; for (const dir of requiredDirs) { try { await access(dir); } catch { if (options.fix) { await mkdir(dir, { recursive: true }); warnings.push(`Created missing directory: ${dir}`); } else { issues.push(`Missing directory: ${dir}`); } } } this.updateSpinner('Checking script files...'); // Check script files try { const scriptsPath = projectConfig?.scriptsPath || './scripts'; await access(scriptsPath); // TODO: Validate individual script files } catch { warnings.push('Scripts directory not found or empty'); } this.updateSpinner('Checking server connectivity...'); // Check server connectivity try { const isConnected = await this.restClient.checkConnection(); if (!isConnected) { warnings.push('Cannot connect to MUD server'); } } catch { warnings.push('Server connectivity check failed'); } if (issues.length === 0 && warnings.length === 0) { this.succeedSpinner('Project validation passed'); } else if (issues.length === 0) { this.warnSpinner('Project validation completed with warnings'); console.log(chalk.yellow('Warnings:')); for (const warning of warnings) { console.log(chalk.yellow(` ⚠ ${warning}`)); } } else { this.failSpinner('Project validation failed'); console.log(chalk.red('Issues:')); for (const issue of issues) { console.log(chalk.red(` āœ— ${issue}`)); } if (warnings.length > 0) { console.log(chalk.yellow('Warnings:')); for (const warning of warnings) { console.log(chalk.yellow(` ⚠ ${warning}`)); } } if (options.fix) { console.log(chalk.blue('\nRun with --fix to attempt automatic fixes')); } } } catch (error) { this.failSpinner('Validation error'); throw error; } } async buildProject(options = {}) { const spinner = this.startSpinner('Building project...'); try { const projectConfig = await this.loadProjectConfig(); if (!projectConfig) { this.failSpinner('No project configuration found'); return; } const outputPath = options.output || projectConfig.buildPath || './build'; this.updateSpinner('Compiling scripts...'); // Compile all scripts const response = await this.restClient.makeRequest('POST', '/api/build/project', { scriptsPath: projectConfig.scriptsPath, outputPath, options: { minify: options.minify, watch: options.watch } }); if (!response.success) { this.failSpinner('Build failed'); console.log(chalk.red('Build errors:')); for (const error of response.errors || []) { console.log(chalk.red(` āœ— ${error}`)); } return; } this.succeedSpinner('Project built successfully'); console.log(chalk.gray(`Output: ${outputPath}`)); console.log(chalk.gray(`Files: ${response.filesProcessed || 0}`)); if (options.watch) { console.log(chalk.blue('šŸ‘€ Watching for changes... Press Ctrl+C to stop')); // Keep process alive for watch mode process.on('SIGINT', () => { console.log(chalk.blue('\nšŸ‘‹ Stopping watch mode...')); process.exit(0); }); } } catch (error) { this.failSpinner('Build error'); throw error; } } async cleanProject(options = {}) { const spinner = this.startSpinner('Cleaning project...'); try { const { rm } = await import('fs/promises'); // Clean build directory try { await rm('./build', { recursive: true, force: true }); this.verbose('Cleaned build directory'); } catch { this.verbose('Build directory not found'); } if (options.all) { this.updateSpinner('Cleaning all generated files...'); // Clean additional generated files const cleanPatterns = [ './node_modules', './*.log', './coverage', './.nyc_output' ]; for (const pattern of cleanPatterns) { try { await rm(pattern, { recursive: true, force: true }); this.verbose(`Cleaned ${pattern}`); } catch { // Ignore if file doesn't exist } } } this.succeedSpinner('Project cleaned'); } catch (error) { this.failSpinner('Clean failed'); throw error; } } async deployProject(options = {}) { const spinner = this.startSpinner('Preparing deployment...'); try { const projectConfig = await this.loadProjectConfig(); if (!projectConfig) { this.failSpinner('No project configuration found'); return; } this.updateSpinner('Validating deployment...'); // Build project first await this.buildProject({ output: projectConfig.buildPath }); this.updateSpinner('Deploying to server...'); const deploymentData = { project: projectConfig.name, version: projectConfig.version, environment: options.env, dryRun: options.dryRun }; const response = await this.restClient.makeRequest('POST', '/api/deploy/project', deploymentData); if (!response.success) { this.failSpinner('Deployment failed'); if (response.errors) { for (const error of response.errors) { console.log(chalk.red(` āœ— ${error}`)); } } return; } this.succeedSpinner(options.dryRun ? 'Deployment validation passed' : 'Project deployed successfully'); if (response.deploymentId) { console.log(chalk.gray(`Deployment ID: ${response.deploymentId}`)); } if (response.scriptsDeployed) { console.log(chalk.gray(`Scripts deployed: ${response.scriptsDeployed}`)); } } catch (error) { this.failSpinner('Deployment error'); throw error; } } async configureProject(key, value, options = {}) { try { if (options.list) { await this.listConfiguration(); return; } if (!key) { this.error('Configuration key is required. Use --list to see all configuration.'); return; } if (!value) { await this.getConfiguration(key); return; } await this.setConfiguration(key, value, options.global); } catch (error) { this.error('Configuration error', error); } } async listConfiguration() { console.log(chalk.bold('šŸ”§ Project Configuration:')); const projectConfig = await this.loadProjectConfig(); if (projectConfig) { for (const [key, value] of Object.entries(projectConfig)) { console.log(` ${chalk.cyan(key)}: ${chalk.white(String(value))}`); } } console.log(chalk.bold('\n🌐 CLI Configuration:')); const cliConfig = this.config.getAll(); for (const [key, value] of Object.entries(cliConfig)) { console.log(` ${chalk.cyan(key)}: ${chalk.white(String(value))}`); } } async getConfiguration(key) { const projectConfig = await this.loadProjectConfig(); if (projectConfig && key in projectConfig) { console.log(`${chalk.cyan(key)}: ${chalk.white(String(projectConfig[key]))}`); } else { const cliValue = this.config.get(key); if (cliValue !== undefined) { console.log(`${chalk.cyan(key)}: ${chalk.white(String(cliValue))}`); } else { this.warn(`Configuration key '${key}' not found`); } } } async setConfiguration(key, value, global = false) { if (global) { // Set CLI configuration this.config.set(key, value); await this.config.save(); this.success(`Global configuration updated: ${key} = ${value}`); } else { // Set project configuration const projectConfig = await this.loadProjectConfig() || { ...DEFAULT_PROJECT_CONFIG }; projectConfig[key] = value; await this.saveProjectConfig(projectConfig); this.success(`Project configuration updated: ${key} = ${value}`); } } async generateDocs(options = {}) { const spinner = this.startSpinner('Generating documentation...'); try { const projectConfig = await this.loadProjectConfig(); if (!projectConfig) { this.failSpinner('No project configuration found'); return; } this.updateSpinner('Analyzing project structure...'); const docsData = { project: projectConfig, scripts: [], // TODO: Analyze scripts apis: [], // TODO: Extract API information format: options.format }; const response = await this.restClient.makeRequest('POST', '/api/docs/generate', docsData); if (!response.success) { this.failSpinner('Documentation generation failed'); return; } this.updateSpinner('Writing documentation files...'); // Save generated documentation const outputPath = options.output || './docs'; await mkdir(outputPath, { recursive: true }); // Save files based on format if (options.format === 'html') { await writeFile(join(outputPath, 'index.html'), response.content); } else { await writeFile(join(outputPath, 'README.md'), response.content); } this.succeedSpinner('Documentation generated'); console.log(chalk.gray(`Output: ${outputPath}`)); } catch (error) { this.failSpinner('Documentation generation error'); throw error; } } async healthCheck(options = {}) { const spinner = this.startSpinner('Checking project health...'); try { const health = { project: 'unknown', server: 'unknown', scripts: 'unknown', dependencies: 'unknown' }; // Check project configuration this.updateSpinner('Checking project configuration...'); try { const projectConfig = await this.loadProjectConfig(); health.project = projectConfig ? 'healthy' : 'missing'; } catch { health.project = 'error'; } // Check server connectivity this.updateSpinner('Checking server connectivity...'); try { const serverHealth = await this.restClient.getServerHealth(); health.server = serverHealth.status || 'unknown'; } catch { health.server = 'disconnected'; } // Check scripts this.updateSpinner('Checking scripts...'); try { await access('./scripts'); health.scripts = 'available'; } catch { health.scripts = 'missing'; } // Check dependencies this.updateSpinner('Checking dependencies...'); try { await access('./node_modules'); health.dependencies = 'installed'; } catch { health.dependencies = 'missing'; } this.succeedSpinner('Health check completed'); console.log(chalk.bold('šŸ„ Project Health Status:')); console.log(` Project Config: ${this.formatHealthStatus(health.project)}`); console.log(` Server: ${this.formatHealthStatus(health.server)}`); console.log(` Scripts: ${this.formatHealthStatus(health.scripts)}`); console.log(` Dependencies: ${this.formatHealthStatus(health.dependencies)}`); const overallHealth = Object.values(health).every(status => status === 'healthy' || status === 'available' || status === 'installed'); console.log(`\nOverall: ${overallHealth ? chalk.green('āœ“ Healthy') : chalk.yellow('⚠ Issues detected')}`); } catch (error) { this.failSpinner('Health check error'); throw error; } } async backupProject(options = {}) { const spinner = this.startSpinner('Creating project backup...'); try { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupFile = options.output || `backup-${timestamp}.tar.gz`; this.updateSpinner('Compressing project files...'); // Create tar archive const tar = await import('tar'); const filesToBackup = [ 'scripts/', 'templates/', 'mud.config.json', 'package.json' ]; if (options.includeBuild) { filesToBackup.push('build/'); } await tar.create({ gzip: true, file: backupFile, filter: (path) => !path.includes('node_modules') && !path.includes('.git') }, filesToBackup); this.succeedSpinner(`Backup created: ${chalk.green(backupFile)}`); } catch (error) { this.failSpinner('Backup failed'); throw error; } } async restoreProject(backupFile, options = {}) { const spinner = this.startSpinner('Restoring project from backup...'); try { // Check if backup file exists await access(backupFile); this.updateSpinner('Extracting backup...'); // Extract tar archive const tar = await import('tar'); await tar.extract({ file: backupFile, cwd: process.cwd() }); this.succeedSpinner('Project restored successfully'); console.log(chalk.gray('You may need to run `npm install` to restore dependencies')); } catch (error) { this.failSpinner('Restore failed'); throw error; } } // Helper methods async loadProjectConfig() { try { const configData = await readFile('mud.config.json', 'utf-8'); return JSON.parse(configData); } catch { return null; } } async saveProjectConfig(config) { await writeFile('mud.config.json', JSON.stringify(config, null, 2)); } async createProjectStructure(projectPath, options) { const directories = [ 'scripts', 'templates', 'build', 'docs', 'tests' ]; for (const dir of directories) { await mkdir(join(projectPath, dir), { recursive: true }); } } async createConfigFiles(projectPath, config) { // Create project config await writeFile(join(projectPath, 'mud.config.json'), JSON.stringify(config, null, 2)); // Create package.json if it doesn't exist const packageJson = { name: config.name, version: config.version, description: config.description, author: config.author, scripts: { start: "mud project build && mud project deploy", dev: "mud project build --watch", test: "mud script test **/*.mud" }, dependencies: {}, devDependencies: {} }; await writeFile(join(projectPath, 'package.json'), JSON.stringify(packageJson, null, 2)); } async generateTemplateFiles(projectPath, template) { const scriptsPath = join(projectPath, 'scripts'); // Create basic template files based on template type const templates = { basic: ['welcome.mud', 'commands/help.mud'], advanced: ['welcome.mud', 'commands/help.mud', 'systems/combat.mud', 'npcs/guard.mud'], 'combat-focused': ['systems/combat.mud', 'systems/damage.mud', 'weapons/sword.mud'], 'social-focused': ['commands/say.mud', 'commands/tell.mud', 'systems/chat.mud'] }; const templateFiles = templates[template] || templates.basic; for (const file of templateFiles) { const filePath = join(scriptsPath, file); const dir = filePath.substring(0, filePath.lastIndexOf('/')); await mkdir(dir, { recursive: true }); await writeFile(filePath, this.getTemplateContent(file)); } } getTemplateContent(fileName) { // Return appropriate template content based on file name if (fileName.includes('welcome')) { return `// Welcome script export function execute(context) { return { success: true, message: "Welcome to the MUD!" }; }`; } return `// ${fileName} script export function execute(context) { // Add your script logic here return { success: true, message: "Script executed successfully" }; }`; } async installDependencies(projectPath) { const { spawn } = await import('child_process'); return new Promise((resolve, reject) => { const npm = spawn('npm', ['install'], { cwd: projectPath, stdio: 'pipe' }); npm.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`npm install failed with code ${code}`)); } }); npm.on('error', reject); }); } formatHealthStatus(status) { const statusColors = { healthy: 'green', available: 'green', installed: 'green', missing: 'red', error: 'red', disconnected: 'yellow', unknown: 'gray' }; const color = statusColors[status] || 'gray'; const symbols = { healthy: 'āœ“', available: 'āœ“', installed: 'āœ“', missing: 'āœ—', error: 'āœ—', disconnected: '⚠', unknown: '?' }; const symbol = symbols[status] || '?'; return chalk[color](`${symbol} ${status}`); } } //# sourceMappingURL=ProjectCommands.js.map