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 2026-0001 --open # uses client.default_template
28invoice invoices mark 2026-0001 paid # auto-stamps paid_at
29invoice invoices duplicate 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 2026-0001 --notes "Net 14 — early-payment 2% discount"
56invoice invoices items add 2026-0001 "Extra fee:1:500"
57invoice invoices credit-note 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` to verify typst is installed & DB is ready.
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- `mark issued` / `mark paid` auto-stamp `issued_at` / `paid_at` (first transition only).
83- `invoices list` shows totals per invoice (computed with `rust_decimal`).
84- Every tax value is computed with `rust_decimal` — no float rounding.
85"#;
86
87pub fn run(_cmd: SkillCmd, ctx: Ctx) -> Result<()> {
88 let targets = [
89 dirs_path(".claude/skills/invoice-cli/SKILL.md"),
90 dirs_path(".codex/skills/invoice-cli/SKILL.md"),
91 dirs_path(".gemini/skills/invoice-cli/SKILL.md"),
92 ];
93 let mut written = Vec::new();
94 for t in targets {
95 if let Some(parent) = t.parent() {
96 std::fs::create_dir_all(parent)?;
97 }
98 std::fs::write(&t, SKILL_MD)?;
99 written.push(t.display().to_string());
100 }
101
102 print_success(ctx, &written, |paths| {
103 for p in paths {
104 println!("installed → {}", p);
105 }
106 });
107 Ok(())
108}
109
110fn dirs_path(rel: &str) -> std::path::PathBuf {
111 let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
112 std::path::PathBuf::from(home).join(rel)
113}