// SPDX-FileCopyrightText: (C) Copyright 2026 Pim van Pelt // SPDX-License-Identifier: Apache-2.0 // Command example is a self-contained demo of git.ipng.ch/ipng/golang-cli. // // It builds a tiny "server inventory" CLI over an in-memory client — no gRPC, // no external services. It shows the building blocks of the package: // // - the tree built with cli.For/Builder (no repeated [inventory] type param); // - dynamic slot resolvers, including a context-dependent one (dynServices // lists the services of the captured earlier on the path); // - command functions that hand one result to cli.Emit, which renders it as // colorized text or, under -json, as JSON — the command supplies both; // - cli.App, which wires flags (-color/-json/-version), the banner, and the // one-shot-vs-interactive split into a single Main(). // // Run it interactively (TAB to complete, '?' for help): // // go run ./example // inv> show server # completes to web1 / web2 / db1 // inv> show server web1 service ? # lists web1's services // inv> colors # the ANSI palette // // Or one-shot — text or JSON (one-shot is script-safe: color off unless -color): // // go run ./example show server web1 // go run ./example -json show server web1 // go run ./example -color show server web1 package main import ( "context" "fmt" "sort" "strings" cli "git.ipng.ch/ipng/golang-cli" "git.ipng.ch/ipng/golang-cli/keypress" ) // inventory is the "client" C that the tree is generic over. In a real app this // would be a gRPC client; here it is just in-memory data. It is passed unchanged // to every Dynamic and Run function. type inventory struct { // servers maps a server name to the services running on it. servers map[string][]string } func newInventory() inventory { return inventory{servers: map[string][]string{ "web1": {"http", "https", "ssh"}, "web2": {"http", "https"}, "db1": {"postgres", "ssh"}, }} } func (inv inventory) names() []string { out := make([]string, 0, len(inv.servers)) for n := range inv.servers { out = append(out, n) } sort.Strings(out) return out } // --- dynamic resolvers ------------------------------------------------------- // dynServers lists every server name. args is unused here. func dynServers(_ context.Context, inv inventory, _ []string) []string { return inv.names() } // dynServices is context-dependent: it lists the services of the server that // was captured as args[0] earlier on the path. This is what lets // "show server web1 service " offer only web1's services. func dynServices(_ context.Context, inv inventory, args []string) []string { if len(args) == 0 { return nil } svcs := append([]string(nil), inv.servers[args[0]]...) sort.Strings(svcs) return svcs } // --- command functions ------------------------------------------------------- // // Each hands cli.Emit a text form and a machine value; the framework prints one // or the other based on -json. Text uses cli.KV (blue "key=") and cli.Paint. func runShowServers(_ context.Context, inv inventory, _ []string) error { names := inv.names() return cli.Emit(cli.KV("servers", strings.Join(names, ", ")), map[string]any{"servers": names}) } type serverView struct { Name string `json:"name"` Count int `json:"count"` Services []string `json:"services"` } func runShowServer(_ context.Context, inv inventory, args []string) error { name := args[0] svcs, ok := inv.servers[name] if !ok { return fmt.Errorf("no such server: %s", name) } text := fmt.Sprintf("%s %s %s", cli.KV("name", name), cli.KV("count", fmt.Sprintf("%d", len(svcs))), cli.KV("services", strings.Join(svcs, ", "))) return cli.Emit(text, serverView{Name: name, Count: len(svcs), Services: svcs}) } func runShowServerServices(_ context.Context, inv inventory, args []string) error { name := args[0] text := fmt.Sprintf("%s %s", cli.KV("server", name), cli.KV("services", strings.Join(inv.servers[name], ", "))) return cli.Emit(text, map[string]any{"server": name, "services": inv.servers[name]}) } func runShowServerService(_ context.Context, inv inventory, args []string) error { server, svc := args[0], args[1] for _, s := range inv.servers[server] { if s == svc { text := cli.KV(server+"/"+svc, cli.Paint("running", cli.Green)) return cli.Emit(text, map[string]any{"server": server, "service": svc, "running": true}) } } return fmt.Errorf("%s is not running %s", server, svc) } func runPing(_ context.Context, inv inventory, args []string) error { name := args[0] if _, ok := inv.servers[name]; !ok { return fmt.Errorf("no such server: %s", name) } return cli.Emit(fmt.Sprintf("pong from %s", cli.Paint(name, cli.Green)), map[string]any{"server": name, "reply": "pong"}) } // runColors demonstrates the palette. Each line shows a value painted with one // of the standard colors; with -color=false the same text prints unadorned. func runColors(context.Context, inventory, []string) error { fmt.Println("status words via Paint (try -color=false to disable):") fmt.Println(" " + cli.KV("status", cli.Paint("OK", cli.Green))) fmt.Println(" " + cli.KV("status", cli.Paint("DEGRADED", cli.Yellow))) fmt.Println(" " + cli.KV("status", cli.Paint("DOWN", cli.Red))) fmt.Println(" " + cli.KV("note", cli.Paint("highlight", cli.Cyan))) fmt.Println("the raw helpers:") fmt.Printf(" Paint(%q, cli.Red) => %s\n", "text", cli.Paint("text", cli.Red)) fmt.Printf(" Paint(%q, cli.Green) => %s\n", "text", cli.Paint("text", cli.Green)) fmt.Printf(" Paint(%q, cli.Blue) => %s\n", "text", cli.Paint("text", cli.Blue)) fmt.Printf(" Paint(%q, cli.Yellow) => %s\n", "text", cli.Paint("text", cli.Yellow)) fmt.Printf(" KV(%q, %q) => %s\n", "key", "value", cli.KV("key", "value")) return nil } // runWatch streams a few simulated events, stopping early if a key is pressed. // In a real CLI this would be a server-side stream; here it is a bounded loop so // the demo never hangs when stdin is not a terminal. Press any key (in a TTY) to // stop it before it finishes. func runWatch(ctx context.Context, _ inventory, _ []string) error { ctx, cancel := context.WithCancel(ctx) defer cancel() go keypress.WaitForKey(ctx, cancel) for i := 1; i <= 5; i++ { select { case <-ctx.Done(): return nil // key pressed default: } if err := cli.Emit(cli.KV("event", fmt.Sprintf("%d", i)), map[string]any{"event": i}); err != nil { return err } } return nil } // runInspect demonstrates the default renderer: it builds one value (the model) // and hands it to cli.Render, which prints it as JSON under -json or as painted // text (blue keys, bright-white values, nested objects indented) otherwise — no // per-command text code. func runInspect(_ context.Context, inv inventory, _ []string) error { type host struct { Name string `json:"name"` Services []string `json:"services"` } f := struct { Count int `json:"count"` Hosts []host `json:"hosts"` }{Count: len(inv.servers)} for _, n := range inv.names() { f.Hosts = append(f.Hosts, host{Name: n, Services: inv.servers[n]}) } return cli.Render(f) } func runQuit(context.Context, inventory, []string) error { return cli.ErrQuit } // buildTree is the single source of truth for the command set, built with the // cli.Builder so no node repeats the [inventory] type parameter. func buildTree() *cli.Node[inventory] { b := cli.For[inventory]() return b.Root( b.Dir("show", "show inventory state", b.Cmd("server", "list servers (add for one)", runShowServers, b.Slot("", "show one server", dynServers, runShowServer, b.Cmd("service", "list a server's services", runShowServerServices, b.Slot("", "show one service", dynServices, runShowServerService))))), b.Dir("ping", "ping a server", b.Slot("", "ping this server", dynServers, runPing)), b.Cmd("watch", "stream a few events (any key stops it)", runWatch), b.Cmd("inspect", "render the fleet via the default renderer (try -json / -color)", runInspect), b.Cmd("colors", "show the ANSI color palette", runColors), b.Cmd("quit", "exit the shell", runQuit), b.Cmd("exit", "exit the shell", runQuit), ) } func main() { (&cli.App[inventory]{ Name: "example", Version: "1.3.0", Prompt: "inv> ", Root: buildTree(), JSON: true, // commands use cli.Emit, so advertise -json Greeting: "golang-cli example — try: show server, ping db1, colors, '?' for help, TAB to complete", // Local CLI: no -server flag. Connect just hands over the in-memory data, // proving App is transport-agnostic (it never dials anything itself). Connect: func(context.Context, string) (inventory, func(), error) { return newInventory(), nil, nil }, FormatError: func(err error) string { return cli.Paint(err.Error(), cli.Red) }, }).Main() }