ghostrun-cli 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/CODE_OF_CONDUCT.md +41 -0
- package/CONTRIBUTING.md +90 -0
- package/README.md +259 -418
- package/REFERENCE.md +165 -0
- package/ghostrun.js +65 -28
- package/package.json +12 -4
package/README.md
CHANGED
|
@@ -1,10 +1,62 @@
|
|
|
1
1
|
# GhostRun
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/ghostrun-cli)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](http://makeapullrequest.com)
|
|
6
|
+
[](https://www.npmjs.com/package/ghostrun-cli)
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
**Record once. Replay as a ghost.**
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
GhostRun is a local-first CLI for browser automation, API testing, and load testing — all in one tool. Record a real browser flow, replay it headlessly, test REST APIs with assertions, and run VU-based load tests. No cloud. No account. Runs entirely on your machine.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [What is GhostRun?](#what-is-ghostrun)
|
|
17
|
+
- [Install](#install)
|
|
18
|
+
- [Quick Start](#quick-start)
|
|
19
|
+
- [The Three Modes](#the-three-modes)
|
|
20
|
+
- [Browser Automation](#1-browser-automation)
|
|
21
|
+
- [API Testing](#2-api-testing)
|
|
22
|
+
- [Load Testing](#3-load-testing)
|
|
23
|
+
- [Commands](#commands)
|
|
24
|
+
- [Setup](#setup)
|
|
25
|
+
- [Recording](#recording)
|
|
26
|
+
- [Running Flows](#running-flows)
|
|
27
|
+
- [Flow Management](#flow-management)
|
|
28
|
+
- [Environments](#environments)
|
|
29
|
+
- [Run History](#run-history)
|
|
30
|
+
- [Scheduling](#scheduling)
|
|
31
|
+
- [Test Suites](#test-suites)
|
|
32
|
+
- [Web Dashboard](#web-dashboard)
|
|
33
|
+
- [Chat Assistant](#chat-assistant)
|
|
34
|
+
- [MCP Server](#mcp-server)
|
|
35
|
+
- [Reports](#reports)
|
|
36
|
+
- [Selector Repair](#selector-repair)
|
|
37
|
+
- [Screenshot Diff](#screenshot-diff)
|
|
38
|
+
- [CI/CD Integration](#cicd-integration)
|
|
39
|
+
- [AI Setup](#ai-setup)
|
|
40
|
+
- [Data & Privacy](#data--privacy)
|
|
41
|
+
- [Contributing](#contributing)
|
|
42
|
+
- [Trust & Transparency](#trust--transparency)
|
|
43
|
+
- [License](#license)
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## What is GhostRun?
|
|
48
|
+
|
|
49
|
+
Most testing tools make you choose — browser OR API OR load testing. GhostRun does all three from a single CLI, using the same flow format.
|
|
50
|
+
|
|
51
|
+
| What you want to do | How GhostRun helps |
|
|
52
|
+
|---------------------|-------------------|
|
|
53
|
+
| Test a web UI regression | Record a browser flow once, replay headlessly on every deploy |
|
|
54
|
+
| Test a REST API | Write or import flows with HTTP requests, assertions, and variable extraction |
|
|
55
|
+
| Stress-test an endpoint | Run any API flow as a load test with configurable VUs and duration |
|
|
56
|
+
| Monitor a live site | Schedule flows to run on a cron and alert on failure |
|
|
57
|
+
| Give AI agents your test suite | MCP server exposes all flows as tools for Claude, Cursor, etc. |
|
|
58
|
+
|
|
59
|
+
Everything is stored locally in SQLite (`~/.ghostrun/`). No accounts, no telemetry, no cloud.
|
|
8
60
|
|
|
9
61
|
---
|
|
10
62
|
|
|
@@ -12,105 +64,116 @@ Memory-driven browser automation and API testing CLI — record real browser flo
|
|
|
12
64
|
|
|
13
65
|
```bash
|
|
14
66
|
npm install -g ghostrun-cli
|
|
67
|
+
ghostrun init # guided setup: installs Chromium, configures AI (optional)
|
|
15
68
|
```
|
|
16
69
|
|
|
17
70
|
Or run from source:
|
|
18
71
|
|
|
19
72
|
```bash
|
|
20
|
-
git clone https://github.com/
|
|
21
|
-
cd ghostrun
|
|
22
|
-
|
|
23
|
-
npm run build
|
|
24
|
-
node ghostrun.js init # guided setup
|
|
73
|
+
git clone https://github.com/TechBuiltBySharan/ghostrun
|
|
74
|
+
cd ghostrun && npm install && npm run build
|
|
75
|
+
node ghostrun.js init
|
|
25
76
|
```
|
|
26
77
|
|
|
78
|
+
**Requirements:** Node 18+, macOS/Linux/Windows
|
|
79
|
+
|
|
27
80
|
---
|
|
28
81
|
|
|
29
82
|
## Quick Start
|
|
30
83
|
|
|
84
|
+
**Record a browser flow:**
|
|
85
|
+
|
|
31
86
|
```bash
|
|
32
|
-
ghostrun
|
|
33
|
-
ghostrun
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
87
|
+
ghostrun learn https://yourapp.com # real browser opens, you interact, GhostRun records
|
|
88
|
+
ghostrun run "My Flow" # replay headlessly
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Test an API:**
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
ghostrun flow:from-curl "curl -X POST https://api.example.com/login \
|
|
95
|
+
-H 'Content-Type: application/json' \
|
|
96
|
+
-d '{\"email\":\"user@example.com\",\"password\":\"secret\"}'"
|
|
97
|
+
ghostrun run "POST /login"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Run a load test:**
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
ghostrun perf:run "POST /login" --vus 20 --duration 30
|
|
39
104
|
```
|
|
40
105
|
|
|
41
106
|
---
|
|
42
107
|
|
|
43
|
-
##
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
| Web dashboard | No | `ghostrun serve --ui` |
|
|
59
|
-
| **API testing** | No | HTTP requests, assertions, variable extraction |
|
|
60
|
-
| **Environment profiles** | No | Named env sets, injected at runtime |
|
|
61
|
-
| **Load testing** | No | VU-based perf runs, p50/p95/p99 stats |
|
|
62
|
-
| **k6 export** | No | `perf:export` generates a k6 script |
|
|
63
|
-
| **Failure analysis** | Optional ✨ | Plain-English explanation of why it failed |
|
|
64
|
-
| **Auto run summary** | Optional ✨ | Attached to every failed run automatically |
|
|
65
|
-
| **Chat assistant** | Optional ✨ | Q&A + run flows by name via `ghostrun chat` |
|
|
66
|
-
|
|
67
|
-
**Bottom line:** Record, replay, schedule, diff, and fix flows entirely offline. AI adds explanations and a conversational interface.
|
|
108
|
+
## The Three Modes
|
|
109
|
+
|
|
110
|
+
### 1. Browser Automation
|
|
111
|
+
|
|
112
|
+
GhostRun opens a real browser (Playwright/Chromium), watches your interactions, and saves them as a flow. Replay runs headlessly.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
ghostrun learn https://yourapp.com # record
|
|
116
|
+
ghostrun run <id|name> # replay headlessly
|
|
117
|
+
ghostrun run <id|name> --visible # replay with browser window visible
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**What gets recorded:** clicks, form fills, navigation, waits, checkboxes, dropdowns, keyboard input, file uploads, scroll, drag and drop.
|
|
121
|
+
|
|
122
|
+
**Selector repair:** If a flow breaks after a UI update, `ghostrun flow:fix <id>` opens the browser, replays up to the broken step, and lets you click the correct element. Selector is updated automatically — no manual JSON editing.
|
|
68
123
|
|
|
69
124
|
---
|
|
70
125
|
|
|
71
|
-
|
|
126
|
+
### 2. API Testing
|
|
72
127
|
|
|
73
|
-
|
|
128
|
+
When all steps in a flow are API actions (no `click`, `fill`, etc.), GhostRun skips Playwright entirely. Execution is ~30ms per run.
|
|
74
129
|
|
|
75
|
-
|
|
130
|
+
**Three ways to create an API flow:**
|
|
76
131
|
|
|
77
132
|
```bash
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
133
|
+
# From a curl command
|
|
134
|
+
ghostrun flow:from-curl "curl -X GET https://api.example.com/users \
|
|
135
|
+
-H 'Authorization: Bearer {{token}}'"
|
|
136
|
+
|
|
137
|
+
# From an OpenAPI/Swagger spec
|
|
138
|
+
ghostrun flow:from-spec openapi.json # JSON or YAML
|
|
139
|
+
ghostrun flow:from-spec swagger.yaml
|
|
140
|
+
|
|
141
|
+
# Import a hand-crafted .flow.json
|
|
142
|
+
ghostrun flow:import my-api-tests.flow.json
|
|
82
143
|
```
|
|
83
144
|
|
|
84
|
-
**
|
|
145
|
+
**API flow features:**
|
|
146
|
+
- HTTP requests with custom headers, JSON body, bearer auth
|
|
147
|
+
- Response assertions (status code, JSON path, headers, response time)
|
|
148
|
+
- Variable extraction from responses — use in subsequent steps
|
|
149
|
+
- Named environment profiles (dev / staging / prod)
|
|
85
150
|
|
|
86
|
-
|
|
87
|
-
|-------|------|---------|
|
|
88
|
-
| `gemma3:4b` | 2.6 GB | Apple Silicon M1/M2/M3, fast |
|
|
89
|
-
| `gemma2:9b` | 5.4 GB | Better quality, more RAM needed |
|
|
90
|
-
| `llama3.2:3b` | 2.0 GB | Fastest, lighter quality |
|
|
151
|
+
---
|
|
91
152
|
|
|
92
|
-
|
|
153
|
+
### 3. Load Testing
|
|
93
154
|
|
|
94
|
-
|
|
155
|
+
Run any API flow as a load test. GhostRun sends parallel VU requests, collects timing, and prints a latency breakdown.
|
|
95
156
|
|
|
96
157
|
```bash
|
|
97
|
-
|
|
158
|
+
ghostrun perf:run <id|name> # defaults: 10 VUs, 30s
|
|
159
|
+
ghostrun perf:run <id|name> --vus 50 --duration 60 --ramp-up 10
|
|
160
|
+
ghostrun perf:compare <run-A-id> <run-B-id> # diff two runs
|
|
161
|
+
ghostrun perf:export <id|name> # generate a k6 script
|
|
98
162
|
```
|
|
99
163
|
|
|
100
|
-
|
|
164
|
+
**Output includes:** HTTP requests, success rate, avg RPS, p50 / p95 / p99 latency, min/max, per-step breakdown, checks passed/failed.
|
|
165
|
+
|
|
166
|
+
**perf:compare** shows side-by-side deltas with color-coded improvement/regression:
|
|
101
167
|
|
|
102
168
|
```
|
|
103
|
-
|
|
169
|
+
Before After Delta
|
|
170
|
+
p50 latency 142ms 98ms ↓ 44ms ✓
|
|
171
|
+
p95 latency 310ms 201ms ↓ 109ms ✓
|
|
172
|
+
p99 latency 580ms 390ms ↓ 190ms ✓
|
|
173
|
+
avg RPS 47.2 68.1 ↑ 20.9 ✓
|
|
104
174
|
```
|
|
105
175
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
| Variable | Default | Description |
|
|
109
|
-
|----------|---------|-------------|
|
|
110
|
-
| `GHOSTRUN_AI_PROVIDER` | auto | `ollama`, `anthropic`, or auto |
|
|
111
|
-
| `GHOSTRUN_OLLAMA_URL` | `http://localhost:11434` | Ollama server URL |
|
|
112
|
-
| `GHOSTRUN_OLLAMA_MODEL` | auto-detected | Model to use |
|
|
113
|
-
| `ANTHROPIC_API_KEY` | — | Anthropic API key (cloud fallback) |
|
|
176
|
+
**perf:export** generates a valid k6 script with VU stages, `http.get`/`http.post` calls, `check()` assertions, and `Trend` metrics per step.
|
|
114
177
|
|
|
115
178
|
---
|
|
116
179
|
|
|
@@ -120,65 +183,89 @@ run → try Ollama → if down → try Anthropic → if no key → skip AI silen
|
|
|
120
183
|
|
|
121
184
|
```bash
|
|
122
185
|
ghostrun init # guided setup wizard
|
|
123
|
-
ghostrun status # stats
|
|
186
|
+
ghostrun status # stats, AI provider, data path
|
|
124
187
|
```
|
|
125
188
|
|
|
126
189
|
### Recording
|
|
127
190
|
|
|
128
191
|
```bash
|
|
129
|
-
ghostrun learn <url>
|
|
130
|
-
ghostrun learn <url> --name "Login" # with explicit name
|
|
192
|
+
ghostrun learn <url> # open browser and record a flow
|
|
193
|
+
ghostrun learn <url> --name "Login" # with an explicit name
|
|
131
194
|
```
|
|
132
195
|
|
|
133
|
-
### Running
|
|
196
|
+
### Running Flows
|
|
134
197
|
|
|
135
198
|
```bash
|
|
136
|
-
ghostrun run <id|name>
|
|
137
|
-
ghostrun run <id|name> --visible
|
|
138
|
-
ghostrun run <id|name> --
|
|
139
|
-
ghostrun run <id|name> --
|
|
140
|
-
ghostrun run <id|name> --
|
|
141
|
-
ghostrun run <id|name> --session-
|
|
199
|
+
ghostrun run <id|name> # headless execution
|
|
200
|
+
ghostrun run <id|name> --visible # show the browser window
|
|
201
|
+
ghostrun run <id|name> --var key=val # inject a variable
|
|
202
|
+
ghostrun run <id|name> --output json # JSON output (for scripting/CI)
|
|
203
|
+
ghostrun run <id|name> --report html # save an HTML run report
|
|
204
|
+
ghostrun run <id|name> --session-save <name> # save browser cookies/storage
|
|
205
|
+
ghostrun run <id|name> --session-load <name> # restore browser cookies/storage
|
|
142
206
|
```
|
|
143
207
|
|
|
144
208
|
### Flow Management
|
|
145
209
|
|
|
146
210
|
```bash
|
|
147
|
-
ghostrun flow:list
|
|
148
|
-
ghostrun flow:show <id|name>
|
|
149
|
-
ghostrun flow:
|
|
150
|
-
ghostrun flow:
|
|
151
|
-
ghostrun flow:
|
|
152
|
-
ghostrun flow:
|
|
153
|
-
ghostrun flow:
|
|
154
|
-
ghostrun flow:
|
|
211
|
+
ghostrun flow:list # list all flows
|
|
212
|
+
ghostrun flow:show <id|name> # show steps for a flow
|
|
213
|
+
ghostrun flow:rename <id|name> <new-name> # rename a flow
|
|
214
|
+
ghostrun flow:clone <id|name> # duplicate a flow
|
|
215
|
+
ghostrun flow:delete <id|name> # delete a flow
|
|
216
|
+
ghostrun flow:export <id|name> # export to .flow.json
|
|
217
|
+
ghostrun flow:import <file> # import from .flow.json
|
|
218
|
+
ghostrun flow:fix <id|name> # fix broken selectors interactively
|
|
219
|
+
ghostrun flow:from-curl "<curl>" # create a flow from a curl command
|
|
220
|
+
ghostrun flow:from-spec <file> # create flows from an OpenAPI spec
|
|
155
221
|
```
|
|
156
222
|
|
|
157
|
-
###
|
|
223
|
+
### Environments
|
|
224
|
+
|
|
225
|
+
Named variable sets injected at run time. Perfect for dev / staging / prod.
|
|
158
226
|
|
|
159
227
|
```bash
|
|
160
|
-
ghostrun
|
|
161
|
-
ghostrun
|
|
228
|
+
ghostrun env:create <name> [base-url] # create an environment
|
|
229
|
+
ghostrun env:set <name> <key=value> # add or update a variable
|
|
230
|
+
ghostrun env:list # list all environments
|
|
231
|
+
ghostrun env:show <name> # show variables in an environment
|
|
232
|
+
ghostrun env:use <name> # set as the active environment
|
|
233
|
+
ghostrun env:delete <name> # delete an environment
|
|
162
234
|
```
|
|
163
235
|
|
|
236
|
+
Reference variables in any URL, header, or body field with `{{variableName}}`. The active environment's variables are injected automatically before each run.
|
|
237
|
+
|
|
164
238
|
### Run History
|
|
165
239
|
|
|
166
240
|
```bash
|
|
167
241
|
ghostrun run:list # list recent runs
|
|
168
|
-
ghostrun run:show <id> # step
|
|
169
|
-
ghostrun run:diff <id1> <id2> #
|
|
170
|
-
ghostrun run:analyze <id> # AI failure analysis (
|
|
242
|
+
ghostrun run:show <id> # step-by-step detail, screenshots, extracted data
|
|
243
|
+
ghostrun run:diff <id1> <id2> # pixel-level screenshot comparison
|
|
244
|
+
ghostrun run:analyze <id> # AI failure analysis (requires AI setup)
|
|
245
|
+
ghostrun var:dump <run-id> # show all variables extracted during a run
|
|
171
246
|
```
|
|
172
247
|
|
|
173
248
|
### Scheduling
|
|
174
249
|
|
|
175
250
|
```bash
|
|
176
|
-
ghostrun flow:schedule <id> "<cron>" # e.g. "0 9 * * *" = daily 9am
|
|
251
|
+
ghostrun flow:schedule <id> "<cron>" # e.g. "0 9 * * *" = daily at 9am
|
|
177
252
|
ghostrun schedule:list # list all schedules
|
|
178
253
|
ghostrun schedule:remove <id> # remove a schedule
|
|
179
254
|
ghostrun serve # start the scheduler daemon
|
|
180
255
|
```
|
|
181
256
|
|
|
257
|
+
### Test Suites
|
|
258
|
+
|
|
259
|
+
Group flows and run them together:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
ghostrun suite:create <name> # create a suite
|
|
263
|
+
ghostrun suite:add <suite> <flow> # add a flow to the suite
|
|
264
|
+
ghostrun suite:run <suite> # run all flows in the suite
|
|
265
|
+
ghostrun suite:list # list suites
|
|
266
|
+
ghostrun suite:show <suite> # show flows in a suite
|
|
267
|
+
```
|
|
268
|
+
|
|
182
269
|
### Web Dashboard
|
|
183
270
|
|
|
184
271
|
```bash
|
|
@@ -186,152 +273,91 @@ ghostrun serve --ui # launch dashboard at http://localhost:30
|
|
|
186
273
|
ghostrun serve --ui --port 8080 # custom port
|
|
187
274
|
```
|
|
188
275
|
|
|
189
|
-
The dashboard shows
|
|
190
|
-
- All flows with one-click run buttons
|
|
191
|
-
- Live run log with SSE streaming
|
|
192
|
-
- Run history with status and duration
|
|
193
|
-
- Chat tab for natural-language interaction
|
|
276
|
+
The dashboard shows all flows with one-click run, a live log stream, run history, and a chat tab.
|
|
194
277
|
|
|
195
278
|
### Chat Assistant
|
|
196
279
|
|
|
280
|
+
Ask questions about your flows in plain English:
|
|
281
|
+
|
|
197
282
|
```bash
|
|
198
|
-
ghostrun chat
|
|
283
|
+
ghostrun chat
|
|
199
284
|
```
|
|
200
285
|
|
|
201
|
-
|
|
286
|
+
Examples:
|
|
202
287
|
- `did my login flow pass recently?`
|
|
203
288
|
- `what flows do I have?`
|
|
204
|
-
- `run the login flow` ← executes
|
|
289
|
+
- `run the login flow` ← executes with confirmation
|
|
205
290
|
|
|
206
|
-
|
|
291
|
+
Requires Ollama (local, free) or an Anthropic API key. See [AI Setup](#ai-setup).
|
|
207
292
|
|
|
208
|
-
|
|
209
|
-
ghostrun suite:create <name> # create a suite
|
|
210
|
-
ghostrun suite:add <suite> <flow> # add a flow to a suite
|
|
211
|
-
ghostrun suite:list # list suites
|
|
212
|
-
ghostrun suite:show <suite> # show flows in suite
|
|
213
|
-
ghostrun suite:run <suite> # run all flows in suite
|
|
214
|
-
```
|
|
293
|
+
### MCP Server
|
|
215
294
|
|
|
216
|
-
|
|
295
|
+
Expose GhostRun to AI agents (Claude Desktop, Cursor, etc.):
|
|
217
296
|
|
|
218
297
|
```bash
|
|
219
|
-
|
|
220
|
-
ghostrun baseline:clear <flow-id> # clear baselines
|
|
221
|
-
ghostrun baseline:show <flow-id> # list baselines
|
|
298
|
+
node mcp-server.js
|
|
222
299
|
```
|
|
223
300
|
|
|
224
|
-
|
|
301
|
+
Tools exposed: `list_flows`, `get_flow`, `run_flow`, `get_run_result`, `list_runs`, `delete_flow`, `get_status`.
|
|
225
302
|
|
|
226
|
-
|
|
227
|
-
ghostrun flow:from-curl # paste a curl command → instant flow
|
|
228
|
-
ghostrun flow:from-curl "curl -X POST https://api.example.com/users -H 'Content-Type: application/json' -d '{\"name\":\"Alice\"}'"
|
|
229
|
-
ghostrun flow:from-spec openapi.json # import all endpoints from an OpenAPI spec
|
|
230
|
-
ghostrun flow:from-spec swagger.yaml # YAML supported too
|
|
231
|
-
ghostrun flow:import <file> # import a .flow.json directly
|
|
232
|
-
```
|
|
303
|
+
See [MCP-SETUP.md](MCP-SETUP.md) for connection setup.
|
|
233
304
|
|
|
234
|
-
|
|
305
|
+
---
|
|
235
306
|
|
|
236
|
-
|
|
307
|
+
## Reports
|
|
237
308
|
|
|
238
309
|
```bash
|
|
239
|
-
ghostrun run <id> --report html
|
|
240
|
-
ghostrun perf:run <id> --report html
|
|
310
|
+
ghostrun run <id> --report html # browser or API run report
|
|
311
|
+
ghostrun perf:run <id> --report html # load test report
|
|
241
312
|
```
|
|
242
313
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
### API Testing
|
|
246
|
-
|
|
247
|
-
Import an API flow from a `.flow.json` file (no browser needed):
|
|
248
|
-
|
|
249
|
-
```bash
|
|
250
|
-
ghostrun api:learn # interactive: pick a .flow.json file to import
|
|
251
|
-
ghostrun flow:import <file> # import directly by path
|
|
252
|
-
ghostrun run <id|name> # runs pure API flows without launching a browser
|
|
253
|
-
```
|
|
314
|
+
Dark-themed, self-contained HTML files saved to the current directory. Include per-step timing, status, screenshots (for browser flows), and extracted data. Shareable without any external dependencies.
|
|
254
315
|
|
|
255
|
-
|
|
316
|
+
---
|
|
256
317
|
|
|
257
|
-
|
|
318
|
+
## Selector Repair
|
|
258
319
|
|
|
259
|
-
|
|
320
|
+
When a UI update breaks a selector:
|
|
260
321
|
|
|
261
322
|
```bash
|
|
262
|
-
ghostrun
|
|
263
|
-
ghostrun env:list # list all environments
|
|
264
|
-
ghostrun env:show <name> # show variables in an environment
|
|
265
|
-
ghostrun env:set <name> <key=value> # add or update a variable
|
|
266
|
-
ghostrun env:use <name> # set as active environment
|
|
267
|
-
ghostrun env:delete <name> # delete an environment
|
|
323
|
+
ghostrun flow:fix <id|name>
|
|
268
324
|
```
|
|
269
325
|
|
|
270
|
-
The
|
|
326
|
+
The browser opens, replays all passing steps automatically, then **pauses on the broken step** and asks you to click the correct element. The selector is updated and saved. No JSON editing needed.
|
|
271
327
|
|
|
272
|
-
|
|
328
|
+
---
|
|
273
329
|
|
|
274
|
-
|
|
275
|
-
ghostrun var:dump <run-id> # show all variables extracted during a run
|
|
276
|
-
```
|
|
330
|
+
## Screenshot Diff
|
|
277
331
|
|
|
278
|
-
|
|
332
|
+
Compare any two runs pixel-by-pixel — no AI needed:
|
|
279
333
|
|
|
280
334
|
```bash
|
|
281
|
-
ghostrun
|
|
282
|
-
ghostrun perf:run <id|name> --vus 20 --duration 30 --ramp-up 5
|
|
283
|
-
ghostrun perf:export <id|name> # generate a k6 script from the flow
|
|
284
|
-
ghostrun perf:export <id|name> --output mytest.js
|
|
285
|
-
ghostrun perf:list # list past perf runs
|
|
286
|
-
ghostrun perf:show <perf-run-id> # show stats for a specific perf run
|
|
335
|
+
ghostrun run:diff <run1-id> <run2-id>
|
|
287
336
|
```
|
|
288
337
|
|
|
289
|
-
**perf:run options:**
|
|
290
|
-
|
|
291
|
-
| Flag | Default | Description |
|
|
292
|
-
|------|---------|-------------|
|
|
293
|
-
| `--vus <n>` | 10 | Number of virtual users |
|
|
294
|
-
| `--duration <s>` | 30 | Test duration in seconds |
|
|
295
|
-
| `--ramp-up <s>` | 5 | Ramp-up time (VUs staggered over this window) |
|
|
296
|
-
| `--timeout <ms>` | 10000 | Per-request timeout in ms |
|
|
297
|
-
|
|
298
|
-
Output includes: HTTP Requests, HTTP Success Rate, Avg RPS, p50/p95/p99 latency, min/max, per-step breakdown, and separate Checks Passed/Failed count.
|
|
299
|
-
|
|
300
|
-
**perf:compare** — diff two runs side by side to see if a deploy made things faster or slower:
|
|
301
|
-
|
|
302
|
-
```bash
|
|
303
|
-
ghostrun perf:compare <run-A-id> <run-B-id>
|
|
304
338
|
```
|
|
339
|
+
Step Status Diff % Name
|
|
340
|
+
───────────────────────────────────────────
|
|
341
|
+
1 same 0.0% Navigate to homepage
|
|
342
|
+
2 same 0.1% Click Login
|
|
343
|
+
3 changed 12.4% Fill email field
|
|
344
|
+
4 same 0.0% Submit form
|
|
305
345
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
**perf:export** generates a valid k6 JavaScript file with:
|
|
309
|
-
- VU stages matching your `--vus`/`--duration`/`--ramp-up` config
|
|
310
|
-
- `http.get`/`http.post` calls with headers and JSON body
|
|
311
|
-
- `check()` assertions mapped to your `assert:response` steps
|
|
312
|
-
- `Trend` metrics per step for p95 thresholds
|
|
313
|
-
- `{{variable}}` → template literal interpolation
|
|
314
|
-
|
|
315
|
-
### MCP Server
|
|
316
|
-
|
|
317
|
-
```bash
|
|
318
|
-
node mcp-server.js # start MCP server (Claude Desktop, Cursor, etc.)
|
|
346
|
+
3 same 1 changed
|
|
347
|
+
Diff images: ~/.ghostrun/diffs/abc123_vs_def456/
|
|
319
348
|
```
|
|
320
349
|
|
|
321
|
-
See [MCP-SETUP.md](MCP-SETUP.md) for connection setup.
|
|
322
|
-
|
|
323
|
-
Tools exposed: `list_flows`, `get_flow`, `run_flow`, `get_run_result`, `list_runs`, `delete_flow`, `get_status`
|
|
324
|
-
|
|
325
350
|
---
|
|
326
351
|
|
|
327
352
|
## CI/CD Integration
|
|
328
353
|
|
|
329
|
-
|
|
354
|
+
GhostRun exits with code `1` on failure and `0` on success — standard CI behaviour, no extra flags needed.
|
|
355
|
+
|
|
356
|
+
### GitHub Actions example
|
|
330
357
|
|
|
331
358
|
```yaml
|
|
332
359
|
# .github/workflows/ghostrun.yml
|
|
333
360
|
name: GhostRun Tests
|
|
334
|
-
|
|
335
361
|
on: [push, pull_request]
|
|
336
362
|
|
|
337
363
|
jobs:
|
|
@@ -339,7 +365,6 @@ jobs:
|
|
|
339
365
|
runs-on: ubuntu-latest
|
|
340
366
|
steps:
|
|
341
367
|
- uses: actions/checkout@v4
|
|
342
|
-
|
|
343
368
|
- uses: actions/setup-node@v4
|
|
344
369
|
with:
|
|
345
370
|
node-version: 20
|
|
@@ -349,16 +374,12 @@ jobs:
|
|
|
349
374
|
|
|
350
375
|
- name: Install Playwright browsers
|
|
351
376
|
run: npx playwright install chromium --with-deps
|
|
377
|
+
# Skip this step for pure API flows — no browser needed
|
|
352
378
|
|
|
353
|
-
- name: Import
|
|
379
|
+
- name: Import and run flows
|
|
354
380
|
run: |
|
|
355
|
-
ghostrun flow:import test-flows/
|
|
356
|
-
ghostrun
|
|
357
|
-
|
|
358
|
-
- name: Run flows
|
|
359
|
-
run: |
|
|
360
|
-
ghostrun run "API Health Check" --report html
|
|
361
|
-
ghostrun run "Auth + User List" --report html
|
|
381
|
+
ghostrun flow:import test-flows/auth.flow.json
|
|
382
|
+
ghostrun run "Auth Flow" --report html
|
|
362
383
|
|
|
363
384
|
- name: Upload reports
|
|
364
385
|
uses: actions/upload-artifact@v4
|
|
@@ -368,265 +389,85 @@ jobs:
|
|
|
368
389
|
path: ghostrun-report-*.html
|
|
369
390
|
```
|
|
370
391
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
GhostRun exits with code `1` on failure and `0` on pass — standard CI behaviour, no extra flags needed.
|
|
374
|
-
|
|
375
|
-
### API-only flows in CI
|
|
376
|
-
|
|
377
|
-
API flows skip Playwright entirely — no `playwright install` step needed:
|
|
378
|
-
|
|
379
|
-
```yaml
|
|
380
|
-
- name: Install GhostRun (API testing only)
|
|
381
|
-
run: npm install -g ghostrun-cli
|
|
382
|
-
# No playwright install needed for pure API flows
|
|
392
|
+
---
|
|
383
393
|
|
|
384
|
-
|
|
385
|
-
run: ghostrun run "Auth + User List"
|
|
386
|
-
```
|
|
394
|
+
## AI Setup
|
|
387
395
|
|
|
388
|
-
|
|
396
|
+
Every core feature works with zero AI. AI adds failure explanations and a chat interface — both are optional.
|
|
389
397
|
|
|
390
|
-
|
|
398
|
+
### Option 1 — Ollama (local, recommended)
|
|
391
399
|
|
|
392
|
-
|
|
400
|
+
No API key, no internet required. Runs on your machine.
|
|
393
401
|
|
|
394
402
|
```bash
|
|
395
|
-
|
|
403
|
+
brew install ollama
|
|
404
|
+
ollama serve &
|
|
405
|
+
ollama pull gemma3:4b # 2.6 GB, fast on Apple Silicon
|
|
396
406
|
```
|
|
397
407
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
408
|
+
| Model | Size | Best for |
|
|
409
|
+
|-------|------|---------|
|
|
410
|
+
| `gemma3:4b` | 2.6 GB | Apple Silicon M1/M2/M3 |
|
|
411
|
+
| `gemma2:9b` | 5.4 GB | Better quality |
|
|
412
|
+
| `llama3.2:3b` | 2.0 GB | Fastest, lighter quality |
|
|
401
413
|
|
|
402
|
-
|
|
414
|
+
Override: `export GHOSTRUN_OLLAMA_MODEL=llama3.2:3b`
|
|
403
415
|
|
|
404
|
-
|
|
416
|
+
### Option 2 — Anthropic (cloud fallback)
|
|
405
417
|
|
|
406
418
|
```bash
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
Step Status Diff % Screenshot
|
|
410
|
-
──────────────────────────────────────────────────────────
|
|
411
|
-
1 same 0.0% Navigate to homepage
|
|
412
|
-
2 same 0.1% Click Login
|
|
413
|
-
3 changed 12.4% Fill email field
|
|
414
|
-
4 same 0.0% Submit form
|
|
415
|
-
|
|
416
|
-
3 same 1 changed
|
|
417
|
-
Diff images: ~/.ghostrun/diffs/abc123_vs_def456/
|
|
419
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
418
420
|
```
|
|
419
421
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
## Flow Actions Reference
|
|
423
|
-
|
|
424
|
-
All actions you can use in recorded or imported `.flow.json` files:
|
|
425
|
-
|
|
426
|
-
### Navigation
|
|
427
|
-
|
|
428
|
-
| Action | Fields | Description |
|
|
429
|
-
|--------|--------|-------------|
|
|
430
|
-
| `navigate` | `url` | Go to URL |
|
|
431
|
-
| `reload` | — | Reload the current page |
|
|
432
|
-
| `back` | — | Browser back |
|
|
433
|
-
| `forward` | — | Browser forward |
|
|
434
|
-
|
|
435
|
-
### Interaction
|
|
436
|
-
|
|
437
|
-
| Action | Fields | Description |
|
|
438
|
-
|--------|--------|-------------|
|
|
439
|
-
| `click` | `selector` | Left-click an element |
|
|
440
|
-
| `dblclick` | `selector` | Double-click an element |
|
|
441
|
-
| `fill` | `selector`, `value` | Clear field and type value |
|
|
442
|
-
| `type` | `selector`, `value`, `delay?` | Type with configurable key delay (ms) |
|
|
443
|
-
| `clear` | `selector` | Clear a field |
|
|
444
|
-
| `select` | `selector`, `value` | Select a dropdown option by value |
|
|
445
|
-
| `check` | `selector`, `value: "true"\|"false"` | Check/uncheck a checkbox |
|
|
446
|
-
| `focus` | `selector` | Focus an element |
|
|
447
|
-
| `hover` | `selector` | Mouse hover |
|
|
448
|
-
| `drag` | `selector`, `targetSelector` | Drag one element to another |
|
|
449
|
-
| `keyboard` | `key`, `selector?` | Press a key (e.g. `Enter`, `Tab`, `Control+a`) |
|
|
450
|
-
| `upload` | `selector`, `value` | Set file input (comma-separated paths) |
|
|
451
|
-
|
|
452
|
-
### Waiting
|
|
453
|
-
|
|
454
|
-
| Action | Fields | Description |
|
|
455
|
-
|--------|--------|-------------|
|
|
456
|
-
| `wait` | `selector` | Wait for element to appear |
|
|
457
|
-
| `wait:text` | `selector`, `value` | Wait until element contains text |
|
|
458
|
-
| `wait:url` | `value` | Wait for URL to match pattern |
|
|
459
|
-
| `wait:ms` | `value` | Wait for N milliseconds |
|
|
460
|
-
|
|
461
|
-
### Scrolling
|
|
462
|
-
|
|
463
|
-
| Action | Fields | Description |
|
|
464
|
-
|--------|--------|-------------|
|
|
465
|
-
| `scroll` | `selector?` | Scroll to element (or page) |
|
|
466
|
-
| `scroll:element` | `selector` | Scroll element into view |
|
|
467
|
-
| `scroll:bottom` | — | Scroll to bottom of page |
|
|
468
|
-
| `scroll:load` | `value?` | Scroll to bottom, wait for load (repeat N times) |
|
|
469
|
-
| `next:page` | `selector?` | Click next page link and wait |
|
|
470
|
-
|
|
471
|
-
### Assertions
|
|
472
|
-
|
|
473
|
-
| Action | Fields | Description |
|
|
474
|
-
|--------|--------|-------------|
|
|
475
|
-
| `assert:visible` | `selector` | Assert element is visible |
|
|
476
|
-
| `assert:hidden` | `selector` | Assert element is not visible |
|
|
477
|
-
| `assert:text` | `selector`, `value` | Assert element contains text |
|
|
478
|
-
| `assert:not-text` | `selector`, `value` | Assert element does NOT contain text |
|
|
479
|
-
| `assert:value` | `selector`, `value` | Assert input value |
|
|
480
|
-
| `assert:count` | `selector`, `value` | Assert number of matching elements |
|
|
481
|
-
| `assert:attr` | `selector`, `value: "attr=expected"` | Assert element attribute |
|
|
422
|
+
### Fallback order
|
|
482
423
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|--------|--------|-------------|
|
|
487
|
-
| `extract` | `selector`, `value: "variableName"` | Extract text → variable |
|
|
488
|
-
| `screenshot` | — | Capture screenshot at this step |
|
|
489
|
-
|
|
490
|
-
### Browser State
|
|
491
|
-
|
|
492
|
-
| Action | Fields | Description |
|
|
493
|
-
|--------|--------|-------------|
|
|
494
|
-
| `cookie:set` | `value: "name=value; domain=..."` | Set a cookie |
|
|
495
|
-
| `cookie:clear` | — | Clear all cookies |
|
|
496
|
-
| `storage:set` | `selector: "key"`, `value: "val"` | Set localStorage item |
|
|
497
|
-
| `eval` | `value` | Execute JavaScript on the page |
|
|
498
|
-
| `iframe:enter` | `selector` | Enter an iframe context |
|
|
499
|
-
| `iframe:exit` | — | Exit iframe context, return to main frame |
|
|
500
|
-
|
|
501
|
-
### API — HTTP Requests
|
|
502
|
-
|
|
503
|
-
| Action | Fields | Description |
|
|
504
|
-
|--------|--------|-------------|
|
|
505
|
-
| `http:request` | `method`, `url`, `headers?`, `body?`, `auth?`, `extract?` | Make an HTTP request. `auth` supports `{ type: "bearer", token: "{{var}}" }`. `extract` is a map of `variableName → $.jsonPath`. |
|
|
506
|
-
|
|
507
|
-
### API — Assertions
|
|
508
|
-
|
|
509
|
-
| Action | Fields | Description |
|
|
510
|
-
|--------|--------|-------------|
|
|
511
|
-
| `assert:response` | `assert: "status"`, `expected` | Assert HTTP status code |
|
|
512
|
-
| `assert:response` | `assert: "json:path"`, `path`, `expected` | Assert JSONPath value equals expected |
|
|
513
|
-
| `assert:response` | `assert: "json:exists"`, `path` | Assert JSONPath exists in response |
|
|
514
|
-
| `assert:response` | `assert: "header"`, `header`, `expected` | Assert response header value |
|
|
515
|
-
| `assert:response` | `assert: "body:contains"`, `expected` | Assert raw body contains string |
|
|
516
|
-
| `assert:response` | `assert: "time"`, `expected` | Assert response time < expected ms |
|
|
517
|
-
|
|
518
|
-
### API — Variables & Flow Control
|
|
519
|
-
|
|
520
|
-
| Action | Fields | Description |
|
|
521
|
-
|--------|--------|-------------|
|
|
522
|
-
| `set:variable` | `variable`, `value` | Set a named variable (supports `{{interpolation}}`) |
|
|
523
|
-
| `extract:json` | `variable`, `path` | Extract a value from the last response body via JSONPath |
|
|
524
|
-
| `env:switch` | `value` | Switch active environment mid-flow |
|
|
525
|
-
|
|
526
|
-
### Variables
|
|
527
|
-
|
|
528
|
-
Use `{{variableName}}` in any `value`, `url`, `selector`, or `body` field to inject variables:
|
|
529
|
-
|
|
530
|
-
```json
|
|
531
|
-
{ "action": "fill", "selector": "#email", "value": "{{userEmail}}" }
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
Pass at runtime: `ghostrun run <id> --var userEmail=user@example.com`
|
|
535
|
-
|
|
536
|
-
Extracted values (from `extract:` steps) are automatically available as variables in subsequent steps.
|
|
424
|
+
```
|
|
425
|
+
Run → try Ollama → if down → try Anthropic → if no key → skip AI silently
|
|
426
|
+
```
|
|
537
427
|
|
|
538
|
-
|
|
428
|
+
### Environment variables
|
|
539
429
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
|
545
|
-
|
|
546
|
-
| Canvas drawing | ❌ Not supported | `<canvas>` elements — no visual capture |
|
|
547
|
-
| WebGL / Three.js | ❌ Not supported | GPU-rendered content |
|
|
548
|
-
| Browser native dialogs | ⚠️ Partial | `alert()`/`confirm()`/`prompt()` auto-dismissed |
|
|
549
|
-
| File download verification | ⚠️ Partial | Download triggers but content is not validated |
|
|
550
|
-
| WebRTC / media streams | ❌ Not supported | Camera, mic, screen capture APIs |
|
|
551
|
-
| Browser extensions | ❌ Not supported | Extension UI is not accessible via Playwright |
|
|
552
|
-
| Shadow DOM (closed mode) | ⚠️ Limited | Open shadow DOM works; closed mode requires `eval:` workaround |
|
|
553
|
-
| Multi-tab / popup flows | ⚠️ Partial | New tabs opened by click are not automatically followed |
|
|
554
|
-
| OS-level dialogs | ❌ Not supported | Native file picker, print dialog, OS auth prompts |
|
|
555
|
-
| CAPTCHAs | ❌ Not supported | By design — no circumvention |
|
|
556
|
-
| Biometric auth | ❌ Not supported | Touch ID, Face ID, WebAuthn |
|
|
557
|
-
| Browser gestures (pinch/zoom) | ❌ Not supported | Mobile multi-touch gestures |
|
|
558
|
-
| Hover-only menus (CSS `:hover`) | ✅ Works | Use `hover` action before clicking submenu items |
|
|
559
|
-
| Right-click context menus | ⚠️ Limited | Browser context menus not accessible; app-level menus often work |
|
|
560
|
-
| Drag and drop | ✅ Works | Use `drag` action with `selector` + `targetSelector` |
|
|
561
|
-
| Infinite scroll / lazy load | ✅ Works | Use `scroll:load` with repeat count |
|
|
562
|
-
|
|
563
|
-
**Workarounds for unsupported interactions:**
|
|
564
|
-
- Use `eval:` to run JavaScript directly: `{ "action": "eval", "value": "document.querySelector('#btn').click()" }`
|
|
565
|
-
- Use `wait:ms:` to pause before difficult timing-sensitive interactions
|
|
566
|
-
- For shadow DOM: `{ "action": "eval", "value": "document.querySelector('my-el').shadowRoot.querySelector('button').click()" }`
|
|
430
|
+
| Variable | Default | Description |
|
|
431
|
+
|----------|---------|-------------|
|
|
432
|
+
| `GHOSTRUN_AI_PROVIDER` | `auto` | `ollama`, `anthropic`, or `auto` |
|
|
433
|
+
| `GHOSTRUN_OLLAMA_URL` | `http://localhost:11434` | Ollama server URL |
|
|
434
|
+
| `GHOSTRUN_OLLAMA_MODEL` | auto-detected | Model to use with Ollama |
|
|
435
|
+
| `ANTHROPIC_API_KEY` | — | Anthropic API key (cloud fallback) |
|
|
567
436
|
|
|
568
437
|
---
|
|
569
438
|
|
|
570
|
-
## Data
|
|
439
|
+
## Data & Privacy
|
|
571
440
|
|
|
572
|
-
All data
|
|
441
|
+
All data lives locally in `~/.ghostrun/`:
|
|
573
442
|
|
|
574
443
|
```
|
|
575
444
|
~/.ghostrun/
|
|
576
|
-
├── data/ghostrun.db # SQLite: flows, runs, steps, schedules,
|
|
577
|
-
│ # environments, api_responses, perf_runs
|
|
445
|
+
├── data/ghostrun.db # SQLite: flows, runs, steps, schedules, environments
|
|
578
446
|
├── screenshots/ # PNG per step per run
|
|
579
447
|
├── baselines/ # Visual baseline reference screenshots
|
|
580
448
|
└── diffs/ # Screenshot diff images
|
|
581
449
|
```
|
|
582
450
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
## Privacy
|
|
586
|
-
|
|
587
|
-
PII is sanitized before storage — emails, phones, cards, JWTs, API keys, passwords are replaced with `[EMAIL]`, `[PHONE]`, etc. Nothing sensitive is sent to AI unless you explicitly call `run:analyze`.
|
|
451
|
+
**PII sanitization:** Emails, phone numbers, credit cards, JWTs, API keys, and passwords are redacted before storage. Nothing sensitive is sent to AI unless you explicitly call `run:analyze`.
|
|
588
452
|
|
|
589
453
|
---
|
|
590
454
|
|
|
591
|
-
##
|
|
592
|
-
|
|
593
|
-
```bash
|
|
594
|
-
npm install
|
|
595
|
-
npm run build # compiles .ts → .js via esbuild
|
|
596
|
-
```
|
|
455
|
+
## Contributing
|
|
597
456
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
## Publishing to npm
|
|
601
|
-
|
|
602
|
-
```bash
|
|
603
|
-
# 1. Log in to npm
|
|
604
|
-
npm login
|
|
605
|
-
|
|
606
|
-
# 2. Make sure the build is fresh
|
|
607
|
-
npm run build
|
|
608
|
-
|
|
609
|
-
# 3. Dry-run to verify what gets published
|
|
610
|
-
npm publish --dry-run
|
|
611
|
-
|
|
612
|
-
# 4. Publish
|
|
613
|
-
npm publish --access public
|
|
614
|
-
```
|
|
615
|
-
|
|
616
|
-
After publishing, users can install with:
|
|
617
|
-
```bash
|
|
618
|
-
npm install -g ghostrun-cli
|
|
619
|
-
ghostrun init
|
|
620
|
-
```
|
|
457
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for how to get started, and [REFERENCE.md](REFERENCE.md) for the full flow actions reference.
|
|
621
458
|
|
|
622
459
|
---
|
|
623
460
|
|
|
624
|
-
##
|
|
461
|
+
## Trust & Transparency
|
|
625
462
|
|
|
626
|
-
|
|
463
|
+
- **100% local by default** — no cloud, no telemetry, no tracking
|
|
464
|
+
- **Open source (MIT)** — full source at [github.com/TechBuiltBySharan/ghostrun](https://github.com/TechBuiltBySharan/ghostrun)
|
|
465
|
+
- **No surprise costs** — AI works offline via [Ollama](https://ollama.com) (free); Anthropic key is optional
|
|
466
|
+
- **No vendor lock-in** — flows are plain JSON files you own; export, import, version-control them like code
|
|
467
|
+
- Built with the help of [Claude](https://claude.ai) and [Goose](https://goose-docs.ai)
|
|
627
468
|
|
|
628
469
|
---
|
|
629
470
|
|
|
630
471
|
## License
|
|
631
472
|
|
|
632
|
-
MIT
|
|
473
|
+
MIT — see [LICENSE](LICENSE)
|