Add workspace/environment schemas and shared agent hints

This commit is contained in:
Gregory Schier
2026-02-23 07:25:33 -08:00
parent 1e7e1232da
commit 53d86f5568
11 changed files with 141 additions and 12 deletions

View File

@@ -8,7 +8,9 @@ Current top-level commands:
```text ```text
yaakcli send <request_id> yaakcli send <request_id>
yaakcli agent-help
yaakcli workspace list yaakcli workspace list
yaakcli workspace schema [--pretty]
yaakcli workspace show <workspace_id> yaakcli workspace show <workspace_id>
yaakcli workspace create --name <name> yaakcli workspace create --name <name>
yaakcli workspace create --json '{"name":"My Workspace"}' 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 update --json '{"id":"fl_abc","name":"Auth v2"}'
yaakcli folder delete <folder_id> [--yes] yaakcli folder delete <folder_id> [--yes]
yaakcli environment list <workspace_id> yaakcli environment list <workspace_id>
yaakcli environment schema [--pretty]
yaakcli environment show <environment_id> yaakcli environment show <environment_id>
yaakcli environment create <workspace_id> --name <name> yaakcli environment create <workspace_id> --name <name>
yaakcli environment create --json '{"workspaceId":"wk_abc","name":"Production"}' yaakcli environment create --json '{"workspaceId":"wk_abc","name":"Production"}'
@@ -51,6 +54,8 @@ Notes:
- `delete` commands prompt for confirmation unless `--yes` is provided. - `delete` commands prompt for confirmation unless `--yes` is provided.
- In non-interactive mode, `delete` commands require `--yes`. - In non-interactive mode, `delete` commands require `--yes`.
- `create` and `update` commands support `--json` and positional JSON shorthand. - `create` and `update` commands support `--json` and positional JSON shorthand.
- For `create` commands, use one input mode at a time. Example: do not combine `<workspace_id>` with `--json`.
- Template tags use `${[ ... ]}` syntax (for example `${[API_BASE_URL]}`), not `{{ ... }}`.
- `update` uses JSON Merge Patch semantics (RFC 7386) for partial updates. - `update` uses JSON Merge Patch semantics (RFC 7386) for partial updates.
## Examples ## Examples

View File

@@ -5,6 +5,13 @@ use std::path::PathBuf;
#[command(name = "yaak")] #[command(name = "yaak")]
#[command(about = "Yaak CLI - API client from the command line")] #[command(about = "Yaak CLI - API client from the command line")]
#[command(version = crate::version::cli_version())] #[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 { pub struct Cli {
/// Use a custom data directory /// Use a custom data directory
#[arg(long, global = true)] #[arg(long, global = true)]
@@ -71,6 +78,7 @@ pub struct SendArgs {
} }
#[derive(Args)] #[derive(Args)]
#[command(disable_help_subcommand = true)]
pub struct WorkspaceArgs { pub struct WorkspaceArgs {
#[command(subcommand)] #[command(subcommand)]
pub command: WorkspaceCommands, pub command: WorkspaceCommands,
@@ -81,6 +89,13 @@ pub enum WorkspaceCommands {
/// List all workspaces /// List all workspaces
List, 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 a workspace as JSON
Show { Show {
/// Workspace ID /// Workspace ID
@@ -125,6 +140,7 @@ pub enum WorkspaceCommands {
} }
#[derive(Args)] #[derive(Args)]
#[command(disable_help_subcommand = true)]
pub struct RequestArgs { pub struct RequestArgs {
#[command(subcommand)] #[command(subcommand)]
pub command: RequestCommands, pub command: RequestCommands,
@@ -212,6 +228,7 @@ pub enum RequestSchemaType {
} }
#[derive(Args)] #[derive(Args)]
#[command(disable_help_subcommand = true)]
pub struct FolderArgs { pub struct FolderArgs {
#[command(subcommand)] #[command(subcommand)]
pub command: FolderCommands, pub command: FolderCommands,
@@ -268,6 +285,7 @@ pub enum FolderCommands {
} }
#[derive(Args)] #[derive(Args)]
#[command(disable_help_subcommand = true)]
pub struct EnvironmentArgs { pub struct EnvironmentArgs {
#[command(subcommand)] #[command(subcommand)]
pub command: EnvironmentCommands, pub command: EnvironmentCommands,
@@ -281,6 +299,13 @@ pub enum EnvironmentCommands {
workspace_id: String, 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 an environment as JSON
Show { Show {
/// Environment ID /// Environment ID
@@ -288,15 +313,22 @@ pub enum EnvironmentCommands {
}, },
/// Create an environment /// Create an environment
#[command(after_help = r#"Modes (choose one):
1) yaak environment create <workspace_id> --name <name>
2) yaak environment create --json '{"workspaceId":"wk_abc","name":"Production"}'
3) yaak environment create '{"workspaceId":"wk_abc","name":"Production"}'
Do not combine <workspace_id> with --json."#)]
Create { 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<String>, workspace_id: Option<String>,
/// Environment name /// Environment name
#[arg(short, long)] #[arg(short, long)]
name: Option<String>, name: Option<String>,
/// JSON payload /// JSON payload (use instead of WORKSPACE_ID/--name)
#[arg(long)] #[arg(long)]
json: Option<String>, json: Option<String>,
}, },
@@ -324,6 +356,7 @@ pub enum EnvironmentCommands {
} }
#[derive(Args)] #[derive(Args)]
#[command(disable_help_subcommand = true)]
pub struct AuthArgs { pub struct AuthArgs {
#[command(subcommand)] #[command(subcommand)]
pub command: AuthCommands, pub command: AuthCommands,
@@ -342,6 +375,7 @@ pub enum AuthCommands {
} }
#[derive(Args)] #[derive(Args)]
#[command(disable_help_subcommand = true)]
pub struct PluginArgs { pub struct PluginArgs {
#[command(subcommand)] #[command(subcommand)]
pub command: PluginCommands, pub command: PluginCommands,

View File

@@ -5,6 +5,8 @@ use crate::utils::json::{
apply_merge_patch, is_json_shorthand, parse_optional_json, parse_required_json, require_id, apply_merge_patch, is_json_shorthand, parse_optional_json, parse_required_json, require_id,
validate_create_id, validate_create_id,
}; };
use crate::utils::schema::append_agent_hints;
use schemars::schema_for;
use yaak_models::models::Environment; use yaak_models::models::Environment;
use yaak_models::util::UpdateSource; use yaak_models::util::UpdateSource;
@@ -13,6 +15,7 @@ type CommandResult<T = ()> = std::result::Result<T, String>;
pub fn run(ctx: &CliContext, args: EnvironmentArgs) -> i32 { pub fn run(ctx: &CliContext, args: EnvironmentArgs) -> i32 {
let result = match args.command { let result = match args.command {
EnvironmentCommands::List { workspace_id } => list(ctx, &workspace_id), EnvironmentCommands::List { workspace_id } => list(ctx, &workspace_id),
EnvironmentCommands::Schema { pretty } => schema(pretty),
EnvironmentCommands::Show { environment_id } => show(ctx, &environment_id), EnvironmentCommands::Show { environment_id } => show(ctx, &environment_id),
EnvironmentCommands::Create { workspace_id, name, json } => { EnvironmentCommands::Create { workspace_id, name, json } => {
create(ctx, 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 { fn list(ctx: &CliContext, workspace_id: &str) -> CommandResult {
let environments = ctx let environments = ctx
.db() .db()

View File

@@ -5,6 +5,7 @@ use crate::utils::json::{
apply_merge_patch, is_json_shorthand, parse_optional_json, parse_required_json, require_id, apply_merge_patch, is_json_shorthand, parse_optional_json, parse_required_json, require_id,
validate_create_id, validate_create_id,
}; };
use crate::utils::schema::append_agent_hints;
use schemars::schema_for; use schemars::schema_for;
use serde_json::{Map, Value, json}; use serde_json::{Map, Value, json};
use std::collections::HashMap; 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); 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 { if let Err(error) = merge_auth_schema_from_plugins(ctx, &mut schema).await {
eprintln!("Warning: Failed to enrich authentication schema from plugins: {error}"); eprintln!("Warning: Failed to enrich authentication schema from plugins: {error}");
} }
let output = if pretty { let output =
serde_json::to_string_pretty(&schema) if pretty { serde_json::to_string_pretty(&schema) } else { serde_json::to_string(&schema) }
} else { .map_err(|e| format!("Failed to format schema JSON: {e}"))?;
serde_json::to_string(&schema)
}
.map_err(|e| format!("Failed to format schema JSON: {e}"))?;
println!("{output}"); println!("{output}");
Ok(()) Ok(())
} }

View File

@@ -4,6 +4,8 @@ use crate::utils::confirm::confirm_delete;
use crate::utils::json::{ use crate::utils::json::{
apply_merge_patch, parse_optional_json, parse_required_json, require_id, validate_create_id, 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::models::Workspace;
use yaak_models::util::UpdateSource; use yaak_models::util::UpdateSource;
@@ -12,6 +14,7 @@ type CommandResult<T = ()> = std::result::Result<T, String>;
pub fn run(ctx: &CliContext, args: WorkspaceArgs) -> i32 { pub fn run(ctx: &CliContext, args: WorkspaceArgs) -> i32 {
let result = match args.command { let result = match args.command {
WorkspaceCommands::List => list(ctx), WorkspaceCommands::List => list(ctx),
WorkspaceCommands::Schema { pretty } => schema(pretty),
WorkspaceCommands::Show { workspace_id } => show(ctx, &workspace_id), WorkspaceCommands::Show { workspace_id } => show(ctx, &workspace_id),
WorkspaceCommands::Create { name, json, json_input } => create(ctx, name, json, json_input), WorkspaceCommands::Create { name, json, json_input } => create(ctx, name, json, json_input),
WorkspaceCommands::Update { json, json_input } => update(ctx, 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 { fn list(ctx: &CliContext) -> CommandResult {
let workspaces = let workspaces =
ctx.db().list_workspaces().map_err(|e| format!("Failed to list workspaces: {e}"))?; ctx.db().list_workspaces().map_err(|e| format!("Failed to list workspaces: {e}"))?;

View File

@@ -1,3 +1,4 @@
pub mod confirm; pub mod confirm;
pub mod http; pub mod http;
pub mod json; pub mod json;
pub mod schema;

View File

@@ -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') ]}",
}),
);
}

View File

@@ -78,3 +78,19 @@ fn json_create_and_update_merge_patch_round_trip() {
.stdout(contains("\"name\": \"Json Environment\"")) .stdout(contains("\"name\": \"Json Environment\""))
.stdout(contains("\"color\": \"#00ff00\"")); .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\""));
}

View File

@@ -190,6 +190,9 @@ fn request_schema_http_outputs_json_schema() {
.assert() .assert()
.success() .success()
.stdout(contains("\"type\":\"object\"")) .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("\"authentication\":"))
.stdout(contains("/foo/:id/comments/:commentId")) .stdout(contains("/foo/:id/comments/:commentId"))
.stdout(contains("put concrete values in `urlParameters`")); .stdout(contains("put concrete values in `urlParameters`"));

View File

@@ -57,3 +57,19 @@ fn json_create_and_update_merge_patch_round_trip() {
.stdout(contains("\"name\": \"Json Workspace\"")) .stdout(contains("\"name\": \"Json Workspace\""))
.stdout(contains("\"description\": \"Updated via JSON\"")); .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\""));
}

View File

@@ -74,7 +74,7 @@ pub struct ClientCertificate {
pub enabled: bool, pub enabled: bool,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct DnsOverride { 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")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")] #[ts(export, export_to = "gen_models.ts")]
#[enum_def(table_name = "workspaces")] #[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")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")] #[ts(export, export_to = "gen_models.ts")]
#[enum_def(table_name = "environments")] #[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")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct EnvironmentVariable { pub struct EnvironmentVariable {