1use std::path::Path;
5use std::process::{Command, Stdio};
6use std::time::{Duration, Instant};
7
8pub trait Machine {
10 fn installed(&self, program: &str) -> bool;
12 fn docker_running(&self) -> bool;
14 fn os(&self) -> String;
16 fn run(&self, program: &str, args: &[&str], dir: &Path) -> Result<(), String>;
18}
19
20pub struct System;
22
23impl Machine for System {
24 fn installed(&self, program: &str) -> bool {
25 Command::new(program)
26 .arg("--version")
27 .stdout(Stdio::null())
28 .stderr(Stdio::null())
29 .status()
30 .is_ok_and(|status| status.success())
31 }
32
33 fn docker_running(&self) -> bool {
34 let Ok(mut child) = Command::new("docker")
36 .arg("info")
37 .stdout(Stdio::null())
38 .stderr(Stdio::null())
39 .spawn()
40 else {
41 return false;
42 };
43 let started = Instant::now();
44 while started.elapsed() < Duration::from_secs(5) {
45 match child.try_wait() {
46 Ok(Some(status)) => return status.success(),
47 Ok(None) => std::thread::sleep(Duration::from_millis(50)),
48 Err(_) => return false,
49 }
50 }
51 let _ = child.kill();
52 false
53 }
54
55 fn os(&self) -> String {
56 match std::env::consts::OS {
57 "macos" => "macOS".into(),
58 "windows" => "Windows".into(),
59 "linux" => linux_distribution().unwrap_or_else(|| "Linux".into()),
60 other => other.into(),
61 }
62 }
63
64 fn run(&self, program: &str, args: &[&str], dir: &Path) -> Result<(), String> {
65 let output = Command::new(program)
66 .args(args)
67 .current_dir(dir)
68 .output()
69 .map_err(|error| error.to_string())?;
70 match output.status.success() {
71 true => Ok(()),
72 false => Err(String::from_utf8_lossy(&output.stderr).trim().to_string()),
73 }
74 }
75}
76
77fn linux_distribution() -> Option<String> {
79 let release = std::fs::read_to_string("/etc/os-release").ok()?;
80 release.lines().find_map(|line| {
81 let value = line.strip_prefix("PRETTY_NAME=")?;
82 Some(value.trim_matches('"').to_string())
83 })
84}
85
86pub struct Tool {
88 pub program: &'static str,
89 pub name: &'static str,
90 pub why: &'static str,
92}
93
94pub const GIT: Tool = Tool {
95 program: "git",
96 name: "git",
97 why: "to version the project, so that every change can be reviewed and undone",
98};
99
100pub const DOCKER: Tool = Tool {
101 program: "docker",
102 name: "Docker",
103 why: "to run the app, its database and its front end in dev without installing them",
104};
105
106impl Tool {
107 pub fn install_prompt(&self, os: &str) -> String {
109 format!(
110 "I am on {os} and I need {name} for a Balise project, {why}. \
111 Explain how to install it on my system, step by step, and how to check that it works.\n",
112 name = self.name,
113 why = self.why
114 )
115 }
116
117 pub fn learn_prompt(&self) -> String {
119 format!(
120 "I am starting a Balise project, which uses {name} {why}. \
121 Give me a short tutorial: what {name} is, why Balise uses it, and the few commands worth knowing.\n",
122 name = self.name,
123 why = self.why
124 )
125 }
126}