Skip to main content

contract_cli/
cli.rs

1use clap::{Parser, Subcommand};
2
3#[derive(Parser, Debug)]
4#[command(
5    name = "contract",
6    version,
7    about = "Beautiful contracts from the CLI — NDA, consulting, MSA, SOW, service"
8)]
9pub struct Cli {
10    /// Emit JSON envelope on stdout (auto-detected when piped)
11    #[arg(long, global = true)]
12    pub json: bool,
13    /// Suppress human output
14    #[arg(long, global = true)]
15    pub quiet: bool,
16    #[command(subcommand)]
17    pub command: Commands,
18}
19
20#[derive(Subcommand, Debug)]
21pub enum Commands {
22    /// Manage issuers (companies you contract AS)
23    #[command(visible_alias = "issuers", subcommand)]
24    Issuer(IssuerCmd),
25
26    /// Manage clients (counterparties)
27    #[command(subcommand)]
28    Clients(ClientCmd),
29
30    /// Manage contracts (new/list/show/render/mark/sign/edit/duplicate/delete)
31    #[command(subcommand)]
32    Contracts(ContractCmd),
33
34    /// Shorthand: `contract new …` (= `contract contracts new …`)
35    #[command(name = "new")]
36    New(ContractNewArgs),
37
38    /// Shorthand: `contract list` (= `contract contracts list`)
39    #[command(name = "list", alias = "ls")]
40    List(ContractListArgs),
41
42    /// Shorthand: `contract show <number>`
43    #[command(name = "show", alias = "get")]
44    Show { number: String },
45
46    /// Shorthand: `contract render <number>`
47    #[command(name = "render")]
48    Render(ContractRenderArgs),
49
50    /// Shorthand: `contract mark <number> <status>`
51    #[command(name = "mark")]
52    Mark { number: String, status: String },
53
54    /// Shorthand: `contract sign <number> --side us|them --name "..."`
55    #[command(name = "sign")]
56    Sign(SignArgs),
57
58    /// Browse clause packs (the building blocks per contract kind)
59    #[command(subcommand)]
60    Pack(PackCmd),
61
62    /// Inspect / render templates
63    #[command(subcommand)]
64    Template(TemplateCmd),
65
66    /// Show / edit config
67    #[command(subcommand)]
68    Config(ConfigCmd),
69
70    /// Self-describing JSON manifest for agents
71    #[command(alias = "info")]
72    AgentInfo,
73
74    /// Install the embedded skill into ~/.claude, ~/.codex, ~/.gemini
75    #[command(subcommand)]
76    Skill(SkillCmd),
77
78    /// Run dependency & config diagnostics
79    Doctor,
80
81    /// Self-update from GitHub Releases
82    Update {
83        #[arg(long)]
84        check: bool,
85    },
86}
87
88// ─── Issuers ─────────────────────────────────────────────────────────────
89
90#[derive(Subcommand, Debug)]
91pub enum IssuerCmd {
92    /// Add a new issuer
93    #[command(alias = "new")]
94    Add {
95        slug: String,
96        #[arg(long)]
97        name: String,
98        #[arg(long)]
99        legal_name: Option<String>,
100        #[arg(long, default_value = "sg")]
101        jurisdiction: String,
102        #[arg(long)]
103        tax_id: Option<String>,
104        #[arg(long)]
105        company_no: Option<String>,
106        #[arg(long)]
107        address: String,
108        #[arg(long)]
109        email: Option<String>,
110        #[arg(long)]
111        phone: Option<String>,
112        /// Path to logo image (PNG/SVG/JPG) — used in contract header
113        #[arg(long)]
114        logo: Option<String>,
115        /// Default output dir for `contract render`
116        #[arg(long)]
117        output_dir: Option<String>,
118    },
119    Edit {
120        slug: String,
121        #[arg(long)]
122        name: Option<String>,
123        #[arg(long)]
124        legal_name: Option<String>,
125        #[arg(long)]
126        jurisdiction: Option<String>,
127        #[arg(long)]
128        tax_id: Option<String>,
129        #[arg(long)]
130        company_no: Option<String>,
131        #[arg(long)]
132        address: Option<String>,
133        #[arg(long)]
134        email: Option<String>,
135        #[arg(long)]
136        phone: Option<String>,
137        #[arg(long)]
138        logo: Option<String>,
139        #[arg(long)]
140        logo_clear: bool,
141        #[arg(long)]
142        output_dir: Option<String>,
143    },
144    #[command(alias = "ls")]
145    List,
146    #[command(alias = "get")]
147    Show { slug: String },
148    #[command(alias = "rm")]
149    Delete { slug: String },
150}
151
152// ─── Clients ─────────────────────────────────────────────────────────────
153
154#[derive(Subcommand, Debug)]
155pub enum ClientCmd {
156    #[command(alias = "new")]
157    Add {
158        slug: String,
159        #[arg(long)]
160        name: String,
161        /// Legal entity name on the contract (e.g. "Meridian & Co. Pty Ltd")
162        #[arg(long)]
163        legal_name: Option<String>,
164        /// Registration / company number
165        #[arg(long)]
166        company_no: Option<String>,
167        /// Legal jurisdiction of incorporation (e.g. "Singapore", "Delaware")
168        #[arg(long)]
169        jurisdiction: Option<String>,
170        #[arg(long)]
171        attn: Option<String>,
172        #[arg(long)]
173        country: Option<String>,
174        #[arg(long)]
175        address: String,
176        #[arg(long)]
177        email: Option<String>,
178        #[arg(long)]
179        notes: Option<String>,
180    },
181    Edit {
182        slug: String,
183        #[arg(long)]
184        name: Option<String>,
185        #[arg(long)]
186        legal_name: Option<String>,
187        #[arg(long)]
188        company_no: Option<String>,
189        #[arg(long)]
190        jurisdiction: Option<String>,
191        #[arg(long)]
192        attn: Option<String>,
193        #[arg(long)]
194        country: Option<String>,
195        #[arg(long)]
196        address: Option<String>,
197        #[arg(long)]
198        email: Option<String>,
199        #[arg(long)]
200        notes: Option<String>,
201    },
202    #[command(alias = "ls")]
203    List,
204    #[command(alias = "get")]
205    Show { slug: String },
206    #[command(alias = "rm")]
207    Delete { slug: String },
208}
209
210// ─── Contracts ───────────────────────────────────────────────────────────
211
212#[derive(clap::Args, Debug, Clone)]
213pub struct ContractNewArgs {
214    /// Contract kind: nda | consulting | msa | sow | service
215    #[arg(long)]
216    pub kind: String,
217    /// Issuer slug (your side)
218    #[arg(long = "as")]
219    pub r#as: Option<String>,
220    /// Client slug (counterparty)
221    #[arg(long)]
222    pub client: String,
223    /// Title — defaults to a sensible per-kind label
224    #[arg(long)]
225    pub title: Option<String>,
226    /// Effective date (YYYY-MM-DD; defaults to today)
227    #[arg(long)]
228    pub effective: Option<String>,
229    /// Explicit end date (YYYY-MM-DD). Mutually exclusive with --term-months.
230    #[arg(long, conflicts_with = "term_months")]
231    pub end: Option<String>,
232    /// Term length in months. Mutually exclusive with --end.
233    #[arg(long)]
234    pub term_months: Option<i64>,
235    /// Term length in years (sugar for --term-months N*12)
236    #[arg(long, conflicts_with_all = ["end", "term_months"])]
237    pub term_years: Option<i64>,
238    /// Governing law (e.g. "Singapore", "England and Wales", "Delaware")
239    #[arg(long)]
240    pub governing_law: Option<String>,
241    /// Court venue (e.g. "Courts of Singapore")
242    #[arg(long)]
243    pub venue: Option<String>,
244    /// Fee spec for consulting / msa / sow / service.
245    /// Formats: "fixed:8400:SGD" | "hourly:200:SGD" | "daily:1500:SGD"
246    ///        | "retainer:5000:SGD/month".
247    #[arg(long)]
248    pub fee: Option<String>,
249    /// Payment schedule: on-completion | monthly | on-milestone | upon-invoice
250    #[arg(long)]
251    pub fee_schedule: Option<String>,
252    /// NDA: unilateral | mutual (default mutual)
253    #[arg(long)]
254    pub mutuality: Option<String>,
255    /// NDA: disclosing side — us | them | both (default both for mutual)
256    #[arg(long)]
257    pub disclosing_side: Option<String>,
258    /// NDA / consulting: purpose / scope summary
259    #[arg(long)]
260    pub purpose: Option<String>,
261    /// Consulting: deliverable lines (repeat). e.g. --deliverable "Discovery"
262    #[arg(long = "deliverable")]
263    pub deliverables: Vec<String>,
264    /// Consulting: who owns work-product IP (client | consultant | shared)
265    #[arg(long)]
266    pub ip_assignment: Option<String>,
267    /// Days of notice required for termination for convenience
268    #[arg(long)]
269    pub termination_notice_days: Option<i64>,
270    /// Pack slug (default = "standard"). Picks a different clause library.
271    #[arg(long)]
272    pub pack: Option<String>,
273    /// Append a clause slug from the pack that isn't included by default
274    #[arg(long = "include")]
275    pub include: Vec<String>,
276    /// Remove a clause slug that the pack would include by default
277    #[arg(long = "exclude")]
278    pub exclude: Vec<String>,
279    /// Free-form notes (not rendered on the contract body, kept for reference)
280    #[arg(long)]
281    pub notes: Option<String>,
282    /// Override the rendered template (else config default)
283    #[arg(long)]
284    pub template: Option<String>,
285}
286
287#[derive(clap::Args, Debug, Clone)]
288pub struct ContractListArgs {
289    #[arg(long)]
290    pub kind: Option<String>,
291    #[arg(long)]
292    pub status: Option<String>,
293    #[arg(long = "as")]
294    pub issuer: Option<String>,
295}
296
297#[derive(clap::Args, Debug, Clone)]
298pub struct ContractRenderArgs {
299    pub number: String,
300    /// Template override
301    #[arg(long)]
302    pub template: Option<String>,
303    /// Output path (defaults to issuer default_output_dir / ./contract-<number>.pdf)
304    #[arg(long, short)]
305    pub out: Option<String>,
306    /// Open the PDF after rendering
307    #[arg(long)]
308    pub open: bool,
309    /// Render with DRAFT watermark — implicit when status != signed/active
310    #[arg(long)]
311    pub draft: bool,
312    /// Force render without DRAFT watermark even if status != signed/active.
313    /// Use when you genuinely want a clean copy for review before signing.
314    #[arg(long = "final", conflicts_with = "draft")]
315    pub final_render: bool,
316}
317
318#[derive(clap::Args, Debug, Clone)]
319pub struct SignArgs {
320    pub number: String,
321    /// us | them
322    #[arg(long)]
323    pub side: String,
324    #[arg(long)]
325    pub name: String,
326    #[arg(long)]
327    pub title: Option<String>,
328    /// Signature date (YYYY-MM-DD; defaults to today)
329    #[arg(long)]
330    pub date: Option<String>,
331}
332
333#[derive(Subcommand, Debug)]
334pub enum ContractCmd {
335    /// Create a new contract
336    #[command(alias = "new")]
337    New(ContractNewArgs),
338    /// List contracts
339    #[command(alias = "ls")]
340    List(ContractListArgs),
341    /// Show one contract's metadata + clause list
342    #[command(alias = "get")]
343    Show { number: String },
344    /// Edit DRAFT contract metadata (status != draft is immutable)
345    Edit {
346        number: String,
347        #[arg(long)]
348        client: Option<String>,
349        #[arg(long)]
350        title: Option<String>,
351        #[arg(long)]
352        effective: Option<String>,
353        #[arg(long)]
354        end: Option<String>,
355        #[arg(long)]
356        term_months: Option<i64>,
357        #[arg(long)]
358        governing_law: Option<String>,
359        #[arg(long)]
360        venue: Option<String>,
361        #[arg(long)]
362        fee: Option<String>,
363        #[arg(long)]
364        fee_schedule: Option<String>,
365        #[arg(long)]
366        notes: Option<String>,
367        #[arg(long)]
368        template: Option<String>,
369    },
370    /// Render contract to PDF
371    Render(ContractRenderArgs),
372    /// Update status: draft | sent | signed | active | expired | terminated
373    Mark { number: String, status: String },
374    /// Record a signature (auto-bumps status to "signed" when both sides done)
375    Sign(SignArgs),
376    /// Manage the clause set for a contract
377    #[command(subcommand)]
378    Clauses(ClauseCmd),
379    /// Clone an existing contract as a new draft
380    Duplicate {
381        number: String,
382        #[arg(long)]
383        client: Option<String>,
384        #[arg(long = "as")]
385        r#as: Option<String>,
386    },
387    /// Delete a contract (draft only unless --force)
388    #[command(alias = "rm")]
389    Delete {
390        number: String,
391        #[arg(long)]
392        force: bool,
393    },
394}
395
396#[derive(Subcommand, Debug)]
397pub enum ClauseCmd {
398    /// List clauses currently attached to a contract
399    #[command(alias = "ls")]
400    List { number: String },
401    /// Add a clause to a contract (pack default or custom from --from-file)
402    Add {
403        number: String,
404        slug: String,
405        /// Override heading
406        #[arg(long)]
407        heading: Option<String>,
408        /// Custom body markdown — direct string
409        #[arg(long)]
410        body: Option<String>,
411        /// Custom body markdown — read from file
412        #[arg(long = "from-file")]
413        from_file: Option<String>,
414        /// Insert at zero-indexed position (default: end)
415        #[arg(long)]
416        position: Option<i64>,
417    },
418    /// Edit a clause that is currently attached
419    Edit {
420        number: String,
421        slug: String,
422        #[arg(long)]
423        heading: Option<String>,
424        #[arg(long)]
425        body: Option<String>,
426        #[arg(long = "from-file")]
427        from_file: Option<String>,
428    },
429    /// Remove a clause
430    #[command(alias = "rm")]
431    Remove { number: String, slug: String },
432    /// Move a clause to a new position
433    Move {
434        number: String,
435        slug: String,
436        position: i64,
437    },
438    /// Reset the contract's clause set to the pack default
439    Reset { number: String },
440}
441
442// ─── Packs / Templates / Config / Skill ───────────────────────────────────
443
444#[derive(Subcommand, Debug)]
445pub enum PackCmd {
446    /// List available kind/pack combinations
447    #[command(alias = "ls")]
448    List,
449    /// Show available clauses (slug + heading) for a kind + pack
450    Show {
451        kind: String,
452        #[arg(long, default_value = "standard")]
453        pack: String,
454    },
455}
456
457#[derive(Subcommand, Debug)]
458pub enum TemplateCmd {
459    #[command(alias = "ls")]
460    List,
461    /// Render a preview contract with synthetic data
462    Preview {
463        name: String,
464        /// Which contract kind to preview (default consulting)
465        #[arg(long, default_value = "consulting")]
466        kind: String,
467        #[arg(long, short)]
468        out: Option<String>,
469    },
470}
471
472#[derive(Subcommand, Debug)]
473pub enum ConfigCmd {
474    Show,
475    Path,
476    Set { key: String, value: String },
477}
478
479#[derive(Subcommand, Debug)]
480pub enum SkillCmd {
481    Install,
482}