diff --git a/cmd/limactl/main.go b/cmd/limactl/main.go index 6ee01a0347e..bd04a1a9304 100644 --- a/cmd/limactl/main.go +++ b/cmd/limactl/main.go @@ -4,9 +4,11 @@ package main import ( + "context" "errors" "fmt" "os" + "os/exec" "path/filepath" "runtime" "strings" @@ -40,7 +42,8 @@ func main() { } } } - if err := newApp().Execute(); err != nil { + rootCmd := newApp() + if err := executeWithPluginSupport(rootCmd, os.Args[1:]); err != nil { handleExitCoder(err) logrus.Fatal(err) } @@ -215,6 +218,67 @@ func handleExitCoder(err error) { } } +// executeWithPluginSupport handles command execution with plugin support. +func executeWithPluginSupport(rootCmd *cobra.Command, args []string) error { + if len(args) > 0 { + cmd, _, err := rootCmd.Find(args) + if err != nil || cmd == rootCmd { + // Function calls os.Exit() if it found and executed the plugin + runExternalPlugin(rootCmd.Context(), args[0], args[1:]) + } + } + + rootCmd.SetArgs(args) + return rootCmd.Execute() +} + +func runExternalPlugin(ctx context.Context, name string, args []string) { + if ctx == nil { + ctx = context.Background() + } + + if err := updatePathEnv(); err != nil { + logrus.Warnf("failed to update PATH environment: %v", err) + // PATH update failure shouldn't prevent plugin execution + } + + externalCmd := "limactl-" + name + execPath, err := exec.LookPath(externalCmd) + if err != nil { + return + } + + logrus.Debugf("found external command: %s", execPath) + + cmd := exec.CommandContext(ctx, execPath, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = os.Environ() + + handleExitCoder(cmd.Run()) + os.Exit(0) //nolint:revive // it's intentional to call os.Exit in this function +} + +func updatePathEnv() error { + exe, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable path: %w", err) + } + + binDir := filepath.Dir(exe) + currentPath := os.Getenv("PATH") + newPath := binDir + string(filepath.ListSeparator) + currentPath + + if err := os.Setenv("PATH", newPath); err != nil { + return fmt.Errorf("failed to set PATH environment: %w", err) + } + + logrus.Debugf("updated PATH to prioritize %s", binDir) + + return nil +} + // WrapArgsError annotates cobra args error with some context, so the error message is more user-friendly. func WrapArgsError(argFn cobra.PositionalArgs) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { diff --git a/website/content/en/docs/usage/_index.md b/website/content/en/docs/usage/_index.md index 7a77bb564c4..74ae09a8ea6 100644 --- a/website/content/en/docs/usage/_index.md +++ b/website/content/en/docs/usage/_index.md @@ -72,6 +72,61 @@ Then you can connect directly: ssh lima-default ``` +### Command Aliasing (Plugin System) + +Lima supports a plugin-like command aliasing system similar to `git`, `kubectl`, and `docker`. When you run a `limactl` command that doesn't exist, Lima will automatically look for an external program named `limactl-` in your system's PATH. + +#### Creating Custom Aliases + +To create a custom alias, create an executable script with the name `limactl-` and place it somewhere in your PATH. + +**Example: Creating a `ps` alias for listing instances** + +1. Create a script called `limactl-ps`: + ```bash + #!/bin/sh + # Show instances in a compact format + limactl list --format table "$@" + ``` + +2. Make it executable and place it in your PATH: + ```bash + chmod +x limactl-ps + sudo mv limactl-ps /usr/local/bin/ + ``` + +3. Now you can use it: + ```bash + limactl ps # Shows instances in table format + limactl ps --quiet # Shows only instance names + ``` + +**Example: Creating an `sh` alias** + +```bash +#!/bin/sh +# limactl-sh - Connect to an instance shell +limactl shell "$@" +``` + +After creating this alias: +```bash +limactl sh default # Equivalent to: limactl shell default +limactl sh myinstance bash # Equivalent to: limactl shell myinstance bash +``` + +#### How It Works + +1. When you run `limactl `, Lima first tries to find a built-in command +2. If no built-in command is found, Lima searches for `limactl-` in your PATH +3. If found, Lima executes the external program and passes all remaining arguments to it +4. If not found, Lima shows the standard "unknown command" error + +This system allows you to: +- Create personal shortcuts and aliases +- Extend Lima's functionality without modifying the core application +- Share custom commands with your team by distributing scripts + ### Shell completion - To enable bash completion, add `source <(limactl completion bash)` to `~/.bash_profile`. - To enable zsh completion, see `limactl completion zsh --help`