Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 11 additions & 24 deletions src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(
Expand Down Expand Up @@ -230,7 +230,7 @@ pub fn login() {
let entries: Vec<config::WorkspaceEntry> = 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 }
Expand All @@ -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) => {
Expand Down Expand Up @@ -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()
Expand Down
8 changes: 7 additions & 1 deletion src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
},

/// Get details for a workspace
Get {
/// Workspace ID (defaults to first workspace from login)
Expand Down
21 changes: 21 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,27 @@ pub fn save_workspaces(profile: &str, workspaces: Vec<WorkspaceEntry>) -> 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<String>, profile_config: &ProfileConfig) -> Result<String, String> {
if let Some(id) = provided {
return Ok(id);
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 } => {
Expand Down
97 changes: 94 additions & 3 deletions src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,94 @@ struct ListResponse {
workspaces: Vec<Workspace>,
}

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<Workspace> {
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::<ListResponse>() {
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<String> = 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,
Expand All @@ -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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: list could use the new load_client and fetch_all_workspaces helpers introduced in this same PR, which would eliminate the duplicated auth/client setup here (lines 107–138). The only extra piece list needs that load_client doesn't return is default_id from the profile's workspace list — you could either return the full ProfileConfig from load_client, or call config::load("default") once and build the client from that.


let url = format!("{}/workspaces", profile_config.api_url);
let client = reqwest::blocking::Client::new();

Expand All @@ -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);
}

Expand All @@ -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}");
}
Expand Down
Loading