1use anyhow::{Context, Result};
2use std::process::Command;
3
4#[cfg(feature = "native")]
5use tracing::{debug, info};
6
7#[cfg(not(feature = "native"))]
9macro_rules! info {
10 ($($arg:tt)*) => {{}};
11}
12
13#[cfg(not(feature = "native"))]
14macro_rules! debug {
15 ($($arg:tt)*) => {{}};
16}
17
18#[derive(Debug, Clone)]
20pub struct ToolInfo {
21 pub name: String,
22 pub version: Option<String>,
23 pub path: String,
24 pub available: bool,
25}
26
27#[derive(Debug, Clone)]
29pub struct ToolRegistry {
30 pub decy: Option<ToolInfo>,
31 pub depyler: Option<ToolInfo>,
32 pub bashrs: Option<ToolInfo>,
33 pub ruchy: Option<ToolInfo>,
34 pub trueno: Option<ToolInfo>,
35 pub aprender: Option<ToolInfo>,
36 pub realizar: Option<ToolInfo>,
37 pub renacer: Option<ToolInfo>,
38 pub pmat: Option<ToolInfo>,
39}
40
41impl ToolRegistry {
42 pub fn detect() -> Self {
44 info!("Detecting installed Pragmatic AI Labs tools...");
45
46 Self {
47 decy: detect_tool("decy"),
48 depyler: detect_tool("depyler"),
49 bashrs: detect_tool("bashrs"),
50 ruchy: detect_tool("ruchy"),
51 trueno: detect_tool("trueno"),
52 aprender: detect_tool("aprender"),
53 realizar: detect_tool("realizar"),
54 renacer: detect_tool("renacer"),
55 pmat: detect_tool("pmat"),
56 }
57 }
58
59 pub fn has_transpiler(&self) -> bool {
61 contract_pre_transpile!(self);
62 self.decy.is_some() || self.depyler.is_some() || self.bashrs.is_some()
63 }
64
65 pub fn get_transpiler_for_language(&self, lang: &crate::types::Language) -> Option<&ToolInfo> {
67 contract_pre_transpile!(lang);
68 use crate::types::Language;
69
70 match lang {
71 Language::C | Language::Cpp => self.decy.as_ref(),
72 Language::Python => self.depyler.as_ref(),
73 Language::Shell => self.bashrs.as_ref(),
74 _ => None,
75 }
76 }
77
78 pub fn available_tools(&self) -> Vec<String> {
80 let mut tools = Vec::new();
81
82 if let Some(tool) = &self.decy {
83 if tool.available {
84 tools.push("Decy (C/C++ → Rust)".to_string());
85 }
86 }
87 if let Some(tool) = &self.depyler {
88 if tool.available {
89 tools.push("Depyler (Python → Rust)".to_string());
90 }
91 }
92 if let Some(tool) = &self.bashrs {
93 if tool.available {
94 tools.push("Bashrs (Shell → Rust)".to_string());
95 }
96 }
97 if let Some(tool) = &self.ruchy {
98 if tool.available {
99 tools.push("Ruchy (Rust scripting)".to_string());
100 }
101 }
102 if let Some(tool) = &self.pmat {
103 if tool.available {
104 tools.push("PMAT (Quality analysis)".to_string());
105 }
106 }
107 if let Some(tool) = &self.trueno {
108 if tool.available {
109 tools.push("Trueno (Multi-target compute)".to_string());
110 }
111 }
112 if let Some(tool) = &self.aprender {
113 if tool.available {
114 tools.push("Aprender (ML library)".to_string());
115 }
116 }
117 if let Some(tool) = &self.realizar {
118 if tool.available {
119 tools.push("Realizar (Inference runtime)".to_string());
120 }
121 }
122 if let Some(tool) = &self.renacer {
123 if tool.available {
124 tools.push("Renacer (Syscall tracing)".to_string());
125 }
126 }
127
128 tools
129 }
130
131 pub fn get_installation_instructions(&self, needed_tools: &[&str]) -> Vec<String> {
133 let mut instructions = Vec::new();
134
135 for tool in needed_tools {
136 let instruction = match *tool {
137 "decy" if self.decy.is_none() => Some("Install Decy: cargo install decy"),
138 "depyler" if self.depyler.is_none() => {
139 Some("Install Depyler: cargo install depyler")
140 }
141 "bashrs" if self.bashrs.is_none() => Some("Install Bashrs: cargo install bashrs"),
142 "ruchy" if self.ruchy.is_none() => Some("Install Ruchy: cargo install ruchy"),
143 "pmat" if self.pmat.is_none() => Some("Install PMAT: cargo install pmat"),
144 "trueno" if self.trueno.is_none() => {
145 Some("Install Trueno: Add 'trueno' to Cargo.toml dependencies")
146 }
147 "aprender" if self.aprender.is_none() => {
148 Some("Install Aprender: Add 'aprender' to Cargo.toml dependencies")
149 }
150 "realizar" if self.realizar.is_none() => {
151 Some("Install Realizar: Add 'realizar' to Cargo.toml dependencies")
152 }
153 "renacer" if self.renacer.is_none() => {
154 Some("Install Renacer: cargo install renacer")
155 }
156 _ => None,
157 };
158
159 if let Some(inst) = instruction {
160 instructions.push(inst.to_string());
161 }
162 }
163
164 instructions
165 }
166}
167
168#[cfg(feature = "native")]
170fn detect_tool(name: &str) -> Option<ToolInfo> {
171 debug!("Checking for tool: {}", name);
172
173 let path = match which::which(name) {
175 Ok(p) => p.to_string_lossy().to_string(),
176 Err(_) => {
177 debug!("Tool '{}' not found in PATH", name);
178 return None;
179 }
180 };
181
182 let version = get_tool_version(name);
184
185 debug!("Found tool '{}' at '{}' (version: {:?})", name, path, version);
186
187 Some(ToolInfo { name: name.to_string(), version, path, available: true })
188}
189
190#[cfg(not(feature = "native"))]
192fn detect_tool(name: &str) -> Option<ToolInfo> {
193 let _ = name;
194 None
195}
196
197fn get_tool_version(name: &str) -> Option<String> {
199 let output = Command::new(name).arg("--version").output().ok()?;
200
201 if !output.status.success() {
202 return None;
203 }
204
205 let stdout = String::from_utf8_lossy(&output.stdout);
206 let version_line = stdout.lines().next()?;
207
208 let parts: Vec<&str> = version_line.split_whitespace().collect();
214 let version = (*parts.last()?).to_string();
215
216 Some(version)
217}
218
219pub fn run_tool(
221 tool_name: &str,
222 args: &[&str],
223 working_dir: Option<&std::path::Path>,
224) -> Result<String> {
225 debug!("Running tool: {} {:?}", tool_name, args);
226
227 let mut cmd = Command::new(tool_name);
228 cmd.args(args);
229
230 if let Some(dir) = working_dir {
231 cmd.current_dir(dir);
232 }
233
234 let output = cmd.output().with_context(|| format!("Failed to run tool: {}", tool_name))?;
235
236 if !output.status.success() {
237 let stderr = String::from_utf8_lossy(&output.stderr);
238 anyhow::bail!(
239 "Tool '{}' failed with exit code {:?}: {}",
240 tool_name,
241 output.status.code(),
242 stderr
243 );
244 }
245
246 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
247 Ok(stdout)
248}
249
250pub fn transpile_python(
252 input_path: &std::path::Path,
253 output_path: &std::path::Path,
254) -> Result<String> {
255 contract_pre_transpile!(input_path);
256 info!("Transpiling Python with Depyler: {:?} → {:?}", input_path, output_path);
257
258 let input_str = input_path.to_string_lossy();
259 let output_str = output_path.to_string_lossy();
260
261 let args = vec![
262 "transpile",
263 "--input",
264 &input_str,
265 "--output",
266 &output_str,
267 "--format",
268 "project", ];
270
271 let result = run_tool("depyler", &args, None);
272 if let Ok(ref val) = result {
273 contract_post_configuration!(val);
274 }
275 result
276}
277
278pub fn transpile_shell(
280 input_path: &std::path::Path,
281 output_path: &std::path::Path,
282) -> Result<String> {
283 contract_pre_transpile!(input_path);
284 info!("Transpiling Shell with Bashrs: {:?} → {:?}", input_path, output_path);
285
286 let input_str = input_path.to_string_lossy();
287 let output_str = output_path.to_string_lossy();
288
289 let args = vec![
290 "build",
291 &input_str,
292 "-o",
293 &output_str,
294 "--target",
295 "posix", "--verify",
297 "strict", ];
299
300 let result = run_tool("bashrs", &args, None);
301 if let Ok(ref val) = result {
302 contract_post_configuration!(val);
303 }
304 result
305}
306
307pub fn transpile_c_cpp(
309 input_path: &std::path::Path,
310 output_path: &std::path::Path,
311) -> Result<String> {
312 contract_pre_transpile!(input_path);
313 info!("Transpiling C/C++ with Decy: {:?} → {:?}", input_path, output_path);
314
315 let input_str = input_path.to_string_lossy();
316 let output_str = output_path.to_string_lossy();
317
318 let args = vec!["transpile", "--input", &input_str, "--output", &output_str];
320
321 let result = run_tool("decy", &args, None);
322 if let Ok(ref val) = result {
323 contract_post_configuration!(val);
324 }
325 result
326}
327
328pub fn analyze_quality(path: &std::path::Path) -> Result<String> {
330 contract_pre_analyze!(path);
331 info!("Running PMAT quality analysis: {:?}", path);
332
333 let path_str = path.to_string_lossy();
334
335 let args = vec!["analyze", "complexity", &path_str, "--format", "json"];
336
337 let result = run_tool("pmat", &args, None);
338 if let Ok(ref val) = result {
339 contract_post_configuration!(val);
340 }
341 result
342}
343
344pub fn run_ruchy_script(script_path: &std::path::Path) -> Result<String> {
346 info!("Running Ruchy script: {:?}", script_path);
347
348 let script_str = script_path.to_string_lossy();
349
350 let args = vec!["run", &script_str];
351
352 run_tool("ruchy", &args, None)
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 use std::path::PathBuf;
359
360 #[test]
365 fn test_tool_info_creation() {
366 let tool = ToolInfo {
367 name: "decy".to_string(),
368 version: Some("1.0.0".to_string()),
369 path: "/usr/local/bin/decy".to_string(),
370 available: true,
371 };
372
373 assert_eq!(tool.name, "decy");
374 assert_eq!(tool.version, Some("1.0.0".to_string()));
375 assert_eq!(tool.path, "/usr/local/bin/decy");
376 assert!(tool.available);
377 }
378
379 #[test]
380 fn test_tool_info_no_version() {
381 let tool = ToolInfo {
382 name: "test_tool".to_string(),
383 version: None,
384 path: "/bin/test".to_string(),
385 available: true,
386 };
387
388 assert_eq!(tool.name, "test_tool");
389 assert!(tool.version.is_none());
390 }
391
392 #[test]
393 fn test_tool_info_clone() {
394 let tool1 = ToolInfo {
395 name: "depyler".to_string(),
396 version: Some("2.0.0".to_string()),
397 path: "/usr/bin/depyler".to_string(),
398 available: true,
399 };
400
401 let tool2 = tool1.clone();
402 assert_eq!(tool1.name, tool2.name);
403 assert_eq!(tool1.version, tool2.version);
404 assert_eq!(tool1.path, tool2.path);
405 assert_eq!(tool1.available, tool2.available);
406 }
407
408 #[test]
409 fn test_tool_info_debug() {
410 let tool = ToolInfo {
411 name: "bashrs".to_string(),
412 version: Some("0.5.0".to_string()),
413 path: "/usr/local/bin/bashrs".to_string(),
414 available: true,
415 };
416
417 let debug_str = format!("{:?}", tool);
418 assert!(debug_str.contains("bashrs"));
419 assert!(debug_str.contains("0.5.0"));
420 }
421
422 fn create_test_registry() -> ToolRegistry {
427 ToolRegistry {
428 decy: Some(ToolInfo {
429 name: "decy".to_string(),
430 version: Some("1.0.0".to_string()),
431 path: "/usr/bin/decy".to_string(),
432 available: true,
433 }),
434 depyler: Some(ToolInfo {
435 name: "depyler".to_string(),
436 version: Some("2.0.0".to_string()),
437 path: "/usr/bin/depyler".to_string(),
438 available: true,
439 }),
440 bashrs: None,
441 ruchy: Some(ToolInfo {
442 name: "ruchy".to_string(),
443 version: Some("0.3.0".to_string()),
444 path: "/usr/bin/ruchy".to_string(),
445 available: true,
446 }),
447 trueno: None,
448 aprender: None,
449 realizar: None,
450 renacer: None,
451 pmat: Some(ToolInfo {
452 name: "pmat".to_string(),
453 version: Some("1.5.0".to_string()),
454 path: "/usr/bin/pmat".to_string(),
455 available: true,
456 }),
457 }
458 }
459
460 fn create_empty_registry() -> ToolRegistry {
461 ToolRegistry {
462 decy: None,
463 depyler: None,
464 bashrs: None,
465 ruchy: None,
466 trueno: None,
467 aprender: None,
468 realizar: None,
469 renacer: None,
470 pmat: None,
471 }
472 }
473
474 #[test]
475 fn test_tool_registry_clone() {
476 let registry1 = create_test_registry();
477 let registry2 = registry1.clone();
478
479 assert!(registry2.decy.is_some());
480 assert!(registry2.depyler.is_some());
481 assert!(registry2.bashrs.is_none());
482 }
483
484 #[test]
485 fn test_tool_registry_debug() {
486 let registry = create_test_registry();
487 let debug_str = format!("{:?}", registry);
488 assert!(debug_str.contains("decy"));
489 assert!(debug_str.contains("depyler"));
490 }
491
492 #[test]
493 fn test_tool_detection() {
494 let registry = ToolRegistry::detect();
496
497 println!("Available tools: {:?}", registry.available_tools());
500 }
501
502 #[test]
503 fn test_has_transpiler_with_tools() {
504 let registry = create_test_registry();
505 assert!(registry.has_transpiler());
506 }
507
508 #[test]
509 fn test_has_transpiler_empty() {
510 let registry = create_empty_registry();
511 assert!(!registry.has_transpiler());
512 }
513
514 #[test]
515 fn test_has_transpiler_only_decy() {
516 let mut registry = create_empty_registry();
517 registry.decy = Some(ToolInfo {
518 name: "decy".to_string(),
519 version: None,
520 path: "/usr/bin/decy".to_string(),
521 available: true,
522 });
523 assert!(registry.has_transpiler());
524 }
525
526 #[test]
527 fn test_has_transpiler_only_depyler() {
528 let mut registry = create_empty_registry();
529 registry.depyler = Some(ToolInfo {
530 name: "depyler".to_string(),
531 version: None,
532 path: "/usr/bin/depyler".to_string(),
533 available: true,
534 });
535 assert!(registry.has_transpiler());
536 }
537
538 #[test]
539 fn test_has_transpiler_only_bashrs() {
540 let mut registry = create_empty_registry();
541 registry.bashrs = Some(ToolInfo {
542 name: "bashrs".to_string(),
543 version: None,
544 path: "/usr/bin/bashrs".to_string(),
545 available: true,
546 });
547 assert!(registry.has_transpiler());
548 }
549
550 #[test]
551 fn test_get_transpiler_for_language_c() {
552 let registry = create_test_registry();
553 let tool = registry.get_transpiler_for_language(&crate::types::Language::C);
554 assert!(tool.is_some());
555 assert_eq!(tool.expect("unexpected failure").name, "decy");
556 }
557
558 #[test]
559 fn test_get_transpiler_for_language_cpp() {
560 let registry = create_test_registry();
561 let tool = registry.get_transpiler_for_language(&crate::types::Language::Cpp);
562 assert!(tool.is_some());
563 assert_eq!(tool.expect("unexpected failure").name, "decy");
564 }
565
566 #[test]
567 fn test_get_transpiler_for_language_python() {
568 let registry = create_test_registry();
569 let tool = registry.get_transpiler_for_language(&crate::types::Language::Python);
570 assert!(tool.is_some());
571 assert_eq!(tool.expect("unexpected failure").name, "depyler");
572 }
573
574 #[test]
575 fn test_get_transpiler_for_language_shell() {
576 let mut registry = create_test_registry();
577 registry.bashrs = Some(ToolInfo {
578 name: "bashrs".to_string(),
579 version: None,
580 path: "/usr/bin/bashrs".to_string(),
581 available: true,
582 });
583
584 let tool = registry.get_transpiler_for_language(&crate::types::Language::Shell);
585 assert!(tool.is_some());
586 assert_eq!(tool.expect("unexpected failure").name, "bashrs");
587 }
588
589 #[test]
590 fn test_get_transpiler_for_language_rust() {
591 let registry = create_test_registry();
592 let tool = registry.get_transpiler_for_language(&crate::types::Language::Rust);
593 assert!(tool.is_none());
594 }
595
596 #[test]
597 fn test_get_transpiler_for_language_javascript() {
598 let registry = create_test_registry();
599 let tool = registry.get_transpiler_for_language(&crate::types::Language::JavaScript);
600 assert!(tool.is_none());
601 }
602
603 #[test]
604 fn test_get_transpiler_for_language_other() {
605 let registry = create_test_registry();
606 let tool = registry
607 .get_transpiler_for_language(&crate::types::Language::Other("Kotlin".to_string()));
608 assert!(tool.is_none());
609 }
610
611 #[test]
612 fn test_available_tools_all_installed() {
613 let mut registry = create_test_registry();
614 registry.bashrs = Some(ToolInfo {
615 name: "bashrs".to_string(),
616 version: Some("1.0.0".to_string()),
617 path: "/usr/bin/bashrs".to_string(),
618 available: true,
619 });
620 registry.trueno = Some(ToolInfo {
621 name: "trueno".to_string(),
622 version: Some("2.0.0".to_string()),
623 path: "/usr/bin/trueno".to_string(),
624 available: true,
625 });
626 registry.aprender = Some(ToolInfo {
627 name: "aprender".to_string(),
628 version: Some("1.0.0".to_string()),
629 path: "/usr/bin/aprender".to_string(),
630 available: true,
631 });
632 registry.realizar = Some(ToolInfo {
633 name: "realizar".to_string(),
634 version: Some("1.0.0".to_string()),
635 path: "/usr/bin/realizar".to_string(),
636 available: true,
637 });
638 registry.renacer = Some(ToolInfo {
639 name: "renacer".to_string(),
640 version: Some("1.0.0".to_string()),
641 path: "/usr/bin/renacer".to_string(),
642 available: true,
643 });
644
645 let tools = registry.available_tools();
646 assert_eq!(tools.len(), 9);
647 assert!(tools.contains(&"Decy (C/C++ → Rust)".to_string()));
648 assert!(tools.contains(&"Depyler (Python → Rust)".to_string()));
649 assert!(tools.contains(&"Bashrs (Shell → Rust)".to_string()));
650 assert!(tools.contains(&"Ruchy (Rust scripting)".to_string()));
651 assert!(tools.contains(&"PMAT (Quality analysis)".to_string()));
652 }
653
654 #[test]
655 fn test_available_tools_empty() {
656 let registry = create_empty_registry();
657 let tools = registry.available_tools();
658 assert_eq!(tools.len(), 0);
659 }
660
661 #[test]
662 fn test_available_tools_partial() {
663 let registry = create_test_registry();
664 let tools = registry.available_tools();
665
666 assert_eq!(tools.len(), 4);
668 assert!(tools.contains(&"Decy (C/C++ → Rust)".to_string()));
669 assert!(tools.contains(&"Depyler (Python → Rust)".to_string()));
670 assert!(tools.contains(&"Ruchy (Rust scripting)".to_string()));
671 assert!(tools.contains(&"PMAT (Quality analysis)".to_string()));
672 }
673
674 #[test]
675 fn test_available_tools_unavailable_flag() {
676 let mut registry = create_test_registry();
677 if let Some(tool) = &mut registry.depyler {
679 tool.available = false;
680 }
681
682 let tools = registry.available_tools();
683 assert!(!tools.iter().any(|t| t.contains("Depyler")));
685 }
686
687 #[test]
688 fn test_get_installation_instructions_all_missing() {
689 let registry = create_empty_registry();
690 let instructions = registry.get_installation_instructions(&[
691 "decy", "depyler", "bashrs", "ruchy", "pmat", "trueno", "aprender", "realizar",
692 "renacer",
693 ]);
694
695 assert_eq!(instructions.len(), 9);
696 assert!(instructions.contains(&"Install Decy: cargo install decy".to_string()));
697 assert!(instructions.contains(&"Install Depyler: cargo install depyler".to_string()));
698 assert!(instructions.contains(&"Install Bashrs: cargo install bashrs".to_string()));
699 assert!(instructions.contains(&"Install Ruchy: cargo install ruchy".to_string()));
700 assert!(instructions.contains(&"Install PMAT: cargo install pmat".to_string()));
701 assert!(instructions
702 .contains(&"Install Trueno: Add 'trueno' to Cargo.toml dependencies".to_string()));
703 assert!(instructions
704 .contains(&"Install Aprender: Add 'aprender' to Cargo.toml dependencies".to_string()));
705 assert!(instructions
706 .contains(&"Install Realizar: Add 'realizar' to Cargo.toml dependencies".to_string()));
707 assert!(instructions.contains(&"Install Renacer: cargo install renacer".to_string()));
708 }
709
710 #[test]
711 fn test_get_installation_instructions_none_missing() {
712 let registry = create_test_registry();
713 let instructions =
714 registry.get_installation_instructions(&["decy", "depyler", "ruchy", "pmat"]);
715
716 assert_eq!(instructions.len(), 0);
718 }
719
720 #[test]
721 fn test_get_installation_instructions_partial() {
722 let registry = create_test_registry();
723 let instructions = registry.get_installation_instructions(&["decy", "bashrs", "trueno"]);
724
725 assert_eq!(instructions.len(), 2);
727 assert!(instructions.contains(&"Install Bashrs: cargo install bashrs".to_string()));
728 assert!(instructions
729 .contains(&"Install Trueno: Add 'trueno' to Cargo.toml dependencies".to_string()));
730 }
731
732 #[test]
733 fn test_get_installation_instructions_unknown_tool() {
734 let registry = create_empty_registry();
735 let instructions = registry.get_installation_instructions(&["unknown_tool", "decy"]);
736
737 assert_eq!(instructions.len(), 1);
739 assert!(instructions.contains(&"Install Decy: cargo install decy".to_string()));
740 }
741
742 #[test]
743 fn test_get_installation_instructions_empty_list() {
744 let registry = create_test_registry();
745 let instructions = registry.get_installation_instructions(&[]);
746
747 assert_eq!(instructions.len(), 0);
748 }
749
750 #[test]
755 fn test_transpile_python_paths() {
756 let input = PathBuf::from("/path/to/input.py");
757 let output = PathBuf::from("/path/to/output");
758
759 let _result = transpile_python(&input, &output);
762 }
763
764 #[test]
765 fn test_transpile_shell_paths() {
766 let input = PathBuf::from("/path/to/script.sh");
767 let output = PathBuf::from("/path/to/output");
768
769 let _result = transpile_shell(&input, &output);
771 }
772
773 #[test]
774 fn test_transpile_c_cpp_paths() {
775 let input = PathBuf::from("/path/to/code.c");
776 let output = PathBuf::from("/path/to/output");
777
778 let _result = transpile_c_cpp(&input, &output);
780 }
781
782 #[test]
783 fn test_analyze_quality_path() {
784 let path = PathBuf::from("/path/to/project");
785
786 let _result = analyze_quality(&path);
788 }
789
790 #[test]
791 fn test_run_ruchy_script_path() {
792 let script = PathBuf::from("/path/to/script.ruchy");
793
794 let _result = run_ruchy_script(&script);
796 }
797
798 #[test]
799 fn test_run_tool_basic_args() {
800 let result = run_tool("echo", &["test"], None);
802
803 if let Ok(output) = result {
805 assert!(output.contains("test"));
806 }
807 }
808
809 #[test]
810 fn test_run_tool_with_working_dir() {
811 use std::env;
812 let current_dir = env::current_dir().expect("current_dir failed");
813
814 let result = run_tool("pwd", &[], Some(¤t_dir));
815
816 if let Ok(output) = result {
818 assert!(!output.is_empty());
819 }
820 }
821}