diff --git a/CHANGELOG.md b/CHANGELOG.md index bcf9b5d..5416177 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). #### Unreleased +- refactor: rename project [`#24`](https://github.com/i18nhero/cli/pull/24) - feat: add flag for saving missing translations [`#23`](https://github.com/i18nhero/cli/pull/23) - feat(cli): json5 output [`#19`](https://github.com/i18nhero/cli/pull/19) - feat: generate yml output [`#18`](https://github.com/i18nhero/cli/pull/18) diff --git a/Cargo.lock b/Cargo.lock index 736d4dd..23e6ad1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -268,6 +268,19 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + [[package]] name = "cookie" version = "0.18.1" @@ -349,6 +362,19 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror", + "zeroize", +] + [[package]] name = "digest" version = "0.10.7" @@ -365,6 +391,12 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.34" @@ -639,12 +671,15 @@ version = "0.0.0" dependencies = [ "clap", "clap_complete", + "console", + "dialoguer", "i18nhero-config", "json5", "reqwest", "serde", "serde_json", "serde_yml", + "tokio", ] [[package]] @@ -734,6 +769,12 @@ dependencies = [ "serde", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.158" @@ -756,6 +797,16 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.22" @@ -825,6 +876,29 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1041,6 +1115,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redox_syscall" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355ae415ccd3a04315d3f8246e86d67689ea74d88d915576e1589a351062a13b" +dependencies = [ + "bitflags", +] + [[package]] name = "reqwest" version = "0.12.7" @@ -1202,6 +1285,12 @@ dependencies = [ "syn", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.209" @@ -1283,12 +1372,27 @@ dependencies = [ "digest", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.9" @@ -1462,11 +1566,25 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys 0.52.0", ] +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio-rustls" version = "0.26.0" @@ -1576,6 +1694,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 7159991..3edd815 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ serde = { version = "1.0.209", features = ["derive"] } serde_json = "1.0.128" serde_yml = "0.0.12" tempfile = "3.12.0" +tokio = { version = "1.40.0", features = ["full"] } # Config for 'cargo dist' [workspace.metadata.dist] diff --git a/i18nhero.json b/i18nhero.json index a8223f5..71f3cf0 100644 --- a/i18nhero.json +++ b/i18nhero.json @@ -1,3 +1,9 @@ { - "$schema": "https://raw.githubusercontent.com/i18nhero/cli/main/schemas/v0.0.0/i18nhero.schema.json" + "$schema": "https://raw.githubusercontent.com/i18nhero/cli/main/schemas/v0.0.0/i18nhero.schema.json", + "project_id": "66da3bc6de9d3506120c0a87", + "output": { + "path": "lang", + "format": "json", + "save_missing_values": false + } } diff --git a/packages/i18nhero-config/src/lib.rs b/packages/i18nhero-config/src/lib.rs index 23cc429..cd8e5bf 100644 --- a/packages/i18nhero-config/src/lib.rs +++ b/packages/i18nhero-config/src/lib.rs @@ -1,6 +1,5 @@ -use std::str::FromStr; - use error::ConfigError; +use std::str::FromStr; pub mod error; @@ -84,6 +83,14 @@ impl Default for CliConfig { } impl CliConfig { + #[inline] + pub fn new(project_id: String) -> Self { + Self { + project_id, + ..Default::default() + } + } + #[inline] pub fn load(path: impl AsRef) -> Result { let content = std::fs::read_to_string(path)?; diff --git a/packages/i18nhero/Cargo.toml b/packages/i18nhero/Cargo.toml index 3d49cdb..b7155cd 100644 --- a/packages/i18nhero/Cargo.toml +++ b/packages/i18nhero/Cargo.toml @@ -23,7 +23,10 @@ i18nhero-config = { workspace = true } clap = { workspace = true } clap_complete = { workspace = true } json5 = { workspace = true } -reqwest = { workspace = true } +reqwest.workspace = true serde = { workspace = true } serde_json = { workspace = true } serde_yml = { workspace = true } +dialoguer = "0.11.0" +console = "0.15.8" +tokio = { workspace = true } diff --git a/packages/i18nhero/src/auth/mod.rs b/packages/i18nhero/src/auth/mod.rs new file mode 100644 index 0000000..0c7b35b --- /dev/null +++ b/packages/i18nhero/src/auth/mod.rs @@ -0,0 +1,9 @@ +use crate::error::CliError; + +pub async fn get_api_key() -> Result { + // TODO: + + let api_key = "66d7828c3c19bf6163ac150a".to_owned(); + + Ok(api_key) +} diff --git a/packages/i18nhero/src/commands/init.rs b/packages/i18nhero/src/commands/init.rs index f3c8676..c145362 100644 --- a/packages/i18nhero/src/commands/init.rs +++ b/packages/i18nhero/src/commands/init.rs @@ -4,4 +4,7 @@ use clap::Args; pub struct InitCommandArguments { #[arg(long, default_value_t = false)] pub overwrite: bool, + + #[arg(long, hide = true)] + pub api_host: Option, } diff --git a/packages/i18nhero/src/error/mod.rs b/packages/i18nhero/src/error/mod.rs index 80e03ed..fb303d0 100644 --- a/packages/i18nhero/src/error/mod.rs +++ b/packages/i18nhero/src/error/mod.rs @@ -8,6 +8,8 @@ pub enum CliError { Reqwest(reqwest::Error), ConfigAlreadyExists, MissingProjectId, + NoConnectedOrganizations, + NoAvailableProjects((String, String)), } impl std::error::Error for CliError {} @@ -25,6 +27,13 @@ impl core::fmt::Display for CliError { Self::ConfigAlreadyExists => write!(f, "A configuration file already exists"), Self::MissingProjectId => write!(f, "project_id must be set in config"), + Self::NoConnectedOrganizations => { + write!(f, "You do not have development access to any organizations") + } + Self::NoAvailableProjects((organization_title, organization_id)) => write!( + f, + "You do not have development access to any projects for {organization_title} ({organization_id})" + ), } } } diff --git a/packages/i18nhero/src/init/mod.rs b/packages/i18nhero/src/init/mod.rs index 261dd17..a55bcdd 100644 --- a/packages/i18nhero/src/init/mod.rs +++ b/packages/i18nhero/src/init/mod.rs @@ -1,14 +1,125 @@ +use dialoguer::{theme::ColorfulTheme, Select}; use i18nhero_config::CliConfig; -use crate::{commands::init::InitCommandArguments, config::CONFIG_PATH, error::CliError}; +use crate::{ + auth::get_api_key, commands::init::InitCommandArguments, config::CONFIG_PATH, error::CliError, + terminal::print_configuration_file_created, DEFAULT_API_HOST, +}; + +#[derive(serde::Deserialize, Debug)] +pub struct Organization { + pub _id: String, + + pub title: String, +} + +async fn get_organizations(host: &str, api_key: &str) -> Vec { + let http_client = reqwest::Client::new(); + + http_client + .get(format!("{host}/organizations")) + .header("x-api-key", api_key) + .send() + .await + .unwrap() + .json::>() + .await + .unwrap() +} + +fn select_organization(organizations: &Vec) -> usize { + let mut options = Vec::with_capacity(organizations.len()); + + for org in organizations { + options.push(format!("{} ({})", org.title, org._id)); + } + + Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Organization") + .default(0) + .items(&options[..]) + .interact() + .unwrap() +} + +#[derive(Debug, serde::Deserialize)] +struct Project { + _id: String, + title: String, +} + +async fn get_organization_projects( + host: &str, + api_key: &str, + organization_id: &str, +) -> Vec { + let http_client = reqwest::Client::new(); + + http_client + .get(format!("{host}/organizations/{organization_id}/projects")) + .header("x-api-key", api_key) + .send() + .await + .unwrap() + .json::>() + .await + .unwrap() +} + +fn select_project(projects: &Vec) -> usize { + let mut options = Vec::with_capacity(projects.len()); + + for project in projects { + options.push(format!("{} ({})", project.title, project._id)); + } + + Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Project") + .default(0) + .items(&options[..]) + .interact() + .unwrap() +} #[inline] -pub fn run(arguments: &InitCommandArguments) -> Result<(), CliError> { +pub async fn run(arguments: &InitCommandArguments) -> Result<(), CliError> { if !arguments.overwrite && std::fs::exists(CONFIG_PATH)? { return Err(CliError::ConfigAlreadyExists); } - let config = CliConfig::default(); + let api_key = get_api_key().await?; + + let host = arguments + .api_host + .as_ref() + .map_or(DEFAULT_API_HOST, |api_host| api_host); + + let organizations = get_organizations(host, &api_key).await; + + if organizations.is_empty() { + return Err(CliError::NoConnectedOrganizations); + } + + let organization_index = select_organization(&organizations); + + // we can unwrap here since it can't be out of bounds + let selected_organization = organizations.get(organization_index).unwrap(); + + let projects = get_organization_projects(host, &api_key, &selected_organization._id).await; + + if projects.is_empty() { + return Err(CliError::NoAvailableProjects(( + selected_organization.title.to_string(), + selected_organization._id.to_string(), + ))); + } + + let project_index = select_project(&projects); + + // we can unwrap here since it can't be out of bounds + let selected_project = projects.get(project_index).unwrap(); + + let config = CliConfig::new(selected_project._id.to_string()); let mut json = serde_json::to_string_pretty(&config)?; @@ -16,7 +127,7 @@ pub fn run(arguments: &InitCommandArguments) -> Result<(), CliError> { std::fs::write(CONFIG_PATH, json)?; - println!("Configuration file has been created"); + print_configuration_file_created(); Ok(()) } diff --git a/packages/i18nhero/src/main.rs b/packages/i18nhero/src/main.rs index 111275d..fa4b5f2 100644 --- a/packages/i18nhero/src/main.rs +++ b/packages/i18nhero/src/main.rs @@ -4,21 +4,25 @@ use config::CONFIG_PATH; use error::CliError; use i18nhero_config::CliConfig; +mod auth; mod commands; mod completions; mod config; mod error; -pub mod generators; +mod generators; mod init; mod pull; mod push; +mod terminal; -pub const DEFAULT_API_HOST: &str = "https://web.api.i18nhero.com"; +pub const DEFAULT_API_HOST: &str = "https://api.i18nhero.com"; + +pub const DEFAULT_WEB_API_HOST: &str = "https://web.api.i18nhero.com"; #[inline] -fn _main() -> Result<(), CliError> { +async fn _main() -> Result<(), CliError> { match Cli::parse().command { - CliCommand::Init(arguments) => init::run(&arguments).map_err(CliError::from), + CliCommand::Init(arguments) => init::run(&arguments).await.map_err(CliError::from), CliCommand::Pull(arguments) => { let config = CliConfig::load(CONFIG_PATH)?; @@ -37,9 +41,10 @@ fn _main() -> Result<(), CliError> { } } -fn main() { - if let Err(error) = _main() { - eprintln!("{error}"); +#[tokio::main] +async fn main() { + if let Err(error) = _main().await { + terminal::print_error(&error); std::process::exit(1) } diff --git a/packages/i18nhero/src/pull/mod.rs b/packages/i18nhero/src/pull/mod.rs index 8e79ef8..fd7b076 100644 --- a/packages/i18nhero/src/pull/mod.rs +++ b/packages/i18nhero/src/pull/mod.rs @@ -1,6 +1,8 @@ use i18nhero_config::{CliConfig, CliConfigOutputFormat}; -use crate::{commands::pull::PullCommandArguments, error::CliError, generators, DEFAULT_API_HOST}; +use crate::{ + commands::pull::PullCommandArguments, error::CliError, generators, DEFAULT_WEB_API_HOST, +}; #[derive(Debug, serde::Deserialize)] struct PullLocale { @@ -69,7 +71,7 @@ pub fn run(arguments: &PullCommandArguments, config: &CliConfig) -> Result<(), C arguments .api_host .as_ref() - .map_or(DEFAULT_API_HOST, |api_host| api_host), + .map_or(DEFAULT_WEB_API_HOST, |api_host| api_host), &config.project_id, )?; diff --git a/packages/i18nhero/src/push/mod.rs b/packages/i18nhero/src/push/mod.rs index 33a2960..c4a1c7b 100644 --- a/packages/i18nhero/src/push/mod.rs +++ b/packages/i18nhero/src/push/mod.rs @@ -1,6 +1,6 @@ use i18nhero_config::{CliConfig, CliConfigOutputFormat}; -use crate::{commands::push::PushCommandArguments, error::CliError, DEFAULT_API_HOST}; +use crate::{commands::push::PushCommandArguments, error::CliError, DEFAULT_WEB_API_HOST}; #[derive(Debug, serde::Serialize)] struct PushLocale { @@ -60,7 +60,7 @@ pub fn run(arguments: &PushCommandArguments, config: &CliConfig) -> Result<(), C arguments .api_host .as_ref() - .map_or(DEFAULT_API_HOST, |api_host| api_host), + .map_or(DEFAULT_WEB_API_HOST, |api_host| api_host), &config.project_id, &locales, )?; diff --git a/packages/i18nhero/src/terminal/mod.rs b/packages/i18nhero/src/terminal/mod.rs new file mode 100644 index 0000000..a758435 --- /dev/null +++ b/packages/i18nhero/src/terminal/mod.rs @@ -0,0 +1,16 @@ +use crate::error::CliError; + +#[inline] +pub fn print_error(error: &CliError) { + eprintln!("{}", console::style(format!("{error}")).red().bold()); +} + +#[inline] +pub fn print_configuration_file_created() { + println!( + "\n{}", + console::style("Configuration file has been created!") + .green() + .bold() + ); +}