Compare commits

...

5 Commits

Author SHA1 Message Date
Gregory Schier
2439ffd28c chore: remove prompt form CLI test plugin scaffold 2026-03-03 16:01:25 -08:00
Gregory Schier
ad31755dbb feat(cli): add plugin install and complete prompt/render host events 2026-03-03 15:58:23 -08:00
Gregory Schier
b01b3a4c57 cli: implement plugin host render/send context and cookie jar support 2026-03-03 14:56:39 -08:00
Gregory Schier
ef63b88710 cli: add grpc/template render handlers and unsupported event errors 2026-03-03 09:00:14 -08:00
Gregory Schier
fb5ad8c7f7 cli: handle send/render http plugin host requests 2026-03-03 08:05:54 -08:00
18 changed files with 1846 additions and 313 deletions

125
Cargo.lock generated
View File

@@ -1200,7 +1200,7 @@ dependencies = [
"encode_unicode", "encode_unicode",
"libc", "libc",
"once_cell", "once_cell",
"unicode-width", "unicode-width 0.2.2",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
@@ -1405,6 +1405,31 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crossterm"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
dependencies = [
"bitflags 1.3.2",
"crossterm_winapi",
"libc",
"mio 0.8.11",
"parking_lot",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "crunchy" name = "crunchy"
version = "0.2.3" version = "0.2.3"
@@ -2294,6 +2319,15 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "fuzzy-matcher"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
dependencies = [
"thread_local 1.1.9",
]
[[package]] [[package]]
name = "fxhash" name = "fxhash"
version = "0.2.1" version = "0.2.1"
@@ -3164,6 +3198,24 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "inquire"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a"
dependencies = [
"bitflags 2.11.0",
"crossterm",
"dyn-clone",
"fuzzy-matcher",
"fxhash",
"newline-converter",
"once_cell",
"tempfile",
"unicode-segmentation",
"unicode-width 0.1.14",
]
[[package]] [[package]]
name = "interfaces" name = "interfaces"
version = "0.0.8" version = "0.0.8"
@@ -3756,6 +3808,18 @@ dependencies = [
"simd-adler32", "simd-adler32",
] ]
[[package]]
name = "mio"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log 0.4.29",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.0.4" version = "1.0.4"
@@ -3851,6 +3915,15 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "newline-converter"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "nibble_vec" name = "nibble_vec"
version = "0.1.0" version = "0.1.0"
@@ -3942,7 +4015,7 @@ dependencies = [
"kqueue", "kqueue",
"libc", "libc",
"log 0.4.29", "log 0.4.29",
"mio", "mio 1.0.4",
"notify-types", "notify-types",
"walkdir", "walkdir",
"windows-sys 0.59.0", "windows-sys 0.59.0",
@@ -4501,7 +4574,7 @@ dependencies = [
"textwrap", "textwrap",
"thiserror 2.0.17", "thiserror 2.0.17",
"unicode-segmentation", "unicode-segmentation",
"unicode-width", "unicode-width 0.2.2",
] ]
[[package]] [[package]]
@@ -6171,7 +6244,7 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77dff57c9de498bb1eb5b1ce682c2e3a0ae956b266fa0933c3e151b87b078967" checksum = "77dff57c9de498bb1eb5b1ce682c2e3a0ae956b266fa0933c3e151b87b078967"
dependencies = [ dependencies = [
"unicode-width", "unicode-width 0.2.2",
"yansi", "yansi",
] ]
@@ -6196,7 +6269,7 @@ dependencies = [
"kqueue", "kqueue",
"libc", "libc",
"log 0.4.29", "log 0.4.29",
"mio", "mio 1.0.4",
"rolldown-notify-types", "rolldown-notify-types",
"walkdir", "walkdir",
"windows-sys 0.61.2", "windows-sys 0.61.2",
@@ -7173,6 +7246,27 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-mio"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
dependencies = [
"libc",
"mio 0.8.11",
"signal-hook",
]
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.5" version = "1.4.5"
@@ -8068,7 +8162,7 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
dependencies = [ dependencies = [
"smawk", "smawk",
"unicode-linebreak", "unicode-linebreak",
"unicode-width", "unicode-width 0.2.2",
] ]
[[package]] [[package]]
@@ -8215,7 +8309,7 @@ checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
dependencies = [ dependencies = [
"bytes", "bytes",
"libc", "libc",
"mio", "mio 1.0.4",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"socket2 0.6.1", "socket2 0.6.1",
@@ -8785,6 +8879,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.2.2" version = "0.2.2"
@@ -9562,6 +9662,15 @@ dependencies = [
"windows-targets 0.42.2", "windows-targets 0.42.2",
] ]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"
@@ -10141,6 +10250,7 @@ dependencies = [
name = "yaak-cli" name = "yaak-cli"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"arboard",
"assert_cmd", "assert_cmd",
"base64 0.22.1", "base64 0.22.1",
"clap", "clap",
@@ -10150,6 +10260,7 @@ dependencies = [
"futures", "futures",
"hex", "hex",
"include_dir", "include_dir",
"inquire",
"keyring", "keyring",
"log 0.4.29", "log 0.4.29",
"oxc_resolver", "oxc_resolver",

View File

@@ -9,12 +9,14 @@ name = "yaak"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
arboard = "3"
base64 = "0.22" base64 = "0.22"
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
console = "0.15" console = "0.15"
dirs = "6" dirs = "6"
env_logger = "0.11" env_logger = "0.11"
futures = "0.3" futures = "0.3"
inquire = { version = "0.7", features = ["editor"] }
hex = { workspace = true } hex = { workspace = true }
include_dir = "0.7" include_dir = "0.7"
keyring = { workspace = true, features = ["apple-native", "windows-native", "sync-secret-service"] } keyring = { workspace = true, features = ["apple-native", "windows-native", "sync-secret-service"] }

View File

@@ -21,6 +21,10 @@ pub struct Cli {
#[arg(long, short, global = true)] #[arg(long, short, global = true)]
pub environment: Option<String>, pub environment: Option<String>,
/// Cookie jar ID to use when sending requests
#[arg(long = "cookie-jar", global = true, value_name = "COOKIE_JAR_ID")]
pub cookie_jar: Option<String>,
/// Enable verbose send output (events and streamed response body) /// Enable verbose send output (events and streamed response body)
#[arg(long, short, global = true)] #[arg(long, short, global = true)]
pub verbose: bool, pub verbose: bool,
@@ -58,6 +62,9 @@ pub enum Commands {
/// Send a request, folder, or workspace by ID /// Send a request, folder, or workspace by ID
Send(SendArgs), Send(SendArgs),
/// Cookie jar commands
CookieJar(CookieJarArgs),
/// Workspace commands /// Workspace commands
Workspace(WorkspaceArgs), Workspace(WorkspaceArgs),
@@ -85,6 +92,22 @@ pub struct SendArgs {
pub fail_fast: bool, 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<String>,
},
}
#[derive(Args)] #[derive(Args)]
#[command(disable_help_subcommand = true)] #[command(disable_help_subcommand = true)]
pub struct WorkspaceArgs { pub struct WorkspaceArgs {
@@ -158,8 +181,8 @@ pub struct RequestArgs {
pub enum RequestCommands { pub enum RequestCommands {
/// List requests in a workspace /// List requests in a workspace
List { List {
/// Workspace ID /// Workspace ID (optional when exactly one workspace exists)
workspace_id: String, workspace_id: Option<String>,
}, },
/// Show a request as JSON /// Show a request as JSON
@@ -267,8 +290,8 @@ pub struct FolderArgs {
pub enum FolderCommands { pub enum FolderCommands {
/// List folders in a workspace /// List folders in a workspace
List { List {
/// Workspace ID /// Workspace ID (optional when exactly one workspace exists)
workspace_id: String, workspace_id: Option<String>,
}, },
/// Show a folder as JSON /// Show a folder as JSON
@@ -324,8 +347,8 @@ pub struct EnvironmentArgs {
pub enum EnvironmentCommands { pub enum EnvironmentCommands {
/// List environments in a workspace /// List environments in a workspace
List { List {
/// Workspace ID /// Workspace ID (optional when exactly one workspace exists)
workspace_id: String, workspace_id: Option<String>,
}, },
/// Output JSON schema for environment create/update payloads /// Output JSON schema for environment create/update payloads
@@ -421,6 +444,9 @@ pub enum PluginCommands {
/// Generate a "Hello World" Yaak plugin /// Generate a "Hello World" Yaak plugin
Generate(GenerateArgs), Generate(GenerateArgs),
/// Install a plugin from a local directory or from the registry
Install(InstallPluginArgs),
/// Publish a Yaak plugin version to the plugin registry /// Publish a Yaak plugin version to the plugin registry
Publish(PluginPathArg), Publish(PluginPathArg),
} }
@@ -441,3 +467,9 @@ pub struct GenerateArgs {
#[arg(long)] #[arg(long)]
pub dir: Option<PathBuf>, pub dir: Option<PathBuf>,
} }
#[derive(Args, Clone)]
pub struct InstallPluginArgs {
/// Local plugin directory path, or registry plugin spec (@org/plugin[@version])
pub source: String,
}

View File

@@ -0,0 +1,42 @@
use crate::cli::{CookieJarArgs, CookieJarCommands};
use crate::context::CliContext;
use crate::utils::workspace::resolve_workspace_id;
type CommandResult<T = ()> = std::result::Result<T, String>;
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(())
}

View File

@@ -6,6 +6,7 @@ use crate::utils::json::{
parse_required_json, require_id, validate_create_id, parse_required_json, require_id, validate_create_id,
}; };
use crate::utils::schema::append_agent_hints; use crate::utils::schema::append_agent_hints;
use crate::utils::workspace::resolve_workspace_id;
use schemars::schema_for; use schemars::schema_for;
use yaak_models::models::Environment; use yaak_models::models::Environment;
use yaak_models::util::UpdateSource; use yaak_models::util::UpdateSource;
@@ -14,7 +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.as_deref()),
EnvironmentCommands::Schema { pretty } => schema(pretty), 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 } => {
@@ -45,10 +46,11 @@ fn schema(pretty: bool) -> CommandResult {
Ok(()) 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 let environments = ctx
.db() .db()
.list_environments_ensure_base(workspace_id) .list_environments_ensure_base(&workspace_id)
.map_err(|e| format!("Failed to list environments: {e}"))?; .map_err(|e| format!("Failed to list environments: {e}"))?;
if environments.is_empty() { if environments.is_empty() {
@@ -92,8 +94,14 @@ fn create(
validate_create_id(&payload, "environment")?; validate_create_id(&payload, "environment")?;
let mut environment: Environment = serde_json::from_value(payload) let mut environment: Environment = serde_json::from_value(payload)
.map_err(|e| format!("Failed to parse environment create JSON: {e}"))?; .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( merge_workspace_id_arg(
workspace_id_arg.as_deref(), workspace_id_arg.as_deref().or(fallback_workspace_id.as_deref()),
&mut environment.workspace_id, &mut environment.workspace_id,
"environment create", "environment create",
)?; )?;
@@ -111,9 +119,8 @@ fn create(
return Ok(()); return Ok(());
} }
let workspace_id = workspace_id_arg.ok_or_else(|| { let workspace_id =
"environment create requires workspace_id unless JSON payload is provided".to_string() resolve_workspace_id(ctx, workspace_id_arg.as_deref(), "environment create")?;
})?;
let name = name.ok_or_else(|| { let name = name.ok_or_else(|| {
"environment create requires --name unless JSON payload is provided".to_string() "environment create requires --name unless JSON payload is provided".to_string()
})?; })?;

View File

@@ -5,6 +5,7 @@ use crate::utils::json::{
apply_merge_patch, is_json_shorthand, merge_workspace_id_arg, parse_optional_json, apply_merge_patch, is_json_shorthand, merge_workspace_id_arg, parse_optional_json,
parse_required_json, require_id, validate_create_id, parse_required_json, require_id, validate_create_id,
}; };
use crate::utils::workspace::resolve_workspace_id;
use yaak_models::models::Folder; use yaak_models::models::Folder;
use yaak_models::util::UpdateSource; use yaak_models::util::UpdateSource;
@@ -12,7 +13,7 @@ type CommandResult<T = ()> = std::result::Result<T, String>;
pub fn run(ctx: &CliContext, args: FolderArgs) -> i32 { pub fn run(ctx: &CliContext, args: FolderArgs) -> i32 {
let result = match args.command { 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::Show { folder_id } => show(ctx, &folder_id),
FolderCommands::Create { workspace_id, name, json } => { FolderCommands::Create { workspace_id, name, json } => {
create(ctx, 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 = 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() { if folders.is_empty() {
println!("No folders found in workspace {}", workspace_id); println!("No folders found in workspace {}", workspace_id);
} else { } else {
@@ -72,8 +74,14 @@ fn create(
validate_create_id(&payload, "folder")?; validate_create_id(&payload, "folder")?;
let mut folder: Folder = serde_json::from_value(payload) let mut folder: Folder = serde_json::from_value(payload)
.map_err(|e| format!("Failed to parse folder create JSON: {e}"))?; .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( merge_workspace_id_arg(
workspace_id_arg.as_deref(), workspace_id_arg.as_deref().or(fallback_workspace_id.as_deref()),
&mut folder.workspace_id, &mut folder.workspace_id,
"folder create", "folder create",
)?; )?;
@@ -87,9 +95,7 @@ fn create(
return Ok(()); return Ok(());
} }
let workspace_id = workspace_id_arg.ok_or_else(|| { let workspace_id = resolve_workspace_id(ctx, workspace_id_arg.as_deref(), "folder create")?;
"folder create requires workspace_id unless JSON payload is provided".to_string()
})?;
let name = name.ok_or_else(|| { let name = name.ok_or_else(|| {
"folder create requires --name unless JSON payload is provided".to_string() "folder create requires --name unless JSON payload is provided".to_string()
})?; })?;

View File

@@ -1,4 +1,5 @@
pub mod auth; pub mod auth;
pub mod cookie_jar;
pub mod environment; pub mod environment;
pub mod folder; pub mod folder;
pub mod plugin; pub mod plugin;

View File

@@ -1,4 +1,5 @@
use crate::cli::{GenerateArgs, PluginArgs, PluginCommands, PluginPathArg}; use crate::cli::{GenerateArgs, InstallPluginArgs, PluginPathArg};
use crate::context::CliContext;
use crate::ui; use crate::ui;
use crate::utils::http; use crate::utils::http;
use keyring::Entry; use keyring::Entry;
@@ -15,6 +16,11 @@ use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use walkdir::WalkDir; use walkdir::WalkDir;
use yaak_api::{ApiClientKind, yaak_api_client};
use yaak_models::models::{Plugin, PluginSource};
use yaak_models::util::UpdateSource;
use yaak_plugins::events::PluginContext;
use yaak_plugins::install::download_and_install;
use zip::CompressionMethod; use zip::CompressionMethod;
use zip::write::SimpleFileOptions; use zip::write::SimpleFileOptions;
@@ -57,12 +63,13 @@ pub async fn run_build(args: PluginPathArg) -> i32 {
} }
} }
pub async fn run(args: PluginArgs) -> i32 { pub async fn run_install(context: &CliContext, args: InstallPluginArgs) -> i32 {
match args.command { match install(context, args).await {
PluginCommands::Build(args) => run_build(args).await, Ok(()) => 0,
PluginCommands::Dev(args) => run_dev(args).await, Err(error) => {
PluginCommands::Generate(args) => run_generate(args).await, ui::error(&error);
PluginCommands::Publish(args) => run_publish(args).await, 1
}
} }
} }
@@ -250,6 +257,113 @@ async fn publish(args: PluginPathArg) -> CommandResult {
Ok(()) Ok(())
} }
async fn install(context: &CliContext, args: InstallPluginArgs) -> CommandResult {
if args.source.starts_with('@') {
let (name, version) =
parse_registry_install_spec(args.source.as_str()).ok_or_else(|| {
"Invalid registry plugin spec. Expected format: @org/plugin or @org/plugin@version"
.to_string()
})?;
return install_from_registry(context, name, version).await;
}
install_from_directory(context, args.source.as_str()).await
}
async fn install_from_registry(
context: &CliContext,
name: String,
version: Option<String>,
) -> CommandResult {
let current_version = crate::version::cli_version();
let http_client = yaak_api_client(ApiClientKind::Cli, current_version)
.map_err(|err| format!("Failed to initialize API client: {err}"))?;
let installing_version = version.clone().unwrap_or_else(|| "latest".to_string());
ui::info(&format!("Installing registry plugin {name}@{installing_version}"));
let plugin_context = PluginContext::new(Some("cli".to_string()), None);
let installed = download_and_install(
context.plugin_manager(),
context.query_manager(),
&http_client,
&plugin_context,
name.as_str(),
version,
)
.await
.map_err(|err| format!("Failed to install plugin: {err}"))?;
ui::success(&format!("Installed plugin {}@{}", installed.name, installed.version));
Ok(())
}
async fn install_from_directory(context: &CliContext, source: &str) -> CommandResult {
let plugin_dir = resolve_plugin_dir(Some(PathBuf::from(source)))?;
let plugin_dir_str = plugin_dir
.to_str()
.ok_or_else(|| {
format!("Plugin directory path is not valid UTF-8: {}", plugin_dir.display())
})?
.to_string();
ui::info(&format!("Installing plugin from directory {}", plugin_dir.display()));
let plugin = context
.db()
.upsert_plugin(
&Plugin {
directory: plugin_dir_str,
url: None,
enabled: true,
source: PluginSource::Filesystem,
..Default::default()
},
&UpdateSource::Background,
)
.map_err(|err| format!("Failed to save plugin in database: {err}"))?;
let plugin_context = PluginContext::new(Some("cli".to_string()), None);
context
.plugin_manager()
.add_plugin(&plugin_context, &plugin)
.await
.map_err(|err| format!("Failed to load plugin runtime: {err}"))?;
ui::success(&format!("Installed plugin from {}", plugin.directory));
Ok(())
}
fn parse_registry_install_spec(source: &str) -> Option<(String, Option<String>)> {
if !source.starts_with('@') || !source.contains('/') {
return None;
}
let rest = source.get(1..)?;
let version_split = rest.rfind('@').map(|idx| idx + 1);
let (name, version) = match version_split {
Some(at_idx) => {
let (name, version) = source.split_at(at_idx);
let version = version.strip_prefix('@').unwrap_or_default();
if version.is_empty() {
return None;
}
(name.to_string(), Some(version.to_string()))
}
None => (source.to_string(), None),
};
if !name.starts_with('@') {
return None;
}
let without_scope = name.get(1..)?;
let (scope, plugin_name) = without_scope.split_once('/')?;
if scope.is_empty() || plugin_name.is_empty() {
return None;
}
Some((name, version))
}
#[derive(Deserialize)] #[derive(Deserialize)]
struct PublishResponse { struct PublishResponse {
version: String, version: String,

View File

@@ -6,6 +6,7 @@ use crate::utils::json::{
parse_required_json, require_id, validate_create_id, parse_required_json, require_id, validate_create_id,
}; };
use crate::utils::schema::append_agent_hints; use crate::utils::schema::append_agent_hints;
use crate::utils::workspace::resolve_workspace_id;
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;
@@ -24,13 +25,16 @@ pub async fn run(
ctx: &CliContext, ctx: &CliContext,
args: RequestArgs, args: RequestArgs,
environment: Option<&str>, environment: Option<&str>,
cookie_jar_id: Option<&str>,
verbose: bool, verbose: bool,
) -> i32 { ) -> i32 {
let result = match args.command { 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::Show { request_id } => show(ctx, &request_id),
RequestCommands::Send { 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, Ok(()) => 0,
Err(error) => { Err(error) => {
eprintln!("Error: {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 let requests = ctx
.db() .db()
.list_http_requests(workspace_id) .list_http_requests(&workspace_id)
.map_err(|e| format!("Failed to list requests: {e}"))?; .map_err(|e| format!("Failed to list requests: {e}"))?;
if requests.is_empty() { if requests.is_empty() {
println!("No requests found in workspace {}", workspace_id); println!("No requests found in workspace {}", workspace_id);
@@ -350,8 +355,14 @@ fn create(
validate_create_id(&payload, "request")?; validate_create_id(&payload, "request")?;
let mut request: HttpRequest = serde_json::from_value(payload) let mut request: HttpRequest = serde_json::from_value(payload)
.map_err(|e| format!("Failed to parse request create JSON: {e}"))?; .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( merge_workspace_id_arg(
workspace_id_arg.as_deref(), workspace_id_arg.as_deref().or(fallback_workspace_id.as_deref()),
&mut request.workspace_id, &mut request.workspace_id,
"request create", "request create",
)?; )?;
@@ -365,9 +376,7 @@ fn create(
return Ok(()); return Ok(());
} }
let workspace_id = workspace_id_arg.ok_or_else(|| { let workspace_id = resolve_workspace_id(ctx, workspace_id_arg.as_deref(), "request create")?;
"request create requires workspace_id unless JSON payload is provided".to_string()
})?;
let name = name.unwrap_or_default(); let name = name.unwrap_or_default();
let url = url.unwrap_or_default(); let url = url.unwrap_or_default();
let method = method.unwrap_or_else(|| "GET".to_string()); let method = method.unwrap_or_else(|| "GET".to_string());
@@ -436,6 +445,7 @@ pub async fn send_request_by_id(
ctx: &CliContext, ctx: &CliContext,
request_id: &str, request_id: &str,
environment: Option<&str>, environment: Option<&str>,
cookie_jar_id: Option<&str>,
verbose: bool, verbose: bool,
) -> Result<(), String> { ) -> Result<(), String> {
let request = let request =
@@ -447,6 +457,7 @@ pub async fn send_request_by_id(
&http_request.id, &http_request.id,
&http_request.workspace_id, &http_request.workspace_id,
environment, environment,
cookie_jar_id,
verbose, verbose,
) )
.await .await
@@ -465,9 +476,13 @@ async fn send_http_request_by_id(
request_id: &str, request_id: &str,
workspace_id: &str, workspace_id: &str,
environment: Option<&str>, environment: Option<&str>,
cookie_jar_id: Option<&str>,
verbose: bool, verbose: bool,
) -> Result<(), String> { ) -> Result<(), String> {
let plugin_context = PluginContext::new(None, Some(workspace_id.to_string())); let cookie_jar_id = resolve_cookie_jar_id(ctx, workspace_id, cookie_jar_id)?;
let plugin_context =
PluginContext::new(Some("cli".to_string()), Some(workspace_id.to_string()));
let (event_tx, mut event_rx) = mpsc::channel::<SenderHttpResponseEvent>(100); let (event_tx, mut event_rx) = mpsc::channel::<SenderHttpResponseEvent>(100);
let (body_chunk_tx, mut body_chunk_rx) = mpsc::unbounded_channel::<Vec<u8>>(); let (body_chunk_tx, mut body_chunk_rx) = mpsc::unbounded_channel::<Vec<u8>>();
@@ -495,7 +510,7 @@ async fn send_http_request_by_id(
request_id, request_id,
environment_id: environment, environment_id: environment,
update_source: UpdateSource::Sync, update_source: UpdateSource::Sync,
cookie_jar_id: None, cookie_jar_id,
response_dir: &response_dir, response_dir: &response_dir,
emit_events_to: Some(event_tx), emit_events_to: Some(event_tx),
emit_response_body_chunks_to: Some(body_chunk_tx), emit_response_body_chunks_to: Some(body_chunk_tx),
@@ -512,3 +527,22 @@ async fn send_http_request_by_id(
result.map_err(|e| e.to_string())?; result.map_err(|e| e.to_string())?;
Ok(()) Ok(())
} }
pub(crate) fn resolve_cookie_jar_id(
ctx: &CliContext,
workspace_id: &str,
explicit_cookie_jar_id: Option<&str>,
) -> Result<Option<String>, 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)
}

View File

@@ -2,6 +2,7 @@ use crate::cli::SendArgs;
use crate::commands::request; use crate::commands::request;
use crate::context::CliContext; use crate::context::CliContext;
use futures::future::join_all; use futures::future::join_all;
use yaak_models::queries::any_request::AnyRequest;
enum ExecutionMode { enum ExecutionMode {
Sequential, Sequential,
@@ -12,9 +13,10 @@ pub async fn run(
ctx: &CliContext, ctx: &CliContext,
args: SendArgs, args: SendArgs,
environment: Option<&str>, environment: Option<&str>,
cookie_jar_id: Option<&str>,
verbose: bool, verbose: bool,
) -> i32 { ) -> i32 {
match send_target(ctx, args, environment, verbose).await { match send_target(ctx, args, environment, cookie_jar_id, verbose).await {
Ok(()) => 0, Ok(()) => 0,
Err(error) => { Err(error) => {
eprintln!("Error: {error}"); eprintln!("Error: {error}");
@@ -27,30 +29,70 @@ async fn send_target(
ctx: &CliContext, ctx: &CliContext,
args: SendArgs, args: SendArgs,
environment: Option<&str>, environment: Option<&str>,
cookie_jar_id: Option<&str>,
verbose: bool, verbose: bool,
) -> Result<(), String> { ) -> Result<(), String> {
let mode = if args.parallel { ExecutionMode::Parallel } else { ExecutionMode::Sequential }; let mode = if args.parallel { ExecutionMode::Parallel } else { ExecutionMode::Sequential };
if ctx.db().get_any_request(&args.id).is_ok() { if let Ok(request) = ctx.db().get_any_request(&args.id) {
return request::send_request_by_id(ctx, &args.id, environment, verbose).await; 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)?; let request_ids = collect_folder_request_ids(ctx, &args.id)?;
if request_ids.is_empty() { if request_ids.is_empty() {
println!("No requests found in folder {}", args.id); println!("No requests found in folder {}", args.id);
return Ok(()); 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)?; let request_ids = collect_workspace_request_ids(ctx, &args.id)?;
if request_ids.is_empty() { if request_ids.is_empty() {
println!("No requests found in workspace {}", args.id); println!("No requests found in workspace {}", args.id);
return Ok(()); 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)) Err(format!("Could not resolve ID '{}' as request, folder, or workspace", args.id))
@@ -131,6 +173,7 @@ async fn send_many(
mode: ExecutionMode, mode: ExecutionMode,
fail_fast: bool, fail_fast: bool,
environment: Option<&str>, environment: Option<&str>,
cookie_jar_id: Option<&str>,
verbose: bool, verbose: bool,
) -> Result<(), String> { ) -> Result<(), String> {
let mut success_count = 0usize; let mut success_count = 0usize;
@@ -139,7 +182,15 @@ async fn send_many(
match mode { match mode {
ExecutionMode::Sequential => { ExecutionMode::Sequential => {
for request_id in request_ids { 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, Ok(()) => success_count += 1,
Err(error) => { Err(error) => {
failures.push((request_id, error)); failures.push((request_id, error));
@@ -156,7 +207,14 @@ async fn send_many(
.map(|request_id| async move { .map(|request_id| async move {
( (
request_id.clone(), 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::<Vec<_>>(); .collect::<Vec<_>>();

View File

@@ -18,6 +18,14 @@ const EMBEDDED_PLUGIN_RUNTIME: &str = include_str!(concat!(
static EMBEDDED_VENDORED_PLUGINS: Dir<'_> = static EMBEDDED_VENDORED_PLUGINS: Dir<'_> =
include_dir!("$CARGO_MANIFEST_DIR/../../crates-tauri/yaak-app/vendored/plugins"); include_dir!("$CARGO_MANIFEST_DIR/../../crates-tauri/yaak-app/vendored/plugins");
#[derive(Clone, Debug, Default)]
pub struct CliExecutionContext {
pub request_id: Option<String>,
pub workspace_id: Option<String>,
pub environment_id: Option<String>,
pub cookie_jar_id: Option<String>,
}
pub struct CliContext { pub struct CliContext {
data_dir: PathBuf, data_dir: PathBuf,
query_manager: QueryManager, query_manager: QueryManager,
@@ -28,15 +36,14 @@ pub struct CliContext {
} }
impl CliContext { impl CliContext {
pub async fn initialize(data_dir: PathBuf, app_id: &str, with_plugins: bool) -> Self { pub async fn new(
let db_path = data_dir.join("db.sqlite"); data_dir: PathBuf,
let blob_path = data_dir.join("blobs.sqlite"); query_manager: QueryManager,
blob_manager: BlobManager,
let (query_manager, blob_manager, _rx) = yaak_models::init_standalone(&db_path, &blob_path) encryption_manager: Arc<EncryptionManager>,
.expect("Failed to initialize database"); with_plugins: bool,
execution_context: CliExecutionContext,
let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id)); ) -> Self {
let plugin_manager = if with_plugins { let plugin_manager = if with_plugins {
let vendored_plugin_dir = data_dir.join("vendored-plugins"); let vendored_plugin_dir = data_dir.join("vendored-plugins");
let installed_plugin_dir = data_dir.join("installed-plugins"); let installed_plugin_dir = data_dir.join("installed-plugins");
@@ -73,7 +80,17 @@ impl CliContext {
}; };
let plugin_event_bridge = if let Some(plugin_manager) = &plugin_manager { let plugin_event_bridge = if let Some(plugin_manager) = &plugin_manager {
Some(CliPluginEventBridge::start(plugin_manager.clone(), query_manager.clone()).await) Some(
CliPluginEventBridge::start(
plugin_manager.clone(),
query_manager.clone(),
blob_manager.clone(),
encryption_manager.clone(),
data_dir.clone(),
execution_context.clone(),
)
.await,
)
} else { } else {
None None
}; };

View File

@@ -8,12 +8,16 @@ mod version;
mod version_check; mod version_check;
use clap::Parser; use clap::Parser;
use cli::{Cli, Commands, RequestCommands}; use cli::{Cli, Commands, PluginCommands, 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] #[tokio::main]
async fn 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 { if let Some(log_level) = log {
match log_level { match log_level {
@@ -35,72 +39,299 @@ async fn main() {
version_check::maybe_check_for_updates().await; version_check::maybe_check_for_updates().await;
let needs_context = matches!( let db_path = data_dir.join("db.sqlite");
&command, let blob_path = data_dir.join("blobs.sqlite");
Commands::Send(_) let (query_manager, blob_manager, _rx) =
| Commands::Workspace(_) match yaak_models::init_standalone(&db_path, &blob_path) {
| Commands::Request(_) Ok(v) => v,
| Commands::Folder(_) Err(err) => {
| Commands::Environment(_) eprintln!("Error: Failed to initialize database: {err}");
); std::process::exit(1);
}
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 exit_code = match command { let exit_code = match command {
Commands::Auth(args) => commands::auth::run(args).await, Commands::Auth(args) => commands::auth::run(args).await,
Commands::Plugin(args) => commands::plugin::run(args).await, Commands::Plugin(args) => match args.command {
PluginCommands::Build(args) => commands::plugin::run_build(args).await,
PluginCommands::Dev(args) => commands::plugin::run_dev(args).await,
PluginCommands::Generate(args) => commands::plugin::run_generate(args).await,
PluginCommands::Publish(args) => commands::plugin::run_publish(args).await,
PluginCommands::Install(install_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)),
true,
execution_context,
)
.await;
let exit_code = commands::plugin::run_install(&context, install_args).await;
context.shutdown().await;
exit_code
}
},
Commands::Build(args) => commands::plugin::run_build(args).await, Commands::Build(args) => commands::plugin::run_build(args).await,
Commands::Dev(args) => commands::plugin::run_dev(args).await, Commands::Dev(args) => commands::plugin::run_dev(args).await,
Commands::Generate(args) => commands::plugin::run_generate(args).await, Commands::Generate(args) => commands::plugin::run_generate(args).await,
Commands::Publish(args) => commands::plugin::run_publish(args).await, Commands::Publish(args) => commands::plugin::run_publish(args).await,
Commands::Send(args) => { Commands::Send(args) => {
commands::send::run( let query_manager = query_manager.clone();
context.as_ref().expect("context initialized for send"), let blob_manager = blob_manager.clone();
args,
let execution_context_result = resolve_send_execution_context(
&query_manager,
&args.id,
environment.as_deref(), environment.as_deref(),
verbose, cookie_jar.as_deref(),
) );
.await 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( Commands::CookieJar(args) => {
context.as_ref().expect("context initialized for workspace"), let query_manager = query_manager.clone();
args, let blob_manager = blob_manager.clone();
), let execution_context = CliExecutionContext::default();
Commands::Request(args) => {
commands::request::run( let context = CliContext::new(
context.as_ref().expect("context initialized for request"), data_dir.clone(),
args, query_manager.clone(),
environment.as_deref(), blob_manager,
verbose, 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(args) => {
commands::folder::run(context.as_ref().expect("context initialized for folder"), args) let query_manager = query_manager.clone();
} let blob_manager = blob_manager.clone();
Commands::Environment(args) => commands::environment::run( let execution_context = CliExecutionContext::default();
context.as_ref().expect("context initialized for environment"),
args,
),
};
if let Some(context) = &context { let context = CliContext::new(
context.shutdown().await; 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 { if exit_code != 0 {
std::process::exit(exit_code); 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<CliExecutionContext, String> {
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<CliExecutionContext, String> {
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<Option<String>, 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)
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,19 @@
use crate::context::CliContext;
pub fn resolve_workspace_id(
ctx: &CliContext,
workspace_id: Option<&str>,
command_name: &str,
) -> Result<String, String> {
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.")),
}
}

View File

@@ -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<HttpHeader>, }; export type CallHttpAuthenticationRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, method: string, url: string, headers: Array<HttpHeader>, };
export type CallHttpAuthenticationResponse = { export type CallHttpAuthenticationResponse = {
/** /**
* HTTP headers to add to the request. Existing headers will be replaced, while * HTTP headers to add to the request. Existing headers will be replaced, while
* new headers will be added. * new headers will be added.
*/ */
setHeaders?: Array<HttpHeader>, setHeaders?: Array<HttpHeader>,
/** /**
* Query parameters to add to the request. Existing params will be replaced, while * Query parameters to add to the request. Existing params will be replaced, while
* new params will be added. * new params will be added.
@@ -78,7 +78,7 @@ export type ExportHttpRequestRequest = { httpRequest: HttpRequest, };
export type ExportHttpRequestResponse = { content: string, }; export type ExportHttpRequestResponse = { content: string, };
export type FileFilter = { name: string, export type FileFilter = { name: string,
/** /**
* File extensions to require * File extensions to require
*/ */
@@ -100,149 +100,149 @@ export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hid
export type FormInputBanner = { inputs?: Array<FormInput>, hidden?: boolean, color?: Color, }; export type FormInputBanner = { inputs?: Array<FormInput>, 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 * 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 * Whether this input is visible for the given configuration. Use this to
* make branching forms. * make branching forms.
*/ */
hidden?: boolean, hidden?: boolean,
/** /**
* Whether the user must fill in the argument * Whether the user must fill in the argument
*/ */
optional?: boolean, optional?: boolean,
/** /**
* The label of the input * The label of the input
*/ */
label?: string, label?: string,
/** /**
* Visually hide the label of the input * Visually hide the label of the input
*/ */
hideLabel?: boolean, hideLabel?: boolean,
/** /**
* The default value * The default value
*/ */
defaultValue?: string, disabled?: boolean, defaultValue?: string, disabled?: boolean,
/** /**
* Longer description of the input, likely shown in a tooltip * Longer description of the input, likely shown in a tooltip
*/ */
description?: string, }; 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 * 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 * Whether this input is visible for the given configuration. Use this to
* make branching forms. * make branching forms.
*/ */
hidden?: boolean, hidden?: boolean,
/** /**
* Whether the user must fill in the argument * Whether the user must fill in the argument
*/ */
optional?: boolean, optional?: boolean,
/** /**
* The label of the input * The label of the input
*/ */
label?: string, label?: string,
/** /**
* Visually hide the label of the input * Visually hide the label of the input
*/ */
hideLabel?: boolean, hideLabel?: boolean,
/** /**
* The default value * The default value
*/ */
defaultValue?: string, disabled?: boolean, defaultValue?: string, disabled?: boolean,
/** /**
* Longer description of the input, likely shown in a tooltip * Longer description of the input, likely shown in a tooltip
*/ */
description?: string, }; description?: string, };
export type FormInputEditor = { export type FormInputEditor = {
/** /**
* Placeholder for the text input * Placeholder for the text input
*/ */
placeholder?: string | null, placeholder?: string | null,
/** /**
* Don't show the editor gutter (line numbers, folds, etc.) * Don't show the editor gutter (line numbers, folds, etc.)
*/ */
hideGutter?: boolean, hideGutter?: boolean,
/** /**
* Language for syntax highlighting * Language for syntax highlighting
*/ */
language?: EditorLanguage, readOnly?: boolean, language?: EditorLanguage, readOnly?: boolean,
/** /**
* Fixed number of visible rows * Fixed number of visible rows
*/ */
rows?: number, completionOptions?: Array<GenericCompletionOption>, rows?: number, completionOptions?: Array<GenericCompletionOption>,
/** /**
* The name of the input. The value will be stored at this object attribute in the resulting data * 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 * Whether this input is visible for the given configuration. Use this to
* make branching forms. * make branching forms.
*/ */
hidden?: boolean, hidden?: boolean,
/** /**
* Whether the user must fill in the argument * Whether the user must fill in the argument
*/ */
optional?: boolean, optional?: boolean,
/** /**
* The label of the input * The label of the input
*/ */
label?: string, label?: string,
/** /**
* Visually hide the label of the input * Visually hide the label of the input
*/ */
hideLabel?: boolean, hideLabel?: boolean,
/** /**
* The default value * The default value
*/ */
defaultValue?: string, disabled?: boolean, defaultValue?: string, disabled?: boolean,
/** /**
* Longer description of the input, likely shown in a tooltip * Longer description of the input, likely shown in a tooltip
*/ */
description?: string, }; description?: string, };
export type FormInputFile = { export type FormInputFile = {
/** /**
* The title of the file selection window * The title of the file selection window
*/ */
title: string, title: string,
/** /**
* Allow selecting multiple files * Allow selecting multiple files
*/ */
multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array<FileFilter>, multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array<FileFilter>,
/** /**
* The name of the input. The value will be stored at this object attribute in the resulting data * 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 * Whether this input is visible for the given configuration. Use this to
* make branching forms. * make branching forms.
*/ */
hidden?: boolean, hidden?: boolean,
/** /**
* Whether the user must fill in the argument * Whether the user must fill in the argument
*/ */
optional?: boolean, optional?: boolean,
/** /**
* The label of the input * The label of the input
*/ */
label?: string, label?: string,
/** /**
* Visually hide the label of the input * Visually hide the label of the input
*/ */
hideLabel?: boolean, hideLabel?: boolean,
/** /**
* The default value * The default value
*/ */
defaultValue?: string, disabled?: boolean, defaultValue?: string, disabled?: boolean,
/** /**
* Longer description of the input, likely shown in a tooltip * Longer description of the input, likely shown in a tooltip
*/ */
@@ -250,63 +250,63 @@ description?: string, };
export type FormInputHStack = { inputs?: Array<FormInput>, hidden?: boolean, }; export type FormInputHStack = { inputs?: Array<FormInput>, 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 * 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 * Whether this input is visible for the given configuration. Use this to
* make branching forms. * make branching forms.
*/ */
hidden?: boolean, hidden?: boolean,
/** /**
* Whether the user must fill in the argument * Whether the user must fill in the argument
*/ */
optional?: boolean, optional?: boolean,
/** /**
* The label of the input * The label of the input
*/ */
label?: string, label?: string,
/** /**
* Visually hide the label of the input * Visually hide the label of the input
*/ */
hideLabel?: boolean, hideLabel?: boolean,
/** /**
* The default value * The default value
*/ */
defaultValue?: string, disabled?: boolean, defaultValue?: string, disabled?: boolean,
/** /**
* Longer description of the input, likely shown in a tooltip * Longer description of the input, likely shown in a tooltip
*/ */
description?: string, }; 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 * 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 * Whether this input is visible for the given configuration. Use this to
* make branching forms. * make branching forms.
*/ */
hidden?: boolean, hidden?: boolean,
/** /**
* Whether the user must fill in the argument * Whether the user must fill in the argument
*/ */
optional?: boolean, optional?: boolean,
/** /**
* The label of the input * The label of the input
*/ */
label?: string, label?: string,
/** /**
* Visually hide the label of the input * Visually hide the label of the input
*/ */
hideLabel?: boolean, hideLabel?: boolean,
/** /**
* The default value * The default value
*/ */
defaultValue?: string, disabled?: boolean, defaultValue?: string, disabled?: boolean,
/** /**
* Longer description of the input, likely shown in a tooltip * 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 FormInputMarkdown = { content: string, hidden?: boolean, };
export type FormInputSelect = { export type FormInputSelect = {
/** /**
* The options that will be available in the select input * The options that will be available in the select input
*/ */
options: Array<FormInputSelectOption>, options: Array<FormInputSelectOption>,
/** /**
* The name of the input. The value will be stored at this object attribute in the resulting data * 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 * Whether this input is visible for the given configuration. Use this to
* make branching forms. * make branching forms.
*/ */
hidden?: boolean, hidden?: boolean,
/** /**
* Whether the user must fill in the argument * Whether the user must fill in the argument
*/ */
optional?: boolean, optional?: boolean,
/** /**
* The label of the input * The label of the input
*/ */
label?: string, label?: string,
/** /**
* Visually hide the label of the input * Visually hide the label of the input
*/ */
hideLabel?: boolean, hideLabel?: boolean,
/** /**
* The default value * The default value
*/ */
defaultValue?: string, disabled?: boolean, defaultValue?: string, disabled?: boolean,
/** /**
* Longer description of the input, likely shown in a tooltip * 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 FormInputSelectOption = { label: string, value: string, };
export type FormInputText = { export type FormInputText = {
/** /**
* Placeholder for the text input * Placeholder for the text input
*/ */
placeholder?: string | null, placeholder?: string | null,
/** /**
* Placeholder for the text input * Placeholder for the text input
*/ */
password?: boolean, password?: boolean,
/** /**
* Whether to allow newlines in the input, like a <textarea/> * Whether to allow newlines in the input, like a <textarea/>
*/ */
multiLine?: boolean, completionOptions?: Array<GenericCompletionOption>, multiLine?: boolean, completionOptions?: Array<GenericCompletionOption>,
/** /**
* The name of the input. The value will be stored at this object attribute in the resulting data * 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 * Whether this input is visible for the given configuration. Use this to
* make branching forms. * make branching forms.
*/ */
hidden?: boolean, hidden?: boolean,
/** /**
* Whether the user must fill in the argument * Whether the user must fill in the argument
*/ */
optional?: boolean, optional?: boolean,
/** /**
* The label of the input * The label of the input
*/ */
label?: string, label?: string,
/** /**
* Visually hide the label of the input * Visually hide the label of the input
*/ */
hideLabel?: boolean, hideLabel?: boolean,
/** /**
* The default value * The default value
*/ */
defaultValue?: string, disabled?: boolean, defaultValue?: string, disabled?: boolean,
/** /**
* Longer description of the input, likely shown in a tooltip * Longer description of the input, likely shown in a tooltip
*/ */
@@ -474,7 +474,7 @@ export type ListOpenWorkspacesResponse = { workspaces: Array<WorkspaceInfo>, };
export type OpenExternalUrlRequest = { url: string, }; export type OpenExternalUrlRequest = { url: string, };
export type OpenWindowRequest = { url: string, export type OpenWindowRequest = { url: string,
/** /**
* Label for the window. If not provided, a random one will be generated. * Label for the window. If not provided, a random one will be generated.
*/ */
@@ -486,15 +486,15 @@ export type PromptFormRequest = { id: string, title: string, description?: strin
export type PromptFormResponse = { values: { [key in string]?: JsonPrimitive } | null, done?: boolean, }; export type PromptFormResponse = { values: { [key in string]?: JsonPrimitive } | null, done?: boolean, };
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string, export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
/** /**
* Text to add to the confirmation button * Text to add to the confirmation button
*/ */
confirmText?: string, password?: boolean, confirmText?: string, password?: boolean,
/** /**
* Text to add to the cancel button * Text to add to the cancel button
*/ */
cancelText?: string, cancelText?: string,
/** /**
* Require the user to enter a non-empty value * Require the user to enter a non-empty value
*/ */
@@ -524,12 +524,12 @@ export type SetKeyValueResponse = {};
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, timeout?: number, }; export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, timeout?: number, };
export type TemplateFunction = { name: string, previewType?: TemplateFunctionPreviewType, description?: string, export type TemplateFunction = { name: string, previewType?: TemplateFunctionPreviewType, description?: string,
/** /**
* Also support alternative names. This is useful for not breaking existing * Also support alternative names. This is useful for not breaking existing
* tags when changing the `name` property * tags when changing the `name` property
*/ */
aliases?: Array<string>, args: Array<TemplateFunctionArg>, aliases?: Array<string>, args: Array<TemplateFunctionArg>,
/** /**
* A list of arg names to show in the inline preview. If not provided, none will be shown (for privacy reasons). * A list of arg names to show in the inline preview. If not provided, none will be shown (for privacy reasons).
*/ */
@@ -546,23 +546,23 @@ export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, }
export type TemplateRenderResponse = { data: JsonValue, }; export type TemplateRenderResponse = { data: JsonValue, };
export type Theme = { export type Theme = {
/** /**
* How the theme is identified. This should never be changed * How the theme is identified. This should never be changed
*/ */
id: string, id: string,
/** /**
* The friendly name of the theme to be displayed to the user * The friendly name of the theme to be displayed to the user
*/ */
label: string, label: string,
/** /**
* Whether the theme will be used for dark or light appearance * Whether the theme will be used for dark or light appearance
*/ */
dark: boolean, dark: boolean,
/** /**
* The default top-level colors for the theme * The default top-level colors for the theme
*/ */
base: ThemeComponentColors, base: ThemeComponentColors,
/** /**
* Optionally override theme for individual UI components for more control * Optionally override theme for individual UI components for more control
*/ */

View File

@@ -18,7 +18,12 @@ export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
export type EncryptedKey = { encryptedKey: string, }; export type EncryptedKey = { encryptedKey: string, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, }; export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null,
/**
* Variables defined in this environment scope.
* Child environments override parent variables by name.
*/
variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
@@ -34,9 +39,17 @@ export type GrpcEvent = { model: "grpc_event", id: string, createdAt: string, up
export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end"; export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end";
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, }; export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number,
/**
* Server URL (http for plaintext or https for secure)
*/
url: string, };
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, }; export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string,
/**
* URL parameters used for both path placeholders (`:id`) and query string entries.
*/
urlParameters: Array<HttpUrlParameter>, };
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, }; export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
@@ -55,11 +68,18 @@ export type HttpResponseHeader = { name: string, value: string, };
export type HttpResponseState = "initialized" | "connected" | "closed"; export type HttpResponseState = "initialized" | "connected" | "closed";
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id?: string, }; export type HttpUrlParameter = { enabled?: boolean,
/**
* Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
* Other entries are appended as query parameters
*/
name: string, value: string, id?: string, };
export type KeyValue = { model: "key_value", id: string, createdAt: string, updatedAt: string, key: string, namespace: string, value: string, }; export type KeyValue = { model: "key_value", id: string, createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, }; export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, source: PluginSource, };
export type PluginSource = "bundled" | "filesystem" | "registry";
export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, bypass: string, disabled: boolean, } | { "type": "disabled" }; export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, bypass: string, disabled: boolean, } | { "type": "disabled" };
@@ -77,7 +97,11 @@ export type WebsocketEvent = { model: "websocket_event", id: string, createdAt:
export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text"; export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text";
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, }; export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string,
/**
* URL parameters used for both path placeholders (`:id`) and query string entries.
*/
urlParameters: Array<HttpUrlParameter>, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, }; export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };

View File

@@ -317,7 +317,8 @@ async function getResponse(
finalBehavior === 'always' || finalBehavior === 'always' ||
(finalBehavior === BEHAVIOR_TTL && shouldSendExpired(response, ttl)) (finalBehavior === BEHAVIOR_TTL && shouldSendExpired(response, ttl))
) { ) {
// NOTE: Render inside this conditional, or we'll get infinite recursion (render->render->...) // Explicitly render the request before send (instead of relying on send() to render) so that we can
// preserve the render purpose.
const renderedHttpRequest = await ctx.httpRequest.render({ httpRequest, purpose }); const renderedHttpRequest = await ctx.httpRequest.render({ httpRequest, purpose });
response = await ctx.httpRequest.send({ httpRequest: renderedHttpRequest }); response = await ctx.httpRequest.send({ httpRequest: renderedHttpRequest });
} }