diff --git a/examples/enterprise-virtual-commands.mjs b/examples/enterprise-virtual-commands.mjs new file mode 100644 index 0000000..dc23796 --- /dev/null +++ b/examples/enterprise-virtual-commands.mjs @@ -0,0 +1,218 @@ +#!/usr/bin/env bun +import { $ } from '../src/$.mjs'; + +console.log('๐Ÿข Enterprise Virtual Commands Demo\n'); + +// Install deployment tools +console.log('๐Ÿ“ฆ Installing enterprise deployment tools...'); +const deployResult = await $.install('@command-stream/deploy-tools'); +console.log(`โœ… ${deployResult.message}\n`); + +// Create enterprise-specific custom commands +console.log('๐Ÿ”ง Creating enterprise custom commands...\n'); + +// 1. Environment validation command +$.create('validate-env', async ({ args }) => { + const env = args[0] || 'development'; + const validEnvs = ['development', 'staging', 'production']; + + if (!validEnvs.includes(env)) { + return { + stdout: '', + stderr: `โŒ Invalid environment: ${env}. Valid options: ${validEnvs.join(', ')}\n`, + code: 1 + }; + } + + return { + stdout: `โœ… Environment '${env}' is valid\n`, + stderr: '', + code: 0 + }; +}); + +// 2. Security check command +$.create('security-check', async ({ args }) => { + const checks = [ + 'Checking for secrets in code...', + 'Validating SSL certificates...', + 'Scanning for vulnerabilities...', + 'Verifying access controls...' + ]; + + let output = '๐Ÿ”’ Running security checks:\n'; + for (const check of checks) { + output += ` ${check} โœ…\n`; + } + output += '๐Ÿ›ก๏ธ All security checks passed!\n'; + + return { stdout: output, stderr: '', code: 0 }; +}); + +// 3. Performance monitoring command +$.create('perf-monitor', async function* ({ args }) { + const duration = parseInt(args[0]) || 3; + yield '๐Ÿ“Š Starting performance monitoring...\n'; + + for (let i = 1; i <= duration; i++) { + const cpu = Math.round(Math.random() * 100); + const memory = Math.round(Math.random() * 8000); + yield `[${i}s] CPU: ${cpu}%, Memory: ${memory}MB\n`; + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + yield '๐Ÿ“ˆ Performance monitoring complete!\n'; +}, { streaming: true }); + +// Create deployment pipeline with middleware +console.log('๐Ÿš€ Setting up deployment pipeline...\n'); + +// Extend deploy command with validation middleware +$.extend('deploy', { + pre: async (context) => { + console.log('๐Ÿ” Pre-deployment validation...'); + const env = context.args[0] || 'staging'; + + // Run validation + const validation = await $`validate-env ${env}`; + if (validation.code !== 0) { + throw new Error(`Validation failed: ${validation.stderr}`); + } + + // Run security check + const security = await $`security-check`; + if (security.code !== 0) { + throw new Error(`Security check failed: ${security.stderr}`); + } + + console.log('โœ… Pre-deployment checks passed'); + return context; + }, + post: async (result, context) => { + if (result.code === 0) { + console.log('๐ŸŽ‰ Post-deployment: Notifying team...'); + result.stdout += '\n๐Ÿ“ง Team notification sent!\n'; + } + return result; + } +}); + +// Create comprehensive deployment command composition +$.compose('full-deploy', [ + 'validate-env', + 'security-check', + 'deploy' +], { + mode: 'sequence', + continueOnError: false +}); + +// Create monitoring pipeline +$.compose('deploy-with-monitoring', [ + 'full-deploy', + 'perf-monitor' +], { + mode: 'sequence' +}); + +// Demonstrate enterprise workflow +console.log('๐Ÿข Enterprise Deployment Workflow Demo:\n'); + +try { + // Test environment validation + console.log('1๏ธโƒฃ Testing environment validation...'); + const validationTest = await $`validate-env production`; + console.log(validationTest.stdout); + + // Test security check + console.log('2๏ธโƒฃ Running security checks...'); + const securityTest = await $`security-check`; + console.log(securityTest.stdout); + + // Test performance monitoring + console.log('3๏ธโƒฃ Performance monitoring (3 seconds)...'); + const perfTest = await $`perf-monitor 3`; + console.log(perfTest.stdout); + + // Full deployment with validation and monitoring + console.log('4๏ธโƒฃ Full deployment pipeline...'); + const deployTest = await $`deploy production`; + console.log(deployTest.stdout); + + console.log('\nโœจ Enterprise deployment completed successfully!'); + +} catch (error) { + console.error('โŒ Deployment failed:', error.message); +} + +// Create development helper commands +console.log('\n๐Ÿ› ๏ธ Development Helper Commands:\n'); + +$.create('db-migrate', async ({ args }) => { + const direction = args[0] || 'up'; + return { + stdout: `๐Ÿ—„๏ธ Running database migration (${direction})...\nโœ… Migration complete!\n`, + stderr: '', + code: 0 + }; +}); + +$.create('test-suite', async ({ args }) => { + const suite = args[0] || 'all'; + const testCounts = { unit: 245, integration: 67, e2e: 23 }; + let output = `๐Ÿงช Running ${suite} tests...\n`; + + if (suite === 'all') { + Object.entries(testCounts).forEach(([type, count]) => { + output += ` ${type}: ${count} tests โœ…\n`; + }); + } else if (testCounts[suite]) { + output += ` ${suite}: ${testCounts[suite]} tests โœ…\n`; + } + + output += '๐ŸŽฏ All tests passed!\n'; + return { stdout: output, stderr: '', code: 0 }; +}); + +$.create('build-assets', async ({ args }) => { + const env = args[0] || 'development'; + return { + stdout: `๐Ÿ“ฆ Building assets for ${env}...\n๐Ÿ—๏ธ Webpack bundling complete!\nโœจ Assets optimized!\n`, + stderr: '', + code: 0 + }; +}); + +// Create complete CI/CD pipeline +$.compose('ci-pipeline', [ + 'test-suite', + 'build-assets', + 'security-check', + 'validate-env' +], { + mode: 'sequence' +}); + +$.compose('cd-pipeline', [ + 'ci-pipeline', + 'db-migrate', + 'deploy' +], { + mode: 'sequence' +}); + +console.log('5๏ธโƒฃ Testing CI/CD pipeline...'); +const cicdTest = await $`ci-pipeline`; +console.log(cicdTest.stdout); + +// Show marketplace packages for enterprise +console.log('\n๐Ÿ›’ Enterprise Package Recommendations:\n'); +const enterpriseSearch = await $.marketplace.search('deploy'); +enterpriseSearch.results.forEach(pkg => { + console.log(`๐Ÿ“ฆ ${pkg.name} - ${pkg.description}`); + console.log(` โญ Rating: ${pkg.rating}/5.0 | ๐Ÿ“ˆ Downloads: ${pkg.downloads}`); + console.log(` ๐Ÿ”ง Commands: ${pkg.commands.join(', ')}\n`); +}); + +console.log('๐Ÿš€ Enterprise Virtual Commands Demo Complete!'); +console.log('๐Ÿ’ผ Ready for production deployment workflows!'); \ No newline at end of file diff --git a/examples/test-hot-reload-cmd.mjs b/examples/test-hot-reload-cmd.mjs new file mode 100644 index 0000000..13724ac --- /dev/null +++ b/examples/test-hot-reload-cmd.mjs @@ -0,0 +1,9 @@ +// Example hot-reloadable command for development +export default async function testHotReload({ args }) { + const message = args[0] || 'Hello from hot-reloaded command!'; + return { + stdout: `๐Ÿ”ฅ HOT RELOAD v1.0: ${message}\n`, + stderr: '', + code: 0 + }; +} \ No newline at end of file diff --git a/examples/virtual-ecosystem-demo.mjs b/examples/virtual-ecosystem-demo.mjs new file mode 100644 index 0000000..fc1afb8 --- /dev/null +++ b/examples/virtual-ecosystem-demo.mjs @@ -0,0 +1,106 @@ +#!/usr/bin/env bun +import { $ } from '../src/$.mjs'; + +console.log('๐Ÿš€ Virtual Commands Ecosystem Demo\n'); + +// 1. Package Installation +console.log('๐Ÿ“ฆ Installing command packages...'); +const gitToolsResult = await $.install('@command-stream/git-tools'); +console.log(`โœ… ${gitToolsResult.message}`); +console.log(` Commands: ${gitToolsResult.commands.join(', ')}\n`); + +const fileToolsResult = await $.install('@command-stream/file-tools'); +console.log(`โœ… ${fileToolsResult.message}`); +console.log(` Commands: ${fileToolsResult.commands.join(', ')}\n`); + +// 2. Using installed commands +console.log('๐Ÿ”ง Using installed commands...'); +const gitStatus = await $`git-status-clean`; +console.log(`Git Status: ${gitStatus.stdout.trim()}`); + +const enhancedLs = await $`enhanced-ls`; +console.log(`Enhanced LS:\n${enhancedLs.stdout}`); + +// 3. Creating custom commands +console.log('โšก Creating custom commands...'); +$.create('welcome', async ({ args }) => { + const name = args[0] || 'Developer'; + const message = `๐ŸŽ‰ Welcome to command-stream, ${name}!\n`; + return { stdout: message, stderr: '', code: 0 }; +}); + +const welcome = await $`welcome Alice`; +console.log(welcome.stdout.trim()); + +// 4. Creating streaming command +$.create('countdown', async function* ({ args }) { + const count = parseInt(args[0]) || 5; + for (let i = count; i >= 1; i--) { + yield `โฐ ${i}...\n`; + await new Promise(resolve => setTimeout(resolve, 500)); + } + yield `๐ŸŽฏ Launch!\n`; +}, { streaming: true }); + +console.log('\n๐Ÿš€ Countdown demo:'); +const countdownResult = await $`countdown 3`; +console.log(countdownResult.stdout); + +// 5. Command extension with middleware +console.log('๐Ÿ”ง Extending commands with middleware...'); +$.extend('welcome', async (result, context) => { + return { + ...result, + stdout: `[๐ŸŒŸ ENHANCED] ${result.stdout}` + }; +}); + +const enhancedWelcome = await $`welcome Bob`; +console.log(enhancedWelcome.stdout.trim()); + +// 6. Command composition +console.log('\n๐Ÿ”— Command composition demo...'); +$.create('gen-data', async ({ args }) => { + const items = ['apple', 'banana', 'cherry']; + return { stdout: items.join('\n') + '\n', stderr: '', code: 0 }; +}); + +$.create('format-data', async ({ stdin }) => { + const formatted = (stdin || '').split('\n') + .filter(line => line.trim()) + .map((item, i) => `${i + 1}. ๐ŸŽ ${item}`) + .join('\n'); + return { stdout: formatted + '\n', stderr: '', code: 0 }; +}); + +$.compose('fruit-list', ['gen-data', 'format-data'], { mode: 'pipeline' }); + +const fruitList = await $`fruit-list`; +console.log('Fruit List:'); +console.log(fruitList.stdout); + +// 7. Marketplace demonstration +console.log('๐Ÿ›’ Marketplace features...'); +const searchResults = await $.marketplace.search('git'); +console.log('Search results for "git":'); +searchResults.results.forEach(pkg => { + console.log(` ๐Ÿ“ฆ ${pkg.name} v${pkg.version} (${pkg.downloads} downloads, โญ${pkg.rating})`); + console.log(` ${pkg.description}`); + console.log(` Commands: ${pkg.commands.join(', ')}\n`); +}); + +const packageInfo = await $.marketplace.info('@command-stream/git-tools'); +console.log('Package info for git-tools:'); +console.log(` Name: ${packageInfo.name}`); +console.log(` Version: ${packageInfo.version}`); +console.log(` Commands: ${packageInfo.commands.join(', ')}`); +console.log(` Installed: ${packageInfo.installed ? 'โœ…' : 'โŒ'}`); + +const installedPackages = $.marketplace.list(); +console.log(`\n๐Ÿ“‹ Installed packages (${installedPackages.length}):`); +installedPackages.forEach(pkg => { + console.log(` ๐Ÿ“ฆ ${pkg.name} v${pkg.version} - ${pkg.commands.length} commands`); +}); + +console.log('\n๐ŸŽ‰ Virtual Commands Ecosystem Demo Complete!'); +console.log('๐Ÿ”ฅ No competitor can match this extensible shell environment!'); \ No newline at end of file diff --git a/package.json b/package.json index 6723c5b..9ecf987 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "command-stream", - "version": "0.7.1", + "version": "0.8.0", "description": "Modern $ shell utility library with streaming, async iteration, and EventEmitter support, optimized for Bun runtime", "type": "module", "main": "src/$.mjs", diff --git a/src/$.mjs b/src/$.mjs index 46c7258..ba43c14 100755 --- a/src/$.mjs +++ b/src/$.mjs @@ -4615,6 +4615,504 @@ function processOutput(data, options = {}) { return data; } +// Advanced Virtual Commands Ecosystem +// Storage for installed packages and middleware +const installedPackages = new Map(); +const commandMiddleware = new Map(); +const commandComposition = new Map(); + +// Package installation system +async function installPackage(packageName, options = {}) { + trace('VirtualEcosystem', () => `Installing package: ${packageName}`); + + try { + // Validate package name + if (!packageName || typeof packageName !== 'string') { + throw new Error('Package name must be a valid string'); + } + + // Check if already installed + if (installedPackages.has(packageName)) { + if (!options.force) { + trace('VirtualEcosystem', () => `Package ${packageName} already installed`); + return { success: true, message: `Package ${packageName} already installed` }; + } + } + + // For now, simulate package installation + // In real implementation, this would fetch from npm or custom registry + const mockPackage = await mockPackageResolver(packageName, options); + + if (mockPackage) { + installedPackages.set(packageName, { + name: packageName, + version: mockPackage.version || '1.0.0', + commands: mockPackage.commands || {}, + installed: new Date(), + options + }); + + // Register commands from the package + Object.entries(mockPackage.commands || {}).forEach(([cmdName, handler]) => { + register(cmdName, handler); + trace('VirtualEcosystem', () => `Registered command ${cmdName} from package ${packageName}`); + }); + + return { + success: true, + message: `Successfully installed ${packageName}@${mockPackage.version}`, + commands: Object.keys(mockPackage.commands || {}) + }; + } else { + throw new Error(`Package ${packageName} not found`); + } + } catch (error) { + trace('VirtualEcosystem', () => `Failed to install ${packageName}: ${error.message}`); + return { success: false, error: error.message }; + } +} + +// Mock package resolver for demonstration +async function mockPackageResolver(packageName, options = {}) { + // Simulate some popular command packages + const mockPackages = { + '@command-stream/git-tools': { + version: '1.0.0', + commands: { + 'git-status-clean': async ({ args, cwd }) => { + // Simple git status command that shows clean status + return { stdout: 'Working directory clean\n', stderr: '', code: 0 }; + }, + 'git-branch-list': async ({ args, cwd }) => { + return { stdout: '* main\n feature\n develop\n', stderr: '', code: 0 }; + } + } + }, + '@command-stream/file-tools': { + version: '1.2.0', + commands: { + 'enhanced-ls': async ({ args, cwd }) => { + // Enhanced ls with colors and formatting + return { stdout: 'file1.txt\nfile2.js\ndir/\n', stderr: '', code: 0 }; + }, + 'tree-view': async ({ args, cwd }) => { + return { stdout: '.\nโ”œโ”€โ”€ file1.txt\nโ”œโ”€โ”€ file2.js\nโ””โ”€โ”€ dir/\n', stderr: '', code: 0 }; + } + } + }, + '@command-stream/deploy-tools': { + version: '2.1.0', + commands: { + 'deploy': async ({ args, cwd }) => { + const env = args[0] || 'staging'; + return { stdout: `Deploying to ${env}...\nDeployment successful!\n`, stderr: '', code: 0 }; + } + } + } + }; + + // Simulate async package fetch + await new Promise(resolve => setTimeout(resolve, 100)); + + return mockPackages[packageName] || null; +} + +// Create custom virtual command +function createVirtualCommand(name, handler, options = {}) { + trace('VirtualEcosystem', () => `Creating virtual command: ${name}`); + + if (!name || typeof name !== 'string') { + throw new Error('Command name must be a valid string'); + } + + if (!handler || typeof handler !== 'function') { + throw new Error('Command handler must be a function'); + } + + // Wrap handler to support additional features + const wrappedHandler = async (context) => { + const startTime = Date.now(); + try { + trace('VirtualCommand', () => `Executing custom command: ${name}`); + + // Support for async generators (streaming) + if (options.streaming && handler.constructor.name === 'AsyncGeneratorFunction') { + return await handleStreamingCommand(handler, context); + } + + const result = await handler(context); + + const duration = Date.now() - startTime; + trace('VirtualCommand', () => `Custom command ${name} completed in ${duration}ms`); + + return result; + } catch (error) { + trace('VirtualCommand', () => `Custom command ${name} failed: ${error.message}`); + return { stdout: '', stderr: error.message, code: 1 }; + } + }; + + // Store metadata about the command + const commandMetadata = { + name, + handler: wrappedHandler, + originalHandler: handler, + options, + created: new Date(), + type: 'custom' + }; + + register(name, wrappedHandler); + virtualCommands.set(name + '_metadata', commandMetadata); + + return { + name, + registered: true, + metadata: commandMetadata + }; +} + +// Handle streaming commands (async generators) +async function handleStreamingCommand(generator, context) { + trace('VirtualCommand', () => 'Handling streaming virtual command'); + + let stdout = ''; + let stderr = ''; + let code = 0; + + try { + const gen = generator(context); + + for await (const chunk of gen) { + if (typeof chunk === 'string') { + stdout += chunk; + } else if (chunk && typeof chunk === 'object') { + if (chunk.stdout) stdout += chunk.stdout; + if (chunk.stderr) stderr += chunk.stderr; + if (chunk.code !== undefined) code = chunk.code; + } + } + + return { stdout, stderr, code }; + } catch (error) { + return { stdout, stderr: error.message, code: 1 }; + } +} + +// Extend existing commands with middleware +function extendCommand(commandName, middleware, options = {}) { + trace('VirtualEcosystem', () => `Extending command: ${commandName}`); + + if (!commandName || typeof commandName !== 'string') { + throw new Error('Command name must be a valid string'); + } + + if (!middleware || (typeof middleware !== 'function' && typeof middleware !== 'object')) { + throw new Error('Middleware must be a function or object with pre/post methods'); + } + + // Get existing command - could be original or already wrapped + const existingHandler = virtualCommands.get(commandName); + if (!existingHandler) { + throw new Error(`Command ${commandName} not found`); + } + + // Store middleware chain + if (!commandMiddleware.has(commandName)) { + commandMiddleware.set(commandName, []); + } + + // Get the original handler if this command has already been extended + const originalHandler = commandMiddleware.has(commandName) && commandMiddleware.get(commandName).length > 0 + ? commandMiddleware.get(commandName)[0].originalHandler || existingHandler + : existingHandler; + + // Store original handler reference on first extension + if (commandMiddleware.get(commandName).length === 0) { + commandMiddleware.get(commandName).push({ originalHandler, middleware: null, options: {} }); + } + + commandMiddleware.get(commandName).push({ middleware, options }); + + // Create wrapped handler with all middleware + const wrappedHandler = async (context) => { + let result = context; + + // Apply pre-middleware in order + const middlewares = commandMiddleware.get(commandName) || []; + for (const { middleware: mw } of middlewares) { + if (mw && mw.pre) { + result = await mw.pre(result) || result; + } + } + + // Execute original command (skip the first entry which stores original handler) + const originalResult = await originalHandler(result); + + // Apply post-middleware in order + let finalResult = originalResult; + for (const { middleware: mw } of middlewares) { + if (mw && mw.post) { + finalResult = await mw.post(finalResult, result) || finalResult; + } else if (mw && typeof mw === 'function' && !mw.pre && !mw.post) { + // Simple middleware function + finalResult = await mw(finalResult, result) || finalResult; + } + } + + return finalResult; + }; + + // Re-register with middleware + register(commandName, wrappedHandler); + + return { + command: commandName, + middlewareCount: commandMiddleware.get(commandName).length - 1, // Subtract 1 for original handler entry + extended: true + }; +} + +// Marketplace for command discovery +const marketplace = { + // Search for available command packages + async search(query, options = {}) { + trace('VirtualEcosystem', () => `Searching marketplace for: ${query}`); + + // Mock marketplace search results + const mockResults = [ + { + name: '@command-stream/git-tools', + version: '1.0.0', + description: 'Enhanced git command utilities', + commands: ['git-status-clean', 'git-branch-list'], + downloads: 1250, + rating: 4.8 + }, + { + name: '@command-stream/file-tools', + version: '1.2.0', + description: 'Advanced file system operations', + commands: ['enhanced-ls', 'tree-view'], + downloads: 890, + rating: 4.6 + }, + { + name: '@command-stream/deploy-tools', + version: '2.1.0', + description: 'Deployment automation commands', + commands: ['deploy'], + downloads: 2100, + rating: 4.9 + } + ]; + + // Filter results based on query + const filtered = mockResults.filter(pkg => + pkg.name.toLowerCase().includes(query.toLowerCase()) || + pkg.description.toLowerCase().includes(query.toLowerCase()) || + pkg.commands.some(cmd => cmd.toLowerCase().includes(query.toLowerCase())) + ); + + // Sort by relevance (downloads * rating for now) + const sorted = filtered.sort((a, b) => (b.downloads * b.rating) - (a.downloads * a.rating)); + + return { + query, + results: sorted.slice(0, options.limit || 10), + total: sorted.length + }; + }, + + // Get package details + async info(packageName) { + trace('VirtualEcosystem', () => `Getting package info for: ${packageName}`); + + const mockPackage = await mockPackageResolver(packageName); + if (!mockPackage) { + throw new Error(`Package ${packageName} not found`); + } + + return { + name: packageName, + version: mockPackage.version, + commands: Object.keys(mockPackage.commands || {}), + description: `Mock package ${packageName}`, + installed: installedPackages.has(packageName) + }; + }, + + // List installed packages + list() { + trace('VirtualEcosystem', () => 'Listing installed packages'); + + return Array.from(installedPackages.values()).map(pkg => ({ + name: pkg.name, + version: pkg.version, + commands: Object.keys(pkg.commands || {}), + installed: pkg.installed + })); + } +}; + +// Command composition system +function composeCommands(name, commands, options = {}) { + trace('VirtualEcosystem', () => `Composing command: ${name} from ${commands.length} commands`); + + if (!name || typeof name !== 'string') { + throw new Error('Composed command name must be a valid string'); + } + + if (!Array.isArray(commands) || commands.length === 0) { + throw new Error('Commands must be a non-empty array'); + } + + const composedHandler = async (context) => { + let results = []; + let finalStdout = ''; + let finalStderr = ''; + let finalCode = 0; + + for (let i = 0; i < commands.length; i++) { + const cmd = commands[i]; + let cmdResult; + + if (typeof cmd === 'string') { + // Check if it's a virtual command first + if (virtualCommands.has(cmd)) { + const handler = virtualCommands.get(cmd); + cmdResult = await handler(context); + } else { + // Execute as shell command + const runner = new ProcessRunner(cmd, {}, context.cwd); + cmdResult = await runner.run(); + } + } else if (typeof cmd === 'function') { + // Execute as function + cmdResult = await cmd(context); + } else { + throw new Error(`Invalid command at index ${i}: ${cmd}`); + } + + results.push(cmdResult); + + if (options.mode === 'pipeline') { + // Pass stdout of previous command as stdin of next + context.stdin = cmdResult.stdout; + } else if (options.mode === 'sequence') { + // Accumulate all outputs + finalStdout += cmdResult.stdout || ''; + finalStderr += cmdResult.stderr || ''; + + // Stop on first error unless continueOnError is true + if (cmdResult.code !== 0 && !options.continueOnError) { + finalCode = cmdResult.code; + break; + } + } + } + + return { + stdout: finalStdout || results[results.length - 1]?.stdout || '', + stderr: finalStderr || results[results.length - 1]?.stderr || '', + code: finalCode || results[results.length - 1]?.code || 0, + results: options.detailed ? results : undefined + }; + }; + + // Store composition metadata + commandComposition.set(name, { + name, + commands, + options, + created: new Date() + }); + + register(name, composedHandler); + + return { + name, + commands: commands.length, + registered: true + }; +} + +// Hot-reloadable command development +const hotReloadableCommands = new Map(); + +function enableHotReload(commandName, filePath, options = {}) { + trace('VirtualEcosystem', () => `Enabling hot reload for: ${commandName} at ${filePath}`); + + if (!fs.existsSync(filePath)) { + throw new Error(`Command file not found: ${filePath}`); + } + + const watcher = fs.watchFile(filePath, { interval: options.interval || 1000 }, async () => { + try { + trace('VirtualEcosystem', () => `Hot reloading command: ${commandName}`); + + // Clear require cache for the module + delete require.cache[require.resolve(path.resolve(filePath))]; + + // Re-import the command + const commandModule = await import(path.resolve(filePath) + '?t=' + Date.now()); + const newHandler = commandModule.default || commandModule[commandName]; + + if (newHandler) { + register(commandName, newHandler); + trace('VirtualEcosystem', () => `Successfully hot reloaded: ${commandName}`); + } + } catch (error) { + trace('VirtualEcosystem', () => `Hot reload failed for ${commandName}: ${error.message}`); + } + }); + + hotReloadableCommands.set(commandName, { + filePath, + watcher, + options + }); + + return { + command: commandName, + filePath, + watching: true + }; +} + +function disableHotReload(commandName) { + trace('VirtualEcosystem', () => `Disabling hot reload for: ${commandName}`); + + const reloadInfo = hotReloadableCommands.get(commandName); + if (reloadInfo) { + fs.unwatchFile(reloadInfo.filePath); + hotReloadableCommands.delete(commandName); + return true; + } + return false; +} + +// Cleanup function for ecosystem state +function cleanupEcosystemState() { + installedPackages.clear(); + commandMiddleware.clear(); + commandComposition.clear(); + hotReloadableCommands.forEach(({ watcher, filePath }) => { + fs.unwatchFile(filePath); + }); + hotReloadableCommands.clear(); +} + +// Add new methods to the $tagged function +$tagged.install = installPackage; +$tagged.create = createVirtualCommand; +$tagged.extend = extendCommand; +$tagged.marketplace = marketplace; +$tagged.compose = composeCommands; +$tagged.hotReload = enableHotReload; +$tagged.disableHotReload = disableHotReload; +$tagged.cleanupEcosystem = cleanupEcosystemState; + // Initialize built-in commands trace('Initialization', () => 'Registering built-in virtual commands'); registerBuiltins(); @@ -4642,6 +5140,15 @@ export { configureAnsi, getAnsiConfig, processOutput, - forceCleanupAll + forceCleanupAll, + // Virtual Commands Ecosystem + installPackage, + createVirtualCommand, + extendCommand, + composeCommands, + enableHotReload, + disableHotReload, + marketplace, + cleanupEcosystemState }; export default $tagged; \ No newline at end of file diff --git a/tests/virtual-ecosystem.test.mjs b/tests/virtual-ecosystem.test.mjs new file mode 100644 index 0000000..1b04875 --- /dev/null +++ b/tests/virtual-ecosystem.test.mjs @@ -0,0 +1,487 @@ +import { test, expect, describe, beforeEach, afterEach } from 'bun:test'; +import { beforeTestCleanup, afterTestCleanup } from './test-cleanup.mjs'; +import { $, shell, enableVirtualCommands, installPackage, createVirtualCommand, extendCommand, composeCommands, marketplace, cleanupEcosystemState, unregister, listCommands } from '../src/$.mjs'; + +// Helper function to setup shell settings +function setupShellSettings() { + shell.errexit(false); + shell.verbose(false); + shell.xtrace(false); + shell.pipefail(false); + shell.nounset(false); + enableVirtualCommands(); +} + +describe('Virtual Commands Ecosystem', () => { + beforeEach(async () => { + await beforeTestCleanup(); + cleanupEcosystemState(); + + // Clean up any custom commands from previous tests + const commands = listCommands(); + const customCommands = commands.filter(cmd => + !['cd', 'pwd', 'echo', 'sleep', 'true', 'false', 'which', 'exit', 'env', 'cat', 'ls', 'mkdir', 'rm', 'mv', 'cp', 'touch', 'basename', 'dirname', 'yes', 'seq', 'test'].includes(cmd) + ); + customCommands.forEach(cmd => unregister(cmd)); + + setupShellSettings(); + }); + + afterEach(async () => { + cleanupEcosystemState(); + await afterTestCleanup(); + }); + + describe('Package Installation System', () => { + test('should install git-tools package and register commands', async () => { + const result = await $.install('@command-stream/git-tools'); + + expect(result.success).toBe(true); + expect(result.commands).toContain('git-status-clean'); + expect(result.commands).toContain('git-branch-list'); + }); + + test('should install file-tools package and register commands', async () => { + const result = await $.install('@command-stream/file-tools'); + + expect(result.success).toBe(true); + expect(result.commands).toContain('enhanced-ls'); + expect(result.commands).toContain('tree-view'); + }); + + test('should prevent duplicate installations without force', async () => { + await $.install('@command-stream/git-tools'); + const result = await $.install('@command-stream/git-tools'); + + expect(result.success).toBe(true); + expect(result.message).toContain('already installed'); + }); + + test('should allow forced reinstallation', async () => { + await $.install('@command-stream/git-tools'); + const result = await $.install('@command-stream/git-tools', { force: true }); + + expect(result.success).toBe(true); + }); + + test('should handle package not found', async () => { + const result = await $.install('@nonexistent/package'); + + expect(result.success).toBe(false); + expect(result.error).toContain('not found'); + }); + + test('should execute installed git commands', async () => { + await $.install('@command-stream/git-tools'); + + const statusResult = await $`git-status-clean`; + expect(statusResult.code).toBe(0); + expect(statusResult.stdout).toContain('Working directory clean'); + + const branchResult = await $`git-branch-list`; + expect(branchResult.code).toBe(0); + expect(branchResult.stdout).toContain('main'); + }); + + test('should execute installed file commands', async () => { + await $.install('@command-stream/file-tools'); + + const lsResult = await $`enhanced-ls`; + expect(lsResult.code).toBe(0); + expect(lsResult.stdout).toContain('file1.txt'); + + const treeResult = await $`tree-view`; + expect(treeResult.code).toBe(0); + expect(treeResult.stdout).toContain('โ”œโ”€โ”€'); + }); + + test('should execute deploy command with arguments', async () => { + await $.install('@command-stream/deploy-tools'); + + const deployResult = await $`deploy production`; + expect(deployResult.code).toBe(0); + expect(deployResult.stdout).toContain('Deploying to production'); + expect(deployResult.stdout).toContain('Deployment successful!'); + }); + }); + + describe('Custom Command Creation', () => { + test('should create and execute basic custom command', async () => { + const result = $.create('greet', async ({ args }) => { + const name = args[0] || 'World'; + return { stdout: `Hello, ${name}!\n`, stderr: '', code: 0 }; + }); + + expect(result.registered).toBe(true); + expect(result.name).toBe('greet'); + + const cmdResult = await $`greet Alice`; + expect(cmdResult.stdout).toBe('Hello, Alice!\n'); + expect(cmdResult.code).toBe(0); + }); + + test('should create command with error handling', async () => { + $.create('fail-test', async ({ args }) => { + if (args[0] === 'error') { + throw new Error('Test error'); + } + return { stdout: 'Success\n', stderr: '', code: 0 }; + }); + + const successResult = await $`fail-test success`; + expect(successResult.code).toBe(0); + expect(successResult.stdout).toBe('Success\n'); + + const errorResult = await $`fail-test error`; + expect(errorResult.code).toBe(1); + expect(errorResult.stderr).toContain('Test error'); + }); + + test('should create streaming command with async generator', async () => { + $.create('stream-numbers', async function* ({ args }) { + const count = parseInt(args[0]) || 3; + for (let i = 1; i <= count; i++) { + yield `Number ${i}\n`; + // Small delay to simulate actual streaming + await new Promise(resolve => setTimeout(resolve, 10)); + } + }, { streaming: true }); + + const result = await $`stream-numbers 2`; + expect(result.code).toBe(0); + expect(result.stdout).toBe('Number 1\nNumber 2\n'); + }); + + test('should validate command creation parameters', () => { + expect(() => { + $.create('', () => {}); + }).toThrow('Command name must be a valid string'); + + expect(() => { + $.create('test-cmd', null); + }).toThrow('Command handler must be a function'); + }); + }); + + describe('Command Extension and Middleware', () => { + test('should extend existing command with simple middleware', async () => { + // First create a base command + $.create('base-cmd', async ({ args }) => { + return { stdout: args.join(' ') + '\n', stderr: '', code: 0 }; + }); + + // Extend it with middleware + const extendResult = $.extend('base-cmd', async (result, context) => { + return { + ...result, + stdout: '[EXTENDED] ' + result.stdout + }; + }); + + expect(extendResult.extended).toBe(true); + expect(extendResult.command).toBe('base-cmd'); + + const cmdResult = await $`base-cmd hello world`; + expect(cmdResult.stdout).toBe('[EXTENDED] hello world\n'); + }); + + test('should extend command with pre/post middleware', async () => { + $.create('log-cmd', async ({ args }) => { + return { stdout: `Output: ${args.join(' ')}\n`, stderr: '', code: 0 }; + }); + + $.extend('log-cmd', { + pre: async (context) => { + // Modify args before execution + return { ...context, args: context.args.map(arg => arg.toUpperCase()) }; + }, + post: async (result, context) => { + // Modify result after execution + return { + ...result, + stdout: `[POST] ${result.stdout}` + }; + } + }); + + const result = await $`log-cmd hello world`; + expect(result.stdout).toBe('[POST] Output: HELLO WORLD\n'); + }); + + test('should handle multiple middleware layers', async () => { + $.create('multi-cmd', async ({ args }) => { + return { stdout: args.join(' ') + '\n', stderr: '', code: 0 }; + }); + + // First extension + $.extend('multi-cmd', async (result) => ({ + ...result, + stdout: '[FIRST] ' + result.stdout + })); + + // Second extension + $.extend('multi-cmd', async (result) => ({ + ...result, + stdout: '[SECOND] ' + result.stdout + })); + + const result = await $`multi-cmd test`; + expect(result.stdout).toBe('[SECOND] [FIRST] test\n'); + }); + + test('should validate extension parameters', () => { + expect(() => { + $.extend('', () => {}); + }).toThrow('Command name must be a valid string'); + + expect(() => { + $.extend('nonexistent', () => {}); + }).toThrow('Command nonexistent not found'); + + expect(() => { + $.extend('echo', null); + }).toThrow('Middleware must be a function'); + }); + }); + + describe('Command Composition System', () => { + test('should compose commands in sequence mode', async () => { + // Create individual commands + $.create('cmd1', async () => ({ stdout: 'First\n', stderr: '', code: 0 })); + $.create('cmd2', async () => ({ stdout: 'Second\n', stderr: '', code: 0 })); + + const composeResult = $.compose('sequence-test', ['cmd1', 'cmd2'], { mode: 'sequence' }); + expect(composeResult.registered).toBe(true); + expect(composeResult.commands).toBe(2); + + const result = await $`sequence-test`; + expect(result.code).toBe(0); + expect(result.stdout).toBe('First\nSecond\n'); + }); + + test('should compose commands in pipeline mode', async () => { + $.create('producer', async () => ({ stdout: 'data', stderr: '', code: 0 })); + $.create('processor', async ({ stdin }) => ({ + stdout: `processed: ${stdin}\n`, + stderr: '', + code: 0 + })); + + $.compose('pipeline-test', ['producer', 'processor'], { mode: 'pipeline' }); + + const result = await $`pipeline-test`; + expect(result.code).toBe(0); + expect(result.stdout).toBe('processed: data\n'); + }); + + test('should handle command composition errors', async () => { + $.create('good-cmd', async () => ({ stdout: 'OK\n', stderr: '', code: 0 })); + $.create('bad-cmd', async () => ({ stdout: '', stderr: 'Error\n', code: 1 })); + + // Should stop on error by default + $.compose('error-test', ['good-cmd', 'bad-cmd'], { mode: 'sequence' }); + + const result = await $`error-test`; + expect(result.code).toBe(1); + expect(result.stdout).toBe('OK\n'); + }); + + test('should continue on error when configured', async () => { + $.create('good-cmd2', async () => ({ stdout: 'OK\n', stderr: '', code: 0 })); + $.create('bad-cmd2', async () => ({ stdout: '', stderr: 'Error\n', code: 1 })); + $.create('final-cmd', async () => ({ stdout: 'Final\n', stderr: '', code: 0 })); + + $.compose('continue-test', ['good-cmd2', 'bad-cmd2', 'final-cmd'], { + mode: 'sequence', + continueOnError: true + }); + + const result = await $`continue-test`; + expect(result.code).toBe(0); // Final command succeeded + expect(result.stdout).toBe('OK\nFinal\n'); + }); + + test('should validate composition parameters', () => { + expect(() => { + $.compose('', []); + }).toThrow('Composed command name must be a valid string'); + + expect(() => { + $.compose('test', []); + }).toThrow('Commands must be a non-empty array'); + + expect(() => { + $.compose('test', null); + }).toThrow('Commands must be a non-empty array'); + }); + }); + + describe('Marketplace System', () => { + test('should search marketplace with query', async () => { + const results = await $.marketplace.search('git'); + + expect(results.query).toBe('git'); + expect(results.total).toBeGreaterThan(0); + expect(results.results.length).toBeGreaterThan(0); + + const gitTools = results.results.find(r => r.name === '@command-stream/git-tools'); + expect(gitTools).toBeDefined(); + expect(gitTools.commands).toContain('git-status-clean'); + }); + + test('should search marketplace with description match', async () => { + const results = await $.marketplace.search('file system'); + + expect(results.total).toBeGreaterThan(0); + const fileTools = results.results.find(r => r.name === '@command-stream/file-tools'); + expect(fileTools).toBeDefined(); + }); + + test('should search marketplace with command name match', async () => { + const results = await $.marketplace.search('deploy'); + + expect(results.total).toBeGreaterThan(0); + const deployTools = results.results.find(r => r.name === '@command-stream/deploy-tools'); + expect(deployTools).toBeDefined(); + }); + + test('should limit search results', async () => { + const results = await $.marketplace.search('tools', { limit: 1 }); + + expect(results.results.length).toBe(1); + }); + + test('should get package info', async () => { + const info = await $.marketplace.info('@command-stream/git-tools'); + + expect(info.name).toBe('@command-stream/git-tools'); + expect(info.version).toBe('1.0.0'); + expect(info.commands).toContain('git-status-clean'); + expect(info.installed).toBe(false); + }); + + test('should show installed status in package info', async () => { + await $.install('@command-stream/git-tools'); + const info = await $.marketplace.info('@command-stream/git-tools'); + + expect(info.installed).toBe(true); + }); + + test('should handle package not found in info', async () => { + await expect($.marketplace.info('@nonexistent/package')).rejects.toThrow('not found'); + }); + + test('should list installed packages', async () => { + await $.install('@command-stream/git-tools'); + await $.install('@command-stream/file-tools'); + + const installed = $.marketplace.list(); + + expect(installed.length).toBe(2); + expect(installed.find(p => p.name === '@command-stream/git-tools')).toBeDefined(); + expect(installed.find(p => p.name === '@command-stream/file-tools')).toBeDefined(); + }); + }); + + describe('Integration Tests', () => { + test('should demonstrate complete ecosystem workflow', async () => { + // 1. Search marketplace + const searchResults = await $.marketplace.search('git'); + expect(searchResults.results.length).toBeGreaterThan(0); + + // 2. Install package + const installResult = await $.install('@command-stream/git-tools'); + expect(installResult.success).toBe(true); + + // 3. Use installed command + const gitResult = await $`git-status-clean`; + expect(gitResult.code).toBe(0); + + // 4. Create custom command + $.create('git-quick-status', async () => { + return { stdout: 'Quick status: All good!\n', stderr: '', code: 0 }; + }); + + // 5. Extend existing command + $.extend('git-status-clean', async (result) => ({ + ...result, + stdout: '[ENHANCED] ' + result.stdout + })); + + const enhancedResult = await $`git-status-clean`; + expect(enhancedResult.stdout).toContain('[ENHANCED]'); + + // 6. Compose commands + $.compose('git-full-check', ['git-status-clean', 'git-quick-status'], { + mode: 'sequence' + }); + + const composedResult = await $`git-full-check`; + expect(composedResult.stdout).toContain('Working directory clean'); + expect(composedResult.stdout).toContain('Quick status: All good!'); + + // 7. Check marketplace listing + const installedPackages = $.marketplace.list(); + expect(installedPackages.length).toBeGreaterThan(0); + }); + + test('should handle complex streaming and composition', async () => { + // Create streaming data producer + $.create('data-stream', async function* ({ args }) { + const items = ['item1', 'item2', 'item3']; + for (const item of items) { + yield `${item}\n`; + } + }, { streaming: true }); + + // Create data processor + $.create('process-data', async ({ stdin }) => { + const processed = (stdin || '').split('\n') + .filter(line => line.trim()) + .map(line => `processed: ${line}`) + .join('\n'); + return { stdout: processed + '\n', stderr: '', code: 0 }; + }); + + // Compose them + $.compose('stream-pipeline', ['data-stream', 'process-data'], { + mode: 'pipeline' + }); + + const result = await $`stream-pipeline`; + expect(result.stdout).toContain('processed: item1'); + expect(result.stdout).toContain('processed: item2'); + expect(result.stdout).toContain('processed: item3'); + }); + }); + + describe('Error Handling and Edge Cases', () => { + test('should handle invalid package installation gracefully', async () => { + const result = await $.install(null); + expect(result.success).toBe(false); + expect(result.error).toContain('Package name must be a valid string'); + }); + + test('should handle command creation with invalid parameters', () => { + expect(() => $.create(123, () => {})).toThrow(); + expect(() => $.create('test', 'not a function')).toThrow(); + }); + + test('should handle extension of non-existent commands', () => { + expect(() => $.extend('does-not-exist', () => {})).toThrow('Command does-not-exist not found'); + }); + + test('should handle composition with invalid commands', async () => { + $.compose('invalid-compose', ['nonexistent-cmd'], { mode: 'sequence' }); + + const result = await $`invalid-compose`; + expect(result.code).toBe(1); + }); + + test('should handle marketplace search with empty query', async () => { + const results = await $.marketplace.search(''); + expect(results.results).toBeDefined(); + expect(Array.isArray(results.results)).toBe(true); + }); + }); +}); \ No newline at end of file