feat: route root-level commands to plugins in ~/.picoclaw/plugins
- Added root args validator to picoclaw command to route unknown
commands to plugins (e.g., picoclaw service → picoclaw-service)
- Removed built-in 'plugins' subcommand - now handled as plugin
- Added prefix matching to support 'picoclaw-X' → 'X' shorthand
- Added exported functions (FindPlugin, ExecPlugin, ListPlugins)
- Updated tests for new command structure
Example usage:
picoclaw service status → runs picoclaw-service
picoclaw launcher → runs picoclaw-launcher
picoclaw plugins-list → runs picoclaw-plugins-list
💘 Generated with Crush
Assisted-by: MiniMax-M2.5 via Crush <crush@charm.land>
This commit is contained in:
parent
5afa5cf7ff
commit
2e6a51168f
3 changed files with 118 additions and 6 deletions
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue