Skip to main content

contract_cli/commands/
skill.rs

1use crate::cli::SkillCmd;
2use crate::error::Result;
3use crate::output::{print_success, Ctx};
4
5// The skill is a signpost, not a manual: the binary carries all workflow
6// knowledge in agent-info and --help, so the skill body stays tiny and the
7// description does the routing work.
8const SKILL_MD: &str = r#"---
9name: contract-cli
10description: >
11  Draft, render, and sign business contracts as beautiful PDFs — NDA, NCNDA
12  (non-circumvention), consulting, MSA, SOW, service, loan. Use when the user
13  asks to draft, generate, render, e-sign, or manage a contract, NDA, NCNDA,
14  or loan agreement. Pick a PDF template by look with `contract template find
15  "<description>"` and a contract kind with `contract kinds find "<need>"`.
16  Shares issuers/clients with invoice-cli. Run `contract agent-info` for all
17  commands, flags, and exit codes.
18---
19
20`contract` is a stateful CLI for drafting and rendering business contracts.
21
22- Run `contract agent-info | jq` for the full manifest (commands, exit codes, envelope).
23- The SQLite DB is SHARED with invoice-cli: ALWAYS `contract issuer list` and
24  `contract clients list` before creating entities — reuse existing slugs.
25- `contract kinds find "<what you need>"` and `contract template find "<look>"`
26  resolve fuzzy descriptions to exact names; you make the final pick.
27- Drafts render with a DRAFT watermark; `--final` produces the signing copy.
28- Not legal advice — suggest lawyer review for material engagements.
29"#;
30
31fn targets() -> [std::path::PathBuf; 3] {
32    [
33        dirs_path(".claude/skills/contract-cli/SKILL.md"),
34        dirs_path(".codex/skills/contract-cli/SKILL.md"),
35        dirs_path(".gemini/skills/contract-cli/SKILL.md"),
36    ]
37}
38
39pub fn run(cmd: SkillCmd, ctx: Ctx) -> Result<()> {
40    match cmd {
41        SkillCmd::Install => {
42            let mut written = Vec::new();
43            for t in targets() {
44                if let Some(parent) = t.parent() {
45                    std::fs::create_dir_all(parent)?;
46                }
47                std::fs::write(&t, SKILL_MD)?;
48                written.push(t.display().to_string());
49            }
50            print_success(ctx, &written, |paths| {
51                for p in paths {
52                    println!("installed → {}", p);
53                }
54            });
55            Ok(())
56        }
57        SkillCmd::Status => {
58            #[derive(serde::Serialize)]
59            struct SkillStatus {
60                path: String,
61                installed: bool,
62                current: bool,
63            }
64            let statuses: Vec<SkillStatus> = targets()
65                .iter()
66                .map(|t| {
67                    let on_disk = std::fs::read_to_string(t).ok();
68                    SkillStatus {
69                        path: t.display().to_string(),
70                        installed: on_disk.is_some(),
71                        current: on_disk.as_deref() == Some(SKILL_MD),
72                    }
73                })
74                .collect();
75            print_success(ctx, &statuses, |ss| {
76                for s in ss {
77                    let state = if s.current {
78                        "current"
79                    } else if s.installed {
80                        "STALE — run: contract skill install"
81                    } else {
82                        "not installed"
83                    };
84                    println!("  {} — {}", s.path, state);
85                }
86            });
87            Ok(())
88        }
89    }
90}
91
92fn dirs_path(rel: &str) -> std::path::PathBuf {
93    let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
94    std::path::PathBuf::from(home).join(rel)
95}