From eb3e4f15cf802156cfaadf8dc7620cf8ba3ea084 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 2 Apr 2021 13:42:27 -0700 Subject: [PATCH] rules editor create/update tweaks == Description This adds basic changes for the create/update path. For now we're keeping them separate with an assertion that we want the straightforward edit paths to bypass the survey bits -- since that becomes unnecessary (e.g. when creating a rule, there's only way to do that -- and that's by editing the rule). For update, we'll utilize the surveyext bits heavily bollowed from github to have the `?` prompt. --- internal/cli/flags.go | 53 +++++++++++++- internal/cli/rules.go | 35 ++++++--- internal/prompt/surveyext.go | 135 +++++++++++++++++++++++++++++++++++ 3 files changed, 211 insertions(+), 12 deletions(-) create mode 100644 internal/prompt/surveyext.go diff --git a/internal/cli/flags.go b/internal/cli/flags.go index 3941b93c6..3c27b42bd 100644 --- a/internal/cli/flags.go +++ b/internal/cli/flags.go @@ -3,6 +3,8 @@ package cli import ( "fmt" + "github.com/AlecAivazis/survey/v2" + "github.com/auth0/auth0-cli/internal/prompt" "github.com/spf13/cobra" ) @@ -55,6 +57,56 @@ func (f *Flag) SelectU(cmd *cobra.Command, value interface{}, options []string, return selectFlag(cmd, f, value, options, defaultValue, true) } +func (f *Flag) EditorPrompt(cmd *cobra.Command, value *string, initialValue, filename string, infoFn func()) error { + out, err := prompt.CaptureInputViaEditor( + initialValue, + filename, + infoFn, + ) + if err != nil { + return err + } + + *value = out + return nil +} + +func (f *Flag) EditorPromptU(cmd *cobra.Command, value *string, initialValue, filename string, infoFn func()) error { + response := map[string]interface{}{} + + questions := []*survey.Question{ + { + Name: f.Name, + Prompt: &prompt.Editor{ + BlankAllowed: true, + Editor: &survey.Editor{ + Help: f.Help, + Message: f.Name, + FileName: filename, + Default: initialValue, + HideDefault: true, + AppendDefault: true, + }, + }, + }, + } + + if err := survey.Ask(questions, &response); err != nil { + return err + } + + // Since we have BlankAllowed=true, an empty answer means we'll use the + // initialValue provided since this path is for the Update path. + answer, ok := response[f.Name].(string) + if ok && answer != "" { + *value = answer + } else { + *value = initialValue + } + + return nil +} + func (f *Flag) RegisterString(cmd *cobra.Command, value *string, defaultValue string) { registerString(cmd, f, value, defaultValue, false) } @@ -152,7 +204,6 @@ func shouldAsk(cmd *cobra.Command, f *Flag, isUpdate bool) bool { if !f.IsRequired && !f.AlwaysPrompt { return false } - return shouldPromptWhenFlagless(cmd, f.LongForm) } diff --git a/internal/cli/rules.go b/internal/cli/rules.go index fac10b1fd..de0366e80 100644 --- a/internal/cli/rules.go +++ b/internal/cli/rules.go @@ -6,7 +6,6 @@ import ( "github.com/auth0/auth0-cli/internal/ansi" "github.com/auth0/auth0-cli/internal/auth0" - "github.com/auth0/auth0-cli/internal/prompt" "github.com/spf13/cobra" "gopkg.in/auth0.v5/management" ) @@ -39,6 +38,14 @@ var ( Help: "Enable (or disable) a rule.", } + ruleScript = Flag{ + Name: "Script", + LongForm: "script", + ShortForm: "s", + Help: "Script contents for the rule", + IsRequired: true, + } + ruleTemplateOptions = pickerOptions{ {"Empty rule", ruleTemplateEmptyRule}, {"Add email to access token", ruleTemplateAddEmailToAccessToken}, @@ -100,6 +107,7 @@ func createRuleCmd(cli *cli) *cobra.Command { var inputs struct { Name string Template string + Script string Enabled bool } @@ -126,12 +134,12 @@ auth0 rules create -n "My Rule" -t "Empty rule" --enabled=false`, // TODO(cyx): we can re-think this once we have // `--stdin` based commands. For now we don't have // those yet, so keeping this simple. - script, err := prompt.CaptureInputViaEditor( + err := ruleScript.EditorPrompt( + cmd, + &inputs.Script, ruleTemplateOptions.getValue(inputs.Template), inputs.Name+".*.js", - func() { - cli.renderer.Infof("%s once you close the editor, the rule will be saved. To cancel, CTRL+C.", ansi.Faint("Hint:")) - }, + cli.ruleEditorHint, ) if err != nil { return fmt.Errorf("Failed to capture input from the editor: %w", err) @@ -139,7 +147,7 @@ auth0 rules create -n "My Rule" -t "Empty rule" --enabled=false`, rule := &management.Rule{ Name: &inputs.Name, - Script: auth0.String(script), + Script: auth0.String(inputs.Script), Enabled: &inputs.Enabled, } @@ -251,6 +259,7 @@ func updateRuleCmd(cli *cli) *cobra.Command { var inputs struct { ID string Name string + Script string Enabled bool } @@ -291,12 +300,12 @@ auth0 rules update -n "My Updated Rule" --enabled=false`, // TODO(cyx): we can re-think this once we have // `--stdin` based commands. For now we don't have // those yet, so keeping this simple. - script, err := prompt.CaptureInputViaEditor( + err = ruleScript.EditorPromptU( + cmd, + &inputs.Script, rule.GetScript(), rule.GetName()+".*.js", - func() { - cli.renderer.Infof("%s once you close the editor, the rule will be saved. To cancel, CTRL+C.", ansi.Faint("Hint:")) - }, + cli.ruleEditorHint, ) if err != nil { return fmt.Errorf("Failed to capture input from the editor: %w", err) @@ -312,7 +321,7 @@ auth0 rules update -n "My Updated Rule" --enabled=false`, // display. rule = &management.Rule{ Name: &inputs.Name, - Script: &script, + Script: &inputs.Script, Enabled: &inputs.Enabled, } @@ -352,3 +361,7 @@ func (c *cli) rulePickerOptions() (pickerOptions, error) { return opts, nil } + +func (c *cli) ruleEditorHint() { + c.renderer.Infof("%s once you close the editor, the rule will be saved. To cancel, CTRL+C.", ansi.Faint("Hint:")) +} diff --git a/internal/prompt/surveyext.go b/internal/prompt/surveyext.go new file mode 100644 index 000000000..5e52635e8 --- /dev/null +++ b/internal/prompt/surveyext.go @@ -0,0 +1,135 @@ +package prompt + +import ( + "path/filepath" + + "github.com/AlecAivazis/survey/v2" + "github.com/AlecAivazis/survey/v2/terminal" +) + +// EXTENDED to enable different prompting behavior +type Editor struct { + *survey.Editor + EditorCommand string + BlankAllowed bool +} + +func (e *Editor) editorCommand() string { + if e.EditorCommand == "" { + return getDefaultEditor() + } + + return e.EditorCommand +} + +// EXTENDED to change prompt text +var EditorQuestionTemplate = ` +{{- 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 }} {{color "reset"}} +{{- if .ShowAnswer}} + {{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}} +{{- else }} + {{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}} + {{- if and .Default (not .HideDefault)}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}} + {{- color "cyan"}}[(e) to launch {{ .EditorCommand }}{{- if .BlankAllowed }}, enter to skip{{ end }}] {{color "reset"}} +{{- end}}` + +// EXTENDED to pass editor name (to use in prompt) +type EditorTemplateData struct { + survey.Editor + EditorCommand string + BlankAllowed bool + Answer string + ShowAnswer bool + ShowHelp bool + Config *survey.PromptConfig +} + +// EXTENDED to augment prompt text and keypress handling +func (e *Editor) prompt(initialValue string, config *survey.PromptConfig) (interface{}, error) { + err := e.Render( + EditorQuestionTemplate, + // EXTENDED to support printing editor in prompt and BlankAllowed + EditorTemplateData{ + Editor: *e.Editor, + BlankAllowed: e.BlankAllowed, + EditorCommand: filepath.Base(e.editorCommand()), + Config: config, + }, + ) + if err != nil { + return "", err + } + + // start reading runes from the standard in + rr := e.NewRuneReader() + _ = rr.SetTermMode() + defer func() { _ = rr.RestoreTermMode() }() + + cursor := e.NewCursor() + cursor.Hide() + defer cursor.Show() + + for { + // EXTENDED to handle the e to edit / enter to skip behavior + BlankAllowed + r, _, err := rr.ReadRune() + if err != nil { + return "", err + } + if r == 'e' { + break + } + if r == '\r' || r == '\n' { + if e.BlankAllowed { + return "", nil + } else { + continue + } + } + if r == terminal.KeyInterrupt { + return "", terminal.InterruptErr + } + if r == terminal.KeyEndTransmission { + break + } + if string(r) == config.HelpInput && e.Help != "" { + err = e.Render( + EditorQuestionTemplate, + EditorTemplateData{ + // EXTENDED to support printing editor in prompt, BlankAllowed + Editor: *e.Editor, + BlankAllowed: e.BlankAllowed, + EditorCommand: filepath.Base(e.editorCommand()), + ShowHelp: true, + Config: config, + }, + ) + if err != nil { + return "", err + } + } + continue + } + + text, err := CaptureInputViaEditor(initialValue, e.FileName, nil) + if err != nil { + return "", err + } + + // check length, return default value on empty + if len(text) == 0 && !e.AppendDefault { + return e.Default, nil + } + + return text, nil +} + +// EXTENDED This is straight copypasta from survey to get our overridden prompt called. +func (e *Editor) Prompt(config *survey.PromptConfig) (interface{}, error) { + initialValue := "" + if e.Default != "" && e.AppendDefault { + initialValue = e.Default + } + return e.prompt(initialValue, config) +}