diff --git a/cmd/ghcs/output/format_json.go b/cmd/ghcs/output/format_json.go index 37208629c..8488e8dfa 100644 --- a/cmd/ghcs/output/format_json.go +++ b/cmd/ghcs/output/format_json.go @@ -3,6 +3,8 @@ package output import ( "encoding/json" "io" + "strings" + "unicode" ) type jsonwriter struct { @@ -19,7 +21,7 @@ func (j *jsonwriter) SetHeader(cols []string) { func (j *jsonwriter) Append(values []string) { row := make(map[string]string) for i, v := range values { - row[j.cols[i]] = v + row[camelize(j.cols[i])] = v } j.data = append(j.data, row) } @@ -31,3 +33,23 @@ func (j *jsonwriter) Render() { } _ = enc.Encode(j.data) } + +func camelize(s string) string { + var b strings.Builder + capitalizeNext := false + for i, r := range s { + if r == ' ' { + capitalizeNext = true + continue + } + if capitalizeNext { + b.WriteRune(unicode.ToUpper(r)) + capitalizeNext = false + } else if i == 0 { + b.WriteRune(unicode.ToLower(r)) + } else { + b.WriteRune(r) + } + } + return b.String() +} diff --git a/cmd/ghcs/output/format_table.go b/cmd/ghcs/output/format_table.go index 97e7cab58..e0345672d 100644 --- a/cmd/ghcs/output/format_table.go +++ b/cmd/ghcs/output/format_table.go @@ -15,9 +15,7 @@ type Table interface { } func NewTable(w io.Writer, asJSON bool) Table { - f, ok := w.(*os.File) - isTTY := ok && term.IsTerminal(int(f.Fd())) - + isTTY := isTTY(w) if asJSON { return &jsonwriter{w: w, pretty: isTTY} } @@ -26,3 +24,8 @@ func NewTable(w io.Writer, asJSON bool) Table { } return &tabwriter{w: w} } + +func isTTY(w io.Writer) bool { + f, ok := w.(*os.File) + return ok && term.IsTerminal(int(f.Fd())) +} diff --git a/cmd/ghcs/output/logger.go b/cmd/ghcs/output/logger.go new file mode 100644 index 000000000..32d05acc8 --- /dev/null +++ b/cmd/ghcs/output/logger.go @@ -0,0 +1,45 @@ +package output + +import ( + "fmt" + "io" +) + +func NewLogger(stdout, stderr io.Writer, disabled bool) *Logger { + return &Logger{ + out: stdout, + errout: stderr, + enabled: !disabled && isTTY(stdout), + } +} + +type Logger struct { + out io.Writer + errout io.Writer + enabled bool +} + +func (l *Logger) Print(v ...interface{}) (int, error) { + if !l.enabled { + return 0, nil + } + return fmt.Fprint(l.out, v...) +} + +func (l *Logger) Println(v ...interface{}) (int, error) { + if !l.enabled { + return 0, nil + } + return fmt.Fprintln(l.out, v...) +} + +func (l *Logger) Printf(f string, v ...interface{}) (int, error) { + if !l.enabled { + return 0, nil + } + return fmt.Fprintf(l.out, f, v...) +} + +func (l *Logger) Errorf(f string, v ...interface{}) (int, error) { + return fmt.Fprintf(l.errout, f, v...) +} diff --git a/cmd/ghcs/ports.go b/cmd/ghcs/ports.go index 77d1b00f7..fbbffcf1d 100644 --- a/cmd/ghcs/ports.go +++ b/cmd/ghcs/ports.go @@ -10,25 +10,36 @@ import ( "strings" "github.com/github/ghcs/api" + "github.com/github/ghcs/cmd/ghcs/output" "github.com/github/ghcs/internal/codespaces" "github.com/github/go-liveshare" "github.com/muhammadmuzzammil1998/jsonc" - "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" ) +type PortsOptions struct { + CodespaceName string + AsJSON bool +} + func NewPortsCmd() *cobra.Command { + opts := &PortsOptions{} + portsCmd := &cobra.Command{ Use: "ports", Short: "Forward ports from a GitHub Codespace.", RunE: func(cmd *cobra.Command, args []string) error { - return Ports() + return Ports(opts) }, } + portsCmd.Flags().StringVarP(&opts.CodespaceName, "name", "n", "", "Name of Codespace to use") + portsCmd.Flags().BoolVar(&opts.AsJSON, "json", false, "Output as JSON") + portsCmd.AddCommand(NewPortsPublicCmd()) portsCmd.AddCommand(NewPortsPrivateCmd()) portsCmd.AddCommand(NewPortsForwardCmd()) + return portsCmd } @@ -36,16 +47,17 @@ func init() { rootCmd.AddCommand(NewPortsCmd()) } -func Ports() error { +func Ports(opts *PortsOptions) error { apiClient := api.New(os.Getenv("GITHUB_TOKEN")) ctx := context.Background() + log := output.NewLogger(os.Stdout, os.Stderr, opts.AsJSON) user, err := apiClient.GetUser(ctx) if err != nil { return fmt.Errorf("error getting user: %v", err) } - codespace, err := codespaces.ChooseCodespace(ctx, apiClient, user) + codespace, token, err := codespaces.GetOrChooseCodespace(ctx, apiClient, user, opts.CodespaceName) if err != nil { if err == codespaces.ErrNoCodespaces { fmt.Println(err.Error()) @@ -56,33 +68,23 @@ func Ports() error { devContainerCh := getDevContainer(ctx, apiClient, codespace) - token, err := apiClient.GetCodespaceToken(ctx, user.Login, codespace.Name) - if err != nil { - return fmt.Errorf("error getting codespace token: %v", err) - } - liveShareClient, err := codespaces.ConnectToLiveshare(ctx, apiClient, token, codespace) if err != nil { return fmt.Errorf("error connecting to liveshare: %v", err) } - fmt.Println("Loading ports...") + log.Println("Loading ports...") ports, err := getPorts(ctx, liveShareClient) if err != nil { return fmt.Errorf("error getting ports: %v", err) } - if len(ports) == 0 { - fmt.Println("This codespace has no open ports") - return nil - } - devContainerResult := <-devContainerCh if devContainerResult.Err != nil { - fmt.Printf("Failed to get port names: %v\n", devContainerResult.Err.Error()) + _, _ = log.Errorf("Failed to get port names: %v\n", devContainerResult.Err.Error()) } - table := tablewriter.NewWriter(os.Stdout) + table := output.NewTable(os.Stdout, opts.AsJSON) table.SetHeader([]string{"Label", "Source Port", "Destination Port", "Public", "Browse URL"}) for _, port := range ports { sourcePort := strconv.Itoa(port.SourcePort) @@ -104,7 +106,6 @@ func Ports() error { table.Render() return nil - } func getPorts(ctx context.Context, lsclient *liveshare.Client) (liveshare.Ports, error) {