Skip to main content

bock_pkg/
commands.rs

1//! High-level package manager commands (`add`, `remove`, `tree`, etc.).
2
3use 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
12/// The default manifest file name.
13pub const MANIFEST_FILE: &str = "bock.package";
14
15/// The default lockfile name.
16pub const LOCKFILE: &str = "bock.lock";
17
18/// Find the manifest file in the given directory or its ancestors.
19pub 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
34/// Initialize a new package manifest in the given directory.
35pub 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
61/// Add a dependency to the manifest.
62///
63/// If no version is specified, defaults to `"*"` (any version).
64pub 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
75/// Remove a dependency from the manifest.
76pub 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
91/// Resolve dependencies and generate/update the lockfile.
92pub 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    // Convert manifest dependencies to name→version_req map
100    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
118/// Display the dependency tree.
119pub 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    // Convert manifest dependencies to name→version_req map
127    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    // Read resolved versions from lockfile if it exists
134    let resolved = if lock_path.exists() {
135        let lockfile = Lockfile::from_file(&lock_path)?;
136        lockfile.to_resolved()?
137    } else {
138        // Resolve on the fly
139        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 a dep and register it in the registry
217        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, &registry).unwrap();
223        assert_eq!(lockfile.get_version("foo"), Some("1.2.0"));
224
225        // Check lockfile was written
226        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        // Create a lockfile first
240        resolve_and_lock(&path, &registry).unwrap();
241
242        let tree_output = show_tree(&path, &registry).unwrap();
243        assert!(tree_output.contains("my-app v0.1.0"));
244        assert!(tree_output.contains("foo v1.0.0"));
245    }
246}