fur_cli/commands/
new.rs

1use std::fs::{self, File};
2use std::io::{self, Write};
3use std::path::Path;
4use serde_json::{json, Value};
5use uuid::Uuid;
6use colored::*;
7
8use crate::schema::{make_index_metadata, make_thread_metadata};
9use crate::frs::avatars::{load_avatars, save_avatars, get_random_emoji_for_name};
10
11fn init_fur_dir(fur_dir: &Path) -> io::Result<()> {
12    fs::create_dir_all(fur_dir.join("threads"))?;
13    fs::create_dir_all(fur_dir.join("messages"))?;
14
15    let mut f = File::create(fur_dir.join("index.json"))?;
16    let initial_index = make_index_metadata();
17    f.write_all(serde_json::to_string_pretty(&initial_index)?.as_bytes())?;
18    Ok(())
19}
20
21pub fn onboarding_interactive() -> (String, String) {
22    // Main avatar
23    println!("\n{}", "== Main Avatar ==".bright_magenta().bold());
24    println!(
25        "{}",
26        "This is YOU (or your team). The default voice in this thread.\n\
27         Whenever you jot without specifying an avatar, it will be attributed here."
28            .bright_cyan()
29    );
30    print!("{}", "Main avatar name [me]: ");
31    io::stdout().flush().unwrap();
32    let mut main_in = String::new();
33    io::stdin().read_line(&mut main_in).unwrap();
34    let mut main_name = main_in.trim().to_string();
35    if main_name.is_empty() {
36        main_name = "me".to_string();
37    } else if main_name == "main" {
38        println!(
39            "{}",
40            "[WARN] 'main' is reserved as a pointer. Using 'me' instead."
41                .yellow()
42                .bold()
43        );
44        main_name = "me".to_string();
45    }
46    println!(
47        "{}",
48        format!("[OK] Main avatar set: {}", main_name)
49            .bright_green()
50            .bold()
51    );
52
53    // === Secondary Avatar ===
54    println!("\n{}", "== Secondary Avatar ==".bright_magenta().bold());
55    println!(
56        "{}",
57        "You can't have a conversation with one person.\n\
58         Let's log at least one other avatar. This could be an AI, your boss, your therapist, or karen_from_hr."
59            .bright_cyan()
60    );
61    print!("{}", "Another avatar [ai]: ");
62    io::stdout().flush().unwrap();
63    let mut other_in = String::new();
64    io::stdin().read_line(&mut other_in).unwrap();
65    let other_name = {
66        let trimmed = other_in.trim();
67        if trimmed.is_empty() {
68            "ai".to_string()
69        } else {
70            trimmed.to_string()
71        }
72    };
73    println!(
74        "{}",
75        format!("[OK] Other avatar set: {}", other_name)
76            .bright_green()
77            .bold()
78    );
79
80    (main_name, other_name)
81}
82
83/// Non-interactive onboarding (useful for scripts or tests)
84pub fn onboarding_auto(main: &str, other: &str) {
85    let mut avatars = load_avatars();
86    avatars["main"] = json!(main);
87    avatars[main] = json!("🦊");
88    avatars[other] = json!(get_random_emoji_for_name(other));
89    save_avatars(&avatars);
90}
91
92
93
94fn run_new_internal(
95    name: String,
96    auto: bool,
97    main_avatar: Option<String>,
98    other_avatar: Option<String>,
99) {
100    let fur_dir = Path::new(".fur");
101
102    if !fur_dir.exists() {
103        init_fur_dir(fur_dir).expect("Failed to create .fur structure");
104        println!("{}", "[INIT] .fur/ directory created".bright_green().bold());
105
106        if auto {
107            onboarding_auto(
108                main_avatar.as_deref().unwrap_or("me"),
109                other_avatar.as_deref().unwrap_or("ai"),
110            );
111        } else {
112            let (main, other) = onboarding_interactive();
113            onboarding_auto(&main, &other);
114        }
115
116        println!(
117            "\n{}",
118            "Ready! Use:\n  fur jot <your message>\n  fur jot <other avatar> <their message>"
119                .bright_cyan()
120        );
121    }
122
123    let thread_id = Uuid::new_v4().to_string();
124    let thread_meta = make_thread_metadata(&name, &thread_id);
125
126    // --- Write thread ---
127    let thread_path = fur_dir.join("threads").join(format!("{}.json", thread_id));
128    fs::write(&thread_path, serde_json::to_string_pretty(&thread_meta).unwrap())
129        .expect("Could not write thread file");
130
131    // --- Update index ---
132    let index_path = fur_dir.join("index.json");
133    let mut index: Value = serde_json::from_str(&fs::read_to_string(&index_path).unwrap()).unwrap();
134
135    if let Some(arr) = index["threads"].as_array_mut() {
136        arr.push(json!(thread_id.clone()));
137    } else {
138        index["threads"] = json!([thread_id.clone()]);
139    }
140    index["active_thread"] = json!(thread_id.clone());
141    index["current_message"] = Value::Null;
142
143    fs::write(&index_path, serde_json::to_string_pretty(&index).unwrap()).unwrap();
144
145    println!(
146        "{}",
147        format!("[NEW] Thread created: {} — \"{}\"", &thread_id[..8], name)
148            .bright_green()
149            .bold()
150    );
151}
152
153// === legacy-compatible wrapper ===
154pub fn run_new(name: String) {
155    run_new_internal(name, false, None, None);
156}
157
158