Metadata-Version: 2.4
Name: devnomads-cli
Version: 0.11.1
Summary: Manage your DevNomads services from the command line
Author-email: DevNomads <support@devnomads.nl>
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: typer>=0.19
Requires-Dist: httpx>=0.27
Requires-Dist: rich>=13
Requires-Dist: cryptography>=42
Requires-Dist: devnomads[acme]>=0.6.0
Dynamic: license-file

# dncli

Manage your [DevNomads](https://devnomads.nl) services from the
command line: inspect your services, manage DNS zones and records,
and move zones over from other providers - from a shell or a script.

The command is **`dncli`** - named to avoid clashing with the macOS
`dnctl` traffic-shaper at `/usr/sbin/dnctl`. Install it from PyPI
as **`devnomads-cli`**.

## Install

Install dncli with [uv](https://docs.astral.sh/uv/). It gives dncli
its own isolated environment and puts the `dncli` command on your
PATH:

```sh
uv tool install devnomads-cli
```

No uv yet? Install it first:

```sh
curl -LsSf https://astral.sh/uv/install.sh | sh                               # macOS, Linux
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"  # Windows
```

pipx (`pipx install devnomads-cli`) and pip (`pip install
devnomads-cli`) work as well.

### Updates

Update with:

```sh
uv tool upgrade devnomads-cli
```

dncli checks PyPI at most once a day and tells you when a new version
is out, with the command to update the way you installed it. It only
does so on an interactive terminal, never in pipes, scripts or CI; set
`DN_NO_UPDATE_CHECK=1` to turn it off.

dncli also warns when the DevNomads API reports a major version it was
not built for, and tells you when the API no longer supports your
dncli version. Update dncli in both cases.

### Shell completion

Enable shell completion (bash, zsh, fish, PowerShell):

```sh
dncli --install-completion
```

## Getting started

Create an API key in the [DevNomads
portal](https://portal.devnomads.nl) under **Profiel -> API
Sleutels**, then store it:

```sh
dncli configure
```

The key is written to `~/.config/dncli/credentials`, readable only
by you. From here every command works:

```sh
dncli services list
dncli domains list
dncli containers show <id>
```

Every DevNomads product has its own command group - `apps`,
`buckets`, `containers`, `databases`, `domains`, `emails`,
`forwards`, `proxies`, `servers`, `sites` and more, plus `keys` for
your SSH keys and `jobs` for background work. Explore with
`dncli --help` and `dncli <group> --help`.

Commands accept any unambiguous prefix: `dncli e l` is `dncli emails
list`. An ambiguous prefix lists the candidates.

## Managing DNS

```sh
dncli dns zones list
dncli dns zones show example.com
dncli dns records list example.com
dncli dns records set example.com www A 192.0.2.1 --ttl 3600
dncli dns records delete example.com www A
```

Anywhere a zone is expected you may pass the service id of a domain
instead of its name, so the ids from `dncli domains list` work here
too:

```sh
dncli dns records list 42
```

An id always means that domain's own zone. A delegated subzone such
as `foo.example.com` has no domain service of its own, so address it
by name.

## Moving a zone from another provider

`dncli dns transfer` copies a zone from your current DNS provider
into DevNomads. It reads the records at the source (never writes
there), shows you the changes, and applies them after confirmation:

```sh
dncli configure --provider transip       # store source credentials
dncli dns transfer --from transip --zone example.com --dry-run
dncli dns transfer --from transip --zone example.com
```

Supported source providers: TransIP and AuroraDNS. The zone must
already exist at DevNomads; register or transfer the domain in the
portal first.

## Multiple accounts

Store each account as a named profile and select it per command or
per shell:

```sh
dncli configure --profile acme
dncli services list --profile acme
export DN_PROFILE=acme
```

Manage stored profiles:

```sh
dncli configure list                 # profiles, with keys masked
dncli configure set-key acme         # replace a profile's API key
dncli configure delete acme          # remove a profile
```

`set-key --stdin` reads the new key from standard input, and
`delete --yes` skips the question, for use in scripts.

## Scripting

Output is a human-readable table on a terminal and JSON when piped,
so pipelines get parseable output without any flags; `--out
table|json|csv|tsv` forces a specific format. Data goes to stdout,
everything else (warnings, prompts, status messages) to stderr, so
`dncli ... | jq .` is always safe.

The `csv` and `tsv` formats use the same columns as the table view, so
`dncli servers list --out csv` is ready for a spreadsheet.

List tables show a focused set of columns: fields that are empty for
every row, and nested collections (a server's IPs, a container's
instances, a mailbox list) are left out of the overview. Use `<group>
show <id>` or `--out json` for the complete record.

To pick exactly the columns you want, pass `--columns` (`-c`) a
comma-separated list, e.g. `dncli services list -c entity,started_at`.
It applies to every format, projecting json and csv/tsv to just those
fields, and reports an error on an unknown column name.

Commands that take a service id also accept a unique entity name and
resolve it for you, so `dncli domains show example.com` works as well
as `dncli domains show 1234`. Names match regardless of case. A purely
numeric value is always treated as an id; an entity that matches more
than one service is rejected, so pass the id when an entity is not
unique. Other references, such as cluster, key or user ids, take the
numeric id only.

Some changes run in the background: creating a site, server, proxy or
domain, for example, queues one or more jobs in a batch. Such a command
prints the queued jobs and returns right away; add `--wait` to follow
the batch until every job has finished and see how each one ended.
With `--wait` the exit status is 0 only when every job completed. You
can look a batch up later with `dncli jobs batches show <batch_id>`.

The exit status is 0 when the API accepted the request and 1 for any
error response, so `dncli ... && next-step` is safe in scripts. Every
command takes `--timeout <seconds>` (default 300) for how long to wait
for the API, including the time spent waiting on `--wait`.

In CI and pipelines, skip the credentials file and pass the key via
the environment:

```sh
export DN_API_KEY=...        # beats any stored profile
dncli services list | jq -r '.[].entity'
```

## AI agents

dncli ships with a skill that teaches AI coding agents how to use it
safely: where ids come from, what costs money or deletes data, and to
ask before changing anything. It works with Claude Code, Codex, Gemini
CLI, GitHub Copilot, Cursor and OpenCode:

```sh
dncli skill install                      # every agent found on this machine
dncli skill install --agent codex        # a specific agent; repeat for more
dncli skill install --project            # into the current project instead
```

Run it again after upgrading dncli to update the skill; `dncli skill
show` prints it.

## Certificates

`dncli` issues Let's Encrypt certificates using the DNS-01 challenge:

```sh
dncli certs issue example.com -d www.example.com --wildcard
```

The first argument is the primary domain (the certificate CN); add
extra names (SANs) by repeating `--san`/`-d`. `--wildcard`/`-w` is a
shortcut that adds `*.<domain>` as a SAN - a wildcard is never
accepted as the CN itself. Every name must live in
one of your DevNomads DNS zones, and that zone must be delegated to the
DevNomads nameservers (`one.dns.infrapod.nl`, `two.dns.infrapod.nl`,
`three.dns.infrapod.eu`) - otherwise issuance is refused, since the
DNS-01 challenge records would not be visible to the CA.

The certificate is always written to `~/.config/dncli/certs/<domain>/`
as `cert.pem`, `fullchain.pem`, `chain.pem`, and `privkey.pem` (0600).
Pass `--out <file>` to additionally export a single PEM bundle of
exactly three blocks - the key, the certificate, and the issuing
intermediate, in that order. Add `-v`/`--verbose` for detailed ACME
progress.

Keys are ECDSA P-384 by default. Pick another with `--key-type`
(`ecdsa256`, `ecdsa384`, `ecdsa521`, `rsa2048`, `rsa4096`).

Let's Encrypt is the certificate authority by default. Pass
`--provider zerossl` to use ZeroSSL instead. ZeroSSL requires External
Account Binding credentials (create an EAB key pair in the ZeroSSL
dashboard under Developer > EAB Credentials), supplied with `--eab-kid`
and `--eab-hmac-key` or the `DN_ZEROSSL_EAB_KID` and
`DN_ZEROSSL_EAB_HMAC_KEY` environment variables:

```sh
dncli certs issue example.com --provider zerossl \
  --eab-kid <kid> --eab-hmac-key <hmac>
```

`--staging` (the Let's Encrypt staging CA) applies only to the
letsencrypt provider. `certs renew` takes the same `--provider` and EAB
options.

Each CA gets its own ACME account: the key lives in
`~/.config/dncli/acme/` as `account.pem` for Let's Encrypt production
and `account-<provider>.pem` for ZeroSSL and the staging CA.

Re-running `certs issue` is a no-op while the existing certificate is
still valid for more than 21 days; pass `--force` to re-issue anyway.

List what you have issued and re-export any of them - as a PEM bundle
or a PKCS#12 (`.pfx`) file - without re-issuing. `certs list` includes a
`provider` column showing the issuing CA (recorded at issue time, or
inferred from the certificate's issuer for older certificates):

```sh
dncli certs list
dncli certs export example.com --out bundle.pem    # omit --out to print
dncli certs export example.com --format pfx --out bundle.pfx
dncli certs export example.com --format pfx --out bundle.pfx --passphrase secret
```

The `.pfx` is unencrypted unless you pass `--passphrase` (alias
`--password`). Both bundle formats carry the same three items: key,
certificate, and the issuing intermediate.

`dncli certs delete <domain>` removes a stored certificate and its
private key from disk (after confirmation; `--yes` skips it). By
default it only deletes the local files; add `--revoke` to also revoke
the certificate at the CA that issued it (the files are kept if
revocation fails).

A dehydrated-compatible DNS-01 hook ships as `dncli-dns-hook`:

```sh
# dehydrated/config
CHALLENGETYPE="dns-01"
HOOK="dncli-dns-hook"
```

## License

MIT.
