aboutsummaryrefslogtreecommitdiff
path: root/mingling_cli/src
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-08-07 16:05:00 +0800
committer魏曹先生 <1992414357@qq.com>2026-08-07 16:57:24 +0800
commit1d7efb22dc82780dc3fb1d3311188c0514d93991 (patch)
treeb6708a5d19b238249961aac8b607bea2da055565 /mingling_cli/src
parent180f72d307d75351bc8baa19b8ac662ef58fc35f (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.rs14
-rw-r--r--mingling_cli/src/pkg_mgr/cmd_pkg_disable.rs108
-rw-r--r--mingling_cli/src/pkg_mgr/cmd_pkg_enable.rs142
-rw-r--r--mingling_cli/src/pkg_mgr/cmd_uninstall.rs8
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)]