From c31d477a9040eb5946a53d0ef71993672f7b50f4 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Sun, 22 Feb 2026 16:03:05 -0800 Subject: [PATCH] cli: share HTTP helpers and improve schema guidance --- .github/workflows/release-cli-npm.yml | 20 +------- crates-cli/yaak-cli/src/cli.rs | 2 +- crates-cli/yaak-cli/src/commands/auth.rs | 36 ++------------ crates-cli/yaak-cli/src/commands/plugin.rs | 34 ++------------ crates-cli/yaak-cli/src/commands/request.rs | 33 +++++++++++++ crates-cli/yaak-cli/src/main.rs | 1 + crates-cli/yaak-cli/src/utils/http.rs | 47 +++++++++++++++++++ crates-cli/yaak-cli/src/utils/mod.rs | 1 + crates-cli/yaak-cli/src/version.rs | 3 ++ crates-cli/yaak-cli/tests/request_commands.rs | 4 +- 10 files changed, 97 insertions(+), 84 deletions(-) create mode 100644 crates-cli/yaak-cli/src/utils/http.rs create mode 100644 crates-cli/yaak-cli/src/version.rs diff --git a/.github/workflows/release-cli-npm.yml b/.github/workflows/release-cli-npm.yml index 3d5c9161..e0413584 100644 --- a/.github/workflows/release-cli-npm.yml +++ b/.github/workflows/release-cli-npm.yml @@ -122,25 +122,7 @@ jobs: fi VERSION="${VERSION#v}" echo "Building yaak version: $VERSION" - python - "$VERSION" <<'PY' - import pathlib - import re - import sys - - version = sys.argv[1] - manifest = pathlib.Path("crates-cli/yaak-cli/Cargo.toml") - contents = manifest.read_text() - updated, replacements = re.subn( - r'(?m)^version = ".*"$', - f'version = "{version}"', - contents, - count=1, - ) - if replacements != 1: - raise SystemExit("Failed to update yaak-cli version in Cargo.toml") - manifest.write_text(updated) - print(f"Updated {manifest} to version {version}") - PY + echo "YAAK_CLI_VERSION=$VERSION" >> "$GITHUB_ENV" - name: Build yaak run: cargo build --locked --release -p yaak-cli --bin yaak --target ${{ matrix.target }} diff --git a/crates-cli/yaak-cli/src/cli.rs b/crates-cli/yaak-cli/src/cli.rs index b320e66a..aac78599 100644 --- a/crates-cli/yaak-cli/src/cli.rs +++ b/crates-cli/yaak-cli/src/cli.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; #[derive(Parser)] #[command(name = "yaak")] #[command(about = "Yaak CLI - API client from the command line")] -#[command(version)] +#[command(version = crate::version::cli_version())] pub struct Cli { /// Use a custom data directory #[arg(long, global = true)] diff --git a/crates-cli/yaak-cli/src/commands/auth.rs b/crates-cli/yaak-cli/src/commands/auth.rs index 50259397..058014e1 100644 --- a/crates-cli/yaak-cli/src/commands/auth.rs +++ b/crates-cli/yaak-cli/src/commands/auth.rs @@ -1,5 +1,6 @@ use crate::cli::{AuthArgs, AuthCommands}; use crate::ui; +use crate::utils::http; use base64::Engine as _; use keyring::Entry; use rand::RngCore; @@ -136,10 +137,8 @@ async fn whoami() -> CommandResult { }; let url = format!("{}/api/v1/whoami", environment.api_base_url()); - let response = reqwest::Client::new() + let response = http::build_client(Some(&token))? .get(url) - .header("X-Yaak-Session", token) - .header(reqwest::header::USER_AGENT, user_agent()) .send() .await .map_err(|e| format!("Failed to call whoami endpoint: {e}"))?; @@ -156,7 +155,7 @@ async fn whoami() -> CommandResult { .to_string(), ); } - return Err(parse_api_error(status.as_u16(), &body)); + return Err(http::parse_api_error(status.as_u16(), &body)); } println!("{body}"); @@ -342,9 +341,8 @@ async fn write_redirect(stream: &mut TcpStream, location: &str) -> std::io::Resu } async fn exchange_access_token(oauth: &OAuthFlow, code: &str) -> CommandResult { - let response = reqwest::Client::new() + let response = http::build_client(None)? .post(&oauth.token_url) - .header(reqwest::header::USER_AGENT, user_agent()) .form(&[ ("grant_type", "authorization_code"), ("client_id", OAUTH_CLIENT_ID), @@ -406,38 +404,12 @@ fn delete_auth_token(environment: Environment) -> CommandResult { } } -fn parse_api_error(status: u16, body: &str) -> String { - if let Ok(value) = serde_json::from_str::(body) { - if let Some(message) = value.get("message").and_then(Value::as_str) { - return message.to_string(); - } - if let Some(error) = value.get("error").and_then(Value::as_str) { - return error.to_string(); - } - } - - format!("API error {status}: {body}") -} - fn random_hex(bytes: usize) -> String { let mut data = vec![0_u8; bytes]; OsRng.fill_bytes(&mut data); hex::encode(data) } -fn user_agent() -> String { - format!("YaakCli/{} ({})", env!("CARGO_PKG_VERSION"), ua_platform()) -} - -fn ua_platform() -> &'static str { - match std::env::consts::OS { - "windows" => "Win", - "darwin" => "Mac", - "linux" => "Linux", - _ => "Unknown", - } -} - fn confirm_open_browser() -> CommandResult { if !io::stdin().is_terminal() { return Ok(true); diff --git a/crates-cli/yaak-cli/src/commands/plugin.rs b/crates-cli/yaak-cli/src/commands/plugin.rs index b78d4645..aff6c64e 100644 --- a/crates-cli/yaak-cli/src/commands/plugin.rs +++ b/crates-cli/yaak-cli/src/commands/plugin.rs @@ -1,5 +1,6 @@ use crate::cli::{GenerateArgs, PluginArgs, PluginCommands, PluginPathArg}; use crate::ui; +use crate::utils::http; use keyring::Entry; use rand::Rng; use rolldown::{ @@ -7,7 +8,6 @@ use rolldown::{ WatchOption, Watcher, }; use serde::Deserialize; -use serde_json::Value; use std::collections::HashSet; use std::fs; use std::io::{self, IsTerminal, Read, Write}; @@ -186,10 +186,8 @@ async fn publish(args: PluginPathArg) -> CommandResult { ui::info("Uploading plugin"); let url = format!("{}/api/v1/plugins/publish", environment.api_base_url()); - let response = reqwest::Client::new() + let response = http::build_client(Some(&token))? .post(url) - .header("X-Yaak-Session", token) - .header(reqwest::header::USER_AGENT, user_agent()) .header(reqwest::header::CONTENT_TYPE, "application/zip") .body(archive) .send() @@ -201,7 +199,7 @@ async fn publish(args: PluginPathArg) -> CommandResult { response.text().await.map_err(|e| format!("Failed reading publish response body: {e}"))?; if !status.is_success() { - return Err(parse_api_error(status.as_u16(), &body)); + return Err(http::parse_api_error(status.as_u16(), &body)); } let published: PublishResponse = serde_json::from_str(&body) @@ -389,32 +387,6 @@ fn get_auth_token(environment: Environment) -> CommandResult> { } } -fn parse_api_error(status: u16, body: &str) -> String { - if let Ok(value) = serde_json::from_str::(body) { - if let Some(message) = value.get("message").and_then(Value::as_str) { - return message.to_string(); - } - if let Some(error) = value.get("error").and_then(Value::as_str) { - return error.to_string(); - } - } - - format!("API error {status}: {body}") -} - -fn user_agent() -> String { - format!("YaakCli/{} ({})", env!("CARGO_PKG_VERSION"), ua_platform()) -} - -fn ua_platform() -> &'static str { - match std::env::consts::OS { - "windows" => "Win", - "darwin" => "Mac", - "linux" => "Linux", - _ => "Unknown", - } -} - fn random_name() -> String { const ADJECTIVES: &[&str] = &[ "young", "youthful", "yellow", "yielding", "yappy", "yawning", "yummy", "yucky", "yearly", diff --git a/crates-cli/yaak-cli/src/commands/request.rs b/crates-cli/yaak-cli/src/commands/request.rs index a9f6a713..cae31811 100644 --- a/crates-cli/yaak-cli/src/commands/request.rs +++ b/crates-cli/yaak-cli/src/commands/request.rs @@ -85,6 +85,8 @@ async fn schema(ctx: &CliContext, request_type: RequestSchemaType) -> CommandRes .map_err(|e| format!("Failed to serialize WebSocket request schema: {e}"))?, }; + enrich_schema_guidance(&mut schema, request_type); + if let Err(error) = merge_auth_schema_from_plugins(ctx, &mut schema).await { eprintln!("Warning: Failed to enrich authentication schema from plugins: {error}"); } @@ -95,6 +97,37 @@ async fn schema(ctx: &CliContext, request_type: RequestSchemaType) -> CommandRes Ok(()) } +fn enrich_schema_guidance(schema: &mut Value, request_type: RequestSchemaType) { + if !matches!(request_type, RequestSchemaType::Http) { + return; + } + + let Some(properties) = schema.get_mut("properties").and_then(Value::as_object_mut) else { + return; + }; + + if let Some(url_schema) = properties.get_mut("url").and_then(Value::as_object_mut) { + append_description( + url_schema, + "For path segments like `/foo/:id/comments/:commentId`, put concrete values in `urlParameters` using names without `:` (for example `id`, `commentId`).", + ); + } +} + +fn append_description(schema: &mut Map, extra: &str) { + match schema.get_mut("description") { + Some(Value::String(existing)) if !existing.trim().is_empty() => { + if !existing.ends_with(' ') { + existing.push(' '); + } + existing.push_str(extra); + } + _ => { + schema.insert("description".to_string(), Value::String(extra.to_string())); + } + } +} + async fn merge_auth_schema_from_plugins( ctx: &CliContext, schema: &mut Value, diff --git a/crates-cli/yaak-cli/src/main.rs b/crates-cli/yaak-cli/src/main.rs index afafc069..3b9827bf 100644 --- a/crates-cli/yaak-cli/src/main.rs +++ b/crates-cli/yaak-cli/src/main.rs @@ -4,6 +4,7 @@ mod context; mod plugin_events; mod ui; mod utils; +mod version; use clap::Parser; use cli::{Cli, Commands, RequestCommands}; diff --git a/crates-cli/yaak-cli/src/utils/http.rs b/crates-cli/yaak-cli/src/utils/http.rs new file mode 100644 index 00000000..b3075ca2 --- /dev/null +++ b/crates-cli/yaak-cli/src/utils/http.rs @@ -0,0 +1,47 @@ +use reqwest::Client; +use reqwest::header::{HeaderMap, HeaderName, HeaderValue, USER_AGENT}; +use serde_json::Value; + +pub fn build_client(session_token: Option<&str>) -> Result { + let mut headers = HeaderMap::new(); + let user_agent = HeaderValue::from_str(&user_agent()) + .map_err(|e| format!("Failed to build user-agent header: {e}"))?; + headers.insert(USER_AGENT, user_agent); + + if let Some(token) = session_token { + let token_value = HeaderValue::from_str(token) + .map_err(|e| format!("Failed to build session header: {e}"))?; + headers.insert(HeaderName::from_static("x-yaak-session"), token_value); + } + + Client::builder() + .default_headers(headers) + .build() + .map_err(|e| format!("Failed to initialize HTTP client: {e}")) +} + +pub fn parse_api_error(status: u16, body: &str) -> String { + if let Ok(value) = serde_json::from_str::(body) { + if let Some(message) = value.get("message").and_then(Value::as_str) { + return message.to_string(); + } + if let Some(error) = value.get("error").and_then(Value::as_str) { + return error.to_string(); + } + } + + format!("API error {status}: {body}") +} + +fn user_agent() -> String { + format!("YaakCli/{} ({})", crate::version::cli_version(), ua_platform()) +} + +fn ua_platform() -> &'static str { + match std::env::consts::OS { + "windows" => "Win", + "darwin" => "Mac", + "linux" => "Linux", + _ => "Unknown", + } +} diff --git a/crates-cli/yaak-cli/src/utils/mod.rs b/crates-cli/yaak-cli/src/utils/mod.rs index 1112de80..b12fb14e 100644 --- a/crates-cli/yaak-cli/src/utils/mod.rs +++ b/crates-cli/yaak-cli/src/utils/mod.rs @@ -1,2 +1,3 @@ pub mod confirm; +pub mod http; pub mod json; diff --git a/crates-cli/yaak-cli/src/version.rs b/crates-cli/yaak-cli/src/version.rs new file mode 100644 index 00000000..ec745c65 --- /dev/null +++ b/crates-cli/yaak-cli/src/version.rs @@ -0,0 +1,3 @@ +pub fn cli_version() -> &'static str { + option_env!("YAAK_CLI_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")) +} diff --git a/crates-cli/yaak-cli/tests/request_commands.rs b/crates-cli/yaak-cli/tests/request_commands.rs index 41b2aa5e..41ce02ac 100644 --- a/crates-cli/yaak-cli/tests/request_commands.rs +++ b/crates-cli/yaak-cli/tests/request_commands.rs @@ -190,7 +190,9 @@ fn request_schema_http_outputs_json_schema() { .assert() .success() .stdout(contains("\"type\": \"object\"")) - .stdout(contains("\"authentication\"")); + .stdout(contains("\"authentication\"")) + .stdout(contains("/foo/:id/comments/:commentId")) + .stdout(contains("put concrete values in `urlParameters`")); } #[test]