From 38da7b4bc02b3a2815e188fb2cc03610ab5b8696 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Tue, 21 Oct 2025 21:49:37 +0800 Subject: Completed jv binary --- src/bin/jv.rs | 274 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- src/bin/jvv.rs | 44 +++++---- 2 files changed, 299 insertions(+), 19 deletions(-) diff --git a/src/bin/jv.rs b/src/bin/jv.rs index a8b0a1f..9530203 100644 --- a/src/bin/jv.rs +++ b/src/bin/jv.rs @@ -1,7 +1,279 @@ +use clap::{Parser, Subcommand, arg, command}; +use just_enough_vcs_cli::utils::{lang_selector::current_locales, md_colored::md}; +use rust_i18n::{set_locale, t}; + // Import i18n files rust_i18n::i18n!("locales", fallback = "en"); +#[derive(Parser, Debug)] +#[command( + disable_help_flag = true, + disable_version_flag = true, + disable_help_subcommand = true, + help_template = "{all-args}" +)] + +struct JustEnoughVcsWorkspace { + #[command(subcommand)] + command: JustEnoughVcsWorkspaceCommand, +} + +#[derive(Subcommand, Debug)] +enum JustEnoughVcsWorkspaceCommand { + // Member management + /// Manage your local accounts + #[command(subcommand)] + Account(AccountManage), + + /// Create an empty workspace + Create(CreateWorkspaceArgs), + + /// Create an empty workspace in the current directory + Init(InitWorkspaceArgs), + + /// Get workspace information in the current directory + #[command(alias = "h")] + Here(HereArgs), + + // Sheet management + /// Manage sheets in the workspace + #[command(subcommand)] + Sheet(SheetManage), + + // File management + /// Track files to the upstream vault + /// First track - Create and upload the "First Version", then hold them + /// Subsequent tracks - Update files with new versions + #[command(alias = "t")] + Track(TrackFileArgs), + + /// Hold files for editing + #[command(alias = "hd")] + Hold(HoldFileArgs), + + /// Throw files, and release edit rights + #[command(alias = "tr")] + Throw(ThrowFileArgs), + + /// Move or rename files safely + #[command(alias = "mv")] + Move(MoveFileArgs), + + /// Export files to other worksheet + #[command(alias = "out")] + Export(ExportFileArgs), + + /// Import files from reference sheet or import area + #[command(alias = "in")] + Import(ImportFileArgs), + + // Connection management + /// Direct to an upstream vault and stain this workspace + Direct(DirectArgs), + + /// DANGER ZONE : Unstain this workspace + Unstain(UnstainArgs), + + // Other + /// Query built-in documentation + Docs(DocsArgs), +} + +#[derive(Subcommand, Debug)] +enum AccountManage { + /// Show help information + #[command(alias = "--help", alias = "-h")] + Help, +} + +#[derive(Subcommand, Debug)] +enum SheetManage { + /// Show help information + #[command(alias = "--help", alias = "-h")] + Help, +} + +#[derive(Parser, Debug)] +struct CreateWorkspaceArgs { + /// Show help information + #[arg(short, long)] + help: bool, +} + +#[derive(Parser, Debug)] +struct InitWorkspaceArgs { + /// Show help information + #[arg(short, long)] + help: bool, +} + +#[derive(Parser, Debug)] +struct HereArgs { + /// Show help information + #[arg(short, long)] + help: bool, +} + +#[derive(Parser, Debug)] +struct TrackFileArgs { + /// Show help information + #[arg(short, long)] + help: bool, +} + +#[derive(Parser, Debug)] +struct HoldFileArgs { + /// Show help information + #[arg(short, long)] + help: bool, +} + +#[derive(Parser, Debug)] +struct ThrowFileArgs { + /// Show help information + #[arg(short, long)] + help: bool, +} + +#[derive(Parser, Debug)] +struct MoveFileArgs { + /// Show help information + #[arg(short, long)] + help: bool, +} + +#[derive(Parser, Debug)] +struct ExportFileArgs { + /// Show help information + #[arg(short, long)] + help: bool, +} + +#[derive(Parser, Debug)] +struct ImportFileArgs { + /// Show help information + #[arg(short, long)] + help: bool, +} + +#[derive(Parser, Debug)] +struct DirectArgs { + /// Show help information + #[arg(short, long)] + help: bool, +} + +#[derive(Parser, Debug)] +struct UnstainArgs { + /// Show help information + #[arg(short, long)] + help: bool, +} + +#[derive(Parser, Debug)] +struct DocsArgs { + /// Show help information + #[arg(short, long)] + help: bool, +} + #[tokio::main] async fn main() { - println!("Hello, World!") + // Init i18n + set_locale(¤t_locales()); + + // Init colored + #[cfg(windows)] + colored::control::set_virtual_terminal(true).unwrap(); + + let Ok(parser) = JustEnoughVcsWorkspace::try_parse() else { + println!("{}", md(t!("jv.help"))); + return; + }; + + match parser.command { + JustEnoughVcsWorkspaceCommand::Account(account_manage) => match account_manage { + AccountManage::Help => { + println!("{}", md(t!("jv.account"))); + } + }, + JustEnoughVcsWorkspaceCommand::Create(create_workspace_args) => { + if create_workspace_args.help { + println!("{}", md(t!("jv.create"))); + return; + } + } + JustEnoughVcsWorkspaceCommand::Init(init_workspace_args) => { + if init_workspace_args.help { + println!("{}", md(t!("jv.init"))); + return; + } + } + JustEnoughVcsWorkspaceCommand::Here(here_args) => { + if here_args.help { + println!("{}", md(t!("jv.here"))); + return; + } + } + JustEnoughVcsWorkspaceCommand::Sheet(sheet_manage) => match sheet_manage { + SheetManage::Help => { + println!("{}", md(t!("jv.sheet"))); + return; + } + }, + JustEnoughVcsWorkspaceCommand::Track(track_file_args) => { + if track_file_args.help { + println!("{}", md(t!("jv.track"))); + return; + } + } + JustEnoughVcsWorkspaceCommand::Hold(hold_file_args) => { + if hold_file_args.help { + println!("{}", md(t!("jv.hold"))); + return; + } + } + JustEnoughVcsWorkspaceCommand::Throw(throw_file_args) => { + if throw_file_args.help { + println!("{}", md(t!("jv.throw"))); + return; + } + } + JustEnoughVcsWorkspaceCommand::Move(move_file_args) => { + if move_file_args.help { + println!("{}", md(t!("jv.move"))); + return; + } + } + JustEnoughVcsWorkspaceCommand::Export(export_file_args) => { + if export_file_args.help { + println!("{}", md(t!("jv.export"))); + return; + } + } + JustEnoughVcsWorkspaceCommand::Import(import_file_args) => { + if import_file_args.help { + println!("{}", md(t!("jv.import"))); + return; + } + } + JustEnoughVcsWorkspaceCommand::Direct(direct_args) => { + if direct_args.help { + println!("{}", md(t!("jv.direct"))); + return; + } + } + JustEnoughVcsWorkspaceCommand::Unstain(unstain_args) => { + if unstain_args.help { + println!("{}", md(t!("jv.unstain"))); + return; + } + } + JustEnoughVcsWorkspaceCommand::Docs(docs_args) => { + if docs_args.help { + println!("{}", md(t!("jv.docs"))); + return; + } + } + } } diff --git a/src/bin/jvv.rs b/src/bin/jvv.rs index 1dd2275..50dc3b3 100644 --- a/src/bin/jvv.rs +++ b/src/bin/jvv.rs @@ -8,7 +8,7 @@ use just_enough_vcs::{ }, vcs::{ connection::action_service::server_entry, - constants::{SERVER_FILE_VAULT, SERVER_FILE_VF_META}, + constants::SERVER_FILE_VAULT, current::current_vault_path, data::{ member::Member, @@ -35,11 +35,11 @@ rust_i18n::i18n!("locales/help_docs", fallback = "en"); )] struct JustEnoughVcsVault { #[command(subcommand)] - command: JustEnoughVcsCommand, + command: JustEnoughVcsVaultCommand, } #[derive(Subcommand, Debug)] -enum JustEnoughVcsCommand { +enum JustEnoughVcsVaultCommand { /// Get vault info in the current directory Here(HereArgs), @@ -161,28 +161,28 @@ async fn main() { }; match parser.command { - JustEnoughVcsCommand::Here(here_args) => { + JustEnoughVcsVaultCommand::Here(here_args) => { if here_args.help { println!("{}", md(t!("jvv.here"))); return; } jvv_here(here_args).await; } - JustEnoughVcsCommand::Create(create_vault_args) => { + JustEnoughVcsVaultCommand::Create(create_vault_args) => { if create_vault_args.help { println!("{}", md(t!("jvv.create"))); return; } jvv_create(create_vault_args).await; } - JustEnoughVcsCommand::Init(init_vault_args) => { + JustEnoughVcsVaultCommand::Init(init_vault_args) => { if init_vault_args.help { println!("{}", md(t!("jvv.init"))); return; } jvv_init(init_vault_args).await; } - JustEnoughVcsCommand::Member(member_manage) => { + JustEnoughVcsVaultCommand::Member(member_manage) => { let vault_cfg = VaultConfig::read() .await .unwrap_or_else(|_| panic!("{}", t!("jvv.fail.no_vault_here").trim().to_string())); @@ -226,7 +226,7 @@ async fn main() { } } } - JustEnoughVcsCommand::Service(service_manage) => match service_manage { + JustEnoughVcsVaultCommand::Service(service_manage) => match service_manage { ServiceManage::Listen(listen_args) => { if listen_args.help { println!("{}", md(t!("jvv.service"))); @@ -284,14 +284,8 @@ async fn jvv_here(_args: HereArgs) { while let Ok(Some(entry)) = entries.next_entry().await { if let Ok(metadata) = entry.metadata().await { if metadata.is_file() { - if entry - .file_name() - .to_string_lossy() - .ends_with(SERVER_FILE_VF_META) - { - num_vf += 1; - total_size += metadata.len(); - } + num_vf += 1; + total_size += metadata.len(); } } } @@ -332,6 +326,21 @@ async fn jvv_here(_args: HereArgs) { }; let num_ref_sheet_managed_files = ref_sheet.mapping().len(); + let total_size_str = if total_size < 1024 { + format!("{} B", total_size) + } else if total_size < 1024 * 1024 { + format!("{:.2} KB", total_size as f64 / 1024.0) + } else if total_size < 1024 * 1024 * 1024 { + format!("{:.2} MB", total_size as f64 / (1024.0 * 1024.0)) + } else if total_size < 1024 * 1024 * 1024 * 1024 { + format!("{:.2} GB", total_size as f64 / (1024.0 * 1024.0 * 1024.0)) + } else { + format!( + "{:.2} TB", + total_size as f64 / (1024.0 * 1024.0 * 1024.0 * 1024.0) + ) + }; + // Success println!( "{}", @@ -340,11 +349,10 @@ async fn jvv_here(_args: HereArgs) { name = vault_name, num_sheets = num_sheets, num_vf = num_vf, - total_size = total_size, num_mem = num_mem, num_pk = num_pk, num_ref_sheet_managed_files = num_ref_sheet_managed_files, - total_size_gb = (total_size as f64) / (1024.0 * 1024.0 * 1024.0) + total_size = total_size_str )) ) } -- cgit From c7a7785eda77e96a99c58291d08a091a5d8846eb Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Tue, 21 Oct 2025 21:48:23 +0800 Subject: Update locales files --- locales/help_docs/en.yml | 207 +++++++++++++++++++++++++++++++++++++++- locales/help_docs/zh-CN.yml | 227 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 429 insertions(+), 5 deletions(-) diff --git a/locales/help_docs/en.yml b/locales/help_docs/en.yml index df6ee2b..6092873 100644 --- a/locales/help_docs/en.yml +++ b/locales/help_docs/en.yml @@ -99,7 +99,7 @@ jvv: here: info: | Found vault *`%{name}`*! (%{num_mem} Members, %{num_pk} registered PubKeys) - Managing %{num_sheets} Sheets, %{num_vf} VirtualFiles, total %{total_size_gb} GB + Managing %{num_sheets} Sheets, %{num_vf} VirtualFiles, total %{total_size} **Tip**: To start the server, run jvv service listen @@ -123,3 +123,208 @@ jvv: service: listen: Listening for client connections in vault `%{path}` ... + +jv: + help: | + **JustEnoughVCS Local Workspace Commands** + This program connects to upstream vaults to synchronize and commit changes to local workspace files for collaborative work. + + **Common Aliases**: + jv u Download latest information, jv t Track files, jv mv -a Auto-move files, jv in/out Import or export files + + **Create Workspace**: + jv create - Create a workspace in the given directory name + jv init - Create a workspace in the current directory + + **Connect to Upstream Vault**: + jv direct - Direct your workspace to this vault + After specifying an upstream vault, your workspace will be *stained*, and you will not be able to connect to vaults with different identifiers + + jv unstain - Unstain your workspace (remove stain identifier), after which the workspace will be unrelated to any upstream vault + + **Account Management**: + jv account list - List all accounts on this computer and whether private keys are registered + jv account as - Switch current account + jv account add - Add an account to this computer + jv account remove - Delete this account + jv account mvkey - Move private key to specified account + + **Information Synchronization**: + jv update - Download latest information from upstream workspace + + **Sheet Operations**: + jv sheet list - List all sheets in the upstream workspace + jv sheet use - Use the specified sheet to start current work (automatically created if it doesn't exist) + jv sheet exit - Exit current work + + jv import - Import files from import area + jv import - Import files from reference sheet + jv export -m -n - Export specified file package to import area of other sheets + + **File Operations** + jv move - Safely rename files + jv move auto - Automatically handle local file moves or renames + jv track - Track and upload files to upstream vault + + **Query Built-in Documentation** + jv docs list - List all available documentation + jv docs - View content of specified documentation + or jv docs -e - Return temporary path of specific documentation for editor opening + **Example**: jv docs get-started -e | nano + + You can use jv --help to query more detailed help! + + **Tip**: If you need to understand JustEnoughVCS collaboration paradigms, use jv docs get-started + + account: | + **Manage Local Accounts** + **Usage**: + jv account list - List all accounts on this computer and whether private keys are registered + jv account as - Switch current account + jv account add - Add an account to this computer + jv account remove - Delete this account + jv account mvkey - Move private key to specified account + + Accounts are identity identifiers on the local computer, each account can be associated with different private keys. + You need to set up accounts before interacting with upstream vaults. + + sheet: | + **Manage File Sheets** + **Usage**: + jv sheet list - List all sheets in the upstream workspace + jv sheet use - Use the specified sheet to start current work (automatically created if it doesn't exist) + jv sheet exit - Exit current work + + Sheets are core concepts in JustEnoughVCS, each sheet represents an independent file collection. + You can switch work between different sheets, or export files from one sheet to another. + + create: | + **Create Workspace with Specified Name** + **Usage**: jv create + + **Example**: jv create my_workspace + Creates a directory named 'my_workspace' in the current directory and initializes a workspace inside it. + + Workspace is your local working environment for file synchronization and version control with upstream vaults. + + init: | + **Create Workspace in Current Directory** + **Usage**: jv init + + This command creates a workspace in the current directory, with the workspace name determined by the current directory name. + + If the current directory is not empty, this operation will fail. Please ensure you execute this command in an empty directory. + + here: | + **Query Directory Information Here** + **Usage**: jv here + + Displays detailed information about current directory files, including: + - File name, size, version number + - Current file holder + - Latest version commit information + + This is a quick way to understand the current state of your workspace directory. + + track: | + **Track Local Files** + **Usage**: jv track + + **Example**: jv track src/main.rs + First track - Create and upload the "First Version", then automatically hold them + Subsequent tracks - Update files with new versions + + Tracking files is the basic operation of version control, ensuring your changes can be synchronized to the upstream vault. + + hold: | + **Hold Files: Obtain File Edit Rights from Upstream Vault** + **Usage**: jv hold + + **Example**: jv hold src/lib.rs + When you need to edit a file, you must first hold the file's edit rights. + After holding a file, other collaborators will not be able to edit the same file simultaneously, avoiding conflicts. + + After editing, remember to track the file to save changes. + + throw: | + **Throw Files: Release File Edit Rights from Upstream Vault** + **Usage**: jv throw + + **Example**: jv throw src/config.rs + When you no longer need to edit a file, you can throw the file's edit rights. + After throwing, other collaborators can hold and edit the file. + + If you have made changes to the file but haven't tracked them, throwing will lose those changes. + + move: | + **Move Local Files** + **Usage**: + jv move - Safely rename or move files + jv move auto - Automatically handle local file moves or renames + + **Example**: + jv move old_name.txt new_name.txt + jv move src/old_dir/file.rs src/new_dir/file.rs + jv move auto + + Safe move operations preserve file version history, while auto-move detects and handles all renames. + + export: | + **Export Files to Import Area of Other Sheets** + **Usage**: jv export -m -n + + **Example**: jv export data.csv analytics -m "Export analysis data" -n "analysis_data" + This operation packages the specified files and sends them to the import area of the target sheet. + + Other collaborators can use the jv import command in the target sheet to import these files. + + import: | + **Import Files to Current Sheet** + **Usage**: + jv import - Import files from import area + jv import - Import files from reference sheet + + **Example**: + jv import Player_Import - From import area defined name + jv import ref@Data/Player.csv - From reference sheet path + + Import operation copies files from other sheets or import areas to the current workspace. + + direct: | + **Direct to Specified Upstream Vault and Stain This Workspace** + **Usage**: jv direct + + **Example**: jv direct your_vault.org + This operation connects the current workspace to the specified upstream vault and adds a stain identifier to the workspace. + + After staining, the workspace will only be able to interact with vaults of the specified identifier, ensuring data consistency. + + unstain: | + **Unstain This Workspace** + **Usage**: jv unstain + + **DANGER ZONE**: This operation removes the workspace's stain identifier, after which the workspace will be disconnected from the upstream vault. + + After unstaining, the workspace will no longer be associated with any specific vault and can be reconnected to other vaults. + However, please note that this may cause data synchronization issues, use with caution. + + update: | + **Download Latest Information from Upstream Vault** + **Usage**: jv update + + This operation synchronizes the latest file status, sheet information, and member information from the upstream vault. + + It is recommended to perform an update operation before starting work to ensure you have the latest working environment. + + docs: | + **Query Built-in Documentation** + **Usage**: + jv docs list - List all available documentation + jv docs - View content of specified documentation + jv docs -e - Return temporary path of specific documentation for editor opening + + **Example**: + jv docs get-started + jv docs collaboration -e | nano + + Built-in documentation includes JustEnoughVCS usage guides, collaboration paradigms, and best practices. diff --git a/locales/help_docs/zh-CN.yml b/locales/help_docs/zh-CN.yml index 792d9c8..2025cbe 100644 --- a/locales/help_docs/zh-CN.yml +++ b/locales/help_docs/zh-CN.yml @@ -18,11 +18,11 @@ jvv: **服务**: jvv service listen - 在当前库中运行服务端 - 另外,你可以使用 jvv <命令名称> --help 来查询更详细的帮助! + 另外,您可以使用 jvv <命令名称> --help 来查询更详细的帮助! here: | **显示此处库的信息** - **例如**:cd ./你需要查询的库/ && jvv here + **例如**:cd ./您需要查询的库/ && jvv here create: | **在给定的目录名称中创建库** @@ -55,7 +55,7 @@ jvv: **服务设置相关** **用法**: jvv service listen - 在当前库中启动服务器,以接受客户端连接 - 你可以使用 --no-log 来禁用日志输出 + 您可以使用 --no-log 来禁用日志输出 fail: jvcs: JustEnoughVCS 错误:%{err} @@ -97,7 +97,7 @@ jvv: here: info: | 找到库 *`%{name}`*!(%{num_mem} 成员,%{num_pk} 位已注册公钥) - 其中,管理着 %{num_sheets} 张表,%{num_vf} 个虚拟文件,总共 %{total_size_gb} GB + 其中,管理着 %{num_sheets} 张表,%{num_vf} 个虚拟文件,总共 %{total_size} **提示**:若要启动服务端,请运行 jvv service listen @@ -119,3 +119,222 @@ jvv: service: listen: 正在库 `%{path}` 监听来自客户端的连接 ... + +jv: + help: | + **JustEnoughVCS 本地工作区命令** + 该程序将连接至上游库,用以同步、提交本地工作区文件的变化,以供协同创作。 + + **常用别名**: + jv u 下载最新信息,jv t 追踪文件,jv mv -a 自动移动文件,jv in/out 导入或导出文件 + + **创建工作区**: + jv create <名称> - 在给定名称的目录创建工作区 + jv init - 在当前目录创建工作区 + + **指向上游库**: + jv direct <上游地址> - 将您的工作区指向该库 + 指定上游库后,您的工作区将被 *染色*,此后,您将无法连接至不同标识的库 + + jv unstain - 将您的工作区祛色(移除染色标识),此后该工作区将与上游库无关 + + **账户管理**: + jv account list - 列出该计算机所有的账户,以及是否注册私钥 + jv account as <账户名称> - 切换当前账户 + jv account add <账户名称> - 为当前计算机添加账户 + jv account remove <账户名称> - 删除该账户 + jv account mvkey <账户名称> <私钥文件> - 移动私钥到指定账户 + + **信息同步**: + jv update - 从上游工作区下载最新的信息 + + **表操作**: + jv sheet list - 列出上游工作区的所有表 + jv sheet use <表名称> - 使用指定的表开始当前工作(不存在则自动创建) + jv sheet exit - 退出当前工作 + + jv import <文件包名称> - 从导入区导入文件 + jv import <参照表中目录> - 从参照表导入文件 + jv export <文件> <表名称> -m <描述> -n <文件包名称> - 导出指定的文件包到其他表的导入区 + + **文件操作** + jv move <文件> <到> - 安全地重命名文件 + jv move auto - 自动处理本地文件的移动或重命名 + jv track <文件> - 追踪、上传文件到上游库 + + **查询内建文档** + jv docs list - 列出所有可用的文档 + jv docs <文档名称> - 查看指定文档的内容 + 或 jv docs <文档名称> -e - 返回特定文档的临时路径以供编辑器打开 + **例如**:jv docs get-started -e | nano + + 您可以使用 jv <命令名称> --help 来查询更详细的帮助! + + **提示**:若您需要了解 JustEnoughVCS 的协作范式,请使用 jv docs get-started + + account: | + **管理本地账户** + **用法**: + jv account list - 列出该计算机所有的账户,以及是否注册私钥 + jv account as <账户名称> - 切换当前账户 + jv account add <账户名称> - 为当前计算机添加账户 + jv account remove <账户名称> - 删除该账户 + jv account mvkey <账户名称> <私钥文件> - 移动私钥到指定账户 + + 账户是本地计算机上的身份标识,每个账户可以关联不同的私钥。 + 您需要先设置账户才能与上游库进行交互。 + + + sheet: | + **管理文件表** + **用法**: + jv sheet list - 列出上游工作区的所有表 + jv sheet use <表名称> - 使用指定的表开始当前工作(不存在则自动创建) + jv sheet exit - 退出当前工作 + + 表是 JustEnoughVCS 中的核心概念,每个表代表一个独立的文件集合。 + 您可以在不同的表之间切换工作,或者将文件从一个表导出到另一个表。 + + + create: | + **创建指定名称的工作区** + **用法**:jv create <工作区名称> + + **例如**:jv create my_workspace + 上述操作会在当前目录创建名为 my_workspace 的目录,并在其中初始化工作区。 + + 工作区是您本地的工作环境,用于与上游库进行文件同步和版本控制。 + + + init: | + **在此目录创建工作区** + **用法**:jv init + + 该命令会在当前所在的目录创建工作区,工作区名称由当前所在目录名称决定。 + + 如果当前目录不为空,该操作将会失败。请确保在空目录中执行此命令。 + + + here: | + **查询此处目录信息** + **用法**:jv here + + 显示当前目录文件的详细信息,包括: + - 文件名称、大小、版本号 + - 文件当前的持有人 + - 文件最新版本的提交信息 + + 这是了解当前工作区目录状态的快速方式。 + + + track: | + **追踪本地的文件** + **用法**:jv track <文件路径> + + **例如**:jv track src/main.rs + 第一次追踪文件时,会创建并上传 “第一版本”,然后自动持有该文件的编辑权。 + 后续追踪同一文件时,会更新文件的新版本。 + + 追踪文件是版本控制的基础操作,确保您的更改能够同步到上游库。 + + + hold: | + **拿取文件:从上游库获得该文件的编辑权** + **用法**:jv hold <文件路径> + + **例如**:jv hold src/lib.rs + 当您需要编辑某个文件时,必须先持有该文件的编辑权。 + 持有文件后,其他协作者将无法同时编辑该文件,避免冲突。 + + 编辑完成后,请记得追踪文件以保存更改。 + + + throw: | + **丢弃文件:从上游库放弃该文件的编辑权** + **用法**:jv throw <文件路径> + + **例如**:jv throw src/config.rs + 当您不再需要编辑某个文件时,可以丢弃该文件的编辑权。 + 丢弃后,其他协作者就可以持有并编辑该文件。 + + 如果您对文件进行了更改但尚未追踪,丢弃操作会丢失这些更改。 + + + move: | + **移动本地文件** + **用法**: + jv move <源文件> <目标位置> - 安全地重命名或移动文件 + jv move auto - 自动处理本地文件的移动或重命名 + + **例如**: + jv move old_name.txt new_name.txt + jv move src/old_dir/file.rs src/new_dir/file.rs + jv move auto + + 安全移动操作会保持文件的版本历史,而自动移动会检测并处理所有重命名。 + + + export: | + **将文件导出至其他表的待导入区** + **用法**:jv export <文件> <目标表> -m <描述> -n <文件包名称> + + **例如**:jv export data.csv analytics -m "导出分析数据" -n "analysis_data" + 该操作会将指定的文件打包并发送到目标表的导入区。 + + 其他协作者可以在目标表中使用 jv import 命令来导入这些文件。 + + + import: | + **导入文件到当前表** + **用法**: + jv import <文件包名称> - 从导入区导入文件 + jv import <参照表中目录> - 从参照表导入文件 + + **例如**: + jv import Player_Import - 来自导入区定义的名称 + jv import ref@Data/Player.csv - 来自参照表的路径 + + 导入操作会将文件从其他表或导入区复制到当前工作区。 + + + direct: | + **定向到指定上游库,并染色该工作区** + **用法**:jv direct <上游库地址> + + **例如**:jv direct your_vault.org + 该操作会将当前工作区连接到指定的上游库,并为工作区添加染色标识。 + + 染色后,该工作区将只能与指定标识的库进行交互,确保数据一致性。 + + + unstain: | + **为工作区祛色** + **用法**:jv unstain + + **危险操作**:该操作会移除工作区的染色标识,此后该工作区将与上游库断开连接。 + + 祛色后,工作区将不再与任何特定库关联,可以重新连接到其他库。 + 但请注意,这可能会导致数据同步问题,请谨慎使用。 + + + update: | + **从上游库下载最新的信息** + **用法**:jv update + + 该操作会从上游库同步最新的文件状态、表信息和成员信息。 + + 建议在开始工作前先执行更新操作,确保您拥有最新的工作环境。 + + + docs: | + **查询内建文档** + **用法**: + jv docs list - 列出所有可用的文档 + jv docs <文档名称> - 查看指定文档的内容 + jv docs <文档名称> -e - 返回特定文档的临时路径以供编辑器打开 + + **例如**: + jv docs get-started + jv docs collaboration -e | nano + + 内建文档包含 JustEnoughVCS 的使用指南、协作范式和最佳实践。 -- cgit From 1dd3c4abec5a84ae1bd8dd0a93670a5bef601ab3 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Fri, 24 Oct 2025 16:16:45 +0800 Subject: Add cli error handle. --- Cargo.lock | 1 + locales/help_docs/en.yml | 46 +++++++++++ locales/help_docs/zh-CN.yml | 111 +++++++++++++++++++-------- src/bin/jv.rs | 180 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 305 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ccc1187..359c4cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -844,6 +844,7 @@ dependencies = [ name = "just_enough_vcs" version = "0.0.0" dependencies = [ + "action_system", "cfg_file", "string_proc", "tcp_connection", diff --git a/locales/help_docs/en.yml b/locales/help_docs/en.yml index 6092873..db190e0 100644 --- a/locales/help_docs/en.yml +++ b/locales/help_docs/en.yml @@ -328,3 +328,49 @@ jv: jv docs collaboration -e | nano Built-in documentation includes JustEnoughVCS usage guides, collaboration paradigms, and best practices. + + fail: + parse: + str_to_sockaddr: | + Could not recognize *`%{str}`* as an IP address + This is a problem with your input syntax! Please check **the content you entered** + + action_operation_fail: + main: | + This error is from the JustEnoughVCS core component + **Error Message**: %{err} + + type_connection: | + Based on the returned error context, this is a **network connection** issue + **Please check**: + 1. Whether your network connection is working properly + 2. Whether you have permission to connect to this address + + type_auth: | + Based on the returned error context, this is a **permission** issue + This indicates you don't have sufficient permissions to perform this operation. + + type_fsio: | + Based on the returned error context, this is a **file read/write** related issue + **Please check**: + 1. Whether your disk has sufficient space and is operating normally + 2. Whether your Local Workspace files are being used by other processes + + type_serialize: | + Based on the returned error context, this is a **serialization or deserialization** related issue + **Please check** + 1. Whether the parameter format you passed meets the requirements for serialization/deserialization + 2. Whether this version of JustEnoughVCS client/server supports this type of serialization + + type_other: | + Unfortunately, based on the returned context information, + this is not an expected error. + Please submit an Issue at the JustEnoughVCS Git Repository + + JustEnoughVCS needs your feedback, which will make this project more "JustEnough" + + info_contact_admin: | + If necessary, please contact the **administrator** of the Upstream Vault. + If you are the **administrator** and confirm this issue affects server operation, please: + 1. Check if your JustEnoughVCS server is up to date + 2. Submit an Issue at the JustEnoughVCS Git Repository diff --git a/locales/help_docs/zh-CN.yml b/locales/help_docs/zh-CN.yml index 2025cbe..ea66095 100644 --- a/locales/help_docs/zh-CN.yml +++ b/locales/help_docs/zh-CN.yml @@ -84,10 +84,10 @@ jvv: ref_sheet_not_found: 未找到参照表 `ref`,该参照表理应存在! create: - not_empty: 禁止的操作!指定的目录已经存在。 + not_empty: 禁止的操作!指定的目录已经存在 init: - not_empty: 禁止的操作!该目录不为空。 + not_empty: 禁止的操作!该目录不为空 member: register: 创建成员失败!请检查是否存在同名的成员 @@ -123,7 +123,7 @@ jvv: jv: help: | **JustEnoughVCS 本地工作区命令** - 该程序将连接至上游库,用以同步、提交本地工作区文件的变化,以供协同创作。 + 该程序将连接至上游库,用以同步、提交本地工作区文件的变化,以供协同创作 **常用别名**: jv u 下载最新信息,jv t 追踪文件,jv mv -a 自动移动文件,jv in/out 导入或导出文件 @@ -181,8 +181,8 @@ jv: jv account remove <账户名称> - 删除该账户 jv account mvkey <账户名称> <私钥文件> - 移动私钥到指定账户 - 账户是本地计算机上的身份标识,每个账户可以关联不同的私钥。 - 您需要先设置账户才能与上游库进行交互。 + 账户是本地计算机上的身份标识,每个账户可以关联不同的私钥 + 您需要先设置账户才能与上游库进行交互 sheet: | @@ -192,8 +192,8 @@ jv: jv sheet use <表名称> - 使用指定的表开始当前工作(不存在则自动创建) jv sheet exit - 退出当前工作 - 表是 JustEnoughVCS 中的核心概念,每个表代表一个独立的文件集合。 - 您可以在不同的表之间切换工作,或者将文件从一个表导出到另一个表。 + 表是 JustEnoughVCS 中的核心概念,每个表代表一个独立的文件集合 + 您可以在不同的表之间切换工作,或者将文件从一个表导出到另一个表 create: | @@ -201,18 +201,18 @@ jv: **用法**:jv create <工作区名称> **例如**:jv create my_workspace - 上述操作会在当前目录创建名为 my_workspace 的目录,并在其中初始化工作区。 + 上述操作会在当前目录创建名为 my_workspace 的目录,并在其中初始化工作区 - 工作区是您本地的工作环境,用于与上游库进行文件同步和版本控制。 + 工作区是您本地的工作环境,用于与上游库进行文件同步和版本控制 init: | **在此目录创建工作区** **用法**:jv init - 该命令会在当前所在的目录创建工作区,工作区名称由当前所在目录名称决定。 + 该命令会在当前所在的目录创建工作区,工作区名称由当前所在目录名称决定 - 如果当前目录不为空,该操作将会失败。请确保在空目录中执行此命令。 + 如果当前目录不为空,该操作将会失败,请确保在空目录中执行此命令 here: | @@ -224,7 +224,7 @@ jv: - 文件当前的持有人 - 文件最新版本的提交信息 - 这是了解当前工作区目录状态的快速方式。 + 这是了解当前工作区目录状态的快速方式 track: | @@ -232,10 +232,10 @@ jv: **用法**:jv track <文件路径> **例如**:jv track src/main.rs - 第一次追踪文件时,会创建并上传 “第一版本”,然后自动持有该文件的编辑权。 - 后续追踪同一文件时,会更新文件的新版本。 + 第一次追踪文件时,会创建并上传 “第一版本”,然后自动持有该文件的编辑权 + 后续追踪同一文件时,会更新文件的新版本 - 追踪文件是版本控制的基础操作,确保您的更改能够同步到上游库。 + 追踪文件是版本控制的基础操作,确保您的更改能够同步到上游库 hold: | @@ -243,10 +243,10 @@ jv: **用法**:jv hold <文件路径> **例如**:jv hold src/lib.rs - 当您需要编辑某个文件时,必须先持有该文件的编辑权。 - 持有文件后,其他协作者将无法同时编辑该文件,避免冲突。 + 当您需要编辑某个文件时,必须先持有该文件的编辑权 + 持有文件后,其他协作者将无法同时编辑该文件,避免冲突 - 编辑完成后,请记得追踪文件以保存更改。 + 编辑完成后,请记得追踪文件以保存更改 throw: | @@ -254,10 +254,10 @@ jv: **用法**:jv throw <文件路径> **例如**:jv throw src/config.rs - 当您不再需要编辑某个文件时,可以丢弃该文件的编辑权。 - 丢弃后,其他协作者就可以持有并编辑该文件。 + 当您不再需要编辑某个文件时,可以丢弃该文件的编辑权 + 丢弃后,其他协作者就可以持有并编辑该文件 - 如果您对文件进行了更改但尚未追踪,丢弃操作会丢失这些更改。 + 如果您对文件进行了更改但尚未追踪,丢弃操作会丢失这些更改 move: | @@ -271,7 +271,7 @@ jv: jv move src/old_dir/file.rs src/new_dir/file.rs jv move auto - 安全移动操作会保持文件的版本历史,而自动移动会检测并处理所有重命名。 + 安全移动操作会保持文件的版本历史,而自动移动会检测并处理所有重命名 export: | @@ -279,9 +279,9 @@ jv: **用法**:jv export <文件> <目标表> -m <描述> -n <文件包名称> **例如**:jv export data.csv analytics -m "导出分析数据" -n "analysis_data" - 该操作会将指定的文件打包并发送到目标表的导入区。 + 该操作会将指定的文件打包并发送到目标表的导入区 - 其他协作者可以在目标表中使用 jv import 命令来导入这些文件。 + 其他协作者可以在目标表中使用 jv import 命令来导入这些文件 import: | @@ -294,7 +294,7 @@ jv: jv import Player_Import - 来自导入区定义的名称 jv import ref@Data/Player.csv - 来自参照表的路径 - 导入操作会将文件从其他表或导入区复制到当前工作区。 + 导入操作会将文件从其他表或导入区复制到当前工作区 direct: | @@ -302,28 +302,28 @@ jv: **用法**:jv direct <上游库地址> **例如**:jv direct your_vault.org - 该操作会将当前工作区连接到指定的上游库,并为工作区添加染色标识。 + 该操作会将当前工作区连接到指定的上游库,并为工作区添加染色标识 - 染色后,该工作区将只能与指定标识的库进行交互,确保数据一致性。 + 染色后,该工作区将只能与指定标识的库进行交互,确保数据一致性 unstain: | **为工作区祛色** **用法**:jv unstain - **危险操作**:该操作会移除工作区的染色标识,此后该工作区将与上游库断开连接。 + **危险操作**:该操作会移除工作区的染色标识,此后该工作区将与上游库断开连接 - 祛色后,工作区将不再与任何特定库关联,可以重新连接到其他库。 - 但请注意,这可能会导致数据同步问题,请谨慎使用。 + 祛色后,工作区将不再与任何特定库关联,可以重新连接到其他库 + 但请注意,这可能会导致数据同步问题,请谨慎使用 update: | **从上游库下载最新的信息** **用法**:jv update - 该操作会从上游库同步最新的文件状态、表信息和成员信息。 + 该操作会从上游库同步最新的文件状态、表信息和成员信息 - 建议在开始工作前先执行更新操作,确保您拥有最新的工作环境。 + 建议在开始工作前先执行更新操作,确保您拥有最新的工作环境 docs: | @@ -337,4 +337,49 @@ jv: jv docs get-started jv docs collaboration -e | nano - 内建文档包含 JustEnoughVCS 的使用指南、协作范式和最佳实践。 + 内建文档包含 JustEnoughVCS 的使用指南、协作范式和最佳实践 + + fail: + parse: + str_to_sockaddr: | + 无法将 *`%{str}`* 识别为IP地址 + 这是您的输入语法问题!请检查 **输入的内容** + + action_operation_fail: + main: | + 此错误来自 JustEnoughVCS 核心组件 + **错误信息**:%{err} + + type_connection: | + 根据返回的错误上下文,这是一个**网络连接**问题 + **请检查**: + 1. 您的网络连接是否通畅 + 2. 您是否有权限连接至该地址 + + type_auth: | + 根据返回的错误上下文,这是一个**权限**问题 + 这说明您没有足够的权限去做此项操作。 + + type_fsio: | + 根据返回的错误上下文,这是一个**文件读写**相关的问题 + **请检查**: + 1. 您的磁盘是否有足够的空间,并且能正常流畅地运转 + 2. 您的本地工作区文件是否被其他进程占用 + + type_serialize: | + 根据返回的错误上下文,这是一个**序列化或反序列化**相关的问题 + **请检查** + 1. 您传入的参数格式是否符合序列化或反序列化的要求 + 2. 该版本的 JustEnoughVCS 客户端/服务端 是否支持该类型的序列化 + + type_other: | + 很遗憾,根据返回的上下文信息,这并不是一个符合预期的错误。 + 请前往 JustEnoughVCS 所在的 Git 版本库提交 Issue + + JustEnoughVCS 需要您的反馈,这会让该项目变得更加 "JustEnough" + + info_contact_admin: | + 如有必要,请联系上游库的 **管理员**。 + 若您作为 **管理员**,在确认该问题影响服务器运作,请 + 1. 检查您的 JustEnoughVCS 服务端是否最新 + 2. 前往 JustEnoughVCS 所在的 Git 版本库提交 Issue diff --git a/src/bin/jv.rs b/src/bin/jv.rs index 9530203..f67b37b 100644 --- a/src/bin/jv.rs +++ b/src/bin/jv.rs @@ -1,4 +1,11 @@ +use std::{net::SocketAddr, str::FromStr}; + use clap::{Parser, Subcommand, arg, command}; +use just_enough_vcs::{ + system::action_system::action::ActionContext, + utils::tcp_connection::error::TcpTargetError, + vcs::{actions::local_actions::proc_set_upstream_vault_action, registry::client_registry}, +}; use just_enough_vcs_cli::utils::{lang_selector::current_locales, md_colored::md}; use rust_i18n::{set_locale, t}; @@ -84,6 +91,12 @@ enum AccountManage { /// Show help information #[command(alias = "--help", alias = "-h")] Help, + + /// Register a member to this computer + Add(AccountAddArgs), + + /// Remove a account from this computer + Remove(AccountRemoveArgs), } #[derive(Subcommand, Debug)] @@ -93,6 +106,18 @@ enum SheetManage { Help, } +#[derive(Parser, Debug)] +struct AccountAddArgs { + /// Member name + member_name: String, +} + +#[derive(Parser, Debug)] +struct AccountRemoveArgs { + /// Member name + member_name: String, +} + #[derive(Parser, Debug)] struct CreateWorkspaceArgs { /// Show help information @@ -161,6 +186,9 @@ struct DirectArgs { /// Show help information #[arg(short, long)] help: bool, + + /// Upstream vault address + upstream: String, } #[derive(Parser, Debug)] @@ -196,24 +224,29 @@ async fn main() { AccountManage::Help => { println!("{}", md(t!("jv.account"))); } + AccountManage::Add(account_add_args) => todo!(), + AccountManage::Remove(account_remove_args) => todo!(), }, JustEnoughVcsWorkspaceCommand::Create(create_workspace_args) => { if create_workspace_args.help { println!("{}", md(t!("jv.create"))); return; } + jv_create(create_workspace_args).await; } JustEnoughVcsWorkspaceCommand::Init(init_workspace_args) => { if init_workspace_args.help { println!("{}", md(t!("jv.init"))); return; } + jv_init(init_workspace_args).await; } JustEnoughVcsWorkspaceCommand::Here(here_args) => { if here_args.help { println!("{}", md(t!("jv.here"))); return; } + jv_here(here_args).await; } JustEnoughVcsWorkspaceCommand::Sheet(sheet_manage) => match sheet_manage { SheetManage::Help => { @@ -226,54 +259,201 @@ async fn main() { println!("{}", md(t!("jv.track"))); return; } + jv_track(track_file_args).await; } JustEnoughVcsWorkspaceCommand::Hold(hold_file_args) => { if hold_file_args.help { println!("{}", md(t!("jv.hold"))); return; } + jv_hold(hold_file_args).await; } JustEnoughVcsWorkspaceCommand::Throw(throw_file_args) => { if throw_file_args.help { println!("{}", md(t!("jv.throw"))); return; } + jv_throw(throw_file_args).await; } JustEnoughVcsWorkspaceCommand::Move(move_file_args) => { if move_file_args.help { println!("{}", md(t!("jv.move"))); return; } + jv_move(move_file_args).await; } JustEnoughVcsWorkspaceCommand::Export(export_file_args) => { if export_file_args.help { println!("{}", md(t!("jv.export"))); return; } + jv_export(export_file_args).await; } JustEnoughVcsWorkspaceCommand::Import(import_file_args) => { if import_file_args.help { println!("{}", md(t!("jv.import"))); return; } + jv_import(import_file_args).await; } JustEnoughVcsWorkspaceCommand::Direct(direct_args) => { if direct_args.help { println!("{}", md(t!("jv.direct"))); return; } + jv_direct(direct_args).await; } JustEnoughVcsWorkspaceCommand::Unstain(unstain_args) => { if unstain_args.help { println!("{}", md(t!("jv.unstain"))); return; } + jv_unstain(unstain_args).await; } JustEnoughVcsWorkspaceCommand::Docs(docs_args) => { if docs_args.help { println!("{}", md(t!("jv.docs"))); return; } + jv_docs(docs_args).await; + } + } +} + +async fn jv_create(_args: CreateWorkspaceArgs) { + todo!() +} + +async fn jv_init(_args: InitWorkspaceArgs) { + todo!() +} + +async fn jv_here(_args: HereArgs) { + todo!() +} + +async fn jv_track(_args: TrackFileArgs) { + todo!() +} + +async fn jv_hold(_args: HoldFileArgs) { + todo!() +} + +async fn jv_throw(_args: ThrowFileArgs) { + todo!() +} + +async fn jv_move(_args: MoveFileArgs) { + todo!() +} + +async fn jv_export(_args: ExportFileArgs) { + todo!() +} + +async fn jv_import(_args: ImportFileArgs) { + todo!() +} + +async fn jv_direct(args: DirectArgs) { + let pool = client_registry::client_action_pool(); + let upstream = match SocketAddr::from_str(&args.upstream) { + Ok(result) => result, + Err(_) => { + eprintln!( + "{}", + md(t!( + "jv.fail.parse.str_to_sockaddr", + str = &args.upstream.trim() + )) + ); + return; + } + }; + let ctx = ActionContext::local(); + match proc_set_upstream_vault_action(&pool, ctx, upstream).await { + Err(e) => handle_err(e), + _ => {} + }; +} + +async fn jv_unstain(_args: UnstainArgs) { + todo!() +} + +async fn jv_docs(_args: DocsArgs) { + todo!() +} + +pub fn handle_err(err: TcpTargetError) { + let e: Option<(String, String, bool)> = match err { + TcpTargetError::Io(err) => Some(fsio(err)), + TcpTargetError::File(err) => Some(fsio(err)), + + TcpTargetError::Serialization(err) => Some(serialize(err)), + + TcpTargetError::Authentication(err) => Some(auth(err)), + + TcpTargetError::Network(err) => Some(connection(err)), + TcpTargetError::Timeout(err) => Some(connection(err)), + TcpTargetError::Protocol(err) => Some(connection(err)), + + _ => Some(( + err.to_string(), + md(t!("jv.fail.action_operation_fail.type_other")), + false, + )), + }; + + if let Some((err_text, err_tip, has_tip)) = e { + eprintln!( + "{}\n{}", + md(t!("jv.fail.action_operation_fail.main", err = err_text)), + err_tip, + ); + + if has_tip { + eprintln!( + "{}", + md(t!("jv.fail.action_operation_fail.info_contact_admin")) + ) } } } + +type ErrorText = String; +type ErrorTip = String; +type HasTip = bool; + +fn fsio(err: String) -> (ErrorText, ErrorTip, HasTip) { + ( + err, + md(t!("jv.fail.action_operation_fail.type_fsio")).to_string(), + true, + ) +} + +fn serialize(err: String) -> (ErrorText, ErrorTip, HasTip) { + ( + err, + md(t!("jv.fail.action_operation_fail.type_serialize")).to_string(), + true, + ) +} + +fn auth(err: String) -> (ErrorText, ErrorTip, HasTip) { + ( + err, + md(t!("jv.fail.action_operation_fail.type_auth")).to_string(), + true, + ) +} + +fn connection(err: String) -> (ErrorText, ErrorTip, HasTip) { + ( + err, + md(t!("jv.fail.action_operation_fail.type_connection")).to_string(), + true, + ) +} -- cgit From 77d162ad9974752c0caa3340b20c91049c69feba Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Fri, 24 Oct 2025 18:26:22 +0800 Subject: Update locales files --- locales/help_docs/en.yml | 55 ++++++++++++++++++++++++++++++++++++++------- locales/help_docs/zh-CN.yml | 44 ++++++++++++++++++++++++++++++++++-- 2 files changed, 89 insertions(+), 10 deletions(-) diff --git a/locales/help_docs/en.yml b/locales/help_docs/en.yml index db190e0..c72754c 100644 --- a/locales/help_docs/en.yml +++ b/locales/help_docs/en.yml @@ -119,7 +119,7 @@ jvv: footer: | **Among them, %{num} members have registered PubKeys.** - status_key_registered: (PubKey Exists) + status_key_registered: (Registered) service: listen: Listening for client connections in vault `%{path}` ... @@ -147,7 +147,7 @@ jv: jv account as - Switch current account jv account add - Add an account to this computer jv account remove - Delete this account - jv account mvkey - Move private key to specified account + jv account movekey - Move private key to specified account **Information Synchronization**: jv update - Download latest information from upstream workspace @@ -183,7 +183,7 @@ jv: jv account as - Switch current account jv account add - Add an account to this computer jv account remove - Delete this account - jv account mvkey - Move private key to specified account + jv account movekey - Move private key to specified account Accounts are identity identifiers on the local computer, each account can be associated with different private keys. You need to set up accounts before interacting with upstream vaults. @@ -329,11 +329,14 @@ jv: Built-in documentation includes JustEnoughVCS usage guides, collaboration paradigms, and best practices. - fail: - parse: - str_to_sockaddr: | - Could not recognize *`%{str}`* as an IP address - This is a problem with your input syntax! Please check **the content you entered** + fail: + path_not_found: | + The directory `%{path}` cannot be found! + + parse: + str_to_sockaddr: | + Could not recognize *`%{str}`* as an IP address + This is a problem with your input syntax! Please check **the content you entered** action_operation_fail: main: | @@ -374,3 +377,39 @@ jv: If you are the **administrator** and confirm this issue affects server operation, please: 1. Check if your JustEnoughVCS server is up to date 2. Submit an Issue at the JustEnoughVCS Git Repository + + account: + no_user_dir: Cannot find user directory! + add: Failed to add account `%{account}`, please check if the account already exists. + remove: Failed to remove account `%{account}`, please check if the account exists. + list: Failed to get account list! + not_found: Cannot find account `%{account}`! + + init_create_dir_not_empty: | + The current directory is not empty! + If you are certain you want to create here, please use --force to force execution! + + create: Failed to create local workspace! + init: Failed to create workspace here! + get_current_dir: Failed to get current directory! + + workspace_not_found: | + Local workspace not found! Please use this command within a local workspace + If you wish to use this directory as a local workspace, please use jv init + + read_cfg: Failed to read local workspace configuration file! + write_cfg: Failed to write changes to local workspace configuration file! + + success: + account: + as: Successfully switched this workspace's account to `%{account}` + add: Successfully added account `%{account}`! + remove: Successfully removed account `%{account}`! + list: + header: | + **There are %{num} account(s) on this computer:** + + status_has_key: (Registered) + move_key: Successfully moved the private key to the account directory! + create: Successfully created local workspace! + init: Successfully created workspace here! diff --git a/locales/help_docs/zh-CN.yml b/locales/help_docs/zh-CN.yml index ea66095..442e7f9 100644 --- a/locales/help_docs/zh-CN.yml +++ b/locales/help_docs/zh-CN.yml @@ -143,7 +143,7 @@ jv: jv account as <账户名称> - 切换当前账户 jv account add <账户名称> - 为当前计算机添加账户 jv account remove <账户名称> - 删除该账户 - jv account mvkey <账户名称> <私钥文件> - 移动私钥到指定账户 + jv account movekey <账户名称> <私钥文件> - 移动私钥到指定账户 **信息同步**: jv update - 从上游工作区下载最新的信息 @@ -179,7 +179,7 @@ jv: jv account as <账户名称> - 切换当前账户 jv account add <账户名称> - 为当前计算机添加账户 jv account remove <账户名称> - 删除该账户 - jv account mvkey <账户名称> <私钥文件> - 移动私钥到指定账户 + jv account movekey <账户名称> <私钥文件> - 移动私钥到指定账户 账户是本地计算机上的身份标识,每个账户可以关联不同的私钥 您需要先设置账户才能与上游库进行交互 @@ -340,6 +340,9 @@ jv: 内建文档包含 JustEnoughVCS 的使用指南、协作范式和最佳实践 fail: + path_not_found: | + 您给出的目录 `%{path}` 无法找到! + parse: str_to_sockaddr: | 无法将 *`%{str}`* 识别为IP地址 @@ -383,3 +386,40 @@ jv: 若您作为 **管理员**,在确认该问题影响服务器运作,请 1. 检查您的 JustEnoughVCS 服务端是否最新 2. 前往 JustEnoughVCS 所在的 Git 版本库提交 Issue + + account: + no_user_dir: 无法找到用户目录! + add: 添加账户 `%{account}` 失败,请检查账户是否已存在。 + remove: 删除账户 `%{account}` 失败,请检查账户是否存在。 + list: 获取账户列表失败! + move_key: 将该私钥移动至账户目录失败! + not_found: 无法找到账户 `%{account}`! + + init_create_dir_not_empty: | + 当前目录并不是空的! + 若您确实确定在此处创建,请使用 --force 来强制执行! + + create: 创建本地工作区失败! + init: 在此处创建本地工作区失败! + get_current_dir: 无法获得当前目录! + + workspace_not_found: | + 无法找到本地工作区!请在本地工作区内使用该命令 + 若您希望将该目录作为本地工作区,请使用 jv init + + read_cfg: 无法读取本地工作区的配置文件! + write_cfg: 无法将修改写入本地工作区的配置文件! + + success: + account: + as: 成功将此工作区的账户切换至 `%{account}` + add: 成功添加账户 `%{account}`! + remove: 成功删除账户 `%{account}`! + list: + header: | + **当前计算机上有 %{num} 个账户:** + + status_has_key: (已注册私钥) + move_key: 成功将该私钥移动至账户目录! + create: 成功创建本地工作区! + init: 成功在此处创建工作区! -- cgit From b40443cae7b963048ddd3be8b55a4feaf1e6612d Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Fri, 24 Oct 2025 18:26:47 +0800 Subject: Completed "Account Manage" parts of jv --- src/bin/jv.rs | 322 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- src/bin/jvv.rs | 76 +++++++++----- 2 files changed, 356 insertions(+), 42 deletions(-) diff --git a/src/bin/jv.rs b/src/bin/jv.rs index f67b37b..3a22528 100644 --- a/src/bin/jv.rs +++ b/src/bin/jv.rs @@ -1,4 +1,16 @@ -use std::{net::SocketAddr, str::FromStr}; +use std::{env::current_dir, net::SocketAddr, path::PathBuf, str::FromStr}; + +use just_enough_vcs::{ + utils::cfg_file::config::ConfigFile, + vcs::{ + current::current_local_path, + data::{ + local::{LocalWorkspace, config::LocalConfig}, + member::Member, + user::UserDirectory, + }, + }, +}; use clap::{Parser, Subcommand, arg, command}; use just_enough_vcs::{ @@ -8,6 +20,7 @@ use just_enough_vcs::{ }; use just_enough_vcs_cli::utils::{lang_selector::current_locales, md_colored::md}; use rust_i18n::{set_locale, t}; +use tokio::fs; // Import i18n files rust_i18n::i18n!("locales", fallback = "en"); @@ -29,7 +42,7 @@ struct JustEnoughVcsWorkspace { enum JustEnoughVcsWorkspaceCommand { // Member management /// Manage your local accounts - #[command(subcommand)] + #[command(subcommand, alias = "acc")] Account(AccountManage), /// Create an empty workspace @@ -96,7 +109,19 @@ enum AccountManage { Add(AccountAddArgs), /// Remove a account from this computer + #[command(alias = "rm")] Remove(AccountRemoveArgs), + + /// List all accounts in this computer + #[command(alias = "ls")] + List(AccountListArgs), + + /// Set current local workspace account + As(SetLocalWorkspaceAccountArgs), + + /// Move private key file to account + #[command(alias = "mvkey", alias = "mvk")] + MoveKey(MoveKeyToAccountArgs), } #[derive(Subcommand, Debug)] @@ -106,37 +131,86 @@ enum SheetManage { Help, } +#[derive(Parser, Debug)] +struct CreateWorkspaceArgs { + /// Show help information + #[arg(short, long)] + help: bool, + + /// Workspace directory path + path: PathBuf, + + /// Force create, ignore files in the directory + #[arg(short, long)] + force: bool, +} + +#[derive(Parser, Debug)] +struct InitWorkspaceArgs { + /// Show help information + #[arg(short, long)] + help: bool, + + /// Force create, ignore files in the directory + #[arg(short, long)] + force: bool, +} + +#[derive(Parser, Debug)] +struct HereArgs { + /// Show help information + #[arg(short, long)] + help: bool, +} + #[derive(Parser, Debug)] struct AccountAddArgs { - /// Member name - member_name: String, + /// Show help information + #[arg(short, long)] + help: bool, + + /// Account name + account_name: String, } #[derive(Parser, Debug)] struct AccountRemoveArgs { - /// Member name - member_name: String, + /// Show help information + #[arg(short, long)] + help: bool, + + /// Account name + account_name: String, } #[derive(Parser, Debug)] -struct CreateWorkspaceArgs { +struct AccountListArgs { /// Show help information #[arg(short, long)] help: bool, } #[derive(Parser, Debug)] -struct InitWorkspaceArgs { +struct SetLocalWorkspaceAccountArgs { /// Show help information #[arg(short, long)] help: bool, + + /// Account name + account_name: String, } #[derive(Parser, Debug)] -struct HereArgs { +struct MoveKeyToAccountArgs { /// Show help information #[arg(short, long)] help: bool, + + /// Account name + account_name: String, + + /// Private key file path + key_path: PathBuf, } #[derive(Parser, Debug)] @@ -220,13 +294,56 @@ async fn main() { }; match parser.command { - JustEnoughVcsWorkspaceCommand::Account(account_manage) => match account_manage { - AccountManage::Help => { - println!("{}", md(t!("jv.account"))); + JustEnoughVcsWorkspaceCommand::Account(account_manage) => { + let user_dir = match UserDirectory::current_doc_dir() { + Some(dir) => dir, + None => { + eprintln!("{}", t!("jv.fail.account.no_user_dir")); + return; + } + }; + + match account_manage { + AccountManage::Help => { + println!("{}", md(t!("jv.account"))); + } + AccountManage::Add(account_add_args) => { + if account_add_args.help { + println!("{}", md(t!("jv.account"))); + return; + } + jv_account_add(user_dir, account_add_args).await; + } + AccountManage::Remove(account_remove_args) => { + if account_remove_args.help { + println!("{}", md(t!("jv.account"))); + return; + } + jv_account_remove(user_dir, account_remove_args).await; + } + AccountManage::List(account_list_args) => { + if account_list_args.help { + println!("{}", md(t!("jv.account"))); + return; + } + jv_account_list(user_dir, account_list_args).await; + } + AccountManage::As(set_local_workspace_account_args) => { + if set_local_workspace_account_args.help { + println!("{}", md(t!("jv.account"))); + return; + } + jv_account_as(user_dir, set_local_workspace_account_args).await; + } + AccountManage::MoveKey(move_key_to_account_args) => { + if move_key_to_account_args.help { + println!("{}", md(t!("jv.account"))); + return; + } + jv_account_move_key(user_dir, move_key_to_account_args).await; + } } - AccountManage::Add(account_add_args) => todo!(), - AccountManage::Remove(account_remove_args) => todo!(), - }, + } JustEnoughVcsWorkspaceCommand::Create(create_workspace_args) => { if create_workspace_args.help { println!("{}", md(t!("jv.create"))); @@ -320,12 +437,53 @@ async fn main() { } } -async fn jv_create(_args: CreateWorkspaceArgs) { - todo!() +async fn jv_create(args: CreateWorkspaceArgs) { + let path = args.path; + + if !args.force && path.exists() && !is_directory_empty(&path).await { + eprintln!("{}", t!("jv.fail.init_create_dir_not_empty").trim()); + return; + } + + match LocalWorkspace::setup_local_workspace(path).await { + Ok(_) => { + println!("{}", t!("jv.success.create")); + } + Err(e) => { + eprintln!("{}", t!("jv.fail.create", error = e.to_string())); + } + } } -async fn jv_init(_args: InitWorkspaceArgs) { - todo!() +async fn jv_init(args: InitWorkspaceArgs) { + let path = match current_dir() { + Ok(path) => path, + Err(e) => { + eprintln!("{}", t!("jv.fail.get_current_dir", error = e.to_string())); + return; + } + }; + + if !args.force && path.exists() && !is_directory_empty(&path).await { + eprintln!("{}", t!("jv.fail.init_create_dir_not_empty").trim()); + return; + } + + match LocalWorkspace::setup_local_workspace(path).await { + Ok(_) => { + println!("{}", t!("jv.success.init")); + } + Err(e) => { + eprintln!("{}", t!("jv.fail.init", error = e.to_string())); + } + } +} + +async fn is_directory_empty(path: &PathBuf) -> bool { + match fs::read_dir(path).await { + Ok(mut entries) => entries.next_entry().await.unwrap().is_none(), + Err(_) => false, + } } async fn jv_here(_args: HereArgs) { @@ -356,6 +514,132 @@ async fn jv_import(_args: ImportFileArgs) { todo!() } +async fn jv_account_add(user_dir: UserDirectory, args: AccountAddArgs) { + let member = Member::new(args.account_name.clone()); + + match user_dir.register_account(member).await { + Ok(_) => { + println!( + "{}", + t!("jv.success.account.add", account = args.account_name) + ); + } + Err(_) => { + eprintln!("{}", t!("jv.fail.account.add", account = args.account_name)); + } + } +} + +async fn jv_account_remove(user_dir: UserDirectory, args: AccountRemoveArgs) { + match user_dir.remove_account(&args.account_name) { + Ok(_) => { + println!( + "{}", + t!("jv.success.account.remove", account = args.account_name) + ); + } + Err(_) => { + eprintln!( + "{}", + t!("jv.fail.account.remove", account = args.account_name) + ); + } + } +} + +async fn jv_account_list(user_dir: UserDirectory, _args: AccountListArgs) { + match user_dir.account_ids() { + Ok(account_ids) => { + println!( + "{}", + md(t!( + "jv.success.account.list.header", + num = account_ids.len() + )) + ); + + let mut i = 0; + for account_id in account_ids { + println!("{}. {} {}", i + 1, &account_id, { + if user_dir.has_private_key(&account_id) { + t!("jv.success.account.list.status_has_key") + } else { + std::borrow::Cow::Borrowed("") + } + }); + i += 1; + } + } + Err(_) => { + eprintln!("{}", t!("jv.fail.account.list")); + } + } +} + +async fn jv_account_as(user_dir: UserDirectory, args: SetLocalWorkspaceAccountArgs) { + // Account exist + let Ok(member) = user_dir.account(&args.account_name).await else { + eprintln!( + "{}", + t!("jv.fail.account.not_found", account = args.account_name) + ); + return; + }; + + let Some(_local_dir) = current_local_path() else { + eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); + return; + }; + + let Ok(mut local_cfg) = LocalConfig::read().await else { + eprintln!("{}", md(t!("jv.fail.read_cfg"))); + return; + }; + + local_cfg.set_current_account(member.id()); + + let Ok(_) = LocalConfig::write(&local_cfg).await else { + eprintln!("{}", t!("jv.fail.write_cfg").trim()); + return; + }; + + println!( + "{}", + t!("jv.success.account.as", account = member.id()).trim() + ); +} + +async fn jv_account_move_key(user_dir: UserDirectory, args: MoveKeyToAccountArgs) { + // Key file exist + if !args.key_path.exists() { + eprintln!( + "{}", + t!("jv.fail.path_not_found", path = args.key_path.display()) + ); + return; + } + + // Account exist + let Ok(_member) = user_dir.account(&args.account_name).await else { + eprintln!( + "{}", + t!("jv.fail.account.not_found", account = args.account_name) + ); + return; + }; + + // Rename key file + match fs::rename( + args.key_path, + user_dir.account_private_key_path(&args.account_name), + ) + .await + { + Ok(_) => println!("{}", t!("jv.success.account.move_key")), + Err(_) => eprintln!("{}", t!("jv.fail.account.move_key")), + } +} + async fn jv_direct(args: DirectArgs) { let pool = client_registry::client_action_pool(); let upstream = match SocketAddr::from_str(&args.upstream) { diff --git a/src/bin/jvv.rs b/src/bin/jvv.rs index 50dc3b3..1886daa 100644 --- a/src/bin/jvv.rs +++ b/src/bin/jvv.rs @@ -153,7 +153,10 @@ async fn main() { // Init colored #[cfg(windows)] - colored::control::set_virtual_terminal(true).unwrap(); + if let Err(err) = colored::control::set_virtual_terminal(true) { + eprintln!("{}", t!("jvv.fail.colored_control", err = err.to_string())); + return; + } let Ok(parser) = JustEnoughVcsVault::try_parse() else { println!("{}", md(t!("jvv.help"))); @@ -183,9 +186,13 @@ async fn main() { jvv_init(init_vault_args).await; } JustEnoughVcsVaultCommand::Member(member_manage) => { - let vault_cfg = VaultConfig::read() - .await - .unwrap_or_else(|_| panic!("{}", t!("jvv.fail.no_vault_here").trim().to_string())); + let vault_cfg = match VaultConfig::read().await { + Ok(cfg) => cfg, + Err(_) => { + eprintln!("{}", t!("jvv.fail.no_vault_here").trim()); + return; + } + }; let vault = match Vault::init_current_dir(vault_cfg) { Some(vault) => vault, @@ -358,19 +365,28 @@ async fn jvv_here(_args: HereArgs) { } async fn jvv_init(_args: InitVaultArgs) { - let current_dir = std::env::current_dir() - .unwrap_or_else(|_| panic!("{}", t!("jvv.fail.std.current_dir").trim().to_string())); - if current_dir.read_dir().unwrap().next().is_some() { - eprintln!("{}", t!("jvv.fail.init.not_empty")); - return; + let current_dir = match std::env::current_dir() { + Ok(dir) => dir, + Err(_) => { + eprintln!("{}", t!("jvv.fail.std.current_dir").trim()); + return; + } + }; + if let Ok(mut entries) = current_dir.read_dir() { + if entries.next().is_some() { + eprintln!("{}", t!("jvv.fail.init.not_empty")); + return; + } } // Setup vault - let vault_name = current_dir - .file_name() - .unwrap_or_else(|| panic!("{}", t!("jvv.fail.std.current_dir_name").trim().to_string())) - .to_string_lossy() - .to_string(); + let vault_name = match current_dir.file_name() { + Some(name) => name.to_string_lossy().to_string(), + None => { + eprintln!("{}", t!("jvv.fail.std.current_dir_name").trim()); + return; + } + }; let vault_name = pascal_case!(vault_name); if let Err(err) = Vault::setup_vault(current_dir.clone()).await { @@ -401,8 +417,13 @@ async fn jvv_init(_args: InitVaultArgs) { } async fn jvv_create(args: CreateVaultArgs) { - let current_dir = std::env::current_dir() - .unwrap_or_else(|_| panic!("{}", t!("jvv.fail.std.current_dir").trim().to_string())); + let current_dir = match std::env::current_dir() { + Ok(dir) => dir, + Err(_) => { + eprintln!("{}", t!("jvv.fail.std.current_dir").trim()); + return; + } + }; let target_dir = current_dir.join(args.vault_name.clone()); // Create directory @@ -417,9 +438,11 @@ async fn jvv_create(args: CreateVaultArgs) { return; } - if target_dir.read_dir().unwrap().next().is_some() { - eprintln!("{}", t!("jvv.fail.create.not_empty")); - return; + if let Ok(mut entries) = target_dir.read_dir() { + if entries.next().is_some() { + eprintln!("{}", t!("jvv.fail.create.not_empty")); + return; + } } // Setup vault @@ -490,9 +513,13 @@ async fn jvv_member_remove(vault: Vault, args: MemberRemoveArgs) { async fn jvv_member_list(vault: Vault, _args: MemberListArgs) { // Get id list - let ids = vault - .member_ids() - .unwrap_or_else(|_| panic!("{}", t!("jvv.fail.member.list").trim().to_string())); + let ids = match vault.member_ids() { + Ok(ids) => ids, + Err(_) => { + eprintln!("{}", t!("jvv.fail.member.list").trim()); + return; + } + }; // Print header println!( @@ -560,7 +587,10 @@ async fn jvv_service_listen(args: ListenArgs) { "{}", t!( "jvv.success.service.listen", - path = current_vault.file_name().unwrap().display() + path = match current_vault.file_name() { + Some(name) => name.to_string_lossy(), + None => std::borrow::Cow::Borrowed("unknown"), + } ) ) } -- cgit From cf6218b25e0134e1b12bdf90d98189d94b18f170 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 27 Oct 2025 17:55:23 +0800 Subject: Add socket address helper for domain resolution --- src/bin/jv.rs | 2 +- src/utils.rs | 1 + src/utils/socket_addr_helper.rs | 186 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 src/utils/socket_addr_helper.rs diff --git a/src/bin/jv.rs b/src/bin/jv.rs index 3a22528..edab4ef 100644 --- a/src/bin/jv.rs +++ b/src/bin/jv.rs @@ -642,7 +642,7 @@ async fn jv_account_move_key(user_dir: UserDirectory, args: MoveKeyToAccountArgs async fn jv_direct(args: DirectArgs) { let pool = client_registry::client_action_pool(); - let upstream = match SocketAddr::from_str(&args.upstream) { + let upstream = match socket_addr_helper::get_socket_addr(&args.upstream, PORT).await { Ok(result) => result, Err(_) => { eprintln!( diff --git a/src/utils.rs b/src/utils.rs index 87cc1b4..e034bcd 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,3 +1,4 @@ pub mod build_env_logger; pub mod lang_selector; pub mod md_colored; +pub mod socket_addr_helper; diff --git a/src/utils/socket_addr_helper.rs b/src/utils/socket_addr_helper.rs new file mode 100644 index 0000000..c6805da --- /dev/null +++ b/src/utils/socket_addr_helper.rs @@ -0,0 +1,186 @@ +use std::net::SocketAddr; +use tokio::net::lookup_host; + +/// Helper function to parse a string into a SocketAddr with optional default port +pub async fn get_socket_addr( + address_str: impl AsRef, + default_port: u16, +) -> Result { + let address = address_str.as_ref().trim(); + + // Check if the address contains a port + if let Some((host, port_str)) = parse_host_and_port(address) { + let port = port_str.parse::().map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("Invalid port number '{}': {}", port_str, e), + ) + })?; + + return resolve_to_socket_addr(&host, port).await; + } + + // No port specified, use default port + resolve_to_socket_addr(address, default_port).await +} + +/// Parse host and port from address string +fn parse_host_and_port(address: &str) -> Option<(&str, &str)> { + if address.starts_with('[') { + if let Some(close_bracket) = address.find(']') { + if close_bracket + 1 < address.len() && address.as_bytes()[close_bracket + 1] == b':' { + let host = &address[1..close_bracket]; + let port = &address[close_bracket + 2..]; + return Some((host, port)); + } + } + } + + // Handle IPv4 addresses and hostnames with ports + if let Some(colon_pos) = address.rfind(':') { + // Check if this is not part of an IPv6 address without brackets + if !address.contains('[') && !address.contains(']') { + let host = &address[..colon_pos]; + let port = &address[colon_pos + 1..]; + + // Basic validation to avoid false positives + if !host.is_empty() && !port.is_empty() && port.chars().all(|c| c.is_ascii_digit()) { + return Some((host, port)); + } + } + } + + None +} + +/// Resolve host to SocketAddr, handling both IP addresses and domain names +async fn resolve_to_socket_addr(host: &str, port: u16) -> Result { + // First try to parse as IP address (IPv4 or IPv6) + if let Ok(ip_addr) = host.parse() { + return Ok(SocketAddr::new(ip_addr, port)); + } + + // If it's not a valid IP address, treat it as a domain name and perform DNS lookup + let lookup_addr = format!("{}:{}", host, port); + let mut addrs = lookup_host(&lookup_addr).await?; + + if let Some(addr) = addrs.next() { + Ok(addr) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("Could not resolve host '{}'", host), + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_ipv4_with_port() { + let result = get_socket_addr("127.0.0.1:8080", 80).await; + assert!(result.is_ok()); + let addr = result.unwrap(); + assert_eq!(addr.port(), 8080); + assert_eq!(addr.ip().to_string(), "127.0.0.1"); + } + + #[tokio::test] + async fn test_ipv4_without_port() { + let result = get_socket_addr("192.168.1.1", 443).await; + assert!(result.is_ok()); + let addr = result.unwrap(); + assert_eq!(addr.port(), 443); + assert_eq!(addr.ip().to_string(), "192.168.1.1"); + } + + #[tokio::test] + async fn test_ipv6_with_port() { + let result = get_socket_addr("[::1]:8080", 80).await; + assert!(result.is_ok()); + let addr = result.unwrap(); + assert_eq!(addr.port(), 8080); + assert_eq!(addr.ip().to_string(), "::1"); + } + + #[tokio::test] + async fn test_ipv6_without_port() { + let result = get_socket_addr("[::1]", 443).await; + assert!(result.is_ok()); + let addr = result.unwrap(); + assert_eq!(addr.port(), 443); + assert_eq!(addr.ip().to_string(), "::1"); + } + + #[tokio::test] + async fn test_invalid_port() { + let result = get_socket_addr("127.0.0.1:99999", 80).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_empty_string() { + let result = get_socket_addr("", 80).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_whitespace_trimming() { + let result = get_socket_addr(" 127.0.0.1:8080 ", 80).await; + assert!(result.is_ok()); + let addr = result.unwrap(); + assert_eq!(addr.port(), 8080); + } + + #[tokio::test] + async fn test_domain_name_with_port() { + // This test will only pass if localhost resolves + let result = get_socket_addr("localhost:8080", 80).await; + if result.is_ok() { + let addr = result.unwrap(); + assert_eq!(addr.port(), 8080); + // localhost should resolve to 127.0.0.1 or ::1 + assert!(addr.ip().is_loopback()); + } + } + + #[tokio::test] + async fn test_domain_name_without_port() { + // This test will only pass if localhost resolves + let result = get_socket_addr("localhost", 443).await; + if result.is_ok() { + let addr = result.unwrap(); + assert_eq!(addr.port(), 443); + // localhost should resolve to 127.0.0.1 or ::1 + assert!(addr.ip().is_loopback()); + } + } + + #[tokio::test] + async fn test_parse_host_and_port() { + // IPv4 with port + assert_eq!( + parse_host_and_port("192.168.1.1:8080"), + Some(("192.168.1.1", "8080")) + ); + + // IPv6 with port + assert_eq!(parse_host_and_port("[::1]:8080"), Some(("::1", "8080"))); + + // Hostname with port + assert_eq!( + parse_host_and_port("example.com:443"), + Some(("example.com", "443")) + ); + + // No port + assert_eq!(parse_host_and_port("192.168.1.1"), None); + assert_eq!(parse_host_and_port("example.com"), None); + + // Invalid cases + assert_eq!(parse_host_and_port(":"), None); + assert_eq!(parse_host_and_port("192.168.1.1:"), None); + } +} -- cgit From 5417544f09c9b1964067d8e5597cce5873b76ee3 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 27 Oct 2025 17:56:12 +0800 Subject: Implement jv_direct connection handling --- src/bin/jv.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/src/bin/jv.rs b/src/bin/jv.rs index edab4ef..6df4b0a 100644 --- a/src/bin/jv.rs +++ b/src/bin/jv.rs @@ -1,8 +1,10 @@ -use std::{env::current_dir, net::SocketAddr, path::PathBuf, str::FromStr}; +use std::{env::current_dir, net::SocketAddr, path::PathBuf}; use just_enough_vcs::{ - utils::cfg_file::config::ConfigFile, + system::action_system::action::ActionContext, + utils::{cfg_file::config::ConfigFile, tcp_connection::instance::ConnectionInstance}, vcs::{ + constants::PORT, current::current_local_path, data::{ local::{LocalWorkspace, config::LocalConfig}, @@ -14,13 +16,14 @@ use just_enough_vcs::{ use clap::{Parser, Subcommand, arg, command}; use just_enough_vcs::{ - system::action_system::action::ActionContext, utils::tcp_connection::error::TcpTargetError, vcs::{actions::local_actions::proc_set_upstream_vault_action, registry::client_registry}, }; -use just_enough_vcs_cli::utils::{lang_selector::current_locales, md_colored::md}; +use just_enough_vcs_cli::utils::{ + lang_selector::current_locales, md_colored::md, socket_addr_helper, +}; use rust_i18n::{set_locale, t}; -use tokio::fs; +use tokio::{fs, net::TcpSocket}; // Import i18n files rust_i18n::i18n!("locales", fallback = "en"); @@ -655,7 +658,14 @@ async fn jv_direct(args: DirectArgs) { return; } }; - let ctx = ActionContext::local(); + + let Some(instance) = connect(upstream).await else { + // Since connect() function already printed error messages, we only handle the return here + return; + }; + + let ctx = ActionContext::local().insert_instance(instance); + match proc_set_upstream_vault_action(&pool, ctx, upstream).await { Err(e) => handle_err(e), _ => {} @@ -741,3 +751,32 @@ fn connection(err: String) -> (ErrorText, ErrorTip, HasTip) { true, ) } + +async fn connect(upstream: SocketAddr) -> Option { + // Create Socket + let socket = if upstream.is_ipv4() { + match TcpSocket::new_v4() { + Ok(socket) => socket, + Err(_) => { + eprintln!("{}", t!("jv.fail.create_socket").trim()); + return None; + } + } + } else { + match TcpSocket::new_v6() { + Ok(socket) => socket, + Err(_) => { + eprintln!("{}", t!("jv.fail.create_socket").trim()); + return None; + } + } + }; + + // Connect + let Ok(stream) = socket.connect(upstream).await else { + eprintln!("{}", t!("jv.fail.connection_failed").trim()); + return None; + }; + + Some(ConnectionInstance::from(stream)) +} -- cgit From 28bdbb4ba457eb9e0707d2760f5d042ca6b7db68 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 27 Oct 2025 17:57:24 +0800 Subject: Add output for jvv service listen command results --- locales/help_docs/en.yml | 11 +++++++---- locales/help_docs/zh-CN.yml | 12 +++++++----- src/bin/jvv.rs | 16 +++++++++++++--- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/locales/help_docs/en.yml b/locales/help_docs/en.yml index c72754c..5dfcb9d 100644 --- a/locales/help_docs/en.yml +++ b/locales/help_docs/en.yml @@ -61,7 +61,6 @@ jvv: fail: jvcs: "JustEnoughVCS Error: %{err}" - no_vault_here: No vault found here tokio: @@ -80,9 +79,7 @@ jvv: If you wish to use this directory as a vault, please use jvv init --help to view related help vault_init_failed: Failed to initialize vault! - member_ids_failed: Failed to get member IDs! - ref_sheet_not_found: Reference sheet `ref` not found, but it should exist! create: @@ -95,6 +92,11 @@ jvv: register: Failed to create member! Please check if a member with the same name already exists list: Failed to get member ID! + service: + listen_done: | + Server forced to close due to error! + Error message: %{error} + success: here: info: | @@ -122,7 +124,8 @@ jvv: status_key_registered: (Registered) service: - listen: Listening for client connections in vault `%{path}` ... + listen_start: Listening for client connections in vault `%{path}` ... + listen_done: Server shutdown! jv: help: | diff --git a/locales/help_docs/zh-CN.yml b/locales/help_docs/zh-CN.yml index 442e7f9..548d029 100644 --- a/locales/help_docs/zh-CN.yml +++ b/locales/help_docs/zh-CN.yml @@ -59,7 +59,6 @@ jvv: fail: jvcs: JustEnoughVCS 错误:%{err} - no_vault_here: 此处并没有找到库文件 tokio: @@ -78,9 +77,7 @@ jvv: 若您希望将该目录作为库,请使用 jvv init --help 查看相关帮助 vault_init_failed: 初始化库失败! - member_ids_failed: 获得成员 ID 失败! - ref_sheet_not_found: 未找到参照表 `ref`,该参照表理应存在! create: @@ -93,6 +90,11 @@ jvv: register: 创建成员失败!请检查是否存在同名的成员 list: 获取成员 ID 失败! + service: + listen_done: | + 服务端被错误强制关闭! + 错误信息:%{error} + success: here: info: | @@ -102,7 +104,6 @@ jvv: **提示**:若要启动服务端,请运行 jvv service listen create: 成功在 `%{name}` 创建库! - init: 在此处初始化库成功! member: @@ -118,7 +119,8 @@ jvv: status_key_registered: (公钥存在) service: - listen: 正在库 `%{path}` 监听来自客户端的连接 ... + listen_start: 正在库 `%{path}` 监听来自客户端的连接 ... + listen_done: 服务端运行结束! jv: help: | diff --git a/src/bin/jvv.rs b/src/bin/jvv.rs index 1886daa..f6fbeaa 100644 --- a/src/bin/jvv.rs +++ b/src/bin/jvv.rs @@ -19,7 +19,7 @@ use just_enough_vcs::{ use just_enough_vcs_cli::utils::{ build_env_logger::build_env_logger, lang_selector::current_locales, md_colored::md, }; -use log::info; +use log::{error, info}; use rust_i18n::{set_locale, t}; use tokio::fs::{self}; @@ -586,7 +586,7 @@ async fn jvv_service_listen(args: ListenArgs) { info!( "{}", t!( - "jvv.success.service.listen", + "jvv.success.service.listen_start", path = match current_vault.file_name() { Some(name) => name.to_string_lossy(), None => std::borrow::Cow::Borrowed("unknown"), @@ -595,5 +595,15 @@ async fn jvv_service_listen(args: ListenArgs) { ) } - let _ = server_entry(current_vault).await; + match server_entry(current_vault).await { + Ok(_) => { + info!("{}", t!("jvv.success.service.listen_done").trim()); + } + Err(e) => { + error!( + "{}", + t!("jvv.fail.service.listen_done", error = e.to_string()).trim() + ); + } + } } -- cgit From 189122ad8f0aa8f378a69c921eb2bafb51ae351f Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 27 Oct 2025 17:57:48 +0800 Subject: Update translation for error messages --- locales/help_docs/en.yml | 6 ++++-- locales/help_docs/zh-CN.yml | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/locales/help_docs/en.yml b/locales/help_docs/en.yml index 5dfcb9d..2d4bbdc 100644 --- a/locales/help_docs/en.yml +++ b/locales/help_docs/en.yml @@ -338,8 +338,7 @@ jv: parse: str_to_sockaddr: | - Could not recognize *`%{str}`* as an IP address - This is a problem with your input syntax! Please check **the content you entered** + *`%{str}`* is not a valid IP address, please check your input! action_operation_fail: main: | @@ -403,6 +402,9 @@ jv: read_cfg: Failed to read local workspace configuration file! write_cfg: Failed to write changes to local workspace configuration file! + create_socket: Failed to create TCP socket! + connection_failed: Failed to connect to target server! + success: account: as: Successfully switched this workspace's account to `%{account}` diff --git a/locales/help_docs/zh-CN.yml b/locales/help_docs/zh-CN.yml index 548d029..417d2d4 100644 --- a/locales/help_docs/zh-CN.yml +++ b/locales/help_docs/zh-CN.yml @@ -347,8 +347,7 @@ jv: parse: str_to_sockaddr: | - 无法将 *`%{str}`* 识别为IP地址 - 这是您的输入语法问题!请检查 **输入的内容** + 无法将 *`%{str}`* 识别为IP地址,请检查您的输入! action_operation_fail: main: | @@ -412,6 +411,9 @@ jv: read_cfg: 无法读取本地工作区的配置文件! write_cfg: 无法将修改写入本地工作区的配置文件! + create_socket: 无法创建 TCP 套接字! + connection_failed: 无法连接至目标服务器! + success: account: as: 成功将此工作区的账户切换至 `%{account}` -- cgit From 3a667bc2fec10bb54bf75155aac3fbb413e61c3d Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 27 Oct 2025 17:58:00 +0800 Subject: Add target directory to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 1dc8293..405606d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ # All temp directory **/.temp/ + +# Target directory +/target/ -- cgit From 40551da6ec3cc29de0c94f600b2fe55dd0749ef8 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 29 Oct 2025 15:26:55 +0800 Subject: Consolidate thiserror dependency versions --- Cargo.lock | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 359c4cb..6fb294c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1219,7 +1219,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.17", + "thiserror", ] [[package]] @@ -1650,38 +1650,18 @@ dependencies = [ "rsa", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror", "tokio", "uuid", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.17", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -1867,7 +1847,7 @@ dependencies = [ "serde_json", "string_proc", "tcp_connection", - "thiserror 1.0.69", + "thiserror", "tokio", "vcs_data", ] -- cgit From ed027187568c91a14c545c1962a219552e4654e7 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 29 Oct 2025 15:27:11 +0800 Subject: Add input utility functions for user confirmation --- src/utils/input.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/utils/input.rs diff --git a/src/utils/input.rs b/src/utils/input.rs new file mode 100644 index 0000000..217cede --- /dev/null +++ b/src/utils/input.rs @@ -0,0 +1,52 @@ +/// Confirm the current operation +/// Waits for user input of 'y' or 'n' +pub async fn confirm_hint(text: impl Into) -> bool { + use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader}; + + let prompt = text.into().trim().to_string(); + + let mut stdout = io::stdout(); + let mut stdin = BufReader::new(io::stdin()); + + stdout + .write_all(prompt.as_bytes()) + .await + .expect("Failed to write prompt"); + stdout.flush().await.expect("Failed to flush stdout"); + + let mut input = String::new(); + stdin + .read_line(&mut input) + .await + .expect("Failed to read input"); + + input.trim().eq_ignore_ascii_case("y") +} + +/// Confirm the current operation, or execute a closure if rejected +/// Waits for user input of 'y' or 'n' +/// If 'n' is entered, executes the provided closure and returns false +pub async fn confirm_hint_or(text: impl Into, on_reject: F) -> bool +where + F: FnOnce(), +{ + let confirmed = confirm_hint(text).await; + if !confirmed { + on_reject(); + } + confirmed +} + +/// Confirm the current operation, and execute a closure if confirmed +/// Waits for user input of 'y' or 'n' +/// If 'y' is entered, executes the provided closure and returns true +pub async fn confirm_hint_then(text: impl Into, on_confirm: F) -> bool +where + F: FnOnce(), +{ + let confirmed = confirm_hint(text).await; + if confirmed { + on_confirm(); + } + confirmed +} -- cgit From 40507c8e3b5751cd0488ae0bdcf11909eeff520d Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 29 Oct 2025 15:27:26 +0800 Subject: Update localization files for new workspace management features - Add common confirmation prompt - Improve service command documentation with port option - Add workspace staining/unstaining functionality - Simplify error messages and improve user guidance - Add success and warning messages for direct/unstain operations --- locales/help_docs/en.yml | 85 ++++++++++++++++++++++----------------------- locales/help_docs/zh-CN.yml | 80 +++++++++++++++++++++--------------------- 2 files changed, 81 insertions(+), 84 deletions(-) diff --git a/locales/help_docs/en.yml b/locales/help_docs/en.yml index 2d4bbdc..b886cfa 100644 --- a/locales/help_docs/en.yml +++ b/locales/help_docs/en.yml @@ -1,3 +1,7 @@ +common: + confirm: | + Confirm to perform the above operation? (Y/n): + jvv: help: | **JustEnoughVCS Upstream Vault Commands** @@ -56,8 +60,9 @@ jvv: service: | **Service Management** **Usage**: - jvv service listen - Start the vault server in the current vault to accept client connections - You can use --no-log to disable log output + jvv service listen - Start the server in the current vault + You can use --port to set the listening port, + use --no-log to disable log output fail: jvcs: "JustEnoughVCS Error: %{err}" @@ -332,53 +337,21 @@ jv: Built-in documentation includes JustEnoughVCS usage guides, collaboration paradigms, and best practices. + confirm: + direct: | + Do you want to direct the current local workspace to the upstream vault %{upstream}? + fail: path_not_found: | The directory `%{path}` cannot be found! parse: str_to_sockaddr: | - *`%{str}`* is not a valid IP address, please check your input! - - action_operation_fail: - main: | - This error is from the JustEnoughVCS core component - **Error Message**: %{err} - - type_connection: | - Based on the returned error context, this is a **network connection** issue - **Please check**: - 1. Whether your network connection is working properly - 2. Whether you have permission to connect to this address - - type_auth: | - Based on the returned error context, this is a **permission** issue - This indicates you don't have sufficient permissions to perform this operation. - - type_fsio: | - Based on the returned error context, this is a **file read/write** related issue - **Please check**: - 1. Whether your disk has sufficient space and is operating normally - 2. Whether your Local Workspace files are being used by other processes - - type_serialize: | - Based on the returned error context, this is a **serialization or deserialization** related issue - **Please check** - 1. Whether the parameter format you passed meets the requirements for serialization/deserialization - 2. Whether this version of JustEnoughVCS client/server supports this type of serialization - - type_other: | - Unfortunately, based on the returned context information, - this is not an expected error. - Please submit an Issue at the JustEnoughVCS Git Repository - - JustEnoughVCS needs your feedback, which will make this project more "JustEnough" - - info_contact_admin: | - If necessary, please contact the **administrator** of the Upstream Vault. - If you are the **administrator** and confirm this issue affects server operation, please: - 1. Check if your JustEnoughVCS server is up to date - 2. Submit an Issue at the JustEnoughVCS Git Repository + Error: %{err} + Cannot recognize *`%{str}`* as a valid address, please check your input! + + from_just_version_control: | + **Error**: `%{err}` (This error is provided by JustEnoughVCS) account: no_user_dir: Cannot find user directory! @@ -405,6 +378,14 @@ jv: create_socket: Failed to create TCP socket! connection_failed: Failed to connect to target server! + unstain: | + The current workspace is not stained, no need to unstain + + warn: + unstain: | + This operation will disconnect the current workspace from the upstream vault `%{upstream}` + If you reconnect to a vault with a mismatched identifier, it will cause serious problems, please operate with caution! + success: account: as: Successfully switched this workspace's account to `%{account}` @@ -418,3 +399,21 @@ jv: move_key: Successfully moved the private key to the account directory! create: Successfully created local workspace! init: Successfully created workspace here! + unstain: | + Successfully unstained! + The current workspace no longer belongs to any upstream vault, please direct to a new upstream vault before working + **Tip**: Use `jv direct ` to redirect to a new upstream vault + + result: + common: + authroize_failed: | + Authentication failed: %{err}! + + direct: + directed_and_stained: | + Successfully directed to upstream vault `%{upstream}`! + Workspace has been **stained**, ready to start working! + + already_stained: | + Current workspace is already stained and cannot be directed to other upstream vaults with different identifiers + Please use `jv unstain` to remove the stain first diff --git a/locales/help_docs/zh-CN.yml b/locales/help_docs/zh-CN.yml index 417d2d4..751a2d3 100644 --- a/locales/help_docs/zh-CN.yml +++ b/locales/help_docs/zh-CN.yml @@ -1,3 +1,7 @@ +common: + confirm: | + 是否执行上述操作?(Y/n): + jvv: help: | **JustEnoughVCS 上游库命令** @@ -55,7 +59,7 @@ jvv: **服务设置相关** **用法**: jvv service listen - 在当前库中启动服务器,以接受客户端连接 - 您可以使用 --no-log 来禁用日志输出 + 您可以使用 --port 来设定监听的端口,使用 --no-log 禁用日志输出 fail: jvcs: JustEnoughVCS 错误:%{err} @@ -341,52 +345,21 @@ jv: 内建文档包含 JustEnoughVCS 的使用指南、协作范式和最佳实践 + confirm: + direct: | + 是否需要将当前本地工作区定向到上游库 %{upstream}? + fail: path_not_found: | 您给出的目录 `%{path}` 无法找到! parse: str_to_sockaddr: | - 无法将 *`%{str}`* 识别为IP地址,请检查您的输入! - - action_operation_fail: - main: | - 此错误来自 JustEnoughVCS 核心组件 - **错误信息**:%{err} - - type_connection: | - 根据返回的错误上下文,这是一个**网络连接**问题 - **请检查**: - 1. 您的网络连接是否通畅 - 2. 您是否有权限连接至该地址 - - type_auth: | - 根据返回的错误上下文,这是一个**权限**问题 - 这说明您没有足够的权限去做此项操作。 - - type_fsio: | - 根据返回的错误上下文,这是一个**文件读写**相关的问题 - **请检查**: - 1. 您的磁盘是否有足够的空间,并且能正常流畅地运转 - 2. 您的本地工作区文件是否被其他进程占用 - - type_serialize: | - 根据返回的错误上下文,这是一个**序列化或反序列化**相关的问题 - **请检查** - 1. 您传入的参数格式是否符合序列化或反序列化的要求 - 2. 该版本的 JustEnoughVCS 客户端/服务端 是否支持该类型的序列化 - - type_other: | - 很遗憾,根据返回的上下文信息,这并不是一个符合预期的错误。 - 请前往 JustEnoughVCS 所在的 Git 版本库提交 Issue - - JustEnoughVCS 需要您的反馈,这会让该项目变得更加 "JustEnough" - - info_contact_admin: | - 如有必要,请联系上游库的 **管理员**。 - 若您作为 **管理员**,在确认该问题影响服务器运作,请 - 1. 检查您的 JustEnoughVCS 服务端是否最新 - 2. 前往 JustEnoughVCS 所在的 Git 版本库提交 Issue + 错误:%{err} + 无法将 *`%{str}`* 识别为有效地址,请检查您的输入! + + from_just_version_control: | + **错误**:`%{err}`(该错误由 JustEnoughVCS 提供) account: no_user_dir: 无法找到用户目录! @@ -414,6 +387,14 @@ jv: create_socket: 无法创建 TCP 套接字! connection_failed: 无法连接至目标服务器! + unstain: | + 当前工作区并未被染色,无需祛色 + + warn: + unstain: | + 此操作将会断开当前工作区与上游库 `%{upstream}` 的关联 + 若重新连接至标识不匹配的库,会导致严重的问题,请谨慎操作! + success: account: as: 成功将此工作区的账户切换至 `%{account}` @@ -427,3 +408,20 @@ jv: move_key: 成功将该私钥移动至账户目录! create: 成功创建本地工作区! init: 成功在此处创建工作区! + unstain: | + 成功祛色! + 当前工作区不再属于任何上游库,请工作前定向至新的上游库 + **提示**:使用 `jv direct <上游库地址>` 重新定向至新的上游库 + + result: + common: + authroize_failed: 身份认证失败:%{err}! + + direct: + directed_and_stained: | + 成功定向到上游库 `%{upstream}`! + 工作区已被 **染色**,现可开始工作! + + already_stained: | + 当前工作区已被染色,无法定向其他不同标识的上游库 + 请先使用 jv unstain 祛色 -- cgit From fdffd0d081465fa4d08645f7e1b546e1addb2ef9 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 29 Oct 2025 15:28:04 +0800 Subject: Add confirmation options and alias commands - Add -C/--confirm flags to skip confirmation for direct/unstain commands - Add command aliases for account and vault subcommands (+/-/ls) - Improve error handling with better error messages - Implement unstain command functionality - Add port option to vault listen command - Refactor error handling to use centralized function --- src/bin/jv.rs | 144 ++++++++++++++++++++++++++++----------------------------- src/bin/jvv.rs | 27 ++++++++++- src/utils.rs | 1 + 3 files changed, 98 insertions(+), 74 deletions(-) diff --git a/src/bin/jv.rs b/src/bin/jv.rs index 6df4b0a..a8850b7 100644 --- a/src/bin/jv.rs +++ b/src/bin/jv.rs @@ -1,9 +1,10 @@ -use std::{env::current_dir, net::SocketAddr, path::PathBuf}; +use std::{env::current_dir, net::SocketAddr, path::PathBuf, process::exit}; use just_enough_vcs::{ system::action_system::action::ActionContext, utils::{cfg_file::config::ConfigFile, tcp_connection::instance::ConnectionInstance}, vcs::{ + actions::local_actions::SetUpstreamVaultActionResult, constants::PORT, current::current_local_path, data::{ @@ -20,7 +21,7 @@ use just_enough_vcs::{ vcs::{actions::local_actions::proc_set_upstream_vault_action, registry::client_registry}, }; use just_enough_vcs_cli::utils::{ - lang_selector::current_locales, md_colored::md, socket_addr_helper, + input::confirm_hint_or, lang_selector::current_locales, md_colored::md, socket_addr_helper, }; use rust_i18n::{set_locale, t}; use tokio::{fs, net::TcpSocket}; @@ -109,10 +110,11 @@ enum AccountManage { Help, /// Register a member to this computer + #[command(alias = "+")] Add(AccountAddArgs), /// Remove a account from this computer - #[command(alias = "rm")] + #[command(alias = "rm", alias = "-")] Remove(AccountRemoveArgs), /// List all accounts in this computer @@ -266,6 +268,10 @@ struct DirectArgs { /// Upstream vault address upstream: String, + + /// Whether to skip confirmation + #[arg(short = 'C', long)] + confirm: bool, } #[derive(Parser, Debug)] @@ -273,6 +279,10 @@ struct UnstainArgs { /// Show help information #[arg(short, long)] help: bool, + + /// Whether to skip confirmation + #[arg(short = 'C', long)] + confirm: bool, } #[derive(Parser, Debug)] @@ -644,15 +654,24 @@ async fn jv_account_move_key(user_dir: UserDirectory, args: MoveKeyToAccountArgs } async fn jv_direct(args: DirectArgs) { + if !args.confirm { + println!( + "{}", + t!("jv.confirm.direct", upstream = args.upstream).trim() + ); + confirm_hint_or(t!("common.confirm"), || exit(1)).await; + } + let pool = client_registry::client_action_pool(); let upstream = match socket_addr_helper::get_socket_addr(&args.upstream, PORT).await { Ok(result) => result, - Err(_) => { + Err(e) => { eprintln!( "{}", md(t!( "jv.fail.parse.str_to_sockaddr", - str = &args.upstream.trim() + str = &args.upstream.trim(), + err = e )) ); return; @@ -668,88 +687,69 @@ async fn jv_direct(args: DirectArgs) { match proc_set_upstream_vault_action(&pool, ctx, upstream).await { Err(e) => handle_err(e), - _ => {} + Ok(result) => match result { + SetUpstreamVaultActionResult::DirectedAndStained => { + println!( + "{}", + md(t!( + "jv.result.direct.directed_and_stained", + upstream = upstream + )) + ) + } + SetUpstreamVaultActionResult::AlreadyStained => { + eprintln!("{}", md(t!("jv.result.direct.already_stained"))) + } + SetUpstreamVaultActionResult::AuthorizeFailed(e) => { + println!( + "{}", + md(t!("jv.result.direct.directed_and_stained", err = e)) + ) + } + }, }; } -async fn jv_unstain(_args: UnstainArgs) { - todo!() -} - -async fn jv_docs(_args: DocsArgs) { - todo!() -} - -pub fn handle_err(err: TcpTargetError) { - let e: Option<(String, String, bool)> = match err { - TcpTargetError::Io(err) => Some(fsio(err)), - TcpTargetError::File(err) => Some(fsio(err)), - - TcpTargetError::Serialization(err) => Some(serialize(err)), - - TcpTargetError::Authentication(err) => Some(auth(err)), - - TcpTargetError::Network(err) => Some(connection(err)), - TcpTargetError::Timeout(err) => Some(connection(err)), - TcpTargetError::Protocol(err) => Some(connection(err)), +async fn jv_unstain(args: UnstainArgs) { + let Some(_local_dir) = current_local_path() else { + eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); + return; + }; - _ => Some(( - err.to_string(), - md(t!("jv.fail.action_operation_fail.type_other")), - false, - )), + let Ok(mut local_cfg) = LocalConfig::read().await else { + eprintln!("{}", md(t!("jv.fail.read_cfg"))); + return; }; - if let Some((err_text, err_tip, has_tip)) = e { - eprintln!( - "{}\n{}", - md(t!("jv.fail.action_operation_fail.main", err = err_text)), - err_tip, - ); + if !local_cfg.stained() { + eprintln!("{}", md(t!("jv.fail.unstain"))); + return; + } - if has_tip { - eprintln!( - "{}", - md(t!("jv.fail.action_operation_fail.info_contact_admin")) - ) - } + if !args.confirm { + println!( + "{}", + md(t!("jv.warn.unstain", upstream = local_cfg.vault_addr())) + ); + confirm_hint_or(t!("common.confirm"), || exit(1)).await; } -} -type ErrorText = String; -type ErrorTip = String; -type HasTip = bool; + local_cfg.unstain(); -fn fsio(err: String) -> (ErrorText, ErrorTip, HasTip) { - ( - err, - md(t!("jv.fail.action_operation_fail.type_fsio")).to_string(), - true, - ) -} + let Ok(_) = LocalConfig::write(&local_cfg).await else { + eprintln!("{}", t!("jv.fail.write_cfg").trim()); + return; + }; -fn serialize(err: String) -> (ErrorText, ErrorTip, HasTip) { - ( - err, - md(t!("jv.fail.action_operation_fail.type_serialize")).to_string(), - true, - ) + println!("{}", md(t!("jv.success.unstain"))); } -fn auth(err: String) -> (ErrorText, ErrorTip, HasTip) { - ( - err, - md(t!("jv.fail.action_operation_fail.type_auth")).to_string(), - true, - ) +async fn jv_docs(_args: DocsArgs) { + todo!() } -fn connection(err: String) -> (ErrorText, ErrorTip, HasTip) { - ( - err, - md(t!("jv.fail.action_operation_fail.type_connection")).to_string(), - true, - ) +pub fn handle_err(err: TcpTargetError) { + eprintln!("{}", md(t!("jv.fail.from_just_version_control", err = err))) } async fn connect(upstream: SocketAddr) -> Option { diff --git a/src/bin/jvv.rs b/src/bin/jvv.rs index f6fbeaa..57e9dbf 100644 --- a/src/bin/jvv.rs +++ b/src/bin/jvv.rs @@ -41,32 +41,42 @@ struct JustEnoughVcsVault { #[derive(Subcommand, Debug)] enum JustEnoughVcsVaultCommand { /// Get vault info in the current directory + #[command(alias = "-H")] Here(HereArgs), /// Create a new directory and initialize a vault + #[command(alias = "-c")] Create(CreateVaultArgs), /// Create a vault in the current directory + #[command(alias = "-i")] Init(InitVaultArgs), /// Member manage - #[command(subcommand)] + #[command(subcommand, alias = "-m")] Member(MemberManage), /// Manage service #[command(subcommand)] Service(ServiceManage), + + // Short commands + #[command(alias = "-l", alias = "listen")] + ServiceListen(ListenArgs), } #[derive(Subcommand, Debug)] enum MemberManage { /// Register a member to the vault + #[command(alias = "+")] Register(MemberRegisterArgs), /// Remove a member from the vault + #[command(alias = "-")] Remove(MemberRemoveArgs), /// List all members in the vault + #[command(alias = "ls")] List(MemberListArgs), /// Show help information @@ -144,6 +154,10 @@ struct ListenArgs { /// Disable logging #[arg(short, long)] no_log: bool, + + /// Custom port + #[arg(short, long)] + port: Option, } #[tokio::main] @@ -246,6 +260,14 @@ async fn main() { return; } }, + // Short commands + JustEnoughVcsVaultCommand::ServiceListen(listen_args) => { + if listen_args.help { + println!("{}", md(t!("jvv.service"))); + return; + } + jvv_service_listen(listen_args).await; + } } } @@ -595,7 +617,8 @@ async fn jvv_service_listen(args: ListenArgs) { ) } - match server_entry(current_vault).await { + let port = if let Some(port) = args.port { port } else { 0 }; + match server_entry(current_vault, port).await { Ok(_) => { info!("{}", t!("jvv.success.service.listen_done").trim()); } diff --git a/src/utils.rs b/src/utils.rs index e034bcd..dfa6926 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,5 @@ pub mod build_env_logger; +pub mod input; pub mod lang_selector; pub mod md_colored; pub mod socket_addr_helper; -- cgit From 0c0499abfb94d57d9b81c63b3df6e7e5e42a570d Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 29 Oct 2025 16:23:02 +0800 Subject: Apply clippy suggestion --- src/bin/jvv.rs | 17 +++++++---------- src/utils/socket_addr_helper.rs | 10 ++++------ 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/bin/jvv.rs b/src/bin/jvv.rs index 57e9dbf..6bcf5c0 100644 --- a/src/bin/jvv.rs +++ b/src/bin/jvv.rs @@ -311,12 +311,11 @@ async fn jvv_here(_args: HereArgs) { if let Ok(mut entries) = fs::read_dir(&virtual_file_root).await { while let Ok(Some(entry)) = entries.next_entry().await { - if let Ok(metadata) = entry.metadata().await { - if metadata.is_file() { + if let Ok(metadata) = entry.metadata().await + && metadata.is_file() { num_vf += 1; total_size += metadata.len(); } - } } } @@ -394,12 +393,11 @@ async fn jvv_init(_args: InitVaultArgs) { return; } }; - if let Ok(mut entries) = current_dir.read_dir() { - if entries.next().is_some() { + if let Ok(mut entries) = current_dir.read_dir() + && entries.next().is_some() { eprintln!("{}", t!("jvv.fail.init.not_empty")); return; } - } // Setup vault let vault_name = match current_dir.file_name() { @@ -460,12 +458,11 @@ async fn jvv_create(args: CreateVaultArgs) { return; } - if let Ok(mut entries) = target_dir.read_dir() { - if entries.next().is_some() { + if let Ok(mut entries) = target_dir.read_dir() + && entries.next().is_some() { eprintln!("{}", t!("jvv.fail.create.not_empty")); return; } - } // Setup vault let vault_name = pascal_case!(args.vault_name); @@ -617,7 +614,7 @@ async fn jvv_service_listen(args: ListenArgs) { ) } - let port = if let Some(port) = args.port { port } else { 0 }; + let port = args.port.unwrap_or_default(); match server_entry(current_vault, port).await { Ok(_) => { info!("{}", t!("jvv.success.service.listen_done").trim()); diff --git a/src/utils/socket_addr_helper.rs b/src/utils/socket_addr_helper.rs index c6805da..fd7b346 100644 --- a/src/utils/socket_addr_helper.rs +++ b/src/utils/socket_addr_helper.rs @@ -17,7 +17,7 @@ pub async fn get_socket_addr( ) })?; - return resolve_to_socket_addr(&host, port).await; + return resolve_to_socket_addr(host, port).await; } // No port specified, use default port @@ -26,15 +26,13 @@ pub async fn get_socket_addr( /// Parse host and port from address string fn parse_host_and_port(address: &str) -> Option<(&str, &str)> { - if address.starts_with('[') { - if let Some(close_bracket) = address.find(']') { - if close_bracket + 1 < address.len() && address.as_bytes()[close_bracket + 1] == b':' { + if address.starts_with('[') + && let Some(close_bracket) = address.find(']') + && close_bracket + 1 < address.len() && address.as_bytes()[close_bracket + 1] == b':' { let host = &address[1..close_bracket]; let port = &address[close_bracket + 2..]; return Some((host, port)); } - } - } // Handle IPv4 addresses and hostnames with ports if let Some(colon_pos) = address.rfind(':') { -- cgit From 251218ed09d640d7af44f26c6917d8fdb90fc263 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 29 Oct 2025 16:34:43 +0800 Subject: Add input_with_editor function for text editing This function opens the system editor with default text in a cache file, reads back the modified content after editing, and removes comment lines. --- src/utils/input.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/utils/input.rs b/src/utils/input.rs index 217cede..a728c77 100644 --- a/src/utils/input.rs +++ b/src/utils/input.rs @@ -1,3 +1,5 @@ +use tokio::{fs, process::Command}; + /// Confirm the current operation /// Waits for user input of 'y' or 'n' pub async fn confirm_hint(text: impl Into) -> bool { @@ -50,3 +52,54 @@ where } confirmed } + +/// Input text using the system editor +/// Opens the system editor (from EDITOR environment variable) with default text in a cache file, +/// then reads back the modified content after the editor closes, removing comment lines +pub async fn input_with_editor( + default_text: impl AsRef, + cache_file: impl AsRef, + comment_char: impl AsRef, +) -> Result { + let cache_path = cache_file.as_ref(); + let default_content = default_text.as_ref(); + let comment_prefix = comment_char.as_ref(); + + // Write default text to cache file + fs::write(cache_path, default_content).await?; + + // Get editor from environment variable + let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string()); + + // Open editor with cache file + let status = Command::new(editor).arg(cache_path).status().await?; + + if !status.success() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Editor exited with non-zero status", + )); + } + + // Read the modified content + let content = fs::read_to_string(cache_path).await?; + + // Remove comment lines and trim + let processed_content: String = content + .lines() + .filter_map(|line| { + let trimmed = line.trim(); + if trimmed.starts_with(comment_prefix) { + None + } else { + Some(line) + } + }) + .collect::>() + .join("\n"); + + // Delete the cache file + let _ = fs::remove_file(cache_path).await; + + Ok(processed_content) +} -- cgit From 3a3f40b2abbaa47063cdc3aeb0149e3d02276c1e Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Thu, 30 Oct 2025 09:38:19 +0800 Subject: Simplify commands and improve jv error handling - Add help command alias for jv - Improve parser error message formatting - Fix code formatting in jvv commands --- locales/help_docs/en.yml | 4 ++++ locales/help_docs/zh-CN.yml | 4 ++++ src/bin/jv.rs | 9 ++++++++- src/bin/jvv.rs | 27 +++++++++++++++------------ 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/locales/help_docs/en.yml b/locales/help_docs/en.yml index b886cfa..5a12f8f 100644 --- a/locales/help_docs/en.yml +++ b/locales/help_docs/en.yml @@ -346,6 +346,10 @@ jv: The directory `%{path}` cannot be found! parse: + parser_failed: | + Incorrect command input! + Please use `jv -h` to view help + str_to_sockaddr: | Error: %{err} Cannot recognize *`%{str}`* as a valid address, please check your input! diff --git a/locales/help_docs/zh-CN.yml b/locales/help_docs/zh-CN.yml index 751a2d3..4d3212e 100644 --- a/locales/help_docs/zh-CN.yml +++ b/locales/help_docs/zh-CN.yml @@ -354,6 +354,10 @@ jv: 您给出的目录 `%{path}` 无法找到! parse: + parser_failed: | + 错误的命令输入! + 请使用 `jv -h` 查看帮助 + str_to_sockaddr: | 错误:%{err} 无法将 *`%{str}`* 识别为有效地址,请检查您的输入! diff --git a/src/bin/jv.rs b/src/bin/jv.rs index a8850b7..95176b5 100644 --- a/src/bin/jv.rs +++ b/src/bin/jv.rs @@ -44,6 +44,9 @@ struct JustEnoughVcsWorkspace { #[derive(Subcommand, Debug)] enum JustEnoughVcsWorkspaceCommand { + #[command(alias = "--help", alias = "-h")] + Help, + // Member management /// Manage your local accounts #[command(subcommand, alias = "acc")] @@ -302,11 +305,15 @@ async fn main() { colored::control::set_virtual_terminal(true).unwrap(); let Ok(parser) = JustEnoughVcsWorkspace::try_parse() else { - println!("{}", md(t!("jv.help"))); + println!("{}", md(t!("jv.fail.parse.parser_failed"))); return; }; match parser.command { + JustEnoughVcsWorkspaceCommand::Help => { + println!("{}", md(t!("jv.help"))); + } + JustEnoughVcsWorkspaceCommand::Account(account_manage) => { let user_dir = match UserDirectory::current_doc_dir() { Some(dir) => dir, diff --git a/src/bin/jvv.rs b/src/bin/jvv.rs index 6bcf5c0..8480029 100644 --- a/src/bin/jvv.rs +++ b/src/bin/jvv.rs @@ -312,10 +312,11 @@ async fn jvv_here(_args: HereArgs) { if let Ok(mut entries) = fs::read_dir(&virtual_file_root).await { while let Ok(Some(entry)) = entries.next_entry().await { if let Ok(metadata) = entry.metadata().await - && metadata.is_file() { - num_vf += 1; - total_size += metadata.len(); - } + && metadata.is_file() + { + num_vf += 1; + total_size += metadata.len(); + } } } @@ -394,10 +395,11 @@ async fn jvv_init(_args: InitVaultArgs) { } }; if let Ok(mut entries) = current_dir.read_dir() - && entries.next().is_some() { - eprintln!("{}", t!("jvv.fail.init.not_empty")); - return; - } + && entries.next().is_some() + { + eprintln!("{}", t!("jvv.fail.init.not_empty")); + return; + } // Setup vault let vault_name = match current_dir.file_name() { @@ -459,10 +461,11 @@ async fn jvv_create(args: CreateVaultArgs) { } if let Ok(mut entries) = target_dir.read_dir() - && entries.next().is_some() { - eprintln!("{}", t!("jvv.fail.create.not_empty")); - return; - } + && entries.next().is_some() + { + eprintln!("{}", t!("jvv.fail.create.not_empty")); + return; + } // Setup vault let vault_name = pascal_case!(args.vault_name); -- cgit