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>
7.2 KiB
Goals
What this crate is trying to be, what it is deliberately not, and what is
still open. Same purpose as invoiceninjars-sdk's own GOALS.md: several
choices here look like oversights until you know the reasoning, and that
reasoning should live here rather than only in commit messages.
What this is for
A command-line way to drive the Invoice Ninja v5 API through
invoiceninja-sdk, for the cases Rust code is the wrong tool for: a quick
"what does this endpoint actually return", a one-off fix from a terminal, or
a shell script that doesn't want to be a Rust program. The SDK's own
GOALS.md names this exact need — its examples/ cover ad-hoc probing
today, and this crate is the standalone version of that.
It is a client over the SDK, not a second implementation of anything the SDK already does. If a command needs logic beyond "turn flags and JSON into one SDK call", that is usually a sign the logic belongs in the SDK instead.
Goals
1. Every command is a thin translation, with one documented exception
list, get, create, and delete do nothing but turn CLI flags into a
ListParams/model/id and call the matching SDK method. update is the
one exception, and it stays an exception rather than a pattern to extend
casually: it fetches the record, overlays the caller's JSON on top, and
writes the merged result back, because Invoice Ninja's PUT is a full
replace and the SDK's models default an omitted field rather than leaving it
alone. See the comment in src/commands/clients.rs and the matching section
in CLAUDE.md. Any other deviation from "thin translation" should be
written down the same way.
2. --output json is always the full, lossless record
The table view is a convenience subset — whatever columns a resource's
print_table picks — and it is allowed to leave things out. --output json
is not: it is always the model's own Serialize output, including
extra, so it is a drop-in input to --data/--file for a later update
or create and a reliable thing to pipe into jq. This is the CLI-level
version of the SDK's "never lose data in a round trip" goal.
3. One pattern across resources
Adding a resource should be mechanical, not a new design each time: a
Command variant, a src/commands/<resource>.rs with the same
run/list/get/create/update/delete/print_table shape as
clients.rs, and a registration line in two places. CLAUDE.md's "Adding a
resource" section is the checklist; if a resource needs something the
template doesn't cover (bulk, uploads, entity actions, non-CRUD endpoints),
that is scope for a deliberate addition to the template, not a one-off.
4. Scriptable by default
Errors go to stderr and a failure exits non-zero; nothing is printed to
stdout on an error path that a script could mistake for a result. Secrets
(--token, --api-password) never appear in --help or in any echoed
default — see hide_env_values on each in src/cli.rs. The SDK already
keeps both out of its own Debug output; this crate's job is not to undo
that by printing them itself.
5. Inherit the SDK's safety defaults rather than re-deciding them
This crate does not expose its own retry/timeout policy or add a write path
the SDK doesn't already consider safe. If the SDK decides POST isn't
replayed on a 5xx because it might double-create an invoice, that reasoning
applies here unchanged — the CLI is not a place to relax it for convenience.
Non-goals
- No Cargo workspace with the SDK. Explained in
CLAUDE.md: the SDK is its own git repo with its own CI and crates.io release, mirrored read-only to GitHub. Now that the SDK is published (0.2.0, 2026-10-25), the dependency is a plain crates.io version requirement — there isn't even a path dependency linking the two build graphs anymore, let alone a workspace. This doesn't mean co-development across both crates is gone:.cargo/config.toml.exampledocuments a per-developerpathsoverride back onto a local../invoiceninjars-sdkcheckout, gitignored so it never becomes the default for anyone else or for CI. SeeCLAUDE.md. - No interactive mode or TUI. Every command should work non-interactively from a script; a prompt-driven mode would be a different tool.
- No local state, cache, or config file — yet. Credentials and the base URL come from flags or environment variables only, resolved fresh on every invocation. See the open question below on multi-account profiles before assuming this is permanent.
- No stability promise before 1.0. Same posture as the SDK: flags are a
compatibility surface too, and pre-1.0 they can change. Record breaking
changes somewhere legible (a
CHANGELOG.md, once there's enough history to need one) rather than letting them be discovered at the command line.
Open questions
These are genuinely undecided; deciding one is a change to this file.
- Bulk actions. The SDK exposes
bulk_<resource>(archive/restore/delete plus resource-specific actions) on every resource with a/bulkroute. Nothing here calls it yet. Worth having, but a bulkdeletefrom a CLI invites a worse mistake than a one-recorddelete— whether that needs a confirmation prompt, a--yesflag, or a dry-run preview is unresolved. --dry-runforcreate/update/delete. Would print what the merged body or the target of a delete would be without sending the request. Cheap forcreate/delete; forupdateit would mean exposing the fetch-merge result for inspection before the write half runs.- Multi-account profiles. Flags and env vars cover one account per invocation. A config file keyed by profile name (base URL + token) would help anyone working against more than one instance regularly, at the cost of being the first place this crate stores anything on disk.
- How closely should list flags track
ListParams? Right nowclients listexposes exactly whatListParamshas typed setters for, plus--param key=valuefor the rest. A per-resource convenience filter (e.g. a--unpaidshorthand forclient_status=unpaid) would read nicer but means hand-picking which filters earn a dedicated flag per resource. - Whether this crate gets its own CI. No longer blocked on needing a
sibling checkout — the SDK dependency switched from a path to a crates.io
version requirement (
invoiceninja-sdk = "0.2.0") once 0.2.0 was published on 2026-10-25. A workflow here can now justcargo buildagainst the registry like any other Rust project; it doesn't need to check outinvoiceninjars-sdkat all. - Whether the Nix packaging goes further than this repo's own flake.
flake.nixnow haspackages.default/devShells.default(built with a pinnedrust-overlaytoolchain, for parity betweennix buildandnix develop) andoverlays.default(for another flake to pullinvoiceninja-cliinto its ownpkgs, built with that nixpkgs's ownrustPlatforminstead of the pin). The overlay path means this crate'srust-version = "1.88"has to be met by whatever rustc the consuming channel ships — there's no guard here if it isn't, just a build failure over there. Whether that's worth submitting to nixpkgs proper (and how an MSRV bump would then need to be communicated beyond this repo) is unresolved.