From 35d9ed901a075ffe2cff4cf8af31443c89c84d8c Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Mon, 23 Feb 2026 07:25:33 -0800 Subject: [PATCH] Add workspace/environment schemas and shared agent hints --- crates-cli/yaak-cli/README.md | 5 +++ crates-cli/yaak-cli/src/cli.rs | 38 ++++++++++++++++++- .../yaak-cli/src/commands/environment.rs | 20 ++++++++++ crates-cli/yaak-cli/src/commands/request.rs | 11 +++--- crates-cli/yaak-cli/src/commands/workspace.rs | 20 ++++++++++ crates-cli/yaak-cli/src/utils/mod.rs | 1 + crates-cli/yaak-cli/src/utils/schema.rs | 15 ++++++++ .../yaak-cli/tests/environment_commands.rs | 16 ++++++++ crates-cli/yaak-cli/tests/request_commands.rs | 3 ++ .../yaak-cli/tests/workspace_commands.rs | 16 ++++++++ crates/yaak-models/src/models.rs | 8 ++-- 11 files changed, 141 insertions(+), 12 deletions(-) create mode 100644 crates-cli/yaak-cli/src/utils/schema.rs diff --git a/crates-cli/yaak-cli/README.md b/crates-cli/yaak-cli/README.md index a262d4d6..8d73f360 100644 --- a/crates-cli/yaak-cli/README.md +++ b/crates-cli/yaak-cli/README.md @@ -8,7 +8,9 @@ Current top-level commands: ```text yaakcli send +yaakcli agent-help yaakcli workspace list +yaakcli workspace schema [--pretty] yaakcli workspace show yaakcli workspace create --name yaakcli workspace create --json '{"name":"My Workspace"}' @@ -31,6 +33,7 @@ yaakcli folder create '{"workspaceId":"wk_abc","name":"Auth"}' yaakcli folder update --json '{"id":"fl_abc","name":"Auth v2"}' yaakcli folder delete [--yes] yaakcli environment list +yaakcli environment schema [--pretty] yaakcli environment show yaakcli environment create --name yaakcli environment create --json '{"workspaceId":"wk_abc","name":"Production"}' @@ -51,6 +54,8 @@ Notes: - `delete` commands prompt for confirmation unless `--yes` is provided. - In non-interactive mode, `delete` commands require `--yes`. - `create` and `update` commands support `--json` and positional JSON shorthand. +- For `create` commands, use one input mode at a time. Example: do not combine `` with `--json`. +- Template tags use `${[ ... ]}` syntax (for example `${[API_BASE_URL]}`), not `{{ ... }}`. - `update` uses JSON Merge Patch semantics (RFC 7386) for partial updates. ## Examples diff --git a/crates-cli/yaak-cli/src/cli.rs b/crates-cli/yaak-cli/src/cli.rs index bc5f3014..a09ee3cd 100644 --- a/crates-cli/yaak-cli/src/cli.rs +++ b/crates-cli/yaak-cli/src/cli.rs @@ -5,6 +5,13 @@ use std::path::PathBuf; #[command(name = "yaak")] #[command(about = "Yaak CLI - API client from the command line")] #[command(version = crate::version::cli_version())] +#[command(disable_help_subcommand = true)] +#[command(after_help = r#"Agent Hints: + - Template variable syntax is ${[ my_var ]}, not {{ ... }} + - Template function syntax is ${[ namespace.my_func(a='aaa',b='bbb') ]} + - View JSONSchema for models before creating or updating (eg. `yaak request schema http`) + - Deletion requires confirmation (--yes for non-interactive environments) + "#)] pub struct Cli { /// Use a custom data directory #[arg(long, global = true)] @@ -71,6 +78,7 @@ pub struct SendArgs { } #[derive(Args)] +#[command(disable_help_subcommand = true)] pub struct WorkspaceArgs { #[command(subcommand)] pub command: WorkspaceCommands, @@ -81,6 +89,13 @@ pub enum WorkspaceCommands { /// List all workspaces List, + /// Output JSON schema for workspace create/update payloads + Schema { + /// Pretty-print schema JSON output + #[arg(long)] + pretty: bool, + }, + /// Show a workspace as JSON Show { /// Workspace ID @@ -125,6 +140,7 @@ pub enum WorkspaceCommands { } #[derive(Args)] +#[command(disable_help_subcommand = true)] pub struct RequestArgs { #[command(subcommand)] pub command: RequestCommands, @@ -212,6 +228,7 @@ pub enum RequestSchemaType { } #[derive(Args)] +#[command(disable_help_subcommand = true)] pub struct FolderArgs { #[command(subcommand)] pub command: FolderCommands, @@ -268,6 +285,7 @@ pub enum FolderCommands { } #[derive(Args)] +#[command(disable_help_subcommand = true)] pub struct EnvironmentArgs { #[command(subcommand)] pub command: EnvironmentCommands, @@ -281,6 +299,13 @@ pub enum EnvironmentCommands { workspace_id: String, }, + /// Output JSON schema for environment create/update payloads + Schema { + /// Pretty-print schema JSON output + #[arg(long)] + pretty: bool, + }, + /// Show an environment as JSON Show { /// Environment ID @@ -288,15 +313,22 @@ pub enum EnvironmentCommands { }, /// Create an environment + #[command(after_help = r#"Modes (choose one): + 1) yaak environment create --name + 2) yaak environment create --json '{"workspaceId":"wk_abc","name":"Production"}' + 3) yaak environment create '{"workspaceId":"wk_abc","name":"Production"}' + +Do not combine with --json."#)] Create { - /// Workspace ID (or positional JSON payload shorthand) + /// Workspace ID for flag-based mode, or positional JSON payload shorthand + #[arg(value_name = "WORKSPACE_ID_OR_JSON")] workspace_id: Option, /// Environment name #[arg(short, long)] name: Option, - /// JSON payload + /// JSON payload (use instead of WORKSPACE_ID/--name) #[arg(long)] json: Option, }, @@ -324,6 +356,7 @@ pub enum EnvironmentCommands { } #[derive(Args)] +#[command(disable_help_subcommand = true)] pub struct AuthArgs { #[command(subcommand)] pub command: AuthCommands, @@ -342,6 +375,7 @@ pub enum AuthCommands { } #[derive(Args)] +#[command(disable_help_subcommand = true)] pub struct PluginArgs { #[command(subcommand)] pub command: PluginCommands, diff --git a/crates-cli/yaak-cli/src/commands/environment.rs b/crates-cli/yaak-cli/src/commands/environment.rs index deb13677..1b65e78b 100644 --- a/crates-cli/yaak-cli/src/commands/environment.rs +++ b/crates-cli/yaak-cli/src/commands/environment.rs @@ -5,6 +5,8 @@ use crate::utils::json::{ apply_merge_patch, is_json_shorthand, parse_optional_json, parse_required_json, require_id, validate_create_id, }; +use crate::utils::schema::append_agent_hints; +use schemars::schema_for; use yaak_models::models::Environment; use yaak_models::util::UpdateSource; @@ -13,6 +15,7 @@ type CommandResult = std::result::Result; pub fn run(ctx: &CliContext, args: EnvironmentArgs) -> i32 { let result = match args.command { EnvironmentCommands::List { workspace_id } => list(ctx, &workspace_id), + EnvironmentCommands::Schema { pretty } => schema(pretty), EnvironmentCommands::Show { environment_id } => show(ctx, &environment_id), EnvironmentCommands::Create { workspace_id, name, json } => { create(ctx, workspace_id, name, json) @@ -30,6 +33,23 @@ pub fn run(ctx: &CliContext, args: EnvironmentArgs) -> i32 { } } +fn schema(pretty: bool) -> CommandResult { + let mut schema = + serde_json::to_value(schema_for!(Environment)).map_err(|e| format!( + "Failed to serialize environment schema: {e}" + ))?; + append_agent_hints(&mut schema); + + let output = if pretty { + serde_json::to_string_pretty(&schema) + } else { + serde_json::to_string(&schema) + } + .map_err(|e| format!("Failed to format environment schema JSON: {e}"))?; + println!("{output}"); + Ok(()) +} + fn list(ctx: &CliContext, workspace_id: &str) -> CommandResult { let environments = ctx .db() diff --git a/crates-cli/yaak-cli/src/commands/request.rs b/crates-cli/yaak-cli/src/commands/request.rs index a2b61c8f..bb0d77bf 100644 --- a/crates-cli/yaak-cli/src/commands/request.rs +++ b/crates-cli/yaak-cli/src/commands/request.rs @@ -5,6 +5,7 @@ use crate::utils::json::{ apply_merge_patch, is_json_shorthand, parse_optional_json, parse_required_json, require_id, validate_create_id, }; +use crate::utils::schema::append_agent_hints; use schemars::schema_for; use serde_json::{Map, Value, json}; use std::collections::HashMap; @@ -86,17 +87,15 @@ async fn schema(ctx: &CliContext, request_type: RequestSchemaType, pretty: bool) }; enrich_schema_guidance(&mut schema, request_type); + append_agent_hints(&mut schema); if let Err(error) = merge_auth_schema_from_plugins(ctx, &mut schema).await { eprintln!("Warning: Failed to enrich authentication schema from plugins: {error}"); } - let output = if pretty { - serde_json::to_string_pretty(&schema) - } else { - serde_json::to_string(&schema) - } - .map_err(|e| format!("Failed to format schema JSON: {e}"))?; + let output = + if pretty { serde_json::to_string_pretty(&schema) } else { serde_json::to_string(&schema) } + .map_err(|e| format!("Failed to format schema JSON: {e}"))?; println!("{output}"); Ok(()) } diff --git a/crates-cli/yaak-cli/src/commands/workspace.rs b/crates-cli/yaak-cli/src/commands/workspace.rs index 838223aa..76d85316 100644 --- a/crates-cli/yaak-cli/src/commands/workspace.rs +++ b/crates-cli/yaak-cli/src/commands/workspace.rs @@ -4,6 +4,8 @@ use crate::utils::confirm::confirm_delete; use crate::utils::json::{ apply_merge_patch, parse_optional_json, parse_required_json, require_id, validate_create_id, }; +use crate::utils::schema::append_agent_hints; +use schemars::schema_for; use yaak_models::models::Workspace; use yaak_models::util::UpdateSource; @@ -12,6 +14,7 @@ type CommandResult = std::result::Result; pub fn run(ctx: &CliContext, args: WorkspaceArgs) -> i32 { let result = match args.command { WorkspaceCommands::List => list(ctx), + WorkspaceCommands::Schema { pretty } => schema(pretty), WorkspaceCommands::Show { workspace_id } => show(ctx, &workspace_id), WorkspaceCommands::Create { name, json, json_input } => create(ctx, name, json, json_input), WorkspaceCommands::Update { json, json_input } => update(ctx, json, json_input), @@ -27,6 +30,23 @@ pub fn run(ctx: &CliContext, args: WorkspaceArgs) -> i32 { } } +fn schema(pretty: bool) -> CommandResult { + let mut schema = + serde_json::to_value(schema_for!(Workspace)).map_err(|e| format!( + "Failed to serialize workspace schema: {e}" + ))?; + append_agent_hints(&mut schema); + + let output = if pretty { + serde_json::to_string_pretty(&schema) + } else { + serde_json::to_string(&schema) + } + .map_err(|e| format!("Failed to format workspace schema JSON: {e}"))?; + println!("{output}"); + Ok(()) +} + fn list(ctx: &CliContext) -> CommandResult { let workspaces = ctx.db().list_workspaces().map_err(|e| format!("Failed to list workspaces: {e}"))?; diff --git a/crates-cli/yaak-cli/src/utils/mod.rs b/crates-cli/yaak-cli/src/utils/mod.rs index b12fb14e..0707af26 100644 --- a/crates-cli/yaak-cli/src/utils/mod.rs +++ b/crates-cli/yaak-cli/src/utils/mod.rs @@ -1,3 +1,4 @@ pub mod confirm; pub mod http; pub mod json; +pub mod schema; diff --git a/crates-cli/yaak-cli/src/utils/schema.rs b/crates-cli/yaak-cli/src/utils/schema.rs new file mode 100644 index 00000000..2e5792f0 --- /dev/null +++ b/crates-cli/yaak-cli/src/utils/schema.rs @@ -0,0 +1,15 @@ +use serde_json::{Value, json}; + +pub fn append_agent_hints(schema: &mut Value) { + let Some(schema_obj) = schema.as_object_mut() else { + return; + }; + + schema_obj.insert( + "x-yaak-agent-hints".to_string(), + json!({ + "templateVariableSyntax": "${[ my_var ]}", + "templateFunctionSyntax": "${[ namespace.my_func(a='aaa',b='bbb') ]}", + }), + ); +} diff --git a/crates-cli/yaak-cli/tests/environment_commands.rs b/crates-cli/yaak-cli/tests/environment_commands.rs index c632c569..04264a1d 100644 --- a/crates-cli/yaak-cli/tests/environment_commands.rs +++ b/crates-cli/yaak-cli/tests/environment_commands.rs @@ -78,3 +78,19 @@ fn json_create_and_update_merge_patch_round_trip() { .stdout(contains("\"name\": \"Json Environment\"")) .stdout(contains("\"color\": \"#00ff00\"")); } + +#[test] +fn environment_schema_outputs_json_schema() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let data_dir = temp_dir.path(); + + cli_cmd(data_dir) + .args(["environment", "schema"]) + .assert() + .success() + .stdout(contains("\"type\":\"object\"")) + .stdout(contains("\"x-yaak-agent-hints\"")) + .stdout(contains("\"templateVariableSyntax\":\"${[ my_var ]}\"")) + .stdout(contains("\"templateFunctionSyntax\":\"${[ namespace.my_func(a='aaa',b='bbb') ]}\"")) + .stdout(contains("\"workspaceId\"")); +} diff --git a/crates-cli/yaak-cli/tests/request_commands.rs b/crates-cli/yaak-cli/tests/request_commands.rs index 1af18f71..4b1391dd 100644 --- a/crates-cli/yaak-cli/tests/request_commands.rs +++ b/crates-cli/yaak-cli/tests/request_commands.rs @@ -190,6 +190,9 @@ fn request_schema_http_outputs_json_schema() { .assert() .success() .stdout(contains("\"type\":\"object\"")) + .stdout(contains("\"x-yaak-agent-hints\"")) + .stdout(contains("\"templateVariableSyntax\":\"${[ my_var ]}\"")) + .stdout(contains("\"templateFunctionSyntax\":\"${[ namespace.my_func(a='aaa',b='bbb') ]}\"")) .stdout(contains("\"authentication\":")) .stdout(contains("/foo/:id/comments/:commentId")) .stdout(contains("put concrete values in `urlParameters`")); diff --git a/crates-cli/yaak-cli/tests/workspace_commands.rs b/crates-cli/yaak-cli/tests/workspace_commands.rs index f888beda..995532e2 100644 --- a/crates-cli/yaak-cli/tests/workspace_commands.rs +++ b/crates-cli/yaak-cli/tests/workspace_commands.rs @@ -57,3 +57,19 @@ fn json_create_and_update_merge_patch_round_trip() { .stdout(contains("\"name\": \"Json Workspace\"")) .stdout(contains("\"description\": \"Updated via JSON\"")); } + +#[test] +fn workspace_schema_outputs_json_schema() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let data_dir = temp_dir.path(); + + cli_cmd(data_dir) + .args(["workspace", "schema"]) + .assert() + .success() + .stdout(contains("\"type\":\"object\"")) + .stdout(contains("\"x-yaak-agent-hints\"")) + .stdout(contains("\"templateVariableSyntax\":\"${[ my_var ]}\"")) + .stdout(contains("\"templateFunctionSyntax\":\"${[ namespace.my_func(a='aaa',b='bbb') ]}\"")) + .stdout(contains("\"name\"")); +} diff --git a/crates/yaak-models/src/models.rs b/crates/yaak-models/src/models.rs index 7de47a90..95796031 100644 --- a/crates/yaak-models/src/models.rs +++ b/crates/yaak-models/src/models.rs @@ -74,7 +74,7 @@ pub struct ClientCertificate { pub enabled: bool, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "gen_models.ts")] pub struct DnsOverride { @@ -293,7 +293,7 @@ impl UpsertModelInfo for Settings { } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] #[ts(export, export_to = "gen_models.ts")] #[enum_def(table_name = "workspaces")] @@ -590,7 +590,7 @@ impl UpsertModelInfo for CookieJar { } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] #[ts(export, export_to = "gen_models.ts")] #[enum_def(table_name = "environments")] @@ -700,7 +700,7 @@ impl UpsertModelInfo for Environment { } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] #[ts(export, export_to = "gen_models.ts")] pub struct EnvironmentVariable {