diff --git a/Makefile b/Makefile index 1a739bca..2e1e05d2 100644 --- a/Makefile +++ b/Makefile @@ -31,3 +31,6 @@ clean: ## Remove binary dump-state: ./contracts/anvil/dump-state.sh + +build/test-plugin: + @go build -buildmode=plugin -o ./bin/test-plugin.so cmd/test-plugin/main.go diff --git a/cmd/devkit/main.go b/cmd/devkit/main.go index 38e12222..50e74528 100644 --- a/cmd/devkit/main.go +++ b/cmd/devkit/main.go @@ -2,6 +2,8 @@ package main import ( "context" + "devkit-cli/pkg/plugin" + "fmt" "log" "os" @@ -24,6 +26,17 @@ func main() { UseShortOptionHandling: true, } + plugins, err := plugin.LoadPlugins("~/.devkit/plugins") + if err != nil { + log.Fatalf("Error loading plugins: %v", err) + } + fmt.Printf("Plugins loaded: %+v\n", plugins) + for _, p := range plugins { + if p != nil { + app.Commands = append(app.Commands, p.GetCommands()...) + } + } + // Apply both middleware functions to all commands hooks.ApplyMiddleware(app.Commands, hooks.WithEnvLoader, hooks.WithTelemetry) diff --git a/cmd/test-plugin/main.go b/cmd/test-plugin/main.go new file mode 100644 index 00000000..faadb67d --- /dev/null +++ b/cmd/test-plugin/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "devkit-cli/pkg/plugin" + "github.com/urfave/cli/v2" +) + +type TestPlugin struct { +} + +func (p *TestPlugin) Version() string { + return "v1.0.0" +} + +func (p *TestPlugin) Name() string { + return "TestPlugin" +} + +func (p *TestPlugin) GetCommands() []*cli.Command { + return []*cli.Command{ + { + Name: "test", + Usage: "Test command from TestPlugin", + Action: func(c *cli.Context) error { + println("Test command executed") + return nil + }, + }, + } +} + +func GetPlugin() plugin.IPlugin { + return &TestPlugin{} +} diff --git a/pkg/commands/avs.go b/pkg/commands/avs.go index 10755925..4c649901 100644 --- a/pkg/commands/avs.go +++ b/pkg/commands/avs.go @@ -16,3 +16,11 @@ var AVSCommand = &cli.Command{ ReleaseCommand, }, } + +func MergeCommands(cmds ...*cli.Command) []*cli.Command { + merged := make([]*cli.Command, 0) + for _, cmd := range cmds { + merged = append(merged, cmd) + } + return merged +} diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go new file mode 100644 index 00000000..cbb5103c --- /dev/null +++ b/pkg/plugin/plugin.go @@ -0,0 +1,87 @@ +package plugin + +import ( + "fmt" + "github.com/urfave/cli/v2" + "log" + "os" + "path/filepath" + goplugin "plugin" + "strings" +) + +type IPlugin interface { + GetCommands() []*cli.Command + Name() string + Version() string +} + +func resolvePluginPath(pluginPath string) (string, error) { + if pluginPath == "~" || strings.HasPrefix(pluginPath, "~/") { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get home directory: %v", err) + } + if pluginPath == "~" { + pluginPath = homeDir + } else { + + } + pluginPath = filepath.Join(homeDir, pluginPath[2:]) + } + return filepath.Abs(pluginPath) +} + +func LoadPlugins(pluginPath string) ([]IPlugin, error) { + absPluginPath, err := resolvePluginPath(pluginPath) + if err != nil { + return nil, fmt.Errorf("failed to resolve symlinks: %v", err) + } + fmt.Printf("Resolved path: %s\n", absPluginPath) + + pathStat, err := os.Stat(absPluginPath) + if err != nil { + if os.IsNotExist(err) { + log.Printf("warning: plugin path does not exist: %s", absPluginPath) + return nil, nil + } + return nil, fmt.Errorf("failed to stat plugin path: %v", err) + } + if !pathStat.IsDir() { + return nil, fmt.Errorf("plugin path is not a directory: %s", absPluginPath) + } + + var plugins []IPlugin + files, err := filepath.Glob(filepath.Join(absPluginPath, "*.so")) + if err != nil { + return nil, fmt.Errorf("failed to glob plugin files: %v", err) + } + + if len(files) == 0 { + return nil, nil + } + + for _, file := range files { + p, err := goplugin.Open(file) + if err != nil { + return nil, fmt.Errorf("failed to open plugin file %s: %v", file, err) + } + + sym, err := p.Lookup("GetPlugin") + if err != nil { + log.Printf("failed to lookup GetPlugin in %s: %v", file, err) + continue + } + + getPlugin, ok := sym.(func() IPlugin) + if !ok { + log.Printf("Plugin %s has invalid 'GetPlugin' symbol", file) + continue + } + + plugin := getPlugin() + plugins = append(plugins, plugin) + log.Printf("Loaded plugin: %s, version: %s", plugin.Name(), plugin.Version()) + } + return plugins, nil +}