torvyn_cli/commands/
check.rs1use crate::cli::CheckArgs;
7use crate::errors::CliError;
8use crate::output::terminal;
9use crate::output::{CommandResult, HumanRenderable, OutputContext};
10use serde::Serialize;
11use std::path::Path;
12
13#[derive(Debug, Serialize)]
15pub struct CheckResult {
16 pub all_passed: bool,
18 pub wit_files_parsed: usize,
20 pub error_count: usize,
22 pub warning_count: usize,
24 pub diagnostics: Vec<CheckDiagnostic>,
26}
27
28#[derive(Debug, Serialize, Clone)]
30pub struct CheckDiagnostic {
31 pub severity: String,
33 pub code: String,
35 pub message: String,
37 pub location: Option<String>,
39 pub help: Option<String>,
41}
42
43impl HumanRenderable for CheckResult {
44 fn render_human(&self, ctx: &OutputContext) {
45 if self.all_passed {
46 terminal::print_success(ctx, "Torvyn.toml is valid");
47 terminal::print_success(
48 ctx,
49 &format!(
50 "WIT contracts parsed ({} file(s), 0 errors)",
51 self.wit_files_parsed
52 ),
53 );
54 terminal::print_success(ctx, "World definition resolves correctly");
55 terminal::print_success(ctx, "Capability declarations consistent");
56 eprintln!();
57 eprintln!(" All checks passed.");
58 } else {
59 for d in &self.diagnostics {
60 let prefix = if d.severity == "error" {
61 if ctx.color_enabled {
62 format!(
63 "{}",
64 console::style(format!("error[{}]:", d.code)).red().bold()
65 )
66 } else {
67 format!("error[{}]:", d.code)
68 }
69 } else if ctx.color_enabled {
70 format!(
71 "{}",
72 console::style(format!("warning[{}]:", d.code))
73 .yellow()
74 .bold()
75 )
76 } else {
77 format!("warning[{}]:", d.code)
78 };
79 eprintln!("\n{prefix} {}", d.message);
80 if let Some(loc) = &d.location {
81 eprintln!(" --> {loc}");
82 }
83 if let Some(help) = &d.help {
84 if ctx.color_enabled {
85 eprintln!(" {} {help}", console::style("help:").cyan().bold());
86 } else {
87 eprintln!(" help: {help}");
88 }
89 }
90 }
91 eprintln!(
92 "\n {} error(s), {} warning(s)",
93 self.error_count, self.warning_count
94 );
95 }
96 }
97}
98
99pub async fn execute(
103 args: &CheckArgs,
104 ctx: &OutputContext,
105) -> Result<CommandResult<CheckResult>, CliError> {
106 let manifest_path = &args.manifest;
107
108 if !manifest_path.exists() {
110 return Err(CliError::Config {
111 detail: format!("Manifest not found: {}", manifest_path.display()),
112 file: Some(manifest_path.display().to_string()),
113 suggestion:
114 "Run this command from a Torvyn project directory, or use --manifest <path>.".into(),
115 });
116 }
117
118 ctx.print_debug(&format!("Checking manifest: {}", manifest_path.display()));
119
120 let mut diagnostics = Vec::new();
121 let mut wit_files_parsed = 0;
122
123 let manifest_content = std::fs::read_to_string(manifest_path).map_err(|e| CliError::Io {
125 detail: format!("Cannot read manifest: {e}"),
126 path: Some(manifest_path.display().to_string()),
127 })?;
128
129 match torvyn_config::ComponentManifest::from_toml_str(
130 &manifest_content,
131 manifest_path.to_str().unwrap_or("Torvyn.toml"),
132 ) {
133 Ok(_manifest) => {
134 ctx.print_debug("Manifest parsed successfully");
135 }
136 Err(errors) => {
137 for err in &errors {
138 diagnostics.push(CheckDiagnostic {
139 severity: "error".into(),
140 code: err.code.to_string(),
141 message: err.message.clone(),
142 location: Some(err.file.clone()),
143 help: if err.suggestion.is_empty() {
144 None
145 } else {
146 Some(err.suggestion.clone())
147 },
148 });
149 }
150 }
151 }
152
153 let project_dir = manifest_path.parent().unwrap_or(Path::new("."));
155 let wit_dir = project_dir.join("wit");
156
157 if wit_dir.exists() {
158 if let Ok(entries) = std::fs::read_dir(&wit_dir) {
160 for entry in entries.flatten() {
161 if entry
162 .path()
163 .extension()
164 .map(|e| e == "wit")
165 .unwrap_or(false)
166 {
167 wit_files_parsed += 1;
168 }
169 }
170 }
171
172 let wit_parser = torvyn_contracts::WitParserImpl::new();
174 let result = torvyn_contracts::validate_component(project_dir, &wit_parser);
175 for diag in &result.diagnostics {
176 let severity = match diag.severity {
177 torvyn_contracts::Severity::Error => "error",
178 torvyn_contracts::Severity::Warning => "warning",
179 torvyn_contracts::Severity::Hint => "warning",
180 };
181
182 let location = diag.locations.first().map(|l| {
183 format!(
184 "{}:{}:{}",
185 l.location.file.display(),
186 l.location.line,
187 l.location.column
188 )
189 });
190
191 diagnostics.push(CheckDiagnostic {
192 severity: severity.into(),
193 code: format!("{}", diag.code),
194 message: diag.message.clone(),
195 location,
196 help: diag.help.clone(),
197 });
198 }
199 } else {
200 diagnostics.push(CheckDiagnostic {
201 severity: "warning".into(),
202 code: "E0100".into(),
203 message: "No wit/ directory found".into(),
204 location: Some(project_dir.display().to_string()),
205 help: Some("Create a wit/ directory with your component's world definition.".into()),
206 });
207 }
208
209 let error_count = diagnostics.iter().filter(|d| d.severity == "error").count();
210 let warning_count = diagnostics
211 .iter()
212 .filter(|d| d.severity == "warning")
213 .count();
214
215 let all_passed = if args.strict {
216 error_count == 0 && warning_count == 0
217 } else {
218 error_count == 0
219 };
220
221 let result = CheckResult {
222 all_passed,
223 wit_files_parsed,
224 error_count,
225 warning_count,
226 diagnostics: diagnostics.clone(),
227 };
228
229 if !all_passed {
230 return Err(CliError::Contract {
231 detail: format!("{error_count} error(s) found during validation"),
232 diagnostics: result
233 .diagnostics
234 .iter()
235 .filter(|d| d.severity == "error")
236 .map(|d| d.message.clone())
237 .collect(),
238 });
239 }
240
241 Ok(CommandResult {
242 success: true,
243 command: "check".into(),
244 data: result,
245 warnings: vec![],
246 })
247}