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:
+64
@@ -0,0 +1,64 @@
|
||||
// SPDX-FileCopyrightText: (C) Copyright 2026 Pim van Pelt <pim@ipng.ch>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cli
|
||||
|
||||
import "context"
|
||||
|
||||
// Builder constructs Node[C] values without repeating the [C] type parameter at
|
||||
// every node. Obtain one with For[C], then use Root/Dir/Cmd/SlotDir/Slot:
|
||||
//
|
||||
// b := cli.For[Client]()
|
||||
// root := b.Root(
|
||||
// b.Dir("show", "show state",
|
||||
// b.Cmd("version", "print version", runVersion),
|
||||
// b.Cmd("server", "list servers (add <name> for one)", runServers,
|
||||
// b.Slot("<name>", "show one", dynServers, runServer))),
|
||||
// b.Cmd("quit", "exit", runQuit),
|
||||
// )
|
||||
//
|
||||
// The naming is symmetric: Dir/Cmd are fixed keyword nodes (without/with an
|
||||
// action), SlotDir/Slot are dynamic argument nodes (without/with an action).
|
||||
// The builder is a zero-size value, so constructing many nodes from one is free.
|
||||
//
|
||||
// It is purely a convenience: every method returns a plain *Node[C], so builder
|
||||
// and struct-literal construction interoperate freely — you can mix the two,
|
||||
// and a node built either way can still be mutated afterward (e.g. to wire a
|
||||
// circular slot: opt := b.Slot(...); opt.Children = []*cli.Node[C]{opt}).
|
||||
type Builder[C any] struct{}
|
||||
|
||||
// For returns a Builder bound to client type C.
|
||||
func For[C any]() Builder[C] { return Builder[C]{} }
|
||||
|
||||
// Root returns the (wordless) root node holding the given top-level children.
|
||||
func (Builder[C]) Root(children ...*Node[C]) *Node[C] {
|
||||
return &Node[C]{Children: children}
|
||||
}
|
||||
|
||||
// Dir is a fixed keyword node with no action of its own — a grouping of
|
||||
// subcommands (e.g. "show", "create").
|
||||
func (Builder[C]) Dir(word, help string, children ...*Node[C]) *Node[C] {
|
||||
return &Node[C]{Word: word, Help: help, Children: children}
|
||||
}
|
||||
|
||||
// Cmd is a fixed keyword node that runs an action. It may also carry children:
|
||||
// a node that both does something and has subcommands (e.g. "show instance",
|
||||
// which lists members and also accepts "<id>").
|
||||
func (Builder[C]) Cmd(word, help string, run func(context.Context, C, []string) error, children ...*Node[C]) *Node[C] {
|
||||
return &Node[C]{Word: word, Help: help, Run: run, Children: children}
|
||||
}
|
||||
|
||||
// SlotDir is a dynamic slot that captures any token as an argument but has no
|
||||
// action of its own — a navigational placeholder (e.g. "<id>" in
|
||||
// "instance <id> evpn ..."). dyn yields the live completion candidates, given
|
||||
// the args captured by earlier slots on the path.
|
||||
func (Builder[C]) SlotDir(word, help string, dyn func(context.Context, C, []string) []string, children ...*Node[C]) *Node[C] {
|
||||
return &Node[C]{Word: word, Help: help, Dynamic: dyn, Children: children}
|
||||
}
|
||||
|
||||
// Slot is a dynamic slot that both captures a token argument and runs an action
|
||||
// (e.g. "delete group <id>"). It may also carry children. dyn yields the live
|
||||
// completion candidates for the slot, given the args captured by earlier slots.
|
||||
func (Builder[C]) Slot(word, help string, dyn func(context.Context, C, []string) []string, run func(context.Context, C, []string) error, children ...*Node[C]) *Node[C] {
|
||||
return &Node[C]{Word: word, Help: help, Dynamic: dyn, Run: run, Children: children}
|
||||
}
|
||||
Reference in New Issue
Block a user