4242#include < vix/cli/commands/UpdateCommand.hpp>
4343#include < vix/cli/commands/OutdatedCommand.hpp>
4444#include < vix/cli/commands/MakeCommand.hpp>
45+ #include < vix/cli/commands/CompletionCommand.hpp>
4546#include < vix/utils/Env.hpp>
4647#include < vix/cli/Style.hpp>
4748#include < vix/utils/Logger.hpp>
5657#include < algorithm>
5758#include < cctype>
5859#include < filesystem>
60+ #include < functional>
61+ #include < climits>
62+ #include < functional>
63+ #include < climits>
5964
6065namespace vix
6166{
@@ -92,6 +97,57 @@ namespace vix
9297 return std::nullopt ;
9398 }
9499
100+ static int levenshtein_distance (const std::string &a, const std::string &b)
101+ {
102+ const size_t m = a.size ();
103+ const size_t n = b.size ();
104+
105+ std::vector<int > prev (n + 1 ), curr (n + 1 );
106+
107+ for (size_t j = 0 ; j <= n; ++j)
108+ prev[j] = j;
109+
110+ for (size_t i = 1 ; i <= m; ++i)
111+ {
112+ curr[0 ] = i;
113+ for (size_t j = 1 ; j <= n; ++j)
114+ {
115+ int cost = (a[i - 1 ] == b[j - 1 ]) ? 0 : 1 ;
116+
117+ curr[j] = std::min ({prev[j] + 1 ,
118+ curr[j - 1 ] + 1 ,
119+ prev[j - 1 ] + cost});
120+ }
121+ prev = curr;
122+ }
123+
124+ return prev[n];
125+ }
126+
127+ static std::optional<std::string> find_closest_command (
128+ const std::string &input,
129+ const std::unordered_map<std::string, vix::cli::dispatch::Entry> &entries)
130+ {
131+ int bestScore = INT_MAX;
132+ std::string best;
133+
134+ for (const auto &[name, _] : entries)
135+ {
136+ int d = levenshtein_distance (input, name);
137+
138+ if (d < bestScore)
139+ {
140+ bestScore = d;
141+ best = name;
142+ }
143+ }
144+
145+ if (bestScore <= 3 )
146+ return best;
147+
148+ return std::nullopt ;
149+ }
150+
95151 void apply_log_level_from_env (Logger &logger)
96152 {
97153 if (const char *env = vix::utils::vix_getenv (" VIX_LOG_LEVEL" ))
@@ -290,8 +346,20 @@ namespace vix
290346 {
291347 if (!dispatcher.has (cmd))
292348 {
293- std::cerr << " vix: unknown command '" << cmd << " '\n\n " ;
294- return help ({});
349+ vix::cli::util::err_line (
350+ std::cerr,
351+ " unrecognized subcommand " + vix::cli::util::quote (cmd));
352+
353+ auto suggestion = find_closest_command (cmd, dispatcher.entries ());
354+
355+ if (suggestion.has_value ())
356+ {
357+ vix::cli::util::tip_line (
358+ std::cerr,
359+ " A similar command exists: " + vix::cli::util::quote (suggestion.value ()));
360+ }
361+
362+ return 1 ;
295363 }
296364 return dispatcher.help (cmd);
297365 }
@@ -307,8 +375,19 @@ namespace vix
307375
308376 if (!dispatcher.has (cmd))
309377 {
310- std::cerr << " vix: unknown command '" << cmd << " '\n\n " ;
311- help ({});
378+ vix::cli::util::err_line (
379+ std::cerr,
380+ " unrecognized subcommand " + vix::cli::util::quote (cmd));
381+
382+ auto suggestion = find_closest_command (cmd, dispatcher.entries ());
383+
384+ if (suggestion.has_value ())
385+ {
386+ vix::cli::util::tip_line (
387+ std::cerr,
388+ " A similar command exists: " + vix::cli::util::quote (suggestion.value ()));
389+ }
390+
312391 return 1 ;
313392 }
314393
@@ -372,6 +451,8 @@ namespace vix
372451 return commands::StoreCommand::help ();
373452 if (cmd == " publish" )
374453 return commands::PublishCommand::help ();
454+ if (cmd == " completion" )
455+ return commands::CompletionCommand::help ();
375456 if (cmd == " deps" )
376457 {
377458 vix::cli::util::warn_line (std::cout, " 'vix deps' is deprecated, use 'vix install'" );
@@ -480,6 +561,7 @@ namespace vix
480561 // Help
481562 out << indent (2 ) << " Help:\n " ;
482563 out << indent (3 ) << " help [command] Show command help\n " ;
564+ out << indent (3 ) << " completion Generate shell completion script\n " ;
483565 out << indent (3 ) << " version Show version\n\n " ;
484566
485567 out << indent (1 ) << " Global options:\n " ;
0 commit comments