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(-) (limited to 'src') 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 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. --- src/bin/jv.rs | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) (limited to 'src') 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 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(-) (limited to 'src') 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 (limited to 'src') 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(-) (limited to 'src') 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 --- src/bin/jvv.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) (limited to 'src') 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 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 (limited to 'src') 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 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(-) (limited to 'src') 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(-) (limited to 'src') 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(+) (limited to 'src') 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 --- src/bin/jv.rs | 9 ++++++++- src/bin/jvv.rs | 27 +++++++++++++++------------ 2 files changed, 23 insertions(+), 13 deletions(-) (limited to 'src') 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