@xec-sh/cli
Version:
Xec: The Universal Shell for TypeScript
424 lines (419 loc) • 15.6 kB
JavaScript
import path from 'path';
import fs from 'fs-extra';
import { fileURLToPath } from 'url';
import { Command } from 'commander';
import * as clack from '@clack/prompts';
import { CommandRegistry } from '@xec-sh/core';
import { ScriptLoader } from './script-loader.js';
import { initializeGlobalModuleContext } from './module-loader.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export class CliCommandManager {
constructor() {
this.commands = new Map();
this.registry = new CommandRegistry();
this.commandDirs = [];
this.initialized = false;
this.initializeCommandDirs();
this.scriptLoader = new ScriptLoader({
verbose: process.env['XEC_DEBUG'] === 'true',
preferredCDN: 'esm.sh',
cache: true
});
}
initializeCommandDirs() {
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);
}
}
async ensureInitialized() {
if (this.initialized)
return;
try {
await initializeGlobalModuleContext({
verbose: process.env['XEC_DEBUG'] === 'true',
preferredCDN: 'esm.sh'
});
this.initialized = true;
}
catch (err) {
if (process.env['XEC_DEBUG']) {
console.warn('Failed to initialize module context:', err);
}
}
}
async discoverAndLoad(program) {
await this.ensureInitialized();
const builtIn = await this.discoverBuiltInCommands();
const dynamic = await this.discoverDynamicCommands();
const allCommands = new Map();
builtIn.forEach(cmd => allCommands.set(cmd.name, cmd));
dynamic.forEach(cmd => allCommands.set(cmd.name, cmd));
this.commands = allCommands;
await this.loadDynamicCommands(program);
this.buildRegistry(program);
return Array.from(allCommands.values());
}
async discoverAll() {
await this.ensureInitialized();
const builtIn = await this.discoverBuiltInCommands();
const dynamic = await this.discoverDynamicCommands();
const allCommands = new Map();
builtIn.forEach(cmd => allCommands.set(cmd.name, cmd));
dynamic.forEach(cmd => allCommands.set(cmd.name, cmd));
this.commands = allCommands;
return Array.from(allCommands.values());
}
async discoverBuiltInCommands() {
const commandsDir = path.join(__dirname, '../commands');
const commands = [];
if (!await fs.pathExists(commandsDir)) {
return commands;
}
const files = await fs.readdir(commandsDir);
for (const file of files) {
if (!file.endsWith('.js') && !file.endsWith('.ts'))
continue;
if (file.endsWith('.d.ts'))
continue;
const basename = path.basename(file, path.extname(file));
if (basename.includes('.test') || basename.includes('.spec'))
continue;
const filePath = path.join(commandsDir, file);
const metadata = await this.extractCommandMetadata(filePath, basename);
commands.push({
name: basename,
type: 'built-in',
path: filePath,
loaded: true,
...metadata
});
}
return commands;
}
async discoverDynamicCommands() {
const commands = [];
for (const dir of this.commandDirs) {
if (await fs.pathExists(dir)) {
await this.discoverCommandsInDirectory(dir, commands, '');
}
}
return commands;
}
async discoverCommandsInDirectory(dir, commands, 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.discoverCommandsInDirectory(fullPath, commands, newPrefix);
}
else if (this.isCommandFile(entry.name)) {
const basename = path.basename(entry.name, path.extname(entry.name));
const commandName = prefix ? `${prefix}:${basename}` : basename;
const metadata = await this.extractCommandMetadata(fullPath, commandName);
commands.push({
name: commandName,
type: 'dynamic',
path: fullPath,
loaded: false,
...metadata
});
}
}
}
catch (error) {
}
}
async loadDynamicCommands(program) {
const dynamicCommands = this.getDynamicCommands();
if (process.env['XEC_DEBUG'] && dynamicCommands.length > 0) {
clack.log.info(`Loading ${dynamicCommands.length} dynamic commands`);
}
for (const cmd of dynamicCommands) {
const result = await this.scriptLoader.loadDynamicCommand(cmd.path, program, cmd.name);
if (result.success) {
cmd.loaded = true;
}
else {
cmd.loaded = false;
cmd.error = result.error;
}
}
this.reportLoadingSummary();
}
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 extractCommandMetadata(filePath, commandName) {
try {
const module = await import(fileURLToPath(new URL(`file://${filePath}`, import.meta.url)));
if (module.metadata) {
return module.metadata;
}
if (module.default || module.command) {
const program = new Command();
const commandFn = module.default || module.command;
if (typeof commandFn === 'function') {
commandFn(program);
if (program.commands.length > 0) {
const cmd = program.commands[0];
if (cmd) {
return {
description: cmd.description(),
aliases: cmd.aliases(),
usage: cmd.usage()
};
}
}
}
}
}
catch {
}
try {
const content = await fs.readFile(filePath, 'utf-8');
const description = this.parseDescription(content, commandName);
return { description };
}
catch {
return {};
}
}
parseDescription(content, commandName) {
const patterns = [
/\/\*\*[\s\S]*?\*\s*(.+?)[\s\S]*?\*\//,
/\.description\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/,
/description\s*:\s*['"`]([^'"`]+)['"`]/,
/\/\/\s*(?:Command|Description):\s*(.+)/i
];
for (const pattern of patterns) {
const match = content.match(pattern);
if (match && match[1]) {
return match[1].trim();
}
}
const defaults = {
'config': 'Manage xec configuration',
'copy': 'Copy files between local and remote systems',
'forward': 'Set up port forwarding and tunnels',
'in': 'Execute commands in containers or pods',
'inspect': 'Inspect xec resources and configuration',
'logs': 'View logs from various sources',
'new': 'Create new xec resources',
'on': 'Execute commands on remote hosts via SSH',
'run': 'Run scripts and evaluate code',
'secrets': 'Manage encrypted secrets',
'watch': 'Watch files and execute commands on changes'
};
return defaults[commandName];
}
buildRegistry(program) {
const registry = new CommandRegistry();
if (program.name() && program.name() !== 'xec') {
registry.register(this.extractCommandInfo(program));
}
this.registerCommandsRecursively(program, registry);
this.registry = registry;
return registry;
}
registerCommandsRecursively(cmd, registry, parentName = '') {
cmd.commands.forEach(subCmd => {
const info = this.extractCommandInfo(subCmd);
if (parentName) {
info.command = `${parentName} ${info.command}`;
}
registry.register(info);
if (subCmd.commands && subCmd.commands.length > 0) {
this.registerCommandsRecursively(subCmd, registry, info.command);
}
});
}
extractCommandInfo(cmd) {
return {
command: cmd.name(),
description: cmd.description(),
aliases: cmd.aliases(),
usage: cmd.usage() || `xec ${cmd.name()} [options]`
};
}
reportLoadingSummary() {
const dynamic = this.getDynamicCommands();
const loaded = dynamic.filter(cmd => cmd.loaded);
const failed = dynamic.filter(cmd => !cmd.loaded && cmd.error);
if (process.env['XEC_DEBUG'] && dynamic.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}`);
});
}
}
}
findCommand(program, nameOrAlias) {
if (!nameOrAlias || !program || !program.commands)
return null;
const searchTerm = nameOrAlias.toLowerCase();
for (const cmd of program.commands) {
if (cmd.name().toLowerCase() === searchTerm) {
return cmd;
}
}
for (const cmd of program.commands) {
const aliases = cmd.aliases();
if (aliases && aliases.some(alias => alias.toLowerCase() === searchTerm)) {
return cmd;
}
}
return null;
}
addCommandDirectory(dir) {
if (!this.commandDirs.includes(dir)) {
this.commandDirs.push(dir);
}
}
getCommands() {
return Array.from(this.commands.values());
}
getBuiltInCommands() {
return this.getCommands().filter(cmd => cmd.type === 'built-in');
}
getDynamicCommands() {
return this.getCommands().filter(cmd => cmd.type === 'dynamic');
}
getLoadedCommands() {
return this.getCommands().filter(cmd => cmd.loaded);
}
getFailedCommands() {
return this.getCommands().filter(cmd => !cmd.loaded && cmd.error);
}
getCommand(name) {
return this.commands.get(name);
}
hasCommand(name) {
return this.commands.has(name);
}
getCommandDirectories() {
return this.commandDirs;
}
getRegistry() {
return this.registry;
}
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 managerInstance;
export function getCliCommandManager() {
if (!managerInstance) {
managerInstance = new CliCommandManager();
}
return managerInstance;
}
export async function discoverAndLoadCommands(program) {
const manager = getCliCommandManager();
return manager.discoverAndLoad(program);
}
export async function discoverAllCommands() {
const manager = getCliCommandManager();
return manager.discoverAll();
}
export async function loadDynamicCommands(program) {
const manager = getCliCommandManager();
await manager.discoverAndLoad(program);
return manager.getDynamicCommands()
.filter(cmd => cmd.loaded)
.map(cmd => cmd.name);
}
export function buildCommandRegistry(program) {
const manager = getCliCommandManager();
return manager.getRegistry();
}
export function registerCliCommands(program) {
return buildCommandRegistry(program);
}
export function findCommand(program, nameOrAlias) {
const manager = getCliCommandManager();
return manager.findCommand(program, nameOrAlias);
}
export { getCliCommandManager as getCommandManager };
export { getCliCommandManager as getDynamicCommandLoader };
export { getCliCommandManager as getCommandDiscovery };
export { CliCommandManager as CommandManager };
export { CliCommandManager as DynamicCommandLoader };
//# sourceMappingURL=cli-command-manager.js.map