1use serde_json::json;
7
8use crate::error::Result;
9use crate::output::{print_raw, Ctx};
10
11fn arg(name: &str, ty: &str, required: bool, desc: &str) -> serde_json::Value {
12 json!({ "name": name, "kind": "positional", "type": ty, "required": required, "description": desc })
13}
14
15fn opt(name: &str, ty: &str, desc: &str) -> serde_json::Value {
16 json!({ "name": name, "type": ty, "required": false, "description": desc })
17}
18
19fn req_opt(name: &str, ty: &str, desc: &str) -> serde_json::Value {
20 json!({ "name": name, "type": ty, "required": true, "description": desc })
21}
22
23fn cmd(desc: &str, aliases: &[&str], args: Vec<serde_json::Value>, options: Vec<serde_json::Value>) -> serde_json::Value {
24 let mut v = json!({ "description": desc, "args": args, "options": options });
25 if !aliases.is_empty() {
26 v["aliases"] = json!(aliases);
27 }
28 v
29}
30
31pub fn run(_ctx: Ctx) -> Result<()> {
32 let config_path = crate::config::config_path()?.display().to_string();
33 let state_dir = crate::config::state_path()?.display().to_string();
34 let database = crate::config::db_path()?.display().to_string();
35
36 let slug = |d: &str| vec![arg("slug", "string", true, d)];
37 let number = || vec![arg("number", "string", true, "Contract number, e.g. NDA-acme-2026-0001")];
38
39 let entity_opts = |contract_side: bool| {
40 let mut o = vec![
41 opt("--name", "string", "Display name"),
42 opt("--legal-name", "string", "Legal entity name used on contracts"),
43 opt("--company-no", "string", "Registration / company number"),
44 opt("--jurisdiction", "string", "Jurisdiction (issuers: sg|uk|us|eu; clients: free text)"),
45 opt("--address", "string", "Address lines separated by \\n"),
46 opt("--email", "string", "Contact email"),
47 ];
48 if contract_side {
49 o.push(opt("--tax-id", "string", "Tax / VAT id"));
50 o.push(opt("--phone", "string", "Phone"));
51 o.push(opt("--logo", "string", "Path to logo image for the contract header"));
52 o.push(opt("--output-dir", "string", "Default output dir for render"));
53 } else {
54 o.push(opt("--attn", "string", "Attention line"));
55 o.push(opt("--country", "string", "ISO country code"));
56 o.push(opt("--notes", "string", "Free-form notes"));
57 }
58 o
59 };
60
61 let new_opts = vec![
62 req_opt("--kind", "string", "nda | ncnda | consulting | msa | sow | service | loan"),
63 opt("--as", "string", "Issuer slug (your side); falls back to client default, then config"),
64 req_opt("--client", "string", "Client slug (counterparty)"),
65 opt("--title", "string", "Contract title (defaults per kind)"),
66 opt("--effective", "string", "Effective date YYYY-MM-DD (default today)"),
67 opt("--end", "string", "End date YYYY-MM-DD (mutually exclusive with --term-months)"),
68 opt("--term-months", "int", "Term length in months"),
69 opt("--term-years", "int", "Term length in years (sugar for months × 12)"),
70 opt("--governing-law", "string", "Governing law, e.g. 'England and Wales'"),
71 opt("--venue", "string", "Court venue override"),
72 opt("--fee", "string", "type:amount:currency — fixed:8400:SGD | hourly:200:SGD | daily:1500:SGD | retainer:5000:SGD"),
73 opt("--fee-schedule", "string", "on-completion | monthly | on-milestone | upon-invoice"),
74 opt("--mutuality", "string", "nda/ncnda: mutual | unilateral"),
75 opt("--disclosing-side", "string", "nda: us | them | both"),
76 opt("--purpose", "string", "Purpose / scope summary"),
77 opt("--deliverable", "string", "Deliverable line (repeatable)"),
78 opt("--ip-assignment", "string", "client | consultant | shared"),
79 opt("--termination-notice-days", "int", "Notice days for termination for convenience"),
80 opt("--term", "string", "Arbitrary term key=value for pack {{vars}} (repeatable), e.g. principal_text='£10,000 (ten thousand pounds)'"),
81 opt("--pack", "string", "Clause pack slug (default: standard)"),
82 opt("--include", "string", "Extra pack clause slug to include (repeatable)"),
83 opt("--exclude", "string", "Default pack clause slug to drop (repeatable)"),
84 opt("--template", "string", "Default render template for this contract"),
85 opt("--notes", "string", "Internal notes (never rendered)"),
86 ];
87
88 let render_opts = vec![
89 opt("--template", "string", "Template override (see: template list)"),
90 opt("--out", "string", "Output PDF path"),
91 opt("--open", "bool", "Open the PDF after rendering"),
92 opt("--draft", "bool", "Force the DRAFT watermark"),
93 opt("--final", "bool", "Force a clean copy (no watermark)"),
94 ];
95
96 let commands = json!({
97 "issuer add": cmd("Register an issuer (your side; shared with invoice-cli)", &["issuer new"], slug("Issuer slug"), entity_opts(true)),
98 "issuer edit": cmd("Update issuer fields", &[], slug("Issuer slug"), {
99 let mut o = entity_opts(true);
100 o.push(opt("--logo-clear", "bool", "Remove the stored logo"));
101 o
102 }),
103 "issuer list": cmd("List issuers (shared with invoice-cli)", &["issuer ls"], vec![], vec![]),
104 "issuer show": cmd("Show one issuer", &["issuer get"], slug("Issuer slug"), vec![]),
105 "issuer delete": cmd("Delete an issuer", &["issuer rm"], slug("Issuer slug"), vec![]),
106 "clients add": cmd("Register a counterparty (shared with invoice-cli)", &["client add", "clients new"], slug("Client slug"), entity_opts(false)),
107 "clients edit": cmd("Update client fields (legal-name/company-no/jurisdiction feed the party block)", &["client edit"], slug("Client slug"), entity_opts(false)),
108 "clients list": cmd("List clients (shared with invoice-cli)", &["clients ls"], vec![], vec![]),
109 "clients show": cmd("Show one client", &["clients get"], slug("Client slug"), vec![]),
110 "clients delete": cmd("Delete a client", &["clients rm"], slug("Client slug"), vec![]),
111 "new": cmd("Create a contract from a clause pack", &["contracts new", "contracts create"], vec![], new_opts),
112 "list": cmd("List contracts", &["ls", "contracts list"], vec![], vec![
113 opt("--kind", "string", "Filter by kind"),
114 opt("--status", "string", "Filter by status"),
115 opt("--as", "string", "Filter by issuer slug"),
116 ]),
117 "show": cmd("Show one contract + clause list", &["get", "contracts show"], number(), vec![]),
118 "edit": cmd("Edit DRAFT contract metadata (sent/signed are immutable)", &["contracts edit"], number(), vec![
119 opt("--client", "string", "Re-point at a different client slug"),
120 opt("--title", "string", "New title"),
121 opt("--effective", "string", "Effective date YYYY-MM-DD"),
122 opt("--end", "string", "End date (clears term-months)"),
123 opt("--term-months", "int", "Term months (clears end date)"),
124 opt("--governing-law", "string", "Governing law"),
125 opt("--venue", "string", "Venue"),
126 opt("--fee", "string", "type:amount:currency"),
127 opt("--fee-schedule", "string", "on-completion | monthly | on-milestone | upon-invoice"),
128 opt("--term", "string", "Arbitrary term key=value (repeatable)"),
129 opt("--notes", "string", "Internal notes"),
130 opt("--template", "string", "Default template"),
131 ]),
132 "render": cmd("Render to PDF (DRAFT watermark unless executed or --final)", &["contracts render"], number(), render_opts),
133 "mark": cmd("Update status: draft|sent|signed|active|expired|terminated (forward-only once executed)", &["contracts mark"], vec![
134 arg("number", "string", true, "Contract number"),
135 arg("status", "string", true, "Target status"),
136 ], vec![]),
137 "sign": cmd("Record one party's signature; auto-bumps to signed when both sides sign", &["contracts sign"], number(), vec![
138 req_opt("--side", "string", "us | them"),
139 req_opt("--name", "string", "Signer full name"),
140 opt("--title", "string", "Signer title"),
141 opt("--date", "string", "Signature date YYYY-MM-DD (default today)"),
142 opt("--force", "bool", "Overwrite an already-recorded signature"),
143 ]),
144 "contracts clauses list": cmd("Show clauses attached to a contract", &["contracts clauses ls"], number(), vec![]),
145 "contracts clauses add": cmd("Add a pack clause, or a custom clause via --body/--from-file", &[], vec![
146 arg("number", "string", true, "Contract number"),
147 arg("slug", "string", true, "Clause slug"),
148 ], vec![
149 opt("--heading", "string", "Override heading"),
150 opt("--body", "string", "Custom body markdown"),
151 opt("--from-file", "string", "Custom body markdown from file"),
152 opt("--position", "int", "Zero-indexed insert position (default: end)"),
153 ]),
154 "contracts clauses edit": cmd("Override heading/body of an attached clause", &[], vec![
155 arg("number", "string", true, "Contract number"),
156 arg("slug", "string", true, "Clause slug"),
157 ], vec![
158 opt("--heading", "string", "New heading"),
159 opt("--body", "string", "New body markdown"),
160 opt("--from-file", "string", "New body markdown from file"),
161 ]),
162 "contracts clauses remove": cmd("Remove a clause", &["contracts clauses rm"], vec![
163 arg("number", "string", true, "Contract number"),
164 arg("slug", "string", true, "Clause slug"),
165 ], vec![]),
166 "contracts clauses move": cmd("Re-order a clause", &[], vec![
167 arg("number", "string", true, "Contract number"),
168 arg("slug", "string", true, "Clause slug"),
169 arg("position", "int", true, "New zero-indexed position"),
170 ], vec![]),
171 "contracts clauses reset": cmd("Reset the clause set to pack default", &[], number(), vec![]),
172 "duplicate": cmd("Clone a contract as a fresh draft (end date cleared, term kept)", &["clone", "contracts duplicate"], number(), vec![
173 opt("--client", "string", "Retarget to another client"),
174 opt("--as", "string", "Retarget to another issuer"),
175 ]),
176 "delete": cmd("Delete a contract (draft only unless --force)", &["rm", "contracts delete"], number(), vec![
177 opt("--force", "bool", "Allow deleting a non-draft contract"),
178 ]),
179 "pack list": cmd("List kind/pack combinations", &["pack ls", "packs list"], vec![], vec![]),
180 "pack show": cmd("Show available clauses for a kind+pack", &["pack get"], vec![arg("kind", "string", true, "Contract kind")], vec![
181 opt("--pack", "string", "Pack slug (default: standard)"),
182 ]),
183 "template list": cmd("List PDF templates with descriptions, moods, tags, fonts", &["template ls", "templates list"], vec![], vec![]),
184 "template find": cmd("Rank templates against a free-text description of the look (local scoring; caller picks)", &["template suggest"], vec![
185 arg("query", "string", true, "e.g. 'magazine masthead' or 'swiss minimal'"),
186 ], vec![]),
187 "template preview": cmd("Render a sample contract PDF with synthetic data", &[], vec![arg("name", "string", true, "Template name")], vec![
188 opt("--kind", "string", "Contract kind to preview (default consulting)"),
189 opt("--out", "string", "Output path"),
190 ]),
191 "kinds list": cmd("List contract kinds with descriptions and trigger tags", &["kinds ls", "kind list"], vec![], vec![]),
192 "kinds find": cmd("Rank contract kinds against a free-text description of the need (local scoring; caller picks)", &["kinds suggest"], vec![
193 arg("query", "string", true, "e.g. 'stop them going around me to my contact'"),
194 ], vec![]),
195 "config show": cmd("Display current configuration", &[], vec![], vec![]),
196 "config path": cmd("Print the config file path", &[], vec![], vec![]),
197 "config set": cmd("Set a config key", &[], vec![
198 arg("key", "string", true, "default_issuer | default_template | open_pdf | self_update"),
199 arg("value", "string", true, "New value"),
200 ], vec![]),
201 "agent-info": cmd("This manifest", &["info"], vec![], vec![]),
202 "doctor": cmd("Check typst, DB, packs, templates, shared entities", &[], vec![], vec![]),
203 "skill install": cmd("Install the embedded skill for Claude/Codex/Gemini", &[], vec![], vec![]),
204 "skill status": cmd("Report where the skill is installed and whether it is current", &[], vec![], vec![]),
205 "update": cmd("Distribution-aware update via brew or cargo", &[], vec![], vec![
206 opt("--check", "bool", "Check only — never mutates"),
207 ]),
208 });
209
210 let templates = crate::typst_assets::list_template_meta().unwrap_or_default();
211 let packs = crate::clauses::list_packs();
212 let kinds: Vec<serde_json::Value> = crate::kinds::KINDS
213 .iter()
214 .map(|k| json!({ "kind": k.kind, "description": k.description, "tags": k.tags }))
215 .collect();
216
217 let manifest = json!({
218 "name": "contract",
219 "version": env!("CARGO_PKG_VERSION"),
220 "description": env!("CARGO_PKG_DESCRIPTION"),
221 "commands": commands,
222 "global_flags": {
223 "--json": { "description": "Force JSON envelope output (auto-enabled when piped)", "type": "bool", "default": false },
224 "--quiet": { "description": "Suppress informational human output", "type": "bool", "default": false },
225 },
226 "exit_codes": {
227 "0": "Success",
228 "1": "Transient error (IO, render) — retry",
229 "2": "Config error — fix setup",
230 "3": "Bad input / not found / ambiguous — fix arguments",
231 "4": "Rate limited — wait and retry",
232 },
233 "envelope": {
234 "version": "1",
235 "success": "{ version, status, data }",
236 "error": "{ version, status, error: { code, message, suggestion } }",
237 },
238 "config": {
239 "path": config_path,
240 "env_prefix": "PAPERFOOT_",
241 },
242 "auto_json_when_piped": true,
243 "state_dir": state_dir,
244 "database": database,
245 "templates": templates,
246 "packs": packs.into_iter().map(|(k, p)| json!({"kind": k, "pack": p})).collect::<Vec<_>>(),
247 "kinds": kinds,
248 "fee_spec": "type:amount:currency — e.g. fixed:8400:SGD | hourly:200:SGD | daily:1500:SGD | retainer:5000:SGD",
249 "first_run": [
250 "contract doctor --json",
251 "contract issuer list --json # CHECK FIRST — shared with invoice-cli",
252 "contract clients list --json # CHECK FIRST — shared with invoice-cli",
253 "# Only if a needed issuer/client is missing, then:",
254 "contract issuer add <slug> --name <display-name> --jurisdiction sg|uk|us|eu --address \"line1\\nline2\"",
255 "contract clients add <slug> --name <client-name> --legal-name <legal> --address \"line1\\nline2\"",
256 "contract new --kind nda --as <issuer> --client <client> --purpose 'description'",
257 "contract render <number> --open",
258 ],
259 "shared_state": {
260 "database": database,
261 "shared_with": ["invoice-cli (binary: invoice)"],
262 "shared_tables": ["issuers", "clients", "number_series"],
263 "discovery_workflow": "Before creating a new issuer or client, ALWAYS run `contract issuer list --json` and `contract clients list --json` (or the invoice-cli equivalents). The accounting suite's whole point is one source of truth — duplicating entities here pollutes invoicing too. If the entity exists under a different slug, prefer using the existing slug over creating a new one.",
264 "legal_fields_on_clients": "Three columns added in V7 are contract-specific: `legal_name`, `company_no`, `legal_jurisdiction`. If an existing client (from invoice-cli) is missing them, fill via: contract clients edit <slug> --legal-name X --company-no Y --jurisdiction Z."
265 },
266 "examples": [
267 { "goal": "Quick mutual NDA",
268 "command": "contract new --kind nda --as acme --client meridian --purpose 'evaluation of a joint product' --term-years 3" },
269 { "goal": "Non-circumvention agreement protecting an introduction",
270 "command": "contract new --kind ncnda --as boris --client partner --purpose 'introduction to prospective lenders for the transaction' --term-years 2" },
271 { "goal": "Consulting agreement with fixed fee",
272 "command": "contract new --kind consulting --as acme --client meridian --purpose 'design a dashboard' --fee fixed:8400:SGD --term-months 3 --deliverable 'Design' --deliverable 'Build'" },
273 { "goal": "Interest-free loan with fixed repayment date",
274 "command": "contract new --kind loan --as boris --client friend --term principal_text='£10,000 (ten thousand pounds sterling)' --term repayment_date=2026-12-01 --term interest_text='interest-free'" },
275 { "goal": "Pick a template by describing the look",
276 "command": "contract template find \"magazine masthead serif\"" },
277 { "goal": "Render with no watermark",
278 "command": "contract render NDA-acme-2026-0001 --final --open" },
279 ],
280 "guardrails": [
281 "BEFORE creating any issuer or client, run `contract issuer list` and `contract clients list` — the DB is shared with invoice-cli; duplicates pollute both tools.",
282 "Run doctor before first use.",
283 "Use --json for agents; stdout is data, stderr is diagnostics.",
284 "Drafts render with a DRAFT watermark by default. Use --final to suppress before sending out for signature.",
285 "Once a contract is sent/signed, only the status / signature columns can change; executed contracts only move forward.",
286 "These clauses are practical plain-English starting points, not legal advice. Have a lawyer review for material engagements.",
287 ],
288 });
289 print_raw(&manifest);
290 Ok(())
291}