1use super::{Result, BuildError};
4use colored::Colorize;
5use std::path::{Path, PathBuf};
6use std::fs;
7
8pub fn find_project_root() -> Result<PathBuf> {
10 let current_dir = std::env::current_dir()
11 .map_err(|e| BuildError::Build(format!("Failed to get current directory: {}", e)))?;
12
13 let mut dir = current_dir.as_path();
14
15 loop {
16 let config_files = [
18 "kotoba-build.toml",
19 "kotoba-build.json",
20 "kotoba-build.yaml",
21 "package.json",
22 "Cargo.toml",
23 ];
24
25 for config_file in &config_files {
26 if dir.join(config_file).exists() {
27 return Ok(dir.to_path_buf());
28 }
29 }
30
31 if let Some(parent) = dir.parent() {
33 dir = parent;
34 } else {
35 break;
36 }
37 }
38
39 Ok(current_dir)
41}
42
43pub fn detect_project_type(project_root: &Path) -> ProjectType {
45 if project_root.join("Cargo.toml").exists() {
47 return ProjectType::Rust;
48 }
49
50 if project_root.join("package.json").exists() {
52 return ProjectType::NodeJs;
53 }
54
55 if project_root.join("requirements.txt").exists() ||
57 project_root.join("pyproject.toml").exists() ||
58 project_root.join("setup.py").exists() {
59 return ProjectType::Python;
60 }
61
62 if project_root.join("go.mod").exists() {
64 return ProjectType::Go;
65 }
66
67 ProjectType::Generic
69}
70
71#[derive(Debug, Clone, PartialEq)]
73pub enum ProjectType {
74 Rust,
75 NodeJs,
76 Python,
77 Go,
78 Generic,
79}
80
81impl std::fmt::Display for ProjectType {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 match self {
84 ProjectType::Rust => write!(f, "Rust"),
85 ProjectType::NodeJs => write!(f, "Node.js"),
86 ProjectType::Python => write!(f, "Python"),
87 ProjectType::Go => write!(f, "Go"),
88 ProjectType::Generic => write!(f, "Generic"),
89 }
90 }
91}
92
93pub fn ensure_dir_exists(dir_path: &Path) -> Result<()> {
95 if !dir_path.exists() {
96 fs::create_dir_all(dir_path)
97 .map_err(|e| BuildError::Build(format!("Failed to create directory {}: {}", dir_path.display(), e)))?;
98 println!("📁 Created directory: {}", dir_path.display());
99 }
100 Ok(())
101}
102
103pub fn copy_file(src: &Path, dst: &Path) -> Result<()> {
105 if let Some(parent) = dst.parent() {
106 ensure_dir_exists(parent)?;
107 }
108
109 fs::copy(src, dst)
110 .map_err(|e| BuildError::Build(format!("Failed to copy {} to {}: {}", src.display(), dst.display(), e)))?;
111
112 println!("📄 Copied: {} -> {}", src.display(), dst.display());
113 Ok(())
114}
115
116pub fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
118 if !src.exists() {
119 return Ok(());
120 }
121
122 ensure_dir_exists(dst)?;
123
124 for entry in fs::read_dir(src)
125 .map_err(|e| BuildError::Build(format!("Failed to read directory {}: {}", src.display(), e)))?
126 {
127 let entry = entry
128 .map_err(|e| BuildError::Build(format!("Failed to read entry: {}", e)))?;
129 let entry_path = entry.path();
130 let dst_path = dst.join(entry.file_name());
131
132 if entry_path.is_dir() {
133 copy_dir_recursive(&entry_path, &dst_path)?;
134 } else {
135 copy_file(&entry_path, &dst_path)?;
136 }
137 }
138
139 Ok(())
140}
141
142pub fn remove_file(file_path: &Path) -> Result<()> {
144 if file_path.exists() {
145 fs::remove_file(file_path)
146 .map_err(|e| BuildError::Build(format!("Failed to remove file {}: {}", file_path.display(), e)))?;
147 println!("🗑️ Removed: {}", file_path.display());
148 }
149 Ok(())
150}
151
152pub fn remove_dir_recursive(dir_path: &Path) -> Result<()> {
154 if dir_path.exists() {
155 fs::remove_dir_all(dir_path)
156 .map_err(|e| BuildError::Build(format!("Failed to remove directory {}: {}", dir_path.display(), e)))?;
157 println!("🗑️ Removed directory: {}", dir_path.display());
158 }
159 Ok(())
160}
161
162pub fn format_file_size(bytes: u64) -> String {
164 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
165
166 if bytes == 0 {
167 return "0 B".to_string();
168 }
169
170 let base = 1024_f64;
171 let log = (bytes as f64).log(base);
172 let unit_index = log.floor() as usize;
173
174 if unit_index >= UNITS.len() {
175 return format!("{} {}", bytes, UNITS[0]);
176 }
177
178 let size = bytes as f64 / base.powi(unit_index as i32);
179 format!("{:.1} {}", size, UNITS[unit_index])
180}
181
182pub fn format_duration(duration: std::time::Duration) -> String {
184 let total_seconds = duration.as_secs();
185 let hours = total_seconds / 3600;
186 let minutes = (total_seconds % 3600) / 60;
187 let seconds = total_seconds % 60;
188 let millis = duration.subsec_millis();
189
190 if hours > 0 {
191 format!("{}h {}m {}s", hours, minutes, seconds)
192 } else if minutes > 0 {
193 format!("{}m {}s", minutes, seconds)
194 } else if seconds > 0 {
195 format!("{}.{:03}s", seconds, millis)
196 } else {
197 format!("{}ms", millis)
198 }
199}
200
201pub fn create_progress_bar(total: u64, message: &str) -> indicatif::ProgressBar {
203 use indicatif::{ProgressBar, ProgressStyle};
204
205 let pb = ProgressBar::new(total);
206 pb.set_style(
207 ProgressStyle::default_bar()
208 .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos:>7}/{len:7} {msg}")
209 .unwrap()
210 .progress_chars("#>-"),
211 );
212 pb.set_message(message.to_string());
213 pb
214}
215
216pub fn create_spinner(message: &str) -> indicatif::ProgressBar {
218 use indicatif::{ProgressBar, ProgressStyle};
219
220 let pb = ProgressBar::new_spinner();
221 pb.set_style(
222 ProgressStyle::default_spinner()
223 .template("{spinner:.green} {msg}")
224 .unwrap(),
225 );
226 pb.set_message(message.to_string());
227 pb.enable_steady_tick(std::time::Duration::from_millis(100));
228 pb
229}
230
231pub fn print_success(message: &str) {
233 println!("✅ {}", message.green());
234}
235
236pub fn print_error(message: &str) {
238 println!("❌ {}", message.red());
239}
240
241pub fn print_warning(message: &str) {
243 println!("⚠️ {}", message.yellow());
244}
245
246pub fn print_info(message: &str) {
248 println!("ℹ️ {}", message.blue());
249}
250
251pub fn parse_cli_args() -> clap::Command {
253 use clap::{Arg, Command};
254
255 Command::new("kotoba-build")
256 .version(env!("CARGO_PKG_VERSION"))
257 .author("Kotoba Team")
258 .about("Kotoba Build Tool - Project build and task management")
259 .arg(
260 Arg::new("task")
261 .help("Task to run")
262 .value_name("TASK")
263 .index(1),
264 )
265 .arg(
266 Arg::new("config")
267 .long("config")
268 .short('c')
269 .help("Path to config file")
270 .value_name("FILE"),
271 )
272 .arg(
273 Arg::new("watch")
274 .long("watch")
275 .short('w')
276 .help("Watch for file changes")
277 .action(clap::ArgAction::SetTrue),
278 )
279 .arg(
280 Arg::new("verbose")
281 .long("verbose")
282 .short('v')
283 .help("Verbose output")
284 .action(clap::ArgAction::SetTrue),
285 )
286 .arg(
287 Arg::new("list")
288 .long("list")
289 .short('l')
290 .help("List available tasks")
291 .action(clap::ArgAction::SetTrue),
292 )
293 .arg(
294 Arg::new("clean")
295 .long("clean")
296 .help("Clean build artifacts")
297 .action(clap::ArgAction::SetTrue),
298 )
299}
300
301pub fn get_env_var(key: &str, default: &str) -> String {
303 std::env::var(key).unwrap_or_else(|_| default.to_string())
304}
305
306pub fn detect_platform() -> String {
308 format!("{}-{}",
309 std::env::consts::OS,
310 std::env::consts::ARCH
311 )
312}
313
314pub fn get_cache_dir() -> Result<PathBuf> {
316 let cache_dir = dirs::cache_dir()
317 .unwrap_or_else(|| std::env::temp_dir())
318 .join("kotoba-build");
319
320 ensure_dir_exists(&cache_dir)?;
321 Ok(cache_dir)
322}
323
324pub fn get_temp_dir() -> Result<PathBuf> {
326 let temp_dir = std::env::temp_dir().join("kotoba-build");
327 ensure_dir_exists(&temp_dir)?;
328 Ok(temp_dir)
329}
330
331pub fn generate_config_template() -> String {
333 r#"# Kotoba Build Configuration
334name = "my-project"
335version = "0.1.0"
336description = "My awesome project"
337
338[tasks.dev]
339command = "cargo"
340args = ["run"]
341description = "Start development server"
342
343[tasks.build]
344command = "cargo"
345args = ["build", "--release"]
346description = "Build project in release mode"
347
348[tasks.test]
349command = "cargo"
350args = ["test"]
351description = "Run tests"
352
353[tasks.clean]
354command = "cargo"
355args = ["clean"]
356description = "Clean build artifacts"
357
358[tasks.lint]
359command = "cargo"
360args = ["clippy"]
361description = "Run linter"
362
363[dependencies]
364tokio = "1.0"
365serde = "1.0"
366
367[build]
368target = "x86_64-unknown-linux-gnu"
369release = false
370opt_level = "0"
371debug = true
372
373[dev]
374port = 3000
375host = "localhost"
376hot_reload = true
377open = false
378"#.to_string()
379}
380
381pub async fn run_command_safely(command: &str, args: &[&str], cwd: Option<&Path>) -> Result<String> {
383 use tokio::process::Command;
384
385 let mut cmd = Command::new(command);
386 cmd.args(args);
387
388 if let Some(cwd) = cwd {
389 cmd.current_dir(cwd);
390 }
391
392 let output = cmd.output().await
393 .map_err(|e| BuildError::Build(format!("Failed to execute command: {}", e)))?;
394
395 if output.status.success() {
396 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
397 Ok(stdout)
398 } else {
399 let stderr = String::from_utf8_lossy(&output.stderr);
400 Err(BuildError::Build(format!("Command failed: {}", stderr)))
401 }
402}
403
404pub fn is_process_running(pid: u32) -> bool {
406 #[cfg(unix)]
407 {
408 use std::os::unix::process::ExitStatusExt;
409 use nix::sys::signal;
410 use nix::unistd::Pid;
411
412 let pid = Pid::from_raw(pid as i32);
413 signal::kill(pid, None).is_ok()
414 }
415
416 #[cfg(windows)]
417 {
418 use std::process::Command;
419 let output = Command::new("tasklist")
420 .args(&["/FI", &format!("PID eq {}", pid), "/NH"])
421 .output();
422
423 matches!(output, Ok(o) if o.status.success())
424 }
425
426 #[cfg(not(any(unix, windows)))]
427 {
428 false
429 }
430}
431
432pub fn get_cpu_count() -> usize {
434 std::thread::available_parallelism()
435 .map(|n| n.get())
436 .unwrap_or(1)
437}
438
439pub fn get_memory_usage() -> Result<f64> {
441 #[cfg(unix)]
442 {
443 use std::fs;
444 let statm = fs::read_to_string("/proc/self/statm")
445 .map_err(|e| BuildError::Build(format!("Failed to read memory stats: {}", e)))?;
446
447 let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as f64;
448 let rss_pages: f64 = statm.split_whitespace()
449 .nth(1)
450 .and_then(|s| s.parse().ok())
451 .unwrap_or(0.0);
452
453 Ok((rss_pages * page_size) / (1024.0 * 1024.0))
454 }
455
456 #[cfg(windows)]
457 {
458 Ok(0.0)
460 }
461
462 #[cfg(not(any(unix, windows)))]
463 {
464 Ok(0.0)
465 }
466}