Skip to main content

algocline_app/service/
pkg_unlink.rs

1//! `alc_pkg_unlink` — remove a symlinked package from `~/.algocline/packages/`.
2//!
3//! Only operates on symlinks. For installed (copied) packages, use `pkg_remove`.
4
5use super::alc_toml::validate_package_name;
6use super::resolve::packages_dir;
7use super::AppService;
8
9impl AppService {
10    /// Remove a symlinked package from the global cache.
11    ///
12    /// - If `~/.algocline/packages/{name}` is a symlink: removes it.
13    /// - If it is a real directory: returns an error directing to `pkg_remove`.
14    /// - If it does not exist: returns an error.
15    pub async fn pkg_unlink(&self, name: String) -> Result<String, String> {
16        validate_package_name(&name)?;
17
18        let pkgs = packages_dir()?;
19        let dest = pkgs.join(&name);
20
21        // Use symlink_metadata so dangling symlinks are also detected.
22        match dest.symlink_metadata() {
23            Ok(m) => {
24                if m.file_type().is_symlink() {
25                    std::fs::remove_file(&dest)
26                        .map_err(|e| format!("Failed to remove symlink {}: {e}", dest.display()))?;
27                } else {
28                    return Err(format!(
29                        "Package '{}' is not a symlink. Use pkg_remove to remove installed packages.",
30                        name
31                    ));
32                }
33            }
34            Err(_) => {
35                return Err(format!("Package '{name}' is not installed"));
36            }
37        }
38
39        Ok(serde_json::json!({ "unlinked": name }).to_string())
40    }
41}
42
43// ─── Tests ───────────────────────────────────────────────────────
44
45#[cfg(all(test, unix))]
46mod tests {
47    use std::os::unix::fs::symlink;
48
49    use crate::service::test_support::{make_app_service, FakeHome};
50
51    #[tokio::test]
52    async fn pkg_unlink_removes_symlink() {
53        let env = FakeHome::new();
54        let home = &env.home;
55
56        let pkgs = home.join(".algocline").join("packages");
57        std::fs::create_dir_all(&pkgs).unwrap();
58
59        let target = home.join("my_pkg");
60        std::fs::create_dir_all(&target).unwrap();
61        let dest = pkgs.join("my_pkg");
62        symlink(&target, &dest).unwrap();
63
64        let svc = make_app_service().await;
65        let result = svc.pkg_unlink("my_pkg".to_string()).await.unwrap();
66
67        let json: serde_json::Value = serde_json::from_str(&result).unwrap();
68        assert_eq!(json["unlinked"], "my_pkg");
69        assert!(dest.symlink_metadata().is_err());
70    }
71
72    #[tokio::test]
73    async fn pkg_unlink_real_dir_returns_error() {
74        let env = FakeHome::new();
75        let home = &env.home;
76
77        let pkgs = home.join(".algocline").join("packages");
78        let dest = pkgs.join("my_pkg");
79        std::fs::create_dir_all(&dest).unwrap();
80
81        let svc = make_app_service().await;
82        let err = svc.pkg_unlink("my_pkg".to_string()).await.unwrap_err();
83
84        assert!(err.contains("not a symlink"), "got: {err}");
85    }
86
87    #[tokio::test]
88    async fn pkg_unlink_not_installed_returns_error() {
89        let env = FakeHome::new();
90        let home = &env.home;
91
92        let pkgs = home.join(".algocline").join("packages");
93        std::fs::create_dir_all(&pkgs).unwrap();
94
95        let svc = make_app_service().await;
96        let err = svc.pkg_unlink("nonexistent".to_string()).await.unwrap_err();
97
98        assert!(err.contains("not installed"), "got: {err}");
99    }
100
101    #[tokio::test]
102    async fn pkg_unlink_dangling_symlink_removed() {
103        let env = FakeHome::new();
104        let home = &env.home;
105
106        let pkgs = home.join(".algocline").join("packages");
107        std::fs::create_dir_all(&pkgs).unwrap();
108
109        let dest = pkgs.join("dangling_pkg");
110        symlink(home.join("nowhere"), &dest).unwrap();
111        assert!(!dest.exists()); // dangling
112
113        let svc = make_app_service().await;
114        let result = svc.pkg_unlink("dangling_pkg".to_string()).await.unwrap();
115
116        let json: serde_json::Value = serde_json::from_str(&result).unwrap();
117        assert_eq!(json["unlinked"], "dangling_pkg");
118        assert!(dest.symlink_metadata().is_err());
119    }
120}