Add workspace/environment schemas and shared agent hints

This commit is contained in:
Gregory Schier
2026-02-23 07:25:33 -08:00
parent f04b34be1a
commit 35d9ed901a
11 changed files with 141 additions and 12 deletions

View File

@@ -8,7 +8,9 @@ Current top-level commands:
```text
yaakcli send <request_id>
yaakcli agent-help
yaakcli workspace list
yaakcli workspace schema [--pretty]
yaakcli workspace show <workspace_id>
yaakcli workspace create --name <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 <folder_id> [--yes]
yaakcli environment list <workspace_id>
yaakcli environment schema [--pretty]
yaakcli environment show <environment_id>
yaakcli environment create <workspace_id> --name <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 `<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.
## Examples

View File

@@ -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 <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 {
/// 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>,
/// Environment name
#[arg(short, long)]
name: Option<String>,
/// JSON payload
/// JSON payload (use instead of WORKSPACE_ID/--name)
#[arg(long)]
json: Option<String>,
},
@@ -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,

View File

@@ -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<T = ()> = std::result::Result<T, String>;
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()

View File

@@ -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(())
}

View File

@@ -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<T = ()> = std::result::Result<T, String>;
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}"))?;

View File

@@ -1,3 +1,4 @@
pub mod confirm;
pub mod http;
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("\"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()
.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`"));

View File

@@ -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\""));
}

View File

@@ -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 {