refactor(maglevc): build the CLI on the golang-cli library

Replace maglevc's hand-rolled command-tree CLI with
git.ipng.ch/ipng/golang-cli v1.3.0, mirroring the evpnc refactor. The
tree (commands.go) and the gRPC-status error unwrap (color.go) stay
app-specific; the generic parts — parse tree, completion, '?'-help, the
readline shell, one-shot dispatch, color helpers, and the watch keypress
handler — now come from the library.

- main.go: a single cli.App[grpcapi.MaglevClient] with a Connect
  callback; drops the flag/color-default/dispatch boilerplate.
- commands.go: `type node = cli.Node[grpcapi.MaglevClient]`; label() ->
  cli.Label(); dyn* gain the captured-args parameter the library's
  Dynamic signature carries; runQuit returns cli.ErrQuit.
- watch.go: keypress.WaitForKey replaces the inline cbreak helper.
- color.go: only formatError remains, reading cli.ColorEnabled().
- delete tree.go, complete.go, shell.go.
- tests use the library API; add TestTreeValid (cli.Validate).

Behavior is unchanged except labels/errors now use the library's bright
ANSI palette (was dark); escape lengths are identical so tabwriter
alignment is unaffected. maglevc additionally gains the OpenBSD readline
fix and BSD-correct watch keypress it previously lacked. Builds on linux
and openbsd.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-05 22:38:34 +02:00
parent d2ee6d009e
commit 76fbe2eee0
10 changed files with 227 additions and 788 deletions
+29 -70
View File
@@ -1,94 +1,53 @@
// SPDX-License-Identifier: Apache-2.0
// Command maglevc is the vpp-maglev CLI. It talks only to maglevd. With no
// arguments it starts an interactive shell (readline, tab-completion, '?' help,
// prefix abbreviation); with arguments it runs one command and exits. The
// command set is a single declarative tree (buildTree) -- dispatch, help, and
// completion are all derived from it, via the git.ipng.ch/ipng/golang-cli
// library.
package main
import (
"context"
"flag"
"fmt"
"os"
"strings"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
cli "git.ipng.ch/ipng/golang-cli"
buildinfo "git.ipng.ch/ipng/vpp-maglev/cmd"
"git.ipng.ch/ipng/vpp-maglev/internal/grpcapi"
"git.ipng.ch/ipng/vpp-maglev/internal/netutil"
)
// defaultGRPCPort is the maglevd gRPC port (mirrors the server's
// -grpc-addr default in cmd/server/main.go). Used when -server is given
// without an explicit ":<port>" so operators can type "--server chbtl2"
// instead of "--server chbtl2:9090".
// defaultGRPCPort is the maglevd gRPC port (mirrors the server's -grpc-addr
// default), used when -server is given without an explicit ":<port>".
const defaultGRPCPort = "9090"
func main() {
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", formatError(err))
os.Exit(1)
}
(&cli.App[grpcapi.MaglevClient]{
Name: "maglevc",
Version: buildinfo.Version(),
Commit: buildinfo.Commit(),
Date: buildinfo.Date(),
Prompt: "maglev> ",
Root: buildTree(),
DefaultServer: "localhost:9090",
ServerEnv: "MAGLEV_SERVER",
Connect: connect,
FormatError: formatError,
}).Main()
}
func run() error {
defaultServer := "localhost:9090"
if v := os.Getenv("MAGLEV_SERVER"); v != "" {
defaultServer = v
}
serverAddr := flag.String("server", defaultServer, "maglev server address (env: MAGLEV_SERVER)")
color := flag.Bool("color", true, "colorize static labels in output (defaults to false in one-shot mode)")
printVersion := flag.Bool("version", false, "print version and exit")
flag.Parse()
if *printVersion {
fmt.Printf("maglevc %s (commit %s, built %s)\n",
buildinfo.Version(), buildinfo.Commit(), buildinfo.Date())
return nil
}
// Detect whether -color was explicitly set so we can pick a
// mode-aware default: color is useful in the interactive shell but
// noise (ANSI escapes) when piping one-shot output into scripts.
colorExplicit := false
flag.Visit(func(f *flag.Flag) {
if f.Name == "color" {
colorExplicit = true
}
})
addr := netutil.EnsurePort(*serverAddr, defaultGRPCPort)
conn, err := grpc.NewClient(addr,
grpc.WithTransportCredentials(insecure.NewCredentials()))
// connect dials maglevd and returns the gRPC client. App resolves -server (env
// MAGLEV_SERVER, default localhost:9090) and hands us the raw address; we ensure
// a port and dial insecure, mirroring maglevd's default transport.
func connect(_ context.Context, server string) (grpcapi.MaglevClient, func(), error) {
addr := netutil.EnsurePort(server, defaultGRPCPort)
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return fmt.Errorf("connect %s: %w", addr, err)
return nil, nil, fmt.Errorf("connect %s: %w", addr, err)
}
defer func() { _ = conn.Close() }()
client := grpcapi.NewMaglevClient(conn)
ctx := context.Background()
args := flag.Args()
if len(args) == 0 {
// Interactive shell: color defaults to true.
if colorExplicit {
colorEnabled = *color
} else {
colorEnabled = true
}
fmt.Printf("maglevc %s (commit %s, built %s)\n",
buildinfo.Version(), buildinfo.Commit(), buildinfo.Date())
return runShell(ctx, client)
}
// One-shot command from CLI arguments: color defaults to false so
// output is script-safe. Operators wanting color can still pass
// -color=true explicitly.
if colorExplicit {
colorEnabled = *color
} else {
colorEnabled = false
}
root := buildTree()
tokens := splitTokens(strings.Join(args, " "))
return dispatch(ctx, root, client, tokens)
return grpcapi.NewMaglevClient(conn), func() { _ = conn.Close() }, nil
}