chasm_cli/commands/
export_import.rs

1// Copyright (c) 2024-2026 Nervosys LLC
2// SPDX-License-Identifier: Apache-2.0
3//! Export and import commands
4
5use anyhow::{Context, Result};
6use colored::*;
7use std::path::Path;
8
9use crate::workspace::{get_workspace_by_hash, get_workspace_by_path};
10
11/// Export chat sessions from a workspace
12pub 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    // Create destination directory
27    let dest_path = Path::new(destination);
28    std::fs::create_dir_all(dest_path)?;
29
30    // Copy all session files
31    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
53/// Import chat sessions into a workspace
54pub 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    // Create chatSessions directory if it doesn't exist
74    std::fs::create_dir_all(&workspace.chat_sessions_path)?;
75
76    // Import all JSON files
77    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
112/// Move chat sessions from one workspace to another
113pub 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    // Create chatSessions directory in target if needed
128    std::fs::create_dir_all(&target_ws.chat_sessions_path)?;
129
130    // Move all session files
131    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
153/// Export specific sessions by ID
154pub 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    // Filter workspaces by project path if provided
167    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
242/// Import specific session files
243pub 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
302/// Move all sessions from one workspace to another (by hash)
303pub fn move_workspace(source_hash: &str, target: &str) -> Result<()> {
304    // Try target as path first, then as hash
305    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
318/// Move specific sessions by ID
319pub 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        // Skip target workspace
344        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
395/// Move sessions from one path to another
396pub 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}