Skip to main content

invoice_cli/
cli.rs

1use clap::{Parser, Subcommand};
2
3#[derive(Parser, Debug)]
4#[command(name = "invoice", version, about = "Beautiful invoices from the CLI")]
5pub struct Cli {
6    /// Emit JSON envelope on stdout (auto-detected when piped)
7    #[arg(long, global = true)]
8    pub json: bool,
9    /// Suppress human output
10    #[arg(long, global = true)]
11    pub quiet: bool,
12    #[command(subcommand)]
13    pub command: Commands,
14}
15
16#[derive(Subcommand, Debug)]
17pub enum Commands {
18    /// Manage issuers (the companies you invoice AS — supports multiple)
19    #[command(visible_alias = "issuer", subcommand)]
20    Issuers(IssuerCmd),
21
22    /// Manage clients (the companies you invoice TO)
23    #[command(subcommand)]
24    Clients(ClientCmd),
25
26    /// Manage reusable products/line-items
27    #[command(subcommand)]
28    Products(ProductCmd),
29
30    /// Create, list, show, render, or mark invoices
31    #[command(subcommand)]
32    Invoices(InvoiceCmd),
33
34    /// Template operations (list, preview, set default)
35    #[command(subcommand)]
36    Template(TemplateCmd),
37
38    /// Show / edit config
39    #[command(subcommand)]
40    Config(ConfigCmd),
41
42    /// Self-describing JSON manifest for agents
43    #[command(alias = "info")]
44    AgentInfo,
45
46    /// Install the embedded skill file to ~/.claude/skills/
47    #[command(subcommand)]
48    Skill(SkillCmd),
49
50    /// Run dependency & config diagnostics
51    Doctor,
52
53    /// Self-update from GitHub Releases
54    Update {
55        /// Don't install, just report latest version
56        #[arg(long)]
57        check: bool,
58    },
59}
60
61#[derive(Subcommand, Debug)]
62pub enum IssuerCmd {
63    /// Add a new issuer
64    #[command(alias = "new")]
65    Add {
66        slug: String,
67        #[arg(long)]
68        name: String,
69        #[arg(long)]
70        legal_name: Option<String>,
71        #[arg(long, default_value = "sg")]
72        jurisdiction: String,
73        #[arg(long)]
74        tax_registered: bool,
75        #[arg(long)]
76        tax_id: Option<String>,
77        #[arg(long)]
78        company_no: Option<String>,
79        #[arg(long)]
80        address: String,
81        #[arg(long)]
82        email: Option<String>,
83        #[arg(long)]
84        phone: Option<String>,
85        /// Bank / payment detail line as "Label: Value". Repeat for each
86        /// line. Example:
87        ///   --bank-line "Bank: DBS" --bank-line "Account: 1234567890"
88        ///   --bank-line "Bank Code: 7171" --bank-line "SWIFT: DBSSSGSG"
89        /// Lines render as a two-column list on the invoice PDF.
90        #[arg(long = "bank-line")]
91        bank_line: Vec<String>,
92        #[arg(long, default_value = "vienna")]
93        template: String,
94        /// Path to a logo image (PNG/SVG/JPG). Rendered in template header.
95        #[arg(long)]
96        logo: Option<String>,
97        /// Default directory for `invoices render` output when --out is
98        /// omitted. Leading `~/` is expanded. Example:
99        ///   --output-dir "~/Documents/Invoices/Paperfoot"
100        #[arg(long)]
101        output_dir: Option<String>,
102        /// Default notes auto-populated into new invoices (free-form
103        /// multi-line). Use for payment terms, reverse-charge disclaimers,
104        /// etc.
105        #[arg(long)]
106        notes: Option<String>,
107        /// Invoice number format. Tokens: {issuer}, {year}, {seq}, {seq:04}.
108        /// Default includes {issuer} so multiple companies cannot collide.
109        #[arg(long)]
110        number_format: Option<String>,
111    },
112    /// Edit an existing issuer — pass only the fields you want to change
113    Edit {
114        slug: String,
115        #[arg(long)]
116        name: Option<String>,
117        #[arg(long)]
118        legal_name: Option<String>,
119        #[arg(long)]
120        jurisdiction: Option<String>,
121        #[arg(long)]
122        tax_registered: Option<bool>,
123        #[arg(long)]
124        tax_id: Option<String>,
125        #[arg(long)]
126        company_no: Option<String>,
127        #[arg(long)]
128        tagline: Option<String>,
129        #[arg(long)]
130        address: Option<String>,
131        #[arg(long)]
132        email: Option<String>,
133        #[arg(long)]
134        phone: Option<String>,
135        /// Bank / payment detail line as "Label: Value". Repeat for each
136        /// line. When any --bank-line is passed, REPLACES all existing
137        /// bank details on the issuer.
138        #[arg(long = "bank-line")]
139        bank_line: Vec<String>,
140        /// Remove all bank details from the issuer.
141        #[arg(long)]
142        bank_clear: bool,
143        #[arg(long)]
144        template: Option<String>,
145        #[arg(long)]
146        currency: Option<String>,
147        #[arg(long)]
148        symbol: Option<String>,
149        /// Invoice number format. Tokens: {issuer}, {year}, {seq}, {seq:04}.
150        /// Use a unique prefix per issuer for globally addressable invoice ids.
151        #[arg(long)]
152        number_format: Option<String>,
153        #[arg(long)]
154        logo: Option<String>,
155        /// Remove the logo from the issuer (falls back to the star mark).
156        #[arg(long)]
157        logo_clear: bool,
158        /// Default directory for `invoices render` output when --out is
159        /// omitted. Leading `~/` is expanded.
160        #[arg(long)]
161        output_dir: Option<String>,
162        /// Default notes auto-populated into new invoices.
163        #[arg(long)]
164        notes: Option<String>,
165    },
166    /// Shorthand: change the issuer's default template
167    SetTemplate { slug: String, template: String },
168    #[command(alias = "ls")]
169    List,
170    #[command(alias = "get")]
171    Show { slug: String },
172    #[command(alias = "rm")]
173    Delete { slug: String },
174}
175
176#[derive(Subcommand, Debug)]
177pub enum ClientCmd {
178    #[command(alias = "new")]
179    Add {
180        slug: String,
181        #[arg(long)]
182        name: String,
183        #[arg(long)]
184        attn: Option<String>,
185        #[arg(long)]
186        country: Option<String>,
187        #[arg(long)]
188        tax_id: Option<String>,
189        #[arg(long)]
190        address: String,
191        #[arg(long)]
192        email: Option<String>,
193        #[arg(long)]
194        notes: Option<String>,
195        /// Default issuer slug — `invoices new` uses this when `--as` omitted
196        #[arg(long)]
197        default_issuer: Option<String>,
198        /// Preferred template for this client's invoices
199        #[arg(long)]
200        default_template: Option<String>,
201    },
202    /// Edit an existing client — pass only the fields you want to change
203    Edit {
204        slug: String,
205        #[arg(long)]
206        name: Option<String>,
207        #[arg(long)]
208        attn: Option<String>,
209        #[arg(long)]
210        country: Option<String>,
211        #[arg(long)]
212        tax_id: Option<String>,
213        #[arg(long)]
214        address: Option<String>,
215        #[arg(long)]
216        email: Option<String>,
217        #[arg(long)]
218        notes: Option<String>,
219        #[arg(long)]
220        default_issuer: Option<String>,
221        #[arg(long)]
222        default_template: Option<String>,
223    },
224    /// Shorthand: pin a default issuer for this client
225    SetIssuer { slug: String, issuer_slug: String },
226    /// Shorthand: pin a preferred template for this client
227    SetTemplate { slug: String, template: String },
228    #[command(alias = "ls")]
229    List,
230    #[command(alias = "get")]
231    Show { slug: String },
232    #[command(alias = "rm")]
233    Delete { slug: String },
234}
235
236#[derive(Subcommand, Debug)]
237pub enum ProductCmd {
238    #[command(alias = "new")]
239    Add {
240        slug: String,
241        #[arg(long)]
242        description: String,
243        #[arg(long)]
244        subtitle: Option<String>,
245        #[arg(long, default_value = "unit")]
246        unit: String,
247        /// Unit price as a decimal (e.g. 220.00)
248        #[arg(long)]
249        price: String,
250        #[arg(long)]
251        currency: String,
252        #[arg(long, default_value = "0")]
253        tax_rate: String,
254    },
255    /// Edit an existing product — pass only the fields you want to change
256    Edit {
257        slug: String,
258        #[arg(long)]
259        description: Option<String>,
260        #[arg(long)]
261        subtitle: Option<String>,
262        #[arg(long)]
263        unit: Option<String>,
264        #[arg(long)]
265        price: Option<String>,
266        #[arg(long)]
267        currency: Option<String>,
268        #[arg(long)]
269        tax_rate: Option<String>,
270    },
271    #[command(alias = "ls")]
272    List,
273    #[command(alias = "get")]
274    Show { slug: String },
275    #[command(alias = "rm")]
276    Delete { slug: String },
277}
278
279#[derive(Subcommand, Debug)]
280pub enum InvoiceCmd {
281    /// Create a new invoice
282    New {
283        /// Issuer slug (the "as" — whose invoice is this?). Optional if the
284        /// client has a `default_issuer` pinned.
285        #[arg(long)]
286        r#as: Option<String>,
287        /// Client slug
288        #[arg(long)]
289        client: String,
290        /// Item in the form: "product-slug" OR "Description:qty:price:rate"
291        #[arg(long = "item")]
292        items: Vec<String>,
293        /// Due date (e.g. "2026-05-17" or "7d"). Defaults to one week
294        /// after issue.
295        #[arg(long, default_value = "7d")]
296        due: String,
297        /// Terms label (default: "Pay in full")
298        #[arg(long, default_value = "Pay in full")]
299        terms: String,
300        #[arg(long)]
301        notes: Option<String>,
302        /// Currency override (otherwise uses issuer's default)
303        #[arg(long)]
304        currency: Option<String>,
305        /// Reverse-charge flag (EU B2B cross-border)
306        #[arg(long)]
307        reverse_charge: bool,
308        /// Payment URL (Stripe Payment Link, EPC-QR, any URL) encoded as QR
309        #[arg(long)]
310        pay_link: Option<String>,
311        /// Invoice-level discount rate (percent, e.g. "10" for 10% off subtotal)
312        #[arg(long)]
313        discount_rate: Option<String>,
314        /// Invoice-level fixed discount in major units (e.g. "50.00")
315        #[arg(long)]
316        discount_fixed: Option<String>,
317    },
318    /// Edit an existing DRAFT invoice's metadata (issued/paid/void invoices
319    /// are immutable — use a credit note instead).
320    Edit {
321        number: String,
322        #[arg(long)]
323        client: Option<String>,
324        #[arg(long)]
325        due: Option<String>,
326        #[arg(long)]
327        terms: Option<String>,
328        #[arg(long)]
329        notes: Option<String>,
330        #[arg(long)]
331        currency: Option<String>,
332        #[arg(long)]
333        pay_link: Option<String>,
334        #[arg(long)]
335        reverse_charge: Option<bool>,
336        #[arg(long)]
337        discount_rate: Option<String>,
338        #[arg(long)]
339        discount_fixed: Option<String>,
340    },
341    /// Manage line items on a DRAFT invoice
342    #[command(subcommand)]
343    Items(InvoiceItemCmd),
344    /// Issue a credit note against an existing invoice
345    CreditNote {
346        /// Source invoice number
347        number: String,
348        /// Copy ALL line items from source and reverse their quantities.
349        /// Mutually exclusive with --item.
350        #[arg(long, conflicts_with = "items")]
351        full: bool,
352        /// Explicit items to include on the credit note (same format as
353        /// `invoices new --item`). Positive refund amounts are stored as
354        /// credits automatically.
355        #[arg(long = "item")]
356        items: Vec<String>,
357        #[arg(long)]
358        notes: Option<String>,
359        #[arg(long)]
360        pay_link: Option<String>,
361    },
362    /// Ageing report for unpaid invoices, bucketed 0-30 / 31-60 / 61-90 / 90+
363    Aging {
364        #[arg(long = "as")]
365        issuer: Option<String>,
366    },
367    /// Export invoices as CSV / JSON — month-end accountant handoff
368    Export {
369        /// YYYY-MM-DD inclusive lower bound on issue_date
370        #[arg(long)]
371        from: Option<String>,
372        /// YYYY-MM-DD inclusive upper bound on issue_date
373        #[arg(long)]
374        to: Option<String>,
375        /// csv | json (default csv)
376        #[arg(long, default_value = "csv")]
377        format: String,
378        /// Output path. Defaults to stdout.
379        #[arg(long, short)]
380        out: Option<String>,
381        #[arg(long = "as")]
382        issuer: Option<String>,
383    },
384    /// Clone an existing invoice's line items into a new draft — same client,
385    /// new number + dates. Handy for recurring billing.
386    Duplicate {
387        number: String,
388        /// Override the client (defaults to the source invoice's client)
389        #[arg(long)]
390        client: Option<String>,
391        /// Override the issuer (defaults to the source invoice's issuer)
392        #[arg(long = "as")]
393        r#as: Option<String>,
394        /// New due date (e.g. "2026-05-17" or "7d"). Defaults to "7d".
395        #[arg(long, default_value = "7d")]
396        due: String,
397    },
398    #[command(alias = "ls")]
399    List {
400        #[arg(long)]
401        status: Option<String>,
402        #[arg(long = "as")]
403        issuer: Option<String>,
404        /// Only show invoices past due date and not paid/void
405        #[arg(long)]
406        overdue: bool,
407    },
408    #[command(alias = "get")]
409    Show { number: String },
410    /// Render invoice to PDF
411    Render {
412        number: String,
413        /// Template to use (overrides issuer default)
414        #[arg(long)]
415        template: Option<String>,
416        /// Output path (defaults to ./invoice-<number>.pdf)
417        #[arg(long, short)]
418        out: Option<String>,
419        /// Open the PDF after rendering (macOS open / linux xdg-open)
420        #[arg(long)]
421        open: bool,
422    },
423    /// Mark status (draft/issued/paid/void)
424    Mark { number: String, status: String },
425    #[command(alias = "rm")]
426    Delete {
427        number: String,
428        /// Allow deleting a non-draft invoice. Breaks number-sequence
429        /// integrity — prefer `mark void` or credit note in most cases.
430        #[arg(long)]
431        force: bool,
432    },
433}
434
435#[derive(Subcommand, Debug)]
436pub enum InvoiceItemCmd {
437    /// Add a line item to a draft invoice
438    Add {
439        number: String,
440        /// Item spec: "product-slug[:qty]" OR "Description:qty:price[:rate]"
441        spec: String,
442        #[arg(long)]
443        subtitle: Option<String>,
444        #[arg(long)]
445        discount_rate: Option<String>,
446        #[arg(long)]
447        discount_fixed: Option<String>,
448    },
449    /// Remove the item at `position` (zero-indexed) from a draft invoice
450    #[command(alias = "rm")]
451    Remove { number: String, position: i64 },
452    /// Edit the item at `position` — any subset of fields
453    Edit {
454        number: String,
455        position: i64,
456        #[arg(long)]
457        description: Option<String>,
458        #[arg(long)]
459        subtitle: Option<String>,
460        #[arg(long)]
461        qty: Option<String>,
462        #[arg(long)]
463        unit: Option<String>,
464        #[arg(long)]
465        price: Option<String>,
466        #[arg(long)]
467        tax_rate: Option<String>,
468        #[arg(long)]
469        discount_rate: Option<String>,
470        #[arg(long)]
471        discount_fixed: Option<String>,
472    },
473}
474
475#[derive(Subcommand, Debug)]
476pub enum TemplateCmd {
477    #[command(alias = "ls")]
478    List,
479    /// Render a preview with synthetic data
480    Preview {
481        name: String,
482        #[arg(long, short)]
483        out: Option<String>,
484    },
485}
486
487#[derive(Subcommand, Debug)]
488pub enum ConfigCmd {
489    Show,
490    Path,
491    Set { key: String, value: String },
492}
493
494#[derive(Subcommand, Debug)]
495pub enum SkillCmd {
496    Install,
497}