diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 6f4849ce..4c3a96b1 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -162,6 +162,7 @@ aurs: - "git" package: |- install -Dm755 "./shopware-cli" "${pkgdir}/usr/bin/shopware-cli" + ln -sf "shopware-cli" "${pkgdir}/usr/bin/swx" # license install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/shopware-cli/LICENSE" @@ -185,6 +186,7 @@ nix: name: "Shopware Bot" email: github@shopware.com post_install: | + ln -sf "$out/bin/shopware-cli" "$out/bin/swx" installShellCompletion --cmd shopware-cli \ --bash <($out/bin/shopware-cli completion bash) \ --zsh <($out/bin/shopware-cli completion zsh) \ @@ -202,6 +204,9 @@ nfpms: description: A cli which contains handy helpful commands for daily Shopware tasks license: MIT contents: + - src: /usr/bin/shopware-cli + dst: /usr/bin/swx + type: symlink - src: ./completions/shopware-cli.bash dst: /etc/bash_completion.d/shopware-cli - src: ./completions/shopware-cli.fish @@ -225,6 +230,12 @@ homebrew_casks: homepage: https://shopware.com description: Shopware CLI helps Shopware developers manage extensions license: MIT + hooks: + post: + install: | + system_command "/bin/ln", args: ["-sf", "#{HOMEBREW_PREFIX}/bin/shopware-cli", "#{HOMEBREW_PREFIX}/bin/swx"] + uninstall: | + system_command "/bin/rm", args: ["-f", "#{HOMEBREW_PREFIX}/bin/swx"] completions: bash: completions/shopware-cli.bash zsh: completions/shopware-cli.zsh diff --git a/cmd/root.go b/cmd/root.go index 836dc0c8..a909f081 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,7 +3,9 @@ package cmd import ( "context" "os" + "path" "slices" + "strings" "github.com/mattn/go-isatty" "github.com/spf13/cobra" @@ -30,15 +32,80 @@ var rootCmd = &cobra.Command{ } func Execute(ctx context.Context) { - ctx = logging.WithLogger(ctx, logging.NewLogger(slices.Contains(os.Args, "--verbose"))) - ctx = system.WithInteraction(ctx, !slices.Contains(os.Args, "--no-interaction") && !slices.Contains(os.Args, "-n") && isatty.IsTerminal(os.Stdin.Fd())) + rootCmd.Use = commandNameFromArgs(os.Args) + args := mapAliasArgs(os.Args) + ctx = logging.WithLogger(ctx, logging.NewLogger(slices.Contains(args, "--verbose"))) + ctx = system.WithInteraction(ctx, !slices.Contains(args, "--no-interaction") && !slices.Contains(args, "-n") && isatty.IsTerminal(os.Stdin.Fd())) accountApi.SetUserAgent("shopware-cli/" + version) + rootCmd.SetArgs(args) if err := rootCmd.ExecuteContext(ctx); err != nil { logging.FromContext(ctx).Fatalln(err) } } +func mapAliasArgs(argv []string) []string { + if len(argv) == 0 { + return nil + } + + args := argv[1:] + if !isSwxAlias(argv[0]) { + return args + } + + if len(args) > 0 { + // Let users generate completion scripts for `swx` itself. + if args[0] == "completion" { + return args + } + + // Cobra shell completion calls these internal commands. + // Prefixing `project console` preserves swx-as-console behavior for completions. + if args[0] == "__complete" || args[0] == "__completeNoDesc" { + aliasedCompletionArgs := make([]string, 0, len(args)+2) + aliasedCompletionArgs = append(aliasedCompletionArgs, args[0], "project", "console") + aliasedCompletionArgs = append(aliasedCompletionArgs, args[1:]...) + + return aliasedCompletionArgs + } + } + + // When invoked via the `swx` symlink, forward everything to `project console`. + aliasedArgs := make([]string, 0, len(args)+3) + aliasedArgs = append(aliasedArgs, "project", "console") + + if len(args) == 0 { + aliasedArgs = append(aliasedArgs, "list") + } else { + aliasedArgs = append(aliasedArgs, args...) + } + + return aliasedArgs +} + +func isSwxAlias(binaryPath string) bool { + return strings.EqualFold(commandNameFromBinaryPath(binaryPath), "swx") +} + +func commandNameFromArgs(argv []string) string { + if len(argv) == 0 { + return rootCmd.Use + } + + return commandNameFromBinaryPath(argv[0]) +} + +func commandNameFromBinaryPath(binaryPath string) string { + normalizedPath := strings.ReplaceAll(binaryPath, "\\", "/") + binaryName := strings.TrimSuffix(path.Base(normalizedPath), path.Ext(normalizedPath)) + if binaryName == "" { + return rootCmd.Use + } + + return binaryName +} + func init() { rootCmd.SilenceErrors = true diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 00000000..25a5b7cd --- /dev/null +++ b/cmd/root_test.go @@ -0,0 +1,87 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMapAliasArgs_NoArgs(t *testing.T) { + t.Parallel() + assert.Equal(t, []string{}, mapAliasArgs([]string{"shopware-cli"})) +} + +func TestMapAliasArgs_RegularBinary(t *testing.T) { + t.Parallel() + args := mapAliasArgs([]string{"shopware-cli", "project", "console", "debug:router"}) + + assert.Equal(t, []string{"project", "console", "debug:router"}, args) +} + +func TestMapAliasArgs_SwxAlias(t *testing.T) { + t.Parallel() + args := mapAliasArgs([]string{"/usr/local/bin/swx", "debug:router", "--env=prod"}) + + assert.Equal(t, []string{"project", "console", "debug:router", "--env=prod"}, args) +} + +func TestMapAliasArgs_SwxAliasWithoutArgs(t *testing.T) { + t.Parallel() + args := mapAliasArgs([]string{"/usr/local/bin/swx"}) + + assert.Equal(t, []string{"project", "console", "list"}, args) +} + +func TestMapAliasArgs_SwxExeAlias(t *testing.T) { + t.Parallel() + args := mapAliasArgs([]string{"C:\\tools\\swx.exe", "cache:clear"}) + + assert.Equal(t, []string{"project", "console", "cache:clear"}, args) +} + +func TestMapAliasArgs_SwxCaseInsensitive(t *testing.T) { + t.Parallel() + args := mapAliasArgs([]string{"/usr/local/bin/SWX", "cache:clear"}) + + assert.Equal(t, []string{"project", "console", "cache:clear"}, args) +} + +func TestMapAliasArgs_SwxCompletion(t *testing.T) { + t.Parallel() + args := mapAliasArgs([]string{"/usr/local/bin/swx", "completion", "bash"}) + + assert.Equal(t, []string{"completion", "bash"}, args) +} + +func TestMapAliasArgs_SwxInternalCompletion(t *testing.T) { + t.Parallel() + args := mapAliasArgs([]string{"/usr/local/bin/swx", "__complete", "cache:clear"}) + + assert.Equal(t, []string{"__complete", "project", "console", "cache:clear"}, args) +} + +func TestMapAliasArgs_SwxInternalCompletionNoDesc(t *testing.T) { + t.Parallel() + args := mapAliasArgs([]string{"/usr/local/bin/swx", "__completeNoDesc", "cache:clear"}) + + assert.Equal(t, []string{"__completeNoDesc", "project", "console", "cache:clear"}, args) +} + +func TestMapAliasArgs_SwxHelp(t *testing.T) { + t.Parallel() + assert.Equal(t, []string{"project", "console", "--help"}, mapAliasArgs([]string{"/usr/local/bin/swx", "--help"})) + assert.Equal(t, []string{"project", "console", "-h"}, mapAliasArgs([]string{"/usr/local/bin/swx", "-h"})) +} + +func TestMapAliasArgs_SwxVersion(t *testing.T) { + t.Parallel() + assert.Equal(t, []string{"project", "console", "--version"}, mapAliasArgs([]string{"/usr/local/bin/swx", "--version"})) + assert.Equal(t, []string{"project", "console", "-v"}, mapAliasArgs([]string{"/usr/local/bin/swx", "-v"})) +} + +func TestCommandNameFromArgs(t *testing.T) { + t.Parallel() + assert.Equal(t, "shopware-cli", commandNameFromArgs([]string{"/usr/local/bin/shopware-cli"})) + assert.Equal(t, "swx", commandNameFromArgs([]string{"C:\\tools\\swx.exe"})) + assert.Equal(t, "shopware-cli", commandNameFromArgs(nil)) +}