diff --git a/src/auth.rs b/src/auth.rs index e8c4af1..3c672af 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -26,7 +26,6 @@ pub fn status(profile: &str) { let api_key = match &profile_config.api_key { Some(key) if key != "PLACEHOLDER" => key.clone(), _ => { - print_row("Profile", &profile.white().to_string()); print_row("Authenticated", &"No".red().to_string()); print_row("API Key", &"Not configured".red().to_string()); return; @@ -42,17 +41,18 @@ pub fn status(profile: &str) { .send() { Ok(resp) if resp.status().is_success() => { - print_row("Profile", &profile.white().to_string()); print_row("API URL", &profile_config.api_url.cyan().to_string()); print_row("Authenticated", &"Yes".green().to_string()); print_row("API Key", &format!("{}{source_label}", "Valid".green())); match profile_config.workspaces.first() { - Some(w) => print_row("Default Workspace", &format!("{} ({})", w.public_id, w.name).cyan().to_string()), - None => print_row("Default Workspace", &"None".dark_grey().to_string()), + Some(w) => { + print_row("Workspace", &format!("{} {}", w.name.as_str().cyan(), format!("({})", w.public_id).dark_grey())); + print_row("", &"use 'hotdata workspaces set' to switch workspaces".dark_grey().to_string()); + } + None => print_row("Current Workspace", &"None".dark_grey().to_string()), } } Ok(resp) => { - print_row("Profile", &profile.white().to_string()); print_row("API URL", &profile_config.api_url.cyan().to_string()); print_row("Authenticated", &"No".red().to_string()); print_row( @@ -230,7 +230,7 @@ pub fn login() { let entries: Vec = ws.workspaces.into_iter() .map(|w| config::WorkspaceEntry { public_id: w.public_id, name: w.name }) .collect(); - let first = entries.first().map(|w| format!("{} ({})", w.public_id, w.name)); + let first = entries.first().cloned(); let _ = config::save_workspaces("default", entries); first } else { None } @@ -246,24 +246,11 @@ pub fn login() { .unwrap(); match default_workspace { - Some(id) => { - stdout() - .execute(SetForegroundColor(Color::DarkGrey)) - .unwrap() - .execute(Print(format!("Default workspace: {id}\n"))) - .unwrap() - .execute(ResetColor) - .unwrap(); - } - None => { - stdout() - .execute(SetForegroundColor(Color::DarkGrey)) - .unwrap() - .execute(Print("No default workspace configured.\n")) - .unwrap() - .execute(ResetColor) - .unwrap(); + Some(w) => { + print_row("Workspace", &format!("{} {}", w.name.as_str().cyan(), format!("({})", w.public_id).dark_grey())); + print_row("", &"use 'hotdata workspaces set' to switch workspaces".dark_grey().to_string()); } + None => print_row("Workspace", &"None".dark_grey().to_string()), } } Ok(r) => { @@ -309,7 +296,7 @@ fn print_row(label: &str, value: &str) { stdout() .execute(SetForegroundColor(Color::DarkGrey)) .unwrap() - .execute(Print(format!("{:<20}", format!("{label}:")))) + .execute(Print(format!("{:<16}", if label.is_empty() { String::new() } else { format!("{label}:") }))) .unwrap() .execute(ResetColor) .unwrap() diff --git a/src/command.rs b/src/command.rs index b61b212..2c24684 100644 --- a/src/command.rs +++ b/src/command.rs @@ -242,10 +242,16 @@ pub enum WorkspaceCommands { /// List all workspaces List { /// Output format - #[arg(long, default_value = "yaml", value_parser = ["table", "json", "yaml"])] + #[arg(long, default_value = "table", value_parser = ["table", "json", "yaml"])] format: String, }, + /// Set the default workspace + Set { + /// Workspace ID to set as default (omit for interactive selection) + workspace_id: Option, + }, + /// Get details for a workspace Get { /// Workspace ID (defaults to first workspace from login) diff --git a/src/config.rs b/src/config.rs index 32a0a7a..f7bcba7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -150,6 +150,27 @@ pub fn save_workspaces(profile: &str, workspaces: Vec) -> Result fs::write(&config_path, content).map_err(|e| format!("error writing config file: {e}")) } +pub fn save_default_workspace(profile: &str, workspace: WorkspaceEntry) -> Result<(), String> { + let user_dirs = UserDirs::new().ok_or("could not determine home directory")?; + let config_path = user_dirs.home_dir().join(".hotdata").join("config.yml"); + + let mut config_file: ConfigFile = if config_path.exists() { + let content = fs::read_to_string(&config_path) + .map_err(|e| format!("error reading config file: {e}"))?; + serde_yaml::from_str(&content).map_err(|e| format!("error parsing config file: {e}"))? + } else { + ConfigFile { profiles: HashMap::new() } + }; + + let entry = config_file.profiles.entry(profile.to_string()).or_default(); + entry.workspaces.retain(|w| w.public_id != workspace.public_id); + entry.workspaces.insert(0, workspace); + + let content = serde_yaml::to_string(&config_file) + .map_err(|e| format!("error serializing config: {e}"))?; + fs::write(&config_path, content).map_err(|e| format!("error writing config file: {e}")) +} + pub fn resolve_workspace_id(provided: Option, profile_config: &ProfileConfig) -> Result { if let Some(id) = provided { return Ok(id); diff --git a/src/main.rs b/src/main.rs index bebd1bb..4379d87 100644 --- a/src/main.rs +++ b/src/main.rs @@ -95,6 +95,7 @@ fn main() { Commands::Profile { .. } => eprintln!("not yet implemented"), Commands::Workspaces { command } => match command { WorkspaceCommands::List { format } => workspace::list(&format), + WorkspaceCommands::Set { workspace_id } => workspace::set(workspace_id.as_deref()), _ => eprintln!("not yet implemented"), }, Commands::Connections { workspace_id, command } => { diff --git a/src/workspace.rs b/src/workspace.rs index e2b77a6..e7b31cb 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -15,6 +15,94 @@ struct ListResponse { workspaces: Vec, } +fn load_client() -> (reqwest::blocking::Client, String, String) { + let profile_config = match config::load("default") { + Ok(c) => c, + Err(e) => { + eprintln!("{e}"); + std::process::exit(1); + } + }; + let api_key = match &profile_config.api_key { + Some(key) if key != "PLACEHOLDER" => key.clone(), + _ => { + eprintln!("error: not authenticated. Run 'hotdata auth login' to log in."); + std::process::exit(1); + } + }; + let api_url = profile_config.api_url.to_string(); + (reqwest::blocking::Client::new(), api_key, api_url) +} + +fn fetch_all_workspaces(client: &reqwest::blocking::Client, api_key: &str, api_url: &str) -> Vec { + let url = format!("{api_url}/workspaces"); + let resp = match client + .get(&url) + .header("Authorization", format!("Bearer {api_key}")) + .send() + { + Ok(r) => r, + Err(e) => { + eprintln!("error connecting to API: {e}"); + std::process::exit(1); + } + }; + if !resp.status().is_success() { + eprintln!("error: {}", crate::util::api_error(resp.text().unwrap_or_default())); + std::process::exit(1); + } + match resp.json::() { + Ok(b) => b.workspaces, + Err(e) => { + eprintln!("error parsing response: {e}"); + std::process::exit(1); + } + } +} + +pub fn set(workspace_id: Option<&str>) { + let (client, api_key, api_url) = load_client(); + let workspaces = fetch_all_workspaces(&client, &api_key, &api_url); + + let chosen = match workspace_id { + Some(id) => { + match workspaces.iter().find(|w| w.public_id == id) { + Some(w) => config::WorkspaceEntry { public_id: w.public_id.clone(), name: w.name.clone() }, + None => { + eprintln!("error: workspace '{id}' not found or you don't have access to it."); + std::process::exit(1); + } + } + } + None => { + if workspaces.is_empty() { + eprintln!("error: no workspaces available."); + std::process::exit(1); + } + let options: Vec = workspaces.iter() + .map(|w| format!("{} ({})", w.name, w.public_id)) + .collect(); + let selection = match inquire::Select::new("Select default workspace:", options.clone()).prompt() { + Ok(s) => s, + Err(_) => std::process::exit(1), + }; + let idx = options.iter().position(|o| o == &selection).unwrap(); + let w = &workspaces[idx]; + config::WorkspaceEntry { public_id: w.public_id.clone(), name: w.name.clone() } + } + }; + + if let Err(e) = config::save_default_workspace("default", chosen.clone()) { + eprintln!("error saving config: {e}"); + std::process::exit(1); + } + + use crossterm::style::Stylize; + println!("{}", "Default workspace updated".green()); + println!("id: {}", chosen.public_id); + println!("name: {}", chosen.name); +} + pub fn list(format: &str) { let profile_config = match config::load("default") { Ok(c) => c, @@ -32,6 +120,8 @@ pub fn list(format: &str) { } }; + let default_id = profile_config.workspaces.first().map(|w| w.public_id.as_str()).unwrap_or("").to_string(); + let url = format!("{}/workspaces", profile_config.api_url); let client = reqwest::blocking::Client::new(); @@ -48,7 +138,7 @@ pub fn list(format: &str) { }; if !resp.status().is_success() { - eprintln!("error: HTTP {}", resp.status()); + eprintln!("error: {}", crate::util::api_error(resp.text().unwrap_or_default())); std::process::exit(1); } @@ -69,9 +159,10 @@ pub fn list(format: &str) { } "table" => { let mut table = crate::util::make_table(); - table.set_header(["PUBLIC_ID", "NAME", "ACTIVE", "FAVORITE", "PROVISION_STATUS"]); + table.set_header(["DEFAULT", "PUBLIC_ID", "NAME", "PROVISION_STATUS"]); for w in &body.workspaces { - table.add_row([&w.public_id, &w.name, &w.active.to_string(), &w.favorite.to_string(), &w.provision_status]); + let marker = if w.public_id == default_id { "*" } else { "" }; + table.add_row([marker, &w.public_id, &w.name, &w.provision_status]); } println!("{table}"); }