[object Object]

← back to Cli Printing Press

feat(generator): auto-detect array responses and render as formatted tables

c5618c0830d4d7e623413af52861fef59469ffe8 · 2026-03-23 20:09:51 -0700 · Matt Van Horn

List commands now show formatted tables by default instead of raw JSON.
printOutput() detects arrays, extracts field names, and renders with
tabwriter alignment. Prioritizes common fields (id, name, status, type).
--json flag still returns raw JSON. Single objects pretty-print as JSON.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit c5618c0830d4d7e623413af52861fef59469ffe8
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date:   Mon Mar 23 20:09:51 2026 -0700

    feat(generator): auto-detect array responses and render as formatted tables
    
    List commands now show formatted tables by default instead of raw JSON.
    printOutput() detects arrays, extracts field names, and renders with
    tabwriter alignment. Prioritizes common fields (id, name, status, type).
    --json flag still returns raw JSON. Single objects pretty-print as JSON.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
 internal/generator/templates/command.go.tmpl |  11 +--
 internal/generator/templates/helpers.go.tmpl | 109 +++++++++++++++++++++++++++
 2 files changed, 111 insertions(+), 9 deletions(-)

diff --git a/internal/generator/templates/command.go.tmpl b/internal/generator/templates/command.go.tmpl
index 4b57355f..07d38c5e 100644
--- a/internal/generator/templates/command.go.tmpl
+++ b/internal/generator/templates/command.go.tmpl
@@ -1,16 +1,14 @@
 package cli
 
 import (
-{{- if .Resource.Endpoints}}
-	"encoding/json"
 	"fmt"
-{{- end}}
 	"strings"
 
 	"github.com/spf13/cobra"
 )
 
 var _ = strings.ReplaceAll // ensure import
+var _ = fmt.Sprintf        // ensure import
 
 func new{{camel .FuncPrefix}}Cmd(flags *rootFlags) *cobra.Command {
 	cmd := &cobra.Command{
@@ -97,12 +95,7 @@ func new{{camel $.FuncPrefix}}{{camel $eName}}Cmd(flags *rootFlags) *cobra.Comma
 				return apiErr(err)
 			}
 
-			if flags.asJSON {
-				return flags.printJSON(cmd, json.RawMessage(data))
-			}
-
-			fmt.Fprintln(cmd.OutOrStdout(), string(data))
-			return nil
+			return printOutput(cmd.OutOrStdout(), data, flags.asJSON)
 		},
 	}
 
diff --git a/internal/generator/templates/helpers.go.tmpl b/internal/generator/templates/helpers.go.tmpl
index 3fac6d69..22fc7888 100644
--- a/internal/generator/templates/helpers.go.tmpl
+++ b/internal/generator/templates/helpers.go.tmpl
@@ -1,8 +1,13 @@
 package cli
 
 import (
+	"encoding/json"
 	"errors"
+	"fmt"
+	"io"
+	"sort"
 	"strings"
+	"text/tabwriter"
 )
 
 var As = errors.As
@@ -39,6 +44,110 @@ func firstNonEmpty(items ...string) string {
 	return ""
 }
 
+func newTabWriter(w io.Writer) *tabwriter.Writer {
+	return tabwriter.NewWriter(w, 2, 4, 2, ' ', 0)
+}
+
 func replacePathParam(path, name, value string) string {
 	return strings.ReplaceAll(path, "{"+name+"}", value)
 }
+
+// printOutput auto-detects arrays and renders as tables, or prints raw JSON for objects.
+func printOutput(w io.Writer, data json.RawMessage, asJSON bool) error {
+	if asJSON {
+		enc := json.NewEncoder(w)
+		enc.SetIndent("", "  ")
+		return enc.Encode(data)
+	}
+
+	// Try to detect if response is an array
+	var items []map[string]any
+	if err := json.Unmarshal(data, &items); err == nil && len(items) > 0 {
+		return printAutoTable(w, items)
+	}
+
+	// Single object - pretty print
+	var obj map[string]any
+	if err := json.Unmarshal(data, &obj); err == nil {
+		enc := json.NewEncoder(w)
+		enc.SetIndent("", "  ")
+		return enc.Encode(obj)
+	}
+
+	// Fallback: print raw
+	fmt.Fprintln(w, string(data))
+	return nil
+}
+
+func printAutoTable(w io.Writer, items []map[string]any) error {
+	if len(items) == 0 {
+		return nil
+	}
+
+	// Collect headers from first item, prioritize common fields
+	priority := []string{"id", "name", "username", "title", "status", "type", "email", "description"}
+	headerSet := map[string]struct{}{}
+	for k := range items[0] {
+		headerSet[k] = struct{}{}
+	}
+
+	var headers []string
+	for _, p := range priority {
+		if _, ok := headerSet[p]; ok {
+			headers = append(headers, p)
+			delete(headerSet, p)
+		}
+	}
+	// Add remaining headers sorted
+	var rest []string
+	for k := range headerSet {
+		rest = append(rest, k)
+	}
+	sort.Strings(rest)
+	headers = append(headers, rest...)
+
+	// Limit to 6 columns max for readability
+	if len(headers) > 6 {
+		headers = headers[:6]
+	}
+
+	// Build rows
+	rows := make([][]string, 0, len(items))
+	for _, item := range items {
+		row := make([]string, len(headers))
+		for i, h := range headers {
+			v := item[h]
+			switch val := v.(type) {
+			case string:
+				row[i] = truncate(val, 40)
+			case float64:
+				if val == float64(int64(val)) {
+					row[i] = fmt.Sprintf("%d", int64(val))
+				} else {
+					row[i] = fmt.Sprintf("%g", val)
+				}
+			case bool:
+				row[i] = fmt.Sprintf("%t", val)
+			case nil:
+				row[i] = ""
+			default:
+				b, _ := json.Marshal(val)
+				row[i] = truncate(string(b), 40)
+			}
+		}
+		rows = append(rows, row)
+	}
+
+	// Print with tab alignment using tabwriter
+	tw := newTabWriter(w)
+	upperHeaders := make([]string, len(headers))
+	for i, h := range headers {
+		upperHeaders[i] = strings.ToUpper(h)
+	}
+
+	fmt.Fprintln(tw, strings.Join(upperHeaders, "\t"))
+	for _, row := range rows {
+		fmt.Fprintln(tw, strings.Join(row, "\t"))
+	}
+	return tw.Flush()
+}

← e993ba6e feat(generator): auto-generate usage examples in command hel  ·  back to Cli Printing Press  ·  feat(generator): retry logic, structured exit codes, and dry 086f1d45 →