ursamu-mud
Version:
Ursamu - Modular MUD Engine with sandboxed scripting and plugin system
808 lines ⢠32.7 kB
JavaScript
/**
* 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