algocline_app/service/pkg/
remove.rs1use super::super::alc_toml::{load_alc_toml_document, remove_package_entry, save_alc_toml};
11use super::super::lockfile::{load_lockfile, lockfile_path, save_lockfile};
12use super::super::manifest::{load_manifest, record_remove};
13use super::super::project::resolve_project_root;
14use super::super::AppService;
15
16impl AppService {
17 pub async fn pkg_remove(
28 &self,
29 name: &str,
30 project_root: Option<String>,
31 version: Option<String>,
32 scope: Option<String>,
33 ) -> Result<String, String> {
34 let scope = scope.as_deref().unwrap_or("project");
35 match scope {
36 "project" => remove_from_project(name, project_root, version),
37 "global" => remove_from_global(name),
38 "all" => remove_from_all(name, project_root, version),
39 other => Err(format!(
40 "invalid scope '{other}': expected one of project, global, all"
41 )),
42 }
43 }
44}
45
46fn remove_from_project(
48 name: &str,
49 project_root: Option<String>,
50 version: Option<String>,
51) -> Result<String, String> {
52 let root = resolve_project_root(project_root.as_deref()).ok_or_else(|| {
53 format!(
54 "alc.toml not found: cannot remove '{name}' without a project root. \
55 Provide project_root or run from a project directory."
56 )
57 })?;
58
59 match load_alc_toml_document(&root)? {
61 Some(mut doc) => {
62 remove_package_entry(&mut doc, name);
63 save_alc_toml(&root, &doc)?;
64 }
65 None => {
66 return Err(format!("alc.toml not found at {}", root.display()));
67 }
68 }
69
70 let alc_lock_path = lockfile_path(&root);
73 match load_lockfile(&root)? {
74 Some(mut lock) => {
75 let before = lock.packages.len();
76 lock.packages.retain(|p| {
77 if p.name != name {
78 return true;
79 }
80 match &version {
81 Some(v) => p.version.as_deref() != Some(v.as_str()),
82 None => false,
83 }
84 });
85
86 if lock.packages.len() == before {
87 return Err(format!(
88 "Package '{name}' not found in alc.lock at {}",
89 alc_lock_path.display()
90 ));
91 }
92
93 save_lockfile(&root, &lock)?;
94 }
95 None => {
96 return Err(format!(
97 "Package '{name}' not found in alc.lock at {}",
98 alc_lock_path.display()
99 ));
100 }
101 }
102
103 Ok(serde_json::json!({
104 "removed": name,
105 "scope": "project",
106 "alc_toml": root.join("alc.toml").display().to_string(),
107 "alc_lock": alc_lock_path.display().to_string(),
108 })
109 .to_string())
110}
111
112fn remove_from_global(name: &str) -> Result<String, String> {
115 let manifest = load_manifest()?;
116 if !manifest.packages.contains_key(name) {
117 return Err(format!(
118 "Package '{name}' not found in global manifest (~/.algocline/installed.json)"
119 ));
120 }
121
122 record_remove(name)?;
123
124 Ok(serde_json::json!({
125 "removed": name,
126 "scope": "global",
127 "installed_json": manifest_path_display(),
128 })
129 .to_string())
130}
131
132fn remove_from_all(
135 name: &str,
136 project_root: Option<String>,
137 version: Option<String>,
138) -> Result<String, String> {
139 let project_res = remove_from_project(name, project_root, version);
140 let global_res = remove_from_global(name);
141
142 let (project_ok, project_err) = match project_res {
143 Ok(_) => (true, None),
144 Err(e) => (false, Some(e)),
145 };
146 let (global_ok, global_err) = match global_res {
147 Ok(_) => (true, None),
148 Err(e) => (false, Some(e)),
149 };
150
151 if !project_ok && !global_ok {
152 return Err(format!(
153 "Package '{name}' not found in any scope:\n project: {}\n global: {}",
154 project_err.unwrap_or_default(),
155 global_err.unwrap_or_default()
156 ));
157 }
158
159 Ok(serde_json::json!({
160 "removed": name,
161 "scope": "all",
162 "project_removed": project_ok,
163 "global_removed": global_ok,
164 "project_note": project_err,
165 "global_note": global_err,
166 })
167 .to_string())
168}
169
170fn manifest_path_display() -> String {
174 dirs::home_dir()
175 .map(|h| {
176 h.join(".algocline")
177 .join("installed.json")
178 .display()
179 .to_string()
180 })
181 .unwrap_or_default()
182}