Module 7 · Phase 4 · ~90 min

Phase 4 — Package as a CLI

Polish: rebuild the entrypoint with cobra, add proper subcommand help, structured logging, graceful shutdown, optional config file, and a Makefile. Make this something you'd hand off without embarrassment.

What "production-grade CLI" means in Go

Step 1 — Add cobra

go get github.com/spf13/cobra@latest
go mod tidy

Step 2 — Restructure

cantonctl-starter/
├── cmd/cantonctl/
│   ├── main.go            # thin: instantiates root command, calls Execute()
│   ├── root.go            # root command + persistent flags
│   ├── ping.go            # `ping` subcommand
│   ├── submit.go          # `submit` subcommand
│   └── stream.go          # `stream` subcommand
└── internal/ledger/
    └── client.go          # unchanged from phases 1-3

Step 3 — Sketch of root.go

package main

import (
    "context"
    "log/slog"
    "os"
    "os/signal"
    "syscall"

    "github.com/spf13/cobra"
)

var (
    // Persistent flags — bound on root, available to all subcommands
    flagEndpoint string
    flagTLS      bool
    flagCAFile   string
    flagToken    string
    flagLogLevel string
)

var rootCmd = &cobra.Command{
    Use:   "cantonctl",
    Short: "Talk to a Canton participant's Ledger API.",
    Long:  `cantonctl is a small CLI for poking a Canton participant: ping, submit
commands, stream transaction updates. Use --help on any subcommand for flags.`,
    PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
        configureLogger()
        return nil
    },
    SilenceUsage: true,  // don't reprint help on err
}

func init() {
    rootCmd.PersistentFlags().StringVar(&flagEndpoint, "endpoint", "localhost:5011", "Ledger API endpoint")
    rootCmd.PersistentFlags().BoolVar(&flagTLS, "tls", false, "use TLS")
    rootCmd.PersistentFlags().StringVar(&flagCAFile, "ca", "", "PEM CA bundle (with --tls)")
    rootCmd.PersistentFlags().StringVar(&flagToken, "token", "", "Bearer JWT for the Authorization header")
    rootCmd.PersistentFlags().StringVar(&flagLogLevel, "log-level", "info", "debug, info, warn, error")
}

func main() {
    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer stop()
    if err := rootCmd.ExecuteContext(ctx); err != nil {
        slog.Error("command failed", "err", err)
        os.Exit(1)
    }
}

func configureLogger() {
    var level slog.Level
    level.UnmarshalText([]byte(flagLogLevel))
    handler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: level})
    slog.SetDefault(slog.New(handler))
}

Step 4 — Sketch of ping.go

package main

import (
    "github.com/spf13/cobra"
    "example.com/cantonctl/internal/ledger"
)

func init() {
    rootCmd.AddCommand(&cobra.Command{
        Use:   "ping",
        Short: "Health-check the participant",
        RunE: func(cmd *cobra.Command, args []string) error {
            ctx := cmd.Context()
            c, err := ledger.Dial(ctx, ledger.Config{
                Endpoint: flagEndpoint,
                TLS:      flagTLS,
                Token:    flagToken,
            })
            if err != nil { return err }
            defer c.Close()
            return c.Ping(ctx)
        },
    })
}

Same shape for submit.go and stream.go, with their own flags. Each subcommand stays self-contained — the file structure scales as you add more.

Step 5 — Add a Makefile (optional but tidy)

VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)

.PHONY: build test lint clean

build:
	go build -ldflags "-X main.version=$(VERSION)" -o bin/cantonctl ./cmd/cantonctl

test:
	go test -race ./...

lint:
	go vet ./...

clean:
	rm -rf bin/

Step 6 — Polish checklist

Bonus — observability

If you have time, wire in:

None of these are required. The base CLI is the milestone.

Done when

You can make build and the resulting binary supports ping, submit, and stream against a real Canton sandbox, with clean help text, structured logs, and graceful shutdown. Congratulations — you've built a Canton-aware Go CLI from scratch, which is exactly the kind of thing tooling-focused contributors ship.