helix_core/compiler/tools/
migrate.rs

1use std::path::Path;
2use std::fs;
3use serde_json::Value as JsonValue;
4use anyhow::{Result, Context};
5pub struct Migrator {
6    verbose: bool,
7    _preserve_comments: bool,
8}
9impl Migrator {
10    pub fn new() -> Self {
11        Self {
12            verbose: false,
13            _preserve_comments: true,
14        }
15    }
16    pub fn verbose(mut self, enable: bool) -> Self {
17        self.verbose = enable;
18        self
19    }
20    pub fn migrate_file<P: AsRef<Path>>(
21        &self,
22        input: P,
23        output: Option<P>,
24    ) -> Result<String> {
25        let input_path = input.as_ref();
26        let extension = input_path
27            .extension()
28            .and_then(|s| s.to_str())
29            .ok_or_else(|| anyhow::anyhow!("No file extension found"))?;
30        let content = fs::read_to_string(input_path)
31            .context("Failed to read input file")?;
32        let helix_content = match extension {
33            "json" => self.migrate_json(&content)?,
34            "toml" => self.migrate_toml(&content)?,
35            "yaml" | "yml" => self.migrate_yaml(&content)?,
36            "env" => self.migrate_env(&content)?,
37            _ => return Err(anyhow::anyhow!("Unsupported file type: {}", extension)),
38        };
39        if let Some(output_path) = output {
40            fs::write(output_path.as_ref(), &helix_content)
41                .context("Failed to write output file")?;
42        }
43        Ok(helix_content)
44    }
45    pub fn migrate_json(&self, json_str: &str) -> Result<String> {
46        let value: JsonValue = serde_json::from_str(json_str)
47            .context("Failed to parse JSON")?;
48        let mut output = String::new();
49        output.push_str("# Migrated from JSON\n");
50        output
51            .push_str(
52                "# Send Buddy the Beagle a bitcoin treat for making this page: bc1quct28jtvvuymvkvjfgcedhd7jt0c56975f2fsh\n\n",
53            );
54        if let Some(obj) = value.as_object() {
55            self.convert_json_object_to_hlx(obj, &mut output, 0)?;
56        }
57        Ok(output)
58    }
59    pub fn migrate_toml(&self, toml_str: &str) -> Result<String> {
60        let value: toml::Value = toml::from_str(toml_str)
61            .context("Failed to parse TOML")?;
62        let mut output = String::new();
63        output.push_str("# Migrated from TOML\n");
64        output
65            .push_str(
66                "# Send Buddy the Beagle a bitcoin treat for making this page: bc1quct28jtvvuymvkvjfgcedhd7jt0c56975f2fsh\n\n",
67            );
68        if let Some(table) = value.as_table() {
69            self.convert_toml_table_to_hlx(table, &mut output, 0)?;
70        }
71        Ok(output)
72    }
73    pub fn migrate_yaml(&self, yaml_str: &str) -> Result<String> {
74        let value: serde_yaml::Value = serde_yaml::from_str(yaml_str)
75            .context("Failed to parse YAML")?;
76        let mut output = String::new();
77        output.push_str("# Migrated from YAML\n");
78        output
79            .push_str(
80                "# Send Buddy the Beagle a bitcoin treat for making this page: bc1quct28jtvvuymvkvjfgcedhd7jt0c56975f2fsh\n\n",
81            );
82        if let Some(mapping) = value.as_mapping() {
83            self.convert_yaml_mapping_to_hlx(mapping, &mut output, 0)?;
84        }
85        Ok(output)
86    }
87    pub fn migrate_env(&self, env_str: &str) -> Result<String> {
88        let mut output = String::new();
89        output.push_str("# Migrated from .env\n");
90        output
91            .push_str(
92                "# Send Buddy the Beagle a bitcoin treat for making this page: bc1quct28jtvvuymvkvjfgcedhd7jt0c56975f2fsh\n\n",
93            );
94        output.push_str("context \"environment\" {\n");
95        for line in env_str.lines() {
96            let line = line.trim();
97            if line.is_empty() || line.starts_with('#') {
98                continue;
99            }
100            if let Some((key, value)) = line.split_once('=') {
101                let key = key.trim();
102                let value = value.trim().trim_matches('"');
103                if key.contains("KEY") || key.contains("SECRET") || key.contains("TOKEN")
104                {
105                    output.push_str(&format!("    {} = ${}\n", key.to_lowercase(), key));
106                } else {
107                    output
108                        .push_str(
109                            &format!("    {} = \"{}\"\n", key.to_lowercase(), value),
110                        );
111                }
112            }
113        }
114        output.push_str("}\n");
115        Ok(output)
116    }
117    fn convert_json_object_to_hlx(
118        &self,
119        obj: &serde_json::Map<String, JsonValue>,
120        output: &mut String,
121        indent: usize,
122    ) -> Result<()> {
123        let indent_str = "    ".repeat(indent);
124        for (key, value) in obj {
125            match key.as_str() {
126                "agent" | "agents" => {
127                    self.convert_to_agent_block(key, value, output, indent)?;
128                }
129                "workflow" | "workflows" => {
130                    self.convert_to_workflow_block(key, value, output, indent)?;
131                }
132                "memory" => {
133                    self.convert_to_memory_block(value, output, indent)?;
134                }
135                "context" | "contexts" => {
136                    self.convert_to_context_block(key, value, output, indent)?;
137                }
138                _ => {
139                    match value {
140                        JsonValue::Object(inner) => {
141                            output.push_str(&format!("{}{} {{\n", indent_str, key));
142                            self.convert_json_object_to_hlx(inner, output, indent + 1)?;
143                            output.push_str(&format!("{}}}\n", indent_str));
144                        }
145                        JsonValue::Array(arr) => {
146                            output.push_str(&format!("{}{} [\n", indent_str, key));
147                            for item in arr {
148                                output.push_str(&format!("{}    ", indent_str));
149                                self.write_json_value(item, output)?;
150                                output.push_str("\n");
151                            }
152                            output.push_str(&format!("{}]\n", indent_str));
153                        }
154                        _ => {
155                            output.push_str(&format!("{}{} = ", indent_str, key));
156                            self.write_json_value(value, output)?;
157                            output.push_str("\n");
158                        }
159                    }
160                }
161            }
162        }
163        Ok(())
164    }
165    fn convert_to_agent_block(
166        &self,
167        _key: &str,
168        value: &JsonValue,
169        output: &mut String,
170        indent: usize,
171    ) -> Result<()> {
172        let indent_str = "    ".repeat(indent);
173        if let Some(obj) = value.as_object() {
174            let name = obj.get("name").and_then(|v| v.as_str()).unwrap_or("unnamed");
175            output.push_str(&format!("{}agent \"{}\" {{\n", indent_str, name));
176            for (k, v) in obj {
177                if k == "name" {
178                    continue;
179                }
180                match k.as_str() {
181                    "temperature" => {
182                        if let Some(num) = v.as_f64() {
183                            output
184                                .push_str(
185                                    &format!("{}    temperature = {}\n", indent_str, num),
186                                );
187                        }
188                    }
189                    "max_tokens" => {
190                        if let Some(num) = v.as_i64() {
191                            output
192                                .push_str(
193                                    &format!("{}    max_tokens = {}\n", indent_str, num),
194                                );
195                        }
196                    }
197                    "timeout" => {
198                        if let Some(s) = v.as_str() {
199                            let duration = self.parse_duration_string(s);
200                            output
201                                .push_str(
202                                    &format!("{}    timeout = {}\n", indent_str, duration),
203                                );
204                        }
205                    }
206                    _ => {
207                        output.push_str(&format!("{}    {} = ", indent_str, k));
208                        self.write_json_value(v, output)?;
209                        output.push_str("\n");
210                    }
211                }
212            }
213            output.push_str(&format!("{}}}\n", indent_str));
214        }
215        Ok(())
216    }
217    fn convert_to_workflow_block(
218        &self,
219        _key: &str,
220        value: &JsonValue,
221        output: &mut String,
222        indent: usize,
223    ) -> Result<()> {
224        let indent_str = "    ".repeat(indent);
225        if let Some(obj) = value.as_object() {
226            let name = obj.get("name").and_then(|v| v.as_str()).unwrap_or("unnamed");
227            output.push_str(&format!("{}workflow \"{}\" {{\n", indent_str, name));
228            if let Some(trigger) = obj.get("trigger") {
229                output.push_str(&format!("{}    trigger = ", indent_str));
230                self.write_json_value(trigger, output)?;
231                output.push_str("\n");
232            }
233            if let Some(steps) = obj.get("steps").and_then(|v| v.as_array()) {
234                for (i, step) in steps.iter().enumerate() {
235                    output
236                        .push_str(
237                            &format!("{}    step \"step_{}\" {{\n", indent_str, i + 1),
238                        );
239                    if let Some(step_obj) = step.as_object() {
240                        for (k, v) in step_obj {
241                            output.push_str(&format!("{}        {} = ", indent_str, k));
242                            self.write_json_value(v, output)?;
243                            output.push_str("\n");
244                        }
245                    }
246                    output.push_str(&format!("{}    }}\n", indent_str));
247                }
248            }
249            output.push_str(&format!("{}}}\n", indent_str));
250        }
251        Ok(())
252    }
253    fn convert_to_memory_block(
254        &self,
255        value: &JsonValue,
256        output: &mut String,
257        indent: usize,
258    ) -> Result<()> {
259        let indent_str = "    ".repeat(indent);
260        output.push_str(&format!("{}memory {{\n", indent_str));
261        if let Some(obj) = value.as_object() {
262            for (k, v) in obj {
263                output.push_str(&format!("{}    {} = ", indent_str, k));
264                self.write_json_value(v, output)?;
265                output.push_str("\n");
266            }
267        }
268        output.push_str(&format!("{}}}\n", indent_str));
269        Ok(())
270    }
271    fn convert_to_context_block(
272        &self,
273        _key: &str,
274        value: &JsonValue,
275        output: &mut String,
276        indent: usize,
277    ) -> Result<()> {
278        let indent_str = "    ".repeat(indent);
279        if let Some(obj) = value.as_object() {
280            let name = obj.get("name").and_then(|v| v.as_str()).unwrap_or("default");
281            output.push_str(&format!("{}context \"{}\" {{\n", indent_str, name));
282            for (k, v) in obj {
283                if k == "name" {
284                    continue;
285                }
286                output.push_str(&format!("{}    {} = ", indent_str, k));
287                self.write_json_value(v, output)?;
288                output.push_str("\n");
289            }
290            output.push_str(&format!("{}}}\n", indent_str));
291        }
292        Ok(())
293    }
294    fn convert_toml_table_to_hlx(
295        &self,
296        table: &toml::map::Map<String, toml::Value>,
297        output: &mut String,
298        indent: usize,
299    ) -> Result<()> {
300        let indent_str = "    ".repeat(indent);
301        for (key, value) in table {
302            match value {
303                toml::Value::Table(inner) => {
304                    if key.starts_with("agent") {
305                        output
306                            .push_str(
307                                &format!(
308                                    "{}agent \"{}\" {{\n", indent_str, key
309                                    .trim_start_matches("agent.")
310                                ),
311                            );
312                        self.convert_toml_table_to_hlx(inner, output, indent + 1)?;
313                        output.push_str(&format!("{}}}\n", indent_str));
314                    } else {
315                        output.push_str(&format!("{}{} {{\n", indent_str, key));
316                        self.convert_toml_table_to_hlx(inner, output, indent + 1)?;
317                        output.push_str(&format!("{}}}\n", indent_str));
318                    }
319                }
320                toml::Value::Array(arr) => {
321                    output.push_str(&format!("{}{} [\n", indent_str, key));
322                    for item in arr {
323                        output.push_str(&format!("{}    ", indent_str));
324                        self.write_toml_value(item, output)?;
325                        output.push_str("\n");
326                    }
327                    output.push_str(&format!("{}]\n", indent_str));
328                }
329                _ => {
330                    output.push_str(&format!("{}{} = ", indent_str, key));
331                    self.write_toml_value(value, output)?;
332                    output.push_str("\n");
333                }
334            }
335        }
336        Ok(())
337    }
338    fn convert_yaml_mapping_to_hlx(
339        &self,
340        mapping: &serde_yaml::Mapping,
341        output: &mut String,
342        indent: usize,
343    ) -> Result<()> {
344        let indent_str = "    ".repeat(indent);
345        for (key, value) in mapping {
346            let key_str = match key {
347                serde_yaml::Value::String(s) => s.clone(),
348                serde_yaml::Value::Number(n) => n.to_string(),
349                serde_yaml::Value::Bool(b) => b.to_string(),
350                _ => format!("{:?}", key),
351            };
352            match value {
353                serde_yaml::Value::Mapping(inner) => {
354                    output.push_str(&format!("{}{} {{\n", indent_str, key_str));
355                    self.convert_yaml_mapping_to_hlx(inner, output, indent + 1)?;
356                    output.push_str(&format!("{}}}\n", indent_str));
357                }
358                serde_yaml::Value::Sequence(seq) => {
359                    output.push_str(&format!("{}{} [\n", indent_str, key_str));
360                    for item in seq {
361                        output.push_str(&format!("{}    ", indent_str));
362                        self.write_yaml_value(item, output)?;
363                        output.push_str("\n");
364                    }
365                    output.push_str(&format!("{}]\n", indent_str));
366                }
367                _ => {
368                    output.push_str(&format!("{}{} = ", indent_str, key_str));
369                    self.write_yaml_value(value, output)?;
370                    output.push_str("\n");
371                }
372            }
373        }
374        Ok(())
375    }
376    fn write_json_value(&self, value: &JsonValue, output: &mut String) -> Result<()> {
377        match value {
378            JsonValue::Null => output.push_str("null"),
379            JsonValue::Bool(b) => output.push_str(&b.to_string()),
380            JsonValue::Number(n) => output.push_str(&n.to_string()),
381            JsonValue::String(s) => output.push_str(&format!("\"{}\"", s)),
382            JsonValue::Array(arr) => {
383                output.push('[');
384                for (i, item) in arr.iter().enumerate() {
385                    if i > 0 {
386                        output.push_str(", ");
387                    }
388                    self.write_json_value(item, output)?;
389                }
390                output.push(']');
391            }
392            JsonValue::Object(_) => {
393                output.push_str("{ }");
394            }
395        }
396        Ok(())
397    }
398    fn write_toml_value(&self, value: &toml::Value, output: &mut String) -> Result<()> {
399        match value {
400            toml::Value::String(s) => output.push_str(&format!("\"{}\"", s)),
401            toml::Value::Integer(i) => output.push_str(&i.to_string()),
402            toml::Value::Float(f) => output.push_str(&f.to_string()),
403            toml::Value::Boolean(b) => output.push_str(&b.to_string()),
404            toml::Value::Datetime(d) => output.push_str(&format!("\"{}\"", d)),
405            toml::Value::Array(_) => output.push_str("[]"),
406            toml::Value::Table(_) => output.push_str("{}"),
407        }
408        Ok(())
409    }
410    fn write_yaml_value(
411        &self,
412        value: &serde_yaml::Value,
413        output: &mut String,
414    ) -> Result<()> {
415        match value {
416            serde_yaml::Value::Null => output.push_str("null"),
417            serde_yaml::Value::Bool(b) => output.push_str(&b.to_string()),
418            serde_yaml::Value::Number(n) => output.push_str(&n.to_string()),
419            serde_yaml::Value::String(s) => output.push_str(&format!("\"{}\"", s)),
420            serde_yaml::Value::Sequence(_) => output.push_str("[]"),
421            serde_yaml::Value::Mapping(_) => output.push_str("{}"),
422            serde_yaml::Value::Tagged(_) => output.push_str("null"),
423        }
424        Ok(())
425    }
426    fn parse_duration_string(&self, s: &str) -> String {
427        if s.ends_with("ms") {
428            return format!("{}ms", s.trim_end_matches("ms"));
429        } else if s.ends_with("s") || s.ends_with("sec") || s.ends_with("seconds") {
430            let num = s
431                .chars()
432                .take_while(|c| c.is_numeric() || *c == '.')
433                .collect::<String>();
434            return format!("{}s", num);
435        } else if s.ends_with("m") || s.ends_with("min") || s.ends_with("minutes") {
436            let num = s
437                .chars()
438                .take_while(|c| c.is_numeric() || *c == '.')
439                .collect::<String>();
440            return format!("{}m", num);
441        } else if s.ends_with("h") || s.ends_with("hour") || s.ends_with("hours") {
442            let num = s
443                .chars()
444                .take_while(|c| c.is_numeric() || *c == '.')
445                .collect::<String>();
446            return format!("{}h", num);
447        }
448        format!("\"{}\"", s)
449    }
450}
451impl Default for Migrator {
452    fn default() -> Self {
453        Self::new()
454    }
455}
456#[cfg(test)]
457mod tests {
458    use super::*;
459    #[test]
460    fn test_migrate_json() {
461        let json = r#"{
462            "agent": {
463                "name": "assistant",
464                "model": "gpt-4",
465                "temperature": 0.7
466            }
467        }"#;
468        let migrator = Migrator::new();
469        let result = migrator.migrate_json(json).unwrap();
470        assert!(result.contains("agent \"assistant\""));
471        assert!(result.contains("temperature = 0.7"));
472    }
473    #[test]
474    fn test_migrate_env() {
475        let env = r#"
476DATABASE_URL=postgres://localhost/db
477API_KEY=secret123
478DEBUG=true
479"#;
480        let migrator = Migrator::new();
481        let result = migrator.migrate_env(env).unwrap();
482        assert!(result.contains("context \"environment\""));
483        assert!(result.contains("database_url"));
484        assert!(result.contains("api_key = $API_KEY"));
485    }
486}