Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2c6fcc5
accept full URL in NewAuthorizedClient, default scheme to https
l-hellmann May 13, 2026
b66c389
add CLAUDE.md
l-hellmann May 13, 2026
8478239
add RunRootCmd entry with injectable stdout/stderr and exit code return
l-hellmann May 13, 2026
33e52b1
add integration harness with version and login tests
l-hellmann May 13, 2026
3fd1a6a
allow zcli.yml path override and route config-file notices through in…
l-hellmann May 13, 2026
1c77055
isolate integration tests from user zcli.yml
l-hellmann May 13, 2026
aac7b2f
ignore empty ZEROPS_TOKEN env override
l-hellmann May 13, 2026
7168824
add project list integration test
l-hellmann May 13, 2026
e03b769
expose yamlReader.ResetCache for in-process test isolation
l-hellmann May 13, 2026
d04ccdc
reset yamlReader cache per integration test
l-hellmann May 13, 2026
5eceaea
add service push happy-path integration test
l-hellmann May 13, 2026
49a2242
extend service push tests with setup flag and version-name cases
l-hellmann May 13, 2026
2b6250c
document --setup precedence bug via skipped regression test
l-hellmann May 13, 2026
bc63092
add tier 1 push/deploy tests: error paths, process failure, deploy va…
l-hellmann May 13, 2026
eed4c37
document confirmed nil-deref bug in push log-streaming callback
l-hellmann May 13, 2026
5c032aa
split push/deploy integration tests across files and add tier 2 cases
l-hellmann May 13, 2026
70faee9
switch integration tests to testify assert/require
l-hellmann May 13, 2026
902d614
add tier 3 git archive tests: not-init/no-commits errors, workspace-c…
l-hellmann May 13, 2026
c4b1c79
add workspace-state staged/all, project-flag, and saved-scope tests
l-hellmann May 13, 2026
a4c18d6
document all integration tests in a package-doc test file
l-hellmann May 13, 2026
d5356d2
test interactive selector model via teatest
l-hellmann May 13, 2026
01ce12f
test prompt, input, and selector filter via teatest
l-hellmann May 13, 2026
64e2ea9
test multi-select selector: spacebar toggles, ctrl+a/d, GetOneSelecte…
l-hellmann May 13, 2026
4df5878
test table render, body/row indexing, and cell pretty-flag
l-hellmann May 13, 2026
3b46af0
fix CI lint: gofmt constants, nolint containedctx on RunOptions, t.Co…
l-hellmann May 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

`zcli` is the Zerops CLI written in Go (module `github.com/zeropsio/zcli`, Go 1.24). The entrypoint is `cmd/zcli/main.go`, which delegates to `src/cmd.ExecuteCmd()`. All implementation lives under `src/`.

## Common commands

- `make test` — runs `go test -v ./cmd/... ./src/...`. Run a single test with `go test -v -run TestName ./src/<pkg>/...`.
- `make lint` — runs `golangci-lint` for darwin/arm64, linux/amd64, and windows/amd64 in turn via the `gomodrun` wrapper. Config: `.golangci.yaml`.
- `make all` — cross-builds binaries for windows-amd, linux-amd, darwin-amd, darwin-arm. Single-target builds: `make linux-amd` etc. Builds go through `tools/build.sh`, which sets the version via `-ldflags` from `git rev-parse`/`git describe`, uses build tag `devel`, and writes to `bin/`.
- `make showcase` — runs `src/uxBlock/showcase/main.go` to preview UI elements.
- `gomodrun` (`tools/gomodrun.go`) executes binaries from `./bin` relative to the nearest `go.mod`; the lint target relies on it to find a pinned `golangci-lint`.

## Architecture

### Command layer (`src/cmd`, `src/cmdBuilder`)

Commands are not declared directly against Cobra. Instead, `src/cmdBuilder` wraps Cobra with a fluent builder (`cmdBuilder.NewCmd().Use(...).Arg(...).StringFlag(...).LoggedUserRunFunc(...)`) and `buildCobraCmd.go` translates it into a `*cobra.Command`. The root command lives in `src/cmd/root.go` and registers all subcommands via `AddChildrenCmd`.

Two run-function flavors are central to how commands behave:

- `GuestRunFunc(func(ctx, *GuestCmdData) error)` — runs when there is no auth token. Has access to `CliStorage`, `UxBlocks`, parsed `Args`/`Params`, `Stdout`/`Stderr` printers.
- `LoggedUserRunFunc(func(ctx, *LoggedUserCmdData) error)` — runs when a token is present (from `cliStorage` or the `CliTokenEnvVar` env var). Embeds `*GuestCmdData` and adds `RestApiClient` (a `zeropsRestApiClient.Handler` pointed at the configured region/host), plus optional resolved `Project`/`Service` entities and a `ProjectSelector`.

If only `GuestRunFunc` is defined, it runs for logged-in users too. If only `LoggedUserRunFunc` is defined and no token is set, the command exits with `UnauthenticatedUser`. Implementation: `src/cmdBuilder/createRunFunc.go`.

### Scope levels (`src/cmdBuilder/scopeProject.go`, `scopeService.go`)

Commands that operate on a project or service set `.ScopeLevel(cmdBuilder.ScopeProject(...))` or `.ScopeLevel(cmdBuilder.ScopeService(...))`. The scope level adds the relevant flags/args and, before the run function executes, resolves the target by ID/name into `LoggedUserCmdData.Project` / `.Service` — possibly creating a new project/service interactively when `WithCreateNewProject()` / `WithCreateNewService()` are passed (see `serviceDeploy.go` for an example). Persisted "scope" (saved project) is read from `CliStorage.Data().ScopeProjectId`.

### Flag normalization

`ExecuteRootCmd` sets a Cobra normalization function that converts camelCase flag names to kebab-case (`src/cmdBuilder/executeRootCmd.go`). Flags can be defined as camelCase in code and used as kebab-case on the CLI.

### Persistent state and config

- `cliStorage` (`src/cliStorage`) wraps `storage.New[Data]` (`src/storage`) — a JSON-backed file used to persist the login token, region selection, saved project scope, and VPN key registry. Path comes from `constants.CliDataFilePath()`.
- `constants` exposes platform-specific paths, env var names (e.g. `CliTokenEnvVar`), and the default region (used when no region is configured).
- Log file path comes from `constants.LogFilePath()`; loggers (`src/logger`) split human output from a debug file logger that captures `LogDebug`/error traces.

### UX layer (`src/uxBlock`, `src/uxHelpers`, `src/printer`, `src/terminal`)

All terminal output and interactive prompts go through `uxBlock.Blocks` (constructed in `executeRootCmd.go`). It owns the loggers, terminal size, an `isTerminal` flag, and a cancel function wired to SIGINT/SIGTERM. `uxBlock/models` provides Bubble Tea TUI models (selectors, spinners, tables); `uxHelpers` wraps common flows like `ProjectSelector`. `printer` is the lower-level line printer used directly by commands (`cmdData.Stdout.PrintLines(...)`). `styles` centralizes lipgloss styles, including the custom Cobra help template defined in `src/cmd/root.go`.

### API client and entities

- `src/zeropsRestApiClient` constructs an authorized client around `github.com/zeropsio/zerops-go`. The host comes from the user's saved `RegionData.Address` or `constants.DefaultRegion`, prefixed with `https://`.
- `src/entity` defines domain types (`Project`, `Service`, `AppVersion`, `Container`, `Process`, `Location`, `Org`, `VpnKey`). `src/entity/repository` contains query helpers (`GetProjectById`, etc.) layered on the REST client.
- Errors from the API are unwrapped in `executeRootCmd.printError`: user-facing `errorsx.UserError`, structured `apiError.Error` (printed with YAML-formatted metadata), and Ctrl-C cancellations are each handled distinctly.

### Other notable packages

- `src/archiveClient` — packs source for `service deploy`/`service push`, honoring `.gitignore` and a `deploy-git-folder` flag.
- `src/yamlReader` — reads `zerops.yaml` (or a path passed via `--zerops-yaml-path`).
- `src/wg` + `src/nettools` — WireGuard interface management for `zcli vpn` (`InterfaceExists`, key handling, etc.); requires the `wireguard` and `ping` binaries on the host.
- `src/serviceLogs` — streaming/log retrieval used by `zcli service log`.
- `src/i18n` — message catalog (`en.go`); all user-visible strings use `i18n.T(i18n.<Key>, args...)`. Add new strings to `en.go` rather than inlining literals.
- `src/flagParams` — typed param reader (`Params.GetBool/GetString/...`) bound from Cobra flags inside the run func.
- `src/optional` — `Null[T]` type used by `LoggedUserCmdData.Project`/`Service`.
- `src/gn` — small generic helpers (`TransformSlice`, etc.).

## Adding a new command

1. Create `src/cmd/<name>.go` exporting a `*cmdBuilder.Cmd` constructor.
2. Wire flags/args, choose `ScopeLevel` if it targets a project/service, and provide `LoggedUserRunFunc` (and/or `GuestRunFunc`).
3. Register it in `rootCmd()` in `src/cmd/root.go` (or as a child of an existing parent like `serviceCmd`, `projectCmd`, `vpnCmd`).
4. Add any new user-facing strings to `src/i18n/en.go` and reference them via `i18n.T(...)`.

## Distribution

Released via GitHub Actions (`.github/workflows/`) and republished to npm as `@zerops/zcli` (`tools/npm`). Install scripts: `install.sh` (Linux/macOS), `install.ps1` (Windows). Nix flake (`flake.nix`, `default.nix`) supports `nix develop`/`nix build`.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
define helpMessage
possible targets:
- test
- test-integration
- lint
- all
- windows-amd
Expand All @@ -19,6 +20,9 @@ help:
test:
go test -v ./cmd/... ./src/...

test-integration:
go test -v -tags devel ./src/cmd/...

lint:
GOOS=darwin GOARCH=arm64 gomodrun golangci-lint run ./cmd/... ./src/... --verbose
GOOS=linux GOARCH=amd64 gomodrun golangci-lint run ./cmd/... ./src/... --verbose
Expand Down
17 changes: 10 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
module github.com/zeropsio/zcli

go 1.24
go 1.24.0

require github.com/zeropsio/zerops-go v1.0.18

require (
github.com/charmbracelet/bubbles v0.20.0
github.com/charmbracelet/bubbletea v1.3.4
github.com/charmbracelet/bubbletea v1.3.5
github.com/charmbracelet/lipgloss v1.1.0
github.com/golang/mock v1.6.0
github.com/google/uuid v1.6.0
Expand All @@ -21,17 +21,20 @@ require (
github.com/stretchr/testify v1.10.0
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8
golang.org/x/term v0.28.0
golang.org/x/text v0.21.0
golang.org/x/text v0.28.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/aymanbagabas/go-udiff v0.3.1 // indirect
github.com/charmbracelet/colorprofile v0.3.2 // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b // indirect
github.com/charmbracelet/x/exp/teatest v0.0.0-20260511125431-fe5d686e0c99 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
Expand All @@ -56,7 +59,7 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
16 changes: 16 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,30 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc=
github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/teatest v0.0.0-20260511125431-fe5d686e0c99 h1:E+VCvJLc5yu3YSYj9fKT3x0K1g7xLJtvYyU/hMYF63w=
github.com/charmbracelet/x/exp/teatest v0.0.0-20260511125431-fe5d686e0c99/go.mod h1:aPVjFrBwbJgj5Qz1F0IXsnbcOVJcMKgu1ySUfTAxh7k=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
Expand Down Expand Up @@ -125,6 +135,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand All @@ -135,13 +147,17 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
Expand Down
Loading
Loading