Files
pim e030cd28e9 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>
2026-06-05 21:59:33 +02:00

65 lines
3.1 KiB
Go

// 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}
}