1pub mod protocol;
4pub mod ipc;
5pub mod claude_wrapper;
6
7pub use claude_wrapper::ClaudeWrapper;
8pub use ipc::IpcChannel;
9pub use protocol::{ClaudeRequest, ClaudeResponse, IssueDetail, IssueSeverity, PROTOCOL_VERSION};
10
11use oparry_core::{Error, Result};
13use oparry_parser::{Language, parser_for_language};
14use oparry_validators::Validators;
15use std::path::Path;
16use std::process::{Command, Stdio};
17use std::io::{BufRead, BufReader};
18use tracing::debug;
19
20#[derive(Debug, Clone)]
22pub struct WrapConfig {
23 pub block: bool,
25 pub allowed_patterns: Vec<String>,
27 pub denied_patterns: Vec<String>,
29 pub enable_autofix: bool,
31 pub autofix_strategy: Option<String>,
33 pub dry_run: bool,
35 pub strict_mode: bool,
37 pub force_mode: bool,
39}
40
41impl Default for WrapConfig {
42 fn default() -> Self {
43 Self {
44 block: true,
45 allowed_patterns: vec![
46 "package-lock.json".to_string(),
47 "yarn.lock".to_string(),
48 "pnpm-lock.yaml".to_string(),
49 ".git/".to_string(),
50 ],
51 denied_patterns: vec![
52 "*.tmp".to_string(),
53 "*.bak".to_string(),
54 ],
55 enable_autofix: true,
56 autofix_strategy: Some("moderate".to_string()),
57 dry_run: false,
58 strict_mode: false,
59 force_mode: false,
60 }
61 }
62}
63
64pub struct ValidatorEngine {
66 validators: Validators,
68 strict_mode: bool,
70 force_mode: bool,
72}
73
74impl ValidatorEngine {
75 pub fn new() -> Self {
77 Self {
78 validators: Validators::new(),
79 strict_mode: false,
80 force_mode: false,
81 }
82 }
83
84 pub fn with_validators(validators: Validators) -> Self {
86 Self {
87 validators,
88 strict_mode: false,
89 force_mode: false,
90 }
91 }
92
93 pub fn with_strict_mode(mut self, strict: bool) -> Self {
95 self.strict_mode = strict;
96 self
97 }
98
99 pub fn with_force_mode(mut self, force: bool) -> Self {
101 self.force_mode = force;
102 self
103 }
104
105 pub fn validate_string(&self, source: &str, language: Language, file: &Path) -> oparry_core::ValidationResult {
107 debug!("Validating {} bytes of {:?} code (strict={}, force={})",
108 source.len(), language, self.strict_mode, self.force_mode);
109
110 if self.force_mode {
112 debug!("Force mode enabled - bypassing validation");
113 let mut result = oparry_core::ValidationResult::new();
114 result.files_checked = 1;
115 return result;
116 }
117
118 let mut result = oparry_core::ValidationResult::new();
119 result.files_checked = 1;
120
121 let parser = parser_for_language(language);
123 match parser.parse(source) {
124 Ok(parsed) => {
125 match self.validators.validate(&parsed, file) {
127 Ok(mut validation) => {
128 if self.strict_mode {
130 for issue in &mut validation.issues {
131 if issue.level == oparry_core::IssueLevel::Warning {
132 issue.level = oparry_core::IssueLevel::Error;
133 }
134 }
135 validation.passed = validation.issues.is_empty()
137 || validation.issues.iter().all(|i| i.level == oparry_core::IssueLevel::Note);
138 }
139 result.merge(validation);
140 }
141 Err(e) => {
142 result.add_issue(oparry_core::Issue::error(
143 "validation-error",
144 format!("Validation failed: {}", e)
145 ));
146 }
147 }
148 }
149 Err(e) => {
150 result.add_issue(oparry_core::Issue::error(
152 "parse-error",
153 format!("Failed to parse: {}", e)
154 ));
155 result.passed = false;
156 }
157 }
158
159 result
160 }
161
162 pub fn validators(&self) -> &Validators {
164 &self.validators
165 }
166
167 pub fn is_strict_mode(&self) -> bool {
169 self.strict_mode
170 }
171
172 pub fn is_force_mode(&self) -> bool {
174 self.force_mode
175 }
176}
177
178impl Default for ValidatorEngine {
179 fn default() -> Self {
180 Self::new()
181 }
182}
183
184pub struct StdioWrapper {
186 config: WrapConfig,
187}
188
189impl StdioWrapper {
190 pub fn new(config: WrapConfig) -> Self {
192 Self { config }
193 }
194
195 pub fn wrap_command(&self, cmd: &str, args: &[String]) -> Result<i32> {
197 let mut command = Command::new(cmd);
198 command.args(args);
199
200 command
202 .stdin(Stdio::piped())
203 .stdout(Stdio::piped())
204 .stderr(Stdio::piped());
205
206 let mut child = command.spawn()
207 .map_err(|e| Error::Wrapper(format!("Failed to spawn {}: {}", cmd, e)))?;
208
209 let stdout = child.stdout.take().ok_or_else(|| {
211 Error::Wrapper("Failed to capture stdout".to_string())
212 })?;
213 let stderr = child.stderr.take().ok_or_else(|| {
214 Error::Wrapper("Failed to capture stderr".to_string())
215 })?;
216
217 let stdout_handle = std::thread::spawn(move || {
218 let reader = BufReader::new(stdout);
219 for line in reader.lines() {
220 if let Ok(line) = line {
221 println!("{}", line);
222 }
223 }
224 });
225
226 let stderr_handle = std::thread::spawn(move || {
227 let reader = BufReader::new(stderr);
228 for line in reader.lines() {
229 if let Ok(line) = line {
230 eprintln!("{}", line);
231 }
232 }
233 });
234
235 let status = child.wait()
237 .map_err(|e| Error::Wrapper(format!("Failed to wait for {}: {}", cmd, e)))?;
238
239 stdout_handle.join().map_err(|e| {
241 Error::Wrapper(format!("Stdout thread panicked: {:?}", e))
242 })?;
243 stderr_handle.join().map_err(|e| {
244 Error::Wrapper(format!("Stderr thread panicked: {:?}", e))
245 })?;
246
247 Ok(status.code().unwrap_or(0))
248 }
249
250 pub fn validate_path(&self, path: &Path) -> Result<bool> {
252 let path_str = path.to_string_lossy();
253
254 for pattern in &self.config.denied_patterns {
256 if self.matches_pattern(&path_str, pattern) {
257 if self.config.block {
258 return Err(Error::Validation(format!(
259 "Path '{}' matches denied pattern '{}'",
260 path_str, pattern
261 )));
262 }
263 return Ok(false);
264 }
265 }
266
267 for pattern in &self.config.allowed_patterns {
269 if self.matches_pattern(&path_str, pattern) {
270 return Ok(true);
271 }
272 }
273
274 Ok(true)
275 }
276
277 pub fn matches_pattern(&self, path: &str, pattern: &str) -> bool {
279 if pattern.contains('*') {
280 if pattern.contains("**") {
282 let parts: Vec<&str> = pattern.split("**").collect();
284 for part in parts {
285 if !part.is_empty() && !path.contains(part) {
286 return false;
287 }
288 }
289 return true;
290 } else if pattern.contains('*') {
291 let star_pos = pattern.find('*').unwrap();
293 let prefix = &pattern[..star_pos];
294 let suffix = &pattern[star_pos + 1..];
295 return path.starts_with(prefix) && path.ends_with(suffix)
296 && !path[prefix.len()..path.len() - suffix.len()].contains('/');
297 }
298 }
299
300 path.contains(pattern)
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn test_pattern_matching() {
310 let wrapper = StdioWrapper::new(WrapConfig::default());
311
312 assert!(wrapper.matches_pattern("test.ts", "*.ts"));
314 assert!(wrapper.matches_pattern("src/test.ts", "src/*.ts"));
315 assert!(!wrapper.matches_pattern("test.rs", "*.ts"));
316
317 }
321
322 #[test]
323 fn test_validate_path() {
324 let wrapper = StdioWrapper::new(WrapConfig::default());
325
326 assert!(wrapper.validate_path(Path::new("package-lock.json")).unwrap());
327 assert!(wrapper.validate_path(Path::new("src/test.ts")).unwrap());
328 }
329}