Skip to main content

contract_cli/
cli.rs

1use clap::{Parser, Subcommand};
2
3const HELP_FOOTER: &str = "\
4Tips:
5  • Run `contract agent-info | jq` for the full capability manifest (commands, flags, exit codes)
6  • The DB is SHARED with invoice-cli — always `contract issuer list` / `contract clients list` before creating entities
7  • Pipe any command to jq for structured data: `contract list | jq '.data'`
8  • Template chain at render: --template > contract.default_template > \"helvetica-nera\"
9  • Drafts render with a DRAFT watermark; use --final for a clean signing copy
10  • `contract doctor` verifies typst, the DB, packs, and templates before you start
11
12Examples:
13  contract new --kind nda --as acme --client meridian --purpose 'evaluating a joint venture' --term-years 3
14    Quick mutual NDA with a 3-year confidentiality term
15
16  contract new --kind consulting --as acme --client meridian --fee fixed:8400:SGD --term-months 3 \\
17      --purpose 'design a dashboard' --deliverable 'Designs' --deliverable 'Build' --ip-assignment client
18    Consulting agreement with a fixed fee and deliverables
19
20  contract render NDA-acme-2026-0001 --final --open
21    Render a clean, watermark-free PDF and open it
22
23  contract sign NDA-acme-2026-0001 --side us --name 'B. Djordjevic' --title Director
24    Record one side's signature (status auto-bumps to 'signed' when both sides sign)
25
26  contract template list | jq '.data'
27    Inspect available PDF templates as structured JSON";
28
29#[derive(Parser, Debug)]
30#[command(
31    name = "contract",
32    version,
33    about = "Beautiful contracts from the CLI — NDA, consulting, MSA, SOW, service",
34    after_long_help = HELP_FOOTER,
35    after_help = HELP_FOOTER
36)]
37pub struct Cli {
38    /// Emit JSON envelope on stdout (auto-detected when piped)
39    #[arg(long, global = true)]
40    pub json: bool,
41    /// Suppress human output
42    #[arg(long, global = true)]
43    pub quiet: bool,
44    #[command(subcommand)]
45    pub command: Commands,
46}
47
48#[derive(Subcommand, Debug)]
49pub enum Commands {
50    /// Manage issuers (companies you contract AS)
51    #[command(visible_alias = "issuers", subcommand)]
52    Issuer(IssuerCmd),
53
54    /// Manage clients (counterparties)
55    #[command(visible_alias = "client", subcommand)]
56    Clients(ClientCmd),
57
58    /// Manage contracts (new/list/show/render/mark/sign/edit/duplicate/delete)
59    #[command(subcommand)]
60    Contracts(ContractCmd),
61
62    /// Shorthand: `contract new …` (= `contract contracts new …`)
63    #[command(name = "new")]
64    New(ContractNewArgs),
65
66    /// Shorthand: `contract list` (= `contract contracts list`)
67    #[command(name = "list", alias = "ls")]
68    List(ContractListArgs),
69
70    /// Shorthand: `contract show <number>`
71    #[command(name = "show", alias = "get")]
72    Show { number: String },
73
74    /// Shorthand: `contract render <number>`
75    #[command(name = "render")]
76    Render(ContractRenderArgs),
77
78    /// Shorthand: `contract mark <number> <status>`
79    #[command(name = "mark")]
80    Mark { number: String, status: String },
81
82    /// Shorthand: `contract sign <number> --side us|them --name "..."`
83    #[command(name = "sign")]
84    Sign(SignArgs),
85
86    /// Shorthand: `contract edit <number> …` (= `contract contracts edit …`)
87    #[command(name = "edit")]
88    Edit(ContractEditArgs),
89
90    /// Shorthand: `contract duplicate <number>` (= `contract contracts duplicate …`)
91    #[command(name = "duplicate", visible_alias = "clone")]
92    Duplicate(DuplicateArgs),
93
94    /// Shorthand: `contract delete <number>` (= `contract contracts delete …`)
95    #[command(name = "delete", visible_alias = "rm")]
96    Delete(DeleteArgs),
97
98    /// Browse clause packs (the building blocks per contract kind)
99    #[command(visible_alias = "packs", subcommand)]
100    Pack(PackCmd),
101
102    /// Inspect / find / preview PDF templates
103    #[command(visible_alias = "templates", subcommand)]
104    Template(TemplateCmd),
105
106    /// Browse contract kinds (nda, ncnda, consulting, loan, …)
107    #[command(visible_alias = "kind", subcommand)]
108    Kinds(KindsCmd),
109
110    /// Show / edit config
111    #[command(subcommand)]
112    Config(ConfigCmd),
113
114    /// Self-describing JSON manifest for agents
115    #[command(visible_alias = "info")]
116    AgentInfo,
117
118    /// Install the embedded skill into ~/.claude, ~/.codex, ~/.gemini
119    #[command(subcommand)]
120    Skill(SkillCmd),
121
122    /// Run dependency & config diagnostics
123    Doctor,
124
125    /// Distribution-aware update (brew / cargo)
126    Update {
127        /// Check only — no mutation
128        #[arg(long)]
129        check: bool,
130    },
131
132    /// Hidden: deterministically trigger each exit code (conformance testing)
133    #[command(name = "contract", hide = true)]
134    ExitHook { code: i32 },
135}
136
137// ─── Issuers ─────────────────────────────────────────────────────────────
138
139#[derive(Subcommand, Debug)]
140pub enum IssuerCmd {
141    /// Add a new issuer
142    #[command(visible_alias = "new")]
143    Add {
144        slug: String,
145        #[arg(long)]
146        name: String,
147        #[arg(long)]
148        legal_name: Option<String>,
149        #[arg(long, default_value = "sg")]
150        jurisdiction: String,
151        #[arg(long)]
152        tax_id: Option<String>,
153        #[arg(long)]
154        company_no: Option<String>,
155        #[arg(long)]
156        address: String,
157        #[arg(long)]
158        email: Option<String>,
159        #[arg(long)]
160        phone: Option<String>,
161        /// Path to logo image (PNG/SVG/JPG) — used in contract header
162        #[arg(long)]
163        logo: Option<String>,
164        /// Default output dir for `contract render`
165        #[arg(long)]
166        output_dir: Option<String>,
167    },
168    Edit {
169        slug: String,
170        #[arg(long)]
171        name: Option<String>,
172        #[arg(long)]
173        legal_name: Option<String>,
174        #[arg(long)]
175        jurisdiction: Option<String>,
176        #[arg(long)]
177        tax_id: Option<String>,
178        #[arg(long)]
179        company_no: Option<String>,
180        #[arg(long)]
181        address: Option<String>,
182        #[arg(long)]
183        email: Option<String>,
184        #[arg(long)]
185        phone: Option<String>,
186        #[arg(long)]
187        logo: Option<String>,
188        #[arg(long)]
189        logo_clear: bool,
190        #[arg(long)]
191        output_dir: Option<String>,
192    },
193    #[command(visible_alias = "ls")]
194    List,
195    #[command(visible_alias = "get")]
196    Show { slug: String },
197    #[command(visible_alias = "rm")]
198    Delete { slug: String },
199}
200
201// ─── Clients ─────────────────────────────────────────────────────────────
202
203#[derive(Subcommand, Debug)]
204pub enum ClientCmd {
205    #[command(visible_alias = "new")]
206    Add {
207        slug: String,
208        #[arg(long)]
209        name: String,
210        /// Legal entity name on the contract (e.g. "Meridian & Co. Pty Ltd")
211        #[arg(long)]
212        legal_name: Option<String>,
213        /// Registration / company number
214        #[arg(long)]
215        company_no: Option<String>,
216        /// Legal jurisdiction of incorporation (e.g. "Singapore", "Delaware")
217        #[arg(long)]
218        jurisdiction: Option<String>,
219        #[arg(long)]
220        attn: Option<String>,
221        #[arg(long)]
222        country: Option<String>,
223        #[arg(long)]
224        address: String,
225        #[arg(long)]
226        email: Option<String>,
227        #[arg(long)]
228        notes: Option<String>,
229    },
230    Edit {
231        slug: String,
232        #[arg(long)]
233        name: Option<String>,
234        #[arg(long)]
235        legal_name: Option<String>,
236        #[arg(long)]
237        company_no: Option<String>,
238        #[arg(long)]
239        jurisdiction: Option<String>,
240        #[arg(long)]
241        attn: Option<String>,
242        #[arg(long)]
243        country: Option<String>,
244        #[arg(long)]
245        address: Option<String>,
246        #[arg(long)]
247        email: Option<String>,
248        #[arg(long)]
249        notes: Option<String>,
250    },
251    #[command(visible_alias = "ls")]
252    List,
253    #[command(visible_alias = "get")]
254    Show { slug: String },
255    #[command(visible_alias = "rm")]
256    Delete { slug: String },
257}
258
259// ─── Contracts ───────────────────────────────────────────────────────────
260
261#[derive(clap::Args, Debug, Clone)]
262pub struct ContractNewArgs {
263    /// Contract kind: nda | consulting | msa | sow | service
264    #[arg(long)]
265    pub kind: String,
266    /// Issuer slug (your side)
267    #[arg(long = "as")]
268    pub r#as: Option<String>,
269    /// Client slug (counterparty)
270    #[arg(long)]
271    pub client: String,
272    /// Title — defaults to a sensible per-kind label
273    #[arg(long)]
274    pub title: Option<String>,
275    /// Effective date (YYYY-MM-DD; defaults to today)
276    #[arg(long)]
277    pub effective: Option<String>,
278    /// Explicit end date (YYYY-MM-DD). Mutually exclusive with --term-months.
279    #[arg(long, conflicts_with = "term_months")]
280    pub end: Option<String>,
281    /// Term length in months. Mutually exclusive with --end.
282    #[arg(long)]
283    pub term_months: Option<i64>,
284    /// Term length in years (sugar for --term-months N*12)
285    #[arg(long, conflicts_with_all = ["end", "term_months"])]
286    pub term_years: Option<i64>,
287    /// Governing law (e.g. "Singapore", "England and Wales", "Delaware")
288    #[arg(long)]
289    pub governing_law: Option<String>,
290    /// Court venue (e.g. "Courts of Singapore")
291    #[arg(long)]
292    pub venue: Option<String>,
293    /// Fee spec for consulting / msa / sow / service.
294    /// Formats: "fixed:8400:SGD" | "hourly:200:SGD" | "daily:1500:SGD"
295    ///        | "retainer:5000:SGD/month".
296    #[arg(long)]
297    pub fee: Option<String>,
298    /// Payment schedule: on-completion | monthly | on-milestone | upon-invoice
299    #[arg(long)]
300    pub fee_schedule: Option<String>,
301    /// NDA: unilateral | mutual (default mutual)
302    #[arg(long)]
303    pub mutuality: Option<String>,
304    /// NDA: disclosing side — us | them | both (default both for mutual)
305    #[arg(long)]
306    pub disclosing_side: Option<String>,
307    /// NDA / consulting: purpose / scope summary
308    #[arg(long)]
309    pub purpose: Option<String>,
310    /// Consulting: deliverable lines (repeat). e.g. --deliverable "Discovery"
311    #[arg(long = "deliverable")]
312    pub deliverables: Vec<String>,
313    /// Consulting: who owns work-product IP (client | consultant | shared)
314    #[arg(long)]
315    pub ip_assignment: Option<String>,
316    /// Days of notice required for termination for convenience
317    #[arg(long)]
318    pub termination_notice_days: Option<i64>,
319    /// Set an arbitrary term key used by the clause pack's {{vars}},
320    /// e.g. --term principal_text='£10,000 (ten thousand pounds)' (repeat)
321    #[arg(long = "term")]
322    pub terms: Vec<String>,
323    /// Pack slug (default = "standard"). Picks a different clause library.
324    #[arg(long)]
325    pub pack: Option<String>,
326    /// Append a clause slug from the pack that isn't included by default
327    #[arg(long = "include")]
328    pub include: Vec<String>,
329    /// Remove a clause slug that the pack would include by default
330    #[arg(long = "exclude")]
331    pub exclude: Vec<String>,
332    /// Free-form notes (not rendered on the contract body, kept for reference)
333    #[arg(long)]
334    pub notes: Option<String>,
335    /// Override the rendered template (else config default)
336    #[arg(long)]
337    pub template: Option<String>,
338}
339
340#[derive(clap::Args, Debug, Clone)]
341pub struct ContractListArgs {
342    #[arg(long)]
343    pub kind: Option<String>,
344    #[arg(long)]
345    pub status: Option<String>,
346    #[arg(long = "as")]
347    pub issuer: Option<String>,
348}
349
350#[derive(clap::Args, Debug, Clone)]
351pub struct ContractRenderArgs {
352    pub number: String,
353    /// Template override
354    #[arg(long)]
355    pub template: Option<String>,
356    /// Output path (defaults to issuer default_output_dir / ./contract-<number>.pdf)
357    #[arg(long, short)]
358    pub out: Option<String>,
359    /// Open the PDF after rendering
360    #[arg(long)]
361    pub open: bool,
362    /// Render with DRAFT watermark — implicit when status != signed/active
363    #[arg(long)]
364    pub draft: bool,
365    /// Force render without DRAFT watermark even if status != signed/active.
366    /// Use when you genuinely want a clean copy for review before signing.
367    #[arg(long = "final", conflicts_with = "draft")]
368    pub final_render: bool,
369}
370
371#[derive(clap::Args, Debug, Clone)]
372pub struct SignArgs {
373    pub number: String,
374    /// us | them
375    #[arg(long)]
376    pub side: String,
377    #[arg(long)]
378    pub name: String,
379    #[arg(long)]
380    pub title: Option<String>,
381    /// Signature date (YYYY-MM-DD; defaults to today)
382    #[arg(long)]
383    pub date: Option<String>,
384    /// Re-sign an already-executed contract (overwrites the recorded signature)
385    #[arg(long)]
386    pub force: bool,
387}
388
389#[derive(clap::Args, Debug, Clone)]
390pub struct ContractEditArgs {
391    pub number: String,
392    #[arg(long)]
393    pub client: Option<String>,
394    #[arg(long)]
395    pub title: Option<String>,
396    #[arg(long)]
397    pub effective: Option<String>,
398    /// Explicit end date (YYYY-MM-DD). Mutually exclusive with --term-months.
399    #[arg(long, conflicts_with = "term_months")]
400    pub end: Option<String>,
401    #[arg(long)]
402    pub term_months: Option<i64>,
403    #[arg(long)]
404    pub governing_law: Option<String>,
405    #[arg(long)]
406    pub venue: Option<String>,
407    #[arg(long)]
408    pub fee: Option<String>,
409    #[arg(long)]
410    pub fee_schedule: Option<String>,
411    /// Set a term key directly, e.g. --term repayment_date=2026-12-01 (repeat)
412    #[arg(long = "term")]
413    pub terms: Vec<String>,
414    #[arg(long)]
415    pub notes: Option<String>,
416    #[arg(long)]
417    pub template: Option<String>,
418}
419
420#[derive(clap::Args, Debug, Clone)]
421pub struct DuplicateArgs {
422    pub number: String,
423    #[arg(long)]
424    pub client: Option<String>,
425    #[arg(long = "as")]
426    pub r#as: Option<String>,
427}
428
429#[derive(clap::Args, Debug, Clone)]
430pub struct DeleteArgs {
431    pub number: String,
432    /// Allow deleting a non-draft contract
433    #[arg(long)]
434    pub force: bool,
435}
436
437#[derive(Subcommand, Debug)]
438pub enum ContractCmd {
439    /// Create a new contract
440    #[command(visible_alias = "create")]
441    New(ContractNewArgs),
442    /// List contracts
443    #[command(visible_alias = "ls")]
444    List(ContractListArgs),
445    /// Show one contract's metadata + clause list
446    #[command(visible_alias = "get")]
447    Show { number: String },
448    /// Edit DRAFT contract metadata (status != draft is immutable)
449    Edit(ContractEditArgs),
450    /// Render contract to PDF
451    Render(ContractRenderArgs),
452    /// Update status: draft | sent | signed | active | expired | terminated
453    Mark { number: String, status: String },
454    /// Record a signature (auto-bumps status to "signed" when both sides done)
455    Sign(SignArgs),
456    /// Manage the clause set for a contract
457    #[command(subcommand)]
458    Clauses(ClauseCmd),
459    /// Clone an existing contract as a new draft
460    #[command(visible_alias = "clone")]
461    Duplicate(DuplicateArgs),
462    /// Delete a contract (draft only unless --force)
463    #[command(visible_alias = "rm")]
464    Delete(DeleteArgs),
465}
466
467#[derive(Subcommand, Debug)]
468pub enum ClauseCmd {
469    /// List clauses currently attached to a contract
470    #[command(visible_alias = "ls")]
471    List { number: String },
472    /// Add a clause to a contract (pack default or custom from --from-file)
473    Add {
474        number: String,
475        slug: String,
476        /// Override heading
477        #[arg(long)]
478        heading: Option<String>,
479        /// Custom body markdown — direct string
480        #[arg(long)]
481        body: Option<String>,
482        /// Custom body markdown — read from file
483        #[arg(long = "from-file")]
484        from_file: Option<String>,
485        /// Insert at zero-indexed position (default: end)
486        #[arg(long)]
487        position: Option<i64>,
488    },
489    /// Edit a clause that is currently attached
490    Edit {
491        number: String,
492        slug: String,
493        #[arg(long)]
494        heading: Option<String>,
495        #[arg(long)]
496        body: Option<String>,
497        #[arg(long = "from-file")]
498        from_file: Option<String>,
499    },
500    /// Remove a clause
501    #[command(visible_alias = "rm")]
502    Remove { number: String, slug: String },
503    /// Move a clause to a new position
504    Move {
505        number: String,
506        slug: String,
507        position: i64,
508    },
509    /// Reset the contract's clause set to the pack default
510    Reset { number: String },
511}
512
513// ─── Packs / Templates / Config / Skill ───────────────────────────────────
514
515#[derive(Subcommand, Debug)]
516pub enum PackCmd {
517    /// List available kind/pack combinations
518    #[command(visible_alias = "ls")]
519    List,
520    /// Show available clauses (slug + heading) for a kind + pack
521    #[command(visible_alias = "get")]
522    Show {
523        kind: String,
524        #[arg(long, default_value = "standard")]
525        pack: String,
526    },
527}
528
529#[derive(Subcommand, Debug)]
530pub enum TemplateCmd {
531    /// List templates with descriptions, moods, tags, and fonts
532    #[command(visible_alias = "ls")]
533    List,
534    /// Find a template from a free-text description of the look you want
535    #[command(visible_alias = "suggest")]
536    Find {
537        /// e.g. "magazine masthead", "swiss minimal", "warm cream data-room"
538        #[arg(required = true, num_args = 1..)]
539        query: Vec<String>,
540    },
541    /// Render a preview contract with synthetic data
542    Preview {
543        name: String,
544        /// Which contract kind to preview (default consulting)
545        #[arg(long, default_value = "consulting")]
546        kind: String,
547        #[arg(long, short)]
548        out: Option<String>,
549    },
550}
551
552#[derive(Subcommand, Debug)]
553pub enum KindsCmd {
554    /// List contract kinds with descriptions and trigger tags
555    #[command(visible_alias = "ls")]
556    List,
557    /// Find a contract kind from a free-text description of what you need
558    #[command(visible_alias = "suggest")]
559    Find {
560        /// e.g. "stop them going around me to my contact", "lend money"
561        #[arg(required = true, num_args = 1..)]
562        query: Vec<String>,
563    },
564}
565
566#[derive(Subcommand, Debug)]
567pub enum ConfigCmd {
568    /// Display current configuration
569    Show,
570    /// Print the config file path
571    Path,
572    /// Set a config key (default_issuer, default_template, open_pdf, self_update)
573    Set { key: String, value: String },
574}
575
576#[derive(Subcommand, Debug)]
577pub enum SkillCmd {
578    /// Install the embedded skill into ~/.claude, ~/.codex, ~/.gemini
579    Install,
580    /// Report where the skill is installed and whether it is current
581    Status,
582}