Files
invoiceninja-cli/CLAUDE.md
T
awkawbandClaude Sonnet 5 dde10e522a Initial commit: clients resource scaffold over invoiceninja-sdk
A command-line client for the Invoice Ninja v5 API, built on the
invoiceninja-sdk crate (consumed from crates.io, 0.2.0). Only the
clients resource is wired up so far, as the template the rest follow
- see CLAUDE.md and GOALS.md for the reasoning behind the shape of
things (the update fetch-merge-write pattern, why there's no Cargo
workspace with the SDK, the local-checkout override for
co-development, and the Nix flake packaging via packages.default and
overlays.default).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-26 11:47:15 -04:00

157 lines
7.6 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
A command-line client for the [Invoice Ninja](https://www.invoiceninja.com/)
v5 API, built on [`invoiceninja-sdk`](https://crates.io/crates/invoiceninja-sdk),
consumed as a normal crates.io dependency (not a path dependency on a sibling
checkout — see "Development environment" below for how that changed).
That crate's own `GOALS.md` (at `../invoiceninjars-sdk/GOALS.md`, if you have
a checkout for reference) explains why it's a separate crate rather than a
feature of the library — dependencies, MSRV, and versioning, all pre-1.0.
Read that `GOALS.md` before making a design call here; this project exists
downstream of decisions made there.
**This project's own [`GOALS.md`](GOALS.md) states what it is for, what it
deliberately is not, and what is still undecided** — read it before making a
design call, the same way its counterpart in the SDK works. If a change
would contradict a stated goal or settle an open question, update
`GOALS.md` in the same change.
**Early scaffold.** Only the `clients` resource is wired up, as the template
for the other ~34. See "Adding a resource" below before writing the next one.
## Development environment
Nix-managed, with its own `flake.nix` (not shared with the SDK's):
```sh
nix develop
```
The dev toolchain was originally pinned to match `invoiceninjars-sdk`'s own
(`devVersion` in each `flake.nix`), from back when this crate needed a sibling
checkout to build at all. That's no longer a hard requirement now that the
SDK is on crates.io, but the pin hasn't been revisited — if the two drift,
nothing here forces them back in sync, since the two flakes are independent
files.
**As of 0.2.0 (2026-10-25), the SDK is published on crates.io** and this
crate depends on it as a normal version requirement
(`invoiceninja-sdk = "0.2.0"` in `Cargo.toml`), not a path dependency. That
means:
- No `../invoiceninjars-sdk` checkout is needed to build this crate anymore.
`cargo build` fetches `invoiceninja-sdk` from the registry like any other
dependency.
- Picking up a change made in the SDK's working copy now requires that
change to be published as a new crates.io release first by default — there
is no more "edit the sibling checkout, rebuild here" loop out of the box.
For active co-development across both crates, copy
`.cargo/config.toml.example` to `.cargo/config.toml` (gitignored, so it
stays per-developer and never affects CI or anyone else's build) to get
a `paths` override back onto `../invoiceninjars-sdk`. It requires that
checkout's `[package] version` to match this crate's `invoiceninja-sdk`
requirement in `Cargo.toml` — bump both together if the SDK's working
copy moves to an unreleased version. Delete `.cargo/config.toml` to go
back to the published release.
- Pre-1.0, a y-bump (`0.2.x` → `0.3.0`) in the SDK is a breaking release per
that crate's own `GOALS.md`. The default caret requirement already blocks
picking that up automatically; bumping the version in `Cargo.toml` is a
deliberate, reviewed step, not something `cargo update` should do on its
own.
**Watch the SDK's own package/crate name.** It has changed at least once
already (`invoiceninjars` → `invoiceninja-sdk`, crate path `invoiceninja_sdk`)
without a corresponding change here landing first. If a build fails with "no
matching package named `invoiceninja-sdk` found" or an unresolved
`invoiceninja_sdk::` path, check the crates.io listing (or
`../invoiceninjars-sdk/Cargo.toml`'s `[package] name`, if you have a
checkout) first.
## Common commands
```sh
cargo build
cargo run -- clients list # needs IN_API_TOKEN, or --token
cargo fmt --check
cargo clippy --all-targets
```
No test suite yet — nothing here has behavior worth testing beyond what
`clap`'s derive and the SDK's own test suite already cover. Add one (clap's
`Cli::try_parse_from` is the usual way to test argument parsing without
spawning a process) when a command does more than translate flags into an
SDK call.
## Architecture
- `src/cli.rs` — all `clap` derive types. `Cli` holds the global
options (`--base-url`, `--token`, `--api-password`, `--output`) and the
top-level `Command` subcommand enum. **`--token` can't be clap-`required`**
because clap rejects `required` combined with `global` (global is what
lets `--token` appear after the resource subcommand too, not just before
it) — the missing case is caught in `config::build_client` instead, with a
message naming both the flag and the env var. Don't try to make it
`required` again; the comment on the field explains the panic you'd get.
- `src/config.rs` — `build_client` turns a parsed `Cli` into an
`InvoiceNinjaClient`, including the manual token check above.
- `src/body.rs` — how `create`/`update` get their JSON body (`--data`
inline, `--file` from disk or `-` for stdin, or stdin by default), plus
`merge_shallow`, a one-level-deep object overlay.
- `src/output.rs` — `print_json`, shared by every resource's `--output
json`. Table rendering is **not** here; it's per-resource (see below),
since there's no generic notion of which columns matter.
- `src/commands/clients.rs` — the one resource implemented so far, and the
template for the rest.
### The `update` fetch-merge-write pattern
`update` is not a thin wrapper over the SDK's `update_client`. It:
1. Fetches the current record with `get_client`.
2. Serializes it to `serde_json::Value` and shallow-merges the caller's
`--data`/`--file` JSON on top (`body::merge_shallow`).
3. Deserializes the merged value back into the model and calls
`update_client`.
This exists because Invoice Ninja's `PUT` is a full replace — there's no
PATCH route — and every non-`Option` field on an SDK model decodes a missing
key as that type's default (`""`, `0`, `false`; see
`invoiceninja_sdk`'s `null_to_default`). Sending the caller's patch alone
would silently blank out every field they didn't mention. **Keep this
pattern for every resource's `update`** — it's the CLI-level version of the
SDK's own "never lose data in a round trip" goal, not a one-off for clients.
`create` has no such step: a partial JSON body decodes straight into the
model, since the same defaulting that makes a bare patch dangerous for
`update` is exactly what you want when creating a record whose fields you
haven't all thought about (the API does the equivalent server-side).
### Adding a resource
Each resource beyond `clients` should follow the same shape:
1. Add a variant to `Command` in `src/cli.rs` (e.g. `Invoices(ResourceArgs)`)
— `ResourceArgs` and `Action` are already generic across resources, reuse
them as-is.
2. Add `src/commands/<resource>.rs` mirroring `clients.rs`: a `run` dispatcher
over `Action`, `list`/`get`/`create`/`update`/`delete` functions calling
the matching `list_<resource>`/`get_<resource>`/… methods on
`InvoiceNinjaClient`, and a `print_table` choosing that resource's own key
columns.
3. Register the module in `src/commands/mod.rs` and the dispatch arm in
`main.rs`'s `match cli.command`.
4. Check `../invoiceninjars-sdk/src/client.rs` for whether the resource has a
`/bulk` route (most do; a few — `client_gateway_tokens`, `companies`,
`system_logs` — don't, per that crate's `crud_methods!` vs
`resource_methods!` split). This CLI doesn't expose bulk actions yet
either way; that's a separate addition.
Resources with entity actions, document uploads, or non-CRUD endpoints
(reports, charts, search, statics, PDF downloads, …) will need more than
this template once they're added — the SDK's own `CLAUDE.md` describes which
resources have which extras.