← 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
M internal/generator/templates/command.go.tmplM internal/generator/templates/helpers.go.tmpl
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 →