mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-27 04:47:42 +01:00
Compare commits
2 Commits
codex/cli-
...
yaak-cli-0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0264e59553 | ||
|
|
53d86f5568 |
@@ -22,7 +22,7 @@
|
||||
<!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https://github.com/MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a> <a href="https://github.com/dharsanb"><img src="https://github.com/dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a> <a href="https://github.com/railwayapp"><img src="https://github.com/railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a> <a href="https://github.com/caseyamcl"><img src="https://github.com/caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a> <a href="https://github.com/bytebase"><img src="https://github.com/bytebase.png" width="80px" alt="User avatar: bytebase" /></a> <a href="https://github.com/"><img src="https://raw.githubusercontent.com/JamesIves/github-sponsors-readme-action/dev/.github/assets/placeholder.png" width="80px" alt="User avatar: " /></a> <!-- sponsors-premium -->
|
||||
</p>
|
||||
<p align="center">
|
||||
<!-- sponsors-base --><a href="https://github.com/seanwash"><img src="https://github.com/seanwash.png" width="50px" alt="User avatar: seanwash" /></a> <a href="https://github.com/jerath"><img src="https://github.com/jerath.png" width="50px" alt="User avatar: jerath" /></a> <a href="https://github.com/itsa-sh"><img src="https://github.com/itsa-sh.png" width="50px" alt="User avatar: itsa-sh" /></a> <a href="https://github.com/dmmulroy"><img src="https://github.com/dmmulroy.png" width="50px" alt="User avatar: dmmulroy" /></a> <a href="https://github.com/timcole"><img src="https://github.com/timcole.png" width="50px" alt="User avatar: timcole" /></a> <a href="https://github.com/VLZH"><img src="https://github.com/VLZH.png" width="50px" alt="User avatar: VLZH" /></a> <a href="https://github.com/terasaka2k"><img src="https://github.com/terasaka2k.png" width="50px" alt="User avatar: terasaka2k" /></a> <a href="https://github.com/andriyor"><img src="https://github.com/andriyor.png" width="50px" alt="User avatar: andriyor" /></a> <a href="https://github.com/majudhu"><img src="https://github.com/majudhu.png" width="50px" alt="User avatar: majudhu" /></a> <a href="https://github.com/axelrindle"><img src="https://github.com/axelrindle.png" width="50px" alt="User avatar: axelrindle" /></a> <a href="https://github.com/jirizverina"><img src="https://github.com/jirizverina.png" width="50px" alt="User avatar: jirizverina" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="50px" alt="User avatar: chip-well" /></a> <a href="https://github.com/GRAYAH"><img src="https://github.com/GRAYAH.png" width="50px" alt="User avatar: GRAYAH" /></a> <a href="https://github.com/flashblaze"><img src="https://github.com/flashblaze.png" width="50px" alt="User avatar: flashblaze" /></a> <a href="https://github.com/Frostist"><img src="https://github.com/Frostist.png" width="50px" alt="User avatar: Frostist" /></a> <!-- sponsors-base -->
|
||||
<!-- sponsors-base --><a href="https://github.com/seanwash"><img src="https://github.com/seanwash.png" width="50px" alt="User avatar: seanwash" /></a> <a href="https://github.com/jerath"><img src="https://github.com/jerath.png" width="50px" alt="User avatar: jerath" /></a> <a href="https://github.com/itsa-sh"><img src="https://github.com/itsa-sh.png" width="50px" alt="User avatar: itsa-sh" /></a> <a href="https://github.com/dmmulroy"><img src="https://github.com/dmmulroy.png" width="50px" alt="User avatar: dmmulroy" /></a> <a href="https://github.com/timcole"><img src="https://github.com/timcole.png" width="50px" alt="User avatar: timcole" /></a> <a href="https://github.com/VLZH"><img src="https://github.com/VLZH.png" width="50px" alt="User avatar: VLZH" /></a> <a href="https://github.com/terasaka2k"><img src="https://github.com/terasaka2k.png" width="50px" alt="User avatar: terasaka2k" /></a> <a href="https://github.com/andriyor"><img src="https://github.com/andriyor.png" width="50px" alt="User avatar: andriyor" /></a> <a href="https://github.com/majudhu"><img src="https://github.com/majudhu.png" width="50px" alt="User avatar: majudhu" /></a> <a href="https://github.com/axelrindle"><img src="https://github.com/axelrindle.png" width="50px" alt="User avatar: axelrindle" /></a> <a href="https://github.com/jirizverina"><img src="https://github.com/jirizverina.png" width="50px" alt="User avatar: jirizverina" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="50px" alt="User avatar: chip-well" /></a> <a href="https://github.com/GRAYAH"><img src="https://github.com/GRAYAH.png" width="50px" alt="User avatar: GRAYAH" /></a> <a href="https://github.com/flashblaze"><img src="https://github.com/flashblaze.png" width="50px" alt="User avatar: flashblaze" /></a> <!-- sponsors-base -->
|
||||
</p>
|
||||
|
||||

|
||||
|
||||
@@ -1,66 +1,93 @@
|
||||
# Yaak CLI
|
||||
# yaak-cli
|
||||
|
||||
The `yaak` CLI for publishing plugins and creating/updating/sending requests.
|
||||
Command-line interface for Yaak.
|
||||
|
||||
## Installation
|
||||
## Command Overview
|
||||
|
||||
```sh
|
||||
npm install @yaakapp/cli
|
||||
```
|
||||
|
||||
## Agentic Workflows
|
||||
|
||||
The `yaak` CLI is primarily meant to be used by AI agents, and has the following features:
|
||||
|
||||
- `schema` subcommands to get the JSON Schema for any model (eg. `yaak request schema http`)
|
||||
- `--json '{...}'` input format to create and update data
|
||||
- `--verbose` mode for extracting debug info while sending requests
|
||||
- The ability to send entire workspaces and folders (Supports `--parallel` and `--fail-fast`)
|
||||
|
||||
### Example Prompts
|
||||
|
||||
Use the `yaak` CLI with agents like Claude or Codex to do useful things for you.
|
||||
|
||||
Here are some example prompts:
|
||||
Current top-level commands:
|
||||
|
||||
```text
|
||||
Scan my API routes and create a workspace (using yaak cli) with
|
||||
all the requests needed for me to do manual testing?
|
||||
yaakcli send <request_id>
|
||||
yaakcli agent-help
|
||||
yaakcli workspace list
|
||||
yaakcli workspace schema [--pretty]
|
||||
yaakcli workspace show <workspace_id>
|
||||
yaakcli workspace create --name <name>
|
||||
yaakcli workspace create --json '{"name":"My Workspace"}'
|
||||
yaakcli workspace create '{"name":"My Workspace"}'
|
||||
yaakcli workspace update --json '{"id":"wk_abc","description":"Updated"}'
|
||||
yaakcli workspace delete <workspace_id> [--yes]
|
||||
yaakcli request list <workspace_id>
|
||||
yaakcli request show <request_id>
|
||||
yaakcli request send <request_id>
|
||||
yaakcli request create <workspace_id> --name <name> --url <url> [--method GET]
|
||||
yaakcli request create --json '{"workspaceId":"wk_abc","name":"Users","url":"https://api.example.com/users"}'
|
||||
yaakcli request create '{"workspaceId":"wk_abc","name":"Users","url":"https://api.example.com/users"}'
|
||||
yaakcli request update --json '{"id":"rq_abc","name":"Users v2"}'
|
||||
yaakcli request delete <request_id> [--yes]
|
||||
yaakcli folder list <workspace_id>
|
||||
yaakcli folder show <folder_id>
|
||||
yaakcli folder create <workspace_id> --name <name>
|
||||
yaakcli folder create --json '{"workspaceId":"wk_abc","name":"Auth"}'
|
||||
yaakcli folder create '{"workspaceId":"wk_abc","name":"Auth"}'
|
||||
yaakcli folder update --json '{"id":"fl_abc","name":"Auth v2"}'
|
||||
yaakcli folder delete <folder_id> [--yes]
|
||||
yaakcli environment list <workspace_id>
|
||||
yaakcli environment schema [--pretty]
|
||||
yaakcli environment show <environment_id>
|
||||
yaakcli environment create <workspace_id> --name <name>
|
||||
yaakcli environment create --json '{"workspaceId":"wk_abc","name":"Production"}'
|
||||
yaakcli environment create '{"workspaceId":"wk_abc","name":"Production"}'
|
||||
yaakcli environment update --json '{"id":"ev_abc","color":"#00ff00"}'
|
||||
yaakcli environment delete <environment_id> [--yes]
|
||||
```
|
||||
|
||||
```text
|
||||
Send all the GraphQL requests in my workspace
|
||||
Global options:
|
||||
|
||||
- `--data-dir <path>`: use a custom data directory
|
||||
- `-e, --environment <id>`: environment to use during request rendering/sending
|
||||
- `-v, --verbose`: verbose send output (events and streamed response body)
|
||||
- `--log [level]`: enable CLI logging; optional level is `error|warn|info|debug|trace`
|
||||
|
||||
Notes:
|
||||
|
||||
- `send` is currently a shortcut for sending an HTTP request ID.
|
||||
- `delete` commands prompt for confirmation unless `--yes` is provided.
|
||||
- In non-interactive mode, `delete` commands require `--yes`.
|
||||
- `create` and `update` commands support `--json` and positional JSON shorthand.
|
||||
- For `create` commands, use one input mode at a time. Example: do not combine `<workspace_id>` with `--json`.
|
||||
- Template tags use `${[ ... ]}` syntax (for example `${[API_BASE_URL]}`), not `{{ ... }}`.
|
||||
- `update` uses JSON Merge Patch semantics (RFC 7386) for partial updates.
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
yaakcli workspace list
|
||||
yaakcli workspace create --name "My Workspace"
|
||||
yaakcli workspace show wk_abc
|
||||
yaakcli workspace update --json '{"id":"wk_abc","description":"Team workspace"}'
|
||||
yaakcli request list wk_abc
|
||||
yaakcli request show rq_abc
|
||||
yaakcli request create wk_abc --name "Users" --url "https://api.example.com/users"
|
||||
yaakcli request update --json '{"id":"rq_abc","name":"Users v2"}'
|
||||
yaakcli request send rq_abc -e ev_abc
|
||||
yaakcli request delete rq_abc --yes
|
||||
yaakcli folder create wk_abc --name "Auth"
|
||||
yaakcli folder update --json '{"id":"fl_abc","name":"Auth v2"}'
|
||||
yaakcli environment create wk_abc --name "Production"
|
||||
yaakcli environment update --json '{"id":"ev_abc","color":"#00ff00"}'
|
||||
```
|
||||
|
||||
## Description
|
||||
## Roadmap
|
||||
|
||||
Here's the current print of `yaak --help`
|
||||
Planned command expansion (request schema and polymorphic send) is tracked in `PLAN.md`.
|
||||
|
||||
```text
|
||||
Yaak CLI - API client from the command line
|
||||
When command behavior changes, update this README and verify with:
|
||||
|
||||
Usage: yaak [OPTIONS] <COMMAND>
|
||||
|
||||
Commands:
|
||||
auth Authentication commands
|
||||
plugin Plugin development and publishing commands
|
||||
send Send a request, folder, or workspace by ID
|
||||
workspace Workspace commands
|
||||
request Request commands
|
||||
folder Folder commands
|
||||
environment Environment commands
|
||||
|
||||
Options:
|
||||
--data-dir <DATA_DIR> Use a custom data directory
|
||||
-e, --environment <ENVIRONMENT> Environment ID to use for variable substitution
|
||||
-v, --verbose Enable verbose send output (events and streamed response body)
|
||||
--log [<LEVEL>] Enable CLI logging; optionally set level (error|warn|info|debug|trace) [possible values: error, warn, info, debug, trace]
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
|
||||
Agent Hints:
|
||||
- Template variable syntax is ${[ my_var ]}, not {{ ... }}
|
||||
- Template function syntax is ${[ namespace.my_func(a='aaa',b='bbb') ]}
|
||||
- View JSONSchema for models before creating or updating (eg. `yaak request schema http`)
|
||||
- Deletion requires confirmation (--yes for non-interactive environments)
|
||||
```bash
|
||||
cargo run -q -p yaak-cli -- --help
|
||||
cargo run -q -p yaak-cli -- request --help
|
||||
cargo run -q -p yaak-cli -- workspace --help
|
||||
cargo run -q -p yaak-cli -- folder --help
|
||||
cargo run -q -p yaak-cli -- environment --help
|
||||
```
|
||||
|
||||
@@ -68,8 +68,12 @@ pub struct SendArgs {
|
||||
/// Request, folder, or workspace ID
|
||||
pub id: String,
|
||||
|
||||
/// Execute requests sequentially (default)
|
||||
#[arg(long, conflicts_with = "parallel")]
|
||||
pub sequential: bool,
|
||||
|
||||
/// Execute requests in parallel
|
||||
#[arg(long)]
|
||||
#[arg(long, conflicts_with = "sequential")]
|
||||
pub parallel: bool,
|
||||
|
||||
/// Stop on first request failure when sending folders/workspaces
|
||||
@@ -338,8 +342,8 @@ pub enum EnvironmentCommands {
|
||||
1) yaak environment create <workspace_id> --name <name>
|
||||
2) yaak environment create --json '{"workspaceId":"wk_abc","name":"Production"}'
|
||||
3) yaak environment create '{"workspaceId":"wk_abc","name":"Production"}'
|
||||
4) yaak environment create <workspace_id> --json '{"name":"Production"}'
|
||||
"#)]
|
||||
|
||||
Do not combine <workspace_id> with --json."#)]
|
||||
Create {
|
||||
/// Workspace ID for flag-based mode, or positional JSON payload shorthand
|
||||
#[arg(value_name = "WORKSPACE_ID_OR_JSON")]
|
||||
|
||||
@@ -2,8 +2,8 @@ use crate::cli::{EnvironmentArgs, EnvironmentCommands};
|
||||
use crate::context::CliContext;
|
||||
use crate::utils::confirm::confirm_delete;
|
||||
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,
|
||||
apply_merge_patch, is_json_shorthand, parse_optional_json, parse_required_json, require_id,
|
||||
validate_create_id,
|
||||
};
|
||||
use crate::utils::schema::append_agent_hints;
|
||||
use schemars::schema_for;
|
||||
@@ -34,13 +34,18 @@ pub fn run(ctx: &CliContext, args: EnvironmentArgs) -> i32 {
|
||||
}
|
||||
|
||||
fn schema(pretty: bool) -> CommandResult {
|
||||
let mut schema = serde_json::to_value(schema_for!(Environment))
|
||||
.map_err(|e| format!("Failed to serialize environment schema: {e}"))?;
|
||||
let mut schema =
|
||||
serde_json::to_value(schema_for!(Environment)).map_err(|e| format!(
|
||||
"Failed to serialize environment schema: {e}"
|
||||
))?;
|
||||
append_agent_hints(&mut schema);
|
||||
|
||||
let output =
|
||||
if pretty { serde_json::to_string_pretty(&schema) } else { serde_json::to_string(&schema) }
|
||||
.map_err(|e| format!("Failed to format environment schema JSON: {e}"))?;
|
||||
let output = if pretty {
|
||||
serde_json::to_string_pretty(&schema)
|
||||
} else {
|
||||
serde_json::to_string(&schema)
|
||||
}
|
||||
.map_err(|e| format!("Failed to format environment schema JSON: {e}"))?;
|
||||
println!("{output}");
|
||||
Ok(())
|
||||
}
|
||||
@@ -78,11 +83,17 @@ fn create(
|
||||
name: Option<String>,
|
||||
json: Option<String>,
|
||||
) -> CommandResult {
|
||||
let json_shorthand =
|
||||
workspace_id.as_deref().filter(|v| is_json_shorthand(v)).map(str::to_owned);
|
||||
let workspace_id_arg = workspace_id.filter(|v| !is_json_shorthand(v));
|
||||
if json.is_some() && workspace_id.as_deref().is_some_and(|v| !is_json_shorthand(v)) {
|
||||
return Err(
|
||||
"environment create cannot combine workspace_id with --json payload".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
let payload = parse_optional_json(json, json_shorthand, "environment create")?;
|
||||
let payload = parse_optional_json(
|
||||
json,
|
||||
workspace_id.clone().filter(|v| is_json_shorthand(v)),
|
||||
"environment create",
|
||||
)?;
|
||||
|
||||
if let Some(payload) = payload {
|
||||
if name.is_some() {
|
||||
@@ -92,11 +103,10 @@ 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}"))?;
|
||||
merge_workspace_id_arg(
|
||||
workspace_id_arg.as_deref(),
|
||||
&mut environment.workspace_id,
|
||||
"environment create",
|
||||
)?;
|
||||
|
||||
if environment.workspace_id.is_empty() {
|
||||
return Err("environment create JSON requires non-empty \"workspaceId\"".to_string());
|
||||
}
|
||||
|
||||
if environment.parent_model.is_empty() {
|
||||
environment.parent_model = "environment".to_string();
|
||||
@@ -111,7 +121,7 @@ fn create(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let workspace_id = workspace_id_arg.ok_or_else(|| {
|
||||
let workspace_id = workspace_id.ok_or_else(|| {
|
||||
"environment create requires workspace_id unless JSON payload is provided".to_string()
|
||||
})?;
|
||||
let name = name.ok_or_else(|| {
|
||||
|
||||
@@ -2,8 +2,8 @@ use crate::cli::{FolderArgs, FolderCommands};
|
||||
use crate::context::CliContext;
|
||||
use crate::utils::confirm::confirm_delete;
|
||||
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,
|
||||
apply_merge_patch, is_json_shorthand, parse_optional_json, parse_required_json, require_id,
|
||||
validate_create_id,
|
||||
};
|
||||
use yaak_models::models::Folder;
|
||||
use yaak_models::util::UpdateSource;
|
||||
@@ -58,11 +58,15 @@ fn create(
|
||||
name: Option<String>,
|
||||
json: Option<String>,
|
||||
) -> CommandResult {
|
||||
let json_shorthand =
|
||||
workspace_id.as_deref().filter(|v| is_json_shorthand(v)).map(str::to_owned);
|
||||
let workspace_id_arg = workspace_id.filter(|v| !is_json_shorthand(v));
|
||||
if json.is_some() && workspace_id.as_deref().is_some_and(|v| !is_json_shorthand(v)) {
|
||||
return Err("folder create cannot combine workspace_id with --json payload".to_string());
|
||||
}
|
||||
|
||||
let payload = parse_optional_json(json, json_shorthand, "folder create")?;
|
||||
let payload = parse_optional_json(
|
||||
json,
|
||||
workspace_id.clone().filter(|v| is_json_shorthand(v)),
|
||||
"folder create",
|
||||
)?;
|
||||
|
||||
if let Some(payload) = payload {
|
||||
if name.is_some() {
|
||||
@@ -70,13 +74,12 @@ fn create(
|
||||
}
|
||||
|
||||
validate_create_id(&payload, "folder")?;
|
||||
let mut folder: Folder = serde_json::from_value(payload)
|
||||
let folder: Folder = serde_json::from_value(payload)
|
||||
.map_err(|e| format!("Failed to parse folder create JSON: {e}"))?;
|
||||
merge_workspace_id_arg(
|
||||
workspace_id_arg.as_deref(),
|
||||
&mut folder.workspace_id,
|
||||
"folder create",
|
||||
)?;
|
||||
|
||||
if folder.workspace_id.is_empty() {
|
||||
return Err("folder create JSON requires non-empty \"workspaceId\"".to_string());
|
||||
}
|
||||
|
||||
let created = ctx
|
||||
.db()
|
||||
@@ -87,7 +90,7 @@ fn create(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let workspace_id = workspace_id_arg.ok_or_else(|| {
|
||||
let workspace_id = workspace_id.ok_or_else(|| {
|
||||
"folder create requires workspace_id unless JSON payload is provided".to_string()
|
||||
})?;
|
||||
let name = name.ok_or_else(|| {
|
||||
|
||||
@@ -2,8 +2,8 @@ use crate::cli::{RequestArgs, RequestCommands, RequestSchemaType};
|
||||
use crate::context::CliContext;
|
||||
use crate::utils::confirm::confirm_delete;
|
||||
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,
|
||||
apply_merge_patch, is_json_shorthand, parse_optional_json, parse_required_json, require_id,
|
||||
validate_create_id,
|
||||
};
|
||||
use crate::utils::schema::append_agent_hints;
|
||||
use schemars::schema_for;
|
||||
@@ -11,8 +11,8 @@ use serde_json::{Map, Value, json};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
use tokio::sync::mpsc;
|
||||
use yaak::send::{SendHttpRequestByIdWithPluginsParams, send_http_request_by_id_with_plugins};
|
||||
use yaak_http::sender::HttpResponseEvent as SenderHttpResponseEvent;
|
||||
use yaak::send::{SendHttpRequestByIdWithPluginsParams, send_http_request_by_id_with_plugins};
|
||||
use yaak_models::models::{GrpcRequest, HttpRequest, WebsocketRequest};
|
||||
use yaak_models::queries::any_request::AnyRequest;
|
||||
use yaak_models::util::UpdateSource;
|
||||
@@ -336,11 +336,15 @@ fn create(
|
||||
url: Option<String>,
|
||||
json: Option<String>,
|
||||
) -> CommandResult {
|
||||
let json_shorthand =
|
||||
workspace_id.as_deref().filter(|v| is_json_shorthand(v)).map(str::to_owned);
|
||||
let workspace_id_arg = workspace_id.filter(|v| !is_json_shorthand(v));
|
||||
if json.is_some() && workspace_id.as_deref().is_some_and(|v| !is_json_shorthand(v)) {
|
||||
return Err("request create cannot combine workspace_id with --json payload".to_string());
|
||||
}
|
||||
|
||||
let payload = parse_optional_json(json, json_shorthand, "request create")?;
|
||||
let payload = parse_optional_json(
|
||||
json,
|
||||
workspace_id.clone().filter(|v| is_json_shorthand(v)),
|
||||
"request create",
|
||||
)?;
|
||||
|
||||
if let Some(payload) = payload {
|
||||
if name.is_some() || method.is_some() || url.is_some() {
|
||||
@@ -348,13 +352,12 @@ fn create(
|
||||
}
|
||||
|
||||
validate_create_id(&payload, "request")?;
|
||||
let mut request: HttpRequest = serde_json::from_value(payload)
|
||||
let request: HttpRequest = serde_json::from_value(payload)
|
||||
.map_err(|e| format!("Failed to parse request create JSON: {e}"))?;
|
||||
merge_workspace_id_arg(
|
||||
workspace_id_arg.as_deref(),
|
||||
&mut request.workspace_id,
|
||||
"request create",
|
||||
)?;
|
||||
|
||||
if request.workspace_id.is_empty() {
|
||||
return Err("request create JSON requires non-empty \"workspaceId\"".to_string());
|
||||
}
|
||||
|
||||
let created = ctx
|
||||
.db()
|
||||
@@ -365,7 +368,7 @@ fn create(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let workspace_id = workspace_id_arg.ok_or_else(|| {
|
||||
let workspace_id = workspace_id.ok_or_else(|| {
|
||||
"request create requires workspace_id unless JSON payload is provided".to_string()
|
||||
})?;
|
||||
let name = name.unwrap_or_default();
|
||||
|
||||
@@ -38,16 +38,12 @@ impl CliContext {
|
||||
let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id));
|
||||
|
||||
let plugin_manager = if with_plugins {
|
||||
let embedded_vendored_plugin_dir = data_dir.join("vendored-plugins");
|
||||
let bundled_plugin_dir =
|
||||
resolve_bundled_plugin_dir_for_cli(&embedded_vendored_plugin_dir);
|
||||
let vendored_plugin_dir = data_dir.join("vendored-plugins");
|
||||
let installed_plugin_dir = data_dir.join("installed-plugins");
|
||||
let node_bin_path = PathBuf::from("node");
|
||||
|
||||
if bundled_plugin_dir == embedded_vendored_plugin_dir {
|
||||
prepare_embedded_vendored_plugins(&embedded_vendored_plugin_dir)
|
||||
.expect("Failed to prepare bundled plugins");
|
||||
}
|
||||
prepare_embedded_vendored_plugins(&vendored_plugin_dir)
|
||||
.expect("Failed to prepare bundled plugins");
|
||||
|
||||
let plugin_runtime_main =
|
||||
std::env::var("YAAK_PLUGIN_RUNTIME").map(PathBuf::from).unwrap_or_else(|_| {
|
||||
@@ -56,13 +52,13 @@ impl CliContext {
|
||||
});
|
||||
|
||||
match PluginManager::new(
|
||||
bundled_plugin_dir,
|
||||
embedded_vendored_plugin_dir,
|
||||
vendored_plugin_dir,
|
||||
installed_plugin_dir,
|
||||
node_bin_path,
|
||||
plugin_runtime_main,
|
||||
&query_manager,
|
||||
&PluginContext::new_empty(),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -135,20 +131,3 @@ fn prepare_embedded_vendored_plugins(vendored_plugin_dir: &Path) -> std::io::Res
|
||||
EMBEDDED_VENDORED_PLUGINS.extract(vendored_plugin_dir)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_bundled_plugin_dir_for_cli(embedded_vendored_plugin_dir: &Path) -> PathBuf {
|
||||
if !cfg!(debug_assertions) {
|
||||
return embedded_vendored_plugin_dir.to_path_buf();
|
||||
}
|
||||
|
||||
let plugins_dir = match std::env::current_dir() {
|
||||
Ok(cwd) => cwd.join("plugins"),
|
||||
Err(_) => return embedded_vendored_plugin_dir.to_path_buf(),
|
||||
};
|
||||
|
||||
if !plugins_dir.is_dir() {
|
||||
return embedded_vendored_plugin_dir.to_path_buf();
|
||||
}
|
||||
|
||||
plugins_dir.canonicalize().unwrap_or(plugins_dir)
|
||||
}
|
||||
|
||||
@@ -63,30 +63,6 @@ pub fn validate_create_id(payload: &Value, context: &str) -> JsonResult<()> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge_workspace_id_arg(
|
||||
workspace_id_from_arg: Option<&str>,
|
||||
payload_workspace_id: &mut String,
|
||||
context: &str,
|
||||
) -> JsonResult<()> {
|
||||
if let Some(workspace_id_arg) = workspace_id_from_arg {
|
||||
if payload_workspace_id.is_empty() {
|
||||
*payload_workspace_id = workspace_id_arg.to_string();
|
||||
} else if payload_workspace_id != workspace_id_arg {
|
||||
return Err(format!(
|
||||
"{context} got conflicting workspace_id values between positional arg and JSON payload"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if payload_workspace_id.is_empty() {
|
||||
return Err(format!(
|
||||
"{context} requires non-empty \"workspaceId\" in JSON payload or positional workspace_id"
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_merge_patch<T>(existing: &T, patch: &Value, id: &str, context: &str) -> JsonResult<T>
|
||||
where
|
||||
T: Serialize + DeserializeOwned,
|
||||
|
||||
@@ -79,54 +79,6 @@ fn json_create_and_update_merge_patch_round_trip() {
|
||||
.stdout(contains("\"color\": \"#00ff00\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_merges_positional_workspace_id_into_json_payload() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let data_dir = temp_dir.path();
|
||||
seed_workspace(data_dir, "wk_test");
|
||||
|
||||
let create_assert = cli_cmd(data_dir)
|
||||
.args([
|
||||
"environment",
|
||||
"create",
|
||||
"wk_test",
|
||||
"--json",
|
||||
r#"{"name":"Merged Environment"}"#,
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
let environment_id = parse_created_id(&create_assert.get_output().stdout, "environment create");
|
||||
|
||||
cli_cmd(data_dir)
|
||||
.args(["environment", "show", &environment_id])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("\"workspaceId\": \"wk_test\""))
|
||||
.stdout(contains("\"name\": \"Merged Environment\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_rejects_conflicting_workspace_ids_between_arg_and_json() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let data_dir = temp_dir.path();
|
||||
seed_workspace(data_dir, "wk_test");
|
||||
seed_workspace(data_dir, "wk_other");
|
||||
|
||||
cli_cmd(data_dir)
|
||||
.args([
|
||||
"environment",
|
||||
"create",
|
||||
"wk_test",
|
||||
"--json",
|
||||
r#"{"workspaceId":"wk_other","name":"Mismatch"}"#,
|
||||
])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains(
|
||||
"environment create got conflicting workspace_id values between positional arg and JSON payload",
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environment_schema_outputs_json_schema() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
@@ -139,8 +91,6 @@ fn environment_schema_outputs_json_schema() {
|
||||
.stdout(contains("\"type\":\"object\""))
|
||||
.stdout(contains("\"x-yaak-agent-hints\""))
|
||||
.stdout(contains("\"templateVariableSyntax\":\"${[ my_var ]}\""))
|
||||
.stdout(contains(
|
||||
"\"templateFunctionSyntax\":\"${[ namespace.my_func(a='aaa',b='bbb') ]}\"",
|
||||
))
|
||||
.stdout(contains("\"templateFunctionSyntax\":\"${[ namespace.my_func(a='aaa',b='bbb') ]}\""))
|
||||
.stdout(contains("\"workspaceId\""));
|
||||
}
|
||||
|
||||
@@ -72,51 +72,3 @@ fn json_create_and_update_merge_patch_round_trip() {
|
||||
.stdout(contains("\"name\": \"Json Folder\""))
|
||||
.stdout(contains("\"description\": \"Folder Description\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_merges_positional_workspace_id_into_json_payload() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let data_dir = temp_dir.path();
|
||||
seed_workspace(data_dir, "wk_test");
|
||||
|
||||
let create_assert = cli_cmd(data_dir)
|
||||
.args([
|
||||
"folder",
|
||||
"create",
|
||||
"wk_test",
|
||||
"--json",
|
||||
r#"{"name":"Merged Folder"}"#,
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
let folder_id = parse_created_id(&create_assert.get_output().stdout, "folder create");
|
||||
|
||||
cli_cmd(data_dir)
|
||||
.args(["folder", "show", &folder_id])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("\"workspaceId\": \"wk_test\""))
|
||||
.stdout(contains("\"name\": \"Merged Folder\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_rejects_conflicting_workspace_ids_between_arg_and_json() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let data_dir = temp_dir.path();
|
||||
seed_workspace(data_dir, "wk_test");
|
||||
seed_workspace(data_dir, "wk_other");
|
||||
|
||||
cli_cmd(data_dir)
|
||||
.args([
|
||||
"folder",
|
||||
"create",
|
||||
"wk_test",
|
||||
"--json",
|
||||
r#"{"workspaceId":"wk_other","name":"Mismatch"}"#,
|
||||
])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains(
|
||||
"folder create got conflicting workspace_id values between positional arg and JSON payload",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -130,54 +130,6 @@ fn create_allows_workspace_only_with_empty_defaults() {
|
||||
assert_eq!(request.url, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_merges_positional_workspace_id_into_json_payload() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let data_dir = temp_dir.path();
|
||||
seed_workspace(data_dir, "wk_test");
|
||||
|
||||
let create_assert = cli_cmd(data_dir)
|
||||
.args([
|
||||
"request",
|
||||
"create",
|
||||
"wk_test",
|
||||
"--json",
|
||||
r#"{"name":"Merged Request","url":"https://example.com"}"#,
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
let request_id = parse_created_id(&create_assert.get_output().stdout, "request create");
|
||||
|
||||
cli_cmd(data_dir)
|
||||
.args(["request", "show", &request_id])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("\"workspaceId\": \"wk_test\""))
|
||||
.stdout(contains("\"name\": \"Merged Request\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_rejects_conflicting_workspace_ids_between_arg_and_json() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let data_dir = temp_dir.path();
|
||||
seed_workspace(data_dir, "wk_test");
|
||||
seed_workspace(data_dir, "wk_other");
|
||||
|
||||
cli_cmd(data_dir)
|
||||
.args([
|
||||
"request",
|
||||
"create",
|
||||
"wk_test",
|
||||
"--json",
|
||||
r#"{"workspaceId":"wk_other","name":"Mismatch"}"#,
|
||||
])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains(
|
||||
"request create got conflicting workspace_id values between positional arg and JSON payload",
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_send_persists_response_body_and_events() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
@@ -239,9 +191,7 @@ fn request_schema_http_outputs_json_schema() {
|
||||
.stdout(contains("\"type\":\"object\""))
|
||||
.stdout(contains("\"x-yaak-agent-hints\""))
|
||||
.stdout(contains("\"templateVariableSyntax\":\"${[ my_var ]}\""))
|
||||
.stdout(contains(
|
||||
"\"templateFunctionSyntax\":\"${[ namespace.my_func(a='aaa',b='bbb') ]}\"",
|
||||
))
|
||||
.stdout(contains("\"templateFunctionSyntax\":\"${[ namespace.my_func(a='aaa',b='bbb') ]}\""))
|
||||
.stdout(contains("\"authentication\":"))
|
||||
.stdout(contains("/foo/:id/comments/:commentId"))
|
||||
.stdout(contains("put concrete values in `urlParameters`"));
|
||||
|
||||
@@ -362,7 +362,7 @@ async fn handle_host_plugin_request<R: Runtime>(
|
||||
workspace_id: http_request.workspace_id.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
&UpdateSource::Plugin,
|
||||
&blobs,
|
||||
)?
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@ use crate::error::Result;
|
||||
use crate::models_ext::QueryManagerExt;
|
||||
use log::{error, info, warn};
|
||||
use serde::Serialize;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -244,11 +243,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
.path()
|
||||
.resolve("vendored/plugins", BaseDirectory::Resource)
|
||||
.expect("failed to resolve plugin directory resource");
|
||||
let bundled_plugin_dir = if is_dev() {
|
||||
resolve_workspace_plugins_dir().unwrap_or_else(|| vendored_plugin_dir.clone())
|
||||
} else {
|
||||
vendored_plugin_dir.clone()
|
||||
};
|
||||
|
||||
let installed_plugin_dir = app_handle
|
||||
.path()
|
||||
@@ -272,6 +266,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
.expect("failed to resolve plugin runtime")
|
||||
.join("index.cjs");
|
||||
|
||||
let dev_mode = is_dev();
|
||||
let query_manager =
|
||||
app_handle.state::<yaak_models::query_manager::QueryManager>().inner().clone();
|
||||
|
||||
@@ -279,13 +274,13 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
let app_handle_clone = app_handle.clone();
|
||||
tauri::async_runtime::block_on(async move {
|
||||
let manager = PluginManager::new(
|
||||
bundled_plugin_dir,
|
||||
vendored_plugin_dir,
|
||||
installed_plugin_dir,
|
||||
node_bin_path,
|
||||
plugin_runtime_main,
|
||||
&query_manager,
|
||||
&PluginContext::new_empty(),
|
||||
dev_mode,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to initialize plugins");
|
||||
@@ -327,11 +322,3 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
fn resolve_workspace_plugins_dir() -> Option<PathBuf> {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../..")
|
||||
.join("plugins")
|
||||
.canonicalize()
|
||||
.ok()
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ impl HttpConnectionManager {
|
||||
connections.retain(|_, (_, last_used)| last_used.elapsed() <= self.ttl);
|
||||
|
||||
if let Some((cached, last_used)) = connections.get_mut(&id) {
|
||||
info!("Re-using HTTP client {id}");
|
||||
*last_used = Instant::now();
|
||||
return Ok(CachedClient {
|
||||
client: cached.client.clone(),
|
||||
|
||||
@@ -24,6 +24,7 @@ use crate::plugin_handle::PluginHandle;
|
||||
use crate::server_ws::PluginRuntimeServerWebsocket;
|
||||
use log::{error, info, warn};
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -45,9 +46,9 @@ pub struct PluginManager {
|
||||
kill_tx: tokio::sync::watch::Sender<bool>,
|
||||
killed_rx: Arc<Mutex<Option<oneshot::Receiver<()>>>>,
|
||||
ws_service: Arc<PluginRuntimeServerWebsocket>,
|
||||
bundled_plugin_dir: PathBuf,
|
||||
vendored_plugin_dir: PathBuf,
|
||||
pub(crate) installed_plugin_dir: PathBuf,
|
||||
dev_mode: bool,
|
||||
}
|
||||
|
||||
/// Callback for plugin initialization events (e.g., toast notifications)
|
||||
@@ -57,21 +58,21 @@ impl PluginManager {
|
||||
/// Create a new PluginManager with the given paths.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `bundled_plugin_dir` - Directory to scan for bundled plugins
|
||||
/// * `vendored_plugin_dir` - Path to vendored plugins directory
|
||||
/// * `installed_plugin_dir` - Path to installed plugins directory
|
||||
/// * `node_bin_path` - Path to the yaaknode binary
|
||||
/// * `plugin_runtime_main` - Path to the plugin runtime index.cjs
|
||||
/// * `query_manager` - Query manager for bundled plugin registration and loading
|
||||
/// * `plugin_context` - Context to use while initializing plugins
|
||||
/// * `dev_mode` - Whether the app is in dev mode (affects plugin loading)
|
||||
pub async fn new(
|
||||
bundled_plugin_dir: PathBuf,
|
||||
vendored_plugin_dir: PathBuf,
|
||||
installed_plugin_dir: PathBuf,
|
||||
node_bin_path: PathBuf,
|
||||
plugin_runtime_main: PathBuf,
|
||||
query_manager: &QueryManager,
|
||||
plugin_context: &PluginContext,
|
||||
dev_mode: bool,
|
||||
) -> Result<PluginManager> {
|
||||
let (events_tx, mut events_rx) = mpsc::channel(2048);
|
||||
let (kill_server_tx, kill_server_rx) = tokio::sync::watch::channel(false);
|
||||
@@ -88,9 +89,9 @@ impl PluginManager {
|
||||
ws_service: Arc::new(ws_service.clone()),
|
||||
kill_tx: kill_server_tx,
|
||||
killed_rx: Arc::new(Mutex::new(Some(killed_rx))),
|
||||
bundled_plugin_dir,
|
||||
vendored_plugin_dir,
|
||||
installed_plugin_dir,
|
||||
dev_mode,
|
||||
};
|
||||
|
||||
// Forward events to subscribers
|
||||
@@ -191,11 +192,25 @@ impl PluginManager {
|
||||
Ok(plugin_manager)
|
||||
}
|
||||
|
||||
/// Get the vendored plugin directory path (resolves dev mode path if applicable)
|
||||
pub fn get_plugins_dir(&self) -> PathBuf {
|
||||
if self.dev_mode {
|
||||
// Use plugins directly for easy development
|
||||
// Tauri runs from crates-tauri/yaak-app/, so go up two levels to reach project root
|
||||
env::current_dir()
|
||||
.map(|cwd| cwd.join("../../plugins").canonicalize().unwrap())
|
||||
.unwrap_or_else(|_| self.vendored_plugin_dir.clone())
|
||||
} else {
|
||||
self.vendored_plugin_dir.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Read plugin directories from disk and return their paths.
|
||||
/// This is useful for discovering bundled plugins.
|
||||
pub async fn list_bundled_plugin_dirs(&self) -> Result<Vec<String>> {
|
||||
info!("Loading bundled plugins from {:?}", self.bundled_plugin_dir);
|
||||
read_plugins_dir(&self.bundled_plugin_dir).await
|
||||
let plugins_dir = self.get_plugins_dir();
|
||||
info!("Loading bundled plugins from {plugins_dir:?}");
|
||||
read_plugins_dir(&plugins_dir).await
|
||||
}
|
||||
|
||||
pub async fn uninstall(&self, plugin_context: &PluginContext, dir: &str) -> Result<()> {
|
||||
|
||||
@@ -273,5 +273,6 @@ pub fn find_client_certificate(
|
||||
});
|
||||
}
|
||||
|
||||
debug!("No matching client certificate found for {}", url_string);
|
||||
None
|
||||
}
|
||||
|
||||
7
npm/README.md
Normal file
7
npm/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Yaak CLI NPM Packages
|
||||
|
||||
The Rust `yaak` CLI binary is published to NPM with a meta package (`@yaakapp/cli`) and
|
||||
platform-specific optional dependency packages. The package exposes both `yaak` and `yaakcli`
|
||||
commands for compatibility.
|
||||
|
||||
This follows the same strategy previously used in the standalone `yaak-cli` repo.
|
||||
@@ -1,5 +1,5 @@
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const cliReadme = path.join(__dirname, "..", "..", "crates-cli", "yaak-cli", "README.md");
|
||||
fs.copyFileSync(cliReadme, path.join(__dirname, "README.md"));
|
||||
const readme = path.join(__dirname, "..", "..", "README.md");
|
||||
fs.copyFileSync(readme, path.join(__dirname, "README.md"));
|
||||
|
||||
56
package-lock.json
generated
56
package-lock.json
generated
@@ -73,7 +73,7 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.13",
|
||||
"@tauri-apps/cli": "^2.9.6",
|
||||
"@yaakapp/cli": "^0.4.0",
|
||||
"@yaakapp/cli": "^0.4.0-beta.2",
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"nodejs-file-downloader": "^4.13.0",
|
||||
@@ -4326,9 +4326,9 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@yaakapp/cli": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@yaakapp/cli/-/cli-0.4.0.tgz",
|
||||
"integrity": "sha512-8xnu2oFWlgV+xeIAHMuEgsqX6Sxq4UYrSH2WbafwDLbSep6fxpO74tiBH7xp4wakt/7Bcy9a2Q5R9nkAc1ZUdA==",
|
||||
"version": "0.4.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/@yaakapp/cli/-/cli-0.4.0-beta.2.tgz",
|
||||
"integrity": "sha512-UXPxTS9oWVCIr4rShC7HjcAX+gSmw/BQ5F1Xp3Rub3vY/G7+513JJsc1HhLGVZqFfOVRSMEKRxtF9/9okSyiHg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
@@ -4336,18 +4336,18 @@
|
||||
"yaakcli": "bin/cli.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@yaakapp/cli-darwin-arm64": "0.4.0",
|
||||
"@yaakapp/cli-darwin-x64": "0.4.0",
|
||||
"@yaakapp/cli-linux-arm64": "0.4.0",
|
||||
"@yaakapp/cli-linux-x64": "0.4.0",
|
||||
"@yaakapp/cli-win32-arm64": "0.4.0",
|
||||
"@yaakapp/cli-win32-x64": "0.4.0"
|
||||
"@yaakapp/cli-darwin-arm64": "0.4.0-beta.2",
|
||||
"@yaakapp/cli-darwin-x64": "0.4.0-beta.2",
|
||||
"@yaakapp/cli-linux-arm64": "0.4.0-beta.2",
|
||||
"@yaakapp/cli-linux-x64": "0.4.0-beta.2",
|
||||
"@yaakapp/cli-win32-arm64": "0.4.0-beta.2",
|
||||
"@yaakapp/cli-win32-x64": "0.4.0-beta.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@yaakapp/cli-darwin-arm64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@yaakapp/cli-darwin-arm64/-/cli-darwin-arm64-0.4.0.tgz",
|
||||
"integrity": "sha512-bl8+VQNPMabXNGQCa7u6w0JGe3CmzYZPsGE8Q+5wGSxa3trGf1bmq/fMW5JXrMi1P7Laepnyad0TGGP/2C8uwQ==",
|
||||
"version": "0.4.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/@yaakapp/cli-darwin-arm64/-/cli-darwin-arm64-0.4.0-beta.2.tgz",
|
||||
"integrity": "sha512-mqkyH5tIPRLs9JumP9ZmzjB5gIwmOL1yCDoJ1qVU8DIJ7mwlcQaPGYTK98pVdBcKOjofVakBTcpol9P8rBv4qw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4358,9 +4358,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@yaakapp/cli-darwin-x64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@yaakapp/cli-darwin-x64/-/cli-darwin-x64-0.4.0.tgz",
|
||||
"integrity": "sha512-R+ETXNBWvmA3W88ZoTk/JtG/PZaUb85y3SwBgMbwcgdhBVwNS/g+DbCspcTFI5zs8Txsf5VuiFU+dW9M9olZ6A==",
|
||||
"version": "0.4.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/@yaakapp/cli-darwin-x64/-/cli-darwin-x64-0.4.0-beta.2.tgz",
|
||||
"integrity": "sha512-QI/H2yUF8CkJq+cnRthoUWWTEJPH4QPA78FYcGjFRhvBaj1m2G/GlCA5NkTXm/fvIjNkQEODSihXrhU+zoSSCw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4371,9 +4371,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@yaakapp/cli-linux-arm64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@yaakapp/cli-linux-arm64/-/cli-linux-arm64-0.4.0.tgz",
|
||||
"integrity": "sha512-Pf7VyQf4r85FsI0qYnnst7URQF8/RxSZZj79cXLai0FnN3fDiypX4CmHx765bJxgfQZlBvqVmvPAaMW/TeiJEQ==",
|
||||
"version": "0.4.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/@yaakapp/cli-linux-arm64/-/cli-linux-arm64-0.4.0-beta.2.tgz",
|
||||
"integrity": "sha512-nvAp97LkgRpqVHyMwDdpkzlKOWG2kJXezCLRZaRWaEpbnNuviSF+0yzCuFGZRHEEspj7B0TiM+sKGkpvjNlweA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4384,9 +4384,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@yaakapp/cli-linux-x64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@yaakapp/cli-linux-x64/-/cli-linux-x64-0.4.0.tgz",
|
||||
"integrity": "sha512-bYWWfHAIW81A+ydJChjH1Qo3+aihz9gFLh7/9MOa6CJgnC6H3V5cnapmh50Hddt9l5ic02aA1FB8ORQOXxb01A==",
|
||||
"version": "0.4.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/@yaakapp/cli-linux-x64/-/cli-linux-x64-0.4.0-beta.2.tgz",
|
||||
"integrity": "sha512-9/qAMNrtE9glxih3XWGfFssIJpQ4mHNUTuWYKroc0aZZUrunnCw3tX1tQtFDxy0QRIZcGlBeBRtgxuuBd2fYbg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4397,9 +4397,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@yaakapp/cli-win32-arm64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@yaakapp/cli-win32-arm64/-/cli-win32-arm64-0.4.0.tgz",
|
||||
"integrity": "sha512-8X12xkyidyYZ5vtarZGFSYR6HJbUMFUsNxYPNQccnYJIY+soNkjJHOWDjaRvBzCbR8MLT9N04Y5PE/Jv20gXpA==",
|
||||
"version": "0.4.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/@yaakapp/cli-win32-arm64/-/cli-win32-arm64-0.4.0-beta.2.tgz",
|
||||
"integrity": "sha512-eM1zL+hl0y3NBLxWO90y9VyaFsAf0HAsECBWvhKhvEdd6KG4K1XzpXrC30cHQBGePIrCa/az8eSuvTde0Z2C/g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4410,9 +4410,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@yaakapp/cli-win32-x64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@yaakapp/cli-win32-x64/-/cli-win32-x64-0.4.0.tgz",
|
||||
"integrity": "sha512-wansfrCCycFcFclowQQxfsNLIAyATyqnnbITED5gUfUrBf8NFHrG0sWVCWlXUhHU7YvpmqL7CsdtlMkIGiZCPQ==",
|
||||
"version": "0.4.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/@yaakapp/cli-win32-x64/-/cli-win32-x64-0.4.0-beta.2.tgz",
|
||||
"integrity": "sha512-ySdiK0h216EqURkM5KZoqbPTgbIX4eNK/IgrKwSazxRb369HOZYQ8X68as+VRxEL4NCMmWlQNdbBDuf+apg/mg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.13",
|
||||
"@tauri-apps/cli": "^2.9.6",
|
||||
"@yaakapp/cli": "^0.4.0",
|
||||
"@yaakapp/cli": "^0.4.0-beta.2",
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"nodejs-file-downloader": "^4.13.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@yaak/action-send-folder",
|
||||
"displayName": "Send All",
|
||||
"description": "Send all HTTP requests in a folder sequentially in tree order",
|
||||
"description": "Send all HTTP requests in a folder sequentially",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mountain-loop/yaak.git",
|
||||
|
||||
@@ -14,44 +14,22 @@ export const plugin: PluginDefinition = {
|
||||
ctx.httpRequest.list(),
|
||||
]);
|
||||
|
||||
// Build the send order to match tree ordering:
|
||||
// sort siblings by sortPriority then updatedAt, and traverse folders depth-first.
|
||||
const compareByOrder = (
|
||||
a: Pick<typeof allFolders[number], 'sortPriority' | 'updatedAt'>,
|
||||
b: Pick<typeof allFolders[number], 'sortPriority' | 'updatedAt'>,
|
||||
) => {
|
||||
if (a.sortPriority === b.sortPriority) {
|
||||
return a.updatedAt > b.updatedAt ? 1 : -1;
|
||||
}
|
||||
return a.sortPriority - b.sortPriority;
|
||||
};
|
||||
|
||||
const childrenByFolderId = new Map<string, Array<typeof allFolders[number] | typeof allRequests[number]>>();
|
||||
for (const folder of allFolders) {
|
||||
if (folder.folderId == null) continue;
|
||||
const children = childrenByFolderId.get(folder.folderId) ?? [];
|
||||
children.push(folder);
|
||||
childrenByFolderId.set(folder.folderId, children);
|
||||
}
|
||||
for (const request of allRequests) {
|
||||
if (request.folderId == null) continue;
|
||||
const children = childrenByFolderId.get(request.folderId) ?? [];
|
||||
children.push(request);
|
||||
childrenByFolderId.set(request.folderId, children);
|
||||
}
|
||||
|
||||
const requestsToSend: typeof allRequests = [];
|
||||
const collectRequests = (folderId: string) => {
|
||||
const children = (childrenByFolderId.get(folderId) ?? []).slice().sort(compareByOrder);
|
||||
for (const child of children) {
|
||||
if (child.model === 'folder') {
|
||||
collectRequests(child.id);
|
||||
} else if (child.model === 'http_request') {
|
||||
requestsToSend.push(child);
|
||||
// Build a set of all folder IDs that are descendants of the target folder
|
||||
const folderIds = new Set<string>([targetFolder.id]);
|
||||
const addDescendants = (parentId: string) => {
|
||||
for (const folder of allFolders) {
|
||||
if (folder.folderId === parentId && !folderIds.has(folder.id)) {
|
||||
folderIds.add(folder.id);
|
||||
addDescendants(folder.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
collectRequests(targetFolder.id);
|
||||
addDescendants(targetFolder.id);
|
||||
|
||||
// Filter HTTP requests to those in the target folder or its descendants
|
||||
const requestsToSend = allRequests.filter(
|
||||
(req) => req.folderId != null && folderIds.has(req.folderId),
|
||||
);
|
||||
|
||||
if (requestsToSend.length === 0) {
|
||||
await ctx.toast.show({
|
||||
@@ -62,7 +40,7 @@ export const plugin: PluginDefinition = {
|
||||
return;
|
||||
}
|
||||
|
||||
// Send requests sequentially in the calculated folder order.
|
||||
// Send each request sequentially
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
|
||||
@@ -72,10 +72,6 @@ export const plugin: PluginDefinition = {
|
||||
name: 'header',
|
||||
label: 'Header Name',
|
||||
async dynamic(ctx, args) {
|
||||
// Dynamic form config also runs during send-time rendering.
|
||||
// Keep this preview-only to avoid side-effect request sends.
|
||||
if (args.purpose !== 'preview') return null;
|
||||
|
||||
const response = await getResponse(ctx, {
|
||||
requestId: String(args.values.request || ''),
|
||||
purpose: args.purpose,
|
||||
@@ -150,10 +146,6 @@ export const plugin: PluginDefinition = {
|
||||
label: 'JSONPath or XPath',
|
||||
placeholder: '$.books[0].id or /books[0]/id',
|
||||
dynamic: async (ctx, args) => {
|
||||
// Dynamic form config also runs during send-time rendering.
|
||||
// Keep this preview-only to avoid side-effect request sends.
|
||||
if (args.purpose !== 'preview') return null;
|
||||
|
||||
const resp = await getResponse(ctx, {
|
||||
requestId: String(args.values.request || ''),
|
||||
purpose: 'preview',
|
||||
|
||||
Reference in New Issue
Block a user