mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-14 15:42:02 +02:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e032c4c8d6 | ||
|
|
c3fc3fb68a |
@@ -0,0 +1,172 @@
|
||||
---
|
||||
name: use-yaak
|
||||
description: >
|
||||
Build and run HTTP API requests with the Yaak CLI (`yaak`): create workspaces,
|
||||
folders, environments and variables, author HTTP requests, configure
|
||||
authentication (OAuth 2.0, bearer tokens, API keys, basic, JWT, AWS SigV4),
|
||||
send them individually or a whole folder/workspace at once, chain one
|
||||
request's response into the next, and import existing APIs from OpenAPI,
|
||||
Postman, Insomnia, or cURL. Use this skill whenever the user mentions Yaak, a
|
||||
Yaak workspace, or the `yaak` command, and also when they ask to try, hit,
|
||||
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
|
||||
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 the requests should be
|
||||
saved, reused, shared, or run as a set.
|
||||
allowed-tools: Bash(yaak:*), Bash(which:*), Bash(command:*), Bash(npm:*), Bash(npx:*)
|
||||
---
|
||||
|
||||
# Use Yaak
|
||||
|
||||
<!--
|
||||
Managed by the Yaak CLI. `yaak agent install` replaces this file wholesale on
|
||||
every run, so local edits are lost. To add your own guidance, write a separate
|
||||
skill or use your tool's project instructions instead.
|
||||
yaak-cli-version: __YAAK_CLI_VERSION__
|
||||
-->
|
||||
|
||||
Yaak is a desktop API client. The `yaak` CLI reads and writes the **same local
|
||||
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
|
||||
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
|
||||
|
||||
- **Workspace** (`wk_…`) is the top-level container.
|
||||
- **Folder** (`fl_…`) groups requests and can nest. Folders carry headers and
|
||||
authentication that child requests inherit, which is the usual way to apply
|
||||
one token to a whole group.
|
||||
- **Request** (`rq_…`) is a single HTTP, gRPC, or WebSocket request. The CLI can
|
||||
currently only create and send HTTP ones.
|
||||
- **Environment** (`ev_…`) holds variables. Each workspace has a base
|
||||
environment plus any number of sub-environments; a sub-environment overrides
|
||||
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 prefix-typed, so you can always tell what an ID refers to. Commands that
|
||||
take a workspace ID infer it when exactly one workspace exists.
|
||||
|
||||
## Getting oriented
|
||||
|
||||
```bash
|
||||
yaak --version || npm install -g @yaakapp/cli
|
||||
yaak workspace list
|
||||
```
|
||||
|
||||
Pick the workspace matching the user's project before changing anything, and
|
||||
create one only when nothing fits.
|
||||
|
||||
### Skill freshness
|
||||
|
||||
The CLI writes this skill, so an upgraded CLI can leave it behind. That failure
|
||||
is silent: nothing errors, the skill just stops mentioning things the CLI can
|
||||
now do. Check once per session, alongside the commands above:
|
||||
|
||||
```bash
|
||||
yaak --help 2>&1 | grep -A2 "Agent tooling:"
|
||||
```
|
||||
|
||||
If it reports the skill is out of date, run `yaak agent install` and tell the
|
||||
user to restart their coding tool. This session keeps running on the old copy
|
||||
until they do, so finish the current request either way. Check once and do not
|
||||
re-run it after acting.
|
||||
|
||||
**When the CLI and this skill disagree, the CLI is right.**
|
||||
|
||||
## Core workflows
|
||||
|
||||
**Start from a spec when one exists.** `yaak import <file>` auto-detects OpenAPI,
|
||||
Swagger, Postman, Insomnia, cURL, and Yaak exports, and beats authoring requests
|
||||
by hand every time.
|
||||
|
||||
**Make the host swappable.** Put the base URL in a base-environment variable,
|
||||
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.
|
||||
|
||||
**Chain instead of shell-plumbing.** A request can read another request's
|
||||
response directly, and Yaak sends the dependency first if it needs to:
|
||||
|
||||
```
|
||||
${[ response.body.path(request='rq_login', path='$.token') ]}
|
||||
```
|
||||
|
||||
Run `yaak template-function show response.body.path` for its arguments,
|
||||
including how to control when the upstream request re-sends. Chain when a
|
||||
request genuinely depends on another's response; to merely run requests in
|
||||
order, `yaak send <fl_id>` already does that.
|
||||
|
||||
**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.
|
||||
|
||||
## Reading results
|
||||
|
||||
A plain send writes only the response body to stdout. Yaak also stores every
|
||||
response, so the reliable way to see what happened is to ask afterwards rather
|
||||
than to parse the send output:
|
||||
|
||||
```bash
|
||||
yaak response show rq_abc123 # latest response for a request, as JSON
|
||||
yaak response list rq_abc123 # its history, newest first
|
||||
yaak response body rq_abc123 # just the body
|
||||
```
|
||||
|
||||
`response show` gives status, reason, timing, headers, the final URL, and any
|
||||
transport error. Pass a response ID for a specific one.
|
||||
|
||||
`-v` on a send prints the same information prefixed `*`, `>`, and `<`, with the
|
||||
body after the last `<` header line:
|
||||
|
||||
```bash
|
||||
yaak -v request send rq_abc123 2>&1 | grep '^< HTTP'
|
||||
```
|
||||
|
||||
That works, but `response show` is still better when you need to act on the
|
||||
result, since it gives you fields rather than text to parse.
|
||||
|
||||
Exit code 1 means the send did not complete: an unresolved template variable, an
|
||||
unreachable host, a TLS failure. **HTTP error statuses are not failures.** Like
|
||||
`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;
|
||||
check the status.
|
||||
|
||||
## Execution rules
|
||||
|
||||
1. Resolve the workspace before mutating, and prefer an existing one.
|
||||
2. Read the schema rather than guessing field names, auth fields, or body shapes.
|
||||
3. `update` takes a JSON merge patch keyed by `id`: send only what changes, and
|
||||
note that arrays are replaced wholesale, not merged.
|
||||
4. Deletes need `--yes` in a non-interactive shell. Confirm with the user first.
|
||||
5. Never write a real secret into an environment variable on the user's behalf.
|
||||
Reference one and let them fill in the value.
|
||||
6. Verify what you built by sending it, and report the real HTTP status.
|
||||
@@ -1,17 +1,17 @@
|
||||
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub const AGENT_HINTS: &str = 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)"#;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "yaak")]
|
||||
#[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 {
|
||||
/// Use a custom data directory
|
||||
#[arg(long, global = true)]
|
||||
@@ -39,6 +39,9 @@ pub struct Cli {
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
/// Install Yaak skills for AI coding agents
|
||||
Agent(AgentArgs),
|
||||
|
||||
/// Authentication commands
|
||||
Auth(AuthArgs),
|
||||
|
||||
@@ -82,6 +85,105 @@ pub enum Commands {
|
||||
|
||||
/// Environment commands
|
||||
Environment(EnvironmentArgs),
|
||||
|
||||
/// Template function commands
|
||||
#[command(alias = "func")]
|
||||
TemplateFunction(TemplateFunctionArgs),
|
||||
|
||||
/// Response commands
|
||||
Response(ResponseArgs),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct ResponseArgs {
|
||||
#[command(subcommand)]
|
||||
pub command: ResponseCommands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum ResponseCommands {
|
||||
/// List stored responses for a request or workspace
|
||||
List {
|
||||
/// Request or workspace ID (optional when exactly one workspace exists)
|
||||
id: Option<String>,
|
||||
|
||||
/// Maximum number of responses to return, newest first
|
||||
#[arg(long)]
|
||||
limit: Option<u64>,
|
||||
},
|
||||
|
||||
/// Show a response as JSON, including status, timing, and headers
|
||||
Show {
|
||||
/// Response ID, or a request ID to use its most recent response
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// Write a stored response body to stdout
|
||||
Body {
|
||||
/// Response ID, or a request ID to use its most recent response
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// Delete stored responses for a request or workspace
|
||||
Delete {
|
||||
/// Request or workspace ID
|
||||
id: String,
|
||||
|
||||
/// Skip confirmation prompt
|
||||
#[arg(short, long)]
|
||||
yes: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[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)]
|
||||
pub struct AgentArgs {
|
||||
#[command(subcommand)]
|
||||
pub command: AgentCommands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum AgentCommands {
|
||||
/// Install the Yaak skill so coding agents know how to drive the CLI
|
||||
#[command(alias = "update", alias = "add")]
|
||||
Install {
|
||||
/// Install for specific agents instead of all detected ones
|
||||
#[arg(long = "agent", value_name = "AGENT")]
|
||||
agent: Option<Vec<String>>,
|
||||
},
|
||||
|
||||
/// Remove the Yaak skill
|
||||
#[command(alias = "uninstall", alias = "rm")]
|
||||
Remove {
|
||||
/// Remove for specific agents instead of all detected ones
|
||||
#[arg(long = "agent", value_name = "AGENT")]
|
||||
agent: Option<Vec<String>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
@@ -328,6 +430,13 @@ pub enum FolderCommands {
|
||||
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 {
|
||||
/// Folder ID
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
use crate::cli::{AgentArgs, AgentCommands};
|
||||
use crate::ui;
|
||||
use crate::version;
|
||||
use include_dir::{Dir, include_dir};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
static SKILL_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/skills/use-yaak");
|
||||
|
||||
const SKILL_NAME: &str = "use-yaak";
|
||||
|
||||
/// Substituted at install time so the installed skill records which CLI wrote it,
|
||||
/// letting `--help` tell the user when an upgrade has left their copy behind.
|
||||
const VERSION_PLACEHOLDER: &str = "__YAAK_CLI_VERSION__";
|
||||
const VERSION_MARKER: &str = "yaak-cli-version:";
|
||||
|
||||
type CommandResult<T = ()> = std::result::Result<T, String>;
|
||||
|
||||
/// A coding tool that reads skills from a directory in the user's home.
|
||||
struct Target {
|
||||
/// Display name used in output.
|
||||
label: &'static str,
|
||||
/// Directory holding all skills for this tool (`…/skills`).
|
||||
skills_dir: PathBuf,
|
||||
}
|
||||
|
||||
pub fn run(args: AgentArgs) -> i32 {
|
||||
let result = match args.command {
|
||||
AgentCommands::Install { agent } => install(agent),
|
||||
AgentCommands::Remove { agent } => remove(agent),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(()) => 0,
|
||||
Err(error) => {
|
||||
ui::error(&error);
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn install(agent: Option<Vec<String>>) -> CommandResult {
|
||||
let targets = resolve_targets(agent)?;
|
||||
|
||||
let mut installed = 0usize;
|
||||
for target in &targets {
|
||||
let dir = target.skills_dir.join(SKILL_NAME);
|
||||
match write_skill(&dir) {
|
||||
Ok(()) => {
|
||||
installed += 1;
|
||||
ui::success(&format!("{} -> {}", target.label, dir.display()));
|
||||
}
|
||||
Err(error) => ui::warning_stderr(&format!("{}: {}", target.label, error)),
|
||||
}
|
||||
}
|
||||
|
||||
if installed == 0 {
|
||||
return Err("Failed to install the Yaak skill anywhere".to_string());
|
||||
}
|
||||
|
||||
ui::info("Restart your coding tool to pick up the skill");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove(agent: Option<Vec<String>>) -> CommandResult {
|
||||
let targets = resolve_targets(agent)?;
|
||||
|
||||
let mut removed = 0usize;
|
||||
for target in &targets {
|
||||
let dir = target.skills_dir.join(SKILL_NAME);
|
||||
if !dir.exists() {
|
||||
continue;
|
||||
}
|
||||
match fs::remove_dir_all(&dir) {
|
||||
Ok(()) => {
|
||||
removed += 1;
|
||||
ui::success(&format!("Removed {}", dir.display()));
|
||||
}
|
||||
Err(error) => {
|
||||
ui::warning_stderr(&format!("Failed to remove {}: {error}", dir.display()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if removed == 0 {
|
||||
ui::info("No Yaak skill was installed");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The skill directory belongs to the CLI, so every install overwrites what is there
|
||||
/// and prunes what this version no longer ships. Preserving local edits would be worse
|
||||
/// than losing them: an edited file would be skipped by every future install and go
|
||||
/// stale forever, against a CLI that keeps changing.
|
||||
///
|
||||
/// NOTE: atomicity is per file, not for the install as a whole. That is total today
|
||||
/// because the skill is a single file, but adding a second one would mean a reader
|
||||
/// could catch a mix of old and new files. Replacing the directory as a unit is not an
|
||||
/// option (`rename` refuses a non-empty destination), so a multi-file skill would need
|
||||
/// the published path to become a symlink swapped between versioned directories.
|
||||
fn write_skill(dir: &Path) -> CommandResult {
|
||||
fs::create_dir_all(dir).map_err(|e| format!("Failed to create {}: {e}", dir.display()))?;
|
||||
|
||||
let mut written = Vec::new();
|
||||
for file in walk(&SKILL_DIR) {
|
||||
let relative = file.path().to_path_buf();
|
||||
let destination = dir.join(&relative);
|
||||
if let Some(parent) = destination.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("Failed to create {}: {e}", parent.display()))?;
|
||||
}
|
||||
|
||||
let contents = stamp_version(file.contents());
|
||||
write_atomic(&destination, &contents)?;
|
||||
written.push(relative);
|
||||
}
|
||||
|
||||
prune_unshipped(dir, &written);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write via a sibling temp file and rename over the destination. `rename` replaces an
|
||||
/// existing file atomically, so a reader sees either the old copy or the new one, and a
|
||||
/// crash part way through cannot leave a half-written skill behind.
|
||||
fn write_atomic(destination: &Path, contents: &[u8]) -> CommandResult {
|
||||
let parent = destination
|
||||
.parent()
|
||||
.ok_or_else(|| format!("Invalid skill path {}", destination.display()))?;
|
||||
let name = destination
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.ok_or_else(|| format!("Invalid skill path {}", destination.display()))?;
|
||||
let temp = parent.join(format!(".{name}.tmp-{}", std::process::id()));
|
||||
|
||||
fs::write(&temp, contents)
|
||||
.map_err(|e| format!("Failed to write {}: {e}", destination.display()))?;
|
||||
|
||||
match fs::rename(&temp, destination) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(error) => {
|
||||
let _ = fs::remove_file(&temp);
|
||||
Err(format!("Failed to write {}: {error}", destination.display()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stamp the running version into the skill so it carries its own provenance.
|
||||
fn stamp_version(contents: &[u8]) -> Vec<u8> {
|
||||
match std::str::from_utf8(contents) {
|
||||
Ok(text) => text.replace(VERSION_PLACEHOLDER, version::cli_version()).into_bytes(),
|
||||
Err(_) => contents.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop anything this version does not ship, so files an older version wrote (or a
|
||||
/// leftover temp file) cannot linger beside the refreshed skill.
|
||||
fn prune_unshipped(dir: &Path, written: &[PathBuf]) {
|
||||
let Ok(entries) = fs::read_dir(dir) else {
|
||||
return;
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
let Ok(relative) = path.strip_prefix(dir) else {
|
||||
continue;
|
||||
};
|
||||
if written.iter().any(|w| w == relative || w.starts_with(relative)) {
|
||||
continue;
|
||||
}
|
||||
if path.is_dir() {
|
||||
let _ = fs::remove_dir_all(&path);
|
||||
} else {
|
||||
let _ = fs::remove_file(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the version an installed skill was stamped with.
|
||||
fn installed_version(dir: &Path) -> Option<String> {
|
||||
let text = fs::read_to_string(dir.join("SKILL.md")).ok()?;
|
||||
text.lines()
|
||||
.find_map(|line| line.trim().strip_prefix(VERSION_MARKER))
|
||||
.map(|v| v.trim().to_string())
|
||||
.filter(|v| !v.is_empty() && v != VERSION_PLACEHOLDER)
|
||||
}
|
||||
|
||||
/// Health summary appended to root `--help`, so an agent can notice in one command
|
||||
/// that an upgraded CLI has left the installed skill behind. Silent when nothing is
|
||||
/// installed anywhere, to avoid nagging users who do not use agent tooling.
|
||||
pub fn help_section() -> Option<String> {
|
||||
let targets = resolve_targets(None).ok()?;
|
||||
let current = version::cli_version();
|
||||
|
||||
let mut stale = Vec::new();
|
||||
let mut installed = 0usize;
|
||||
for target in &targets {
|
||||
let dir = target.skills_dir.join(SKILL_NAME);
|
||||
if !dir.join("SKILL.md").exists() {
|
||||
continue;
|
||||
}
|
||||
installed += 1;
|
||||
|
||||
let found = installed_version(&dir).unwrap_or_default();
|
||||
if found != current {
|
||||
let found = if found.is_empty() { "unknown".to_string() } else { found };
|
||||
stale.push(format!("{} ({found})", target.label));
|
||||
}
|
||||
}
|
||||
|
||||
if installed == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
if stale.is_empty() {
|
||||
return Some(format!(
|
||||
"Agent tooling:\n Yaak skill is installed and up to date ({current})"
|
||||
));
|
||||
}
|
||||
|
||||
Some(format!(
|
||||
"Agent tooling:\n Yaak skill is out of date for {} — CLI is {current}\n Run `yaak agent install`, then restart your coding tool",
|
||||
stale.join(", ")
|
||||
))
|
||||
}
|
||||
|
||||
/// Flatten the embedded skill directory into its files, recursing into subdirectories.
|
||||
fn walk<'a>(dir: &'a Dir<'a>) -> Vec<&'a include_dir::File<'a>> {
|
||||
let mut files: Vec<_> = dir.files().collect();
|
||||
for child in dir.dirs() {
|
||||
files.extend(walk(child));
|
||||
}
|
||||
files
|
||||
}
|
||||
|
||||
/// `~/.agents/skills` is the cross-tool location and is always written. Tool-specific
|
||||
/// directories are written only when that tool is already set up on this machine, so
|
||||
/// installing never creates a config directory for a tool the user does not use.
|
||||
fn resolve_targets(requested: Option<Vec<String>>) -> CommandResult<Vec<Target>> {
|
||||
let home = dirs::home_dir().ok_or("Could not determine home directory")?;
|
||||
|
||||
let known: Vec<(&str, PathBuf, PathBuf)> = vec![
|
||||
("agents", home.join(".agents"), home.join(".agents").join("skills")),
|
||||
("claude-code", home.join(".claude"), home.join(".claude").join("skills")),
|
||||
("cursor", home.join(".cursor"), home.join(".cursor").join("skills")),
|
||||
("codex", home.join(".codex"), home.join(".codex").join("skills")),
|
||||
("opencode", home.join(".opencode"), home.join(".opencode").join("skills")),
|
||||
];
|
||||
|
||||
if let Some(requested) = requested {
|
||||
let mut targets = Vec::new();
|
||||
for name in requested {
|
||||
let found =
|
||||
known.iter().find(|(label, _, _)| *label == name.as_str()).ok_or_else(|| {
|
||||
let names: Vec<_> = known.iter().map(|(l, _, _)| *l).collect();
|
||||
format!("Unknown agent '{name}'. Known agents: {}", names.join(", "))
|
||||
})?;
|
||||
targets.push(Target { label: found.0, skills_dir: found.2.clone() });
|
||||
}
|
||||
return Ok(targets);
|
||||
}
|
||||
|
||||
let targets: Vec<Target> = known
|
||||
.into_iter()
|
||||
.filter(|(label, marker, _)| *label == "agents" || marker.exists())
|
||||
.map(|(label, _, skills_dir)| Target { label, skills_dir })
|
||||
.collect();
|
||||
|
||||
Ok(targets)
|
||||
}
|
||||
@@ -5,7 +5,9 @@ use crate::utils::json::{
|
||||
apply_merge_patch, is_json_shorthand, merge_workspace_id_arg, parse_optional_json,
|
||||
parse_required_json, require_id, validate_create_id,
|
||||
};
|
||||
use crate::utils::schema::append_agent_hints;
|
||||
use crate::utils::workspace::resolve_workspace_id;
|
||||
use schemars::schema_for;
|
||||
use yaak_models::models::Folder;
|
||||
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 {
|
||||
let result = match args.command {
|
||||
FolderCommands::List { workspace_id } => list(ctx, workspace_id.as_deref()),
|
||||
FolderCommands::Schema { pretty } => schema(pretty),
|
||||
FolderCommands::Show { folder_id } => show(ctx, &folder_id),
|
||||
FolderCommands::Create { 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 {
|
||||
let workspace_id = resolve_workspace_id(ctx, workspace_id, "folder list")?;
|
||||
let folders =
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod agent;
|
||||
pub mod auth;
|
||||
pub mod cookie_jar;
|
||||
pub mod environment;
|
||||
@@ -5,5 +6,7 @@ pub mod folder;
|
||||
pub mod import_export;
|
||||
pub mod plugin;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
pub mod send;
|
||||
pub mod template_function;
|
||||
pub mod workspace;
|
||||
|
||||
@@ -107,6 +107,47 @@ async fn schema(ctx: &CliContext, request_type: RequestSchemaType, pretty: bool)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `request create` only builds HTTP requests, but `request schema` will happily hand
|
||||
/// out gRPC and WebSocket schemas. Without this, a gRPC payload deserializes into an
|
||||
/// HttpRequest with its unknown fields dropped, quietly producing a broken request
|
||||
/// (the gRPC method name lands in `method`, and `service` disappears).
|
||||
fn reject_non_http_payload(payload: &Value, context: &str) -> CommandResult {
|
||||
let Some(object) = payload.as_object() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let Some(model) = object.get("model").and_then(Value::as_str)
|
||||
&& !model.is_empty()
|
||||
&& model != "http_request"
|
||||
{
|
||||
return Err(format!(
|
||||
"{context} only supports HTTP requests, but the payload has \"model\": \"{model}\". The CLI cannot work with {model} requests yet; use the Yaak app instead."
|
||||
));
|
||||
}
|
||||
|
||||
let known: std::collections::BTreeSet<String> = serde_json::to_value(schema_for!(HttpRequest))
|
||||
.ok()
|
||||
.and_then(|schema| schema.get("properties").and_then(Value::as_object).cloned())
|
||||
.map(|properties| properties.keys().cloned().collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
if known.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let unknown: Vec<&str> =
|
||||
object.keys().filter(|key| !known.contains(*key)).map(String::as_str).collect();
|
||||
|
||||
if !unknown.is_empty() {
|
||||
return Err(format!(
|
||||
"{context} got fields that are not part of an HTTP request: {}. If this is a gRPC or WebSocket request, the CLI cannot work with it yet. Run `yaak request schema http` for the valid fields.",
|
||||
unknown.join(", ")
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn enrich_schema_guidance(schema: &mut Value, request_type: RequestSchemaType) {
|
||||
if !matches!(request_type, RequestSchemaType::Http) {
|
||||
return;
|
||||
@@ -119,7 +160,21 @@ fn enrich_schema_guidance(schema: &mut Value, request_type: RequestSchemaType) {
|
||||
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`).",
|
||||
"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.",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -353,6 +408,7 @@ fn create(
|
||||
}
|
||||
|
||||
validate_create_id(&payload, "request")?;
|
||||
reject_non_http_payload(&payload, "request create")?;
|
||||
let mut request: HttpRequest = serde_json::from_value(payload)
|
||||
.map_err(|e| format!("Failed to parse request create JSON: {e}"))?;
|
||||
let fallback_workspace_id = if workspace_id_arg.is_none() && request.workspace_id.is_empty()
|
||||
@@ -401,6 +457,7 @@ fn create(
|
||||
fn update(ctx: &CliContext, json: Option<String>, json_input: Option<String>) -> CommandResult {
|
||||
let patch = parse_required_json(json, json_input, "request update")?;
|
||||
let id = require_id(&patch, "request update")?;
|
||||
reject_non_http_payload(&patch, "request update")?;
|
||||
|
||||
let existing = ctx
|
||||
.db()
|
||||
@@ -493,14 +550,24 @@ async fn send_http_request_by_id(
|
||||
}
|
||||
}
|
||||
});
|
||||
// Verbose mode has a second writer on stdout (the event task above), and the two
|
||||
// race: the body could land in the middle of the headers, or run onto the same
|
||||
// line as the status. So buffer the body while verbose and write it once the
|
||||
// events are done. Without -v nothing else writes, so stream it straight through.
|
||||
let body_handle = tokio::task::spawn_blocking(move || {
|
||||
let mut buffered = Vec::new();
|
||||
let mut stdout = std::io::stdout();
|
||||
while let Some(chunk) = body_chunk_rx.blocking_recv() {
|
||||
if verbose {
|
||||
buffered.extend_from_slice(&chunk);
|
||||
continue;
|
||||
}
|
||||
if stdout.write_all(&chunk).is_err() {
|
||||
break;
|
||||
}
|
||||
let _ = stdout.flush();
|
||||
}
|
||||
buffered
|
||||
});
|
||||
let response_dir = ctx.data_dir().join("responses");
|
||||
|
||||
@@ -522,8 +589,16 @@ async fn send_http_request_by_id(
|
||||
})
|
||||
.await;
|
||||
|
||||
// Await the events first so every `*`, `>`, and `<` line is out before the body.
|
||||
let _ = event_handle.await;
|
||||
let _ = body_handle.await;
|
||||
if let Ok(buffered) = body_handle.await
|
||||
&& !buffered.is_empty()
|
||||
{
|
||||
let mut stdout = std::io::stdout();
|
||||
let _ = stdout.write_all(&buffered);
|
||||
let _ = stdout.flush();
|
||||
}
|
||||
|
||||
result.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
use crate::cli::{ResponseArgs, ResponseCommands};
|
||||
use crate::context::CliContext;
|
||||
use crate::utils::confirm::confirm_delete;
|
||||
use crate::utils::workspace::resolve_workspace_id;
|
||||
use std::io::Write;
|
||||
use yaak_models::models::HttpResponse;
|
||||
use yaak_models::util::UpdateSource;
|
||||
|
||||
type CommandResult<T = ()> = std::result::Result<T, String>;
|
||||
|
||||
pub fn run(ctx: &CliContext, args: ResponseArgs) -> i32 {
|
||||
let result = match args.command {
|
||||
ResponseCommands::List { id, limit } => list(ctx, id.as_deref(), limit),
|
||||
ResponseCommands::Show { id } => show(ctx, &id),
|
||||
ResponseCommands::Body { id } => body(ctx, &id),
|
||||
ResponseCommands::Delete { id, yes } => delete(ctx, &id, yes),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(()) => 0,
|
||||
Err(error) => {
|
||||
eprintln!("Error: {error}");
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Accepts a response ID, or a request ID meaning "the latest response for that request".
|
||||
fn resolve_response(ctx: &CliContext, id: &str) -> CommandResult<HttpResponse> {
|
||||
if let Ok(response) = ctx.db().get_http_response(id) {
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
if ctx.db().get_http_request(id).is_ok() {
|
||||
return ctx
|
||||
.db()
|
||||
.list_http_responses_for_request(id, Some(1))
|
||||
.map_err(|e| format!("Failed to list responses: {e}"))?
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| format!("Request {id} has no stored responses yet"));
|
||||
}
|
||||
|
||||
Err(format!("Could not resolve ID '{id}' as a response or request"))
|
||||
}
|
||||
|
||||
fn list(ctx: &CliContext, id: Option<&str>, limit: Option<u64>) -> CommandResult {
|
||||
// An explicit request ID scopes to that request; anything else lists the workspace.
|
||||
let responses = match id {
|
||||
Some(id) if ctx.db().get_http_request(id).is_ok() => ctx
|
||||
.db()
|
||||
.list_http_responses_for_request(id, limit)
|
||||
.map_err(|e| format!("Failed to list responses: {e}"))?,
|
||||
other => {
|
||||
let workspace_id = resolve_workspace_id(ctx, other, "response list")?;
|
||||
ctx.db()
|
||||
.list_http_responses(&workspace_id, limit)
|
||||
.map_err(|e| format!("Failed to list responses: {e}"))?
|
||||
}
|
||||
};
|
||||
|
||||
if responses.is_empty() {
|
||||
println!("No responses found");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for response in responses {
|
||||
let status = match response.error {
|
||||
// Errors can be a paragraph of nested debug output; `show` has the full text.
|
||||
Some(error) => {
|
||||
let first_line = error.lines().next().unwrap_or_default();
|
||||
let summary = match first_line.char_indices().nth(80) {
|
||||
Some((cut, _)) => format!("{}...", &first_line[..cut]),
|
||||
None => first_line.to_string(),
|
||||
};
|
||||
format!("ERROR {summary}")
|
||||
}
|
||||
None => {
|
||||
let reason = response.status_reason.unwrap_or_default();
|
||||
format!("{} {}", response.status, reason).trim_end().to_string()
|
||||
}
|
||||
};
|
||||
println!("{} - {} {} ({}ms)", response.id, status, response.url, response.elapsed);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show(ctx: &CliContext, id: &str) -> CommandResult {
|
||||
let response = resolve_response(ctx, id)?;
|
||||
let output = serde_json::to_string_pretty(&response)
|
||||
.map_err(|e| format!("Failed to serialize response: {e}"))?;
|
||||
println!("{output}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn body(ctx: &CliContext, id: &str) -> CommandResult {
|
||||
let response = resolve_response(ctx, id)?;
|
||||
let Some(body_path) = response.body_path else {
|
||||
return Err(format!("Response {} has no stored body", response.id));
|
||||
};
|
||||
|
||||
let bytes = std::fs::read(&body_path)
|
||||
.map_err(|e| format!("Failed to read response body at {body_path}: {e}"))?;
|
||||
|
||||
let mut stdout = std::io::stdout();
|
||||
stdout.write_all(&bytes).map_err(|e| format!("Failed to write response body: {e}"))?;
|
||||
stdout.flush().map_err(|e| format!("Failed to flush response body: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete(ctx: &CliContext, id: &str, yes: bool) -> CommandResult {
|
||||
// Deleting is scoped to a whole request or workspace, since a single stored
|
||||
// response is rarely the thing someone wants to remove.
|
||||
if ctx.db().get_http_request(id).is_ok() {
|
||||
if !yes && !confirm_delete("responses for request", id) {
|
||||
println!("Aborted");
|
||||
return Ok(());
|
||||
}
|
||||
let count = ctx
|
||||
.db()
|
||||
.delete_all_http_responses_for_request(id, &UpdateSource::Sync)
|
||||
.map_err(|e| format!("Failed to delete responses: {e}"))?;
|
||||
println!("Deleted {count} responses for request {id}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let workspace_id = resolve_workspace_id(ctx, Some(id), "response delete")?;
|
||||
if !yes && !confirm_delete("responses for workspace", &workspace_id) {
|
||||
println!("Aborted");
|
||||
return Ok(());
|
||||
}
|
||||
let count = ctx
|
||||
.db()
|
||||
.delete_all_http_responses_for_workspace(&workspace_id, &UpdateSource::Sync)
|
||||
.map_err(|e| format!("Failed to delete responses: {e}"))?;
|
||||
println!("Deleted {count} responses for workspace {workspace_id}");
|
||||
Ok(())
|
||||
}
|
||||
@@ -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()) }
|
||||
}
|
||||
@@ -7,15 +7,31 @@ mod utils;
|
||||
mod version;
|
||||
mod version_check;
|
||||
|
||||
use clap::Parser;
|
||||
use cli::{Cli, Commands, PluginCommands, RequestCommands};
|
||||
use clap::{CommandFactory, FromArgMatches};
|
||||
use cli::{AGENT_HINTS, Cli, Commands, PluginCommands, RequestCommands};
|
||||
use context::{CliContext, CliExecutionContext};
|
||||
use std::path::PathBuf;
|
||||
use yaak_models::queries::any_request::AnyRequest;
|
||||
|
||||
/// Built at runtime so root `--help` can report whether the installed agent skill is
|
||||
/// still in step with this CLI version.
|
||||
fn help_footer() -> String {
|
||||
match commands::agent::help_section() {
|
||||
Some(section) => format!("{AGENT_HINTS}\n\n{section}"),
|
||||
None => format!(
|
||||
"{AGENT_HINTS}\n - Run `yaak agent install` to install the Yaak skill for AI coding agents"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let Cli { data_dir, environment, cookie_jar, verbose, log, command } = Cli::parse();
|
||||
let matches = Cli::command().after_help(help_footer()).get_matches();
|
||||
let Cli { data_dir, environment, cookie_jar, verbose, log, command } =
|
||||
match Cli::from_arg_matches(&matches) {
|
||||
Ok(cli) => cli,
|
||||
Err(error) => error.exit(),
|
||||
};
|
||||
|
||||
if let Some(log_level) = log {
|
||||
match log_level {
|
||||
@@ -36,6 +52,20 @@ async fn main() {
|
||||
version_check::maybe_check_for_updates().await;
|
||||
|
||||
let exit_code = match command {
|
||||
Commands::Agent(args) => commands::agent::run(args),
|
||||
Commands::Response(args) => {
|
||||
let context = CliContext::new(data_dir.clone(), app_id);
|
||||
let exit_code = commands::response::run(&context, args);
|
||||
context.shutdown().await;
|
||||
exit_code
|
||||
}
|
||||
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::Import(args) => {
|
||||
let mut context = CliContext::new(data_dir.clone(), app_id);
|
||||
|
||||
@@ -1630,10 +1630,11 @@ async fn cmd_delete_all_http_responses<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
window: WebviewWindow<R>,
|
||||
) -> YaakResult<()> {
|
||||
Ok(app_handle.db().delete_all_http_responses_for_request(
|
||||
app_handle.db().delete_all_http_responses_for_request(
|
||||
request_id,
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)?)
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
// On Nvidia + Wayland, WebKit2GTK's DMA-BUF renderer triggers a protocol error (71) due to
|
||||
// an explicit sync bug (https://bugs.webkit.org/show_bug.cgi?id=280210). Disabling explicit
|
||||
// sync via the Nvidia driver avoids the crash without disabling hardware acceleration.
|
||||
#[cfg(target_os = "linux")]
|
||||
if std::env::var("__NV_DISABLE_EXPLICIT_SYNC").is_err()
|
||||
&& std::env::var("WAYLAND_DISPLAY").is_ok()
|
||||
&& std::path::Path::new("/sys/module/nvidia").exists()
|
||||
{
|
||||
// SAFETY: called before any threads are spawned.
|
||||
unsafe { std::env::set_var("__NV_DISABLE_EXPLICIT_SYNC", "1") };
|
||||
}
|
||||
|
||||
tauri_app_client_lib::run();
|
||||
}
|
||||
|
||||
@@ -949,7 +949,7 @@ pub struct ParentHeaders {
|
||||
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")]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
#[enum_def(table_name = "folders")]
|
||||
|
||||
@@ -30,29 +30,33 @@ impl<'a> ClientDb<'a> {
|
||||
self.find_many(HttpResponseIden::WorkspaceId, workspace_id, limit)
|
||||
}
|
||||
|
||||
/// Returns the number of responses deleted.
|
||||
pub fn delete_all_http_responses_for_request(
|
||||
&self,
|
||||
request_id: &str,
|
||||
source: &UpdateSource,
|
||||
) -> Result<()> {
|
||||
) -> Result<usize> {
|
||||
let responses = self.list_http_responses_for_request(request_id, None)?;
|
||||
let count = responses.len();
|
||||
for m in responses {
|
||||
self.delete(&m, source)?;
|
||||
}
|
||||
Ok(())
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Returns the number of responses deleted.
|
||||
pub fn delete_all_http_responses_for_workspace(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
source: &UpdateSource,
|
||||
) -> Result<()> {
|
||||
) -> Result<usize> {
|
||||
let responses =
|
||||
self.find_many::<HttpResponse>(HttpResponseIden::WorkspaceId, workspace_id, None)?;
|
||||
let count = responses.len();
|
||||
for m in responses {
|
||||
self.delete(&m, source)?;
|
||||
}
|
||||
Ok(())
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub fn delete_http_response(
|
||||
|
||||
Reference in New Issue
Block a user