1use std::path::PathBuf;
7use serde::{Deserialize, Serialize};
8use colored::Colorize;
9
10#[derive(Debug, thiserror::Error)]
12pub enum BuildError {
13 #[error("Configuration error: {0}")]
14 Config(String),
15
16 #[error("Task execution error: {0}")]
17 Task(String),
18
19 #[error("Build error: {0}")]
20 Build(String),
21
22 #[error("IO error: {0}")]
23 Io(#[from] std::io::Error),
24
25 #[error("JSON error: {0}")]
26 Json(#[from] serde_json::Error),
27
28 #[error("TOML error: {0}")]
29 Toml(#[from] toml::de::Error),
30
31 #[error("Anyhow error: {0}")]
32 Anyhow(#[from] anyhow::Error),
33}
34
35pub type Result<T> = std::result::Result<T, BuildError>;
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct BuildConfig {
40 pub name: String,
41 pub version: String,
42 pub description: Option<String>,
43 pub tasks: std::collections::HashMap<String, TaskConfig>,
44 pub dependencies: std::collections::HashMap<String, String>,
45}
46
47impl Default for BuildConfig {
48 fn default() -> Self {
49 Self {
50 name: "kotoba-project".to_string(),
51 version: "0.1.0".to_string(),
52 description: None,
53 tasks: std::collections::HashMap::new(),
54 dependencies: std::collections::HashMap::new(),
55 }
56 }
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct TaskConfig {
62 pub command: String,
63 pub args: Vec<String>,
64 pub description: Option<String>,
65 pub depends_on: Vec<String>,
66 pub cwd: Option<String>,
67 pub env: Option<std::collections::HashMap<String, String>>,
68}
69
70#[derive(Debug)]
72pub struct BuildEngine {
73 config: BuildConfig,
74 project_root: PathBuf,
75}
76
77impl BuildEngine {
78 pub async fn new(project_root: PathBuf) -> Result<Self> {
79 let config = Self::load_config(&project_root).await?;
80 Ok(Self { config, project_root })
81 }
82
83 pub async fn default() -> Result<Self> {
84 let project_root = std::env::current_dir()?;
85 Self::new(project_root).await
86 }
87
88 async fn load_config(project_root: &std::path::Path) -> Result<BuildConfig> {
89 let config_path = project_root.join("kotoba-build.toml");
90 if config_path.exists() {
91 let content = tokio::fs::read_to_string(config_path).await?;
92 let config: BuildConfig = toml::from_str(&content)?;
93 Ok(config)
94 } else {
95 Ok(BuildConfig::default())
96 }
97 }
98
99 pub async fn run_task(&self, task_name: &str) -> Result<()> {
100 match self.config.tasks.get(task_name) {
101 Some(task) => {
102 println!("Running task: {}", task_name.green());
103 self.execute_task(task).await
104 }
105 None => Err(BuildError::Task(format!("Task '{}' not found", task_name))),
106 }
107 }
108
109 async fn execute_task(&self, task: &TaskConfig) -> Result<()> {
110 use tokio::process::Command;
111
112 let mut cmd = Command::new(&task.command);
113 cmd.args(&task.args);
114 cmd.current_dir(&self.project_root);
115
116 let output = cmd.output().await?;
117 if output.status.success() {
118 println!("Task completed successfully!");
119 Ok(())
120 } else {
121 let stderr = String::from_utf8_lossy(&output.stderr);
122 Err(BuildError::Task(format!("Command failed: {}", stderr)))
123 }
124 }
125
126 pub async fn build(&self) -> Result<()> {
127 println!("Building project...");
128 let output_dir = self.project_root.join("dist");
130 tokio::fs::create_dir_all(&output_dir).await?;
131 println!("Build completed successfully!");
132 Ok(())
133 }
134
135 pub async fn list_tasks(&self) -> Vec<(String, String)> {
136 self.config.tasks.iter()
137 .map(|(name, task)| {
138 let desc = task.description.clone()
139 .unwrap_or_else(|| format!("Run {}", name));
140 (name.clone(), desc)
141 })
142 .collect()
143 }
144}
145
146pub mod config;
148pub mod tasks;
149pub mod watcher;
150pub mod utils;