fix(skills): register subcommands during command construction
- Move subcommand registration out of PersistentPreRunE - Ensure `picoclaw skills <subcommand>` resolves correctly - Minor install command and test cleanups
This commit is contained in:
parent
c45811f2d9
commit
e62b4e0ac5
12 changed files with 109 additions and 52 deletions
|
|
@ -10,46 +10,70 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/skills"
|
"github.com/sipeed/picoclaw/pkg/skills"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type deps struct {
|
||||||
|
workspace string
|
||||||
|
installer *skills.SkillInstaller
|
||||||
|
skillsLoader *skills.SkillsLoader
|
||||||
|
}
|
||||||
|
|
||||||
func NewSkillsCommand() *cobra.Command {
|
func NewSkillsCommand() *cobra.Command {
|
||||||
|
var d deps
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "skills",
|
Use: "skills",
|
||||||
Short: "Manage skills",
|
Short: "Manage skills",
|
||||||
|
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
cfg, err := internal2.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error loading config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.workspace = cfg.WorkspacePath()
|
||||||
|
d.installer = skills.NewSkillInstaller(d.workspace)
|
||||||
|
|
||||||
|
// get global config directory and builtin skills directory
|
||||||
|
globalDir := filepath.Dir(internal2.GetConfigPath())
|
||||||
|
globalSkillsDir := filepath.Join(globalDir, "skills")
|
||||||
|
builtinSkillsDir := filepath.Join(globalDir, "picoclaw", "skills")
|
||||||
|
d.skillsLoader = skills.NewSkillsLoader(d.workspace, globalSkillsDir, builtinSkillsDir)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
return cmd.Help()
|
return cmd.Help()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var loaded bool
|
installerFn := func() (*skills.SkillInstaller, error) {
|
||||||
|
if d.installer == nil {
|
||||||
cmd.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {
|
return nil, fmt.Errorf("skills installer is not initialized")
|
||||||
cfg, err := internal2.LoadConfig()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error loading config: %w", err)
|
|
||||||
}
|
}
|
||||||
|
return d.installer, nil
|
||||||
workspace := cfg.WorkspacePath()
|
|
||||||
installer := skills.NewSkillInstaller(workspace)
|
|
||||||
|
|
||||||
// get global config directory and builtin skills directory
|
|
||||||
globalDir := filepath.Dir(internal2.GetConfigPath())
|
|
||||||
globalSkillsDir := filepath.Join(globalDir, "skills")
|
|
||||||
builtinSkillsDir := filepath.Join(globalDir, "picoclaw", "skills")
|
|
||||||
skillsLoader := skills.NewSkillsLoader(workspace, globalSkillsDir, builtinSkillsDir)
|
|
||||||
|
|
||||||
if !loaded {
|
|
||||||
cmd.AddCommand(
|
|
||||||
newListCommand(skillsLoader),
|
|
||||||
newInstallCommand(installer),
|
|
||||||
newInstallBuiltinCommand(workspace),
|
|
||||||
newListBuiltinCommand(),
|
|
||||||
newRemoveCommand(installer),
|
|
||||||
newSearchCommand(installer),
|
|
||||||
newShowCommand(skillsLoader),
|
|
||||||
)
|
|
||||||
loaded = true
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loaderFn := func() (*skills.SkillsLoader, error) {
|
||||||
|
if d.skillsLoader == nil {
|
||||||
|
return nil, fmt.Errorf("skills loader is not initialized")
|
||||||
|
}
|
||||||
|
return d.skillsLoader, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
workspaceFn := func() (string, error) {
|
||||||
|
if d.workspace == "" {
|
||||||
|
return "", fmt.Errorf("workspace is not initialized")
|
||||||
|
}
|
||||||
|
return d.workspace, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(
|
||||||
|
newListCommand(loaderFn),
|
||||||
|
newInstallCommand(installerFn),
|
||||||
|
newInstallBuiltinCommand(workspaceFn),
|
||||||
|
newListBuiltinCommand(),
|
||||||
|
newRemoveCommand(installerFn),
|
||||||
|
newSearchCommand(installerFn),
|
||||||
|
newShowCommand(loaderFn),
|
||||||
|
)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/skills"
|
"github.com/sipeed/picoclaw/pkg/skills"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newInstallCommand(installer *skills.SkillInstaller) *cobra.Command {
|
func newInstallCommand(installerFn func() (*skills.SkillInstaller, error)) *cobra.Command {
|
||||||
var registry string
|
var registry string
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
|
|
@ -20,9 +20,7 @@ picoclaw skills install sipeed/picoclaw-skills/weather
|
||||||
picoclaw skills install --registry clawhub github
|
picoclaw skills install --registry clawhub github
|
||||||
`,
|
`,
|
||||||
Args: func(cmd *cobra.Command, args []string) error {
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
reg, _ := cmd.Flags().GetString("registry")
|
if registry != "" {
|
||||||
|
|
||||||
if reg != "" {
|
|
||||||
if len(args) != 2 {
|
if len(args) != 2 {
|
||||||
return fmt.Errorf("when --registry is set, exactly 2 arguments are required: <name> <slug>")
|
return fmt.Errorf("when --registry is set, exactly 2 arguments are required: <name> <slug>")
|
||||||
}
|
}
|
||||||
|
|
@ -36,6 +34,11 @@ picoclaw skills install --registry clawhub github
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
RunE: func(_ *cobra.Command, args []string) error {
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
|
installer, err := installerFn()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if registry != "" {
|
if registry != "" {
|
||||||
cfg, err := internal.LoadConfig()
|
cfg, err := internal.LoadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -49,7 +52,7 @@ picoclaw skills install --registry clawhub github
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringVar(®istry, "registry", "", "--registry <name> <slug>")
|
cmd.Flags().StringVar(®istry, "registry", "", "Install from registry: --registry <name> <slug>")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,18 @@ package skills
|
||||||
|
|
||||||
import "github.com/spf13/cobra"
|
import "github.com/spf13/cobra"
|
||||||
|
|
||||||
func newInstallBuiltinCommand(workspace string) *cobra.Command {
|
func newInstallBuiltinCommand(workspaceFn func() (string, error)) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "install-builtin",
|
Use: "install-builtin",
|
||||||
Short: "Install all builtin skills to workspace",
|
Short: "Install all builtin skills to workspace",
|
||||||
Example: `picoclaw skills install-builtin`,
|
Example: `picoclaw skills install-builtin`,
|
||||||
Run: func(_ *cobra.Command, _ []string) {
|
RunE: func(_ *cobra.Command, _ []string) error {
|
||||||
|
workspace, err := workspaceFn()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
skillsInstallBuiltinCmd(workspace)
|
skillsInstallBuiltinCmd(workspace)
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,15 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewInstallbuiltinSubcommand(t *testing.T) {
|
func TestNewInstallbuiltinSubcommand(t *testing.T) {
|
||||||
cmd := newInstallBuiltinCommand("")
|
cmd := newInstallBuiltinCommand(nil)
|
||||||
|
|
||||||
require.NotNil(t, cmd)
|
require.NotNil(t, cmd)
|
||||||
|
|
||||||
assert.Equal(t, "install-builtin", cmd.Use)
|
assert.Equal(t, "install-builtin", cmd.Use)
|
||||||
assert.Equal(t, "Install all builtin skills to workspace", cmd.Short)
|
assert.Equal(t, "Install all builtin skills to workspace", cmd.Short)
|
||||||
|
|
||||||
assert.NotNil(t, cmd.Run)
|
assert.Nil(t, cmd.Run)
|
||||||
|
assert.NotNil(t, cmd.RunE)
|
||||||
|
|
||||||
assert.True(t, cmd.HasExample())
|
assert.True(t, cmd.HasExample())
|
||||||
assert.False(t, cmd.HasSubCommands())
|
assert.False(t, cmd.HasSubCommands())
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,18 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/skills"
|
"github.com/sipeed/picoclaw/pkg/skills"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newListCommand(skillsLoader *skills.SkillsLoader) *cobra.Command {
|
func newListCommand(loaderFn func() (*skills.SkillsLoader, error)) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "list",
|
Use: "list",
|
||||||
Short: "List installed skills",
|
Short: "List installed skills",
|
||||||
Example: `picoclaw skills list`,
|
Example: `picoclaw skills list`,
|
||||||
Run: func(_ *cobra.Command, _ []string) {
|
RunE: func(_ *cobra.Command, _ []string) error {
|
||||||
skillsListCmd(skillsLoader)
|
loader, err := loaderFn()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
skillsListCmd(loader)
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ func TestNewListSubcommand(t *testing.T) {
|
||||||
assert.Equal(t, "list", cmd.Use)
|
assert.Equal(t, "list", cmd.Use)
|
||||||
assert.Equal(t, "List installed skills", cmd.Short)
|
assert.Equal(t, "List installed skills", cmd.Short)
|
||||||
|
|
||||||
assert.NotNil(t, cmd.Run)
|
assert.Nil(t, cmd.Run)
|
||||||
|
assert.NotNil(t, cmd.RunE)
|
||||||
|
|
||||||
assert.True(t, cmd.HasExample())
|
assert.True(t, cmd.HasExample())
|
||||||
assert.False(t, cmd.HasSubCommands())
|
assert.False(t, cmd.HasSubCommands())
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,20 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/skills"
|
"github.com/sipeed/picoclaw/pkg/skills"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newRemoveCommand(installer *skills.SkillInstaller) *cobra.Command {
|
func newRemoveCommand(installerFn func() (*skills.SkillInstaller, error)) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "remove",
|
Use: "remove",
|
||||||
Aliases: []string{"rm", "uninstall"},
|
Aliases: []string{"rm", "uninstall"},
|
||||||
Short: "Remove installed skill",
|
Short: "Remove installed skill",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Example: `picoclaw skills remove weather`,
|
Example: `picoclaw skills remove weather`,
|
||||||
Run: func(_ *cobra.Command, args []string) {
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
|
installer, err := installerFn()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
skillsRemoveCmd(installer, args[0])
|
skillsRemoveCmd(installer, args[0])
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ func TestNewRemoveSubcommand(t *testing.T) {
|
||||||
assert.Equal(t, "remove", cmd.Use)
|
assert.Equal(t, "remove", cmd.Use)
|
||||||
assert.Equal(t, "Remove installed skill", cmd.Short)
|
assert.Equal(t, "Remove installed skill", cmd.Short)
|
||||||
|
|
||||||
assert.NotNil(t, cmd.Run)
|
assert.Nil(t, cmd.Run)
|
||||||
|
assert.NotNil(t, cmd.RunE)
|
||||||
|
|
||||||
assert.True(t, cmd.HasExample())
|
assert.True(t, cmd.HasExample())
|
||||||
assert.False(t, cmd.HasSubCommands())
|
assert.False(t, cmd.HasSubCommands())
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,17 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/skills"
|
"github.com/sipeed/picoclaw/pkg/skills"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newSearchCommand(installer *skills.SkillInstaller) *cobra.Command {
|
func newSearchCommand(installerFn func() (*skills.SkillInstaller, error)) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "search",
|
Use: "search",
|
||||||
Short: "Search available skills",
|
Short: "Search available skills",
|
||||||
Run: func(_ *cobra.Command, _ []string) {
|
RunE: func(_ *cobra.Command, _ []string) error {
|
||||||
|
installer, err := installerFn()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
skillsSearchCmd(installer)
|
skillsSearchCmd(installer)
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ func TestNewSearchSubcommand(t *testing.T) {
|
||||||
assert.Equal(t, "search", cmd.Use)
|
assert.Equal(t, "search", cmd.Use)
|
||||||
assert.Equal(t, "Search available skills", cmd.Short)
|
assert.Equal(t, "Search available skills", cmd.Short)
|
||||||
|
|
||||||
assert.NotNil(t, cmd.Run)
|
assert.Nil(t, cmd.Run)
|
||||||
|
assert.NotNil(t, cmd.RunE)
|
||||||
|
|
||||||
assert.False(t, cmd.HasSubCommands())
|
assert.False(t, cmd.HasSubCommands())
|
||||||
assert.False(t, cmd.HasFlags())
|
assert.False(t, cmd.HasFlags())
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,19 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/skills"
|
"github.com/sipeed/picoclaw/pkg/skills"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newShowCommand(skillsLoader *skills.SkillsLoader) *cobra.Command {
|
func newShowCommand(loaderFn func() (*skills.SkillsLoader, error)) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "show",
|
Use: "show",
|
||||||
Short: "Show skill details",
|
Short: "Show skill details",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Example: `picoclaw skills show weather`,
|
Example: `picoclaw skills show weather`,
|
||||||
Run: func(_ *cobra.Command, args []string) {
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
skillsShowCmd(skillsLoader, args[0])
|
loader, err := loaderFn()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
skillsShowCmd(loader, args[0])
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ func TestNewShowSubcommand(t *testing.T) {
|
||||||
assert.Equal(t, "show", cmd.Use)
|
assert.Equal(t, "show", cmd.Use)
|
||||||
assert.Equal(t, "Show skill details", cmd.Short)
|
assert.Equal(t, "Show skill details", cmd.Short)
|
||||||
|
|
||||||
assert.NotNil(t, cmd.Run)
|
assert.Nil(t, cmd.Run)
|
||||||
|
assert.NotNil(t, cmd.RunE)
|
||||||
|
|
||||||
assert.True(t, cmd.HasExample())
|
assert.True(t, cmd.HasExample())
|
||||||
assert.False(t, cmd.HasSubCommands())
|
assert.False(t, cmd.HasSubCommands())
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue