1use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5
6use crate::error::{PkgError, PkgResult};
7use crate::lockfile::Lockfile;
8use crate::manifest::Manifest;
9use crate::resolver::PackageRegistry;
10use crate::tree;
11
12pub const MANIFEST_FILE: &str = "bock.package";
14
15pub const LOCKFILE: &str = "bock.lock";
17
18pub fn find_manifest(start_dir: &Path) -> PkgResult<PathBuf> {
20 let mut dir = start_dir.to_path_buf();
21 loop {
22 let candidate = dir.join(MANIFEST_FILE);
23 if candidate.exists() {
24 return Ok(candidate);
25 }
26 if !dir.pop() {
27 return Err(PkgError::Io(format!(
28 "no {MANIFEST_FILE} found in {start_dir:?} or any parent directory"
29 )));
30 }
31 }
32}
33
34pub fn init(dir: &Path, name: &str) -> PkgResult<PathBuf> {
36 let manifest_path = dir.join(MANIFEST_FILE);
37 if manifest_path.exists() {
38 return Err(PkgError::Io(format!(
39 "{MANIFEST_FILE} already exists in {}",
40 dir.display()
41 )));
42 }
43
44 let manifest = Manifest {
45 package: crate::manifest::PackageSection {
46 name: name.to_string(),
47 version: "0.1.0".to_string(),
48 targets: None,
49 },
50 dependencies: Default::default(),
51 dev_dependencies: BTreeMap::new(),
52 features: BTreeMap::new(),
53 };
54
55 let content = manifest.to_toml_string()?;
56 std::fs::write(&manifest_path, content).map_err(|e| PkgError::Io(e.to_string()))?;
57
58 Ok(manifest_path)
59}
60
61pub fn add(manifest_path: &Path, name: &str, version: Option<&str>) -> PkgResult<()> {
65 let mut manifest = Manifest::from_file(manifest_path)?;
66 let version_str = version.unwrap_or("*").to_string();
67 manifest.add_dependency(name.to_string(), version_str.clone());
68
69 let content = manifest.to_toml_string()?;
70 std::fs::write(manifest_path, content).map_err(|e| PkgError::Io(e.to_string()))?;
71
72 Ok(())
73}
74
75pub fn remove(manifest_path: &Path, name: &str) -> PkgResult<()> {
77 let mut manifest = Manifest::from_file(manifest_path)?;
78
79 if !manifest.remove_dependency(name) {
80 return Err(PkgError::PackageNotFound(format!(
81 "'{name}' is not listed in [dependencies]"
82 )));
83 }
84
85 let content = manifest.to_toml_string()?;
86 std::fs::write(manifest_path, content).map_err(|e| PkgError::Io(e.to_string()))?;
87
88 Ok(())
89}
90
91pub fn resolve_and_lock(manifest_path: &Path, registry: &PackageRegistry) -> PkgResult<Lockfile> {
93 let manifest = Manifest::from_file(manifest_path)?;
94 let lock_path = manifest_path
95 .parent()
96 .unwrap_or(Path::new("."))
97 .join(LOCKFILE);
98
99 let direct_deps: BTreeMap<String, String> = manifest
101 .dependencies
102 .iter()
103 .filter_map(|(name, spec)| spec.version_req().map(|v| (name.clone(), v.to_string())))
104 .collect();
105
106 let resolved = registry.resolve(
107 &manifest.package.name,
108 &manifest.package.version,
109 &direct_deps,
110 )?;
111
112 let lockfile = Lockfile::from_resolved(&resolved);
113 lockfile.write_to_file(&lock_path)?;
114
115 Ok(lockfile)
116}
117
118pub fn show_tree(manifest_path: &Path, registry: &PackageRegistry) -> PkgResult<String> {
120 let manifest = Manifest::from_file(manifest_path)?;
121 let lock_path = manifest_path
122 .parent()
123 .unwrap_or(Path::new("."))
124 .join(LOCKFILE);
125
126 let direct_deps: BTreeMap<String, String> = manifest
128 .dependencies
129 .iter()
130 .filter_map(|(name, spec)| spec.version_req().map(|v| (name.clone(), v.to_string())))
131 .collect();
132
133 let resolved = if lock_path.exists() {
135 let lockfile = Lockfile::from_file(&lock_path)?;
136 lockfile.to_resolved()?
137 } else {
138 registry.resolve(
140 &manifest.package.name,
141 &manifest.package.version,
142 &direct_deps,
143 )?
144 };
145
146 Ok(tree::render_tree(
147 &manifest.package.name,
148 &manifest.package.version,
149 &direct_deps,
150 &resolved,
151 registry,
152 ))
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn init_creates_manifest() {
161 let dir = tempfile::tempdir().unwrap();
162 let path = init(dir.path(), "test-project").unwrap();
163 assert!(path.exists());
164
165 let manifest = Manifest::from_file(&path).unwrap();
166 assert_eq!(manifest.package.name, "test-project");
167 assert_eq!(manifest.package.version, "0.1.0");
168 }
169
170 #[test]
171 fn init_fails_if_exists() {
172 let dir = tempfile::tempdir().unwrap();
173 init(dir.path(), "test").unwrap();
174 let result = init(dir.path(), "test");
175 assert!(result.is_err());
176 }
177
178 #[test]
179 fn add_dependency_to_manifest() {
180 let dir = tempfile::tempdir().unwrap();
181 let path = init(dir.path(), "my-app").unwrap();
182
183 add(&path, "foo", Some("^1.0")).unwrap();
184
185 let manifest = Manifest::from_file(&path).unwrap();
186 assert!(manifest.dependencies.contains_key("foo"));
187 assert_eq!(manifest.dependencies["foo"].version_req(), Some("^1.0"));
188 }
189
190 #[test]
191 fn remove_dependency_from_manifest() {
192 let dir = tempfile::tempdir().unwrap();
193 let path = init(dir.path(), "my-app").unwrap();
194
195 add(&path, "foo", Some("^1.0")).unwrap();
196 remove(&path, "foo").unwrap();
197
198 let manifest = Manifest::from_file(&path).unwrap();
199 assert!(!manifest.dependencies.contains_key("foo"));
200 }
201
202 #[test]
203 fn remove_nonexistent_returns_error() {
204 let dir = tempfile::tempdir().unwrap();
205 let path = init(dir.path(), "my-app").unwrap();
206
207 let result = remove(&path, "nonexistent");
208 assert!(matches!(result, Err(PkgError::PackageNotFound(_))));
209 }
210
211 #[test]
212 fn resolve_and_lock_creates_lockfile() {
213 let dir = tempfile::tempdir().unwrap();
214 let path = init(dir.path(), "my-app").unwrap();
215
216 add(&path, "foo", Some("^1.0")).unwrap();
218
219 let mut registry = PackageRegistry::new();
220 registry.register("foo", "1.2.0", BTreeMap::new()).unwrap();
221
222 let lockfile = resolve_and_lock(&path, ®istry).unwrap();
223 assert_eq!(lockfile.get_version("foo"), Some("1.2.0"));
224
225 let lock_path = dir.path().join(LOCKFILE);
227 assert!(lock_path.exists());
228 }
229
230 #[test]
231 fn show_tree_renders_deps() {
232 let dir = tempfile::tempdir().unwrap();
233 let path = init(dir.path(), "my-app").unwrap();
234 add(&path, "foo", Some("^1.0")).unwrap();
235
236 let mut registry = PackageRegistry::new();
237 registry.register("foo", "1.0.0", BTreeMap::new()).unwrap();
238
239 resolve_and_lock(&path, ®istry).unwrap();
241
242 let tree_output = show_tree(&path, ®istry).unwrap();
243 assert!(tree_output.contains("my-app v0.1.0"));
244 assert!(tree_output.contains("foo v1.0.0"));
245 }
246}