342 lines
11 KiB
Markdown
342 lines
11 KiB
Markdown
# greq
|
||
|
||
**greq** — a git-native, TUI-based API client for the terminal. Like Postman, but it lives in your `.greq/` folder, is version-controllable, and never opens a browser.
|
||
|
||
```
|
||
╭──────────────────────────╮╭──────────────────────────────────────────────────╮
|
||
│ GREQ [1:default] ││ → POST https://api.example.com/auth/login │
|
||
├──────────────────────────┤│──────────────────────────────────────────────────│
|
||
│ ▶ GET Health Check ││ { │
|
||
│ ▸ AUTH ││ "token": "eyJhbGciOiJIUzI1NiJ9...", │
|
||
│ ▶ POST Login ││ "user_id": 42 │
|
||
│ GET Refresh ││ } │
|
||
│ ▸ USERS ││ │
|
||
│ GET List Users ││──────────────────────────────────────────────────│
|
||
│ GET Get User ││ ● 200 OK │ ⚡ 213ms │ ◎ 1.2 KB │ app/json │
|
||
╰──────────────────────────╯╰──────────────────────────────────────────────────╯
|
||
Enter:run s:stress c:curl i:import d:diff S:snapshot h:history n:new e:edit
|
||
```
|
||
|
||
---
|
||
|
||
## Installation
|
||
|
||
```bash
|
||
git clone <repo>
|
||
cd greq
|
||
go build -o greq .
|
||
./greq # looks for .greq/ in the current directory
|
||
```
|
||
|
||
> **Dependencies:** Go 1.21+. For clipboard support on Linux you may need `xclip` (X11) or `wl-clipboard` (Wayland).
|
||
|
||
---
|
||
|
||
## Project structure
|
||
|
||
```
|
||
greq/
|
||
├── main.go # entry point
|
||
├── internal/
|
||
│ ├── api/
|
||
│ │ ├── client.go # HTTP client, variable resolution, token extraction
|
||
│ │ ├── chain.go # request chaining ({{last_FIELD}})
|
||
│ │ ├── curl.go # cURL import / export
|
||
│ │ ├── lua_test_runner.go # Lua test script runner (gopher-lua)
|
||
│ │ ├── script.go # pre_script shell execution
|
||
│ │ └── stress.go # load test runner (100×, 10 workers)
|
||
│ ├── storage/
|
||
│ │ ├── types.go # RequestFile, RequestItem structs
|
||
│ │ ├── loader.go # YAML loading, env list
|
||
│ │ ├── writer.go # YAML saving
|
||
│ │ └── config.go # .greq/config.yaml (e.g. editor setting)
|
||
│ └── tui/
|
||
│ ├── model.go # Bubble Tea Model, states, item types
|
||
│ ├── update.go # Update logic, key handlers, commands
|
||
│ ├── view.go # View rendering (header, panels, overlays)
|
||
│ ├── styles.go # Lipgloss styles, colours, env colour
|
||
│ ├── delegate.go # Custom list delegate (folder headers)
|
||
│ ├── history.go # HistoryEntry, stats bar, history table
|
||
│ ├── diff.go # LCS line diff, snapshot save/load
|
||
│ └── editor_picker.go # Available editor detection
|
||
└── .greq/ # project-level data (can be committed to git!)
|
||
├── config.yaml # user settings
|
||
├── env.yaml # default environment
|
||
├── env.local.yaml # optional: local env
|
||
├── env.staging.yaml # optional: staging env
|
||
├── env.prod.yaml # optional: prod env
|
||
├── requests/
|
||
│ ├── health.yaml # root-level request
|
||
│ ├── auth/
|
||
│ │ └── login.yaml # auth folder
|
||
│ └── users/
|
||
│ ├── list.yaml
|
||
│ └── get_user.yaml
|
||
└── snapshots/ # response baselines (d/S keys)
|
||
└── login.json
|
||
```
|
||
|
||
---
|
||
|
||
## .greq/ folder layout
|
||
|
||
### `env.yaml` — variables
|
||
|
||
```yaml
|
||
base_url: https://api.example.com
|
||
user_id: "42"
|
||
api_key: secret123
|
||
```
|
||
|
||
Reference them in any request with `{{base_url}}`, `{{user_id}}`, etc.
|
||
|
||
### Request YAML schema
|
||
|
||
```yaml
|
||
name: Login # display name in the list
|
||
method: POST # GET / POST / PUT / DELETE / PATCH
|
||
url: "{{base_url}}/auth/login" # variables allowed
|
||
|
||
headers:
|
||
Content-Type: application/json
|
||
X-Api-Key: "{{api_key}}"
|
||
|
||
body: |
|
||
{
|
||
"username": "admin",
|
||
"password": "secret"
|
||
}
|
||
|
||
# Shell script — runs BEFORE the request (to generate extra headers)
|
||
pre_script: |
|
||
TS=$(date +%s)
|
||
echo "X-Timestamp: $TS"
|
||
|
||
# Lua script — runs AFTER the response (for assertions)
|
||
test_script: |
|
||
assert_status(200)
|
||
assert_eq(json_body.success, true, "login successful")
|
||
if json_body.token then pass("token present") else fail("no token") end
|
||
```
|
||
|
||
### `config.yaml` — settings
|
||
|
||
```yaml
|
||
editor: nvim # default editor for the 'e' key
|
||
```
|
||
|
||
---
|
||
|
||
## Keybindings
|
||
|
||
### Main view
|
||
|
||
| Key | Action |
|
||
|---|---|
|
||
| `Enter` | Run the selected request |
|
||
| `s` | Load test (100 requests, Min/Max/Avg/error rate) |
|
||
| `e` | Edit request in `$EDITOR` (or configured editor) |
|
||
| `n` | New request wizard |
|
||
| `i` | Import a cURL command |
|
||
| `c` | Copy selected request as cURL |
|
||
| `d` | Diff: current response vs. saved baseline |
|
||
| `S` | Save current response as baseline |
|
||
| `h` | History overlay (all requests in the session) |
|
||
| `r` | Reload request list and env |
|
||
| `/` | Fuzzy search across requests |
|
||
| `Tab` | Toggle focus: list ↔ response panel |
|
||
| `1`–`9` | Switch environment (env.local.yaml, env.staging.yaml, etc.) |
|
||
| `q` / `Ctrl+C` | Quit |
|
||
|
||
### Response panel (after Tab)
|
||
|
||
| Key | Action |
|
||
|---|---|
|
||
| `↑` / `↓` | Scroll |
|
||
| `PgUp` / `PgDn` | Page scroll |
|
||
| `Tab` | Return to list |
|
||
|
||
### Overlays (History, Diff)
|
||
|
||
| Key | Action |
|
||
|---|---|
|
||
| `↑` / `↓` | Scroll |
|
||
| `Esc` / `h` / `d` | Close |
|
||
|
||
---
|
||
|
||
## Features
|
||
|
||
### Variables and Request Chaining
|
||
|
||
Values from `env.yaml` can be used as `{{key}}` placeholders in URLs, headers, and bodies.
|
||
|
||
If a response contains JSON, every top-level field is automatically available in the next request as `{{last_FIELD}}`:
|
||
|
||
```
|
||
Login response: { "id": 42, "token": "abc" }
|
||
→ {{last_id}} = "42"
|
||
→ {{last_token}} = "abc"
|
||
|
||
Next request URL: {{base_url}}/users/{{last_id}}
|
||
```
|
||
|
||
The `token`, `access_token`, and `jwt` fields are also automatically injected as `Authorization: Bearer …` headers in subsequent requests.
|
||
|
||
### Lua test scripts
|
||
|
||
`test_script` runs in Lua after the response arrives.
|
||
|
||
**Available variables:**
|
||
|
||
| Variable | Type | Description |
|
||
|---|---|---|
|
||
| `status` | number | HTTP status code (e.g. `200`) |
|
||
| `status_text` | string | Full status string (e.g. `"200 OK"`) |
|
||
| `body` | string | Raw response body |
|
||
| `json_body` | table / nil | Parsed JSON, or `nil` |
|
||
| `headers` | table | Response headers (lowercase keys) |
|
||
| `duration_ms` | number | Response time in milliseconds |
|
||
| `size` | number | Body size in bytes |
|
||
|
||
**Helper functions:**
|
||
|
||
```lua
|
||
pass("message") -- green assertion
|
||
fail("message") -- red assertion
|
||
assert_eq(a, b, "message") -- fail if a ~= b
|
||
assert_status(200) -- fail if status does not match
|
||
json_decode("...") -- JSON string → Lua table
|
||
```
|
||
|
||
**Example:**
|
||
|
||
```lua
|
||
assert_status(200)
|
||
|
||
if json_body == nil then
|
||
fail("response is not JSON")
|
||
return
|
||
end
|
||
|
||
assert_eq(json_body.role, "admin", "admin permission")
|
||
|
||
if duration_ms > 500 then
|
||
fail("too slow: " .. duration_ms .. "ms")
|
||
else
|
||
pass("response time OK: " .. duration_ms .. "ms")
|
||
end
|
||
```
|
||
|
||
Results appear in the stats bar badge: `✓ 3 tests` or `✗ 1/3 failed`.
|
||
|
||
### Shell pre-script
|
||
|
||
`pre_script` runs **before** the request, in a shell. Lines printed to stdout matching `Key: Value` are injected as extra request headers.
|
||
|
||
```yaml
|
||
pre_script: |
|
||
TS=$(date +%s)
|
||
SIG=$(echo -n "POST${TS}" | openssl dgst -sha256 -hmac "$HMAC_SECRET" -binary | base64)
|
||
echo "X-Timestamp: $TS"
|
||
echo "X-Signature: $SIG"
|
||
```
|
||
|
||
If the script exits with a non-zero code, the request is aborted.
|
||
|
||
### cURL Import / Export
|
||
|
||
**Export (`c`):** Copies the selected request as a ready-to-use `curl` command to the clipboard (variables resolved). If no clipboard tool is available, the command is displayed in the right panel.
|
||
|
||
**Import (`i`):** Paste a `curl` command (e.g. from Chrome DevTools → Copy as cURL) and greq parses it automatically, opening the wizard pre-filled.
|
||
|
||
### Load Testing ⚡
|
||
|
||
Press `s` on any selected request and greq fires it **100 times** concurrently (10 workers at a time).
|
||
|
||
The response panel shows the aggregated stats:
|
||
|
||
```
|
||
⚡ LOAD TEST RESULTS
|
||
|
||
Requests 100
|
||
Concurrency 10 workers
|
||
|
||
Min 43ms
|
||
Avg 97ms
|
||
Max 312ms
|
||
|
||
Errors 0 (0.0%)
|
||
```
|
||
|
||
If any requests failed, the `Errors` line is highlighted in red. Press `Enter` afterwards to go back to normal request mode.
|
||
|
||
### Response Diff
|
||
|
||
```
|
||
S → saves the current response as a baseline (.greq/snapshots/)
|
||
d → shows what changed since the baseline (red/green per line)
|
||
```
|
||
|
||
Useful when hitting the same endpoint at different times and wanting to see exactly what changed in the response.
|
||
|
||
### Environments (`1`–`9`)
|
||
|
||
Multiple env files can coexist:
|
||
|
||
```
|
||
.greq/env.yaml → 1 (default, purple header)
|
||
.greq/env.local.yaml → 2 (green header)
|
||
.greq/env.staging.yaml → 3 (yellow header)
|
||
.greq/env.prod.yaml → 4 (RED header — warning)
|
||
```
|
||
|
||
The header colour instantly signals which environment is active, so you don't accidentally delete production data.
|
||
|
||
---
|
||
|
||
## Quick start
|
||
|
||
```bash
|
||
# 1. Create the .greq folder at your project root
|
||
mkdir -p .greq/requests/auth
|
||
|
||
# 2. Write an env file
|
||
cat > .greq/env.yaml << EOF
|
||
base_url: https://api.example.com
|
||
EOF
|
||
|
||
# 3. Create a request
|
||
cat > .greq/requests/auth/login.yaml << EOF
|
||
name: Login
|
||
method: POST
|
||
url: "{{base_url}}/auth/login"
|
||
headers:
|
||
Content-Type: application/json
|
||
body: |
|
||
{"username": "admin", "password": "secret"}
|
||
test_script: |
|
||
assert_status(200)
|
||
if json_body.token then pass("token OK") else fail("no token") end
|
||
EOF
|
||
|
||
# 4. Run it
|
||
./greq
|
||
```
|
||
|
||
`greq` always looks for the `.greq/` folder in the **current directory**, so each git repo can have its own request set.
|
||
|
||
---
|
||
|
||
## Dependencies
|
||
|
||
| Package | Purpose |
|
||
|---|---|
|
||
| `charmbracelet/bubbletea` | TUI framework |
|
||
| `charmbracelet/bubbles` | List, textinput, viewport components |
|
||
| `charmbracelet/lipgloss` | Styles, colours, layout |
|
||
| `alecthomas/chroma/v2` | JSON syntax highlighting |
|
||
| `yuin/gopher-lua` | Lua interpreter for test scripts |
|
||
| `atotto/clipboard` | Clipboard support |
|
||
| `gopkg.in/yaml.v3` | YAML read/write |
|