UNPKG

@xec-sh/core

Version:

Universal shell execution engine

205 lines 6.66 kB
function levenshteinDistance(a, b) { const matrix = []; for (let i = 0; i <= b.length; i++) { matrix[i] = new Array(a.length + 1).fill(0); } for (let i = 0; i <= b.length; i++) { const row = matrix[i]; if (row) { row[0] = i; } } for (let j = 0; j <= a.length; j++) { if (matrix[0]) { matrix[0][j] = j; } } for (let i = 1; i <= b.length; i++) { for (let j = 1; j <= a.length; j++) { if (!matrix[i]) continue; if (b.charAt(i - 1) === a.charAt(j - 1)) { const row = matrix[i]; if (row) { row[j] = matrix[i - 1]?.[j - 1] ?? 0; } } else { const substitution = (matrix[i - 1]?.[j - 1] ?? 0) + 1; const insertion = (matrix[i]?.[j - 1] ?? 0) + 1; const deletion = (matrix[i - 1]?.[j] ?? 0) + 1; const row = matrix[i]; if (row) { row[j] = Math.min(substitution, insertion, deletion); } } } } return matrix[b.length]?.[a.length] ?? 0; } export function findSimilar(input, candidates, options = {}) { const { maxDistance = 3, maxSuggestions = 3, caseSensitive = false } = options; const normalizedInput = caseSensitive ? input : input.toLowerCase(); const distances = candidates.map(candidate => { const normalizedCandidate = caseSensitive ? candidate : candidate.toLowerCase(); return { candidate, distance: levenshteinDistance(normalizedInput, normalizedCandidate) }; }); const suggestions = distances .filter(({ distance }) => distance <= maxDistance) .sort((a, b) => a.distance - b.distance) .slice(0, maxSuggestions) .map(({ candidate }) => candidate); return suggestions; } export class CommandRegistry { constructor() { this.commands = new Map(); this.aliases = new Map(); } register(command) { this.commands.set(command.command, command); if (command.aliases) { command.aliases.forEach(alias => { this.aliases.set(alias, command.command); }); } } registerAll(commands) { commands.forEach(cmd => this.register(cmd)); } getAllCommands() { return Array.from(this.commands.keys()); } getCommand(name) { const actualCommand = this.aliases.get(name) || name; return this.commands.get(actualCommand); } findSimilarCommands(input, maxSuggestions = 3) { const allCommands = [ ...this.getAllCommands(), ...Array.from(this.aliases.keys()) ]; const similar = findSimilar(input, allCommands, { maxSuggestions, caseSensitive: false }); const seen = new Set(); const suggestions = []; similar.forEach(name => { const command = this.getCommand(name); if (command && !seen.has(command.command)) { seen.add(command.command); suggestions.push(command); } }); return suggestions; } formatSuggestions(input, suggestions, options = {}) { if (suggestions.length === 0) { return ''; } const lines = []; const useColor = options.color ?? true; const yellow = useColor ? '\x1b[33m' : ''; const cyan = useColor ? '\x1b[36m' : ''; const dim = useColor ? '\x1b[2m' : ''; const reset = useColor ? '\x1b[0m' : ''; lines.push(`${yellow}Did you mean:${reset}`); suggestions.forEach(suggestion => { let line = ` ${cyan}${suggestion.command}${reset}`; if (suggestion.description) { line += ` ${dim}- ${suggestion.description}${reset}`; } lines.push(line); if (suggestion.usage) { lines.push(` ${dim}Usage: ${suggestion.usage}${reset}`); } }); return lines.join('\n'); } } export const defaultCommandRegistry = new CommandRegistry(); defaultCommandRegistry.registerAll([ { command: 'exec', description: 'Execute a command', aliases: ['e', 'run'], usage: 'xec exec [options] <command>' }, { command: 'ssh', description: 'Execute command via SSH', usage: 'xec ssh <host> <command>' }, { command: 'docker', description: 'Execute command in Docker container', aliases: ['d'], usage: 'xec docker <container> <command>' }, { command: 'k8s', description: 'Execute command in Kubernetes pod', aliases: ['kubernetes', 'kubectl'], usage: 'xec k8s <pod> <command>' }, { command: 'on', description: 'Execute command on SSH host', usage: 'xec on <host> <command>' }, { command: 'in', description: 'Execute command in container/pod', usage: 'xec in <container|pod:name> <command>' }, { command: 'copy', description: 'Copy files between hosts/containers', aliases: ['cp'], usage: 'xec copy <source> <destination>' }, { command: 'forward', description: 'Set up port forwarding', aliases: ['tunnel', 'port-forward'], usage: 'xec forward <source> [to] <destination>' }, { command: 'logs', description: 'View logs from container/pod/file', aliases: ['log', 'tail'], usage: 'xec logs <source> [options]' }, { command: 'config', description: 'Manage configuration', aliases: ['cfg'], usage: 'xec config [get|set|list] [key] [value]' }, { command: 'interactive', description: 'Start interactive mode', aliases: ['i', 'repl'], usage: 'xec interactive' } ]); export function checkForCommandTypo(input, registry = defaultCommandRegistry) { const suggestions = registry.findSimilarCommands(input, 1); if (suggestions.length > 0) { return registry.formatSuggestions(input, suggestions); } return null; } export function getCommandCompletions(partial, registry = defaultCommandRegistry) { const allCommands = [ ...registry.getAllCommands() ]; return allCommands .filter((cmd) => cmd.toLowerCase().startsWith(partial.toLowerCase())) .sort(); } //# sourceMappingURL=suggestions.js.map