diff --git a/cmd/picoclaw/internal/plugins/command.go b/cmd/picoclaw/internal/plugins/command.go index 4801dd047..1cb3827b2 100644 --- a/cmd/picoclaw/internal/plugins/command.go +++ b/cmd/picoclaw/internal/plugins/command.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "github.com/spf13/cobra" @@ -127,6 +128,37 @@ func findPlugin(name string) (string, error) { } } + // Try prefix match - if user types "X", match "X-*" or "*-X" + entries, err := os.ReadDir(pluginsDir) + if err == nil { + var matches []string + for _, entry := range entries { + if entry.IsDir() { + continue + } + info, err := entry.Info() + if err != nil { + continue + } + if info.Mode()&0111 == 0 { + continue + } + pluginName := entry.Name() + // Check if name is a prefix or suffix of plugin name + if strings.HasPrefix(pluginName, name+"-") || + strings.HasPrefix(pluginName, name) || + strings.HasSuffix(pluginName, "-"+name) { + matches = append(matches, pluginName) + } + } + if len(matches) == 1 { + return filepath.Join(pluginsDir, matches[0]), nil + } + if len(matches) > 1 { + return "", fmt.Errorf("multiple plugins match %q: %v", name, matches) + } + } + return "", fmt.Errorf("plugin %q not found", name) } @@ -194,3 +226,18 @@ func newListCommand() *cobra.Command { }, } } + +// FindPlugin returns the path to a plugin by name, or an error if not found +func FindPlugin(name string) (string, error) { + return findPlugin(name) +} + +// ExecPlugin executes a plugin with the given arguments +func ExecPlugin(pluginPath string, args []string) { + execPlugin(pluginPath, args) +} + +// ListPlugins returns a list of available plugin names +func ListPlugins() ([]string, error) { + return listPlugins() +} diff --git a/cmd/picoclaw/main.go b/cmd/picoclaw/main.go index 0649eaebf..45b8b05f0 100644 --- a/cmd/picoclaw/main.go +++ b/cmd/picoclaw/main.go @@ -29,9 +29,14 @@ func NewPicoclawCommand() *cobra.Command { short := fmt.Sprintf("%s picoclaw - Personal AI Assistant v%s\n\n", internal.Logo, internal.GetVersion()) cmd := &cobra.Command{ - Use: "picoclaw", - Short: short, - Example: "picoclaw list", + Use: "picoclaw", + Short: short, + Example: "picoclaw list", + Args: rootArgsValidator, + ValidArgsFunction: rootCompleteArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, } cmd.AddCommand( @@ -43,7 +48,6 @@ func NewPicoclawCommand() *cobra.Command { cron.NewCronCommand(), migrate.NewMigrateCommand(), skills.NewSkillsCommand(), - plugins.NewPluginsCommand(), version.NewVersionCommand(), ) @@ -63,6 +67,67 @@ const ( "\033[0m\r\n" ) +// rootArgsValidator checks if args should be routed to a plugin +func rootArgsValidator(cmd *cobra.Command, args []string) error { + // No args - let cobra show help + if len(args) == 0 { + return nil + } + + // Check if it's a known subcommand + knownCommands := map[string]bool{ + "onboard": true, + "agent": true, + "auth": true, + "gateway": true, + "status": true, + "cron": true, + "migrate": true, + "skills": true, + "version": true, + "help": true, + "-h": true, + "--help": true, + } + + if knownCommands[args[0]] { + return nil + } + + // Try to find a plugin with this name + pluginPath, err := plugins.FindPlugin(args[0]) + if err != nil { + // Not a known command and not a plugin - print error and exit + fmt.Fprintf(os.Stderr, "picoclaw: %q is not a picoclaw command. See 'picoclaw --help'.\n", args[0]) + os.Exit(1) + } + + // Execute the plugin and exit + plugins.ExecPlugin(pluginPath, args[1:]) + // Should not reach here + return nil +} + +// rootCompleteArgs provides completion for plugin names +func rootCompleteArgs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + // Get list of plugins for completion + pluginList, err := plugins.ListPlugins() + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + // Also include built-in commands + builtIn := []string{"onboard", "agent", "auth", "gateway", "status", "cron", "migrate", "skills", "plugins", "version"} + + // Combine both lists + result := append(builtIn, pluginList...) + return result, cobra.ShellCompDirectiveNoFileComp +} + func main() { fmt.Printf("%s", banner) cmd := NewPicoclawCommand() diff --git a/cmd/picoclaw/main_test.go b/cmd/picoclaw/main_test.go index 3740ba358..7b113988a 100644 --- a/cmd/picoclaw/main_test.go +++ b/cmd/picoclaw/main_test.go @@ -26,8 +26,8 @@ func TestNewPicoclawCommand(t *testing.T) { assert.False(t, cmd.HasFlags()) - assert.Nil(t, cmd.Run) - assert.Nil(t, cmd.RunE) + // RunE is set to handle plugin execution + assert.NotNil(t, cmd.RunE) assert.Nil(t, cmd.PersistentPreRun) assert.Nil(t, cmd.PersistentPostRun)