From b5451fce73adb7ce5e620c7ea47f39e885a0d9c2 Mon Sep 17 00:00:00 2001 From: mitchell Date: Fri, 15 Nov 2024 15:44:35 -0500 Subject: [PATCH] Initial support for enabling prompts to react to `--force` flag. --- cmd/state/internal/cmdtree/clean.go | 14 ++++++-- cmd/state/internal/cmdtree/packages.go | 9 +++++ cmd/state/internal/cmdtree/reset.go | 4 ++- cmd/state/internal/cmdtree/revert.go | 4 ++- cmd/state/internal/cmdtree/update.go | 8 +++-- cmd/state/internal/cmdtree/use.go | 4 ++- internal/locale/locales/en-us.yaml | 6 ++++ internal/prompt/prompt.go | 49 ++++++++++++++++++++----- internal/runbits/auth/keypair.go | 20 ++++++----- internal/runbits/auth/login.go | 2 +- internal/runbits/cves/cves.go | 29 +++++++-------- internal/runners/clean/cache.go | 50 ++++++++++++++------------ internal/runners/clean/config.go | 26 ++++++++------ internal/runners/clean/uninstall.go | 32 +++++++++-------- internal/runners/projects/delete.go | 10 ++++-- internal/runners/projects/edit.go | 13 +++---- internal/runners/projects/move.go | 13 +++---- internal/runners/publish/publish.go | 12 +++---- internal/runners/push/push.go | 20 +++++++---- internal/runners/reset/reset.go | 10 +++--- internal/runners/revert/revert.go | 10 +++--- internal/runners/scripts/edit.go | 4 +-- internal/runners/update/lock.go | 18 +++++----- internal/runners/update/unlock.go | 22 ++++++------ internal/runners/upgrade/upgrade.go | 6 +++- internal/runners/use/reset.go | 13 ++++--- test/integration/projects_int_test.go | 2 +- 27 files changed, 257 insertions(+), 153 deletions(-) diff --git a/cmd/state/internal/cmdtree/clean.go b/cmd/state/internal/cmdtree/clean.go index 758f6bb4a1..37ca6cfbe8 100644 --- a/cmd/state/internal/cmdtree/clean.go +++ b/cmd/state/internal/cmdtree/clean.go @@ -59,7 +59,12 @@ func newCleanUninstallCommand(prime *primer.Values, globals *globalOptions) *cap return err } - params.NonInteractive = globals.NonInteractive // distinct from --force + if globals.NonInteractive { + prime.Prompt().SetInteractive(false) + } + if params.Force { + prime.Prompt().EnableForce() + } return runner.Run(¶ms) }, ) @@ -83,7 +88,9 @@ func newCleanCacheCommand(prime *primer.Values, globals *globalOptions) *captain }, }, func(ccmd *captain.Command, _ []string) error { - params.Force = globals.NonInteractive + if globals.NonInteractive { + prime.Prompt().SetInteractive(false) + } return runner.Run(¶ms) }, ) @@ -107,6 +114,9 @@ func newCleanConfigCommand(prime *primer.Values) *captain.Command { }, []*captain.Argument{}, func(ccmd *captain.Command, _ []string) error { + if params.Force { + prime.Prompt().EnableForce() + } return runner.Run(¶ms) }, ) diff --git a/cmd/state/internal/cmdtree/packages.go b/cmd/state/internal/cmdtree/packages.go index 636dc02dc3..820b189927 100644 --- a/cmd/state/internal/cmdtree/packages.go +++ b/cmd/state/internal/cmdtree/packages.go @@ -54,6 +54,7 @@ func newInstallCommand(prime *primer.Values) *captain.Command { runner := install.New(prime, model.NamespacePackage) params := install.Params{} + force := false var packagesRaw string cmd := captain.NewCommand( @@ -67,6 +68,11 @@ func newInstallCommand(prime *primer.Values) *captain.Command { Description: locale.T("package_flag_ts_description"), Value: ¶ms.Timestamp, }, + { + Name: "force", + Description: locale.Tl("package_flag_force_description", "Ignore security policy preventing packages with CVEs from being installed (not recommended)"), + Value: &force, + }, }, []*captain.Argument{ { @@ -82,6 +88,9 @@ func newInstallCommand(prime *primer.Values) *captain.Command { return locale.WrapInputError(err, "err_install_packages_args", "Invalid install arguments") } } + if force { + prime.Prompt().EnableForce() + } return runner.Run(params) }, ) diff --git a/cmd/state/internal/cmdtree/reset.go b/cmd/state/internal/cmdtree/reset.go index 44f550cc09..2a86485ab7 100644 --- a/cmd/state/internal/cmdtree/reset.go +++ b/cmd/state/internal/cmdtree/reset.go @@ -25,7 +25,9 @@ func newResetCommand(prime *primer.Values, globals *globalOptions) *captain.Comm }, }, func(ccmd *captain.Command, args []string) error { - params.Force = globals.NonInteractive + if globals.NonInteractive { + prime.Prompt().SetInteractive(false) + } return runner.Run(params) }, ).SetGroup(VCSGroup).SetSupportsStructuredOutput() diff --git a/cmd/state/internal/cmdtree/revert.go b/cmd/state/internal/cmdtree/revert.go index 8d01f8b34c..6150d53bf1 100644 --- a/cmd/state/internal/cmdtree/revert.go +++ b/cmd/state/internal/cmdtree/revert.go @@ -32,7 +32,9 @@ func newRevertCommand(prime *primer.Values, globals *globalOptions) *captain.Com }, }, func(ccmd *captain.Command, args []string) error { - params.Force = globals.NonInteractive + if globals.NonInteractive { + prime.Prompt().SetInteractive(false) + } return runner.Run(params) }, ).SetGroup(VCSGroup).SetSupportsStructuredOutput() diff --git a/cmd/state/internal/cmdtree/update.go b/cmd/state/internal/cmdtree/update.go index a027d6dbe6..45f20cab21 100644 --- a/cmd/state/internal/cmdtree/update.go +++ b/cmd/state/internal/cmdtree/update.go @@ -52,7 +52,9 @@ func newUpdateLockCommand(prime *primer.Values, globals *globalOptions) *captain }, []*captain.Argument{}, func(cmd *captain.Command, args []string) error { - params.NonInteractive = globals.NonInteractive + if globals.NonInteractive { + prime.Prompt().SetInteractive(false) + } return runner.Run(¶ms) }, ) @@ -73,7 +75,9 @@ func newUpdateUnlockCommand(prime *primer.Values, globals *globalOptions) *capta []*captain.Flag{}, []*captain.Argument{}, func(cmd *captain.Command, args []string) error { - params.NonInteractive = globals.NonInteractive + if globals.NonInteractive { + prime.Prompt().SetInteractive(false) + } return runner.Run(¶ms) }, ) diff --git a/cmd/state/internal/cmdtree/use.go b/cmd/state/internal/cmdtree/use.go index 2971339b51..5c4bd8d9bf 100644 --- a/cmd/state/internal/cmdtree/use.go +++ b/cmd/state/internal/cmdtree/use.go @@ -44,7 +44,9 @@ func newUseResetCommand(prime *primer.Values, globals *globalOptions) *captain.C []*captain.Flag{}, []*captain.Argument{}, func(_ *captain.Command, _ []string) error { - params.Force = globals.NonInteractive + if globals.NonInteractive { + prime.Prompt().SetInteractive(false) + } return use.NewReset(prime).Run(params) }, ) diff --git a/internal/locale/locales/en-us.yaml b/internal/locale/locales/en-us.yaml index 7030506100..add273ea81 100644 --- a/internal/locale/locales/en-us.yaml +++ b/internal/locale/locales/en-us.yaml @@ -1165,6 +1165,12 @@ warning_vulnerable_short: prompt_continue_pkg_operation: other: | Do you want to continue installing this dependency despite its vulnerabilities? +prompt_continue_force: + other: "Continuing because the '[ACTIONABLE]--force[/RESET]' flag is set." +prompt_continue_non_interactive: + other: "Continuting because State Tool is running in non-interactive mode." +prompt_abort_non_interactive: + other: "Aborting because State Tool is running in non-interactive mode. To bypass you can use the '[ACTIONABLE]--force[/RESET]' flag." unstable_command_warning: other: | This command is still in beta. If you want to opt-in to unstable features, run the following command: diff --git a/internal/prompt/prompt.go b/internal/prompt/prompt.go index be6def602c..4735947305 100644 --- a/internal/prompt/prompt.go +++ b/internal/prompt/prompt.go @@ -9,23 +9,37 @@ import ( "gopkg.in/AlecAivazis/survey.v1/terminal" "github.com/ActiveState/cli/internal/analytics/constants" + "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/output" ) +type ConfirmKind int + +const ( + // A confirm prompt was completed by the user. + User ConfirmKind = iota + // A confirm prompt was completed in non-interactive mode. + NonInteractive + // A confirm prompt was completed via --force. + Force +) + type EventDispatcher interface { EventWithLabel(category, action string, label string, dim ...*dimensions.Values) } -// Prompter is the interface used to run our prompt from, useful for mocking in tests +// Prompter is the interface used to run our prompt from type Prompter interface { Input(title, message string, defaultResponse *string, flags ...ValidatorFlag) (string, error) InputAndValidate(title, message string, defaultResponse *string, validator ValidatorFunc, flags ...ValidatorFlag) (string, error) Select(title, message string, choices []string, defaultResponse *string) (string, error) - Confirm(title, message string, defaultChoice *bool) (bool, error) + Confirm(title, message string, defaultChoice *bool, forcedChoice *bool) (bool, ConfirmKind, error) InputSecret(title, message string, flags ...ValidatorFlag) (string, error) IsInteractive() bool + SetInteractive(bool) + EnableForce() } // ValidatorFunc is a function pass to the Prompter to perform validation @@ -39,11 +53,12 @@ type Prompt struct { out output.Outputer analytics EventDispatcher isInteractive bool + isForced bool } // New creates a new prompter func New(isInteractive bool, an EventDispatcher) Prompter { - return &Prompt{output.Get(), an, isInteractive} + return &Prompt{output.Get(), an, isInteractive, false} } // IsInteractive checks if the prompts can be interactive or should just return default values @@ -51,6 +66,16 @@ func (p *Prompt) IsInteractive() bool { return p.isInteractive } +func (p *Prompt) SetInteractive(interactive bool) { + p.isInteractive = interactive +} + +// EnableForce forces confirm prompts to return the force value (which is often different from the +// non-interactive value). +func (p *Prompt) EnableForce() { + p.isForced = true +} + // ValidatorFlag represents flags for prompt functions to change their behavior on. type ValidatorFlag int @@ -158,13 +183,21 @@ func (p *Prompt) Select(title, message string, choices []string, defaultChoice * } // Confirm prompts user for yes or no response. -func (p *Prompt) Confirm(title, message string, defaultChoice *bool) (bool, error) { +func (p *Prompt) Confirm(title, message string, defaultChoice *bool, forcedChoice *bool) (bool, ConfirmKind, error) { + if p.isForced { + if forcedChoice == nil { + return false, Force, errs.New("No force option given for force-enabled prompt") + } + logging.Debug("Prompt %s confirmed with choice %v in force mode", title, forcedChoice) + return *forcedChoice, Force, nil + } + if !p.isInteractive { if defaultChoice != nil { logging.Debug("Prompt %s confirmed with default choice %v in non-interactive mode", title, defaultChoice) - return *defaultChoice, nil + return *defaultChoice, NonInteractive, nil } - return false, interactiveInputError(message) + return false, NonInteractive, interactiveInputError(message) } if title != "" { p.out.Notice(output.Emphasize(title)) @@ -186,11 +219,11 @@ func (p *Prompt) Confirm(title, message string, defaultChoice *bool) (bool, erro if err == terminal.InterruptErr { p.analytics.EventWithLabel(constants.CatPrompt, title, "interrupt") } - return false, locale.NewInputError(err.Error()) + return false, User, locale.NewInputError(err.Error()) } p.analytics.EventWithLabel(constants.CatPrompt, title, translateConfirm(resp)) - return resp, nil + return resp, User, nil } func translateConfirm(confirm bool) string { diff --git a/internal/runbits/auth/keypair.go b/internal/runbits/auth/keypair.go index 611060376e..c92ee7974b 100644 --- a/internal/runbits/auth/keypair.go +++ b/internal/runbits/auth/keypair.go @@ -9,6 +9,7 @@ import ( "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/prompt" + "github.com/ActiveState/cli/internal/rtutils/ptr" secretsapi "github.com/ActiveState/cli/pkg/platform/api/secrets" secretsModels "github.com/ActiveState/cli/pkg/platform/api/secrets/secrets_models" "github.com/ActiveState/cli/pkg/platform/authentication" @@ -112,19 +113,20 @@ func promptForPreviousPassphrase(prompt prompt.Prompter) (string, error) { return passphrase, nil } -func promptUserToRegenerateKeypair(passphrase string, cfg keypairs.Configurable, out output.Outputer, prompt prompt.Prompter, auth *authentication.Auth) error { - var err error +func promptUserToRegenerateKeypair(passphrase string, cfg keypairs.Configurable, out output.Outputer, prmpt prompt.Prompter, auth *authentication.Auth) error { // previous passphrase is invalid, inform user and ask if they want to generate a new keypair out.Notice(locale.T("auth_generate_new_keypair_message")) - yes, err := prompt.Confirm("", locale.T("auth_confirm_generate_new_keypair_prompt"), new(bool)) + yes, kind, err := prmpt.Confirm("", locale.T("auth_confirm_generate_new_keypair_prompt"), ptr.To(false), nil) if err != nil { - return err + return errs.Wrap(err, "Unable to confirm") } - if yes { - _, err = keypairs.GenerateAndSaveEncodedKeypair(cfg, secretsapi.Get(auth), passphrase, constants.DefaultRSABitLength, auth) - // TODO delete user's secrets - } else { - err = locale.NewError("auth_err_unrecoverable_keypair") + if !yes { + if kind == prompt.NonInteractive { + return locale.NewInputError("prompt_abort_non_interactive") + } + return locale.NewInputError("auth_err_unrecoverable_keypair") } + _, err = keypairs.GenerateAndSaveEncodedKeypair(cfg, secretsapi.Get(auth), passphrase, constants.DefaultRSABitLength, auth) + // TODO delete user's secrets return err } diff --git a/internal/runbits/auth/login.go b/internal/runbits/auth/login.go index 1159613f9a..785a759f02 100644 --- a/internal/runbits/auth/login.go +++ b/internal/runbits/auth/login.go @@ -266,7 +266,7 @@ func authenticateWithBrowser(out output.Outputer, auth *authentication.Auth, pro var cont bool var err error for !cont { - cont, err = prompt.Confirm(locale.Tl("continue", "Continue?"), locale.T("auth_press_enter"), ptr.To(false)) + cont, _, err = prompt.Confirm(locale.Tl("continue", "Continue?"), locale.T("auth_press_enter"), ptr.To(false), nil) if err != nil { return errs.Wrap(err, "Prompt failed") } diff --git a/internal/runbits/cves/cves.go b/internal/runbits/cves/cves.go index b9b598482b..0ecb893c8f 100644 --- a/internal/runbits/cves/cves.go +++ b/internal/runbits/cves/cves.go @@ -12,6 +12,7 @@ import ( configMediator "github.com/ActiveState/cli/internal/mediators/config" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" + "github.com/ActiveState/cli/internal/prompt" "github.com/ActiveState/cli/internal/rtutils/ptr" "github.com/ActiveState/cli/pkg/buildplan" vulnModel "github.com/ActiveState/cli/pkg/platform/api/vulnerabilities/model" @@ -104,20 +105,24 @@ func (c *CveReport) Report(newBuildPlan *buildplan.BuildPlan, oldBuildPlan *buil } c.summarizeCVEs(vulnerabilities) - cont, err := c.promptForSecurity() + + confirm, kind, err := c.prime.Prompt().Confirm("", locale.Tr("prompt_continue_pkg_operation"), ptr.To(false), ptr.To(true)) if err != nil { - return errs.Wrap(err, "Failed to prompt for security") + return errs.Wrap(err, "Unable to confirm") } - - if !cont { - if !c.prime.Prompt().IsInteractive() { + if !confirm { + if kind == prompt.NonInteractive { return errs.AddTips( - locale.NewInputError("err_pkgop_security_prompt", "Operation aborted due to security prompt"), + locale.NewInputError("prompt_abort_non_interactive"), locale.Tl("more_info_prompt", "To disable security prompting run: [ACTIONABLE]state config set security.prompt.enabled false[/RESET]"), ) } - return locale.NewInputError("err_pkgop_security_prompt", "Operation aborted due to security prompt") + return locale.NewInputError("err_pkgop_security_prompt", "Operation aborted by user") + } + if kind == prompt.Force { + c.prime.Output().Notice(locale.T("prompt_continue_force")) } + c.prime.Output().Notice("") // Empty line return nil } @@ -225,16 +230,6 @@ func (c *CveReport) summarizeCVEs(vulnerabilities model.VulnerableIngredientsByL out.Print("") } -func (c *CveReport) promptForSecurity() (bool, error) { - confirm, err := c.prime.Prompt().Confirm("", locale.Tr("prompt_continue_pkg_operation"), ptr.To(false)) - if err != nil { - return false, locale.WrapError(err, "err_pkgop_confirm", "Need a confirmation.") - } - c.prime.Output().Notice("") // Empty line - - return confirm, nil -} - func changedRequirements(oldBuildPlan *buildplan.BuildPlan, newBuildPlan *buildplan.BuildPlan) []string { var names []string var oldRequirements buildplan.Requirements diff --git a/internal/runners/clean/cache.go b/internal/runners/clean/cache.go index e4f48eb14a..e8ca73adf4 100644 --- a/internal/runners/clean/cache.go +++ b/internal/runners/clean/cache.go @@ -11,6 +11,7 @@ import ( "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/output" + "github.com/ActiveState/cli/internal/prompt" "github.com/ActiveState/cli/internal/svcctl" "github.com/ActiveState/cli/pkg/projectfile" "github.com/ActiveState/cli/pkg/runtime_helpers" @@ -20,13 +21,12 @@ type Cache struct { prime primeable output output.Outputer config configurable - confirm promptable + confirm prompt.Prompter path string ipComm svcctl.IPCommunicator } type CacheParams struct { - Force bool Project string } @@ -34,7 +34,7 @@ func NewCache(prime primeable) *Cache { return newCache(prime, prime.Output(), prime.Config(), prime.Prompt(), prime.IPComm()) } -func newCache(prime primeable, output output.Outputer, cfg configurable, confirm promptable, ipComm svcctl.IPCommunicator) *Cache { +func newCache(prime primeable, output output.Outputer, cfg configurable, confirm prompt.Prompter, ipComm svcctl.IPCommunicator) *Cache { return &Cache{ prime: prime, output: output, @@ -58,7 +58,7 @@ func (c *Cache) Run(params *CacheParams) error { } for _, projectPath := range paths { - err := c.removeProjectCache(projectPath, params.Project, params.Force) + err := c.removeProjectCache(projectPath, params.Project) if err != nil { return err } @@ -66,18 +66,20 @@ func (c *Cache) Run(params *CacheParams) error { return nil } - return c.removeCache(c.path, params.Force) + return c.removeCache(c.path) } -func (c *Cache) removeCache(path string, force bool) error { - if !force { - ok, err := c.confirm.Confirm(locale.T("confirm"), locale.T("clean_cache_confirm"), new(bool)) - if err != nil { - return err - } - if !ok { - return locale.NewInputError("err_clean_cache_not_confirmed", "Cleaning of cache aborted by user") - } +func (c *Cache) removeCache(path string) error { + defaultValue := !c.prime.Prompt().IsInteractive() + ok, kind, err := c.prime.Prompt().Confirm(locale.T("confirm"), locale.T("clean_cache_confirm"), &defaultValue, nil) + if err != nil { + return errs.Wrap(err, "Could not confirm") + } + if !ok { + return locale.NewInputError("err_clean_cache_not_confirmed", "Cleaning of cache aborted by user") + } + if kind == prompt.NonInteractive { + c.prime.Output().Notice(locale.T("prompt_continue_non_interactive")) } inUse, err := c.checkPathInUse(path) @@ -97,15 +99,17 @@ func (c *Cache) removeCache(path string, force bool) error { return nil } -func (c *Cache) removeProjectCache(projectDir, namespace string, force bool) error { - if !force { - ok, err := c.confirm.Confirm(locale.T("confirm"), locale.Tr("clean_cache_artifact_confirm", namespace), new(bool)) - if err != nil { - return err - } - if !ok { - return locale.NewInputError("err_clean_cache_artifact_not_confirmed", "Cleaning of cached runtime aborted by user") - } +func (c *Cache) removeProjectCache(projectDir, namespace string) error { + defaultValue := !c.prime.Prompt().IsInteractive() + ok, kind, err := c.prime.Prompt().Confirm(locale.T("confirm"), locale.Tr("clean_cache_artifact_confirm", namespace), &defaultValue, nil) + if err != nil { + return errs.Wrap(err, "Could not confirm") + } + if !ok { + return locale.NewInputError("err_clean_cache_artifact_not_confirmed", "Cleaning of cached runtime aborted by user") + } + if kind == prompt.NonInteractive { + c.prime.Output().Notice(locale.T("prompt_continue_non_interactive")) } inUse, err := c.checkPathInUse(projectDir) diff --git a/internal/runners/clean/config.go b/internal/runners/clean/config.go index 333b2b0375..6524b86371 100644 --- a/internal/runners/clean/config.go +++ b/internal/runners/clean/config.go @@ -8,6 +8,7 @@ import ( "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/output" + "github.com/ActiveState/cli/internal/prompt" "github.com/ActiveState/cli/internal/rtutils/ptr" "github.com/ActiveState/cli/internal/svcctl" "github.com/ActiveState/cli/pkg/project" @@ -25,7 +26,7 @@ type configurable interface { type Config struct { output output.Outputer - confirm promptable + confirm prompt.Prompter cfg configurable ipComm svcctl.IPCommunicator } @@ -38,7 +39,7 @@ func NewConfig(prime primeable) *Config { return newConfig(prime.Output(), prime.Prompt(), prime.Config(), prime.IPComm()) } -func newConfig(out output.Outputer, confirm promptable, cfg configurable, ipComm svcctl.IPCommunicator) *Config { +func newConfig(out output.Outputer, confirm prompt.Prompter, cfg configurable, ipComm svcctl.IPCommunicator) *Config { return &Config{ output: out, confirm: confirm, @@ -52,14 +53,19 @@ func (c *Config) Run(params *ConfigParams) error { return locale.NewError("err_clean_cache_activated") } - if !params.Force { - ok, err := c.confirm.Confirm(locale.T("confirm"), locale.T("clean_config_confirm"), ptr.To(true)) - if err != nil { - return locale.WrapError(err, "err_clean_config_confirm", "Could not confirm clean config choice") - } - if !ok { - return locale.NewInputError("err_clean_config_aborted", "Cleaning of config aborted by user") - } + defaultChoice := !c.confirm.IsInteractive() + ok, kind, err := c.confirm.Confirm(locale.T("confirm"), locale.T("clean_config_confirm"), &defaultChoice, ptr.To(true)) + if err != nil { + return errs.Wrap(err, "Unable to confirm") + } + if !ok { + return locale.NewInputError("err_clean_config_aborted", "Cleaning of config aborted by user") + } + switch kind { + case prompt.NonInteractive: + c.output.Notice(locale.T("prompt_continue_non_interactive")) + case prompt.Force: + c.output.Notice(locale.T("prompt_continue_force")) } if err := stopServices(c.cfg, c.output, c.ipComm, params.Force); err != nil { diff --git a/internal/runners/clean/uninstall.go b/internal/runners/clean/uninstall.go index 8b6071e1a7..1b888697c1 100644 --- a/internal/runners/clean/uninstall.go +++ b/internal/runners/clean/uninstall.go @@ -13,27 +13,23 @@ import ( "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" + "github.com/ActiveState/cli/internal/prompt" + "github.com/ActiveState/cli/internal/rtutils/ptr" "github.com/ActiveState/cli/internal/svcctl" ) -type promptable interface { - Confirm(title, message string, defaultChoice *bool) (bool, error) - Select(title, message string, choices []string, defaultChoice *string) (string, error) -} - type Uninstall struct { out output.Outputer - prompt promptable + prompt prompt.Prompter cfg *config.Instance ipComm svcctl.IPCommunicator an analytics.Dispatcher } type UninstallParams struct { - Force bool - NonInteractive bool - All bool - Prompt bool + Force bool + All bool + Prompt bool } type primeable interface { @@ -49,7 +45,7 @@ func NewUninstall(prime primeable) (*Uninstall, error) { return newUninstall(prime.Output(), prime.Prompt(), prime.Config(), prime.IPComm(), prime.Analytics()) } -func newUninstall(out output.Outputer, prompt promptable, cfg *config.Instance, ipComm svcctl.IPCommunicator, an analytics.Dispatcher) (*Uninstall, error) { +func newUninstall(out output.Outputer, prompt prompt.Prompter, cfg *config.Instance, ipComm svcctl.IPCommunicator, an analytics.Dispatcher) (*Uninstall, error) { return &Uninstall{ out: out, prompt: prompt, @@ -81,19 +77,25 @@ func (u *Uninstall) Run(params *UninstallParams) error { if selection == choices[1] { params.All = true } - } else if !params.Force { - defaultChoice := params.NonInteractive + } else { + defaultChoice := !u.prompt.IsInteractive() confirmMessage := locale.T("uninstall_confirm") if params.All { confirmMessage = locale.T("uninstall_confirm_all") } - ok, err := u.prompt.Confirm(locale.T("confirm"), confirmMessage, &defaultChoice) + ok, kind, err := u.prompt.Confirm(locale.T("confirm"), confirmMessage, &defaultChoice, ptr.To(true)) if err != nil { - return locale.WrapError(err, "err_uninstall_confirm", "Could not confirm uninstall choice") + return errs.Wrap(err, "Unable to confirm") } if !ok { return locale.NewInputError("err_uninstall_aborted", "Uninstall aborted by user") } + switch kind { + case prompt.NonInteractive: + u.out.Notice(locale.T("prompt_continue_non_interactive")) + case prompt.Force: + u.out.Notice(locale.T("prompt_continue_force")) + } } if err := stopServices(u.cfg, u.out, u.ipComm, params.Force); err != nil { diff --git a/internal/runners/projects/delete.go b/internal/runners/projects/delete.go index 2cae69944a..20912814eb 100644 --- a/internal/runners/projects/delete.go +++ b/internal/runners/projects/delete.go @@ -1,6 +1,7 @@ package projects import ( + "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/prompt" @@ -36,14 +37,17 @@ func (d *Delete) Run(params *DeleteParams) error { return locale.NewInputError("err_projects_delete_authenticated", "You need to be authenticated to delete a project.") } - defaultChoice := !d.out.Config().Interactive - confirm, err := d.prompt.Confirm("", locale.Tl("project_delete_confim", "Are you sure you want to delete the project {{.V0}}?", params.Project.String()), &defaultChoice) + defaultChoice := !d.prompt.IsInteractive() + confirm, kind, err := d.prompt.Confirm("", locale.Tl("project_delete_confim", "Are you sure you want to delete the project {{.V0}}?", params.Project.String()), &defaultChoice, nil) if err != nil { - return locale.WrapError(err, "err_project_delete_confirm", "Could not confirm delete choice") + return errs.Wrap(err, "Unable to confirm") } if !confirm { return locale.NewInputError("err_project_delete_aborted", "Delete aborted by user") } + if kind == prompt.NonInteractive { + d.out.Notice(locale.T("prompt_continue_non_interactive")) + } err = model.DeleteProject(params.Project.Owner, params.Project.Project, d.auth) if err != nil { diff --git a/internal/runners/projects/edit.go b/internal/runners/projects/edit.go index 1bf55860de..8919008058 100644 --- a/internal/runners/projects/edit.go +++ b/internal/runners/projects/edit.go @@ -90,15 +90,16 @@ func (e *Edit) Run(params *EditParams) error { editMsg += locale.Tl("edit_prompt_confirm", "Continue?") - defaultChoice := !e.out.Config().Interactive - edit, err := e.prompt.Confirm("", editMsg, &defaultChoice) + defaultChoice := !e.prompt.IsInteractive() + edit, kind, err := e.prompt.Confirm("", editMsg, &defaultChoice, nil) if err != nil { - return locale.WrapError(err, "err_edit_prompt", "Could not prompt for edit confirmation") + return errs.Wrap(err, "Unable to confirm") } - if !edit { - e.out.Print(locale.Tl("edit_cancelled", "Project edit cancelled")) - return nil + return locale.NewInputError("edit_cancelled", "Project edit cancelled") + } + if kind == prompt.NonInteractive { + e.out.Notice(locale.T("prompt_continue_non_interactive")) } if err = model.EditProject(params.Namespace.Owner, params.Namespace.Project, editable, e.auth); err != nil { diff --git a/internal/runners/projects/move.go b/internal/runners/projects/move.go index c3d4b40c31..fa9e7ed2a1 100644 --- a/internal/runners/projects/move.go +++ b/internal/runners/projects/move.go @@ -41,15 +41,16 @@ func (m *Move) Run(params *MoveParams) error { return locale.NewInputError("err_project_move_auth", "In order to move your project you need to be authenticated. Please run '[ACTIONABLE]state auth[/RESET]' to authenticate.") } - defaultChoice := !m.out.Config().Interactive - move, err := m.prompt.Confirm("", locale.Tr("move_prompt", params.Namespace.String(), params.NewOwner, params.Namespace.Project), &defaultChoice) + defaultChoice := !m.prompt.IsInteractive() + move, kind, err := m.prompt.Confirm("", locale.Tr("move_prompt", params.Namespace.String(), params.NewOwner, params.Namespace.Project), &defaultChoice, nil) if err != nil { - return locale.WrapError(err, "err_move_prompt", "Could not prompt for move confirmation") + return errs.Wrap(err, "Unable to confirm") } - if !move { - m.out.Print(locale.Tl("move_cancelled", "Project move aborted by user")) - return nil + return locale.NewInputError("move_cancelled", "Project move aborted by user") + } + if kind == prompt.NonInteractive { + m.out.Notice(locale.T("prompt_continue_non_interactive")) } if err = model.MoveProject(params.Namespace.Owner, params.Namespace.Project, params.NewOwner, m.auth); err != nil { diff --git a/internal/runners/publish/publish.go b/internal/runners/publish/publish.go index 6964d1c98b..2b8af90997 100644 --- a/internal/runners/publish/publish.go +++ b/internal/runners/publish/publish.go @@ -218,22 +218,22 @@ func (r *Runner) Run(params *Params) error { return errs.Wrap(err, "Could not marshal publish variables") } - cont, err := r.prompt.Confirm( + cont, kind, err := r.prompt.Confirm( "", locale.Tl("uploadingredient_confirm", `Prepared the following ingredient: {{.V0}} Do you want to publish this ingredient? -`, string(b)), - ptr.To(true), - ) +`, string(b)), ptr.To(true), nil) if err != nil { return errs.Wrap(err, "Confirmation failed") } if !cont { - r.out.Print(locale.Tl("uploadingredient_cancel", "Publish cancelled")) - return nil + return locale.NewInputError("uploadingredient_cancel", "Publish cancelled") + } + if kind == prompt.NonInteractive { + r.out.Notice(locale.T("prompt_continue_non_interactive")) } r.out.Notice(locale.Tl("uploadingredient_uploading", "Publishing ingredient...")) diff --git a/internal/runners/push/push.go b/internal/runners/push/push.go index ca1c774161..10f935dfe8 100644 --- a/internal/runners/push/push.go +++ b/internal/runners/push/push.go @@ -128,10 +128,15 @@ func (r *Push) Run(params PushParams) (rerr error) { // Ask to create a copy if the user does not have org permissions if intend&pushFromNoPermission > 0 && !params.Namespace.IsValid() { - var err error - createCopy, err := r.prompt.Confirm("", locale.T("push_prompt_not_authorized"), ptr.To(true)) - if err != nil || !createCopy { - return err + createCopy, kind, err := r.prompt.Confirm("", locale.T("push_prompt_not_authorized"), ptr.To(true), nil) + if err != nil { + return errs.Wrap(err, "Unable to confirm") + } + if !createCopy { + return nil + } + if kind == prompt.NonInteractive { + r.out.Notice(locale.T("prompt_continue_non_interactive")) } } @@ -167,16 +172,19 @@ func (r *Push) Run(params PushParams) (rerr error) { // If the user didn't necessarily intend to create the project we should ask them for confirmation if intend&intendCreateProject == 0 { - createProject, err := r.prompt.Confirm( + createProject, kind, err := r.prompt.Confirm( locale.Tl("create_project", "Create Project"), locale.Tl("push_confirm_create_project", "You are about to create the project [NOTICE]{{.V0}}[/RESET]. Continue?", targetNamespace.String()), - ptr.To(true)) + ptr.To(true), nil) if err != nil { return errs.Wrap(err, "Confirmation failed") } if !createProject { return rationalize.ErrActionAborted } + if kind == prompt.NonInteractive { + r.out.Notice(locale.T("prompt_continue_non_interactive")) + } } r.out.Notice(locale.Tl("push_creating_project", "Creating project [NOTICE]{{.V1}}[/RESET] under [NOTICE]{{.V0}}[/RESET] on the ActiveState Platform", targetNamespace.Owner, targetNamespace.Project)) diff --git a/internal/runners/reset/reset.go b/internal/runners/reset/reset.go index 45e042e725..944842bf67 100644 --- a/internal/runners/reset/reset.go +++ b/internal/runners/reset/reset.go @@ -26,7 +26,6 @@ import ( const local = "LOCAL" type Params struct { - Force bool CommitID string } @@ -117,14 +116,17 @@ func (r *Reset) Run(params *Params) error { } r.out.Notice(locale.Tl("reset_commit", "Your project will be reset to [ACTIONABLE]{{.V0}}[/RESET]\n", commitID.String())) if commitID != localCommitID { - defaultChoice := params.Force || !r.out.Config().Interactive - confirm, err := r.prompt.Confirm("", locale.Tl("reset_confim", "Resetting is destructive. You will lose any changes that were not pushed. Are you sure you want to do this?"), &defaultChoice) + defaultChoice := !r.prime.Prompt().IsInteractive() + confirm, kind, err := r.prime.Prompt().Confirm("", locale.Tl("reset_confim", "Resetting is destructive. You will lose any changes that were not pushed. Are you sure you want to do this?"), &defaultChoice, nil) if err != nil { - return locale.WrapError(err, "err_reset_confirm", "Could not confirm reset choice") + return errs.Wrap(err, "Unable to confirm") } if !confirm { return locale.NewInputError("err_reset_aborted", "Reset aborted by user") } + if kind == prompt.NonInteractive { + r.prime.Output().Notice(locale.T("prompt_continue_non_interactive")) + } } err = localcommit.Set(r.project.Dir(), commitID.String()) diff --git a/internal/runners/revert/revert.go b/internal/runners/revert/revert.go index 60da9a2441..335bab3af4 100644 --- a/internal/runners/revert/revert.go +++ b/internal/runners/revert/revert.go @@ -40,7 +40,6 @@ type Revert struct { type Params struct { CommitID string To bool - Force bool } type primeable interface { @@ -139,14 +138,17 @@ func (r *Revert) Run(params *Params) (rerr error) { } } - defaultChoice := params.Force || !r.out.Config().Interactive - revert, err := r.prompt.Confirm("", locale.Tl("revert_confirm", "Continue?"), &defaultChoice) + defaultChoice := !r.prime.Prompt().IsInteractive() + revert, kind, err := r.prime.Prompt().Confirm("", locale.Tl("revert_confirm", "Continue?"), &defaultChoice, nil) if err != nil { - return locale.WrapError(err, "err_revert_confirm", "Could not confirm revert choice") + return errs.Wrap(err, "Unable to confirm") } if !revert { return locale.NewInputError("err_revert_aborted", "Revert aborted by user") } + if kind == prompt.NonInteractive { + r.prime.Output().Notice(locale.T("prompt_continue_non_interactive")) + } revertCommit, err := revertFunc(revertParams, bp) if err != nil { diff --git a/internal/runners/scripts/edit.go b/internal/runners/scripts/edit.go index fa34bc2ce6..3f3e04be19 100644 --- a/internal/runners/scripts/edit.go +++ b/internal/runners/scripts/edit.go @@ -15,6 +15,7 @@ import ( "github.com/ActiveState/cli/internal/osutils" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/prompt" + "github.com/ActiveState/cli/internal/rtutils/ptr" "github.com/ActiveState/cli/internal/runbits/rationalize" "github.com/ActiveState/cli/internal/scriptfile" "github.com/ActiveState/cli/pkg/project" @@ -236,8 +237,7 @@ func startInteractive(sw *scriptWatcher, scriptName string, output output.Output go sw.run(scriptName, output, cfg, proj) for { - doneConfirmDefault := true - doneEditing, err := prompt.Confirm("", locale.T("prompt_done_editing"), &doneConfirmDefault) + doneEditing, _, err := prompt.Confirm("", locale.T("prompt_done_editing"), ptr.To(true), nil) if err != nil { return errs.Wrap(err, "Prompter returned with failure.") } diff --git a/internal/runners/update/lock.go b/internal/runners/update/lock.go index 4be61c0b72..707d01b8a7 100644 --- a/internal/runners/update/lock.go +++ b/internal/runners/update/lock.go @@ -41,8 +41,7 @@ func (stv *StateToolChannelVersion) Type() string { } type LockParams struct { - Channel StateToolChannelVersion - NonInteractive bool + Channel StateToolChannelVersion } type Lock struct { @@ -72,8 +71,8 @@ func (l *Lock) Run(params *LockParams) error { l.out.Notice(locale.Tl("locking_version", "Locking State Tool version for current project.")) - if l.project.IsLocked() && !params.NonInteractive { - if err := confirmLock(l.prompt); err != nil { + if l.project.IsLocked() { + if err := confirmLock(l.prompt, l.out); err != nil { return locale.WrapError(err, "err_update_lock_confirm", "Could not confirm whether to lock update.") } } @@ -128,17 +127,20 @@ func (l *Lock) Run(params *LockParams) error { return nil } -func confirmLock(prom prompt.Prompter) error { +func confirmLock(prom prompt.Prompter, out output.Outputer) error { + defaultChoice := !prom.IsInteractive() msg := locale.T("confirm_update_locked_version_prompt") - confirmed, err := prom.Confirm(locale.T("confirm"), msg, new(bool)) + confirmed, kind, err := prom.Confirm(locale.T("confirm"), msg, &defaultChoice, nil) if err != nil { - return err + return errs.Wrap(err, "Unable to confirm") } - if !confirmed { return locale.NewInputError("err_update_lock_noconfirm", "Cancelling by your request.") } + if kind == prompt.NonInteractive { + out.Notice(locale.T("prompt_continue_non_interactive")) + } return nil } diff --git a/internal/runners/update/unlock.go b/internal/runners/update/unlock.go index 91021941d7..8eae02f42d 100644 --- a/internal/runners/update/unlock.go +++ b/internal/runners/update/unlock.go @@ -2,6 +2,7 @@ package update import ( "github.com/ActiveState/cli/internal/constants" + "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/multilog" "github.com/ActiveState/cli/internal/output" @@ -13,7 +14,6 @@ import ( ) type UnlockParams struct { - NonInteractive bool } type Unlock struct { @@ -44,15 +44,13 @@ func (u *Unlock) Run(params *UnlockParams) error { u.out.Notice(locale.Tl("unlocking_version", "Unlocking State Tool version for current project.")) - if !params.NonInteractive { - err := confirmUnlock(u.prompt) - if err != nil { - return locale.WrapError(err, "err_update_unlock_confirm", "Unlock cancelled by user.") - } + err := confirmUnlock(u.prompt, u.out) + if err != nil { + return locale.WrapError(err, "err_update_unlock_confirm", "Unlock cancelled by user.") } // Invalidate the installer version lock. - err := u.cfg.Set(updater.CfgKeyInstallVersion, "") + err = u.cfg.Set(updater.CfgKeyInstallVersion, "") if err != nil { multilog.Error("Failed to invalidate installer version lock on `state update lock` invocation: %v", err) } @@ -71,18 +69,20 @@ func (u *Unlock) Run(params *UnlockParams) error { return nil } -func confirmUnlock(prom prompt.Prompter) error { +func confirmUnlock(prom prompt.Prompter, out output.Outputer) error { msg := locale.T("confirm_update_unlocked_version_prompt") defaultChoice := !prom.IsInteractive() - confirmed, err := prom.Confirm(locale.T("confirm"), msg, &defaultChoice) + confirmed, kind, err := prom.Confirm(locale.T("confirm"), msg, &defaultChoice, nil) if err != nil { - return err + return errs.Wrap(err, "Unable to confirm") } - if !confirmed { return locale.NewInputError("err_update_lock_noconfirm", "Cancelling by your request.") } + if kind == prompt.NonInteractive { + out.Notice(locale.T("prompt_continue_non_interactive")) + } return nil } diff --git a/internal/runners/upgrade/upgrade.go b/internal/runners/upgrade/upgrade.go index eb82b499c2..22d8caeb2f 100644 --- a/internal/runners/upgrade/upgrade.go +++ b/internal/runners/upgrade/upgrade.go @@ -12,6 +12,7 @@ import ( "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" + "github.com/ActiveState/cli/internal/prompt" "github.com/ActiveState/cli/internal/rtutils/ptr" "github.com/ActiveState/cli/internal/runbits/commits_runbit" "github.com/ActiveState/cli/internal/runbits/rationalize" @@ -283,13 +284,16 @@ func (u *Upgrade) renderUserFacing(changes []structuredChange, expand bool) erro out.Print(tbl.Render()) out.Notice(" ") // Empty line (prompts use Notice) - confirm, err := u.prime.Prompt().Confirm("", locale.Tr("upgrade_confirm"), ptr.To(true)) + confirm, kind, err := u.prime.Prompt().Confirm("", locale.Tr("upgrade_confirm"), ptr.To(true), nil) if err != nil { return errs.Wrap(err, "confirmation failed") } if !confirm { return ErrAbort } + if kind == prompt.NonInteractive { + u.prime.Output().Notice(locale.T("prompt_continue_non_interactive")) + } return nil } diff --git a/internal/runners/use/reset.go b/internal/runners/use/reset.go index c8a570423e..85e5b0283e 100644 --- a/internal/runners/use/reset.go +++ b/internal/runners/use/reset.go @@ -4,6 +4,7 @@ import ( "runtime" "github.com/ActiveState/cli/internal/config" + "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/globaldefault" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" @@ -20,7 +21,6 @@ type Reset struct { } type ResetParams struct { - Force bool } func NewReset(prime primeable) *Reset { @@ -39,15 +39,18 @@ func (u *Reset) Run(params *ResetParams) error { return locale.NewInputError(locale.T("use_reset_notice_not_reset")) } - defaultChoice := params.Force || !u.out.Config().Interactive - ok, err := u.prompt.Confirm(locale.T("confirm"), - locale.Tl("use_reset_confirm", "You are about to stop using your project runtime. Continue?"), &defaultChoice) + defaultChoice := !u.prompt.IsInteractive() + ok, kind, err := u.prompt.Confirm(locale.T("confirm"), + locale.Tl("use_reset_confirm", "You are about to stop using your project runtime. Continue?"), &defaultChoice, nil) if err != nil { - return err + return errs.Wrap(err, "Unable to confirm") } if !ok { return locale.NewInputError("err_reset_aborted", "Reset aborted by user") } + if kind == prompt.NonInteractive { + u.out.Notice(locale.T("prompt_continue_non_interactive")) + } reset, err := globaldefault.ResetDefaultActivation(u.subshell, u.config) if err != nil { diff --git a/test/integration/projects_int_test.go b/test/integration/projects_int_test.go index d60c8f335a..7316466515 100644 --- a/test/integration/projects_int_test.go +++ b/test/integration/projects_int_test.go @@ -161,7 +161,7 @@ func (suite *ProjectsIntegrationTestSuite) TestMove() { cp.Expect("Continue? (y/N)") cp.SendLine("n") cp.Expect("aborted") - cp.ExpectExitCode(0) + cp.ExpectNotExitCode(0) } func TestProjectsIntegrationTestSuite(t *testing.T) {