From 0d474e7913a50417ed0dd4e8ac4548e674211f5f Mon Sep 17 00:00:00 2001 From: Yoshihiro OKUMURA Date: Fri, 17 Apr 2026 18:45:52 +0900 Subject: [PATCH] fix: align all command outputs with Python reference implementation - fix(connection): fix create_folder API body (name, parent_id, description, template_id) - feat(shared): add Unicode NFC normalization helper and find_subfolder_by_name() - feat(Cargo): add unicode-normalization dependency - fix(shared,mkdir,rm,cp,mv,upload): apply NFC normalization to path and name comparisons - fix(labs): rewrite output as aligned table (Name/PI/Laboratory), remove cache fallback - fix(mkdir): silent on success; align error message with Python - fix(rm): silent on success; use find_subfolder_by_name for NFC-aware lookup - fix(cp): silent on success; align all error messages; add no-op when src==dest - fix(mv): silent on success; align all error messages; add no-op when src==dest - fix(login): change output to 'Login Successful' - fix(logout): remove all output (silent like Python) - fix(chacl): remove success message (silent like Python) - fix(metadata): use compact JSON output (to_string instead of to_string_pretty) - fix(file_metadata): use compact JSON output - fix(ls): use compact JSON output; add blank line after entries in recursive plain mode - fix(config): silent on create/update/delete; add colon in list short format; remove empty-state messages; align error messages ('is already exists.' / 'is not exists.') Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 1 + Cargo.toml | 1 + src/commands/chacl.rs | 1 - src/commands/config.rs | 18 ++----- src/commands/cp.rs | 96 +++++++++++++++++++---------------- src/commands/file_metadata.rs | 2 +- src/commands/labs.rs | 77 +++++++++++----------------- src/commands/login.rs | 2 +- src/commands/logout.rs | 7 +-- src/commands/ls.rs | 4 +- src/commands/metadata.rs | 2 +- src/commands/mkdir.rs | 23 +++------ src/commands/mv.rs | 92 ++++++++++++++++++--------------- src/commands/rm.rs | 16 ++---- src/commands/shared.rs | 23 +++++++-- src/commands/upload.rs | 8 +-- src/connection.rs | 7 ++- src/main.rs | 18 ++----- 18 files changed, 189 insertions(+), 209 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a66fa9d..19a1963 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1039,6 +1039,7 @@ dependencies = [ "serde_json", "sha2", "tokio", + "unicode-normalization", "validators", ] diff --git a/Cargo.toml b/Cargo.toml index 19cf335..30ed9d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,3 +28,4 @@ fs2 = "0.4" ctrlc = "3" os_info = "3" dotenvy = "0.15" +unicode-normalization = "0.1" diff --git a/src/commands/chacl.rs b/src/commands/chacl.rs index fb2f8db..41b93a7 100644 --- a/src/commands/chacl.rs +++ b/src/commands/chacl.rs @@ -45,6 +45,5 @@ pub async fn chacl( if !resp.status().is_success() { return Err(format!("ACL change failed: {}", resp.status()).into()); } - println!("ACL changed successfully for: {}", remote_path); Ok(()) } diff --git a/src/commands/config.rs b/src/commands/config.rs index 6701e84..bc723a3 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -64,7 +64,7 @@ pub fn config_create(remote: &str, url: &str) -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box> { let path = config_path(); if !path.exists() { - println!("No config file found at {}", path.to_string_lossy()); return Ok(()); } sanitize_config_file(&path)?; @@ -113,23 +110,17 @@ pub fn config_list(long: bool) -> Result<(), Box> { let mut conf = Ini::new(); let _ = conf.load(&path_str)?; let map = conf.get_map().unwrap_or_default(); - let mut printed = false; for (sec, props) in map.iter() { if sec == "default" { continue; } if !long { - println!("{}", sec); - printed = true; + println!("{}:", sec); } else { let url = props.get("url").and_then(|v| v.clone()).unwrap_or_default(); println!("{}:\t{}", sec, url); - printed = true; } } - if !printed { - println!("No remotes configured"); - } Ok(()) } @@ -144,7 +135,7 @@ pub fn config_delete(remote: &str) -> Result<(), Box> { // fallback: reconstruct by removing the section in memory map and writing file let mut map = conf.get_map().unwrap_or_default(); if map.remove(remote).is_none() { - return Err(format!("Remote host `{}` does not exist.", remote).into()); + return Err(format!("Remote host `{}` is not exists.", remote).into()); } // build new Ini from map let mut new_ini = Ini::new(); @@ -154,7 +145,6 @@ pub fn config_delete(remote: &str) -> Result<(), Box> { } } write_ini_atomic(&path, &new_ini)?; - println!("Deleted remote host `{}`.", remote); Ok(()) } diff --git a/src/commands/cp.rs b/src/commands/cp.rs index 3244df9..3872993 100644 --- a/src/commands/cp.rs +++ b/src/commands/cp.rs @@ -1,6 +1,6 @@ use crate::commands::shared::{ - create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache, load_cache_with_token_refresh, - parse_remote_path, + create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache, + find_subfolder_by_name, load_cache_with_token_refresh, nfc, parse_remote_path, }; pub async fn cp( @@ -13,10 +13,10 @@ pub async fn cp( let (d_remote, d_lab, d_path) = parse_remote_path(dest_path)?; if s_remote != d_remote { - return Err("Source and destination must use the same remote.".into()); + return Err("Remote host mismatched.".into()); } if s_lab != d_lab { - return Err("Source and destination must be in the same laboratory.".into()); + return Err("Laboratory mismatched.".into()); } let cache = load_cache_with_token_refresh(&s_remote).await?; @@ -28,11 +28,12 @@ pub async fn cp( let (s_dirname, s_basename) = split_path(&s_path); // If dest ends with '/', treat it as a directory and preserve src basename - let (d_dirname, d_basename) = if dest_ends_with_slash { + let (d_dirname, d_basename_raw) = if dest_ends_with_slash { (d_path.clone(), s_basename.clone()) } else { split_path(&d_path) }; + let d_basename = nfc(&d_basename_raw); let s_parent_folder = find_folder(&conn, lab_id, &s_dirname, None).await?; let s_parent_files = conn.list_all_files(&s_parent_folder.id).await?; @@ -46,12 +47,16 @@ pub async fn cp( if find_file_by_name(&d_parent_files, &d_basename).is_some() { return Err(format!("File `{}` already exists.", d_basename).into()); } - if d_parent_folder - .sub_folders - .iter() - .any(|f| f.name.to_lowercase() == d_basename.to_lowercase()) - { - return Err("Cannot overwrite non-folder with folder.".into()); + if find_subfolder_by_name(&d_parent_folder.sub_folders, &d_basename).is_some() { + return Err(format!( + "Cannot overwrite non-folder `{}` with folder `{}`.", + d_basename, d_path + ) + .into()); + } + // No-op if source and destination are identical + if s_parent_folder.id == d_parent_folder.id && d_basename == s_basename { + return Ok(()); } let body = serde_json::json!({"folder": d_parent_folder.id, "name": d_basename}); let resp = conn @@ -64,48 +69,51 @@ pub async fn cp( if !resp.status().is_success() { return Err(format!("Copy failed: {}", resp.status()).into()); } - println!("Copied: {} -> {}", src_path, dest_path); return Ok(()); } // Try source as a folder - if let Some(src_folder) = s_parent_folder - .sub_folders - .iter() - .find(|f| f.name.to_lowercase() == s_basename.to_lowercase()) - { - if !recursive { + let src_folder = match find_subfolder_by_name(&s_parent_folder.sub_folders, &s_basename) { + Some(f) => f, + None => return Err(format!("File or folder `{}` not found.", s_basename).into()), + }; + if !recursive { + return Err(format!("Cannot copy `{}`: Is a folder.", s_path).into()); + } + let src_folder_id = src_folder.id.clone(); + if find_file_by_name(&d_parent_files, &d_basename).is_some() { + return Err(format!( + "Cannot overwrite non-folder `{}` with folder `{}`.", + d_basename, s_path + ) + .into()); + } + if let Some(d_folder) = find_subfolder_by_name(&d_parent_folder.sub_folders, &d_basename) { + if d_folder.id == src_folder_id { return Err( - format!("{}: is a folder (use -r to copy folders)", src_path).into(), + format!("`{}` and `{}` are the same folder.", s_path, s_path).into(), ); } - let src_folder_id = src_folder.id.clone(); - if find_file_by_name(&d_parent_files, &d_basename).is_some() { - return Err(format!("File `{}` already exists.", d_basename).into()); - } - if d_parent_folder - .sub_folders - .iter() - .any(|f| f.name.to_lowercase() == d_basename.to_lowercase()) - { - return Err("Folder not empty.".into()); - } - let body = serde_json::json!({"parent": d_parent_folder.id, "name": d_basename}); - let resp = conn - .client - .post(conn.build_url(&format!("v3/folders/{}/copy/", src_folder_id))) - .headers(conn.prepare_headers()) - .json(&body) - .send() - .await?; - if !resp.status().is_success() { - return Err(format!("Copy failed: {}", resp.status()).into()); - } - println!("Copied: {} -> {}", src_path, dest_path); + return Err( + format!("Cannot move `{}` to `{}`: Folder not empty.", s_path, d_path).into(), + ); + } + // No-op if source and destination are identical + if s_parent_folder.id == d_parent_folder.id && s_basename == d_basename { return Ok(()); } - - Err(format!("Source `{}` not found.", src_path).into()) + let body = serde_json::json!({"parent": d_parent_folder.id, "name": d_basename}); + let resp = conn + .client + .post(conn.build_url(&format!("v3/folders/{}/copy/", src_folder_id))) + .headers(conn.prepare_headers()) + .json(&body) + .send() + .await?; + if !resp.status().is_success() { + return Err(format!("Copy failed: {}", resp.status()).into()); + } + Ok(()) } /// Split a path into (parent_dir, basename). diff --git a/src/commands/file_metadata.rs b/src/commands/file_metadata.rs index 7859b11..758d99f 100644 --- a/src/commands/file_metadata.rs +++ b/src/commands/file_metadata.rs @@ -28,6 +28,6 @@ pub async fn file_metadata(remote_path: &str, password: Option<&str>) -> Result< let resp = conn.get(&format!("v3/files/{}/metadata/", file.id)).await?; let json: serde_json::Value = resp.json().await?; - println!("{}", serde_json::to_string_pretty(&json)?); + println!("{}", serde_json::to_string(&json)?); Ok(()) } diff --git a/src/commands/labs.rs b/src/commands/labs.rs index 0a6de86..353d8c2 100644 --- a/src/commands/labs.rs +++ b/src/commands/labs.rs @@ -1,56 +1,37 @@ -use crate::connection::MDRSConnection; +use crate::commands::shared::{create_authenticated_conn, load_cache_with_token_refresh}; -use std::fs; -use std::sync::Arc; +pub async fn labs(remote: &str) -> Result<(), Box> { + let cache = load_cache_with_token_refresh(remote).await?; + let conn = create_authenticated_conn(remote, &cache)?; + let labs = conn.list_laboratories().await?; -pub async fn labs( - conn: Arc, - remote_label: &str, -) -> Result<(), Box> { - // Try API first - match conn.list_laboratories().await { - Ok(labs) => { - println!("Laboratories:"); - for lab in labs.items { - println!( - " {} (PI: {}, Full: {})", - lab.name, lab.pi_name, lab.full_name - ); - } - return Ok(()); - } - Err(_) => { - // fallback to cache - } + let header = ("Name", "PI", "Laboratory"); + let mut w_name = header.0.len(); + let mut w_pi = header.1.len(); + let mut w_full = header.2.len(); + + for lab in &labs.items { + w_name = w_name.max(lab.name.len()); + w_pi = w_pi.max(lab.pi_name.len()); + w_full = w_full.max(lab.full_name.len()); } - // fallback: read cache file using remote_label - let cache_path = crate::settings::SETTINGS - .config_dirname - .join("cache") - .join(format!("{}.json", remote_label)); - if !cache_path.exists() { - println!("No laboratories available (API failed and no cache)"); - return Ok(()); - } - let text = fs::read_to_string(&cache_path)?; - let v: serde_json::Value = serde_json::from_str(&text)?; - // Cache stores laboratories as `{"items": [...]}` (Python-compatible format) - let labs_arr = v - .get("laboratories") - .and_then(|l| l.get("items")) - .and_then(|a| a.as_array()); - if let Some(arr) = labs_arr { - println!("Laboratories (from cache):"); - for lab in arr { - let name = lab.get("name").and_then(|s| s.as_str()).unwrap_or(""); - let pi = lab.get("pi_name").and_then(|s| s.as_str()).unwrap_or(""); - let full = lab.get("full_name").and_then(|s| s.as_str()).unwrap_or(""); - println!(" {} (PI: {}, Full: {})", name, pi, full); - } - } else { - println!("No laboratories found in cache"); + println!( + "{: Result<(), Box> { let cache_path = crate::settings::SETTINGS .config_dirname .join("cache") .join(format!("{}.json", remote)); if cache_path.exists() { - fs::remove_file(&cache_path)?; - println!("Logged out from {}", remote); - } else { - println!("No login cache found for {}", remote); + std::fs::remove_file(&cache_path)?; } Ok(()) } diff --git a/src/commands/ls.rs b/src/commands/ls.rs index de2e485..97ecf6c 100644 --- a/src/commands/ls.rs +++ b/src/commands/ls.rs @@ -29,7 +29,7 @@ pub async fn ls( } else { build_folder_json_flat(&conn, &folder, &labname).await? }; - println!("{}", serde_json::to_string_pretty(&output)?); + println!("{}", serde_json::to_string(&output)?); } else if is_recursive { let prefix = format!("{}:/{}", remote, labname); ls_plain_recursive(&conn, folder, &labname, &prefix, password).await?; @@ -167,6 +167,8 @@ fn ls_plain_recursive<'a>( print_folder_plain(&sub_folders, &files_sorted, access, labname, false); + println!(); + for sf in sub_folders { if sf.lock { match password { diff --git a/src/commands/metadata.rs b/src/commands/metadata.rs index 980f29e..f856c51 100644 --- a/src/commands/metadata.rs +++ b/src/commands/metadata.rs @@ -13,6 +13,6 @@ pub async fn metadata(remote_path: &str, password: Option<&str>) -> Result<(), B .get(&format!("v3/folders/{}/metadata/", folder.id)) .await?; let json: serde_json::Value = resp.json().await?; - println!("{}", serde_json::to_string_pretty(&json)?); + println!("{}", serde_json::to_string(&json)?); Ok(()) } diff --git a/src/commands/mkdir.rs b/src/commands/mkdir.rs index d718a1b..ea7c7c7 100644 --- a/src/commands/mkdir.rs +++ b/src/commands/mkdir.rs @@ -1,6 +1,6 @@ use crate::commands::shared::{ - create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache, load_cache_with_token_refresh, - parse_remote_path, + create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache, + find_subfolder_by_name, load_cache_with_token_refresh, nfc, parse_remote_path, }; pub async fn mkdir(remote_path: &str) -> Result<(), Box> { @@ -26,26 +26,19 @@ pub async fn mkdir(remote_path: &str) -> Result<(), Box> let lab = find_lab_in_cache(&cache, &labname)?; let parent_folder = find_folder(&conn, lab.id, parent_path, None).await?; - // Check for name conflict in sub-folders - if parent_folder - .sub_folders - .iter() - .any(|f| f.name == new_folder_name) - { - return Err(format!("'{}' already exists as a folder", new_folder_name).into()); - } - // Check for name conflict in files + // Check for name conflict in sub-folders or files let files = conn.list_all_files(&parent_folder.id).await?; - if find_file_by_name(&files, new_folder_name).is_some() { - return Err(format!("'{}' already exists as a file", new_folder_name).into()); + if find_subfolder_by_name(&parent_folder.sub_folders, new_folder_name).is_some() + || find_file_by_name(&files, new_folder_name).is_some() + { + return Err(format!("Cannot create folder `{}`: File exists.", path).into()); } let resp = conn - .create_folder(&parent_folder.id, new_folder_name) + .create_folder(&parent_folder.id, &nfc(new_folder_name)) .await?; if !resp.status().is_success() { return Err(format!("Failed to create folder: {}", resp.status()).into()); } - println!("Created folder: {}", new_folder_name); Ok(()) } diff --git a/src/commands/mv.rs b/src/commands/mv.rs index 83cabde..106c4c8 100644 --- a/src/commands/mv.rs +++ b/src/commands/mv.rs @@ -1,6 +1,6 @@ use crate::commands::shared::{ - create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache, load_cache_with_token_refresh, - parse_remote_path, + create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache, + find_subfolder_by_name, load_cache_with_token_refresh, nfc, parse_remote_path, }; pub async fn mv(src_path: &str, dest_path: &str) -> Result<(), Box> { @@ -9,10 +9,10 @@ pub async fn mv(src_path: &str, dest_path: &str) -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box {}", src_path, dest_path); return Ok(()); } // Try source as a folder - if let Some(src_folder) = s_parent_folder - .sub_folders - .iter() - .find(|f| f.name.to_lowercase() == s_basename.to_lowercase()) - { - let src_folder_id = src_folder.id.clone(); - if find_file_by_name(&d_parent_files, &d_basename).is_some() { - return Err(format!("File `{}` already exists.", d_basename).into()); + let src_folder = match find_subfolder_by_name(&s_parent_folder.sub_folders, &s_basename) { + Some(f) => f, + None => return Err(format!("File or folder `{}` not found.", s_basename).into()), + }; + let src_folder_id = src_folder.id.clone(); + if find_file_by_name(&d_parent_files, &d_basename).is_some() { + return Err(format!( + "Cannot overwrite non-folder `{}` with folder `{}`.", + d_basename, s_path + ) + .into()); + } + if let Some(d_folder) = find_subfolder_by_name(&d_parent_folder.sub_folders, &d_basename) { + if d_folder.id == src_folder_id { + return Err( + format!("`{}` and `{}` are the same folder.", s_path, s_path).into(), + ); } - if d_parent_folder - .sub_folders - .iter() - .any(|f| f.name.to_lowercase() == d_basename.to_lowercase()) - { - return Err("Folder not empty.".into()); - } - let body = serde_json::json!({"parent": d_parent_folder.id, "name": d_basename}); - let resp = conn - .client - .post(conn.build_url(&format!("v3/folders/{}/move/", src_folder_id))) - .headers(conn.prepare_headers()) - .json(&body) - .send() - .await?; - if !resp.status().is_success() { - return Err(format!("Move failed: {}", resp.status()).into()); - } - println!("Moved: {} -> {}", src_path, dest_path); + return Err( + format!("Cannot move `{}` to `{}`: Folder not empty.", s_path, d_path).into(), + ); + } + // No-op if source and destination are identical + if s_parent_folder.id == d_parent_folder.id && s_basename == d_basename { return Ok(()); } - - Err(format!("Source `{}` not found.", src_path).into()) + let body = serde_json::json!({"parent": d_parent_folder.id, "name": d_basename}); + let resp = conn + .client + .post(conn.build_url(&format!("v3/folders/{}/move/", src_folder_id))) + .headers(conn.prepare_headers()) + .json(&body) + .send() + .await?; + if !resp.status().is_success() { + return Err(format!("Move failed: {}", resp.status()).into()); + } + Ok(()) } /// Split a path into (parent_dir, basename). diff --git a/src/commands/rm.rs b/src/commands/rm.rs index 998b8c8..f196732 100644 --- a/src/commands/rm.rs +++ b/src/commands/rm.rs @@ -1,6 +1,6 @@ use crate::commands::shared::{ - create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache, load_cache_with_token_refresh, - parse_remote_path, + create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache, + find_subfolder_by_name, load_cache_with_token_refresh, parse_remote_path, }; pub async fn rm(remote_path: &str, recursive: bool) -> Result<(), Box> { @@ -36,18 +36,13 @@ pub async fn rm(remote_path: &str, recursive: bool) -> Result<(), Box Result<(), Box( .ok_or_else(|| format!("Laboratory `{}` not found.", labname).into()) } +/// Apply Unicode NFC normalization to a string. +pub fn nfc(s: &str) -> String { + s.chars().nfc().collect() +} + /// Resolve a folder by path using the API (GET v3/folders/?path=...&laboratory_id=...) pub async fn find_folder( conn: &MDRSConnection, @@ -383,7 +389,8 @@ pub async fn find_folder( path: &str, password: Option<&str>, ) -> Result> { - let folders = conn.list_folders_by_path(lab_id, path).await?; + let normalized_path = nfc(path); + let folders = conn.list_folders_by_path(lab_id, &normalized_path).await?; if folders.is_empty() { return Err(format!("Folder `{}` not found.", path).into()); } @@ -409,10 +416,16 @@ pub async fn find_folder( Ok(folder) } -/// Find a file by name (case-insensitive) in a file list +/// Find a file by name (NFC-normalized, case-insensitive) in a file list. pub fn find_file_by_name<'a>(files: &'a [File], name: &str) -> Option<&'a File> { - let name_lower = name.to_lowercase(); - files.iter().find(|f| f.name.to_lowercase() == name_lower) + let name_lower = nfc(name).to_lowercase(); + files.iter().find(|f| nfc(&f.name).to_lowercase() == name_lower) +} + +/// Find a sub-folder by name (NFC-normalized, case-insensitive). +pub fn find_subfolder_by_name<'a>(subfolders: &'a [FolderSimple], name: &str) -> Option<&'a FolderSimple> { + let name_lower = nfc(name).to_lowercase(); + subfolders.iter().find(|f| nfc(&f.name).to_lowercase() == name_lower) } /// Format an ISO 8601 timestamp as "YYYY/MM/DD HH:MM:SS" diff --git a/src/commands/upload.rs b/src/commands/upload.rs index a4631ef..6a25e6d 100644 --- a/src/commands/upload.rs +++ b/src/commands/upload.rs @@ -1,7 +1,7 @@ use crate::models::folder::FolderSimple; use crate::commands::shared::{ - create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache, load_cache_with_token_refresh, - parse_remote_path, + create_authenticated_conn, find_file_by_name, find_folder, find_lab_in_cache, + load_cache_with_token_refresh, nfc, parse_remote_path, }; use futures::stream::{FuturesUnordered, StreamExt}; use std::path::PathBuf; @@ -128,10 +128,10 @@ async fn find_or_create_folder( existing: &[FolderSimple], name: &str, ) -> Result> { - if let Some(sf) = existing.iter().find(|f| f.name == name) { + if let Some(sf) = existing.iter().find(|f| nfc(&f.name).to_lowercase() == nfc(name).to_lowercase()) { return Ok(sf.id.clone()); } - let resp = conn.create_folder(parent_id, name).await?; + let resp = conn.create_folder(parent_id, &nfc(name)).await?; if !resp.status().is_success() { return Err(format!("Failed to create remote folder: {}", name).into()); } diff --git a/src/connection.rs b/src/connection.rs index 9a63dc1..fa8dd88 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -103,7 +103,12 @@ impl MDRSConnection { parent_id: &str, folder_name: &str, ) -> reqwest::Result { - let body = serde_json::json!({"parent": parent_id, "name": folder_name}); + let body = serde_json::json!({ + "name": folder_name, + "parent_id": parent_id, + "description": "", + "template_id": -1, + }); self.client .post(self.build_url("v3/folders/")) .headers(self.prepare_headers()) diff --git a/src/main.rs b/src/main.rs index 8af4640..ee4d32e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -275,21 +275,9 @@ fn main() { } Commands::Labs { remote } => { let remote = remote.trim_end_matches(':'); - match crate::commands::config::get_remote_url(remote) { - Ok(Some(url)) => { - let conn = std::sync::Arc::new(crate::connection::MDRSConnection::new(&url)); - let rt = tokio::runtime::Runtime::new().unwrap(); - if let Err(e) = rt.block_on(crate::commands::labs::labs(conn.clone(), remote)) { - handle_error(e); - } - } - Ok(None) => { - eprintln!("Error: Remote host `{}` is not configured", remote); - std::process::exit(2); - } - Err(e) => { - handle_error(e); - } + let rt = tokio::runtime::Runtime::new().unwrap(); + if let Err(e) = rt.block_on(commands::labs::labs(remote)) { + handle_error(e); } }