sparrow/onboarding/
migration.rs1use std::path::PathBuf;
4
5pub struct Migration;
6
7#[derive(Debug, Clone)]
8pub struct MigrationResult {
9 pub tool: String,
10 pub agents: usize,
11 pub skills: usize,
12 pub cron_jobs: usize,
13 pub config_entries: usize,
14 pub surfaces: usize,
15}
16
17impl Migration {
18 pub fn import_openclaw(path: &PathBuf) -> anyhow::Result<MigrationResult> {
20 let mut result = MigrationResult {
21 tool: "openclaw".into(),
22 agents: 0,
23 skills: 0,
24 cron_jobs: 0,
25 config_entries: 0,
26 surfaces: 0,
27 };
28
29 let agents_dir = path.join("agents");
31 if agents_dir.exists() {
32 result.agents = std::fs::read_dir(&agents_dir)?
33 .filter_map(|e| e.ok())
34 .filter(|e| e.path().extension().map(|x| x == "md").unwrap_or(false))
35 .count();
36 }
37
38 let skills_dir = path.join("skills");
40 if skills_dir.exists() {
41 result.skills = std::fs::read_dir(&skills_dir)?
42 .filter_map(|e| e.ok())
43 .filter(|e| e.path().is_dir())
44 .count();
45 }
46
47 let cron_file = path.join("cron.json");
49 if cron_file.exists() {
50 if let Ok(content) = std::fs::read_to_string(&cron_file) {
51 if let Ok(jobs) = serde_json::from_str::<Vec<serde_json::Value>>(&content) {
52 result.cron_jobs = jobs.len();
53 }
54 }
55 }
56
57 Ok(result)
58 }
59
60 pub fn import_claude_code(path: &PathBuf) -> anyhow::Result<MigrationResult> {
62 let mut result = MigrationResult {
63 tool: "claude-code".into(),
64 agents: 0,
65 skills: 0,
66 cron_jobs: 0,
67 config_entries: 0,
68 surfaces: 0,
69 };
70
71 let claude_md = path.join("CLAUDE.md");
73 if claude_md.exists() {
74 let content = std::fs::read_to_string(&claude_md)?;
75 let soul = format!(
77 "# Imported from Claude Code\nname = \"claude-code-import\"\nrole = \"assistant\"\npersonality = \"\"\"\n{}\n\"\"\"\n",
78 content.lines().take(50).collect::<Vec<_>>().join("\n")
79 );
80 let dest = dirs::config_dir()
81 .unwrap_or_default()
82 .join("sparrow")
83 .join("agents")
84 .join("claude-code-import.soul.toml");
85 std::fs::create_dir_all(dest.parent().unwrap())?;
86 std::fs::write(&dest, soul)?;
87 result.agents = 1;
88 result.config_entries = content.lines().count();
89 }
90
91 let mcp_config = path.join(".mcp.json");
93 if mcp_config.exists() {
94 result.config_entries += 1;
95 }
96
97 let settings = path.join(".claude").join("settings.json");
99 if settings.exists() {
100 result.config_entries += 1;
101 }
102
103 Ok(result)
104 }
105
106 pub fn import_codex(path: &PathBuf) -> anyhow::Result<MigrationResult> {
108 let mut result = MigrationResult {
109 tool: "codex".into(),
110 agents: 0,
111 skills: 0,
112 cron_jobs: 0,
113 config_entries: 0,
114 surfaces: 0,
115 };
116
117 let agents_md = path.join("AGENTS.md");
119 if agents_md.exists() {
120 let content = std::fs::read_to_string(&agents_md)?;
121 result.agents = 1;
122 result.config_entries = content.lines().count();
123 }
124
125 let config_yaml = path.join("codex.yaml");
127 if config_yaml.exists() || path.join("codex.yml").exists() {
128 result.config_entries += 1;
129 }
130
131 Ok(result)
132 }
133
134 pub fn import_opencode(path: &PathBuf) -> anyhow::Result<MigrationResult> {
136 let mut result = MigrationResult {
137 tool: "opencode".into(),
138 agents: 0,
139 skills: 0,
140 cron_jobs: 0,
141 config_entries: 0,
142 surfaces: 0,
143 };
144
145 let config_json = path.join("opencode.json");
147 if config_json.exists() {
148 let content = std::fs::read_to_string(&config_json)?;
149 if let Ok(cfg) = serde_json::from_str::<serde_json::Value>(&content) {
150 result.config_entries = cfg.as_object().map(|o| o.len()).unwrap_or(0);
151 }
152 }
153
154 Ok(result)
155 }
156
157 pub fn import_hermes(path: &PathBuf) -> anyhow::Result<MigrationResult> {
159 let mut result = MigrationResult {
160 tool: "hermes".into(),
161 agents: 0,
162 skills: 0,
163 cron_jobs: 0,
164 config_entries: 0,
165 surfaces: 0,
166 };
167
168 let agents_dir = path.join("agents");
170 if agents_dir.exists() {
171 result.agents = std::fs::read_dir(&agents_dir)?
172 .filter_map(|e| e.ok())
173 .filter(|e| e.path().extension().map(|x| x == "md").unwrap_or(false))
174 .count();
175 }
176
177 let skills_dir = path.join("skills");
179 if skills_dir.exists() {
180 result.skills = std::fs::read_dir(&skills_dir)?
181 .filter_map(|e| e.ok())
182 .filter(|e| e.path().is_dir())
183 .count();
184 }
185
186 let config_yaml = path.join("hermes.yaml");
188 if config_yaml.exists() {
189 if let Ok(content) = std::fs::read_to_string(&config_yaml) {
190 if let Ok(cfg) = serde_json::from_str::<serde_json::Value>(&content) {
191 result.config_entries = cfg.as_object().map(|o| o.len()).unwrap_or(0);
192 }
193 }
194 }
195
196 Ok(result)
197 }
198
199 pub fn detect_installed() -> Vec<String> {
201 let mut found = Vec::new();
202 let home = dirs::home_dir().unwrap_or_default();
203
204 let tools: Vec<(&str, PathBuf)> = vec![
205 ("openclaw", home.join(".openclaw")),
206 ("claude-code", home.join(".claude")),
207 ("codex", home.join(".codex")),
208 ("opencode", home.join(".config").join("opencode")),
209 ("hermes", home.join(".hermes")),
210 ];
211
212 for (name, path) in tools {
213 if path.exists() {
214 found.push(name.to_string());
215 }
216 }
217 found
218 }
219}