fur_cli/commands/
xclone.rs1use std::fs;
2use std::path::{Path};
3use serde_json::{Value, json};
4use crate::helpers::cloning::{
5 load_conversation_metadata,
6 make_new_conversation_header,
7 build_id_remap,
8};
9
10fn ensure_target_project(dst_root: &Path) {
12 let threads = dst_root.join("threads");
13 let messages = dst_root.join("messages");
14 let chats = dst_root.parent().unwrap().join("chats"); fs::create_dir_all(&threads).unwrap();
17 fs::create_dir_all(&messages).unwrap();
18 fs::create_dir_all(&chats).unwrap();
19}
20
21fn clone_messages_into_target(
23 src_msgs_dir: &Path,
24 dst_msgs_dir: &Path,
25 id_map: &std::collections::HashMap<String, String>,
26 old_messages: &[String],
27) {
28 for old_id in old_messages {
29 let src_path = src_msgs_dir.join(format!("{}.json", old_id));
30
31 let old_msg: Value =
32 serde_json::from_str(&fs::read_to_string(&src_path).unwrap()).unwrap();
33
34 let new_id = id_map.get(old_id).unwrap();
35 let mut new_msg = old_msg.clone();
36
37 new_msg["id"] = json!(new_id);
39
40 if let Some(p) = old_msg["parent"].as_str() {
42 new_msg["parent"] = id_map.get(p).map(|s| json!(s)).unwrap_or(json!(null));
43 } else {
44 new_msg["parent"] = json!(null);
45 }
46
47 if let Some(children) = old_msg["children"].as_array() {
48 new_msg["children"] = json!(children.iter()
49 .filter_map(|v| v.as_str())
50 .filter_map(|id| id_map.get(id))
51 .collect::<Vec<_>>());
52 }
53
54 if let Some(branches) = old_msg["branches"].as_array() {
55 new_msg["branches"] = json!(branches.iter()
56 .filter_map(|v| v.as_str())
57 .filter_map(|id| id_map.get(id))
58 .collect::<Vec<_>>());
59 }
60
61 if let Some(md_rel) = old_msg["markdown"].as_str() {
63 let src_md = src_msgs_dir.parent().unwrap().parent().unwrap().join(md_rel);
64 let dst_md = dst_msgs_dir
65 .parent().unwrap().parent().unwrap() .join(md_rel); if src_md.exists() {
69 fs::create_dir_all(dst_md.parent().unwrap()).unwrap();
70 fs::copy(&src_md, &dst_md)
71 .expect("❌ Failed to copy markdown attachment");
72 }
73
74 new_msg["markdown"] = json!(md_rel);
75 } else {
76 new_msg["markdown"] = json!(null);
77 }
78
79 let dst_path = dst_msgs_dir.join(format!("{}.json", new_id));
81 fs::write(dst_path, serde_json::to_string_pretty(&new_msg).unwrap()).unwrap();
82 }
83}
84
85fn write_new_convo_into_target(
87 dst_root: &Path,
88 new_tid: &str,
89 new_title: &str,
90 timestamp: &str,
91 id_map: &std::collections::HashMap<String, String>,
92 old_messages: &[String],
93) {
94 let threads_dir = dst_root.join("threads");
95
96 let new_messages: Vec<String> = old_messages
97 .iter()
98 .map(|old| id_map.get(old).unwrap().clone())
99 .collect();
100
101 let convo = json!({
102 "id": new_tid,
103 "title": new_title,
104 "created_at": timestamp,
105 "messages": new_messages,
106 "tags": []
107 });
108
109 let new_path = threads_dir.join(format!("{}.json", new_tid));
110 fs::write(new_path, serde_json::to_string_pretty(&convo).unwrap()).unwrap();
111}
112
113fn update_target_index(dst_root: &Path, new_tid: &str) {
115 let index_path = dst_root.join("index.json");
116
117 let mut index: Value = if index_path.exists() {
118 serde_json::from_str(&fs::read_to_string(&index_path).unwrap()).unwrap()
119 } else {
120 json!({
122 "threads": [],
123 "active_thread": null,
124 "current_message": null,
125 "created_at": chrono::Utc::now().to_rfc3339()
126 })
127 };
128
129 index["threads"].as_array_mut().unwrap().push(json!(new_tid));
130 index["active_thread"] = json!(new_tid);
131 index["current_message"] = json!(null);
132
133 fs::write(index_path, serde_json::to_string_pretty(&index).unwrap()).unwrap();
134}
135
136pub fn run_xclone(to: &str, tid: &str, title: Option<String>) {
138 let src_root = Path::new(".fur");
139 let dst_root = Path::new(to).join(".fur");
140
141 ensure_target_project(&dst_root);
142
143 let src_convo_path = src_root.join("threads").join(format!("{}.json", tid));
144 if !src_convo_path.exists() {
145 eprintln!("❌ Conversation {} not found.", tid);
146 return;
147 }
148
149 let (old_title, old_messages) = load_conversation_metadata(&src_convo_path);
150
151 let (new_tid, new_title, timestamp) =
152 make_new_conversation_header(&old_title, title);
153
154 let id_map = build_id_remap(&old_messages);
155
156 clone_messages_into_target(
157 &src_root.join("messages"),
158 &dst_root.join("messages"),
159 &id_map,
160 &old_messages,
161 );
162
163 write_new_convo_into_target(
164 &dst_root,
165 &new_tid,
166 &new_title,
167 ×tamp,
168 &id_map,
169 &old_messages,
170 );
171
172 update_target_index(&dst_root, &new_tid);
173
174 println!(
175 "🌀 Deep-cloned conversation \"{}\" → {} into {}",
176 old_title,
177 &new_tid[..8],
178 dst_root.display()
179 );
180}