Skip to main content

oxi/storage/
resource_loader_compat.rs

1//! Compatibility module for resource loading functions
2//!
3//! This module provides the original resource loading functionality
4//! that was moved to a separate module to avoid conflicts.
5
6use std::collections::HashMap;
7use std::fs;
8use std::path::{Path, PathBuf};
9
10/// Resource type
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12pub enum ResourceType {
13    /// skill variant.
14    Skill,
15    /// extension variant.
16    Extension,
17    /// theme variant.
18    Theme,
19    /// prompt variant.
20    Prompt,
21}
22
23/// A loaded resource
24#[derive(Debug, Clone)]
25#[allow(dead_code)]
26pub struct Resource {
27    /// Resource ID
28    pub id: String,
29    /// Resource type
30    pub resource_type: ResourceType,
31    /// Path to the resource file/directory
32    pub path: PathBuf,
33    /// Resource content or metadata
34    pub content: Option<String>,
35    /// Source (local, npm, git, etc.)
36    pub source: String,
37}
38
39/// Resource loading result
40#[derive(Debug)]
41pub struct LoadResult<T> {
42    /// Loaded items
43    pub items: Vec<T>,
44    /// Any errors encountered
45    pub errors: Vec<LoadError>,
46    /// Diagnostics
47    pub diagnostics: Vec<ResourceDiagnostic>,
48}
49
50/// Load error
51#[derive(Debug, Clone)]
52pub struct LoadError {
53    /// pub.
54    pub path: PathBuf,
55    /// pub.
56    pub error: String,
57}
58
59/// Resource diagnostic
60#[derive(Debug, Clone)]
61pub struct ResourceDiagnostic {
62    /// pub.
63    pub severity: DiagnosticSeverity,
64    /// pub.
65    pub message: String,
66    /// pub.
67    pub path: Option<PathBuf>,
68}
69
70/// Diagnostic severity
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum DiagnosticSeverity {
73    /// warning variant.
74    Warning,
75    /// error variant.
76    Error,
77    /// info variant.
78    Info,
79}
80
81/// Resource path configuration
82#[derive(Debug, Clone)]
83#[allow(dead_code)]
84pub struct ResourcePaths {
85    /// Base directory for resources
86    pub base_dir: PathBuf,
87    /// Additional paths to search
88    pub additional_paths: Vec<PathBuf>,
89    /// Whether to include default paths
90    pub include_defaults: bool,
91}
92
93impl Default for ResourcePaths {
94    fn default() -> Self {
95        Self {
96            base_dir: dirs::config_dir()
97                .unwrap_or_else(|| PathBuf::from("."))
98                .join("oxi"),
99            additional_paths: Vec::new(),
100            include_defaults: true,
101        }
102    }
103}
104
105/// Load skills from a directory (impl version)
106pub fn load_skills_from_dir_impl(dir: &Path) -> LoadResult<Skill> {
107    let mut items = Vec::new();
108    let mut errors = Vec::new();
109    let mut diagnostics = Vec::new();
110
111    if !dir.exists() {
112        return LoadResult {
113            items,
114            errors,
115            diagnostics,
116        };
117    }
118
119    if let Ok(entries) = fs::read_dir(dir) {
120        for entry in entries.flatten() {
121            let path = entry.path();
122            if path.is_dir() || path.extension().map(|e| e == "md").unwrap_or(false) {
123                match load_skill_impl(&path) {
124                    Ok(skill) => items.push(skill),
125                    Err(e) => {
126                        errors.push(LoadError {
127                            path: path.clone(),
128                            error: e.clone(),
129                        });
130                        diagnostics.push(ResourceDiagnostic {
131                            severity: DiagnosticSeverity::Error,
132                            message: e,
133                            path: Some(path),
134                        });
135                    }
136                }
137            }
138        }
139    }
140
141    LoadResult {
142        items,
143        errors,
144        diagnostics,
145    }
146}
147
148/// Load a single skill (impl version)
149pub fn load_skill_impl(path: &Path) -> Result<Skill, String> {
150    let content = if path.is_file() {
151        fs::read_to_string(path).map_err(|e| e.to_string())?
152    } else if path.is_dir() {
153        let skill_md = path.join("SKILL.md");
154        if skill_md.exists() {
155            fs::read_to_string(&skill_md).map_err(|e| e.to_string())?
156        } else {
157            return Err("No SKILL.md found in directory".to_string());
158        }
159    } else {
160        return Err("Invalid skill path".to_string());
161    };
162
163    let id = path
164        .file_stem()
165        .and_then(|s| s.to_str())
166        .unwrap_or("unknown")
167        .to_string();
168
169    let name = extract_yaml_field(&content, "name").or_else(|| Some(id.clone()));
170    let description = extract_yaml_field(&content, "description");
171
172    Ok(Skill {
173        id,
174        path: path.to_path_buf(),
175        content,
176        name,
177        description,
178        source: "local".to_string(),
179    })
180}
181
182/// A loaded skill
183#[derive(Debug, Clone)]
184pub struct Skill {
185    /// pub.
186    pub id: String,
187    /// pub.
188    pub path: PathBuf,
189    /// pub.
190    pub content: String,
191    /// pub.
192    pub name: Option<String>,
193    /// pub.
194    pub description: Option<String>,
195    /// pub.
196    pub source: String,
197}
198
199/// Load themes from a directory (impl version)
200pub fn load_themes_from_dir_impl(dir: &Path) -> LoadResult<Theme> {
201    let mut items = Vec::new();
202    let mut errors = Vec::new();
203    let mut diagnostics = Vec::new();
204
205    if !dir.exists() {
206        return LoadResult {
207            items,
208            errors,
209            diagnostics,
210        };
211    }
212
213    if let Ok(entries) = fs::read_dir(dir) {
214        for entry in entries.flatten() {
215            let path = entry.path();
216            if path.extension().map(|e| e == "json").unwrap_or(false) {
217                match load_theme_impl(&path) {
218                    Ok(theme) => items.push(theme),
219                    Err(e) => {
220                        errors.push(LoadError {
221                            path: path.clone(),
222                            error: e.clone(),
223                        });
224                        diagnostics.push(ResourceDiagnostic {
225                            severity: DiagnosticSeverity::Warning,
226                            message: e,
227                            path: Some(path),
228                        });
229                    }
230                }
231            }
232        }
233    }
234
235    LoadResult {
236        items,
237        errors,
238        diagnostics,
239    }
240}
241
242/// Load a single theme (impl version)
243pub fn load_theme_impl(path: &Path) -> Result<Theme, String> {
244    let content = fs::read_to_string(path).map_err(|e| e.to_string())?;
245    let json: serde_json::Value = serde_json::from_str(&content).map_err(|e| e.to_string())?;
246
247    let name = json
248        .get("name")
249        .and_then(|v| v.as_str())
250        .map(String::from)
251        .unwrap_or_else(|| {
252            path.file_stem()
253                .and_then(|s| s.to_str())
254                .unwrap_or("unnamed")
255                .to_string()
256        });
257
258    Ok(Theme {
259        id: name.to_lowercase().replace(' ', "_"),
260        name,
261        path: path.to_path_buf(),
262        content: json,
263        source: "local".to_string(),
264    })
265}
266
267/// A loaded theme
268#[derive(Debug, Clone)]
269pub struct Theme {
270    /// pub.
271    pub id: String,
272    /// pub.
273    pub name: String,
274    /// pub.
275    pub path: PathBuf,
276    /// pub.
277    pub content: serde_json::Value,
278    /// pub.
279    pub source: String,
280}
281
282/// Load prompts from a directory (impl version)
283pub fn load_prompts_from_dir_impl(dir: &Path) -> LoadResult<Prompt> {
284    let mut items = Vec::new();
285    let mut errors = Vec::new();
286    let mut diagnostics = Vec::new();
287
288    if !dir.exists() {
289        return LoadResult {
290            items,
291            errors,
292            diagnostics,
293        };
294    }
295
296    if let Ok(entries) = fs::read_dir(dir) {
297        for entry in entries.flatten() {
298            let path = entry.path();
299            if path.is_file() && path.extension().map(|e| e == "md").unwrap_or(false) {
300                match load_prompt_impl(&path) {
301                    Ok(prompt) => items.push(prompt),
302                    Err(e) => {
303                        errors.push(LoadError {
304                            path: path.clone(),
305                            error: e.clone(),
306                        });
307                        diagnostics.push(ResourceDiagnostic {
308                            severity: DiagnosticSeverity::Warning,
309                            message: e,
310                            path: Some(path),
311                        });
312                    }
313                }
314            }
315        }
316    }
317
318    LoadResult {
319        items,
320        errors,
321        diagnostics,
322    }
323}
324
325/// Load a single prompt (impl version)
326pub fn load_prompt_impl(path: &Path) -> Result<Prompt, String> {
327    let content = fs::read_to_string(path).map_err(|e| e.to_string())?;
328
329    let name = path
330        .file_stem()
331        .and_then(|s| s.to_str())
332        .unwrap_or("unknown")
333        .to_string();
334
335    Ok(Prompt {
336        id: name.clone(),
337        name,
338        path: path.to_path_buf(),
339        content,
340        description: None,
341        source: "local".to_string(),
342    })
343}
344
345/// A loaded prompt template
346#[derive(Debug, Clone)]
347pub struct Prompt {
348    /// pub.
349    pub id: String,
350    /// pub.
351    pub name: String,
352    /// pub.
353    pub path: PathBuf,
354    /// pub.
355    pub content: String,
356    /// pub.
357    pub description: Option<String>,
358    /// pub.
359    pub source: String,
360}
361
362/// Extract a YAML frontmatter field
363fn extract_yaml_field(content: &str, field: &str) -> Option<String> {
364    if !content.starts_with("---") {
365        return None;
366    }
367
368    if let Some(end) = content[3..].find("---") {
369        let frontmatter = &content[3..end + 3];
370        for line in frontmatter.lines() {
371            if let Some(value) = line.strip_prefix(&format!("{}:", field)) {
372                let value = value.trim();
373                let value = value.trim_matches('"').trim_matches('\'');
374                return Some(value.to_string());
375            }
376        }
377    }
378
379    None
380}
381
382/// Watch a directory for changes
383#[allow(dead_code)]
384pub struct ResourceWatcher {
385    paths: Vec<PathBuf>,
386    #[allow(clippy::type_complexity)]
387    callbacks: HashMap<PathBuf, Vec<Box<dyn Fn(ResourceChange) + Send + Sync>>>,
388}
389
390impl ResourceWatcher {
391    /// TODO.
392    #[allow(dead_code)]
393    pub fn new() -> Self {
394        Self {
395            paths: Vec::new(),
396            callbacks: HashMap::new(),
397        }
398    }
399
400    /// TODO: document this function.
401    #[allow(dead_code)]
402    pub fn add_path(&mut self, path: PathBuf) {
403        self.paths.push(path.clone());
404        self.callbacks.entry(path).or_default();
405    }
406
407    /// TODO: document this function.
408    #[allow(dead_code)]
409    pub fn on_change<F>(&mut self, path: &Path, callback: F)
410    where
411        F: Fn(ResourceChange) + Send + Sync + 'static,
412    {
413        let path = path.to_path_buf();
414        self.callbacks
415            .entry(path.clone())
416            .or_default()
417            .push(Box::new(callback));
418    }
419
420    /// TODO: document this function.
421    #[allow(dead_code)]
422    pub fn check_changes(&mut self) {
423        for path in &self.paths {
424            if let Ok(metadata) = fs::metadata(path) {
425                if metadata.modified().is_ok() {
426                    let change = ResourceChange {
427                        path: path.clone(),
428                        kind: ChangeKind::Modified,
429                    };
430                    if let Some(callbacks) = self.callbacks.get(path) {
431                        for callback in callbacks {
432                            callback(change.clone());
433                        }
434                    }
435                }
436            }
437        }
438    }
439}
440
441impl Default for ResourceWatcher {
442    fn default() -> Self {
443        Self::new()
444    }
445}
446
447/// A resource change event
448#[derive(Debug, Clone)]
449#[allow(dead_code)]
450pub struct ResourceChange {
451    /// pub.
452    pub path: PathBuf,
453    /// pub.
454    pub kind: ChangeKind,
455}
456
457/// Change kind
458#[derive(Debug, Clone, Copy)]
459#[allow(dead_code)]
460pub enum ChangeKind {
461    /// created variant.
462    Created,
463    /// modified variant.
464    Modified,
465    /// deleted variant.
466    Deleted,
467}
468
469/// Load all resources from default locations
470#[allow(dead_code)]
471pub fn load_all_resources_impl(base_dir: &Path) -> LoadAllResourcesResult {
472    let mut errors = Vec::new();
473    let mut diagnostics = Vec::new();
474
475    let skills_base = base_dir.join("skills");
476    let skills_result = load_skills_from_dir_impl(&skills_base);
477    errors.extend(skills_result.errors);
478    diagnostics.extend(skills_result.diagnostics);
479
480    let themes_base = base_dir.join("themes");
481    let themes_result = load_themes_from_dir_impl(&themes_base);
482    errors.extend(themes_result.errors);
483    diagnostics.extend(themes_result.diagnostics);
484
485    let prompts_base = base_dir.join("prompts");
486    let prompts_result = load_prompts_from_dir_impl(&prompts_base);
487    errors.extend(prompts_result.errors);
488    diagnostics.extend(prompts_result.diagnostics);
489
490    LoadAllResourcesResult {
491        skills: skills_result.items,
492        themes: themes_result.items,
493        prompts: prompts_result.items,
494        errors,
495        diagnostics,
496    }
497}
498
499/// Result of loading all resources
500#[allow(dead_code)]
501pub struct LoadAllResourcesResult {
502    /// pub.
503    pub skills: Vec<Skill>,
504    /// pub.
505    pub themes: Vec<Theme>,
506    /// pub.
507    pub prompts: Vec<Prompt>,
508    /// pub.
509    pub errors: Vec<LoadError>,
510    /// pub.
511    pub diagnostics: Vec<ResourceDiagnostic>,
512}