From 3fdbcdec76587e084360234fef27b52e1f1d2320 Mon Sep 17 00:00:00 2001 From: Max Eshleman Date: Wed, 20 Nov 2024 13:48:13 -0800 Subject: [PATCH] Add text output to CLI (#133) use text output if output is unspecified and either `CI` environment variable is set or command is being piped --- cmd/deploycancel.go | 28 +++++++++++++----- cmd/deploycreate.go | 13 ++++---- cmd/deploylist.go | 16 +++++----- cmd/environment.go | 11 +++---- cmd/jobcancel.go | 24 ++++++++++----- cmd/jobcreate.go | 13 ++++---- cmd/joblist.go | 11 +++---- cmd/logs.go | 6 +++- cmd/project.go | 11 +++---- cmd/restart.go | 11 +++---- cmd/root.go | 14 +++++---- cmd/service.go | 11 +++---- cmd/workspaceset.go | 25 +++++++++++----- go.mod | 11 +++++-- go.sum | 31 +++++++++++++++---- pkg/command/formatter.go | 6 +++- pkg/command/wrapper.go | 40 ++++++++++++++++++------- pkg/deploy/tui.go | 22 ++++++++++++++ pkg/text/string.go | 11 +++++++ pkg/text/table.go | 64 ++++++++++++++++++++++++++++++++++++++++ 20 files changed, 275 insertions(+), 104 deletions(-) create mode 100644 pkg/text/string.go create mode 100644 pkg/text/table.go diff --git a/cmd/deploycancel.go b/cmd/deploycancel.go index 853a85e..f1121a9 100644 --- a/cmd/deploycancel.go +++ b/cmd/deploycancel.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/renderinc/cli/pkg/command" + "github.com/renderinc/cli/pkg/text" "github.com/renderinc/cli/pkg/tui/views" ) @@ -29,15 +30,14 @@ func init() { return err } - if nonInteractive, err := command.NonInteractive( + nonInteractive, err := command.NonInteractiveWithConfirm( cmd, - func() (any, error) { - return views.CancelDeploy(cmd.Context(), input) - }, - func() (string, error) { - return views.RequireConfirmationForCancelDeploy(cmd.Context(), input) - }, - ); err != nil { + cancelDeploy(cmd.Context(), input), + text.FormatString, + confirmDeploy(cmd.Context(), input), + ) + + if err != nil { return err } else if nonInteractive { return nil @@ -47,3 +47,15 @@ func init() { return nil } } + +func cancelDeploy(ctx context.Context, input views.DeployCancelInput) func() (string, error) { + return func() (string, error) { + return views.CancelDeploy(ctx, input) + } +} + +func confirmDeploy(ctx context.Context, input views.DeployCancelInput) func() (string, error) { + return func() (string, error) { + return views.RequireConfirmationForCancelDeploy(ctx, input) + } +} diff --git a/cmd/deploycreate.go b/cmd/deploycreate.go index 4c20f3c..661638b 100644 --- a/cmd/deploycreate.go +++ b/cmd/deploycreate.go @@ -10,6 +10,7 @@ import ( "github.com/renderinc/cli/pkg/client" "github.com/renderinc/cli/pkg/command" "github.com/renderinc/cli/pkg/resource" + "github.com/renderinc/cli/pkg/text" "github.com/renderinc/cli/pkg/tui/views" "github.com/renderinc/cli/pkg/types" ) @@ -45,13 +46,11 @@ func init() { return fmt.Errorf("failed to parse command: %w", err) } - if nonInteractive, err := command.NonInteractive( - cmd, - func() (any, error) { - return views.CreateDeploy(cmd.Context(), input) - }, - views.DeployCreateConfirm(cmd.Context(), input), - ); err != nil { + if nonInteractive, err := command.NonInteractiveWithConfirm(cmd, func() (*client.Deploy, error) { + return views.CreateDeploy(cmd.Context(), input) + }, func(deploy *client.Deploy) string { + return text.FormatStringF("Created deploy %s for service %s", deploy.Id, input.ServiceID) + }, views.DeployCreateConfirm(cmd.Context(), input)); err != nil { return err } else if nonInteractive { return nil diff --git a/cmd/deploylist.go b/cmd/deploylist.go index 33444a5..0daf769 100644 --- a/cmd/deploylist.go +++ b/cmd/deploylist.go @@ -4,10 +4,12 @@ import ( "context" tea "github.com/charmbracelet/bubbletea" + "github.com/spf13/cobra" + "github.com/renderinc/cli/pkg/client" "github.com/renderinc/cli/pkg/deploy" "github.com/renderinc/cli/pkg/pointers" - "github.com/spf13/cobra" + "github.com/renderinc/cli/pkg/text" "github.com/renderinc/cli/pkg/command" "github.com/renderinc/cli/pkg/resource" @@ -52,7 +54,7 @@ func commandsForDeploy(dep *client.Deploy, serviceID string) []views.PaletteComm ResourceIDs: []string{serviceID}, StartTime: startTime, EndTime: endTime, - Direction: "forward", + Direction: "forward", }, "Logs", ) @@ -85,13 +87,9 @@ func init() { input := views.DeployListInput{ServiceID: serviceID} - if nonInteractive, err := command.NonInteractive( - cmd, - func() (any, error) { - return views.LoadDeployList(cmd.Context(), input) - }, - nil, - ); err != nil { + if nonInteractive, err := command.NonInteractive(cmd, func() ([]*client.Deploy, error) { + return views.LoadDeployList(cmd.Context(), input) + }, text.DeployTable); err != nil { return err } else if nonInteractive { return nil diff --git a/cmd/environment.go b/cmd/environment.go index 71e5074..458039e 100644 --- a/cmd/environment.go +++ b/cmd/environment.go @@ -9,6 +9,7 @@ import ( "github.com/renderinc/cli/pkg/client" "github.com/renderinc/cli/pkg/command" "github.com/renderinc/cli/pkg/project" + "github.com/renderinc/cli/pkg/text" "github.com/renderinc/cli/pkg/tui" "github.com/renderinc/cli/pkg/tui/views" ) @@ -45,13 +46,9 @@ func init() { return err } - if nonInteractive, err := command.NonInteractive( - cmd, - func() (any, error) { - return views.LoadEnvironments(cmd.Context(), input) - }, - nil, - ); err != nil { + if nonInteractive, err := command.NonInteractive(cmd, func() ([]*client.Environment, error) { + return views.LoadEnvironments(cmd.Context(), input) + }, text.EnvironmentTable); err != nil { return err } else if nonInteractive { return nil diff --git a/cmd/jobcancel.go b/cmd/jobcancel.go index f7bd2da..c29145f 100644 --- a/cmd/jobcancel.go +++ b/cmd/jobcancel.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/renderinc/cli/pkg/command" + "github.com/renderinc/cli/pkg/text" "github.com/renderinc/cli/pkg/tui/views" ) @@ -28,14 +29,11 @@ func init() { return err } - if nonInteractive, err := command.NonInteractive( + if nonInteractive, err := command.NonInteractiveWithConfirm( cmd, - func() (any, error) { - return views.CancelJob(cmd.Context(), input) - }, - func() (string, error) { - return views.RequireConfirmationForCancelJob(cmd.Context(), input) - }, + cancelJob(cmd, input), + text.FormatString, + confirmJobCancel(cmd, input), ); err != nil { return err } else if nonInteractive { @@ -46,3 +44,15 @@ func init() { return nil } } + +func cancelJob(cmd *cobra.Command, input views.JobCancelInput) func() (string, error) { + return func() (string, error) { + return views.CancelJob(cmd.Context(), input) + } +} + +func confirmJobCancel(cmd *cobra.Command, input views.JobCancelInput) func() (string, error) { + return func() (string, error) { + return views.RequireConfirmationForCancelJob(cmd.Context(), input) + } +} diff --git a/cmd/jobcreate.go b/cmd/jobcreate.go index 7adf33d..816753e 100644 --- a/cmd/jobcreate.go +++ b/cmd/jobcreate.go @@ -10,6 +10,7 @@ import ( clientjob "github.com/renderinc/cli/pkg/client/jobs" "github.com/renderinc/cli/pkg/command" "github.com/renderinc/cli/pkg/resource" + "github.com/renderinc/cli/pkg/text" "github.com/renderinc/cli/pkg/tui/views" ) @@ -43,13 +44,11 @@ func init() { return fmt.Errorf("failed to parse command: %w", err) } - if nonInteractive, err := command.NonInteractive( - cmd, - func() (any, error) { - return views.CreateJob(cmd.Context(), input) - }, - nil, - ); err != nil { + if nonInteractive, err := command.NonInteractive(cmd, func() (*clientjob.Job, error) { + return views.CreateJob(cmd.Context(), input) + }, func(j *clientjob.Job) string { + return text.FormatStringF("Created job %s for %s", j.Id, input.ServiceID) + }); err != nil { return err } else if nonInteractive { return nil diff --git a/cmd/joblist.go b/cmd/joblist.go index ed7f7a5..f9084ae 100644 --- a/cmd/joblist.go +++ b/cmd/joblist.go @@ -12,6 +12,7 @@ import ( "github.com/renderinc/cli/pkg/job" "github.com/renderinc/cli/pkg/pointers" "github.com/renderinc/cli/pkg/resource" + "github.com/renderinc/cli/pkg/text" "github.com/renderinc/cli/pkg/tui/views" ) @@ -97,13 +98,9 @@ func init() { return fmt.Errorf("failed to parse command: %w", err) } - if nonInteractive, err := command.NonInteractive( - cmd, - func() (any, error) { - return views.LoadJobListData(cmd.Context(), input) - }, - nil, - ); err != nil { + if nonInteractive, err := command.NonInteractive(cmd, func() ([]*clientjob.Job, error) { + return views.LoadJobListData(cmd.Context(), input) + }, text.JobTable); err != nil { return err } else if nonInteractive { return nil diff --git a/cmd/logs.go b/cmd/logs.go index b8d1135..c40a879 100644 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -3,7 +3,9 @@ package cmd import ( "context" "encoding/json" + "fmt" "io" + "time" tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" @@ -40,6 +42,8 @@ func writeLog(format command.Output, out io.Writer, log *lclient.Log) error { str, err = json.MarshalIndent(log, "", " ") } else if format == command.YAML { str, err = yaml.Marshal(log) + } else if format == command.TEXT { + str = []byte(fmt.Sprintf("%s %s\n", log.Timestamp.Format(time.DateTime), log.Message)) } if err != nil { @@ -121,7 +125,7 @@ func init() { } format := command.GetFormatFromContext(cmd.Context()) - if format != nil && (*format == command.JSON || *format == command.YAML) { + if format != nil && (*format != command.Interactive) { return nonInteractiveLogs(format, cmd, input) } diff --git a/cmd/project.go b/cmd/project.go index 404865d..61d1ae8 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -8,6 +8,7 @@ import ( "github.com/renderinc/cli/pkg/client" "github.com/renderinc/cli/pkg/command" + "github.com/renderinc/cli/pkg/text" "github.com/renderinc/cli/pkg/tui" "github.com/renderinc/cli/pkg/tui/views" ) @@ -42,13 +43,9 @@ func init() { rootCmd.AddCommand(projectCmd) projectCmd.RunE = func(cmd *cobra.Command, args []string) error { - if nonInteractive, err := command.NonInteractive( - cmd, - func() (any, error) { - return views.LoadProjects(cmd.Context(), views.ProjectInput{}) - }, - nil, - ); err != nil { + if nonInteractive, err := command.NonInteractive(cmd, func() ([]*client.Project, error) { + return views.LoadProjects(cmd.Context(), views.ProjectInput{}) + }, text.ProjectTable); err != nil { return err } else if nonInteractive { return nil diff --git a/cmd/restart.go b/cmd/restart.go index 1d975fc..f12c473 100644 --- a/cmd/restart.go +++ b/cmd/restart.go @@ -8,6 +8,7 @@ import ( "github.com/renderinc/cli/pkg/command" "github.com/renderinc/cli/pkg/resource" + "github.com/renderinc/cli/pkg/text" "github.com/renderinc/cli/pkg/tui/views" ) @@ -32,13 +33,9 @@ func init() { return err } - if nonInteractive, err := command.NonInteractive( - cmd, - func() (any, error) { - return views.RestartResource(cmd.Context(), input) - }, - nil, - ); err != nil { + if nonInteractive, err := command.NonInteractive(cmd, func() (string, error) { + return views.RestartResource(cmd.Context(), input) + }, text.FormatString); err != nil { return err } else if nonInteractive { return nil diff --git a/cmd/root.go b/cmd/root.go index 0ae7a9f..3f5e278 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -67,6 +67,9 @@ var rootCmd = &cobra.Command{ println(err.Error()) os.Exit(1) } + if output == command.Interactive && (isPipe() || isCI()) { + output = command.TEXT + } ctx = command.SetFormatInContext(ctx, &output) if output == command.Interactive { @@ -85,10 +88,6 @@ var rootCmd = &cobra.Command{ output := command.GetFormatFromContext(ctx) if output == nil || *output == command.Interactive { - if isPipe() { - return errors.New("please specify `-o json` or `-o yaml` to pipe output") - } - stack := tui.GetStackFromContext(ctx) if stack == nil { return nil @@ -128,6 +127,11 @@ func isPipe() bool { return !isTerminal } +func isCI() bool { + ci := os.Getenv("CI") + return ci == "true" || ci == "1" +} + // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { @@ -145,7 +149,7 @@ func init() { rootCmd.Version = cfg.Version rootCmd.CompletionOptions.DisableDefaultCmd = true - rootCmd.PersistentFlags().StringP("output", "o", "interactive", "interactive, json, or yaml") + rootCmd.PersistentFlags().StringP("output", "o", "interactive", "interactive, json, yaml, or text") rootCmd.PersistentFlags().Bool(command.ConfirmFlag, false, "set to skip confirmation prompts") // Flags from the old CLI that we error with a helpful message diff --git a/cmd/service.go b/cmd/service.go index 311998c..eca8439 100644 --- a/cmd/service.go +++ b/cmd/service.go @@ -14,6 +14,7 @@ import ( "github.com/renderinc/cli/pkg/redis" "github.com/renderinc/cli/pkg/resource" "github.com/renderinc/cli/pkg/service" + "github.com/renderinc/cli/pkg/text" "github.com/renderinc/cli/pkg/tui" "github.com/renderinc/cli/pkg/tui/views" "github.com/renderinc/cli/pkg/types" @@ -205,13 +206,9 @@ func init() { return err } - if nonInteractive, err := command.NonInteractive( - cmd, - func() (any, error) { - return views.LoadResourceData(cmd.Context(), in) - }, - nil, - ); err != nil { + if nonInteractive, err := command.NonInteractive(cmd, func() ([]resource.Resource, error) { + return views.LoadResourceData(cmd.Context(), in) + }, text.ResourceTable); err != nil { return err } else if nonInteractive { return nil diff --git a/cmd/workspaceset.go b/cmd/workspaceset.go index e4e144b..b6d2a07 100644 --- a/cmd/workspaceset.go +++ b/cmd/workspaceset.go @@ -9,6 +9,7 @@ import ( "github.com/renderinc/cli/pkg/client" "github.com/renderinc/cli/pkg/command" + "github.com/renderinc/cli/pkg/text" "github.com/renderinc/cli/pkg/tui/views" ) @@ -53,13 +54,23 @@ func nonInteractiveSetWorkspace(cmd *cobra.Command, workspaceIDOrName string) er return printWorkspace(cmd, "Workspace set to", o) } +type printableOwner struct { + *client.Owner `json:"inline"` + prefix string +} + +func (p *printableOwner) String() string { + return fmt.Sprintf("%s: %s (%s)\n", p.prefix, p.Name, p.Id) +} + func printWorkspace(cmd *cobra.Command, prefix string, o *client.Owner) error { - printedData, err := command.PrintData(cmd, o) - if err != nil { - return err + po := &printableOwner{ + Owner: o, + prefix: prefix, } - if !printedData { - fmt.Printf("%s: %s (%s)\n", prefix, o.Name, o.Id) - } - return nil + + _, err := command.PrintData(cmd, po, func(p *printableOwner) string { + return text.FormatStringF("%s: %s (%s)", prefix, o.Name, o.Id) + }) + return err } diff --git a/go.mod b/go.mod index fda0667..3540899 100644 --- a/go.mod +++ b/go.mod @@ -13,10 +13,11 @@ require ( github.com/charmbracelet/x/exp/teatest v0.0.0-20240919170804-a4978c8e603a github.com/evertras/bubble-table v0.17.0 github.com/gorilla/websocket v1.5.3 + github.com/jedib0t/go-pretty v4.3.0+incompatible github.com/oapi-codegen/runtime v1.1.1 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 golang.org/x/sync v0.8.0 golang.org/x/text v0.18.0 gopkg.in/yaml.v3 v3.0.1 @@ -24,6 +25,7 @@ require ( require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-udiff v0.2.0 // indirect github.com/catppuccin/go v0.2.0 // indirect @@ -33,19 +35,24 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/google/uuid v1.5.0 // indirect + github.com/go-openapi/errors v0.22.0 // indirect + github.com/go-openapi/strfmt v0.23.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect + github.com/oklog/ulid v1.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sahilm/fuzzy v0.1.1 // indirect + go.mongodb.org/mongo-driver v1.14.0 // indirect golang.org/x/sys v0.25.0 // indirect ) diff --git a/go.sum b/go.sum index 41a595d..7c03d61 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6 github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -42,13 +44,23 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/evertras/bubble-table v0.17.0 h1:qQU4bi3IRxuZ5+Fvm3esyU/ucH9ufRXWhWL0fFuMn9c= github.com/evertras/bubble-table v0.17.0/go.mod h1:ifHujS1YxwnYSOgcR2+m3GnJ84f7CVU/4kUOxUCjEbQ= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= +github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= +github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= +github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= +github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -62,6 +74,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -70,8 +84,12 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -88,8 +106,10 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= +go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -98,7 +118,8 @@ golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/command/formatter.go b/pkg/command/formatter.go index cc25634..d100019 100644 --- a/pkg/command/formatter.go +++ b/pkg/command/formatter.go @@ -2,6 +2,7 @@ package command import ( "fmt" + "strings" "github.com/spf13/cobra" ) @@ -12,6 +13,7 @@ const ( Interactive Output = "interactive" JSON Output = "json" YAML Output = "yaml" + TEXT Output = "text" ) func (o *Output) Interactive() bool { @@ -19,11 +21,13 @@ func (o *Output) Interactive() bool { } func StringToOutput(s string) (Output, error) { - switch s { + switch strings.ToLower(s) { case "json": return JSON, nil case "yaml": return YAML, nil + case "text": + return TEXT, nil case "interactive": return Interactive, nil default: diff --git a/pkg/command/wrapper.go b/pkg/command/wrapper.go index 4513184..0d70fc3 100644 --- a/pkg/command/wrapper.go +++ b/pkg/command/wrapper.go @@ -33,10 +33,18 @@ type WrapOptions[T any] struct { RequireConfirm RequireConfirm[T] } -func NonInteractive(cmd *cobra.Command, loadData func() (any, error), confirmMessageFunc func() (string, error)) (bool, error) { +type LoadDataFunc[T any] func() (T, error) +type FormatTextFunc[T any] func(T) string +type ConfirmFunc func() (string, error) + +func NonInteractive[T any](cmd *cobra.Command, loadData LoadDataFunc[T], formatText FormatTextFunc[T]) (bool, error) { + return NonInteractiveWithConfirm(cmd, loadData, formatText, nil) +} + +func NonInteractiveWithConfirm[T any](cmd *cobra.Command, loadData LoadDataFunc[T], formatText FormatTextFunc[T], confirmMessageFunc ConfirmFunc) (bool, error) { outputFormat := GetFormatFromContext(cmd.Context()) - if outputFormat == nil || !(*outputFormat == JSON || *outputFormat == YAML) { + if outputFormat == nil || (*outputFormat == Interactive) { return false, nil } @@ -68,20 +76,20 @@ func NonInteractive(cmd *cobra.Command, loadData func() (any, error), confirmMes return false, convertToUserFacingErr(err) } - return PrintData(cmd, data) + return PrintData(cmd, data, formatText) +} + +type TextTable interface { + Header() []string + Row() []string } -func PrintData(cmd *cobra.Command, data any) (bool, error) { +func PrintData[T any](cmd *cobra.Command, data T, formatText FormatTextFunc[T]) (bool, error) { outputFormat := GetFormatFromContext(cmd.Context()) switch *outputFormat { case JSON: - jsonStr, err := json.MarshalIndent(data, "", " ") - if err != nil { - return true, err - } - _, err = cmd.OutOrStdout().Write(jsonStr) - return true, err + return true, printJSON(cmd, data) case YAML: yamlStr, err := yaml.Marshal(data) if err != nil { @@ -89,10 +97,22 @@ func PrintData(cmd *cobra.Command, data any) (bool, error) { } _, err = cmd.OutOrStdout().Write(yamlStr) return true, err + case TEXT: + _, err := cmd.OutOrStdout().Write([]byte(formatText(data))) + return true, err } return false, nil } +func printJSON(cmd *cobra.Command, data any) error { + jsonStr, err := json.MarshalIndent(data, "", " ") + if err != nil { + return err + } + _, err = cmd.OutOrStdout().Write(jsonStr) + return err +} + func wrappedModel(model tea.Model, cmd *cobra.Command, breadcrumb string, in any) (*tui.ModelWithCmd, error) { var cmdString string diff --git a/pkg/deploy/tui.go b/pkg/deploy/tui.go index ff8d514..965c87a 100644 --- a/pkg/deploy/tui.go +++ b/pkg/deploy/tui.go @@ -97,3 +97,25 @@ func triggerValue(trigger *client.DeployTrigger) string { words := strings.Split(triggerStr, "_") return strings.Join(words, " ") } + +func Header() []string { + return []string{"Status", "Commit/Image", "Trigger", "Created", "Finished", "ID"} +} + +func Row(deploy *client.Deploy) []string { + var commitOrImage string + if deploy.Image != nil { + commitOrImage = pointers.StringValue(deploy.Image.Ref) + } else if deploy.Commit != nil { + commitOrImage = pointers.StringValue(deploy.Commit.Id) + } + + return []string{ + deployStatusValue(deploy.Status), + commitOrImage, + triggerValue(deploy.Trigger), + pointers.TimeValue(deploy.CreatedAt), + pointers.TimeValue(deploy.FinishedAt), + deploy.Id, + } +} diff --git a/pkg/text/string.go b/pkg/text/string.go new file mode 100644 index 0000000..4026472 --- /dev/null +++ b/pkg/text/string.go @@ -0,0 +1,11 @@ +package text + +import "fmt" + +func FormatString(s string) string { + return FormatStringF(s) +} + +func FormatStringF(s string, a ...any) string { + return fmt.Sprintf(s+"\n", a...) +} diff --git a/pkg/text/table.go b/pkg/text/table.go new file mode 100644 index 0000000..1cfef64 --- /dev/null +++ b/pkg/text/table.go @@ -0,0 +1,64 @@ +package text + +import ( + "github.com/jedib0t/go-pretty/table" + + "github.com/renderinc/cli/pkg/client" + clientjob "github.com/renderinc/cli/pkg/client/jobs" + "github.com/renderinc/cli/pkg/deploy" + "github.com/renderinc/cli/pkg/resource" +) + +func ResourceTable(v []resource.Resource) string { + t := table.NewWriter() + t.AppendHeader(table.Row{"Name", "Project", "Environment", "Type", "ID"}) + for _, r := range v { + t.AppendRow(table.Row{r.Name(), r.ProjectName(), r.EnvironmentName(), r.Type(), r.ID()}) + } + return FormatString(t.Render()) +} + +func JobTable(v []*clientjob.Job) string { + t := table.NewWriter() + t.AppendHeader(table.Row{"Command", "Started", "Finished", "Plan", "ID"}) + for _, r := range v { + t.AppendRow(table.Row{r.StartCommand, r.StartedAt, r.FinishedAt, r.PlanId, r.Id}) + } + return FormatString(t.Render()) +} + +func DeployTable(v []*client.Deploy) string { + t := table.NewWriter() + t.AppendHeader(toRow(deploy.Header())) + for _, r := range v { + t.AppendRow(toRow(deploy.Row(r))) + } + return FormatString(t.Render()) +} + +func ProjectTable(v []*client.Project) string { + t := table.NewWriter() + t.AppendHeader(table.Row{"Name", "ID"}) + for _, r := range v { + t.AppendRow(table.Row{r.Name, r.Id}) + } + return FormatString(t.Render()) +} + +func EnvironmentTable(v []*client.Environment) string { + t := table.NewWriter() + t.AppendHeader(table.Row{"Name", "Protected", "ID"}) + for _, r := range v { + t.AppendRow(table.Row{r.Name, r.ProtectedStatus, r.Id}) + } + return FormatString(t.Render()) +} + +func toRow(r []string) table.Row { + row := table.Row{} + for _, r := range r { + row = append(row, r) + } + + return row +}