Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 4 additions & 12 deletions cmd/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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},
Expand Down
123 changes: 123 additions & 0 deletions internal/iostreams/charm.go
Original file line number Diff line number Diff line change
@@ -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
}
20 changes: 20 additions & 0 deletions internal/iostreams/survey.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
28 changes: 5 additions & 23 deletions internal/pkg/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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())
Expand Down
Loading