diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-08-07 15:48:37 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-08-09 02:56:28 +0800 |
| commit | c2de5313748e7d5f8ad7efee9ffecb513994443a (patch) | |
| tree | c449707d80a435ba14684c3b0387ea971da66ea7 /mingling_cli/src/pkg_mgr/cmd_uninstall.rs | |
| parent | b5251db3f8bfb1a9b8ba946570baabce813c6ba8 (diff) | |
feat(pkg_mgr): add uninstall command and improve install
Implement `mling uninstall` to remove installed packages by name or
name@version, with shell completion support. Rework install to build
release binaries, copy them plus completion scripts to a per-version
directory under the user data dir.
Diffstat (limited to 'mingling_cli/src/pkg_mgr/cmd_uninstall.rs')
| -rw-r--r-- | mingling_cli/src/pkg_mgr/cmd_uninstall.rs | 149 |
1 files changed, 149 insertions, 0 deletions
diff --git a/mingling_cli/src/pkg_mgr/cmd_uninstall.rs b/mingling_cli/src/pkg_mgr/cmd_uninstall.rs new file mode 100644 index 0000000..d9a3004 --- /dev/null +++ b/mingling_cli/src/pkg_mgr/cmd_uninstall.rs @@ -0,0 +1,149 @@ +use std::{fs, io, path::PathBuf}; + +use mingling::{ + Grouped, LazyRes, Routable, ShellContext, Suggest, SuggestItem, + macros::{ + arg, buffer, chain, command, completion, metadata, pack, pack_err, r_println, renderer, + routeify, + }, + metadata::Description, + picker::{EntryPicker, PickerArg}, +}; + +use crate::{ + Next, + metadata::setup::ResMetadata, + pkg_mgr::{ErrorNoDataDirectory, ErrorRootPackageNotFound, ResPackagesDir}, +}; + +/// Optional positional argument: package spec (`name` or `name@version`) +pub static ARG_PACKAGE: PickerArg<Option<String>> = arg![Option<String>]; + +pack_err!(ErrorPackageSpecInvalid = String); +pack!(StateUninstallPackages = Vec<String>); + +#[derive(Debug, Default, Grouped)] +pub struct ResultUninstall { + pub removed: Vec<PathBuf>, + pub not_installed: Vec<PathBuf>, +} + +/// `{data_dir}/.mingling` +#[metadata(EntryUninstall)] +pub fn desc_uninstall() -> Description { + "Uninstall the project from the Mingling package list".into() +} + +#[command(routeify)] +pub fn uninstall( + args: EntryUninstall, + packages_dir: &ResPackagesDir, + metadata: &mut LazyRes<ResMetadata>, +) -> Next { + let spec = args.pick(&ARG_PACKAGE).to_result()?; + let packages_dir = &packages_dir.path; + if packages_dir.as_os_str().is_empty() { + return ErrorNoDataDirectory::default().to_chain(); + } + + let targets = match spec { + // No spec: uninstall the version installed by the current project + None => { + let metadata = metadata.get_ref().data(); + let root_package = metadata + .root_package() + .or_else(|| metadata.workspace_packages().first().copied()) + .ok_or(ErrorRootPackageNotFound::default())?; + vec![format!("{}@{}", root_package.name, root_package.version)] + } + // `name` matches every installed version, `name@version` matches exactly + Some(spec) => { + if spec.contains('/') || spec.contains('\\') || spec.contains("..") { + return ErrorPackageSpecInvalid::new(spec).to_chain(); + } + if spec.contains('@') { + vec![spec] + } else { + let prefix = format!("{spec}@"); + if !packages_dir.is_dir() { + Vec::new() + } else { + fs::read_dir(packages_dir) + .map_err(|e| { + io::Error::new( + e.kind(), + format!("failed to read {}: {e}", packages_dir.display()), + ) + })? + .filter_map(|e| e.ok()) + .filter_map(|e| e.file_name().into_string().ok()) + .filter(|name| name.starts_with(&prefix)) + .collect() + } + } + } + }; + + StateUninstallPackages::new(targets).to_chain() +} + +#[chain(routeify)] +pub fn handle_state_uninstall_packages( + p: StateUninstallPackages, + packages_dir: &ResPackagesDir, +) -> Next { + let packages_dir = &packages_dir.path; + if packages_dir.as_os_str().is_empty() { + return ErrorNoDataDirectory::default().to_chain(); + } + let mut removed = Vec::new(); + let mut not_installed = Vec::new(); + + for name in p.inner { + let dir = packages_dir.join(&name); + if !dir.exists() { + not_installed.push(dir); + continue; + } + fs::remove_dir_all(&dir).map_err(|e| { + io::Error::new(e.kind(), format!("failed to remove {}: {e}", dir.display())) + })?; + removed.push(dir); + } + + ResultUninstall { + removed, + not_installed, + } + .to_chain() +} + +#[renderer(buffer)] +pub fn render_result_uninstall(r: ResultUninstall) { + if r.removed.is_empty() && r.not_installed.is_empty() { + r_println!("No matching packages installed"); + } else { + for dir in r.removed { + r_println!("Uninstalled: {}", dir.display()); + } + for dir in r.not_installed { + r_println!("Not installed: {}", dir.display()); + } + } +} + +#[completion(EntryUninstall)] +pub fn complete_uninstall(ctx: &ShellContext, packages_dir: &ResPackagesDir) -> Suggest { + if ctx.previous_word != "uninstall" { + return Suggest::FileCompletion; + } + let mut suggest = Suggest::new(); + if let Ok(entries) = fs::read_dir(&packages_dir.path) { + for entry in entries.flatten() { + if let Some(name) = entry.file_name().to_str() { + suggest.insert(SuggestItem::Simple(name.to_string())); + } + } + } + suggest +} |
