diff --git a/crates-cli/yaak-cli/src/cli.rs b/crates-cli/yaak-cli/src/cli.rs index ec9a9b3c..975d94d6 100644 --- a/crates-cli/yaak-cli/src/cli.rs +++ b/crates-cli/yaak-cli/src/cli.rs @@ -21,6 +21,10 @@ pub struct Cli { #[arg(long, short, global = true)] pub environment: Option, + /// Cookie jar ID to use when sending requests + #[arg(long = "cookie-jar", global = true, value_name = "COOKIE_JAR_ID")] + pub cookie_jar: Option, + /// Enable verbose send output (events and streamed response body) #[arg(long, short, global = true)] pub verbose: bool, @@ -58,6 +62,9 @@ pub enum Commands { /// Send a request, folder, or workspace by ID Send(SendArgs), + /// Cookie jar commands + CookieJar(CookieJarArgs), + /// Workspace commands Workspace(WorkspaceArgs), @@ -85,6 +92,22 @@ pub struct SendArgs { pub fail_fast: bool, } +#[derive(Args)] +#[command(disable_help_subcommand = true)] +pub struct CookieJarArgs { + #[command(subcommand)] + pub command: CookieJarCommands, +} + +#[derive(Subcommand)] +pub enum CookieJarCommands { + /// List cookie jars in a workspace + List { + /// Workspace ID (optional when exactly one workspace exists) + workspace_id: Option, + }, +} + #[derive(Args)] #[command(disable_help_subcommand = true)] pub struct WorkspaceArgs { @@ -158,8 +181,8 @@ pub struct RequestArgs { pub enum RequestCommands { /// List requests in a workspace List { - /// Workspace ID - workspace_id: String, + /// Workspace ID (optional when exactly one workspace exists) + workspace_id: Option, }, /// Show a request as JSON @@ -267,8 +290,8 @@ pub struct FolderArgs { pub enum FolderCommands { /// List folders in a workspace List { - /// Workspace ID - workspace_id: String, + /// Workspace ID (optional when exactly one workspace exists) + workspace_id: Option, }, /// Show a folder as JSON @@ -324,8 +347,8 @@ pub struct EnvironmentArgs { pub enum EnvironmentCommands { /// List environments in a workspace List { - /// Workspace ID - workspace_id: String, + /// Workspace ID (optional when exactly one workspace exists) + workspace_id: Option, }, /// Output JSON schema for environment create/update payloads diff --git a/crates-cli/yaak-cli/src/commands/cookie_jar.rs b/crates-cli/yaak-cli/src/commands/cookie_jar.rs new file mode 100644 index 00000000..3494cde2 --- /dev/null +++ b/crates-cli/yaak-cli/src/commands/cookie_jar.rs @@ -0,0 +1,42 @@ +use crate::cli::{CookieJarArgs, CookieJarCommands}; +use crate::context::CliContext; +use crate::utils::workspace::resolve_workspace_id; + +type CommandResult = std::result::Result; + +pub fn run(ctx: &CliContext, args: CookieJarArgs) -> i32 { + let result = match args.command { + CookieJarCommands::List { workspace_id } => list(ctx, workspace_id.as_deref()), + }; + + match result { + Ok(()) => 0, + Err(error) => { + eprintln!("Error: {error}"); + 1 + } + } +} + +fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult { + let workspace_id = resolve_workspace_id(ctx, workspace_id, "cookie-jar list")?; + let cookie_jars = ctx + .db() + .list_cookie_jars(&workspace_id) + .map_err(|e| format!("Failed to list cookie jars: {e}"))?; + + if cookie_jars.is_empty() { + println!("No cookie jars found in workspace {}", workspace_id); + } else { + for cookie_jar in cookie_jars { + println!( + "{} - {} ({} cookies)", + cookie_jar.id, + cookie_jar.name, + cookie_jar.cookies.len() + ); + } + } + + Ok(()) +} diff --git a/crates-cli/yaak-cli/src/commands/environment.rs b/crates-cli/yaak-cli/src/commands/environment.rs index 02bb31e9..2c060a92 100644 --- a/crates-cli/yaak-cli/src/commands/environment.rs +++ b/crates-cli/yaak-cli/src/commands/environment.rs @@ -6,6 +6,7 @@ use crate::utils::json::{ parse_required_json, require_id, validate_create_id, }; use crate::utils::schema::append_agent_hints; +use crate::utils::workspace::resolve_workspace_id; use schemars::schema_for; use yaak_models::models::Environment; use yaak_models::util::UpdateSource; @@ -14,7 +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::List { workspace_id } => list(ctx, workspace_id.as_deref()), EnvironmentCommands::Schema { pretty } => schema(pretty), EnvironmentCommands::Show { environment_id } => show(ctx, &environment_id), EnvironmentCommands::Create { workspace_id, name, json } => { @@ -45,10 +46,11 @@ fn schema(pretty: bool) -> CommandResult { Ok(()) } -fn list(ctx: &CliContext, workspace_id: &str) -> CommandResult { +fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult { + let workspace_id = resolve_workspace_id(ctx, workspace_id, "environment list")?; let environments = ctx .db() - .list_environments_ensure_base(workspace_id) + .list_environments_ensure_base(&workspace_id) .map_err(|e| format!("Failed to list environments: {e}"))?; if environments.is_empty() { @@ -92,8 +94,14 @@ fn create( validate_create_id(&payload, "environment")?; let mut environment: Environment = serde_json::from_value(payload) .map_err(|e| format!("Failed to parse environment create JSON: {e}"))?; + let fallback_workspace_id = + if workspace_id_arg.is_none() && environment.workspace_id.is_empty() { + Some(resolve_workspace_id(ctx, None, "environment create")?) + } else { + None + }; merge_workspace_id_arg( - workspace_id_arg.as_deref(), + workspace_id_arg.as_deref().or(fallback_workspace_id.as_deref()), &mut environment.workspace_id, "environment create", )?; @@ -111,9 +119,8 @@ fn create( return Ok(()); } - let workspace_id = workspace_id_arg.ok_or_else(|| { - "environment create requires workspace_id unless JSON payload is provided".to_string() - })?; + let workspace_id = + resolve_workspace_id(ctx, workspace_id_arg.as_deref(), "environment create")?; let name = name.ok_or_else(|| { "environment create requires --name unless JSON payload is provided".to_string() })?; diff --git a/crates-cli/yaak-cli/src/commands/folder.rs b/crates-cli/yaak-cli/src/commands/folder.rs index a5f99092..926c76b8 100644 --- a/crates-cli/yaak-cli/src/commands/folder.rs +++ b/crates-cli/yaak-cli/src/commands/folder.rs @@ -5,6 +5,7 @@ use crate::utils::json::{ apply_merge_patch, is_json_shorthand, merge_workspace_id_arg, parse_optional_json, parse_required_json, require_id, validate_create_id, }; +use crate::utils::workspace::resolve_workspace_id; use yaak_models::models::Folder; use yaak_models::util::UpdateSource; @@ -12,7 +13,7 @@ type CommandResult = std::result::Result; pub fn run(ctx: &CliContext, args: FolderArgs) -> i32 { let result = match args.command { - FolderCommands::List { workspace_id } => list(ctx, &workspace_id), + FolderCommands::List { workspace_id } => list(ctx, workspace_id.as_deref()), FolderCommands::Show { folder_id } => show(ctx, &folder_id), FolderCommands::Create { workspace_id, name, json } => { create(ctx, workspace_id, name, json) @@ -30,9 +31,10 @@ pub fn run(ctx: &CliContext, args: FolderArgs) -> i32 { } } -fn list(ctx: &CliContext, workspace_id: &str) -> CommandResult { +fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult { + let workspace_id = resolve_workspace_id(ctx, workspace_id, "folder list")?; let folders = - ctx.db().list_folders(workspace_id).map_err(|e| format!("Failed to list folders: {e}"))?; + ctx.db().list_folders(&workspace_id).map_err(|e| format!("Failed to list folders: {e}"))?; if folders.is_empty() { println!("No folders found in workspace {}", workspace_id); } else { @@ -72,8 +74,14 @@ fn create( validate_create_id(&payload, "folder")?; let mut folder: Folder = serde_json::from_value(payload) .map_err(|e| format!("Failed to parse folder create JSON: {e}"))?; + let fallback_workspace_id = if workspace_id_arg.is_none() && folder.workspace_id.is_empty() + { + Some(resolve_workspace_id(ctx, None, "folder create")?) + } else { + None + }; merge_workspace_id_arg( - workspace_id_arg.as_deref(), + workspace_id_arg.as_deref().or(fallback_workspace_id.as_deref()), &mut folder.workspace_id, "folder create", )?; @@ -87,9 +95,7 @@ fn create( return Ok(()); } - let workspace_id = workspace_id_arg.ok_or_else(|| { - "folder create requires workspace_id unless JSON payload is provided".to_string() - })?; + let workspace_id = resolve_workspace_id(ctx, workspace_id_arg.as_deref(), "folder create")?; let name = name.ok_or_else(|| { "folder create requires --name unless JSON payload is provided".to_string() })?; diff --git a/crates-cli/yaak-cli/src/commands/mod.rs b/crates-cli/yaak-cli/src/commands/mod.rs index b2a0bf23..dc52933a 100644 --- a/crates-cli/yaak-cli/src/commands/mod.rs +++ b/crates-cli/yaak-cli/src/commands/mod.rs @@ -1,4 +1,5 @@ pub mod auth; +pub mod cookie_jar; pub mod environment; pub mod folder; pub mod plugin; diff --git a/crates-cli/yaak-cli/src/commands/request.rs b/crates-cli/yaak-cli/src/commands/request.rs index 43d76f52..74c97490 100644 --- a/crates-cli/yaak-cli/src/commands/request.rs +++ b/crates-cli/yaak-cli/src/commands/request.rs @@ -6,6 +6,7 @@ use crate::utils::json::{ parse_required_json, require_id, validate_create_id, }; use crate::utils::schema::append_agent_hints; +use crate::utils::workspace::resolve_workspace_id; use schemars::schema_for; use serde_json::{Map, Value, json}; use std::collections::HashMap; @@ -24,13 +25,16 @@ pub async fn run( ctx: &CliContext, args: RequestArgs, environment: Option<&str>, + cookie_jar_id: Option<&str>, verbose: bool, ) -> i32 { let result = match args.command { - RequestCommands::List { workspace_id } => list(ctx, &workspace_id), + RequestCommands::List { workspace_id } => list(ctx, workspace_id.as_deref()), RequestCommands::Show { request_id } => show(ctx, &request_id), RequestCommands::Send { request_id } => { - return match send_request_by_id(ctx, &request_id, environment, verbose).await { + return match send_request_by_id(ctx, &request_id, environment, cookie_jar_id, verbose) + .await + { Ok(()) => 0, Err(error) => { eprintln!("Error: {error}"); @@ -63,10 +67,11 @@ pub async fn run( } } -fn list(ctx: &CliContext, workspace_id: &str) -> CommandResult { +fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult { + let workspace_id = resolve_workspace_id(ctx, workspace_id, "request list")?; let requests = ctx .db() - .list_http_requests(workspace_id) + .list_http_requests(&workspace_id) .map_err(|e| format!("Failed to list requests: {e}"))?; if requests.is_empty() { println!("No requests found in workspace {}", workspace_id); @@ -350,8 +355,14 @@ fn create( validate_create_id(&payload, "request")?; let mut request: HttpRequest = serde_json::from_value(payload) .map_err(|e| format!("Failed to parse request create JSON: {e}"))?; + let fallback_workspace_id = if workspace_id_arg.is_none() && request.workspace_id.is_empty() + { + Some(resolve_workspace_id(ctx, None, "request create")?) + } else { + None + }; merge_workspace_id_arg( - workspace_id_arg.as_deref(), + workspace_id_arg.as_deref().or(fallback_workspace_id.as_deref()), &mut request.workspace_id, "request create", )?; @@ -365,9 +376,7 @@ fn create( return Ok(()); } - let workspace_id = workspace_id_arg.ok_or_else(|| { - "request create requires workspace_id unless JSON payload is provided".to_string() - })?; + let workspace_id = resolve_workspace_id(ctx, workspace_id_arg.as_deref(), "request create")?; let name = name.unwrap_or_default(); let url = url.unwrap_or_default(); let method = method.unwrap_or_else(|| "GET".to_string()); @@ -436,6 +445,7 @@ pub async fn send_request_by_id( ctx: &CliContext, request_id: &str, environment: Option<&str>, + cookie_jar_id: Option<&str>, verbose: bool, ) -> Result<(), String> { let request = @@ -447,6 +457,7 @@ pub async fn send_request_by_id( &http_request.id, &http_request.workspace_id, environment, + cookie_jar_id, verbose, ) .await @@ -465,8 +476,11 @@ async fn send_http_request_by_id( request_id: &str, workspace_id: &str, environment: Option<&str>, + cookie_jar_id: Option<&str>, verbose: bool, ) -> Result<(), String> { + let cookie_jar_id = resolve_cookie_jar_id(ctx, workspace_id, cookie_jar_id)?; + let plugin_context = PluginContext::new(None, Some(workspace_id.to_string())); let (event_tx, mut event_rx) = mpsc::channel::(100); @@ -495,7 +509,7 @@ async fn send_http_request_by_id( request_id, environment_id: environment, update_source: UpdateSource::Sync, - cookie_jar_id: None, + cookie_jar_id, response_dir: &response_dir, emit_events_to: Some(event_tx), emit_response_body_chunks_to: Some(body_chunk_tx), @@ -512,3 +526,22 @@ async fn send_http_request_by_id( result.map_err(|e| e.to_string())?; Ok(()) } + +pub(crate) fn resolve_cookie_jar_id( + ctx: &CliContext, + workspace_id: &str, + explicit_cookie_jar_id: Option<&str>, +) -> Result, String> { + if let Some(cookie_jar_id) = explicit_cookie_jar_id { + return Ok(Some(cookie_jar_id.to_string())); + } + + let default_cookie_jar = ctx + .db() + .list_cookie_jars(workspace_id) + .map_err(|e| format!("Failed to list cookie jars: {e}"))? + .into_iter() + .min_by_key(|jar| jar.created_at) + .map(|jar| jar.id); + Ok(default_cookie_jar) +} diff --git a/crates-cli/yaak-cli/src/commands/send.rs b/crates-cli/yaak-cli/src/commands/send.rs index d885c45a..ba96235c 100644 --- a/crates-cli/yaak-cli/src/commands/send.rs +++ b/crates-cli/yaak-cli/src/commands/send.rs @@ -2,6 +2,7 @@ use crate::cli::SendArgs; use crate::commands::request; use crate::context::CliContext; use futures::future::join_all; +use yaak_models::queries::any_request::AnyRequest; enum ExecutionMode { Sequential, @@ -12,9 +13,10 @@ pub async fn run( ctx: &CliContext, args: SendArgs, environment: Option<&str>, + cookie_jar_id: Option<&str>, verbose: bool, ) -> i32 { - match send_target(ctx, args, environment, verbose).await { + match send_target(ctx, args, environment, cookie_jar_id, verbose).await { Ok(()) => 0, Err(error) => { eprintln!("Error: {error}"); @@ -27,30 +29,70 @@ async fn send_target( ctx: &CliContext, args: SendArgs, environment: Option<&str>, + cookie_jar_id: Option<&str>, verbose: bool, ) -> Result<(), String> { let mode = if args.parallel { ExecutionMode::Parallel } else { ExecutionMode::Sequential }; - if ctx.db().get_any_request(&args.id).is_ok() { - return request::send_request_by_id(ctx, &args.id, environment, verbose).await; + if let Ok(request) = ctx.db().get_any_request(&args.id) { + let workspace_id = match &request { + AnyRequest::HttpRequest(r) => r.workspace_id.clone(), + AnyRequest::GrpcRequest(r) => r.workspace_id.clone(), + AnyRequest::WebsocketRequest(r) => r.workspace_id.clone(), + }; + let resolved_cookie_jar_id = + request::resolve_cookie_jar_id(ctx, &workspace_id, cookie_jar_id)?; + + return request::send_request_by_id( + ctx, + &args.id, + environment, + resolved_cookie_jar_id.as_deref(), + verbose, + ) + .await; } - if ctx.db().get_folder(&args.id).is_ok() { + if let Ok(folder) = ctx.db().get_folder(&args.id) { + let resolved_cookie_jar_id = + request::resolve_cookie_jar_id(ctx, &folder.workspace_id, cookie_jar_id)?; + let request_ids = collect_folder_request_ids(ctx, &args.id)?; if request_ids.is_empty() { println!("No requests found in folder {}", args.id); return Ok(()); } - return send_many(ctx, request_ids, mode, args.fail_fast, environment, verbose).await; + return send_many( + ctx, + request_ids, + mode, + args.fail_fast, + environment, + resolved_cookie_jar_id.as_deref(), + verbose, + ) + .await; } - if ctx.db().get_workspace(&args.id).is_ok() { + if let Ok(workspace) = ctx.db().get_workspace(&args.id) { + let resolved_cookie_jar_id = + request::resolve_cookie_jar_id(ctx, &workspace.id, cookie_jar_id)?; + let request_ids = collect_workspace_request_ids(ctx, &args.id)?; if request_ids.is_empty() { println!("No requests found in workspace {}", args.id); return Ok(()); } - return send_many(ctx, request_ids, mode, args.fail_fast, environment, verbose).await; + return send_many( + ctx, + request_ids, + mode, + args.fail_fast, + environment, + resolved_cookie_jar_id.as_deref(), + verbose, + ) + .await; } Err(format!("Could not resolve ID '{}' as request, folder, or workspace", args.id)) @@ -131,6 +173,7 @@ async fn send_many( mode: ExecutionMode, fail_fast: bool, environment: Option<&str>, + cookie_jar_id: Option<&str>, verbose: bool, ) -> Result<(), String> { let mut success_count = 0usize; @@ -139,7 +182,15 @@ async fn send_many( match mode { ExecutionMode::Sequential => { for request_id in request_ids { - match request::send_request_by_id(ctx, &request_id, environment, verbose).await { + match request::send_request_by_id( + ctx, + &request_id, + environment, + cookie_jar_id, + verbose, + ) + .await + { Ok(()) => success_count += 1, Err(error) => { failures.push((request_id, error)); @@ -156,7 +207,14 @@ async fn send_many( .map(|request_id| async move { ( request_id.clone(), - request::send_request_by_id(ctx, request_id, environment, verbose).await, + request::send_request_by_id( + ctx, + request_id, + environment, + cookie_jar_id, + verbose, + ) + .await, ) }) .collect::>(); diff --git a/crates-cli/yaak-cli/src/context.rs b/crates-cli/yaak-cli/src/context.rs index c0750008..54205db7 100644 --- a/crates-cli/yaak-cli/src/context.rs +++ b/crates-cli/yaak-cli/src/context.rs @@ -18,6 +18,14 @@ const EMBEDDED_PLUGIN_RUNTIME: &str = include_str!(concat!( static EMBEDDED_VENDORED_PLUGINS: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../crates-tauri/yaak-app/vendored/plugins"); +#[derive(Clone, Debug, Default)] +pub struct CliExecutionContext { + pub request_id: Option, + pub workspace_id: Option, + pub environment_id: Option, + pub cookie_jar_id: Option, +} + pub struct CliContext { data_dir: PathBuf, query_manager: QueryManager, @@ -28,15 +36,14 @@ pub struct CliContext { } impl CliContext { - pub async fn initialize(data_dir: PathBuf, app_id: &str, with_plugins: bool) -> Self { - let db_path = data_dir.join("db.sqlite"); - let blob_path = data_dir.join("blobs.sqlite"); - - let (query_manager, blob_manager, _rx) = yaak_models::init_standalone(&db_path, &blob_path) - .expect("Failed to initialize database"); - - let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id)); - + pub async fn new( + data_dir: PathBuf, + query_manager: QueryManager, + blob_manager: BlobManager, + encryption_manager: Arc, + with_plugins: bool, + execution_context: CliExecutionContext, + ) -> Self { let plugin_manager = if with_plugins { let vendored_plugin_dir = data_dir.join("vendored-plugins"); let installed_plugin_dir = data_dir.join("installed-plugins"); @@ -80,6 +87,7 @@ impl CliContext { blob_manager.clone(), encryption_manager.clone(), data_dir.clone(), + execution_context.clone(), ) .await, ) diff --git a/crates-cli/yaak-cli/src/main.rs b/crates-cli/yaak-cli/src/main.rs index 26a8fa23..cc5e6e21 100644 --- a/crates-cli/yaak-cli/src/main.rs +++ b/crates-cli/yaak-cli/src/main.rs @@ -9,11 +9,15 @@ mod version_check; use clap::Parser; use cli::{Cli, Commands, RequestCommands}; -use context::CliContext; +use context::{CliContext, CliExecutionContext}; +use std::sync::Arc; +use yaak_crypto::manager::EncryptionManager; +use yaak_models::queries::any_request::AnyRequest; +use yaak_models::query_manager::QueryManager; #[tokio::main] async fn main() { - let Cli { data_dir, environment, verbose, log, command } = Cli::parse(); + let Cli { data_dir, environment, cookie_jar, verbose, log, command } = Cli::parse(); if let Some(log_level) = log { match log_level { @@ -35,28 +39,16 @@ async fn main() { version_check::maybe_check_for_updates().await; - let needs_context = matches!( - &command, - Commands::Send(_) - | Commands::Workspace(_) - | Commands::Request(_) - | Commands::Folder(_) - | Commands::Environment(_) - ); - - let needs_plugins = matches!( - &command, - Commands::Send(_) - | Commands::Request(cli::RequestArgs { - command: RequestCommands::Send { .. } | RequestCommands::Schema { .. }, - }) - ); - - let context = if needs_context { - Some(CliContext::initialize(data_dir, app_id, needs_plugins).await) - } else { - None - }; + let db_path = data_dir.join("db.sqlite"); + let blob_path = data_dir.join("blobs.sqlite"); + let (query_manager, blob_manager, _rx) = + match yaak_models::init_standalone(&db_path, &blob_path) { + Ok(v) => v, + Err(err) => { + eprintln!("Error: Failed to initialize database: {err}"); + std::process::exit(1); + } + }; let exit_code = match command { Commands::Auth(args) => commands::auth::run(args).await, @@ -66,41 +58,258 @@ async fn main() { Commands::Generate(args) => commands::plugin::run_generate(args).await, Commands::Publish(args) => commands::plugin::run_publish(args).await, Commands::Send(args) => { - commands::send::run( - context.as_ref().expect("context initialized for send"), - args, + let query_manager = query_manager.clone(); + let blob_manager = blob_manager.clone(); + + let execution_context_result = resolve_send_execution_context( + &query_manager, + &args.id, environment.as_deref(), - verbose, - ) - .await + cookie_jar.as_deref(), + ); + match execution_context_result { + Ok(execution_context) => { + let context = CliContext::new( + data_dir.clone(), + query_manager.clone(), + blob_manager, + Arc::new(EncryptionManager::new(query_manager, app_id)), + true, + execution_context, + ) + .await; + + let exit_code = commands::send::run( + &context, + args, + environment.as_deref(), + cookie_jar.as_deref(), + verbose, + ) + .await; + context.shutdown().await; + exit_code + } + Err(error) => { + eprintln!("Error: {error}"); + 1 + } + } } - Commands::Workspace(args) => commands::workspace::run( - context.as_ref().expect("context initialized for workspace"), - args, - ), - Commands::Request(args) => { - commands::request::run( - context.as_ref().expect("context initialized for request"), - args, - environment.as_deref(), - verbose, + Commands::CookieJar(args) => { + let query_manager = query_manager.clone(); + let blob_manager = blob_manager.clone(); + let execution_context = CliExecutionContext::default(); + + let context = CliContext::new( + data_dir.clone(), + query_manager.clone(), + blob_manager, + Arc::new(EncryptionManager::new(query_manager, app_id)), + false, + execution_context, ) - .await + .await; + let exit_code = commands::cookie_jar::run(&context, args); + context.shutdown().await; + exit_code + } + Commands::Workspace(args) => { + let query_manager = query_manager.clone(); + let blob_manager = blob_manager.clone(); + let execution_context = CliExecutionContext::default(); + + let context = CliContext::new( + data_dir.clone(), + query_manager.clone(), + blob_manager, + Arc::new(EncryptionManager::new(query_manager, app_id)), + false, + execution_context, + ) + .await; + let exit_code = commands::workspace::run(&context, args); + context.shutdown().await; + exit_code + } + Commands::Request(args) => { + let query_manager = query_manager.clone(); + let blob_manager = blob_manager.clone(); + + let execution_context_result = match &args.command { + RequestCommands::Send { request_id } => resolve_request_execution_context( + &query_manager, + request_id, + environment.as_deref(), + cookie_jar.as_deref(), + ), + _ => Ok(CliExecutionContext::default()), + }; + match execution_context_result { + Ok(execution_context) => { + let with_plugins = matches!( + &args.command, + RequestCommands::Send { .. } | RequestCommands::Schema { .. } + ); + let context = CliContext::new( + data_dir.clone(), + query_manager.clone(), + blob_manager, + Arc::new(EncryptionManager::new(query_manager, app_id)), + with_plugins, + execution_context, + ) + .await; + + let exit_code = commands::request::run( + &context, + args, + environment.as_deref(), + cookie_jar.as_deref(), + verbose, + ) + .await; + context.shutdown().await; + exit_code + } + Err(error) => { + eprintln!("Error: {error}"); + 1 + } + } } Commands::Folder(args) => { - commands::folder::run(context.as_ref().expect("context initialized for folder"), args) - } - Commands::Environment(args) => commands::environment::run( - context.as_ref().expect("context initialized for environment"), - args, - ), - }; + let query_manager = query_manager.clone(); + let blob_manager = blob_manager.clone(); + let execution_context = CliExecutionContext::default(); - if let Some(context) = &context { - context.shutdown().await; - } + let context = CliContext::new( + data_dir.clone(), + query_manager.clone(), + blob_manager, + Arc::new(EncryptionManager::new(query_manager, app_id)), + false, + execution_context, + ) + .await; + let exit_code = commands::folder::run(&context, args); + context.shutdown().await; + exit_code + } + Commands::Environment(args) => { + let query_manager = query_manager.clone(); + let blob_manager = blob_manager.clone(); + let execution_context = CliExecutionContext::default(); + + let context = CliContext::new( + data_dir.clone(), + query_manager.clone(), + blob_manager, + Arc::new(EncryptionManager::new(query_manager, app_id)), + false, + execution_context, + ) + .await; + let exit_code = commands::environment::run(&context, args); + context.shutdown().await; + exit_code + } + }; if exit_code != 0 { std::process::exit(exit_code); } } + +fn resolve_send_execution_context( + query_manager: &QueryManager, + id: &str, + environment: Option<&str>, + explicit_cookie_jar_id: Option<&str>, +) -> Result { + if let Ok(request) = query_manager.connect().get_any_request(id) { + let (request_id, workspace_id) = match request { + AnyRequest::HttpRequest(r) => (Some(r.id), r.workspace_id), + AnyRequest::GrpcRequest(r) => (Some(r.id), r.workspace_id), + AnyRequest::WebsocketRequest(r) => (Some(r.id), r.workspace_id), + }; + let cookie_jar_id = + resolve_cookie_jar_id(query_manager, &workspace_id, explicit_cookie_jar_id)?; + return Ok(CliExecutionContext { + request_id, + workspace_id: Some(workspace_id), + environment_id: environment.map(str::to_string), + cookie_jar_id, + }); + } + + if let Ok(folder) = query_manager.connect().get_folder(id) { + let cookie_jar_id = + resolve_cookie_jar_id(query_manager, &folder.workspace_id, explicit_cookie_jar_id)?; + return Ok(CliExecutionContext { + request_id: None, + workspace_id: Some(folder.workspace_id), + environment_id: environment.map(str::to_string), + cookie_jar_id, + }); + } + + if let Ok(workspace) = query_manager.connect().get_workspace(id) { + let cookie_jar_id = + resolve_cookie_jar_id(query_manager, &workspace.id, explicit_cookie_jar_id)?; + return Ok(CliExecutionContext { + request_id: None, + workspace_id: Some(workspace.id), + environment_id: environment.map(str::to_string), + cookie_jar_id, + }); + } + + Err(format!("Could not resolve ID '{}' as request, folder, or workspace", id)) +} + +fn resolve_request_execution_context( + query_manager: &QueryManager, + request_id: &str, + environment: Option<&str>, + explicit_cookie_jar_id: Option<&str>, +) -> Result { + let request = query_manager + .connect() + .get_any_request(request_id) + .map_err(|e| format!("Failed to get request: {e}"))?; + + let workspace_id = match request { + AnyRequest::HttpRequest(r) => r.workspace_id, + AnyRequest::GrpcRequest(r) => r.workspace_id, + AnyRequest::WebsocketRequest(r) => r.workspace_id, + }; + let cookie_jar_id = + resolve_cookie_jar_id(query_manager, &workspace_id, explicit_cookie_jar_id)?; + + Ok(CliExecutionContext { + request_id: Some(request_id.to_string()), + workspace_id: Some(workspace_id), + environment_id: environment.map(str::to_string), + cookie_jar_id, + }) +} + +fn resolve_cookie_jar_id( + query_manager: &QueryManager, + workspace_id: &str, + explicit_cookie_jar_id: Option<&str>, +) -> Result, String> { + if let Some(cookie_jar_id) = explicit_cookie_jar_id { + return Ok(Some(cookie_jar_id.to_string())); + } + + let default_cookie_jar = query_manager + .connect() + .list_cookie_jars(workspace_id) + .map_err(|e| format!("Failed to list cookie jars: {e}"))? + .into_iter() + .min_by_key(|jar| jar.created_at) + .map(|jar| jar.id); + Ok(default_cookie_jar) +} diff --git a/crates-cli/yaak-cli/src/plugin_events.rs b/crates-cli/yaak-cli/src/plugin_events.rs index 29bff774..f0f730af 100644 --- a/crates-cli/yaak-cli/src/plugin_events.rs +++ b/crates-cli/yaak-cli/src/plugin_events.rs @@ -1,3 +1,4 @@ +use crate::context::CliExecutionContext; use serde_json::Value; use std::collections::BTreeMap; use std::path::PathBuf; @@ -15,9 +16,10 @@ use yaak_models::query_manager::QueryManager; use yaak_models::render::make_vars_hashmap; use yaak_models::util::UpdateSource; use yaak_plugins::events::{ - EmptyPayload, ErrorResponse, InternalEvent, InternalEventPayload, ListOpenWorkspacesResponse, - RenderGrpcRequestResponse, RenderHttpRequestResponse, SendHttpRequestResponse, - TemplateRenderResponse, WorkspaceInfo, + EmptyPayload, ErrorResponse, GetCookieValueResponse, InternalEvent, InternalEventPayload, + ListCookieNamesResponse, ListOpenWorkspacesResponse, PluginContext, RenderGrpcRequestResponse, + RenderHttpRequestResponse, SendHttpRequestResponse, TemplateRenderResponse, WindowInfoResponse, + WorkspaceInfo, }; use yaak_plugins::manager::PluginManager; use yaak_plugins::template_callback::PluginTemplateCallback; @@ -34,6 +36,7 @@ struct CliHostContext { plugin_manager: Arc, encryption_manager: Arc, response_dir: PathBuf, + execution_context: CliExecutionContext, } impl CliPluginEventBridge { @@ -43,6 +46,7 @@ impl CliPluginEventBridge { blob_manager: BlobManager, encryption_manager: Arc, data_dir: PathBuf, + execution_context: CliExecutionContext, ) -> Self { let (rx_id, mut rx) = plugin_manager.subscribe("cli").await; let rx_id_for_task = rx_id.clone(); @@ -53,6 +57,7 @@ impl CliPluginEventBridge { plugin_manager, encryption_manager, response_dir: data_dir.join("responses"), + execution_context, }); let task = tokio::spawn(async move { @@ -109,13 +114,14 @@ async fn build_plugin_reply( event: &InternalEvent, plugin_name: &str, ) -> Option { + let execution_context = &host_context.execution_context; + let shared_workspace_id = + event.context.workspace_id.as_deref().or(execution_context.workspace_id.as_deref()); + match handle_shared_plugin_event( &host_context.query_manager, &event.payload, - SharedPluginEventContext { - plugin_name, - workspace_id: event.context.workspace_id.as_deref(), - }, + SharedPluginEventContext { plugin_name, workspace_id: shared_workspace_id }, ) { GroupedPluginEvent::Handled(payload) => payload, GroupedPluginEvent::ToHandle(host_request) => match host_request { @@ -147,7 +153,12 @@ async fn build_plugin_reply( HostRequest::SendHttpRequest(send_http_request_request) => { let mut http_request = send_http_request_request.http_request.clone(); if http_request.workspace_id.is_empty() { - let Some(workspace_id) = event.context.workspace_id.clone() else { + let Some(workspace_id) = event + .context + .workspace_id + .clone() + .or_else(|| execution_context.workspace_id.clone()) + else { return Some(InternalEventPayload::ErrorResponse(ErrorResponse { error: "workspace_id is required to send HTTP requests in CLI" .to_string(), @@ -156,18 +167,38 @@ async fn build_plugin_reply( http_request.workspace_id = workspace_id; } - let mut plugin_context = event.context.clone(); - if plugin_context.workspace_id.is_none() { - plugin_context.workspace_id = Some(http_request.workspace_id.clone()); - } + let cookie_jar_id = + if let Some(cookie_jar_id) = execution_context.cookie_jar_id.clone() { + Some(cookie_jar_id) + } else { + match host_context + .query_manager + .connect() + .list_cookie_jars(http_request.workspace_id.as_str()) + { + Ok(cookie_jars) => cookie_jars + .into_iter() + .min_by_key(|jar| jar.created_at) + .map(|jar| jar.id), + Err(err) => { + return Some(InternalEventPayload::ErrorResponse(ErrorResponse { + error: format!("Failed to list cookie jars in CLI: {err}"), + })); + } + } + }; + let plugin_context = PluginContext { + workspace_id: Some(http_request.workspace_id.clone()), + ..event.context.clone() + }; match send_http_request_with_plugins(SendHttpRequestWithPluginsParams { query_manager: &host_context.query_manager, blob_manager: &host_context.blob_manager, request: http_request, - environment_id: None, + environment_id: execution_context.environment_id.as_deref(), update_source: UpdateSource::Plugin, - cookie_jar_id: None, + cookie_jar_id, response_dir: &host_context.response_dir, emit_events_to: None, emit_response_body_chunks_to: None, @@ -191,7 +222,12 @@ async fn build_plugin_reply( HostRequest::RenderGrpcRequest(render_grpc_request_request) => { let mut grpc_request = render_grpc_request_request.grpc_request.clone(); if grpc_request.workspace_id.is_empty() { - let Some(workspace_id) = event.context.workspace_id.clone() else { + let Some(workspace_id) = event + .context + .workspace_id + .clone() + .or_else(|| execution_context.workspace_id.clone()) + else { return Some(InternalEventPayload::ErrorResponse(ErrorResponse { error: "workspace_id is required to render gRPC requests in CLI" .to_string(), @@ -200,16 +236,16 @@ async fn build_plugin_reply( grpc_request.workspace_id = workspace_id; } - let mut plugin_context = event.context.clone(); - if plugin_context.workspace_id.is_none() { - plugin_context.workspace_id = Some(grpc_request.workspace_id.clone()); - } + let plugin_context = PluginContext { + workspace_id: Some(grpc_request.workspace_id.clone()), + ..event.context.clone() + }; let environment_chain = match host_context.query_manager.connect().resolve_environments( &grpc_request.workspace_id, grpc_request.folder_id.as_deref(), - None, + execution_context.environment_id.as_deref(), ) { Ok(chain) => chain, Err(err) => { @@ -246,7 +282,12 @@ async fn build_plugin_reply( HostRequest::RenderHttpRequest(render_http_request_request) => { let mut http_request = render_http_request_request.http_request.clone(); if http_request.workspace_id.is_empty() { - let Some(workspace_id) = event.context.workspace_id.clone() else { + let Some(workspace_id) = event + .context + .workspace_id + .clone() + .or_else(|| execution_context.workspace_id.clone()) + else { return Some(InternalEventPayload::ErrorResponse(ErrorResponse { error: "workspace_id is required to render HTTP requests in CLI" .to_string(), @@ -255,16 +296,16 @@ async fn build_plugin_reply( http_request.workspace_id = workspace_id; } - let mut plugin_context = event.context.clone(); - if plugin_context.workspace_id.is_none() { - plugin_context.workspace_id = Some(http_request.workspace_id.clone()); - } + let plugin_context = PluginContext { + workspace_id: Some(http_request.workspace_id.clone()), + ..event.context.clone() + }; let environment_chain = match host_context.query_manager.connect().resolve_environments( &http_request.workspace_id, http_request.folder_id.as_deref(), - None, + execution_context.environment_id.as_deref(), ) { Ok(chain) => chain, Err(err) => { @@ -299,30 +340,36 @@ async fn build_plugin_reply( } } HostRequest::TemplateRender(template_render_request) => { - let Some(workspace_id) = event.context.workspace_id.clone() else { + let Some(workspace_id) = event + .context + .workspace_id + .clone() + .or_else(|| execution_context.workspace_id.clone()) + else { return Some(InternalEventPayload::ErrorResponse(ErrorResponse { error: "workspace_id is required to render templates in CLI".to_string(), })); }; - let mut plugin_context = event.context.clone(); - if plugin_context.workspace_id.is_none() { - plugin_context.workspace_id = Some(workspace_id.clone()); - } - - let environment_chain = match host_context - .query_manager - .connect() - .resolve_environments(&workspace_id, None, None) - { - Ok(chain) => chain, - Err(err) => { - return Some(InternalEventPayload::ErrorResponse(ErrorResponse { - error: format!("Failed to resolve environments in CLI: {err}"), - })); - } + let plugin_context = PluginContext { + workspace_id: Some(workspace_id.clone()), + ..event.context.clone() }; + let environment_chain = + match host_context.query_manager.connect().resolve_environments( + &workspace_id, + None, + execution_context.environment_id.as_deref(), + ) { + Ok(chain) => chain, + Err(err) => { + return Some(InternalEventPayload::ErrorResponse(ErrorResponse { + error: format!("Failed to resolve environments in CLI: {err}"), + })); + } + }; + let template_callback = PluginTemplateCallback::new( host_context.plugin_manager.clone(), host_context.encryption_manager.clone(), @@ -381,20 +428,64 @@ async fn build_plugin_reply( })) } HostRequest::ListCookieNames(_) => { - Some(InternalEventPayload::ErrorResponse(ErrorResponse { - error: "Unsupported plugin request in CLI: list_cookie_names_request" - .to_string(), + let Some(cookie_jar_id) = execution_context.cookie_jar_id.as_deref() else { + return Some(InternalEventPayload::ListCookieNamesResponse( + ListCookieNamesResponse { names: Vec::new() }, + )); + }; + + let cookie_jar = + match host_context.query_manager.connect().get_cookie_jar(cookie_jar_id) { + Ok(cookie_jar) => cookie_jar, + Err(err) => { + return Some(InternalEventPayload::ErrorResponse(ErrorResponse { + error: format!("Failed to load cookie jar in CLI: {err}"), + })); + } + }; + + let names = cookie_jar + .cookies + .into_iter() + .filter_map(|c| parse_cookie_name_value(&c.raw_cookie).map(|(name, _)| name)) + .collect(); + + Some(InternalEventPayload::ListCookieNamesResponse(ListCookieNamesResponse { + names, })) } - HostRequest::GetCookieValue(_) => { - Some(InternalEventPayload::ErrorResponse(ErrorResponse { - error: "Unsupported plugin request in CLI: get_cookie_value_request" - .to_string(), - })) + HostRequest::GetCookieValue(req) => { + let Some(cookie_jar_id) = execution_context.cookie_jar_id.as_deref() else { + return Some(InternalEventPayload::GetCookieValueResponse( + GetCookieValueResponse { value: None }, + )); + }; + + let cookie_jar = + match host_context.query_manager.connect().get_cookie_jar(cookie_jar_id) { + Ok(cookie_jar) => cookie_jar, + Err(err) => { + return Some(InternalEventPayload::ErrorResponse(ErrorResponse { + error: format!("Failed to load cookie jar in CLI: {err}"), + })); + } + }; + + let value = cookie_jar.cookies.into_iter().find_map(|c| { + let (name, value) = parse_cookie_name_value(&c.raw_cookie)?; + if name == req.name { Some(value) } else { None } + }); + Some(InternalEventPayload::GetCookieValueResponse(GetCookieValueResponse { value })) } - HostRequest::WindowInfo(_) => { - Some(InternalEventPayload::ErrorResponse(ErrorResponse { - error: "Unsupported plugin request in CLI: window_info_request".to_string(), + HostRequest::WindowInfo(req) => { + Some(InternalEventPayload::WindowInfoResponse(WindowInfoResponse { + label: req.label.clone(), + request_id: execution_context.request_id.clone(), + workspace_id: execution_context + .workspace_id + .clone() + .or_else(|| event.context.workspace_id.clone()), + environment_id: execution_context.environment_id.clone(), })) } HostRequest::OtherRequest(payload) => { @@ -470,3 +561,9 @@ async fn render_grpc_request_for_cli( Ok(GrpcRequest { url, metadata, authentication, ..grpc_request.to_owned() }) } + +fn parse_cookie_name_value(raw_cookie: &str) -> Option<(String, String)> { + let first_part = raw_cookie.split(';').next()?.trim(); + let (name, value) = first_part.split_once('=')?; + Some((name.trim().to_string(), value.to_string())) +} diff --git a/crates-cli/yaak-cli/src/utils/mod.rs b/crates-cli/yaak-cli/src/utils/mod.rs index 0707af26..f8932a62 100644 --- a/crates-cli/yaak-cli/src/utils/mod.rs +++ b/crates-cli/yaak-cli/src/utils/mod.rs @@ -2,3 +2,4 @@ pub mod confirm; pub mod http; pub mod json; pub mod schema; +pub mod workspace; diff --git a/crates-cli/yaak-cli/src/utils/workspace.rs b/crates-cli/yaak-cli/src/utils/workspace.rs new file mode 100644 index 00000000..a1f54693 --- /dev/null +++ b/crates-cli/yaak-cli/src/utils/workspace.rs @@ -0,0 +1,19 @@ +use crate::context::CliContext; + +pub fn resolve_workspace_id( + ctx: &CliContext, + workspace_id: Option<&str>, + command_name: &str, +) -> Result { + if let Some(workspace_id) = workspace_id { + return Ok(workspace_id.to_string()); + } + + let workspaces = + ctx.db().list_workspaces().map_err(|e| format!("Failed to list workspaces: {e}"))?; + match workspaces.as_slice() { + [] => Err(format!("No workspaces found. {command_name} requires a workspace ID.")), + [workspace] => Ok(workspace.id.clone()), + _ => Err(format!("Multiple workspaces found. {command_name} requires a workspace ID.")), + } +} diff --git a/crates/yaak-plugins/bindings/gen_events.ts b/crates/yaak-plugins/bindings/gen_events.ts index 57310aaa..ba130b7b 100644 --- a/crates/yaak-plugins/bindings/gen_events.ts +++ b/crates/yaak-plugins/bindings/gen_events.ts @@ -18,12 +18,12 @@ export type CallHttpAuthenticationActionRequest = { index: number, pluginRefId: export type CallHttpAuthenticationRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, method: string, url: string, headers: Array, }; -export type CallHttpAuthenticationResponse = { +export type CallHttpAuthenticationResponse = { /** * HTTP headers to add to the request. Existing headers will be replaced, while * new headers will be added. */ -setHeaders?: Array, +setHeaders?: Array, /** * Query parameters to add to the request. Existing params will be replaced, while * new params will be added. @@ -78,7 +78,7 @@ export type ExportHttpRequestRequest = { httpRequest: HttpRequest, }; export type ExportHttpRequestResponse = { content: string, }; -export type FileFilter = { name: string, +export type FileFilter = { name: string, /** * File extensions to require */ @@ -100,149 +100,149 @@ export type FormInputAccordion = { label: string, inputs?: Array, hid export type FormInputBanner = { inputs?: Array, hidden?: boolean, color?: Color, }; -export type FormInputBase = { +export type FormInputBase = { /** * The name of the input. The value will be stored at this object attribute in the resulting data */ -name: string, +name: string, /** * Whether this input is visible for the given configuration. Use this to * make branching forms. */ -hidden?: boolean, +hidden?: boolean, /** * Whether the user must fill in the argument */ -optional?: boolean, +optional?: boolean, /** * The label of the input */ -label?: string, +label?: string, /** * Visually hide the label of the input */ -hideLabel?: boolean, +hideLabel?: boolean, /** * The default value */ -defaultValue?: string, disabled?: boolean, +defaultValue?: string, disabled?: boolean, /** * Longer description of the input, likely shown in a tooltip */ description?: string, }; -export type FormInputCheckbox = { +export type FormInputCheckbox = { /** * The name of the input. The value will be stored at this object attribute in the resulting data */ -name: string, +name: string, /** * Whether this input is visible for the given configuration. Use this to * make branching forms. */ -hidden?: boolean, +hidden?: boolean, /** * Whether the user must fill in the argument */ -optional?: boolean, +optional?: boolean, /** * The label of the input */ -label?: string, +label?: string, /** * Visually hide the label of the input */ -hideLabel?: boolean, +hideLabel?: boolean, /** * The default value */ -defaultValue?: string, disabled?: boolean, +defaultValue?: string, disabled?: boolean, /** * Longer description of the input, likely shown in a tooltip */ description?: string, }; -export type FormInputEditor = { +export type FormInputEditor = { /** * Placeholder for the text input */ -placeholder?: string | null, +placeholder?: string | null, /** * Don't show the editor gutter (line numbers, folds, etc.) */ -hideGutter?: boolean, +hideGutter?: boolean, /** * Language for syntax highlighting */ -language?: EditorLanguage, readOnly?: boolean, +language?: EditorLanguage, readOnly?: boolean, /** * Fixed number of visible rows */ -rows?: number, completionOptions?: Array, +rows?: number, completionOptions?: Array, /** * The name of the input. The value will be stored at this object attribute in the resulting data */ -name: string, +name: string, /** * Whether this input is visible for the given configuration. Use this to * make branching forms. */ -hidden?: boolean, +hidden?: boolean, /** * Whether the user must fill in the argument */ -optional?: boolean, +optional?: boolean, /** * The label of the input */ -label?: string, +label?: string, /** * Visually hide the label of the input */ -hideLabel?: boolean, +hideLabel?: boolean, /** * The default value */ -defaultValue?: string, disabled?: boolean, +defaultValue?: string, disabled?: boolean, /** * Longer description of the input, likely shown in a tooltip */ description?: string, }; -export type FormInputFile = { +export type FormInputFile = { /** * The title of the file selection window */ -title: string, +title: string, /** * Allow selecting multiple files */ -multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array, +multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array, /** * The name of the input. The value will be stored at this object attribute in the resulting data */ -name: string, +name: string, /** * Whether this input is visible for the given configuration. Use this to * make branching forms. */ -hidden?: boolean, +hidden?: boolean, /** * Whether the user must fill in the argument */ -optional?: boolean, +optional?: boolean, /** * The label of the input */ -label?: string, +label?: string, /** * Visually hide the label of the input */ -hideLabel?: boolean, +hideLabel?: boolean, /** * The default value */ -defaultValue?: string, disabled?: boolean, +defaultValue?: string, disabled?: boolean, /** * Longer description of the input, likely shown in a tooltip */ @@ -250,63 +250,63 @@ description?: string, }; export type FormInputHStack = { inputs?: Array, hidden?: boolean, }; -export type FormInputHttpRequest = { +export type FormInputHttpRequest = { /** * The name of the input. The value will be stored at this object attribute in the resulting data */ -name: string, +name: string, /** * Whether this input is visible for the given configuration. Use this to * make branching forms. */ -hidden?: boolean, +hidden?: boolean, /** * Whether the user must fill in the argument */ -optional?: boolean, +optional?: boolean, /** * The label of the input */ -label?: string, +label?: string, /** * Visually hide the label of the input */ -hideLabel?: boolean, +hideLabel?: boolean, /** * The default value */ -defaultValue?: string, disabled?: boolean, +defaultValue?: string, disabled?: boolean, /** * Longer description of the input, likely shown in a tooltip */ description?: string, }; -export type FormInputKeyValue = { +export type FormInputKeyValue = { /** * The name of the input. The value will be stored at this object attribute in the resulting data */ -name: string, +name: string, /** * Whether this input is visible for the given configuration. Use this to * make branching forms. */ -hidden?: boolean, +hidden?: boolean, /** * Whether the user must fill in the argument */ -optional?: boolean, +optional?: boolean, /** * The label of the input */ -label?: string, +label?: string, /** * Visually hide the label of the input */ -hideLabel?: boolean, +hideLabel?: boolean, /** * The default value */ -defaultValue?: string, disabled?: boolean, +defaultValue?: string, disabled?: boolean, /** * Longer description of the input, likely shown in a tooltip */ @@ -314,36 +314,36 @@ description?: string, }; export type FormInputMarkdown = { content: string, hidden?: boolean, }; -export type FormInputSelect = { +export type FormInputSelect = { /** * The options that will be available in the select input */ -options: Array, +options: Array, /** * The name of the input. The value will be stored at this object attribute in the resulting data */ -name: string, +name: string, /** * Whether this input is visible for the given configuration. Use this to * make branching forms. */ -hidden?: boolean, +hidden?: boolean, /** * Whether the user must fill in the argument */ -optional?: boolean, +optional?: boolean, /** * The label of the input */ -label?: string, +label?: string, /** * Visually hide the label of the input */ -hideLabel?: boolean, +hideLabel?: boolean, /** * The default value */ -defaultValue?: string, disabled?: boolean, +defaultValue?: string, disabled?: boolean, /** * Longer description of the input, likely shown in a tooltip */ @@ -351,44 +351,44 @@ description?: string, }; export type FormInputSelectOption = { label: string, value: string, }; -export type FormInputText = { +export type FormInputText = { /** * Placeholder for the text input */ -placeholder?: string | null, +placeholder?: string | null, /** * Placeholder for the text input */ -password?: boolean, +password?: boolean, /** * Whether to allow newlines in the input, like a