diff --git a/README.md b/README.md index 4edf93e..902a374 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@ # CMDR -[![test](https://github.com/MrLYC/cmdr/actions/workflows/unittest.yml/badge.svg)](https://github.com/MrLYC/cmdr/actions/workflows/unittest.yml) [![release](https://github.com/MrLYC/cmdr/actions/workflows/release.yml/badge.svg)](https://github.com/MrLYC/cmdr/actions/workflows/main.yml) [![codecov](https://codecov.io/gh/MrLYC/cmdr/branch/main/graph/badge.svg?token=mo4TJP4mQt)](https://codecov.io/gh/MrLYC/cmdr) +[![test](https://github.com/MrLYC/cmdr/actions/workflows/unittest.yml/badge.svg)](https://github.com/MrLYC/cmdr/actions/workflows/unittest.yml) [![release](https://github.com/MrLYC/cmdr/actions/workflows/release.yml/badge.svg)](https://github.com/MrLYC/cmdr/actions/workflows/main.yml) [![codecov](https://codecov.io/gh/MrLYC/cmdr/branch/master/graph/badge.svg?token=mo4TJP4mQt)](https://codecov.io/gh/MrLYC/cmdr) CMDR is a simple command version management tool that helps you quickly switch from multiple command versions. ## Installation -Download the latest release from [GitHub](https://github.com/MrLYC/cmdr/releases) and make sure it is executable. -Run the following command to install it in your system: -```shell -% /path/to/cmdr setup -``` - -Check the CMDR version information by running the following command: -```shell -% cmdr version -a -``` +1. Download the latest release from [GitHub](https://github.com/MrLYC/cmdr/releases); +2. Make sure the download asset is executable; +3. Run the following command to install the binary: + ```shell + % /path/to/cmdr init + ``` +4. Restart your shell and run the following command to verify the installation: + ```shell + % cmdr version + ``` ## Get Started To install a new command, run the following command: @@ -32,7 +32,7 @@ Use a specified command version: ``` ## Upgrade -To upgrade the CMDR, run: +To upgrade the CMDR, just run: ```shell % cmdr upgrade ``` \ No newline at end of file diff --git a/cmd/command/define.go b/cmd/command/define.go index b31ee79..2e91877 100644 --- a/cmd/command/define.go +++ b/cmd/command/define.go @@ -35,6 +35,7 @@ func init() { flags.StringP("location", "l", "", "command location") flags.BoolP("activate", "a", false, "activate command") + helper := utils.NewDefaultCobraCommandCompleteHelper(defineCmd) cfg := core.GetConfiguration() utils.PanicOnError("binding flags", @@ -48,5 +49,8 @@ func init() { defineCmd.MarkFlagRequired("location"), cfg.BindPFlag(core.CfgKeyXCommandDefineActivate, flags.Lookup("activate")), + + helper.RegisterNameFunc(), + helper.RegisterVersionFunc(), ) } diff --git a/cmd/command/install.go b/cmd/command/install.go index 377e11a..18936fb 100644 --- a/cmd/command/install.go +++ b/cmd/command/install.go @@ -35,6 +35,7 @@ func init() { flags.StringP("location", "l", "", "command location") flags.BoolP("activate", "a", false, "activate command") + helper := utils.NewDefaultCobraCommandCompleteHelper(installCmd) cfg := core.GetConfiguration() utils.PanicOnError("binding flags", @@ -48,5 +49,8 @@ func init() { installCmd.MarkFlagRequired("location"), cfg.BindPFlag(core.CfgKeyXCommandInstallActivate, flags.Lookup("activate")), + + helper.RegisterNameFunc(), + helper.RegisterVersionFunc(), ) } diff --git a/cmd/command/list.go b/cmd/command/list.go index 8b01506..86b0748 100644 --- a/cmd/command/list.go +++ b/cmd/command/list.go @@ -74,5 +74,7 @@ func init() { cfg.BindPFlag(core.CfgKeyXCommandListVersion, flags.Lookup("version")), cfg.BindPFlag(core.CfgKeyXCommandListLocation, flags.Lookup("location")), cfg.BindPFlag(core.CfgKeyXCommandListActivate, flags.Lookup("activate")), + + utils.NewDefaultCobraCommandCompleteHelper(listCmd).RegisterAll(), ) } diff --git a/cmd/command/remove.go b/cmd/command/remove.go index 4217e2e..037fe96 100644 --- a/cmd/command/remove.go +++ b/cmd/command/remove.go @@ -44,5 +44,7 @@ func init() { cfg.BindPFlag(core.CfgKeyXCommandRemoveVersion, flags.Lookup("version")), removeCmd.MarkFlagRequired("version"), + + utils.NewDefaultCobraCommandCompleteHelper(removeCmd).RegisterAll(), ) } diff --git a/cmd/command/unset.go b/cmd/command/unset.go index bce6fb0..10c1b74 100644 --- a/cmd/command/unset.go +++ b/cmd/command/unset.go @@ -26,5 +26,7 @@ func init() { utils.PanicOnError("binding flags", cfg.BindPFlag(core.CfgKeyXCommandUnsetName, flags.Lookup("name")), unsetCmd.MarkFlagRequired("name"), + + utils.NewDefaultCobraCommandCompleteHelper(unsetCmd).RegisterAll(), ) } diff --git a/cmd/command/use.go b/cmd/command/use.go index 9a81068..92b74b2 100644 --- a/cmd/command/use.go +++ b/cmd/command/use.go @@ -34,5 +34,7 @@ func init() { cfg.BindPFlag(core.CfgKeyXCommandUseVersion, flags.Lookup("version")), useCmd.MarkFlagRequired("version"), + + utils.NewDefaultCobraCommandCompleteHelper(useCmd).RegisterAll(), ) } diff --git a/core/manager/database.go b/core/manager/database.go index dd39bea..a565bcf 100644 --- a/core/manager/database.go +++ b/core/manager/database.go @@ -95,6 +95,17 @@ func (f *CommandFilter) Count() (int, error) { return len(f.commands), nil } +func (f *CommandFilter) AddCommand(commands ...core.Command) { + for _, command := range commands { + f.commands = append(f.commands, &Command{ + Name: command.GetName(), + Version: command.GetVersion(), + Activated: command.GetActivated(), + Location: command.GetLocation(), + }) + } +} + func NewCommandFilter(commands []*Command) *CommandFilter { return &CommandFilter{commands} } diff --git a/core/utils/cmdr.go b/core/utils/cmdr.go index f229164..aaa510e 100644 --- a/core/utils/cmdr.go +++ b/core/utils/cmdr.go @@ -2,11 +2,9 @@ package utils import ( "context" - "fmt" "strings" "github.com/pkg/errors" - "github.com/spf13/cobra" "github.com/mrlyc/cmdr/core" ) @@ -89,18 +87,3 @@ func UpgradeCmdr(ctx context.Context, cfg core.Configuration, url, version strin return nil } - -func RunCobraCommandWith(provider core.CommandProvider, fn func(cfg core.Configuration, manager core.CommandManager) error) func(cmd *cobra.Command, args []string) { - return func(cmd *cobra.Command, args []string) { - cfg := core.GetConfiguration() - - manager, err := core.NewCommandManager(provider, cfg) - if err != nil { - ExitOnError("Failed to create command manager", err) - } - - defer CallClose(manager) - - ExitOnError(fmt.Sprintf("Failed to run command %s", cmd.Name()), fn(cfg, manager)) - } -} diff --git a/core/utils/command.go b/core/utils/command.go new file mode 100644 index 0000000..a9bf79e --- /dev/null +++ b/core/utils/command.go @@ -0,0 +1,205 @@ +package utils + +import ( + "fmt" + "strings" + "sync" + + "github.com/spf13/cobra" + + "github.com/mrlyc/cmdr/core" +) + +func RunCobraCommandWith(provider core.CommandProvider, fn func(cfg core.Configuration, manager core.CommandManager) error) func(cmd *cobra.Command, args []string) { + return func(cmd *cobra.Command, args []string) { + cfg := core.GetConfiguration() + + manager, err := core.NewCommandManager(provider, cfg) + if err != nil { + ExitOnError("Failed to create command manager", err) + } + + defer CallClose(manager) + + ExitOnError(fmt.Sprintf("Failed to run command %s", cmd.Name()), fn(cfg, manager)) + } +} + +type CobraCommandCompleteHelper struct { + managerProvider core.CommandProvider + commands []core.Command + queryOnce sync.Once + cobraCommand *cobra.Command + flagName string + flagVersion string + flagLocation string + flagActivate string +} + +func (h *CobraCommandCompleteHelper) updateQuery(query core.CommandQuery) core.CommandQuery { + flags := h.cobraCommand.Flags() + name, err := flags.GetString(h.flagName) + if err == nil && name != "" { + query.WithName(name) + } + + version, err := flags.GetString(h.flagVersion) + if err == nil && version != "" { + query.WithVersion(version) + } + + location, err := flags.GetString(h.flagLocation) + if err == nil && location != "" { + query.WithLocation(location) + } + + activate, err := flags.GetBool(h.flagActivate) + if err == nil && activate { + query.WithActivated(activate) + } + + return query +} + +func (h *CobraCommandCompleteHelper) getCommands() []core.Command { + logger := core.Logger + + h.queryOnce.Do(func() { + manager, err := core.NewCommandManager(h.managerProvider, core.GetConfiguration()) + if err != nil { + logger.Debug("Failed to create command manager", map[string]interface{}{ + "error": err, + }) + return + } + + defer manager.Close() + + query, err := manager.Query() + if err != nil { + logger.Debug("Failed to create command query", map[string]interface{}{ + "error": err, + }) + return + } + + h.commands, err = h.updateQuery(query).All() + if err != nil { + logger.Debug("Failed to query commands", map[string]interface{}{ + "error": err, + }) + return + } + }) + + return h.commands +} + +func (h *CobraCommandCompleteHelper) isFlagSet(name string) bool { + flags := h.cobraCommand.Flags() + return flags.Lookup(name) != nil +} + +func (h *CobraCommandCompleteHelper) GetNameSlice(prefix string) []string { + commands := h.getCommands() + results := make([]string, 0, len(commands)) + + for _, command := range commands { + name := command.GetName() + if strings.HasPrefix(name, prefix) { + results = append(results, name) + } + } + + return results +} + +func (h *CobraCommandCompleteHelper) GetVersionSlice(prefix string) []string { + commands := h.getCommands() + results := make([]string, 0, len(commands)) + + for _, command := range commands { + version := command.GetVersion() + if strings.HasPrefix(version, prefix) { + results = append(results, version) + } + } + + return results +} + +func (h *CobraCommandCompleteHelper) GetLocationSlice(prefix string) []string { + commands := h.getCommands() + results := make([]string, 0, len(commands)) + + for _, command := range commands { + location := command.GetLocation() + if strings.HasPrefix(location, prefix) { + results = append(results, location) + } + } + + return results +} + +func (h *CobraCommandCompleteHelper) RegisterNameFunc() error { + return h.cobraCommand.RegisterFlagCompletionFunc( + h.flagName, + func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return h.GetNameSlice(toComplete), cobra.ShellCompDirectiveDefault + }, + ) +} + +func (h *CobraCommandCompleteHelper) RegisterVersionFunc() error { + return h.cobraCommand.RegisterFlagCompletionFunc( + h.flagVersion, + func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return h.GetVersionSlice(toComplete), cobra.ShellCompDirectiveDefault + }, + ) +} + +func (h *CobraCommandCompleteHelper) RegisterLocationFunc() error { + return h.cobraCommand.RegisterFlagCompletionFunc( + h.flagLocation, + func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return h.GetLocationSlice(toComplete), cobra.ShellCompDirectiveDefault + }, + ) +} + +func (h *CobraCommandCompleteHelper) RegisterAll() error { + mappings := map[string]func() error{ + h.flagName: h.RegisterNameFunc, + h.flagVersion: h.RegisterVersionFunc, + h.flagLocation: h.RegisterLocationFunc, + } + + for name, fn := range mappings { + if h.isFlagSet(name) { + err := fn() + if err != nil { + return err + } + } + } + + return nil +} + +func NewCobraCommandCompleteHelper(cmd *cobra.Command, provider core.CommandProvider) *CobraCommandCompleteHelper { + return &CobraCommandCompleteHelper{ + managerProvider: provider, + cobraCommand: cmd, + commands: make([]core.Command, 0), + flagName: "name", + flagVersion: "version", + flagLocation: "location", + flagActivate: "activate", + } +} + +func NewDefaultCobraCommandCompleteHelper(cmd *cobra.Command) *CobraCommandCompleteHelper { + return NewCobraCommandCompleteHelper(cmd, core.CommandProviderDefault) +} diff --git a/core/utils/command_test.go b/core/utils/command_test.go new file mode 100644 index 0000000..f327fe9 --- /dev/null +++ b/core/utils/command_test.go @@ -0,0 +1,130 @@ +package utils_test + +import ( + "github.com/golang/mock/gomock" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/spf13/cobra" + + "github.com/mrlyc/cmdr/core" + "github.com/mrlyc/cmdr/core/mock" + "github.com/mrlyc/cmdr/core/utils" +) + +var _ = Describe("Command", func() { + Context("CobraCommandCompleteHelper", func() { + var ( + ctrl *gomock.Controller + mockManager *mock.MockCommandManager + mockQuery *mock.MockCommandQuery + commandA, commandB *mock.MockCommand + cobraCommand *cobra.Command + helper *utils.CobraCommandCompleteHelper + ) + + BeforeEach(func() { + ctrl = gomock.NewController(GinkgoT()) + mockQuery = mock.NewMockCommandQuery(ctrl) + mockManager = mock.NewMockCommandManager(ctrl) + cobraCommand = &cobra.Command{} + helper = utils.NewCobraCommandCompleteHelper(cobraCommand, core.CommandProviderUnknown) + + mockManager.EXPECT().Query().Return(mockQuery, nil).AnyTimes() + mockQuery.EXPECT().All().Return([]core.Command{commandA, commandB}, nil).AnyTimes() + + core.RegisterCommandManagerFactory( + core.CommandProviderUnknown, + func(cfg core.Configuration) (core.CommandManager, error) { + return mockManager, nil + }, + ) + + commandA = mock.NewMockCommand(ctrl) + commandA.EXPECT().GetName().Return("command-a").AnyTimes() + commandA.EXPECT().GetVersion().Return("1.0.0").AnyTimes() + commandA.EXPECT().GetLocation().Return("/path/to/command-a").AnyTimes() + + commandB = mock.NewMockCommand(ctrl) + commandB.EXPECT().GetName().Return("command-b").AnyTimes() + commandB.EXPECT().GetVersion().Return("1.0.1").AnyTimes() + commandB.EXPECT().GetLocation().Return("/path/to/command-b").AnyTimes() + }) + + AfterEach(func() { + ctrl.Finish() + }) + + It("should register command when flags not set", func() { + Expect(helper.RegisterAll()).To(BeNil()) + }) + + It("should register all command when flags set", func() { + flags := cobraCommand.Flags() + flags.String("name", "", "command name") + flags.String("version", "", "command version") + flags.String("location", "", "command location") + + Expect(helper.RegisterAll()).To(BeNil()) + }) + + Context("Complete", func() { + BeforeEach(func() { + mockManager.EXPECT().Close() + }) + + It("should query command name when it is not empty", func() { + mockQuery.EXPECT().WithName("x").Return(mockQuery) + + cobraCommand.Flags().String("name", "x", "command name") + Expect(helper.RegisterNameFunc()).To(BeNil()) + + helper.GetNameSlice("") + }) + + It("should query command name by prefix", func() { + cobraCommand.Flags().String("name", "", "command name") + Expect(helper.RegisterNameFunc()).To(BeNil()) + + Expect(helper.GetNameSlice("command")).To(Equal([]string{"command-a", "command-b"})) + Expect(helper.GetNameSlice("command-a")).To(Equal([]string{"command-a"})) + }) + + It("should query command version when it is not empty", func() { + mockQuery.EXPECT().WithVersion("x").Return(mockQuery) + + cobraCommand.Flags().String("version", "x", "command version") + Expect(helper.RegisterVersionFunc()).To(BeNil()) + + helper.GetVersionSlice("") + }) + + It("should query command version by prefix", func() { + cobraCommand.Flags().String("version", "", "command version") + Expect(helper.RegisterVersionFunc()).To(BeNil()) + + Expect(helper.GetVersionSlice("1.0")).To(Equal([]string{"1.0.0", "1.0.1"})) + Expect(helper.GetVersionSlice("1.0.1")).To(Equal([]string{"1.0.1"})) + }) + + It("should query command location when it is not empty", func() { + mockQuery.EXPECT().WithLocation("x").Return(mockQuery) + + cobraCommand.Flags().String("location", "x", "command location") + Expect(helper.RegisterLocationFunc()).To(BeNil()) + + helper.GetLocationSlice("") + }) + + It("should query command location by prefix", func() { + cobraCommand.Flags().String("location", "", "command location") + Expect(helper.RegisterLocationFunc()).To(BeNil()) + + Expect(helper.GetLocationSlice("/path/to/command")).To(Equal( + []string{"/path/to/command-a", "/path/to/command-b"}, + )) + Expect(helper.GetLocationSlice("/path/to/command-a")).To(Equal([]string{"/path/to/command-a"})) + }) + }) + + }) +})