feat: 目录结构迁移优化
- 将 MirrorRegistry MirrorTarget 迁移到 model.rs - TargetConfig 独立实现 FromLua,解耦反序列化与业务校验 - mirror—shim 新增 command.rs 并将 to_command 移动到 command.rs - 其他优化
This commit is contained in:
@@ -11,6 +11,8 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
mirror-core = { path = "../mirror-core" }
|
||||
clap = { version = "4.6.6", features = ["cargo", "color", "derive","string"] }
|
||||
inquire="0.9.4"
|
||||
tempfile="3.27.0"# 用于安全原子写入
|
||||
mlua = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
+24
-2
@@ -2,16 +2,20 @@ use crate::commands::dynamic::MrCommand;
|
||||
use clap::{Arg, ArgAction, Command};
|
||||
use mirror_core::LogLevel;
|
||||
use std::ffi::OsString;
|
||||
// 核心保留字:系统底座命令,神圣不可侵犯
|
||||
const RESERVED_COMMANDS: &[&str] = &["use", "log", "help"];
|
||||
// 引入强类型枚举
|
||||
pub fn build(dynamic_cmds: MrCommand) -> Command {
|
||||
let mut app = Command::new("mr")
|
||||
.about(env!("CARGO_PKG_DESCRIPTION"))
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.arg_required_else_help(true)
|
||||
.subcommand(log_subcommand());
|
||||
.subcommand(log_subcommand())
|
||||
.subcommand(use_subcommand());
|
||||
|
||||
for (name, sub_cmd) in dynamic_cmds.commands {
|
||||
if name == "log" {
|
||||
if RESERVED_COMMANDS.contains(&name.as_str()) {
|
||||
// 用户要是手痒写了 use.lua 或 log.lua,直接装作没看见
|
||||
continue;
|
||||
}
|
||||
// let a =name.as_str()
|
||||
@@ -24,6 +28,24 @@ pub fn build(dynamic_cmds: MrCommand) -> Command {
|
||||
}
|
||||
app
|
||||
}
|
||||
/// 核心内置命令:版本切换
|
||||
fn use_subcommand() -> Command {
|
||||
Command::new("use")
|
||||
.about("切换指定工具的生效版本 (支持交互式选择)")
|
||||
.arg(
|
||||
Arg::new("tool")
|
||||
.required(true)
|
||||
.value_name("TOOL")
|
||||
.help("目标工具名称(在 mirror.lua 中声明的 key,例如 node)"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("version")
|
||||
.required(false)
|
||||
.value_name("VERSION")
|
||||
.help("目标版本号(省略时进入交互式单选)"),
|
||||
)
|
||||
}
|
||||
|
||||
/// 构建基础 CLI 骨架(内置 Rust 原生命令与参数规则)
|
||||
fn log_subcommand() -> Command {
|
||||
Command::new("log")
|
||||
|
||||
@@ -3,7 +3,7 @@ use mirror_core::{conversion_err, validation_error};
|
||||
use mlua::{FromLua, Lua, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
const ALLOWED_KEYS: &[&str] = &["handler", "description"];
|
||||
const ALLOWED_KEYS: &[&str] = &["module", "description"];
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct MrCommand {
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
pub(crate) mod builtin;
|
||||
pub(crate) mod dynamic;
|
||||
pub(crate) mod dynamic;
|
||||
mod use_cmd;
|
||||
|
||||
pub use use_cmd::switch_version;
|
||||
@@ -0,0 +1,145 @@
|
||||
use inquire::Select;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
// 引入你的 mirror-core 类型
|
||||
use mirror_core::{MirrorRegistry, TargetConfig};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UseError {
|
||||
ToolNotFound(String),
|
||||
NotVersioned(String),
|
||||
HomeDirNotFound(PathBuf),
|
||||
NoVersionsAvailable(PathBuf),
|
||||
UserCancelled,
|
||||
Io(io::Error),
|
||||
TargetValidationFailed(PathBuf),
|
||||
}
|
||||
|
||||
/// 执行版本切换主流程
|
||||
pub fn switch_version(
|
||||
registry: &MirrorRegistry,
|
||||
tool_name: &str,
|
||||
target_version: Option<&str>,
|
||||
) -> Result<(), UseError> {
|
||||
// 1. 获取配置并断言必须是多版本工具
|
||||
let spec = registry
|
||||
.targets
|
||||
.get(tool_name)
|
||||
.ok_or_else(|| UseError::ToolNotFound(tool_name.to_string()))?;
|
||||
|
||||
let (home, version_file) = match &spec.target {
|
||||
TargetConfig::Versioned {
|
||||
home, version_file, ..
|
||||
} => {
|
||||
let v_file = version_file.as_deref().unwrap_or(".version");
|
||||
(home, v_file)
|
||||
}
|
||||
TargetConfig::Static(_) => {
|
||||
return Err(UseError::NotVersioned(tool_name.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
if !home.exists() {
|
||||
return Err(UseError::HomeDirNotFound(home.clone()));
|
||||
}
|
||||
|
||||
// 2. 扫描候选版本目录
|
||||
let available_versions = scan_versions(home)?;
|
||||
if available_versions.is_empty() {
|
||||
return Err(UseError::NoVersionsAvailable(home.clone()));
|
||||
}
|
||||
|
||||
// 3. 确定目标版本(命令行传入 OR 唤起 Inquire 单选菜单)
|
||||
let selected_ver = match target_version {
|
||||
Some(ver) => {
|
||||
// 如果用户传了参,校验是否在候选集中(防呆)
|
||||
if !available_versions.iter().any(|v| v == ver) {
|
||||
eprintln!(
|
||||
"警告: 目录 {:?} 下未发现版本 '{}',仍将强制写入。",
|
||||
home, ver
|
||||
);
|
||||
}
|
||||
ver.to_string()
|
||||
}
|
||||
None => {
|
||||
// 尝试读取当前已激活的版本,作为 Inquire 的默认初始高亮项
|
||||
let current_version_path = home.join(version_file);
|
||||
let current_ver = fs::read_to_string(¤t_version_path)
|
||||
.map(|s| s.trim().to_string())
|
||||
.ok();
|
||||
|
||||
prompt_user_select(&available_versions, current_ver.as_deref())?
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 原子化持久化写入版本文件
|
||||
let dest_version_path = home.join(version_file);
|
||||
atomic_write(&dest_version_path, &selected_ver).map_err(UseError::Io)?;
|
||||
|
||||
println!("✔ 已成功将 '{}' 切换至版本: {}", tool_name, selected_ver);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 扫描 home 目录下的非隐藏有效子文件夹作为版本列表
|
||||
fn scan_versions(home: &Path) -> Result<Vec<String>, UseError> {
|
||||
let mut versions = Vec::new();
|
||||
|
||||
let entries = fs::read_dir(home).map_err(UseError::Io)?;
|
||||
for entry in entries.filter_map(|e| e.ok()) {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
// 排除以点开头的隐藏目录(如 .git, .cache)
|
||||
if !dir_name.starts_with('.') {
|
||||
versions.push(dir_name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自然排序版本列表
|
||||
versions.sort();
|
||||
Ok(versions)
|
||||
}
|
||||
|
||||
/// 唤起 Inquire 进行交互单选
|
||||
fn prompt_user_select(
|
||||
versions: &[String],
|
||||
default_version: Option<&str>,
|
||||
) -> Result<String, UseError> {
|
||||
let mut prompt = Select::new("请选择要激活的目标版本:", versions.to_vec());
|
||||
|
||||
// 如果能读取到当前版本,默认选中该项
|
||||
if let Some(def) = default_version {
|
||||
if let Some(idx) = versions.iter().position(|v| v == def) {
|
||||
prompt.starting_cursor = idx;
|
||||
}
|
||||
}
|
||||
|
||||
prompt.prompt().map_err(|_| UseError::UserCancelled)
|
||||
}
|
||||
|
||||
/// 使用临时文件同目录重命名实现“原子写入”
|
||||
///
|
||||
/// 杜绝中途掉电、杀死进程导致的空文件损坏
|
||||
fn atomic_write(dest_path: &Path, content: &str) -> io::Result<()> {
|
||||
let parent_dir = dest_path
|
||||
.parent()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Parent dir not found"))?;
|
||||
|
||||
// 必须在目标文件的同级目录下创建临时文件,跨盘符/分区会导致原子重命名 (Rename) 失败
|
||||
let mut temp_file = NamedTempFile::new_in(parent_dir)?;
|
||||
|
||||
// 写入纯净内容,并强制落盘刷新 (Flush)
|
||||
temp_file.write_all(content.trim().as_bytes())?;
|
||||
temp_file.write_all(b"\n")?; // 末尾留标准换行符
|
||||
temp_file.as_file().sync_all()?;
|
||||
|
||||
// 核心操作:原子覆盖(在 Windows 上内部会调用 ReplaceFileW,在 Unix 则是 rename)
|
||||
temp_file.persist(dest_path).map_err(|e| e.error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,8 +2,10 @@ use crate::cli;
|
||||
use crate::commands::{builtin, dynamic::MrCommand};
|
||||
use crate::error::ExecuteError;
|
||||
use crate::plugins::register_mr_command;
|
||||
use crate::commands::switch_version;
|
||||
use anyhow::{Context, Result};
|
||||
use mirror_core::{Layout, LuaRuntime, register_basics};
|
||||
use mirror_core::MirrorRegistry;
|
||||
// 成功的标准退出码
|
||||
pub const EXIT_SUCCESS: i32 = 0;
|
||||
|
||||
@@ -14,7 +16,7 @@ pub const EXIT_COMMAND_NOT_FOUND: i32 = 12; // 找不到指定的子命令
|
||||
pub const EXIT_BUILTIN_ERROR: i32 = 13; // 原生命令(如 log)执行失败
|
||||
|
||||
pub fn run(layout: &Layout) -> Result<(), ExecuteError> {
|
||||
let registry_file = layout.base_dir.join("commands").join("commands.lua");
|
||||
|
||||
|
||||
let runtime = LuaRuntime::builder()
|
||||
.with(|lua| register_basics(lua, layout))
|
||||
@@ -22,6 +24,7 @@ pub fn run(layout: &Layout) -> Result<(), ExecuteError> {
|
||||
.build()
|
||||
.map_err(|e| ExecuteError::new(EXIT_RUNTIME_ERROR, e))?;
|
||||
|
||||
let registry_file = layout.base_dir.join("commands").join("commands.lua");
|
||||
let commands: MrCommand = runtime
|
||||
.eval_script(®istry_file)
|
||||
.with_context(|| format!("加载命令配置失败: {}", registry_file.display()))
|
||||
@@ -31,6 +34,24 @@ pub fn run(layout: &Layout) -> Result<(), ExecuteError> {
|
||||
let matches = app.get_matches();
|
||||
|
||||
match matches.subcommand() {
|
||||
|
||||
Some(("use", sub_matches)) => {
|
||||
let tool = sub_matches.get_one::<String>("tool").unwrap();
|
||||
let version = sub_matches.get_one::<String>("version").map(|s| s.as_str());
|
||||
|
||||
// 惰性加载 mirror.lua:按需获取 MirrorRegistry
|
||||
let mirror_file = layout.base_dir.join("mirror.lua");
|
||||
let registry: MirrorRegistry = runtime
|
||||
.eval_script(&mirror_file)
|
||||
.with_context(|| format!("解析主配置失败: {}", mirror_file.display()))
|
||||
.map_err(|e| ExecuteError::new(EXIT_CONFIG_ERROR, e))?;
|
||||
|
||||
if let Err(e) = switch_version(®istry, tool, version) {
|
||||
eprintln!("[mr] 切换失败: {:?}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 分支 A: 原生 Rust 实现的命令
|
||||
Some(("log", sub_m)) => {
|
||||
builtin::log_handle(&layout, sub_m)
|
||||
@@ -58,7 +79,7 @@ pub fn run(layout: &Layout) -> Result<(), ExecuteError> {
|
||||
}
|
||||
|
||||
// 分支 C: 未输入子命令(通常已被 Clap 的 arg_required_else_help 拦截)
|
||||
None => {}
|
||||
None => {}//unreachable!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use mirror_core::{Error, Result};
|
||||
use mirror_core::Result;
|
||||
use mlua::{Lua, Table, Value};
|
||||
|
||||
/// 仅存在于 CLI 侧的 DSL 扩展函数
|
||||
|
||||
Reference in New Issue
Block a user