Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0afa06c
refactor: route remaining survey callers through the testable shim
nf-matt May 22, 2026
2198e03
chore: add huh v2 dependency
nf-matt May 22, 2026
469441f
refactor(testable): introduce Prompter interface with huh-backed Confirm
nf-matt May 22, 2026
671e399
refactor(cfaws): migrate env.go to testable.Confirm
nf-matt May 22, 2026
e51ca33
refactor(testable): add Prompter.Select with huh-backed implementation
nf-matt May 22, 2026
a04d223
refactor: migrate simple survey.Select callers to testable.Select
nf-matt May 22, 2026
f689ec9
refactor(testable): add Prompter.Input with huh-backed implementation
nf-matt May 22, 2026
7750db6
refactor: migrate simple survey.Input callers to testable.Input
nf-matt May 22, 2026
b5bc940
refactor(testable): add Prompter.Password with huh-backed implementation
nf-matt May 22, 2026
4a4ad10
refactor: migrate Confirm callers and the lone Password caller
nf-matt May 22, 2026
160d4ad
refactor(testable): add validator-accepting Input and Select methods
nf-matt May 22, 2026
d6375c2
refactor: migrate validator-using survey callers
nf-matt May 22, 2026
56f4d80
refactor(testable): add SelectWithFilter using bubbles/list
nf-matt May 22, 2026
324a275
refactor(assume): migrate profile picker to testable.SelectWithFilter
nf-matt May 22, 2026
26983da
refactor(testable): add ConfirmWithHelp and InputWithHelp
nf-matt May 22, 2026
bc1d5b3
refactor: migrate Help-using survey callers
nf-matt May 22, 2026
6afe523
chore: drop AlecAivazis/survey/v2 dependency
nf-matt May 22, 2026
3d91fd6
feat(testable): theme huh forms and the filter picker
nf-matt May 22, 2026
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
33 changes: 26 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
module github.com/fwdcloudsec/granted

go 1.25.7
go 1.25.8

require (
github.com/99designs/keyring v1.2.2
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/aws/aws-sdk-go-v2 v1.41.2
github.com/aws/aws-sdk-go-v2/config v1.27.11
github.com/aws/aws-sdk-go-v2/service/sso v1.20.5
Expand All @@ -17,37 +16,59 @@ require (
)

require (
charm.land/bubbles/v2 v2.0.0
charm.land/bubbletea/v2 v2.0.2
charm.land/huh/v2 v2.0.3
github.com/alessio/shellescape v1.4.2
github.com/common-fate/clio v1.2.3
github.com/common-fate/grab v1.3.0
github.com/fatih/color v1.16.0
github.com/google/uuid v1.6.0
github.com/hashicorp/go-version v1.7.0
github.com/schollz/progressbar/v3 v3.13.1
go.uber.org/zap v1.26.0
gopkg.in/yaml.v3 v3.0.1
)

require (
charm.land/lipgloss/v2 v2.0.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mitchellh/reflectwalk v1.0.0 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
Expand Down Expand Up @@ -77,17 +98,15 @@ require (
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
github.com/joho/godotenv v1.4.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/olekukonko/tablewriter v0.0.5
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/stretchr/testify v1.10.0
go.uber.org/ratelimit v0.3.0
golang.org/x/sync v0.19.0
golang.org/x/sys v0.41.0
golang.org/x/sys v0.42.0
golang.org/x/term v0.40.0 // indirect
golang.org/x/text v0.34.0
gopkg.in/ini.v1 v1.67.0
Expand Down
87 changes: 66 additions & 21 deletions go.sum

Large diffs are not rendered by default.

10 changes: 2 additions & 8 deletions pkg/alias/alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import (
"path/filepath"
"strings"

"github.com/AlecAivazis/survey/v2"
"github.com/common-fate/clio"
"github.com/fwdcloudsec/granted/internal/build"
"github.com/fwdcloudsec/granted/pkg/shells"
"github.com/fwdcloudsec/granted/pkg/testable"
"github.com/fatih/color"
)

Expand Down Expand Up @@ -197,13 +197,7 @@ func SetupShellWizard(autoConfigure bool) error {
// skip prompt if autoConfigure is set to true
if !autoConfigure {
clio.Info("To assume roles with Granted, we need to add an alias to your shell profile (https://docs.commonfate.io/granted/internals/shell-alias)")
withStdio := survey.WithStdio(os.Stdin, os.Stderr, os.Stderr)
in := &survey.Confirm{
Message: fmt.Sprintf("Install %s alias at %s", shell, cfg.File),
Default: true,
}
var confirm bool
err = survey.AskOne(in, &confirm, withStdio)
confirm, err := testable.Confirm(fmt.Sprintf("Install %s alias at %s", shell, cfg.File), true)
if err != nil {
return err
}
Expand Down
52 changes: 8 additions & 44 deletions pkg/assume/assume.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (

"os"

"github.com/AlecAivazis/survey/v2"
"github.com/alessio/shellescape"
"github.com/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
Expand Down Expand Up @@ -600,7 +599,7 @@ func EnvKeys(creds aws.Credentials, region string) []string {
"AWS_REGION=" + region}
}

func filterMultiToken(filterValue string, optValue string, optIndex int) bool {
func filterMultiToken(filterValue, optValue string) bool {
optValue = strings.ToLower(optValue)
filters := strings.Split(strings.ToLower(filterValue), " ")
for _, filter := range filters {
Expand All @@ -625,7 +624,6 @@ func printFlagUsage(region, service string) {
}

func QueryProfiles(profiles *cfaws.Profiles) (string, error) {
withStdio := survey.WithStdio(os.Stdin, os.Stderr, os.Stderr)
// load config to check frecency enabled
cfg, err := config.Load()
if err != nil {
Expand Down Expand Up @@ -660,57 +658,23 @@ func QueryProfiles(profiles *cfaws.Profiles) (string, error) {
profileNameMap[stringKey] = pn
profileKeys[i] = stringKey
}
var promptHeader string
// only add the description headers if there are profiles using descriptions
if hasDescriptions {
promptHeader = fmt.Sprintf(`{{- " %s\n"}}`, color.New(color.Underline, color.Bold).Sprintf("%-"+strconv.Itoa(longestProfileNameLength)+"s%s", "Profile", "Description"))
}
// This overrides the default prompt template to add a header row above the options
// this should be reset back to the original template after the call to AskOne
originalSelectTemplate := survey.SelectQuestionTemplate
survey.SelectQuestionTemplate = fmt.Sprintf(`
{{- define "option"}}
{{- if eq .SelectedIndex .CurrentIndex }}{{color .Config.Icons.SelectFocus.Format }}{{ .Config.Icons.SelectFocus.Text }} {{else}}{{color "default"}} {{end}}
{{- .CurrentOpt.Value}}{{ if ne ($.GetDescription .CurrentOpt) "" }} - {{color "cyan"}}{{ $.GetDescription .CurrentOpt }}{{end}}
{{- color "reset"}}
{{end}}
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
{{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}}
{{- if .ShowAnswer}}{{color "cyan"}} {{.Answer}}{{color "reset"}}{{"\n"}}
{{- else}}
{{- " "}}{{- color "cyan"}}[Use arrows to move, type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}}
{{- "\n"}}
%s{{- "\n"}}
{{- range $ix, $option := .PageEntries}}
{{- template "option" $.IterateOption $ix $option}}
{{- end}}
{{- end}}`, promptHeader)

clio.NewLine()
// Replicate the logic from original assume fn.
in := survey.Select{
Message: "Please select the profile you would like to assume:",
Options: profileKeys,
Filter: filterMultiToken,
}
if len(profileKeys) == 0 {
return "", clierr.New("Granted couldn't find any AWS profiles in your config file or your credentials file",
clierr.Info("You can add profiles to your AWS config by following our guide: "),
clierr.Info("https://docs.commonfate.io/granted/getting-started#set-up-your-aws-profile-file"),
)
}

var profileName string
clio.NewLine()
if hasDescriptions {
header := color.New(color.Underline, color.Bold).Sprintf("%-"+strconv.Itoa(longestProfileNameLength)+"s%s", "Profile", "Description")
fmt.Fprintln(os.Stderr, " "+header)
}

err = testable.AskOne(&in, &profileName, withStdio)
selected, err := testable.SelectWithFilter("Please select the profile you would like to assume:", profileKeys, filterMultiToken)
if err != nil {
return "", err
}
// Reset the template for select questions to the original
survey.SelectQuestionTemplate = originalSelectTemplate
profileName = profileNameMap[profileName]
// background task to update the frecency cache

return profileName, nil
return profileNameMap[selected], nil
}
58 changes: 16 additions & 42 deletions pkg/browser/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"strings"
"time"

"github.com/AlecAivazis/survey/v2"
"github.com/common-fate/clio"
"github.com/fwdcloudsec/granted/pkg/config"
"github.com/fwdcloudsec/granted/pkg/testable"
Expand Down Expand Up @@ -47,14 +46,11 @@ func UserHasDefaultBrowser(ctx *cli.Context) (bool, error) {
func HandleManualBrowserSelection() (string, error) {
// didn't find it, request manual input

withStdio := survey.WithStdio(os.Stdin, os.Stderr, os.Stderr)
in := survey.Select{
Message: "Select one of the browsers from the list",
Options: []string{"Chrome", "Brave", "Edge", "Vivaldi", "Firefox", "Waterfox", "Chromium", "Safari", "Stdout", "FirefoxStdout", "Firefox Developer Edition", "Firefox Nightly", "Arc", "Zen", "Custom"},
}
var selection string
clio.NewLine()
err := testable.AskOne(&in, &selection, withStdio)
selection, err := testable.Select(
"Select one of the browsers from the list",
[]string{"Chrome", "Brave", "Edge", "Vivaldi", "Firefox", "Waterfox", "Chromium", "Safari", "Stdout", "FirefoxStdout", "Firefox Developer Edition", "Firefox Nightly", "Arc", "Zen", "Custom"},
)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -194,7 +190,6 @@ func DetectInstallation(browserKey string) (string, bool) {
}

func HandleBrowserWizard(ctx *cli.Context) (string, error) {
withStdio := survey.WithStdio(os.Stdin, os.Stderr, os.Stderr)
browserName, err := Find()
if err != nil {
return "", err
Expand All @@ -204,12 +199,7 @@ func HandleBrowserWizard(ctx *cli.Context) (string, error) {
clio.Info("Thanks for using Granted!")
clio.Infof("By default, Granted will open the AWS console with this browser: %s", browserTitle)
clio.Warn("Granted works best with Firefox but also supports Chrome, Brave, and Edge (https://docs.commonfate.io/granted/introduction#supported-browsers). You can change this setting later by running 'granted browser set'")
in := survey.Confirm{
Message: "Use Firefox as default Granted browser?",
Default: true,
}
var confirm bool
err = testable.AskOne(&in, &confirm, withStdio)
confirm, err := testable.Confirm("Use Firefox as default Granted browser?", true)
if err != nil {
return "", err
}
Expand All @@ -223,7 +213,6 @@ func HandleBrowserWizard(ctx *cli.Context) (string, error) {
// ConfigureBrowserSelection will verify the existance of the browser executable and promot for a path if it cannot be found
func ConfigureBrowserSelection(browserName string, path string) error {
browserKey := GetBrowserKey(browserName)
withStdio := survey.WithStdio(os.Stdin, os.Stderr, os.Stderr)
title := cases.Title(language.AmericanEnglish)
browserTitle := title.String(strings.ToLower(browserKey))
// We allow users to configure a custom install path if we cannot detect the installation
Expand All @@ -250,9 +239,9 @@ func ConfigureBrowserSelection(browserName string, path string) error {
validPath := false
for !validPath {
// prompt for custom path
bpIn := survey.Input{Message: fmt.Sprintf("Please enter the full path to your browser installation for %s:", browserTitle)}
clio.NewLine()
err := testable.AskOne(&bpIn, &customBrowserPath, withStdio)
var err error
customBrowserPath, err = testable.Input(fmt.Sprintf("Please enter the full path to your browser installation for %s:", browserTitle), "")
if err != nil {
return err
}
Expand Down Expand Up @@ -304,14 +293,11 @@ func GrantedIntroduction() {
}

func SSOBrowser(grantedDefaultBrowser string) error {
withStdio := survey.WithStdio(os.Stdin, os.Stderr, os.Stderr)
in := &survey.Confirm{
Message: "Use a different browser than your default browser for SSO login?",
Default: false,
Help: "For example, if you normally use a password manager in Chrome for your AWS login but Chrome is not your default browser, you would choose to use Chrome for SSO logins. You can change this later by running 'granted browser set-sso'",
}
var confirm bool
err := testable.AskOne(in, &confirm, withStdio)
confirm, err := testable.ConfirmWithHelp(
"Use a different browser than your default browser for SSO login?",
false,
"For example, if you normally use a password manager in Chrome for your AWS login but Chrome is not your default browser, you would choose to use Chrome for SSO logins. You can change this later by running 'granted browser set-sso'",
)
if err != nil {
return err
}
Expand Down Expand Up @@ -342,14 +328,8 @@ func RunFirefoxExtensionPrompts(browserPath string, browserName string) error {

label := fmt.Sprintf("Open %s to download the extension?", browserName)

withStdio := survey.WithStdio(os.Stdin, os.Stderr, os.Stderr)
in := &survey.Select{
Message: label,
Options: []string{"Yes", "Already installed", "No"},
}
var out string
clio.NewLine()
err := testable.AskOne(in, &out, withStdio)
out, err := testable.Select(label, []string{"Yes", "Already installed", "No"})
if err != nil {
return err
}
Expand All @@ -376,13 +356,8 @@ func RunFirefoxExtensionPrompts(browserPath string, browserName string) error {
return err
}
time.Sleep(time.Second * 2)
confIn := &survey.Confirm{
Message: "Type Y to continue once you have installed the extension",
Default: true,
}
var confirm bool
clio.NewLine()
err = testable.AskOne(confIn, &confirm, withStdio)
confirm, err := testable.Confirm("Type Y to continue once you have installed the extension", true)
if err != nil {
return err
}
Expand All @@ -401,7 +376,6 @@ func AskAndGetBrowserPath() (string, error) {
}

browserKey := GetBrowserKey(outcome)
withStdio := survey.WithStdio(os.Stdin, os.Stderr, os.Stderr)
title := cases.Title(language.AmericanEnglish)
browserTitle := title.String(strings.ToLower(browserKey))
// We allow users to configure a custom install path is we cannot detect the installation
Expand All @@ -416,9 +390,9 @@ func AskAndGetBrowserPath() (string, error) {
validPath := false
for !validPath {
// prompt for custom path
bpIn := survey.Input{Message: fmt.Sprintf("Please enter the full path to your browser installation for %s:", browserTitle)}
clio.NewLine()
err := testable.AskOne(&bpIn, &customBrowserPath, withStdio)
var err error
customBrowserPath, err = testable.Input(fmt.Sprintf("Please enter the full path to your browser installation for %s:", browserTitle), "")
if err != nil {
return "", err
}
Expand Down
7 changes: 1 addition & 6 deletions pkg/cfaws/creds.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"os"
"time"

"github.com/AlecAivazis/survey/v2"
"github.com/aws/aws-sdk-go-v2/aws"
ssotypes "github.com/aws/aws-sdk-go-v2/service/sso/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
Expand Down Expand Up @@ -122,9 +121,5 @@ func GetCredentialsCreds(ctx context.Context, c *Profile) (aws.Credentials, erro
}

func MfaTokenProvider() (string, error) {
in := survey.Input{Message: "MFA Token"}
var out string
withStdio := survey.WithStdio(os.Stdin, os.Stderr, os.Stderr)
err := testable.AskOne(&in, &out, withStdio)
return out, err
return testable.Input("MFA Token", "")
}
5 changes: 1 addition & 4 deletions pkg/cfaws/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (

"os"

"github.com/AlecAivazis/survey/v2"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/common-fate/clio"
"github.com/fwdcloudsec/granted/pkg/testable"
Expand All @@ -15,10 +14,8 @@ import (
// WriteCredentialsToDotenv will check if a .env file exists and prompt to create one if it does not.
// After the file exists, it will be opened, credentaisl added and then written to disc
func WriteCredentialsToDotenv(region string, creds aws.Credentials) error {
withStdio := survey.WithStdio(os.Stdin, os.Stderr, os.Stderr)
if _, err := os.Stat("./.env"); os.IsNotExist(err) {
ans := false
err = testable.AskOne(&survey.Confirm{Message: "No .env file found in the current directory, would you like to create one?"}, &ans, withStdio)
ans, err := testable.Confirm("No .env file found in the current directory, would you like to create one?", false)
if err != nil {
return err
}
Expand Down
Loading