ricecoder_storage/industry/
kiro.rs

1//! Kiro configuration adapter
2//!
3//! Reads and converts Kiro configuration files (.kiro/ directory)
4//! into RiceCoder's internal configuration format.
5
6use crate::config::{Config, SteeringRule};
7use crate::error::StorageResult;
8use crate::types::DocumentFormat;
9use std::path::Path;
10use tracing::debug;
11
12use super::adapter::IndustryFileAdapter;
13
14/// Kiro adapter
15pub struct KiroAdapter;
16
17impl KiroAdapter {
18    /// Create a new Kiro adapter
19    pub fn new() -> Self {
20        KiroAdapter
21    }
22
23    /// Read .kiro/ directory configuration
24    fn read_kiro_config(&self, project_root: &Path) -> StorageResult<Option<String>> {
25        let kiro_dir = project_root.join(".kiro");
26
27        if !kiro_dir.exists() {
28            debug!("No .kiro directory found at {:?}", kiro_dir);
29            return Ok(None);
30        }
31
32        debug!("Reading .kiro configuration from {:?}", kiro_dir);
33
34        // Try to read specs and steering files
35        let mut combined_content = String::new();
36
37        // Read specs
38        let specs_dir = kiro_dir.join("specs");
39        if specs_dir.exists() {
40            if let Ok(entries) = std::fs::read_dir(&specs_dir) {
41                for entry in entries.flatten() {
42                    if let Ok(metadata) = entry.metadata() {
43                        if metadata.is_file() {
44                            if let Ok(content) = std::fs::read_to_string(entry.path()) {
45                                combined_content.push_str(&format!(
46                                    "# Spec: {}\n{}\n\n",
47                                    entry.path().display(),
48                                    content
49                                ));
50                            }
51                        }
52                    }
53                }
54            }
55        }
56
57        // Read steering
58        let steering_dir = kiro_dir.join("steering");
59        if steering_dir.exists() {
60            if let Ok(entries) = std::fs::read_dir(&steering_dir) {
61                for entry in entries.flatten() {
62                    if let Ok(metadata) = entry.metadata() {
63                        if metadata.is_file() {
64                            if let Ok(content) = std::fs::read_to_string(entry.path()) {
65                                combined_content.push_str(&format!(
66                                    "# Steering: {}\n{}\n\n",
67                                    entry.path().display(),
68                                    content
69                                ));
70                            }
71                        }
72                    }
73                }
74            }
75        }
76
77        if combined_content.is_empty() {
78            Ok(None)
79        } else {
80            Ok(Some(combined_content))
81        }
82    }
83}
84
85impl Default for KiroAdapter {
86    fn default() -> Self {
87        Self::new()
88    }
89}
90
91impl IndustryFileAdapter for KiroAdapter {
92    fn name(&self) -> &'static str {
93        "kiro"
94    }
95
96    fn can_handle(&self, project_root: &Path) -> bool {
97        project_root.join(".kiro").exists()
98    }
99
100    fn read_config(&self, project_root: &Path) -> StorageResult<Config> {
101        let mut config = Config::default();
102
103        if let Ok(Some(kiro_config)) = self.read_kiro_config(project_root) {
104            debug!("Adding Kiro configuration as steering rule");
105            config.steering.push(SteeringRule {
106                name: "kiro-config".to_string(),
107                content: kiro_config,
108                format: DocumentFormat::Markdown,
109            });
110        }
111
112        Ok(config)
113    }
114
115    fn priority(&self) -> u32 {
116        // Kiro has highest priority among industry files (it's the native format)
117        100
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use std::fs;
125    use tempfile::TempDir;
126
127    #[test]
128    fn test_kiro_adapter_detects_directory() {
129        let temp_dir = TempDir::new().unwrap();
130        let kiro_dir = temp_dir.path().join(".kiro");
131        fs::create_dir(&kiro_dir).unwrap();
132
133        let adapter = KiroAdapter::new();
134        assert!(adapter.can_handle(temp_dir.path()));
135    }
136
137    #[test]
138    fn test_kiro_adapter_no_directory() {
139        let temp_dir = TempDir::new().unwrap();
140
141        let adapter = KiroAdapter::new();
142        assert!(!adapter.can_handle(temp_dir.path()));
143    }
144
145    #[test]
146    fn test_kiro_adapter_reads_specs() {
147        let temp_dir = TempDir::new().unwrap();
148        let kiro_dir = temp_dir.path().join(".kiro");
149        fs::create_dir(&kiro_dir).unwrap();
150        let specs_dir = kiro_dir.join("specs");
151        fs::create_dir(&specs_dir).unwrap();
152        fs::write(specs_dir.join("spec1.md"), "# Spec 1").unwrap();
153
154        let adapter = KiroAdapter::new();
155        let config = adapter.read_config(temp_dir.path()).unwrap();
156
157        assert_eq!(config.steering.len(), 1);
158        assert_eq!(config.steering[0].name, "kiro-config");
159        assert!(config.steering[0].content.contains("Spec 1"));
160    }
161
162    #[test]
163    fn test_kiro_adapter_reads_steering() {
164        let temp_dir = TempDir::new().unwrap();
165        let kiro_dir = temp_dir.path().join(".kiro");
166        fs::create_dir(&kiro_dir).unwrap();
167        let steering_dir = kiro_dir.join("steering");
168        fs::create_dir(&steering_dir).unwrap();
169        fs::write(steering_dir.join("rules.md"), "# Rules").unwrap();
170
171        let adapter = KiroAdapter::new();
172        let config = adapter.read_config(temp_dir.path()).unwrap();
173
174        assert_eq!(config.steering.len(), 1);
175        assert_eq!(config.steering[0].name, "kiro-config");
176        assert!(config.steering[0].content.contains("Rules"));
177    }
178
179    #[test]
180    fn test_kiro_adapter_priority() {
181        let adapter = KiroAdapter::new();
182        assert_eq!(adapter.priority(), 100);
183    }
184
185    #[test]
186    fn test_kiro_adapter_name() {
187        let adapter = KiroAdapter::new();
188        assert_eq!(adapter.name(), "kiro");
189    }
190}