invoice-cli 0.5.13

Beautiful invoices from the CLI — international, stateful, agent-friendly
Documentation
use crate::cli::SkillCmd;
use crate::error::Result;
use crate::output::{print_success, Ctx};

const SKILL_MD: &str = r#"---
name: invoice-cli
description: >
  Generate beautiful, internationally-compliant invoices (PDF) from the CLI.
  Stateful (SQLite) — supports multiple issuer companies, clients, products,
  tax profiles (SG GST / UK VAT / US / EU / custom), multiple Typst templates.
  Use when the user asks to create, list, render, mark paid, or manage
  invoices, or to manage clients / products / invoicing entities.
---

## invoice-cli

`invoice` is a stateful CLI for generating, tracking, and rendering invoices.

### Quick start

```
invoice issuer add acme --name "Acme Studio" --jurisdiction sg --tax-registered --tax-id "GST M2-..." --address "1 Marina Bay\nSingapore" --bank-line "Bank: DBS" --bank-line "Account: 1234567890"
invoice clients add meridian --name "Meridian & Co." --country US --address "..." \
    --default-issuer acme --default-template boutique
invoice products add design --description "Design engagement" --unit project --price 8400 --currency SGD --tax-rate 9
invoice invoices new --client meridian --item design --due 30d   # no --as needed: uses client default
invoice invoices render acme-2026-0001 --open                     # uses client.default_template
invoice invoices mark acme-2026-0001 paid                         # auto-stamps paid_at
invoice invoices duplicate acme-2026-0001                         # clone for next month's billing
```

### Editing existing records

```
invoice issuer edit acme --phone "+65 ..." --bank-line "Bank: DBS" --bank-line "Account: 1234567890"
invoice clients edit meridian --default-template tiefletter-gold
invoice products edit design --price 9200
invoice issuer set-template acme boutique    # shorthand for --template
invoice clients set-issuer meridian acme     # shorthand
```

### Logo

Attach a logo image (PNG/SVG/JPG) to an issuer; the template renders it in the header.

```
invoice issuer edit acme --logo ~/Pictures/acme.png
```

### Editing drafts & credit notes

DRAFT invoices are mutable; once `issued`/`paid`/`void` they're immutable — use a credit note.

```
invoice invoices edit acme-2026-0001 --notes "Net 14 — early-payment 2% discount"
invoice invoices items add acme-2026-0001 "Extra fee:1:500"
invoice invoices credit-note acme-2026-0001 --item "Refund:1:500" --notes "Goodwill credit"
```

### Aging & export

```
invoice invoices aging                                                        # 0-30/31-60/61-90/90+ buckets
invoice invoices export --from 2026-01-01 --to 2026-03-31 --format csv --out q1.csv
```

### Discounts

Apply a discount at invoice level (percent OR fixed amount, mutually exclusive).

```
invoice invoices new --client meridian --item design --discount-rate 10
```

### Tips

- Run `invoice agent-info` for the full JSON capability manifest.
- Run `invoice doctor --json` to verify typst, DB, default issuer, and multi-company numbering.
- Item spec supports `product-slug[:qty]` OR `description:qty:price[:rate]`.
- Template resolution at render: `--template` flag > client.default_template > issuer.default_template > `vienna`.
- `--as` picks the issuer; omit it when the client has `default_issuer` pinned or `config.default_issuer` is set.
- New issuers default to `{issuer}-{year}-{seq:04}` so invoice numbers are globally addressable; use `issuer edit --number-format` for per-company custom prefixes.
- Use invoice numbers returned by JSON responses instead of predicting the next sequence.
- `mark issued` / `mark paid` auto-stamp `issued_at` / `paid_at` (first transition only).
- `invoices list` shows totals per invoice (computed with `rust_decimal`).
- Every tax value is computed with `rust_decimal` — no float rounding.
"#;

pub fn run(_cmd: SkillCmd, ctx: Ctx) -> Result<()> {
    let targets = [
        dirs_path(".claude/skills/invoice-cli/SKILL.md"),
        dirs_path(".codex/skills/invoice-cli/SKILL.md"),
        dirs_path(".gemini/skills/invoice-cli/SKILL.md"),
    ];
    let mut written = Vec::new();
    for t in targets {
        if let Some(parent) = t.parent() {
            std::fs::create_dir_all(parent)?;
        }
        std::fs::write(&t, SKILL_MD)?;
        written.push(t.display().to_string());
    }

    print_success(ctx, &written, |paths| {
        for p in paths {
            println!("installed → {}", p);
        }
    });
    Ok(())
}

fn dirs_path(rel: &str) -> std::path::PathBuf {
    let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
    std::path::PathBuf::from(home).join(rel)
}