Skip to content

Commit f81dbb6

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

File tree

3 files changed

+234
-20
lines changed

3 files changed

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

0 commit comments

Comments
 (0)