Skip to main content

algocline_app/service/pkg/
remove.rs

1//! `pkg_remove` — remove a package from project scope (alc.lock) or global scope (filesystem).
2
3use super::super::lockfile::{load_lockfile, lockfile_path, save_lockfile};
4use super::super::manifest;
5use super::super::path::ContainedPath;
6use super::super::project::resolve_project_root;
7use super::super::resolve::packages_dir;
8use super::super::AppService;
9
10impl AppService {
11    /// Remove an installed package.
12    ///
13    /// - `project_root` specified or `scope == "project"`: removes the entry
14    ///   from `alc.lock` only. The physical files are **not** deleted.
15    /// - `scope == "global"`: removes from `~/.algocline/packages/` (existing
16    ///   behavior), even if the package exists in `alc.lock`.
17    /// - Default (no scope, no project_root): tries project first, falls back
18    ///   to global.
19    pub async fn pkg_remove(
20        &self,
21        name: &str,
22        project_root: Option<String>,
23        scope: Option<String>,
24    ) -> Result<String, String> {
25        let effective_scope =
26            determine_remove_scope(name, project_root.as_deref(), scope.as_deref());
27
28        match effective_scope {
29            RemoveScope::Project(root) => {
30                // Remove from alc.lock only; do NOT delete physical files.
31                let mut lock = match load_lockfile(&root)? {
32                    Some(l) => l,
33                    None => {
34                        return Err(format!(
35                            "Package '{name}' not found in project (no alc.lock at {})",
36                            root.display()
37                        ));
38                    }
39                };
40
41                let before = lock.packages.len();
42                lock.packages.retain(|p| p.name != name);
43
44                if lock.packages.len() == before {
45                    return Err(format!(
46                        "Package '{name}' not found in alc.lock at {}",
47                        root.display()
48                    ));
49                }
50
51                save_lockfile(&root, &lock)?;
52
53                Ok(serde_json::json!({
54                    "removed": name,
55                    "scope": "project",
56                    "lockfile": lockfile_path(&root).display().to_string(),
57                })
58                .to_string())
59            }
60            RemoveScope::Global => {
61                // Original behavior: delete physical files.
62                let pkg_dir = packages_dir()?;
63                let dest = ContainedPath::child(&pkg_dir, name)?;
64
65                if !dest.as_ref().exists() {
66                    return Err(format!("Package '{name}' not found"));
67                }
68
69                std::fs::remove_dir_all(&dest)
70                    .map_err(|e| format!("Failed to remove '{name}': {e}"))?;
71
72                // Remove from manifest (best-effort)
73                let _ = manifest::record_remove(name);
74
75                Ok(serde_json::json!({ "removed": name, "scope": "global" }).to_string())
76            }
77        }
78    }
79}
80
81// ─── Remove scope resolution ─────────────────────────────────────
82
83enum RemoveScope {
84    Project(std::path::PathBuf),
85    Global,
86}
87
88/// Determine the effective remove scope.
89///
90/// Priority:
91/// 1. `scope == "global"` → always Global (ignores project_root).
92/// 2. `project_root` is `Some` → Project (use that root).
93/// 3. `scope == "project"` → Project (auto-detect root, must find package).
94/// 4. Auto-detect: resolve root; if alc.lock contains the package → Project,
95///    otherwise → Global.
96fn determine_remove_scope(
97    name: &str,
98    project_root: Option<&str>,
99    scope: Option<&str>,
100) -> RemoveScope {
101    // Explicit global scope — skip all project logic.
102    if scope == Some("global") {
103        return RemoveScope::Global;
104    }
105
106    let root = match resolve_project_root(project_root) {
107        Some(r) => r,
108        None => return RemoveScope::Global,
109    };
110
111    // Explicit project_root provided → always project scope.
112    if project_root.is_some() {
113        return RemoveScope::Project(root);
114    }
115
116    // Explicit scope == "project" → project scope (auto-detected root).
117    if scope == Some("project") {
118        return RemoveScope::Project(root);
119    }
120
121    // Auto-detection: only use project scope when the package is actually
122    // listed in alc.lock.
123    match load_lockfile(&root) {
124        Ok(Some(lock)) if lock.packages.iter().any(|p| p.name == name) => {
125            RemoveScope::Project(root)
126        }
127        _ => RemoveScope::Global,
128    }
129}