snapshot of https://github.com/mountain-loop/yaak.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Lint and Test / Lint/Test (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-23 17:39:41 +05:00
commit 906fb231cc
1544 changed files with 164131 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
# Copy as cUrl
A request action plugin for Yaak that converts HTTP requests into [curl](https://curl.se)
commands, making it easy to share, debug, and execute requests outside Yaak.
![Screenshot of context menu](screenshot.png)
## Overview
This plugin adds a 'Copy as Curl' action to HTTP requests, converting any request into its
equivalent curl command. This is useful for debugging, sharing requests with team members,
and executing requests in terminal environments where `curl` is available.
## How It Works
The plugin analyzes the given HTTP request and generates a properly formatted curl command
that includes:
- HTTP method (GET, POST, PUT, DELETE, etc.)
- Request URL with query parameters
- Headers (including authentication headers)
- Request body (for POST, PUT, PATCH requests)
- Authentication credentials
## Usage
1. Configure an HTTP request as usual in Yaak
2. Right-click on the request in the sidebar
3. Select 'Copy as Curl'
4. The command is copied to your clipboard
5. Share or execute the command
## Generated Curl Examples
### Simple GET Request
```bash
curl -X GET 'https://api.example.com/users' \
--header 'Accept: application/json'
```
### POST Request with JSON Data
```bash
curl -X POST 'https://api.example.com/users' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data '{
"name": "John Doe",
"email": "john@example.com"
}'
```
### Request with Multi-part Form Data
```bash
curl -X POST 'yaak.app' \
--header 'Content-Type: multipart/form-data' \
--form 'hello=world' \
--form file=@/path/to/file.json
```
### Request with Authentication
```bash
curl -X GET 'https://api.example.com/protected' \
--user 'username:password'
```

View File

@@ -0,0 +1,17 @@
{
"name": "@yaak/action-copy-curl",
"displayName": "Copy as Curl",
"version": "0.1.0",
"private": true,
"description": "Copy request as a curl command",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/action-copy-curl"
},
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"test": "vp test --run tests"
}
}

View File

@@ -0,0 +1,179 @@
import type { HttpRequest, PluginDefinition } from "@yaakapp/api";
const NEWLINE = "\\\n ";
export const plugin: PluginDefinition = {
httpRequestActions: [
{
label: "Copy as Curl",
icon: "copy",
async onSelect(ctx, args) {
const rendered_request = await ctx.httpRequest.render({
httpRequest: args.httpRequest,
purpose: "send",
});
const data = await convertToCurl(rendered_request);
await ctx.clipboard.copyText(data);
await ctx.toast.show({
message: "Command copied to clipboard",
icon: "copy",
color: "success",
});
},
},
],
};
export async function convertToCurl(request: Partial<HttpRequest>) {
const xs = ["curl"];
// Add method and URL all on first line
if (request.method) xs.push("-X", request.method);
// Build final URL with parameters (compatible with old curl)
let finalUrl = request.url || "";
const urlParams = (request.urlParameters ?? []).filter(onlyEnabled);
if (urlParams.length > 0) {
// Build url
const [base, hash] = finalUrl.split("#");
const separator = base?.includes("?") ? "&" : "?";
const queryString = urlParams
.map((p) => `${encodeURIComponent(p.name)}=${encodeURIComponent(p.value)}`)
.join("&");
finalUrl = base + separator + queryString + (hash ? `#${hash}` : "");
}
// Add API key authentication
if (request.authenticationType === "apikey") {
if (request.authentication?.location === "query") {
const sep = finalUrl.includes("?") ? "&" : "?";
finalUrl = [
finalUrl,
sep,
encodeURIComponent(request.authentication?.key ?? "token"),
"=",
encodeURIComponent(request.authentication?.value ?? ""),
].join("");
} else {
request.headers = request.headers ?? [];
request.headers.push({
name: request.authentication?.key ?? "X-Api-Key",
value: request.authentication?.value ?? "",
});
}
}
xs.push(quote(finalUrl));
xs.push(NEWLINE);
// Add headers
for (const h of (request.headers ?? []).filter(onlyEnabled)) {
xs.push("--header", quote(`${h.name}: ${h.value}`));
xs.push(NEWLINE);
}
// Add form params
const type = request.bodyType ?? "none";
if (
(type === "multipart/form-data" || type === "application/x-www-form-urlencoded") &&
Array.isArray(request.body?.form)
) {
const flag = request.bodyType === "multipart/form-data" ? "--form" : "--data";
for (const p of (request.body?.form ?? []).filter(onlyEnabled)) {
if (p.file) {
let v = `${p.name}=@${p.file}`;
v += p.contentType ? `;type=${p.contentType}` : "";
// A bare `;` separates commands and a path can hold spaces, so this
// argument needs quoting like every other one.
xs.push(flag, quote(v));
} else {
xs.push(flag, quote(`${p.name}=${p.value}`));
}
xs.push(NEWLINE);
}
} else if (type === "graphql" && typeof request.body?.query === "string") {
const body = {
query: request.body.query || "",
variables: maybeParseJSON(request.body.variables, undefined),
operationName: request.body.operationName || undefined,
};
xs.push("--data", quote(JSON.stringify(body)));
xs.push(NEWLINE);
} else if (type !== "none" && typeof request.body?.text === "string") {
xs.push("--data", quote(request.body.text));
xs.push(NEWLINE);
}
// Add basic/digest authentication
if (request.authentication?.disabled !== true) {
if (request.authenticationType === "basic" || request.authenticationType === "digest") {
if (request.authenticationType === "digest") xs.push("--digest");
xs.push(
"--user",
quote(
`${request.authentication?.username ?? ""}:${request.authentication?.password ?? ""}`,
),
);
xs.push(NEWLINE);
}
// Add bearer authentication
if (request.authenticationType === "bearer") {
const value =
`${request.authentication?.prefix ?? "Bearer"} ${request.authentication?.token ?? ""}`.trim();
xs.push("--header", quote(`Authorization: ${value}`));
xs.push(NEWLINE);
}
if (request.authenticationType === "auth-aws-sig-v4") {
xs.push(
"--aws-sigv4",
[
"aws",
"amz",
request.authentication?.region ?? "",
request.authentication?.service ?? "",
].join(":"),
);
xs.push(NEWLINE);
xs.push(
"--user",
quote(
`${request.authentication?.accessKeyId ?? ""}:${request.authentication?.secretAccessKey ?? ""}`,
),
);
if (request.authentication?.sessionToken) {
xs.push(NEWLINE);
xs.push("--header", quote(`X-Amz-Security-Token: ${request.authentication.sessionToken}`));
}
xs.push(NEWLINE);
}
}
// Remove trailing newline
if (xs[xs.length - 1] === NEWLINE) {
xs.splice(xs.length - 1, 1);
}
return xs.join(" ");
}
function quote(arg: string): string {
// A single-quoted POSIX string takes no escapes, so `\'` does not close it:
// the string ends one character early and the rest of the command is left
// dangling. Step out of the quotes, emit an escaped quote, step back in.
const escaped = arg.replace(/'/g, `'\\''`);
return `'${escaped}'`;
}
function onlyEnabled(v: { name?: string; enabled?: boolean }): boolean {
return v.enabled !== false && !!v.name;
}
function maybeParseJSON<T>(v: string, fallback: T) {
try {
return JSON.parse(v);
} catch {
return fallback;
}
}

View File

@@ -0,0 +1,470 @@
import { describe, expect, test } from "vite-plus/test";
import { convertToCurl } from "../src";
describe("exporter-curl", () => {
test("Exports GET with params", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
urlParameters: [
{ name: "a", value: "aaa" },
{ name: "b", value: "bbb", enabled: true },
{ name: "c", value: "ccc", enabled: false },
],
}),
).toEqual([`curl 'https://yaak.app?a=aaa&b=bbb'`].join(" \\n "));
});
test("Exports GET with params and hash", async () => {
expect(
await convertToCurl({
url: "https://yaak.app/path#section",
urlParameters: [
{ name: "a", value: "aaa" },
{ name: "b", value: "bbb", enabled: true },
{ name: "c", value: "ccc", enabled: false },
],
}),
).toEqual([`curl 'https://yaak.app/path?a=aaa&b=bbb#section'`].join(" \\n "));
});
test("Exports POST with url form data", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
method: "POST",
bodyType: "application/x-www-form-urlencoded",
body: {
form: [
{ name: "a", value: "aaa" },
{ name: "b", value: "bbb", enabled: true },
{ name: "c", value: "ccc", enabled: false },
],
},
}),
).toEqual(
[`curl -X POST 'https://yaak.app'`, `--data 'a=aaa'`, `--data 'b=bbb'`].join(" \\\n "),
);
});
test("Exports POST with GraphQL data", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
method: "POST",
bodyType: "graphql",
body: {
query: "{foo,bar}",
variables: '{"a": "aaa", "b": "bbb"}',
},
}),
).toEqual(
[
`curl -X POST 'https://yaak.app'`,
`--data '{"query":"{foo,bar}","variables":{"a":"aaa","b":"bbb"}}'`,
].join(" \\\n "),
);
});
test("Exports POST with GraphQL operation name", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
method: "POST",
bodyType: "graphql",
body: {
query: "query Foo { foo } query Bar { bar }",
operationName: "Foo",
},
}),
).toEqual(
[
`curl -X POST 'https://yaak.app'`,
`--data '{"query":"query Foo { foo } query Bar { bar }","operationName":"Foo"}'`,
].join(" \\\n "),
);
});
test("Exports POST with GraphQL data no variables", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
method: "POST",
bodyType: "graphql",
body: {
query: "{foo,bar}",
},
}),
).toEqual(
[`curl -X POST 'https://yaak.app'`, `--data '{"query":"{foo,bar}"}'`].join(" \\\n "),
);
});
test("Exports PUT with multipart form", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
method: "PUT",
bodyType: "multipart/form-data",
body: {
form: [
{ name: "a", value: "aaa" },
{ name: "b", value: "bbb", enabled: true },
{ name: "c", value: "ccc", enabled: false },
{ name: "f", file: "/foo/bar.png", contentType: "image/png" },
],
},
}),
).toEqual(
[
`curl -X PUT 'https://yaak.app'`,
`--form 'a=aaa'`,
`--form 'b=bbb'`,
"--form 'f=@/foo/bar.png;type=image/png'",
].join(" \\\n "),
);
});
test("Exports JSON body", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
method: "POST",
bodyType: "application/json",
body: {
text: `{"foo":"bar's"}`,
},
headers: [{ name: "Content-Type", value: "application/json" }],
}),
).toEqual(
[
`curl -X POST 'https://yaak.app'`,
`--header 'Content-Type: application/json'`,
`--data '{"foo":"bar'\\''s"}'`,
].join(" \\\n "),
);
});
test("Quotes an apostrophe so the command still parses", async () => {
// POSIX single quotes take no escapes: `\'` ends the string one
// character early and everything after it is left dangling, so the copied
// command is a syntax error rather than a request.
const command = await convertToCurl({
url: "https://yaak.app/it's",
method: "POST",
bodyType: "application/json",
body: { text: `{"note":"don't stop"}` },
headers: [{ name: "X-Note", value: "it's fine" }],
});
expect(command).toEqual(
[
`curl -X POST 'https://yaak.app/it'\\''s'`,
`--header 'X-Note: it'\\''s fine'`,
`--data '{"note":"don'\\''t stop"}'`,
].join(" \\\n "),
);
});
test("Quotes a file form field so its type suffix survives", async () => {
// A bare `;` separates commands, so an unquoted `f=@x.png;type=image/png`
// reaches curl as `f=@x.png` and the rest runs as its own command.
expect(
await convertToCurl({
url: "https://yaak.app",
method: "POST",
bodyType: "multipart/form-data",
body: { form: [{ name: "f", file: "/my files/a.png", contentType: "image/png" }] },
}),
).toEqual(
[`curl -X POST 'https://yaak.app'`, `--form 'f=@/my files/a.png;type=image/png'`].join(
" \\\n ",
),
);
});
test("Exports multi-line JSON body", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
method: "POST",
bodyType: "application/json",
body: {
text: `{"foo":"bar",\n"baz":"qux"}`,
},
headers: [{ name: "Content-Type", value: "application/json" }],
}),
).toEqual(
[
`curl -X POST 'https://yaak.app'`,
`--header 'Content-Type: application/json'`,
`--data '{"foo":"bar",\n"baz":"qux"}'`,
].join(" \\\n "),
);
});
test("Exports headers", async () => {
expect(
await convertToCurl({
headers: [
{ name: "a", value: "aaa" },
{ name: "b", value: "bbb", enabled: true },
{ name: "c", value: "ccc", enabled: false },
],
}),
).toEqual([`curl ''`, `--header 'a: aaa'`, `--header 'b: bbb'`].join(" \\\n "));
});
test("Basic auth", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
authenticationType: "basic",
authentication: {
username: "user",
password: "pass",
},
}),
).toEqual([`curl 'https://yaak.app'`, `--user 'user:pass'`].join(" \\\n "));
});
test("Basic auth disabled", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
authenticationType: "basic",
authentication: {
disabled: true,
username: "user",
password: "pass",
},
}),
).toEqual([`curl 'https://yaak.app'`].join(" \\\n "));
});
test("Broken basic auth", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
authenticationType: "basic",
authentication: {},
}),
).toEqual([`curl 'https://yaak.app'`, `--user ':'`].join(" \\\n "));
});
test("Digest auth", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
authenticationType: "digest",
authentication: {
username: "user",
password: "pass",
},
}),
).toEqual([`curl 'https://yaak.app'`, `--digest --user 'user:pass'`].join(" \\\n "));
});
test("Bearer auth", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
authenticationType: "bearer",
authentication: {
token: "tok",
},
}),
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer tok'`].join(" \\\n "));
});
test("Bearer auth with custom prefix", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
authenticationType: "bearer",
authentication: {
token: "abc123",
prefix: "Token",
},
}),
).toEqual(
[`curl 'https://yaak.app'`, `--header 'Authorization: Token abc123'`].join(" \\\n "),
);
});
test("Bearer auth with empty prefix", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
authenticationType: "bearer",
authentication: {
token: "xyz789",
prefix: "",
},
}),
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: xyz789'`].join(" \\\n "));
});
test("Broken bearer auth", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
authenticationType: "bearer",
authentication: {
username: "user",
password: "pass",
},
}),
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer'`].join(" \\\n "));
});
test("AWS v4 auth", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
authenticationType: "auth-aws-sig-v4",
authentication: {
accessKeyId: "ak",
secretAccessKey: "sk",
sessionToken: "",
region: "us-east-1",
service: "s3",
},
}),
).toEqual(
[`curl 'https://yaak.app'`, "--aws-sigv4 aws:amz:us-east-1:s3", `--user 'ak:sk'`].join(
" \\\n ",
),
);
});
test("AWS v4 auth with session", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
authenticationType: "auth-aws-sig-v4",
authentication: {
accessKeyId: "ak",
secretAccessKey: "sk",
sessionToken: "st",
region: "us-east-1",
service: "s3",
},
}),
).toEqual(
[
`curl 'https://yaak.app'`,
"--aws-sigv4 aws:amz:us-east-1:s3",
`--user 'ak:sk'`,
`--header 'X-Amz-Security-Token: st'`,
].join(" \\\n "),
);
});
test("API key auth header", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
authenticationType: "apikey",
authentication: {
location: "header",
key: "X-Header",
value: "my-token",
},
}),
).toEqual([`curl 'https://yaak.app'`, `--header 'X-Header: my-token'`].join(" \\\n "));
});
test("API key auth header query", async () => {
expect(
await convertToCurl({
url: "https://yaak.app?hi=there",
urlParameters: [{ name: "param", value: "hi" }],
authenticationType: "apikey",
authentication: {
location: "query",
key: "foo",
value: "bar",
},
}),
).toEqual([`curl 'https://yaak.app?hi=there&param=hi&foo=bar'`].join(" \\\n "));
});
test("API key auth header query with params", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
urlParameters: [{ name: "param", value: "hi" }],
authenticationType: "apikey",
authentication: {
location: "query",
key: "foo",
value: "bar",
},
}),
).toEqual([`curl 'https://yaak.app?param=hi&foo=bar'`].join(" \\\n "));
});
test("API key auth header default", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
authenticationType: "apikey",
authentication: {
location: "header",
},
}),
).toEqual([`curl 'https://yaak.app'`, `--header 'X-Api-Key: '`].join(" \\\n "));
});
test("API key auth query", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
authenticationType: "apikey",
authentication: {
location: "query",
key: "foo",
value: "bar-baz",
},
}),
).toEqual([`curl 'https://yaak.app?foo=bar-baz'`].join(" \\\n "));
});
test("API key auth query with existing", async () => {
expect(
await convertToCurl({
url: "https://yaak.app?foo=bar&baz=qux",
authenticationType: "apikey",
authentication: {
location: "query",
key: "hi",
value: "there",
},
}),
).toEqual([`curl 'https://yaak.app?foo=bar&baz=qux&hi=there'`].join(" \\\n "));
});
test("API key auth query default", async () => {
expect(
await convertToCurl({
url: "https://yaak.app?foo=bar&baz=qux",
authenticationType: "apikey",
authentication: {
location: "query",
},
}),
).toEqual([`curl 'https://yaak.app?foo=bar&baz=qux&token='`].join(" \\\n "));
});
test("Stale body data", async () => {
expect(
await convertToCurl({
url: "https://yaak.app",
bodyType: "none",
body: {
text: "ignore me",
},
}),
).toEqual([`curl 'https://yaak.app'`].join(" \\\n "));
});
});

View File

@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}