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