ztnet 0.1.5

ZTNet CLI — manage ZeroTier networks via ZTNet
# 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

| Crate | Purpose |
|-------|---------|
| `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`).