chasm_cli/commands/
migration.rs1use anyhow::{Context, Result};
6use colored::*;
7use serde::{Deserialize, Serialize};
8use std::path::Path;
9
10use crate::workspace::discover_workspaces;
11
12#[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
28pub 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 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 let ws_dir = output_path.join(&ws.hash);
64 std::fs::create_dir_all(&ws_dir)?;
65
66 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 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 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
124pub 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 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 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 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 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 std::fs::create_dir_all(&dst_dir)?;
189
190 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 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}