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
113pub fn move_sessions(source_hash: &str, target_path: &str) -> Result<()> {
115 let source_ws = get_workspace_by_hash(source_hash)?
116 .context(format!("Source workspace not found: {}", source_hash))?;
117
118 let target_ws = get_workspace_by_path(target_path)?.context(format!(
119 "Target workspace not found for path: {}",
120 target_path
121 ))?;
122
123 if source_ws.workspace_path == target_ws.workspace_path {
125 println!(
126 "{} Source and target are the same workspace",
127 "[!]".yellow()
128 );
129 return Ok(());
130 }
131
132 move_sessions_internal(&source_ws, &target_ws, target_path)
133}
134
135fn move_sessions_to_workspace(source_ws: &Workspace, target_ws: &Workspace) -> Result<()> {
137 let target_path: &str = target_ws
138 .project_path
139 .as_deref()
140 .unwrap_or("target workspace");
141 move_sessions_internal(source_ws, target_ws, target_path)
142}
143
144fn move_sessions_internal(
146 source_ws: &Workspace,
147 target_ws: &Workspace,
148 display_path: &str,
149) -> Result<()> {
150 if !source_ws.has_chat_sessions {
151 println!("No chat sessions to move.");
152 return Ok(());
153 }
154
155 if source_ws.workspace_path == target_ws.workspace_path {
157 println!(
158 "{} Source and target are the same workspace",
159 "[!]".yellow()
160 );
161 return Ok(());
162 }
163
164 std::fs::create_dir_all(&target_ws.chat_sessions_path)?;
166
167 let mut moved_count = 0;
169 let mut skipped_count = 0;
170 for entry in std::fs::read_dir(&source_ws.chat_sessions_path)? {
171 let entry = entry?;
172 let src_file = entry.path();
173
174 if src_file.extension().map(|e| e == "json").unwrap_or(false) {
175 let dest_file = target_ws.chat_sessions_path.join(entry.file_name());
176
177 if dest_file.exists() {
179 skipped_count += 1;
180 continue;
181 }
182
183 std::fs::rename(&src_file, &dest_file)?;
184 moved_count += 1;
185 }
186 }
187
188 println!(
189 "{} Moved {} chat session(s) to {}",
190 "[OK]".green(),
191 moved_count,
192 display_path
193 );
194
195 if skipped_count > 0 {
196 println!(
197 "{} Skipped {} session(s) that already exist in target",
198 "[!]".yellow(),
199 skipped_count
200 );
201 }
202
203 Ok(())
204}
205
206pub fn export_specific_sessions(
208 destination: &str,
209 session_ids: &[String],
210 project_path: Option<&str>,
211) -> Result<()> {
212 use crate::workspace::{discover_workspaces, get_chat_sessions_from_workspace, normalize_path};
213
214 let dest_path = Path::new(destination);
215 std::fs::create_dir_all(dest_path)?;
216
217 let workspaces = discover_workspaces()?;
218
219 let filtered: Vec<_> = if let Some(path) = project_path {
221 let normalized = normalize_path(path);
222 workspaces
223 .into_iter()
224 .filter(|ws| {
225 ws.project_path
226 .as_ref()
227 .map(|p| normalize_path(p) == normalized)
228 .unwrap_or(false)
229 })
230 .collect()
231 } else {
232 workspaces
233 };
234
235 let normalized_ids: Vec<String> = session_ids
236 .iter()
237 .flat_map(|s| s.split(',').map(|p| p.trim().to_lowercase()))
238 .filter(|s| !s.is_empty())
239 .collect();
240
241 let mut exported_count = 0;
242 let mut found_ids = Vec::new();
243
244 for ws in filtered {
245 if !ws.has_chat_sessions {
246 continue;
247 }
248
249 let sessions = get_chat_sessions_from_workspace(&ws.workspace_path)?;
250
251 for session in sessions {
252 let session_id = session.session.session_id.clone().unwrap_or_else(|| {
253 session
254 .path
255 .file_stem()
256 .map(|s| s.to_string_lossy().to_string())
257 .unwrap_or_default()
258 });
259
260 let matches = normalized_ids.iter().any(|req_id| {
261 session_id.to_lowercase().contains(req_id)
262 || req_id.contains(&session_id.to_lowercase())
263 });
264
265 if matches && !found_ids.contains(&session_id) {
266 let filename = session
267 .path
268 .file_name()
269 .map(|n| n.to_string_lossy().to_string())
270 .unwrap_or_default();
271
272 let dest_file = dest_path.join(&filename);
273 std::fs::copy(&session.path, &dest_file)?;
274 exported_count += 1;
275 found_ids.push(session_id);
276 println!(
277 " {} Exported: {}",
278 "[OK]".green(),
279 session.session.title()
280 );
281 }
282 }
283 }
284
285 println!(
286 "\n{} Exported {} session(s) to {}",
287 "[OK]".green().bold(),
288 exported_count,
289 destination
290 );
291
292 Ok(())
293}
294
295pub fn import_specific_sessions(
297 session_files: &[String],
298 target_path: Option<&str>,
299 force: bool,
300) -> Result<()> {
301 let target_ws = if let Some(path) = target_path {
302 get_workspace_by_path(path)?.context(format!("Workspace not found for path: {}", path))?
303 } else {
304 let cwd = std::env::current_dir()?;
305 get_workspace_by_path(cwd.to_str().unwrap_or(""))?
306 .context("Current directory is not a VS Code workspace")?
307 };
308
309 std::fs::create_dir_all(&target_ws.chat_sessions_path)?;
310
311 let mut imported_count = 0;
312 let mut skipped_count = 0;
313
314 for file_path in session_files {
315 let src_path = Path::new(file_path);
316
317 if !src_path.exists() {
318 println!("{} File not found: {}", "[!]".yellow(), file_path);
319 continue;
320 }
321
322 let filename = src_path
323 .file_name()
324 .map(|n| n.to_string_lossy().to_string())
325 .unwrap_or_default();
326
327 let dest_file = target_ws.chat_sessions_path.join(&filename);
328
329 if dest_file.exists() && !force {
330 println!(" {} Skipping (exists): {}", "[!]".yellow(), filename);
331 skipped_count += 1;
332 } else {
333 std::fs::copy(src_path, &dest_file)?;
334 imported_count += 1;
335 println!(" {} Imported: {}", "[OK]".green(), filename);
336 }
337 }
338
339 println!(
340 "\n{} Imported {} session(s)",
341 "[OK]".green().bold(),
342 imported_count
343 );
344 if skipped_count > 0 {
345 println!(
346 "{} Skipped {} existing. Use --force to overwrite.",
347 "[!]".yellow(),
348 skipped_count
349 );
350 }
351
352 Ok(())
353}
354
355pub fn move_workspace(source_hash: &str, target: &str) -> Result<()> {
357 let source_ws = get_workspace_by_hash(source_hash)?
359 .context(format!("Source workspace not found: {}", source_hash))?;
360
361 let target_ws = get_workspace_by_hash(target)?
364 .or_else(|| get_workspace_by_path(target).ok().flatten())
365 .context(format!("Target workspace not found: {}", target))?;
366
367 move_sessions_to_workspace(&source_ws, &target_ws)
368}
369
370pub fn move_specific_sessions(session_ids: &[String], target_path: &str) -> Result<()> {
372 use crate::workspace::{discover_workspaces, get_chat_sessions_from_workspace, normalize_path};
373
374 let target_ws = get_workspace_by_path(target_path)?
375 .context(format!("Target workspace not found: {}", target_path))?;
376
377 std::fs::create_dir_all(&target_ws.chat_sessions_path)?;
378
379 let workspaces = discover_workspaces()?;
380
381 let normalized_ids: Vec<String> = session_ids
382 .iter()
383 .flat_map(|s| s.split(',').map(|p| p.trim().to_lowercase()))
384 .filter(|s| !s.is_empty())
385 .collect();
386
387 let mut moved_count = 0;
388 let mut found_ids = Vec::new();
389
390 for ws in workspaces {
391 if !ws.has_chat_sessions {
392 continue;
393 }
394
395 if ws
397 .project_path
398 .as_ref()
399 .map(|p| normalize_path(p) == normalize_path(target_path))
400 .unwrap_or(false)
401 {
402 continue;
403 }
404
405 let sessions = get_chat_sessions_from_workspace(&ws.workspace_path)?;
406
407 for session in sessions {
408 let session_id = session.session.session_id.clone().unwrap_or_else(|| {
409 session
410 .path
411 .file_stem()
412 .map(|s| s.to_string_lossy().to_string())
413 .unwrap_or_default()
414 });
415
416 let matches = normalized_ids.iter().any(|req_id| {
417 session_id.to_lowercase().contains(req_id)
418 || req_id.contains(&session_id.to_lowercase())
419 });
420
421 if matches && !found_ids.contains(&session_id) {
422 let filename = session
423 .path
424 .file_name()
425 .map(|n| n.to_string_lossy().to_string())
426 .unwrap_or_default();
427
428 let dest_file = target_ws.chat_sessions_path.join(&filename);
429 std::fs::rename(&session.path, &dest_file)?;
430 moved_count += 1;
431 found_ids.push(session_id);
432 println!(" {} Moved: {}", "[OK]".green(), session.session.title());
433 }
434 }
435 }
436
437 println!(
438 "\n{} Moved {} session(s) to {}",
439 "[OK]".green().bold(),
440 moved_count,
441 target_path
442 );
443
444 Ok(())
445}
446
447pub fn move_by_path(source_path: &str, target_path: &str) -> Result<()> {
449 let source_ws = get_workspace_by_path(source_path)?
450 .context(format!("Source workspace not found: {}", source_path))?;
451
452 let target_ws = get_workspace_by_path(target_path)?
453 .context(format!("Target workspace not found: {}", target_path))?;
454
455 if !source_ws.has_chat_sessions {
456 println!("No chat sessions to move.");
457 return Ok(());
458 }
459
460 std::fs::create_dir_all(&target_ws.chat_sessions_path)?;
461
462 let mut moved_count = 0;
463 for entry in std::fs::read_dir(&source_ws.chat_sessions_path)? {
464 let entry = entry?;
465 let src_file = entry.path();
466
467 if src_file.extension().map(|e| e == "json").unwrap_or(false) {
468 let dest_file = target_ws.chat_sessions_path.join(entry.file_name());
469 std::fs::rename(&src_file, &dest_file)?;
470 moved_count += 1;
471 }
472 }
473
474 println!(
475 "{} Moved {} chat session(s) from {} to {}",
476 "[OK]".green(),
477 moved_count,
478 source_path,
479 target_path
480 );
481
482 Ok(())
483}