File size: 2,400 Bytes
eb3f11e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import { levenshteinDistance } from "../../shared/levenshtein-distance.js";
import { formatCliCommand } from "../command-format.js";
import { getCoreCliCommandNamesCore } from "./core-command-descriptors.js";
import { getSubCliEntriesCore } from "./subcli-descriptors.js";

const EXPLICIT_COMMAND_ALIASES = new Map<string, string>([
  ["upgrade", "update"],
  ["udpate", "update"],
]);

const MAX_SUGGESTIONS = 3;

function uniqueSortedCommandNames(commands: Iterable<string>): string[] {
  return [...new Set([...commands].filter(Boolean))].toSorted((left, right) =>
    left.localeCompare(right),
  );
}

export function formatCliCommandSuggestions(
  input: string,
  commandPath: readonly string[] = [],
  candidates?: Iterable<string>,
): string | undefined {
  const normalizedInput = input.trim().toLowerCase();
  if (!normalizedInput) {
    return undefined;
  }

  const knownCommands = uniqueSortedCommandNames(
    candidates ??
      (commandPath.length === 0
        ? [...getCoreCliCommandNamesCore(), ...getSubCliEntriesCore().map((entry) => entry.name)]
        : []),
  );
  const explicitAlias = EXPLICIT_COMMAND_ALIASES.get(normalizedInput);
  if (explicitAlias && knownCommands.includes(explicitAlias)) {
    return formatCliSuggestionLines([explicitAlias], commandPath);
  }
  const suggestions = findCliCommandSuggestions(normalizedInput, knownCommands);
  if (suggestions.length === 0) {
    return undefined;
  }
  return formatCliSuggestionLines(suggestions, commandPath);
}

function findCliCommandSuggestions(input: string, candidates: readonly string[]): string[] {
  const maxDistance = Math.max(1, Math.floor(input.length * 0.4));
  return candidates
    .map((command) => ({ command, distance: levenshteinDistance(input, command) }))
    .filter(({ command, distance }) => command !== input && distance <= maxDistance)
    .toSorted(
      (left, right) => left.distance - right.distance || left.command.localeCompare(right.command),
    )
    .slice(0, MAX_SUGGESTIONS)
    .map(({ command }) => command);
}

function formatCliSuggestionLines(
  suggestions: readonly string[],
  commandPath: readonly string[],
): string {
  const commandPrefix = ["openclaw", ...commandPath].join(" ");
  const commandLines = suggestions
    .map((command) => `  ${formatCliCommand(`${commandPrefix} ${command}`)}`)
    .join("\n");
  return `Did you mean this?\n${commandLines}`;
}