chasm_cli/commands/
migration.rs

1// Copyright (c) 2024-2026 Nervosys LLC
2// SPDX-License-Identifier: Apache-2.0
3//! Migration commands
4
5use anyhow::{Context, Result};
6use colored::*;
7use serde::{Deserialize, Serialize};
8use std::path::Path;
9
10use crate::workspace::discover_workspaces;
11
12/// Migration package manifest
13#[derive(Debug, Serialize, Deserialize)]
14struct MigrationManifest {
15    version: String,
16    created_at: String,
17    source_os: String,
18    workspaces: Vec<MigrationWorkspace>,
19}
20
21#[derive(Debug, Serialize, Deserialize)]
22struct MigrationWorkspace {
23    hash: String,
24    project_path: Option<String>,
25    session_count: usize,
26}
27
28/// Create a migration package
29pub fn create_migration(output: &str, projects: Option<&str>, include_all: bool) -> Result<()> {
30    let output_path = Path::new(output);
31    std::fs::create_dir_all(output_path)?;
32
33    let workspaces = discover_workspaces()?;
34
35    // Filter workspaces
36    let filtered: Vec<_> = if include_all {
37        workspaces.iter().filter(|w| w.has_chat_sessions).collect()
38    } else if let Some(project_list) = projects {
39        let paths: Vec<&str> = project_list.split(',').map(|p| p.trim()).collect();
40        workspaces
41            .iter()
42            .filter(|w| {
43                w.project_path
44                    .as_ref()
45                    .map(|p| paths.iter().any(|path| p.contains(path)))
46                    .unwrap_or(false)
47            })
48            .collect()
49    } else {
50        workspaces.iter().filter(|w| w.has_chat_sessions).collect()
51    };
52
53    if filtered.is_empty() {
54        println!("{} No workspaces with chat sessions found", "[!]".yellow());
55        return Ok(());
56    }
57
58    let mut total_sessions = 0;
59    let mut manifest_workspaces = Vec::new();
60
61    for ws in &filtered {
62        // Create workspace directory in package
63        let ws_dir = output_path.join(&ws.hash);
64        std::fs::create_dir_all(&ws_dir)?;
65
66        // Copy chat sessions
67        let sessions_dir = ws_dir.join("chatSessions");
68        std::fs::create_dir_all(&sessions_dir)?;
69
70        let mut session_count = 0;
71        if ws.chat_sessions_path.exists() {
72            for entry in std::fs::read_dir(&ws.chat_sessions_path)? {
73                let entry = entry?;
74                let src = entry.path();
75                if src.extension().map(|e| e == "json").unwrap_or(false) {
76                    let dst = sessions_dir.join(entry.file_name());
77                    std::fs::copy(&src, &dst)?;
78                    session_count += 1;
79                }
80            }
81        }
82
83        // Save workspace.json
84        let ws_json = serde_json::json!({
85            "folder": ws.project_path.as_ref().map(|p| format!("file:///{}", p.replace('\\', "/").replace(' ', "%20")))
86        });
87        std::fs::write(
88            ws_dir.join("workspace.json"),
89            serde_json::to_string_pretty(&ws_json)?,
90        )?;
91
92        manifest_workspaces.push(MigrationWorkspace {
93            hash: ws.hash.clone(),
94            project_path: ws.project_path.clone(),
95            session_count,
96        });
97
98        total_sessions += session_count;
99    }
100
101    // Create manifest
102    let manifest = MigrationManifest {
103        version: "1.0".to_string(),
104        created_at: chrono::Utc::now().to_rfc3339(),
105        source_os: std::env::consts::OS.to_string(),
106        workspaces: manifest_workspaces,
107    };
108
109    std::fs::write(
110        output_path.join("manifest.json"),
111        serde_json::to_string_pretty(&manifest)?,
112    )?;
113
114    println!("{} Migration package created", "[OK]".green());
115    println!("   Package: {}", output_path.display());
116    println!("   Workspaces: {}", filtered.len());
117    println!("   Sessions: {}", total_sessions);
118    println!("\nTo restore on new machine:");
119    println!("   csm restore-migration \"{}\"", output_path.display());
120
121    Ok(())
122}
123
124/// Restore a migration package
125pub fn restore_migration(package: &str, mapping: Option<&str>, dry_run: bool) -> Result<()> {
126    let package_path = Path::new(package);
127
128    if !package_path.exists() {
129        anyhow::bail!("Migration package not found: {}", package);
130    }
131
132    // Load manifest
133    let manifest_path = package_path.join("manifest.json");
134    let manifest: MigrationManifest = serde_json::from_str(
135        &std::fs::read_to_string(&manifest_path).context("Failed to read manifest.json")?,
136    )?;
137
138    // Parse path mapping
139    let path_map: std::collections::HashMap<String, String> = if let Some(m) = mapping {
140        m.split(';')
141            .filter_map(|pair| {
142                let parts: Vec<&str> = pair.split(':').collect();
143                if parts.len() == 2 {
144                    Some((parts[0].trim().to_string(), parts[1].trim().to_string()))
145                } else {
146                    None
147                }
148            })
149            .collect()
150    } else {
151        std::collections::HashMap::new()
152    };
153
154    let storage_path = crate::workspace::get_workspace_storage_path()?;
155
156    println!("{} Restoring migration package", "[P]".blue());
157    println!("   Source: {}", package_path.display());
158    println!("   Target: {}", storage_path.display());
159
160    if dry_run {
161        println!("\n{} DRY RUN - No changes will be made", "[!]".yellow());
162    }
163
164    let mut actions = Vec::new();
165    let mut skipped = Vec::new();
166
167    for ws in &manifest.workspaces {
168        let src_dir = package_path.join(&ws.hash);
169
170        // Apply path mapping
171        let new_path = ws
172            .project_path
173            .as_ref()
174            .map(|p| path_map.get(p).cloned().unwrap_or_else(|| p.clone()));
175
176        // Check if target path exists
177        if let Some(ref path) = new_path {
178            if !Path::new(path).exists() {
179                skipped.push(format!("{}: path does not exist", path));
180                continue;
181            }
182        }
183
184        let dst_dir = storage_path.join(&ws.hash);
185
186        if !dry_run {
187            // Create destination directory
188            std::fs::create_dir_all(&dst_dir)?;
189
190            // Copy workspace.json
191            let ws_json_src = src_dir.join("workspace.json");
192            if ws_json_src.exists() {
193                std::fs::copy(&ws_json_src, dst_dir.join("workspace.json"))?;
194            }
195
196            // Copy chat sessions
197            let sessions_src = src_dir.join("chatSessions");
198            let sessions_dst = dst_dir.join("chatSessions");
199
200            if sessions_src.exists() {
201                std::fs::create_dir_all(&sessions_dst)?;
202                for entry in std::fs::read_dir(&sessions_src)? {
203                    let entry = entry?;
204                    std::fs::copy(entry.path(), sessions_dst.join(entry.file_name()))?;
205                }
206            }
207        }
208
209        actions.push(format!(
210            "Restored: {} ({} sessions)",
211            new_path.as_deref().unwrap_or("(unknown)"),
212            ws.session_count
213        ));
214    }
215
216    println!("\n{} Actions:", "[*]".blue());
217    for action in &actions {
218        println!("   {}", action);
219    }
220
221    if !skipped.is_empty() {
222        println!("\n{} Skipped:", "[!]".yellow());
223        for skip in &skipped {
224            println!("   {}", skip);
225        }
226    }
227
228    if !dry_run {
229        println!("\n{} Migration restored successfully!", "[OK]".green());
230    }
231
232    Ok(())
233}