mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-27 05:44:02 +02:00
Move CLI facts out of the skill and into the CLI
The skill had grown into a reference manual: body type tables, auth strategy lists, a template function table, field-by-field OAuth 2.0 config. All of it goes stale, because the user's CLI version and their installed plugins decide what actually exists. On this machine a faker plugin contributes 274 template functions; the skill listed eleven, and got some names wrong. The CLI should carry that knowledge, so: - `yaak template-function list [filter]` and `template-function show <name>` report what the loaded plugins actually provide, the same way `request schema http` already merges in plugin auth strategies. - `yaak folder schema` now exists, so folder payloads are discoverable like every other model. Required deriving JsonSchema on Folder. - The request schema documents `bodyType` and `body` shapes, and states that setting a body type does not add a Content-Type header. The skill drops to a single 135-line SKILL.md that teaches the model, the workflows, and how to interrogate the CLI, and says outright that the CLI wins when the two disagree.
This commit is contained in:
@@ -11,256 +11,125 @@ description: >
|
|||||||
call, exercise, or smoke test an HTTP or REST endpoint, to save or organize
|
call, exercise, or smoke test an HTTP or REST endpoint, to save or organize
|
||||||
API requests for reuse, to set up API requests for manual testing, to add auth
|
API requests for reuse, to set up API requests for manual testing, to add auth
|
||||||
to a saved request, to turn an OpenAPI or Postman collection into runnable
|
to a saved request, to turn an OpenAPI or Postman collection into runnable
|
||||||
requests, or to run a saved request suite against staging versus production. Prefer this over one-off `curl` commands whenever
|
requests, or to run a saved request suite against staging versus production.
|
||||||
the requests should be saved, reused, shared, or run as a set.
|
Prefer this over one-off `curl` commands whenever the requests should be
|
||||||
|
saved, reused, shared, or run as a set.
|
||||||
allowed-tools: Bash(yaak:*), Bash(which:*), Bash(command:*), Bash(npm:*), Bash(npx:*)
|
allowed-tools: Bash(yaak:*), Bash(which:*), Bash(command:*), Bash(npm:*), Bash(npx:*)
|
||||||
---
|
---
|
||||||
|
|
||||||
# Use Yaak
|
# Use Yaak
|
||||||
|
|
||||||
Yaak is a desktop API client. The `yaak` CLI reads and writes the **same local
|
Yaak is a desktop API client. The `yaak` CLI reads and writes the **same local
|
||||||
database as the desktop app**, so anything created here shows up in the app
|
database as the desktop app**, so anything you create shows up in the app
|
||||||
immediately, and vice versa. There is no server and no sign-in: `yaak auth` is
|
immediately, and vice versa. There is no server and no sign-in: `yaak auth` is
|
||||||
only for publishing plugins to the Yaak registry, not for any workflow below.
|
only for publishing plugins to the Yaak registry.
|
||||||
|
|
||||||
|
Two consequences worth holding onto. Requests you create are permanent user data
|
||||||
|
in an app they use, not scratch files, so name them the way the user would and
|
||||||
|
clean up anything created just to test. And because the app is right there, the
|
||||||
|
CLI is usually the wrong place to *read* a response in detail; it is the right
|
||||||
|
place to build, organize, and run requests.
|
||||||
|
|
||||||
|
## The CLI describes itself
|
||||||
|
|
||||||
|
**This skill deliberately does not list fields, body types, auth strategies, or
|
||||||
|
template functions.** The user's CLI version and installed plugins decide what
|
||||||
|
exists, so any list written here would eventually be wrong. Ask the CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yaak --help # commands, plus agent hints at the bottom
|
||||||
|
yaak <command> --help # flags for one command
|
||||||
|
yaak request schema http --pretty # full request model, with guidance per field
|
||||||
|
yaak template-function list [filter] # template functions from installed plugins
|
||||||
|
yaak template-function show <name> # one function's arguments
|
||||||
|
```
|
||||||
|
|
||||||
|
`request schema http` is generated from the real model and merges in the auth
|
||||||
|
strategies contributed by plugins, so it is the authoritative answer for what a
|
||||||
|
request payload may contain and what each auth strategy needs. `workspace`,
|
||||||
|
`environment`, and `folder` have `schema` subcommands too.
|
||||||
|
|
||||||
|
Read the relevant schema before writing a JSON payload you are not certain of.
|
||||||
|
That is faster than a failed send, and it stays correct as Yaak changes.
|
||||||
|
|
||||||
## Resource model
|
## Resource model
|
||||||
|
|
||||||
- **Workspace** (`wk_…`) is the top-level container. It owns everything else.
|
- **Workspace** (`wk_…`) is the top-level container.
|
||||||
- **Folder** (`fl_…`) groups requests inside a workspace and can nest. Folders
|
- **Folder** (`fl_…`) groups requests and can nest. Folders carry headers and
|
||||||
carry headers and authentication that child requests inherit.
|
authentication that child requests inherit, which is the usual way to apply
|
||||||
- **Request** (`rq_…`) is a single HTTP, gRPC, or WebSocket request.
|
one token to a whole group.
|
||||||
- **Environment** (`ev_…`) holds variables. Every workspace has one base
|
- **Request** (`rq_…`) is a single HTTP, gRPC, or WebSocket request. The CLI can
|
||||||
environment ("Global Variables") plus any number of sub-environments; a
|
currently only create and send HTTP ones.
|
||||||
sub-environment overrides base variables of the same name.
|
- **Environment** (`ev_…`) holds variables. Each workspace has a base
|
||||||
- **Cookie jar** (`cj_…`) stores cookies per workspace. The oldest jar is used
|
environment plus any number of sub-environments; a sub-environment overrides
|
||||||
by default; no setup needed.
|
base variables of the same name and is chosen per send with `-e`.
|
||||||
|
- **Cookie jar** (`cj_…`) stores cookies per workspace. The oldest is used by
|
||||||
|
default, so this normally needs no attention.
|
||||||
|
|
||||||
IDs are stable and prefix-typed, so you can always tell what an ID refers to.
|
IDs are prefix-typed, so you can always tell what an ID refers to. Commands that
|
||||||
Most commands take a workspace ID positionally and **infer it when the machine
|
take a workspace ID infer it when exactly one workspace exists.
|
||||||
has exactly one workspace**. Pass it explicitly once a second workspace exists.
|
|
||||||
|
|
||||||
## Preflight
|
## Getting oriented
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yaak --version || npm install -g @yaakapp/cli
|
yaak --version || npm install -g @yaakapp/cli
|
||||||
yaak workspace list
|
yaak workspace list
|
||||||
```
|
```
|
||||||
|
|
||||||
`workspace list` prints `wk_… - Name` per line, or `No workspaces found`. Pick
|
Pick the workspace matching the user's project before changing anything, and
|
||||||
the workspace that matches the user's project before mutating anything; create
|
create one only when nothing fits. If a documented command is unrecognized, the
|
||||||
one only when nothing fits.
|
CLI is older than this skill: update it with `npm install -g @yaakapp/cli@latest`
|
||||||
|
and re-run `yaak agent install`, then tell the user to restart their coding tool.
|
||||||
|
**When the CLI and this skill disagree, the CLI is right.**
|
||||||
|
|
||||||
If a subcommand documented here is not recognized, the CLI is older than this
|
## Core workflows
|
||||||
skill. Update it, then refresh the skill so the two stay in lockstep:
|
|
||||||
|
|
||||||
```bash
|
**Start from a spec when one exists.** `yaak import <file>` auto-detects OpenAPI,
|
||||||
npm install -g @yaakapp/cli@latest && yaak agent install
|
Swagger, Postman, Insomnia, cURL, and Yaak exports, and beats authoring requests
|
||||||
```
|
by hand every time.
|
||||||
|
|
||||||
Mention to the user that they need to restart their coding tool for the
|
**Make the host swappable.** Put the base URL in a base-environment variable,
|
||||||
refreshed skill to load; the current session keeps using the old copy.
|
reference it as `${[ base_url ]}`, then add a sub-environment per deployment
|
||||||
|
target. Now `yaak -e ev_staging send <wk_id>` runs everything against staging.
|
||||||
|
|
||||||
## Command map
|
**Chain instead of shell-plumbing.** A request can read another request's
|
||||||
|
response directly, and Yaak sends the dependency first if it needs to:
|
||||||
| Goal | Command |
|
|
||||||
|---|---|
|
|
||||||
| List | `yaak {workspace,folder,request,environment,cookie-jar} list` |
|
|
||||||
| Inspect one | `yaak {workspace,folder,request,environment} show <id>` |
|
|
||||||
| Create | `yaak {workspace,folder,request,environment} create` |
|
|
||||||
| Update | `yaak {workspace,folder,request,environment} update --json '{"id":"…",…}'` |
|
|
||||||
| Delete | `yaak … delete <id> --yes` |
|
|
||||||
| Inspect the model | `yaak request schema http --pretty`, `yaak workspace schema`, `yaak environment schema` |
|
|
||||||
| Send one request | `yaak request send <rq_id>` |
|
|
||||||
| Send a folder or whole workspace | `yaak send <fl_id\|wk_id>` |
|
|
||||||
| Import an existing API | `yaak import <file>` |
|
|
||||||
| Export | `yaak export <file> [workspace_id…]` |
|
|
||||||
|
|
||||||
Global flags go anywhere but are clearest before the subcommand:
|
|
||||||
`-e/--environment <ev_id>`, `--cookie-jar <cj_id>`, `-v/--verbose`,
|
|
||||||
`--data-dir <path>` (point at an isolated database, useful for scratch work).
|
|
||||||
|
|
||||||
## Creating and updating
|
|
||||||
|
|
||||||
Simple requests take flags:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yaak request create wk_abc123 --name "List Pets" --method GET --url "https://api.example.com/pets"
|
|
||||||
```
|
|
||||||
|
|
||||||
Anything richer than name/method/URL takes a JSON payload, either positionally
|
|
||||||
or via `--json`. **Read the schema before writing a payload you are unsure
|
|
||||||
of** — it is generated from the real model and includes the plugin-provided
|
|
||||||
authentication variants:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yaak request schema http --pretty
|
|
||||||
```
|
|
||||||
|
|
||||||
That schema is also the authoritative list of **authentication strategies**,
|
|
||||||
including ones contributed by plugins. Each appears as a named variant under
|
|
||||||
`authentication`, with its own fields, required list, and enums, so there is
|
|
||||||
never a reason to guess auth config:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# every installed strategy, with the value to use for authenticationType
|
|
||||||
yaak request schema http | jq -r '.properties.authentication.oneOf[]
|
|
||||||
| select(.title) | "\(.title): \(.description)"'
|
|
||||||
|
|
||||||
# the full shape of one of them
|
|
||||||
yaak request schema http | jq '.properties.authentication.oneOf[]
|
|
||||||
| select(.title == "OAuth 2.0")'
|
|
||||||
```
|
|
||||||
|
|
||||||
The first prints lines like `OAuth 2.0: Authentication values for strategy
|
|
||||||
'oauth2'`. **The title is a display label, not the value** — `authenticationType`
|
|
||||||
takes the quoted strategy name, so NTLM Auth is `windows` and AWS Signature is
|
|
||||||
`awsv4`.
|
|
||||||
|
|
||||||
The second prints the fields. OAuth 2.0 has fifteen of them plus an enum of valid
|
|
||||||
`grantType` values (`authorization_code`, `implicit`, `password`,
|
|
||||||
`client_credentials`), which is exactly the sort of thing that comes out wrong
|
|
||||||
when guessed. The command loads plugins, so it reflects what is actually
|
|
||||||
installed rather than a fixed list, and it returns in well under a second.
|
|
||||||
|
|
||||||
Rules that are easy to get wrong:
|
|
||||||
|
|
||||||
- **Setting `bodyType` does not add a `Content-Type` header.** The app adds one
|
|
||||||
when you pick a body type in the UI, but creating a request from the CLI skips
|
|
||||||
that step, and the body goes out untyped. Add the header yourself, using the
|
|
||||||
same value as `bodyType` (with `other` → `text/plain` and `graphql` →
|
|
||||||
`application/json`). Multipart is the exception: leave it alone, the sender
|
|
||||||
supplies the boundary.
|
|
||||||
- **Path parameters must keep the leading colon.** For `/pets/:petId`, the
|
|
||||||
`urlParameters` entry is named `:petId`, not `petId`. Get it wrong and the
|
|
||||||
placeholder stays literal in the path while the value is appended to the query
|
|
||||||
string, which usually 404s with no error.
|
|
||||||
- **Create** payloads must omit `id` (or set it to `""`).
|
|
||||||
- **Update** payloads must include `id`, and are applied as a JSON merge patch:
|
|
||||||
keys you omit are left alone, and a key set to `null` is deleted. There is no
|
|
||||||
need to send the whole object.
|
|
||||||
- Flags and JSON cannot be combined on the same command.
|
|
||||||
- `request create` and `request list` are HTTP-only. gRPC and WebSocket requests
|
|
||||||
exist in the model and can be sent from the app, but the CLI cannot yet create
|
|
||||||
or send them.
|
|
||||||
|
|
||||||
The first two fail silently, so verify a new request with `yaak -v request send
|
|
||||||
<id>` and check the `> ` lines actually show the path and headers you intended.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yaak request create wk_abc123 --json '{
|
|
||||||
"name": "Create Pet", "method": "POST", "url": "${[ base_url ]}/pets",
|
|
||||||
"bodyType": "application/json",
|
|
||||||
"body": {"text": "{\"name\":\"Rex\"}"},
|
|
||||||
"headers": [{"name": "Content-Type", "value": "application/json", "enabled": true}]
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
See [requests.md](references/requests.md) for bodies, headers, authentication,
|
|
||||||
path parameters, and folder inheritance.
|
|
||||||
|
|
||||||
## Template variables
|
|
||||||
|
|
||||||
Yaak's template syntax is `${[ … ]}`, **not** `{{ … }}`. It works in URLs,
|
|
||||||
headers, bodies, and auth fields:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
${[ base_url ]}/pets/${[ pet_id ]}
|
${[ response.body.path(request='rq_login', path='$.token') ]}
|
||||||
${[ response.body.path(request='rq_abc123', path='$.token') ]}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Referencing a variable that no active environment defines is a hard error and
|
Run `yaak template-function show response.body.path` for its arguments,
|
||||||
the request is not sent, so an unresolved variable can never silently reach the
|
including how to control when the upstream request re-sends. Chain when a
|
||||||
network. See [environments.md](references/environments.md) for variable scoping
|
request genuinely depends on another's response; to merely run requests in
|
||||||
and [chaining.md](references/chaining.md) for pulling values out of earlier
|
order, `yaak send <fl_id>` already does that.
|
||||||
responses.
|
|
||||||
|
|
||||||
## Sending
|
**Run a set.** `yaak send` accepts a folder or workspace ID, with `--fail-fast`
|
||||||
|
and `--parallel`. Workspace and request IDs survive an export/import, so a
|
||||||
|
committed `yaak export` plus `--data-dir ./.yaak` gives a runnable suite in CI.
|
||||||
|
|
||||||
```bash
|
## Reading results
|
||||||
yaak request send rq_abc123 # body only, on stdout
|
|
||||||
yaak -e ev_staging request send rq_abc123 # against a sub-environment
|
|
||||||
yaak -v request send rq_abc123 # request/response metadata too
|
|
||||||
yaak send fl_abc123 --fail-fast # every request in a folder
|
|
||||||
yaak send wk_abc123 --parallel # every request in a workspace
|
|
||||||
```
|
|
||||||
|
|
||||||
**Reading the result.** A plain send writes only the response body to stdout,
|
A plain send writes only the response body to stdout. Add `-v` for the request
|
||||||
with no trailing newline. Like `curl`, the exit code reflects whether the
|
and response metadata, where lines are prefixed `*`, `>`, and `<`:
|
||||||
request completed, not the HTTP status — a 404 or 500 exits 0. Use `-v` when the
|
|
||||||
status matters:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yaak -v request send rq_abc123 2>&1 | grep '^< HTTP'
|
yaak -v request send rq_abc123 2>&1 | grep '^< HTTP'
|
||||||
```
|
```
|
||||||
|
|
||||||
Under `-v`, connection/request/response lines (`*`, `>`, `<`) and the body all
|
Exit code 1 means the send did not complete: an unresolved template variable, an
|
||||||
go to stdout, with the body following the last `<` header line. Grep for the
|
unreachable host, a TLS failure. **HTTP error statuses are not failures.** Like
|
||||||
prefixes you need rather than assuming a clean split.
|
`curl`, a 404 or 500 exits 0, and a folder of requests that all return 500
|
||||||
|
reports success. Never tell the user an API is healthy based on a clean exit;
|
||||||
Exit code 1 means the send itself failed: an unresolved template variable, an
|
check the status yourself with `-v`.
|
||||||
unreachable host, a TLS failure. For folders and workspaces the last line is
|
|
||||||
`Send summary: N succeeded, M failed`, per-request errors follow on stderr, and
|
|
||||||
the exit code is 1 if any request failed.
|
|
||||||
|
|
||||||
## Running a suite in CI
|
|
||||||
|
|
||||||
Workspace and request IDs survive an export/import, so a committed export gives
|
|
||||||
a stable, runnable suite on a machine that has never seen the app:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install -g @yaakapp/cli
|
|
||||||
yaak --data-dir ./.yaak import ./api-export.json
|
|
||||||
yaak --data-dir ./.yaak -e ev_ci send wk_abc123 --fail-fast
|
|
||||||
```
|
|
||||||
|
|
||||||
`--data-dir` keeps the run isolated from any real Yaak install, and the IDs in
|
|
||||||
the export are the same ones you used locally. Produce the export with
|
|
||||||
`yaak export ./api-export.json wk_abc123`, adding
|
|
||||||
`--include-private-environments` only if the suite needs values you are willing
|
|
||||||
to commit — otherwise keep secrets in a CI-only environment and inject them.
|
|
||||||
|
|
||||||
**The caveat that matters here:** a failing *assertion* is not a concept Yaak
|
|
||||||
has, and HTTP error statuses do not fail the run. A workspace of requests that
|
|
||||||
all return 500 exits 0. The exit code catches unreachable hosts, TLS failures,
|
|
||||||
and unresolved variables only. To gate CI on status codes, run with `-v` and
|
|
||||||
check the `< HTTP` lines yourself. Say this plainly rather than implying a green
|
|
||||||
run means the API is healthy.
|
|
||||||
|
|
||||||
## Importing
|
|
||||||
|
|
||||||
`yaak import` auto-detects OpenAPI/Swagger, Postman, Insomnia, cURL, and Yaak
|
|
||||||
exports, and creates a new workspace by default:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yaak import ./openapi.yaml
|
|
||||||
yaak import ./collection.json --workspace-id wk_abc123 # merge into an existing one
|
|
||||||
```
|
|
||||||
|
|
||||||
This is almost always faster than authoring requests by hand when a spec exists.
|
|
||||||
See [import-export.md](references/import-export.md).
|
|
||||||
|
|
||||||
## Routing
|
|
||||||
|
|
||||||
| Task | Reference |
|
|
||||||
|---|---|
|
|
||||||
| Request bodies, headers, auth, path/query params, folder inheritance | [requests.md](references/requests.md) |
|
|
||||||
| Environment hierarchy, variables, per-environment runs | [environments.md](references/environments.md) |
|
|
||||||
| Using one response inside the next request; template functions | [chaining.md](references/chaining.md) |
|
|
||||||
| OpenAPI/Postman/Insomnia/cURL import, exporting workspaces | [import-export.md](references/import-export.md) |
|
|
||||||
|
|
||||||
## Execution rules
|
## Execution rules
|
||||||
|
|
||||||
1. Resolve the workspace before mutating. Do not create a second workspace when
|
1. Resolve the workspace before mutating, and prefer an existing one.
|
||||||
an existing one matches the user's project.
|
2. Read the schema rather than guessing field names, auth fields, or body shapes.
|
||||||
2. Read the schema before writing a non-trivial JSON payload. Do not guess field
|
3. `update` takes a JSON merge patch keyed by `id`: send only what changes, and
|
||||||
names.
|
note that arrays are replaced wholesale, not merged.
|
||||||
3. Prefer `update` merge patches over re-sending whole objects.
|
4. Deletes need `--yes` in a non-interactive shell. Confirm with the user first.
|
||||||
4. Deletes require `--yes` in a non-interactive shell; otherwise they block on a
|
|
||||||
prompt. Confirm intent with the user before deleting anything.
|
|
||||||
5. Never write a real secret into an environment variable on the user's behalf.
|
5. Never write a real secret into an environment variable on the user's behalf.
|
||||||
Reference one (`${[ api_token ]}`) and let the user fill in the value — see
|
Reference one and let them fill in the value.
|
||||||
[environments.md](references/environments.md).
|
6. Verify what you built by sending it, and report the real HTTP status.
|
||||||
6. After creating requests, verify by sending one, and report the actual HTTP
|
|
||||||
status from `-v` rather than inferring success from exit code 0.
|
|
||||||
7. Requests you create are permanent user data in their app, not scratch. Name
|
|
||||||
them the way the user would, and clean up anything you created purely to test.
|
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
# Chaining requests
|
|
||||||
|
|
||||||
The usual way to feed one response into the next request is a shell pipeline:
|
|
||||||
send, pipe through `jq`, stash in a variable, interpolate into the next command.
|
|
||||||
Yaak does not need that. A request can reference another request's response
|
|
||||||
directly, and Yaak resolves the dependency at send time — including sending the
|
|
||||||
upstream request first if it has to.
|
|
||||||
|
|
||||||
This is the highest-leverage thing the CLI offers, because the chain is stored
|
|
||||||
in the workspace. The user can re-run it from the app, and it keeps working
|
|
||||||
after the shell session is gone.
|
|
||||||
|
|
||||||
## Reading a value out of another response
|
|
||||||
|
|
||||||
```
|
|
||||||
${[ response.body.path(request='rq_login', path='$.token') ]}
|
|
||||||
```
|
|
||||||
|
|
||||||
`request` is the upstream request's ID. `path` is JSONPath for JSON responses
|
|
||||||
and XPath for XML. So a login-then-call-the-API pair is two requests and no glue:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yaak request create wk_abc123 --json '{
|
|
||||||
"name": "Login",
|
|
||||||
"method": "POST",
|
|
||||||
"url": "${[ base_url ]}/auth/login",
|
|
||||||
"bodyType": "application/json",
|
|
||||||
"body": {"text": "{\"user\":\"demo\",\"pass\":\"${[ password ]}\"}"},
|
|
||||||
"headers": [{"name": "Content-Type", "value": "application/json", "enabled": true}]
|
|
||||||
}'
|
|
||||||
# -> Created request: rq_login
|
|
||||||
|
|
||||||
yaak request create wk_abc123 --json '{
|
|
||||||
"name": "List Orders",
|
|
||||||
"method": "GET",
|
|
||||||
"url": "${[ base_url ]}/orders",
|
|
||||||
"authenticationType": "bearer",
|
|
||||||
"authentication": {"token": "${[ response.body.path(request='rq_login', path='$.token') ]}"}
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
Sending "List Orders" now sends "Login" first when it needs to, extracts
|
|
||||||
`$.token`, and puts it in the `Authorization` header.
|
|
||||||
|
|
||||||
Note the quoting: template function arguments use **single quotes**, so inside a
|
|
||||||
single-quoted shell string write the payload to a file, or escape as above, or
|
|
||||||
switch the outer shell quoting to double quotes and escape the inner JSON.
|
|
||||||
Writing the JSON payload to a file and using `--json "$(cat payload.json)"` is
|
|
||||||
the least error-prone for anything complex.
|
|
||||||
|
|
||||||
## Controlling when the upstream request re-sends
|
|
||||||
|
|
||||||
The `behavior` argument decides whether the dependency is actually sent:
|
|
||||||
|
|
||||||
| `behavior` | Meaning |
|
|
||||||
|---|---|
|
|
||||||
| `smart` (default) | Send only if there is no stored response yet |
|
|
||||||
| `always` | Send every time |
|
|
||||||
| `ttl` | Send if the newest response is older than `ttl` seconds (`0` never expires) |
|
|
||||||
|
|
||||||
```
|
|
||||||
${[ response.body.path(request='rq_login', path='$.token', behavior='ttl', ttl='300') ]}
|
|
||||||
```
|
|
||||||
|
|
||||||
`smart` is right for a token you fetch once. `ttl` matches a real token lifetime
|
|
||||||
and is usually the best choice for auth. `always` is for values that must be
|
|
||||||
fresh on every call, like a nonce.
|
|
||||||
|
|
||||||
## Other response accessors
|
|
||||||
|
|
||||||
```
|
|
||||||
${[ response.header(request='rq_login', header='X-Request-Id') ]}
|
|
||||||
${[ response.body.raw(request='rq_login') ]}
|
|
||||||
```
|
|
||||||
|
|
||||||
`response.body.path` also accepts `behavior`/`ttl`, and has an alias of plain
|
|
||||||
`response`.
|
|
||||||
|
|
||||||
## Other useful template functions
|
|
||||||
|
|
||||||
These come from bundled plugins and work anywhere a value is rendered:
|
|
||||||
|
|
||||||
| Function | Use |
|
|
||||||
|---|---|
|
|
||||||
| `uuid.v4()`, also `v1`, `v3`, `v5`, `v6`, `v7` | Idempotency keys, unique record names |
|
|
||||||
| `timestamp.unix()`, `timestamp.unixMillis()`, `timestamp.iso8601()` | Timestamps in bodies or signatures |
|
|
||||||
| `timestamp.format(...)`, `timestamp.offset(...)` | Formatted or relative times |
|
|
||||||
| `random.range(min='1', max='100', decimals='0')` | Sample data |
|
|
||||||
| `hash.sha256(input='…', encoding='hex')` | Digests — also `md5`, `sha1`, `sha512` |
|
|
||||||
| `hmac.sha256(input='…', key='…', encoding='hex')` | Signed request signatures |
|
|
||||||
| `base64.encode(input='…')`, `base64.decode(...)` | Encoded values |
|
|
||||||
| `url.encode(input='…')`, `url.decode(...)` | Escaping values for URLs |
|
|
||||||
| `fs.readFile(path='/abs/path', trim='true')` | Pull a value from a file on disk |
|
|
||||||
| `cookie.value(name='session')` | Read a cookie from the jar |
|
|
||||||
| `1password.item(...)` | Fetch a secret from 1Password rather than storing it |
|
|
||||||
|
|
||||||
Note the shapes that are easy to misremember: the encoders are `base64.encode`
|
|
||||||
and `url.encode`, not `encode.base64`; the random function is `random.range`,
|
|
||||||
not `random.number`; and there is no `timestamp.now`. Argument names vary per
|
|
||||||
function, and a wrong name renders as an error rather than an empty string, so
|
|
||||||
check the function in the app's template editor when unsure.
|
|
||||||
|
|
||||||
## When to chain and when not to
|
|
||||||
|
|
||||||
Chain when the dependency is part of the API's real shape: log in, then call;
|
|
||||||
create a resource, then fetch it by the returned ID. The workspace becomes a
|
|
||||||
runnable description of the API, which is the point.
|
|
||||||
|
|
||||||
Do not chain to smuggle in shell logic. If a value needs a conditional, a
|
|
||||||
computation, or a retry, do that in the shell and set an environment variable.
|
|
||||||
And do not build a long chain just to run a group of requests — `yaak send
|
|
||||||
<fl_id>` already sends every request in a folder sequentially, with
|
|
||||||
`--fail-fast` to stop at the first failure and `--parallel` when order does not
|
|
||||||
matter. Reach for a chain when a request genuinely *depends* on another's
|
|
||||||
response, not merely when it should run after it.
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
# Environments and variables
|
|
||||||
|
|
||||||
## The hierarchy
|
|
||||||
|
|
||||||
Every workspace gets a base environment named **Global Variables** for free. Its
|
|
||||||
`parentModel` is `workspace`, and its variables apply to every send regardless of
|
|
||||||
which environment is selected.
|
|
||||||
|
|
||||||
Sub-environments (`parentModel: "environment"`) sit under the base and are chosen
|
|
||||||
per send with `-e`. A sub-environment variable overrides a base variable of the
|
|
||||||
same name; names it does not define fall through to the base.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yaak environment list wk_abc123
|
|
||||||
# ev_staging - Staging (environment)
|
|
||||||
# ev_production - Production (environment)
|
|
||||||
# ev_base - Global Variables (workspace)
|
|
||||||
```
|
|
||||||
|
|
||||||
The trailing parenthetical is `parentModel`, which is how you tell the base
|
|
||||||
environment from the rest.
|
|
||||||
|
|
||||||
## Setting variables
|
|
||||||
|
|
||||||
Variables are an array on the environment, so an update replaces the whole list.
|
|
||||||
Read first, then write the full array back:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yaak environment show ev_base
|
|
||||||
yaak environment update --json '{
|
|
||||||
"id": "ev_base",
|
|
||||||
"variables": [
|
|
||||||
{"name": "base_url", "value": "https://api.example.com", "enabled": true},
|
|
||||||
{"name": "api_version", "value": "v1", "enabled": true}
|
|
||||||
]
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
Create a sub-environment with its variables in one step:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yaak environment create wk_abc123 --json '{
|
|
||||||
"name": "Staging",
|
|
||||||
"parentModel": "environment",
|
|
||||||
"variables": [{"name": "base_url", "value": "https://staging.example.com", "enabled": true}]
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
`enabled: false` keeps a variable defined but inert, which is how the app models
|
|
||||||
a commented-out value.
|
|
||||||
|
|
||||||
## Using them
|
|
||||||
|
|
||||||
`${[ name ]}` resolves anywhere a value is rendered — URL, headers, body, and
|
|
||||||
authentication fields:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yaak request create wk_abc123 --json '{
|
|
||||||
"name": "List Pets",
|
|
||||||
"method": "GET",
|
|
||||||
"url": "${[ base_url ]}/${[ api_version ]}/pets",
|
|
||||||
"authenticationType": "bearer",
|
|
||||||
"authentication": {"token": "${[ api_token ]}"}
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
Then run the same request against different targets:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yaak request send rq_abc123 # base environment only
|
|
||||||
yaak -e ev_staging request send rq_abc123 # staging overrides base_url
|
|
||||||
yaak -e ev_production send fl_smoke_tests # whole folder against production
|
|
||||||
```
|
|
||||||
|
|
||||||
`-e` is global, so it applies to `send`, `request send`, folder sends, and
|
|
||||||
workspace sends alike.
|
|
||||||
|
|
||||||
## Unresolved variables fail loudly
|
|
||||||
|
|
||||||
Referencing a name no active environment defines aborts before anything is sent:
|
|
||||||
|
|
||||||
```
|
|
||||||
Error: Failed to render request templates: Render Error: Variable "api_token" is not defined in active environment
|
|
||||||
```
|
|
||||||
|
|
||||||
Exit code 1. This is a feature worth relying on — a typo in a variable name can
|
|
||||||
never quietly send a request to `https:///pets`. When a send fails this way, the
|
|
||||||
fix is either the variable name in the request or the `-e` environment, not a
|
|
||||||
retry.
|
|
||||||
|
|
||||||
## Secrets
|
|
||||||
|
|
||||||
Do not write real credentials into environment variables on the user's behalf.
|
|
||||||
Create the reference and let the user supply the value:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yaak environment update --json '{"id":"ev_base","variables":[{"name":"api_token","value":"","enabled":true}]}'
|
|
||||||
```
|
|
||||||
|
|
||||||
Then tell the user which variable to fill in, and in which environment.
|
|
||||||
|
|
||||||
Environments have a `public` flag that mirrors the app's Sharable/Private toggle.
|
|
||||||
`public: false` (the default) marks the environment Private, which keeps it out
|
|
||||||
of `yaak export` unless `--include-private-environments` is passed. Secrets
|
|
||||||
belong in a private environment; values safe to commit or share belong in a
|
|
||||||
sharable one.
|
|
||||||
|
|
||||||
## Cookies
|
|
||||||
|
|
||||||
Cookie jars are per-workspace and require no setup — the oldest jar is used
|
|
||||||
automatically. Sending cookies is off by default per request; enable it with the
|
|
||||||
inherited setting, on the folder if it should apply to a whole group:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"settingSendCookies": {"enabled": true, "value": true},
|
|
||||||
"settingStoreCookies": {"enabled": true, "value": true}
|
|
||||||
```
|
|
||||||
|
|
||||||
With both on, a login request stores its `Set-Cookie` and later requests in the
|
|
||||||
same jar send it back, which is often simpler than threading a token by hand.
|
|
||||||
Use `--cookie-jar cj_abc123` to select a non-default jar, and
|
|
||||||
`yaak cookie-jar list <wk_id>` to see the jars and their cookie counts.
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
# Importing and exporting
|
|
||||||
|
|
||||||
## Import
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yaak import ./openapi.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
One command, one positional file path. The format is auto-detected — OpenAPI 3,
|
|
||||||
Swagger 2, Postman collections, Postman environments, Insomnia exports, cURL
|
|
||||||
commands, and Yaak's own export format are all supported by bundled importer
|
|
||||||
plugins. Output is a one-line summary:
|
|
||||||
|
|
||||||
```
|
|
||||||
Imported 1 workspace, 1 environment, 1 folder, 3 HTTP requests
|
|
||||||
```
|
|
||||||
|
|
||||||
By default this creates a **new workspace**. To merge into one that already
|
|
||||||
exists:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yaak import ./collection.json --workspace-id wk_abc123
|
|
||||||
```
|
|
||||||
|
|
||||||
Not every importer honours `--workspace-id`; the flag applies where the importer
|
|
||||||
supports it, and otherwise a new workspace is still created. Run
|
|
||||||
`yaak workspace list` afterwards to see what you actually got.
|
|
||||||
|
|
||||||
**Reach for this first.** When the user has an OpenAPI spec, a Postman
|
|
||||||
collection, or even a directory of `curl` commands in a README, importing beats
|
|
||||||
authoring requests by hand — it is one command, it preserves names and grouping,
|
|
||||||
and it will not typo a URL. Author requests by hand when there is no spec, or
|
|
||||||
when the user wants a small hand-picked set rather than every endpoint.
|
|
||||||
|
|
||||||
A cURL import is a fast way to turn something the user already has into a saved
|
|
||||||
request:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "curl -X POST https://api.example.com/pets -H 'Content-Type: application/json' -d '{\"name\":\"Rex\"}'" > /tmp/req.txt
|
|
||||||
yaak import /tmp/req.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
## After importing
|
|
||||||
|
|
||||||
An imported spec gives you requests pointing at whatever `servers` the spec
|
|
||||||
declared. The usual follow-up is to make the host swappable:
|
|
||||||
|
|
||||||
1. `yaak request list <wk_id>` to see what landed.
|
|
||||||
2. Put the host in a base environment variable (`base_url`).
|
|
||||||
3. Update the imported requests to use `${[ base_url ]}`, then add a
|
|
||||||
sub-environment per deployment target.
|
|
||||||
|
|
||||||
See [environments.md](environments.md). At that point `yaak -e ev_staging send
|
|
||||||
<wk_id>` runs the whole imported API against staging.
|
|
||||||
|
|
||||||
## Export
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yaak export ./backup.json # the only workspace, when there is one
|
|
||||||
yaak export ./backup.json wk_abc123 # a specific workspace
|
|
||||||
yaak export ./backup.json wk_abc wk_def # several
|
|
||||||
yaak export ./backup.json --all # everything
|
|
||||||
```
|
|
||||||
|
|
||||||
Private environments are **excluded** unless you pass
|
|
||||||
`--include-private-environments`. That default exists so an export can be
|
|
||||||
committed to a repository without leaking credentials — do not add the flag
|
|
||||||
just to make an export look complete. Add it only when the user explicitly wants
|
|
||||||
a full backup, and say so when you do.
|
|
||||||
|
|
||||||
The output is Yaak's own format, so `yaak import ./backup.json` round-trips it.
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
# Authoring requests
|
|
||||||
|
|
||||||
Everything here is a field on the HTTP request model. Run
|
|
||||||
`yaak request schema http --pretty` to see the full, current schema, including
|
|
||||||
the authentication variants contributed by installed plugins.
|
|
||||||
|
|
||||||
## Two ways to create
|
|
||||||
|
|
||||||
Name, method, and URL have flags. Everything else needs JSON:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yaak request create wk_abc123 --name "List Pets" --method GET --url "https://api.example.com/pets"
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yaak request create wk_abc123 --json '{
|
|
||||||
"name": "Create Pet",
|
|
||||||
"method": "POST",
|
|
||||||
"url": "https://api.example.com/pets",
|
|
||||||
"bodyType": "application/json",
|
|
||||||
"body": {"text": "{\"name\":\"Rex\",\"species\":\"dog\"}"},
|
|
||||||
"headers": [{"name": "Content-Type", "value": "application/json", "enabled": true}]
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
Flags and JSON cannot be mixed on the same command. The workspace ID may be
|
|
||||||
omitted when only one workspace exists, and may also be carried inside the
|
|
||||||
payload as `workspaceId`.
|
|
||||||
|
|
||||||
## Bodies
|
|
||||||
|
|
||||||
`bodyType` decides how `body` is encoded onto the wire, and `body` is a
|
|
||||||
free-form object whose shape depends on that type. For text-ish types the
|
|
||||||
content lives in `body.text` as a **string**, so a JSON payload is
|
|
||||||
double-encoded: a JSON string containing JSON.
|
|
||||||
|
|
||||||
| `bodyType` | `body` |
|
|
||||||
|---|---|
|
|
||||||
| `application/json` | `{"text": "{\"key\":\"value\"}"}` |
|
|
||||||
| `text/xml` | `{"text": "<root/>"}` |
|
|
||||||
| `other` | `{"text": "…"}` |
|
|
||||||
| `graphql` | `{"query": "{ pets { id } }", "variables": "{\"n\":1}", "operationName": ""}` |
|
|
||||||
| `application/x-www-form-urlencoded` | `{"form": [{"name":"a","value":"1","enabled":true}]}` |
|
|
||||||
| `multipart/form-data` | `{"form": [{"name":"file","file":"/abs/path","enabled":true}]}` |
|
|
||||||
| `binary` | `{"filePath": "/abs/path"}` |
|
|
||||||
| `null` (omitted) | no body |
|
|
||||||
|
|
||||||
For `graphql`, note that `variables` is a **string** of JSON, not an object, and
|
|
||||||
that a GraphQL request sent with method `GET` moves query/variables/operationName
|
|
||||||
into the query string and sends no body at all.
|
|
||||||
|
|
||||||
For `multipart/form-data`, each entry is either a text field (`value`) or a file
|
|
||||||
(`file`, an absolute path), and may carry its own `contentType`.
|
|
||||||
|
|
||||||
**Add the `Content-Type` header yourself.** In the desktop app, choosing a body
|
|
||||||
type also writes a matching `Content-Type` into the request's headers, so it is
|
|
||||||
stored on the request rather than inferred at send time. Creating a request from
|
|
||||||
the CLI skips that step: `bodyType` alone controls how the body is *encoded*, and
|
|
||||||
nothing adds the header. A JSON body with no `Content-Type` goes out as untyped
|
|
||||||
bytes, which many APIs answer with 400 or 415.
|
|
||||||
|
|
||||||
```json
|
|
||||||
"bodyType": "application/json",
|
|
||||||
"body": {"text": "{\"name\":\"Rex\"}"},
|
|
||||||
"headers": [{"name": "Content-Type", "value": "application/json", "enabled": true}]
|
|
||||||
```
|
|
||||||
|
|
||||||
Use the same value as `bodyType`, with two exceptions the app also makes: `other`
|
|
||||||
pairs with `text/plain`, and `graphql` pairs with `application/json`. Multipart is
|
|
||||||
the one case to leave alone — the sender replaces that header with one carrying
|
|
||||||
the generated boundary.
|
|
||||||
|
|
||||||
Requests created this way end up identical to app-created ones, which matters
|
|
||||||
because the user will open them in the app afterwards.
|
|
||||||
|
|
||||||
## Headers
|
|
||||||
|
|
||||||
```json
|
|
||||||
"headers": [
|
|
||||||
{"name": "Accept", "value": "application/json", "enabled": true},
|
|
||||||
{"name": "X-Debug", "value": "1", "enabled": false}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
`enabled: false` keeps a header in the app for the user to toggle without
|
|
||||||
sending it. Values accept template variables.
|
|
||||||
|
|
||||||
## URL parameters
|
|
||||||
|
|
||||||
One array covers both query string entries and path placeholders. A parameter
|
|
||||||
fills a path placeholder only when its **name starts with a colon** and matches
|
|
||||||
the placeholder in the URL. Everything else becomes a query string entry:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"url": "https://api.example.com/pets/:petId/visits",
|
|
||||||
"urlParameters": [
|
|
||||||
{"name": ":petId", "value": "42", "enabled": true},
|
|
||||||
{"name": "limit", "value": "10", "enabled": true}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
That sends `https://api.example.com/pets/42/visits?limit=10` — `:petId` is
|
|
||||||
substituted into the path and dropped from the query string, `limit` is not.
|
|
||||||
|
|
||||||
This is the single easiest thing to get wrong here, and it fails **silently**.
|
|
||||||
Naming the parameter `petId` instead of `:petId` leaves `/pets/:petId/visits` in
|
|
||||||
the path as literal text and appends `?petId=42`, which most servers answer with
|
|
||||||
a 404. Always include the colon, and confirm with `yaak -v request send <id>`
|
|
||||||
that the `> GET …` line shows a substituted path.
|
|
||||||
|
|
||||||
## Authentication
|
|
||||||
|
|
||||||
`authenticationType` names a strategy and `authentication` holds its values. The
|
|
||||||
strategy names are not always what you would guess — the built-ins are `basic`,
|
|
||||||
`bearer`, `apikey`, `jwt`, `oauth1`, `oauth2`, `awsv4` (not "aws"), and
|
|
||||||
`windows` (not "ntlm"). Installed plugins can add more.
|
|
||||||
|
|
||||||
```json
|
|
||||||
"authenticationType": "bearer",
|
|
||||||
"authentication": {"token": "${[ api_token ]}"}
|
|
||||||
```
|
|
||||||
|
|
||||||
```json
|
|
||||||
"authenticationType": "basic",
|
|
||||||
"authentication": {"username": "admin", "password": "${[ admin_password ]}"}
|
|
||||||
```
|
|
||||||
|
|
||||||
```json
|
|
||||||
"authenticationType": "apikey",
|
|
||||||
"authentication": {"location": "header", "key": "X-Api-Key", "value": "${[ api_key ]}"}
|
|
||||||
```
|
|
||||||
|
|
||||||
OAuth 2.0 is the one to look up rather than attempt from memory. It has fifteen
|
|
||||||
fields, six of them required, and `grantType` is an enum:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"authenticationType": "oauth2",
|
|
||||||
"authentication": {
|
|
||||||
"grantType": "client_credentials",
|
|
||||||
"clientId": "${[ client_id ]}",
|
|
||||||
"clientSecret": "${[ client_secret ]}",
|
|
||||||
"accessTokenUrl": "https://auth.example.com/oauth/token",
|
|
||||||
"scope": "read:pets",
|
|
||||||
"credentials": "body",
|
|
||||||
"tokenName": "access_token",
|
|
||||||
"headerName": "Authorization",
|
|
||||||
"usePkce": false,
|
|
||||||
"useExternalBrowser": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`grantType` accepts `authorization_code`, `implicit`, `password`, or
|
|
||||||
`client_credentials`, and which other fields matter depends on which you pick:
|
|
||||||
`authorization_code` also wants `authorizationUrl` and `redirectUri`, while
|
|
||||||
`client_credentials` does not.
|
|
||||||
|
|
||||||
The exact fields for every strategy, and which are required, come from the
|
|
||||||
schema, which enumerates each installed strategy as a named variant under
|
|
||||||
`authentication`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# display name plus the value to use for authenticationType
|
|
||||||
yaak request schema http | jq -r '.properties.authentication.oneOf[]
|
|
||||||
| select(.title) | "\(.title): \(.description)"'
|
|
||||||
|
|
||||||
# the full shape of one strategy
|
|
||||||
yaak request schema http | jq '.properties.authentication.oneOf[]
|
|
||||||
| select(.title == "OAuth 2.0")'
|
|
||||||
```
|
|
||||||
|
|
||||||
Because the list is built by loading plugins, it covers plugin-contributed
|
|
||||||
strategies too, not just the built-ins. Read it rather than guessing.
|
|
||||||
|
|
||||||
Set `authenticationType` to `null` to send no auth and stop inheriting from the
|
|
||||||
parent folder.
|
|
||||||
|
|
||||||
## Folders and inheritance
|
|
||||||
|
|
||||||
Folders are containers *and* a place to put shared configuration. Headers and
|
|
||||||
authentication set on a folder apply to every request inside it, so the common
|
|
||||||
pattern is one folder per API surface holding the auth:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yaak folder create wk_abc123 --name "Admin API"
|
|
||||||
yaak folder update --json '{
|
|
||||||
"id": "fl_abc123",
|
|
||||||
"authenticationType": "bearer",
|
|
||||||
"authentication": {"token": "${[ admin_token ]}"},
|
|
||||||
"headers": [{"name": "X-Api-Version", "value": "2024-01-01", "enabled": true}]
|
|
||||||
}'
|
|
||||||
yaak request create wk_abc123 --json '{"name":"List Users","method":"GET","url":"${[ base_url ]}/users","folderId":"fl_abc123"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
The request above sends both the folder's bearer token and its version header
|
|
||||||
without repeating either. A request that sets its own `authenticationType`
|
|
||||||
overrides the folder's.
|
|
||||||
|
|
||||||
Nest folders by setting a folder's `folderId`. `yaak send <fl_id>` sends every
|
|
||||||
request in the folder recursively.
|
|
||||||
|
|
||||||
## Per-request settings
|
|
||||||
|
|
||||||
Each `setting*` field is an inherited toggle shaped
|
|
||||||
`{"enabled": bool, "value": …}`, where `enabled` means "override the inherited
|
|
||||||
value" rather than "turn the feature on":
|
|
||||||
|
|
||||||
```json
|
|
||||||
"settingFollowRedirects": {"enabled": true, "value": false},
|
|
||||||
"settingRequestTimeout": {"enabled": true, "value": 5000}
|
|
||||||
```
|
|
||||||
|
|
||||||
Available: `settingFollowRedirects`, `settingRequestTimeout` (ms, `0` for none),
|
|
||||||
`settingValidateCertificates`, `settingSendCookies`, `settingStoreCookies`.
|
|
||||||
|
|
||||||
## Updating
|
|
||||||
|
|
||||||
Updates are JSON merge patches keyed by `id`. Send only what changes:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yaak request update --json '{"id":"rq_abc123","method":"PATCH"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
Arrays are replaced wholesale, not merged — to add one header, read the current
|
|
||||||
list with `yaak request show rq_abc123` and write the full new array back.
|
|
||||||
Setting a key to `null` removes it.
|
|
||||||
|
|
||||||
## Ordering
|
|
||||||
|
|
||||||
`sortPriority` (a float) controls display order in the app sidebar. Leave it at
|
|
||||||
`0` unless the user cares; requests created with the same priority fall back to
|
|
||||||
creation order.
|
|
||||||
@@ -86,6 +86,36 @@ pub enum Commands {
|
|||||||
|
|
||||||
/// Environment commands
|
/// Environment commands
|
||||||
Environment(EnvironmentArgs),
|
Environment(EnvironmentArgs),
|
||||||
|
|
||||||
|
/// Template function commands
|
||||||
|
#[command(alias = "func")]
|
||||||
|
TemplateFunction(TemplateFunctionArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct TemplateFunctionArgs {
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: TemplateFunctionCommands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum TemplateFunctionCommands {
|
||||||
|
/// List template functions provided by installed plugins
|
||||||
|
List {
|
||||||
|
/// Only show functions whose name contains this text
|
||||||
|
#[arg(value_name = "FILTER")]
|
||||||
|
filter: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Show a template function's arguments as JSON
|
||||||
|
Show {
|
||||||
|
/// Template function name (for example: response.body.path)
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
/// Pretty-print JSON output
|
||||||
|
#[arg(long)]
|
||||||
|
pretty: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
@@ -361,6 +391,13 @@ pub enum FolderCommands {
|
|||||||
workspace_id: Option<String>,
|
workspace_id: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Output JSON schema for folder create/update payloads
|
||||||
|
Schema {
|
||||||
|
/// Pretty-print schema JSON output
|
||||||
|
#[arg(long)]
|
||||||
|
pretty: bool,
|
||||||
|
},
|
||||||
|
|
||||||
/// Show a folder as JSON
|
/// Show a folder as JSON
|
||||||
Show {
|
Show {
|
||||||
/// Folder ID
|
/// Folder ID
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ use crate::utils::json::{
|
|||||||
apply_merge_patch, is_json_shorthand, merge_workspace_id_arg, parse_optional_json,
|
apply_merge_patch, is_json_shorthand, merge_workspace_id_arg, parse_optional_json,
|
||||||
parse_required_json, require_id, validate_create_id,
|
parse_required_json, require_id, validate_create_id,
|
||||||
};
|
};
|
||||||
|
use crate::utils::schema::append_agent_hints;
|
||||||
use crate::utils::workspace::resolve_workspace_id;
|
use crate::utils::workspace::resolve_workspace_id;
|
||||||
|
use schemars::schema_for;
|
||||||
use yaak_models::models::Folder;
|
use yaak_models::models::Folder;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
|
|
||||||
@@ -14,6 +16,7 @@ type CommandResult<T = ()> = std::result::Result<T, String>;
|
|||||||
pub fn run(ctx: &CliContext, args: FolderArgs) -> i32 {
|
pub fn run(ctx: &CliContext, args: FolderArgs) -> i32 {
|
||||||
let result = match args.command {
|
let result = match args.command {
|
||||||
FolderCommands::List { workspace_id } => list(ctx, workspace_id.as_deref()),
|
FolderCommands::List { workspace_id } => list(ctx, workspace_id.as_deref()),
|
||||||
|
FolderCommands::Schema { pretty } => schema(pretty),
|
||||||
FolderCommands::Show { folder_id } => show(ctx, &folder_id),
|
FolderCommands::Show { folder_id } => show(ctx, &folder_id),
|
||||||
FolderCommands::Create { workspace_id, name, json } => {
|
FolderCommands::Create { workspace_id, name, json } => {
|
||||||
create(ctx, workspace_id, name, json)
|
create(ctx, workspace_id, name, json)
|
||||||
@@ -31,6 +34,18 @@ pub fn run(ctx: &CliContext, args: FolderArgs) -> i32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn schema(pretty: bool) -> CommandResult {
|
||||||
|
let mut schema = serde_json::to_value(schema_for!(Folder))
|
||||||
|
.map_err(|e| format!("Failed to serialize folder 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 folder schema JSON: {e}"))?;
|
||||||
|
println!("{output}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult {
|
fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult {
|
||||||
let workspace_id = resolve_workspace_id(ctx, workspace_id, "folder list")?;
|
let workspace_id = resolve_workspace_id(ctx, workspace_id, "folder list")?;
|
||||||
let folders =
|
let folders =
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ pub mod import_export;
|
|||||||
pub mod plugin;
|
pub mod plugin;
|
||||||
pub mod request;
|
pub mod request;
|
||||||
pub mod send;
|
pub mod send;
|
||||||
|
pub mod template_function;
|
||||||
pub mod workspace;
|
pub mod workspace;
|
||||||
|
|||||||
@@ -122,6 +122,20 @@ fn enrich_schema_guidance(schema: &mut Value, request_type: RequestSchemaType) {
|
|||||||
"For path segments like `/foo/:id/comments/:commentId`, put concrete values in `urlParameters` using names that keep the leading `:` (for example `:id`, `:commentId`). A name without the `:` is sent as a query string parameter instead, leaving the placeholder in the path.",
|
"For path segments like `/foo/:id/comments/:commentId`, put concrete values in `urlParameters` using names that keep the leading `:` (for example `:id`, `:commentId`). A name without the `:` is sent as a query string parameter instead, leaving the placeholder in the path.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(body_type_schema) = properties.get_mut("bodyType").and_then(Value::as_object_mut) {
|
||||||
|
append_description(
|
||||||
|
body_type_schema,
|
||||||
|
"Known values: `application/json`, `text/xml`, `application/x-www-form-urlencoded`, `multipart/form-data`, `graphql`, `binary`, `other`, or null for no body. This selects how `body` is encoded; it does NOT add a `Content-Type` header. Add that header yourself, matching the body type (`other` pairs with `text/plain` and `graphql` with `application/json`). Multipart is the exception: its header is generated at send time to carry the boundary.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(body_schema) = properties.get_mut("body").and_then(Value::as_object_mut) {
|
||||||
|
append_description(
|
||||||
|
body_schema,
|
||||||
|
"Shape depends on `bodyType`. Text-ish types (`application/json`, `text/xml`, `other`) use `{\"text\": \"...\"}` where the value is a string, so JSON bodies are a JSON string containing JSON. Form types use `{\"form\": [{\"name\": \"a\", \"value\": \"1\", \"enabled\": true}]}`, and a multipart entry may use `file` (an absolute path) and `contentType` instead of `value`. `binary` uses `{\"filePath\": \"/abs/path\"}`. `graphql` uses `{\"query\": \"...\", \"variables\": \"{}\", \"operationName\": \"\"}` where `variables` is a string of JSON.",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn append_description(schema: &mut Map<String, Value>, extra: &str) {
|
fn append_description(schema: &mut Map<String, Value>, extra: &str) {
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
use crate::cli::{TemplateFunctionArgs, TemplateFunctionCommands};
|
||||||
|
use crate::context::CliContext;
|
||||||
|
use yaak_plugins::events::{PluginContext, TemplateFunction};
|
||||||
|
|
||||||
|
type CommandResult<T = ()> = std::result::Result<T, String>;
|
||||||
|
|
||||||
|
pub async fn run(ctx: &CliContext, args: TemplateFunctionArgs) -> i32 {
|
||||||
|
let result = match args.command {
|
||||||
|
TemplateFunctionCommands::List { filter } => list(ctx, filter.as_deref()).await,
|
||||||
|
TemplateFunctionCommands::Show { name, pretty } => show(ctx, &name, pretty).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(()) => 0,
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("Error: {error}");
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Template functions come from plugins, so the only accurate list is the one the
|
||||||
|
/// installed plugins report right now.
|
||||||
|
async fn all(ctx: &CliContext) -> CommandResult<Vec<TemplateFunction>> {
|
||||||
|
let plugin_context = PluginContext::new_empty();
|
||||||
|
let summaries = ctx
|
||||||
|
.plugin_manager()
|
||||||
|
.get_template_function_summaries(&plugin_context)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to list template functions: {e}"))?;
|
||||||
|
|
||||||
|
let mut functions: Vec<TemplateFunction> =
|
||||||
|
summaries.into_iter().flat_map(|summary| summary.functions).collect();
|
||||||
|
functions.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
Ok(functions)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list(ctx: &CliContext, filter: Option<&str>) -> CommandResult {
|
||||||
|
let mut functions = all(ctx).await?;
|
||||||
|
|
||||||
|
if let Some(filter) = filter {
|
||||||
|
let needle = filter.to_lowercase();
|
||||||
|
functions.retain(|f| f.name.to_lowercase().contains(&needle));
|
||||||
|
}
|
||||||
|
|
||||||
|
if functions.is_empty() {
|
||||||
|
match filter {
|
||||||
|
Some(filter) => println!("No template functions matching '{filter}'"),
|
||||||
|
None => println!("No template functions found"),
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
for function in functions {
|
||||||
|
let args = function.args.iter().filter_map(arg_name).collect::<Vec<_>>().join(", ");
|
||||||
|
match function.description {
|
||||||
|
Some(description) if !description.is_empty() => {
|
||||||
|
println!("{}({}) - {}", function.name, args, description)
|
||||||
|
}
|
||||||
|
_ => println!("{}({})", function.name, args),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn show(ctx: &CliContext, name: &str, pretty: bool) -> CommandResult {
|
||||||
|
let functions = all(ctx).await?;
|
||||||
|
let function = functions
|
||||||
|
.iter()
|
||||||
|
.find(|f| {
|
||||||
|
f.name == name
|
||||||
|
|| f.aliases.as_ref().is_some_and(|aliases| aliases.iter().any(|a| a == name))
|
||||||
|
})
|
||||||
|
.ok_or_else(|| {
|
||||||
|
let names = functions.iter().map(|f| f.name.as_str()).collect::<Vec<_>>();
|
||||||
|
format!("No template function named '{name}'. Available: {}", names.join(", "))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let output = if pretty {
|
||||||
|
serde_json::to_string_pretty(function)
|
||||||
|
} else {
|
||||||
|
serde_json::to_string(function)
|
||||||
|
}
|
||||||
|
.map_err(|e| format!("Failed to serialize template function: {e}"))?;
|
||||||
|
|
||||||
|
println!("{output}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arg_name(arg: &yaak_plugins::events::TemplateFunctionArg) -> Option<String> {
|
||||||
|
use yaak_plugins::events::{FormInput, TemplateFunctionArg};
|
||||||
|
|
||||||
|
let TemplateFunctionArg::FormInput(input) = arg;
|
||||||
|
let base = match input {
|
||||||
|
FormInput::Text(v) => &v.base,
|
||||||
|
FormInput::Editor(v) => &v.base,
|
||||||
|
FormInput::Select(v) => &v.base,
|
||||||
|
FormInput::Checkbox(v) => &v.base,
|
||||||
|
FormInput::File(v) => &v.base,
|
||||||
|
FormInput::HttpRequest(v) => &v.base,
|
||||||
|
FormInput::KeyValue(v) => &v.base,
|
||||||
|
// Layout-only inputs have no value of their own
|
||||||
|
FormInput::Accordion(_)
|
||||||
|
| FormInput::HStack(_)
|
||||||
|
| FormInput::Banner(_)
|
||||||
|
| FormInput::Markdown(_) => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if base.name.trim().is_empty() { None } else { Some(base.name.clone()) }
|
||||||
|
}
|
||||||
@@ -37,6 +37,13 @@ async fn main() {
|
|||||||
|
|
||||||
let exit_code = match command {
|
let exit_code = match command {
|
||||||
Commands::Agent(args) => commands::agent::run(args),
|
Commands::Agent(args) => commands::agent::run(args),
|
||||||
|
Commands::TemplateFunction(args) => {
|
||||||
|
let mut context = CliContext::new(data_dir.clone(), app_id);
|
||||||
|
context.init_plugins(CliExecutionContext::default()).await;
|
||||||
|
let exit_code = commands::template_function::run(&context, args).await;
|
||||||
|
context.shutdown().await;
|
||||||
|
exit_code
|
||||||
|
}
|
||||||
Commands::Auth(args) => commands::auth::run(args).await,
|
Commands::Auth(args) => commands::auth::run(args).await,
|
||||||
Commands::Import(args) => {
|
Commands::Import(args) => {
|
||||||
let mut context = CliContext::new(data_dir.clone(), app_id);
|
let mut context = CliContext::new(data_dir.clone(), app_id);
|
||||||
|
|||||||
@@ -949,7 +949,7 @@ pub struct ParentHeaders {
|
|||||||
pub headers: Vec<HttpRequestHeader>,
|
pub headers: Vec<HttpRequestHeader>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 = "folders")]
|
#[enum_def(table_name = "folders")]
|
||||||
|
|||||||
Reference in New Issue
Block a user