feat: builder + App runner, Makefile (v1.1.0)
Builder (cli.For[C]): Root/Dir/Cmd/SlotDir/Slot construct the tree without repeating the [C] type parameter at every node; returns plain *Node[C] so it interoperates with struct-literal construction. App[C]: collapses the per-binary main.go — standard flags (-color/-json/-version, -server when configured), mode-aware color defaults, version banner, client connect, and the one-shot-vs-shell split — into one Main(). Transport-agnostic via a Connect callback, so it never assumes gRPC. Makefile: `make check` = fixstyle vet lint test (the pre-commit gate), plus build and a linux+openbsd cross target. The example now dogfoods both Builder and App. Tests cover the builder tree and App's one-shot dispatch / -version / nil-Connect paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+32
-63
@@ -6,13 +6,13 @@
|
||||
// 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:
|
||||
//
|
||||
// - a declarative command tree (buildTree);
|
||||
// - 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 <server> 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;
|
||||
// - the color helpers: cli.KV paints the "key=" of a key=value pair blue and
|
||||
// cli.Paint colors status words, both honoring the -color flag.
|
||||
// - 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):
|
||||
//
|
||||
@@ -21,18 +21,16 @@
|
||||
// inv> show server web1 service ? # lists web1's services
|
||||
// inv> colors # the ANSI palette
|
||||
//
|
||||
// Or one-shot — text or JSON:
|
||||
// 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=false show server web1
|
||||
// go run ./example -color show server web1
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -158,65 +156,36 @@ func runColors(context.Context, inventory, []string) error {
|
||||
|
||||
func runQuit(context.Context, inventory, []string) error { return cli.ErrQuit }
|
||||
|
||||
// buildTree is the single source of truth for the command set: dispatch, help,
|
||||
// and tab-completion are all derived from it.
|
||||
// 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] {
|
||||
// show server [<name> [service [<svc>]]]
|
||||
serviceName := &cli.Node[inventory]{Word: "<svc>", Help: "show one service", Dynamic: dynServices, Run: runShowServerService}
|
||||
service := &cli.Node[inventory]{Word: "service", Help: "list a server's services", Run: runShowServerServices, Children: []*cli.Node[inventory]{serviceName}}
|
||||
serverName := &cli.Node[inventory]{Word: "<name>", Help: "show one server", Dynamic: dynServers, Run: runShowServer, Children: []*cli.Node[inventory]{service}}
|
||||
server := &cli.Node[inventory]{Word: "server", Help: "list servers (add <name> for one)", Run: runShowServers, Children: []*cli.Node[inventory]{serverName}}
|
||||
show := &cli.Node[inventory]{Word: "show", Help: "show inventory state", Children: []*cli.Node[inventory]{server}}
|
||||
|
||||
// ping <server>
|
||||
ping := &cli.Node[inventory]{Word: "ping", Help: "ping a server", Children: []*cli.Node[inventory]{
|
||||
{Word: "<name>", Help: "ping this server", Dynamic: dynServers, Run: runPing},
|
||||
}}
|
||||
|
||||
return &cli.Node[inventory]{Children: []*cli.Node[inventory]{
|
||||
show,
|
||||
ping,
|
||||
{Word: "colors", Help: "show the ANSI color palette", Run: runColors},
|
||||
{Word: "quit", Help: "exit the shell", Run: runQuit},
|
||||
{Word: "exit", Help: "exit the shell", Run: runQuit},
|
||||
}}
|
||||
b := cli.For[inventory]()
|
||||
return b.Root(
|
||||
b.Dir("show", "show inventory state",
|
||||
b.Cmd("server", "list servers (add <name> for one)", runShowServers,
|
||||
b.Slot("<name>", "show one server", dynServers, runShowServer,
|
||||
b.Cmd("service", "list a server's services", runShowServerServices,
|
||||
b.Slot("<svc>", "show one service", dynServices, runShowServerService))))),
|
||||
b.Dir("ping", "ping a server",
|
||||
b.Slot("<name>", "ping this server", dynServers, runPing)),
|
||||
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() {
|
||||
color := flag.Bool("color", true, "colorize output: blue key= labels and painted status words")
|
||||
jsonOut := flag.Bool("json", false, "emit JSON instead of text (one-shot mode)")
|
||||
flag.Parse()
|
||||
|
||||
if *jsonOut {
|
||||
cli.SetFormat(cli.FormatJSON)
|
||||
}
|
||||
// JSON output is machine-facing, so suppress ANSI escapes in that mode.
|
||||
cli.SetColor(*color && !*jsonOut)
|
||||
|
||||
inv := newInventory()
|
||||
root := buildTree()
|
||||
ctx := context.Background()
|
||||
|
||||
// With arguments: run one command and exit (script-friendly).
|
||||
if args := flag.Args(); len(args) > 0 {
|
||||
if err := cli.Dispatch(ctx, root, inv, cli.SplitTokens(strings.Join(args, " "))); err != nil {
|
||||
fmt.Fprintln(os.Stderr, cli.Paint(err.Error(), cli.Red))
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// No arguments: interactive REPL with completion and '?'-help. Errors are
|
||||
// painted red via FormatError.
|
||||
fmt.Println("golang-cli example — try: show server, ping db1, colors, '?' for help, TAB to complete")
|
||||
shell := &cli.Shell[inventory]{
|
||||
Root: root,
|
||||
Client: inv,
|
||||
Prompt: "inv> ",
|
||||
(&cli.App[inventory]{
|
||||
Name: "example",
|
||||
Version: "1.1.0",
|
||||
Prompt: "inv> ",
|
||||
Root: buildTree(),
|
||||
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) },
|
||||
}
|
||||
if err := shell.Run(ctx); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}).Main()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user