Skip to main content

sparrow/onboarding/
migration.rs

1// ─── Migration paths from competitor tools (§4 + WS10) ────────────────────────
2
3use 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    /// Import from OpenClaw (already partially implemented)
19    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        // Agents/SOUL
30        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        // Skills
39        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        // Cron
48        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    /// Import from Claude Code
61    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        // CLAUDE.md → Sparrow SOUL
72        let claude_md = path.join("CLAUDE.md");
73        if claude_md.exists() {
74            let content = std::fs::read_to_string(&claude_md)?;
75            // Convert to Sparrow SOUL
76            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        // MCP servers → Sparrow MCP
92        let mcp_config = path.join(".mcp.json");
93        if mcp_config.exists() {
94            result.config_entries += 1;
95        }
96
97        // .claude/settings.json → config
98        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    /// Import from Codex
107    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        // AGENTS.md → agent import
118        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        // codex.yaml → config
126        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    /// Import from OpenCode
135    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        // opencode.json → config
146        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    /// Import from Hermes Agent
158    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        // agents/*.md → Sparrow SOULs
169        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        // skills/ → Sparrow skills
178        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        // hermes.yaml → config
187        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    /// Auto-detect installed tools and offer import
200    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}