diff --git a/.gitignore b/.gitignore index 4022e20..d71e71b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ Cargo.lock # Added by cargo /target + +keyshade.toml diff --git a/Cargo.toml b/Cargo.toml index bc55384..c405484 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,10 +12,17 @@ edition = "2021" clap = { version = "4.5.4", features = ["derive"] } colored = "2.1.0" directories = "5.0.1" +futures-util = "0.3.30" inquire = "0.7.4" +net = "0.0.2" serde = {version = "1.0.197", features = ["derive"] } +serde_json = "1.0.117" spinners = "4.1.1" +tokio = { version = "1.38.0", features = ["rt-multi-thread"] } +tokio-tungstenite = "0.23.1" toml = "0.8.12" +websocket = "0.27.1" +whoami = "1.5.1" # We optimize the release build for size. (https://doc.rust-lang.org/cargo/reference/profiles.html) [profile.release] diff --git a/src/commands/add.rs b/src/commands/add.rs deleted file mode 100644 index 28ffa0e..0000000 --- a/src/commands/add.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::{ - fs::{self, OpenOptions}, - path::Path, -}; - -use colored::Colorize; -use directories::UserDirs; -use inquire::Password; -use spinners::{Spinner, Spinners}; -use std::io::Write; - -use crate::{constants::CONFIG_FILE_NAME, generate_workspace_toml}; -/// Adds a new workspace and project to the keyshades-cli configuration file. -/// -/// # Arguments -/// -/// * `wrkspc` - A reference to a `String` representing the workspace name. -/// * `prjct` - An optional reference to a `String` representing the project name. -/// -/// # Example -/// -/// ``` -/// let workspace = "my_workspace".to_string(); -/// let project = Some("my_project".to_string()); -/// add(&workspace, project.as_ref()); -/// ``` -pub fn add(wrkspc: &String, prjct: Option<&String>) { - let mut api_key_input: String = String::new(); - let mut private_key_input: String = String::new(); - - if let Some(user_dirs) = UserDirs::new() { - let config_dir: &Path = user_dirs.home_dir(); - let config_file_path = config_dir.join(CONFIG_FILE_NAME); - match fs::read_to_string(config_file_path.clone()) { - Ok(_config_file) => { - let mut file = OpenOptions::new() - .write(true) - .append(true) - .open(config_file_path.clone()) - .unwrap(); - if prjct.is_some() { - api_key_input = Password::new("Enter your API Key:") - .without_confirmation() - .prompt() - .unwrap(); - private_key_input = Password::new("Enter your Private Key:") - .without_confirmation() - .prompt() - .unwrap(); - } - let mut sp = Spinner::new(Spinners::Dots9, "Creating config file...".into()); - if let Some(project) = prjct { - let config_str: String = generate_workspace_toml!( - wrkspc, - Some(project.to_string()), - api_key_input, - private_key_input - ); - // fs::write(config_dir.join(CONFIG_FILE_NAME), config_str).unwrap(); - if let Err(e) = writeln!(file, "{}", config_str) { - eprintln!("Couldn't write to file: {}", e); - } - } else { - let config_str: String = generate_workspace_toml!(wrkspc, None, "", ""); - // fs::write(config_dir.join(CONFIG_FILE_NAME), config_str).unwrap(); - if let Err(e) = writeln!(file, "{}", config_str) { - eprintln!("Couldn't write to file: {}", e); - } - } - - sp.stop(); - println!("\n{}", "Config file created 🎉".bright_green()); - } - Err(_e) => { - println!("{}", "Config file does not exist".bright_yellow().bold()); - println!( - "{}", - "Please run the configure command first".bright_yellow() - ); - println!("{}", "Usage: ks configure -h".bright_yellow()); - } - } - } else { - eprintln!( - "{}", - "Error: Could not find the user's home directory".bright_red() - ); - } -} diff --git a/src/commands/configure.rs b/src/commands/configure.rs index f09e653..aa32423 100644 --- a/src/commands/configure.rs +++ b/src/commands/configure.rs @@ -1,78 +1,171 @@ -use colored::Colorize; -use inquire::Password; -use spinners::{Spinner, Spinners}; -use std::{fs, path::Path}; - -use directories::UserDirs; - -use crate::{constants::CONFIG_FILE_NAME, generate_config_toml}; - -/// Configures the keyshades-cli by creating a configuration file in the user's home directory. -/// -/// # Arguments -/// -/// * `wrkspc` - A reference to a `String` representing the workspace name. -/// * `prjct` - An optional reference to a `String` representing the project name. -/// -/// # Example -/// -/// ``` -/// let workspace = "my_workspace".to_string(); -/// let project = Some("my_project".to_string()); -/// configure(&workspace, project.as_ref()); -/// ``` -pub fn configure(wrkspc: &String, prjct: Option<&String>) { - let mut api_key_input: String = String::new(); - let mut private_key_input: String = String::new(); - - println!("\n{}\n", "* Configuring the keyshades-cli".on_cyan().bold()); - - if let Some(user_dirs) = UserDirs::new() { - let config_dir: &Path = user_dirs.home_dir(); - // Linux: /home/JohnDoe - // Windows: C:\Users\JohnDoe - // macOS: /Users/JohnDoe - - match fs::read_to_string(config_dir.join(CONFIG_FILE_NAME)) { - Ok(_config_file) => { - println!("{}", "Config file exists 🙌".bright_green()); +use clap::ArgMatches; +use std::io; +use std::io::Write; + +use crate::{ + file_exists, generate_project_toml, generate_user_root_toml, + get_os_specific_user_root_config_path, read_from_terminal, read_securely_from_terminal, + write_file, +}; + +use super::AbstractCommandInterface; + +#[derive(Debug)] +struct ConfigureCommandParsedData { + workspace: String, + project: String, + environment: String, + api_key: String, + private_key: String, +} + +pub struct ConfigureCommand<'a> { + parsed_data: ConfigureCommandParsedData, + args: &'a ArgMatches, +} + +impl<'a> ConfigureCommand<'a> { + pub fn new(args: &'a ArgMatches) -> ConfigureCommand<'a> { + ConfigureCommand { + parsed_data: ConfigureCommandParsedData { + workspace: String::new(), + project: String::new(), + environment: String::new(), + api_key: String::new(), + private_key: String::new(), + }, + args, + } + } + + fn create_keyshade_toml(&self) -> Result<(), io::Error> { + println!("Creating keyshade.toml..."); + + // Get the parsed toml content + let toml_content = generate_project_toml!( + &self.parsed_data.workspace, + &self.parsed_data.project, + &self.parsed_data.environment + ); + + // Write the toml content to the file + write_file!("keyshade.toml", toml_content); + + println!("keyshade.toml created successfully!"); + + Ok(()) + } + + fn create_user_root_toml(&self) -> Result<(), io::Error> { + println!("Creating user root toml..."); + + // Get the user root toml path + let user_root_toml_path = get_os_specific_user_root_config_path!(self.parsed_data.project); + + // Get the parsed toml content + let toml_content = generate_user_root_toml!( + &self.parsed_data.api_key, + &self.parsed_data.private_key, + &self.parsed_data.project + ); + + // Write the toml content to the file + write_file!(&user_root_toml_path, toml_content); + + println!("User root toml created successfully!"); + + Ok(()) + } +} + +impl<'a> AbstractCommandInterface for ConfigureCommand<'a> { + fn parse_args(&mut self) -> Result<(), io::Error> { + let args = self.args; + + let workspace = if let Some(w) = args.get_one::("WORKSPACE") { + w.to_string() + } else { + read_from_terminal!("Enter the workspace name: ") + }; + + // Read project name + let project = if let Some(p) = args.get_one::("PROJECT") { + p.to_string() + } else { + read_from_terminal!("Enter the project name:") + }; + + // Read environment name + let environment = if let Some(e) = args.get_one::("ENVIRONMENT") { + e.to_string() + } else { + read_from_terminal!("Enter the environment name: ") + }; + + // Read API Key + let api_key = if let Some(a) = args.get_one::("API_KEY") { + a.to_string() + } else { + read_securely_from_terminal!("Enter your API Key:") + }; + + // Read Private Key + let private_key = if let Some(p) = args.get_one::("PRIVATE_KEY") { + p.to_string() + } else { + read_securely_from_terminal!("Enter your Private Key:") + }; + + self.parsed_data = ConfigureCommandParsedData { + workspace, + project, + environment, + api_key, + private_key, + }; + + Ok(()) + } + + async fn execute(&self) -> Result<(), io::Error> { + let mut should_upsert_keyshade_toml = true; + let mut should_upsert_user_root_toml = true; + + // Check if keyshade.toml exists in the current directory + if file_exists!("keyshade.toml") { + // If it does, ask if the users want to overwrite it + let choice = read_from_terminal!( + "keyshade.toml already exists. Do you want to overwrite it? (y/n): " + ); + + if choice.to_lowercase() != "y" { + println!("Skipping keyshade.toml creation..."); + should_upsert_keyshade_toml = false; } - Err(_e) => { - // add a new workspace and project if the file does not exist - - if prjct.is_some() { - api_key_input = Password::new("Enter your API Key:") - .without_confirmation() - .prompt() - .unwrap(); - private_key_input = Password::new("Enter your Private Key:") - .without_confirmation() - .prompt() - .unwrap(); - } - - let mut sp = Spinner::new(Spinners::Dots9, "Creating config file...".into()); - if let Some(project) = prjct { - let config_str: String = generate_config_toml!( - wrkspc, - Some(project.to_string()), - api_key_input, - private_key_input - ); - fs::write(config_dir.join(CONFIG_FILE_NAME), config_str).unwrap(); - } else { - let config_str: String = generate_config_toml!(wrkspc, None, "", ""); - fs::write(config_dir.join(CONFIG_FILE_NAME), config_str).unwrap(); - } - - sp.stop(); - println!("\n{}", "Config file created 🎉".bright_green()); + } + + // Check if user root toml exists + let user_root_toml_path = get_os_specific_user_root_config_path!(self.parsed_data.project); + if file_exists!(&user_root_toml_path) { + // If it does, ask if the users want to overwrite it + let choice = read_from_terminal!(format!( + "{} already exists. Do you want to overwrite it? (y/n): ", + user_root_toml_path + )); + + if choice.to_lowercase() != "y" { + println!("Skipping user root toml creation..."); + should_upsert_user_root_toml = false; } } - } else { - eprintln!( - "{}", - "Error: Could not find the user's home directory".bright_red() - ); + + if should_upsert_keyshade_toml { + self.create_keyshade_toml()?; + } + + if should_upsert_user_root_toml { + self.create_user_root_toml()?; + } + Ok(()) } } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 25ac6c7..9ae2de2 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,123 +1,9 @@ -use crate::{ - commands::add::add, - commands::configure::configure, - constants::{ABOUT, VERSION}, -}; -mod add; -mod configure; -mod run_command_with_env; -use clap::{arg, Arg, ArgAction, ArgMatches, Command}; +use std::io; -use self::run_command_with_env::run; +pub mod configure; +pub mod run; -/// Constructs the command line interface for the keyshades CLI. -pub fn cli() -> Command { - Command::new("keyshades-cli") - .alias("ks") - .version(VERSION) - .about(ABOUT) - .max_term_width(100) - .subcommand_required(true) - .arg_required_else_help(true) - .subcommand( - Command::new("configure") - .alias("conf") - .about("Configure the keyshades CLI, alias to `conf`") - .arg( - Arg::new("WORKSPACE") - .long("workspace") - .short('w') - .action(ArgAction::Set) - .help("Configure the workspace") - .required(true), - ) - .arg( - Arg::new("PROJECT") - .long("project") - .short('p') - .action(ArgAction::Set) - .help("Configure the project"), - ), - ) - .subcommand( - Command::new("add") - .about("Add a new project to the workspace or a new workspace") - .arg( - Arg::new("WORKSPACE") - .long("workspace") - .short('w') - .action(ArgAction::Set) - .help("Add the workspace") - .required(true), - ) - .arg( - Arg::new("PROJECT") - .long("project") - .short('p') - .action(ArgAction::Set) - .help("Add the project, to add a project you must specify the workspace"), - ), - ) - .subcommand( - Command::new("remove") - .alias("rm") - .about("Remove project(s) or workspace(s), alias to `rm`") - .arg(arg!(-w --workspace "Configure the workspace ").required(true)) - .arg(arg!(-p --project "Configure the project")), - ) - .subcommand( - Command::new("list") - .alias("li") - .about("List project(s) or workspace(s), alias to `li`") - .arg(arg!(-w --workspace "list all workspace").required(true)) - .arg(arg!(-p --project "list all projects")), - ) - .subcommand( - Command::new("run") - .alias("r") - .about("run a command, alias to `r`") - .arg( - Arg::new("COMMAND") - .help("The command to run") - .value_name("COMMAND") - .allow_hyphen_values(true) - .last(true) - .num_args(1..) - .action(ArgAction::Set) - .required(true), - ), - ) -} - -pub fn execution() { - let matches: ArgMatches = cli().get_matches(); - match matches.subcommand() { - Some(("configure", sub_m)) => { - let workspace: &String = sub_m.get_one::("WORKSPACE").unwrap(); - let project: Option<&String> = sub_m.get_one::("PROJECT"); - configure(workspace, project); - } - Some(("add", sub_m)) => { - let workspace: &String = sub_m.get_one::("WORKSPACE").unwrap(); - let project: Option<&String> = sub_m.get_one::("PROJECT"); - add(workspace, project); - } - Some(("remove", sub_m)) => { - dbg!(sub_m); - } - Some(("list", sub_m)) => { - dbg!(sub_m); - } - Some(("run", sub_m)) => { - run(sub_m); - } - _ => { - println!("No subcommand was used"); - } - } -} - -/// Executes the command line application. -pub fn main() { - execution(); +pub trait AbstractCommandInterface { + fn parse_args(&mut self) -> Result<(), io::Error>; + async fn execute(&self) -> Result<(), io::Error>; } diff --git a/src/commands/run.rs b/src/commands/run.rs new file mode 100644 index 0000000..8102721 --- /dev/null +++ b/src/commands/run.rs @@ -0,0 +1,132 @@ +use std::io; +use std::thread; +use websocket::client::ClientBuilder; +use websocket::OwnedMessage; +use serde_json::json; + +use crate::{ + file_exists, get_os_specific_user_root_config_path, + models::toml_model::{ProjectRootConfig, UserRootConfig}, + read_file, +}; + +use super::AbstractCommandInterface; + +pub struct RunCommand { + workspace: String, + project: String, + environment: String, + api_key: String, + private_key: String, +} + +impl RunCommand { + pub fn new() -> RunCommand { + RunCommand { + workspace: String::new(), + project: String::new(), + environment: String::new(), + api_key: String::new(), + private_key: String::new(), + } + } + + fn read_keyshade_toml(&self) -> Result { + // Check if the keyshade.toml exists + if !file_exists!("keyshade.toml") { + panic!("keyshade.toml not found. Please run `keyshade configure` first."); + } + + // Read the keyshade.toml + let keyshade_toml_content = read_file!("keyshade.toml"); + + // Parse the keyshade.toml + let keyshade_toml: ProjectRootConfig = toml::from_str(&keyshade_toml_content).unwrap(); + + Ok(keyshade_toml) + } + + fn read_user_root_toml(&self, project: &String) -> Result { + // Check if the user root toml exists + let user_root_toml_path = get_os_specific_user_root_config_path!(project); + if !file_exists!(&user_root_toml_path) { + panic!("User root toml not found. Please run `keyshade configure` first."); + } + + // Read the user root toml + let user_root_toml_content = read_file!(&user_root_toml_path); + + // Parse the user root toml + let user_root_toml: UserRootConfig = toml::from_str(&user_root_toml_content).unwrap(); + + Ok(user_root_toml) + } +} + +impl AbstractCommandInterface for RunCommand { + fn parse_args(&mut self) -> Result<(), io::Error> { + let keyshade_toml = self.read_keyshade_toml().unwrap(); + let user_root_toml = self.read_user_root_toml(&keyshade_toml.project).unwrap(); + + self.workspace = keyshade_toml.workspace; + self.project = keyshade_toml.project; + self.environment = keyshade_toml.environment; + self.api_key = user_root_toml.api_key; + self.private_key = user_root_toml.private_key; + + Ok(()) + } + + fn execute(&self) -> Result<(), io::Error> { + print!("Running the project: "); + println!( + "{} {} {} {}", + self.workspace, self.project, self.environment, self.api_key + ); + + // Create a ClientBuilder + let mut client = ClientBuilder::new("ws://localhost:4200/change-notifier") + .unwrap() + .add_protocol("rust-websocket") + .custom_headers(&vec![ + ("x-keyshade-token".to_string(), self.api_key.clone()), + ("sec-websocket-key".to_string(), self.private_key.clone()), + ("host".to_string(), "localhost:4200".to_string()), + ]) + .connect_insecure() + .expect("Failed to connect to WebSocket server"); + + println!("Connected"); + + // Register client app + let register_message = json!({ + "workspaceName": self.workspace, + "projectName": self.project, + "environmentName": self.environment + }); + + client + .send_message(&OwnedMessage::Text(register_message.to_string())) + .expect("Failed to send register message"); + + // Create a thread to receive messages + let (mut receiver, _) = client.split().unwrap(); + thread::spawn(move || { + for message in receiver.incoming_messages() { + match message { + Ok(OwnedMessage::Text(text)) => println!("Change received: {}", text), + Ok(_) => (), + Err(e) => { + println!("Error: {:?}", e); + break; + } + } + } + }); + + // Keep the main thread running + loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + } + } +} \ No newline at end of file diff --git a/src/commands/run_command_with_env.rs b/src/commands/run_command_with_env.rs deleted file mode 100644 index ae99bdb..0000000 --- a/src/commands/run_command_with_env.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::{collections::HashMap, process::Command}; - -use clap::ArgMatches; - -/// Runs a command with environment variables. -/// -/// # Arguments -/// -/// * `sub_m` - A reference to a `ArgMatches` struct. -/// -/// # Remarks -/// -/// The `run` function takes a reference to a `ArgMatches` struct as an argument. The `ArgMatches` struct is used to parse the command line arguments. The `run` function then creates a `HashMap` of environment variables and inserts some key-value pairs. The `run` function then extracts the command from the `ArgMatches` struct and executes it using the `Command` struct from the `std::process` module. The output of the command is then printed to the console. -/// -/// # Example of command: -/// ```bash -/// keyshades-cli run -- node index.js -/// `````` -pub fn run(sub_m: &ArgMatches) { - let mut env_vars: HashMap = HashMap::new(); // ! this will be replaced with the env vars from the backend - env_vars.insert("NAME".to_string(), "Sawan".to_string()); - env_vars.insert("AGE".to_string(), "21".to_string()); - env_vars.insert("COUNTRY".to_string(), "India".to_string()); - - let command: Vec<&str> = sub_m - .get_many::("COMMAND") - .unwrap_or_default() - .map(|v| v.as_str()) - .collect::>(); - if let Some((program, args)) = command.split_first() { - let output = Command::new(program).args(args).envs(env_vars).output(); - - match output { - Ok(output) => { - // Print the output - if !output.stdout.is_empty() { - println!("{}", String::from_utf8_lossy(&output.stdout)); - } - // Print the error - if !output.stderr.is_empty() { - eprintln!("{}", String::from_utf8_lossy(&output.stderr)); - } - } - Err(e) => { - eprintln!("Failed to execute command: {}", e); - } - } - } else { - println!("No command provided"); - } -} diff --git a/src/constants.rs b/src/constants.rs index 0940aa0..d0334b2 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,11 +1,11 @@ /// The version of the application. pub const VERSION: &str = env!("CARGO_PKG_VERSION"); -pub const ABOUT: &str = "A command line utility for keyshades. +pub const ABOUT: &str = "A command line utility for keyshade. -This tools helps you to populate your secrects to your application, and manage them in a secure way. +This tools helps you to populate your secrets and configurations to your application, and manage them in a secure way. Use `--help` on the subcommands to learn more about them."; pub const CONFIG_FILE_NAME: &str = ".keyshade.toml"; -pub const BASE_URL: &str = "https://api.keyshades.com"; \ No newline at end of file +pub const BASE_URL: &str = "https://api.keyshade.xyz"; diff --git a/src/macros/command_line.rs b/src/macros/command_line.rs new file mode 100644 index 0000000..218e9ef --- /dev/null +++ b/src/macros/command_line.rs @@ -0,0 +1,53 @@ +#[macro_export] +/// Reads a line of input from the terminal. +/// +/// # Arguments +/// +/// * `$prompt` - The prompt to display to the user. +/// +/// # Returns +/// +/// A string representation of the user input. +/// +/// # Example +/// +/// ```rust +/// let input: String = read_from_terminal!("Enter your name:").unwrap(); +/// println!("{}", input); +/// ``` +macro_rules! read_from_terminal { + ($prompt:expr) => {{ + let mut input = String::new(); + print!("{}", $prompt); + ::std::io::stdout().flush().unwrap(); + ::std::io::stdin().read_line(&mut input).unwrap(); + input.trim().to_string() + }}; +} + +#[macro_export] +/// Reads a line of input securely from the terminal. +/// +/// # Arguments +/// +/// * `$prompt` - The prompt to display to the user. +/// +/// # Returns +/// +/// A string representation of the user input. +/// +/// # Example +/// +/// ```rust +/// let input: String = read_securely_from_terminal!("Enter your password:").unwrap(); +/// println!("{}", input); +/// ``` +macro_rules! read_securely_from_terminal { + ($prompt:expr) => {{ + let input = ::inquire::Password::new($prompt) + .without_confirmation() + .prompt() + .unwrap(); + input.trim().to_string() + }}; +} diff --git a/src/macros/command_line_macros.rs b/src/macros/command_line_macros.rs deleted file mode 100644 index b70b006..0000000 --- a/src/macros/command_line_macros.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[macro_export] -macro_rules! create_arg { - () => {} -} \ No newline at end of file diff --git a/src/macros/file_io.rs b/src/macros/file_io.rs new file mode 100644 index 0000000..dc61abf --- /dev/null +++ b/src/macros/file_io.rs @@ -0,0 +1,32 @@ +#[macro_export] +macro_rules! file_exists { + ($file_name:expr) => {{ + ::std::fs::metadata($file_name).is_ok() + }}; +} + +#[macro_export] +macro_rules! read_file { + ($file_name:expr) => {{ + let mut file = ::std::fs::File::open($file_name).unwrap(); + let mut contents = String::new(); + file.read_to_string(&mut contents).unwrap(); + contents + }}; +} + +#[macro_export] +macro_rules! write_file { + ($file_name:expr, $contents:expr) => {{ + let path = ::std::path::Path::new($file_name); + + // Create the parent directory if it doesn't exist + if let Some(parent) = path.parent() { + ::std::fs::create_dir_all(parent).unwrap(); + } + + // Write the contents to the file + let mut file = ::std::fs::File::create(&path).unwrap(); + file.write_all($contents.as_bytes()).unwrap(); + }}; +} diff --git a/src/macros/generate_toml_macros.rs b/src/macros/generate_toml_macros.rs deleted file mode 100644 index 781ccc9..0000000 --- a/src/macros/generate_toml_macros.rs +++ /dev/null @@ -1,148 +0,0 @@ -#[macro_export] -/// Macro to generate a TOML string representation of a project. -/// -/// # Arguments -/// -/// * `$project` - The project name. -/// * `$api_key` - The API key for the project. -/// * `$private_key` - The private key for the project. -/// -/// # Returns -/// -/// A string representation of the project in TOML format. -/// -/// # Example -/// -/// ```rust -/// let project_toml: String = generate_project_toml!("MyProject", "api_key", "private_key"); -/// println!("{}", project_toml); -/// ``` -macro_rules! generate_project_toml { - ($project:expr, $api_key:expr, $private_key:expr) => {{ - let mut project_map = ::std::collections::HashMap::new(); - project_map.insert( - $project.to_string(), - $crate::models::toml_model::Project { - api_key: $api_key.to_string(), - private_key: $private_key.to_string(), - }, - ); - ::toml::to_string_pretty(&project_map).unwrap() - }}; -} - -#[macro_export] -/// Generates a TOML string representation of a workspace with the given parameters. -/// -/// # Arguments -/// -/// * `$wrkspc` - The workspace name. -/// * `$prjct` - An optional project name. -/// * `$api_key_input` - The API key input. -/// * `$private_key_input` - The private key input. -/// -/// # Returns -/// -/// A TOML string representation of the workspace. -/// -/// # Example -/// -/// ## Workspace with a project -/// -/// ```rust -/// let workspace_toml: String = generate_workspace_toml!("MyWorkspace", Some("MyProject").to_string(), "api_key", "private_key"); -/// println!("{}", workspace_toml); -/// ``` -/// -/// ## Workspace without a project -/// -/// ```rust -/// let workspace_toml: String = generate_workspace_toml!("MyWorkspace", None, "", ""); -/// println!("{}", workspace_toml); -/// ``` -macro_rules! generate_workspace_toml { - ($wrkspc:expr, $prjct:expr, $api_key_input:expr, $private_key_input:expr) => {{ - let mut workspace_map = ::std::collections::HashMap::new(); - workspace_map.insert( - $wrkspc.to_string(), - $crate::models::toml_model::Workspace { - projects: match $prjct { - Some(prjct) => { - let mut project_map = ::std::collections::HashMap::new(); - project_map.insert( - prjct, - $crate::models::toml_model::Project { - api_key: $api_key_input.to_string(), - private_key: $private_key_input.to_string(), - }, - ); - Some(project_map) - } - None => None, - }, - }, - ); - ::toml::to_string_pretty(&workspace_map).unwrap() - }}; -} - -#[macro_export] -/// Generates a TOML string representation of a configuration with the given parameters. -/// -/// # Arguments -/// -/// * `$wrkspc` - The workspace name. -/// * `$prjct` - An optional project name. -/// * `$api_key_input` - The API key input. -/// * `$private_key_input` - The private key input. -/// -/// # Returns -/// -/// A TOML string representation of the configuration. -/// -/// # Example -/// -/// ## Configuration with a project -/// -/// ```rust -/// let configuration_toml: String = generate_config_toml!("MyWorkspace", Some("MyProject").to_string(), "api_key", "private_key"); -/// println!("{}", configuration_toml); -/// ``` -/// -/// ## Configuration without a project -/// -/// ```rust -/// let configuration_toml: String = generate_config_toml!("MyWorkspace", None, "", ""); -/// println!("{}", configuration_toml); -/// ``` -macro_rules! generate_config_toml { - ($wrkspc:expr, $prjct:expr, $api_key_input:expr, $private_key_input:expr) => {{ - let mut workspace_map = ::std::collections::HashMap::new(); - workspace_map.insert( - $wrkspc.to_string(), - $crate::models::toml_model::Workspace { - projects: match $prjct { - Some(prjct) => { - let mut project_map = ::std::collections::HashMap::new(); - project_map.insert( - prjct, - $crate::models::toml_model::Project { - api_key: $api_key_input.to_string(), - private_key: $private_key_input.to_string(), - }, - ); - Some(project_map) - } - None => None, - }, - }, - ); - - let config = $crate::models::toml_model::Configure { - base_url: $crate::constants::BASE_URL.to_string(), - workspaces: workspace_map, - }; - - ::toml::to_string_pretty(&config).unwrap() - }}; -} diff --git a/src/macros/mod.rs b/src/macros/mod.rs index e7a8824..df0cddc 100644 --- a/src/macros/mod.rs +++ b/src/macros/mod.rs @@ -1,2 +1,3 @@ -pub mod command_line_macros; -pub mod generate_toml_macros; \ No newline at end of file +pub mod command_line; +pub mod file_io; +pub mod toml; diff --git a/src/macros/toml.rs b/src/macros/toml.rs new file mode 100644 index 0000000..332d354 --- /dev/null +++ b/src/macros/toml.rs @@ -0,0 +1,35 @@ +#[macro_export] +macro_rules! get_os_specific_user_root_config_path { + ($project: expr) => {{ + if cfg!(windows) { + format!( + "C:\\Users\\{}\\.keyshade\\{}.toml", + ::whoami::username(), + $project + ) + } else { + format!("/home/{}/.keyshade/{}.toml", ::whoami::username(), $project) + } + }}; +} + +#[macro_export] +macro_rules! generate_user_root_toml { + ($api_key: expr, $private_key: expr, $project: expr) => {{ + let mut user_root_map = ::std::collections::HashMap::new(); + user_root_map.insert("api_key".to_string(), $api_key); + user_root_map.insert("private_key".to_string(), $private_key); + ::toml::to_string_pretty(&user_root_map).unwrap() + }}; +} + +#[macro_export] +macro_rules! generate_project_toml { + ($workspace: expr, $project: expr, $environment: expr) => {{ + let mut project_map = ::std::collections::HashMap::new(); + project_map.insert("workspace".to_string(), $workspace); + project_map.insert("project".to_string(), $project); + project_map.insert("environment".to_string(), $environment); + ::toml::to_string_pretty(&project_map).unwrap() + }}; +} diff --git a/src/main.rs b/src/main.rs index be0af14..90eab45 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,15 +2,113 @@ mod commands; mod constants; mod macros; mod models; -fn main() { - commands::main(); - // let workspace_str = - // generate_workspace_toml!("my_workspace", None, "", ""); - // println!("{}", workspace_str); - // let project_str = generate_project_toml!("my_project", "api_key", "private_key"); - // println!("{}", project_str); +use std::io; - // let config_str: String = generate_config_toml!("my_workspace", None, "", ""); - // println!("{}", config_str); +use crate::constants::{ABOUT, VERSION}; +use clap::{Arg, ArgAction, ArgMatches, Command}; + +use commands::{configure::ConfigureCommand, run::RunCommand, AbstractCommandInterface}; + +fn cli() -> Command { + Command::new("keyshades-cli") + .alias("ks") + .version(VERSION) + .about(ABOUT) + .max_term_width(100) + .subcommand_required(true) + .arg_required_else_help(true) + .subcommand( + Command::new("configure") + .alias("conf") + .about("Configure the keyshades CLI, alias to `conf`") + .arg( + Arg::new("WORKSPACE") + .long("workspace") + .short('w') + .action(ArgAction::Set) + .help("Configure the workspace"), + ) + .arg( + Arg::new("PROJECT") + .long("project") + .short('p') + .action(ArgAction::Set) + .help("Configure the project"), + ) + .arg( + Arg::new("ENVIRONMENT") + .long("environment") + .short('e') + .action(ArgAction::Set) + .help("Configure the environment"), + ) + .arg( + Arg::new("API_KEY") + .long("api-key") + .short('a') + .action(ArgAction::Set) + .help("Configure the API key"), + ) + .arg( + Arg::new("PRIVATE_KEY") + .long("private-key") + .short('k') + .action(ArgAction::Set) + .help("Configure the private key"), + ), + ) + .subcommand( + Command::new("run") + .alias("r") + .about("Run the keyshades CLI, alias to `r`"), + ) +} + +pub enum CommandEnum<'a> { + Configure(ConfigureCommand<'a>), + Run(RunCommand), +} + +impl<'a> AbstractCommandInterface for CommandEnum<'a> { + fn parse_args(&mut self) -> Result<(), io::Error> { + match self { + CommandEnum::Configure(command) => command.parse_args(), + CommandEnum::Run(command) => command.parse_args(), + } + } + + async fn execute(&self) -> Result<(), io::Error> { + match self { + CommandEnum::Configure(command) => command.execute().await, + CommandEnum::Run(command) => command.execute().await, + } + } +} + +#[tokio::main] +async fn main() { + let matches: ArgMatches = cli().get_matches(); + let mut command: Option = None; + + // Get the subcommand implementation based on the user input + match matches.subcommand() { + Some(("configure", args)) => { + command = Some(CommandEnum::Configure(ConfigureCommand::new(args))); + } + Some(("run", _args)) => { + command = Some(CommandEnum::Run(RunCommand::new())); + } + _ => { + println!("Error: No subcommand provided. Usage: ks [SUBCOMMAND] [OPTIONS]"); + } + } + + // Execute the subcommand + if let Some(mut c) = command { + c.parse_args().unwrap(); + let _ = c.execute().await; + } else { + panic!("Error: No subcommand provided. Usage: ks [SUBCOMMAND] [OPTIONS]"); + } } diff --git a/src/models/mod.rs b/src/models/mod.rs index 2057c97..9039f5b 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1 +1 @@ -pub mod toml_model; \ No newline at end of file +pub mod toml_model; diff --git a/src/models/toml_model.rs b/src/models/toml_model.rs index fd7de5a..6943543 100644 --- a/src/models/toml_model.rs +++ b/src/models/toml_model.rs @@ -1,22 +1,14 @@ -use std::collections::HashMap; - use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] -pub struct Project { +pub struct UserRootConfig { pub api_key: String, pub private_key: String, } #[derive(Debug, Serialize, Deserialize)] -pub struct Workspace { - #[serde(flatten)] - pub projects: Option>, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Configure { - pub base_url: String, - #[serde(flatten)] - pub workspaces: HashMap, +pub struct ProjectRootConfig { + pub workspace: String, + pub project: String, + pub environment: String, }