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:
@@ -72,6 +72,47 @@ Return `cli.ErrQuit` from a command's `Run` to stop the REPL. `Shell.FormatError
|
||||
lets you render command errors however you like (e.g. unwrap a gRPC status to its
|
||||
message and color it); it defaults to `err.Error()`.
|
||||
|
||||
## Less boilerplate: `Builder` and `App`
|
||||
|
||||
`cli.For[C]()` returns a `Builder` so no node repeats the `[C]` type parameter.
|
||||
The names are symmetric — `Dir`/`Cmd` for fixed keywords (without/with an
|
||||
action), `SlotDir`/`Slot` for dynamic argument nodes:
|
||||
|
||||
```go
|
||||
b := cli.For[inventory]()
|
||||
root := b.Root(
|
||||
b.Dir("show", "show state",
|
||||
b.Cmd("server", "list servers", runShowServers,
|
||||
b.Slot("<name>", "show one", dynServers, runShowServer))),
|
||||
b.Cmd("quit", "exit", runQuit),
|
||||
)
|
||||
```
|
||||
|
||||
`cli.App[C]` wraps the whole process entry point — the standard flags
|
||||
(`-color`, `-json`, `-version`, and `-server` when configured), mode-aware color
|
||||
defaults, the version banner, connecting the client, and the one-shot-vs-shell
|
||||
split — into a single `Main()`. It is transport-agnostic: it never dials
|
||||
anything itself, so supply `Connect` to build the client (dial gRPC, open a
|
||||
socket, or return an in-memory value):
|
||||
|
||||
```go
|
||||
func main() {
|
||||
(&cli.App[inventory]{
|
||||
Name: "inv", Version: "1.1.0", Prompt: "inv> ", Root: buildTree(),
|
||||
// Local CLI: no -server flag. A networked CLI sets DefaultServer/ServerEnv
|
||||
// and dials inside Connect.
|
||||
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()
|
||||
}
|
||||
```
|
||||
|
||||
Both are additive conveniences: every builder method returns a plain
|
||||
`*cli.Node[C]`, so builder and struct-literal construction interoperate, and you
|
||||
can still drive the lifecycle yourself with `Shell`/`Dispatch` instead of `App`.
|
||||
|
||||
## Output: color and JSON
|
||||
|
||||
A command describes its result **once** and the framework renders it:
|
||||
|
||||
Reference in New Issue
Block a user