Files
invoiceninja-cli/CLAUDE.md
T
awkawbandClaude Sonnet 5 96fba9a303 Add the remaining 22 simple-CRUD resources
Finishes the mechanical rollout: every resource using
resource_methods!/crud_methods! in the SDK (list/get/create/update/
delete, no bulk/uploads/entity-actions) is now wired up, 33 in
total. recurring_quotes.rs mirrors recurring_invoices.rs the same
way quotes.rs mirrors invoices.rs, since RecurringQuote is also a
type alias for RecurringInvoice in the SDK.

client_gateway_tokens, companies, and system_logs use the SDK's
crud_methods! rather than resource_methods! (no /bulk route in
Invoice Ninja for these) - doesn't change anything here, since this
CLI doesn't expose bulk actions yet either way.

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

9.9 KiB

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 v5 API, built on 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 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.

Scaffold status. Every resource that fits the simple CRUD template (resource_methods!/crud_methods! in the SDK — list/get/create/update/ delete, no bulk/uploads/entity-actions) is wired up, 33 in total; clients was the template the rest followed. See "Adding a resource" below for what a resource needing more than the template (bulk actions, uploads, entity actions, non-CRUD endpoints) will need beyond this.

Development environment

Nix-managed, with its own flake.nix (not shared with the SDK's):

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.

Rust version and CI

Version Declared in
Dev toolchain 1.99.1 devVersion in flake.nix, test job in .github/workflows/ci.yml
MSRV 1.88 rust-version in Cargo.toml (inherited from the SDK's own floor — see the comment there), msrv job in ci.yml

Unlike invoiceninjars-sdk's flake.nix, this crate's has no separate msrvVersion/toolchain — the msrv CI job is the only place 1.88 actually gets built against, not a Nix devShell. If that ever earns its keep, mirror the SDK's msrvToolchain/shellFor pattern rather than inventing a new one.

CI runs on GitHub Actions, against the GitHub push mirror (Gitea is canonical — see .gitea/remote config — but Actions run on GitHub, the same arrangement as invoiceninjars-sdk's own CI). Three jobs, mirroring the SDK's ci.yml job-for-job:

  • test — the pinned dev toolchain; cargo build --all-targets, cargo test, cargo fmt --check, cargo clippy --all-targets -- -D warnings.
  • msrv — build and test only, on the declared 1.88 floor. No lint or format check: those vary by release, and pinning them to the oldest supported toolchain isn't useful.
  • stable (advisory) — latest stable rustc, runs on push/PR and weekly via schedule (Mondays 06:00 UTC) so a new Rust release that breaks the build or fires a new clippy lint doesn't sit unnoticed for weeks between pushes. Deliberately not continue-on-error — a real red X, not a silent annotation — so if branch protection ever requires status checks, keep stable out of the required list; that setting, not anything in ci.yml, is what should stop it blocking merges. Expect it red occasionally through no fault of the change under test, and fix it when bumping devVersion.

The test and msrv jobs skip scheduled runs (if: github.event_name != 'schedule') — a pinned toolchain against a locked dependency graph can't change on a timer, so the weekly run is for stable only.

Common commands

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 first resource implemented, and the template every other src/commands/<resource>.rs follows (33 of them now; see "Adding a resource" below).

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.