algocline_app/service/
update.rs1use super::alc_toml::{load_alc_toml, PackageDep};
4use super::lockfile::{lockfile_path, save_lockfile, LockFile, LockPackage};
5use super::path::copy_dir;
6use super::resolve::packages_dir;
7use super::source::PackageSource;
8use super::AppService;
9
10impl AppService {
11 pub async fn update(&self, project_root: Option<String>) -> Result<String, String> {
12 let root = self
13 .resolve_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(&self.log_config.app_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 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 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 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 assert!(result.contains("Git source not supported"), "{result}");
152 }
153}