summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-01-24 07:03:40 +0800
committer魏曹先生 <1992414357@qq.com>2026-01-24 07:03:40 +0800
commit3fa56b997b44caba630a5dbc67687923978c5c7d (patch)
treeb1f62fe31f88fb666e810637df8ca7a60265a3ee
parent92f2cdd3dfa378cfcfb9085fedd601b27e499ee7 (diff)
Add command aliases, error handling improvements, and flag aliases
- Add aliases for status command: sign, sheet, sheet.add, drop, drop.cat - Improve error handling with detailed localized messages for prepare, execute, and render phases - Add flag aliases: -L for --lang and -R for --renderer - Implement similar command suggestions using Levenshtein distance - Fix command matching logic to avoid ambiguous prefix issues
-rw-r--r--.cargo/registry.toml20
-rw-r--r--resources/locales/jvn/en.yml86
-rw-r--r--resources/locales/jvn/zh-CN.yml84
-rw-r--r--src/bin/jvn.rs172
-rw-r--r--src/systems/cmd/processer.rs17
-rw-r--r--src/utils.rs1
-rw-r--r--src/utils/levenshtein_distance.rs34
7 files changed, 364 insertions, 50 deletions
diff --git a/.cargo/registry.toml b/.cargo/registry.toml
index 3ebfa17..a957b9b 100644
--- a/.cargo/registry.toml
+++ b/.cargo/registry.toml
@@ -9,6 +9,26 @@
# node = "name"
# type = "your_command::JVUnknownCommand"
+[cmd.sign]
+node = "sign"
+type = "cmds::status::JVStatusCommand"
+
+[cmd.sheet]
+node = "sheet"
+type = "cmds::status::JVStatusCommand"
+
+[cmd.sheet_add]
+node = "sheet.add"
+type = "cmds::status::JVStatusCommand"
+
+[cmd.drop]
+node = "drop"
+type = "cmds::status::JVStatusCommand"
+
+[cmd.drop_cat]
+node = "drop.cat"
+type = "cmds::status::JVStatusCommand"
+
#################
### Renderers ###
#################
diff --git a/resources/locales/jvn/en.yml b/resources/locales/jvn/en.yml
index 525d8c5..d7dead3 100644
--- a/resources/locales/jvn/en.yml
+++ b/resources/locales/jvn/en.yml
@@ -1,19 +1,16 @@
-process_error:
- prepare_error: |
- [[YELLOW]]Preparation Phase Error:[[/]]
- %{error}
- execute_error: |
- [[RED]]Execution Phase Error:[[/]]
- %{error}
- render_error: |
- [[YELLOW]]Rendering Phase Error:[[/]]
- %{error}
-
- Tip: If you need to ignore error output,
- please append the `--no-error-logs` parameter to the command.
+help: |
+ NO
+process_error:
no_matching_command: |
No matching command found!
+ Use `jv -h` to get help
+
+ no_matching_command_but_similar: |
+ No matching command found, but similar commands were found:
+ jv %{similars}
+
+ Use `jv -h` to get help
ambiguous_command: |
Multiple commands found, unable to determine which one you want:
@@ -33,3 +30,66 @@ process_error:
other: |
%{error}
+
+prepare_error:
+ io: |
+ I/O error in preparation phase!
+ Error: %{error}
+
+ error: |
+ Unknown error in preparation phase!
+ Error: %{error}
+
+ local_workspace_not_found: |
+ Local workspace not found!
+ Create or enter a workspace directory first.
+
+ local_config_not_found: |
+ Failed to read workspace config. File may not exist or format mismatch.
+ Use `jv update` and try again.
+
+ latest_info_not_found: |
+ Unable to read latest upstream info!
+ Use `jv update` and try again.
+
+ latest_file_data_not_exist: |
+ Unable to read latest file info for member `%{member_id}`!
+ Use `jv update` and try again.
+
+ cached_sheet_not_found: |
+ Unable to read cached upstream sheet `%{sheet_name}`!
+ Use `jv update` and try again.
+
+ local_sheet_not_found: |
+ Unable to read local sheet `%{sheet_name}` for member `%{member_id}`.
+
+ local_status_analyze_failed: |
+ Failed to analyze local workspace!
+
+ no_sheet_in_use: |
+ No sheet in use. Use `jv use <structure_sheet_name>`.
+
+execute_error:
+ io: |
+ I/O error in execution phase!
+ Error: %{error}
+
+ error: |
+ Error in execution phase!
+ Error: %{error}
+
+render_error:
+ io: |
+ I/O error in rendering phase!
+ Error: %{error}
+
+ error: |
+ Error in rendering phase!
+ Error: %{error}
+
+ serialize_failed: |
+ Data serialization error!
+ Error: %{error}
+
+ renderer_not_found: |
+ Renderer `%{renderer_name}` not found!
diff --git a/resources/locales/jvn/zh-CN.yml b/resources/locales/jvn/zh-CN.yml
index ffb033c..194bddd 100644
--- a/resources/locales/jvn/zh-CN.yml
+++ b/resources/locales/jvn/zh-CN.yml
@@ -1,18 +1,15 @@
+help: |
+ NO
+
process_error:
- prepare_error: |
- [[YELLOW]]准备阶段错误:[[/]]
- %{error}
- execute_error: |
- [[RED]]执行阶段错误:[[/]]
- %{error}
- render_error: |
- [[YELLOW]]渲染阶段错误:[[/]]
- %{error}
+ no_matching_command: |
+ 无法匹配该命令,使用 `jv -h` 查看帮助
- 提示:若您需要忽略错误输出,请在命令后追加 `--no-error-logs` 参数
+ no_matching_command_but_similar: |
+ 无法找到匹配的命令,但找到相似命令:
+ jv %{similars}
- no_matching_command: |
- 无法找到匹配的命令!
+ 使用 `jv -h` 查看帮助
ambiguous_command: |
找到多个命令,无法确定您想要哪一个:
@@ -32,3 +29,66 @@ process_error:
other: |
%{error}
+
+prepare_error:
+ io: |
+ 命令在准备阶段发生了 I/O 错误!
+ 错误信息:%{error}
+
+ error: |
+ 命令在准备阶段发生未知错误!
+ 错误信息:%{error}
+
+ local_workspace_not_found: |
+ 无法找到本地工作区!
+ 请先创建或进入本地工作区目录
+
+ local_config_not_found: |
+ 读取本地工作区配置文件失败,它可能不存在或格式不匹配
+ 请使用 `jv update` 更新工作区信息后再尝试
+
+ latest_info_not_found: |
+ 无法找到或读取最新上游信息!
+ 请使用 `jv update` 更新工作区信息后再尝试
+
+ latest_file_data_not_exist: |
+ 无法找到或读取成员 `%{member_id}` 的最新文件信息!
+ 请使用 `jv update` 更新工作区信息后再尝试
+
+ cached_sheet_not_found: |
+ 无法找到或读取上游结构表 `%{sheet_name}` 的缓存信息!
+ 请使用 `jv update` 更新工作区信息后再尝试
+
+ local_sheet_not_found: |
+ 无法找到或读取成员 `%{member_id}` 的本地结构表 `%{sheet_name}`
+
+ local_status_analyze_failed: |
+ 分析本地工作区失败!
+
+ no_sheet_in_use: |
+ 当前没有在使用表,请使用 `jv use <结构表名称>` 使用一张结构表
+
+execute_error:
+ io: |
+ 命令在运行阶段发生了 I/O 错误!
+ 错误信息:%{error}
+
+ error: |
+ 命令在运行阶段发生错误!
+ 错误信息:%{error}
+
+render_error:
+ io: |
+ 命令在渲染阶段发生了 I/O 错误!
+ 错误信息:%{error}
+
+ error: |
+ 命令在渲染阶段发生错误!
+ 错误信息:%{error}
+
+ serialize_failed: |
+ 数据在序列化时发生了错误!
+ 错误信息:%{error}
+
+ renderer_not_found: |
+ 无法找到渲染器 `%{renderer_name}`!
diff --git a/src/bin/jvn.rs b/src/bin/jvn.rs
index 25bf219..fda0c26 100644
--- a/src/bin/jvn.rs
+++ b/src/bin/jvn.rs
@@ -1,5 +1,10 @@
+use std::process::exit;
+
+use just_enough_vcs_cli::systems::cmd::_registry::jv_cmd_nodes;
use just_enough_vcs_cli::systems::cmd::cmd_system::JVCommandContext;
+use just_enough_vcs_cli::systems::cmd::errors::{CmdExecuteError, CmdPrepareError, CmdRenderError};
use just_enough_vcs_cli::utils::display::md;
+use just_enough_vcs_cli::utils::levenshtein_distance::levenshtein_distance;
use just_enough_vcs_cli::{
systems::cmd::{errors::CmdProcessError, processer::jv_cmd_process},
utils::env::current_locales,
@@ -46,23 +51,35 @@ async fn main() {
let mut args: Vec<String> = std::env::args().skip(1).collect();
// Init i18n
- let lang = special_argument!(args, "--lang").unwrap_or(current_locales());
+ let lang = special_argument!(args, "--lang")
+ .or_else(|| special_argument!(args, "-L"))
+ .unwrap_or(current_locales());
set_locale(&lang);
- // Init colored
- #[cfg(windows)]
- colored::control::set_virtual_terminal(true).unwrap();
-
- let renderer_override = special_argument!(args, "--renderer").unwrap_or("default".to_string());
+ // Renderer
+ let renderer_override = special_argument!(args, "--renderer")
+ .or_else(|| special_argument!(args, "-R"))
+ .unwrap_or("default".to_string());
+ // Other flags
let no_error_logs = special_flag!(args, "--no-error-logs");
let quiet = special_flag!(args, "--quiet") || special_flag!(args, "-q");
let help = special_flag!(args, "--help") || special_flag!(args, "-h");
let confirmed = special_flag!(args, "--confirm") || special_flag!(args, "-C");
+ // Init colored
+ #[cfg(windows)]
+ colored::control::set_virtual_terminal(true).unwrap();
+
+ // Handle help when no arguments provided
+ if args.len() < 1 && help {
+ eprintln!("{}", md(t!("help")));
+ exit(1);
+ }
+
// Process commands
let render_result = match jv_cmd_process(
- args,
+ &args,
JVCommandContext { help, confirmed },
renderer_override,
)
@@ -73,22 +90,13 @@ async fn main() {
if !no_error_logs {
match e {
CmdProcessError::Prepare(cmd_prepare_error) => {
- eprintln!(
- "{}",
- md(t!("process_error.prepare_error", error = cmd_prepare_error))
- );
+ handle_prepare_error(cmd_prepare_error);
}
CmdProcessError::Execute(cmd_execute_error) => {
- eprintln!(
- "{}",
- md(t!("process_error.execute_error", error = cmd_execute_error))
- );
+ handle_execute_error(cmd_execute_error);
}
CmdProcessError::Render(cmd_render_error) => {
- eprintln!(
- "{}",
- md(t!("process_error.render_error", error = cmd_render_error))
- );
+ handle_render_error(cmd_render_error);
}
CmdProcessError::Error(error) => {
eprintln!("{}", md(t!("process_error.other", error = error)));
@@ -97,7 +105,7 @@ async fn main() {
eprintln!("{}", md(t!("process_error.no_node_found", node = node)));
}
CmdProcessError::NoMatchingCommand => {
- eprintln!("{}", md(t!("process_error.no_matching_command")));
+ handle_no_matching_command_error(args);
}
CmdProcessError::AmbiguousCommand(nodes) => {
let nodes_list = nodes
@@ -129,3 +137,127 @@ async fn main() {
print!("{}", render_result);
}
}
+
+fn handle_no_matching_command_error(args: Vec<String>) {
+ let mut similar_nodes: Vec<String> = Vec::new();
+ for node in jv_cmd_nodes() {
+ let node_len = node.split(" ").collect::<Vec<&str>>().iter().len();
+ let args_len = args.len();
+ if args_len < node_len {
+ continue;
+ }
+ let args_str = args[..node_len].join(" ");
+ let distance = levenshtein_distance(args_str.as_str(), node.as_str());
+ if distance <= 2 {
+ similar_nodes.push(node);
+ }
+ }
+ if similar_nodes.len() < 1 {
+ eprintln!("{}", md(t!("process_error.no_matching_command")));
+ } else {
+ eprintln!(
+ "{}",
+ md(t!(
+ "process_error.no_matching_command_but_similar",
+ similars = similar_nodes[0]
+ ))
+ );
+ }
+}
+
+fn handle_prepare_error(cmd_prepare_error: CmdPrepareError) {
+ match cmd_prepare_error {
+ CmdPrepareError::Io(error) => {
+ eprintln!("{}", md(t!("prepare_error.io", error = error.to_string())));
+ }
+ CmdPrepareError::Error(msg) => {
+ eprintln!("{}", md(t!("prepare_error.error", error = msg)));
+ }
+ CmdPrepareError::LocalWorkspaceNotFound => {
+ eprintln!("{}", md(t!("prepare_error.local_workspace_not_found")));
+ }
+ CmdPrepareError::LocalConfigNotFound => {
+ eprintln!("{}", md(t!("prepare_error.local_config_not_found")));
+ }
+ CmdPrepareError::LatestInfoNotFound => {
+ eprintln!("{}", md(t!("prepare_error.latest_info_not_found")));
+ }
+ CmdPrepareError::LatestFileDataNotExist(member_id) => {
+ eprintln!(
+ "{}",
+ md(t!(
+ "prepare_error.latest_file_data_not_exist",
+ member_id = member_id
+ ))
+ );
+ }
+ CmdPrepareError::CachedSheetNotFound(sheet_name) => {
+ eprintln!(
+ "{}",
+ md(t!(
+ "prepare_error.cached_sheet_not_found",
+ sheet_name = sheet_name
+ ))
+ );
+ }
+ CmdPrepareError::LocalSheetNotFound(member_id, sheet_name) => {
+ eprintln!(
+ "{}",
+ md(t!(
+ "prepare_error.local_sheet_not_found",
+ member_id = member_id,
+ sheet_name = sheet_name
+ ))
+ );
+ }
+ CmdPrepareError::LocalStatusAnalyzeFailed => {
+ eprintln!("{}", md(t!("prepare_error.local_status_analyze_failed")));
+ }
+ CmdPrepareError::NoSheetInUse => {
+ eprintln!("{}", md(t!("prepare_error.no_sheet_in_use")));
+ }
+ }
+}
+
+fn handle_execute_error(cmd_execute_error: CmdExecuteError) {
+ match cmd_execute_error {
+ CmdExecuteError::Io(error) => {
+ eprintln!("{}", md(t!("execute_error.io", error = error.to_string())));
+ }
+ CmdExecuteError::Prepare(cmd_prepare_error) => handle_prepare_error(cmd_prepare_error),
+ CmdExecuteError::Error(msg) => {
+ eprintln!("{}", md(t!("execute_error.error", error = msg)));
+ }
+ }
+}
+
+fn handle_render_error(cmd_render_error: CmdRenderError) {
+ match cmd_render_error {
+ CmdRenderError::Io(error) => {
+ eprintln!("{}", md(t!("render_error.io", error = error.to_string())));
+ }
+ CmdRenderError::Prepare(cmd_prepare_error) => handle_prepare_error(cmd_prepare_error),
+ CmdRenderError::Execute(cmd_execute_error) => handle_execute_error(cmd_execute_error),
+ CmdRenderError::Error(msg) => {
+ eprintln!("{}", md(t!("render_error.error", error = msg)));
+ }
+ CmdRenderError::SerializeFailed(error) => {
+ eprintln!(
+ "{}",
+ md(t!(
+ "render_error.serialize_failed",
+ error = error.to_string()
+ ))
+ );
+ }
+ CmdRenderError::RendererNotFound(renderer_name) => {
+ eprintln!(
+ "{}",
+ md(t!(
+ "render_error.renderer_not_found",
+ renderer_name = renderer_name
+ ))
+ );
+ }
+ }
+}
diff --git a/src/systems/cmd/processer.rs b/src/systems/cmd/processer.rs
index d357e44..7c464a2 100644
--- a/src/systems/cmd/processer.rs
+++ b/src/systems/cmd/processer.rs
@@ -4,17 +4,24 @@ use crate::systems::cmd::errors::CmdProcessError;
use crate::systems::cmd::renderer::JVRenderResult;
pub async fn jv_cmd_process(
- args: Vec<String>,
+ args: &Vec<String>,
ctx: JVCommandContext,
renderer_override: String,
) -> Result<JVRenderResult, CmdProcessError> {
let nodes = jv_cmd_nodes();
- let command = args.join(" ");
- // Find nodes that match the beginning of the command
+ // Why add a space?
+ // Add a space at the end of the command string for precise command prefix matching.
+ // For example: when the input command is "bananas", if there are two commands "banana" and "bananas",
+ // without a space it might incorrectly match "banana" (because "bananas".starts_with("banana") is true).
+ // After adding a space, "bananas " will not match "banana ", thus avoiding ambiguity caused by overlapping prefixes.
+ let command = format!("{} ", args.join(" "));
+
+ // Find all nodes that match the command prefix
let matching_nodes: Vec<&String> = nodes
.iter()
- .filter(|node| command.starts_with(node.as_str()))
+ // Also add a space to the node string to ensure consistent matching logic
+ .filter(|node| command.starts_with(&format!("{} ", node)))
.collect();
match matching_nodes.len() {
@@ -25,7 +32,7 @@ pub async fn jv_cmd_process(
1 => {
let matched_prefix = matching_nodes[0];
let prefix_len = matched_prefix.split_whitespace().count();
- let trimmed_args: Vec<String> = args.into_iter().skip(prefix_len).collect();
+ let trimmed_args: Vec<String> = args.into_iter().cloned().skip(prefix_len).collect();
return jv_cmd_process_node(matched_prefix, trimmed_args, ctx, renderer_override).await;
}
_ => {
diff --git a/src/utils.rs b/src/utils.rs
index 8c2e306..7d3cb5e 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -3,6 +3,7 @@ pub mod env;
pub mod fs;
pub mod globber;
pub mod input;
+pub mod levenshtein_distance;
pub mod logger;
pub mod push_version;
pub mod socket_addr_helper;
diff --git a/src/utils/levenshtein_distance.rs b/src/utils/levenshtein_distance.rs
new file mode 100644
index 0000000..6bdb7e7
--- /dev/null
+++ b/src/utils/levenshtein_distance.rs
@@ -0,0 +1,34 @@
+use std::cmp::min;
+
+pub fn levenshtein_distance(a: &str, b: &str) -> usize {
+ let a_chars: Vec<char> = a.chars().collect();
+ let b_chars: Vec<char> = b.chars().collect();
+ let a_len = a_chars.len();
+ let b_len = b_chars.len();
+
+ if a_len == 0 {
+ return b_len;
+ }
+ if b_len == 0 {
+ return a_len;
+ }
+
+ let mut dp = vec![vec![0; b_len + 1]; a_len + 1];
+
+ for (i, row) in dp.iter_mut().enumerate() {
+ row[0] = i;
+ }
+
+ for (j, cell) in dp[0].iter_mut().enumerate() {
+ *cell = j;
+ }
+
+ for (i, a_char) in a_chars.iter().enumerate() {
+ for (j, b_char) in b_chars.iter().enumerate() {
+ let cost = if a_char == b_char { 0 } else { 1 };
+ dp[i + 1][j + 1] = min(dp[i][j + 1] + 1, min(dp[i + 1][j] + 1, dp[i][j] + cost));
+ }
+ }
+
+ dp[a_len][b_len]
+}