UNPKG

@xec-sh/cli

Version:

Xec: The Universal Shell for TypeScript

209 lines (204 loc) 7.5 kB
import path from 'path'; import fs from 'fs-extra'; import * as clack from '@clack/prompts'; import { ScriptLoader } from './script-loader.js'; import { initializeGlobalModuleContext } from './module-loader.js'; export class DynamicCommandLoader { constructor() { this.commands = new Map(); this.commandDirs = []; this.scriptLoader = new ScriptLoader({ verbose: process.env['XEC_DEBUG'] === 'true', preferredCDN: 'esm.sh', cache: true }); this.commandDirs = [ path.join(process.cwd(), '.xec', 'commands'), path.join(process.cwd(), '.xec', 'cli') ]; let currentDir = process.cwd(); for (let i = 0; i < 3; i++) { const parentDir = path.dirname(currentDir); if (parentDir === currentDir) break; const parentCommandsDir = path.join(parentDir, '.xec', 'commands'); if (!this.commandDirs.includes(parentCommandsDir)) { this.commandDirs.push(parentCommandsDir); } currentDir = parentDir; } if (process.env['XEC_COMMANDS_PATH']) { const additionalPaths = process.env['XEC_COMMANDS_PATH'].split(':'); this.commandDirs.push(...additionalPaths); } initializeGlobalModuleContext({ verbose: process.env['XEC_DEBUG'] === 'true', preferredCDN: 'esm.sh' }).catch(err => { if (process.env['XEC_DEBUG']) { console.warn('Failed to initialize module context:', err); } }); } async loadCommands(program) { if (process.env['XEC_DEBUG']) { clack.log.info(`Loading dynamic commands from directories: ${this.commandDirs.join(', ')}`); } for (const dir of this.commandDirs) { if (await fs.pathExists(dir)) { if (process.env['XEC_DEBUG']) { clack.log.info(`Loading commands from directory: ${dir}`); } await this.loadCommandsFromDirectory(dir, program); } else if (process.env['XEC_DEBUG']) { clack.log.warn(`Command directory does not exist: ${dir}`); } } } async loadCommandsFromDirectory(dir, program, prefix = '') { try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { const newPrefix = prefix ? `${prefix}:${entry.name}` : entry.name; await this.loadCommandsFromDirectory(fullPath, program, newPrefix); } else if (this.isCommandFile(entry.name)) { await this.loadCommandFile(fullPath, program, prefix); } } } catch (error) { if (process.env['XEC_DEBUG']) { console.error(`Failed to load commands from ${dir}:`, error); } } } isCommandFile(filename) { const ext = path.extname(filename); const basename = path.basename(filename, ext); if (basename.startsWith('.') || basename.endsWith('.test') || basename.endsWith('.spec')) { return false; } return ['.js', '.mjs', '.ts', '.tsx'].includes(ext); } async loadCommandFile(filePath, program, prefix) { const ext = path.extname(filePath); const basename = path.basename(filePath, ext); const commandName = prefix ? `${prefix}:${basename}` : basename; this.commands.set(commandName, { name: commandName, path: filePath, loaded: false }); const result = await this.scriptLoader.loadDynamicCommand(filePath, program, commandName); if (result.success) { this.commands.get(commandName).loaded = true; } else { this.commands.get(commandName).error = result.error; } } getCommands() { return Array.from(this.commands.values()); } getLoadedCommands() { return this.getCommands().filter(cmd => cmd.loaded); } getFailedCommands() { return this.getCommands().filter(cmd => !cmd.loaded && cmd.error); } reportLoadingSummary() { const commands = this.getCommands(); const loaded = this.getLoadedCommands(); const failed = this.getFailedCommands(); if (process.env['XEC_DEBUG'] && commands.length > 0) { clack.log.info(`Dynamic commands: ${loaded.length} loaded, ${failed.length} failed`); if (failed.length > 0) { clack.log.warn('Failed commands:'); failed.forEach(cmd => { clack.log.error(` - ${cmd.name}: ${cmd.error}`); }); } } } addCommandDirectory(dir) { if (!this.commandDirs.includes(dir)) { this.commandDirs.push(dir); } } getCommandDirectories() { return this.commandDirs; } static generateCommandTemplate(name, description = 'A custom command') { return `/** * ${description} * This will be available as: xec ${name} [args...] */ export default function command(program) { program .command('${name} [args...]') .description('${description}') .option('-v, --verbose', 'Enable verbose output') .action(async (args, options) => { const { log } = await import('@clack/prompts'); log.info('Running ${name} command'); if (options.verbose) { log.info('Arguments:', args); log.info('Options:', options); } // Your command logic here const { $ } = await import('@xec-sh/core'); try { // Example: Run a command const result = await $\`echo "Running ${name}"\`; log.success(result.stdout); } catch (error) { log.error(error.message); process.exit(1); } }); } `; } static async validateCommandFile(filePath) { try { const content = await fs.readFile(filePath, 'utf-8'); if (!content.includes('export default') && !content.includes('export function command')) { return { valid: false, error: 'Command file must export a default function or "command" function' }; } if (!content.includes('.command(') && !content.includes('program.command(')) { return { valid: false, error: 'Command file must register at least one command' }; } return { valid: true }; } catch (error) { return { valid: false, error: error instanceof Error ? error.message : 'Failed to read command file' }; } } } let loader; export function getDynamicCommandLoader() { if (!loader) { loader = new DynamicCommandLoader(); } return loader; } export async function loadDynamicCommands(program) { const loader = getDynamicCommandLoader(); await loader.loadCommands(program); loader.reportLoadingSummary(); return loader.getLoadedCommands().map(cmd => cmd.name); } //# sourceMappingURL=dynamic-commands.js.map