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
12fn is_session_file(path: &Path) -> bool {
14 path.extension()
15 .map(|e| e == "json" || e == "jsonl")
16 .unwrap_or(false)
17}
18
19pub fn export_sessions(destination: &str, hash: Option<&str>, path: Option<&str>) -> Result<()> {
21 let workspace = if let Some(h) = hash {
22 get_workspace_by_hash(h)?.context(format!("Workspace not found with hash: {}", h))?
23 } else if let Some(p) = path {
24 get_workspace_by_path(p)?.context(format!("Workspace not found for path: {}", p))?
25 } else {
26 anyhow::bail!("Must specify either --hash or --path");
27 };
28
29 if !workspace.has_chat_sessions {
30 println!("No chat sessions to export.");
31 return Ok(());
32 }
33
34 let dest_path = Path::new(destination);
36 std::fs::create_dir_all(dest_path)?;
37
38 let mut exported_count = 0;
40 for entry in std::fs::read_dir(&workspace.chat_sessions_path)? {
41 let entry = entry?;
42 let src_path = entry.path();
43
44 if is_session_file(&src_path) {
45 let dest_file = dest_path.join(entry.file_name());
46 std::fs::copy(&src_path, &dest_file)?;
47 exported_count += 1;
48 }
49 }
50
51 println!(
52 "{} Exported {} chat session(s) to {}",
53 "[OK]".green(),
54 exported_count,
55 destination
56 );
57
58 Ok(())
59}
60
61pub fn export_batch(destination: &str, project_paths: &[String]) -> Result<()> {
63 let dest_base = Path::new(destination);
64 std::fs::create_dir_all(dest_base)?;
65
66 let mut total_exported = 0;
67 let mut total_projects = 0;
68 let mut projects_with_sessions = 0;
69
70 println!(
71 "\n{} Batch Exporting Sessions",
72 "=".repeat(60).dimmed()
73 );
74 println!("{}", "=".repeat(60).dimmed());
75
76 for project_path in project_paths {
77 total_projects += 1;
78 let project_name = Path::new(project_path)
79 .file_name()
80 .map(|n| n.to_string_lossy().to_string())
81 .unwrap_or_else(|| "unknown".to_string());
82
83 print!(" {} {} ... ", "→".blue(), project_name);
84
85 match get_workspace_by_path(project_path) {
86 Ok(Some(workspace)) => {
87 if !workspace.has_chat_sessions {
88 println!("{}", "no sessions".dimmed());
89 continue;
90 }
91
92 let project_dest = dest_base.join(&project_name);
94 std::fs::create_dir_all(&project_dest)?;
95
96 let mut exported_count = 0;
98 for entry in std::fs::read_dir(&workspace.chat_sessions_path)? {
99 let entry = entry?;
100 let src_path = entry.path();
101
102 if is_session_file(&src_path) {
103 let dest_file = project_dest.join(entry.file_name());
104 std::fs::copy(&src_path, &dest_file)?;
105 exported_count += 1;
106 }
107 }
108
109 if exported_count > 0 {
110 projects_with_sessions += 1;
111 total_exported += exported_count;
112 println!("{} {} session(s)", "[OK]".green(), exported_count);
113 } else {
114 println!("{}", "no sessions".dimmed());
115 }
116 }
117 Ok(None) => {
118 println!("{}", "workspace not found".yellow());
119 }
120 Err(e) => {
121 println!("{} {}", "[ERR]".red(), e);
122 }
123 }
124 }
125
126 println!("{}", "=".repeat(60).dimmed());
127 println!(
128 "\n{} Exported {} session(s) from {}/{} project(s) to {}",
129 "[DONE]".green().bold(),
130 total_exported,
131 projects_with_sessions,
132 total_projects,
133 destination
134 );
135
136 Ok(())
137}
138
139pub fn import_sessions(
141 source: &str,
142 hash: Option<&str>,
143 path: Option<&str>,
144 force: bool,
145) -> Result<()> {
146 let src_path = Path::new(source);
147 if !src_path.exists() {
148 anyhow::bail!("Source path not found: {}", source);
149 }
150
151 let workspace = if let Some(h) = hash {
152 get_workspace_by_hash(h)?.context(format!("Workspace not found with hash: {}", h))?
153 } else if let Some(p) = path {
154 get_workspace_by_path(p)?.context(format!("Workspace not found for path: {}", p))?
155 } else {
156 anyhow::bail!("Must specify either --hash or --path");
157 };
158
159 std::fs::create_dir_all(&workspace.chat_sessions_path)?;
161
162 let mut imported_count = 0;
164 let mut skipped_count = 0;
165
166 for entry in std::fs::read_dir(src_path)? {
167 let entry = entry?;
168 let src_file = entry.path();
169
170 if is_session_file(&src_file) {
171 let dest_file = workspace.chat_sessions_path.join(entry.file_name());
172
173 if dest_file.exists() && !force {
174 skipped_count += 1;
175 } else {
176 std::fs::copy(&src_file, &dest_file)?;
177 imported_count += 1;
178 }
179 }
180 }
181
182 println!(
183 "{} Imported {} chat session(s)",
184 "[OK]".green(),
185 imported_count
186 );
187 if skipped_count > 0 {
188 println!(
189 "{} Skipped {} existing session(s). Use --force to overwrite.",
190 "[!]".yellow(),
191 skipped_count
192 );
193 }
194
195 Ok(())
196}
197
198#[allow(dead_code)]
200pub fn move_sessions(source_hash: &str, target_path: &str) -> Result<()> {
201 let source_ws = get_workspace_by_hash(source_hash)?
202 .context(format!("Source workspace not found: {}", source_hash))?;
203
204 let target_ws = get_workspace_by_path(target_path)?.context(format!(
205 "Target workspace not found for path: {}",
206 target_path
207 ))?;
208
209 if source_ws.workspace_path == target_ws.workspace_path {
211 println!(
212 "{} Source and target are the same workspace",
213 "[!]".yellow()
214 );
215 return Ok(());
216 }
217
218 move_sessions_internal(&source_ws, &target_ws, target_path)
219}
220
221fn move_sessions_to_workspace(source_ws: &Workspace, target_ws: &Workspace) -> Result<()> {
223 let target_path: &str = target_ws
224 .project_path
225 .as_deref()
226 .unwrap_or("target workspace");
227 move_sessions_internal(source_ws, target_ws, target_path)
228}
229
230fn move_sessions_internal(
232 source_ws: &Workspace,
233 target_ws: &Workspace,
234 display_path: &str,
235) -> Result<()> {
236 if !source_ws.has_chat_sessions {
237 println!("No chat sessions to move.");
238 return Ok(());
239 }
240
241 if source_ws.workspace_path == target_ws.workspace_path {
243 println!(
244 "{} Source and target are the same workspace",
245 "[!]".yellow()
246 );
247 return Ok(());
248 }
249
250 std::fs::create_dir_all(&target_ws.chat_sessions_path)?;
252
253 let mut moved_count = 0;
255 let mut skipped_count = 0;
256 for entry in std::fs::read_dir(&source_ws.chat_sessions_path)? {
257 let entry = entry?;
258 let src_file = entry.path();
259
260 if is_session_file(&src_file) {
261 let dest_file = target_ws.chat_sessions_path.join(entry.file_name());
262
263 if dest_file.exists() {
265 skipped_count += 1;
266 continue;
267 }
268
269 std::fs::rename(&src_file, &dest_file)?;
270 moved_count += 1;
271 }
272 }
273
274 println!(
275 "{} Moved {} chat session(s) to {}",
276 "[OK]".green(),
277 moved_count,
278 display_path
279 );
280
281 if skipped_count > 0 {
282 println!(
283 "{} Skipped {} session(s) that already exist in target",
284 "[!]".yellow(),
285 skipped_count
286 );
287 }
288
289 Ok(())
290}
291
292pub fn export_specific_sessions(
294 destination: &str,
295 session_ids: &[String],
296 project_path: Option<&str>,
297) -> Result<()> {
298 use crate::workspace::{discover_workspaces, get_chat_sessions_from_workspace, normalize_path};
299
300 let dest_path = Path::new(destination);
301 std::fs::create_dir_all(dest_path)?;
302
303 let workspaces = discover_workspaces()?;
304
305 let filtered: Vec<_> = if let Some(path) = project_path {
307 let normalized = normalize_path(path);
308 workspaces
309 .into_iter()
310 .filter(|ws| {
311 ws.project_path
312 .as_ref()
313 .map(|p| normalize_path(p) == normalized)
314 .unwrap_or(false)
315 })
316 .collect()
317 } else {
318 workspaces
319 };
320
321 let normalized_ids: Vec<String> = session_ids
322 .iter()
323 .flat_map(|s| s.split(',').map(|p| p.trim().to_lowercase()))
324 .filter(|s| !s.is_empty())
325 .collect();
326
327 let mut exported_count = 0;
328 let mut found_ids = Vec::new();
329
330 for ws in filtered {
331 if !ws.has_chat_sessions {
332 continue;
333 }
334
335 let sessions = get_chat_sessions_from_workspace(&ws.workspace_path)?;
336
337 for session in sessions {
338 let session_id = session.session.session_id.clone().unwrap_or_else(|| {
339 session
340 .path
341 .file_stem()
342 .map(|s| s.to_string_lossy().to_string())
343 .unwrap_or_default()
344 });
345
346 let matches = normalized_ids.iter().any(|req_id| {
347 session_id.to_lowercase().contains(req_id)
348 || req_id.contains(&session_id.to_lowercase())
349 });
350
351 if matches && !found_ids.contains(&session_id) {
352 let filename = session
353 .path
354 .file_name()
355 .map(|n| n.to_string_lossy().to_string())
356 .unwrap_or_default();
357
358 let dest_file = dest_path.join(&filename);
359 std::fs::copy(&session.path, &dest_file)?;
360 exported_count += 1;
361 found_ids.push(session_id);
362 println!(
363 " {} Exported: {}",
364 "[OK]".green(),
365 session.session.title()
366 );
367 }
368 }
369 }
370
371 println!(
372 "\n{} Exported {} session(s) to {}",
373 "[OK]".green().bold(),
374 exported_count,
375 destination
376 );
377
378 Ok(())
379}
380
381pub fn import_specific_sessions(
383 session_files: &[String],
384 target_path: Option<&str>,
385 force: bool,
386) -> Result<()> {
387 let target_ws = if let Some(path) = target_path {
388 get_workspace_by_path(path)?.context(format!("Workspace not found for path: {}", path))?
389 } else {
390 let cwd = std::env::current_dir()?;
391 get_workspace_by_path(cwd.to_str().unwrap_or(""))?
392 .context("Current directory is not a VS Code workspace")?
393 };
394
395 std::fs::create_dir_all(&target_ws.chat_sessions_path)?;
396
397 let mut imported_count = 0;
398 let mut skipped_count = 0;
399
400 for file_path in session_files {
401 let src_path = Path::new(file_path);
402
403 if !src_path.exists() {
404 println!("{} File not found: {}", "[!]".yellow(), file_path);
405 continue;
406 }
407
408 let filename = src_path
409 .file_name()
410 .map(|n| n.to_string_lossy().to_string())
411 .unwrap_or_default();
412
413 let dest_file = target_ws.chat_sessions_path.join(&filename);
414
415 if dest_file.exists() && !force {
416 println!(" {} Skipping (exists): {}", "[!]".yellow(), filename);
417 skipped_count += 1;
418 } else {
419 std::fs::copy(src_path, &dest_file)?;
420 imported_count += 1;
421 println!(" {} Imported: {}", "[OK]".green(), filename);
422 }
423 }
424
425 println!(
426 "\n{} Imported {} session(s)",
427 "[OK]".green().bold(),
428 imported_count
429 );
430 if skipped_count > 0 {
431 println!(
432 "{} Skipped {} existing. Use --force to overwrite.",
433 "[!]".yellow(),
434 skipped_count
435 );
436 }
437
438 Ok(())
439}
440
441pub fn move_workspace(source_hash: &str, target: &str) -> Result<()> {
443 let source_ws = get_workspace_by_hash(source_hash)?
445 .context(format!("Source workspace not found: {}", source_hash))?;
446
447 let target_ws = get_workspace_by_hash(target)?
450 .or_else(|| get_workspace_by_path(target).ok().flatten())
451 .context(format!("Target workspace not found: {}", target))?;
452
453 move_sessions_to_workspace(&source_ws, &target_ws)
454}
455
456pub fn move_specific_sessions(session_ids: &[String], target_path: &str) -> Result<()> {
458 use crate::workspace::{discover_workspaces, get_chat_sessions_from_workspace, normalize_path};
459
460 let target_ws = get_workspace_by_path(target_path)?
461 .context(format!("Target workspace not found: {}", target_path))?;
462
463 std::fs::create_dir_all(&target_ws.chat_sessions_path)?;
464
465 let workspaces = discover_workspaces()?;
466
467 let normalized_ids: Vec<String> = session_ids
468 .iter()
469 .flat_map(|s| s.split(',').map(|p| p.trim().to_lowercase()))
470 .filter(|s| !s.is_empty())
471 .collect();
472
473 let mut moved_count = 0;
474 let mut found_ids = Vec::new();
475
476 for ws in workspaces {
477 if !ws.has_chat_sessions {
478 continue;
479 }
480
481 if ws
483 .project_path
484 .as_ref()
485 .map(|p| normalize_path(p) == normalize_path(target_path))
486 .unwrap_or(false)
487 {
488 continue;
489 }
490
491 let sessions = get_chat_sessions_from_workspace(&ws.workspace_path)?;
492
493 for session in sessions {
494 let session_id = session.session.session_id.clone().unwrap_or_else(|| {
495 session
496 .path
497 .file_stem()
498 .map(|s| s.to_string_lossy().to_string())
499 .unwrap_or_default()
500 });
501
502 let matches = normalized_ids.iter().any(|req_id| {
503 session_id.to_lowercase().contains(req_id)
504 || req_id.contains(&session_id.to_lowercase())
505 });
506
507 if matches && !found_ids.contains(&session_id) {
508 let filename = session
509 .path
510 .file_name()
511 .map(|n| n.to_string_lossy().to_string())
512 .unwrap_or_default();
513
514 let dest_file = target_ws.chat_sessions_path.join(&filename);
515 std::fs::rename(&session.path, &dest_file)?;
516 moved_count += 1;
517 found_ids.push(session_id);
518 println!(" {} Moved: {}", "[OK]".green(), session.session.title());
519 }
520 }
521 }
522
523 println!(
524 "\n{} Moved {} session(s) to {}",
525 "[OK]".green().bold(),
526 moved_count,
527 target_path
528 );
529
530 Ok(())
531}
532
533pub fn move_by_path(source_path: &str, target_path: &str) -> Result<()> {
535 let source_ws = get_workspace_by_path(source_path)?
536 .context(format!("Source workspace not found: {}", source_path))?;
537
538 let target_ws = get_workspace_by_path(target_path)?
539 .context(format!("Target workspace not found: {}", target_path))?;
540
541 if !source_ws.has_chat_sessions {
542 println!("No chat sessions to move.");
543 return Ok(());
544 }
545
546 std::fs::create_dir_all(&target_ws.chat_sessions_path)?;
547
548 let mut moved_count = 0;
549 for entry in std::fs::read_dir(&source_ws.chat_sessions_path)? {
550 let entry = entry?;
551 let src_file = entry.path();
552
553 if is_session_file(&src_file) {
554 let dest_file = target_ws.chat_sessions_path.join(entry.file_name());
555 std::fs::rename(&src_file, &dest_file)?;
556 moved_count += 1;
557 }
558 }
559
560 println!(
561 "{} Moved {} chat session(s) from {} to {}",
562 "[OK]".green(),
563 moved_count,
564 source_path,
565 target_path
566 );
567
568 Ok(())
569}