invoice_cli/commands/
skill.rs1use crate::cli::SkillCmd;
2use crate::error::Result;
3use crate::output::{print_success, Ctx};
4
5const SKILL_MD: &str = r#"---
6name: invoice-cli
7description: >
8 Generate beautiful, internationally-compliant invoices (PDF) from the CLI.
9 Stateful (SQLite) — supports multiple issuer companies, clients, products,
10 tax profiles (SG GST / UK VAT / US / EU / custom), multiple Typst templates.
11 Use when the user asks to create, list, render, mark paid, or manage
12 invoices, or to manage clients / products / invoicing entities.
13---
14
15## invoice-cli
16
17`invoice` is a stateful CLI for generating, tracking, and rendering invoices.
18
19### Quick start
20
21```
22invoice 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"
23invoice clients add meridian --name "Meridian & Co." --country US --address "..." \
24 --default-issuer acme --default-template boutique
25invoice products add design --description "Design engagement" --unit project --price 8400 --currency SGD --tax-rate 9
26invoice invoices new --client meridian --item design --due 30d # no --as needed: uses client default
27invoice invoices render acme-2026-0001 --open # uses client.default_template
28invoice invoices mark acme-2026-0001 paid # auto-stamps paid_at
29invoice invoices duplicate acme-2026-0001 # clone for next month's billing
30```
31
32### Editing existing records
33
34```
35invoice issuer edit acme --phone "+65 ..." --bank-line "Bank: DBS" --bank-line "Account: 1234567890"
36invoice clients edit meridian --default-template tiefletter-gold
37invoice products edit design --price 9200
38invoice issuer set-template acme boutique # shorthand for --template
39invoice clients set-issuer meridian acme # shorthand
40```
41
42### Logo
43
44Attach a logo image (PNG/SVG/JPG) to an issuer; the template renders it in the header.
45
46```
47invoice issuer edit acme --logo ~/Pictures/acme.png
48```
49
50### Editing drafts & credit notes
51
52DRAFT invoices are mutable; once `issued`/`paid`/`void` they're immutable — use a credit note.
53
54```
55invoice invoices edit acme-2026-0001 --notes "Net 14 — early-payment 2% discount"
56invoice invoices items add acme-2026-0001 "Extra fee:1:500"
57invoice invoices credit-note acme-2026-0001 --item "Refund:1:500" --notes "Goodwill credit"
58```
59
60### Aging & export
61
62```
63invoice invoices aging # 0-30/31-60/61-90/90+ buckets
64invoice invoices export --from 2026-01-01 --to 2026-03-31 --format csv --out q1.csv
65```
66
67### Discounts
68
69Apply a discount at invoice level (percent OR fixed amount, mutually exclusive).
70
71```
72invoice invoices new --client meridian --item design --discount-rate 10
73```
74
75### Tips
76
77- Run `invoice agent-info` for the full JSON capability manifest.
78- Run `invoice doctor --json` to verify typst, DB, default issuer, and multi-company numbering.
79- Item spec supports `product-slug[:qty]` OR `description:qty:price[:rate]`.
80- Template resolution at render: `--template` flag > client.default_template > issuer.default_template > `vienna`.
81- `--as` picks the issuer; omit it when the client has `default_issuer` pinned or `config.default_issuer` is set.
82- New issuers default to `{issuer}-{year}-{seq:04}` so invoice numbers are globally addressable; use `issuer edit --number-format` for per-company custom prefixes.
83- Use invoice numbers returned by JSON responses instead of predicting the next sequence.
84- `mark issued` / `mark paid` auto-stamp `issued_at` / `paid_at` (first transition only).
85- `invoices list` shows totals per invoice (computed with `rust_decimal`).
86- Every tax value is computed with `rust_decimal` — no float rounding.
87"#;
88
89pub fn run(_cmd: SkillCmd, ctx: Ctx) -> Result<()> {
90 let targets = [
91 dirs_path(".claude/skills/invoice-cli/SKILL.md"),
92 dirs_path(".codex/skills/invoice-cli/SKILL.md"),
93 dirs_path(".gemini/skills/invoice-cli/SKILL.md"),
94 ];
95 let mut written = Vec::new();
96 for t in targets {
97 if let Some(parent) = t.parent() {
98 std::fs::create_dir_all(parent)?;
99 }
100 std::fs::write(&t, SKILL_MD)?;
101 written.push(t.display().to_string());
102 }
103
104 print_success(ctx, &written, |paths| {
105 for p in paths {
106 println!("installed → {}", p);
107 }
108 });
109 Ok(())
110}
111
112fn dirs_path(rel: &str) -> std::path::PathBuf {
113 let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
114 std::path::PathBuf::from(home).join(rel)
115}