@xec-sh/cli
Version:
Xec: The Universal Shell for TypeScript
209 lines (204 loc) • 7.5 kB
JavaScript
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