1use std::io::Write;
3use std::path::Path;
4use std::process::Command;
5
6pub fn status(repo_path: &Path) {
8 let mut stdout = std::io::stdout();
9 let _ = status_to(&mut stdout, repo_path);
10}
11
12pub fn status_to(writer: &mut impl Write, repo_path: &Path) -> std::io::Result<()> {
14 writeln!(writer, "系统诊断")?;
15 writeln!(writer, "{}", "-".repeat(50))?;
16
17 let c = crate::contract::load(repo_path);
19 let mut used_langs: Vec<String> = Vec::new();
20 if c.scopes.is_empty() {
21 used_langs = crate::contract::detect_all_languages(repo_path);
22 } else {
23 for s in &c.scopes {
24 let scope_dir = repo_path.join(&s.dir);
25 let mut langs = crate::contract::detect_all_languages(&scope_dir);
26 used_langs.append(&mut langs);
27 }
28 }
29 used_langs.sort();
30 used_langs.dedup();
31
32 writeln!(
34 writer,
35 " {:<12} {}",
36 "git",
37 check_command("git", &["--version"])
38 )?;
39 write_gh_status(writer)?;
40
41 for lang in &["rust", "python", "go", "dart", "typescript"] {
43 if !used_langs.iter().any(|l| l == lang) {
44 continue;
45 }
46 match *lang {
47 "rust" => {
48 writeln!(
49 writer,
50 " {:<12} {}",
51 "cargo",
52 check_command("cargo", &["--version"])
53 )?;
54 writeln!(
55 writer,
56 " {:<12} {}",
57 "rustc",
58 check_command("rustc", &["--version"])
59 )?;
60 }
61 "python" => {
62 writeln!(
63 writer,
64 " {:<12} {}",
65 "python",
66 check_command("python", &["--version"])
67 )?;
68 for sub in &["uv", "pytest", "coverage"] {
69 let v = check_command(sub, &["--version"]);
70 writeln!(writer, " {:<10} {}", sub, v)?;
71 }
72 }
73 "go" => {
74 writeln!(
75 writer,
76 " {:<12} {}",
77 "go",
78 check_command("go", &["version"])
79 )?;
80 }
81 "dart" => {
82 writeln!(
83 writer,
84 " {:<12} {}",
85 "flutter",
86 check_command("flutter", &["--version"])
87 )?;
88 let v = check_command("dart", &["--version"]);
89 writeln!(writer, " {:<10} {}", "dart", v)?;
90 }
91 "typescript" => {
92 writeln!(
93 writer,
94 " {:<12} {}",
95 "node",
96 check_command("node", &["--version"])
97 )?;
98 for sub in &["npm", "npx"] {
99 let v = check_command(sub, &["--version"]);
100 writeln!(writer, " {:<10} {}", sub, v)?;
101 }
102 }
103 _ => {}
104 }
105 }
106
107 Ok(())
108}
109
110fn write_gh_status(writer: &mut impl Write) -> std::io::Result<()> {
111 let ver = check_command("gh", &["--version"]);
112 writeln!(writer, " {:<12} {}", "gh", ver)?;
113 match Command::new("gh").args(["auth", "status"]).output() {
114 Ok(out) if out.status.success() => {
115 let msg = String::from_utf8_lossy(&out.stdout).trim().to_string();
116 let auth_line = msg.lines().nth(1).map(|l| l.trim()).unwrap_or("");
117 writeln!(writer, " ✅ {}", auth_line)?;
118 }
119 Ok(out) => {
120 let msg = String::from_utf8_lossy(&out.stderr).trim().to_string();
121 writeln!(
122 writer,
123 " ❌ {}",
124 msg.lines().next().unwrap_or("")
125 )?;
126 }
127 Err(_) => writeln!(writer, " ❌ 未登录")?,
128 }
129 Ok(())
130}
131
132fn check_command(cmd: &str, args: &[&str]) -> String {
133 match Command::new(cmd).args(args).output() {
134 Ok(out) if out.status.success() => {
135 let ver = String::from_utf8_lossy(&out.stdout)
136 .lines()
137 .next()
138 .unwrap_or("")
139 .trim()
140 .to_string();
141 format!("✅ {}", ver)
142 }
143 Ok(out) => {
144 let msg = String::from_utf8_lossy(&out.stderr).trim().to_string();
145 format!("❌ {}", msg)
146 }
147 Err(e) => match e.kind() {
148 std::io::ErrorKind::NotFound => format!("❌ 未安装"),
149 _ => format!("❌ {}", e),
150 },
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn test_check_git_exists() {
160 let result = check_command("git", &["--version"]);
161 assert!(result.starts_with("✅"), "git 应存在: {}", result);
162 }
163
164 #[test]
165 fn test_check_nonexistent() {
166 let result = check_command("nonexistent_cmd_xyz", &["--version"]);
167 assert!(
168 result.contains("未安装"),
169 "不存在的命令应报未安装: {}",
170 result
171 );
172 }
173
174 #[test]
175 fn test_status_to_python() {
176 let d = tempfile::tempdir().unwrap();
177 std::fs::write(
179 d.path().join("pyproject.toml"),
180 "[project]\nname = \"test\"\n",
181 )
182 .unwrap();
183 let mut buf = Vec::new();
184 status_to(&mut buf, d.path()).unwrap();
185 let output = String::from_utf8(buf).expect("非 UTF-8 输出");
186 assert!(output.contains("git"), "应包含 git");
187 assert!(output.contains("python"), "Python 项目应显示 python");
188 }
189
190 #[test]
191 fn test_status_to_go() {
192 let d = tempfile::tempdir().unwrap();
193 std::fs::write(d.path().join("go.mod"), "module test\n").unwrap();
194 let mut buf = Vec::new();
195 status_to(&mut buf, d.path()).unwrap();
196 let output = String::from_utf8(buf).expect("非 UTF-8 输出");
197 assert!(output.contains("go"), "Go 项目应显示 go 工具链");
198 }
199
200 #[test]
201 fn test_status_to_typescript() {
202 let d = tempfile::tempdir().unwrap();
203 std::fs::write(d.path().join("package.json"), "{\"name\":\"test\"}\n").unwrap();
204 let mut buf = Vec::new();
205 status_to(&mut buf, d.path()).unwrap();
206 let output = String::from_utf8(buf).expect("非 UTF-8 输出");
207 assert!(output.contains("node"), "TS 项目应显示 node 工具链");
208 }
209
210 #[test]
211 fn test_status_to_dart() {
212 let d = tempfile::tempdir().unwrap();
213 std::fs::write(d.path().join("pubspec.yaml"), "name: test\n").unwrap();
214 let mut buf = Vec::new();
215 status_to(&mut buf, d.path()).unwrap();
216 let output = String::from_utf8(buf).expect("非 UTF-8 输出");
217 assert!(output.contains("flutter"), "Dart 项目应显示 flutter 工具链");
218 }
219
220 #[test]
221 fn test_status_to_no_lang() {
222 let d = tempfile::tempdir().unwrap();
223 let mut buf = Vec::new();
225 status_to(&mut buf, d.path()).unwrap();
226 let output = String::from_utf8(buf).expect("非 UTF-8 输出");
227 assert!(output.contains("git"), "应始终显示 git");
228 assert!(output.contains("gh"), "应始终显示 gh");
229 }
232
233 #[test]
234 fn test_status_to_output() {
235 let d = tempfile::tempdir().unwrap();
236 std::fs::write(d.path().join("Cargo.toml"), "[package]\n").unwrap();
238 let mut buf = Vec::new();
239 status_to(&mut buf, d.path()).unwrap();
240 let output = String::from_utf8(buf).expect("非 UTF-8 输出");
241
242 assert!(output.contains("系统诊断"), "应包含标题");
244 assert!(output.contains(&"-".repeat(50)), "应包含分隔线");
245 assert!(output.contains("git"), "应包含 git");
246 assert!(output.contains("gh"), "应包含 gh");
247 assert!(output.contains("cargo"), "应包含 cargo");
249 assert!(output.contains("rustc"), "应包含 rustc");
250 }
251}