light_fw_cli_core/
check.rs1use std::io::{self, IsTerminal, Write};
9use std::path::Path;
10
11use crate::project::analyze_project;
12use crate::registry::ComponentRegistry;
13use crate::report::ProjectReport;
14
15pub fn ensure_components_present(
34 project_root: &Path,
35 install_fn: &dyn Fn(&str) -> crate::Result<()>,
36 auto_install: bool,
37) -> crate::Result<bool> {
38 let report = analyze_project(project_root)?;
39 if report.missing.is_empty() {
40 return Ok(true);
41 }
42
43 let block = format_missing_block(&report);
44 print!("{block}");
45 io::stdout().flush()?;
46
47 if !auto_install {
49 eprintln!(
50 "Автоматическая установка отключена (--no-auto-install): \
51 установите компоненты вручную."
52 );
53 for name in &report.missing {
54 eprintln!(" light add {}", name.name);
55 }
56 return Ok(false);
57 }
58
59 let interactive = is_interactive();
60 let answer = if interactive {
61 print!("Установить их автоматически? (Y/n): ");
62 io::stdout().flush()?;
63 let mut line = String::new();
64 io::stdin().read_line(&mut line)?;
65 parse_yn(&line)
66 } else {
67 eprintln!(
71 "Неинтерактивный режим: устанавливаю недостающие компоненты \
72 автоматически."
73 );
74 true
75 };
76
77 if !answer {
78 for name in &report.missing {
80 eprintln!(" light add {}", name.name);
81 }
82 return Ok(false);
83 }
84
85 for name in &report.missing {
87 eprintln!("light: auto-installing {}", name.name);
88 install_fn(&name.name)?;
89 }
90 Ok(true)
91}
92
93fn is_interactive() -> bool {
99 io::stdin().is_terminal()
100}
101
102pub fn format_missing_block(report: &ProjectReport) -> String {
107 let registry = ComponentRegistry::builtin();
108 let mut out = String::from("⚠️ Обнаружены недостающие компоненты:\n");
109 for comp in &report.missing {
110 let info = registry.by_name(&comp.name);
111 let tag_hint = info
112 .and_then(|c| {
113 c.provides
114 .elements
115 .iter()
116 .find(|el| report.scan.tags.contains_key(*el))
117 })
118 .map(|el| format!(" (для <{el}>)"))
119 .unwrap_or_default();
120 out.push_str(&format!(" - {}{}\n", comp.name, tag_hint));
121 }
122 out
123}
124
125pub fn parse_yn(line: &str) -> bool {
128 let trimmed = line.trim().to_lowercase();
129 matches!(trimmed.as_str(), "" | "y" | "yes")
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::manifest::Dependency;
136 use crate::report::{Issue, ProjectReport, Recommendation};
137 use crate::resolve::RequiredComponent;
138 use crate::scan::ScanResult;
139
140 fn test_report() -> ProjectReport {
141 let mut scan = ScanResult::default();
142 scan.tags.insert("select".to_string(), 1);
143 scan.attributes.insert("required".to_string(), 2);
144
145 ProjectReport {
146 scan,
147 installed: Vec::<Dependency>::new(),
148 missing: vec![
149 RequiredComponent {
150 name: "light-fw-element-form".into(),
151 },
152 RequiredComponent {
153 name: "light-fw-behavior-validation".into(),
154 },
155 ],
156 unused: Vec::new(),
157 issues: Vec::<Issue>::new(),
158 recommendations: Vec::<Recommendation>::new(),
159 }
160 }
161
162 #[test]
163 fn format_missing_block_with_tags() {
164 let report = test_report();
165 let block = format_missing_block(&report);
166 assert!(block.starts_with("⚠️ Обнаружены недостающие компоненты:"));
167 assert!(block.contains("- light-fw-element-form"));
168 assert!(block.contains("- light-fw-behavior-validation"));
169 }
170
171 #[test]
172 fn parse_yn_accepts_yes() {
173 assert!(parse_yn("y"));
174 assert!(parse_yn("Y"));
175 assert!(parse_yn("yes"));
176 assert!(parse_yn("YES"));
177 assert!(parse_yn("Yes"));
178 }
179
180 #[test]
181 fn parse_yn_accepts_empty() {
182 assert!(parse_yn(""));
183 assert!(parse_yn(" \n"));
184 }
185
186 #[test]
187 fn parse_yn_rejects_no() {
188 assert!(!parse_yn("n"));
189 assert!(!parse_yn("no"));
190 assert!(!parse_yn("N"));
191 assert!(!parse_yn("hello"));
192 }
193
194 #[test]
203 fn noninteractive_branch_autinstalls_without_reading_stdin() {
204 use crate::resolve::RequiredComponent;
205
206 let missing = vec![
209 RequiredComponent {
210 name: "light-fw-element-form".into(),
211 },
212 RequiredComponent {
213 name: "light-fw-behavior-validation".into(),
214 },
215 ];
216 let non_interactive_answer = true; let installed = emulate_auto_install(&missing, non_interactive_answer);
220 assert_eq!(
221 installed,
222 vec![
223 "light-fw-element-form".to_string(),
224 "light-fw-behavior-validation".to_string()
225 ],
226 "non-interactive mode installs every missing component"
227 );
228 assert!(
230 non_interactive_answer,
231 "non-interactive mode returns ready=true"
232 );
233
234 assert!(parse_yn("\n"), "empty stdin line means auto-yes");
237 assert!(!parse_yn("n\n"), "explicit no must refuse");
239 let refused = emulate_auto_install(&missing, parse_yn("n\n"));
240 assert!(refused.is_empty(), "no components installed after refusal");
241 }
242
243 fn emulate_auto_install(
248 missing: &[crate::resolve::RequiredComponent],
249 answer: bool,
250 ) -> Vec<String> {
251 let installed: std::cell::RefCell<Vec<String>> = std::cell::RefCell::new(Vec::new());
252 let install_fn = |name: &str| {
253 installed.borrow_mut().push(name.to_string());
254 };
255 if answer {
256 for c in missing {
257 install_fn(&c.name);
258 }
259 }
260 installed.into_inner()
261 }
262}