diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-08-07 16:05:00 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-08-07 16:57:24 +0800 |
| commit | 1d7efb22dc82780dc3fb1d3311188c0514d93991 (patch) | |
| tree | b6708a5d19b238249961aac8b607bea2da055565 /mingling_cli/src | |
| parent | 180f72d307d75351bc8baa19b8ac662ef58fc35f (diff) | |
feat: implement pkg-enable and pkg-disable commands
Add package enable/disable functionality with version matching
for pkg-enable, and fix help text to reflect implemented commands.
Diffstat (limited to 'mingling_cli/src')
| -rw-r--r-- | mingling_cli/src/pkg_mgr.rs | 14 | ||||
| -rw-r--r-- | mingling_cli/src/pkg_mgr/cmd_pkg_disable.rs | 108 | ||||
| -rw-r--r-- | mingling_cli/src/pkg_mgr/cmd_pkg_enable.rs | 142 | ||||
| -rw-r--r-- | mingling_cli/src/pkg_mgr/cmd_uninstall.rs | 8 |
4 files changed, 268 insertions, 4 deletions
diff --git a/mingling_cli/src/pkg_mgr.rs b/mingling_cli/src/pkg_mgr.rs index 7aea59d..2f947df 100644 --- a/mingling_cli/src/pkg_mgr.rs +++ b/mingling_cli/src/pkg_mgr.rs @@ -1,4 +1,6 @@ pub mod cmd_install; +pub mod cmd_pkg_disable; +pub mod cmd_pkg_enable; pub mod cmd_uninstall; use std::path::PathBuf; @@ -12,6 +14,8 @@ use crate::ThisProgram; pack_err!(ErrorRootPackageNotFound); pack_err!(ErrorNoDataDirectory); +pack_err!(ErrorPackageSpecInvalid = String); +pack_err!(ErrorPackageNameRequired); /// The `.mingling` packages directory under the user's data directory. #[derive(Debug, Default, Clone)] @@ -38,3 +42,13 @@ pub fn render_error_root_package_not_found(_: ErrorRootPackageNotFound) { pub fn render_error_no_data_directory(_: ErrorNoDataDirectory) { r_println!("error: failed to determine the data directory"); } + +#[renderer(buffer)] +pub fn render_error_package_spec_invalid(err: ErrorPackageSpecInvalid) { + r_println!("error: invalid package spec: {}", err.info); +} + +#[renderer(buffer)] +pub fn render_error_package_name_required(_: ErrorPackageNameRequired) { + r_println!("error: a package name is required"); +} diff --git a/mingling_cli/src/pkg_mgr/cmd_pkg_disable.rs b/mingling_cli/src/pkg_mgr/cmd_pkg_disable.rs new file mode 100644 index 0000000..679421c --- /dev/null +++ b/mingling_cli/src/pkg_mgr/cmd_pkg_disable.rs @@ -0,0 +1,108 @@ +use std::{fs, io}; + +use mingling::{ + Grouped, 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, + pkg_mgr::{ + ErrorNoDataDirectory, ErrorPackageNameRequired, ErrorPackageSpecInvalid, ResPackagesDir, + }, +}; + +/// Positional argument: package name +pub static ARG_NAME: PickerArg<String> = arg![String]; + +pack_err!(ErrorPackageNotEnabled = String); + +// The name of the package to disable +pack!(StatePkgDisable = String); + +#[derive(Debug, Default, Grouped)] +pub struct ResultPkgDisable { + pub name: String, + pub removed: bool, +} + +#[metadata(EntryPkgDisable)] +pub fn desc_pkg_disable() -> Description { + "Disable the specified package".into() +} + +#[command(node = "pkg-disable", routeify)] +pub fn package_disable(args: EntryPkgDisable, packages_dir: &ResPackagesDir) -> Next { + let name = args + .pick_or_route(&ARG_NAME, || ErrorPackageNameRequired::default().to_chain()) + .to_result()?; + let packages_dir = &packages_dir.path; + if packages_dir.as_os_str().is_empty() { + return ErrorNoDataDirectory::default().to_chain(); + } + if name.contains('/') || name.contains('\\') || name.contains("..") || name.contains('@') { + return ErrorPackageSpecInvalid::new(name).to_chain(); + } + + StatePkgDisable::new(name).to_chain() +} + +#[chain(routeify)] +pub fn handle_state_pkg_disable(p: StatePkgDisable, packages_dir: &ResPackagesDir) -> Next { + let name = p.inner; + let packages_dir = &packages_dir.path; + if packages_dir.as_os_str().is_empty() { + return ErrorNoDataDirectory::default().to_chain(); + } + + let file = packages_dir.join(&name); + if !file.is_file() { + return ErrorPackageNotEnabled::new(name).to_chain(); + } + fs::remove_file(&file).map_err(|e| { + io::Error::new( + e.kind(), + format!("failed to remove {}: {e}", file.display()), + ) + })?; + + ResultPkgDisable { + name, + removed: true, + } + .to_chain() +} + +#[renderer(buffer)] +pub fn render_result_pkg_disable(r: ResultPkgDisable) { + r_println!("Disabled {}", r.name); +} + +#[renderer(buffer)] +pub fn render_error_package_not_enabled(err: ErrorPackageNotEnabled) { + r_println!("error: package is not enabled: {}", err.info); +} + +#[completion(EntryPkgDisable)] +pub fn complete_pkg_disable(ctx: &ShellContext, packages_dir: &ResPackagesDir) -> Suggest { + if ctx.previous_word != "pkg-disable" { + return Suggest::FileCompletion; + } + let mut suggest = Suggest::new(); + if let Ok(entries) = fs::read_dir(&packages_dir.path) { + for entry in entries.flatten() { + if !entry.file_type().is_ok_and(|t| t.is_file()) { + continue; + } + if let Some(name) = entry.file_name().to_str() { + suggest.insert(SuggestItem::Simple(name.to_string())); + } + } + } + suggest +} diff --git a/mingling_cli/src/pkg_mgr/cmd_pkg_enable.rs b/mingling_cli/src/pkg_mgr/cmd_pkg_enable.rs new file mode 100644 index 0000000..b545a4d --- /dev/null +++ b/mingling_cli/src/pkg_mgr/cmd_pkg_enable.rs @@ -0,0 +1,142 @@ +use std::{fs, io}; + +use mingling::{ + Grouped, 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, + pkg_mgr::{ + ErrorNoDataDirectory, ErrorPackageNameRequired, ErrorPackageSpecInvalid, ResPackagesDir, + }, +}; + +/// Positional argument: package spec (`foo`, `foo@0`, `foo@0.1`, `foo@0.1.2`) +pub static ARG_SPEC: PickerArg<String> = arg![String]; + +pack_err!(ErrorNoMatchingVersion = String); +pack!(StatePkgEnable = (String, String)); + +#[derive(Debug, Default, Grouped)] +pub struct ResultPkgEnable { + pub name: String, + pub version: String, +} + +#[metadata(EntryPkgEnable)] +pub fn desc_pkg_enable() -> Description { + "Enable the specified package".into() +} + +#[command(node = "pkg-enable", routeify)] +pub fn package_enable(args: EntryPkgEnable, packages_dir: &ResPackagesDir) -> Next { + let spec = args + .pick_or_route(&ARG_SPEC, || ErrorPackageNameRequired::default().to_chain()) + .to_result()?; + let packages_dir = &packages_dir.path; + if packages_dir.as_os_str().is_empty() { + return ErrorNoDataDirectory::default().to_chain(); + } + if spec.contains('/') || spec.contains('\\') || spec.contains("..") { + return ErrorPackageSpecInvalid::new(spec).to_chain(); + } + + let (name, version_part) = match spec.split_once('@') { + Some((name, version)) => (name.to_string(), Some(version.to_string())), + None => (spec.clone(), None), + }; + + // Collect every installed version matching the spec, pick the newest + let mut candidates: Vec<(String, semver::Version)> = Vec::new(); + if let Ok(entries) = fs::read_dir(packages_dir) { + for entry in entries.flatten() { + if !entry.file_type().is_ok_and(|t| t.is_dir()) { + continue; + } + let Some(dir_name) = entry.file_name().to_str().map(str::to_owned) else { + continue; + }; + let Some((dir_pkg, dir_version)) = dir_name.split_once('@') else { + continue; + }; + if dir_pkg != name { + continue; + } + if let Some(part) = &version_part + && !version_matches(dir_version, part) + { + continue; + } + if let Ok(version) = semver::Version::parse(dir_version) { + candidates.push((dir_name, version)); + } + } + } + + let Some((_, version)) = candidates.into_iter().max_by(|a, b| a.1.cmp(&b.1)) else { + return ErrorNoMatchingVersion::new(spec).to_chain(); + }; + + StatePkgEnable::new((name, version.to_string())).to_chain() +} + +#[chain(routeify)] +pub fn handle_state_pkg_enable(p: StatePkgEnable, packages_dir: &ResPackagesDir) -> Next { + let (name, version) = p.inner; + let packages_dir = &packages_dir.path; + if packages_dir.as_os_str().is_empty() { + return ErrorNoDataDirectory::default().to_chain(); + } + + let file = packages_dir.join(&name); + fs::write(&file, &version).map_err(|e| { + io::Error::new(e.kind(), format!("failed to write {}: {e}", file.display())) + })?; + + ResultPkgEnable { name, version }.to_chain() +} + +#[renderer(buffer)] +pub fn render_result_pkg_enable(r: ResultPkgEnable) { + r_println!("Enabled {}@{}", r.name, r.version); +} + +#[completion(EntryPkgEnable)] +pub fn complete_pkg_enable(ctx: &ShellContext, packages_dir: &ResPackagesDir) -> Suggest { + if ctx.previous_word != "pkg-enable" { + return Suggest::FileCompletion; + } + let mut suggest = Suggest::new(); + if let Ok(entries) = fs::read_dir(&packages_dir.path) { + for entry in entries.flatten() { + if !entry.file_type().is_ok_and(|t| t.is_dir()) { + continue; + } + if let Some(name) = entry.file_name().to_str() { + suggest.insert(SuggestItem::Simple(name.to_string())); + } + } + } + suggest +} + +/// Whether `version` falls under the partial spec, compared segment by segment. +/// Pre-release suffixes are ignored during matching, e.g. `0.2.1` matches `0.2.1-rc10`. +fn version_matches(version: &str, partial: &str) -> bool { + let version = version.split('-').next().unwrap_or(version); + let version_parts: Vec<&str> = version.split('.').collect(); + let partial_parts: Vec<&str> = partial.split('.').collect(); + if partial_parts.len() > version_parts.len() { + return false; + } + partial_parts + .iter() + .zip(version_parts.iter()) + .all(|(p, v)| p == v) +} diff --git a/mingling_cli/src/pkg_mgr/cmd_uninstall.rs b/mingling_cli/src/pkg_mgr/cmd_uninstall.rs index d9a3004..69c5aa5 100644 --- a/mingling_cli/src/pkg_mgr/cmd_uninstall.rs +++ b/mingling_cli/src/pkg_mgr/cmd_uninstall.rs @@ -3,8 +3,7 @@ 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, + arg, buffer, chain, command, completion, metadata, pack, r_println, renderer, routeify, }, metadata::Description, picker::{EntryPicker, PickerArg}, @@ -13,13 +12,14 @@ use mingling::{ use crate::{ Next, metadata::setup::ResMetadata, - pkg_mgr::{ErrorNoDataDirectory, ErrorRootPackageNotFound, ResPackagesDir}, + pkg_mgr::{ + ErrorNoDataDirectory, ErrorPackageSpecInvalid, 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)] |
