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