From ce77780ebcf548b0d823af7ae240364a56fa321a Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Fri, 20 Mar 2026 22:31:41 +0800 Subject: Remove legacy jv and jvv binaries and related files --- src/bin/jv.rs | 6179 -------------------------------------------------------- src/bin/jvv.rs | 749 ------- 2 files changed, 6928 deletions(-) delete mode 100644 src/bin/jv.rs delete mode 100644 src/bin/jvv.rs (limited to 'src') diff --git a/src/bin/jv.rs b/src/bin/jv.rs deleted file mode 100644 index f31284b..0000000 --- a/src/bin/jv.rs +++ /dev/null @@ -1,6179 +0,0 @@ -#![allow(clippy::all)] - -// -// -// ████████ ████████ -// ██▒▒▒▒▒▒▒▒██ ██▒▒▒▒▒▒▒▒██ -// ██ ▒▒██ ██▒▒ ██ █████ ██ ██ ██████ █████ -// ██ ▒▒████████▒▒ ██ ▒▒▒██ ██ ██ ██████ ██████ -// ██ ▒▒▒▒▒▒▒▒ ██ ██ ██ ██ ███▒▒▒█ █▒▒▒▒█ -// ██ ██ ██ ██ ██ ███ ▒ ████ ▒ -// ██ ██ ██ ██ ██ ███ ▒████ -// ██ ████ ████ ██ ██ ▒██ ██▒ ███ ▒▒▒██ -// ██ ████ ████ ██ █ ██ ██ ██ ███ █ ██ ██ -// ██ ████ ████ ██ █ ██ ▒████▒ ▒██████ ██████ -// ██ ▒▒▒▒ ▒▒▒▒ ██ ▒████ ▒██▒ ██████ ▒████▒ -// ██ ██ ▒▒▒▒ ▒▒ ▒▒▒▒▒▒ ▒▒▒▒ -// ██ ██████████ ██ -// ██ ██ ██ -// ████████████████████████████████ -// ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ -// -// The current module `jv.rs` is the legacy implementation, with only necessary fixes to maintain usability. -// The new implementation is located in `jvn.rs`, please refer to it. -// - -use colored::Colorize; -use just_enough_vcs::{ - data::compile_info::CoreCompileInfo, - lib::{ - constants::{ - CLIENT_FILE_TODOLIST, CLIENT_FILE_WORKSPACE, CLIENT_FOLDER_WORKSPACE_ROOT_NAME, - CLIENT_PATH_WORKSPACE_ROOT, PORT, VAULT_HOST_NAME, - }, - data::{ - local::{ - LocalWorkspace, - align_tasks::{AlignTaskName, AlignTasks}, - cached_sheet::CachedSheet, - latest_file_data::LatestFileData, - latest_info::LatestInfo, - modified_status::check_vault_modified, - workspace_analyzer::{AnalyzeResult, FromRelativePathBuf}, - workspace_config::LocalConfig, - }, - member::{Member, MemberId}, - sheet::{SheetData, SheetMappingMetadata}, - user::UserDirectory, - vault::{ - mapping_share::{Share, ShareMergeMode}, - virtual_file::{VirtualFileId, VirtualFileVersion}, - }, - }, - docs::{ASCII_YIZI, document, documents}, - env::{correct_current_dir, current_cfg_dir, current_local_path}, - registry::client_registry, - remote_actions::{ - content_manage::track_file::{ - CreateTaskResult, NextVersion, SyncTaskResult, TrackFileActionArguments, - TrackFileActionResult, UpdateDescription, UpdateTaskResult, VerifyFailReason, - proc_track_file_action, - }, - edit_right_manage::change_virtual_file_edit_right::{ - ChangeVirtualFileEditRightResult, EditRightChangeBehaviour, - proc_change_virtual_file_edit_right_action, - }, - mapping_manage::{ - edit_mapping::{ - EditMappingActionArguments, EditMappingActionResult, EditMappingOperations, - InvalidMoveReason, OperationArgument, proc_edit_mapping_action, - }, - merge_share_mapping::{ - MergeShareMappingActionResult, MergeShareMappingArguments, - proc_merge_share_mapping_action, - }, - share_mapping::{ - ShareMappingActionResult, ShareMappingArguments, proc_share_mapping_action, - }, - }, - sheet_manage::{ - drop_sheet::{DropSheetActionResult, proc_drop_sheet_action}, - make_sheet::{MakeSheetActionResult, proc_make_sheet_action}, - }, - workspace_manage::{ - set_upstream_vault::{ - SetUpstreamVaultActionResult, proc_set_upstream_vault_action, - }, - update_to_latest_info::{ - SyncCachedSheetFailReason, UpdateToLatestInfoResult, - proc_update_to_latest_info_action, - }, - }, - }, - }, - system::action_system::{action::ActionContext, action_pool::ActionPool}, - utils::{ - cfg_file::config::ConfigFile, data_struct::data_sort::quick_sort_with_cmp, sha1_hash, - tcp_connection::instance::ConnectionInstance, - }, -}; -use just_fmt::{ - fmt_path::{fmt_path, fmt_path_str}, - snake_case, -}; -use std::{ - collections::{BTreeMap, HashMap, HashSet}, - env::{current_dir, set_current_dir}, - io::Error, - net::SocketAddr, - path::PathBuf, - process::exit, - str::FromStr, - sync::Arc, - time::SystemTime, -}; - -use clap::{Parser, Subcommand}; -use cli_utils::legacy::{ - display::{SimpleTable, display_width, md, render_share_path_tree, size_str}, - env::{auto_update_outdate, current_locales, enable_auto_update}, - fs::move_across_partitions, - globber::{GlobItem, Globber}, - input::{confirm_hint, confirm_hint_or, input_with_editor, show_in_pager}, - push_version, socket_addr_helper, -}; -use just_enough_vcs::utils::tcp_connection::error::TcpTargetError; -use jvcli::{ - data::{ - compile_info::CompileInfo, - ipaddress_history::{get_recent_ip_address, insert_recent_ip_address}, - }, - legacy_json_output::{ - accounts::{AccountItem, AccountListJsonResult}, - align::{AlignJsonResult, AlignTaskMapping}, - analyzer_result::{AnalyzerJsonResult, ModifiedItem, ModifiedType, MovedItem}, - here::{HereJsonResult, HereJsonResultItem}, - info::{InfoHistory, InfoJsonResult}, - share::{SeeShareResult, ShareItem, ShareListResult}, - sheets::{SheetItem, SheetListJsonResult}, - }, -}; -use rust_i18n::{set_locale, t}; -use tokio::{ - fs::{self}, - net::TcpSocket, - process::Command, - sync::mpsc::{self, Receiver}, -}; - -// Import i18n files -rust_i18n::i18n!("resources/locales/legacy", 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 { - /// Version information - #[command(alias = "--version", alias = "-v")] - Version(VersionArgs), - - /// Display help information - #[command(alias = "--help", alias = "-h")] - Help, - - // Member management - /// Manage your local accounts - #[command(subcommand, alias = "acc")] - 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), - - /// Display current sheet status information - #[command(alias = "s")] - Status(StatusArgs), - - /// Display detailed information about the specified file - Info(InfoArgs), - - // Sheet management - /// Manage sheets in the workspace - #[command(subcommand, alias = "sh")] - 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(MoveMappingArgs), - - /// Share file visibility to other sheets - Share(ShareMappingArgs), - - /// Sync information from upstream vault - #[command(alias = "u")] - Update(UpdateArgs), - - // 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), - - // Lazy commands - /// Try exit current sheet - Exit, - - /// Try exit current sheet and use another sheet - Use(UseArgs), - - /// List all sheets - Sheets, - - /// List all accounts - Accounts, - - /// Align file structure - Align(SheetAlignArgs), - - /// Set current local workspace account - As(SetLocalWorkspaceAccountArgs), - - /// Make a new sheet - Make(SheetMakeArgs), - - /// Drop a sheet - Drop(SheetDropArgs), - - /// As Member, Direct, and Update - #[command(alias = "signin")] - Login(LoginArgs), - - // Completion Helpers - /// Display History IP Address - #[command(name = "_ip_history")] - GetHistoryIpAddress, - - /// Display Workspace Directory - #[command(name = "_workspace_dir")] - GetWorkspaceDir, - - /// Display Current Account - #[command(name = "_account")] - GetCurrentAccount, - - /// Display Current Upstream Vault - #[command(name = "_upstream")] - GetCurrentUpstream, - - /// Display Current Sheet - #[command(name = "_sheet")] - GetCurrentSheet, - - // Debug Tools - #[command(name = "_glob")] - DebugGlob(DebugGlobArgs), -} - -#[derive(Parser, Debug)] -struct VersionArgs { - #[arg(short = 'C', long = "compile-info")] - compile_info: bool, - - #[arg(long)] - without_banner: bool, -} - -#[derive(Subcommand, Debug)] -enum AccountManage { - /// Show help information - #[command(alias = "--help", alias = "-h")] - Help, - - /// Register a member to this computer - #[command(alias = "+")] - Add(AccountAddArgs), - - /// Remove a account from this computer - #[command(alias = "rm", alias = "-")] - 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", alias = "movekey")] - MoveKey(MoveKeyToAccountArgs), - - /// Output public key file to specified directory - #[command(alias = "genpub")] - GeneratePublicKey(GeneratePublicKeyArgs), -} - -#[derive(Subcommand, Debug)] -enum SheetManage { - /// Show help information - #[command(alias = "--help", alias = "-h")] - Help, - - /// List all sheets - #[command(alias = "ls")] - List(SheetListArgs), - - /// Use a sheet - Use(SheetUseArgs), - - /// Exit current sheet - Exit(SheetExitArgs), - - /// Create a new sheet - #[command(alias = "mk")] - Make(SheetMakeArgs), - - /// Drop current sheet - Drop(SheetDropArgs), - - /// Align file structure - Align(SheetAlignArgs), -} - -#[derive(Parser, Debug)] -struct SheetListArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Show other's sheets - #[arg(short, long = "other")] - others: bool, - - /// Show all sheets - #[arg(short = 'A', long)] - all: bool, - - /// Show raw output - #[arg(short, long)] - raw: bool, - - /// Show json output - #[arg(long = "json")] - json_output: bool, - - /// Show json output pretty - #[arg(long = "pretty")] - pretty: bool, -} - -#[derive(Parser, Debug)] -struct SheetUseArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Sheet name - sheet_name: String, -} - -#[derive(Parser, Debug)] -struct SheetExitArgs { - /// Show help information - #[arg(short, long)] - help: bool, -} - -#[derive(Parser, Debug)] -struct SheetMakeArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Sheet name - sheet_name: String, -} - -#[derive(Parser, Debug)] -struct SheetDropArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Whether to skip confirmation - #[arg(short = 'C', long)] - confirm: bool, - - /// Sheet name - sheet_name: String, -} - -#[derive(Parser, Debug)] -struct SheetAlignArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Align task - task: Option, - - /// Align operation - to: Option, - - /// List Option: All - #[arg(long = "all")] - list_all: bool, - - /// List Option: Unsolved - #[arg(long = "unsolved")] - list_unsolved: bool, - - /// List Option: Created - #[arg(long = "created")] - list_created: bool, - - /// Editor mode - #[arg(short, long)] - work: bool, - - /// Show raw output (for list) - #[arg(short, long)] - raw: bool, - - /// Show json output (for list) - #[arg(long = "json")] - json_output: bool, - - /// Show json output pretty - #[arg(long = "pretty")] - pretty: bool, -} - -#[derive(Parser, Debug)] -struct LoginArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Whether to skip confirmation - #[arg(short = 'C', long)] - confirm: bool, - - /// Member ID - login_member_id: MemberId, - - /// Upstream - upstream: String, -} - -#[derive(Parser, Debug)] -struct CreateWorkspaceArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Workspace directory path - path: Option, - - /// 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, -} - -#[derive(Parser, Debug)] -struct HereArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Show help information - #[arg(short = 'd', long = "desc")] - show_description: bool, - - /// Show json output - #[arg(long = "json")] - json_output: bool, - - /// Show json output pretty - #[arg(long = "pretty")] - pretty: bool, -} - -#[derive(Parser, Debug)] -struct StatusArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Show json output - #[arg(long = "json")] - json_output: bool, - - /// Show json output pretty - #[arg(long = "pretty")] - pretty: bool, -} - -#[derive(Parser, Debug)] -struct InfoArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// File pattern - file_pattern: Option, - - /// Full histories output - #[arg(short, long = "full")] - full: bool, - - /// Show json output - #[arg(long = "json")] - json_output: bool, - - /// Show json output pretty - #[arg(long = "pretty")] - pretty: bool, -} - -#[derive(Parser, Debug)] -struct AccountAddArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Account name - account_name: String, - - /// Auto generate ED25519 private key - #[arg(short = 'K', long = "keygen")] - keygen: bool, -} - -#[derive(Parser, Debug)] -struct AccountRemoveArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Account name - account_name: String, -} - -#[derive(Parser, Debug)] -struct AccountListArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Show raw output - #[arg(short, long)] - raw: bool, - - /// Show json output - #[arg(long = "json")] - json_output: bool, - - /// Show json output pretty - #[arg(long = "pretty")] - pretty: bool, -} - -#[derive(Parser, Debug)] -struct SetLocalWorkspaceAccountArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Account name - account_name: String, -} - -#[derive(Parser, Debug)] -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)] -struct GeneratePublicKeyArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Account name - account_name: String, - - /// Private key file path - output_dir: Option, -} - -#[derive(Parser, Debug, Clone)] -struct TrackFileArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Track file pattern - track_file_pattern: Option, - - /// Overwrite modified - #[arg(short = 'o', long = "overwrite")] - allow_overwrite: bool, - - /// Commit - Description - #[arg(short, long)] - desc: Option, - - /// Commit - Description - #[arg(short = 'v', long = "version")] - next_version: Option, - - /// Commit - Editor mode - #[arg(short, long)] - work: bool, -} - -#[derive(Parser, Debug)] -struct HoldFileArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Hold file pattern - hold_file_pattern: Option, - - /// Show fail details - #[arg(short = 'd', long = "details")] - show_fail_details: bool, - - /// Skip failed items - #[arg(short = 'S', long)] - skip_failed: bool, - - /// Skip check - #[arg(short = 'F', long)] - force: bool, -} - -#[derive(Parser, Debug)] -struct ThrowFileArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Throw file pattern - throw_file_pattern: Option, - - /// Show fail details - #[arg(short = 'd', long = "details")] - show_fail_details: bool, - - /// Skip failed items - #[arg(short = 'S', long)] - skip_failed: bool, - - /// Skip check - #[arg(short = 'F', long)] - force: bool, -} - -#[derive(Parser, Debug)] -struct MoveMappingArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Move mapping pattern - move_mapping_pattern: Option, - - /// To mapping pattern - to_mapping_pattern: Option, - - /// Erase - #[arg(short = 'e', long)] - erase: bool, - - /// Only modify upstream mapping - #[arg(short = 'r', long)] - only_remote: bool, -} - -#[derive(Parser, Debug)] -struct ShareMappingArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Arguments 1 - args1: Option, - - /// Arguments 2 - args2: Option, - - /// Arguments 3 - args3: Option, - - /// Safe merge - #[arg(short = 's', long)] - safe: bool, - - /// Skip all conflicting mappings - #[arg(short = 'S', long)] - skip: bool, - - /// Overwrite all conflicting mappings - #[arg(short = 'o', long)] - overwrite: bool, - - /// Reject this share - #[arg(short = 'R', long)] - reject: bool, - - /// Show raw output - #[arg(short = 'r', long)] - raw: bool, - - /// Show json output - #[arg(long = "json")] - json_output: bool, - - /// Show json output pretty - #[arg(long = "pretty")] - pretty: bool, - - /// Share - Editor mode - #[arg(short, long)] - work: bool, -} - -#[derive(Parser, Debug)] -struct UpdateArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Silent mode - #[arg(short, long)] - silent: bool, -} - -#[derive(Parser, Debug)] -struct DirectArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Upstream vault address - upstream: Option, - - /// Whether to skip confirmation - #[arg(short = 'C', long)] - confirm: bool, -} - -#[derive(Parser, Debug)] -struct UnstainArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Whether to skip confirmation - #[arg(short = 'C', long)] - confirm: bool, -} - -#[derive(Parser, Debug)] -struct DocsArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Name of the docs - docs_name: Option, - - /// Direct output - #[arg(short, long)] - direct: bool, - - /// Show raw output for list - #[arg(short, long)] - raw: bool, -} - -#[derive(Parser, Debug)] -struct UseArgs { - sheet_name: String, -} - -#[derive(Parser, Debug)] -struct DebugGlobArgs { - /// Pattern - pattern: String, // Using 'noglob jvv _glob' in ZSH plz -} - -#[tokio::main] -async fn main() { - // Init i18n - set_locale(¤t_locales()); - - // Init colored - #[cfg(windows)] - colored::control::set_virtual_terminal(true).unwrap(); - - // Outdate update - let required_outdated_minutes = auto_update_outdate(); - let outdate_update_enabled = required_outdated_minutes >= 0; - - // Auto update - let enable_auto_update = enable_auto_update(); - - // The following conditions will trigger automatic update: - // 1. Auto-update is enabled - // 2. Vault has been modified OR (timeout update is enabled AND timeout is set to 0) - if enable_auto_update - && (check_vault_modified().await - || outdate_update_enabled && required_outdated_minutes == 0) - { - // Record current directory - let path = match current_dir() { - Ok(path) => path, - Err(e) => { - eprintln!("{}", t!("jv.fail.get_current_dir", error = e.to_string())); - return; - } - }; - // Update - // This will change the current current_dir - jv_update(UpdateArgs { - help: false, - silent: true, - }) - .await; - // Restore current directory - if let Err(e) = set_current_dir(&path) { - eprintln!( - "{}", - t!( - "jv.fail.std.set_current_dir", - dir = path.display(), - error = e - ) - ); - return; - } - } else - // If automatic update and timeout update are enabled, - // but required time > 0 (not in disabled or always-update state) - if enable_auto_update && outdate_update_enabled && required_outdated_minutes > 0 { - // Read the last update time and calculate the duration - if let Ok(local_cfg) = LocalConfig::read().await - && let Some(local_dir) = current_local_path() - && let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path( - &local_dir, - &local_cfg.current_account(), - )) - .await - && let Some(update_instant) = latest_info.update_instant - { - let now = SystemTime::now(); - let duration_secs = now - .duration_since(update_instant) - .unwrap_or_default() - .as_secs(); - - if duration_secs > required_outdated_minutes as u64 * 60 { - // Update - // This will change the current current_dir - jv_update(UpdateArgs { - help: false, - silent: true, - }) - .await - } - }; - } - - let Ok(parser) = JustEnoughVcsWorkspace::try_parse() else { - eprintln!("{}", md(t!("jv.fail.parse.parser_failed"))); - - // Tips - // Guide to create - { - // Check if workspace exist - let Some(local_dir) = current_local_path() else { - println!(); - println!("{}", t!("jv.tip.not_workspace").trim().yellow()); - return; - }; - - let _ = correct_current_dir(); - - // Check if account list is not empty - let Some(dir) = UserDirectory::current_cfg_dir() else { - return; - }; - - if let Ok(ids) = dir.account_ids() - && ids.is_empty() - { - println!(); - println!("{}", t!("jv.tip.no_account").trim().yellow()); - return; - } - - // Check if the workspace has a registered account (account = unknown) - let Some(local_cfg) = LocalConfig::read().await.ok() else { - eprintln!("{}", md(t!("jv.fail.cfg_not_found.local_config"))); - return; - }; - - // Account exists check - if local_cfg.current_account() == "unknown" { - println!(); - println!("{}", t!("jv.tip.no_account_set").trim().yellow()); - } else if dir - .account_ids() - .ok() - .map(|ids| !ids.contains(&local_cfg.current_account())) - .unwrap_or(false) - { - println!(); - println!( - "{}", - t!( - "jv.tip.account_not_exist", - account = local_cfg.current_account() - ) - .trim() - .yellow() - ); - return; - } - - // Outdated - let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path( - &local_dir, - &local_cfg.current_account(), - )) - .await - else { - return; - }; - if let Some(instant) = latest_info.update_instant { - let now = SystemTime::now(); - let duration = now.duration_since(instant).unwrap_or_default(); - - if duration.as_secs() > 60 * required_outdated_minutes.clamp(5, i64::MAX) as u64 { - // Automatically prompt if exceeding the set timeout (at least 5 minutes) - let hours = duration.as_secs() / 3600; - let minutes = (duration.as_secs() % 3600) / 60; - - println!( - "\n{}", - t!("jv.tip.outdated", hour = hours, minutes = minutes) - .trim() - .yellow() - ); - } - } - } - - return; - }; - - match parser.command { - JustEnoughVcsWorkspaceCommand::Version(version_args) => { - let compile_info = CompileInfo::default(); - let core_compile_info = CoreCompileInfo::default(); - if version_args.without_banner { - println!( - "{}", - md(t!( - "jv.version.header", - version = compile_info.cli_version, - vcs_version = core_compile_info.vcs_version - )) - ); - } else { - println!(); - let ascii_art_banner = ASCII_YIZI - .split('\n') - .skip_while(|line| !line.contains("#BANNER START#")) - .skip(1) - .take_while(|line| !line.contains("#BANNER END#")) - .collect::>() - .join("\n"); - - println!( - "{}", - ascii_art_banner - .replace("{banner_line_1}", "JustEnoughVCS") - .replace( - "{banner_line_2}", - &format!( - "{}: {} ({})", - t!("common.word.cli_version"), - &compile_info.cli_version, - &compile_info.date - ) - ) - .replace( - "{banner_line_3}", - &format!( - "{}: {}", - t!("common.word.vcs_version"), - &core_compile_info.vcs_version - ) - ) - ); - - if !version_args.compile_info { - println!(); - } - } - - if version_args.compile_info { - println!( - "\n{}", - md(t!( - "jv.version.compile_info", - build_time = compile_info.date, - build_target = compile_info.target, - build_platform = compile_info.platform, - build_toolchain = compile_info.toolchain, - cli_build_branch = compile_info.build_branch, - cli_build_commit = - &compile_info.build_commit[..7.min(compile_info.build_commit.len())], - core_build_branch = core_compile_info.build_branch, - core_build_commit = &core_compile_info.build_commit - [..7.min(core_compile_info.build_commit.len())] - )) - ); - } - } - - JustEnoughVcsWorkspaceCommand::Help => { - println!("{}", md(t!("jv.help"))); - } - - JustEnoughVcsWorkspaceCommand::Account(account_manage) => { - let user_dir = match UserDirectory::current_cfg_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::GeneratePublicKey(generate_public_key_args) => { - if generate_public_key_args.help { - println!("{}", md(t!("jv.account"))); - return; - } - jv_account_generate_pub_key(user_dir, generate_public_key_args).await; - } - } - } - 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::Status(status_args) => { - if status_args.help { - println!("{}", md(t!("jv.status"))); - return; - } - jv_status(status_args).await; - } - JustEnoughVcsWorkspaceCommand::Info(info_args) => { - if info_args.help { - println!("{}", md(t!("jv.info"))); - return; - } - jv_info(info_args).await; - } - JustEnoughVcsWorkspaceCommand::Sheet(sheet_manage) => match sheet_manage { - SheetManage::Help => { - println!("{}", md(t!("jv.sheet"))); - return; - } - SheetManage::List(sheet_list_args) => jv_sheet_list(sheet_list_args).await, - SheetManage::Use(sheet_use_args) => jv_sheet_use(sheet_use_args).await, - SheetManage::Exit(sheet_exit_args) => { - let _ = jv_sheet_exit(sheet_exit_args).await; - } - SheetManage::Make(sheet_make_args) => jv_sheet_make(sheet_make_args).await, - SheetManage::Drop(sheet_drop_args) => jv_sheet_drop(sheet_drop_args).await, - SheetManage::Align(sheet_align_args) => jv_sheet_align(sheet_align_args).await, - }, - JustEnoughVcsWorkspaceCommand::Track(track_file_args) => { - if track_file_args.help { - 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::Share(share_file_args) => { - if share_file_args.help { - println!("{}", md(t!("jv.share"))); - return; - } - jv_share(share_file_args).await; - } - JustEnoughVcsWorkspaceCommand::Update(update_file_args) => { - if update_file_args.help { - println!("{}", md(t!("jv.update"))); - return; - } - jv_update(update_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; - } - JustEnoughVcsWorkspaceCommand::Exit => { - let _ = jv_sheet_exit(SheetExitArgs { help: false }).await; - } - JustEnoughVcsWorkspaceCommand::Use(use_args) => { - if let Ok(_) = jv_sheet_exit(SheetExitArgs { help: false }).await { - jv_sheet_use(SheetUseArgs { - help: false, - sheet_name: use_args.sheet_name, - }) - .await; - } - } - JustEnoughVcsWorkspaceCommand::Sheets => { - jv_sheet_list(SheetListArgs { - help: false, - others: false, - all: false, - raw: false, - json_output: false, - pretty: false, - }) - .await - } - JustEnoughVcsWorkspaceCommand::Accounts => { - let user_dir = match UserDirectory::current_cfg_dir() { - Some(dir) => dir, - None => { - eprintln!("{}", t!("jv.fail.account.no_user_dir")); - return; - } - }; - jv_account_list( - user_dir, - AccountListArgs { - help: false, - raw: false, - json_output: false, - pretty: false, - }, - ) - .await - } - JustEnoughVcsWorkspaceCommand::Align(sheet_align_args) => { - if sheet_align_args.help { - println!("{}", md(t!("jv.align"))); - return; - } - jv_sheet_align(sheet_align_args).await - } - JustEnoughVcsWorkspaceCommand::As(args) => { - let user_dir = match UserDirectory::current_cfg_dir() { - Some(dir) => dir, - None => { - eprintln!("{}", t!("jv.fail.account.no_user_dir")); - return; - } - }; - jv_account_as(user_dir, args).await - } - JustEnoughVcsWorkspaceCommand::Make(args) => { - jv_sheet_make(args).await; - } - JustEnoughVcsWorkspaceCommand::Drop(args) => { - jv_sheet_drop(args).await; - } - JustEnoughVcsWorkspaceCommand::Login(args) => { - if !args.confirm { - println!( - "{}", - t!( - "jv.confirm.login", - account = args.login_member_id, - upstream = args.upstream - ) - .trim() - .yellow() - ); - confirm_hint_or(t!("common.confirm"), || exit(1)).await; - } - - let user_dir = match UserDirectory::current_cfg_dir() { - Some(dir) => dir, - None => { - eprintln!("{}", t!("jv.fail.account.no_user_dir")); - return; - } - }; - - jv_account_as( - user_dir, - SetLocalWorkspaceAccountArgs { - help: false, - account_name: args.login_member_id, - }, - ) - .await; - - jv_direct(DirectArgs { - help: false, - upstream: Some(args.upstream.clone()), - confirm: true, - }) - .await; - - jv_update(UpdateArgs { - help: false, - silent: true, - }) - .await; - - if let Some(local_dir) = current_local_path() { - let _ = fs::remove_file(local_dir.join(CLIENT_FILE_TODOLIST)).await; - }; - } - - // Completion Helpers - JustEnoughVcsWorkspaceCommand::GetHistoryIpAddress => { - get_recent_ip_address() - .await - .iter() - .for_each(|ip| println!("{}", ip)); - } - JustEnoughVcsWorkspaceCommand::GetWorkspaceDir => { - if let Some(local_dir) = current_local_path() { - println!("{}", local_dir.display()); - return; - }; - exit(1) - } - JustEnoughVcsWorkspaceCommand::GetCurrentAccount => { - let _ = correct_current_dir(); - if let Ok(local_config) = LocalConfig::read().await { - if local_config.is_host_mode() { - println!("host/{}", local_config.current_account()); - return; - } else { - println!("{}", local_config.current_account()); - return; - } - }; - exit(1) - } - JustEnoughVcsWorkspaceCommand::GetCurrentUpstream => { - let _ = correct_current_dir(); - if let Ok(local_config) = LocalConfig::read().await { - println!("{}", local_config.upstream_addr()); - return; - }; - exit(1) - } - JustEnoughVcsWorkspaceCommand::GetCurrentSheet => { - let _ = correct_current_dir(); - if let Ok(local_config) = LocalConfig::read().await { - let sheet_name = local_config.sheet_in_use().clone().unwrap_or_default(); - if !sheet_name.is_empty() { - println!("{}", sheet_name); - return; - } - }; - exit(1) - } - - // Debug Tools - JustEnoughVcsWorkspaceCommand::DebugGlob(glob_args) => { - jv_debug_glob(glob_args).await; - } - } -} - -async fn jv_create(args: CreateWorkspaceArgs) { - let Some(path) = args.path else { - println!("{}", md(t!("jv.create"))); - 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.create")); - } - Err(e) => { - eprintln!("{}", t!("jv.fail.create", error = e.to_string())); - } - } -} - -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 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) { - let Some(local_dir) = current_local_path() else { - eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); - return; - }; - - let Ok(local_cfg) = LocalConfig::read_from(local_dir.join(CLIENT_FILE_WORKSPACE)).await else { - eprintln!("{}", md(t!("jv.fail.cfg_not_found.local_config"))); - return; - }; - - let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path( - &local_dir, - &local_cfg.current_account(), - )) - .await - else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.latest_info", - account = &local_cfg.current_account() - )) - ); - return; - }; - - let Ok(latest_file_data_path) = LatestFileData::data_path(&local_cfg.current_account()) else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.latest_file_data", - account = &local_cfg.current_account() - )) - ); - return; - }; - - let Ok(latest_file_data) = LatestFileData::read_from(&latest_file_data_path).await else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.latest_file_data", - account = &local_cfg.current_account() - )) - ); - return; - }; - - // Print path information - let sheet_name = if let Some(sheet_name) = local_cfg.sheet_in_use() { - sheet_name.to_string() - } else { - "".to_string() - }; - - // Read cached sheet - let Ok(cached_sheet) = CachedSheet::cached_sheet_data(&sheet_name).await else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.cached_sheet", - sheet = &sheet_name - )) - ); - return; - }; - - let Some(local_workspace) = LocalWorkspace::init_current_dir(local_cfg.clone()) else { - eprintln!("{}", md(t!("jv.fail.workspace_not_found")).trim()); - return; - }; - - let Ok(analyzed) = AnalyzeResult::analyze_local_status(&local_workspace).await else { - eprintln!("{}", md(t!("jv.fail.status.analyze")).trim()); - return; - }; - - // Read local sheet - let Ok(local_sheet) = local_workspace - .local_sheet(&local_cfg.current_account(), &sheet_name) - .await - else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.local_sheet", - account = &local_cfg.current_account(), - sheet = &sheet_name - )) - ); - return; - }; - - let path = match current_dir() { - Ok(path) => path, - Err(_) => { - eprintln!("{}", t!("jv.fail.get_current_dir")); - return; - } - }; - - let local_dir = match current_local_path() { - Some(path) => path, - None => { - eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); - return; - } - }; - - let relative_path = match path.strip_prefix(&local_dir) { - Ok(path) => path.display().to_string(), - Err(_) => path.display().to_string(), - }; - - let mut remote_files = mapping_names_here(&path, &local_dir, &cached_sheet); - - let duration_updated = SystemTime::now() - .duration_since(latest_info.update_instant.unwrap_or(SystemTime::now())) - .unwrap_or_default(); - let minutes = duration_updated.as_secs() / 60; - - // JSON output handling - if args.json_output { - let mut json_result = HereJsonResult::default(); - let mut dirs = HashSet::new(); - let mut files = HashSet::new(); - - // Process local files - if let Ok(mut entries) = fs::read_dir(&path).await { - while let Ok(Some(entry)) = entries.next_entry().await { - if let Ok(file_type) = entry.file_type().await { - let file_name = entry.file_name().to_string_lossy().to_string(); - - if file_name == CLIENT_FOLDER_WORKSPACE_ROOT_NAME { - continue; - } - - let metadata = match entry.metadata().await { - Ok(meta) => meta, - Err(_) => continue, - }; - - let size = metadata.len() as usize; - let is_dir = file_type.is_dir(); - - // Skip if already processed - if is_dir { - if dirs.contains(&file_name) { - continue; - } - dirs.insert(file_name.clone()); - } else { - if files.contains(&file_name) { - continue; - } - files.insert(file_name.clone()); - } - - // Get current path - let current_path = PathBuf::from_str(relative_path.as_ref()) - .unwrap() - .join(file_name.clone()); - - let mut current_version = VirtualFileVersion::default(); - let mut modified = false; - let mut holder = None; - - // Get mapping info (only for files) - if !is_dir && let Some(mapping) = cached_sheet.mapping().get(¤t_path) { - let id = mapping.id.clone(); - if let Some(latest_version) = latest_file_data.file_version(&id) { - current_version = latest_version.clone(); - } - if let Some(file_holder) = latest_file_data.file_holder(&id) { - holder = Some(file_holder.clone()); - } - - // Check if file is modified - modified = analyzed.modified.contains(¤t_path); - } - - // Remove from remote files list - remote_files.remove(&file_name); - - // Add to JSON result - json_result.items.push(HereJsonResultItem { - mapping: if is_dir { - "".to_string() - } else { - fmt_path_str(current_path.display().to_string()).unwrap_or_default() - }, - name: file_name.clone(), - current_version, - size, - exist: true, - modified, - holder: holder.unwrap_or_default(), - is_dir, - }); - } - } - } - - // Process remote files (not existing locally) - for (name, metadata) in remote_files { - let current_path = PathBuf::from_str(relative_path.as_ref()) - .unwrap() - .join(name.clone()); - - if let Some(metadata) = metadata { - // It's a file - // Skip if already processed - if files.contains(&name) { - continue; - } - files.insert(name.clone()); - - let holder = latest_file_data.file_holder(&metadata.id).cloned(); - json_result.items.push(HereJsonResultItem { - mapping: fmt_path_str(current_path.display().to_string()).unwrap_or_default(), - name: name.clone(), - current_version: metadata.version, - size: 0, - exist: false, - modified: false, - holder: holder.unwrap_or_default(), - is_dir: false, - }); - } else { - // It's a directory - // Skip if already processed - let trimmed_name = if name.ends_with('/') { - name[..name.len() - 1].to_string() - } else { - name.clone() - }; - - if dirs.contains(&trimmed_name) { - continue; - } - dirs.insert(trimmed_name.clone()); - - json_result.items.push(HereJsonResultItem { - mapping: String::default(), - name: trimmed_name, - current_version: VirtualFileVersion::default(), - size: 0, - exist: false, - modified: false, - holder: String::new(), - is_dir: true, - }); - } - } - - print_json(json_result, args.pretty); - return; - } - - let account_str = if local_cfg.is_host_mode() { - format!("{}/{}", "host".red(), local_cfg.current_account()) - } else { - local_cfg.current_account() - }; - - println!( - "{}", - t!( - "jv.success.here.path_info", - upstream = local_cfg.upstream_addr().to_string(), - account = account_str, - sheet_name = sheet_name.yellow(), - path = relative_path, - minutes = minutes - ) - .trim() - ); - - // Print file info - let mut columns = vec![ - t!("jv.success.here.items.editing"), - t!("jv.success.here.items.holder"), - t!("jv.success.here.items.size"), - t!("jv.success.here.items.version"), - t!("jv.success.here.items.name"), - ]; - if args.show_description { - columns.push(t!("jv.success.here.items.description")); - } - let mut table = SimpleTable::new(columns); - - let mut dir_count = 0; - let mut file_count = 0; - let mut total_size = 0; - - // Exists files - if let Ok(mut entries) = fs::read_dir(&path).await { - while let Ok(Some(entry)) = entries.next_entry().await { - if let Ok(file_type) = entry.file_type().await { - let file_name = entry.file_name().to_string_lossy().to_string(); - - if file_name == CLIENT_FOLDER_WORKSPACE_ROOT_NAME { - continue; - } - - let metadata = match entry.metadata().await { - Ok(meta) => meta, - Err(_) => continue, - }; - - let size = metadata.len(); - let is_dir = file_type.is_dir(); - let mut version = "-".to_string(); - let mut hold = "-".to_string(); - let mut editing = "-".to_string(); - let mut desc = "-".to_string(); - - if is_dir { - // Directory - // Add directory count - dir_count += 1; - - let dir_name = format!("{}/", file_name); - - // Remove remote dirs items that already exist locally - remote_files.remove(&dir_name); - - // Add directory item - let mut line = vec![ - editing.to_string(), - hold.to_string(), - "-".to_string(), - version.to_string(), - t!( - "jv.success.here.append_info.name", - name = dir_name.to_string().cyan() - ) - .trim() - .to_string(), - ]; - if args.show_description { - line.push(desc); - } - table.insert_item(0, line); - } else { - // Local File - // Add file count - file_count += 1; - - // Get current path - let current_path = PathBuf::from_str(relative_path.as_ref()) - .unwrap() - .join(file_name.clone()); - - // Get mapping - if let Some(mapping) = cached_sheet.mapping().get(¤t_path) { - let mut is_file_held = false; - let mut is_version_match = false; - - // Hold status - let id = mapping.id.clone(); - if let Some(holder) = latest_file_data.file_holder(&id) { - if holder == &local_cfg.current_account() { - hold = t!("jv.success.here.append_info.holder.yourself") - .trim() - .green() - .to_string(); - is_file_held = true; - } else { - let holder_text = t!( - "jv.success.here.append_info.holder.others", - holder = holder - ) - .trim() - .truecolor(128, 128, 128); - hold = holder_text.to_string(); - } - } - - // Version status && description - if let Some(latest_version) = latest_file_data.file_version(&id) { - let local_version = local_sheet.mapping_data(¤t_path); - if let Ok(local_mapping) = local_version { - let local_version = local_mapping.version_when_updated(); - - // Append version status - if latest_version == local_version { - version = t!( - "jv.success.here.append_info.version.match", - version = latest_version - ) - .trim() - .to_string(); - is_version_match = true; - } else { - version = t!( - "jv.success.here.append_info.version.unmatch", - remote_version = local_version, - ) - .trim() - .red() - .to_string(); - } - - // Append description - if args.show_description { - let content = local_mapping - .version_desc_when_updated() - .description - .clone(); - - let content = truncate_first_line(content); - - desc = t!( - "jv.success.here.append_info.description", - creator = local_mapping - .version_desc_when_updated() - .creator - .cyan() - .to_string(), - description = content, - ) - .trim() - .to_string(); - } - } - } - - // Editing status - let modified = analyzed.modified.contains(¤t_path); - if !is_file_held || !is_version_match { - if modified { - editing = t!( - "jv.success.here.append_info.editing.cant_edit_but_modified" - ) - .trim() - .red() - .to_string(); - } else { - editing = t!("jv.success.here.append_info.editing.cant_edit") - .trim() - .truecolor(128, 128, 128) - .to_string(); - } - } else if modified { - editing = t!("jv.success.here.append_info.editing.modified") - .trim() - .cyan() - .to_string(); - } else { - editing = t!("jv.success.here.append_info.editing.can_edit") - .trim() - .green() - .to_string(); - } - } - - // Remove remote file items that already exist locally - remote_files.remove(&file_name); - - // Add Table Item - let mut line = vec![ - editing.to_string(), - hold.to_string(), - t!( - "jv.success.here.append_info.size", - size = size_str(size as usize) - ) - .trim() - .yellow() - .to_string(), - version.to_string(), - t!("jv.success.here.append_info.name", name = file_name) - .trim() - .to_string(), - ]; - - if args.show_description { - line.push(desc); - } - - table.push_item(line); - } - - // Total Size - total_size += size; - } - } - } - - // Remote Files - for mapping in remote_files { - if let Some(metadata) = mapping.1 { - let mut hold = "-".to_string(); - if let Some(holder) = latest_file_data.file_holder(&metadata.id) { - if holder == &local_cfg.current_account() { - hold = t!("jv.success.here.append_info.holder.yourself") - .trim() - .green() - .to_string(); - } else { - let holder_text = - t!("jv.success.here.append_info.holder.others", holder = holder) - .trim() - .truecolor(128, 128, 128); - hold = holder_text.to_string(); - } - } - - // File - let mut line = vec![ - t!("jv.success.here.append_info.editing.not_local") - .trim() - .truecolor(128, 128, 128) - .to_string(), - hold.to_string(), - "-".to_string(), - metadata.version, - t!("jv.success.here.append_info.name", name = mapping.0) - .trim() - .truecolor(128, 128, 128) - .to_string(), - ]; - - if args.show_description { - line.push("-".to_string()); - } - - table.push_item(line); - } else { - // Directory - let mut line = vec![ - "-".to_string(), - "-".to_string(), - "-".to_string(), - "-".to_string(), - t!("jv.success.here.append_info.name", name = mapping.0) - .trim() - .truecolor(128, 128, 128) - .to_string(), - ]; - - if args.show_description { - line.push("-".to_string()); - } - - table.push_item(line); - } - } - - println!("{}", table); - - // Print directory info - println!( - "{}", - t!( - "jv.success.here.count_info", - dir_count = dir_count, - file_count = file_count, - size = size_str(total_size as usize) - ) - .trim() - ); -} - -async fn jv_status(args: StatusArgs) { - let Some(local_dir) = current_local_path() else { - eprintln!("{}", md(t!("jv.fail.workspace_not_found")).trim()); - return; - }; - - let Ok(local_cfg) = LocalConfig::read_from(local_dir.join(CLIENT_FILE_WORKSPACE)).await else { - eprintln!("{}", md(t!("jv.fail.cfg_not_found.local_config"))); - return; - }; - - let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path( - &local_dir, - &local_cfg.current_account(), - )) - .await - else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.latest_info", - account = &local_cfg.current_account() - )) - ); - return; - }; - - let account = local_cfg.current_account(); - - let Ok(latest_file_data_path) = LatestFileData::data_path(&account) else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.latest_file_data", - account = &account - )) - ); - return; - }; - - let Ok(latest_file_data) = LatestFileData::read_from(&latest_file_data_path).await else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.latest_file_data", - account = &account - )) - ); - return; - }; - - let Some(sheet_name) = local_cfg.sheet_in_use().clone() else { - eprintln!("{}", md(t!("jv.fail.status.no_sheet_in_use")).trim()); - return; - }; - - let Some(local_workspace) = LocalWorkspace::init_current_dir(local_cfg.clone()) else { - eprintln!("{}", md(t!("jv.fail.workspace_not_found")).trim()); - return; - }; - - let Ok(local_sheet) = local_workspace.local_sheet(&account, &sheet_name).await else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.local_sheet", - account = &account, - sheet = &sheet_name - )) - ); - return; - }; - - let in_ref_sheet = latest_info.reference_sheets.contains(&sheet_name); - let is_host_mode = local_cfg.is_host_mode(); - - let Ok(analyzed) = AnalyzeResult::analyze_local_status(&local_workspace).await else { - eprintln!("{}", md(t!("jv.fail.status.analyze")).trim()); - return; - }; - - let mut created_items: Vec; - let mut erased_items: Vec; - let mut lost_items: Vec; - let mut moved_items: Vec; - let mut modified_items: Vec; - - if args.json_output { - let mut created: Vec = analyzed.created.iter().cloned().collect(); - let mut lost: Vec = analyzed.lost.iter().cloned().collect(); - let mut erased: Vec = analyzed.erased.iter().cloned().collect(); - let mut moved: Vec = analyzed - .moved - .iter() - .map(|(_, (from, to))| MovedItem { - from: from.clone(), - to: to.clone(), - }) - .collect(); - let mut modified: Vec = analyzed - .modified - .iter() - .cloned() - .map(|path| { - let holder_match = { - if let Ok(mapping) = local_sheet.mapping_data(&path) { - let vfid = mapping.mapping_vfid(); - match latest_file_data.file_holder(vfid) { - Some(holder) => holder == &account, - None => false, - } - } else { - false - } - }; - - let base_version_match = { - if let Ok(mapping) = local_sheet.mapping_data(&path) { - let vfid = mapping.mapping_vfid(); - let ver = mapping.version_when_updated(); - if let Some(latest_version) = latest_file_data.file_version(vfid) { - ver == latest_version - } else { - true - } - } else { - true - } - }; - - let modification_type = if !holder_match { - ModifiedType::ModifiedButNotHeld - } else if !base_version_match { - ModifiedType::ModifiedButBaseVersionMismatch - } else { - ModifiedType::Modified - }; - - ModifiedItem { - path, - modification_type, - } - }) - .collect(); - - // Sort - created.sort(); - lost.sort(); - erased.sort(); - moved.sort_by(|a, b| a.from.cmp(&b.from).then(a.to.cmp(&b.to))); - modified.sort_by(|a, b| a.path.cmp(&b.path)); - - let json_result = AnalyzerJsonResult { - created, - lost, - erased, - moved, - modified, - }; - - print_json(json_result, args.pretty); - return; - } else { - // Format created items - created_items = analyzed - .created - .iter() - .map(|path| { - t!( - "jv.success.status.created_item", - path = path.display().to_string() - ) - .trim() - .green() - .to_string() - }) - .collect(); - - // Format erased items - erased_items = analyzed - .erased - .iter() - .map(|path| { - t!( - "jv.success.status.erased_item", - path = path.display().to_string() - ) - .trim() - .magenta() - .to_string() - }) - .collect(); - - // Format lost items - lost_items = analyzed - .lost - .iter() - .map(|path| { - t!( - "jv.success.status.lost_item", - path = path.display().to_string() - ) - .trim() - .red() - .to_string() - }) - .collect(); - - // Format moved items - moved_items = analyzed - .moved - .iter() - .map(|(_, (from, to))| { - t!( - "jv.success.status.moved_item", - from = from.display(), - to = to.display() - ) - .trim() - .yellow() - .to_string() - }) - .collect(); - - // Format modified items - modified_items = analyzed - .modified - .iter() - .map(|path| { - let holder_match = { - if let Ok(mapping) = local_sheet.mapping_data(path) { - let vfid = mapping.mapping_vfid(); - match latest_file_data.file_holder(vfid) { - Some(holder) => holder == &account, - None => false, - } - } else { - false - } - }; - - let base_version_match = { - if let Ok(mapping) = local_sheet.mapping_data(path) { - let vfid = mapping.mapping_vfid(); - let ver = mapping.version_when_updated(); - if let Some(latest_version) = latest_file_data.file_version(vfid) { - ver == latest_version - } else { - true - } - } else { - true - } - }; - - // Holder dismatch - if !holder_match { - return t!( - "jv.success.status.invalid_modified_item", - path = path.display().to_string(), - reason = t!("jv.success.status.invalid_modified_reasons.not_holder") - ) - .trim() - .red() - .to_string(); - } - - // Base version mismatch - if !base_version_match { - return t!( - "jv.success.status.invalid_modified_item", - path = path.display().to_string(), - reason = - t!("jv.success.status.invalid_modified_reasons.base_version_mismatch") - ) - .trim() - .red() - .to_string(); - } - - t!( - "jv.success.status.modified_item", - path = path.display().to_string() - ) - .trim() - .cyan() - .to_string() - }) - .collect(); - } - - let has_struct_changes = !created_items.is_empty() - || !lost_items.is_empty() - || !erased_items.is_empty() - || !moved_items.is_empty(); - let has_file_modifications = !modified_items.is_empty(); - - if has_struct_changes { - sort_paths(&mut created_items); - sort_paths(&mut lost_items); - sort_paths(&mut erased_items); - sort_paths(&mut moved_items); - } - if has_file_modifications { - sort_paths(&mut modified_items); - } - - // Calculate duration since last update - let update_instant = latest_info.update_instant.unwrap_or(SystemTime::now()); - let duration = SystemTime::now() - .duration_since(update_instant) - .unwrap_or_default(); - let h = duration.as_secs() / 3600; - let m = (duration.as_secs() % 3600) / 60; - let s = duration.as_secs() % 60; - - if has_struct_changes { - println!( - "{}", - md(t!( - "jv.success.status.struct_changes_display", - sheet_name = sheet_name, - moved_items = if moved_items.is_empty() { - "".to_string() - } else { - moved_items.join("\n") + "\n" - }, - lost_items = if lost_items.is_empty() { - "".to_string() - } else { - lost_items.join("\n") + "\n" - }, - erased_items = if erased_items.is_empty() { - "".to_string() - } else { - erased_items.join("\n") + "\n" - }, - created_items = if created_items.is_empty() { - "".to_string() - } else { - created_items.join("\n") + "\n" - }, - h = h, - m = m, - s = s - )) - .trim() - ); - } else if has_file_modifications { - println!( - "{}", - md(t!( - "jv.success.status.content_modifies_display", - sheet_name = sheet_name, - modified_items = if modified_items.is_empty() { - "".to_string() - } else { - modified_items.join("\n") - }, - h = h, - m = m, - s = s - )) - .trim() - ); - } else if in_ref_sheet { - println!( - "{}", - md(t!( - "jv.success.status.no_changes_in_reference_sheet", - sheet_name = sheet_name, - h = h, - m = m, - s = s - )) - ); - } else { - println!( - "{}", - md(t!( - "jv.success.status.no_changes", - sheet_name = sheet_name, - h = h, - m = m, - s = s - )) - ); - } - - if in_ref_sheet && !is_host_mode { - println!( - "\n{}", - md(t!("jv.success.status.hint_in_reference_sheet")).yellow() - ); - } - if is_host_mode { - println!("\n{}", md(t!("jv.success.status.hint_as_host"))); - } -} - -async fn jv_info(args: InfoArgs) { - let local_dir = match current_local_path() { - Some(dir) => dir, - None => { - eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); - return; - } - }; - - let query_file_paths = if let Some(pattern) = args.file_pattern.clone() { - let files = glob(pattern, &local_dir).await; - files - .iter() - .filter_map(|f| PathBuf::from_str(f.0).ok()) - .collect::>() - } else { - println!("{}", md(t!("jv.info"))); - return; - }; - - let _ = correct_current_dir(); - - let Ok(local_cfg) = LocalConfig::read().await else { - eprintln!("{}", md(t!("jv.fail.cfg_not_found.local_config"))); - return; - }; - - let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path( - &local_dir, - &local_cfg.current_account(), - )) - .await - else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.latest_info", - account = &local_cfg.current_account() - )) - ); - return; - }; - - let account = local_cfg.current_account(); - - let Ok(latest_file_data_path) = LatestFileData::data_path(&account) else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.latest_file_data", - account = &account - )) - ); - return; - }; - - // Get latest file data - let Ok(latest_file_data) = LatestFileData::read_from(&latest_file_data_path).await else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.latest_file_data", - account = &account - )) - ); - return; - }; - - let Some(sheet_name) = local_cfg.sheet_in_use().clone() else { - eprintln!("{}", md(t!("jv.fail.status.no_sheet_in_use")).trim()); - return; - }; - - let Some(local_workspace) = LocalWorkspace::init_current_dir(local_cfg.clone()) else { - eprintln!("{}", md(t!("jv.fail.workspace_not_found")).trim()); - return; - }; - - let Ok(local_sheet) = local_workspace.local_sheet(&account, &sheet_name).await else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.local_sheet", - account = &account, - sheet = &sheet_name - )) - ); - return; - }; - - if query_file_paths.is_empty() { - return; - } - // File to query - let query_file_path = query_file_paths[0].to_path_buf(); - let Ok(mapping) = local_sheet.mapping_data(&query_file_path) else { - return; - }; - let vfid = mapping.mapping_vfid(); - - let query_file_path_string = - fmt_path_str(query_file_path.display().to_string()).unwrap_or_default(); - - // JSON output handling - if args.json_output { - let mut json_result = InfoJsonResult::default(); - json_result.mapping = query_file_path_string.clone(); - - // Get reference sheet path - json_result.in_ref = if let Some(path) = latest_info.ref_sheet_vfs_mapping.get(vfid) { - path.display().to_string() - } else { - vfid.clone() - }; - - json_result.vfid = vfid.clone(); - - // Get file version in reference sheet - let version_in_ref = if let Some(mapping) = latest_info - .ref_sheet_content - .mapping() - .get(&query_file_path) - { - mapping.version.clone() - } else { - "".to_string() - }; - - // Get current file version - let version_current = latest_file_data - .file_version(vfid) - .cloned() - .unwrap_or_else(|| "".to_string()); - - // Check if file is being edited based on latest version (regardless of hold status) - let modified_correctly = if let Ok(mapping) = local_sheet.mapping_data(&query_file_path) { - // If base editing version is correct - if mapping.version_when_updated() == &version_current { - mapping.last_modifiy_check_result() // Return detection result - } else { - false - } - } else { - false - }; - - // Build history list - if let Some(histories) = latest_file_data.file_histories(vfid) { - for (version, description) in histories { - json_result.histories.push(InfoHistory { - version: version.clone(), - version_creator: description.creator.clone(), - version_description: description.description.clone(), - is_current_version: version == &version_current, - is_ref_version: version == &version_in_ref, - }); - } - } - - // Add current modification if exists - if modified_correctly { - json_result.histories.insert( - 0, - InfoHistory { - version: "CURRENT".to_string(), - version_creator: local_workspace - .config() - .lock() - .await - .current_account() - .clone(), - version_description: t!("jv.success.info.oneline.description_current") - .to_string(), - is_current_version: true, - is_ref_version: false, - }, - ); - } - - print_json(json_result, args.pretty); - return; - } - - // Render initial location - { - println!("{}", query_file_path_string); - } - - // Render reference sheet location, use ID if not found - { - let path_in_ref = if let Some(path) = latest_info.ref_sheet_vfs_mapping.get(vfid) { - path.display().to_string() - } else { - vfid.clone() - }; - - // Offset string - let offset_string = " ".repeat(display_width( - if let Some(last_slash) = query_file_path_string.rfind('/') { - &query_file_path_string[..last_slash] - } else { - "" - }, - )); - - println!( - "{}{}{}", - offset_string, - "\\_ ".truecolor(128, 128, 128), - path_in_ref.cyan() - ); - } - - // Render complete file history - { - if let Some(histories) = latest_file_data.file_histories(vfid) { - // Get file version in reference sheet - let version_in_ref = if let Some(mapping) = latest_info - .ref_sheet_content - .mapping() - .get(&query_file_path) - { - mapping.version.clone() - } else { - "".to_string() - }; - - // Get current file version - let version_current = latest_file_data - .file_version(vfid) - .cloned() - .unwrap_or_else(|| "".to_string()); - - // Check if file is being edited based on latest version (regardless of hold status) - let modified_correctly = if let Ok(mapping) = local_sheet.mapping_data(&query_file_path) - { - // If base editing version is correct - if mapping.version_when_updated() == &version_current { - mapping.last_modifiy_check_result() // Return detection result - } else { - false - } - } else { - false - }; - - // Text - let (prefix_str, version_str, creator_str, description_str) = ( - t!("jv.success.info.oneline.table_headers.prefix"), - t!("jv.success.info.oneline.table_headers.version"), - t!("jv.success.info.oneline.table_headers.creator"), - t!("jv.success.info.oneline.table_headers.description"), - ); - - // Single-line output - if !args.full { - // Create table - let mut table = - SimpleTable::new(vec![prefix_str, version_str, creator_str, description_str]); - - // Append data - for (version, description) in histories { - // If it's reference version, render "@" - // Current version, render "\_" - // Other versions, render "|" - let prefix = if version == &version_in_ref { - "@".cyan().to_string() - } else if version == &version_current { - "|->".yellow().to_string() - } else { - "|".truecolor(128, 128, 128).to_string() - }; - - table.insert_item( - 0, - vec![ - prefix, - version.to_string(), - format!("@{}: ", &description.creator.cyan()), - truncate_first_line(description.description.to_string()), - ], - ); - } - - // If file has new version, append - if modified_correctly { - table.insert_item( - 0, - vec![ - "+".green().to_string(), - "CURRENT".green().to_string(), - format!( - "@{}: ", - local_workspace - .config() - .lock() - .await - .current_account() - .cyan() - ), - format!( - "{}", - t!("jv.success.info.oneline.description_current").green() - ), - ], - ); - } - - // Render table - let table_str = table.to_string(); - if table_str.lines().count() > 1 { - println!(); - } - for line in table_str.lines().skip(1) { - println!("{}", line); - } - } else { - // Multi-line output - if !histories.is_empty() { - println!(); - } - for (version, description) in histories { - println!("{}: {}", version_str, version); - println!("{}: {}", creator_str, description.creator.cyan()); - println!("{}", description.description); - if version != &histories.last().unwrap().0 { - println!("{}", "-".repeat(45)); - } - } - } - } - } -} - -async fn jv_sheet_list(args: SheetListArgs) { - let _ = correct_current_dir(); - - let Some(local_dir) = current_local_path() else { - if !args.raw { - eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); - } - return; - }; - - let Ok(local_cfg) = LocalConfig::read().await else { - if !args.raw { - eprintln!("{}", md(t!("jv.fail.cfg_not_found.local_config"))); - } - return; - }; - - let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path( - &local_dir, - &local_cfg.current_account(), - )) - .await - else { - if !args.raw { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.latest_info", - account = &local_cfg.current_account() - )) - ); - } - return; - }; - - let mut your_sheet_counts = 0; - let mut other_sheet_counts = 0; - - // JSON output handling - if args.json_output { - let mut json_result = SheetListJsonResult::default(); - - // Populate reference sheets - for sheet_name in &latest_info.reference_sheets { - json_result.reference_sheets.push(SheetItem { - name: sheet_name.clone(), - holder: local_cfg.current_account().clone(), - }); - } - - // Populate other sheets - for sheet in &latest_info.invisible_sheets { - json_result.other_sheets.push(SheetItem { - name: sheet.sheet_name.clone(), - holder: sheet.holder_name.clone().unwrap_or_default(), - }); - } - - // Populate my sheets (excluding reference sheets) - for sheet_name in &latest_info.visible_sheets { - if !latest_info.reference_sheets.contains(sheet_name) { - json_result.my_sheets.push(SheetItem { - name: sheet_name.clone(), - holder: local_cfg.current_account().clone(), - }); - } - } - - print_json(json_result, args.pretty); - return; - } - - if args.raw { - // Print your sheets - if !args.others && !args.all || !args.others { - latest_info - .visible_sheets - .iter() - .for_each(|s| println!("{}", s)); - } - // Print other sheets - if args.others || args.all { - latest_info - .invisible_sheets - .iter() - .for_each(|s| println!("{}", s.sheet_name)); - } - } else { - // Print your sheets - if !args.others && !args.all || !args.others { - println!("{}", md(t!("jv.success.sheet.list.your_sheet"))); - let in_use = local_cfg.sheet_in_use(); - for sheet in latest_info.visible_sheets { - let is_ref_sheet = latest_info.reference_sheets.contains(&sheet); - let display_name = if is_ref_sheet { - format!( - "{} {}", - sheet, - md(t!("jv.success.sheet.list.reference_sheet_suffix")) - .truecolor(128, 128, 128) - ) - } else { - sheet.clone() - }; - - if let Some(in_use) = in_use - && in_use == &sheet - { - println!( - "{}", - md(t!( - "jv.success.sheet.list.your_sheet_item_use", - number = your_sheet_counts + 1, - name = display_name.cyan() - )) - ); - } else { - println!( - "{}", - md(t!( - "jv.success.sheet.list.your_sheet_item", - number = your_sheet_counts + 1, - name = display_name - )) - ); - } - your_sheet_counts += 1; - } - } - - // Print other sheets - if args.others || args.all { - if args.all { - println!(); - } - println!("{}", md(t!("jv.success.sheet.list.other_sheet"))); - for sheet in latest_info.invisible_sheets { - if let Some(holder) = sheet.holder_name { - println!( - "{}", - md(t!( - "jv.success.sheet.list.other_sheet_item", - number = other_sheet_counts + 1, - name = sheet.sheet_name, - holder = holder - )) - ); - } else { - println!( - "{}", - md(t!( - "jv.success.sheet.list.other_sheet_item_no_holder", - number = other_sheet_counts + 1, - name = sheet.sheet_name - )) - ); - } - other_sheet_counts += 1; - } - } - - // If not use any sheets, print tips - if local_cfg.sheet_in_use().is_none() { - println!(); - if your_sheet_counts > 0 { - println!("{}", md(t!("jv.success.sheet.list.tip_has_sheet"))); - } else { - println!("{}", md(t!("jv.success.sheet.list.tip_no_sheet"))); - } - } - } -} - -async fn jv_sheet_use(args: SheetUseArgs) { - let Some(local_dir) = current_local_path() else { - eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); - return; - }; - - let current_dir = current_dir().unwrap(); - - if local_dir != current_dir { - eprintln!("{}", t!("jv.fail.not_root_dir").trim()); - return; - } - - let Ok(mut local_cfg) = LocalConfig::read().await else { - eprintln!("{}", md(t!("jv.fail.cfg_not_found.local_config"))); - return; - }; - - match local_cfg.use_sheet(args.sheet_name.clone()).await { - Ok(_) => { - match LocalConfig::write(&local_cfg).await { - Ok(_) => (), - Err(e) => { - eprintln!("{}", md(t!("jv.fail.write_cfg", error = e.to_string()))); - return; - } - }; - - // After successfully switching sheets, status should be automatically prompted - jv_status(StatusArgs { - help: false, - json_output: false, - pretty: false, - }) - .await; - } - Err(e) => match e.kind() { - std::io::ErrorKind::AlreadyExists => {} // Already In Use - std::io::ErrorKind::NotFound => { - eprintln!( - "{}", - md(t!("jv.fail.use.sheet_not_exists", name = args.sheet_name)) - ); - } - std::io::ErrorKind::DirectoryNotEmpty => { - eprintln!( - "{}", - md(t!( - "jv.fail.use.directory_not_empty", - name = args.sheet_name - )) - ); - } - _ => { - handle_err(e.into()); - } - }, - } -} - -async fn jv_sheet_exit(_args: SheetExitArgs) -> Result<(), ()> { - let Some(local_dir) = current_local_path() else { - eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); - return Err(()); - }; - - let current_dir = current_dir().unwrap(); - - if local_dir != current_dir { - eprintln!("{}", t!("jv.fail.not_root_dir").trim()); - return Err(()); - } - - let Ok(mut local_cfg) = LocalConfig::read().await else { - eprintln!("{}", md(t!("jv.fail.cfg_not_found.local_config"))); - return Err(()); - }; - - match local_cfg.exit_sheet().await { - Ok(_) => { - match LocalConfig::write(&local_cfg).await { - Ok(_) => (), - Err(e) => { - eprintln!("{}", md(t!("jv.fail.write_cfg", error = e.to_string()))); - return Err(()); - } - }; - Ok(()) - } - Err(e) => { - handle_err(e.into()); - Err(()) - } - } -} - -async fn jv_sheet_make(args: SheetMakeArgs) { - let sheet_name = snake_case!(args.sheet_name); - - let local_config = match precheck().await { - Some(config) => config, - None => return, - }; - - let (pool, ctx, _output) = match build_pool_and_ctx(&local_config).await { - Some(result) => result, - None => return, - }; - - let Some(local_dir) = current_local_path() else { - eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); - return; - }; - - let latest_info = match LatestInfo::read_from(LatestInfo::latest_info_path( - &local_dir, - &local_config.current_account(), - )) - .await - { - Ok(info) => info, - Err(_) => { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.latest_info", - account = &local_config.current_account() - )) - ); - return; - } - }; - - if latest_info - .invisible_sheets - .iter() - .any(|sheet| sheet.sheet_name == sheet_name && sheet.holder_name.is_none()) - { - println!( - "{}", - md(t!("jv.confirm.sheet.make.restore", sheet_name = sheet_name)).yellow() - ); - if !confirm_hint(t!("common.confirm")).await { - return; - } - } - - match proc_make_sheet_action(&pool, ctx, sheet_name.clone()).await { - Ok(r) => match r { - MakeSheetActionResult::Success => { - println!( - "{}", - md(t!("jv.result.sheet.make.success", name = sheet_name)) - ) - } - MakeSheetActionResult::SuccessRestore => { - println!( - "{}", - md(t!( - "jv.result.sheet.make.success_restore", - name = sheet_name - )) - ) - } - MakeSheetActionResult::AuthorizeFailed(e) => { - eprintln!("{}", md(t!("jv.result.common.authroize_failed", err = e))) - } - MakeSheetActionResult::SheetAlreadyExists => { - eprintln!( - "{}", - md(t!( - "jv.result.sheet.make.sheet_already_exists", - name = sheet_name - )) - ); - } - MakeSheetActionResult::SheetCreationFailed(e) => { - eprintln!( - "{}", - md(t!("jv.result.sheet.make.sheet_creation_failed", err = e)) - ) - } - MakeSheetActionResult::Unknown => todo!(), - }, - Err(e) => handle_err(e), - } -} - -async fn jv_sheet_drop(args: SheetDropArgs) { - let sheet_name = snake_case!(args.sheet_name); - - if !args.confirm { - println!( - "{}", - t!("jv.confirm.sheet.drop", sheet_name = sheet_name) - .trim() - .yellow() - ); - confirm_hint_or(t!("common.confirm"), || exit(1)).await; - } - - let local_config = match precheck().await { - Some(config) => config, - None => return, - }; - - let (pool, ctx, _output) = match build_pool_and_ctx(&local_config).await { - Some(result) => result, - None => return, - }; - - match proc_drop_sheet_action(&pool, ctx, sheet_name.clone()).await { - Ok(r) => match r { - DropSheetActionResult::Success => { - println!( - "{}", - md(t!("jv.result.sheet.drop.success", name = sheet_name)) - ) - } - DropSheetActionResult::SheetInUse => { - eprintln!( - "{}", - md(t!("jv.result.sheet.drop.sheet_in_use", name = sheet_name)) - ) - } - DropSheetActionResult::AuthorizeFailed(e) => { - eprintln!("{}", md(t!("jv.result.common.authroize_failed", err = e))) - } - DropSheetActionResult::SheetNotExists => { - eprintln!( - "{}", - md(t!( - "jv.result.sheet.drop.sheet_not_exists", - name = sheet_name - )) - ) - } - DropSheetActionResult::SheetDropFailed(e) => { - eprintln!( - "{}", - md(t!("jv.result.sheet.drop.sheet_drop_failed", err = e)) - ) - } - DropSheetActionResult::NoHolder => { - eprintln!( - "{}", - md(t!("jv.result.sheet.drop.no_holder", name = sheet_name)) - ) - } - DropSheetActionResult::NotOwner => { - eprintln!( - "{}", - md(t!("jv.result.sheet.drop.not_owner", name = sheet_name)) - ) - } - _ => {} - }, - Err(e) => handle_err(e), - } -} - -async fn jv_sheet_align(args: SheetAlignArgs) { - let Some(local_dir) = current_local_path() else { - eprintln!("{}", md(t!("jv.fail.workspace_not_found")).trim()); - return; - }; - - let local_cfg = match precheck().await { - Some(config) => config, - None => { - return; - } - }; - - let account = local_cfg.current_account(); - - let Some(sheet_name) = local_cfg.sheet_in_use().clone() else { - eprintln!("{}", md(t!("jv.fail.status.no_sheet_in_use")).trim()); - return; - }; - - let Some(local_workspace) = LocalWorkspace::init_current_dir(local_cfg.clone()) else { - eprintln!("{}", md(t!("jv.fail.workspace_not_found")).trim()); - return; - }; - - let Ok(mut local_sheet) = local_workspace.local_sheet(&account, &sheet_name).await else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.local_sheet", - account = &account, - sheet = &sheet_name - )) - ); - return; - }; - - let Ok(analyzed) = AnalyzeResult::analyze_local_status(&local_workspace).await else { - eprintln!("{}", md(t!("jv.fail.status.analyze")).trim()); - return; - }; - - let align_tasks = AlignTasks::from_analyze_result(analyzed); - - // No task input, list all tasks needs align - let Some(task) = args.task else { - // Raw output - if args.raw { - if args.list_all { - align_tasks.created.iter().for_each(|i| println!("{}", i.0)); - align_tasks.moved.iter().for_each(|i| println!("{}", i.0)); - align_tasks.lost.iter().for_each(|i| println!("{}", i.0)); - align_tasks.erased.iter().for_each(|i| println!("{}", i.0)); - return; - } - if args.list_created { - align_tasks.created.iter().for_each(|i| println!("{}", i.0)); - return; - } - if args.list_unsolved { - align_tasks.moved.iter().for_each(|i| println!("{}", i.0)); - align_tasks.lost.iter().for_each(|i| println!("{}", i.0)); - align_tasks.erased.iter().for_each(|i| println!("{}", i.0)); - return; - } - return; - } - - // Json Output - if args.json_output { - let mut json_result = AlignJsonResult::default(); - - for (name, local_path) in &align_tasks.created { - json_result.align_tasks.insert( - name.clone(), - AlignTaskMapping { - local_mapping: local_path.clone(), - remote_mapping: PathBuf::new(), - }, - ); - } - for (name, (remote_path, local_path)) in &align_tasks.moved { - json_result.align_tasks.insert( - name.clone(), - AlignTaskMapping { - local_mapping: local_path.clone(), - remote_mapping: remote_path.clone(), - }, - ); - } - for (name, local_path) in &align_tasks.lost { - json_result.align_tasks.insert( - name.clone(), - AlignTaskMapping { - local_mapping: local_path.clone(), - remote_mapping: PathBuf::new(), - }, - ); - } - for (name, local_path) in &align_tasks.erased { - json_result.align_tasks.insert( - name.clone(), - AlignTaskMapping { - local_mapping: local_path.clone(), - remote_mapping: PathBuf::new(), - }, - ); - } - - print_json(json_result, args.pretty); - return; - } - - let mut table = SimpleTable::new(vec![ - t!("jv.success.sheet.align.task_name").to_string(), - t!("jv.success.sheet.align.local_path").to_string(), - if !align_tasks.moved.is_empty() { - t!("jv.success.sheet.align.remote_path").to_string() - } else { - "".to_string() - }, - ]); - - let mut need_align = 0; - - if !align_tasks.created.is_empty() { - align_tasks.created.iter().for_each(|(n, p)| { - table.push_item(vec![ - format!("+ {}", n).green().to_string(), - p.display().to_string().green().to_string(), - "".to_string(), - ]); - }); - } - - if !align_tasks.lost.is_empty() { - align_tasks.lost.iter().for_each(|(n, p)| { - table.push_item(vec![ - format!("- {}", n).red().to_string(), - p.display().to_string().red().to_string(), - "".to_string(), - ]); - }); - need_align += 1; - } - - if !align_tasks.erased.is_empty() { - align_tasks.erased.iter().for_each(|(n, p)| { - table.push_item(vec![ - format!("& {}", n).magenta().to_string(), - p.display().to_string().magenta().to_string(), - "".to_string(), - ]); - }); - need_align += 1; - } - - if !align_tasks.moved.is_empty() { - align_tasks.moved.iter().for_each(|(n, (rp, lp))| { - table.push_item(vec![ - format!("> {}", n).yellow().to_string(), - lp.display().to_string().yellow().to_string(), - rp.display().to_string(), - ]); - }); - need_align += 1; - } - - if need_align > 0 { - println!( - "{}", - md(t!("jv.success.sheet.align.list", tasks = table.to_string())) - ); - - // Suggestion1: Confirm Erased - if !align_tasks.erased.is_empty() { - println!( - "\n{}", - md(t!( - "jv.success.sheet.align.suggestion_1", - example_erased = align_tasks.erased[0].0 - )) - ) - } else - // Suggestion2: Confirm Lost - if !align_tasks.lost.is_empty() { - println!( - "\n{}", - md(t!( - "jv.success.sheet.align.suggestion_2", - example_lost = align_tasks.lost[0].0 - )) - ) - } else - // Suggestion3: Confirm Moved - if !align_tasks.moved.is_empty() { - println!( - "\n{}", - md(t!( - "jv.success.sheet.align.suggestion_3", - example_moved = align_tasks.moved[0].0 - )) - ) - } - } else { - println!("{}", md(t!("jv.success.sheet.align.no_changes").trim())); - } - - return; - }; - - let Some(to) = args.to else { - eprintln!("{}", md(t!("jv.fail.sheet.align.no_direction"))); - return; - }; - - // Move: alignment mode - if task.starts_with("moved") { - let align_to = match to.trim().to_lowercase().as_str() { - "remote" => "remote", - "local" => "local", - "break" => "break", - _ => { - eprintln!("{}", md(t!("jv.fail.sheet.align.unknown_moved_direction"))); - return; - } - }; - - // Build remote move operations - let operations: HashMap = if task == "moved" { - // Align all moved items - align_tasks - .moved - .iter() - .map(|(_, (remote_path, local_path))| { - ( - remote_path.clone(), - (EditMappingOperations::Move, Some(local_path.clone())), - ) - }) - .collect() - } else { - // Align specific moved item - align_tasks - .moved - .iter() - .filter(|(key, _)| key == &task) - .map(|(_, (remote_path, local_path))| { - ( - remote_path.clone(), - (EditMappingOperations::Move, Some(local_path.clone())), - ) - }) - .collect() - }; - - if align_to == "local" { - // Align to local - // Network move mapping - let (pool, ctx, _output) = match build_pool_and_ctx(&local_cfg).await { - Some(result) => result, - None => return, - }; - - // Process mapping edit, errors are handled internally - let _ = proc_mapping_edit(&pool, ctx, EditMappingActionArguments { operations }).await; - } else if align_to == "remote" { - // Align to remote - // Offline move files - for (remote_path, (_, local_path)) in operations { - let local_path = local_path.unwrap(); - let from = local_dir.join(&local_path); - let to = local_dir.join(&remote_path); - - if to.exists() { - eprintln!( - "{}", - md(t!( - "jv.fail.sheet.align.target_exists", - local = local_path.display(), - remote = remote_path.display() - )) - ); - return; - } - - if let Some(parent) = to.parent() { - if let Err(err) = fs::create_dir_all(parent).await { - eprintln!("{}", md(t!("jv.fail.sheet.align.move_failed", err = err))); - continue; - } - } else { - eprintln!( - "{}", - md(t!( - "jv.fail.sheet.align.move_failed", - err = "no parent directory" - )) - ); - continue; - } - if let Err(err) = fs::rename(from, to).await { - eprintln!("{}", md(t!("jv.fail.sheet.align.move_failed", err = err))); - } - } - } else if align_to == "break" { - for (remote_path, (_, _)) in operations { - let Ok(mapping) = local_sheet.mapping_data_mut(&remote_path) else { - eprintln!( - "{}", - md(t!( - "jv.fail.sheet.align.mapping_not_found", - mapping = remote_path.display() - )) - ); - return; - }; - - // Restore the latest detected hash to the original hash, - // making the analyzer unable to correctly match - // - // That is to say, - // if the file's hash has remained completely unchanged from the beginning to the end, - // then break is also ineffective. - mapping.set_last_modifiy_check_hash(Some(mapping.hash_when_updated().clone())); - } - - // Save sheet - match local_sheet.write().await { - Ok(_) => {} - Err(e) => { - eprintln!("{}", md(t!("jv.fail.write_cfg", error = e.to_string()))); - } - } - } - } - // Lost: match or confirm mode - else if task.starts_with("lost") { - let selected_lost_mapping: Vec<(AlignTaskName, PathBuf)> = align_tasks - .lost - .iter() - .filter(|(name, _)| name.starts_with(&task)) - .cloned() - .collect(); - - if to == "confirm" { - // Confirm mode - for (_, path) in selected_lost_mapping { - if let Err(err) = local_sheet.remove_mapping(&path) { - eprintln!( - "{}", - md(t!("jv.fail.sheet.align.remove_mapping_failed", err = err)) - ); - }; - } - // Save sheet - match local_sheet.write().await { - Ok(_) => {} - Err(e) => { - eprintln!("{}", md(t!("jv.fail.write_cfg", error = e.to_string()))); - return; - } - }; - return; - } - - if to.starts_with("created") { - // Match mode - let created_file: Vec<(AlignTaskName, PathBuf)> = align_tasks - .created - .iter() - .find(|p| p.0.starts_with(&to)) - .cloned() - .into_iter() - .collect(); - - if selected_lost_mapping.is_empty() { - eprintln!("{}", md(t!("jv.fail.sheet.align.no_lost_matched"))); - return; - } - - if created_file.is_empty() { - eprintln!("{}", md(t!("jv.fail.sheet.align.no_created_matched"))); - return; - } - - if selected_lost_mapping.len() > 1 { - eprintln!("{}", md(t!("jv.fail.sheet.align.too_many_lost"))); - return; - } - - if created_file.len() > 1 { - eprintln!("{}", md(t!("jv.fail.sheet.align.too_many_created"))); - return; - } - - // Check completed, match lost and created items - let lost_mapping = &selected_lost_mapping.first().unwrap().1; - let created_file = local_dir.join(&created_file.first().unwrap().1); - - let Ok(hash_calc) = sha1_hash::calc_sha1(&created_file, 4096usize).await else { - eprintln!("{}", md(t!("jv.fail.sheet.align.calc_hash_failed"))); - return; - }; - let Ok(mapping) = local_sheet.mapping_data_mut(lost_mapping) else { - eprintln!( - "{}", - md(t!( - "jv.fail.sheet.align.mapping_not_found", - mapping = lost_mapping.display() - )) - ); - return; - }; - - mapping.set_last_modifiy_check_hash(Some(hash_calc.hash)); - - // Save sheet - match local_sheet.write().await { - Ok(_) => {} - Err(e) => { - eprintln!("{}", md(t!("jv.fail.write_cfg", error = e.to_string()))); - } - } - } - } - // Erased: confirm mode - else if task.starts_with("erased") { - let selected_erased_mapping: Vec<(AlignTaskName, PathBuf)> = align_tasks - .erased - .iter() - .filter(|(name, _)| name.starts_with(&task)) - .cloned() - .collect(); - - if to == "confirm" { - // Confirm mode - for (_, path) in selected_erased_mapping { - if let Err(err) = local_sheet.remove_mapping(&path) { - eprintln!( - "{}", - md(t!("jv.fail.sheet.align.delete_mapping_failed", err = err)) - ); - }; - - let from = local_dir.join(&path); - - if !from.exists() { - continue; - } - - let to = local_dir - .join(CLIENT_FOLDER_WORKSPACE_ROOT_NAME) - .join(".temp") - .join("erased") - .join(path); - let to_path = to - .parent() - .map(|p| p.to_path_buf()) - .unwrap_or_else(|| to.clone()); - - let _ = fs::create_dir_all(&to_path).await; - if let Some(e) = fs::rename(&from, &to).await.err() { - eprintln!( - "{}", - md(t!( - "jv.fail.move.rename_failed", - from = from.display(), - to = to.display(), - error = e - )) - .yellow() - ); - } - } - - // Save sheet - match local_sheet.write().await { - Ok(_) => {} - Err(e) => { - eprintln!("{}", md(t!("jv.fail.write_cfg", error = e.to_string()))); - return; - } - }; - } - } -} - -async fn jv_track(args: TrackFileArgs) { - // Perform glob operation before precheck, as precheck will call set_current_dir - let track_files = if let Some(pattern) = args.track_file_pattern.clone() { - let local_dir = match current_local_path() { - Some(dir) => dir, - None => { - eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); - return; - } - }; - let files = glob(pattern, &local_dir).await; - files - .iter() - .filter_map(|f| PathBuf::from_str(f.0).ok()) - .collect::>() - } else { - println!("{}", md(t!("jv.track"))); - return; - }; - - // set_current_dir called here - let local_config = match precheck().await { - Some(config) => config, - None => { - return; - } - }; - - let Some(local_workspace) = LocalWorkspace::init_current_dir(local_config.clone()) else { - eprintln!("{}", md(t!("jv.fail.workspace_not_found")).trim()); - return; - }; - - if track_files.iter().len() < 1 { - eprintln!("{}", md(t!("jv.fail.track.no_selection"))); - return; - }; - - let (pool, ctx, mut output) = match build_pool_and_ctx(&local_config).await { - Some(result) => result, - None => return, - }; - - let files = track_files.iter().cloned().collect(); - let overwrite = args.allow_overwrite; - let update_info = get_update_info(local_workspace, &files, args).await; - - let track_action = proc_track_file_action( - &pool, - ctx, - TrackFileActionArguments { - relative_pathes: files, - file_update_info: update_info, - print_infos: true, - allow_overwrite_modified: overwrite, - }, - ); - - tokio::select! { - result = track_action => { - match result { - Ok(result) => match result { - TrackFileActionResult::Done { - created, - updated, - synced, - skipped, - } => { - println!( - "{}", - md(t!( - "jv.result.track.done", - count = created.len() + updated.len() + synced.len(), - created = created.len(), - updated = updated.len(), - synced = synced.len() - )) - ); - - if !skipped.is_empty() { - println!( - "\n{}", - md(t!( - "jv.result.track.tip_has_skipped", - skipped_num = skipped.len(), - skipped = skipped - .iter() - .map(|f| f.display().to_string()) - .collect::>() - .join("\n") - .trim() - )) - .yellow() - ); - } - } - TrackFileActionResult::AuthorizeFailed(e) => { - eprintln!("{}", md(t!("jv.result.common.authroize_failed", err = e))) - } - TrackFileActionResult::StructureChangesNotSolved => { - eprintln!("{}", md(t!("jv.result.track.structure_changes_not_solved"))) - } - TrackFileActionResult::CreateTaskFailed(create_task_result) => match create_task_result - { - CreateTaskResult::Success(_) => {} // Success is not handled here - CreateTaskResult::CreateFileOnExistPath(path) => { - eprintln!( - "{}", - md(t!( - "jv.result.track.create_failed.create_file_on_exist_path", - path = path.display() - )) - ) - } - CreateTaskResult::SheetNotFound(sheet) => { - eprintln!( - "{}", - md(t!( - "jv.result.track.create_failed.sheet_not_found", - name = sheet - )) - ) - } - }, - TrackFileActionResult::UpdateTaskFailed(update_task_result) => match update_task_result - { - UpdateTaskResult::Success(_) => {} // Success is not handled here - UpdateTaskResult::VerifyFailed { path, reason } => match reason { - VerifyFailReason::SheetNotFound(sheet_name) => { - eprintln!( - "{}", - md(t!( - "jv.result.track.update_failed.verify.sheet_not_found", - sheet_name = sheet_name - )) - ) - } - VerifyFailReason::MappingNotFound => { - eprintln!( - "{}", - md(t!( - "jv.result.track.update_failed.verify.mapping_not_found", - path = path.display() - )) - ) - } - VerifyFailReason::VirtualFileNotFound(vfid) => { - eprintln!( - "{}", - md(t!( - "jv.result.track.update_failed.verify.virtual_file_not_found", - vfid = vfid - )) - ) - } - VerifyFailReason::VirtualFileReadFailed(vfid) => { - eprintln!( - "{}", - md(t!( - "jv.result.track.update_failed.verify.virtual_file_read_failed", - vfid = vfid - )) - ) - } - VerifyFailReason::NotHeld => { - eprintln!( - "{}", - md(t!( - "jv.result.track.update_failed.verify.not_held", - path = path.display() - )) - ) - } - VerifyFailReason::VersionDismatch(current_version, latest_version) => { - eprintln!( - "{}", - md(t!( - "jv.result.track.update_failed.verify.version_dismatch", - version_current = current_version, - version_latest = latest_version - )) - ) - } - VerifyFailReason::UpdateButNoDescription => { - eprintln!( - "{}", - md(t!( - "jv.result.track.update_failed.verify.update_but_no_description" - )) - ) - } - VerifyFailReason::VersionAlreadyExist(latest_version) => { - eprintln!( - "{}", - md(t!( - "jv.result.track.update_failed.verify.version_already_exist", - path = path.display(), - version = latest_version - )) - ) - } - }, - }, - TrackFileActionResult::SyncTaskFailed(sync_task_result) => match sync_task_result { - SyncTaskResult::Success(_) => {} // Success is not handled here - }, - }, - Err(e) => handle_err(e), - } - } - _ = async { - while let Some(msg) = output.recv().await { - println!("{}", msg); - } - } => {} - } -} - -async fn get_update_info( - workspace: LocalWorkspace, - files: &HashSet, - args: TrackFileArgs, -) -> HashMap { - let mut result = HashMap::new(); - - if files.len() == 1 - && let (Some(desc), Some(ver)) = (&args.desc, &args.next_version) - && let Some(file) = files.iter().next() - { - result.insert(file.clone(), (ver.clone(), desc.clone())); - return result; - } - if args.work { - return start_update_editor(workspace, files, &args).await; - } - - result -} - -async fn start_update_editor( - workspace: LocalWorkspace, - files: &HashSet, - args: &TrackFileArgs, -) -> HashMap { - let account = workspace.config().lock().await.current_account(); - - let Ok(latest_file_data_path) = LatestFileData::data_path(&account) else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.latest_file_data", - account = &account - )) - ); - return HashMap::new(); - }; - - // Get latest file data - let Ok(latest_file_data) = LatestFileData::read_from(&latest_file_data_path).await else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.latest_file_data", - account = &account - )) - ); - return HashMap::new(); - }; - - // Get files - let Ok(analyzed) = AnalyzeResult::analyze_local_status(&workspace).await else { - return HashMap::new(); - }; - // Has unsolved moves, skip - if !analyzed.lost.is_empty() || !analyzed.moved.is_empty() { - return HashMap::new(); - } - // No modified, skip - if analyzed.modified.is_empty() { - return HashMap::new(); - } - // No sheet, skip - let Some(sheet) = workspace.config().lock().await.sheet_in_use().clone() else { - return HashMap::new(); - }; - // No cached sheet, skip - let Ok(cached_sheet) = CachedSheet::cached_sheet_data(&sheet).await else { - return HashMap::new(); - }; - let files: Vec<(PathBuf, VirtualFileVersion)> = files - .iter() - .filter_map(|file| { - if analyzed.modified.contains(file) { - if let Some(mapping_item) = cached_sheet.mapping().get(file) - && let Some(latest_version) = latest_file_data.file_version(&mapping_item.id) - { - return Some((file.clone(), latest_version.clone())); - } - None - } else { - None - } - }) - .collect(); - - // Generate editor text - let mut table = SimpleTable::new_with_padding( - vec![ - t!("editor.modified_line.header.file_path").trim(), - t!("editor.modified_line.header.old_version").trim(), - "", - t!("editor.modified_line.header.new_version").trim(), - ], - 2, - ); - for item in files { - let path = item.0.display().to_string(); - let base_ver = item.1.to_string(); - let next_ver = push_version::push_version(&base_ver).unwrap_or(" ".to_string()); - table.push_item(vec![ - path, - base_ver, - t!("editor.modified_line.content.arrow").trim().to_string(), - next_ver, - ]); - } - let lines = table.to_string(); - - let str = t!( - "editor.update_editor", - modified_lines = lines, - description = args.desc.clone().unwrap_or_default() - ); - - let path = workspace - .local_path() - .join(CLIENT_PATH_WORKSPACE_ROOT) - .join(".UPDATE.md"); - let result = input_with_editor(str, path, "#").await.unwrap_or_default(); - - let mut update_info = HashMap::new(); - - // Parse the result returned from the editor - let lines: Vec<&str> = result.lines().collect(); - let mut i = 0; - - // Find the separator line - let mut separator_index = None; - while i < lines.len() { - let line = lines[i].trim(); - if line.chars().all(|c| c == '-') && line.len() >= 5 { - separator_index = Some(i); - break; - } - i += 1; - } - - if let Some(sep_idx) = separator_index { - // Parse path and version information before the separator - for line in &lines[..sep_idx] { - let trimmed_line = line.trim(); - if trimmed_line.is_empty() { - continue; - } - - // Parse format: /directory/file.extension version -> new_version - if let Some(arrow_pos) = trimmed_line.find("->") { - let before_arrow = &trimmed_line[..arrow_pos].trim(); - let after_arrow = &trimmed_line[arrow_pos + 2..].trim(); - - // Separate path and old version - if let Some(last_space) = before_arrow.rfind(' ') { - let path_str = &before_arrow[..last_space].trim(); - let _old_version = &before_arrow[last_space + 1..].trim(); // Old version, needs parsing but not used - let new_version = after_arrow.trim(); - - if !path_str.is_empty() && !new_version.is_empty() { - let path = PathBuf::from(path_str); - // Get description (all content after the separator) - let description = lines[sep_idx + 1..].join("\n").trim().to_string(); - - // Only add to update_info if description is not empty after trimming - if !description.is_empty() { - update_info.insert(path, (new_version.to_string(), description)); - } - } - } - } - } - } - - update_info -} - -async fn jv_hold(args: HoldFileArgs) { - let Some(local_dir) = current_local_path() else { - eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); - return; - }; - - let Some(hold_file_pattern) = args.hold_file_pattern else { - println!("{}", md(t!("jv.hold"))); - return; - }; - - let files = glob(hold_file_pattern, &local_dir).await; - - let _ = correct_current_dir(); - - jv_change_edit_right( - files - .iter() - .filter_map(|f| PathBuf::from_str(f.0).ok()) - .collect(), - EditRightChangeBehaviour::Hold, - args.show_fail_details, - args.skip_failed, - args.force, - ) - .await; -} - -async fn jv_throw(args: ThrowFileArgs) { - let Some(local_dir) = current_local_path() else { - eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); - return; - }; - - let Some(throw_file_pattern) = args.throw_file_pattern else { - println!("{}", md(t!("jv.throw"))); - return; - }; - - let files = glob(throw_file_pattern, &local_dir).await; - - let _ = correct_current_dir(); - - jv_change_edit_right( - files - .iter() - .filter_map(|f| PathBuf::from_str(f.0).ok()) - .collect(), - EditRightChangeBehaviour::Throw, - args.show_fail_details, - args.skip_failed, - args.force, - ) - .await; -} - -async fn jv_change_edit_right( - files: Vec, - behaviour: EditRightChangeBehaviour, - show_fail_details: bool, - mut skip_failed: bool, - force: bool, -) { - // If both `--details` and `--skip-failed` are set, only enable `--details` - if show_fail_details && skip_failed { - skip_failed = false; - } - - let Some(local_dir) = current_local_path() else { - eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); - return; - }; - - let Ok(local_cfg) = LocalConfig::read_from(local_dir.join(CLIENT_FILE_WORKSPACE)).await else { - eprintln!("{}", md(t!("jv.fail.cfg_not_found.local_config"))); - return; - }; - - let Some(local_workspace) = LocalWorkspace::init_current_dir(local_cfg.clone()) else { - eprintln!("{}", md(t!("jv.fail.workspace_not_found")).trim()); - return; - }; - - // Get files - let Ok(analyzed) = AnalyzeResult::analyze_local_status(&local_workspace).await else { - eprintln!("{}", md(t!("jv.fail.status.analyze")).trim()); - return; - }; - - let account = local_cfg.current_account(); - - let Ok(latest_file_data_path) = LatestFileData::data_path(&account) else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.latest_file_data", - account = &account - )) - ); - return; - }; - - let Ok(latest_file_data) = LatestFileData::read_from(&latest_file_data_path).await else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.latest_file_data", - account = &account - )) - ); - return; - }; - - let Some(sheet_name) = local_cfg.sheet_in_use().clone() else { - eprintln!("{}", md(t!("jv.fail.status.no_sheet_in_use")).trim()); - return; - }; - - let Ok(local_sheet) = local_workspace.local_sheet(&account, &sheet_name).await else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.local_sheet", - account = &account, - sheet = &sheet_name - )) - ); - return; - }; - - let Ok(cached_sheet) = CachedSheet::cached_sheet_data(&sheet_name).await else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.cached_sheet", - sheet = &sheet_name - )) - ); - return; - }; - - let num = files.iter().len(); - if num < 1 { - eprintln!("{}", md(t!("jv.fail.change_edit_right.no_selection"))); - return; - } - - let mut passed_files = Vec::new(); - let mut details = Vec::new(); - let mut failed = 0; - - // Helper function to handle validation failures - fn handle_validation_failure( - show_fail_details: bool, - details: &mut Vec, - failed: &mut usize, - num: usize, - reason: String, - ) -> bool { - if show_fail_details { - details.push(reason); - *failed += 1; - true // Continue to next file - } else { - eprintln!( - "{}", - md(t!("jv.fail.change_edit_right.check_failed", num = num)) - ); - false // Break processing - } - } - - for file in files { - let exists = file.exists(); - - // If force is enabled, add to the list regardless - if force { - passed_files.push(file); - continue; - } - - // Mapping exists - let Some(cached_mapping) = cached_sheet.mapping().get(&file) else { - let reason = t!( - "jv.fail.change_edit_right.check_fail_item", - path = file.display(), - reason = t!("jv.fail.change_edit_right.check_fail_reason.not_found_in_sheet") - ) - .trim() - .to_string(); - - if !handle_validation_failure(show_fail_details, &mut details, &mut failed, num, reason) - { - return; - } - continue; - }; - - let vfid: VirtualFileId = if exists { - // Not tracked - let Ok(local_mapping) = local_sheet.mapping_data(&file) else { - let reason = t!( - "jv.fail.change_edit_right.check_fail_item", - path = file.display(), - reason = t!("jv.fail.change_edit_right.check_fail_reason.not_a_tracked_file") - ) - .trim() - .to_string(); - - if !handle_validation_failure( - show_fail_details, - &mut details, - &mut failed, - num, - reason, - ) { - return; - } - continue; - }; - - let vfid = local_mapping.mapping_vfid(); - let local_version = local_mapping.version_when_updated(); - - // Base version unmatch - if local_version - != latest_file_data - .file_version(vfid) - .unwrap_or(&String::default()) - { - let reason = t!( - "jv.fail.change_edit_right.check_fail_item", - path = file.display(), - reason = t!("jv.fail.change_edit_right.check_fail_reason.base_version_unmatch") - ) - .trim() - .to_string(); - - if !handle_validation_failure( - show_fail_details, - &mut details, - &mut failed, - num, - reason, - ) { - return; - } - continue; - } - - vfid.clone() - } else { - cached_mapping.id.clone() - }; - - // Hold validation - let holder = latest_file_data.file_holder(&vfid); - let validation_passed = match behaviour { - EditRightChangeBehaviour::Hold => { - if holder.is_some_and(|h| h != &account) { - // Has holder but not current account - let holder_name = holder.unwrap(); - let reason = t!( - "jv.fail.change_edit_right.check_fail_item", - path = file.display(), - reason = t!( - "jv.fail.change_edit_right.check_fail_reason.has_holder", - holder = holder_name - ) - ) - .trim() - .to_string(); - - if !handle_validation_failure( - show_fail_details, - &mut details, - &mut failed, - num, - reason, - ) { - return; - } - false - } else if holder.is_some_and(|h| h == &account) { - // Already held by current account - let reason = t!( - "jv.fail.change_edit_right.check_fail_item", - path = file.display(), - reason = t!("jv.fail.change_edit_right.check_fail_reason.already_held") - ) - .trim() - .to_string(); - - if !handle_validation_failure( - show_fail_details, - &mut details, - &mut failed, - num, - reason, - ) { - return; - } - false - } else { - true - } - } - EditRightChangeBehaviour::Throw => { - if holder.is_some_and(|h| h != &account) { - // Not the holder - let reason = t!( - "jv.fail.change_edit_right.check_fail_item", - path = file.display(), - reason = t!("jv.fail.change_edit_right.check_fail_reason.not_holder") - ) - .trim() - .to_string(); - - if !handle_validation_failure( - show_fail_details, - &mut details, - &mut failed, - num, - reason, - ) { - return; - } - false - } else if analyzed.modified.contains(&file) { - // Already modified - let reason = t!( - "jv.fail.change_edit_right.check_fail_item", - path = file.display(), - reason = t!("jv.fail.change_edit_right.check_fail_reason.already_modified") - ) - .trim() - .to_string(); - - if !handle_validation_failure( - show_fail_details, - &mut details, - &mut failed, - num, - reason, - ) { - return; - } - false - } else { - true - } - } - }; - - if validation_passed { - passed_files.push(file); - } - } - - if failed > 0 && show_fail_details { - eprintln!( - "{}", - md(t!( - "jv.fail.change_edit_right.check_failed_details", - num = num, - failed = failed, - items = details.join("\n").trim().yellow() - )) - ); - return; - } - - if !(failed > 0 && skip_failed) && failed != 0 { - return; - } - - let (pool, ctx, _output) = match build_pool_and_ctx(&local_cfg).await { - Some(result) => result, - None => return, - }; - - let passed = passed_files - .iter() - .map(|f| (f.clone(), behaviour.clone())) - .collect(); - - match proc_change_virtual_file_edit_right_action(&pool, ctx, (passed, true)).await { - Ok(r) => match r { - ChangeVirtualFileEditRightResult::Success { - success_hold, - success_throw, - } => { - if !success_hold.is_empty() && success_throw.is_empty() { - println!( - "{}", - md(t!( - "jv.result.change_edit_right.success.hold", - num = success_hold.len() - )) - ) - } else if success_hold.is_empty() && !success_throw.is_empty() { - println!( - "{}", - md(t!( - "jv.result.change_edit_right.success.throw", - num = success_throw.len() - )) - ) - } else if !success_hold.is_empty() && !success_throw.is_empty() { - println!( - "{}", - md(t!( - "jv.result.change_edit_right.success.mixed", - num = success_hold.len() + success_throw.len(), - num_hold = success_hold.len(), - num_throw = success_throw.len() - )) - ) - } else { - eprintln!("{}", md(t!("jv.result.change_edit_right.failed.none"))) - } - } - ChangeVirtualFileEditRightResult::AuthorizeFailed(e) => { - eprintln!("{}", md(t!("jv.result.common.authroize_failed", err = e))) - } - ChangeVirtualFileEditRightResult::DoNothing => { - eprintln!("{}", md(t!("jv.result.change_edit_right.failed.none"))) - } - }, - Err(e) => handle_err(e), - } -} - -async fn jv_move(args: MoveMappingArgs) { - let local_dir = match current_local_path() { - Some(dir) => dir, - None => { - eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); - return; - } - }; - - let move_files = if let Some(from_pattern) = args.move_mapping_pattern.clone() { - let from = glob(from_pattern, &local_dir).await; - from.iter() - .filter_map(|f| PathBuf::from_str(f.0).ok()) - .collect::>() - } else { - println!("{}", md(t!("jv.move"))); - return; - }; - - let to_pattern = if args.to_mapping_pattern.is_some() { - args.to_mapping_pattern.unwrap() - } else if args.erase { - "".to_string() - } else { - eprintln!("{}", md(t!("jv.fail.move.no_target_dir"))); - return; - }; - - let is_to_pattern_a_dir = to_pattern.ends_with('/') || to_pattern.ends_with('\\'); - - let from_mappings = move_files - .iter() - .map(|f| f.display().to_string()) - .collect::>(); - - let base_path = Globber::from(&to_pattern).base().clone(); - let base_path = fmt_path(base_path.strip_prefix(&local_dir).unwrap().join("./")).unwrap(); - let to_path = base_path.join(to_pattern); - - let mut edit_mapping_args: EditMappingActionArguments = EditMappingActionArguments { - operations: HashMap::::new(), - }; - - if args.erase { - // Generate erase operation parameters - for from_mapping in from_mappings { - edit_mapping_args - .operations - .insert(from_mapping.into(), (EditMappingOperations::Erase, None)); - } - } else { - // Generate move operation parameters - // Single file move - if from_mappings.len() == 1 { - let from = fmt_path_str(from_mappings[0].clone()).unwrap(); - let to = if is_to_pattern_a_dir { - // Input is a directory, append the filename - fmt_path( - to_path - .join(from.strip_prefix(&base_path.display().to_string()).unwrap()) - .to_path_buf(), - ) - .unwrap() - } else { - // Input is a filename, use it directly - fmt_path(to_path.to_path_buf()).unwrap() - }; - - let from: PathBuf = from.into(); - // If the from path contains to_path, ignore it to avoid duplicate moves - if !from.starts_with(to_path) { - edit_mapping_args - .operations - .insert(from, (EditMappingOperations::Move, Some(to.clone()))); - } - } else - // Multiple file move - if from_mappings.len() > 1 && is_to_pattern_a_dir { - let to_path = fmt_path(to_path).unwrap(); - for p in &from_mappings { - let name = p.strip_prefix(&base_path.display().to_string()).unwrap(); - let to = fmt_path(to_path.join(name)).unwrap().display().to_string(); - - let from: PathBuf = p.into(); - // If the from path contains to_path, ignore it to avoid duplicate moves - if !from.starts_with(to_path.display().to_string()) { - edit_mapping_args - .operations - .insert(from, (EditMappingOperations::Move, Some(to.into()))); - } - } - } - if from_mappings.len() > 1 && !is_to_pattern_a_dir { - eprintln!("{}", md(t!("jv.fail.move.count_doesnt_match"))); - return; - } - - // NOTE - // if move_file_mappings.len() < 1 { - // This case has already been handled earlier: output Help - // } - } - - let local_cfg = match precheck().await { - Some(config) => config, - None => return, - }; - - let (pool, ctx, _output) = match build_pool_and_ctx(&local_cfg).await { - Some(result) => result, - None => return, - }; - - if proc_mapping_edit(&pool, ctx, edit_mapping_args.clone()) - .await - .is_ok() - { - // If the operation succeeds and only_remote is not enabled, - // synchronize local moves - if !args.only_remote { - let erase_dir = local_dir - .join(CLIENT_FOLDER_WORKSPACE_ROOT_NAME) - .join(".temp") - .join("erased"); - - let mut skipped = 0; - for (from_relative, (operation, to_relative)) in edit_mapping_args.operations { - let from = local_dir.join(&from_relative); - - if !from.exists() { - continue; - } - - let to = match operation { - EditMappingOperations::Move => local_dir.join(to_relative.unwrap()), - EditMappingOperations::Erase => erase_dir.join(&from_relative), - }; - if let Some(to_dir) = to.parent() { - let _ = fs::create_dir_all(to_dir).await; - } - if let Some(e) = fs::rename(&from, &to).await.err() { - eprintln!( - "{}", - md(t!( - "jv.fail.move.rename_failed", - from = from.display(), - to = to.display(), - error = e - )) - .yellow() - ); - skipped += 1; - } - } - if skipped > 0 { - eprintln!("{}", md(t!("jv.fail.move.has_rename_failed"))); - } - } - } -} - -async fn proc_mapping_edit( - pool: &ActionPool, - ctx: ActionContext, - edit_mapping_args: EditMappingActionArguments, -) -> Result<(), ()> { - match proc_edit_mapping_action( - pool, - ctx, - EditMappingActionArguments { - operations: edit_mapping_args.operations.clone(), - }, - ) - .await - { - Ok(r) => match r { - EditMappingActionResult::Success => { - println!("{}", md(t!("jv.result.move.success"))); - Ok(()) - } - EditMappingActionResult::AuthorizeFailed(e) => { - eprintln!("{}", md(t!("jv.result.common.authroize_failed", err = e))); - Err(()) - } - EditMappingActionResult::MappingNotFound(path_buf) => { - eprintln!( - "{}", - md(t!( - "jv.result.move.mapping_not_found", - path = path_buf.display() - )) - ); - Err(()) - } - EditMappingActionResult::InvalidMove(invalid_move_reason) => { - match invalid_move_reason { - InvalidMoveReason::MoveOperationButNoTarget(path_buf) => { - eprintln!( - "{}", - md(t!( - "jv.result.move.invalid_move.no_target", - path = path_buf.display() - )) - ); - } - InvalidMoveReason::ContainsDuplicateMapping(path_buf) => { - eprintln!( - "{}", - md(t!( - "jv.result.move.invalid_move.duplicate_mapping", - path = path_buf.display() - )) - ); - } - } - Err(()) - } - EditMappingActionResult::Unknown => { - eprintln!("{}", md(t!("jv.result.move.unknown"))); - Err(()) - } - EditMappingActionResult::EditNotAllowed => { - eprintln!( - "{}", - md(t!("jv.result.common.not_allowed_in_reference_sheet")) - ); - Err(()) - } - }, - Err(e) => { - handle_err(e); - Err(()) - } - } -} - -async fn jv_share(args: ShareMappingArgs) { - if let (Some(args1), None, None) = (&args.args1, &args.args2, &args.args3) { - // List mode - if args1.trim() == "list" || args1.trim() == "ls" { - share_list(args).await; - return; - } - - share_accept(args1.to_string(), args).await; - return; - } - - if let (Some(args1), Some(args2), None) = (&args.args1, &args.args2, &args.args3) { - // 如果是 work 模式,那么就是分享 - if args.work { - share_out( - args1.to_string(), - args2.to_string(), - String::default(), - args, - ) - .await; - return; - } - - // See mode - if args1.trim() == "see" { - share_see(args2.to_string(), args).await; - return; - } - - share_in(args1.to_string(), args2.to_string(), args).await; - return; - } - - if let (Some(share_pattern), Some(to_sheet), Some(description)) = - (&args.args1, &args.args2, &args.args3) - { - share_out( - share_pattern.to_string(), - to_sheet.to_string(), - description.to_string(), - args, - ) - .await; - return; - } - - println!("{}", md(t!("jv.share"))); -} - -async fn share_list(args: ShareMappingArgs) { - let _ = correct_current_dir(); - - let Some(local_dir) = current_local_path() else { - eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); - return; - }; - - let local_config = match precheck().await { - Some(config) => config, - None => return, - }; - - let sheet_name = local_config.sheet_in_use().clone().unwrap_or_default(); - - let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path( - &local_dir, - &local_config.current_account(), - )) - .await - else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.latest_info", - account = &local_config.current_account() - )) - ); - return; - }; - - if let Some(shares) = latest_info.shares_in_my_sheets.get(&sheet_name) { - // Sort - let mut sorted_shares: BTreeMap = BTreeMap::new(); - for (id, share) in shares { - sorted_shares.insert(id.clone(), share); - } - - if args.json_output { - let share_list: Vec = sorted_shares - .iter() - .map(|(share_id, share)| ShareItem { - share_id: share_id.clone(), - sharer: share.sharer.clone(), - description: share.description.clone(), - file_count: share.mappings.len(), - }) - .collect(); - let result = ShareListResult { share_list }; - print_json(result, args.pretty); - return; - } - - if !args.raw { - // Create table and insert information - let mut table = SimpleTable::new(vec![ - t!("jv.success.share.list.headers.id"), - t!("jv.success.share.list.headers.sharer"), - t!("jv.success.share.list.headers.description"), - t!("jv.success.share.list.headers.file_count"), - ]); - for (id, share) in sorted_shares { - table.insert_item( - 0, - vec![ - id.to_string(), - share.sharer.to_string(), - truncate_first_line(share.description.to_string()), - share.mappings.len().to_string(), - ], - ); - } - - // Render - println!("{}", table); - println!("{}", md(t!("jv.success.share.list.footer"))); - } else { - sorted_shares - .iter() - .for_each(|share| println!("{}", share.0)); - } - } -} - -async fn share_see(share_id: String, args: ShareMappingArgs) { - let _ = correct_current_dir(); - - let Some(local_dir) = current_local_path() else { - eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); - return; - }; - - let local_config = match precheck().await { - Some(config) => config, - None => return, - }; - - let sheet_name = local_config.sheet_in_use().clone().unwrap_or_default(); - - let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path( - &local_dir, - &local_config.current_account(), - )) - .await - else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.latest_info", - account = &local_config.current_account() - )) - ); - return; - }; - - if let Some(shares) = latest_info.shares_in_my_sheets.get(&sheet_name) - && let Some(share) = shares.get(&share_id) - { - if args.json_output { - let result = SeeShareResult { - share_id: share_id.clone(), - sharer: share.sharer.clone(), - description: share.description.clone(), - mappings: share.mappings.clone(), - }; - print_json(result, args.pretty); - return; - } - - println!( - "{}", - md(t!( - "jv.success.share.content", - share_id = share_id, - sharer = share.sharer, - description = share.description, - mappings = render_share_path_tree(&share.mappings) - )) - ); - } -} - -async fn share_accept(import_id: String, args: ShareMappingArgs) { - let _ = correct_current_dir(); - - let Some(local_dir) = current_local_path() else { - eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); - return; - }; - - let local_config = match precheck().await { - Some(config) => config, - None => return, - }; - - let sheet_name = local_config.sheet_in_use().clone().unwrap_or_default(); - - let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path( - &local_dir, - &local_config.current_account(), - )) - .await - else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.latest_info", - account = &local_config.current_account() - )) - ); - return; - }; - - let contains_share = if let Some(share_ids) = latest_info.shares_in_my_sheets.get(&sheet_name) { - share_ids.contains_key(&import_id) - } else { - false - }; - - if !contains_share { - eprintln!( - "{}", - md(t!("jv.fail.share.share_id_not_exist", id = &import_id)) - ); - return; - } - - let (pool, ctx, _output) = match build_pool_and_ctx(&local_config).await { - Some(result) => result, - None => return, - }; - - let share_merge_mode = { - if args.safe { - ShareMergeMode::Safe - } else if args.skip { - ShareMergeMode::Skip - } else if args.overwrite { - ShareMergeMode::Overwrite - } else if args.reject { - ShareMergeMode::RejectAll - } else { - ShareMergeMode::Safe - } - }; - - match proc_merge_share_mapping_action( - &pool, - ctx, - MergeShareMappingArguments { - share_id: import_id.clone(), - share_merge_mode, - }, - ) - .await - { - Ok(r) => match r { - MergeShareMappingActionResult::Success => { - if args.reject { - println!( - "{}", - md(t!( - "jv.result.share.merge_shares.success_reject", - share_id = &import_id - )) - ); - } else { - println!( - "{}", - md(t!( - "jv.result.share.merge_shares.success", - share_id = &import_id, - sheet = &sheet_name - )) - ); - } - } - MergeShareMappingActionResult::HasConflicts => { - eprintln!( - "{}", - md(t!( - "jv.result.share.merge_shares.has_conflicts", - share_id = &import_id - )) - ); - } - MergeShareMappingActionResult::AuthorizeFailed(e) => { - eprintln!("{}", md(t!("jv.result.common.authroize_failed", err = e))); - } - MergeShareMappingActionResult::EditNotAllowed => { - eprintln!( - "{}", - md(t!("jv.result.share.merge_shares.edit_not_allowed")) - ); - } - MergeShareMappingActionResult::ShareIdNotFound(share_id) => { - eprintln!( - "{}", - md(t!( - "jv.result.share.merge_shares.share_id_not_found", - share_id = share_id - )) - ); - } - MergeShareMappingActionResult::MergeFails(error) => { - eprintln!( - "{}", - md(t!( - "jv.result.share.merge_shares.merge_failed", - error = error - )) - ); - } - MergeShareMappingActionResult::Unknown => { - eprintln!("{}", md(t!("jv.result.share.merge_shares.unknown"))); - } - }, - Err(e) => handle_err(e), - } -} - -async fn share_in(_from_sheet: String, _import_pattern: String, _args: ShareMappingArgs) { - // TODO: Implement pull mode logic -} - -async fn share_out( - share_pattern: String, - to_sheet: String, - description: String, - args: ShareMappingArgs, -) { - let mut shared_files = { - let local_dir = match current_local_path() { - Some(dir) => dir, - None => { - eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); - return; - } - }; - let files = glob(share_pattern, &local_dir).await; - files - .iter() - .filter_map(|f| PathBuf::from_str(f.0).ok()) - .collect::>() - }; - - let _ = correct_current_dir(); - - let Some(local_dir) = current_local_path() else { - eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); - return; - }; - - let local_config = match precheck().await { - Some(config) => config, - None => return, - }; - - let Some(local_workspace) = LocalWorkspace::init_current_dir(local_config.clone()) else { - eprintln!("{}", md(t!("jv.fail.workspace_not_found")).trim()); - return; - }; - - let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path( - &local_dir, - &local_config.current_account(), - )) - .await - else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.latest_info", - account = &local_config.current_account() - )) - ); - return; - }; - - // Pre-check if the sheet exists - let contains_in_my_sheet = latest_info.visible_sheets.contains(&to_sheet); - let contains_in_other_sheet = latest_info - .invisible_sheets - .iter() - .any(|info| info.sheet_name == to_sheet); - if !contains_in_my_sheet && !contains_in_other_sheet { - eprintln!( - "{}", - md(t!("jv.fail.share.invalid_target_sheet", sheet = &to_sheet)) - ); - return; - } - - let (pool, ctx, _output) = match build_pool_and_ctx(&local_config).await { - Some(result) => result, - None => return, - }; - - let to_sheet_holder = { - if latest_info.reference_sheets.contains(&to_sheet) { - VAULT_HOST_NAME.to_string() - } else if latest_info.visible_sheets.contains(&to_sheet) { - local_config.current_account() - } else { - let mut holder = String::new(); - for info in &latest_info.invisible_sheets { - if info.sheet_name == to_sheet { - holder = info.holder_name.as_ref().cloned().unwrap_or_default(); - break; - } - } - holder - } - }; - - let Some(description) = (if args.work { - start_share_editor(&local_workspace, &mut shared_files, &to_sheet_holder).await - } else { - Some(description.to_string()) - }) else { - eprintln!("{}", md(t!("jv.fail.share.no_description"))); - return; - }; - - match proc_share_mapping_action( - &pool, - ctx, - ShareMappingArguments { - mappings: shared_files.clone(), - description, - - // Since the Action internally checks the current sheet, - // there's no need to fill in from_sheet here. - // This is prepared for pull operations. - from_sheet: None, - to_sheet: to_sheet.clone(), - }, - ) - .await - { - Ok(r) => match r { - ShareMappingActionResult::Success => { - println!( - "{}", - md(t!( - "jv.result.share.share_mapping.success", - file_nums = shared_files.len(), - to_sheet = to_sheet, - to_sheet_holder = to_sheet_holder - )) - ); - } - ShareMappingActionResult::AuthorizeFailed(e) => { - eprintln!("{}", md(t!("jv.result.common.authroize_failed", err = e))); - } - ShareMappingActionResult::TargetSheetNotFound(sheet) => { - eprintln!( - "{}", - md(t!( - "jv.result.share.share_mapping.target_sheet_not_found", - to_sheet = sheet - )) - ); - } - ShareMappingActionResult::TargetIsSelf => { - eprintln!("{}", md(t!("jv.result.share.share_mapping.target_is_self"))); - } - ShareMappingActionResult::MappingNotFound(path_buf) => { - eprintln!( - "{}", - md(t!( - "jv.result.share.share_mapping.mapping_not_found", - mapping = path_buf.display() - )) - ); - } - ShareMappingActionResult::Unknown => { - eprintln!("{}", md(t!("jv.result.share.share_mapping.unknown"))); - } - }, - Err(e) => handle_err(e), - } -} - -async fn start_share_editor( - workspace: &LocalWorkspace, - shared_files: &mut Vec, - holder: &MemberId, -) -> Option { - let config = workspace.config().lock().await.clone(); - - let sheet_name = config.sheet_in_use().clone(); - let Some(sheet_name) = sheet_name else { - eprintln!("{}", md(t!("jv.fail.status.no_sheet_in_use")).trim()); - return None; - }; - - let account = config.current_account(); - let Ok(local_sheet) = workspace.local_sheet(&account, &sheet_name).await else { - eprintln!( - "{}", - md(t!( - "jv.fail.cfg_not_found.local_sheet", - account = &account, - sheet = &sheet_name - )) - ); - return None; - }; - - // Generate shared files and automatically comment out lost mappings - let shared_files_str: String = shared_files - .iter() - .map(|p| { - if local_sheet.mapping_data(p).is_err() { - format!("# {}", p.display()) - } else { - p.display().to_string() - } - }) - .collect::>() - .join("\n"); - let shared_files_str = shared_files_str.trim(); - - let editor_content = t!( - "editor.share_editor", - sheet = sheet_name, - holder = holder, - shared_files = shared_files_str - ); - - let path = workspace - .local_path() - .join(CLIENT_PATH_WORKSPACE_ROOT) - .join(".SHARE.md"); - - let result = input_with_editor(format!("{}\n", editor_content), path, "#") - .await - .unwrap_or_default(); - - // Find the last separator line (a line that, when trimmed, consists of at least 3 '-' characters) - let lines: Vec<&str> = result.lines().collect(); - let mut last_separator_index = None; - - for (i, line) in lines.iter().enumerate() { - let trimmed = line.trim(); - if trimmed.chars().all(|c| c == '-') && trimmed.len() >= 3 { - last_separator_index = Some(i); - } - } - - // Extract content after the last separator - let description = if let Some(sep_idx) = last_separator_index { - lines[sep_idx + 1..].join("\n").trim().to_string() - } else { - result.trim().to_string() - }; - - // Update shared_files with the first segment (before the first separator) - if let Some(sep_idx) = last_separator_index { - let first_segment: Vec = lines[..sep_idx] - .iter() - .map(|line| line.trim()) - .filter(|line| !line.is_empty()) - .map(PathBuf::from) - .collect(); - *shared_files = first_segment; - } - - if description.trim().is_empty() { - None - } else { - Some(description) - } -} - -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)); - return; - } - } - if args.keygen { - let output_path = current_dir().unwrap().join("tempkey.pem"); - - match Command::new("openssl") - .args([ - "genpkey", - "-algorithm", - "ed25519", - "-out", - &output_path.to_string_lossy(), - ]) - .status() - .await - { - Ok(status) if status.success() => { - jv_account_move_key( - user_dir, - MoveKeyToAccountArgs { - help: false, - account_name: args.account_name, - key_path: output_path, - }, - ) - .await - } - Ok(_) => { - eprintln!("{}", t!("jv.fail.account.keygen")); - } - Err(_) => { - eprintln!("{}", t!("jv.fail.account.keygen_exec")); - } - } - } -} - -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) { - let _ = correct_current_dir(); - - if args.json_output { - let Ok(account_ids) = user_dir.account_ids() else { - return; - }; - let mut result = HashMap::new(); - for account_id in account_ids { - let has_private_key = user_dir.has_private_key(&account_id); - result.insert(account_id, AccountItem { has_private_key }); - } - let json_result = AccountListJsonResult { result }; - print_json(json_result, args.pretty); - return; - } - - if args.raw { - let Ok(account_ids) = user_dir.account_ids() else { - return; - }; - account_ids.iter().for_each(|a| println!("host/{}", a)); - account_ids.iter().for_each(|a| println!("{}", a)); - return; - } - - 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) { - let (account, is_host_mode) = process_account_parameter(args.account_name); - - // Account exist - let Ok(member) = user_dir.account(&account).await else { - eprintln!("{}", t!("jv.fail.account.not_found", account = &account)); - 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.cfg_not_found.local_config"))); - return; - }; - - if local_cfg.set_current_account(member.id()).is_err() { - eprintln!("{}", md(t!("jv.fail.account.as"))); - return; - }; - - local_cfg.set_host_mode(is_host_mode); - - match LocalConfig::write(&local_cfg).await { - Ok(_) => {} - Err(e) => { - eprintln!("{}", md(t!("jv.fail.write_cfg", error = e))); - return; - } - }; - - if is_host_mode { - println!( - "{}", - md(t!("jv.success.account.as_host", account = member.id())) - ); - } else { - println!( - "{}", - t!("jv.success.account.as", account = member.id()).trim() - ); - } -} - -/// Input account, get MemberId and whether it's a host -/// If input is host/xxx, output is xxx, true -/// If input is xxx, output is xxx, false -fn process_account_parameter(account_input: String) -> (MemberId, bool) { - let is_host = account_input.starts_with("host/"); - let account_id = if is_host { - account_input.strip_prefix("host/").unwrap().to_string() - } else { - account_input - }; - (snake_case!(account_id), is_host) -} - -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 move_across_partitions( - 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_account_generate_pub_key(user_dir: UserDirectory, args: GeneratePublicKeyArgs) { - let private_key_path = user_dir.account_private_key_path(&args.account_name); - let target_path = args - .output_dir - .unwrap_or(current_dir().unwrap()) - .join(format!("{}.pem", args.account_name)); - - match Command::new("openssl") - .args([ - "pkey", - "-in", - &private_key_path.to_string_lossy(), - "-pubout", - "-out", - &target_path.to_string_lossy(), - ]) - .status() - .await - { - Ok(status) if status.success() => { - println!( - "{}", - t!( - "jv.success.account.generate_pub_key", - export = target_path.display() - ) - ); - } - Ok(_) => { - eprintln!("{}", t!("jv.fail.account.keygen")); - } - Err(_) => { - eprintln!("{}", t!("jv.fail.account.keygen_exec")); - } - } -} - -async fn jv_update(update_file_args: UpdateArgs) { - let local_config = match precheck().await { - Some(config) => config, - None => return, - }; - - let (pool, ctx, _output) = match build_pool_and_ctx(&local_config).await { - Some(result) => result, - None => return, - }; - - match proc_update_to_latest_info_action(&pool, ctx, ()).await { - Err(e) => handle_err(e), - Ok(result) => { - if !update_file_args.silent { - match result { - UpdateToLatestInfoResult::Success => { - println!("{}", md(t!("jv.result.update.success"))); - } - UpdateToLatestInfoResult::AuthorizeFailed(e) => { - eprintln!("{}", md(t!("jv.result.common.authroize_failed", err = e))) - } - UpdateToLatestInfoResult::SyncCachedSheetFail( - sync_cached_sheet_fail_reason, - ) => match sync_cached_sheet_fail_reason { - SyncCachedSheetFailReason::PathAlreadyExist(path_buf) => { - eprintln!( - "{}", - md(t!( - "jv.result.update.fail.sync_cached_sheet_fail.path_already_exist", - path = path_buf.display() - )) - ); - } - }, - } - } - } - } -} - -async fn jv_direct(args: DirectArgs) { - let Some(upstream) = args.upstream else { - println!("{}", md(t!("jv.direct"))); - return; - }; - - if !args.confirm { - println!( - "{}", - t!("jv.confirm.direct", upstream = upstream).trim().yellow() - ); - 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(&upstream, PORT).await { - Ok(result) => result, - Err(e) => { - eprintln!( - "{}", - md(t!( - "jv.fail.parse.str_to_sockaddr", - str = &upstream.trim(), - err = e - )) - ); - return; - } - }; - - 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), - Ok(result) => match result { - SetUpstreamVaultActionResult::DirectedAndStained => { - println!( - "{}", - md(t!( - "jv.result.direct.directed_and_stained", - upstream = upstream - )) - ); - insert_recent_ip_address(upstream.to_string().trim()).await; - } - SetUpstreamVaultActionResult::Redirected => { - println!( - "{}", - md(t!("jv.result.direct.redirected", upstream = upstream)) - ); - insert_recent_ip_address(upstream.to_string().trim()).await; - } - SetUpstreamVaultActionResult::AlreadyStained => { - eprintln!("{}", md(t!("jv.result.direct.already_stained"))) - } - SetUpstreamVaultActionResult::AuthorizeFailed(e) => { - eprintln!("{}", md(t!("jv.result.common.authroize_failed", err = e))) - } - SetUpstreamVaultActionResult::RedirectFailed(e) => { - eprintln!("{}", md(t!("jv.result.direct.redirect_failed", err = e))) - } - SetUpstreamVaultActionResult::SameUpstream => { - eprintln!("{}", md(t!("jv.result.direct.same_upstream"))) - } - _ => {} - }, - }; -} - -async fn jv_unstain(args: UnstainArgs) { - 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.cfg_not_found.local_config"))); - return; - }; - - if !local_cfg.stained() { - eprintln!("{}", md(t!("jv.fail.unstain"))); - return; - } - - if !args.confirm { - println!( - "{}", - md(t!("jv.confirm.unstain", upstream = local_cfg.vault_addr())).yellow() - ); - confirm_hint_or(t!("common.confirm"), || exit(1)).await; - } - - local_cfg.unstain(); - match LocalConfig::write(&local_cfg).await { - Ok(_) => {} - Err(e) => { - eprintln!("{}", md(t!("jv.fail.write_cfg", error = e.to_string()))); - return; - } - }; - - println!("{}", md(t!("jv.success.unstain"))); -} - -async fn jv_docs(args: DocsArgs) { - let Some(docs_name) = args.docs_name else { - if !args.raw { - println!("{}", md(t!("jv.docs"))); - } - return; - }; - - if docs_name.trim() == "ls" || docs_name.trim() == "list" { - let docs = documents(); - - if args.raw { - docs.iter().for_each(|d| { - if d.starts_with("docs_") { - println!("{}", d.trim_start_matches("docs_")) - } - }); - return; - } - - println!("{}", md(t!("jv.success.docs.list.header"))); - let mut i = 0; - for doc in docs { - if doc.starts_with("docs_") { - println!( - "{}", - md(t!( - "jv.success.docs.list.item", - num = i + 1, - docs_name = doc.trim_start_matches("docs_") - )) - ); - i += 1; - } - } - println!("{}", md(t!("jv.success.docs.list.footer"))); - - return; - } - - let name = format!("docs_{}", snake_case!(docs_name.clone())); - let Some(document) = document(name) else { - eprintln!( - "{}", - md(t!("jv.fail.docs.not_found", docs_name = docs_name)) - ); - return; - }; - - if args.direct { - println!("{}", document.trim()); - } else { - let Some(doc_dir) = current_cfg_dir() else { - eprintln!( - "{}", - md(t!("jv.fail.docs.no_doc_dir", docs_name = docs_name)) - ); - return; - }; - let file = doc_dir.join("DOCS.MD"); - if let Err(e) = show_in_pager(document, file).await { - eprintln!( - "{}", - md(t!( - "jv.fail.docs.open_editor", - err = e, - docs_name = docs_name - )) - ); - } - } -} - -async fn jv_debug_glob(glob_args: DebugGlobArgs) { - let local_dir = match current_local_path() { - Some(dir) => dir, - None => { - // No, dont print anything - // eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); - return; - } - }; - - for path in glob(glob_args.pattern, &local_dir).await.keys() { - println!("{}", path); - } -} - -async fn glob(pattern: impl Into, local_dir: &PathBuf) -> BTreeMap { - let pattern = pattern.into(); - let globber = match get_globber(&pattern, true).await { - Ok(g) => g, - Err(_) => match get_globber(&pattern, false).await { - Ok(g) => g, - Err(_) => return BTreeMap::new(), - }, - }; - - let result = globber.names(); - let base_dir = globber.base(); - - let relative_path = base_dir - .strip_prefix(local_dir) - .unwrap_or(local_dir.as_path()); - - let mut filtered_paths: Vec = result - .into_iter() - .filter_map(|name| Some(relative_path.join(name).display().to_string())) - .collect(); - - let path_map: BTreeMap = filtered_paths.drain(..).map(|path| (path, ())).collect(); - path_map -} - -async fn get_globber( - pattern: impl Into, - with_current_sheet: bool, -) -> Result { - // Build globber - let globber = Globber::from(pattern.into()); - - let globber = if with_current_sheet { - // Get necessary informations - let Some(local_dir) = current_local_path() else { - return Err(Error::new( - std::io::ErrorKind::NotFound, - "Workspace not found", - )); - }; - - let Ok(local_cfg) = LocalConfig::read_from(local_dir.join(CLIENT_FILE_WORKSPACE)).await - else { - return Err(Error::new( - std::io::ErrorKind::NotFound, - "Local Config read failed", - )); - }; - let Some(sheet_name) = local_cfg.sheet_in_use().clone() else { - return Err(Error::new(std::io::ErrorKind::NotFound, "No sheet in use")); - }; - - let Ok(cached_sheet) = CachedSheet::cached_sheet_data(&sheet_name).await else { - return Err(Error::new( - std::io::ErrorKind::NotFound, - "Cached sheet not found", - )); - }; - - let current_dir = current_dir()?; - - if !current_dir.starts_with(&local_dir) { - return Err(Error::new( - std::io::ErrorKind::NotFound, - "Not a local workspace", - )); - } - - // Sheet mode - globber.glob(|current_dir| { - let mut result = HashSet::new(); - - // First, add local files - get_local_files(¤t_dir, &mut result); - - // Start collecting sheet files - // Check if we're in the workspace directory (get current path relative to local workspace) - let Ok(relative_path_to_local) = current_dir.strip_prefix(&local_dir) else { - return result.into_iter().collect(); - }; - - let mut dirs = HashSet::new(); - - cached_sheet.mapping().iter().for_each(|(path, _)| { - let left = relative_path_to_local; - - // Skip: files that don't start with the current directory - let Ok(right) = path.strip_prefix(left) else { - return; - }; - - // File: starts with current directory and doesn't contain "/" - // (since we already filtered out files that don't start with current directory, - // here we just check if it contains a path separator) - let file_name = right.display().to_string(); - if !file_name.contains("/") { - result.insert(GlobItem::File(file_name)); - } else { - // Directory: contains separator, take the first part, add to dirs set - if let Some(first_part) = file_name.split('/').next() { - dirs.insert(first_part.to_string()); - } - } - }); - - dirs.into_iter().for_each(|dir| { - result.insert(GlobItem::Directory(dir)); - }); - - result.into_iter().collect() - }) - } else { - // Local mode - globber.glob(|current| { - let mut items = HashSet::new(); - get_local_files(¤t, &mut items); - items.iter().cloned().collect() - }) - }?; - - Ok(globber) -} - -fn get_local_files(current: &PathBuf, items: &mut HashSet) { - if let Ok(entries) = std::fs::read_dir(current) { - for entry in entries.flatten() { - let path = entry.path(); - let name = path - .file_name() - .and_then(|n| n.to_str()) - .map(|s| s.to_string()) - .unwrap_or_default(); - - if path.is_file() { - items.insert(GlobItem::File(name)); - } else if path.is_dir() && name != CLIENT_FOLDER_WORKSPACE_ROOT_NAME { - items.insert(GlobItem::Directory(name)); - } - } - } -} - -pub fn handle_err(err: TcpTargetError) { - eprintln!("{}", md(t!("jv.fail.from_core", err = err))) -} - -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)) -} - -// Check if the workspace is stained and has a valid configuration -// Returns LocalConfig if valid, None otherwise -async fn precheck() -> Option { - let Some(local_dir) = current_local_path() else { - eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); - return None; - }; - - if let Err(e) = set_current_dir(&local_dir) { - eprintln!( - "{}", - t!( - "jv.fail.std.set_current_dir", - dir = local_dir.display(), - error = e - ) - ); - return None; - } - - let Ok(local_config) = LocalConfig::read().await else { - eprintln!("{}", md(t!("jv.fail.cfg_not_found.local_config"))); - return None; - }; - - if !local_config.stained() { - eprintln!("{}", md(t!("jv.fail.not_stained"))); - return None; - } - - Some(local_config) -} - -/// Build action pool and context for upstream communication -/// Returns Some((ActionPool, ActionContext)) if successful, None otherwise -async fn build_pool_and_ctx( - local_config: &LocalConfig, -) -> Option<(ActionPool, ActionContext, Receiver)> { - let pool = client_registry::client_action_pool(); - let upstream = local_config.upstream_addr(); - - let instance = connect(upstream).await?; - - // Build context and insert instance - let mut ctx = ActionContext::local().insert_instance(instance); - - // Build channel for communication - let (tx, rx) = mpsc::channel::(100); - ctx.insert_arc_data(Arc::new(tx)); - - Some((pool, ctx, rx)) -} - -/// Sort paths in a vector of strings. -/// Paths are strings with structure A/B/C/D/E. -/// Paths with deeper levels (more '/' segments) are sorted first, followed by paths with shallower levels. -/// Within the same level, paths are sorted based on the first letter or digit encountered, with the order A-Z > a-z > 0-9. -fn sort_paths(paths: &mut Vec) { - quick_sort_with_cmp(paths, false, |a, b| { - let depth_a = a.matches('/').count(); - let depth_b = b.matches('/').count(); - - if depth_a != depth_b { - return if depth_a > depth_b { -1 } else { 1 }; - } - a.cmp(b) as i32 - }); -} - -/// Get paths that exist in the Cached Sheet under the current directory -fn mapping_names_here( - current_dir: &PathBuf, - local_dir: &PathBuf, - cached_sheet: &SheetData, -) -> std::collections::BTreeMap> { - let Ok(relative_path) = current_dir.strip_prefix(local_dir) else { - return std::collections::BTreeMap::new(); - }; - - // Collect files directly under current directory - let files_here: std::collections::BTreeMap> = cached_sheet - .mapping() - .iter() - .filter_map(|(f, mapping)| { - // Check if the file is directly under the current directory - f.parent() - .filter(|&parent| parent == relative_path) - .and_then(|_| f.file_name()) - .and_then(|name| name.to_str()) - .map(|s| (s.to_string(), Some(mapping.clone()))) - }) - .collect(); - - // Collect directories that appear in the mapping - let mut dirs_set = std::collections::BTreeSet::new(); - - for (f, _mapping) in cached_sheet.mapping().iter() { - // Get all parent directories of the file relative to the current directory - let mut current = f.as_path(); - - while let Some(parent) = current.parent() { - if parent == relative_path { - // This is a parent directory, not the file itself - if current != f.as_path() - && let Some(dir_name) = current.file_name() - && let Some(dir_str) = dir_name.to_str() - { - dirs_set.insert(format!("{}/", dir_str)); - } - break; - } - current = parent; - } - } - - // Filter out directories that are actually files - let filtered_dirs: std::collections::BTreeMap> = dirs_set - .into_iter() - .filter(|dir_with_slash| { - let dir_name = dir_with_slash.trim_end_matches('/'); - !files_here.contains_key(dir_name) - }) - .map(|dir_name| (dir_name, None)) - .collect(); - - // Combine results - let mut result = files_here; - result.extend(filtered_dirs); - result -} - -/// Trims the content, takes the first line, and truncates it to a display width of 24 characters. -/// If the display width exceeds 24, it truncates and adds "...". -fn truncate_first_line(content: String) -> String { - let trimmed = content.trim(); - let first_line = trimmed.lines().next().unwrap_or(""); - let display_len = display_width(first_line); - if display_len > 24 { - let mut truncated = String::new(); - let mut current_len = 0; - for ch in first_line.chars() { - let ch_width = display_width(&ch.to_string()); - if current_len + ch_width > 24 { - break; - } - truncated.push(ch); - current_len += ch_width; - } - truncated.push_str("..."); - truncated - } else { - first_line.to_string() - } -} - -fn print_json(obj: T, pretty: bool) { - let result = if pretty { - serde_json::to_string_pretty(&obj) - } else { - serde_json::to_string(&obj) - }; - println!("{}", result.unwrap_or("{}".to_string())); -} diff --git a/src/bin/jvv.rs b/src/bin/jvv.rs deleted file mode 100644 index 057e04c..0000000 --- a/src/bin/jvv.rs +++ /dev/null @@ -1,749 +0,0 @@ -#![allow(clippy::all)] - -// -// -// ████████ ████████ -// ██▒▒▒▒▒▒▒▒██ ██▒▒▒▒▒▒▒▒██ -// ██ ▒▒██ ██▒▒ ██ █████ ██ ██ ██████ █████ -// ██ ▒▒████████▒▒ ██ ▒▒▒██ ██ ██ ██████ ██████ -// ██ ▒▒▒▒▒▒▒▒ ██ ██ ██ ██ ███▒▒▒█ █▒▒▒▒█ -// ██ ██ ██ ██ ██ ███ ▒ ████ ▒ -// ██ ██ ██ ██ ██ ███ ▒████ -// ██ ████ ████ ██ ██ ▒██ ██▒ ███ ▒▒▒██ -// ██ ████ ████ ██ █ ██ ██ ██ ███ █ ██ ██ -// ██ ████ ████ ██ █ ██ ▒████▒ ▒██████ ██████ -// ██ ▒▒▒▒ ▒▒▒▒ ██ ▒████ ▒██▒ ██████ ▒████▒ -// ██ ██ ▒▒▒▒ ▒▒ ▒▒▒▒▒▒ ▒▒▒▒ -// ██ ██████████ ██ -// ██ ██ ██ -// ████████████████████████████████ -// ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ -// -// The current module `jvv.rs` is the legacy implementation, with only necessary fixes to maintain usability. -// The new implementation is located in `jvn.rs`, please refer to it. -// - -use clap::{Parser, Subcommand}; -use cli_utils::legacy::{ - display::{md, size_str}, - env::current_locales, - logger::build_env_logger, -}; -use just_enough_vcs::{ - data::compile_info::CoreCompileInfo, - lib::{ - connection::action_service::server_entry, - constants::SERVER_FILE_VAULT, - data::{ - member::Member, - vault::{Vault, vault_config::VaultConfig}, - }, - env::current_vault_path, - }, - utils::cfg_file::config::ConfigFile, -}; -use just_fmt::pascal_case; -use jvcli::data::compile_info::CompileInfo; -use log::{error, info}; -use rust_i18n::{set_locale, t}; -use tokio::fs::{self}; - -// Import i18n files -rust_i18n::i18n!("resources/locales", fallback = "en"); - -#[derive(Parser, Debug)] -#[command( - disable_help_flag = true, - disable_version_flag = true, - disable_help_subcommand = true, - help_template = "{all-args}" -)] -struct JustEnoughVcsVault { - #[command(subcommand)] - command: JustEnoughVcsVaultCommand, -} - -#[derive(Subcommand, Debug)] -enum JustEnoughVcsVaultCommand { - /// Version information - #[command(alias = "--version", alias = "-v")] - Version(VersionArgs), - - /// 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, alias = "-m")] - Member(MemberManage), - - /// Manage service - #[command(subcommand)] - Service(ServiceManage), - - // Short commands - #[command(alias = "-l", alias = "listen")] - ServiceListen(ListenArgs), - - // List all members - #[command(alias = "-M", alias = "members")] - MemberList, -} - -#[derive(Parser, Debug)] -struct VersionArgs { - #[arg(short = 'C', long = "compile-info")] - compile_info: bool, -} - -#[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 - #[command(alias = "--help", alias = "-h")] - Help, -} - -#[derive(Subcommand, Debug)] -enum ServiceManage { - /// Listen connection at current vault - Listen(ListenArgs), - - /// Show help information - #[command(alias = "--help", alias = "-h")] - Help, -} - -#[derive(Parser, Debug)] -struct HereArgs { - /// Show help information - #[arg(short, long)] - help: bool, -} - -#[derive(Parser, Debug)] -struct CreateVaultArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Vault name to create - vault_name: String, -} - -#[derive(Parser, Debug)] -struct InitVaultArgs { - /// Show help information - #[arg(short, long)] - help: bool, -} - -#[derive(Parser, Debug)] -struct MemberRegisterArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Member name - member_name: String, -} - -#[derive(Parser, Debug)] -struct MemberRemoveArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Member name - member_name: String, -} - -#[derive(Parser, Debug)] -struct MemberListArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Show raw output for list - #[arg(short, long)] - raw: bool, -} - -#[derive(Parser, Debug)] -struct ListenArgs { - /// Show help information - #[arg(short, long)] - help: bool, - - /// Disable logging (Override profile) - #[arg(short, long)] - no_log: bool, - - /// Show logging (Override profile) - #[arg(short, long)] - show_log: bool, - - /// Custom port - #[arg(short, long)] - port: Option, -} - -#[tokio::main] -async fn main() { - // Init i18n - set_locale(¤t_locales()); - - // Init colored - #[cfg(windows)] - let _ = colored::control::set_virtual_terminal(true); - - let Ok(parser) = JustEnoughVcsVault::try_parse() else { - println!("{}", md(t!("jvv.help"))); - return; - }; - - match parser.command { - JustEnoughVcsVaultCommand::Version(version_args) => { - let compile_info = CompileInfo::default(); - let core_compile_info = CoreCompileInfo::default(); - println!( - "{}", - md(t!( - "jvv.version.header", - version = compile_info.cli_version, - vcs_version = core_compile_info.vcs_version - )) - ); - - if version_args.compile_info { - println!( - "\n{}", - md(t!( - "jv.version.compile_info", - build_time = compile_info.date, - build_target = compile_info.target, - build_platform = compile_info.platform, - build_toolchain = compile_info.toolchain, - cli_build_branch = compile_info.build_branch, - cli_build_commit = - &compile_info.build_commit[..7.min(compile_info.build_commit.len())], - core_build_branch = core_compile_info.build_branch, - core_build_commit = &core_compile_info.build_commit - [..7.min(core_compile_info.build_commit.len())] - )) - ); - } - } - - JustEnoughVcsVaultCommand::Here(here_args) => { - if here_args.help { - println!("{}", md(t!("jvv.here"))); - return; - } - jvv_here(here_args).await; - } - JustEnoughVcsVaultCommand::Create(create_vault_args) => { - if create_vault_args.help { - println!("{}", md(t!("jvv.create"))); - return; - } - jvv_create(create_vault_args).await; - } - JustEnoughVcsVaultCommand::Init(init_vault_args) => { - if init_vault_args.help { - println!("{}", md(t!("jvv.init"))); - return; - } - jvv_init(init_vault_args).await; - } - JustEnoughVcsVaultCommand::Member(member_manage) => { - 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, - None => { - eprintln!( - "{}", - t!("jvv.fail.jvcs", err = "Failed to initialize vault") - ); - return; - } - }; - - match member_manage { - MemberManage::Register(member_register_args) => { - if member_register_args.help { - println!("{}", md(t!("jvv.member"))); - return; - } - jvv_member_register(vault, member_register_args).await; - } - MemberManage::Remove(member_remove_args) => { - if member_remove_args.help { - println!("{}", md(t!("jvv.member"))); - return; - } - jvv_member_remove(vault, member_remove_args).await; - } - MemberManage::List(member_list_args) => { - if member_list_args.help { - println!("{}", md(t!("jvv.member"))); - return; - } - jvv_member_list(vault, member_list_args).await; - } - MemberManage::Help => { - println!("{}", md(t!("jvv.member"))); - return; - } - } - } - JustEnoughVcsVaultCommand::Service(service_manage) => match service_manage { - ServiceManage::Listen(listen_args) => { - if listen_args.help { - println!("{}", md(t!("jvv.service"))); - return; - } - jvv_service_listen(listen_args).await; - } - ServiceManage::Help => { - println!("{}", md(t!("jvv.service"))); - return; - } - }, - // Short commands - JustEnoughVcsVaultCommand::ServiceListen(listen_args) => { - if listen_args.help { - println!("{}", md(t!("jvv.service"))); - return; - } - jvv_service_listen(listen_args).await; - } - JustEnoughVcsVaultCommand::MemberList => { - 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, - None => { - eprintln!( - "{}", - t!("jvv.fail.jvcs", err = "Failed to initialize vault") - ); - return; - } - }; - jvv_member_list( - vault, - MemberListArgs { - help: false, - raw: false, - }, - ) - .await; - } - } -} - -async fn jvv_here(_args: HereArgs) { - let Some(current_vault) = current_vault_path() else { - eprintln!("{}", t!("jvv.fail.here.cfg_not_found").trim()); - return; - }; - - // Read vault cfg - let vault_cfg_file = current_vault.join(SERVER_FILE_VAULT); - if !vault_cfg_file.exists() { - eprintln!("{}", t!("jvv.fail.here.cfg_not_found").trim()); - return; - } - - let vault_cfg = match VaultConfig::read().await { - Ok(cfg) => cfg, - Err(_) => { - eprintln!("{}", t!("jvv.fail.here.cfg_not_found").trim()); - return; - } - }; - - // Get vault name - let vault_name = vault_cfg.vault_name().clone(); - - // Get vault - let Some(vault) = Vault::init(vault_cfg, current_vault) else { - eprintln!("{}", t!("jvv.fail.here.vault_init_failed").trim()); - return; - }; - - // Get sheet count - let num_sheets = vault.sheet_names().unwrap().iter().len(); - - // Get virtual file count and total size recursively - let virtual_file_root = vault.virtual_file_storage_dir(); - let mut num_vf = 0; - let mut total_size = 0; - - // Recursive function to calculate total size and count files with specific name - async fn calculate_total_size_and_vf_count( - path: std::path::PathBuf, - file_count: &mut u64, - total_size: &mut u64, - ) -> std::io::Result<()> { - let mut entries = fs::read_dir(&path).await?; - while let Some(entry) = entries.next_entry().await? { - let metadata = entry.metadata().await?; - if metadata.is_file() { - *total_size += metadata.len(); - // Check if file name matches SERVER_NAME_VF_META - if entry.file_name() == just_enough_vcs::lib::constants::SERVER_NAME_VF_META { - *file_count += 1; - } - } else if metadata.is_dir() { - Box::pin(calculate_total_size_and_vf_count( - entry.path(), - file_count, - total_size, - )) - .await?; - } - } - Ok(()) - } - - // Calculate with timeout - let timeout_duration = std::time::Duration::from_millis(1200); - let size_result = tokio::time::timeout(timeout_duration, async { - calculate_total_size_and_vf_count(virtual_file_root.clone(), &mut num_vf, &mut total_size) - .await - }) - .await; - - match size_result { - Ok(Ok(_)) => { - // Calculation completed within timeout - } - Ok(Err(e)) => { - eprintln!( - "{}", - t!("jvv.fail.here.size_calc_error", error = e.to_string()).trim() - ); - return; - } - Err(_) => { - // Timeout occurred - println!("{}", t!("jvv.info.here.analyzing_size").trim()); - // Continue with partial calculation (num_vf and total_size as calculated so far) - } - } - - // Get member count - let num_mem = match vault.member_ids() { - Ok(ids) => ids.len(), - Err(_) => { - eprintln!("{}", t!("jvv.fail.here.member_ids_failed").trim()); - return; - } - }; - - // Get public key count - let mut num_pk = 0; - let member_ids = match vault.member_ids() { - Ok(ids) => ids, - Err(_) => { - eprintln!("{}", t!("jvv.fail.here.member_ids_failed").trim()); - return; - } - }; - - for member_id in member_ids { - if vault.member_key_path(&member_id).exists() { - num_pk += 1; - } - } - - // Get reference sheet info - let ref_sheet = match vault.sheet(&"ref".to_string()).await { - Ok(sheet) => sheet, - Err(_) => { - eprintln!("{}", t!("jvv.fail.here.ref_sheet_not_found").trim()); - return; - } - }; - let num_ref_sheet_managed_files = ref_sheet.mapping().len(); - - // Success - println!( - "{}", - md(t!( - "jvv.success.here.info", - name = vault_name, - num_sheets = num_sheets, - num_vf = num_vf, - num_mem = num_mem, - num_pk = num_pk, - num_ref_sheet_managed_files = num_ref_sheet_managed_files, - total_size = size_str(total_size as usize) - )) - ) -} - -async fn jvv_init(_args: InitVaultArgs) { - 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() - && entries.next().is_some() - { - eprintln!("{}", t!("jvv.fail.init.not_empty")); - return; - } - - // Setup vault - 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(), vault_name).await { - eprintln!("{}", t!("jvv.fail.jvcs", err = err.to_string())); - return; - } - - // Success - println!( - "{}", - t!("jvv.success.init", name = current_dir.to_string_lossy()) - ) -} - -async fn jvv_create(args: CreateVaultArgs) { - 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 - if fs::create_dir_all(&target_dir).await.is_err() { - eprintln!( - "{}", - t!( - "jvv.fail.tokio.fs.create_dir", - dir = target_dir.to_string_lossy() - ) - ); - return; - } - - 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); - if let Err(err) = Vault::setup_vault(target_dir.clone(), vault_name).await { - eprintln!("{}", t!("jvv.fail.jvcs", err = err.to_string())); - return; - } - - // Success - println!( - "{}", - t!("jvv.success.create", name = target_dir.to_string_lossy()) - ) -} - -async fn jvv_member_register(vault: Vault, args: MemberRegisterArgs) { - let register = vault - .register_member_to_vault(Member::new(args.member_name.clone())) - .await; - - if register.is_err() { - eprintln!("{}", t!("jvv.fail.member.register").trim()); - return; - } - - println!( - "{}", - t!("jvv.success.member.register", member = args.member_name) - ) -} - -async fn jvv_member_remove(vault: Vault, args: MemberRemoveArgs) { - let _ = vault.remove_member_from_vault(&args.member_name); - - println!( - "{}", - t!("jvv.success.member.remove", member = args.member_name) - ) -} - -async fn jvv_member_list(vault: Vault, args: MemberListArgs) { - // Get id list - let ids = match vault.member_ids() { - Ok(ids) => ids, - Err(_) => { - if !args.raw { - eprintln!("{}", t!("jvv.fail.member.list").trim()); - } - return; - } - }; - - let mut members: Vec = ids.into_iter().collect(); - - // Sort members to put "host" first if it exists - members.sort_by(|a, b| { - if a == "host" { - std::cmp::Ordering::Less - } else if b == "host" { - std::cmp::Ordering::Greater - } else { - a.cmp(b) - } - }); - - if args.raw { - for member in members { - println!("{}", member); - } - } else { - // Print header - println!( - "{}", - md(t!("jvv.success.member.list.header", num = members.len())) - ); - - // Print list - let mut i = 0; - let mut has_pubkey = 0; - for member in members { - println!("{}. {} {}", i + 1, &member, { - // Key registered - if vault.member_key_path(&member).exists() { - has_pubkey += 1; - t!("jvv.success.member.list.status_key_registered") - } else { - std::borrow::Cow::Borrowed("") - } - }); - i += 1; - } - - println!( - "{}", - md(t!("jvv.success.member.list.footer", num = has_pubkey)) - ); - } -} - -async fn jvv_service_listen(args: ListenArgs) { - let Some(current_vault) = current_vault_path() else { - eprintln!("{}", t!("jvv.fail.here.cfg_not_found").trim()); - return; - }; - - let Ok(vault_cfg) = VaultConfig::read().await else { - eprintln!("{}", t!("jvv.fail.here.cfg_not_found").trim()); - return; - }; - - let show_logger = if args.no_log && !args.show_log { - false - } else if !args.no_log && args.show_log { - true - } else if !args.no_log && !args.show_log { - // Read profile - vault_cfg.server_config().is_logger_enabled() - } else { - eprintln!("{}", md(t!("jvv.fail.service.wtf_show_log_and_no_log"))); - return; - }; - - if show_logger { - let logs_dir = current_vault.join("logs"); - if let Err(_) = fs::create_dir_all(&logs_dir).await { - eprintln!( - "{}", - t!( - "jvv.fail.tokio.fs.create_dir", - dir = logs_dir.to_string_lossy() - ) - ); - return; - } - let now = chrono::Local::now(); - let log_filename = format!("log_{}.txt", now.format("%Y-%m-%d-%H-%M-%S")); - build_env_logger( - logs_dir.join(log_filename), - vault_cfg.server_config().logger_level(), - ); - info!( - "{}", - t!( - "jvv.success.service.listen_start", - path = match current_vault.file_name() { - Some(name) => name.to_string_lossy(), - None => std::borrow::Cow::Borrowed("unknown"), - } - ) - ) - } - - let port = args.port.unwrap_or_default(); - match server_entry(current_vault, port).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