Skip to main content

algocline_app/service/
scenario.rs

1use std::path::Path;
2
3use super::path::ContainedPath;
4use super::resolve::{
5    install_scenarios_from_dir, resolve_scenario_source, scenarios_dir, DirEntryFailures,
6};
7use super::AppService;
8
9impl AppService {
10    /// List available scenarios in `~/.algocline/scenarios/`.
11    ///
12    /// Per-entry I/O errors are collected in `"failures"` rather than aborting.
13    pub fn scenario_list(&self) -> Result<String, String> {
14        let dir = scenarios_dir()?;
15        if !dir.exists() {
16            return Ok(serde_json::json!({ "scenarios": [], "failures": [] }).to_string());
17        }
18
19        let entries =
20            std::fs::read_dir(&dir).map_err(|e| format!("Failed to read scenarios dir: {e}"))?;
21
22        let mut scenarios: Vec<serde_json::Value> = Vec::new();
23        let mut failures: DirEntryFailures = Vec::new();
24        for entry_result in entries {
25            let entry = match entry_result {
26                Ok(e) => e,
27                Err(e) => {
28                    failures.push(format!("readdir entry: {e}"));
29                    continue;
30                }
31            };
32            let path = entry.path();
33            let name = match path.file_stem().and_then(|s| s.to_str()) {
34                Some(s) => s.to_string(),
35                None => continue,
36            };
37            let ext = path.extension().and_then(|s| s.to_str());
38            if ext != Some("lua") {
39                continue;
40            }
41            let metadata = std::fs::metadata(&path);
42            let size_bytes = metadata.as_ref().map(|m| m.len()).unwrap_or(0);
43            scenarios.push(serde_json::json!({
44                "name": name,
45                "path": path.to_string_lossy(),
46                "size_bytes": size_bytes,
47            }));
48        }
49
50        scenarios.sort_by(|a, b| {
51            a.get("name")
52                .and_then(|v| v.as_str())
53                .cmp(&b.get("name").and_then(|v| v.as_str()))
54        });
55
56        Ok(serde_json::json!({
57            "scenarios": scenarios,
58            "failures": failures,
59        })
60        .to_string())
61    }
62
63    /// Show the content of a named scenario.
64    pub fn scenario_show(&self, name: &str) -> Result<String, String> {
65        let dir = scenarios_dir()?;
66        let path = ContainedPath::child(&dir, &format!("{name}.lua"))
67            .map_err(|e| format!("Invalid scenario name: {e}"))?;
68        if !path.as_ref().exists() {
69            return Err(format!("Scenario '{name}' not found"));
70        }
71        let content = std::fs::read_to_string(path.as_ref())
72            .map_err(|e| format!("Failed to read scenario '{name}': {e}"))?;
73        Ok(serde_json::json!({
74            "name": name,
75            "path": path.as_ref().to_string_lossy(),
76            "content": content,
77        })
78        .to_string())
79    }
80
81    /// Install scenarios from a Git URL or local path into `~/.algocline/scenarios/`.
82    ///
83    /// Expects the source to contain `.lua` files (at root or in a `scenarios/` subdirectory).
84    pub async fn scenario_install(&self, url: String) -> Result<String, String> {
85        let dest_dir = scenarios_dir()?;
86        std::fs::create_dir_all(&dest_dir)
87            .map_err(|e| format!("Failed to create scenarios dir: {e}"))?;
88
89        // Local path: copy .lua files directly
90        let local_path = Path::new(&url);
91        if local_path.is_absolute() && local_path.is_dir() {
92            return install_scenarios_from_dir(local_path, &dest_dir);
93        }
94
95        // Normalize URL
96        let git_url = if url.starts_with("http://")
97            || url.starts_with("https://")
98            || url.starts_with("file://")
99            || url.starts_with("git@")
100        {
101            url.clone()
102        } else {
103            format!("https://{url}")
104        };
105
106        let staging = tempfile::tempdir().map_err(|e| format!("Failed to create temp dir: {e}"))?;
107
108        let output = tokio::process::Command::new("git")
109            .args([
110                "clone",
111                "--depth",
112                "1",
113                &git_url,
114                &staging.path().to_string_lossy(),
115            ])
116            .output()
117            .await
118            .map_err(|e| format!("Failed to run git: {e}"))?;
119
120        if !output.status.success() {
121            let stderr = String::from_utf8_lossy(&output.stderr);
122            return Err(format!("git clone failed: {stderr}"));
123        }
124
125        let source = resolve_scenario_source(staging.path());
126        install_scenarios_from_dir(&source, &dest_dir)
127    }
128}