feat: convert entire CLI to clibase (#6491)

I'm sorry.
This commit is contained in:
Ammar Bandukwala
2023-03-23 17:42:20 -05:00
committed by GitHub
parent b71b8daa21
commit 2bd6d2908e
345 changed files with 9965 additions and 9082 deletions
+52 -45
View File
@@ -5,11 +5,11 @@ import (
"testing"
"time"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/atomic"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/pty/ptytest"
@@ -24,9 +24,9 @@ func TestAgent(t *testing.T) {
var disconnected atomic.Bool
ptty := ptytest.New(t)
cmd := &cobra.Command{
RunE: func(cmd *cobra.Command, _ []string) error {
err := cliui.Agent(cmd.Context(), cmd.OutOrStdout(), cliui.AgentOptions{
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
err := cliui.Agent(inv.Context(), inv.Stdout, cliui.AgentOptions{
WorkspaceName: "example",
Fetch: func(_ context.Context) (codersdk.WorkspaceAgent, error) {
agent := codersdk.WorkspaceAgent{
@@ -44,12 +44,13 @@ func TestAgent(t *testing.T) {
return err
},
}
cmd.SetOutput(ptty.Output())
cmd.SetIn(ptty.Input())
inv := cmd.Invoke()
ptty.Attach(inv)
done := make(chan struct{})
go func() {
defer close(done)
err := cmd.Execute()
err := inv.Run()
assert.NoError(t, err)
}()
ptty.ExpectMatchContext(ctx, "lost connection")
@@ -66,9 +67,9 @@ func TestAgent_TimeoutWithTroubleshootingURL(t *testing.T) {
wantURL := "https://coder.com/troubleshoot"
var connected, timeout atomic.Bool
cmd := &cobra.Command{
RunE: func(cmd *cobra.Command, _ []string) error {
err := cliui.Agent(cmd.Context(), cmd.OutOrStdout(), cliui.AgentOptions{
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
err := cliui.Agent(inv.Context(), inv.Stdout, cliui.AgentOptions{
WorkspaceName: "example",
Fetch: func(_ context.Context) (codersdk.WorkspaceAgent, error) {
agent := codersdk.WorkspaceAgent{
@@ -91,11 +92,12 @@ func TestAgent_TimeoutWithTroubleshootingURL(t *testing.T) {
},
}
ptty := ptytest.New(t)
cmd.SetOutput(ptty.Output())
cmd.SetIn(ptty.Input())
inv := cmd.Invoke()
ptty.Attach(inv)
done := make(chan error, 1)
go func() {
done <- cmd.ExecuteContext(ctx)
done <- inv.WithContext(ctx).Run()
}()
ptty.ExpectMatchContext(ctx, "Don't panic, your workspace is booting")
timeout.Store(true)
@@ -115,9 +117,10 @@ func TestAgent_StartupTimeout(t *testing.T) {
var status, state atomic.String
setStatus := func(s codersdk.WorkspaceAgentStatus) { status.Store(string(s)) }
setState := func(s codersdk.WorkspaceAgentLifecycle) { state.Store(string(s)) }
cmd := &cobra.Command{
RunE: func(cmd *cobra.Command, _ []string) error {
err := cliui.Agent(cmd.Context(), cmd.OutOrStdout(), cliui.AgentOptions{
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
err := cliui.Agent(inv.Context(), inv.Stdout, cliui.AgentOptions{
WorkspaceName: "example",
Fetch: func(_ context.Context) (codersdk.WorkspaceAgent, error) {
agent := codersdk.WorkspaceAgent{
@@ -144,11 +147,12 @@ func TestAgent_StartupTimeout(t *testing.T) {
}
ptty := ptytest.New(t)
cmd.SetOutput(ptty.Output())
cmd.SetIn(ptty.Input())
inv := cmd.Invoke()
ptty.Attach(inv)
done := make(chan error, 1)
go func() {
done <- cmd.ExecuteContext(ctx)
done <- inv.WithContext(ctx).Run()
}()
setStatus(codersdk.WorkspaceAgentConnecting)
ptty.ExpectMatchContext(ctx, "Don't panic, your workspace is booting")
@@ -173,9 +177,9 @@ func TestAgent_StartErrorExit(t *testing.T) {
var status, state atomic.String
setStatus := func(s codersdk.WorkspaceAgentStatus) { status.Store(string(s)) }
setState := func(s codersdk.WorkspaceAgentLifecycle) { state.Store(string(s)) }
cmd := &cobra.Command{
RunE: func(cmd *cobra.Command, _ []string) error {
err := cliui.Agent(cmd.Context(), cmd.OutOrStdout(), cliui.AgentOptions{
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
err := cliui.Agent(inv.Context(), inv.Stdout, cliui.AgentOptions{
WorkspaceName: "example",
Fetch: func(_ context.Context) (codersdk.WorkspaceAgent, error) {
agent := codersdk.WorkspaceAgent{
@@ -202,11 +206,12 @@ func TestAgent_StartErrorExit(t *testing.T) {
}
ptty := ptytest.New(t)
cmd.SetOutput(ptty.Output())
cmd.SetIn(ptty.Input())
inv := cmd.Invoke()
ptty.Attach(inv)
done := make(chan error, 1)
go func() {
done <- cmd.ExecuteContext(ctx)
done <- inv.WithContext(ctx).Run()
}()
setStatus(codersdk.WorkspaceAgentConnected)
setState(codersdk.WorkspaceAgentLifecycleStarting)
@@ -228,9 +233,9 @@ func TestAgent_NoWait(t *testing.T) {
var status, state atomic.String
setStatus := func(s codersdk.WorkspaceAgentStatus) { status.Store(string(s)) }
setState := func(s codersdk.WorkspaceAgentLifecycle) { state.Store(string(s)) }
cmd := &cobra.Command{
RunE: func(cmd *cobra.Command, _ []string) error {
err := cliui.Agent(cmd.Context(), cmd.OutOrStdout(), cliui.AgentOptions{
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
err := cliui.Agent(inv.Context(), inv.Stdout, cliui.AgentOptions{
WorkspaceName: "example",
Fetch: func(_ context.Context) (codersdk.WorkspaceAgent, error) {
agent := codersdk.WorkspaceAgent{
@@ -257,11 +262,12 @@ func TestAgent_NoWait(t *testing.T) {
}
ptty := ptytest.New(t)
cmd.SetOutput(ptty.Output())
cmd.SetIn(ptty.Input())
inv := cmd.Invoke()
ptty.Attach(inv)
done := make(chan error, 1)
go func() {
done <- cmd.ExecuteContext(ctx)
done <- inv.WithContext(ctx).Run()
}()
setStatus(codersdk.WorkspaceAgentConnecting)
ptty.ExpectMatchContext(ctx, "Don't panic, your workspace is booting")
@@ -270,19 +276,19 @@ func TestAgent_NoWait(t *testing.T) {
require.NoError(t, <-done, "created - should exit early")
setState(codersdk.WorkspaceAgentLifecycleStarting)
go func() { done <- cmd.ExecuteContext(ctx) }()
go func() { done <- inv.WithContext(ctx).Run() }()
require.NoError(t, <-done, "starting - should exit early")
setState(codersdk.WorkspaceAgentLifecycleStartTimeout)
go func() { done <- cmd.ExecuteContext(ctx) }()
go func() { done <- inv.WithContext(ctx).Run() }()
require.NoError(t, <-done, "start timeout - should exit early")
setState(codersdk.WorkspaceAgentLifecycleStartError)
go func() { done <- cmd.ExecuteContext(ctx) }()
go func() { done <- inv.WithContext(ctx).Run() }()
require.NoError(t, <-done, "start error - should exit early")
setState(codersdk.WorkspaceAgentLifecycleReady)
go func() { done <- cmd.ExecuteContext(ctx) }()
go func() { done <- inv.WithContext(ctx).Run() }()
require.NoError(t, <-done, "ready - should exit early")
}
@@ -297,9 +303,9 @@ func TestAgent_LoginBeforeReadyEnabled(t *testing.T) {
var status, state atomic.String
setStatus := func(s codersdk.WorkspaceAgentStatus) { status.Store(string(s)) }
setState := func(s codersdk.WorkspaceAgentLifecycle) { state.Store(string(s)) }
cmd := &cobra.Command{
RunE: func(cmd *cobra.Command, _ []string) error {
err := cliui.Agent(cmd.Context(), cmd.OutOrStdout(), cliui.AgentOptions{
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
err := cliui.Agent(inv.Context(), inv.Stdout, cliui.AgentOptions{
WorkspaceName: "example",
Fetch: func(_ context.Context) (codersdk.WorkspaceAgent, error) {
agent := codersdk.WorkspaceAgent{
@@ -325,12 +331,13 @@ func TestAgent_LoginBeforeReadyEnabled(t *testing.T) {
},
}
inv := cmd.Invoke()
ptty := ptytest.New(t)
cmd.SetOutput(ptty.Output())
cmd.SetIn(ptty.Input())
ptty.Attach(inv)
done := make(chan error, 1)
go func() {
done <- cmd.ExecuteContext(ctx)
done <- inv.WithContext(ctx).Run()
}()
setStatus(codersdk.WorkspaceAgentConnecting)
ptty.ExpectMatchContext(ctx, "Don't panic, your workspace is booting")
@@ -339,18 +346,18 @@ func TestAgent_LoginBeforeReadyEnabled(t *testing.T) {
require.NoError(t, <-done, "created - should exit early")
setState(codersdk.WorkspaceAgentLifecycleStarting)
go func() { done <- cmd.ExecuteContext(ctx) }()
go func() { done <- inv.WithContext(ctx).Run() }()
require.NoError(t, <-done, "starting - should exit early")
setState(codersdk.WorkspaceAgentLifecycleStartTimeout)
go func() { done <- cmd.ExecuteContext(ctx) }()
go func() { done <- inv.WithContext(ctx).Run() }()
require.NoError(t, <-done, "start timeout - should exit early")
setState(codersdk.WorkspaceAgentLifecycleStartError)
go func() { done <- cmd.ExecuteContext(ctx) }()
go func() { done <- inv.WithContext(ctx).Run() }()
require.NoError(t, <-done, "start error - should exit early")
setState(codersdk.WorkspaceAgentLifecycleReady)
go func() { done <- cmd.ExecuteContext(ctx) }()
go func() { done <- inv.WithContext(ctx).Run() }()
require.NoError(t, <-done, "ready - should exit early")
}
+4 -2
View File
@@ -53,6 +53,8 @@ var Styles = struct {
FocusedPrompt: defaultStyles.FocusedPrompt.Foreground(lipgloss.Color("#651fff")),
Fuchsia: defaultStyles.SelectedMenuItem.Copy(),
Logo: defaultStyles.Logo.SetString("Coder"),
Warn: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"}),
Wrap: lipgloss.NewStyle().Width(80),
Warn: lipgloss.NewStyle().Foreground(
lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"},
),
Wrap: lipgloss.NewStyle().Width(80),
}
+9 -7
View File
@@ -7,9 +7,9 @@ import (
"testing"
"time"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/pty/ptytest"
@@ -23,10 +23,10 @@ func TestGitAuth(t *testing.T) {
defer cancel()
ptty := ptytest.New(t)
cmd := &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
var fetched atomic.Bool
return cliui.GitAuth(cmd.Context(), cmd.OutOrStdout(), cliui.GitAuthOptions{
return cliui.GitAuth(inv.Context(), inv.Stdout, cliui.GitAuthOptions{
Fetch: func(ctx context.Context) ([]codersdk.TemplateVersionGitAuth, error) {
defer fetched.Store(true)
return []codersdk.TemplateVersionGitAuth{{
@@ -40,12 +40,14 @@ func TestGitAuth(t *testing.T) {
})
},
}
cmd.SetOutput(ptty.Output())
cmd.SetIn(ptty.Input())
inv := cmd.Invoke().WithContext(ctx)
ptty.Attach(inv)
done := make(chan struct{})
go func() {
defer close(done)
err := cmd.Execute()
err := inv.Run()
assert.NoError(t, err)
}()
ptty.ExpectMatchContext(ctx, "You must authenticate with")
+42 -4
View File
@@ -10,17 +10,22 @@ import (
// cliMessage provides a human-readable message for CLI errors and messages.
type cliMessage struct {
Level string
Style lipgloss.Style
Header string
Prefix string
Lines []string
}
// String formats the CLI message for consumption by a human.
func (m cliMessage) String() string {
var str strings.Builder
_, _ = fmt.Fprintf(&str, "%s\r\n",
Styles.Bold.Render(m.Header))
if m.Prefix != "" {
_, _ = str.WriteString(m.Style.Bold(true).Render(m.Prefix))
}
_, _ = str.WriteString(m.Style.Bold(false).Render(m.Header))
_, _ = str.WriteString("\r\n")
for _, line := range m.Lines {
_, _ = fmt.Fprintf(&str, " %s %s\r\n", m.Style.Render("|"), line)
}
@@ -30,9 +35,42 @@ func (m cliMessage) String() string {
// Warn writes a log to the writer provided.
func Warn(wtr io.Writer, header string, lines ...string) {
_, _ = fmt.Fprint(wtr, cliMessage{
Level: "warning",
Style: Styles.Warn,
Prefix: "WARN: ",
Header: header,
Lines: lines,
}.String())
}
// Warn writes a formatted log to the writer provided.
func Warnf(wtr io.Writer, fmtStr string, args ...interface{}) {
Warn(wtr, fmt.Sprintf(fmtStr, args...))
}
// Info writes a log to the writer provided.
func Info(wtr io.Writer, header string, lines ...string) {
_, _ = fmt.Fprint(wtr, cliMessage{
Header: header,
Lines: lines,
}.String())
}
// Infof writes a formatted log to the writer provided.
func Infof(wtr io.Writer, fmtStr string, args ...interface{}) {
Info(wtr, fmt.Sprintf(fmtStr, args...))
}
// Error writes a log to the writer provided.
func Error(wtr io.Writer, header string, lines ...string) {
_, _ = fmt.Fprint(wtr, cliMessage{
Style: Styles.Error,
Prefix: "ERROR: ",
Header: header,
Lines: lines,
}.String())
}
// Errorf writes a formatted log to the writer provided.
func Errorf(wtr io.Writer, fmtStr string, args ...interface{}) {
Error(wtr, fmt.Sprintf(fmtStr, args...))
}
+28 -11
View File
@@ -6,13 +6,14 @@ import (
"reflect"
"strings"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/clibase"
)
type OutputFormat interface {
ID() string
AttachFlags(cmd *cobra.Command)
AttachOptions(opts *clibase.OptionSet)
Format(ctx context.Context, data any) (string, error)
}
@@ -45,11 +46,11 @@ func NewOutputFormatter(formats ...OutputFormat) *OutputFormatter {
}
}
// AttachFlags attaches the --output flag to the given command, and any
// AttachOptions attaches the --output flag to the given command, and any
// additional flags required by the output formatters.
func (f *OutputFormatter) AttachFlags(cmd *cobra.Command) {
func (f *OutputFormatter) AttachOptions(opts *clibase.OptionSet) {
for _, format := range f.formats {
format.AttachFlags(cmd)
format.AttachOptions(opts)
}
formatNames := make([]string, 0, len(f.formats))
@@ -57,7 +58,15 @@ func (f *OutputFormatter) AttachFlags(cmd *cobra.Command) {
formatNames = append(formatNames, format.ID())
}
cmd.Flags().StringVarP(&f.formatID, "output", "o", f.formats[0].ID(), "Output format. Available formats: "+strings.Join(formatNames, ", "))
*opts = append(*opts,
clibase.Option{
Flag: "output",
FlagShorthand: "o",
Default: f.formats[0].ID(),
Value: clibase.StringOf(&f.formatID),
Description: "Output format. Available formats: " + strings.Join(formatNames, ", ") + ".",
},
)
}
// Format formats the given data using the format specified by the --output
@@ -118,9 +127,17 @@ func (*tableFormat) ID() string {
return "table"
}
// AttachFlags implements OutputFormat.
func (f *tableFormat) AttachFlags(cmd *cobra.Command) {
cmd.Flags().StringSliceVarP(&f.columns, "column", "c", f.defaultColumns, "Columns to display in table output. Available columns: "+strings.Join(f.allColumns, ", "))
// AttachOptions implements OutputFormat.
func (f *tableFormat) AttachOptions(opts *clibase.OptionSet) {
*opts = append(*opts,
clibase.Option{
Flag: "column",
FlagShorthand: "c",
Default: strings.Join(f.defaultColumns, ","),
Value: clibase.StringArrayOf(&f.columns),
Description: "Columns to display in table output. Available columns: " + strings.Join(f.allColumns, ", ") + ".",
},
)
}
// Format implements OutputFormat.
@@ -142,8 +159,8 @@ func (jsonFormat) ID() string {
return "json"
}
// AttachFlags implements OutputFormat.
func (jsonFormat) AttachFlags(_ *cobra.Command) {}
// AttachOptions implements OutputFormat.
func (jsonFormat) AttachOptions(_ *clibase.OptionSet) {}
// Format implements OutputFormat.
func (jsonFormat) Format(_ context.Context, data any) (string, error) {
+23 -15
View File
@@ -6,16 +6,16 @@ import (
"sync/atomic"
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/cli/cliui"
)
type format struct {
id string
attachFlagsFn func(cmd *cobra.Command)
formatFn func(ctx context.Context, data any) (string, error)
id string
attachOptionsFn func(opts *clibase.OptionSet)
formatFn func(ctx context.Context, data any) (string, error)
}
var _ cliui.OutputFormat = &format{}
@@ -24,9 +24,9 @@ func (f *format) ID() string {
return f.id
}
func (f *format) AttachFlags(cmd *cobra.Command) {
if f.attachFlagsFn != nil {
f.attachFlagsFn(cmd)
func (f *format) AttachOptions(opts *clibase.OptionSet) {
if f.attachOptionsFn != nil {
f.attachOptionsFn(opts)
}
}
@@ -82,8 +82,14 @@ func Test_OutputFormatter(t *testing.T) {
cliui.JSONFormat(),
&format{
id: "foo",
attachFlagsFn: func(cmd *cobra.Command) {
cmd.Flags().StringP("foo", "f", "", "foo flag 1234")
attachOptionsFn: func(opts *clibase.OptionSet) {
opts.Add(clibase.Option{
Name: "foo",
Flag: "foo",
FlagShorthand: "f",
Value: clibase.DiscardValue,
Description: "foo flag 1234",
})
},
formatFn: func(_ context.Context, _ any) (string, error) {
atomic.AddInt64(&called, 1)
@@ -92,13 +98,15 @@ func Test_OutputFormatter(t *testing.T) {
},
)
cmd := &cobra.Command{}
f.AttachFlags(cmd)
cmd := &clibase.Cmd{}
f.AttachOptions(&cmd.Options)
selected, err := cmd.Flags().GetString("output")
fs := cmd.Options.FlagSet()
selected, err := fs.GetString("output")
require.NoError(t, err)
require.Equal(t, "json", selected)
usage := cmd.Flags().FlagUsages()
usage := fs.FlagUsages()
require.Contains(t, usage, "Available formats: json, foo")
require.Contains(t, usage, "foo flag 1234")
@@ -112,13 +120,13 @@ func Test_OutputFormatter(t *testing.T) {
require.Equal(t, data, got)
require.EqualValues(t, 0, atomic.LoadInt64(&called))
require.NoError(t, cmd.Flags().Set("output", "foo"))
require.NoError(t, fs.Set("output", "foo"))
out, err = f.Format(ctx, data)
require.NoError(t, err)
require.Equal(t, "foo", out)
require.EqualValues(t, 1, atomic.LoadInt64(&called))
require.NoError(t, cmd.Flags().Set("output", "bar"))
require.NoError(t, fs.Set("output", "bar"))
out, err = f.Format(ctx, data)
require.Error(t, err)
require.ErrorContains(t, err, "bar")
+21 -22
View File
@@ -5,16 +5,15 @@ import (
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/coderd/parameter"
"github.com/coder/coder/codersdk"
)
func ParameterSchema(cmd *cobra.Command, parameterSchema codersdk.ParameterSchema) (string, error) {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), Styles.Bold.Render("var."+parameterSchema.Name))
func ParameterSchema(inv *clibase.Invocation, parameterSchema codersdk.ParameterSchema) (string, error) {
_, _ = fmt.Fprintln(inv.Stdout, Styles.Bold.Render("var."+parameterSchema.Name))
if parameterSchema.Description != "" {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+strings.TrimSpace(strings.Join(strings.Split(parameterSchema.Description, "\n"), "\n "))+"\n")
_, _ = fmt.Fprintln(inv.Stdout, " "+strings.TrimSpace(strings.Join(strings.Split(parameterSchema.Description, "\n"), "\n "))+"\n")
}
var err error
@@ -28,15 +27,15 @@ func ParameterSchema(cmd *cobra.Command, parameterSchema codersdk.ParameterSchem
var value string
if len(options) > 0 {
// Move the cursor up a single line for nicer display!
_, _ = fmt.Fprint(cmd.OutOrStdout(), "\033[1A")
value, err = Select(cmd, SelectOptions{
_, _ = fmt.Fprint(inv.Stdout, "\033[1A")
value, err = Select(inv, SelectOptions{
Options: options,
Default: parameterSchema.DefaultSourceValue,
HideSearch: true,
})
if err == nil {
_, _ = fmt.Fprintln(cmd.OutOrStdout())
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+Styles.Prompt.String()+Styles.Field.Render(value))
_, _ = fmt.Fprintln(inv.Stdout)
_, _ = fmt.Fprintln(inv.Stdout, " "+Styles.Prompt.String()+Styles.Field.Render(value))
}
} else {
text := "Enter a value"
@@ -45,7 +44,7 @@ func ParameterSchema(cmd *cobra.Command, parameterSchema codersdk.ParameterSchem
}
text += ":"
value, err = Prompt(cmd, PromptOptions{
value, err = Prompt(inv, PromptOptions{
Text: Styles.Bold.Render(text),
})
value = strings.TrimSpace(value)
@@ -62,17 +61,17 @@ func ParameterSchema(cmd *cobra.Command, parameterSchema codersdk.ParameterSchem
return value, nil
}
func RichParameter(cmd *cobra.Command, templateVersionParameter codersdk.TemplateVersionParameter) (string, error) {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), Styles.Bold.Render(templateVersionParameter.Name))
func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.TemplateVersionParameter) (string, error) {
_, _ = fmt.Fprintln(inv.Stdout, Styles.Bold.Render(templateVersionParameter.Name))
if templateVersionParameter.DescriptionPlaintext != "" {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+strings.TrimSpace(strings.Join(strings.Split(templateVersionParameter.DescriptionPlaintext, "\n"), "\n "))+"\n")
_, _ = fmt.Fprintln(inv.Stdout, " "+strings.TrimSpace(strings.Join(strings.Split(templateVersionParameter.DescriptionPlaintext, "\n"), "\n "))+"\n")
}
var err error
var value string
if templateVersionParameter.Type == "list(string)" {
// Move the cursor up a single line for nicer display!
_, _ = fmt.Fprint(cmd.OutOrStdout(), "\033[1A")
_, _ = fmt.Fprint(inv.Stdout, "\033[1A")
var options []string
err = json.Unmarshal([]byte(templateVersionParameter.DefaultValue), &options)
@@ -80,29 +79,29 @@ func RichParameter(cmd *cobra.Command, templateVersionParameter codersdk.Templat
return "", err
}
values, err := MultiSelect(cmd, options)
values, err := MultiSelect(inv, options)
if err == nil {
v, err := json.Marshal(&values)
if err != nil {
return "", err
}
_, _ = fmt.Fprintln(cmd.OutOrStdout())
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+Styles.Prompt.String()+Styles.Field.Render(strings.Join(values, ", ")))
_, _ = fmt.Fprintln(inv.Stdout)
_, _ = fmt.Fprintln(inv.Stdout, " "+Styles.Prompt.String()+Styles.Field.Render(strings.Join(values, ", ")))
value = string(v)
}
} else if len(templateVersionParameter.Options) > 0 {
// Move the cursor up a single line for nicer display!
_, _ = fmt.Fprint(cmd.OutOrStdout(), "\033[1A")
_, _ = fmt.Fprint(inv.Stdout, "\033[1A")
var richParameterOption *codersdk.TemplateVersionParameterOption
richParameterOption, err = RichSelect(cmd, RichSelectOptions{
richParameterOption, err = RichSelect(inv, RichSelectOptions{
Options: templateVersionParameter.Options,
Default: templateVersionParameter.DefaultValue,
HideSearch: true,
})
if err == nil {
_, _ = fmt.Fprintln(cmd.OutOrStdout())
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+Styles.Prompt.String()+Styles.Field.Render(richParameterOption.Name))
_, _ = fmt.Fprintln(inv.Stdout)
_, _ = fmt.Fprintln(inv.Stdout, " "+Styles.Prompt.String()+Styles.Field.Render(richParameterOption.Name))
value = richParameterOption.Value
}
} else {
@@ -112,7 +111,7 @@ func RichParameter(cmd *cobra.Command, templateVersionParameter codersdk.Templat
}
text += ":"
value, err = Prompt(cmd, PromptOptions{
value, err = Prompt(inv, PromptOptions{
Text: Styles.Bold.Render(text),
Validate: func(value string) error {
return validateRichPrompt(value, templateVersionParameter)
+34 -17
View File
@@ -11,8 +11,9 @@ import (
"github.com/bgentry/speakeasy"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/clibase"
)
// PromptOptions supply a set of options to the prompt.
@@ -26,8 +27,16 @@ type PromptOptions struct {
const skipPromptFlag = "yes"
func AllowSkipPrompt(cmd *cobra.Command) {
cmd.Flags().BoolP(skipPromptFlag, "y", false, "Bypass prompts")
// SkipPromptOption adds a "--yes/-y" flag to the cmd that can be used to skip
// prompts.
func SkipPromptOption() clibase.Option {
return clibase.Option{
Flag: skipPromptFlag,
FlagShorthand: "y",
Description: "Bypass prompts.",
// Discard
Value: clibase.BoolOf(new(bool)),
}
}
const (
@@ -36,17 +45,17 @@ const (
)
// Prompt asks the user for input.
func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
func Prompt(inv *clibase.Invocation, opts PromptOptions) (string, error) {
// If the cmd has a "yes" flag for skipping confirm prompts, honor it.
// If it's not a "Confirm" prompt, then don't skip. As the default value of
// "yes" makes no sense.
if opts.IsConfirm && cmd.Flags().Lookup(skipPromptFlag) != nil {
if skip, _ := cmd.Flags().GetBool(skipPromptFlag); skip {
if opts.IsConfirm && inv.ParsedFlags().Lookup(skipPromptFlag) != nil {
if skip, _ := inv.ParsedFlags().GetBool(skipPromptFlag); skip {
return ConfirmYes, nil
}
}
_, _ = fmt.Fprint(cmd.OutOrStdout(), Styles.FocusedPrompt.String()+opts.Text+" ")
_, _ = fmt.Fprint(inv.Stdout, Styles.FocusedPrompt.String()+opts.Text+" ")
if opts.IsConfirm {
if len(opts.Default) == 0 {
opts.Default = ConfirmYes
@@ -58,19 +67,24 @@ func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
} else {
renderedNo = Styles.Bold.Render(ConfirmNo)
}
_, _ = fmt.Fprint(cmd.OutOrStdout(), Styles.Placeholder.Render("("+renderedYes+Styles.Placeholder.Render("/"+renderedNo+Styles.Placeholder.Render(") "))))
_, _ = fmt.Fprint(inv.Stdout, Styles.Placeholder.Render("("+renderedYes+Styles.Placeholder.Render("/"+renderedNo+Styles.Placeholder.Render(") "))))
} else if opts.Default != "" {
_, _ = fmt.Fprint(cmd.OutOrStdout(), Styles.Placeholder.Render("("+opts.Default+") "))
_, _ = fmt.Fprint(inv.Stdout, Styles.Placeholder.Render("("+opts.Default+") "))
}
interrupt := make(chan os.Signal, 1)
if inv.Stdin == nil {
panic("inv.Stdin is nil")
}
errCh := make(chan error, 1)
lineCh := make(chan string)
go func() {
var line string
var err error
inFile, isInputFile := cmd.InOrStdin().(*os.File)
inFile, isInputFile := inv.Stdin.(*os.File)
if opts.Secret && isInputFile && isatty.IsTerminal(inFile.Fd()) {
// we don't install a signal handler here because speakeasy has its own
line, err = speakeasy.Ask("")
@@ -78,7 +92,7 @@ func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
signal.Notify(interrupt, os.Interrupt)
defer signal.Stop(interrupt)
reader := bufio.NewReader(cmd.InOrStdin())
reader := bufio.NewReader(inv.Stdin)
line, err = reader.ReadString('\n')
// Check if the first line beings with JSON object or array chars.
@@ -96,7 +110,10 @@ func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
if line == "" {
line = opts.Default
}
lineCh <- line
select {
case <-inv.Context().Done():
case lineCh <- line:
}
}()
select {
@@ -109,16 +126,16 @@ func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
if opts.Validate != nil {
err := opts.Validate(line)
if err != nil {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), defaultStyles.Error.Render(err.Error()))
return Prompt(cmd, opts)
_, _ = fmt.Fprintln(inv.Stdout, defaultStyles.Error.Render(err.Error()))
return Prompt(inv, opts)
}
}
return line, nil
case <-cmd.Context().Done():
return "", cmd.Context().Err()
case <-inv.Context().Done():
return "", inv.Context().Err()
case <-interrupt:
// Print a newline so that any further output starts properly on a new line.
_, _ = fmt.Fprintln(cmd.OutOrStdout())
_, _ = fmt.Fprintln(inv.Stdout)
return "", Canceled
}
}
+24 -18
View File
@@ -8,10 +8,10 @@ import (
"os/exec"
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/pty"
"github.com/coder/coder/pty/ptytest"
@@ -77,9 +77,9 @@ func TestPrompt(t *testing.T) {
resp, err := newPrompt(ptty, cliui.PromptOptions{
Text: "ShouldNotSeeThis",
IsConfirm: true,
}, func(cmd *cobra.Command) {
cliui.AllowSkipPrompt(cmd)
cmd.SetArgs([]string{"-y"})
}, func(inv *clibase.Invocation) {
inv.Command.Options = append(inv.Command.Options, cliui.SkipPromptOption())
inv.Args = []string{"-y"}
})
assert.NoError(t, err)
doneChan <- resp
@@ -145,23 +145,25 @@ func TestPrompt(t *testing.T) {
})
}
func newPrompt(ptty *ptytest.PTY, opts cliui.PromptOptions, cmdOpt func(cmd *cobra.Command)) (string, error) {
func newPrompt(ptty *ptytest.PTY, opts cliui.PromptOptions, invOpt func(inv *clibase.Invocation)) (string, error) {
value := ""
cmd := &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
var err error
value, err = cliui.Prompt(cmd, opts)
value, err = cliui.Prompt(inv, opts)
return err
},
}
inv := cmd.Invoke()
// Optionally modify the cmd
if cmdOpt != nil {
cmdOpt(cmd)
if invOpt != nil {
invOpt(inv)
}
cmd.SetOut(ptty.Output())
cmd.SetErr(ptty.Output())
cmd.SetIn(ptty.Input())
return value, cmd.ExecuteContext(context.Background())
inv.Stdout = ptty.Output()
inv.Stderr = ptty.Output()
inv.Stdin = ptty.Input()
return value, inv.WithContext(context.Background()).Run()
}
func TestPasswordTerminalState(t *testing.T) {
@@ -208,13 +210,17 @@ func TestPasswordTerminalState(t *testing.T) {
// nolint:unused
func passwordHelper() {
cmd := &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
cliui.Prompt(cmd, cliui.PromptOptions{
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
cliui.Prompt(inv, cliui.PromptOptions{
Text: "Password:",
Secret: true,
})
return nil
},
}
cmd.ExecuteContext(context.Background())
err := cmd.Invoke().WithOS().Run()
if err != nil {
panic(err)
}
}
+8 -7
View File
@@ -9,9 +9,9 @@ import (
"testing"
"time"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
@@ -125,9 +125,9 @@ func newProvisionerJob(t *testing.T) provisionerJobTest {
}
jobLock := sync.Mutex{}
logs := make(chan codersdk.ProvisionerJobLog, 1)
cmd := &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
return cliui.ProvisionerJob(cmd.Context(), cmd.OutOrStdout(), cliui.ProvisionerJobOptions{
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
return cliui.ProvisionerJob(inv.Context(), inv.Stdout, cliui.ProvisionerJobOptions{
FetchInterval: time.Millisecond,
Fetch: func() (codersdk.ProvisionerJob, error) {
jobLock.Lock()
@@ -145,13 +145,14 @@ func newProvisionerJob(t *testing.T) provisionerJobTest {
})
},
}
inv := cmd.Invoke()
ptty := ptytest.New(t)
cmd.SetOutput(ptty.Output())
cmd.SetIn(ptty.Input())
ptty.Attach(inv)
done := make(chan struct{})
go func() {
defer close(done)
err := cmd.ExecuteContext(context.Background())
err := inv.WithContext(context.Background()).Run()
if err != nil {
assert.ErrorIs(t, err, cliui.Canceled)
}
+11 -11
View File
@@ -8,9 +8,9 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/codersdk"
)
@@ -68,7 +68,7 @@ type RichSelectOptions struct {
}
// RichSelect displays a list of user options including name and description.
func RichSelect(cmd *cobra.Command, richOptions RichSelectOptions) (*codersdk.TemplateVersionParameterOption, error) {
func RichSelect(inv *clibase.Invocation, richOptions RichSelectOptions) (*codersdk.TemplateVersionParameterOption, error) {
opts := make([]string, len(richOptions.Options))
for i, option := range richOptions.Options {
line := option.Name
@@ -78,7 +78,7 @@ func RichSelect(cmd *cobra.Command, richOptions RichSelectOptions) (*codersdk.Te
opts[i] = line
}
selected, err := Select(cmd, SelectOptions{
selected, err := Select(inv, SelectOptions{
Options: opts,
Default: richOptions.Default,
Size: richOptions.Size,
@@ -97,7 +97,7 @@ func RichSelect(cmd *cobra.Command, richOptions RichSelectOptions) (*codersdk.Te
}
// Select displays a list of user options.
func Select(cmd *cobra.Command, opts SelectOptions) (string, error) {
func Select(inv *clibase.Invocation, opts SelectOptions) (string, error) {
// The survey library used *always* fails when testing on Windows,
// as it requires a live TTY (can't be a conpty). We should fork
// this library to add a dummy fallback, that simply reads/writes
@@ -123,17 +123,17 @@ func Select(cmd *cobra.Command, opts SelectOptions) (string, error) {
is.Help.Text = ""
}
}), survey.WithStdio(fileReadWriter{
Reader: cmd.InOrStdin(),
Reader: inv.Stdin,
}, fileReadWriter{
Writer: cmd.OutOrStdout(),
}, cmd.OutOrStdout()))
Writer: inv.Stdout,
}, inv.Stdout))
if errors.Is(err, terminal.InterruptErr) {
return value, Canceled
}
return value, err
}
func MultiSelect(cmd *cobra.Command, items []string) ([]string, error) {
func MultiSelect(inv *clibase.Invocation, items []string) ([]string, error) {
// Similar hack is applied to Select()
if flag.Lookup("test.v") != nil {
return items, nil
@@ -146,10 +146,10 @@ func MultiSelect(cmd *cobra.Command, items []string) ([]string, error) {
var values []string
err := survey.AskOne(prompt, &values, survey.WithStdio(fileReadWriter{
Reader: cmd.InOrStdin(),
Reader: inv.Stdin,
}, fileReadWriter{
Writer: cmd.OutOrStdout(),
}, cmd.OutOrStdout()))
Writer: inv.Stdout,
}, inv.Stdout))
if errors.Is(err, terminal.InterruptErr) {
return nil, Canceled
}
+21 -22
View File
@@ -1,13 +1,12 @@
package cliui_test
import (
"context"
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/pty/ptytest"
@@ -32,16 +31,16 @@ func TestSelect(t *testing.T) {
func newSelect(ptty *ptytest.PTY, opts cliui.SelectOptions) (string, error) {
value := ""
cmd := &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
var err error
value, err = cliui.Select(cmd, opts)
value, err = cliui.Select(inv, opts)
return err
},
}
cmd.SetOutput(ptty.Output())
cmd.SetIn(ptty.Input())
return value, cmd.ExecuteContext(context.Background())
inv := cmd.Invoke()
ptty.Attach(inv)
return value, inv.Run()
}
func TestRichSelect(t *testing.T) {
@@ -56,11 +55,11 @@ func TestRichSelect(t *testing.T) {
{
Name: "A-Name",
Value: "A-Value",
Description: "A-Description",
Description: "A-Description.",
}, {
Name: "B-Name",
Value: "B-Value",
Description: "B-Description",
Description: "B-Description.",
},
},
})
@@ -73,18 +72,18 @@ func TestRichSelect(t *testing.T) {
func newRichSelect(ptty *ptytest.PTY, opts cliui.RichSelectOptions) (string, error) {
value := ""
cmd := &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
richOption, err := cliui.RichSelect(cmd, opts)
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
richOption, err := cliui.RichSelect(inv, opts)
if err == nil {
value = richOption.Value
}
return err
},
}
cmd.SetOutput(ptty.Output())
cmd.SetIn(ptty.Input())
return value, cmd.ExecuteContext(context.Background())
inv := cmd.Invoke()
ptty.Attach(inv)
return value, inv.Run()
}
func TestMultiSelect(t *testing.T) {
@@ -106,16 +105,16 @@ func TestMultiSelect(t *testing.T) {
func newMultiSelect(ptty *ptytest.PTY, items []string) ([]string, error) {
var values []string
cmd := &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
selectedItems, err := cliui.MultiSelect(cmd, items)
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
selectedItems, err := cliui.MultiSelect(inv, items)
if err == nil {
values = selectedItems
}
return err
},
}
cmd.SetOutput(ptty.Output())
cmd.SetIn(ptty.Input())
return values, cmd.ExecuteContext(context.Background())
inv := cmd.Invoke()
ptty.Attach(inv)
return values, inv.Run()
}