# Development Guide
How to build, test, and contribute to ztnet-cli.
## Prerequisites
- **Rust** 1.81+ (edition 2024)
- **Docker** and **Docker Compose** (for local ZTNet instance)
- **PowerShell** (for Windows scripts) or adapt the commands for your shell
## Building
```bash
# Debug build
cargo build
# Release build
cargo build --release
# Run directly
cargo run -- network list
```
The binary is produced at:
- Debug: `target/debug/ztnet` (or `ztnet.exe` on Windows)
- Release: `target/release/ztnet` (or `ztnet.exe` on Windows)
## Local ZTNet with Docker
The project includes a Docker Compose setup for running a full ZTNet stack locally via the `external/ztnet` git submodule.
### Start the stack
```powershell
# Initialize the submodule (first time)
git submodule update --init
# Start ZTNet + PostgreSQL + ZeroTier controller
powershell -File scripts/ztnet-local.ps1 up
```
ZTNet will be available at `http://localhost:3000`.
### Manage the stack
```powershell
# View running containers
powershell -File scripts/ztnet-local.ps1 ps
# View logs
powershell -File scripts/ztnet-local.ps1 logs
powershell -File scripts/ztnet-local.ps1 logs -Follow
# Stop
powershell -File scripts/ztnet-local.ps1 down
# Stop and remove volumes (full reset)
powershell -File scripts/ztnet-local.ps1 down -Volumes
```
### Bootstrap the first user
On a fresh database, create the first user without authentication:
```bash
target/debug/ztnet user create \
--email admin@example.com \
--password Password123 \
--name "Admin" \
--generate-api-token \
--store-token \
--no-auth
```
This creates the user, generates an API token, and saves it to your config so subsequent commands are authenticated.
## Smoke tests
The integration smoke test verifies the CLI against a running ZTNet instance.
### Running
```powershell
# Set credentials (optional - defaults to test@ztnet.local / TestPassword123!)
$env:ZTNET_SMOKE_EMAIL = "admin@example.com"
$env:ZTNET_SMOKE_PASSWORD = "Password123"
$env:ZTNET_SMOKE_NAME = "Admin"
# Run the smoke test
powershell -File scripts/smoke-test.ps1
```
### What it does
1. Waits for ZTNet to be ready (up to 3 minutes)
2. Bootstraps a user (if email/password provided)
3. Validates authentication with `auth test`
4. Creates a timestamped test network
5. Fetches network details
6. Exports hosts in JSON format
### Full local test cycle
```powershell
# 1. Start ZTNet
powershell -File scripts/ztnet-local.ps1 up
# 2. Build
cargo build
# 3. Run smoke tests
powershell -File scripts/smoke-test.ps1
# 4. Tear down
powershell -File scripts/ztnet-local.ps1 down -Volumes
```
## Project architecture
```
src/
├── main.rs Entry point (tokio async runtime)
├── cli.rs Clap parser, global options, Command enum
├── cli/ CLI definitions (pure argument parsing, no logic)
│ ├── auth.rs
│ ├── config_cmd.rs
│ ├── user.rs
│ ├── org.rs
│ ├── network.rs
│ ├── stats.rs
│ ├── planet.rs
│ ├── export.rs
│ ├── api.rs
│ ├── trpc.rs
│ └── completion.rs
├── app.rs Main dispatcher (routes commands to handlers)
├── app/ Business logic (one file per command group)
│ ├── auth.rs Token and profile management
│ ├── config_cmd.rs Config file operations
│ ├── user.rs User creation
│ ├── org.rs Organization operations
│ ├── network.rs Network CRUD
│ ├── member.rs Member CRUD
│ ├── stats.rs Statistics
│ ├── planet.rs Planet file download
│ ├── export.rs Hosts/CSV/JSON export
│ ├── api.rs Raw HTTP requests
│ ├── trpc.rs tRPC procedure calls
│ ├── common.rs Shared I/O and formatting utilities
│ └── resolve.rs Name-to-ID resolution
├── config.rs TOML config file loading/saving
├── context.rs Config precedence resolution
├── http.rs HTTP client (auth, retries, dry-run)
├── output.rs Output formatting (table, JSON, YAML, raw)
└── error.rs Error types and exit codes
```
### Key design decisions
**Separation of CLI and logic.** The `src/cli/` directory contains only Clap derive structs for argument parsing. The `src/app/` directory contains the actual business logic. This keeps the two concerns decoupled and easy to test independently.
**Config precedence.** Configuration is resolved through a clear chain: CLI flags override environment variables, which override the config file, which provides defaults. The `context.rs` module handles this merging.
**Scoping model.** The same commands work in both personal and organization scope. When `--org` is provided (via flag, env, or context default), API calls are routed to `/api/v1/org/{orgId}/...` instead of `/api/v1/...`.
**Name resolution.** Networks and organizations can be referenced by name. The `resolve.rs` module fetches the list, matches by name, and returns the ID. Ambiguous matches (multiple results) produce a clear error.
**HTTP resilience.** The HTTP client in `http.rs` handles retries with exponential backoff, rate limit detection via `Retry-After` headers, and dry-run mode. All API calls go through this single client.
## Dependencies
| `clap` + `clap_complete` | CLI argument parsing and shell completions |
| `tokio` | Async runtime |
| `reqwest` | HTTP client (with rustls-tls) |
| `serde` + `serde_json` + `serde_yaml` | Serialization |
| `toml` | Config file format |
| `comfy-table` | ASCII table rendering |
| `thiserror` | Error type derivation |
| `humantime` | Duration parsing (e.g., `30s`) |
| `url` | URL parsing and joining |
## Release automation
Releases are automated on GitHub:
- Every push to `master` bumps the patch version in `Cargo.toml` (and the root package version in `Cargo.lock`) and creates a `v<version>` tag via `.github/workflows/version-bump.yml`.
- The version bump workflow triggers the release pipeline via `workflow_dispatch` on the new `v<version>` tag (and `.github/workflows/release.yml` also supports manual tag pushes `v*`):
- runs `cargo test --locked`
- builds release binaries and publishes a GitHub Release
- publishes to crates.io when `CARGO_REGISTRY_TOKEN` is set
- Scoop bucket manifest updates are applied by the `scoop` job in `.github/workflows/release.yml` (updates `bucket/ztnet.json`).
- WinGet PR automation (optional) is applied by the `winget` job in `.github/workflows/release.yml` (requires `WINGET_TOKEN` and an existing base manifest in `microsoft/winget-pkgs`).