chasm_cli/commands/
export_import.rs1use anyhow::{Context, Result};
6use colored::*;
7use std::path::Path;
8
9use crate::workspace::{get_workspace_by_hash, get_workspace_by_path};
10
11pub fn export_sessions(destination: &str, hash: Option<&str>, path: Option<&str>) -> Result<()> {
13 let workspace = if let Some(h) = hash {
14 get_workspace_by_hash(h)?.context(format!("Workspace not found with hash: {}", h))?
15 } else if let Some(p) = path {
16 get_workspace_by_path(p)?.context(format!("Workspace not found for path: {}", p))?
17 } else {
18 anyhow::bail!("Must specify either --hash or --path");
19 };
20
21 if !workspace.has_chat_sessions {
22 println!("No chat sessions to export.");
23 return Ok(());
24 }
25
26 let dest_path = Path::new(destination);
28 std::fs::create_dir_all(dest_path)?;
29
30 let mut exported_count = 0;
32 for entry in std::fs::read_dir(&workspace.chat_sessions_path)? {
33 let entry = entry?;
34 let src_path = entry.path();
35
36 if src_path.extension().map(|e| e == "json").unwrap_or(false) {
37 let dest_file = dest_path.join(entry.file_name());
38 std::fs::copy(&src_path, &dest_file)?;
39 exported_count += 1;
40 }
41 }
42
43 println!(
44 "{} Exported {} chat session(s) to {}",
45 "[OK]".green(),
46 exported_count,
47 destination
48 );
49
50 Ok(())
51}
52
53pub fn import_sessions(
55 source: &str,
56 hash: Option<&str>,
57 path: Option<&str>,
58 force: bool,
59) -> Result<()> {
60 let src_path = Path::new(source);
61 if !src_path.exists() {
62 anyhow::bail!("Source path not found: {}", source);
63 }
64
65 let workspace = if let Some(h) = hash {
66 get_workspace_by_hash(h)?.context(format!("Workspace not found with hash: {}", h))?
67 } else if let Some(p) = path {
68 get_workspace_by_path(p)?.context(format!("Workspace not found for path: {}", p))?
69 } else {
70 anyhow::bail!("Must specify either --hash or --path");
71 };
72
73 std::fs::create_dir_all(&workspace.chat_sessions_path)?;
75
76 let mut imported_count = 0;
78 let mut skipped_count = 0;
79
80 for entry in std::fs::read_dir(src_path)? {
81 let entry = entry?;
82 let src_file = entry.path();
83
84 if src_file.extension().map(|e| e == "json").unwrap_or(false) {
85 let dest_file = workspace.chat_sessions_path.join(entry.file_name());
86
87 if dest_file.exists() && !force {
88 skipped_count += 1;
89 } else {
90 std::fs::copy(&src_file, &dest_file)?;
91 imported_count += 1;
92 }
93 }
94 }
95
96 println!(
97 "{} Imported {} chat session(s)",
98 "[OK]".green(),
99 imported_count
100 );
101 if skipped_count > 0 {
102 println!(
103 "{} Skipped {} existing session(s). Use --force to overwrite.",
104 "[!]".yellow(),
105 skipped_count
106 );
107 }
108
109 Ok(())
110}
111
112pub fn move_sessions(source_hash: &str, target_path: &str) -> Result<()> {
114 let source_ws = get_workspace_by_hash(source_hash)?
115 .context(format!("Source workspace not found: {}", source_hash))?;
116
117 let target_ws = get_workspace_by_path(target_path)?.context(format!(
118 "Target workspace not found for path: {}",
119 target_path
120 ))?;
121
122 if !source_ws.has_chat_sessions {
123 println!("No chat sessions to move.");
124 return Ok(());
125 }
126
127 std::fs::create_dir_all(&target_ws.chat_sessions_path)?;
129
130 let mut moved_count = 0;
132 for entry in std::fs::read_dir(&source_ws.chat_sessions_path)? {
133 let entry = entry?;
134 let src_file = entry.path();
135
136 if src_file.extension().map(|e| e == "json").unwrap_or(false) {
137 let dest_file = target_ws.chat_sessions_path.join(entry.file_name());
138 std::fs::rename(&src_file, &dest_file)?;
139 moved_count += 1;
140 }
141 }
142
143 println!(
144 "{} Moved {} chat session(s) to {}",
145 "[OK]".green(),
146 moved_count,
147 target_path
148 );
149
150 Ok(())
151}
152
153pub fn export_specific_sessions(
155 destination: &str,
156 session_ids: &[String],
157 project_path: Option<&str>,
158) -> Result<()> {
159 use crate::workspace::{discover_workspaces, get_chat_sessions_from_workspace, normalize_path};
160
161 let dest_path = Path::new(destination);
162 std::fs::create_dir_all(dest_path)?;
163
164 let workspaces = discover_workspaces()?;
165
166 let filtered: Vec<_> = if let Some(path) = project_path {
168 let normalized = normalize_path(path);
169 workspaces
170 .into_iter()
171 .filter(|ws| {
172 ws.project_path
173 .as_ref()
174 .map(|p| normalize_path(p) == normalized)
175 .unwrap_or(false)
176 })
177 .collect()
178 } else {
179 workspaces
180 };
181
182 let normalized_ids: Vec<String> = session_ids
183 .iter()
184 .flat_map(|s| s.split(',').map(|p| p.trim().to_lowercase()))
185 .filter(|s| !s.is_empty())
186 .collect();
187
188 let mut exported_count = 0;
189 let mut found_ids = Vec::new();
190
191 for ws in filtered {
192 if !ws.has_chat_sessions {
193 continue;
194 }
195
196 let sessions = get_chat_sessions_from_workspace(&ws.workspace_path)?;
197
198 for session in sessions {
199 let session_id = session.session.session_id.clone().unwrap_or_else(|| {
200 session
201 .path
202 .file_stem()
203 .map(|s| s.to_string_lossy().to_string())
204 .unwrap_or_default()
205 });
206
207 let matches = normalized_ids.iter().any(|req_id| {
208 session_id.to_lowercase().contains(req_id)
209 || req_id.contains(&session_id.to_lowercase())
210 });
211
212 if matches && !found_ids.contains(&session_id) {
213 let filename = session
214 .path
215 .file_name()
216 .map(|n| n.to_string_lossy().to_string())
217 .unwrap_or_default();
218
219 let dest_file = dest_path.join(&filename);
220 std::fs::copy(&session.path, &dest_file)?;
221 exported_count += 1;
222 found_ids.push(session_id);
223 println!(
224 " {} Exported: {}",
225 "[OK]".green(),
226 session.session.title()
227 );
228 }
229 }
230 }
231
232 println!(
233 "\n{} Exported {} session(s) to {}",
234 "[OK]".green().bold(),
235 exported_count,
236 destination
237 );
238
239 Ok(())
240}
241
242pub fn import_specific_sessions(
244 session_files: &[String],
245 target_path: Option<&str>,
246 force: bool,
247) -> Result<()> {
248 let target_ws = if let Some(path) = target_path {
249 get_workspace_by_path(path)?.context(format!("Workspace not found for path: {}", path))?
250 } else {
251 let cwd = std::env::current_dir()?;
252 get_workspace_by_path(cwd.to_str().unwrap_or(""))?
253 .context("Current directory is not a VS Code workspace")?
254 };
255
256 std::fs::create_dir_all(&target_ws.chat_sessions_path)?;
257
258 let mut imported_count = 0;
259 let mut skipped_count = 0;
260
261 for file_path in session_files {
262 let src_path = Path::new(file_path);
263
264 if !src_path.exists() {
265 println!("{} File not found: {}", "[!]".yellow(), file_path);
266 continue;
267 }
268
269 let filename = src_path
270 .file_name()
271 .map(|n| n.to_string_lossy().to_string())
272 .unwrap_or_default();
273
274 let dest_file = target_ws.chat_sessions_path.join(&filename);
275
276 if dest_file.exists() && !force {
277 println!(" {} Skipping (exists): {}", "[!]".yellow(), filename);
278 skipped_count += 1;
279 } else {
280 std::fs::copy(src_path, &dest_file)?;
281 imported_count += 1;
282 println!(" {} Imported: {}", "[OK]".green(), filename);
283 }
284 }
285
286 println!(
287 "\n{} Imported {} session(s)",
288 "[OK]".green().bold(),
289 imported_count
290 );
291 if skipped_count > 0 {
292 println!(
293 "{} Skipped {} existing. Use --force to overwrite.",
294 "[!]".yellow(),
295 skipped_count
296 );
297 }
298
299 Ok(())
300}
301
302pub fn move_workspace(source_hash: &str, target: &str) -> Result<()> {
304 let target_ws = get_workspace_by_path(target)?
306 .or_else(|| get_workspace_by_hash(target).ok().flatten())
307 .context(format!("Target workspace not found: {}", target))?;
308
309 move_sessions(
310 source_hash,
311 target_ws
312 .project_path
313 .as_ref()
314 .unwrap_or(&target.to_string()),
315 )
316}
317
318pub fn move_specific_sessions(session_ids: &[String], target_path: &str) -> Result<()> {
320 use crate::workspace::{discover_workspaces, get_chat_sessions_from_workspace, normalize_path};
321
322 let target_ws = get_workspace_by_path(target_path)?
323 .context(format!("Target workspace not found: {}", target_path))?;
324
325 std::fs::create_dir_all(&target_ws.chat_sessions_path)?;
326
327 let workspaces = discover_workspaces()?;
328
329 let normalized_ids: Vec<String> = session_ids
330 .iter()
331 .flat_map(|s| s.split(',').map(|p| p.trim().to_lowercase()))
332 .filter(|s| !s.is_empty())
333 .collect();
334
335 let mut moved_count = 0;
336 let mut found_ids = Vec::new();
337
338 for ws in workspaces {
339 if !ws.has_chat_sessions {
340 continue;
341 }
342
343 if ws
345 .project_path
346 .as_ref()
347 .map(|p| normalize_path(p) == normalize_path(target_path))
348 .unwrap_or(false)
349 {
350 continue;
351 }
352
353 let sessions = get_chat_sessions_from_workspace(&ws.workspace_path)?;
354
355 for session in sessions {
356 let session_id = session.session.session_id.clone().unwrap_or_else(|| {
357 session
358 .path
359 .file_stem()
360 .map(|s| s.to_string_lossy().to_string())
361 .unwrap_or_default()
362 });
363
364 let matches = normalized_ids.iter().any(|req_id| {
365 session_id.to_lowercase().contains(req_id)
366 || req_id.contains(&session_id.to_lowercase())
367 });
368
369 if matches && !found_ids.contains(&session_id) {
370 let filename = session
371 .path
372 .file_name()
373 .map(|n| n.to_string_lossy().to_string())
374 .unwrap_or_default();
375
376 let dest_file = target_ws.chat_sessions_path.join(&filename);
377 std::fs::rename(&session.path, &dest_file)?;
378 moved_count += 1;
379 found_ids.push(session_id);
380 println!(" {} Moved: {}", "[OK]".green(), session.session.title());
381 }
382 }
383 }
384
385 println!(
386 "\n{} Moved {} session(s) to {}",
387 "[OK]".green().bold(),
388 moved_count,
389 target_path
390 );
391
392 Ok(())
393}
394
395pub fn move_by_path(source_path: &str, target_path: &str) -> Result<()> {
397 let source_ws = get_workspace_by_path(source_path)?
398 .context(format!("Source workspace not found: {}", source_path))?;
399
400 let target_ws = get_workspace_by_path(target_path)?
401 .context(format!("Target workspace not found: {}", target_path))?;
402
403 if !source_ws.has_chat_sessions {
404 println!("No chat sessions to move.");
405 return Ok(());
406 }
407
408 std::fs::create_dir_all(&target_ws.chat_sessions_path)?;
409
410 let mut moved_count = 0;
411 for entry in std::fs::read_dir(&source_ws.chat_sessions_path)? {
412 let entry = entry?;
413 let src_file = entry.path();
414
415 if src_file.extension().map(|e| e == "json").unwrap_or(false) {
416 let dest_file = target_ws.chat_sessions_path.join(entry.file_name());
417 std::fs::rename(&src_file, &dest_file)?;
418 moved_count += 1;
419 }
420 }
421
422 println!(
423 "{} Moved {} chat session(s) from {} to {}",
424 "[OK]".green(),
425 moved_count,
426 source_path,
427 target_path
428 );
429
430 Ok(())
431}