refactor: Cli output via Print() — single output path, redirectable

All CLI output goes through Cli.Print() instead of direct fmt calls.
SetOutput() allows redirecting (testing, logging, etc).

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-20 12:17:30 +00:00
parent 8854d5c79f
commit 6687db76f3

View file

@ -15,6 +15,7 @@ package core
import (
"fmt"
"io"
"os"
"strings"
)
@ -22,9 +23,28 @@ import (
// Cli is the CLI surface for the Core command tree.
type Cli struct {
core *Core
output io.Writer
banner func(*Cli) string
}
// Print writes to the CLI output (defaults to os.Stdout).
//
// c.Cli().Print("hello %s", "world")
func (cl *Cli) Print(format string, args ...any) {
w := cl.output
if w == nil {
w = os.Stdout
}
fmt.Fprintf(w, format+"\n", args...)
}
// SetOutput sets the CLI output writer.
//
// c.Cli().SetOutput(os.Stderr)
func (cl *Cli) SetOutput(w io.Writer) {
cl.output = w
}
// Run resolves os.Args to a command path and executes it.
//
// c.Cli().Run()
@ -37,15 +57,13 @@ func (cl *Cli) Run(args ...string) Result[any] {
clean := FilterArgs(args)
if cl.core == nil || cl.core.commands == nil || len(cl.core.commands.commands) == 0 {
// No commands registered — print banner and exit
if cl.banner != nil {
fmt.Println(cl.banner(cl))
cl.Print(cl.banner(cl))
}
return Result[any]{}
}
// Resolve command path from args
// "deploy to homelab" → try "deploy/to/homelab", then "deploy/to", then "deploy"
var cmd *Command
var remaining []string
@ -59,15 +77,14 @@ func (cl *Cli) Run(args ...string) Result[any] {
}
if cmd == nil {
// No matching command — try root-level action or print help
if cl.banner != nil {
fmt.Println(cl.banner(cl))
cl.Print(cl.banner(cl))
}
cl.PrintHelp()
return Result[any]{}
}
// Build options from remaining args (flags become Options)
// Build options from remaining args
opts := Options{}
for _, arg := range remaining {
key, val, valid := ParseFlag(arg)
@ -80,7 +97,6 @@ func (cl *Cli) Run(args ...string) Result[any] {
} else if !strings.HasPrefix(arg, "-") {
opts = append(opts, Option{K: "_arg", V: arg})
}
// Invalid flags (e.g. -verbose, --v) are silently ignored
}
return cmd.Run(opts)
@ -99,9 +115,9 @@ func (cl *Cli) PrintHelp() {
name = cl.core.app.Name
}
if name != "" {
fmt.Printf("%s commands:\n\n", name)
cl.Print("%s commands:", name)
} else {
fmt.Println("Commands:\n")
cl.Print("Commands:")
}
cl.core.commands.mu.RLock()
@ -112,11 +128,10 @@ func (cl *Cli) PrintHelp() {
continue
}
desc := cl.core.I18n().T(cmd.I18nKey())
// If i18n returned the key itself (no translation), show path only
if desc == cmd.I18nKey() {
fmt.Printf(" %s\n", path)
cl.Print(" %s", path)
} else {
fmt.Printf(" %-30s %s\n", path, desc)
cl.Print(" %-30s %s", path, desc)
}
}
}