mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-27 04:10:03 -05:00
Compare commits
28 Commits
yaak-cli-0
...
codex/cli-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9b90188a0 | ||
|
|
e64404d7a5 | ||
|
|
072b486857 | ||
|
|
23e9cbb376 | ||
|
|
9c09e32a56 | ||
|
|
be26cc4db4 | ||
|
|
a2f12aef35 | ||
|
|
e34301ccab | ||
|
|
8f0062f917 | ||
|
|
68d68035a1 | ||
|
|
ffc80d234c | ||
|
|
644c683714 | ||
|
|
d2c1bd79ac | ||
|
|
020589f2e6 | ||
|
|
b2a70d8938 | ||
|
|
64c626ed30 | ||
|
|
35d9ed901a | ||
|
|
f04b34be1a | ||
|
|
1e7e1232da | ||
|
|
c31d477a90 | ||
|
|
443e1b8262 | ||
|
|
c6b7cb2e32 | ||
|
|
4aef826a80 | ||
|
|
50c7992b42 | ||
|
|
5e9aebda6f | ||
|
|
a1e84c7785 | ||
|
|
fea4411afa | ||
|
|
8315e4afad |
59
.github/workflows/release-api-npm.yml
vendored
Normal file
59
.github/workflows/release-api-npm.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
name: Release API to NPM
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: [yaak-api-*]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: API version to publish (for example 0.9.0 or v0.9.0)
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-npm:
|
||||||
|
name: Publish @yaakapp/api
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
registry-url: https://registry.npmjs.org
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Set @yaakapp/api version
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
WORKFLOW_VERSION: ${{ inputs.version }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||||
|
VERSION="$WORKFLOW_VERSION"
|
||||||
|
else
|
||||||
|
VERSION="${GITHUB_REF_NAME#yaak-api-}"
|
||||||
|
fi
|
||||||
|
VERSION="${VERSION#v}"
|
||||||
|
echo "Preparing @yaakapp/api version: $VERSION"
|
||||||
|
cd packages/plugin-runtime-types
|
||||||
|
npm version "$VERSION" --no-git-tag-version --allow-same-version
|
||||||
|
|
||||||
|
- name: Build @yaakapp/api
|
||||||
|
working-directory: packages/plugin-runtime-types
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Publish @yaakapp/api
|
||||||
|
working-directory: packages/plugin-runtime-types
|
||||||
|
run: npm publish --provenance --access public
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Generate Artifacts
|
name: Release App Artifacts
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags: [v*]
|
tags: [v*]
|
||||||
57
.github/workflows/release-cli-npm.yml
vendored
57
.github/workflows/release-cli-npm.yml
vendored
@@ -14,8 +14,44 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
prepare-vendored-assets:
|
||||||
|
name: Prepare vendored plugin assets
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
|
||||||
|
- name: Install Rust stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build plugin assets
|
||||||
|
env:
|
||||||
|
SKIP_WASM_BUILD: "1"
|
||||||
|
run: |
|
||||||
|
npm run build
|
||||||
|
npm run vendor:vendor-plugins
|
||||||
|
|
||||||
|
- name: Upload vendored assets
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: vendored-assets
|
||||||
|
path: |
|
||||||
|
crates-tauri/yaak-app/vendored/plugin-runtime/index.cjs
|
||||||
|
crates-tauri/yaak-app/vendored/plugins
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
build-binaries:
|
build-binaries:
|
||||||
name: Build ${{ matrix.pkg }}
|
name: Build ${{ matrix.pkg }}
|
||||||
|
needs: prepare-vendored-assets
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@@ -67,6 +103,27 @@ jobs:
|
|||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y pkg-config libdbus-1-dev
|
sudo apt-get install -y pkg-config libdbus-1-dev
|
||||||
|
|
||||||
|
- name: Download vendored assets
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: vendored-assets
|
||||||
|
path: crates-tauri/yaak-app/vendored
|
||||||
|
|
||||||
|
- name: Set CLI build version
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
WORKFLOW_VERSION: ${{ inputs.version }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||||
|
VERSION="$WORKFLOW_VERSION"
|
||||||
|
else
|
||||||
|
VERSION="${GITHUB_REF_NAME#yaak-cli-}"
|
||||||
|
fi
|
||||||
|
VERSION="${VERSION#v}"
|
||||||
|
echo "Building yaak version: $VERSION"
|
||||||
|
echo "YAAK_CLI_VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Build yaak
|
- name: Build yaak
|
||||||
run: cargo build --locked --release -p yaak-cli --bin yaak --target ${{ matrix.target }}
|
run: cargo build --locked --release -p yaak-cli --bin yaak --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
|||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -10149,6 +10149,7 @@ dependencies = [
|
|||||||
"env_logger",
|
"env_logger",
|
||||||
"futures",
|
"futures",
|
||||||
"hex",
|
"hex",
|
||||||
|
"include_dir",
|
||||||
"keyring",
|
"keyring",
|
||||||
"log 0.4.29",
|
"log 0.4.29",
|
||||||
"oxc_resolver",
|
"oxc_resolver",
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https://github.com/MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a> <a href="https://github.com/dharsanb"><img src="https://github.com/dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a> <a href="https://github.com/railwayapp"><img src="https://github.com/railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a> <a href="https://github.com/caseyamcl"><img src="https://github.com/caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a> <a href="https://github.com/bytebase"><img src="https://github.com/bytebase.png" width="80px" alt="User avatar: bytebase" /></a> <a href="https://github.com/"><img src="https://raw.githubusercontent.com/JamesIves/github-sponsors-readme-action/dev/.github/assets/placeholder.png" width="80px" alt="User avatar: " /></a> <!-- sponsors-premium -->
|
<!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https://github.com/MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a> <a href="https://github.com/dharsanb"><img src="https://github.com/dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a> <a href="https://github.com/railwayapp"><img src="https://github.com/railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a> <a href="https://github.com/caseyamcl"><img src="https://github.com/caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a> <a href="https://github.com/bytebase"><img src="https://github.com/bytebase.png" width="80px" alt="User avatar: bytebase" /></a> <a href="https://github.com/"><img src="https://raw.githubusercontent.com/JamesIves/github-sponsors-readme-action/dev/.github/assets/placeholder.png" width="80px" alt="User avatar: " /></a> <!-- sponsors-premium -->
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<!-- sponsors-base --><a href="https://github.com/seanwash"><img src="https://github.com/seanwash.png" width="50px" alt="User avatar: seanwash" /></a> <a href="https://github.com/jerath"><img src="https://github.com/jerath.png" width="50px" alt="User avatar: jerath" /></a> <a href="https://github.com/itsa-sh"><img src="https://github.com/itsa-sh.png" width="50px" alt="User avatar: itsa-sh" /></a> <a href="https://github.com/dmmulroy"><img src="https://github.com/dmmulroy.png" width="50px" alt="User avatar: dmmulroy" /></a> <a href="https://github.com/timcole"><img src="https://github.com/timcole.png" width="50px" alt="User avatar: timcole" /></a> <a href="https://github.com/VLZH"><img src="https://github.com/VLZH.png" width="50px" alt="User avatar: VLZH" /></a> <a href="https://github.com/terasaka2k"><img src="https://github.com/terasaka2k.png" width="50px" alt="User avatar: terasaka2k" /></a> <a href="https://github.com/andriyor"><img src="https://github.com/andriyor.png" width="50px" alt="User avatar: andriyor" /></a> <a href="https://github.com/majudhu"><img src="https://github.com/majudhu.png" width="50px" alt="User avatar: majudhu" /></a> <a href="https://github.com/axelrindle"><img src="https://github.com/axelrindle.png" width="50px" alt="User avatar: axelrindle" /></a> <a href="https://github.com/jirizverina"><img src="https://github.com/jirizverina.png" width="50px" alt="User avatar: jirizverina" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="50px" alt="User avatar: chip-well" /></a> <a href="https://github.com/GRAYAH"><img src="https://github.com/GRAYAH.png" width="50px" alt="User avatar: GRAYAH" /></a> <a href="https://github.com/flashblaze"><img src="https://github.com/flashblaze.png" width="50px" alt="User avatar: flashblaze" /></a> <!-- sponsors-base -->
|
<!-- sponsors-base --><a href="https://github.com/seanwash"><img src="https://github.com/seanwash.png" width="50px" alt="User avatar: seanwash" /></a> <a href="https://github.com/jerath"><img src="https://github.com/jerath.png" width="50px" alt="User avatar: jerath" /></a> <a href="https://github.com/itsa-sh"><img src="https://github.com/itsa-sh.png" width="50px" alt="User avatar: itsa-sh" /></a> <a href="https://github.com/dmmulroy"><img src="https://github.com/dmmulroy.png" width="50px" alt="User avatar: dmmulroy" /></a> <a href="https://github.com/timcole"><img src="https://github.com/timcole.png" width="50px" alt="User avatar: timcole" /></a> <a href="https://github.com/VLZH"><img src="https://github.com/VLZH.png" width="50px" alt="User avatar: VLZH" /></a> <a href="https://github.com/terasaka2k"><img src="https://github.com/terasaka2k.png" width="50px" alt="User avatar: terasaka2k" /></a> <a href="https://github.com/andriyor"><img src="https://github.com/andriyor.png" width="50px" alt="User avatar: andriyor" /></a> <a href="https://github.com/majudhu"><img src="https://github.com/majudhu.png" width="50px" alt="User avatar: majudhu" /></a> <a href="https://github.com/axelrindle"><img src="https://github.com/axelrindle.png" width="50px" alt="User avatar: axelrindle" /></a> <a href="https://github.com/jirizverina"><img src="https://github.com/jirizverina.png" width="50px" alt="User avatar: jirizverina" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="50px" alt="User avatar: chip-well" /></a> <a href="https://github.com/GRAYAH"><img src="https://github.com/GRAYAH.png" width="50px" alt="User avatar: GRAYAH" /></a> <a href="https://github.com/flashblaze"><img src="https://github.com/flashblaze.png" width="50px" alt="User avatar: flashblaze" /></a> <a href="https://github.com/Frostist"><img src="https://github.com/Frostist.png" width="50px" alt="User avatar: Frostist" /></a> <!-- sponsors-base -->
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -16,6 +16,7 @@ dirs = "6"
|
|||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
hex = { workspace = true }
|
hex = { workspace = true }
|
||||||
|
include_dir = "0.7"
|
||||||
keyring = { workspace = true, features = ["apple-native", "windows-native", "sync-secret-service"] }
|
keyring = { workspace = true, features = ["apple-native", "windows-native", "sync-secret-service"] }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
|
|||||||
@@ -1,87 +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
|
```text
|
||||||
yaakcli send <request_id>
|
Scan my API routes and create a workspace (using yaak cli) with
|
||||||
yaakcli workspace list
|
all the requests needed for me to do manual testing?
|
||||||
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 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]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Global options:
|
```text
|
||||||
|
Send all the GraphQL requests in my workspace
|
||||||
- `--data-dir <path>`: use a custom data directory
|
|
||||||
- `-e, --environment <id>`: environment to use during request rendering/sending
|
|
||||||
- `-v, --verbose`: verbose logging and send output
|
|
||||||
|
|
||||||
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.
|
|
||||||
- `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"}'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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
|
Usage: yaak [OPTIONS] <COMMAND>
|
||||||
cargo run -q -p yaak-cli -- --help
|
|
||||||
cargo run -q -p yaak-cli -- request --help
|
Commands:
|
||||||
cargo run -q -p yaak-cli -- workspace --help
|
auth Authentication commands
|
||||||
cargo run -q -p yaak-cli -- folder --help
|
plugin Plugin development and publishing commands
|
||||||
cargo run -q -p yaak-cli -- environment --help
|
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)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ use std::path::PathBuf;
|
|||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "yaak")]
|
#[command(name = "yaak")]
|
||||||
#[command(about = "Yaak CLI - API client from the command line")]
|
#[command(about = "Yaak CLI - API client from the command line")]
|
||||||
|
#[command(version = crate::version::cli_version())]
|
||||||
|
#[command(disable_help_subcommand = true)]
|
||||||
|
#[command(after_help = r#"Agent Hints:
|
||||||
|
- Template variable syntax is ${[ my_var ]}, not {{ ... }}
|
||||||
|
- Template function syntax is ${[ namespace.my_func(a='aaa',b='bbb') ]}
|
||||||
|
- View JSONSchema for models before creating or updating (eg. `yaak request schema http`)
|
||||||
|
- Deletion requires confirmation (--yes for non-interactive environments)
|
||||||
|
"#)]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
/// Use a custom data directory
|
/// Use a custom data directory
|
||||||
#[arg(long, global = true)]
|
#[arg(long, global = true)]
|
||||||
@@ -13,10 +21,14 @@ pub struct Cli {
|
|||||||
#[arg(long, short, global = true)]
|
#[arg(long, short, global = true)]
|
||||||
pub environment: Option<String>,
|
pub environment: Option<String>,
|
||||||
|
|
||||||
/// Enable verbose logging
|
/// Enable verbose send output (events and streamed response body)
|
||||||
#[arg(long, short, global = true)]
|
#[arg(long, short, global = true)]
|
||||||
pub verbose: bool,
|
pub verbose: bool,
|
||||||
|
|
||||||
|
/// Enable CLI logging; optionally set level (error|warn|info|debug|trace)
|
||||||
|
#[arg(long, global = true, value_name = "LEVEL", num_args = 0..=1, ignore_case = true)]
|
||||||
|
pub log: Option<Option<LogLevel>>,
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: Commands,
|
pub command: Commands,
|
||||||
}
|
}
|
||||||
@@ -56,12 +68,8 @@ pub struct SendArgs {
|
|||||||
/// Request, folder, or workspace ID
|
/// Request, folder, or workspace ID
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
||||||
/// Execute requests sequentially (default)
|
|
||||||
#[arg(long, conflicts_with = "parallel")]
|
|
||||||
pub sequential: bool,
|
|
||||||
|
|
||||||
/// Execute requests in parallel
|
/// Execute requests in parallel
|
||||||
#[arg(long, conflicts_with = "sequential")]
|
#[arg(long)]
|
||||||
pub parallel: bool,
|
pub parallel: bool,
|
||||||
|
|
||||||
/// Stop on first request failure when sending folders/workspaces
|
/// Stop on first request failure when sending folders/workspaces
|
||||||
@@ -70,6 +78,7 @@ pub struct SendArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
#[command(disable_help_subcommand = true)]
|
||||||
pub struct WorkspaceArgs {
|
pub struct WorkspaceArgs {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: WorkspaceCommands,
|
pub command: WorkspaceCommands,
|
||||||
@@ -80,6 +89,13 @@ pub enum WorkspaceCommands {
|
|||||||
/// List all workspaces
|
/// List all workspaces
|
||||||
List,
|
List,
|
||||||
|
|
||||||
|
/// Output JSON schema for workspace create/update payloads
|
||||||
|
Schema {
|
||||||
|
/// Pretty-print schema JSON output
|
||||||
|
#[arg(long)]
|
||||||
|
pretty: bool,
|
||||||
|
},
|
||||||
|
|
||||||
/// Show a workspace as JSON
|
/// Show a workspace as JSON
|
||||||
Show {
|
Show {
|
||||||
/// Workspace ID
|
/// Workspace ID
|
||||||
@@ -124,6 +140,7 @@ pub enum WorkspaceCommands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
#[command(disable_help_subcommand = true)]
|
||||||
pub struct RequestArgs {
|
pub struct RequestArgs {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: RequestCommands,
|
pub command: RequestCommands,
|
||||||
@@ -153,6 +170,10 @@ pub enum RequestCommands {
|
|||||||
Schema {
|
Schema {
|
||||||
#[arg(value_enum)]
|
#[arg(value_enum)]
|
||||||
request_type: RequestSchemaType,
|
request_type: RequestSchemaType,
|
||||||
|
|
||||||
|
/// Pretty-print schema JSON output
|
||||||
|
#[arg(long)]
|
||||||
|
pretty: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Create a new HTTP request
|
/// Create a new HTTP request
|
||||||
@@ -206,7 +227,29 @@ pub enum RequestSchemaType {
|
|||||||
Websocket,
|
Websocket,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, ValueEnum)]
|
||||||
|
pub enum LogLevel {
|
||||||
|
Error,
|
||||||
|
Warn,
|
||||||
|
Info,
|
||||||
|
Debug,
|
||||||
|
Trace,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogLevel {
|
||||||
|
pub fn as_filter(self) -> log::LevelFilter {
|
||||||
|
match self {
|
||||||
|
LogLevel::Error => log::LevelFilter::Error,
|
||||||
|
LogLevel::Warn => log::LevelFilter::Warn,
|
||||||
|
LogLevel::Info => log::LevelFilter::Info,
|
||||||
|
LogLevel::Debug => log::LevelFilter::Debug,
|
||||||
|
LogLevel::Trace => log::LevelFilter::Trace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
#[command(disable_help_subcommand = true)]
|
||||||
pub struct FolderArgs {
|
pub struct FolderArgs {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: FolderCommands,
|
pub command: FolderCommands,
|
||||||
@@ -263,6 +306,7 @@ pub enum FolderCommands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
#[command(disable_help_subcommand = true)]
|
||||||
pub struct EnvironmentArgs {
|
pub struct EnvironmentArgs {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: EnvironmentCommands,
|
pub command: EnvironmentCommands,
|
||||||
@@ -276,6 +320,13 @@ pub enum EnvironmentCommands {
|
|||||||
workspace_id: String,
|
workspace_id: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Output JSON schema for environment create/update payloads
|
||||||
|
Schema {
|
||||||
|
/// Pretty-print schema JSON output
|
||||||
|
#[arg(long)]
|
||||||
|
pretty: bool,
|
||||||
|
},
|
||||||
|
|
||||||
/// Show an environment as JSON
|
/// Show an environment as JSON
|
||||||
Show {
|
Show {
|
||||||
/// Environment ID
|
/// Environment ID
|
||||||
@@ -283,15 +334,22 @@ pub enum EnvironmentCommands {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/// Create an environment
|
/// Create an environment
|
||||||
|
#[command(after_help = r#"Modes (choose one):
|
||||||
|
1) yaak environment create <workspace_id> --name <name>
|
||||||
|
2) yaak environment create --json '{"workspaceId":"wk_abc","name":"Production"}'
|
||||||
|
3) yaak environment create '{"workspaceId":"wk_abc","name":"Production"}'
|
||||||
|
4) yaak environment create <workspace_id> --json '{"name":"Production"}'
|
||||||
|
"#)]
|
||||||
Create {
|
Create {
|
||||||
/// Workspace ID (or positional JSON payload shorthand)
|
/// Workspace ID for flag-based mode, or positional JSON payload shorthand
|
||||||
|
#[arg(value_name = "WORKSPACE_ID_OR_JSON")]
|
||||||
workspace_id: Option<String>,
|
workspace_id: Option<String>,
|
||||||
|
|
||||||
/// Environment name
|
/// Environment name
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
|
||||||
/// JSON payload
|
/// JSON payload (use instead of WORKSPACE_ID/--name)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: Option<String>,
|
json: Option<String>,
|
||||||
},
|
},
|
||||||
@@ -319,6 +377,7 @@ pub enum EnvironmentCommands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
#[command(disable_help_subcommand = true)]
|
||||||
pub struct AuthArgs {
|
pub struct AuthArgs {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: AuthCommands,
|
pub command: AuthCommands,
|
||||||
@@ -337,6 +396,7 @@ pub enum AuthCommands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
#[command(disable_help_subcommand = true)]
|
||||||
pub struct PluginArgs {
|
pub struct PluginArgs {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: PluginCommands,
|
pub command: PluginCommands,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::cli::{AuthArgs, AuthCommands};
|
use crate::cli::{AuthArgs, AuthCommands};
|
||||||
use crate::ui;
|
use crate::ui;
|
||||||
|
use crate::utils::http;
|
||||||
use base64::Engine as _;
|
use base64::Engine as _;
|
||||||
use keyring::Entry;
|
use keyring::Entry;
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
@@ -136,10 +137,8 @@ async fn whoami() -> CommandResult {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let url = format!("{}/api/v1/whoami", environment.api_base_url());
|
let url = format!("{}/api/v1/whoami", environment.api_base_url());
|
||||||
let response = reqwest::Client::new()
|
let response = http::build_client(Some(&token))?
|
||||||
.get(url)
|
.get(url)
|
||||||
.header("X-Yaak-Session", token)
|
|
||||||
.header(reqwest::header::USER_AGENT, user_agent())
|
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to call whoami endpoint: {e}"))?;
|
.map_err(|e| format!("Failed to call whoami endpoint: {e}"))?;
|
||||||
@@ -156,7 +155,7 @@ async fn whoami() -> CommandResult {
|
|||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Err(parse_api_error(status.as_u16(), &body));
|
return Err(http::parse_api_error(status.as_u16(), &body));
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("{body}");
|
println!("{body}");
|
||||||
@@ -342,9 +341,8 @@ async fn write_redirect(stream: &mut TcpStream, location: &str) -> std::io::Resu
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn exchange_access_token(oauth: &OAuthFlow, code: &str) -> CommandResult<String> {
|
async fn exchange_access_token(oauth: &OAuthFlow, code: &str) -> CommandResult<String> {
|
||||||
let response = reqwest::Client::new()
|
let response = http::build_client(None)?
|
||||||
.post(&oauth.token_url)
|
.post(&oauth.token_url)
|
||||||
.header(reqwest::header::USER_AGENT, user_agent())
|
|
||||||
.form(&[
|
.form(&[
|
||||||
("grant_type", "authorization_code"),
|
("grant_type", "authorization_code"),
|
||||||
("client_id", OAUTH_CLIENT_ID),
|
("client_id", OAUTH_CLIENT_ID),
|
||||||
@@ -406,38 +404,12 @@ fn delete_auth_token(environment: Environment) -> CommandResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_api_error(status: u16, body: &str) -> String {
|
|
||||||
if let Ok(value) = serde_json::from_str::<Value>(body) {
|
|
||||||
if let Some(message) = value.get("message").and_then(Value::as_str) {
|
|
||||||
return message.to_string();
|
|
||||||
}
|
|
||||||
if let Some(error) = value.get("error").and_then(Value::as_str) {
|
|
||||||
return error.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
format!("API error {status}: {body}")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn random_hex(bytes: usize) -> String {
|
fn random_hex(bytes: usize) -> String {
|
||||||
let mut data = vec![0_u8; bytes];
|
let mut data = vec![0_u8; bytes];
|
||||||
OsRng.fill_bytes(&mut data);
|
OsRng.fill_bytes(&mut data);
|
||||||
hex::encode(data)
|
hex::encode(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn user_agent() -> String {
|
|
||||||
format!("YaakCli/{} ({})", env!("CARGO_PKG_VERSION"), ua_platform())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ua_platform() -> &'static str {
|
|
||||||
match std::env::consts::OS {
|
|
||||||
"windows" => "Win",
|
|
||||||
"darwin" => "Mac",
|
|
||||||
"linux" => "Linux",
|
|
||||||
_ => "Unknown",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn confirm_open_browser() -> CommandResult<bool> {
|
fn confirm_open_browser() -> CommandResult<bool> {
|
||||||
if !io::stdin().is_terminal() {
|
if !io::stdin().is_terminal() {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ use crate::cli::{EnvironmentArgs, EnvironmentCommands};
|
|||||||
use crate::context::CliContext;
|
use crate::context::CliContext;
|
||||||
use crate::utils::confirm::confirm_delete;
|
use crate::utils::confirm::confirm_delete;
|
||||||
use crate::utils::json::{
|
use crate::utils::json::{
|
||||||
apply_merge_patch, is_json_shorthand, parse_optional_json, parse_required_json, require_id,
|
apply_merge_patch, is_json_shorthand, merge_workspace_id_arg, parse_optional_json,
|
||||||
validate_create_id,
|
parse_required_json, require_id, validate_create_id,
|
||||||
};
|
};
|
||||||
|
use crate::utils::schema::append_agent_hints;
|
||||||
|
use schemars::schema_for;
|
||||||
use yaak_models::models::Environment;
|
use yaak_models::models::Environment;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
|
|
||||||
@@ -13,6 +15,7 @@ type CommandResult<T = ()> = std::result::Result<T, String>;
|
|||||||
pub fn run(ctx: &CliContext, args: EnvironmentArgs) -> i32 {
|
pub fn run(ctx: &CliContext, args: EnvironmentArgs) -> i32 {
|
||||||
let result = match args.command {
|
let result = match args.command {
|
||||||
EnvironmentCommands::List { workspace_id } => list(ctx, &workspace_id),
|
EnvironmentCommands::List { workspace_id } => list(ctx, &workspace_id),
|
||||||
|
EnvironmentCommands::Schema { pretty } => schema(pretty),
|
||||||
EnvironmentCommands::Show { environment_id } => show(ctx, &environment_id),
|
EnvironmentCommands::Show { environment_id } => show(ctx, &environment_id),
|
||||||
EnvironmentCommands::Create { workspace_id, name, json } => {
|
EnvironmentCommands::Create { workspace_id, name, json } => {
|
||||||
create(ctx, workspace_id, name, json)
|
create(ctx, workspace_id, name, json)
|
||||||
@@ -30,6 +33,18 @@ pub fn run(ctx: &CliContext, args: EnvironmentArgs) -> i32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn schema(pretty: bool) -> CommandResult {
|
||||||
|
let mut schema = serde_json::to_value(schema_for!(Environment))
|
||||||
|
.map_err(|e| format!("Failed to serialize environment schema: {e}"))?;
|
||||||
|
append_agent_hints(&mut schema);
|
||||||
|
|
||||||
|
let output =
|
||||||
|
if pretty { serde_json::to_string_pretty(&schema) } else { serde_json::to_string(&schema) }
|
||||||
|
.map_err(|e| format!("Failed to format environment schema JSON: {e}"))?;
|
||||||
|
println!("{output}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn list(ctx: &CliContext, workspace_id: &str) -> CommandResult {
|
fn list(ctx: &CliContext, workspace_id: &str) -> CommandResult {
|
||||||
let environments = ctx
|
let environments = ctx
|
||||||
.db()
|
.db()
|
||||||
@@ -63,17 +78,11 @@ fn create(
|
|||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
json: Option<String>,
|
json: Option<String>,
|
||||||
) -> CommandResult {
|
) -> CommandResult {
|
||||||
if json.is_some() && workspace_id.as_deref().is_some_and(|v| !is_json_shorthand(v)) {
|
let json_shorthand =
|
||||||
return Err(
|
workspace_id.as_deref().filter(|v| is_json_shorthand(v)).map(str::to_owned);
|
||||||
"environment create cannot combine workspace_id with --json payload".to_string()
|
let workspace_id_arg = workspace_id.filter(|v| !is_json_shorthand(v));
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let payload = parse_optional_json(
|
let payload = parse_optional_json(json, json_shorthand, "environment create")?;
|
||||||
json,
|
|
||||||
workspace_id.clone().filter(|v| is_json_shorthand(v)),
|
|
||||||
"environment create",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if let Some(payload) = payload {
|
if let Some(payload) = payload {
|
||||||
if name.is_some() {
|
if name.is_some() {
|
||||||
@@ -83,10 +92,11 @@ fn create(
|
|||||||
validate_create_id(&payload, "environment")?;
|
validate_create_id(&payload, "environment")?;
|
||||||
let mut environment: Environment = serde_json::from_value(payload)
|
let mut environment: Environment = serde_json::from_value(payload)
|
||||||
.map_err(|e| format!("Failed to parse environment create JSON: {e}"))?;
|
.map_err(|e| format!("Failed to parse environment create JSON: {e}"))?;
|
||||||
|
merge_workspace_id_arg(
|
||||||
if environment.workspace_id.is_empty() {
|
workspace_id_arg.as_deref(),
|
||||||
return Err("environment create JSON requires non-empty \"workspaceId\"".to_string());
|
&mut environment.workspace_id,
|
||||||
}
|
"environment create",
|
||||||
|
)?;
|
||||||
|
|
||||||
if environment.parent_model.is_empty() {
|
if environment.parent_model.is_empty() {
|
||||||
environment.parent_model = "environment".to_string();
|
environment.parent_model = "environment".to_string();
|
||||||
@@ -101,7 +111,7 @@ fn create(
|
|||||||
return Ok(());
|
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()
|
"environment create requires workspace_id unless JSON payload is provided".to_string()
|
||||||
})?;
|
})?;
|
||||||
let name = name.ok_or_else(|| {
|
let name = name.ok_or_else(|| {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use crate::cli::{FolderArgs, FolderCommands};
|
|||||||
use crate::context::CliContext;
|
use crate::context::CliContext;
|
||||||
use crate::utils::confirm::confirm_delete;
|
use crate::utils::confirm::confirm_delete;
|
||||||
use crate::utils::json::{
|
use crate::utils::json::{
|
||||||
apply_merge_patch, is_json_shorthand, parse_optional_json, parse_required_json, require_id,
|
apply_merge_patch, is_json_shorthand, merge_workspace_id_arg, parse_optional_json,
|
||||||
validate_create_id,
|
parse_required_json, require_id, validate_create_id,
|
||||||
};
|
};
|
||||||
use yaak_models::models::Folder;
|
use yaak_models::models::Folder;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
@@ -58,15 +58,11 @@ fn create(
|
|||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
json: Option<String>,
|
json: Option<String>,
|
||||||
) -> CommandResult {
|
) -> CommandResult {
|
||||||
if json.is_some() && workspace_id.as_deref().is_some_and(|v| !is_json_shorthand(v)) {
|
let json_shorthand =
|
||||||
return Err("folder create cannot combine workspace_id with --json payload".to_string());
|
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(
|
let payload = parse_optional_json(json, json_shorthand, "folder create")?;
|
||||||
json,
|
|
||||||
workspace_id.clone().filter(|v| is_json_shorthand(v)),
|
|
||||||
"folder create",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if let Some(payload) = payload {
|
if let Some(payload) = payload {
|
||||||
if name.is_some() {
|
if name.is_some() {
|
||||||
@@ -74,12 +70,13 @@ fn create(
|
|||||||
}
|
}
|
||||||
|
|
||||||
validate_create_id(&payload, "folder")?;
|
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}"))?;
|
.map_err(|e| format!("Failed to parse folder create JSON: {e}"))?;
|
||||||
|
merge_workspace_id_arg(
|
||||||
if folder.workspace_id.is_empty() {
|
workspace_id_arg.as_deref(),
|
||||||
return Err("folder create JSON requires non-empty \"workspaceId\"".to_string());
|
&mut folder.workspace_id,
|
||||||
}
|
"folder create",
|
||||||
|
)?;
|
||||||
|
|
||||||
let created = ctx
|
let created = ctx
|
||||||
.db()
|
.db()
|
||||||
@@ -90,7 +87,7 @@ fn create(
|
|||||||
return Ok(());
|
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()
|
"folder create requires workspace_id unless JSON payload is provided".to_string()
|
||||||
})?;
|
})?;
|
||||||
let name = name.ok_or_else(|| {
|
let name = name.ok_or_else(|| {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::cli::{GenerateArgs, PluginArgs, PluginCommands, PluginPathArg};
|
use crate::cli::{GenerateArgs, PluginArgs, PluginCommands, PluginPathArg};
|
||||||
use crate::ui;
|
use crate::ui;
|
||||||
|
use crate::utils::http;
|
||||||
use keyring::Entry;
|
use keyring::Entry;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use rolldown::{
|
use rolldown::{
|
||||||
@@ -7,7 +8,6 @@ use rolldown::{
|
|||||||
WatchOption, Watcher,
|
WatchOption, Watcher,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::Value;
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{self, IsTerminal, Read, Write};
|
use std::io::{self, IsTerminal, Read, Write};
|
||||||
@@ -186,10 +186,8 @@ async fn publish(args: PluginPathArg) -> CommandResult {
|
|||||||
|
|
||||||
ui::info("Uploading plugin");
|
ui::info("Uploading plugin");
|
||||||
let url = format!("{}/api/v1/plugins/publish", environment.api_base_url());
|
let url = format!("{}/api/v1/plugins/publish", environment.api_base_url());
|
||||||
let response = reqwest::Client::new()
|
let response = http::build_client(Some(&token))?
|
||||||
.post(url)
|
.post(url)
|
||||||
.header("X-Yaak-Session", token)
|
|
||||||
.header(reqwest::header::USER_AGENT, user_agent())
|
|
||||||
.header(reqwest::header::CONTENT_TYPE, "application/zip")
|
.header(reqwest::header::CONTENT_TYPE, "application/zip")
|
||||||
.body(archive)
|
.body(archive)
|
||||||
.send()
|
.send()
|
||||||
@@ -201,7 +199,7 @@ async fn publish(args: PluginPathArg) -> CommandResult {
|
|||||||
response.text().await.map_err(|e| format!("Failed reading publish response body: {e}"))?;
|
response.text().await.map_err(|e| format!("Failed reading publish response body: {e}"))?;
|
||||||
|
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
return Err(parse_api_error(status.as_u16(), &body));
|
return Err(http::parse_api_error(status.as_u16(), &body));
|
||||||
}
|
}
|
||||||
|
|
||||||
let published: PublishResponse = serde_json::from_str(&body)
|
let published: PublishResponse = serde_json::from_str(&body)
|
||||||
@@ -389,32 +387,6 @@ fn get_auth_token(environment: Environment) -> CommandResult<Option<String>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_api_error(status: u16, body: &str) -> String {
|
|
||||||
if let Ok(value) = serde_json::from_str::<Value>(body) {
|
|
||||||
if let Some(message) = value.get("message").and_then(Value::as_str) {
|
|
||||||
return message.to_string();
|
|
||||||
}
|
|
||||||
if let Some(error) = value.get("error").and_then(Value::as_str) {
|
|
||||||
return error.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
format!("API error {status}: {body}")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn user_agent() -> String {
|
|
||||||
format!("YaakCli/{} ({})", env!("CARGO_PKG_VERSION"), ua_platform())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ua_platform() -> &'static str {
|
|
||||||
match std::env::consts::OS {
|
|
||||||
"windows" => "Win",
|
|
||||||
"darwin" => "Mac",
|
|
||||||
"linux" => "Linux",
|
|
||||||
_ => "Unknown",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn random_name() -> String {
|
fn random_name() -> String {
|
||||||
const ADJECTIVES: &[&str] = &[
|
const ADJECTIVES: &[&str] = &[
|
||||||
"young", "youthful", "yellow", "yielding", "yappy", "yawning", "yummy", "yucky", "yearly",
|
"young", "youthful", "yellow", "yielding", "yappy", "yawning", "yummy", "yucky", "yearly",
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ use crate::cli::{RequestArgs, RequestCommands, RequestSchemaType};
|
|||||||
use crate::context::CliContext;
|
use crate::context::CliContext;
|
||||||
use crate::utils::confirm::confirm_delete;
|
use crate::utils::confirm::confirm_delete;
|
||||||
use crate::utils::json::{
|
use crate::utils::json::{
|
||||||
apply_merge_patch, is_json_shorthand, parse_optional_json, parse_required_json, require_id,
|
apply_merge_patch, is_json_shorthand, merge_workspace_id_arg, parse_optional_json,
|
||||||
validate_create_id,
|
parse_required_json, require_id, validate_create_id,
|
||||||
};
|
};
|
||||||
|
use crate::utils::schema::append_agent_hints;
|
||||||
use schemars::schema_for;
|
use schemars::schema_for;
|
||||||
use serde_json::{Map, Value, json};
|
use serde_json::{Map, Value, json};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::io::Write;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use yaak::send::{SendHttpRequestByIdWithPluginsParams, send_http_request_by_id_with_plugins};
|
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::models::{GrpcRequest, HttpRequest, WebsocketRequest};
|
||||||
use yaak_models::queries::any_request::AnyRequest;
|
use yaak_models::queries::any_request::AnyRequest;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
@@ -35,8 +38,8 @@ pub async fn run(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
RequestCommands::Schema { request_type } => {
|
RequestCommands::Schema { request_type, pretty } => {
|
||||||
return match schema(ctx, request_type).await {
|
return match schema(ctx, request_type, pretty).await {
|
||||||
Ok(()) => 0,
|
Ok(()) => 0,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("Error: {error}");
|
eprintln!("Error: {error}");
|
||||||
@@ -75,7 +78,7 @@ fn list(ctx: &CliContext, workspace_id: &str) -> CommandResult {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn schema(ctx: &CliContext, request_type: RequestSchemaType) -> CommandResult {
|
async fn schema(ctx: &CliContext, request_type: RequestSchemaType, pretty: bool) -> CommandResult {
|
||||||
let mut schema = match request_type {
|
let mut schema = match request_type {
|
||||||
RequestSchemaType::Http => serde_json::to_value(schema_for!(HttpRequest))
|
RequestSchemaType::Http => serde_json::to_value(schema_for!(HttpRequest))
|
||||||
.map_err(|e| format!("Failed to serialize HTTP request schema: {e}"))?,
|
.map_err(|e| format!("Failed to serialize HTTP request schema: {e}"))?,
|
||||||
@@ -85,16 +88,51 @@ async fn schema(ctx: &CliContext, request_type: RequestSchemaType) -> CommandRes
|
|||||||
.map_err(|e| format!("Failed to serialize WebSocket request schema: {e}"))?,
|
.map_err(|e| format!("Failed to serialize WebSocket request schema: {e}"))?,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enrich_schema_guidance(&mut schema, request_type);
|
||||||
|
append_agent_hints(&mut schema);
|
||||||
|
|
||||||
if let Err(error) = merge_auth_schema_from_plugins(ctx, &mut schema).await {
|
if let Err(error) = merge_auth_schema_from_plugins(ctx, &mut schema).await {
|
||||||
eprintln!("Warning: Failed to enrich authentication schema from plugins: {error}");
|
eprintln!("Warning: Failed to enrich authentication schema from plugins: {error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = serde_json::to_string_pretty(&schema)
|
let output =
|
||||||
.map_err(|e| format!("Failed to format schema JSON: {e}"))?;
|
if pretty { serde_json::to_string_pretty(&schema) } else { serde_json::to_string(&schema) }
|
||||||
|
.map_err(|e| format!("Failed to format schema JSON: {e}"))?;
|
||||||
println!("{output}");
|
println!("{output}");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enrich_schema_guidance(schema: &mut Value, request_type: RequestSchemaType) {
|
||||||
|
if !matches!(request_type, RequestSchemaType::Http) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(properties) = schema.get_mut("properties").and_then(Value::as_object_mut) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(url_schema) = properties.get_mut("url").and_then(Value::as_object_mut) {
|
||||||
|
append_description(
|
||||||
|
url_schema,
|
||||||
|
"For path segments like `/foo/:id/comments/:commentId`, put concrete values in `urlParameters` using names without `:` (for example `id`, `commentId`).",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_description(schema: &mut Map<String, Value>, extra: &str) {
|
||||||
|
match schema.get_mut("description") {
|
||||||
|
Some(Value::String(existing)) if !existing.trim().is_empty() => {
|
||||||
|
if !existing.ends_with(' ') {
|
||||||
|
existing.push(' ');
|
||||||
|
}
|
||||||
|
existing.push_str(extra);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
schema.insert("description".to_string(), Value::String(extra.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn merge_auth_schema_from_plugins(
|
async fn merge_auth_schema_from_plugins(
|
||||||
ctx: &CliContext,
|
ctx: &CliContext,
|
||||||
schema: &mut Value,
|
schema: &mut Value,
|
||||||
@@ -298,15 +336,11 @@ fn create(
|
|||||||
url: Option<String>,
|
url: Option<String>,
|
||||||
json: Option<String>,
|
json: Option<String>,
|
||||||
) -> CommandResult {
|
) -> CommandResult {
|
||||||
if json.is_some() && workspace_id.as_deref().is_some_and(|v| !is_json_shorthand(v)) {
|
let json_shorthand =
|
||||||
return Err("request create cannot combine workspace_id with --json payload".to_string());
|
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(
|
let payload = parse_optional_json(json, json_shorthand, "request create")?;
|
||||||
json,
|
|
||||||
workspace_id.clone().filter(|v| is_json_shorthand(v)),
|
|
||||||
"request create",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if let Some(payload) = payload {
|
if let Some(payload) = payload {
|
||||||
if name.is_some() || method.is_some() || url.is_some() {
|
if name.is_some() || method.is_some() || url.is_some() {
|
||||||
@@ -314,12 +348,13 @@ fn create(
|
|||||||
}
|
}
|
||||||
|
|
||||||
validate_create_id(&payload, "request")?;
|
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}"))?;
|
.map_err(|e| format!("Failed to parse request create JSON: {e}"))?;
|
||||||
|
merge_workspace_id_arg(
|
||||||
if request.workspace_id.is_empty() {
|
workspace_id_arg.as_deref(),
|
||||||
return Err("request create JSON requires non-empty \"workspaceId\"".to_string());
|
&mut request.workspace_id,
|
||||||
}
|
"request create",
|
||||||
|
)?;
|
||||||
|
|
||||||
let created = ctx
|
let created = ctx
|
||||||
.db()
|
.db()
|
||||||
@@ -330,7 +365,7 @@ fn create(
|
|||||||
return Ok(());
|
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()
|
"request create requires workspace_id unless JSON payload is provided".to_string()
|
||||||
})?;
|
})?;
|
||||||
let name = name.unwrap_or_default();
|
let name = name.unwrap_or_default();
|
||||||
@@ -434,14 +469,24 @@ async fn send_http_request_by_id(
|
|||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let plugin_context = PluginContext::new(None, Some(workspace_id.to_string()));
|
let plugin_context = PluginContext::new(None, Some(workspace_id.to_string()));
|
||||||
|
|
||||||
let (event_tx, mut event_rx) = mpsc::channel(100);
|
let (event_tx, mut event_rx) = mpsc::channel::<SenderHttpResponseEvent>(100);
|
||||||
|
let (body_chunk_tx, mut body_chunk_rx) = mpsc::unbounded_channel::<Vec<u8>>();
|
||||||
let event_handle = tokio::spawn(async move {
|
let event_handle = tokio::spawn(async move {
|
||||||
while let Some(event) = event_rx.recv().await {
|
while let Some(event) = event_rx.recv().await {
|
||||||
if verbose {
|
if verbose && !matches!(event, SenderHttpResponseEvent::ChunkReceived { .. }) {
|
||||||
println!("{}", event);
|
println!("{}", event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
let body_handle = tokio::task::spawn_blocking(move || {
|
||||||
|
let mut stdout = std::io::stdout();
|
||||||
|
while let Some(chunk) = body_chunk_rx.blocking_recv() {
|
||||||
|
if stdout.write_all(&chunk).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let _ = stdout.flush();
|
||||||
|
}
|
||||||
|
});
|
||||||
let response_dir = ctx.data_dir().join("responses");
|
let response_dir = ctx.data_dir().join("responses");
|
||||||
|
|
||||||
let result = send_http_request_by_id_with_plugins(SendHttpRequestByIdWithPluginsParams {
|
let result = send_http_request_by_id_with_plugins(SendHttpRequestByIdWithPluginsParams {
|
||||||
@@ -453,6 +498,7 @@ async fn send_http_request_by_id(
|
|||||||
cookie_jar_id: None,
|
cookie_jar_id: None,
|
||||||
response_dir: &response_dir,
|
response_dir: &response_dir,
|
||||||
emit_events_to: Some(event_tx),
|
emit_events_to: Some(event_tx),
|
||||||
|
emit_response_body_chunks_to: Some(body_chunk_tx),
|
||||||
plugin_manager: ctx.plugin_manager(),
|
plugin_manager: ctx.plugin_manager(),
|
||||||
encryption_manager: ctx.encryption_manager.clone(),
|
encryption_manager: ctx.encryption_manager.clone(),
|
||||||
plugin_context: &plugin_context,
|
plugin_context: &plugin_context,
|
||||||
@@ -462,24 +508,7 @@ async fn send_http_request_by_id(
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
let _ = event_handle.await;
|
let _ = event_handle.await;
|
||||||
let result = result.map_err(|e| e.to_string())?;
|
let _ = body_handle.await;
|
||||||
|
result.map_err(|e| e.to_string())?;
|
||||||
if verbose {
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
println!(
|
|
||||||
"HTTP {} {}",
|
|
||||||
result.response.status,
|
|
||||||
result.response.status_reason.as_deref().unwrap_or("")
|
|
||||||
);
|
|
||||||
if verbose {
|
|
||||||
for header in &result.response.headers {
|
|
||||||
println!("{}: {}", header.name, header.value);
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
let body = String::from_utf8(result.response_body)
|
|
||||||
.map_err(|e| format!("Failed to read response body: {e}"))?;
|
|
||||||
println!("{}", body);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ use crate::utils::confirm::confirm_delete;
|
|||||||
use crate::utils::json::{
|
use crate::utils::json::{
|
||||||
apply_merge_patch, parse_optional_json, parse_required_json, require_id, validate_create_id,
|
apply_merge_patch, parse_optional_json, parse_required_json, require_id, validate_create_id,
|
||||||
};
|
};
|
||||||
|
use crate::utils::schema::append_agent_hints;
|
||||||
|
use schemars::schema_for;
|
||||||
use yaak_models::models::Workspace;
|
use yaak_models::models::Workspace;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
|
|
||||||
@@ -12,6 +14,7 @@ type CommandResult<T = ()> = std::result::Result<T, String>;
|
|||||||
pub fn run(ctx: &CliContext, args: WorkspaceArgs) -> i32 {
|
pub fn run(ctx: &CliContext, args: WorkspaceArgs) -> i32 {
|
||||||
let result = match args.command {
|
let result = match args.command {
|
||||||
WorkspaceCommands::List => list(ctx),
|
WorkspaceCommands::List => list(ctx),
|
||||||
|
WorkspaceCommands::Schema { pretty } => schema(pretty),
|
||||||
WorkspaceCommands::Show { workspace_id } => show(ctx, &workspace_id),
|
WorkspaceCommands::Show { workspace_id } => show(ctx, &workspace_id),
|
||||||
WorkspaceCommands::Create { name, json, json_input } => create(ctx, name, json, json_input),
|
WorkspaceCommands::Create { name, json, json_input } => create(ctx, name, json, json_input),
|
||||||
WorkspaceCommands::Update { json, json_input } => update(ctx, json, json_input),
|
WorkspaceCommands::Update { json, json_input } => update(ctx, json, json_input),
|
||||||
@@ -27,6 +30,23 @@ pub fn run(ctx: &CliContext, args: WorkspaceArgs) -> i32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn schema(pretty: bool) -> CommandResult {
|
||||||
|
let mut schema =
|
||||||
|
serde_json::to_value(schema_for!(Workspace)).map_err(|e| format!(
|
||||||
|
"Failed to serialize workspace schema: {e}"
|
||||||
|
))?;
|
||||||
|
append_agent_hints(&mut schema);
|
||||||
|
|
||||||
|
let output = if pretty {
|
||||||
|
serde_json::to_string_pretty(&schema)
|
||||||
|
} else {
|
||||||
|
serde_json::to_string(&schema)
|
||||||
|
}
|
||||||
|
.map_err(|e| format!("Failed to format workspace schema JSON: {e}"))?;
|
||||||
|
println!("{output}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn list(ctx: &CliContext) -> CommandResult {
|
fn list(ctx: &CliContext) -> CommandResult {
|
||||||
let workspaces =
|
let workspaces =
|
||||||
ctx.db().list_workspaces().map_err(|e| format!("Failed to list workspaces: {e}"))?;
|
ctx.db().list_workspaces().map_err(|e| format!("Failed to list workspaces: {e}"))?;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use crate::plugin_events::CliPluginEventBridge;
|
use crate::plugin_events::CliPluginEventBridge;
|
||||||
|
use include_dir::{Dir, include_dir};
|
||||||
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
@@ -9,6 +11,13 @@ use yaak_models::query_manager::QueryManager;
|
|||||||
use yaak_plugins::events::PluginContext;
|
use yaak_plugins::events::PluginContext;
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
|
|
||||||
|
const EMBEDDED_PLUGIN_RUNTIME: &str = include_str!(concat!(
|
||||||
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
|
"/../../crates-tauri/yaak-app/vendored/plugin-runtime/index.cjs"
|
||||||
|
));
|
||||||
|
static EMBEDDED_VENDORED_PLUGINS: Dir<'_> =
|
||||||
|
include_dir!("$CARGO_MANIFEST_DIR/../../crates-tauri/yaak-app/vendored/plugins");
|
||||||
|
|
||||||
pub struct CliContext {
|
pub struct CliContext {
|
||||||
data_dir: PathBuf,
|
data_dir: PathBuf,
|
||||||
query_manager: QueryManager,
|
query_manager: QueryManager,
|
||||||
@@ -29,41 +38,40 @@ impl CliContext {
|
|||||||
let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id));
|
let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id));
|
||||||
|
|
||||||
let plugin_manager = if with_plugins {
|
let plugin_manager = if with_plugins {
|
||||||
let vendored_plugin_dir = data_dir.join("vendored-plugins");
|
let embedded_vendored_plugin_dir = data_dir.join("vendored-plugins");
|
||||||
|
let bundled_plugin_dir =
|
||||||
|
resolve_bundled_plugin_dir_for_cli(&embedded_vendored_plugin_dir);
|
||||||
let installed_plugin_dir = data_dir.join("installed-plugins");
|
let installed_plugin_dir = data_dir.join("installed-plugins");
|
||||||
let node_bin_path = PathBuf::from("node");
|
let node_bin_path = PathBuf::from("node");
|
||||||
|
|
||||||
let plugin_runtime_main =
|
if bundled_plugin_dir == embedded_vendored_plugin_dir {
|
||||||
std::env::var("YAAK_PLUGIN_RUNTIME").map(PathBuf::from).unwrap_or_else(|_| {
|
prepare_embedded_vendored_plugins(&embedded_vendored_plugin_dir)
|
||||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
.expect("Failed to prepare bundled plugins");
|
||||||
.join("../../crates-tauri/yaak-app/vendored/plugin-runtime/index.cjs")
|
|
||||||
});
|
|
||||||
|
|
||||||
let plugin_manager = Arc::new(
|
|
||||||
PluginManager::new(
|
|
||||||
vendored_plugin_dir,
|
|
||||||
installed_plugin_dir,
|
|
||||||
node_bin_path,
|
|
||||||
plugin_runtime_main,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await,
|
|
||||||
);
|
|
||||||
|
|
||||||
let plugins = query_manager.connect().list_plugins().unwrap_or_default();
|
|
||||||
if !plugins.is_empty() {
|
|
||||||
let errors = plugin_manager
|
|
||||||
.initialize_all_plugins(plugins, &PluginContext::new_empty())
|
|
||||||
.await;
|
|
||||||
for (plugin_dir, error_msg) in errors {
|
|
||||||
eprintln!(
|
|
||||||
"Warning: Failed to initialize plugin '{}': {}",
|
|
||||||
plugin_dir, error_msg
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(plugin_manager)
|
let plugin_runtime_main =
|
||||||
|
std::env::var("YAAK_PLUGIN_RUNTIME").map(PathBuf::from).unwrap_or_else(|_| {
|
||||||
|
prepare_embedded_plugin_runtime(&data_dir)
|
||||||
|
.expect("Failed to prepare embedded plugin runtime")
|
||||||
|
});
|
||||||
|
|
||||||
|
match PluginManager::new(
|
||||||
|
bundled_plugin_dir,
|
||||||
|
embedded_vendored_plugin_dir,
|
||||||
|
installed_plugin_dir,
|
||||||
|
node_bin_path,
|
||||||
|
plugin_runtime_main,
|
||||||
|
&query_manager,
|
||||||
|
&PluginContext::new_empty(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(plugin_manager) => Some(Arc::new(plugin_manager)),
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Warning: Failed to initialize plugins: {err}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -113,3 +121,34 @@ impl CliContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prepare_embedded_plugin_runtime(data_dir: &Path) -> std::io::Result<PathBuf> {
|
||||||
|
let runtime_dir = data_dir.join("vendored").join("plugin-runtime");
|
||||||
|
fs::create_dir_all(&runtime_dir)?;
|
||||||
|
let runtime_main = runtime_dir.join("index.cjs");
|
||||||
|
fs::write(&runtime_main, EMBEDDED_PLUGIN_RUNTIME)?;
|
||||||
|
Ok(runtime_main)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_embedded_vendored_plugins(vendored_plugin_dir: &Path) -> std::io::Result<()> {
|
||||||
|
fs::create_dir_all(vendored_plugin_dir)?;
|
||||||
|
EMBEDDED_VENDORED_PLUGINS.extract(vendored_plugin_dir)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_bundled_plugin_dir_for_cli(embedded_vendored_plugin_dir: &Path) -> PathBuf {
|
||||||
|
if !cfg!(debug_assertions) {
|
||||||
|
return embedded_vendored_plugin_dir.to_path_buf();
|
||||||
|
}
|
||||||
|
|
||||||
|
let plugins_dir = match std::env::current_dir() {
|
||||||
|
Ok(cwd) => cwd.join("plugins"),
|
||||||
|
Err(_) => return embedded_vendored_plugin_dir.to_path_buf(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !plugins_dir.is_dir() {
|
||||||
|
return embedded_vendored_plugin_dir.to_path_buf();
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins_dir.canonicalize().unwrap_or(plugins_dir)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ mod context;
|
|||||||
mod plugin_events;
|
mod plugin_events;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
mod version;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::{Cli, Commands, RequestCommands};
|
use cli::{Cli, Commands, RequestCommands};
|
||||||
@@ -11,10 +12,18 @@ use context::CliContext;
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let Cli { data_dir, environment, verbose, command } = Cli::parse();
|
let Cli { data_dir, environment, verbose, log, command } = Cli::parse();
|
||||||
|
|
||||||
if verbose {
|
if let Some(log_level) = log {
|
||||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
match log_level {
|
||||||
|
Some(level) => {
|
||||||
|
env_logger::Builder::new().filter_level(level.as_filter()).init();
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
|
||||||
|
.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let app_id = if cfg!(debug_assertions) { "app.yaak.desktop.dev" } else { "app.yaak.desktop" };
|
let app_id = if cfg!(debug_assertions) { "app.yaak.desktop.dev" } else { "app.yaak.desktop" };
|
||||||
|
|||||||
47
crates-cli/yaak-cli/src/utils/http.rs
Normal file
47
crates-cli/yaak-cli/src/utils/http.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use reqwest::Client;
|
||||||
|
use reqwest::header::{HeaderMap, HeaderName, HeaderValue, USER_AGENT};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
pub fn build_client(session_token: Option<&str>) -> Result<Client, String> {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
let user_agent = HeaderValue::from_str(&user_agent())
|
||||||
|
.map_err(|e| format!("Failed to build user-agent header: {e}"))?;
|
||||||
|
headers.insert(USER_AGENT, user_agent);
|
||||||
|
|
||||||
|
if let Some(token) = session_token {
|
||||||
|
let token_value = HeaderValue::from_str(token)
|
||||||
|
.map_err(|e| format!("Failed to build session header: {e}"))?;
|
||||||
|
headers.insert(HeaderName::from_static("x-yaak-session"), token_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Client::builder()
|
||||||
|
.default_headers(headers)
|
||||||
|
.build()
|
||||||
|
.map_err(|e| format!("Failed to initialize HTTP client: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_api_error(status: u16, body: &str) -> String {
|
||||||
|
if let Ok(value) = serde_json::from_str::<Value>(body) {
|
||||||
|
if let Some(message) = value.get("message").and_then(Value::as_str) {
|
||||||
|
return message.to_string();
|
||||||
|
}
|
||||||
|
if let Some(error) = value.get("error").and_then(Value::as_str) {
|
||||||
|
return error.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("API error {status}: {body}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_agent() -> String {
|
||||||
|
format!("YaakCli/{} ({})", crate::version::cli_version(), ua_platform())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ua_platform() -> &'static str {
|
||||||
|
match std::env::consts::OS {
|
||||||
|
"windows" => "Win",
|
||||||
|
"darwin" => "Mac",
|
||||||
|
"linux" => "Linux",
|
||||||
|
_ => "Unknown",
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
pub fn apply_merge_patch<T>(existing: &T, patch: &Value, id: &str, context: &str) -> JsonResult<T>
|
||||||
where
|
where
|
||||||
T: Serialize + DeserializeOwned,
|
T: Serialize + DeserializeOwned,
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
pub mod confirm;
|
pub mod confirm;
|
||||||
|
pub mod http;
|
||||||
pub mod json;
|
pub mod json;
|
||||||
|
pub mod schema;
|
||||||
|
|||||||
15
crates-cli/yaak-cli/src/utils/schema.rs
Normal file
15
crates-cli/yaak-cli/src/utils/schema.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
pub fn append_agent_hints(schema: &mut Value) {
|
||||||
|
let Some(schema_obj) = schema.as_object_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
schema_obj.insert(
|
||||||
|
"x-yaak-agent-hints".to_string(),
|
||||||
|
json!({
|
||||||
|
"templateVariableSyntax": "${[ my_var ]}",
|
||||||
|
"templateFunctionSyntax": "${[ namespace.my_func(a='aaa',b='bbb') ]}",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
3
crates-cli/yaak-cli/src/version.rs
Normal file
3
crates-cli/yaak-cli/src/version.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub fn cli_version() -> &'static str {
|
||||||
|
option_env!("YAAK_CLI_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"))
|
||||||
|
}
|
||||||
@@ -78,3 +78,69 @@ fn json_create_and_update_merge_patch_round_trip() {
|
|||||||
.stdout(contains("\"name\": \"Json Environment\""))
|
.stdout(contains("\"name\": \"Json Environment\""))
|
||||||
.stdout(contains("\"color\": \"#00ff00\""));
|
.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");
|
||||||
|
let data_dir = temp_dir.path();
|
||||||
|
|
||||||
|
cli_cmd(data_dir)
|
||||||
|
.args(["environment", "schema"])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(contains("\"type\":\"object\""))
|
||||||
|
.stdout(contains("\"x-yaak-agent-hints\""))
|
||||||
|
.stdout(contains("\"templateVariableSyntax\":\"${[ my_var ]}\""))
|
||||||
|
.stdout(contains(
|
||||||
|
"\"templateFunctionSyntax\":\"${[ namespace.my_func(a='aaa',b='bbb') ]}\"",
|
||||||
|
))
|
||||||
|
.stdout(contains("\"workspaceId\""));
|
||||||
|
}
|
||||||
|
|||||||
@@ -72,3 +72,51 @@ fn json_create_and_update_merge_patch_round_trip() {
|
|||||||
.stdout(contains("\"name\": \"Json Folder\""))
|
.stdout(contains("\"name\": \"Json Folder\""))
|
||||||
.stdout(contains("\"description\": \"Folder Description\""));
|
.stdout(contains("\"description\": \"Folder Description\""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_merges_positional_workspace_id_into_json_payload() {
|
||||||
|
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||||
|
let data_dir = temp_dir.path();
|
||||||
|
seed_workspace(data_dir, "wk_test");
|
||||||
|
|
||||||
|
let create_assert = cli_cmd(data_dir)
|
||||||
|
.args([
|
||||||
|
"folder",
|
||||||
|
"create",
|
||||||
|
"wk_test",
|
||||||
|
"--json",
|
||||||
|
r#"{"name":"Merged Folder"}"#,
|
||||||
|
])
|
||||||
|
.assert()
|
||||||
|
.success();
|
||||||
|
let folder_id = parse_created_id(&create_assert.get_output().stdout, "folder create");
|
||||||
|
|
||||||
|
cli_cmd(data_dir)
|
||||||
|
.args(["folder", "show", &folder_id])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(contains("\"workspaceId\": \"wk_test\""))
|
||||||
|
.stdout(contains("\"name\": \"Merged Folder\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_rejects_conflicting_workspace_ids_between_arg_and_json() {
|
||||||
|
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||||
|
let data_dir = temp_dir.path();
|
||||||
|
seed_workspace(data_dir, "wk_test");
|
||||||
|
seed_workspace(data_dir, "wk_other");
|
||||||
|
|
||||||
|
cli_cmd(data_dir)
|
||||||
|
.args([
|
||||||
|
"folder",
|
||||||
|
"create",
|
||||||
|
"wk_test",
|
||||||
|
"--json",
|
||||||
|
r#"{"workspaceId":"wk_other","name":"Mismatch"}"#,
|
||||||
|
])
|
||||||
|
.assert()
|
||||||
|
.failure()
|
||||||
|
.stderr(contains(
|
||||||
|
"folder create got conflicting workspace_id values between positional arg and JSON payload",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|||||||
@@ -130,6 +130,54 @@ fn create_allows_workspace_only_with_empty_defaults() {
|
|||||||
assert_eq!(request.url, "");
|
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]
|
#[test]
|
||||||
fn request_send_persists_response_body_and_events() {
|
fn request_send_persists_response_body_and_events() {
|
||||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||||
@@ -156,7 +204,6 @@ fn request_send_persists_response_body_and_events() {
|
|||||||
.args(["request", "send", &request_id])
|
.args(["request", "send", &request_id])
|
||||||
.assert()
|
.assert()
|
||||||
.success()
|
.success()
|
||||||
.stdout(contains("HTTP 200 OK"))
|
|
||||||
.stdout(contains("hello from integration test"));
|
.stdout(contains("hello from integration test"));
|
||||||
|
|
||||||
let qm = query_manager(data_dir);
|
let qm = query_manager(data_dir);
|
||||||
@@ -189,6 +236,26 @@ fn request_schema_http_outputs_json_schema() {
|
|||||||
.args(["request", "schema", "http"])
|
.args(["request", "schema", "http"])
|
||||||
.assert()
|
.assert()
|
||||||
.success()
|
.success()
|
||||||
|
.stdout(contains("\"type\":\"object\""))
|
||||||
|
.stdout(contains("\"x-yaak-agent-hints\""))
|
||||||
|
.stdout(contains("\"templateVariableSyntax\":\"${[ my_var ]}\""))
|
||||||
|
.stdout(contains(
|
||||||
|
"\"templateFunctionSyntax\":\"${[ namespace.my_func(a='aaa',b='bbb') ]}\"",
|
||||||
|
))
|
||||||
|
.stdout(contains("\"authentication\":"))
|
||||||
|
.stdout(contains("/foo/:id/comments/:commentId"))
|
||||||
|
.stdout(contains("put concrete values in `urlParameters`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_schema_http_pretty_prints_with_flag() {
|
||||||
|
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||||
|
let data_dir = temp_dir.path();
|
||||||
|
|
||||||
|
cli_cmd(data_dir)
|
||||||
|
.args(["request", "schema", "http", "--pretty"])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
.stdout(contains("\"type\": \"object\""))
|
.stdout(contains("\"type\": \"object\""))
|
||||||
.stdout(contains("\"authentication\""));
|
.stdout(contains("\"authentication\""));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ fn top_level_send_workspace_sends_http_requests_and_prints_summary() {
|
|||||||
.args(["send", "wk_test"])
|
.args(["send", "wk_test"])
|
||||||
.assert()
|
.assert()
|
||||||
.success()
|
.success()
|
||||||
.stdout(contains("HTTP 200 OK"))
|
|
||||||
.stdout(contains("workspace bulk send"))
|
.stdout(contains("workspace bulk send"))
|
||||||
.stdout(contains("Send summary: 1 succeeded, 0 failed"));
|
.stdout(contains("Send summary: 1 succeeded, 0 failed"));
|
||||||
}
|
}
|
||||||
@@ -62,7 +61,6 @@ fn top_level_send_folder_sends_http_requests_and_prints_summary() {
|
|||||||
.args(["send", "fl_test"])
|
.args(["send", "fl_test"])
|
||||||
.assert()
|
.assert()
|
||||||
.success()
|
.success()
|
||||||
.stdout(contains("HTTP 200 OK"))
|
|
||||||
.stdout(contains("folder bulk send"))
|
.stdout(contains("folder bulk send"))
|
||||||
.stdout(contains("Send summary: 1 succeeded, 0 failed"));
|
.stdout(contains("Send summary: 1 succeeded, 0 failed"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,3 +57,19 @@ fn json_create_and_update_merge_patch_round_trip() {
|
|||||||
.stdout(contains("\"name\": \"Json Workspace\""))
|
.stdout(contains("\"name\": \"Json Workspace\""))
|
||||||
.stdout(contains("\"description\": \"Updated via JSON\""));
|
.stdout(contains("\"description\": \"Updated via JSON\""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_schema_outputs_json_schema() {
|
||||||
|
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||||
|
let data_dir = temp_dir.path();
|
||||||
|
|
||||||
|
cli_cmd(data_dir)
|
||||||
|
.args(["workspace", "schema"])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(contains("\"type\":\"object\""))
|
||||||
|
.stdout(contains("\"x-yaak-agent-hints\""))
|
||||||
|
.stdout(contains("\"templateVariableSyntax\":\"${[ my_var ]}\""))
|
||||||
|
.stdout(contains("\"templateFunctionSyntax\":\"${[ namespace.my_func(a='aaa',b='bbb') ]}\""))
|
||||||
|
.stdout(contains("\"name\""));
|
||||||
|
}
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ async fn send_http_request_inner<R: Runtime>(
|
|||||||
cookie_jar_id,
|
cookie_jar_id,
|
||||||
response_dir: &response_dir,
|
response_dir: &response_dir,
|
||||||
emit_events_to: None,
|
emit_events_to: None,
|
||||||
|
emit_response_body_chunks_to: None,
|
||||||
existing_response: Some(response_ctx.response().clone()),
|
existing_response: Some(response_ctx.response().clone()),
|
||||||
plugin_manager,
|
plugin_manager,
|
||||||
encryption_manager,
|
encryption_manager,
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ async fn handle_host_plugin_request<R: Runtime>(
|
|||||||
workspace_id: http_request.workspace_id.clone(),
|
workspace_id: http_request.workspace_id.clone(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
&UpdateSource::Plugin,
|
&UpdateSource::from_window_label(window.label()),
|
||||||
&blobs,
|
&blobs,
|
||||||
)?
|
)?
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use crate::error::Result;
|
|||||||
use crate::models_ext::QueryManagerExt;
|
use crate::models_ext::QueryManagerExt;
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
@@ -23,12 +24,11 @@ use tokio::sync::Mutex;
|
|||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use yaak_api::yaak_api_client;
|
use yaak_api::yaak_api_client;
|
||||||
use yaak_models::models::Plugin;
|
use yaak_models::models::Plugin;
|
||||||
use yaak_models::util::UpdateSource;
|
|
||||||
use yaak_plugins::api::{
|
use yaak_plugins::api::{
|
||||||
PluginNameVersion, PluginSearchResponse, PluginUpdatesResponse, check_plugin_updates,
|
PluginNameVersion, PluginSearchResponse, PluginUpdatesResponse, check_plugin_updates,
|
||||||
search_plugins,
|
search_plugins,
|
||||||
};
|
};
|
||||||
use yaak_plugins::events::{Color, Icon, PluginContext, ShowToastRequest};
|
use yaak_plugins::events::PluginContext;
|
||||||
use yaak_plugins::install::{delete_and_uninstall, download_and_install};
|
use yaak_plugins::install::{delete_and_uninstall, download_and_install};
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
use yaak_plugins::plugin_meta::get_plugin_meta;
|
use yaak_plugins::plugin_meta::get_plugin_meta;
|
||||||
@@ -244,6 +244,11 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
|||||||
.path()
|
.path()
|
||||||
.resolve("vendored/plugins", BaseDirectory::Resource)
|
.resolve("vendored/plugins", BaseDirectory::Resource)
|
||||||
.expect("failed to resolve plugin directory resource");
|
.expect("failed to resolve plugin directory resource");
|
||||||
|
let bundled_plugin_dir = if is_dev() {
|
||||||
|
resolve_workspace_plugins_dir().unwrap_or_else(|| vendored_plugin_dir.clone())
|
||||||
|
} else {
|
||||||
|
vendored_plugin_dir.clone()
|
||||||
|
};
|
||||||
|
|
||||||
let installed_plugin_dir = app_handle
|
let installed_plugin_dir = app_handle
|
||||||
.path()
|
.path()
|
||||||
@@ -267,63 +272,23 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
|||||||
.expect("failed to resolve plugin runtime")
|
.expect("failed to resolve plugin runtime")
|
||||||
.join("index.cjs");
|
.join("index.cjs");
|
||||||
|
|
||||||
let dev_mode = is_dev();
|
let query_manager =
|
||||||
|
app_handle.state::<yaak_models::query_manager::QueryManager>().inner().clone();
|
||||||
|
|
||||||
// Create plugin manager asynchronously
|
// Create plugin manager asynchronously
|
||||||
let app_handle_clone = app_handle.clone();
|
let app_handle_clone = app_handle.clone();
|
||||||
tauri::async_runtime::block_on(async move {
|
tauri::async_runtime::block_on(async move {
|
||||||
let manager = PluginManager::new(
|
let manager = PluginManager::new(
|
||||||
|
bundled_plugin_dir,
|
||||||
vendored_plugin_dir,
|
vendored_plugin_dir,
|
||||||
installed_plugin_dir,
|
installed_plugin_dir,
|
||||||
node_bin_path,
|
node_bin_path,
|
||||||
plugin_runtime_main,
|
plugin_runtime_main,
|
||||||
dev_mode,
|
&query_manager,
|
||||||
|
&PluginContext::new_empty(),
|
||||||
)
|
)
|
||||||
.await;
|
.await
|
||||||
|
.expect("Failed to initialize plugins");
|
||||||
// Initialize all plugins after manager is created
|
|
||||||
let bundled_dirs = manager
|
|
||||||
.list_bundled_plugin_dirs()
|
|
||||||
.await
|
|
||||||
.expect("Failed to list bundled plugins");
|
|
||||||
|
|
||||||
// Ensure all bundled plugins make it into the database
|
|
||||||
let db = app_handle_clone.db();
|
|
||||||
for dir in &bundled_dirs {
|
|
||||||
if db.get_plugin_by_directory(dir).is_none() {
|
|
||||||
db.upsert_plugin(
|
|
||||||
&Plugin {
|
|
||||||
directory: dir.clone(),
|
|
||||||
enabled: true,
|
|
||||||
url: None,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
&UpdateSource::Background,
|
|
||||||
)
|
|
||||||
.expect("Failed to upsert bundled plugin");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all plugins from database and initialize
|
|
||||||
let plugins = db.list_plugins().expect("Failed to list plugins from database");
|
|
||||||
drop(db); // Explicitly drop the connection before await
|
|
||||||
|
|
||||||
let errors =
|
|
||||||
manager.initialize_all_plugins(plugins, &PluginContext::new_empty()).await;
|
|
||||||
|
|
||||||
// Show toast for any failed plugins
|
|
||||||
for (plugin_dir, error_msg) in errors {
|
|
||||||
let plugin_name = plugin_dir.split('/').last().unwrap_or(&plugin_dir);
|
|
||||||
let toast = ShowToastRequest {
|
|
||||||
message: format!("Failed to start plugin '{}': {}", plugin_name, error_msg),
|
|
||||||
color: Some(Color::Danger),
|
|
||||||
icon: Some(Icon::AlertTriangle),
|
|
||||||
timeout: Some(10000),
|
|
||||||
};
|
|
||||||
if let Err(emit_err) = app_handle_clone.emit("show_toast", toast) {
|
|
||||||
error!("Failed to emit toast for plugin error: {emit_err:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app_handle_clone.manage(manager);
|
app_handle_clone.manage(manager);
|
||||||
});
|
});
|
||||||
@@ -362,3 +327,11 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
|||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_workspace_plugins_dir() -> Option<PathBuf> {
|
||||||
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("../..")
|
||||||
|
.join("plugins")
|
||||||
|
.canonicalize()
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ impl HttpConnectionManager {
|
|||||||
connections.retain(|_, (_, last_used)| last_used.elapsed() <= self.ttl);
|
connections.retain(|_, (_, last_used)| last_used.elapsed() <= self.ttl);
|
||||||
|
|
||||||
if let Some((cached, last_used)) = connections.get_mut(&id) {
|
if let Some((cached, last_used)) = connections.get_mut(&id) {
|
||||||
info!("Re-using HTTP client {id}");
|
|
||||||
*last_used = Instant::now();
|
*last_used = Instant::now();
|
||||||
return Ok(CachedClient {
|
return Ok(CachedClient {
|
||||||
client: cached.client.clone(),
|
client: cached.client.clone(),
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ pub struct ClientCertificate {
|
|||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export, export_to = "gen_models.ts")]
|
#[ts(export, export_to = "gen_models.ts")]
|
||||||
pub struct DnsOverride {
|
pub struct DnsOverride {
|
||||||
@@ -293,7 +293,7 @@ impl UpsertModelInfo for Settings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema, TS)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
#[ts(export, export_to = "gen_models.ts")]
|
#[ts(export, export_to = "gen_models.ts")]
|
||||||
#[enum_def(table_name = "workspaces")]
|
#[enum_def(table_name = "workspaces")]
|
||||||
@@ -590,7 +590,7 @@ impl UpsertModelInfo for CookieJar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema, TS)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
#[ts(export, export_to = "gen_models.ts")]
|
#[ts(export, export_to = "gen_models.ts")]
|
||||||
#[enum_def(table_name = "environments")]
|
#[enum_def(table_name = "environments")]
|
||||||
@@ -611,6 +611,8 @@ pub struct Environment {
|
|||||||
pub base: bool,
|
pub base: bool,
|
||||||
pub parent_model: String,
|
pub parent_model: String,
|
||||||
pub parent_id: Option<String>,
|
pub parent_id: Option<String>,
|
||||||
|
/// Variables defined in this environment scope.
|
||||||
|
/// Child environments override parent variables by name.
|
||||||
pub variables: Vec<EnvironmentVariable>,
|
pub variables: Vec<EnvironmentVariable>,
|
||||||
pub color: Option<String>,
|
pub color: Option<String>,
|
||||||
pub sort_priority: f64,
|
pub sort_priority: f64,
|
||||||
@@ -698,7 +700,7 @@ impl UpsertModelInfo for Environment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema, TS)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
#[ts(export, export_to = "gen_models.ts")]
|
#[ts(export, export_to = "gen_models.ts")]
|
||||||
pub struct EnvironmentVariable {
|
pub struct EnvironmentVariable {
|
||||||
@@ -845,6 +847,8 @@ pub struct HttpUrlParameter {
|
|||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
#[ts(optional, as = "Option<bool>")]
|
#[ts(optional, as = "Option<bool>")]
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
/// Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
|
||||||
|
/// Other entries are appended as query parameters
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
#[ts(optional, as = "Option<String>")]
|
#[ts(optional, as = "Option<String>")]
|
||||||
@@ -877,6 +881,7 @@ pub struct HttpRequest {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub sort_priority: f64,
|
pub sort_priority: f64,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
/// URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||||
pub url_parameters: Vec<HttpUrlParameter>,
|
pub url_parameters: Vec<HttpUrlParameter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1118,6 +1123,7 @@ pub struct WebsocketRequest {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub sort_priority: f64,
|
pub sort_priority: f64,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
/// URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||||
pub url_parameters: Vec<HttpUrlParameter>,
|
pub url_parameters: Vec<HttpUrlParameter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1728,6 +1734,7 @@ pub struct GrpcRequest {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub service: Option<String>,
|
pub service: Option<String>,
|
||||||
pub sort_priority: f64,
|
pub sort_priority: f64,
|
||||||
|
/// Server URL (http for plaintext or https for secure)
|
||||||
pub url: String,
|
pub url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ use crate::plugin_handle::PluginHandle;
|
|||||||
use crate::server_ws::PluginRuntimeServerWebsocket;
|
use crate::server_ws::PluginRuntimeServerWebsocket;
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -34,7 +33,8 @@ use tokio::sync::mpsc::error::TrySendError;
|
|||||||
use tokio::sync::{Mutex, mpsc, oneshot};
|
use tokio::sync::{Mutex, mpsc, oneshot};
|
||||||
use tokio::time::{Instant, timeout};
|
use tokio::time::{Instant, timeout};
|
||||||
use yaak_models::models::Plugin;
|
use yaak_models::models::Plugin;
|
||||||
use yaak_models::util::generate_id;
|
use yaak_models::query_manager::QueryManager;
|
||||||
|
use yaak_models::util::{UpdateSource, generate_id};
|
||||||
use yaak_templates::error::Error::RenderError;
|
use yaak_templates::error::Error::RenderError;
|
||||||
use yaak_templates::error::Result as TemplateResult;
|
use yaak_templates::error::Result as TemplateResult;
|
||||||
|
|
||||||
@@ -45,9 +45,9 @@ pub struct PluginManager {
|
|||||||
kill_tx: tokio::sync::watch::Sender<bool>,
|
kill_tx: tokio::sync::watch::Sender<bool>,
|
||||||
killed_rx: Arc<Mutex<Option<oneshot::Receiver<()>>>>,
|
killed_rx: Arc<Mutex<Option<oneshot::Receiver<()>>>>,
|
||||||
ws_service: Arc<PluginRuntimeServerWebsocket>,
|
ws_service: Arc<PluginRuntimeServerWebsocket>,
|
||||||
|
bundled_plugin_dir: PathBuf,
|
||||||
vendored_plugin_dir: PathBuf,
|
vendored_plugin_dir: PathBuf,
|
||||||
pub(crate) installed_plugin_dir: PathBuf,
|
pub(crate) installed_plugin_dir: PathBuf,
|
||||||
dev_mode: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Callback for plugin initialization events (e.g., toast notifications)
|
/// Callback for plugin initialization events (e.g., toast notifications)
|
||||||
@@ -57,18 +57,22 @@ impl PluginManager {
|
|||||||
/// Create a new PluginManager with the given paths.
|
/// Create a new PluginManager with the given paths.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
/// * `bundled_plugin_dir` - Directory to scan for bundled plugins
|
||||||
/// * `vendored_plugin_dir` - Path to vendored plugins directory
|
/// * `vendored_plugin_dir` - Path to vendored plugins directory
|
||||||
/// * `installed_plugin_dir` - Path to installed plugins directory
|
/// * `installed_plugin_dir` - Path to installed plugins directory
|
||||||
/// * `node_bin_path` - Path to the yaaknode binary
|
/// * `node_bin_path` - Path to the yaaknode binary
|
||||||
/// * `plugin_runtime_main` - Path to the plugin runtime index.cjs
|
/// * `plugin_runtime_main` - Path to the plugin runtime index.cjs
|
||||||
/// * `dev_mode` - Whether the app is in dev mode (affects plugin loading)
|
/// * `query_manager` - Query manager for bundled plugin registration and loading
|
||||||
|
/// * `plugin_context` - Context to use while initializing plugins
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
|
bundled_plugin_dir: PathBuf,
|
||||||
vendored_plugin_dir: PathBuf,
|
vendored_plugin_dir: PathBuf,
|
||||||
installed_plugin_dir: PathBuf,
|
installed_plugin_dir: PathBuf,
|
||||||
node_bin_path: PathBuf,
|
node_bin_path: PathBuf,
|
||||||
plugin_runtime_main: PathBuf,
|
plugin_runtime_main: PathBuf,
|
||||||
dev_mode: bool,
|
query_manager: &QueryManager,
|
||||||
) -> PluginManager {
|
plugin_context: &PluginContext,
|
||||||
|
) -> Result<PluginManager> {
|
||||||
let (events_tx, mut events_rx) = mpsc::channel(2048);
|
let (events_tx, mut events_rx) = mpsc::channel(2048);
|
||||||
let (kill_server_tx, kill_server_rx) = tokio::sync::watch::channel(false);
|
let (kill_server_tx, kill_server_rx) = tokio::sync::watch::channel(false);
|
||||||
let (killed_tx, killed_rx) = oneshot::channel();
|
let (killed_tx, killed_rx) = oneshot::channel();
|
||||||
@@ -84,9 +88,9 @@ impl PluginManager {
|
|||||||
ws_service: Arc::new(ws_service.clone()),
|
ws_service: Arc::new(ws_service.clone()),
|
||||||
kill_tx: kill_server_tx,
|
kill_tx: kill_server_tx,
|
||||||
killed_rx: Arc::new(Mutex::new(Some(killed_rx))),
|
killed_rx: Arc::new(Mutex::new(Some(killed_rx))),
|
||||||
|
bundled_plugin_dir,
|
||||||
vendored_plugin_dir,
|
vendored_plugin_dir,
|
||||||
installed_plugin_dir,
|
installed_plugin_dir,
|
||||||
dev_mode,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Forward events to subscribers
|
// Forward events to subscribers
|
||||||
@@ -151,33 +155,47 @@ impl PluginManager {
|
|||||||
&kill_server_rx,
|
&kill_server_rx,
|
||||||
killed_tx,
|
killed_tx,
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
.unwrap();
|
|
||||||
info!("Waiting for plugins to initialize");
|
info!("Waiting for plugins to initialize");
|
||||||
init_plugins_task.await.unwrap();
|
init_plugins_task.await.map_err(|e| PluginErr(e.to_string()))?;
|
||||||
|
|
||||||
plugin_manager
|
let bundled_dirs = plugin_manager.list_bundled_plugin_dirs().await?;
|
||||||
}
|
let db = query_manager.connect();
|
||||||
|
for dir in bundled_dirs {
|
||||||
/// Get the vendored plugin directory path (resolves dev mode path if applicable)
|
if db.get_plugin_by_directory(&dir).is_none() {
|
||||||
pub fn get_plugins_dir(&self) -> PathBuf {
|
db.upsert_plugin(
|
||||||
if self.dev_mode {
|
&Plugin {
|
||||||
// Use plugins directly for easy development
|
directory: dir,
|
||||||
// Tauri runs from crates-tauri/yaak-app/, so go up two levels to reach project root
|
enabled: true,
|
||||||
env::current_dir()
|
url: None,
|
||||||
.map(|cwd| cwd.join("../../plugins").canonicalize().unwrap())
|
..Default::default()
|
||||||
.unwrap_or_else(|_| self.vendored_plugin_dir.clone())
|
},
|
||||||
} else {
|
&UpdateSource::Background,
|
||||||
self.vendored_plugin_dir.clone()
|
)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let plugins = db.list_plugins()?;
|
||||||
|
drop(db);
|
||||||
|
|
||||||
|
let init_errors = plugin_manager.initialize_all_plugins(plugins, plugin_context).await;
|
||||||
|
if !init_errors.is_empty() {
|
||||||
|
let joined = init_errors
|
||||||
|
.into_iter()
|
||||||
|
.map(|(dir, err)| format!("{dir}: {err}"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("; ");
|
||||||
|
return Err(PluginErr(format!("Failed to initialize plugin(s): {joined}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(plugin_manager)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read plugin directories from disk and return their paths.
|
/// Read plugin directories from disk and return their paths.
|
||||||
/// This is useful for discovering bundled plugins.
|
/// This is useful for discovering bundled plugins.
|
||||||
pub async fn list_bundled_plugin_dirs(&self) -> Result<Vec<String>> {
|
pub async fn list_bundled_plugin_dirs(&self) -> Result<Vec<String>> {
|
||||||
let plugins_dir = self.get_plugins_dir();
|
info!("Loading bundled plugins from {:?}", self.bundled_plugin_dir);
|
||||||
info!("Loading bundled plugins from {plugins_dir:?}");
|
read_plugins_dir(&self.bundled_plugin_dir).await
|
||||||
read_plugins_dir(&plugins_dir).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn uninstall(&self, plugin_context: &PluginContext, dir: &str) -> Result<()> {
|
pub async fn uninstall(&self, plugin_context: &PluginContext, dir: &str) -> Result<()> {
|
||||||
|
|||||||
@@ -273,6 +273,5 @@ pub fn find_client_certificate(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("No matching client certificate found for {}", url_string);
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,6 +239,7 @@ pub struct SendHttpRequestByIdParams<'a, T: TemplateCallback> {
|
|||||||
pub cookie_jar_id: Option<String>,
|
pub cookie_jar_id: Option<String>,
|
||||||
pub response_dir: &'a Path,
|
pub response_dir: &'a Path,
|
||||||
pub emit_events_to: Option<mpsc::Sender<SenderHttpResponseEvent>>,
|
pub emit_events_to: Option<mpsc::Sender<SenderHttpResponseEvent>>,
|
||||||
|
pub emit_response_body_chunks_to: Option<mpsc::UnboundedSender<Vec<u8>>>,
|
||||||
pub cancelled_rx: Option<watch::Receiver<bool>>,
|
pub cancelled_rx: Option<watch::Receiver<bool>>,
|
||||||
pub prepare_sendable_request: Option<&'a dyn PrepareSendableRequest>,
|
pub prepare_sendable_request: Option<&'a dyn PrepareSendableRequest>,
|
||||||
pub executor: Option<&'a dyn SendRequestExecutor>,
|
pub executor: Option<&'a dyn SendRequestExecutor>,
|
||||||
@@ -255,6 +256,7 @@ pub struct SendHttpRequestParams<'a, T: TemplateCallback> {
|
|||||||
pub cookie_jar_id: Option<String>,
|
pub cookie_jar_id: Option<String>,
|
||||||
pub response_dir: &'a Path,
|
pub response_dir: &'a Path,
|
||||||
pub emit_events_to: Option<mpsc::Sender<SenderHttpResponseEvent>>,
|
pub emit_events_to: Option<mpsc::Sender<SenderHttpResponseEvent>>,
|
||||||
|
pub emit_response_body_chunks_to: Option<mpsc::UnboundedSender<Vec<u8>>>,
|
||||||
pub cancelled_rx: Option<watch::Receiver<bool>>,
|
pub cancelled_rx: Option<watch::Receiver<bool>>,
|
||||||
pub auth_context_id: Option<String>,
|
pub auth_context_id: Option<String>,
|
||||||
pub existing_response: Option<HttpResponse>,
|
pub existing_response: Option<HttpResponse>,
|
||||||
@@ -271,6 +273,7 @@ pub struct SendHttpRequestWithPluginsParams<'a> {
|
|||||||
pub cookie_jar_id: Option<String>,
|
pub cookie_jar_id: Option<String>,
|
||||||
pub response_dir: &'a Path,
|
pub response_dir: &'a Path,
|
||||||
pub emit_events_to: Option<mpsc::Sender<SenderHttpResponseEvent>>,
|
pub emit_events_to: Option<mpsc::Sender<SenderHttpResponseEvent>>,
|
||||||
|
pub emit_response_body_chunks_to: Option<mpsc::UnboundedSender<Vec<u8>>>,
|
||||||
pub existing_response: Option<HttpResponse>,
|
pub existing_response: Option<HttpResponse>,
|
||||||
pub plugin_manager: Arc<PluginManager>,
|
pub plugin_manager: Arc<PluginManager>,
|
||||||
pub encryption_manager: Arc<EncryptionManager>,
|
pub encryption_manager: Arc<EncryptionManager>,
|
||||||
@@ -288,6 +291,7 @@ pub struct SendHttpRequestByIdWithPluginsParams<'a> {
|
|||||||
pub cookie_jar_id: Option<String>,
|
pub cookie_jar_id: Option<String>,
|
||||||
pub response_dir: &'a Path,
|
pub response_dir: &'a Path,
|
||||||
pub emit_events_to: Option<mpsc::Sender<SenderHttpResponseEvent>>,
|
pub emit_events_to: Option<mpsc::Sender<SenderHttpResponseEvent>>,
|
||||||
|
pub emit_response_body_chunks_to: Option<mpsc::UnboundedSender<Vec<u8>>>,
|
||||||
pub plugin_manager: Arc<PluginManager>,
|
pub plugin_manager: Arc<PluginManager>,
|
||||||
pub encryption_manager: Arc<EncryptionManager>,
|
pub encryption_manager: Arc<EncryptionManager>,
|
||||||
pub plugin_context: &'a PluginContext,
|
pub plugin_context: &'a PluginContext,
|
||||||
@@ -353,6 +357,7 @@ pub async fn send_http_request_by_id_with_plugins(
|
|||||||
cookie_jar_id: params.cookie_jar_id,
|
cookie_jar_id: params.cookie_jar_id,
|
||||||
response_dir: params.response_dir,
|
response_dir: params.response_dir,
|
||||||
emit_events_to: params.emit_events_to,
|
emit_events_to: params.emit_events_to,
|
||||||
|
emit_response_body_chunks_to: params.emit_response_body_chunks_to,
|
||||||
existing_response: None,
|
existing_response: None,
|
||||||
plugin_manager: params.plugin_manager,
|
plugin_manager: params.plugin_manager,
|
||||||
encryption_manager: params.encryption_manager,
|
encryption_manager: params.encryption_manager,
|
||||||
@@ -397,6 +402,7 @@ pub async fn send_http_request_with_plugins(
|
|||||||
cookie_jar_id: params.cookie_jar_id,
|
cookie_jar_id: params.cookie_jar_id,
|
||||||
response_dir: params.response_dir,
|
response_dir: params.response_dir,
|
||||||
emit_events_to: params.emit_events_to,
|
emit_events_to: params.emit_events_to,
|
||||||
|
emit_response_body_chunks_to: params.emit_response_body_chunks_to,
|
||||||
cancelled_rx: params.cancelled_rx,
|
cancelled_rx: params.cancelled_rx,
|
||||||
auth_context_id: None,
|
auth_context_id: None,
|
||||||
existing_response: params.existing_response,
|
existing_response: params.existing_response,
|
||||||
@@ -427,6 +433,7 @@ pub async fn send_http_request_by_id<T: TemplateCallback>(
|
|||||||
cookie_jar_id: params.cookie_jar_id,
|
cookie_jar_id: params.cookie_jar_id,
|
||||||
response_dir: params.response_dir,
|
response_dir: params.response_dir,
|
||||||
emit_events_to: params.emit_events_to,
|
emit_events_to: params.emit_events_to,
|
||||||
|
emit_response_body_chunks_to: params.emit_response_body_chunks_to,
|
||||||
cancelled_rx: params.cancelled_rx,
|
cancelled_rx: params.cancelled_rx,
|
||||||
existing_response: None,
|
existing_response: None,
|
||||||
prepare_sendable_request: params.prepare_sendable_request,
|
prepare_sendable_request: params.prepare_sendable_request,
|
||||||
@@ -687,13 +694,17 @@ pub async fn send_http_request<T: TemplateCallback>(
|
|||||||
Ok(n) => {
|
Ok(n) => {
|
||||||
written_bytes += n;
|
written_bytes += n;
|
||||||
let start_idx = response_body.len() - n;
|
let start_idx = response_body.len() - n;
|
||||||
file.write_all(&response_body[start_idx..]).await.map_err(|source| {
|
let chunk = &response_body[start_idx..];
|
||||||
|
file.write_all(chunk).await.map_err(|source| {
|
||||||
SendHttpRequestError::WriteResponseBody { path: body_path.clone(), source }
|
SendHttpRequestError::WriteResponseBody { path: body_path.clone(), source }
|
||||||
})?;
|
})?;
|
||||||
file.flush().await.map_err(|source| SendHttpRequestError::WriteResponseBody {
|
file.flush().await.map_err(|source| SendHttpRequestError::WriteResponseBody {
|
||||||
path: body_path.clone(),
|
path: body_path.clone(),
|
||||||
source,
|
source,
|
||||||
})?;
|
})?;
|
||||||
|
if let Some(tx) = params.emit_response_body_chunks_to.as_ref() {
|
||||||
|
let _ = tx.send(chunk.to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let should_update = now.duration_since(last_progress_update).as_millis()
|
let should_update = now.duration_since(last_progress_update).as_millis()
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
const fs = require("node:fs");
|
const fs = require("node:fs");
|
||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
|
|
||||||
const readme = path.join(__dirname, "..", "..", "README.md");
|
const cliReadme = path.join(__dirname, "..", "..", "crates-cli", "yaak-cli", "README.md");
|
||||||
fs.copyFileSync(readme, path.join(__dirname, "README.md"));
|
fs.copyFileSync(cliReadme, path.join(__dirname, "README.md"));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const { copyFileSync, existsSync, readFileSync, writeFileSync } = require("node:fs");
|
const { chmodSync, copyFileSync, existsSync, readFileSync, writeFileSync } = require("node:fs");
|
||||||
const { join } = require("node:path");
|
const { join } = require("node:path");
|
||||||
|
|
||||||
const version = process.env.YAAK_CLI_VERSION?.replace(/^v/, "");
|
const version = process.env.YAAK_CLI_VERSION?.replace(/^v/, "");
|
||||||
@@ -50,6 +50,9 @@ for (const { src, dest } of binaries) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
copyFileSync(src, dest);
|
copyFileSync(src, dest);
|
||||||
|
if (!dest.endsWith(".exe")) {
|
||||||
|
chmodSync(dest, 0o755);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const pkg of packages) {
|
for (const pkg of packages) {
|
||||||
|
|||||||
58
package-lock.json
generated
58
package-lock.json
generated
@@ -37,7 +37,6 @@
|
|||||||
"plugins/template-function-cookie",
|
"plugins/template-function-cookie",
|
||||||
"plugins/template-function-ctx",
|
"plugins/template-function-ctx",
|
||||||
"plugins/template-function-encode",
|
"plugins/template-function-encode",
|
||||||
"plugins/template-function-faker",
|
|
||||||
"plugins/template-function-fs",
|
"plugins/template-function-fs",
|
||||||
"plugins/template-function-hash",
|
"plugins/template-function-hash",
|
||||||
"plugins/template-function-json",
|
"plugins/template-function-json",
|
||||||
@@ -74,7 +73,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.3.13",
|
"@biomejs/biome": "^2.3.13",
|
||||||
"@tauri-apps/cli": "^2.9.6",
|
"@tauri-apps/cli": "^2.9.6",
|
||||||
"@yaakapp/cli": "^0.3.4",
|
"@yaakapp/cli": "^0.4.0",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"nodejs-file-downloader": "^4.13.0",
|
"nodejs-file-downloader": "^4.13.0",
|
||||||
@@ -4327,27 +4326,28 @@
|
|||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/@yaakapp/cli": {
|
"node_modules/@yaakapp/cli": {
|
||||||
"version": "0.3.4",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@yaakapp/cli/-/cli-0.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@yaakapp/cli/-/cli-0.4.0.tgz",
|
||||||
"integrity": "sha512-bSSL3noEfyoPC0M+bj34jbBZbB+gwYLCHL9cf6BYHgkRQKlHFpvN6z8M2jQZljb+CTQdHK0NzosmwHLpjMmAVA==",
|
"integrity": "sha512-8xnu2oFWlgV+xeIAHMuEgsqX6Sxq4UYrSH2WbafwDLbSep6fxpO74tiBH7xp4wakt/7Bcy9a2Q5R9nkAc1ZUdA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
|
"yaak": "bin/cli.js",
|
||||||
"yaakcli": "bin/cli.js"
|
"yaakcli": "bin/cli.js"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@yaakapp/cli-darwin-arm64": "0.3.4",
|
"@yaakapp/cli-darwin-arm64": "0.4.0",
|
||||||
"@yaakapp/cli-darwin-x64": "0.3.4",
|
"@yaakapp/cli-darwin-x64": "0.4.0",
|
||||||
"@yaakapp/cli-linux-arm64": "0.3.4",
|
"@yaakapp/cli-linux-arm64": "0.4.0",
|
||||||
"@yaakapp/cli-linux-x64": "0.3.4",
|
"@yaakapp/cli-linux-x64": "0.4.0",
|
||||||
"@yaakapp/cli-win32-arm64": "0.3.4",
|
"@yaakapp/cli-win32-arm64": "0.4.0",
|
||||||
"@yaakapp/cli-win32-x64": "0.3.4"
|
"@yaakapp/cli-win32-x64": "0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@yaakapp/cli-darwin-arm64": {
|
"node_modules/@yaakapp/cli-darwin-arm64": {
|
||||||
"version": "0.3.4",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@yaakapp/cli-darwin-arm64/-/cli-darwin-arm64-0.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@yaakapp/cli-darwin-arm64/-/cli-darwin-arm64-0.4.0.tgz",
|
||||||
"integrity": "sha512-iTohEO7XSVZwSvTgEQE9my3wGyWtTl1q8yfol7hHwVFTX7G8Geh8X2j2vVokHhj7J9OZL9jtYQWIsM1ekOHSEQ==",
|
"integrity": "sha512-bl8+VQNPMabXNGQCa7u6w0JGe3CmzYZPsGE8Q+5wGSxa3trGf1bmq/fMW5JXrMi1P7Laepnyad0TGGP/2C8uwQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -4358,9 +4358,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@yaakapp/cli-darwin-x64": {
|
"node_modules/@yaakapp/cli-darwin-x64": {
|
||||||
"version": "0.3.4",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@yaakapp/cli-darwin-x64/-/cli-darwin-x64-0.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@yaakapp/cli-darwin-x64/-/cli-darwin-x64-0.4.0.tgz",
|
||||||
"integrity": "sha512-gz7IcjFGKA0cCAum1Aq8kmVg7erYYSrZ9pliDw0NZyObjrBysJcsDXLodEU437u0pihtdCfoLsq3rsYYs8uwCA==",
|
"integrity": "sha512-R+ETXNBWvmA3W88ZoTk/JtG/PZaUb85y3SwBgMbwcgdhBVwNS/g+DbCspcTFI5zs8Txsf5VuiFU+dW9M9olZ6A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -4371,9 +4371,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@yaakapp/cli-linux-arm64": {
|
"node_modules/@yaakapp/cli-linux-arm64": {
|
||||||
"version": "0.3.4",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@yaakapp/cli-linux-arm64/-/cli-linux-arm64-0.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@yaakapp/cli-linux-arm64/-/cli-linux-arm64-0.4.0.tgz",
|
||||||
"integrity": "sha512-Yiwz8PBkXngmr0lTMW1pgy+F/kUISkzvqofdoBseXTrS/GDxoW3ILnG3If30LuIyWWPgqpuU+qKMtbVDzuncPQ==",
|
"integrity": "sha512-Pf7VyQf4r85FsI0qYnnst7URQF8/RxSZZj79cXLai0FnN3fDiypX4CmHx765bJxgfQZlBvqVmvPAaMW/TeiJEQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -4384,9 +4384,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@yaakapp/cli-linux-x64": {
|
"node_modules/@yaakapp/cli-linux-x64": {
|
||||||
"version": "0.3.4",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@yaakapp/cli-linux-x64/-/cli-linux-x64-0.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@yaakapp/cli-linux-x64/-/cli-linux-x64-0.4.0.tgz",
|
||||||
"integrity": "sha512-j7/r18UYNlFChDVU5N5ye3mmL+OR9Uu3LY72JxW+s/SyV69Bo8Griii75Wt19z/jj2ES8pxD+4IJq56VF3wJ7w==",
|
"integrity": "sha512-bYWWfHAIW81A+ydJChjH1Qo3+aihz9gFLh7/9MOa6CJgnC6H3V5cnapmh50Hddt9l5ic02aA1FB8ORQOXxb01A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -4397,9 +4397,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@yaakapp/cli-win32-arm64": {
|
"node_modules/@yaakapp/cli-win32-arm64": {
|
||||||
"version": "0.3.4",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@yaakapp/cli-win32-arm64/-/cli-win32-arm64-0.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@yaakapp/cli-win32-arm64/-/cli-win32-arm64-0.4.0.tgz",
|
||||||
"integrity": "sha512-OUSKOKrSnzrTAGW0c+2ZCwA4yhgw/bA+gyeTvpf7cELVuB0qooGkEcJ3lM7fPMKmUbFU0r+K/Ggq1QMUr7cJLQ==",
|
"integrity": "sha512-8X12xkyidyYZ5vtarZGFSYR6HJbUMFUsNxYPNQccnYJIY+soNkjJHOWDjaRvBzCbR8MLT9N04Y5PE/Jv20gXpA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -4410,9 +4410,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@yaakapp/cli-win32-x64": {
|
"node_modules/@yaakapp/cli-win32-x64": {
|
||||||
"version": "0.3.4",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@yaakapp/cli-win32-x64/-/cli-win32-x64-0.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@yaakapp/cli-win32-x64/-/cli-win32-x64-0.4.0.tgz",
|
||||||
"integrity": "sha512-sVYnW1rROLbzFUCyeZ++ibN+8gJS7FdPnBRHIE0KORfeI4e7Gw/aMUji2qpSZ1gt3DrAU95DDNjBkDvGBAgqag==",
|
"integrity": "sha512-wansfrCCycFcFclowQQxfsNLIAyATyqnnbITED5gUfUrBf8NFHrG0sWVCWlXUhHU7YvpmqL7CsdtlMkIGiZCPQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -70,7 +70,6 @@
|
|||||||
"app-dev": "node scripts/run-dev.mjs",
|
"app-dev": "node scripts/run-dev.mjs",
|
||||||
"migration": "node scripts/create-migration.cjs",
|
"migration": "node scripts/create-migration.cjs",
|
||||||
"build": "npm run --workspaces --if-present build",
|
"build": "npm run --workspaces --if-present build",
|
||||||
"build-plugins": "npm run --workspaces --if-present build",
|
|
||||||
"test": "npm run --workspaces --if-present test",
|
"test": "npm run --workspaces --if-present test",
|
||||||
"icons": "run-p icons:*",
|
"icons": "run-p icons:*",
|
||||||
"icons:dev": "tauri icon crates-tauri/yaak-app/icons/icon-dev.png --output crates-tauri/yaak-app/icons/dev",
|
"icons:dev": "tauri icon crates-tauri/yaak-app/icons/icon-dev.png --output crates-tauri/yaak-app/icons/dev",
|
||||||
@@ -98,7 +97,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.3.13",
|
"@biomejs/biome": "^2.3.13",
|
||||||
"@tauri-apps/cli": "^2.9.6",
|
"@tauri-apps/cli": "^2.9.6",
|
||||||
"@yaakapp/cli": "^0.3.4",
|
"@yaakapp/cli": "^0.4.0",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"nodejs-file-downloader": "^4.13.0",
|
"nodejs-file-downloader": "^4.13.0",
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
"build:copy-types": "run-p build:copy-types:*",
|
"build:copy-types": "run-p build:copy-types:*",
|
||||||
"build:copy-types:root": "cpy --flat ../../crates/yaak-plugins/bindings/*.ts ./src/bindings",
|
"build:copy-types:root": "cpy --flat ../../crates/yaak-plugins/bindings/*.ts ./src/bindings",
|
||||||
"build:copy-types:next": "cpy --flat ../../crates/yaak-plugins/bindings/serde_json/*.ts ./src/bindings/serde_json",
|
"build:copy-types:next": "cpy --flat ../../crates/yaak-plugins/bindings/serde_json/*.ts ./src/bindings/serde_json",
|
||||||
"publish": "npm publish",
|
|
||||||
"prepublishOnly": "npm run build"
|
"prepublishOnly": "npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@yaak/action-send-folder",
|
"name": "@yaak/action-send-folder",
|
||||||
"displayName": "Send All",
|
"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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/mountain-loop/yaak.git",
|
"url": "https://github.com/mountain-loop/yaak.git",
|
||||||
|
|||||||
@@ -14,22 +14,44 @@ export const plugin: PluginDefinition = {
|
|||||||
ctx.httpRequest.list(),
|
ctx.httpRequest.list(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Build a set of all folder IDs that are descendants of the target folder
|
// Build the send order to match tree ordering:
|
||||||
const folderIds = new Set<string>([targetFolder.id]);
|
// sort siblings by sortPriority then updatedAt, and traverse folders depth-first.
|
||||||
const addDescendants = (parentId: string) => {
|
const compareByOrder = (
|
||||||
for (const folder of allFolders) {
|
a: Pick<typeof allFolders[number], 'sortPriority' | 'updatedAt'>,
|
||||||
if (folder.folderId === parentId && !folderIds.has(folder.id)) {
|
b: Pick<typeof allFolders[number], 'sortPriority' | 'updatedAt'>,
|
||||||
folderIds.add(folder.id);
|
) => {
|
||||||
addDescendants(folder.id);
|
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);
|
collectRequests(targetFolder.id);
|
||||||
|
|
||||||
// Filter HTTP requests to those in the target folder or its descendants
|
|
||||||
const requestsToSend = allRequests.filter(
|
|
||||||
(req) => req.folderId != null && folderIds.has(req.folderId),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (requestsToSend.length === 0) {
|
if (requestsToSend.length === 0) {
|
||||||
await ctx.toast.show({
|
await ctx.toast.show({
|
||||||
@@ -40,7 +62,7 @@ export const plugin: PluginDefinition = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send each request sequentially
|
// Send requests sequentially in the calculated folder order.
|
||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
let errorCount = 0;
|
let errorCount = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ export const plugin: PluginDefinition = {
|
|||||||
name: 'header',
|
name: 'header',
|
||||||
label: 'Header Name',
|
label: 'Header Name',
|
||||||
async dynamic(ctx, args) {
|
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, {
|
const response = await getResponse(ctx, {
|
||||||
requestId: String(args.values.request || ''),
|
requestId: String(args.values.request || ''),
|
||||||
purpose: args.purpose,
|
purpose: args.purpose,
|
||||||
@@ -146,6 +150,10 @@ export const plugin: PluginDefinition = {
|
|||||||
label: 'JSONPath or XPath',
|
label: 'JSONPath or XPath',
|
||||||
placeholder: '$.books[0].id or /books[0]/id',
|
placeholder: '$.books[0].id or /books[0]/id',
|
||||||
dynamic: async (ctx, args) => {
|
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, {
|
const resp = await getResponse(ctx, {
|
||||||
requestId: String(args.values.request || ''),
|
requestId: String(args.values.request || ''),
|
||||||
purpose: 'preview',
|
purpose: 'preview',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const { readdirSync, cpSync, existsSync } = require('node:fs');
|
const { readdirSync, cpSync, existsSync, mkdirSync } = require('node:fs');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
|
|
||||||
const pluginsDir = path.join(__dirname, '..', 'plugins');
|
const pluginsDir = path.join(__dirname, '..', 'plugins');
|
||||||
@@ -24,6 +24,7 @@ for (const name of readdirSync(pluginsDir)) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const destDir = path.join(__dirname, '../crates-tauri/yaak-app/vendored/plugins/', name);
|
const destDir = path.join(__dirname, '../crates-tauri/yaak-app/vendored/plugins/', name);
|
||||||
|
mkdirSync(destDir, { recursive: true });
|
||||||
console.log(`Copying ${name} to ${destDir}`);
|
console.log(`Copying ${name} to ${destDir}`);
|
||||||
cpSync(path.join(dir, 'package.json'), path.join(destDir, 'package.json'));
|
cpSync(path.join(dir, 'package.json'), path.join(destDir, 'package.json'));
|
||||||
cpSync(path.join(dir, 'build'), path.join(destDir, 'build'), { recursive: true });
|
cpSync(path.join(dir, 'build'), path.join(destDir, 'build'), { recursive: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user