diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f73150f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.words": ["chrono"] +} diff --git a/Cargo.lock b/Cargo.lock index b314bc2..f0b28ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1748,6 +1748,7 @@ version = "1.23.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea5fab0d6c3c01ae70085a09cb03d4c7a1d6314e2b3e075392783396d724ca0a" dependencies = [ + "getrandom 0.4.3", "js-sys", "serde_core", "wasm-bindgen", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 3035f88..a376599 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -20,5 +20,9 @@ colored = "3.1.1" base64 = "0.22.1" chrono = "0.4.45" dotenvy = "0.15" -uuid = "1.23.5" +uuid = { version = "1.23.5", features = ["v4"] } hex = "0.4" + +[[bin]] +name = "harbor" +path = "src/main.rs" diff --git a/crates/cli/harbor.db b/crates/cli/harbor.db index 7552bbd..cb10868 100644 Binary files a/crates/cli/harbor.db and b/crates/cli/harbor.db differ diff --git a/crates/cli/migrations/2026-07-13-221551-0000_create_tables/up.sql b/crates/cli/migrations/2026-07-13-221551-0000_create_tables/up.sql index 52c8d0a..a62df2b 100644 --- a/crates/cli/migrations/2026-07-13-221551-0000_create_tables/up.sql +++ b/crates/cli/migrations/2026-07-13-221551-0000_create_tables/up.sql @@ -1,11 +1,13 @@ CREATE TABLE projects ( - id TEXT PRIMARY KEY, + id TEXT PRIMARY KEY NOT NULL, name TEXT NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + UNIQUE (name) ); CREATE TABLE secrets ( - id TEXT PRIMARY KEY, + id TEXT PRIMARY KEY NOT NULL, name TEXT NOT NULL, project_id TEXT NOT NULL, config TEXT NOT NULL, diff --git a/crates/cli/src/db/models.rs b/crates/cli/src/db/models.rs index acd9e6b..405f55f 100644 --- a/crates/cli/src/db/models.rs +++ b/crates/cli/src/db/models.rs @@ -11,6 +11,13 @@ pub struct Project { pub created_at: NaiveDateTime, } +#[derive(Insertable)] +#[diesel(table_name = projects)] +pub struct NewProject<'a> { + pub id: &'a str, + pub name: &'a str, +} + #[derive(Debug, Queryable, Selectable)] #[diesel(table_name = secrets)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 5e23d93..e4c9a8f 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,10 +1,12 @@ use base64::{Engine, engine::general_purpose::STANDARD}; -use clap::{Args, Parser, Subcommand}; +use clap::{Parser, Subcommand}; use crypto::helper::gen_key; use keyring::Entry; use std::error::Error; +use std::io::{self, Write}; mod db; mod store; + #[derive(Parser)] #[clap( name = "Harbor", @@ -22,7 +24,13 @@ pub struct Cli { #[derive(Subcommand)] pub enum Commands { - Inject(InjectArgs), + Inject { + #[arg(short, long)] + verbose: bool, + + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + after: Vec, + }, Shell {}, Add {}, Setup {}, @@ -37,30 +45,72 @@ pub enum Commands { }, } -#[derive(Args, Debug)] -pub struct InjectArgs { - #[arg(short, long)] - pub verbose: bool, - - #[arg(trailing_var_arg = true, allow_hyphen_values = true)] - pub after: Vec, -} - #[derive(Subcommand)] pub enum ProjectCommands { List {}, - Create { name: String }, + Create {}, Delete { name: String }, } #[derive(Subcommand)] pub enum ConfigCommands { List {}, - Create { project: String, name: String }, + Create { project: String }, Delete { project: String, name: String }, } -fn gen_or_get_key() -> Result> { +pub mod interactions { + use super::db::models::{NewProject, Project}; + use super::db::{ + establish_connection, + schema::projects::dsl::{name as project_name, projects}, + }; + use diesel::result::{DatabaseErrorKind, Error as DieselError}; + use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper}; + use std::error::Error; + use uuid::Uuid; + + fn construct_error(message: &str) -> Result<(), Box> { + Err(Box::new(std::io::Error::new( + std::io::ErrorKind::AlreadyExists, + message.to_string(), + ))) + } + + pub fn create_project(proj_name: &str) -> Result<(), Box> { + let mut conn = establish_connection(); + + // 1. Check if the project already exists + let existing_project = projects + .filter(project_name.eq(proj_name)) + .select(Project::as_select()) + .first::(&mut conn) + .optional()?; + + if let Some(_) = existing_project { + return construct_error("Project already exists"); + } + + let project_id = Uuid::new_v4().to_string(); + let new_project = NewProject { + id: &project_id, + name: proj_name, + }; + + match diesel::insert_into(projects) + .values(&new_project) + .execute(&mut conn) + { + Ok(_) => Ok(()), + Err(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _)) => { + construct_error("Project already exists") + } + Err(err) => Err(Box::new(err)), + } + } +} + +pub fn gen_or_get_key() -> Result> { let entry = Entry::new("harbor", "encryption-key")?; match entry.get_password() { @@ -84,3 +134,17 @@ fn gen_or_get_key() -> Result> { Err(err) => Err(Box::new(err)), } } + +pub fn get_input(message: &str, prompt: char, new_line: bool) -> Result> { + let mut input = String::new(); + + if new_line { + print!("{}\n{} ", message, prompt); + } else { + print!("{}{} ", message, prompt); + } + + io::stdout().flush()?; + io::stdin().read_line(&mut input)?; + Ok(input.trim().to_string()) +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 25c8e25..5891477 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -3,6 +3,7 @@ use std::process; use clap::Parser; use clap::crate_version; use cli::Cli; +use cli::get_input; use colored::*; const GIT_VERSION: &str = env!("GIT_VERSION"); @@ -20,12 +21,75 @@ fn main() { process::exit(0); } - if cli.command.is_none() { - println!( - "{}\n{}", - "No command was entered.".red(), - "Exiting...".dimmed() - ); - process::exit(0); + match cli.command { + Some(c) => match c { + cli::Commands::Add {} => { + println!("Add command executed"); + } + cli::Commands::Config { command } => match command { + cli::ConfigCommands::List {} => { + println!("Config list command executed"); + } + cli::ConfigCommands::Create { project } => { + println!("Config create command executed for project: {}", project); + } + cli::ConfigCommands::Delete { project, name } => { + println!( + "Config delete command executed for project: {}, name: {}", + project, name + ); + } + }, + cli::Commands::Inject { verbose, after } => { + println!( + "Inject command executed with verbose: {}, after: {:?}", + verbose, after + ); + // for harbor inject -- bun dev + // after = ["bun", "dev"] + } + cli::Commands::List {} => { + println!("List command executed"); + } + cli::Commands::Project { command } => match command { + cli::ProjectCommands::List {} => { + println!("Project list command executed"); + } + cli::ProjectCommands::Create {} => { + let project_name = match get_input("Project name", ':', false) { + Ok(name) => name, + Err(e) => { + eprintln!("Error getting project name: {}", e); + process::exit(1); + } + }; + + match cli::interactions::create_project(&project_name) { + Ok(()) => println!("{}", "Project created successfully".blue()), + Err(e) => { + eprintln!("Error creating project: {}", e); + process::exit(1); + } + } + } + cli::ProjectCommands::Delete { name } => { + println!("Project delete command executed for name: {}", name); + } + }, + cli::Commands::Shell {} => { + println!("Shell command executed"); + } + cli::Commands::Setup {} => { + println!("Setup command executed"); + } + }, + None => { + println!( + "{}\n{}", + "No command was entered.".red(), + "Use --help for more information.".dimmed() + ); + process::exit(0); + } } }