# cz: the remote client `cz` is a command-line client for a Cloudzilla host. It is a plain HTTP client over `/api/**`: it never opens the database or reads `config.yaml`, so it runs on any machine that can reach the server. `cz-admin` is the separate operator tool that runs on the server host (see [configuration](./configuration.md)). ## Install Each release attaches a standalone `cz___.tar.gz` (with a `.sha256`) for every platform the server release covers, next to the `cloudzilla___.tar.gz` server archive. The `cz` archive holds one binary; unpack it onto your `PATH`. From a checkout, `make build` writes `dist/cz`, or `make build-cz` builds only it. `cz` is cgo-free and needs no CSS or templ step. The server archive and Docker image don't include `cz`. ## Logging in ```bash cz auth login --host https://git.example.com # approve in the browser cz auth login --host https://git.example.com --scope repo:read # ask for less echo "$TOKEN" | cz auth login --host https://git.example.com --with-token cz auth status cz auth logout ``` `login` runs the device flow (RFC 8628) by default. It prints a one-time code and the URL `/login/device`, you sign in there in a browser (2FA works) and type the code, and `cz` stores the personal access token the server then issues. Like the `--with-token` path, it verifies the token with one call to `GET /api/user` before saving it, and prints the host, user and where the token went. What `cz` shows and does: - The code (`XXXX-XXXX`) and the URL go to stderr, always. The code is never part of a URL: you read it from your terminal and type it, so a link someone sends you can't skip that check. - The code is copied to the clipboard where a tool exists (`pbcopy`, `wl-copy`, `xclip`, `xsel`, `clip.exe`), silently otherwise. - The plain URL is opened in your browser only when stdin and stdout are both terminals and `--no-browser` isn't set. - Nothing is read from stdin, so it works headless and over SSH: open the URL on any machine, sign in, and type the code. `cz` polls until you approve, deny or the code expires (15 minutes), or you press Ctrl-C. Nothing is stored unless the login succeeds. The token is a normal personal access token named `cz () ยท `, with no expiry. It appears under Settings, Access tokens, where you can revoke it. It asks for `repo:write` by default (the scope that admits merging, creating and forking repositories and pushing); the approval page lets you untick scopes but never add any. `repo:admin` can't be requested this way: create that token under Settings and use `--with-token`. An older server without the endpoint answers `404`, and `cz` points you to `--with-token`. In that case, create a personal access token under your account settings with the scopes from [the table below](#scopes-per-command) and pipe it in. | Flag | Effect | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `--host URL` | The Cloudzilla URL; `CZ_HOST` is used when omitted | | `--scope S` | Scope to request: `repo:read`, `repo:write`, `issues:write`, `pulls:write`. Repeatable or space-separated; default `repo:write` | | `--no-browser` | Print the URL and code without opening a browser | | `--with-token` | Skip the browser: read a personal access token from stdin | | `--insecure-storage` | Store the token in a `0600` file instead of the OS keychain | `cz auth status` fails when nobody is logged in. ### Environment variables | Variable | Effect | | ---------- | ---------------------------------------------- | | `CZ_TOKEN` | Token to use instead of the stored one, for CI | | `CZ_HOST` | Host to use instead of the stored one | With both set, nothing is read from storage. With `CZ_HOST` alone, `cz` refuses to send the stored token to a different host than the one it was saved for and asks you to set `CZ_TOKEN` too. `CZ_TOKEN` alone needs a stored login to supply the host. ### Where the token lives The token goes to the OS keychain. When none is available, or with `--insecure-storage`, it goes to `credentials.json` in a `cz` directory under `os.UserConfigDir()` (`~/.config/cz/credentials.json` on Linux, `~/Library/Application Support/cz/credentials.json` on macOS), mode `0600`. `cz auth status` names the one in use. ## Output On a terminal, commands print human-readable text. `--json`, or a stdout that isn't a terminal, prints JSON instead. Errors are one line on stderr prefixed `cz:`, with a non-zero exit: an unreachable host, a bad token (`401`, run `cz auth login`) and a missing scope each get their own message. A `429` reports the `Retry-After` wait. A bad token is not an error on a public read. `repo list`, `issue list`, `pr list` and the like on a public repository succeed with public data, because the server treats a token it can't verify as anonymous on those routes. If a listing shows only public repositories, run `cz auth status`: it verifies the token and fails when it is bad. ## Scopes per command A token acts as its user but only on the routes its scopes admit ([Token Scopes](./access-control.md#token-scopes)). When the server answers `403 insufficient_scope`, `cz` prints the scope from the `WWW-Authenticate` header. | Command | Scope needed | | ------------------------------------- | ------------------------------------------------------------ | | `cz auth login`, `status` | any scope (calls `GET /api/user`) | | `cz auth logout` | none; removes the local login only | | `cz repo list`, `view` | `repo:read` | | `cz repo create`, `fork` | `repo:write` | | `cz repo clone` | `repo:read` (git fetch) | | `cz issue list`, `view` | `repo:read` | | `cz issue create`, `comment`, `close` | `issues:write` (or `repo:write`) | | `cz pr list`, `view` | `repo:read` | | `cz pr create`, `close`, `review` | `pulls:write` (or `repo:write`) | | `cz pr merge` | `repo:write`. `pulls:write` can't merge or enable auto-merge | | `cz api ` | whatever the request needs | ## Repositories, issues and pull requests Commands that take a repository read it from `-R owner/repo`, or from the `origin` remote of the current checkout. A remote on another host is refused with a message saying so. HTTPS, `ssh://` and `git@host:owner/repo` remotes work. Body text comes from `--body`, `--body-file` (`-` reads stdin), or `$VISUAL`/`$EDITOR` when run on a terminal with neither. ```bash cz repo list [--owner alice] cz repo view alice/site cz repo create site [--org acme] [--private] [-d text] [--readme] [--gitignore Go] [--license mit] cz repo fork alice/site cz repo clone alice/site [directory] [-- git-clone-flags] cz issue list [-R alice/site] [--state open|closed] cz issue view 12 cz issue create -t "Broken link" [-b text] [-l bug] [-a bob] cz issue comment 12 -b text cz issue close 12 cz pr list [--state open|closed|merged|all] cz pr view 7 cz pr create -t "Fix links" [-H branch] [-B main] [-b text] [--draft] cz pr merge 7 [--ff | --merge | --squash] cz pr close 7 cz pr review 7 --approve | --request-changes -b text | --comment -b text ``` - `repo list --owner` filters on the client, because the API has no owner filter. - `issue list --state` and `pr list --state` filter on the client. The server returns every state, and at most 500 issues, with no paging. - `issue create` looks up each `--label` by name before creating anything, so an unknown label fails with no issue created. If a label or assignee step fails after the issue exists, the error gives its number and URL. - `pr create` defaults `--head` to the current branch and `--base` to the repository's default branch. It checks that the head branch is on the remote and refuses if it isn't; the server would accept any head name. - `pr merge` needs `repo:write`. A `pulls:write` token is told so. Conflicts, requested changes, branch protection and required checks come back as the server's reason with a non-zero exit. - `pr close` refuses a pull request that is no longer open. - `pr review --request-changes` and `--comment` need a body. ### How `repo clone` and `pr create` use the token Both run the system `git`. The token is handed to a URL-scoped credential helper through the `CZ_GIT_TOKEN` environment variable of the git child process, and `GIT_TERMINAL_PROMPT=0` is set. It never appears in the URL, the arguments, `.git/config` or shell history. Other processes of the same user can still read a running process's environment (for example with `ps eww` on some systems), so don't run `cz` on a machine where you don't trust those users. ## cz api ```bash cz api GET /api/repos/alice/site/issues -f state=open cz api POST /api/repos/alice/site/issues -f title="Broken link" -f priority=2 cz api POST /api/repos/alice/site/issues --input issue.json ``` - `-f key=value` (repeatable) goes in the query string for `GET` and `HEAD`, and otherwise in a JSON body. `true`, `false` and integers are sent as that JSON type. - `--form` sends the fields as `application/x-www-form-urlencoded` instead of JSON. - `--input FILE` sends a file as the JSON body (`-` reads stdin). It can't be combined with `-f`. - The path must start with `/` and is sent to the logged-in host. ## Safety behaviour - Redirects are never followed. A bad token on a page route redirects to the HTML login, and the `Authorization` header must not travel further; `cz` reports the redirect instead. - The token is sent only to the host it was stored for (see the host-mismatch refusal above). ## What cz can't do - Admin routes (`/api/admin/*`), account routes (`/api/user/*` such as SSH keys, tokens and 2FA), notifications, gists and OAuth apps are closed to tokens whatever their scopes. - `repo:admin` tokens (collaborators, deploy keys, webhooks, transfer, delete) need every request signed with an SSH key. `cz` doesn't sign requests. - Repository imports aren't available over token auth. - Instance operation (migrations, backup, password resets) belongs to `cz-admin`, which needs the server's config and database.