Skip to main content

algocline_app/service/
update.rs

1//! `alc_update` — re-resolve all `alc.toml` entries and rewrite `alc.lock`.
2
3use super::alc_toml::{load_alc_toml, PackageDep};
4use super::lockfile::{lockfile_path, save_lockfile, LockFile, LockPackage};
5use super::path::copy_dir;
6use super::project::resolve_project_root;
7use super::resolve::packages_dir;
8use super::source::PackageSource;
9use super::AppService;
10
11impl AppService {
12    pub async fn update(&self, project_root: Option<String>) -> Result<String, String> {
13        let root = resolve_project_root(project_root.as_deref())
14            .ok_or_else(|| "No alc.toml found. Run alc_init first.".to_string())?;
15
16        let toml = load_alc_toml(&root)?
17            .ok_or_else(|| "alc.toml not found at resolved project root".to_string())?;
18
19        let pkg_dir = packages_dir()?;
20        let mut resolved: Vec<LockPackage> = Vec::new();
21        let mut errors: Vec<String> = Vec::new();
22
23        for (name, dep) in &toml.packages {
24            match dep {
25                PackageDep::Version(v) if v == "*" => {
26                    let dir = pkg_dir.join(name);
27                    if dir.is_dir() {
28                        resolved.push(LockPackage {
29                            name: name.clone(),
30                            version: None,
31                            source: PackageSource::Installed,
32                        });
33                    } else {
34                        errors.push(format!(
35                            "'{name}': not installed (not found in packages_dir)"
36                        ));
37                    }
38                }
39                PackageDep::Version(v) => {
40                    let versioned = pkg_dir.join(format!("{name}@{v}"));
41                    if versioned.is_dir() {
42                        resolved.push(LockPackage {
43                            name: name.clone(),
44                            version: Some(v.clone()),
45                            source: PackageSource::Installed,
46                        });
47                    } else {
48                        let base = pkg_dir.join(name);
49                        if base.is_dir() {
50                            // lazy creation: copy base/ → {name}@{version}/
51                            copy_dir(&base, &versioned)
52                                .map_err(|e| format!("Failed to create {name}@{v}: {e}"))?;
53                            resolved.push(LockPackage {
54                                name: name.clone(),
55                                version: Some(v.clone()),
56                                source: PackageSource::Installed,
57                            });
58                        } else {
59                            errors.push(format!(
60                                "'{name}@{v}': not found in packages_dir (neither versioned nor base dir)"
61                            ));
62                        }
63                    }
64                }
65                PackageDep::Path { path, version: ver } => {
66                    // Phase 1: use version from alc.toml as-is
67                    resolved.push(LockPackage {
68                        name: name.clone(),
69                        version: ver.clone(),
70                        source: PackageSource::Path { path: path.clone() },
71                    });
72                }
73                PackageDep::Git { .. } => {
74                    errors.push(format!("'{name}': Git source not supported in Phase 1"));
75                }
76            }
77        }
78
79        let lock = LockFile {
80            version: 1,
81            packages: resolved.clone(),
82        };
83        save_lockfile(&root, &lock)?;
84
85        let lock_path = lockfile_path(&root);
86        let result = serde_json::json!({
87            "resolved": resolved.len(),
88            "errors": errors,
89            "alc_lock": lock_path.display().to_string(),
90        });
91        Ok(result.to_string())
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use crate::service::test_support::make_app_service as make_service;
98
99    #[tokio::test]
100    async fn update_fails_without_alc_toml() {
101        let tmp = tempfile::tempdir().unwrap();
102        let svc = make_service().await;
103        let err = svc
104            .update(Some(tmp.path().to_str().unwrap().to_string()))
105            .await
106            .unwrap_err();
107        // resolve_project_root returns None (no alc.toml)
108        assert!(
109            err.contains("No alc.toml found") || err.contains("alc.toml not found"),
110            "{err}"
111        );
112    }
113
114    #[tokio::test]
115    async fn update_with_path_dep_writes_lock() {
116        let tmp = tempfile::tempdir().unwrap();
117        let pkg_dir = tmp.path().join("mypkg");
118        std::fs::create_dir_all(&pkg_dir).unwrap();
119
120        std::fs::write(
121            tmp.path().join("alc.toml"),
122            format!("[packages.mypkg]\npath = \"{}\"\n", pkg_dir.display()),
123        )
124        .unwrap();
125
126        let svc = make_service().await;
127        let result = svc
128            .update(Some(tmp.path().to_str().unwrap().to_string()))
129            .await
130            .unwrap();
131        assert!(result.contains("\"resolved\":1"), "{result}");
132        assert!(result.contains("\"errors\":[]"), "{result}");
133        assert!(tmp.path().join("alc.lock").exists());
134    }
135
136    #[tokio::test]
137    async fn update_git_dep_returns_error() {
138        let tmp = tempfile::tempdir().unwrap();
139        std::fs::write(
140            tmp.path().join("alc.toml"),
141            "[packages.mypkg]\ngit = \"https://github.com/user/pkg\"\n",
142        )
143        .unwrap();
144
145        let svc = make_service().await;
146        let result = svc
147            .update(Some(tmp.path().to_str().unwrap().to_string()))
148            .await
149            .unwrap();
150        // errors list is non-empty
151        assert!(result.contains("Git source not supported"), "{result}");
152    }
153}