[object Object]

← back to Cli Printing Press

feat(generator): add color and TTY detection to generated CLIs

3cb6e869a1a2b80c0bb0295e891e8ba43f8fe1e0 · 2026-03-23 23:49:48 -0700 · Matt Van Horn

Add go-isatty dependency, colorEnabled() with NO_COLOR/TERM=dumb support,
bold/green/red/yellow ANSI helpers, bold table headers, colored doctor
status output, and --no-color persistent flag.

Files touched

Diff

commit 3cb6e869a1a2b80c0bb0295e891e8ba43f8fe1e0
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date:   Mon Mar 23 23:49:48 2026 -0700

    feat(generator): add color and TTY detection to generated CLIs
    
    Add go-isatty dependency, colorEnabled() with NO_COLOR/TERM=dumb support,
    bold/green/red/yellow ANSI helpers, bold table headers, colored doctor
    status output, and --no-color persistent flag.
---
 internal/generator/templates/doctor.go.tmpl  | 27 ++++++++++++++--
 internal/generator/templates/go.mod.tmpl     |  1 +
 internal/generator/templates/helpers.go.tmpl | 48 +++++++++++++++++++++++++++-
 internal/generator/templates/root.go.tmpl    |  1 +
 4 files changed, 73 insertions(+), 4 deletions(-)

diff --git a/internal/generator/templates/doctor.go.tmpl b/internal/generator/templates/doctor.go.tmpl
index 8f1742b4..a41e7922 100644
--- a/internal/generator/templates/doctor.go.tmpl
+++ b/internal/generator/templates/doctor.go.tmpl
@@ -3,6 +3,7 @@ package cli
 import (
 	"fmt"
 	"net/http"
+	"strings"
 	"time"
 
 	"github.com/USER/{{.Name}}-cli/internal/config"
@@ -57,11 +58,31 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
 				return flags.printJSON(cmd, report)
 			}
 
-			// Human-readable output
+			// Human-readable output with color
 			w := cmd.OutOrStdout()
-			for _, key := range []string{"config", "config_path", "base_url", "auth", "auth_source", "api", "version"} {
+			checkKeys := []struct{ key, label string }{
+				{"config", "Config"},
+				{"auth", "Auth"},
+				{"api", "API"},
+			}
+			for _, ck := range checkKeys {
+				v, ok := report[ck.key]
+				if !ok {
+					continue
+				}
+				s := fmt.Sprintf("%v", v)
+				indicator := green("OK")
+				if strings.Contains(s, "error") || strings.Contains(s, "not configured") || strings.Contains(s, "unreachable") {
+					indicator = red("FAIL")
+				} else if strings.Contains(s, "not ") {
+					indicator = yellow("WARN")
+				}
+				fmt.Fprintf(w, "  %s %s: %s\n", indicator, ck.label, s)
+			}
+			// Print info keys without status indicator
+			for _, key := range []string{"config_path", "base_url", "auth_source", "version"} {
 				if v, ok := report[key]; ok {
-					fmt.Fprintf(w, "%s=%v\n", key, v)
+					fmt.Fprintf(w, "  %s: %v\n", key, v)
 				}
 			}
 			return nil
diff --git a/internal/generator/templates/go.mod.tmpl b/internal/generator/templates/go.mod.tmpl
index 16f02592..b78cba70 100644
--- a/internal/generator/templates/go.mod.tmpl
+++ b/internal/generator/templates/go.mod.tmpl
@@ -4,6 +4,7 @@ go 1.23
 
 require (
 	github.com/spf13/cobra v1.9.1
+	github.com/mattn/go-isatty v0.0.20
 {{- if eq .Config.Format "toml"}}
 	github.com/pelletier/go-toml/v2 v2.2.4
 {{- end}}
diff --git a/internal/generator/templates/helpers.go.tmpl b/internal/generator/templates/helpers.go.tmpl
index afb1803d..66d826d1 100644
--- a/internal/generator/templates/helpers.go.tmpl
+++ b/internal/generator/templates/helpers.go.tmpl
@@ -9,10 +9,56 @@ import (
 	"sort"
 	"strings"
 	"text/tabwriter"
+
+	"github.com/mattn/go-isatty"
 )
 
 var As = errors.As
 
+// noColor is set by the --no-color flag
+var noColor bool
+
+func colorEnabled() bool {
+	if noColor {
+		return false
+	}
+	if os.Getenv("NO_COLOR") != "" {
+		return false
+	}
+	if os.Getenv("TERM") == "dumb" {
+		return false
+	}
+	return isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
+}
+
+func bold(s string) string {
+	if !colorEnabled() {
+		return s
+	}
+	return "\033[1m" + s + "\033[0m"
+}
+
+func green(s string) string {
+	if !colorEnabled() {
+		return s
+	}
+	return "\033[32m" + s + "\033[0m"
+}
+
+func red(s string) string {
+	if !colorEnabled() {
+		return s
+	}
+	return "\033[31m" + s + "\033[0m"
+}
+
+func yellow(s string) string {
+	if !colorEnabled() {
+		return s
+	}
+	return "\033[33m" + s + "\033[0m"
+}
+
 type cliError struct {
 	code int
 	err  error
@@ -251,7 +297,7 @@ func printAutoTable(w io.Writer, items []map[string]any) error {
 	tw := newTabWriter(w)
 	upperHeaders := make([]string, len(headers))
 	for i, h := range headers {
-		upperHeaders[i] = strings.ToUpper(h)
+		upperHeaders[i] = bold(strings.ToUpper(h))
 	}
 
 	fmt.Fprintln(tw, strings.Join(upperHeaders, "\t"))
diff --git a/internal/generator/templates/root.go.tmpl b/internal/generator/templates/root.go.tmpl
index 55089ee7..95cecc4d 100644
--- a/internal/generator/templates/root.go.tmpl
+++ b/internal/generator/templates/root.go.tmpl
@@ -40,6 +40,7 @@ func Execute() error {
 	rootCmd.PersistentFlags().StringVar(&flags.configPath, "config", "", "Config file path")
 	rootCmd.PersistentFlags().DurationVar(&flags.timeout, "timeout", 30*time.Second, "Request timeout")
 	rootCmd.PersistentFlags().BoolVar(&flags.dryRun, "dry-run", false, "Show request without sending")
+	rootCmd.PersistentFlags().BoolVar(&noColor, "no-color", false, "Disable colored output")
 
 {{- range $name, $resource := .Resources}}
 	rootCmd.AddCommand(new{{camel $name}}Cmd(&flags)) {{/* FuncPrefix matches resource name for top-level */}}

← babd0c66 fix(openapi): filter global query params that appear on >80%  ·  back to Cli Printing Press  ·  feat(cli): accept URLs for --spec with local caching 2e70bc46 →