From 962ccad15d2399d8c89e63e6f975c292f2afc20d Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 23 Feb 2026 23:33:46 -0800 Subject: [PATCH 1/2] revert: login specific prompt logic --- cmd/auth/login.go | 16 ++++------------ internal/pkg/auth/login.go | 28 +++++----------------------- 2 files changed, 9 insertions(+), 35 deletions(-) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 65dd779b..9159e256 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -18,8 +18,6 @@ import ( "context" "fmt" - "github.com/slackapi/slack-cli/internal/config" - "github.com/slackapi/slack-cli/internal/experiment" "github.com/slackapi/slack-cli/internal/iostreams" authpkg "github.com/slackapi/slack-cli/internal/pkg/auth" "github.com/slackapi/slack-cli/internal/shared" @@ -111,7 +109,7 @@ func RunLoginCommand(clients *shared.ClientFactory, cmd *cobra.Command) (types.S return types.SlackAuth{}, err } if selectedAuth.Token != "" { - printAuthSuccess(cmd, clients.Config, clients.IO, credentialsPath, selectedAuth.Token) + printAuthSuccess(cmd, clients.IO, credentialsPath, selectedAuth.Token) printAuthNextSteps(ctx, clients) } return selectedAuth, err @@ -121,14 +119,14 @@ func RunLoginCommand(clients *shared.ClientFactory, cmd *cobra.Command) (types.S if err != nil { return types.SlackAuth{}, err } else { - printAuthSuccess(cmd, clients.Config, clients.IO, credentialsPath, selectedAuth.Token) + printAuthSuccess(cmd, clients.IO, credentialsPath, selectedAuth.Token) printAuthNextSteps(ctx, clients) } return selectedAuth, nil } -func printAuthSuccess(cmd *cobra.Command, config *config.Config, IO iostreams.IOStreamer, credentialsPath string, token string) { +func printAuthSuccess(cmd *cobra.Command, IO iostreams.IOStreamer, credentialsPath string, token string) { ctx := cmd.Context() var secondaryLog string @@ -138,13 +136,7 @@ func printAuthSuccess(cmd *cobra.Command, config *config.Config, IO iostreams.IO secondaryLog = fmt.Sprintf("Service token:\n\n %s\n\nMake sure to copy the token now and save it safely.", token) } - // The legacy prompt leaves no blank line before the success message, so - // print one here. The Charm-based prompt already handles spacing. - if !config.WithExperimentOn(experiment.Charm) { - IO.PrintInfo(ctx, false, "") - } - - IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{ + IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{ Emoji: "key", Text: "You've successfully authenticated!", Secondary: []string{secondaryLog}, diff --git a/internal/pkg/auth/login.go b/internal/pkg/auth/login.go index ef78ff3a..f5cfa5d6 100644 --- a/internal/pkg/auth/login.go +++ b/internal/pkg/auth/login.go @@ -20,12 +20,10 @@ import ( "strings" "time" - "github.com/charmbracelet/huh" "github.com/opentracing/opentracing-go" "github.com/slackapi/slack-cli/internal/api" "github.com/slackapi/slack-cli/internal/auth" "github.com/slackapi/slack-cli/internal/config" - "github.com/slackapi/slack-cli/internal/experiment" "github.com/slackapi/slack-cli/internal/iostreams" "github.com/slackapi/slack-cli/internal/pkg/version" "github.com/slackapi/slack-cli/internal/shared" @@ -142,27 +140,11 @@ func createNewAuth(ctx context.Context, apiClient api.APIInterface, authClient a return types.SlackAuth{}, "", err } - challengeCode := "" - if !config.WithExperimentOn(experiment.Charm) { - challengeCode, err = io.InputPrompt(ctx, "Enter challenge code", iostreams.InputPromptConfig{ - Required: true, - }) - if err != nil { - return types.SlackAuth{}, "", err - } - } else { - form := huh.NewForm( - huh.NewGroup( - huh.NewInput(). - Title("Enter challenge code"). - Validate(huh.ValidateMinLength(1)). - Value(&challengeCode), - ), - ) - err := form.Run() - if err != nil { - return types.SlackAuth{}, "", err - } + challengeCode, err := io.InputPrompt(ctx, "Enter challenge code", iostreams.InputPromptConfig{ + Required: true, + }) + if err != nil { + return types.SlackAuth{}, "", err } authExchangeRes, err := apiClient.ExchangeAuthTicket(ctx, authTicket, challengeCode, version.Get()) From 952c73117d99c5a9e05552e8651e3e509c8a4439 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 23 Feb 2026 23:51:18 -0800 Subject: [PATCH 2/2] feat(experiment): add charm prompts to iostreams --- internal/iostreams/charm.go | 123 +++++++++++++++++++++++++++++++++++ internal/iostreams/survey.go | 20 ++++++ 2 files changed, 143 insertions(+) create mode 100644 internal/iostreams/charm.go diff --git a/internal/iostreams/charm.go b/internal/iostreams/charm.go new file mode 100644 index 00000000..d01b255b --- /dev/null +++ b/internal/iostreams/charm.go @@ -0,0 +1,123 @@ +// Copyright 2022-2026 Salesforce, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package iostreams + +// Charm-based prompt implementations using the huh library +// These are used when the "charm" experiment is enabled + +import ( + "context" + "slices" + + "github.com/charmbracelet/huh" +) + +// charmInputPrompt prompts for text input using a charm huh form +func charmInputPrompt(_ *IOStreams, _ context.Context, message string, cfg InputPromptConfig) (string, error) { + var input string + field := huh.NewInput(). + Title(message). + Value(&input) + if cfg.Required { + field.Validate(huh.ValidateMinLength(1)) + } + err := huh.NewForm(huh.NewGroup(field)).Run() + if err != nil { + return "", err + } + return input, nil +} + +// charmConfirmPrompt prompts for a yes/no confirmation using a charm huh form +func charmConfirmPrompt(_ *IOStreams, _ context.Context, message string, defaultValue bool) (bool, error) { + var choice = defaultValue + field := huh.NewConfirm(). + Title(message). + Value(&choice) + err := huh.NewForm(huh.NewGroup(field)).Run() + if err != nil { + return false, err + } + return choice, nil +} + +// charmSelectPrompt prompts the user to select one option using a charm huh form +func charmSelectPrompt(_ *IOStreams, _ context.Context, msg string, options []string, cfg SelectPromptConfig) (SelectPromptResponse, error) { + var selected string + var opts []huh.Option[string] + for _, opt := range options { + key := opt + if cfg.Description != nil { + if desc := cfg.Description(opt, len(opts)); desc != "" { + key = opt + "\n " + desc + } + } + opts = append(opts, huh.NewOption(key, opt)) + } + + field := huh.NewSelect[string](). + Title(msg). + Options(opts...). + Value(&selected) + + if cfg.PageSize > 0 { + field.Height(cfg.PageSize + 2) + } + + err := huh.NewForm(huh.NewGroup(field)).Run() + if err != nil { + return SelectPromptResponse{}, err + } + + index := slices.Index(options, selected) + return SelectPromptResponse{Prompt: true, Index: index, Option: selected}, nil +} + +// charmPasswordPrompt prompts for a password (hidden input) using a charm huh form +func charmPasswordPrompt(_ *IOStreams, _ context.Context, message string, cfg PasswordPromptConfig) (PasswordPromptResponse, error) { + var input string + field := huh.NewInput(). + Title(message). + EchoMode(huh.EchoModePassword). + Value(&input) + if cfg.Required { + field.Validate(huh.ValidateMinLength(1)) + } + err := huh.NewForm(huh.NewGroup(field)).Run() + if err != nil { + return PasswordPromptResponse{}, err + } + return PasswordPromptResponse{Prompt: true, Value: input}, nil +} + +// charmMultiSelectPrompt prompts the user to select multiple options using a charm huh form +func charmMultiSelectPrompt(_ *IOStreams, _ context.Context, message string, options []string) ([]string, error) { + var selected []string + var opts []huh.Option[string] + for _, opt := range options { + opts = append(opts, huh.NewOption(opt, opt)) + } + + field := huh.NewMultiSelect[string](). + Title(message). + Options(opts...). + Value(&selected) + + err := huh.NewForm(huh.NewGroup(field)).Run() + if err != nil { + return []string{}, err + } + return selected, nil +} diff --git a/internal/iostreams/survey.go b/internal/iostreams/survey.go index 1f26f79e..8e2d95a1 100644 --- a/internal/iostreams/survey.go +++ b/internal/iostreams/survey.go @@ -27,6 +27,7 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2/terminal" + "github.com/slackapi/slack-cli/internal/experiment" "github.com/slackapi/slack-cli/internal/slackerror" "github.com/slackapi/slack-cli/internal/style" "github.com/spf13/pflag" @@ -120,6 +121,9 @@ func (cfg ConfirmPromptConfig) IsRequired() bool { // ConfirmPrompt prompts the user for a "yes" or "no" (true or false) value for // the message func (io *IOStreams) ConfirmPrompt(ctx context.Context, message string, defaultValue bool) (bool, error) { + if io.config.WithExperimentOn(experiment.Charm) { + return charmConfirmPrompt(io, ctx, message, defaultValue) + } // Temporarily swap default template for custom one defaultConfirmTemplate := survey.ConfirmQuestionTemplate @@ -191,6 +195,10 @@ func (cfg InputPromptConfig) IsRequired() bool { // InputPrompt prompts the user for a string value for the message, which can // optionally be made required func (io *IOStreams) InputPrompt(ctx context.Context, message string, cfg InputPromptConfig) (string, error) { + if io.config.WithExperimentOn(experiment.Charm) { + return charmInputPrompt(io, ctx, message, cfg) + } + defaultInputTemplate := survey.InputQuestionTemplate survey.InputQuestionTemplate = InputQuestionTemplate defer func() { @@ -263,6 +271,10 @@ func (cfg MultiSelectPromptConfig) IsRequired() bool { // MultiSelectPrompt prompts the user to select multiple values in a list and // returns the selected values func (io *IOStreams) MultiSelectPrompt(ctx context.Context, message string, options []string) ([]string, error) { + if io.config.WithExperimentOn(experiment.Charm) { + return charmMultiSelectPrompt(io, ctx, message, options) + } + defaultMultiSelectTemplate := survey.MultiSelectQuestionTemplate survey.MultiSelectQuestionTemplate = MultiSelectQuestionTemplate defer func() { @@ -340,6 +352,10 @@ func (io *IOStreams) PasswordPrompt(ctx context.Context, message string, cfg Pas return PasswordPromptResponse{}, errInteractivityFlags(cfg) } + if io.config.WithExperimentOn(experiment.Charm) { + return charmPasswordPrompt(io, ctx, message, cfg) + } + defaultPasswordTemplate := survey.PasswordQuestionTemplate if cfg.Template != "" { survey.PasswordQuestionTemplate = cfg.Template @@ -454,6 +470,10 @@ func (io *IOStreams) SelectPrompt(ctx context.Context, msg string, options []str } } + if io.config.WithExperimentOn(experiment.Charm) { + return charmSelectPrompt(io, ctx, msg, options, cfg) + } + defaultSelectTemplate := survey.SelectQuestionTemplate if cfg.Template != "" { survey.SelectQuestionTemplate = cfg.Template