vika_cli/
formatter.rs

1use crate::error::Result;
2use std::path::Path;
3use std::process::Command;
4
5pub enum Formatter {
6    Prettier,
7    Biome,
8}
9
10pub struct FormatterManager;
11
12impl FormatterManager {
13    pub fn detect_formatter() -> Option<Formatter> {
14        Self::detect_formatter_from_dir(Path::new("."))
15    }
16
17    pub fn detect_formatter_from_dir(dir: &Path) -> Option<Formatter> {
18        // Check for prettier
19        if Self::has_prettier_in_dir(dir) {
20            return Some(Formatter::Prettier);
21        }
22
23        // Check for biome
24        if Self::has_biome_in_dir(dir) {
25            return Some(Formatter::Biome);
26        }
27
28        None
29    }
30
31    fn has_prettier_in_dir(dir: &Path) -> bool {
32        // Check for package.json with prettier
33        let package_json = dir.join("package.json");
34        if package_json.exists() {
35            if let Ok(content) = std::fs::read_to_string(&package_json) {
36                if content.contains("prettier") {
37                    return true;
38                }
39            }
40        }
41
42        // Check for prettier config files
43        dir.join(".prettierrc").exists()
44            || dir.join(".prettierrc.json").exists()
45            || dir.join(".prettierrc.js").exists()
46            || dir.join("prettier.config.js").exists()
47    }
48
49    fn has_biome_in_dir(dir: &Path) -> bool {
50        // Check for biome.json
51        dir.join("biome.json").exists() || dir.join("biome.jsonc").exists()
52    }
53
54    pub fn format_files(files: &[std::path::PathBuf], formatter: Formatter) -> Result<()> {
55        match formatter {
56            Formatter::Prettier => Self::format_with_prettier(files),
57            Formatter::Biome => Self::format_with_biome(files),
58        }
59    }
60
61    fn format_with_prettier(files: &[std::path::PathBuf]) -> Result<()> {
62        let file_paths: Vec<String> = files
63            .iter()
64            .filter_map(|p| p.to_str().filter(|s| !s.is_empty()).map(|s| s.to_string()))
65            .collect();
66
67        if file_paths.is_empty() {
68            return Ok(());
69        }
70
71        // Always use explicit file paths to avoid glob pattern issues
72        // Modern systems can handle long command lines, and if not, we'll handle it gracefully
73        let output = Command::new("npx")
74            .arg("prettier")
75            .arg("--write")
76            .args(&file_paths)
77            .output();
78
79        match output {
80            Ok(output) => {
81                if !output.status.success() {
82                    let stderr = String::from_utf8_lossy(&output.stderr);
83                    let stdout = String::from_utf8_lossy(&output.stdout);
84                    eprintln!("Warning: Prettier exited with error:");
85                    eprintln!("  stderr: {}", stderr);
86                    if !stdout.is_empty() {
87                        eprintln!("  stdout: {}", stdout);
88                    }
89                }
90                Ok(())
91            }
92            Err(e) => {
93                // Silently fail if prettier is not available
94                eprintln!("Warning: Failed to run prettier: {}", e);
95                Ok(())
96            }
97        }
98    }
99
100    fn format_with_biome(files: &[std::path::PathBuf]) -> Result<()> {
101        let file_paths: Vec<String> = files
102            .iter()
103            .filter_map(|p| p.to_str().filter(|s| !s.is_empty()).map(|s| s.to_string()))
104            .collect();
105
106        if file_paths.is_empty() {
107            return Ok(());
108        }
109
110        let output = Command::new("npx")
111            .arg("@biomejs/biome")
112            .arg("format")
113            .arg("--write")
114            .args(&file_paths)
115            .output();
116
117        match output {
118            Ok(_) => Ok(()),
119            Err(e) => {
120                // Silently fail if biome is not available
121                eprintln!("Warning: Failed to run biome: {}", e);
122                Ok(())
123            }
124        }
125    }
126
127    /// Format a single content string using the specified formatter
128    /// Returns the formatted content, or original content if formatting fails
129    /// Uses stdin/stdout to format, ensuring formatter config is found
130    pub fn format_content(content: &str, formatter: Formatter, file_path: &Path) -> Result<String> {
131        use std::process::Stdio;
132
133        // Determine the working directory (where config files are likely located)
134        let work_dir = file_path.parent().unwrap_or(Path::new("."));
135
136        // Use stdin/stdout for formatting to ensure config files are found
137        let format_result = match formatter {
138            Formatter::Prettier => {
139                let mut cmd = Command::new("npx");
140                cmd.arg("prettier")
141                    .arg("--stdin-filepath")
142                    .arg(file_path.to_str().unwrap_or("file.ts"))
143                    .current_dir(work_dir)
144                    .stdin(Stdio::piped())
145                    .stdout(Stdio::piped())
146                    .stderr(Stdio::piped());
147
148                let mut child = cmd.spawn().map_err(|e| {
149                    crate::error::VikaError::from(crate::error::FileSystemError::ReadFileFailed {
150                        path: file_path.display().to_string(),
151                        source: e,
152                    })
153                })?;
154
155                // Write content to stdin
156                if let Some(mut stdin) = child.stdin.take() {
157                    use std::io::Write;
158                    stdin.write_all(content.as_bytes()).map_err(|e| {
159                        crate::error::VikaError::from(
160                            crate::error::FileSystemError::WriteFileFailed {
161                                path: "stdin".to_string(),
162                                source: e,
163                            },
164                        )
165                    })?;
166                }
167
168                child.wait_with_output()
169            }
170            Formatter::Biome => {
171                let mut cmd = Command::new("npx");
172                cmd.arg("@biomejs/biome")
173                    .arg("format")
174                    .arg("--stdin-file-path")
175                    .arg(file_path.to_str().unwrap_or("file.ts"))
176                    .current_dir(work_dir)
177                    .stdin(Stdio::piped())
178                    .stdout(Stdio::piped())
179                    .stderr(Stdio::piped());
180
181                let mut child = cmd.spawn().map_err(|e| {
182                    crate::error::VikaError::from(crate::error::FileSystemError::ReadFileFailed {
183                        path: file_path.display().to_string(),
184                        source: e,
185                    })
186                })?;
187
188                // Write content to stdin
189                if let Some(mut stdin) = child.stdin.take() {
190                    use std::io::Write;
191                    stdin.write_all(content.as_bytes()).map_err(|e| {
192                        crate::error::VikaError::from(
193                            crate::error::FileSystemError::WriteFileFailed {
194                                path: "stdin".to_string(),
195                                source: e,
196                            },
197                        )
198                    })?;
199                }
200
201                child.wait_with_output()
202            }
203        };
204
205        // Read the formatted content from stdout
206        match format_result {
207            Ok(output) if output.status.success() => {
208                String::from_utf8(output.stdout).map_err(|e| {
209                    crate::error::VikaError::from(crate::error::FileSystemError::ReadFileFailed {
210                        path: "stdout".to_string(),
211                        source: std::io::Error::new(std::io::ErrorKind::InvalidData, e),
212                    })
213                })
214            }
215            _ => {
216                // Formatting failed, return original content
217                Ok(content.to_string())
218            }
219        }
220    }
221}