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