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

7.6 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.

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):

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

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.