mastermind_cli/
lib.rs

1use clap::Parser;
2use std::fs;
3use std::path::PathBuf;
4
5pub mod api;
6pub mod clues;
7pub mod configs;
8pub mod model_collection;
9
10mod json;
11
12/// Mastermind - An LLM-powered CLI tool to help you be a better spymaster in Codenames
13#[derive(Parser)]
14#[command(version, about, long_about = None)]
15pub struct Args {
16    /// Print all available language models
17    #[arg(short, long = "get-models")]
18    pub get: bool,
19
20    /// Select language model(s)
21    #[arg(short, long = "set-models", default_missing_value = "interactive", num_args = 0..)]
22    pub models: Option<Vec<String>>,
23
24    /// Specify an output file
25    #[arg(short, long, value_name = "FILE")]
26    pub output: Option<PathBuf>,
27
28    /// Print token usage information
29    #[arg(short, long = "token-usage")]
30    pub token: bool,
31
32    /// File containing words to link together - the words from your team
33    #[arg(required_unless_present = "get")]
34    pub to_link: Option<PathBuf>,
35
36    /// File containing words to avoid - opponent's words, neutral words, and the assassin word
37    #[arg(required_unless_present = "get")]
38    pub to_avoid: Option<PathBuf>,
39}
40
41pub fn read_words_from_file(path: &PathBuf) -> Result<Vec<String>, Box<dyn std::error::Error>> {
42    let contents = fs::read_to_string(path)
43        .map_err(|_| format!("Cannot find file: {}", path.to_string_lossy()))?;
44
45    let words: Vec<String> = contents
46        .lines()
47        .map(|line| line.trim().to_string())
48        .filter(|line| !line.is_empty())
49        .collect();
50
51    if words.is_empty() {
52        Err(format!("File is empty: {}", path.to_string_lossy()).into())
53    } else {
54        Ok(words)
55    }
56}
57
58pub fn write_content_to_file(
59    path: &PathBuf,
60    content: String,
61) -> Result<(), Box<dyn std::error::Error>> {
62    if let Ok(existing_content) = fs::read_to_string(path) {
63        if !existing_content.is_empty() {
64            return Err(format!("File is not empty: {}", path.to_string_lossy()).into());
65        }
66    }
67
68    fs::write(path, content)
69        .map_err(|_| format!("Failed to write to file: {}", path.to_string_lossy()))?;
70
71    Ok(())
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use tempfile::tempdir;
78
79    #[test]
80    fn test_read_words_from_file() {
81        let to_link = read_words_from_file(&PathBuf::from("examples/link.txt"));
82        assert!(to_link.is_ok());
83        let to_avoid = read_words_from_file(&PathBuf::from("examples/avoid.txt"));
84        assert!(to_avoid.is_ok());
85    }
86
87    #[test]
88    fn test_write_content_to_file() {
89        // Invalid path
90        let write_result = write_content_to_file(
91            &PathBuf::from("/none/existent/path/lol"),
92            String::from("useless text"),
93        );
94        assert!(write_result.is_err());
95
96        // Successful write
97        let temp_dir = tempdir().unwrap();
98        let temp_file = temp_dir.path().join("temp.txt");
99        write_content_to_file(&temp_file, String::from("some text")).unwrap();
100        assert_eq!(fs::read_to_string(&temp_file).unwrap(), "some text");
101
102        // Non-empty file
103        let write_result = write_content_to_file(&temp_file, String::from("some text again"));
104        assert!(write_result.is_err());
105    }
106}