@xec-sh/core
Version:
Universal shell execution engine
205 lines • 6.66 kB
JavaScript
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