Skip to content

Commit ed08b59

Browse files
committed
limactl help and info flag should show available plugins
Signed-off-by: olalekan odukoya <[email protected]>
1 parent 4b46444 commit ed08b59

File tree

3 files changed

+235
-20
lines changed

3 files changed

+235
-20
lines changed

cmd/limactl/main.go

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/lima-vm/lima/v2/pkg/fsutil"
2424
"github.com/lima-vm/lima/v2/pkg/limatype/dirnames"
2525
"github.com/lima-vm/lima/v2/pkg/osutil"
26+
"github.com/lima-vm/lima/v2/pkg/plugin"
2627
"github.com/lima-vm/lima/v2/pkg/version"
2728
)
2829

@@ -164,6 +165,24 @@ func newApp() *cobra.Command {
164165
}
165166
rootCmd.AddGroup(&cobra.Group{ID: "basic", Title: "Basic Commands:"})
166167
rootCmd.AddGroup(&cobra.Group{ID: "advanced", Title: "Advanced Commands:"})
168+
169+
// Add custom help function to show plugins
170+
originalHelpFunc := rootCmd.HelpFunc()
171+
rootCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
172+
originalHelpFunc(cmd, args)
173+
174+
plugins, err := plugin.DiscoverPlugins()
175+
if err == nil && len(plugins) > 0 {
176+
fmt.Fprint(cmd.OutOrStdout(), "\nAvailable Plugins:\n")
177+
for _, p := range plugins {
178+
if p.Description != "" {
179+
fmt.Fprintf(cmd.OutOrStdout(), " %-20s %s\n", p.Name, p.Description)
180+
} else {
181+
fmt.Fprintf(cmd.OutOrStdout(), " %s\n", p.Name)
182+
}
183+
}
184+
}
185+
})
167186
rootCmd.AddCommand(
168187
newCreateCommand(),
169188
newStartCommand(),
@@ -231,7 +250,7 @@ func runExternalPlugin(ctx context.Context, name string, args []string) {
231250
ctx = context.Background()
232251
}
233252

234-
if err := updatePathEnv(); err != nil {
253+
if err := plugin.UpdatePathForPlugins(); err != nil {
235254
logrus.Warnf("failed to update PATH environment: %v", err)
236255
// PATH update failure shouldn't prevent plugin execution
237256
}
@@ -256,25 +275,6 @@ func runExternalPlugin(ctx context.Context, name string, args []string) {
256275
logrus.Fatalf("external command %q failed: %v", execPath, err)
257276
}
258277

259-
func updatePathEnv() error {
260-
exe, err := os.Executable()
261-
if err != nil {
262-
return fmt.Errorf("failed to get executable path: %w", err)
263-
}
264-
265-
binDir := filepath.Dir(exe)
266-
currentPath := os.Getenv("PATH")
267-
newPath := binDir + string(filepath.ListSeparator) + currentPath
268-
269-
if err := os.Setenv("PATH", newPath); err != nil {
270-
return fmt.Errorf("failed to set PATH environment: %w", err)
271-
}
272-
273-
logrus.Debugf("updated PATH to prioritize %s", binDir)
274-
275-
return nil
276-
}
277-
278278
// WrapArgsError annotates cobra args error with some context, so the error message is more user-friendly.
279279
func WrapArgsError(argFn cobra.PositionalArgs) cobra.PositionalArgs {
280280
return func(cmd *cobra.Command, args []string) error {

pkg/limainfo/limainfo.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/lima-vm/lima/v2/pkg/limatype"
1515
"github.com/lima-vm/lima/v2/pkg/limatype/dirnames"
1616
"github.com/lima-vm/lima/v2/pkg/limayaml"
17+
"github.com/lima-vm/lima/v2/pkg/plugin"
1718
"github.com/lima-vm/lima/v2/pkg/registry"
1819
"github.com/lima-vm/lima/v2/pkg/templatestore"
1920
"github.com/lima-vm/lima/v2/pkg/usrlocalsharelima"
@@ -29,6 +30,7 @@ type LimaInfo struct {
2930
VMTypesEx map[string]DriverExt `json:"vmTypesEx"` // since Lima v2.0.0
3031
GuestAgents map[limatype.Arch]GuestAgent `json:"guestAgents"` // since Lima v1.1.0
3132
ShellEnvBlock []string `json:"shellEnvBlock"`
33+
Plugins []plugin.Plugin `json:"plugins"`
3234
}
3335

3436
type DriverExt struct {
@@ -95,5 +97,15 @@ func New(ctx context.Context) (*LimaInfo, error) {
9597
Location: bin,
9698
}
9799
}
100+
101+
plugins, err := plugin.DiscoverPlugins()
102+
if err != nil {
103+
logrus.WithError(err).Debug("Failed to discover plugins")
104+
// Don't fail the entire info command if plugin discovery fails
105+
info.Plugins = []plugin.Plugin{}
106+
} else {
107+
info.Plugins = plugins
108+
}
109+
98110
return info, nil
99111
}

pkg/plugin/plugin.go

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// SPDX-FileCopyrightText: Copyright The Lima Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package plugin
5+
6+
import (
7+
"bufio"
8+
"os"
9+
"path/filepath"
10+
"regexp"
11+
"strings"
12+
13+
"github.com/sirupsen/logrus"
14+
15+
"github.com/lima-vm/lima/v2/pkg/usrlocalsharelima"
16+
)
17+
18+
type Plugin struct {
19+
Name string `json:"name"`
20+
Path string `json:"path"`
21+
Description string `json:"description,omitempty"`
22+
}
23+
24+
func DiscoverPlugins() ([]Plugin, error) {
25+
var plugins []Plugin
26+
seen := make(map[string]bool)
27+
28+
dirs := getPluginDirectories()
29+
30+
for _, dir := range dirs {
31+
pluginsInDir, err := scanDirectory(dir)
32+
if err != nil {
33+
logrus.Debugf("Failed to scan directory %s: %v", dir, err)
34+
continue
35+
}
36+
37+
for _, plugin := range pluginsInDir {
38+
if !seen[plugin.Name] {
39+
plugins = append(plugins, plugin)
40+
seen[plugin.Name] = true
41+
}
42+
}
43+
}
44+
45+
return plugins, nil
46+
}
47+
48+
func getPluginDirectories() []string {
49+
var dirs []string
50+
51+
if prefixDir, err := usrlocalsharelima.Prefix(); err == nil {
52+
libexecDir := filepath.Join(prefixDir, "libexec", "lima")
53+
if _, err := os.Stat(libexecDir); err == nil {
54+
dirs = append(dirs, libexecDir)
55+
}
56+
}
57+
58+
pathEnv := os.Getenv("PATH")
59+
if pathEnv != "" {
60+
pathDirs := filepath.SplitList(pathEnv)
61+
dirs = append(dirs, pathDirs...)
62+
}
63+
64+
return dirs
65+
}
66+
67+
func scanDirectory(dir string) ([]Plugin, error) {
68+
var plugins []Plugin
69+
70+
entries, err := os.ReadDir(dir)
71+
if err != nil {
72+
return nil, err
73+
}
74+
75+
for _, entry := range entries {
76+
if entry.IsDir() {
77+
continue
78+
}
79+
80+
name := entry.Name()
81+
if !strings.HasPrefix(name, "limactl-") {
82+
continue
83+
}
84+
85+
pluginName := strings.TrimPrefix(name, "limactl-")
86+
87+
if strings.Contains(pluginName, ".") {
88+
if filepath.Ext(name) == ".exe" {
89+
pluginName = strings.TrimSuffix(pluginName, ".exe")
90+
} else {
91+
continue
92+
}
93+
}
94+
95+
fullPath := filepath.Join(dir, name)
96+
97+
if !isExecutable(fullPath) {
98+
continue
99+
}
100+
101+
plugin := Plugin{
102+
Name: pluginName,
103+
Path: fullPath,
104+
}
105+
106+
if desc := getPluginDescription(fullPath); desc != "" {
107+
plugin.Description = desc
108+
}
109+
110+
plugins = append(plugins, plugin)
111+
}
112+
113+
return plugins, nil
114+
}
115+
116+
func isExecutable(path string) bool {
117+
info, err := os.Stat(path)
118+
if err != nil {
119+
return false
120+
}
121+
122+
mode := info.Mode()
123+
if mode&0o111 != 0 {
124+
return true
125+
}
126+
127+
if filepath.Ext(path) == ".exe" {
128+
return true
129+
}
130+
131+
return false
132+
}
133+
134+
func getPluginDescription(path string) string {
135+
file, err := os.Open(path)
136+
if err != nil {
137+
return ""
138+
}
139+
defer file.Close()
140+
141+
scanner := bufio.NewScanner(file)
142+
var lines []string
143+
lineCount := 0
144+
145+
for scanner.Scan() && lineCount < 20 {
146+
line := strings.TrimSpace(scanner.Text())
147+
if line != "" && !strings.HasPrefix(line, "#!") {
148+
lines = append(lines, line)
149+
}
150+
lineCount++
151+
}
152+
153+
limactlRegex := regexp.MustCompile(`limactl\s+(\w+)`)
154+
155+
for _, line := range lines {
156+
if strings.HasPrefix(line, "#") {
157+
continue
158+
}
159+
160+
matches := limactlRegex.FindStringSubmatch(line)
161+
if len(matches) > 1 {
162+
commandName := matches[1]
163+
return "Alias for " + commandName
164+
}
165+
}
166+
167+
return ""
168+
}
169+
170+
func UpdatePathForPlugins() error {
171+
prefixDir, err := usrlocalsharelima.Prefix()
172+
if err != nil {
173+
return err
174+
}
175+
176+
libexecDir := filepath.Join(prefixDir, "libexec", "lima")
177+
178+
if _, err := os.Stat(libexecDir); err == nil {
179+
currentPath := os.Getenv("PATH")
180+
181+
if !strings.Contains(currentPath, libexecDir) {
182+
newPath := libexecDir + string(filepath.ListSeparator) + currentPath
183+
if err := os.Setenv("PATH", newPath); err != nil {
184+
return err
185+
}
186+
logrus.Debugf("Added %s to PATH for plugin discovery", libexecDir)
187+
}
188+
}
189+
190+
if exe, err := os.Executable(); err == nil {
191+
binDir := filepath.Dir(exe)
192+
currentPath := os.Getenv("PATH")
193+
if !strings.Contains(currentPath, binDir) {
194+
newPath := binDir + string(filepath.ListSeparator) + currentPath
195+
if err := os.Setenv("PATH", newPath); err != nil {
196+
return err
197+
}
198+
logrus.Debugf("Added %s to PATH", binDir)
199+
}
200+
}
201+
202+
return nil
203+
}

0 commit comments

Comments
 (0)