This guide explains how to set up a development environment for ZCP CLI, understand the repository structure, run tests, and contribute new commands.
| Tool | Minimum Version | Notes |
|---|---|---|
| Go | 1.26.1 | As declared in go.mod toolchain directive |
| Make | Any | GNU Make for build targets |
| Git | Any | Required for version embedding via git describe |
Install Go from https://go.dev/dl/. Verify your installation:
go version
# go version go1.26.1 linux/amd64zcp-cli/
├── cmd/
│ └── zcp/
│ ├── main.go # Entry point; wires root command
│ └── root/
│ └── root.go # Root Cobra command; persistent flags; command registration
├── internal/
│ ├── config/ # Config file loading, saving, profile resolution
│ ├── httpclient/ # Shared HTTP client with auth header injection
│ ├── output/ # Table / JSON / YAML output rendering
│ ├── version/ # Version string (set via ldflags at build time)
│ ├── commands/ # Cobra command implementations (instance, region, dns, etc.)
│ └── api/
│ ├── apierrors/ # API error types and response parsing
│ ├── response/ # Generic response envelope types
│ ├── region/ # Region service
│ ├── instance/ # Virtual machine service
│ ├── plan/ # Service plan listing
│ ├── template/ # Template service
│ └── ... # 30+ service packages (volume, network, dns, billing, etc.)
├── docs/ # Markdown documentation
├── scripts/ # Install scripts (install.sh, install.ps1)
├── Makefile # All build, test, and quality targets
├── go.mod # Module definition and dependencies
└── go.sum # Dependency checksums
internal/config— manages~/.config/zcp/config.yaml, profile resolution, and URL precedence logic.pkg/httpclient— a singleClientstruct used by all API service packages and by external consumers (e.g. the Terraform provider). It injects theAuthorization: Bearerheader, setsUser-Agent, and delegates error parsing toapierrors. Also providesGetEnvelope/PostEnvelope/PutEnvelopehelpers for unwrapping the{status, data}response envelope.pkg/api/apierrors— parses ZCP API error envelopes into typedAPIErrorvalues.pkg/api/response— generic response envelope types (Envelope[T],Single[T]) for the paginated API format.internal/output— thePrintertype renders tabular data in table, JSON, or YAML format. All commands use this for consistent output.internal/commands— one file per command group (e.g.,instance.go,region.go,dns.go). Each file registers Cobra subcommands and implementsRunEfunctions.
All targets are defined in the Makefile. Run make help for a full listing.
make build # Build bin/zcp for the current OS/arch
make dev # Alias for build
make build-all # Cross-compile: Linux/Darwin/Windows × amd64/arm64
make build-linux # Linux amd64 + arm64
make build-darwin # macOS amd64 + arm64
make build-windows# Windows amd64 + arm64 (.exe)
make test # Run all tests: go test -v ./...
make test-race # Run tests with race detector: go test -race ./...
make fmt # Format all Go files with gofmt
make vet # Run go vet ./...
make tidy # go mod tidy
make lint # Run staticcheck (must be installed separately)
make install # Copy bin/zcp to /usr/local/bin/zcp
make clean # Remove the bin/ directory
make release-checksums # Generate SHA256 checksums for all release binariesThe version string is embedded at build time:
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
LDFLAGS := -ldflags "-s -w -X $(VERSION_PKG).Version=$(VERSION)"When building outside a git repository, the version defaults to dev.
# All tests, verbose
make test
# With race detector (recommended before submitting a PR)
make test-race
# A specific package
go test -v ./internal/config/...
# A single test function
go test -v -run TestSaveAndLoad ./internal/config/...Tests use only the standard library's net/http/httptest for server mocking. No external test frameworks are used.
- Test files use the
_testpackage suffix (e.g.,package config_test) for black-box testing. - Tests that write config files use
t.TempDir()andt.Setenv("XDG_CONFIG_HOME", ...)to isolate state. - HTTP handler tests use
httptest.NewServerto spin up a local server and assert on request headers, query parameters, and response decoding. - Waiter tests use short
WithPollIntervalandWithWaitTimeoutvalues to keep test duration minimal.
The following steps add a new top-level command group. The example adds zcp network list.
Create pkg/api/network/network.go:
package network
import (
"context"
"github.com/zsoftly/zcp-cli/pkg/httpclient"
)
type Network struct {
UUID string `json:"uuid"`
Name string `json:"name"`
}
type Service struct {
client *httpclient.Client
}
func NewService(client *httpclient.Client) *Service {
return &Service{client: client}
}
func (s *Service) List(ctx context.Context) ([]Network, error) {
// implement API call
return nil, nil
}Create pkg/api/network/network_test.go following the pattern used in pkg/api/region/region_test.go:
- Spin up an
httptest.Serverthat returns fixture JSON. - Assert on the path, query parameters, and decoded result.
- Assert that HTTP error responses surface as errors.
Create internal/commands/network.go:
package commands
import (
"github.com/spf13/cobra"
"github.com/zsoftly/zcp-cli/pkg/api/network"
"github.com/zsoftly/zcp-cli/internal/config"
"github.com/zsoftly/zcp-cli/pkg/httpclient"
"github.com/zsoftly/zcp-cli/internal/output"
)
func NewNetworkCmd(flags *config.GlobalFlags) *cobra.Command {
cmd := &cobra.Command{
Use: "network",
Short: "Manage network resources",
}
cmd.AddCommand(newNetworkListCmd(flags))
return cmd
}
func newNetworkListCmd(flags *config.GlobalFlags) *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List networks",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return err
}
profile, err := config.ResolveProfile(cfg, flags.Profile)
if err != nil {
return err
}
client := httpclient.New(httpclient.Options{
BaseURL: config.ActiveAPIURL(profile, flags.APIURL),
APIKey: profile.APIKey,
SecretKey: profile.SecretKey,
})
svc := network.NewService(client)
networks, err := svc.List(cmd.Context())
if err != nil {
return err
}
p := output.NewPrinter(cmd.OutOrStdout(), output.ParseFormat(flags.Output), flags.NoColor)
headers := []string{"UUID", "NAME"}
rows := make([][]string, len(networks))
for i, n := range networks {
rows[i] = []string{n.UUID, n.Name}
}
return p.PrintTable(headers, rows)
},
}
}In cmd/zcp/root/root.go, add:
rootCmd.AddCommand(commands.NewNetworkCmd(flags))make build
./bin/zcp network list --help
make test- Follow standard Go formatting enforced by
gofmt(make fmt). - All exported types, functions, and constants must have a doc comment.
- Return
errorfromRunErather than callingos.Exitdirectly; Cobra handles printing the error. - Do not use
log.Fatalorfmt.Printlnin command implementations — use theoutput.Printerorcmd.ErrOrStderr(). - Use
context.Contextfor all HTTP calls to support timeout and cancellation. - Keep API service packages free of Cobra and output dependencies. Services receive a
*httpclient.Clientand return domain types and errors.
Dependencies are managed with Go modules.
# Add a new dependency (imported in code first)
go get github.com/some/package@v1.2.3
# Tidy up unused dependencies
make tidy
# Verify the dependency graph
go mod verifyCurrent direct dependencies:
| Package | Purpose |
|---|---|
github.com/spf13/cobra |
CLI framework |
github.com/olekukonko/tablewriter |
Terminal table rendering |
gopkg.in/yaml.v3 |
YAML marshalling/unmarshalling |
Prefer the standard library where possible. Introduce new dependencies only when they provide significant value over a standard library implementation.