Initial commit
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
# 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 |
|
||||
Reference in New Issue
Block a user