diff --git a/GETTING-STARTED.md b/GETTING-STARTED.md index 635147be..8d2f7805 100644 --- a/GETTING-STARTED.md +++ b/GETTING-STARTED.md @@ -1,6 +1,6 @@ # Getting Started with Stakpak CLI -**The most secure agent built for operations & DevOps.** Stakpak CLI is a powerful, security-hardened tool designed for the grittiest parts of software development with enterprise-grade security features. +**The most secure agent built for operations & DevOps** Stakpak CLI is a powerful, security-hardened tool designed for the grittiest parts of software development with enterprise-grade security features. ## 🚀 Quick Start diff --git a/cli/src/commands/auto_update.rs b/cli/src/commands/auto_update.rs index eb4b8575..dcb63776 100644 --- a/cli/src/commands/auto_update.rs +++ b/cli/src/commands/auto_update.rs @@ -257,6 +257,9 @@ async fn update_binary_atomic(os: &str, arch: &str, version: Option) -> base_url: base_url.to_string(), targets: vec![target.to_string()], version: Some(version.clone()), + repo: Some("agent".to_string()), + owner: Some("stakpak".to_string()), + version_arg: None, }; // 3. Get current executable path diff --git a/cli/src/commands/board.rs b/cli/src/commands/board.rs index c7a7d27e..e6a7a1c2 100644 --- a/cli/src/commands/board.rs +++ b/cli/src/commands/board.rs @@ -1,253 +1,30 @@ -use std::process::{Command, Stdio}; +use crate::utils::plugins::{PluginConfig, execute_plugin_command, get_plugin_path}; +use std::process::Command; + +fn get_board_plugin_config() -> PluginConfig { + PluginConfig { + name: "agent-board".to_string(), + base_url: "https://github.com/stakpak/agent-board".to_string(), + targets: vec![ + "linux-x86_64".to_string(), + "windows-x86_64".to_string(), + "darwin-x86_64".to_string(), + "darwin-aarch64".to_string(), + ], + version: None, + repo: Some("agent-board".to_string()), + owner: Some("stakpak".to_string()), + version_arg: None, + } +} /// Pass-through to agent-board plugin. All args after 'board' are forwarded directly. /// Run `stakpak board --help` for available commands. pub async fn run_board(args: Vec) -> Result<(), String> { - let board_path = get_board_plugin_path().await; + let config = get_board_plugin_config(); + let board_path = get_plugin_path(config).await; + let mut cmd = Command::new(board_path); cmd.args(&args); - execute_board_command(cmd) -} - -async fn get_board_plugin_path() -> String { - // Check if we have an existing installation first - let existing = get_existing_board_path().ok(); - let current_version = existing - .as_ref() - .and_then(|path| get_board_version(path).ok()); - - // If we have an existing installation, check if update needed - if let Some(ref path) = existing { - // Try to get latest version from GitHub API - match get_latest_github_release_version().await { - Ok(target_version) => { - if let Some(ref current) = current_version { - if is_version_match(current, &target_version) { - // Already up to date, use existing - return path.clone(); - } - println!( - "agent-board {} is outdated (target: {}), updating...", - current, target_version - ); - } - // Need to update - download new version - match download_board_plugin().await { - Ok(new_path) => { - println!( - "Successfully installed agent-board {} -> {}", - target_version, new_path - ); - return new_path; - } - Err(e) => { - eprintln!("Failed to update agent-board: {}", e); - eprintln!("Using existing version"); - return path.clone(); - } - } - } - Err(_) => { - // Can't check version, use existing installation - return path.clone(); - } - } - } - - // No existing installation - must download - match get_latest_github_release_version().await { - Ok(target_version) => match download_board_plugin().await { - Ok(path) => { - println!( - "Successfully installed agent-board {} -> {}", - target_version, path - ); - path - } - Err(e) => { - eprintln!("Failed to download agent-board: {}", e); - "agent-board".to_string() - } - }, - Err(e) => { - // Try download anyway (uses /latest/ URL) - eprintln!("Warning: Failed to check version: {}", e); - match download_board_plugin().await { - Ok(path) => { - println!("Successfully installed agent-board -> {}", path); - path - } - Err(e) => { - eprintln!("Failed to download agent-board: {}", e); - "agent-board".to_string() - } - } - } - } -} - -async fn get_latest_github_release_version() -> Result { - use stakpak_shared::tls_client::{TlsClientConfig, create_tls_client}; - - let client = create_tls_client(TlsClientConfig::default())?; - let response = client - .get("https://api.github.com/repos/stakpak/agent-board/releases/latest") - .header("User-Agent", "stakpak-cli") - .header("Accept", "application/vnd.github.v3+json") - .send() - .await - .map_err(|e| format!("Failed to fetch latest release: {}", e))?; - - if !response.status().is_success() { - return Err(format!("GitHub API returned: {}", response.status())); - } - - let json: serde_json::Value = response - .json() - .await - .map_err(|e| format!("Failed to parse GitHub response: {}", e))?; - - json["tag_name"] - .as_str() - .map(|s| s.to_string()) - .ok_or_else(|| "No tag_name in release".to_string()) -} - -fn get_existing_board_path() -> Result { - let home_dir = - std::env::var("HOME").map_err(|_| "HOME environment variable not set".to_string())?; - - let plugin_path = std::path::PathBuf::from(&home_dir) - .join(".stakpak") - .join("plugins") - .join("agent-board"); - - if plugin_path.exists() { - Ok(plugin_path.to_string_lossy().to_string()) - } else { - Err("agent-board not found in plugins directory".to_string()) - } -} - -fn get_board_version(path: &str) -> Result { - let output = std::process::Command::new(path) - .arg("version") - .output() - .map_err(|e| format!("Failed to run agent-board version: {}", e))?; - - if !output.status.success() { - return Err("agent-board version command failed".to_string()); - } - - let version_output = String::from_utf8_lossy(&output.stdout); - // Parse version from output like "agent-board v0.1.6" or just "v0.1.6" - let trimmed = version_output.trim(); - if let Some(v) = trimmed.split_whitespace().find(|s| { - s.starts_with('v') - || s.chars() - .next() - .map(|c| c.is_ascii_digit()) - .unwrap_or(false) - }) { - Ok(v.to_string()) - } else { - Ok(trimmed.to_string()) - } -} - -fn is_version_match(current: &str, target: &str) -> bool { - let current_clean = current.strip_prefix('v').unwrap_or(current); - let target_clean = target.strip_prefix('v').unwrap_or(target); - current_clean == target_clean -} - -async fn download_board_plugin() -> Result { - use flate2::read::GzDecoder; - use stakpak_shared::tls_client::{TlsClientConfig, create_tls_client}; - use std::io::Cursor; - use tar::Archive; - - let home_dir = - std::env::var("HOME").map_err(|_| "HOME environment variable not set".to_string())?; - - let plugins_dir = std::path::PathBuf::from(&home_dir) - .join(".stakpak") - .join("plugins"); - - std::fs::create_dir_all(&plugins_dir) - .map_err(|e| format!("Failed to create plugins directory: {}", e))?; - - // Determine platform - let os = std::env::consts::OS; - let arch = std::env::consts::ARCH; - - let target = match (os, arch) { - ("linux", "x86_64") => "linux-x86_64", - ("linux", "aarch64") => "linux-aarch64", - ("macos", "x86_64") => "darwin-x86_64", - ("macos", "aarch64") => "darwin-aarch64", - _ => return Err(format!("Unsupported platform: {} {}", os, arch)), - }; - - let download_url = format!( - "https://github.com/stakpak/agent-board/releases/latest/download/agent-board-{}.tar.gz", - target - ); - - println!("Downloading agent-board plugin..."); - - let client = create_tls_client(TlsClientConfig::default())?; - let response = client - .get(&download_url) - .send() - .await - .map_err(|e| format!("Failed to download agent-board: {}", e))?; - - if !response.status().is_success() { - return Err(format!("Download failed: HTTP {}", response.status())); - } - - let archive_bytes = response - .bytes() - .await - .map_err(|e| format!("Failed to read download: {}", e))?; - - // Extract tar.gz - let cursor = Cursor::new(archive_bytes.as_ref()); - let tar = GzDecoder::new(cursor); - let mut archive = Archive::new(tar); - - archive - .unpack(&plugins_dir) - .map_err(|e| format!("Failed to extract archive: {}", e))?; - - let plugin_path = plugins_dir.join("agent-board"); - - // Make executable on Unix - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mut permissions = std::fs::metadata(&plugin_path) - .map_err(|e| format!("Failed to get file metadata: {}", e))? - .permissions(); - permissions.set_mode(0o755); - std::fs::set_permissions(&plugin_path, permissions) - .map_err(|e| format!("Failed to set executable permissions: {}", e))?; - } - - Ok(plugin_path.to_string_lossy().to_string()) -} - -fn execute_board_command(mut cmd: Command) -> Result<(), String> { - // Pass through stdio directly - no buffering needed - cmd.stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .stdin(Stdio::inherit()); - - let status = cmd - .status() - .map_err(|e| format!("Failed to run agent-board: {}", e))?; - - // Exit with the same code as the plugin - std::process::exit(status.code().unwrap_or(1)); + execute_plugin_command(cmd, "agent-board".to_string()) } diff --git a/cli/src/commands/browser.rs b/cli/src/commands/browser.rs new file mode 100644 index 00000000..354f8a65 --- /dev/null +++ b/cli/src/commands/browser.rs @@ -0,0 +1,50 @@ +use crate::utils::plugins::{PluginConfig, execute_plugin_command, get_plugin_path}; +use std::process::Command; + +fn get_browser_config() -> PluginConfig { + PluginConfig { + name: "browser".to_string(), + base_url: "https://github.com/stakpak/tab".to_string(), + targets: vec![ + "linux-x86_64".to_string(), + "darwin-x86_64".to_string(), + "darwin-aarch64".to_string(), + "windows-x86_64".to_string(), + ], + version: None, + repo: Some("tab".to_string()), + owner: Some("stakpak".to_string()), + version_arg: Some("version".to_string()), + } +} + +fn get_daemon_config() -> PluginConfig { + PluginConfig { + name: "browser-daemon".to_string(), + base_url: "https://github.com/stakpak/tab".to_string(), + targets: vec![ + "linux-x86_64".to_string(), + "darwin-x86_64".to_string(), + "darwin-aarch64".to_string(), + "windows-x86_64".to_string(), + ], + version: None, + repo: Some("tab".to_string()), + owner: Some("stakpak".to_string()), + version_arg: Some("--version".to_string()), + } +} + +pub async fn run_browser(args: Vec) -> Result<(), String> { + let browser_config = get_browser_config(); + let daemon_config = get_daemon_config(); + + // Ensure daemon is available (downloaded if needed) + get_plugin_path(daemon_config).await; + + // Get browser path and run it + let browser_path = get_plugin_path(browser_config).await; + let mut cmd = Command::new(&browser_path); + cmd.args(&args); + execute_plugin_command(cmd, "browser".to_string()) +} diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index 87793fab..7a335993 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -12,6 +12,7 @@ pub mod agent; pub mod auth; pub mod auto_update; pub mod board; +pub mod browser; pub mod mcp; pub mod warden; @@ -158,11 +159,20 @@ pub enum Commands { }, /// Task board for tracking complex work (cards, checklists, comments) /// Run `stakpak board --help` for available commands. + #[command(disable_help_flag = true)] Board { /// Arguments to pass to the board plugin #[arg(trailing_var_arg = true, allow_hyphen_values = true)] args: Vec, }, + /// Browser automation CLI - control a real browser from the command line + /// Run `stakpak browser --help` for available commands. + #[command(disable_help_flag = true)] + Browser { + /// Arguments to pass to the browser plugin + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, /// Update Stakpak Agent to the latest version Update, } @@ -447,6 +457,9 @@ impl Commands { Commands::Board { args } => { board::run_board(args).await?; } + Commands::Browser { args } => { + browser::run_browser(args).await?; + } Commands::Update => { auto_update::run_auto_update().await?; } diff --git a/cli/src/commands/warden.rs b/cli/src/commands/warden.rs index e292c46d..689012e8 100644 --- a/cli/src/commands/warden.rs +++ b/cli/src/commands/warden.rs @@ -141,6 +141,9 @@ async fn get_warden_plugin_path() -> String { "windows-x86_64".to_string(), ], version: None, + repo: Some("warden".to_string()), + owner: Some("stakpak".to_string()), + version_arg: None, }; get_plugin_path(warden_config).await diff --git a/cli/src/utils/plugins.rs b/cli/src/utils/plugins.rs index 8adb4338..b3e89492 100644 --- a/cli/src/utils/plugins.rs +++ b/cli/src/utils/plugins.rs @@ -3,7 +3,7 @@ use stakpak_shared::tls_client::{TlsClientConfig, create_tls_client}; use std::fs; use std::io::Cursor; use std::path::{Path, PathBuf}; -use std::process::Command; +use std::process::{Command, Stdio}; use tar::Archive; use zip::ZipArchive; @@ -13,6 +13,9 @@ pub struct PluginConfig { pub base_url: String, pub targets: Vec, pub version: Option, + pub repo: Option, + pub owner: Option, + pub version_arg: Option, } /// Get the path to a plugin, downloading it if necessary @@ -22,31 +25,44 @@ pub async fn get_plugin_path(config: PluginConfig) -> String { base_url: config.base_url.trim_end_matches('/').to_string(), // Remove trailing slash targets: config.targets, version: config.version, + repo: config.repo, + owner: config.owner, + version_arg: config.version_arg, }; - // Get the target version from the server + // Get the target version from the server or GitHub let target_version = match config.version.clone() { Some(version) => version, - None => match get_latest_version(&config).await { - Ok(version) => version, - Err(e) => { - eprintln!( - "Warning: Failed to check latest version for {}: {}", - config.name, e - ); - // Continue with existing logic if version check fails - return get_plugin_path_without_version_check(&config).await; + None => { + let latest = if let (Some(owner), Some(repo)) = (&config.owner, &config.repo) { + get_latest_github_release_version(owner, repo).await + } else { + get_latest_version(&config).await + }; + + match latest { + Ok(version) => version, + Err(e) => { + eprintln!( + "Warning: Failed to check latest version for {}: {}", + config.name, e + ); + // Continue with existing logic if version check fails + return get_plugin_path_without_version_check(&config).await; + } } - }, + } }; // First check if plugin is available in PATH - if let Ok(system_version) = get_version_from_command(&config.name, &config.name) { + if let Ok(system_version) = + get_version_from_command(&config.name, &config.name, config.version_arg.as_deref()) + { if is_same_version(&system_version, &target_version) { return config.name.clone(); } else { println!( - "{} v{} is outdated (target: v{}), checking plugins directory...", + "{} {} is outdated (target: {}), checking plugins directory...", config.name, system_version, target_version ); } @@ -54,13 +70,14 @@ pub async fn get_plugin_path(config: PluginConfig) -> String { // Check if plugin already exists in plugins directory if let Ok(existing_path) = get_existing_plugin_path(&config.name) - && let Ok(current_version) = get_version_from_command(&existing_path, &config.name) + && let Ok(current_version) = + get_version_from_command(&existing_path, &config.name, config.version_arg.as_deref()) { if is_same_version(¤t_version, &target_version) { return existing_path; } else { println!( - "{} {} is outdated (target: v{}), updating...", + "{} {} is outdated (target: {}), updating...", config.name, current_version, target_version ); } @@ -70,7 +87,7 @@ pub async fn get_plugin_path(config: PluginConfig) -> String { match download_and_install_plugin(&config).await { Ok(path) => { println!( - "Successfully installed {} v{} -> {}", + "Successfully installed {} {} -> {}", config.name, target_version, path ); path @@ -115,14 +132,19 @@ async fn get_plugin_path_without_version_check(config: &PluginConfig) -> String } /// Get version by running a command (can be plugin name or path) -fn get_version_from_command(command: &str, display_name: &str) -> Result { +fn get_version_from_command( + command: &str, + display_name: &str, + version_arg: Option<&str>, +) -> Result { + let arg = version_arg.unwrap_or("version"); let output = Command::new(command) - .arg("version") + .arg(arg) .output() - .map_err(|e| format!("Failed to run {} version command: {}", display_name, e))?; + .map_err(|e| format!("Failed to run {} {} command: {}", display_name, arg, e))?; if !output.status.success() { - return Err(format!("{} version command failed", display_name)); + return Err(format!("{} {} command failed", display_name, arg)); } let version_output = String::from_utf8_lossy(&output.stdout); @@ -133,19 +155,34 @@ fn get_version_from_command(command: &str, display_name: &str) -> Result = full_output.split_whitespace().collect(); - if parts.len() >= 2 { - Ok(parts[1].to_string()) - } else { - // Fallback to full output if parsing fails - Ok(full_output.to_string()) - } + // Split by whitespace and find the part that looks like a version + let version = full_output + .split_whitespace() + .find(|s| { + s.starts_with('v') + || s.chars() + .next() + .map(|c| c.is_ascii_digit()) + .unwrap_or(false) + }) + .map(|s| s.to_string()) + .or_else(|| { + // Fallback to the second part if none start with 'v' or digit + let parts: Vec<&str> = full_output.split_whitespace().collect(); + if parts.len() >= 2 { + Some(parts[1].to_string()) + } else { + None + } + }) + .unwrap_or_else(|| full_output.to_string()); + + Ok(version) } /// Check if a plugin is available in the system PATH pub fn is_plugin_available(plugin_name: &str) -> bool { - get_version_from_command(plugin_name, plugin_name).is_ok() + get_version_from_command(plugin_name, plugin_name, None).is_ok() } /// Fetch the latest version from the remote server @@ -176,21 +213,45 @@ async fn get_latest_version(config: &PluginConfig) -> Result { Ok(version_text.trim().to_string()) } +/// Fetch the latest version from GitHub releases +pub async fn get_latest_github_release_version(owner: &str, repo: &str) -> Result { + let client = create_tls_client(TlsClientConfig::default())?; + let url = format!("https://api.github.com/repos/{owner}/{repo}/releases/latest"); + + let response = client + .get(url) + .header("User-Agent", "stakpak-cli") + .header("Accept", "application/vnd.github.v3+json") + .send() + .await + .map_err(|e| format!("Failed to fetch latest release version: {}", e))?; + + if !response.status().is_success() { + return Err(format!("GitHub API returned: {}", response.status())); + } + + let json: serde_json::Value = response + .json() + .await + .map_err(|e| format!("Failed to parse GitHub response: {}", e))?; + + json["tag_name"] + .as_str() + .map(|s| s.to_string()) + .ok_or_else(|| "No tag_name in release".to_string()) +} + /// Compare two version strings -fn is_same_version(current: &str, latest: &str) -> bool { +pub fn is_same_version(current: &str, latest: &str) -> bool { let current_clean = current.strip_prefix('v').unwrap_or(current); let latest_clean = latest.strip_prefix('v').unwrap_or(latest); current_clean == latest_clean } -/// Check if plugin binary already exists in plugins directory and get its version -fn get_existing_plugin_path(plugin_name: &str) -> Result { - let home_dir = - std::env::var("HOME").map_err(|_| "HOME environment variable not set".to_string())?; - - let stakpak_dir = PathBuf::from(&home_dir).join(".stakpak"); - let plugins_dir = stakpak_dir.join("plugins"); +/// Check if plugin binary already exists in plugins directory +pub fn get_existing_plugin_path(plugin_name: &str) -> Result { + let plugins_dir = get_plugins_dir()?; // Determine the expected binary name based on OS let binary_name = if cfg!(windows) { @@ -212,12 +273,8 @@ fn get_existing_plugin_path(plugin_name: &str) -> Result { } /// Download and install plugin binary to ~/.stakpak/plugins -async fn download_and_install_plugin(config: &PluginConfig) -> Result { - let home_dir = - std::env::var("HOME").map_err(|_| "HOME environment variable not set".to_string())?; - - let stakpak_dir = PathBuf::from(&home_dir).join(".stakpak"); - let plugins_dir = stakpak_dir.join("plugins"); +pub async fn download_and_install_plugin(config: &PluginConfig) -> Result { + let plugins_dir = get_plugins_dir()?; // Create directories if they don't exist fs::create_dir_all(&plugins_dir) @@ -228,6 +285,7 @@ async fn download_and_install_plugin(config: &PluginConfig) -> Result Result Result<(String, String, bool), String> { - let os = std::env::consts::OS; - let arch = std::env::consts::ARCH; + let (platform, arch) = get_platform_suffix()?; // linux x86_64 // Determine the current platform target - let current_target = match (os, arch) { - ("linux", "x86_64") => "linux-x86_64", - ("macos", "x86_64") => "darwin-x86_64", - ("macos", "aarch64") => "darwin-aarch64", - ("windows", "x86_64") => "windows-x86_64", - _ => return Err(format!("Unsupported platform: {} {}", os, arch)), - }; + let current_target = format!("{}-{}", platform, arch); // linux-x86_64 // Check if this target is supported by the plugin if !config.targets.contains(¤t_target.to_string()) { @@ -302,16 +353,29 @@ pub fn get_download_info(config: &PluginConfig) -> Result<(String, String, bool) (config.name.clone(), false) }; - // Construct download URL let extension = if is_zip { "zip" } else { "tar.gz" }; - let download_url = format!( - "{}/{}/{}-{}.{}", - config.base_url, - config.version.clone().unwrap_or("latest".to_string()), - config.name, - current_target, - extension - ); + + let download_url = if config.base_url.contains("github.com") { + match &config.version { + Some(version) => format!( + "{}/releases/download/{}/{}-{}.{}", + config.base_url, version, config.name, current_target, extension + ), + None => format!( + "{}/releases/latest/download/{}-{}.{}", + config.base_url, config.name, current_target, extension + ), + } + } else { + format!( + "{}/{}/{}-{}.{}", + config.base_url, + config.version.clone().unwrap_or("latest".to_string()), + config.name, + current_target, + extension + ) + }; Ok((download_url, binary_name, is_zip)) } @@ -396,3 +460,50 @@ pub fn extract_zip(archive_bytes: &[u8], dest_dir: &Path) -> Result<(), String> Ok(()) } + +pub fn get_home_dir() -> Result { + std::env::var("HOME") + .or_else(|_| std::env::var("USERPROFILE")) + .map_err(|_| "HOME/USERPROFILE environment variable not set".to_string()) +} + +pub fn get_plugins_dir() -> Result { + let home_dir = get_home_dir()?; + Ok(PathBuf::from(&home_dir).join(".stakpak").join("plugins")) +} + +pub fn get_platform_suffix() -> Result<(&'static str, &'static str), String> { + let platform = match std::env::consts::OS { + "linux" => "linux", + "macos" => "darwin", + "windows" => "windows", + os => return Err(format!("Unsupported OS: {}", os)), + }; + + let arch = match std::env::consts::ARCH { + "x86_64" => "x86_64", + "aarch64" => "aarch64", + arch => return Err(format!("Unsupported architecture: {}", arch)), + }; + + Ok((platform, arch)) +} + +pub fn execute_plugin_command(mut cmd: Command, plugin_name: String) -> Result<(), String> { + cmd.stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .stdin(Stdio::inherit()); + + let status = cmd + .status() + .map_err(|e| format!("Failed to execute {} command: {}", plugin_name, e))?; + + if !status.success() { + return Err(format!( + "{} command failed with status: {}", + plugin_name, status + )); + } + + std::process::exit(status.code().unwrap_or(1)); +} diff --git a/libs/api/src/local/hooks/task_board_context/system_prompt.txt b/libs/api/src/local/hooks/task_board_context/system_prompt.txt index 30a76ba5..cf620340 100644 --- a/libs/api/src/local/hooks/task_board_context/system_prompt.txt +++ b/libs/api/src/local/hooks/task_board_context/system_prompt.txt @@ -250,6 +250,10 @@ User: "Create an eks cluster module in terraform" 5. Run Trivy or Terrascan or Checkov if available (in parallel) 6. Make security recommendations to the user and apply if the user approves +# Browser Usage: `stakapk browser` + +When performing any browser-based actions, use stakpak browser for all navigation, interaction, and scraping. Always consult --help before using stakpak browser. + # Task Management: `stakpak board` diff --git a/libs/api/src/stakpak/models.rs b/libs/api/src/stakpak/models.rs index 9eabfd51..ef091ca9 100644 --- a/libs/api/src/stakpak/models.rs +++ b/libs/api/src/stakpak/models.rs @@ -14,31 +14,23 @@ use uuid::Uuid; /// Session visibility #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "UPPERCASE")] +#[derive(Default)] pub enum SessionVisibility { + #[default] Private, Public, } -impl Default for SessionVisibility { - fn default() -> Self { - Self::Private - } -} - /// Session status #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "UPPERCASE")] +#[derive(Default)] pub enum SessionStatus { + #[default] Active, Deleted, } -impl Default for SessionStatus { - fn default() -> Self { - Self::Active - } -} - /// Full session with active checkpoint #[derive(Debug, Clone, Deserialize)] pub struct Session {