ricecoder_storage/industry/
continue_dev.rs

1//! Continue.dev configuration adapter
2//!
3//! Reads and converts Continue.dev configuration files (.continue/ 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/// Continue.dev adapter
15pub struct ContinueDevAdapter;
16
17impl ContinueDevAdapter {
18    /// Create a new Continue.dev adapter
19    pub fn new() -> Self {
20        ContinueDevAdapter
21    }
22
23    /// Read .continue/ directory configuration
24    fn read_continue_config(&self, project_root: &Path) -> StorageResult<Option<String>> {
25        let continue_dir = project_root.join(".continue");
26
27        if !continue_dir.exists() {
28            debug!("No .continue directory found at {:?}", continue_dir);
29            return Ok(None);
30        }
31
32        debug!("Reading .continue configuration from {:?}", continue_dir);
33
34        // Try to read config.json or other config files
35        let config_path = continue_dir.join("config.json");
36        if config_path.exists() {
37            let content = std::fs::read_to_string(&config_path).map_err(|e| {
38                crate::error::StorageError::io_error(
39                    config_path.clone(),
40                    crate::error::IoOperation::Read,
41                    e,
42                )
43            })?;
44            return Ok(Some(content));
45        }
46
47        // If no config.json, try to read all files in the directory
48        let mut combined_content = String::new();
49        if let Ok(entries) = std::fs::read_dir(&continue_dir) {
50            for entry in entries.flatten() {
51                if let Ok(metadata) = entry.metadata() {
52                    if metadata.is_file() {
53                        if let Ok(content) = std::fs::read_to_string(entry.path()) {
54                            combined_content.push_str(&format!(
55                                "# {}\n{}\n\n",
56                                entry.path().display(),
57                                content
58                            ));
59                        }
60                    }
61                }
62            }
63        }
64
65        if combined_content.is_empty() {
66            Ok(None)
67        } else {
68            Ok(Some(combined_content))
69        }
70    }
71}
72
73impl Default for ContinueDevAdapter {
74    fn default() -> Self {
75        Self::new()
76    }
77}
78
79impl IndustryFileAdapter for ContinueDevAdapter {
80    fn name(&self) -> &'static str {
81        "continue"
82    }
83
84    fn can_handle(&self, project_root: &Path) -> bool {
85        project_root.join(".continue").exists()
86    }
87
88    fn read_config(&self, project_root: &Path) -> StorageResult<Config> {
89        let mut config = Config::default();
90
91        if let Ok(Some(continue_config)) = self.read_continue_config(project_root) {
92            debug!("Adding Continue.dev configuration as steering rule");
93            config.steering.push(SteeringRule {
94                name: "continue-config".to_string(),
95                content: continue_config,
96                format: DocumentFormat::Markdown,
97            });
98        }
99
100        Ok(config)
101    }
102
103    fn priority(&self) -> u32 {
104        // Continue.dev has medium priority
105        50
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use std::fs;
113    use tempfile::TempDir;
114
115    #[test]
116    fn test_continue_adapter_detects_directory() {
117        let temp_dir = TempDir::new().unwrap();
118        let continue_dir = temp_dir.path().join(".continue");
119        fs::create_dir(&continue_dir).unwrap();
120
121        let adapter = ContinueDevAdapter::new();
122        assert!(adapter.can_handle(temp_dir.path()));
123    }
124
125    #[test]
126    fn test_continue_adapter_no_directory() {
127        let temp_dir = TempDir::new().unwrap();
128
129        let adapter = ContinueDevAdapter::new();
130        assert!(!adapter.can_handle(temp_dir.path()));
131    }
132
133    #[test]
134    fn test_continue_adapter_reads_config_json() {
135        let temp_dir = TempDir::new().unwrap();
136        let continue_dir = temp_dir.path().join(".continue");
137        fs::create_dir(&continue_dir).unwrap();
138        let config_path = continue_dir.join("config.json");
139        let config_content = r#"{"models": ["gpt-4"]}"#;
140        fs::write(&config_path, config_content).unwrap();
141
142        let adapter = ContinueDevAdapter::new();
143        let config = adapter.read_config(temp_dir.path()).unwrap();
144
145        assert_eq!(config.steering.len(), 1);
146        assert_eq!(config.steering[0].name, "continue-config");
147        assert_eq!(config.steering[0].content, config_content);
148    }
149
150    #[test]
151    fn test_continue_adapter_reads_multiple_files() {
152        let temp_dir = TempDir::new().unwrap();
153        let continue_dir = temp_dir.path().join(".continue");
154        fs::create_dir(&continue_dir).unwrap();
155        fs::write(continue_dir.join("file1.txt"), "content1").unwrap();
156        fs::write(continue_dir.join("file2.txt"), "content2").unwrap();
157
158        let adapter = ContinueDevAdapter::new();
159        let config = adapter.read_config(temp_dir.path()).unwrap();
160
161        assert_eq!(config.steering.len(), 1);
162        assert_eq!(config.steering[0].name, "continue-config");
163        assert!(config.steering[0].content.contains("content1"));
164        assert!(config.steering[0].content.contains("content2"));
165    }
166
167    #[test]
168    fn test_continue_adapter_priority() {
169        let adapter = ContinueDevAdapter::new();
170        assert_eq!(adapter.priority(), 50);
171    }
172
173    #[test]
174    fn test_continue_adapter_name() {
175        let adapter = ContinueDevAdapter::new();
176        assert_eq!(adapter.name(), "continue");
177    }
178}