Now that the SDK dependency resolves from crates.io instead of a sibling checkout, a workflow here needs nothing special to build. Same three jobs as the SDK's own ci.yml: pinned-toolchain test, a build-only msrv job on the declared 1.88 floor, and an advisory stable job that reports red but stays out of required status checks. Verified locally: all four `test`-job commands pass with the pinned 1.99.1 toolchain, and a build+test against an actual rustc 1.88.0 toolchain confirms the msrv job will be green too. Resolves the "own CI" open question in GOALS.md into goal 6; adds the matching "Rust version and CI" section to CLAUDE.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
7.4 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.
6. CI mirrors the SDK's, job for job
.github/workflows/ci.yml has the same three jobs as invoiceninjars-sdk's
own — pinned-toolchain test, build-only msrv on the declared floor, and
an advisory stable that reports red but stays out of required status
checks — rather than inventing a different shape for this crate. See
CLAUDE.md's "Rust version and CI" for the per-job reasoning; a deviation
from this shape should be written down there the same way update's
exception is written down in goal 1.
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 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.