Compare commits

..

19 Commits

Author SHA1 Message Date
Gregory Schier
f302dc39a2 Move local plugin install command into plugins_ext 2026-03-01 16:42:13 -08:00
Gregory Schier
2ca51125a4 Improve plugin source modeling and runtime dedup (#414) 2026-03-01 16:30:43 -08:00
Gregory Schier
2d99e26f19 plugin-events: route model/find requests through shared handler (#409) 2026-02-28 14:16:32 -08:00
Gregory Schier
da1e04d99e Fix Copy as gRPCurl with template-tag payloads (#413) 2026-02-28 07:39:44 -08:00
dependabot[bot]
d875eaa5bf Bump minimatch from 3.1.2 to 3.1.5 (#411)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-28 07:29:04 -08:00
Gregory Schier
5fa2469cd6 Some cleanup 2026-02-28 07:24:30 -08:00
Gregory Schier
49053cb423 Fix [object Object] request descriptions after OpenAPI import (#412) 2026-02-27 15:36:46 -08:00
Gregory Schier
37d0cabb22 fix: preserve drive letter in Windows plugin paths (#410)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 16:23:24 -08:00
Gregory Schier
435ee54140 docs: clarify app vs cli tag naming 2026-02-26 09:40:58 -08:00
dependabot[bot]
407f2c9921 Bump rollup from 4.55.1 to 4.59.0 (#406)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-26 09:31:25 -08:00
Gregory Schier
3a6630a14d tests(cli): prevent request send test teardown hang (#408) 2026-02-26 08:58:13 -08:00
Gregory Schier
ffc80d234c Remove noisy logs 2026-02-26 08:13:18 -08:00
Gregory Schier
644c683714 Update CLI package 2026-02-26 08:13:07 -08:00
Gregory Schier
d2c1bd79ac Fix chaining multiple requests together.
Fixes
https://yaak.app/feedback/posts/request-chaining-issue-on-cold-start
2026-02-26 08:12:54 -08:00
Gregory Schier
020589f2e6 fix: keep Send All response updates window-scoped (#405) 2026-02-25 06:54:59 -08:00
Gregory Schier
b2a70d8938 cli: prep 0.4.0 stable release 2026-02-23 08:55:16 -08:00
Gregory Schier
64c626ed30 Improve CLI streaming output, logging flags, and schema/help ergonomics 2026-02-23 08:06:41 -08:00
Gregory Schier
35d9ed901a Add workspace/environment schemas and shared agent hints 2026-02-23 08:06:41 -08:00
gschier
f04b34be1a Deploying to main from @ mountain-loop/yaak@1e7e1232da 🚀 2026-02-23 15:47:59 +00:00
55 changed files with 1189 additions and 739 deletions

View File

@@ -1,62 +0,0 @@
---
description: Review a PR in a new worktree
allowed-tools: Bash(git worktree:*), Bash(gh pr:*), Bash(git branch:*)
---
Check out a GitHub pull request for review.
## Usage
```
/check-out-pr <PR_NUMBER>
```
## What to do
1. If no PR number is provided, list all open pull requests and ask the user to select one
2. Get PR information using `gh pr view <PR_NUMBER> --json number,headRefName`
3. **Ask the user** whether they want to:
- **A) Check out in current directory** — simple `gh pr checkout <PR_NUMBER>`
- **B) Create a new worktree** — isolated copy at `../yaak-worktrees/pr-<PR_NUMBER>`
4. Follow the appropriate path below
## Option A: Check out in current directory
1. Run `gh pr checkout <PR_NUMBER>`
2. Inform the user which branch they're now on
## Option B: Create a new worktree
1. Create a new worktree at `../yaak-worktrees/pr-<PR_NUMBER>` using `git worktree add` with a timeout of at least 300000ms (5 minutes) since the post-checkout hook runs a bootstrap script
2. Checkout the PR branch in the new worktree using `gh pr checkout <PR_NUMBER>`
3. The post-checkout hook will automatically:
- Create `.env.local` with unique ports
- Copy editor config folders
- Run `npm install && npm run bootstrap`
4. Inform the user:
- Where the worktree was created
- What ports were assigned
- How to access it (cd command)
- How to run the dev server
- How to remove the worktree when done
### Example worktree output
```
Created worktree for PR #123 at ../yaak-worktrees/pr-123
Branch: feature-auth
Ports: Vite (1421), MCP (64344)
To start working:
cd ../yaak-worktrees/pr-123
npm run app-dev
To remove when done:
git worktree remove ../yaak-worktrees/pr-123
```
## Error Handling
- If the PR doesn't exist, show a helpful error
- If the worktree already exists, inform the user and ask if they want to remove and recreate it
- If `gh` CLI is not available, inform the user to install it

View File

@@ -1,35 +0,0 @@
# Worktree Management Skill
## Creating Worktrees
When creating git worktrees for this project, ALWAYS use the path format:
```
../yaak-worktrees/<NAME>
```
For example:
- `git worktree add ../yaak-worktrees/feature-auth`
- `git worktree add ../yaak-worktrees/bugfix-login`
- `git worktree add ../yaak-worktrees/refactor-api`
## What Happens Automatically
The post-checkout hook will automatically:
1. Create `.env.local` with unique ports (YAAK_DEV_PORT and YAAK_PLUGIN_MCP_SERVER_PORT)
2. Copy gitignored editor config folders (.zed, .idea, etc.)
3. Run `npm install && npm run bootstrap`
## Deleting Worktrees
```bash
git worktree remove ../yaak-worktrees/<NAME>
```
## Port Assignments
- Main worktree: 1420 (Vite), 64343 (MCP)
- First worktree: 1421, 64344
- Second worktree: 1422, 64345
- etc.
Each worktree can run `npm run app-dev` simultaneously without conflicts.

View File

@@ -1,46 +0,0 @@
---
name: release-check-out-pr
description: Check out a GitHub pull request for review in this repo, either in the current directory or in a new isolated worktree at ../yaak-worktrees/pr-<PR_NUMBER>. Use when asked to run or replace the old Claude check-out-pr command.
---
# Check Out PR
Check out a PR by number and let the user choose between current-directory checkout and isolated worktree checkout.
## Workflow
1. Confirm `gh` CLI is available.
2. If no PR number is provided, list open PRs (`gh pr list`) and ask the user to choose one.
3. Read PR metadata:
- `gh pr view <PR_NUMBER> --json number,headRefName`
4. Ask the user to choose:
- Option A: check out in the current directory
- Option B: create a new worktree at `../yaak-worktrees/pr-<PR_NUMBER>`
## Option A: Current Directory
1. Run:
- `gh pr checkout <PR_NUMBER>`
2. Report the checked-out branch.
## Option B: New Worktree
1. Use path:
- `../yaak-worktrees/pr-<PR_NUMBER>`
2. Create the worktree with a timeout of at least 5 minutes because checkout hooks run bootstrap.
3. In the new worktree, run:
- `gh pr checkout <PR_NUMBER>`
4. Report:
- Worktree path
- Assigned ports from `.env.local` if present
- How to start work:
- `cd ../yaak-worktrees/pr-<PR_NUMBER>`
- `npm run app-dev`
- How to remove when done:
- `git worktree remove ../yaak-worktrees/pr-<PR_NUMBER>`
## Error Handling
- If PR does not exist, show a clear error.
- If worktree already exists, ask whether to reuse it or remove/recreate it.
- If `gh` is missing, instruct the user to install/authenticate it.

View File

@@ -1,37 +0,0 @@
---
name: worktree-management
description: Manage Yaak git worktrees using the standard ../yaak-worktrees/<NAME> layout, including creation, removal, and expected automatic setup behavior and port assignments.
---
# Worktree Management
Use the Yaak-standard worktree path layout and lifecycle commands.
## Path Convention
Always create worktrees under:
`../yaak-worktrees/<NAME>`
Examples:
- `git worktree add ../yaak-worktrees/feature-auth`
- `git worktree add ../yaak-worktrees/bugfix-login`
- `git worktree add ../yaak-worktrees/refactor-api`
## Automatic Setup After Checkout
Project git hooks automatically:
1. Create `.env.local` with unique `YAAK_DEV_PORT` and `YAAK_PLUGIN_MCP_SERVER_PORT`
2. Copy gitignored editor config folders
3. Run `npm install && npm run bootstrap`
## Remove Worktree
`git worktree remove ../yaak-worktrees/<NAME>`
## Port Pattern
- Main worktree: Vite `1420`, MCP `64343`
- First extra worktree: `1421`, `64344`
- Second extra worktree: `1422`, `64345`
- Continue incrementally for additional worktrees

3
.gitignore vendored
View File

@@ -54,3 +54,6 @@ flatpak/node-sources.json
# Local Codex desktop env state
.codex/environments/environment.toml
# Claude Code local settings
.claude/settings.local.json

2
AGENTS.md Normal file
View File

@@ -0,0 +1,2 @@
- Tag safety: app releases use `v*` tags and CLI releases use `yaak-cli-*` tags; always confirm which one is requested before retagging.
- Do not commit, push, or tag without explicit approval

1
Cargo.lock generated
View File

@@ -10359,7 +10359,6 @@ dependencies = [
"md5 0.7.0",
"path-slash",
"rand 0.9.1",
"regex 1.11.1",
"reqwest",
"serde",
"serde_json",

View File

@@ -22,7 +22,7 @@
<!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https:&#x2F;&#x2F;github.com&#x2F;MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a>&nbsp;&nbsp;<a href="https://github.com/dharsanb"><img src="https:&#x2F;&#x2F;github.com&#x2F;dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a>&nbsp;&nbsp;<a href="https://github.com/railwayapp"><img src="https:&#x2F;&#x2F;github.com&#x2F;railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a>&nbsp;&nbsp;<a href="https://github.com/caseyamcl"><img src="https:&#x2F;&#x2F;github.com&#x2F;caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a>&nbsp;&nbsp;<a href="https://github.com/bytebase"><img src="https:&#x2F;&#x2F;github.com&#x2F;bytebase.png" width="80px" alt="User avatar: bytebase" /></a>&nbsp;&nbsp;<a href="https://github.com/"><img src="https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;JamesIves&#x2F;github-sponsors-readme-action&#x2F;dev&#x2F;.github&#x2F;assets&#x2F;placeholder.png" width="80px" alt="User avatar: " /></a>&nbsp;&nbsp;<!-- sponsors-premium -->
</p>
<p align="center">
<!-- sponsors-base --><a href="https://github.com/seanwash"><img src="https:&#x2F;&#x2F;github.com&#x2F;seanwash.png" width="50px" alt="User avatar: seanwash" /></a>&nbsp;&nbsp;<a href="https://github.com/jerath"><img src="https:&#x2F;&#x2F;github.com&#x2F;jerath.png" width="50px" alt="User avatar: jerath" /></a>&nbsp;&nbsp;<a href="https://github.com/itsa-sh"><img src="https:&#x2F;&#x2F;github.com&#x2F;itsa-sh.png" width="50px" alt="User avatar: itsa-sh" /></a>&nbsp;&nbsp;<a href="https://github.com/dmmulroy"><img src="https:&#x2F;&#x2F;github.com&#x2F;dmmulroy.png" width="50px" alt="User avatar: dmmulroy" /></a>&nbsp;&nbsp;<a href="https://github.com/timcole"><img src="https:&#x2F;&#x2F;github.com&#x2F;timcole.png" width="50px" alt="User avatar: timcole" /></a>&nbsp;&nbsp;<a href="https://github.com/VLZH"><img src="https:&#x2F;&#x2F;github.com&#x2F;VLZH.png" width="50px" alt="User avatar: VLZH" /></a>&nbsp;&nbsp;<a href="https://github.com/terasaka2k"><img src="https:&#x2F;&#x2F;github.com&#x2F;terasaka2k.png" width="50px" alt="User avatar: terasaka2k" /></a>&nbsp;&nbsp;<a href="https://github.com/andriyor"><img src="https:&#x2F;&#x2F;github.com&#x2F;andriyor.png" width="50px" alt="User avatar: andriyor" /></a>&nbsp;&nbsp;<a href="https://github.com/majudhu"><img src="https:&#x2F;&#x2F;github.com&#x2F;majudhu.png" width="50px" alt="User avatar: majudhu" /></a>&nbsp;&nbsp;<a href="https://github.com/axelrindle"><img src="https:&#x2F;&#x2F;github.com&#x2F;axelrindle.png" width="50px" alt="User avatar: axelrindle" /></a>&nbsp;&nbsp;<a href="https://github.com/jirizverina"><img src="https:&#x2F;&#x2F;github.com&#x2F;jirizverina.png" width="50px" alt="User avatar: jirizverina" /></a>&nbsp;&nbsp;<a href="https://github.com/chip-well"><img src="https:&#x2F;&#x2F;github.com&#x2F;chip-well.png" width="50px" alt="User avatar: chip-well" /></a>&nbsp;&nbsp;<a href="https://github.com/GRAYAH"><img src="https:&#x2F;&#x2F;github.com&#x2F;GRAYAH.png" width="50px" alt="User avatar: GRAYAH" /></a>&nbsp;&nbsp;<a href="https://github.com/flashblaze"><img src="https:&#x2F;&#x2F;github.com&#x2F;flashblaze.png" width="50px" alt="User avatar: flashblaze" /></a>&nbsp;&nbsp;<!-- sponsors-base -->
<!-- sponsors-base --><a href="https://github.com/seanwash"><img src="https:&#x2F;&#x2F;github.com&#x2F;seanwash.png" width="50px" alt="User avatar: seanwash" /></a>&nbsp;&nbsp;<a href="https://github.com/jerath"><img src="https:&#x2F;&#x2F;github.com&#x2F;jerath.png" width="50px" alt="User avatar: jerath" /></a>&nbsp;&nbsp;<a href="https://github.com/itsa-sh"><img src="https:&#x2F;&#x2F;github.com&#x2F;itsa-sh.png" width="50px" alt="User avatar: itsa-sh" /></a>&nbsp;&nbsp;<a href="https://github.com/dmmulroy"><img src="https:&#x2F;&#x2F;github.com&#x2F;dmmulroy.png" width="50px" alt="User avatar: dmmulroy" /></a>&nbsp;&nbsp;<a href="https://github.com/timcole"><img src="https:&#x2F;&#x2F;github.com&#x2F;timcole.png" width="50px" alt="User avatar: timcole" /></a>&nbsp;&nbsp;<a href="https://github.com/VLZH"><img src="https:&#x2F;&#x2F;github.com&#x2F;VLZH.png" width="50px" alt="User avatar: VLZH" /></a>&nbsp;&nbsp;<a href="https://github.com/terasaka2k"><img src="https:&#x2F;&#x2F;github.com&#x2F;terasaka2k.png" width="50px" alt="User avatar: terasaka2k" /></a>&nbsp;&nbsp;<a href="https://github.com/andriyor"><img src="https:&#x2F;&#x2F;github.com&#x2F;andriyor.png" width="50px" alt="User avatar: andriyor" /></a>&nbsp;&nbsp;<a href="https://github.com/majudhu"><img src="https:&#x2F;&#x2F;github.com&#x2F;majudhu.png" width="50px" alt="User avatar: majudhu" /></a>&nbsp;&nbsp;<a href="https://github.com/axelrindle"><img src="https:&#x2F;&#x2F;github.com&#x2F;axelrindle.png" width="50px" alt="User avatar: axelrindle" /></a>&nbsp;&nbsp;<a href="https://github.com/jirizverina"><img src="https:&#x2F;&#x2F;github.com&#x2F;jirizverina.png" width="50px" alt="User avatar: jirizverina" /></a>&nbsp;&nbsp;<a href="https://github.com/chip-well"><img src="https:&#x2F;&#x2F;github.com&#x2F;chip-well.png" width="50px" alt="User avatar: chip-well" /></a>&nbsp;&nbsp;<a href="https://github.com/GRAYAH"><img src="https:&#x2F;&#x2F;github.com&#x2F;GRAYAH.png" width="50px" alt="User avatar: GRAYAH" /></a>&nbsp;&nbsp;<a href="https://github.com/flashblaze"><img src="https:&#x2F;&#x2F;github.com&#x2F;flashblaze.png" width="50px" alt="User avatar: flashblaze" /></a>&nbsp;&nbsp;<a href="https://github.com/Frostist"><img src="https:&#x2F;&#x2F;github.com&#x2F;Frostist.png" width="50px" alt="User avatar: Frostist" /></a>&nbsp;&nbsp;<!-- sponsors-base -->
</p>
![Yaak API Client](https://yaak.app/static/screenshot.png)

View File

@@ -1,93 +1,66 @@
# yaak-cli
# Yaak CLI
Command-line interface for Yaak.
The `yaak` CLI for publishing plugins and creating/updating/sending requests.
## Command Overview
## Installation
Current top-level commands:
```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:
```text
yaakcli send <request_id>
yaakcli agent-help
yaakcli workspace list
yaakcli workspace schema [--pretty]
yaakcli workspace show <workspace_id>
yaakcli workspace create --name <name>
yaakcli workspace create --json '{"name":"My Workspace"}'
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]
Scan my API routes and create a workspace (using yaak cli) with
all the requests needed for me to do manual testing?
```
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"}'
```text
Send all the GraphQL requests in my workspace
```
## Roadmap
## Description
Planned command expansion (request schema and polymorphic send) is tracked in `PLAN.md`.
Here's the current print of `yaak --help`
When command behavior changes, update this README and verify with:
```text
Yaak CLI - API client from the command line
```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
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)
```

View File

@@ -68,12 +68,8 @@ 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, conflicts_with = "sequential")]
#[arg(long)]
pub parallel: bool,
/// Stop on first request failure when sending folders/workspaces
@@ -342,8 +338,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"}'
Do not combine <workspace_id> with --json."#)]
4) yaak environment create <workspace_id> --json '{"name":"Production"}'
"#)]
Create {
/// Workspace ID for flag-based mode, or positional JSON payload shorthand
#[arg(value_name = "WORKSPACE_ID_OR_JSON")]

View File

@@ -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, parse_optional_json, parse_required_json, require_id,
validate_create_id,
apply_merge_patch, is_json_shorthand, merge_workspace_id_arg, parse_optional_json,
parse_required_json, require_id, validate_create_id,
};
use crate::utils::schema::append_agent_hints;
use schemars::schema_for;
@@ -34,18 +34,13 @@ 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(())
}
@@ -83,17 +78,11 @@ fn create(
name: Option<String>,
json: Option<String>,
) -> CommandResult {
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 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));
let payload = parse_optional_json(
json,
workspace_id.clone().filter(|v| is_json_shorthand(v)),
"environment create",
)?;
let payload = parse_optional_json(json, json_shorthand, "environment create")?;
if let Some(payload) = payload {
if name.is_some() {
@@ -103,10 +92,11 @@ 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}"))?;
if environment.workspace_id.is_empty() {
return Err("environment create JSON requires non-empty \"workspaceId\"".to_string());
}
merge_workspace_id_arg(
workspace_id_arg.as_deref(),
&mut environment.workspace_id,
"environment create",
)?;
if environment.parent_model.is_empty() {
environment.parent_model = "environment".to_string();
@@ -121,7 +111,7 @@ fn create(
return Ok(());
}
let workspace_id = workspace_id.ok_or_else(|| {
let workspace_id = workspace_id_arg.ok_or_else(|| {
"environment create requires workspace_id unless JSON payload is provided".to_string()
})?;
let name = name.ok_or_else(|| {

View File

@@ -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, parse_optional_json, parse_required_json, require_id,
validate_create_id,
apply_merge_patch, is_json_shorthand, merge_workspace_id_arg, parse_optional_json,
parse_required_json, require_id, validate_create_id,
};
use yaak_models::models::Folder;
use yaak_models::util::UpdateSource;
@@ -58,15 +58,11 @@ fn create(
name: Option<String>,
json: Option<String>,
) -> CommandResult {
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 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));
let payload = parse_optional_json(
json,
workspace_id.clone().filter(|v| is_json_shorthand(v)),
"folder create",
)?;
let payload = parse_optional_json(json, json_shorthand, "folder create")?;
if let Some(payload) = payload {
if name.is_some() {
@@ -74,12 +70,13 @@ fn create(
}
validate_create_id(&payload, "folder")?;
let 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}"))?;
if folder.workspace_id.is_empty() {
return Err("folder create JSON requires non-empty \"workspaceId\"".to_string());
}
merge_workspace_id_arg(
workspace_id_arg.as_deref(),
&mut folder.workspace_id,
"folder create",
)?;
let created = ctx
.db()
@@ -90,7 +87,7 @@ fn create(
return Ok(());
}
let workspace_id = workspace_id.ok_or_else(|| {
let workspace_id = workspace_id_arg.ok_or_else(|| {
"folder create requires workspace_id unless JSON payload is provided".to_string()
})?;
let name = name.ok_or_else(|| {

View File

@@ -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, parse_optional_json, parse_required_json, require_id,
validate_create_id,
apply_merge_patch, is_json_shorthand, merge_workspace_id_arg, 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_http::sender::HttpResponseEvent as SenderHttpResponseEvent;
use yaak::send::{SendHttpRequestByIdWithPluginsParams, send_http_request_by_id_with_plugins};
use yaak_http::sender::HttpResponseEvent as SenderHttpResponseEvent;
use yaak_models::models::{GrpcRequest, HttpRequest, WebsocketRequest};
use yaak_models::queries::any_request::AnyRequest;
use yaak_models::util::UpdateSource;
@@ -336,15 +336,11 @@ fn create(
url: Option<String>,
json: Option<String>,
) -> CommandResult {
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 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));
let payload = parse_optional_json(
json,
workspace_id.clone().filter(|v| is_json_shorthand(v)),
"request create",
)?;
let payload = parse_optional_json(json, json_shorthand, "request create")?;
if let Some(payload) = payload {
if name.is_some() || method.is_some() || url.is_some() {
@@ -352,12 +348,13 @@ fn create(
}
validate_create_id(&payload, "request")?;
let 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}"))?;
if request.workspace_id.is_empty() {
return Err("request create JSON requires non-empty \"workspaceId\"".to_string());
}
merge_workspace_id_arg(
workspace_id_arg.as_deref(),
&mut request.workspace_id,
"request create",
)?;
let created = ctx
.db()
@@ -368,7 +365,7 @@ fn create(
return Ok(());
}
let workspace_id = workspace_id.ok_or_else(|| {
let workspace_id = workspace_id_arg.ok_or_else(|| {
"request create requires workspace_id unless JSON payload is provided".to_string()
})?;
let name = name.unwrap_or_default();

View File

@@ -63,6 +63,30 @@ 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,

View File

@@ -1,9 +1,14 @@
use std::io::{Read, Write};
use std::net::TcpListener;
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::Duration;
pub struct TestHttpServer {
pub url: String,
addr: SocketAddr,
shutdown: Arc<AtomicBool>,
handle: Option<thread::JoinHandle<()>>,
}
@@ -12,29 +17,46 @@ impl TestHttpServer {
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind test HTTP server");
let addr = listener.local_addr().expect("Failed to get local addr");
let url = format!("http://{addr}/test");
listener.set_nonblocking(true).expect("Failed to set test server listener nonblocking");
let shutdown = Arc::new(AtomicBool::new(false));
let shutdown_signal = Arc::clone(&shutdown);
let body_bytes = body.as_bytes().to_vec();
let handle = thread::spawn(move || {
if let Ok((mut stream, _)) = listener.accept() {
let mut request_buf = [0u8; 4096];
let _ = stream.read(&mut request_buf);
while !shutdown_signal.load(Ordering::Relaxed) {
match listener.accept() {
Ok((mut stream, _)) => {
let _ = stream.set_read_timeout(Some(Duration::from_secs(1)));
let mut request_buf = [0u8; 4096];
let _ = stream.read(&mut request_buf);
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body_bytes.len()
);
let _ = stream.write_all(response.as_bytes());
let _ = stream.write_all(&body_bytes);
let _ = stream.flush();
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body_bytes.len()
);
let _ = stream.write_all(response.as_bytes());
let _ = stream.write_all(&body_bytes);
let _ = stream.flush();
break;
}
Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
thread::sleep(Duration::from_millis(10));
}
Err(_) => break,
}
}
});
Self { url, handle: Some(handle) }
Self { url, addr, shutdown, handle: Some(handle) }
}
}
impl Drop for TestHttpServer {
fn drop(&mut self) {
self.shutdown.store(true, Ordering::Relaxed);
let _ = TcpStream::connect(self.addr);
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}

View File

@@ -79,6 +79,54 @@ 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");
@@ -91,6 +139,8 @@ 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\""));
}

View File

@@ -72,3 +72,51 @@ 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",
));
}

View File

@@ -130,6 +130,54 @@ 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");
@@ -191,7 +239,9 @@ 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`"));

View File

@@ -37,8 +37,8 @@ use yaak_grpc::{Code, ServiceDefinition, serialize_message};
use yaak_mac_window::AppHandleMacWindowExt;
use yaak_models::models::{
AnyModel, CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent,
GrpcEventType, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseState, Plugin,
Workspace, WorkspaceMeta,
GrpcEventType, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseState, Workspace,
WorkspaceMeta,
};
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
use yaak_plugins::events::{
@@ -1345,29 +1345,6 @@ async fn cmd_send_http_request<R: Runtime>(
Ok(r)
}
#[tauri::command]
async fn cmd_install_plugin<R: Runtime>(
directory: &str,
url: Option<String>,
plugin_manager: State<'_, PluginManager>,
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
) -> YaakResult<Plugin> {
let plugin = app_handle.db().upsert_plugin(
&Plugin { directory: directory.into(), url, enabled: true, ..Default::default() },
&UpdateSource::from_window_label(window.label()),
)?;
plugin_manager
.add_plugin(
&PluginContext::new(Some(window.label().to_string()), window.workspace_id()),
&plugin,
)
.await?;
Ok(plugin)
}
#[tauri::command]
async fn cmd_reload_plugins<R: Runtime>(
app_handle: AppHandle<R>,
@@ -1652,7 +1629,6 @@ pub fn run() {
cmd_workspace_actions,
cmd_folder_actions,
cmd_import_data,
cmd_install_plugin,
cmd_metadata,
cmd_new_child_window,
cmd_new_main_window,
@@ -1721,6 +1697,7 @@ pub fn run() {
git_ext::cmd_git_rm_remote,
//
// Plugin commands
plugins_ext::cmd_plugins_install_from_directory,
plugins_ext::cmd_plugins_search,
plugins_ext::cmd_plugins_install,
plugins_ext::cmd_plugins_uninstall,

View File

@@ -15,6 +15,7 @@ use yaak_models::error::Result;
use yaak_models::models::{AnyModel, GraphQlIntrospection, GrpcEvent, Settings, WebsocketEvent};
use yaak_models::query_manager::QueryManager;
use yaak_models::util::UpdateSource;
use yaak_plugins::manager::PluginManager;
const MODEL_CHANGES_RETENTION_HOURS: i64 = 1;
const MODEL_CHANGES_POLL_INTERVAL_MS: u64 = 1000;
@@ -255,23 +256,32 @@ pub(crate) fn models_upsert_graphql_introspection<R: Runtime>(
}
#[tauri::command]
pub(crate) fn models_workspace_models<R: Runtime>(
pub(crate) async fn models_workspace_models<R: Runtime>(
window: WebviewWindow<R>,
workspace_id: Option<&str>,
plugin_manager: State<'_, PluginManager>,
) -> Result<String> {
let db = window.db();
let mut l: Vec<AnyModel> = Vec::new();
// Add the settings
l.push(db.get_settings().into());
// Add the global models
{
let db = window.db();
l.push(db.get_settings().into());
l.append(&mut db.list_workspaces()?.into_iter().map(Into::into).collect());
l.append(&mut db.list_key_values()?.into_iter().map(Into::into).collect());
}
// Add global models
l.append(&mut db.list_workspaces()?.into_iter().map(Into::into).collect());
l.append(&mut db.list_key_values()?.into_iter().map(Into::into).collect());
l.append(&mut db.list_plugins()?.into_iter().map(Into::into).collect());
let plugins = {
let db = window.db();
db.list_plugins()?
};
let plugins = plugin_manager.resolve_plugins_for_runtime_from_db(plugins).await;
l.append(&mut plugins.into_iter().map(Into::into).collect());
// Add the workspace children
if let Some(wid) = workspace_id {
let db = window.db();
l.append(&mut db.list_cookie_jars(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_environments_ensure_base(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_folders(wid)?.into_iter().map(Into::into).collect());

View File

@@ -19,13 +19,13 @@ use yaak::plugin_events::{
GroupedPluginEvent, HostRequest, SharedPluginEventContext, handle_shared_plugin_event,
};
use yaak_crypto::manager::EncryptionManager;
use yaak_models::models::{AnyModel, HttpResponse, Plugin};
use yaak_models::models::{HttpResponse, Plugin};
use yaak_models::queries::any_request::AnyRequest;
use yaak_models::util::UpdateSource;
use yaak_plugins::error::Error::PluginErr;
use yaak_plugins::events::{
Color, EmptyPayload, ErrorResponse, FindHttpResponsesResponse, GetCookieValueResponse, Icon,
InternalEvent, InternalEventPayload, ListCookieNamesResponse, ListOpenWorkspacesResponse,
Color, EmptyPayload, ErrorResponse, GetCookieValueResponse, Icon, InternalEvent,
InternalEventPayload, ListCookieNamesResponse, ListOpenWorkspacesResponse,
RenderGrpcRequestResponse, RenderHttpRequestResponse, SendHttpRequestResponse,
ShowToastRequest, TemplateRenderResponse, WindowInfoResponse, WindowNavigateEvent,
WorkspaceInfo,
@@ -190,71 +190,6 @@ async fn handle_host_plugin_request<R: Runtime>(
Ok(None)
}
}
HostRequest::FindHttpResponses(req) => {
let http_responses = app_handle
.db()
.list_http_responses_for_request(&req.request_id, req.limit.map(|l| l as u64))
.unwrap_or_default();
Ok(Some(InternalEventPayload::FindHttpResponsesResponse(FindHttpResponsesResponse {
http_responses,
})))
}
HostRequest::UpsertModel(req) => {
use AnyModel::*;
let model = match &req.model {
HttpRequest(m) => {
HttpRequest(app_handle.db().upsert_http_request(m, &UpdateSource::Plugin)?)
}
GrpcRequest(m) => {
GrpcRequest(app_handle.db().upsert_grpc_request(m, &UpdateSource::Plugin)?)
}
WebsocketRequest(m) => WebsocketRequest(
app_handle.db().upsert_websocket_request(m, &UpdateSource::Plugin)?,
),
Folder(m) => Folder(app_handle.db().upsert_folder(m, &UpdateSource::Plugin)?),
Environment(m) => {
Environment(app_handle.db().upsert_environment(m, &UpdateSource::Plugin)?)
}
Workspace(m) => {
Workspace(app_handle.db().upsert_workspace(m, &UpdateSource::Plugin)?)
}
_ => {
return Err(PluginErr("Upsert not supported for this model type".into()).into());
}
};
Ok(Some(InternalEventPayload::UpsertModelResponse(
yaak_plugins::events::UpsertModelResponse { model },
)))
}
HostRequest::DeleteModel(req) => {
let model = match req.model.as_str() {
"http_request" => AnyModel::HttpRequest(
app_handle.db().delete_http_request_by_id(&req.id, &UpdateSource::Plugin)?,
),
"grpc_request" => AnyModel::GrpcRequest(
app_handle.db().delete_grpc_request_by_id(&req.id, &UpdateSource::Plugin)?,
),
"websocket_request" => AnyModel::WebsocketRequest(
app_handle
.db()
.delete_websocket_request_by_id(&req.id, &UpdateSource::Plugin)?,
),
"folder" => AnyModel::Folder(
app_handle.db().delete_folder_by_id(&req.id, &UpdateSource::Plugin)?,
),
"environment" => AnyModel::Environment(
app_handle.db().delete_environment_by_id(&req.id, &UpdateSource::Plugin)?,
),
_ => {
return Err(PluginErr("Delete not supported for this model type".into()).into());
}
};
Ok(Some(InternalEventPayload::DeleteModelResponse(
yaak_plugins::events::DeleteModelResponse { model },
)))
}
HostRequest::RenderGrpcRequest(req) => {
let window = get_window_from_plugin_context(app_handle, plugin_context)?;
@@ -362,7 +297,7 @@ async fn handle_host_plugin_request<R: Runtime>(
workspace_id: http_request.workspace_id.clone(),
..Default::default()
},
&UpdateSource::Plugin,
&UpdateSource::from_window_label(window.label()),
&blobs,
)?
};

View File

@@ -22,7 +22,8 @@ use tauri::{
use tokio::sync::Mutex;
use ts_rs::TS;
use yaak_api::yaak_api_client;
use yaak_models::models::Plugin;
use yaak_models::models::{Plugin, PluginSource};
use yaak_models::util::UpdateSource;
use yaak_plugins::api::{
PluginNameVersion, PluginSearchResponse, PluginUpdatesResponse, check_plugin_updates,
search_plugins,
@@ -164,6 +165,28 @@ pub async fn cmd_plugins_install<R: Runtime>(
Ok(())
}
#[command]
pub async fn cmd_plugins_install_from_directory<R: Runtime>(
window: WebviewWindow<R>,
directory: &str,
) -> Result<Plugin> {
let plugin = window.db().upsert_plugin(
&Plugin {
directory: directory.into(),
url: None,
enabled: true,
source: PluginSource::Filesystem,
..Default::default()
},
&UpdateSource::from_window_label(window.label()),
)?;
let plugin_manager = Arc::new((*window.state::<PluginManager>()).clone());
plugin_manager.add_plugin(&window.plugin_context(), &plugin).await?;
Ok(plugin)
}
#[command]
pub async fn cmd_plugins_uninstall<R: Runtime>(
plugin_id: &str,

View File

@@ -1,7 +1,6 @@
use crate::client::HttpConnectionOptions;
use crate::dns::LocalhostResolver;
use crate::error::Result;
use log::info;
use reqwest::Client;
use std::collections::BTreeMap;
use std::sync::Arc;
@@ -36,7 +35,6 @@ 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(),

View File

@@ -67,7 +67,9 @@ export type ParentAuthentication = { authentication: Record<string, any>, authen
export type ParentHeaders = { headers: Array<HttpRequestHeader>, };
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 PluginKeyValue = { model: "plugin_key_value", createdAt: string, updatedAt: string, pluginName: string, key: string, value: string, };

View File

@@ -0,0 +1,33 @@
ALTER TABLE plugins
ADD COLUMN source TEXT DEFAULT 'filesystem' NOT NULL;
-- Existing registry installs have a URL; classify them first.
UPDATE plugins
SET source = 'registry'
WHERE url IS NOT NULL;
-- Best-effort bundled backfill for legacy rows.
UPDATE plugins
SET source = 'bundled'
WHERE source = 'filesystem'
AND (
-- Normalize separators so this also works for Windows paths.
replace(directory, '\', '/') LIKE '%/vendored/plugins/%'
OR replace(directory, '\', '/') LIKE '%/vendored-plugins/%'
);
-- Keep one row per exact directory before adding uniqueness.
-- Tie-break by recency.
WITH ranked AS (SELECT id,
ROW_NUMBER() OVER (
PARTITION BY directory
ORDER BY updated_at DESC,
created_at DESC
) AS row_num
FROM plugins)
DELETE
FROM plugins
WHERE id IN (SELECT id FROM ranked WHERE row_num > 1);
CREATE UNIQUE INDEX IF NOT EXISTS idx_plugins_directory_unique
ON plugins (directory);

View File

@@ -2074,6 +2074,46 @@ pub struct Plugin {
pub directory: String,
pub enabled: bool,
pub url: Option<String>,
pub source: PluginSource,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "gen_models.ts")]
pub enum PluginSource {
Bundled,
Filesystem,
Registry,
}
impl FromStr for PluginSource {
type Err = crate::error::Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"bundled" => Ok(Self::Bundled),
"filesystem" => Ok(Self::Filesystem),
"registry" => Ok(Self::Registry),
_ => Ok(Self::default()),
}
}
}
impl Display for PluginSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let str = match self {
PluginSource::Bundled => "bundled".to_string(),
PluginSource::Filesystem => "filesystem".to_string(),
PluginSource::Registry => "registry".to_string(),
};
write!(f, "{}", str)
}
}
impl Default for PluginSource {
fn default() -> Self {
Self::Filesystem
}
}
impl UpsertModelInfo for Plugin {
@@ -2109,6 +2149,7 @@ impl UpsertModelInfo for Plugin {
(Directory, self.directory.into()),
(Url, self.url.into()),
(Enabled, self.enabled.into()),
(Source, self.source.to_string().into()),
])
}
@@ -2119,6 +2160,7 @@ impl UpsertModelInfo for Plugin {
PluginIden::Directory,
PluginIden::Url,
PluginIden::Enabled,
PluginIden::Source,
]
}
@@ -2135,6 +2177,7 @@ impl UpsertModelInfo for Plugin {
url: row.get("url")?,
directory: row.get("directory")?,
enabled: row.get("enabled")?,
source: PluginSource::from_str(row.get::<_, String>("source")?.as_str()).unwrap(),
})
}
}

View File

@@ -26,6 +26,10 @@ impl<'a> DbContext<'a> {
}
pub fn upsert_plugin(&self, plugin: &Plugin, source: &UpdateSource) -> Result<Plugin> {
self.upsert(plugin, source)
let mut plugin_to_upsert = plugin.clone();
if let Some(existing) = self.get_plugin_by_directory(&plugin.directory) {
plugin_to_upsert.id = existing.id;
}
self.upsert(&plugin_to_upsert, source)
}
}

View File

@@ -15,7 +15,6 @@ log = { workspace = true }
md5 = "0.7.0"
path-slash = "0.2.1"
rand = "0.9.0"
regex = "1.10.6"
reqwest = { workspace = true, features = ["json"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }

View File

@@ -24,3 +24,7 @@ export async function checkPluginUpdates() {
export async function updateAllPlugins() {
return invoke<PluginNameVersion[]>('cmd_plugins_update_all', {});
}
export async function installPluginFromDirectory(directory: string) {
return invoke<void>('cmd_plugins_install_from_directory', { directory });
}

View File

@@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
use std::path::Path;
use std::str::FromStr;
use ts_rs::TS;
use yaak_models::models::Plugin;
use yaak_models::models::{Plugin, PluginSource};
/// Get plugin info from the registry.
pub async fn get_plugin(
@@ -58,7 +58,7 @@ pub async fn check_plugin_updates(
) -> Result<PluginUpdatesResponse> {
let name_versions: Vec<PluginNameVersion> = plugins
.into_iter()
.filter(|p| p.url.is_some()) // Only check plugins with URLs (from registry)
.filter(|p| matches!(p.source, PluginSource::Registry)) // Only check registry-installed plugins
.filter_map(|p| match get_plugin_meta(&Path::new(&p.directory)) {
Ok(m) => Some(PluginNameVersion { name: m.name, version: m.version }),
Err(e) => {

View File

@@ -9,7 +9,7 @@ use log::info;
use std::fs::{create_dir_all, remove_dir_all};
use std::io::Cursor;
use std::sync::Arc;
use yaak_models::models::Plugin;
use yaak_models::models::{Plugin, PluginSource};
use yaak_models::query_manager::QueryManager;
use yaak_models::util::UpdateSource;
@@ -78,6 +78,7 @@ pub async fn download_and_install(
directory: plugin_dir_str.clone(),
enabled: true,
url: Some(plugin_version.url.clone()),
source: PluginSource::Registry,
..Default::default()
},
&UpdateSource::Background,

View File

@@ -21,9 +21,10 @@ use crate::events::{
use crate::native_template_functions::{template_function_keyring, template_function_secure};
use crate::nodejs::start_nodejs_plugin_runtime;
use crate::plugin_handle::PluginHandle;
use crate::plugin_meta::get_plugin_meta;
use crate::server_ws::PluginRuntimeServerWebsocket;
use log::{error, info, warn};
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::env;
use std::path::{Path, PathBuf};
use std::sync::Arc;
@@ -33,7 +34,7 @@ use tokio::net::TcpListener;
use tokio::sync::mpsc::error::TrySendError;
use tokio::sync::{Mutex, mpsc, oneshot};
use tokio::time::{Instant, timeout};
use yaak_models::models::Plugin;
use yaak_models::models::{Plugin, PluginSource};
use yaak_models::query_manager::QueryManager;
use yaak_models::util::{UpdateSource, generate_id};
use yaak_templates::error::Error::RenderError;
@@ -162,13 +163,14 @@ impl PluginManager {
let bundled_dirs = plugin_manager.list_bundled_plugin_dirs().await?;
let db = query_manager.connect();
for dir in bundled_dirs {
if db.get_plugin_by_directory(&dir).is_none() {
for dir in &bundled_dirs {
if db.get_plugin_by_directory(dir).is_none() {
db.upsert_plugin(
&Plugin {
directory: dir,
directory: dir.clone(),
enabled: true,
url: None,
source: PluginSource::Bundled,
..Default::default()
},
&UpdateSource::Background,
@@ -213,6 +215,57 @@ impl PluginManager {
read_plugins_dir(&plugins_dir).await
}
pub async fn resolve_plugins_for_runtime_from_db(&self, plugins: Vec<Plugin>) -> Vec<Plugin> {
let bundled_dirs = match self.list_bundled_plugin_dirs().await {
Ok(dirs) => dirs,
Err(err) => {
warn!("Failed to read bundled plugin dirs for resolution: {err:?}");
Vec::new()
}
};
self.resolve_plugins_for_runtime(plugins, bundled_dirs)
}
/// Resolve the plugin set for the current runtime instance.
///
/// Rules:
/// - Drop bundled rows that are not present in this instance's bundled directory list.
/// - Deduplicate by plugin metadata name (fallback to directory key when metadata is unreadable).
/// - Prefer sources in this order: filesystem > registry > bundled.
/// - For same-source conflicts, prefer the most recently installed row (`created_at`).
fn resolve_plugins_for_runtime(
&self,
plugins: Vec<Plugin>,
bundled_dirs: Vec<String>,
) -> Vec<Plugin> {
let bundled_dir_set: HashSet<String> = bundled_dirs.into_iter().collect();
let mut selected: HashMap<String, Plugin> = HashMap::new();
for plugin in plugins {
if matches!(plugin.source, PluginSource::Bundled)
&& !bundled_dir_set.contains(&plugin.directory)
{
continue;
}
let key = match get_plugin_meta(Path::new(&plugin.directory)) {
Ok(meta) => meta.name,
Err(_) => format!("__dir__{}", plugin.directory),
};
match selected.get(&key) {
Some(existing) if !prefer_plugin(&plugin, existing) => {}
_ => {
selected.insert(key, plugin);
}
}
}
let mut resolved = selected.into_values().collect::<Vec<_>>();
resolved.sort_by(|a, b| b.created_at.cmp(&a.created_at));
resolved
}
pub async fn uninstall(&self, plugin_context: &PluginContext, dir: &str) -> Result<()> {
let plugin = self.get_plugin_by_dir(dir).await.ok_or(PluginNotFoundErr(dir.to_string()))?;
self.remove_plugin(plugin_context, &plugin).await
@@ -287,7 +340,8 @@ impl PluginManager {
Ok(())
}
/// Initialize all plugins from the provided list.
/// Initialize all plugins from the provided DB list.
/// Plugin candidates are resolved for this runtime instance before initialization.
/// Returns a list of (plugin_directory, error_message) for any plugins that failed to initialize.
pub async fn initialize_all_plugins(
&self,
@@ -297,15 +351,18 @@ impl PluginManager {
info!("Initializing all plugins");
let start = Instant::now();
let mut errors = Vec::new();
let plugins = self.resolve_plugins_for_runtime_from_db(plugins).await;
// Rebuild runtime handles from scratch to avoid stale/duplicate handles.
let existing_handles = { self.plugin_handles.lock().await.clone() };
for plugin_handle in existing_handles {
if let Err(e) = self.remove_plugin(plugin_context, &plugin_handle).await {
error!("Failed to remove plugin {} {e:?}", plugin_handle.dir);
errors.push((plugin_handle.dir.clone(), e.to_string()));
}
}
for plugin in plugins {
// First remove the plugin if it exists and is enabled
if let Some(plugin_handle) = self.get_plugin_by_dir(&plugin.directory).await {
if let Err(e) = self.remove_plugin(plugin_context, &plugin_handle).await {
error!("Failed to remove plugin {} {e:?}", plugin.directory);
continue;
}
}
if let Err(e) = self.add_plugin(plugin_context, &plugin).await {
warn!("Failed to add plugin {} {e:?}", plugin.directory);
errors.push((plugin.directory.clone(), e.to_string()));
@@ -1063,6 +1120,24 @@ impl PluginManager {
}
}
fn source_priority(source: &PluginSource) -> i32 {
match source {
PluginSource::Filesystem => 3,
PluginSource::Registry => 2,
PluginSource::Bundled => 1,
}
}
fn prefer_plugin(candidate: &Plugin, existing: &Plugin) -> bool {
let candidate_priority = source_priority(&candidate.source);
let existing_priority = source_priority(&existing.source);
if candidate_priority != existing_priority {
return candidate_priority > existing_priority;
}
candidate.created_at > existing.created_at
}
async fn read_plugins_dir(dir: &PathBuf) -> Result<Vec<String>> {
let mut result = read_dir(dir).await?;
let mut dirs: Vec<String> = vec![];
@@ -1081,16 +1156,10 @@ async fn read_plugins_dir(dir: &PathBuf) -> Result<Vec<String>> {
fn fix_windows_paths(p: &PathBuf) -> String {
use dunce;
use path_slash::PathBufExt;
use regex::Regex;
// 1. Remove UNC prefix for Windows paths to pass to sidecar
let safe_path = dunce::simplified(p.as_path()).to_string_lossy().to_string();
// 1. Remove UNC prefix for Windows paths
let safe_path = dunce::simplified(p.as_path());
// 2. Remove the drive letter
let safe_path = Regex::new("^[a-zA-Z]:").unwrap().replace(safe_path.as_str(), "");
// 3. Convert backslashes to forward
let safe_path = PathBuf::from(safe_path.to_string()).to_slash_lossy().to_string();
safe_path
// 2. Convert backslashes to forward slashes for Node.js compatibility
PathBuf::from(safe_path).to_slash_lossy().to_string()
}

View File

@@ -1,5 +1,5 @@
/* tslint:disable */
/* eslint-disable */
export function unescape_template(template: string): any;
export function parse_template(template: string): any;
export function escape_template(template: string): any;
export function parse_template(template: string): any;
export function unescape_template(template: string): any;

View File

@@ -165,10 +165,10 @@ function takeFromExternrefTable0(idx) {
* @param {string} template
* @returns {any}
*/
export function unescape_template(template) {
export function escape_template(template) {
const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.unescape_template(ptr0, len0);
const ret = wasm.escape_template(ptr0, len0);
if (ret[2]) {
throw takeFromExternrefTable0(ret[1]);
}
@@ -193,10 +193,10 @@ export function parse_template(template) {
* @param {string} template
* @returns {any}
*/
export function escape_template(template) {
export function unescape_template(template) {
const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.escape_template(ptr0, len0);
const ret = wasm.unescape_template(ptr0, len0);
if (ret[2]) {
throw takeFromExternrefTable0(ret[1]);
}

View File

Binary file not shown.

View File

@@ -273,6 +273,5 @@ pub fn find_client_certificate(
});
}
debug!("No matching client certificate found for {}", url_string);
None
}

View File

@@ -1,14 +1,16 @@
use yaak_models::models::AnyModel;
use yaak_models::query_manager::QueryManager;
use yaak_models::util::UpdateSource;
use yaak_plugins::events::{
CloseWindowRequest, CopyTextRequest, DeleteKeyValueRequest, DeleteKeyValueResponse,
DeleteModelRequest, ErrorResponse, FindHttpResponsesRequest, GetCookieValueRequest,
GetHttpRequestByIdRequest, GetHttpRequestByIdResponse, GetKeyValueRequest, GetKeyValueResponse,
InternalEventPayload, ListCookieNamesRequest, ListFoldersRequest, ListFoldersResponse,
ListHttpRequestsRequest, ListHttpRequestsResponse, ListOpenWorkspacesRequest,
OpenExternalUrlRequest, OpenWindowRequest, PromptFormRequest, PromptTextRequest,
ReloadResponse, RenderGrpcRequestRequest, RenderHttpRequestRequest, SendHttpRequestRequest,
SetKeyValueRequest, ShowToastRequest, TemplateRenderRequest, UpsertModelRequest,
WindowInfoRequest,
DeleteModelRequest, DeleteModelResponse, ErrorResponse, FindHttpResponsesRequest,
FindHttpResponsesResponse, GetCookieValueRequest, GetHttpRequestByIdRequest,
GetHttpRequestByIdResponse, GetKeyValueRequest, GetKeyValueResponse, InternalEventPayload,
ListCookieNamesRequest, ListFoldersRequest, ListFoldersResponse, ListHttpRequestsRequest,
ListHttpRequestsResponse, ListOpenWorkspacesRequest, OpenExternalUrlRequest, OpenWindowRequest,
PromptFormRequest, PromptTextRequest, ReloadResponse, RenderGrpcRequestRequest,
RenderHttpRequestRequest, SendHttpRequestRequest, SetKeyValueRequest, ShowToastRequest,
TemplateRenderRequest, UpsertModelRequest, UpsertModelResponse, WindowInfoRequest,
};
pub struct SharedPluginEventContext<'a> {
@@ -37,6 +39,9 @@ pub enum SharedRequest<'a> {
GetHttpRequestById(&'a GetHttpRequestByIdRequest),
ListFolders(&'a ListFoldersRequest),
ListHttpRequests(&'a ListHttpRequestsRequest),
FindHttpResponses(&'a FindHttpResponsesRequest),
UpsertModel(&'a UpsertModelRequest),
DeleteModel(&'a DeleteModelRequest),
}
#[derive(Debug)]
@@ -45,9 +50,6 @@ pub enum HostRequest<'a> {
CopyText(&'a CopyTextRequest),
PromptText(&'a PromptTextRequest),
PromptForm(&'a PromptFormRequest),
FindHttpResponses(&'a FindHttpResponsesRequest),
UpsertModel(&'a UpsertModelRequest),
DeleteModel(&'a DeleteModelRequest),
RenderGrpcRequest(&'a RenderGrpcRequestRequest),
RenderHttpRequest(&'a RenderHttpRequestRequest),
TemplateRender(&'a TemplateRenderRequest),
@@ -71,9 +73,6 @@ impl HostRequest<'_> {
HostRequest::CopyText(_) => "copy_text_request".to_string(),
HostRequest::PromptText(_) => "prompt_text_request".to_string(),
HostRequest::PromptForm(_) => "prompt_form_request".to_string(),
HostRequest::FindHttpResponses(_) => "find_http_responses_request".to_string(),
HostRequest::UpsertModel(_) => "upsert_model_request".to_string(),
HostRequest::DeleteModel(_) => "delete_model_request".to_string(),
HostRequest::RenderGrpcRequest(_) => "render_grpc_request_request".to_string(),
HostRequest::RenderHttpRequest(_) => "render_http_request_request".to_string(),
HostRequest::TemplateRender(_) => "template_render_request".to_string(),
@@ -135,13 +134,13 @@ impl<'a> From<&'a InternalEventPayload> for GroupedPluginRequest<'a> {
GroupedPluginRequest::Host(HostRequest::PromptForm(req))
}
InternalEventPayload::FindHttpResponsesRequest(req) => {
GroupedPluginRequest::Host(HostRequest::FindHttpResponses(req))
GroupedPluginRequest::Shared(SharedRequest::FindHttpResponses(req))
}
InternalEventPayload::UpsertModelRequest(req) => {
GroupedPluginRequest::Host(HostRequest::UpsertModel(req))
GroupedPluginRequest::Shared(SharedRequest::UpsertModel(req))
}
InternalEventPayload::DeleteModelRequest(req) => {
GroupedPluginRequest::Host(HostRequest::DeleteModel(req))
GroupedPluginRequest::Shared(SharedRequest::DeleteModel(req))
}
InternalEventPayload::RenderGrpcRequestRequest(req) => {
GroupedPluginRequest::Host(HostRequest::RenderGrpcRequest(req))
@@ -275,17 +274,175 @@ fn build_shared_reply(
http_requests,
})
}
SharedRequest::FindHttpResponses(req) => {
let http_responses = query_manager
.connect()
.list_http_responses_for_request(&req.request_id, req.limit.map(|l| l as u64))
.unwrap_or_default();
InternalEventPayload::FindHttpResponsesResponse(FindHttpResponsesResponse {
http_responses,
})
}
SharedRequest::UpsertModel(req) => {
use AnyModel::*;
let model = match &req.model {
HttpRequest(m) => {
match query_manager.connect().upsert_http_request(m, &UpdateSource::Plugin) {
Ok(model) => HttpRequest(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to upsert HTTP request: {err}"),
});
}
}
}
GrpcRequest(m) => {
match query_manager.connect().upsert_grpc_request(m, &UpdateSource::Plugin) {
Ok(model) => GrpcRequest(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to upsert gRPC request: {err}"),
});
}
}
}
WebsocketRequest(m) => {
match query_manager.connect().upsert_websocket_request(m, &UpdateSource::Plugin)
{
Ok(model) => WebsocketRequest(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to upsert WebSocket request: {err}"),
});
}
}
}
Folder(m) => {
match query_manager.connect().upsert_folder(m, &UpdateSource::Plugin) {
Ok(model) => Folder(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to upsert folder: {err}"),
});
}
}
}
Environment(m) => {
match query_manager.connect().upsert_environment(m, &UpdateSource::Plugin) {
Ok(model) => Environment(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to upsert environment: {err}"),
});
}
}
}
Workspace(m) => {
match query_manager.connect().upsert_workspace(m, &UpdateSource::Plugin) {
Ok(model) => Workspace(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to upsert workspace: {err}"),
});
}
}
}
_ => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: "Upsert not supported for this model type".to_string(),
});
}
};
InternalEventPayload::UpsertModelResponse(UpsertModelResponse { model })
}
SharedRequest::DeleteModel(req) => {
let model = match req.model.as_str() {
"http_request" => {
match query_manager
.connect()
.delete_http_request_by_id(&req.id, &UpdateSource::Plugin)
{
Ok(model) => AnyModel::HttpRequest(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to delete HTTP request: {err}"),
});
}
}
}
"grpc_request" => {
match query_manager
.connect()
.delete_grpc_request_by_id(&req.id, &UpdateSource::Plugin)
{
Ok(model) => AnyModel::GrpcRequest(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to delete gRPC request: {err}"),
});
}
}
}
"websocket_request" => {
match query_manager
.connect()
.delete_websocket_request_by_id(&req.id, &UpdateSource::Plugin)
{
Ok(model) => AnyModel::WebsocketRequest(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to delete WebSocket request: {err}"),
});
}
}
}
"folder" => match query_manager
.connect()
.delete_folder_by_id(&req.id, &UpdateSource::Plugin)
{
Ok(model) => AnyModel::Folder(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to delete folder: {err}"),
});
}
},
"environment" => {
match query_manager
.connect()
.delete_environment_by_id(&req.id, &UpdateSource::Plugin)
{
Ok(model) => AnyModel::Environment(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to delete environment: {err}"),
});
}
}
}
_ => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: "Delete not supported for this model type".to_string(),
});
}
};
InternalEventPayload::DeleteModelResponse(DeleteModelResponse { model })
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use yaak_models::models::{Folder, HttpRequest, Workspace};
use tempfile::TempDir;
use yaak_models::models::{AnyModel, Folder, HttpRequest, Workspace};
use yaak_models::util::UpdateSource;
fn seed_query_manager() -> QueryManager {
let temp_dir = tempfile::TempDir::new().expect("Failed to create temp dir");
fn seed_query_manager() -> (QueryManager, TempDir) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("db.sqlite");
let blob_path = temp_dir.path().join("blobs.sqlite");
let (query_manager, _blob_manager, _rx) =
@@ -332,12 +489,12 @@ mod tests {
)
.expect("Failed to seed request");
query_manager
(query_manager, temp_dir)
}
#[test]
fn list_requests_requires_workspace_when_folder_missing() {
let query_manager = seed_query_manager();
let (query_manager, _temp_dir) = seed_query_manager();
let payload = InternalEventPayload::ListHttpRequestsRequest(
yaak_plugins::events::ListHttpRequestsRequest { folder_id: None },
);
@@ -355,7 +512,7 @@ mod tests {
#[test]
fn list_requests_by_workspace_and_folder() {
let query_manager = seed_query_manager();
let (query_manager, _temp_dir) = seed_query_manager();
let by_workspace_payload = InternalEventPayload::ListHttpRequestsRequest(
yaak_plugins::events::ListHttpRequestsRequest { folder_id: None },
@@ -394,9 +551,83 @@ mod tests {
}
}
#[test]
fn find_http_responses_is_shared_handled() {
let (query_manager, _temp_dir) = seed_query_manager();
let payload = InternalEventPayload::FindHttpResponsesRequest(FindHttpResponsesRequest {
request_id: "rq_test".to_string(),
limit: Some(1),
});
let result = handle_shared_plugin_event(
&query_manager,
&payload,
SharedPluginEventContext { plugin_name: "@yaak/test", workspace_id: Some("wk_test") },
);
match result {
GroupedPluginEvent::Handled(Some(InternalEventPayload::FindHttpResponsesResponse(
resp,
))) => {
assert!(resp.http_responses.is_empty());
}
other => panic!("unexpected find responses result: {other:?}"),
}
}
#[test]
fn upsert_and_delete_model_are_shared_handled() {
let (query_manager, _temp_dir) = seed_query_manager();
let existing = query_manager
.connect()
.get_http_request("rq_test")
.expect("Failed to load seeded request");
let upsert_payload = InternalEventPayload::UpsertModelRequest(UpsertModelRequest {
model: AnyModel::HttpRequest(HttpRequest {
name: "Request Updated".to_string(),
..existing
}),
});
let upsert_result = handle_shared_plugin_event(
&query_manager,
&upsert_payload,
SharedPluginEventContext { plugin_name: "@yaak/test", workspace_id: Some("wk_test") },
);
match upsert_result {
GroupedPluginEvent::Handled(Some(InternalEventPayload::UpsertModelResponse(resp))) => {
match resp.model {
AnyModel::HttpRequest(r) => assert_eq!(r.name, "Request Updated"),
other => panic!("unexpected upsert model type: {other:?}"),
}
}
other => panic!("unexpected upsert result: {other:?}"),
}
let delete_payload = InternalEventPayload::DeleteModelRequest(DeleteModelRequest {
model: "http_request".to_string(),
id: "rq_test".to_string(),
});
let delete_result = handle_shared_plugin_event(
&query_manager,
&delete_payload,
SharedPluginEventContext { plugin_name: "@yaak/test", workspace_id: Some("wk_test") },
);
match delete_result {
GroupedPluginEvent::Handled(Some(InternalEventPayload::DeleteModelResponse(resp))) => {
match resp.model {
AnyModel::HttpRequest(r) => assert_eq!(r.id, "rq_test"),
other => panic!("unexpected delete model type: {other:?}"),
}
}
other => panic!("unexpected delete result: {other:?}"),
}
}
#[test]
fn host_request_classification_works() {
let query_manager = seed_query_manager();
let (query_manager, _temp_dir) = seed_query_manager();
let payload = InternalEventPayload::WindowInfoRequest(WindowInfoRequest {
label: "main".to_string(),
});

View File

@@ -1,7 +0,0 @@
# 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.

View File

@@ -1,5 +1,5 @@
const fs = require("node:fs");
const path = require("node:path");
const readme = path.join(__dirname, "..", "..", "README.md");
fs.copyFileSync(readme, path.join(__dirname, "README.md"));
const cliReadme = path.join(__dirname, "..", "..", "crates-cli", "yaak-cli", "README.md");
fs.copyFileSync(cliReadme, path.join(__dirname, "README.md"));

390
package-lock.json generated
View File

@@ -73,7 +73,7 @@
"devDependencies": {
"@biomejs/biome": "^2.3.13",
"@tauri-apps/cli": "^2.9.6",
"@yaakapp/cli": "^0.4.0-beta.2",
"@yaakapp/cli": "^0.4.0",
"dotenv-cli": "^11.0.0",
"husky": "^9.1.7",
"nodejs-file-downloader": "^4.13.0",
@@ -1491,29 +1491,6 @@
"hono": "^4"
}
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -2242,9 +2219,9 @@
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
"integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"cpu": [
"arm"
],
@@ -2256,9 +2233,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz",
"integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"cpu": [
"arm64"
],
@@ -2270,9 +2247,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz",
"integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"cpu": [
"arm64"
],
@@ -2284,9 +2261,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz",
"integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"cpu": [
"x64"
],
@@ -2298,9 +2275,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz",
"integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"cpu": [
"arm64"
],
@@ -2312,9 +2289,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz",
"integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"cpu": [
"x64"
],
@@ -2326,9 +2303,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz",
"integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"cpu": [
"arm"
],
@@ -2340,9 +2317,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz",
"integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"cpu": [
"arm"
],
@@ -2354,9 +2331,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz",
"integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"cpu": [
"arm64"
],
@@ -2368,9 +2345,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz",
"integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"cpu": [
"arm64"
],
@@ -2382,9 +2359,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz",
"integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
"cpu": [
"loong64"
],
@@ -2396,9 +2373,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz",
"integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"cpu": [
"loong64"
],
@@ -2410,9 +2387,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz",
"integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
"cpu": [
"ppc64"
],
@@ -2424,9 +2401,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz",
"integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"cpu": [
"ppc64"
],
@@ -2438,9 +2415,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz",
"integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"cpu": [
"riscv64"
],
@@ -2452,9 +2429,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz",
"integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"cpu": [
"riscv64"
],
@@ -2466,9 +2443,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz",
"integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"cpu": [
"s390x"
],
@@ -2480,9 +2457,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz",
"integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"cpu": [
"x64"
],
@@ -2494,9 +2471,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz",
"integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"cpu": [
"x64"
],
@@ -2508,9 +2485,9 @@
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz",
"integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
"cpu": [
"x64"
],
@@ -2522,9 +2499,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz",
"integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"cpu": [
"arm64"
],
@@ -2536,9 +2513,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz",
"integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"cpu": [
"arm64"
],
@@ -2550,9 +2527,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz",
"integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"cpu": [
"ia32"
],
@@ -2564,9 +2541,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz",
"integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"cpu": [
"x64"
],
@@ -2578,9 +2555,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz",
"integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"cpu": [
"x64"
],
@@ -4326,9 +4303,9 @@
"link": true
},
"node_modules/@yaakapp/cli": {
"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==",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@yaakapp/cli/-/cli-0.4.0.tgz",
"integrity": "sha512-8xnu2oFWlgV+xeIAHMuEgsqX6Sxq4UYrSH2WbafwDLbSep6fxpO74tiBH7xp4wakt/7Bcy9a2Q5R9nkAc1ZUdA==",
"dev": true,
"hasInstallScript": true,
"bin": {
@@ -4336,18 +4313,18 @@
"yaakcli": "bin/cli.js"
},
"optionalDependencies": {
"@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"
"@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"
}
},
"node_modules/@yaakapp/cli-darwin-arm64": {
"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==",
"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==",
"cpu": [
"arm64"
],
@@ -4358,9 +4335,9 @@
]
},
"node_modules/@yaakapp/cli-darwin-x64": {
"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==",
"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==",
"cpu": [
"x64"
],
@@ -4371,9 +4348,9 @@
]
},
"node_modules/@yaakapp/cli-linux-arm64": {
"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==",
"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==",
"cpu": [
"arm64"
],
@@ -4384,9 +4361,9 @@
]
},
"node_modules/@yaakapp/cli-linux-x64": {
"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==",
"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==",
"cpu": [
"x64"
],
@@ -4397,9 +4374,9 @@
]
},
"node_modules/@yaakapp/cli-win32-arm64": {
"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==",
"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==",
"cpu": [
"arm64"
],
@@ -4410,9 +4387,9 @@
]
},
"node_modules/@yaakapp/cli-win32-x64": {
"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==",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@yaakapp/cli-win32-x64/-/cli-win32-x64-0.4.0.tgz",
"integrity": "sha512-wansfrCCycFcFclowQQxfsNLIAyATyqnnbITED5gUfUrBf8NFHrG0sWVCWlXUhHU7YvpmqL7CsdtlMkIGiZCPQ==",
"cpu": [
"x64"
],
@@ -5799,17 +5776,40 @@
"npm": ">=10"
}
},
"node_modules/cpx2/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/cpx2/node_modules/brace-expansion": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/cpx2/node_modules/minimatch": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
"brace-expansion": "^5.0.2"
},
"engines": {
"node": "20 || >=22"
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -7636,17 +7636,40 @@
"node": ">= 6"
}
},
"node_modules/glob/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/glob/node_modules/minimatch": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
"brace-expansion": "^5.0.2"
},
"engines": {
"node": "20 || >=22"
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -10524,9 +10547,9 @@
"license": "ISC"
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -12951,6 +12974,29 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/rimraf/node_modules/brace-expansion": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/rimraf/node_modules/glob": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz",
@@ -12970,25 +13016,25 @@
}
},
"node_modules/rimraf/node_modules/minimatch": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
"brace-expansion": "^5.0.2"
},
"engines": {
"node": "20 || >=22"
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rollup": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
"integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -13002,31 +13048,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.55.1",
"@rollup/rollup-android-arm64": "4.55.1",
"@rollup/rollup-darwin-arm64": "4.55.1",
"@rollup/rollup-darwin-x64": "4.55.1",
"@rollup/rollup-freebsd-arm64": "4.55.1",
"@rollup/rollup-freebsd-x64": "4.55.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.55.1",
"@rollup/rollup-linux-arm-musleabihf": "4.55.1",
"@rollup/rollup-linux-arm64-gnu": "4.55.1",
"@rollup/rollup-linux-arm64-musl": "4.55.1",
"@rollup/rollup-linux-loong64-gnu": "4.55.1",
"@rollup/rollup-linux-loong64-musl": "4.55.1",
"@rollup/rollup-linux-ppc64-gnu": "4.55.1",
"@rollup/rollup-linux-ppc64-musl": "4.55.1",
"@rollup/rollup-linux-riscv64-gnu": "4.55.1",
"@rollup/rollup-linux-riscv64-musl": "4.55.1",
"@rollup/rollup-linux-s390x-gnu": "4.55.1",
"@rollup/rollup-linux-x64-gnu": "4.55.1",
"@rollup/rollup-linux-x64-musl": "4.55.1",
"@rollup/rollup-openbsd-x64": "4.55.1",
"@rollup/rollup-openharmony-arm64": "4.55.1",
"@rollup/rollup-win32-arm64-msvc": "4.55.1",
"@rollup/rollup-win32-ia32-msvc": "4.55.1",
"@rollup/rollup-win32-x64-gnu": "4.55.1",
"@rollup/rollup-win32-x64-msvc": "4.55.1",
"@rollup/rollup-android-arm-eabi": "4.59.0",
"@rollup/rollup-android-arm64": "4.59.0",
"@rollup/rollup-darwin-arm64": "4.59.0",
"@rollup/rollup-darwin-x64": "4.59.0",
"@rollup/rollup-freebsd-arm64": "4.59.0",
"@rollup/rollup-freebsd-x64": "4.59.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
"@rollup/rollup-linux-arm64-musl": "4.59.0",
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
"@rollup/rollup-linux-loong64-musl": "4.59.0",
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
"@rollup/rollup-linux-x64-gnu": "4.59.0",
"@rollup/rollup-linux-x64-musl": "4.59.0",
"@rollup/rollup-openbsd-x64": "4.59.0",
"@rollup/rollup-openharmony-arm64": "4.59.0",
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
"@rollup/rollup-win32-x64-gnu": "4.59.0",
"@rollup/rollup-win32-x64-msvc": "4.59.0",
"fsevents": "~2.3.2"
}
},

View File

@@ -97,7 +97,7 @@
"devDependencies": {
"@biomejs/biome": "^2.3.13",
"@tauri-apps/cli": "^2.9.6",
"@yaakapp/cli": "^0.4.0-beta.2",
"@yaakapp/cli": "^0.4.0",
"dotenv-cli": "^11.0.0",
"husky": "^9.1.7",
"nodejs-file-downloader": "^4.13.0",

View File

@@ -11,7 +11,7 @@ export const plugin: PluginDefinition = {
async onSelect(ctx, args) {
const rendered_request = await ctx.grpcRequest.render({
grpcRequest: args.grpcRequest,
purpose: 'preview',
purpose: 'send',
});
const data = await convert(rendered_request, args.protoFiles);
await ctx.clipboard.copyText(data);
@@ -103,7 +103,7 @@ export async function convert(request: Partial<GrpcRequest>, allProtoFiles: stri
// Add form params
if (request.message) {
xs.push('-d', `${quote(JSON.stringify(JSON.parse(request.message)))}`);
xs.push('-d', quote(request.message));
xs.push(NEWLINE);
}

View File

@@ -151,7 +151,26 @@ describe('exporter-curl', () => {
[
`grpcurl -import-path '/'`,
`-proto '/foo.proto'`,
`-d '{"foo":"bar","baz":1}'`,
`-d '{\n "foo": "bar",\n "baz": 1\n}'`,
'yaak.app',
].join(' \\\n '),
);
});
test('Sends data with unresolved template tags', async () => {
expect(
await convert(
{
url: 'https://yaak.app',
message: '{"timestamp": ${[ faker "timestamp" ]}, "foo": "bar"}',
},
['/foo.proto'],
),
).toEqual(
[
`grpcurl -import-path '/'`,
`-proto '/foo.proto'`,
`-d '{"timestamp": \${[ faker "timestamp" ]}, "foo": "bar"}'`,
'yaak.app',
].join(' \\\n '),
);

View File

@@ -1,7 +1,7 @@
{
"name": "@yaak/action-send-folder",
"displayName": "Send All",
"description": "Send all HTTP requests in a folder sequentially",
"description": "Send all HTTP requests in a folder sequentially in tree order",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",

View File

@@ -14,22 +14,44 @@ export const plugin: PluginDefinition = {
ctx.httpRequest.list(),
]);
// 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);
// 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);
}
}
};
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),
);
collectRequests(targetFolder.id);
if (requestsToSend.length === 0) {
await ctx.toast.show({
@@ -40,7 +62,7 @@ export const plugin: PluginDefinition = {
return;
}
// Send each request sequentially
// Send requests sequentially in the calculated folder order.
let successCount = 0;
let errorCount = 0;

View File

@@ -7,6 +7,29 @@ describe('importer-openapi', () => {
const p = path.join(__dirname, 'fixtures');
const fixtures = fs.readdirSync(p);
test('Maps operation description to request description', async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: '3.0.0',
info: { title: 'Description Test', version: '1.0.0' },
paths: {
'/klanten': {
get: {
description: 'Lijst van klanten',
responses: { '200': { description: 'ok' } },
},
},
},
}),
);
expect(imported?.resources.httpRequests).toEqual([
expect.objectContaining({
description: 'Lijst van klanten',
}),
]);
});
test('Skips invalid file', async () => {
const imported = await convertOpenApi('{}');
expect(imported).toBeUndefined();

View File

@@ -55,19 +55,11 @@ export function convertPostman(contents: string): ImportPluginResponse | undefin
folders: [],
};
const rawDescription = info.description;
const description =
typeof rawDescription === 'object' && rawDescription != null && 'content' in rawDescription
? String(rawDescription.content)
: rawDescription == null
? undefined
: String(rawDescription);
const workspace: ExportResources['workspaces'][0] = {
model: 'workspace',
id: generateId('workspace'),
name: info.name ? String(info.name) : 'Postman Import',
description,
description: importDescription(info.description),
...globalAuth,
};
exportResources.workspaces.push(workspace);
@@ -139,7 +131,7 @@ export function convertPostman(contents: string): ImportPluginResponse | undefin
workspaceId: workspace.id,
folderId,
name: v.name,
description: r.description ? String(r.description) : undefined,
description: importDescription(r.description),
method: typeof r.method === 'string' ? r.method : 'GET',
url,
urlParameters,
@@ -509,6 +501,26 @@ function toArray<T>(value: unknown): T[] {
return [];
}
function importDescription(rawDescription: unknown): string | undefined {
if (rawDescription == null) {
return undefined;
}
if (typeof rawDescription === 'string') {
return rawDescription;
}
if (typeof rawDescription === 'object' && !Array.isArray(rawDescription)) {
const description = toRecord(rawDescription);
if ('content' in description && description.content != null) {
return String(description.content);
}
return undefined;
}
return String(rawDescription);
}
/** Recursively render all nested object properties */
function convertTemplateSyntax<T>(obj: T): T {
if (typeof obj === 'string') {

View File

@@ -22,4 +22,39 @@ describe('importer-postman', () => {
);
});
}
test('Imports object descriptions without [object Object]', () => {
const result = convertPostman(
JSON.stringify({
info: {
name: 'Description Test',
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
},
item: [
{
name: 'Request 1',
request: {
method: 'GET',
description: {
content: 'Lijst van klanten',
type: 'text/plain',
},
},
},
],
}),
);
expect(result?.resources.workspaces).toEqual([
expect.objectContaining({
name: 'Description Test',
}),
]);
expect(result?.resources.httpRequests).toEqual([
expect.objectContaining({
name: 'Request 1',
description: 'Lijst van klanten',
}),
]);
});
});

View File

@@ -72,6 +72,10 @@ 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,
@@ -146,6 +150,10 @@ 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',

View File

@@ -1,5 +1,7 @@
{
"name": "@yaak/template-function-timestamp",
"displayName": "Timestamp Template Functions",
"description": "Template functions for dealing with timestamps",
"private": true,
"version": "0.1.0",
"scripts": {

View File

@@ -45,6 +45,9 @@ const args = [
...additionalArgs
];
const result = spawnSync('tauri', args, { stdio: 'inherit', shell: false, env: process.env });
// Invoke the tauri CLI JS entry point directly via node to avoid shell escaping issues on Windows
const tauriJs = path.join(rootDir, 'node_modules', '@tauri-apps', 'cli', 'tauri.js');
const result = spawnSync(process.execPath, [tauriJs, ...args], { stdio: 'inherit', env: process.env });
process.exit(result.status || 0);

View File

@@ -15,7 +15,6 @@ import { useDebouncedValue } from '../../hooks/useDebouncedValue';
import { useInstallPlugin } from '../../hooks/useInstallPlugin';
import { usePluginInfo } from '../../hooks/usePluginInfo';
import { usePluginsKey, useRefreshPlugins } from '../../hooks/usePlugins';
import { appInfo } from '../../lib/appInfo';
import { showConfirmDelete } from '../../lib/confirm';
import { minPromiseMillis } from '../../lib/minPromiseMillis';
import { Button } from '../core/Button';
@@ -33,16 +32,6 @@ import { TabContent, Tabs } from '../core/Tabs/Tabs';
import { EmptyStateText } from '../EmptyStateText';
import { SelectFile } from '../SelectFile';
function isPluginBundled(plugin: Plugin, vendoredPluginDir: string): boolean {
const normalizedDir = plugin.directory.replace(/\\/g, '/');
const normalizedVendoredDir = vendoredPluginDir.replace(/\\/g, '/');
return (
normalizedDir.includes(normalizedVendoredDir) ||
normalizedDir.includes('vendored/plugins') ||
normalizedDir.includes('/plugins/')
);
}
interface SettingsPluginsProps {
defaultSubtab?: string;
}
@@ -50,8 +39,8 @@ interface SettingsPluginsProps {
export function SettingsPlugins({ defaultSubtab }: SettingsPluginsProps) {
const [directory, setDirectory] = useState<string | null>(null);
const plugins = useAtomValue(pluginsAtom);
const bundledPlugins = plugins.filter((p) => isPluginBundled(p, appInfo.vendoredPluginDir));
const installedPlugins = plugins.filter((p) => !isPluginBundled(p, appInfo.vendoredPluginDir));
const bundledPlugins = plugins.filter((p) => p.source === 'bundled');
const installedPlugins = plugins.filter((p) => p.source !== 'bundled');
const createPlugin = useInstallPlugin();
const refreshPlugins = useRefreshPlugins();
return (

View File

@@ -896,9 +896,9 @@ function MenuItem({
};
const rightSlot = item.submenu ? (
<Icon icon="chevron_right" color='secondary' />
<Icon icon="chevron_right" color="secondary" />
) : (
(item.rightSlot ?? <Hotkey action={item.hotKeyAction ?? null} />)
(item.rightSlot ?? <Hotkey variant="text" action={item.hotKeyAction ?? null} />)
);
return (
@@ -937,7 +937,7 @@ function MenuItem({
)}
{...props}
>
<div className={classNames('truncate')}>{item.label}</div>
<div className={classNames('truncate min-w-[5rem]')}>{item.label}</div>
</Button>
);
}

View File

@@ -1,11 +1,11 @@
import { invokeCmd } from '../lib/tauri';
import { installPluginFromDirectory } from '@yaakapp-internal/plugins';
import { useFastMutation } from './useFastMutation';
export function useInstallPlugin() {
return useFastMutation<void, unknown, string>({
mutationKey: ['install_plugin'],
mutationFn: async (directory: string) => {
await invokeCmd('cmd_install_plugin', { directory });
await installPluginFromDirectory(directory);
},
});
}

View File

@@ -36,7 +36,6 @@ type TauriCmd =
| 'cmd_http_request_body'
| 'cmd_http_response_body'
| 'cmd_import_data'
| 'cmd_install_plugin'
| 'cmd_metadata'
| 'cmd_restart'
| 'cmd_new_child_window'