kiroo 0.4.0 β 0.7.4
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/README.md +191 -60
- package/bin/kiroo.js +71 -2
- package/package.json +1 -1
- package/src/bench.js +229 -0
- package/src/checker.js +99 -0
- package/src/executor.js +8 -1
- package/src/replay.js +16 -3
- package/src/storage.js +5 -0
package/README.md
CHANGED
|
@@ -16,48 +16,60 @@
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
-
## π
|
|
19
|
+
## π What is Kiroo?
|
|
20
20
|
|
|
21
|
-
Kiroo treats your
|
|
21
|
+
Kiroo is **Version Control for API Interactions**. It treats your requests and responses as first-class, versionable artifacts that live right alongside your code in your Git repository.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Stop copy-pasting JSON into Postman. Stop losing your API history. Start versioning it. π
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## πΈοΈ Visual Dependency Graph (`kiroo graph`)
|
|
28
|
+
|
|
29
|
+
Kiroo doesn't just record requests; it understands the **relationships** between them.
|
|
30
|
+
- **Auto-Tracking**: Kiroo tracks variables created via `--save` and consumed via `{{key}}`.
|
|
31
|
+
- **Insight**: Instantly see how data flows from your `/login` to your `/profile` and beyond.
|
|
26
32
|
|
|
27
33
|
---
|
|
28
34
|
|
|
29
|
-
##
|
|
35
|
+
## π Insights & Performance Dashboard (`kiroo stats`)
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
Monitor your API's health directly from your terminal.
|
|
38
|
+
- **Success Rates**: Real-time 2xx/4xx/5xx distribution.
|
|
39
|
+
- **Performance**: Average response times across all interactions.
|
|
40
|
+
- **Bottlenecks**: Automatically identifies your top 5 slowest endpoints.
|
|
41
|
+
|
|
42
|
+
---
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
## π Instant cURL Import (`kiroo import`)
|
|
45
|
+
|
|
46
|
+
Coming from a browser? Don't type a single header.
|
|
47
|
+
- **Copy-Paste Magic**: Just `Copy as cURL` from Chrome/Firefox and run `kiroo import`.
|
|
48
|
+
- **Clean Parsing**: Automatically handles multi-line commands, quotes, and complex flags.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## β¨ Features that WOW
|
|
53
|
+
|
|
54
|
+
### π’ **Git-Native Testing**
|
|
55
|
+
Capture a **Snapshot** of your entire API state and compare versions to detect breaking changes instantly.
|
|
39
56
|
```bash
|
|
40
|
-
kiroo
|
|
57
|
+
kiroo snapshot save v1-stable
|
|
58
|
+
# ... make changes ...
|
|
59
|
+
kiroo snapshot compare v1-stable current
|
|
41
60
|
```
|
|
42
61
|
|
|
43
|
-
### π **
|
|
44
|
-
|
|
62
|
+
### π **Variable Chaining**
|
|
63
|
+
Chain requests like a pro. Save a token from one response and inject it into the next.
|
|
45
64
|
```bash
|
|
46
|
-
|
|
47
|
-
kiroo
|
|
48
|
-
|
|
49
|
-
# Use it in the next request
|
|
50
|
-
kiroo get /profile -H "Authorization: Bearer {{token}}"
|
|
65
|
+
kiroo post /login --save jwt=data.token
|
|
66
|
+
kiroo get /users -H "Authorization: Bearer {{jwt}}"
|
|
51
67
|
```
|
|
52
68
|
|
|
53
|
-
###
|
|
54
|
-
|
|
69
|
+
### β¨οΈ **Shorthand JSON Parser**
|
|
70
|
+
Forget escaping quotes. Type JSON like a human.
|
|
55
71
|
```bash
|
|
56
|
-
|
|
57
|
-
kiroo snapshot save v1-stable
|
|
58
|
-
|
|
59
|
-
# After refactor
|
|
60
|
-
kiroo snapshot compare v1-stable current
|
|
72
|
+
kiroo post /api/user -d "name=Yash email=yash@kiroo.io role=admin"
|
|
61
73
|
```
|
|
62
74
|
|
|
63
75
|
---
|
|
@@ -66,53 +78,172 @@ kiroo snapshot compare v1-stable current
|
|
|
66
78
|
|
|
67
79
|
### 1. Installation
|
|
68
80
|
```bash
|
|
69
|
-
|
|
70
|
-
git clone https://github.com/yash-pouranik/kiroo.git
|
|
71
|
-
cd kiroo
|
|
72
|
-
|
|
73
|
-
# Install and link
|
|
74
|
-
npm install
|
|
75
|
-
npm link
|
|
81
|
+
npm install -g kiroo
|
|
76
82
|
```
|
|
77
83
|
|
|
78
|
-
### 2.
|
|
84
|
+
### 2. Initialization
|
|
79
85
|
```bash
|
|
80
86
|
kiroo init
|
|
81
87
|
```
|
|
82
88
|
|
|
83
|
-
### 3.
|
|
89
|
+
### 3. Record Your First Request
|
|
84
90
|
```bash
|
|
85
|
-
kiroo
|
|
86
|
-
kiroo get {{baseUrl}}/health
|
|
91
|
+
kiroo get https://api.github.com/users/yash-pouranik
|
|
87
92
|
```
|
|
88
93
|
|
|
89
94
|
---
|
|
90
95
|
|
|
91
|
-
##
|
|
92
|
-
|
|
93
|
-
###
|
|
94
|
-
Kiroo
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
kiroo
|
|
103
|
-
|
|
96
|
+
## π Full Command Documentation
|
|
97
|
+
|
|
98
|
+
### `kiroo init`
|
|
99
|
+
Initialize Kiroo in your current project.
|
|
100
|
+
- **Description**: Creates the `.kiroo/` directory structure and a default `env.json`.
|
|
101
|
+
- **Prerequisites**: None. Run once per project.
|
|
102
|
+
- **Example**:
|
|
103
|
+
```bash
|
|
104
|
+
kiroo init
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### `kiroo get/post/put/delete <url>`
|
|
108
|
+
Execute and record an API interaction.
|
|
109
|
+
- **Description**: Performs an HTTP request, displays the response, and saves it to history.
|
|
110
|
+
- **Prerequisites**: Access to the URL (or a `baseUrl` set in the environment).
|
|
111
|
+
- **Arguments**:
|
|
112
|
+
- `url`: The endpoint (Absolute URL or relative path if `baseUrl` exists).
|
|
113
|
+
- **Options**:
|
|
114
|
+
- `-H, --header <key:value>`: Add custom headers.
|
|
115
|
+
- `-d, --data <data>`: Request body (JSON or shorthand `key=val`).
|
|
116
|
+
- `-s, --save <key=path>`: Save response data to environment variables.
|
|
117
|
+
- **Example**:
|
|
118
|
+
```bash
|
|
119
|
+
kiroo post /api/auth/login -d "email=user@test.com password=123" --save token=data.token
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### `kiroo list`
|
|
123
|
+
View your interaction history.
|
|
124
|
+
- **Description**: Displays a paginated list of all recorded requests.
|
|
125
|
+
- **Arguments**: None.
|
|
126
|
+
- **Options**:
|
|
127
|
+
- `-n, --limit <number>`: How many records to show (Default: 10).
|
|
128
|
+
- `-o, --offset <number>`: How many records to skip (Default: 0).
|
|
129
|
+
- **Example**:
|
|
130
|
+
```bash
|
|
131
|
+
kiroo list -n 20
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### `kiroo replay <id>`
|
|
135
|
+
Re-run a specific interaction.
|
|
136
|
+
- **Description**: Fetches the original request from history and executes it again.
|
|
137
|
+
- **Arguments**:
|
|
138
|
+
- `id`: The timestamp ID of the interaction (found via `kiroo list`).
|
|
139
|
+
- **Example**:
|
|
140
|
+
```bash
|
|
141
|
+
kiroo replay 2026-03-10T14-30-05-123Z
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### `kiroo check <url>`
|
|
145
|
+
Zero-Code Testing engine.
|
|
146
|
+
- **Description**: Executes a request and runs assertions on the response. Exits with code 1 on failure.
|
|
147
|
+
- **Prerequisites**: Access to the URL.
|
|
148
|
+
- **Arguments**:
|
|
149
|
+
- `url`: The endpoint.
|
|
150
|
+
- **Options**:
|
|
151
|
+
- `-m, --method <method>`: HTTP method (GET, POST, etc. Default: GET).
|
|
152
|
+
- `-H, --header <key:value>`: Add custom headers.
|
|
153
|
+
- `-d, --data <data>`: Request body.
|
|
154
|
+
- `--status <numbers>`: Expected HTTP status code.
|
|
155
|
+
- `--has <fields>`: Comma-separated list of expected fields in JSON.
|
|
156
|
+
- `--match <key=val>`: Exact value matching for JSON fields.
|
|
157
|
+
- **Example**:
|
|
158
|
+
```bash
|
|
159
|
+
# Check if login is successful
|
|
160
|
+
kiroo check /api/login -m POST -d "user=yash pass=123" --status 200 --has token
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### `kiroo bench <url>`
|
|
164
|
+
Local load testing and benchmarking.
|
|
165
|
+
- **Description**: Sends multiple concurrent HTTP requests to measure endpoint performance (Latency, RPS, Error Rate).
|
|
166
|
+
- **Prerequisites**: Access to the URL.
|
|
167
|
+
- **Arguments**:
|
|
168
|
+
- `url`: The endpoint (supports Auto-BaseURL).
|
|
169
|
+
- **Options**:
|
|
170
|
+
- `-m, --method <method>`: HTTP method (GET, POST, etc. Default: GET).
|
|
171
|
+
- `-n, --number <number>`: Total requests to send (Default: 10).
|
|
172
|
+
- `-c, --concurrent <number>`: Concurrent workers (Default: 1).
|
|
173
|
+
- `-v, --verbose`: Show the HTTP status, response time, and truncated response body for every individual request instead of a single progress spinner.
|
|
174
|
+
- `-H, --header <key:value>`: Add custom headers.
|
|
175
|
+
- `-d, --data <data>`: Request body.
|
|
176
|
+
- **Example**:
|
|
177
|
+
```bash
|
|
178
|
+
# Send 100 requests in batches of 10
|
|
179
|
+
kiroo bench /api/projects -n 100 -c 10
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### `kiroo graph`
|
|
183
|
+
Visualize API dependencies.
|
|
184
|
+
- **Description**: Generates a tree view showing how data flows between endpoints via saved/used variables.
|
|
185
|
+
- **Prerequisites**: Recorded interactions that use `--save` and `{{variable}}`.
|
|
186
|
+
- **Example**:
|
|
187
|
+
```bash
|
|
188
|
+
kiroo graph
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### `kiroo stats`
|
|
192
|
+
Analytics dashboard.
|
|
193
|
+
- **Description**: Shows performance metrics, success rates, and identify slow endpoints.
|
|
194
|
+
- **Example**:
|
|
195
|
+
```bash
|
|
196
|
+
kiroo stats
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### `kiroo import`
|
|
200
|
+
Import from cURL.
|
|
201
|
+
- **Description**: Converts a cURL command into a Kiroo interaction. Opens an interactive editor if no argument is provided.
|
|
202
|
+
- **Arguments**:
|
|
203
|
+
- `curl`: (Optional) The raw cURL string in quotes.
|
|
204
|
+
- **Example**:
|
|
205
|
+
```bash
|
|
206
|
+
kiroo import "curl https://api.exa.com -H 'Auth: 123'"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### `kiroo snapshot`
|
|
210
|
+
Snapshot management.
|
|
211
|
+
- **Commands**:
|
|
212
|
+
- `save <tag>`: Save current history as a versioned state.
|
|
213
|
+
- `list`: List all saved snapshots.
|
|
214
|
+
- `compare <tag1> <tag2>`: Detect breaking changes between two states.
|
|
215
|
+
- **Example**:
|
|
216
|
+
```bash
|
|
217
|
+
kiroo snapshot compare v1.stable current
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### `kiroo env`
|
|
221
|
+
Environment & Variable management.
|
|
222
|
+
- **Commands**:
|
|
223
|
+
- `list`: View all environments and their variables.
|
|
224
|
+
- `use <name>`: Switch active environment (e.g., `prod`, `local`).
|
|
225
|
+
- `set <key> <value>`: Set a variable in the active environment.
|
|
226
|
+
- `rm <key>`: Remove a variable.
|
|
227
|
+
- **Example**:
|
|
228
|
+
```bash
|
|
229
|
+
kiroo env set baseUrl https://api.myapp.com
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### `kiroo clear`
|
|
233
|
+
Wipe history.
|
|
234
|
+
- **Description**: Deletes all recorded interactions to start fresh.
|
|
235
|
+
- **Options**:
|
|
236
|
+
- `-f, --force`: Clear without a confirmation prompt.
|
|
237
|
+
- **Example**:
|
|
238
|
+
```bash
|
|
239
|
+
kiroo clear --force
|
|
240
|
+
```
|
|
104
241
|
|
|
105
242
|
---
|
|
106
243
|
|
|
107
|
-
##
|
|
244
|
+
## π€ Contributing
|
|
108
245
|
|
|
109
|
-
|
|
110
|
-
| :--- | :---: | :---: | :---: |
|
|
111
|
-
| **CLI-First** | β | β οΈ | β
|
|
|
112
|
-
| **Git-Native** | β | β
| β
|
|
|
113
|
-
| **Auto-Recording** | β | β | β
|
|
|
114
|
-
| **Built-in Replay** | β | β | β
|
|
|
115
|
-
| **Variable Chaining** | β οΈ | β οΈ | β
|
|
|
246
|
+
Kiroo is an open-source project and we love contributions! Check out our [Contribution Guidelines](CONTRIBUTING.md).
|
|
116
247
|
|
|
117
248
|
---
|
|
118
249
|
|
|
@@ -123,5 +254,5 @@ Distributed under the MIT License. See `LICENSE` for more information.
|
|
|
123
254
|
---
|
|
124
255
|
|
|
125
256
|
<div align="center">
|
|
126
|
-
Built with β€οΈ for Developers
|
|
257
|
+
Built with β€οΈ for Developers
|
|
127
258
|
</div>
|
package/bin/kiroo.js
CHANGED
|
@@ -7,17 +7,19 @@ import { listInteractions, replayInteraction } from '../src/replay.js';
|
|
|
7
7
|
import { saveSnapshot, compareSnapshots, listSnapshots } from '../src/snapshot.js';
|
|
8
8
|
import { setEnv, setVar, deleteVar, listEnv } from '../src/env.js';
|
|
9
9
|
import { showGraph } from '../src/graph.js';
|
|
10
|
+
import { validateResponse, showCheckResult } from '../src/checker.js';
|
|
10
11
|
import { initProject } from '../src/init.js';
|
|
11
12
|
import { showStats } from '../src/stats.js';
|
|
12
13
|
import { handleImport } from '../src/import.js';
|
|
13
14
|
import { clearAllInteractions } from '../src/storage.js';
|
|
15
|
+
import { runBenchmark } from '../src/bench.js';
|
|
14
16
|
|
|
15
17
|
const program = new Command();
|
|
16
18
|
|
|
17
19
|
program
|
|
18
20
|
.name('kiroo')
|
|
19
21
|
.description('Git for API interactions. Record, replay, snapshot, and diff your APIs.')
|
|
20
|
-
.version('0.4
|
|
22
|
+
.version('0.7.4');
|
|
21
23
|
|
|
22
24
|
// Init command
|
|
23
25
|
program
|
|
@@ -26,6 +28,56 @@ program
|
|
|
26
28
|
.action(async () => {
|
|
27
29
|
await initProject();
|
|
28
30
|
});
|
|
31
|
+
|
|
32
|
+
// Check command (Zero-Code Testing)
|
|
33
|
+
program
|
|
34
|
+
.command('check <url>')
|
|
35
|
+
.description('Execute a request and validate the response against rules')
|
|
36
|
+
.option('-m, --method <method>', 'HTTP method (GET, POST, etc.)', 'GET')
|
|
37
|
+
.option('-H, --header <header...>', 'Add custom headers')
|
|
38
|
+
.option('-d, --data <data>', 'Request body (JSON or shorthand)')
|
|
39
|
+
.option('--status <code...>', 'Expected HTTP status code')
|
|
40
|
+
.option('--has <fields...>', 'Comma-separated list of expected fields in JSON response')
|
|
41
|
+
.option('--match <matches...>', 'Expected field values (e.g., status=active)')
|
|
42
|
+
.action(async (url, options) => {
|
|
43
|
+
// Execute request
|
|
44
|
+
const response = await executeRequest(options.method || 'GET', url, {
|
|
45
|
+
header: options.header,
|
|
46
|
+
data: options.data,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!response) {
|
|
50
|
+
console.error(chalk.red('\n β No response received to validate.'));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Parse matches: ["key1=val1", "key2=val2"] -> { key1: val1, key2: val2 }
|
|
55
|
+
const matchObj = {};
|
|
56
|
+
if (options.match) {
|
|
57
|
+
options.match.forEach(m => {
|
|
58
|
+
const [k, ...v] = m.split('=');
|
|
59
|
+
if (k) matchObj[k] = v.join('=');
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Parse has: ["id,name"] or ["id", "name"] -> ["id", "name"]
|
|
64
|
+
const hasFields = options.has ? options.has.flatMap(h => h.split(',')).map(f => f.trim()) : [];
|
|
65
|
+
|
|
66
|
+
// Construct rules
|
|
67
|
+
const rules = {
|
|
68
|
+
status: Array.isArray(options.status) ? options.status[0] : options.status,
|
|
69
|
+
has: hasFields,
|
|
70
|
+
match: matchObj
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Validate
|
|
74
|
+
const validation = validateResponse(response, rules);
|
|
75
|
+
showCheckResult(validation);
|
|
76
|
+
|
|
77
|
+
if (!validation.passed) {
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
29
81
|
// sk_live_p7BWJjsYlKmauBOjiEeiLRuu4DokkBWsgYne_E6osTo
|
|
30
82
|
|
|
31
83
|
// HTTP methods as commands
|
|
@@ -47,7 +99,10 @@ program
|
|
|
47
99
|
.command('list')
|
|
48
100
|
.description('List all stored interactions')
|
|
49
101
|
.option('-n, --limit <number>', 'Number of interactions to show', '10')
|
|
50
|
-
.option('-o, --offset <number>', '
|
|
102
|
+
.option('-o, --offset <number>', 'Offset for pagination', '0')
|
|
103
|
+
.option('--date <date>', 'Filter by date (YYYY-MM-DD)')
|
|
104
|
+
.option('--url <url>', 'Filter by URL path')
|
|
105
|
+
.option('--status <status>', 'Filter by HTTP status')
|
|
51
106
|
.action(async (options) => {
|
|
52
107
|
await listInteractions(options);
|
|
53
108
|
});
|
|
@@ -60,6 +115,20 @@ program
|
|
|
60
115
|
await replayInteraction(id);
|
|
61
116
|
});
|
|
62
117
|
|
|
118
|
+
// Bench command (Load Testing)
|
|
119
|
+
program
|
|
120
|
+
.command('bench <url>')
|
|
121
|
+
.description('Run a basic load test against an endpoint')
|
|
122
|
+
.option('-m, --method <method>', 'HTTP method (GET, POST, etc.)', 'GET')
|
|
123
|
+
.option('-n, --number <number>', 'Number of total requests to send', '10')
|
|
124
|
+
.option('-c, --concurrent <number>', 'Number of concurrent requests', '1')
|
|
125
|
+
.option('-H, --header <header...>', 'Add custom headers')
|
|
126
|
+
.option('-v, --verbose', 'Show detailed output for every request')
|
|
127
|
+
.option('-d, --data <data>', 'Request body')
|
|
128
|
+
.action(async (url, options) => {
|
|
129
|
+
await runBenchmark(url, options);
|
|
130
|
+
});
|
|
131
|
+
|
|
63
132
|
// Clear command
|
|
64
133
|
program
|
|
65
134
|
.command('clear')
|
package/package.json
CHANGED
package/src/bench.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import Table from 'cli-table3';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { loadEnv } from './storage.js';
|
|
6
|
+
import { applyEnvReplacements } from './executor.js';
|
|
7
|
+
|
|
8
|
+
export async function runBenchmark(url, options) {
|
|
9
|
+
const envData = loadEnv();
|
|
10
|
+
const currentEnvVars = envData.environments[envData.current] || {};
|
|
11
|
+
|
|
12
|
+
url = applyEnvReplacements(url, currentEnvVars);
|
|
13
|
+
|
|
14
|
+
const method = (options.method || 'GET').toUpperCase();
|
|
15
|
+
const totalRequests = parseInt(options.number) || 10;
|
|
16
|
+
const concurrency = parseInt(options.concurrent) || 1;
|
|
17
|
+
const headersObj = {};
|
|
18
|
+
|
|
19
|
+
if (options.header) {
|
|
20
|
+
options.header.forEach(h => {
|
|
21
|
+
const parts = h.split(':');
|
|
22
|
+
if (parts.length >= 2) {
|
|
23
|
+
let val = parts.slice(1).join(':').trim();
|
|
24
|
+
val = applyEnvReplacements(val, currentEnvVars);
|
|
25
|
+
headersObj[parts[0].trim()] = val;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Auto-BaseURL logic (Synced from executor.js)
|
|
31
|
+
let targetUrl = url;
|
|
32
|
+
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
33
|
+
if (currentEnvVars.baseUrl) {
|
|
34
|
+
let isRelative = false;
|
|
35
|
+
let pathPart = url;
|
|
36
|
+
|
|
37
|
+
// 1. Direct relative path
|
|
38
|
+
if (url.startsWith('/')) {
|
|
39
|
+
isRelative = true;
|
|
40
|
+
}
|
|
41
|
+
// 2. Windows Git Bash conversion: Detect "C:/..." style paths with no protocol
|
|
42
|
+
else if (process.platform === 'win32' && /^[a-zA-Z]:[/\\]/.test(url) && !url.includes('://')) {
|
|
43
|
+
isRelative = true;
|
|
44
|
+
const segments = url.split(/[/\\]/);
|
|
45
|
+
const apiIdx = segments.findIndex(s => s === 'api' || s === 'v1' || s === 'v2');
|
|
46
|
+
if (apiIdx !== -1) {
|
|
47
|
+
pathPart = '/' + segments.slice(apiIdx).join('/');
|
|
48
|
+
} else {
|
|
49
|
+
pathPart = url.replace(/^[a-zA-Z]:/, '').replace(/\\/g, '/');
|
|
50
|
+
if (!pathPart.startsWith('/')) pathPart = '/' + pathPart;
|
|
51
|
+
|
|
52
|
+
// Hard fix for Git Bash root expansion "C:/Program Files/Git/" -> "/"
|
|
53
|
+
const lowerPath = pathPart.toLowerCase();
|
|
54
|
+
if (lowerPath === '/program files/git/' || lowerPath === '/program files/git') {
|
|
55
|
+
pathPart = '/';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// 3. No leading slash but doesn't look like a host
|
|
60
|
+
else if (!url.includes('://') && !url.includes('.') && !url.includes(':') && !url.startsWith('localhost')) {
|
|
61
|
+
isRelative = true;
|
|
62
|
+
pathPart = '/' + url;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (isRelative) {
|
|
66
|
+
const baseUrl = currentEnvVars.baseUrl;
|
|
67
|
+
const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
68
|
+
targetUrl = normalizedBaseUrl + (pathPart.startsWith('/') ? pathPart : '/' + pathPart);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
console.log(chalk.red(`\n β Invalid URL and no baseUrl defined in environment '${envData.current}'\n`));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Parse Body
|
|
77
|
+
let requestData = options.data;
|
|
78
|
+
if (options.data) {
|
|
79
|
+
requestData = applyEnvReplacements(options.data, currentEnvVars);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (requestData && typeof requestData === 'string' && !requestData.trim().startsWith('{')) {
|
|
83
|
+
const pairs = requestData.split(' ');
|
|
84
|
+
requestData = {};
|
|
85
|
+
pairs.forEach(pair => {
|
|
86
|
+
const [key, value] = pair.split('=');
|
|
87
|
+
if (key && value !== undefined) {
|
|
88
|
+
requestData[key] = value;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
} else if (requestData && typeof requestData === 'string') {
|
|
92
|
+
try {
|
|
93
|
+
requestData = JSON.parse(requestData);
|
|
94
|
+
} catch(e) {}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const isVerbose = options.verbose;
|
|
98
|
+
let spinner;
|
|
99
|
+
|
|
100
|
+
if (!isVerbose) {
|
|
101
|
+
spinner = ora(`Benchmarking ${method} ${targetUrl} (Reqs: ${totalRequests}, Workers: ${concurrency})`).start();
|
|
102
|
+
} else {
|
|
103
|
+
console.log(chalk.cyan(`\n Starting Benchmark: ${method} ${targetUrl} (Reqs: ${totalRequests}, Workers: ${concurrency})\n`));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const results = {
|
|
107
|
+
total: totalRequests,
|
|
108
|
+
success: 0,
|
|
109
|
+
failures: 0,
|
|
110
|
+
times: []
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const startTime = Date.now();
|
|
114
|
+
let completedCount = 0;
|
|
115
|
+
let activeWorkers = 0;
|
|
116
|
+
|
|
117
|
+
// Custom concurrency implementation
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
const executeNext = async () => {
|
|
120
|
+
if (completedCount >= totalRequests) {
|
|
121
|
+
if (activeWorkers === 0) finalizeBenchmark();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
activeWorkers++;
|
|
126
|
+
const reqIndex = completedCount++;
|
|
127
|
+
const reqId = reqIndex + 1;
|
|
128
|
+
const reqStartTime = Date.now();
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const response = await axios({
|
|
132
|
+
method: method.toLowerCase(),
|
|
133
|
+
url: targetUrl,
|
|
134
|
+
headers: headersObj,
|
|
135
|
+
data: requestData,
|
|
136
|
+
validateStatus: () => true, // Don't throw on 4xx/5xx
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const duration = Date.now() - reqStartTime;
|
|
140
|
+
results.times.push(duration);
|
|
141
|
+
|
|
142
|
+
let statusColor = response.status >= 400 ? chalk.red : chalk.green;
|
|
143
|
+
|
|
144
|
+
if (response.status >= 200 && response.status < 400) {
|
|
145
|
+
results.success++;
|
|
146
|
+
} else {
|
|
147
|
+
results.failures++;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (isVerbose) {
|
|
151
|
+
console.log(chalk.gray(` [Req ${reqId}/${totalRequests}] `) + statusColor(`${response.status} ${response.statusText}`) + chalk.gray(` - ${duration}ms`));
|
|
152
|
+
|
|
153
|
+
// Print snippet of data if available
|
|
154
|
+
if (response.data) {
|
|
155
|
+
let dataStr = typeof response.data === 'object' ? JSON.stringify(response.data) : String(response.data);
|
|
156
|
+
if (dataStr.length > 200) dataStr = dataStr.substring(0, 197) + '...';
|
|
157
|
+
console.log(chalk.gray(` β³ Data: `) + chalk.white(dataStr));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const duration = Date.now() - reqStartTime;
|
|
162
|
+
results.times.push(duration);
|
|
163
|
+
results.failures++;
|
|
164
|
+
|
|
165
|
+
if (isVerbose) {
|
|
166
|
+
console.log(chalk.gray(` [Req ${reqId}/${totalRequests}] `) + chalk.red(`ERR: ${error.message}`) + chalk.gray(` - ${duration}ms`));
|
|
167
|
+
}
|
|
168
|
+
} finally {
|
|
169
|
+
activeWorkers--;
|
|
170
|
+
// Update spinner if not verbose
|
|
171
|
+
if (!isVerbose) {
|
|
172
|
+
const percent = Math.floor((completedCount / totalRequests) * 100);
|
|
173
|
+
spinner.text = `Benchmarking... ${percent}% [${completedCount}/${totalRequests}]`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
executeNext();
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const finalizeBenchmark = () => {
|
|
181
|
+
const totalTime = Date.now() - startTime;
|
|
182
|
+
if (!isVerbose) spinner.stop();
|
|
183
|
+
|
|
184
|
+
const rps = ((results.total / totalTime) * 1000).toFixed(2);
|
|
185
|
+
|
|
186
|
+
// Calculate min, max, avg
|
|
187
|
+
let min = 0, max = 0, avg = 0;
|
|
188
|
+
if (results.times.length > 0) {
|
|
189
|
+
min = Math.min(...results.times);
|
|
190
|
+
max = Math.max(...results.times);
|
|
191
|
+
avg = Math.round(results.times.reduce((a, b) => a + b, 0) / results.times.length);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
console.log('\n ' + chalk.blue.bold('π Benchmark Results'));
|
|
195
|
+
console.log(' ' + chalk.gray(`${method} ${targetUrl}\n`));
|
|
196
|
+
|
|
197
|
+
const statsTable = new Table({
|
|
198
|
+
colWidths: [20, 15]
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
statsTable.push(
|
|
202
|
+
[chalk.white('Total Requests'), chalk.cyan(results.total)],
|
|
203
|
+
[chalk.white('Concurrency'), chalk.cyan(concurrency)],
|
|
204
|
+
[chalk.white('Success Rate'), results.failures === 0 ? chalk.green('100%') : chalk.yellow(`${((results.success/results.total)*100).toFixed(1)}%`)],
|
|
205
|
+
[chalk.white('Requests/sec'), chalk.magenta(rps)],
|
|
206
|
+
[chalk.gray('---'), chalk.gray('---')],
|
|
207
|
+
[chalk.white('Fastest (Min)'), chalk.green(`${min}ms`)],
|
|
208
|
+
[chalk.white('Slowest (Max)'), chalk.red(`${max}ms`)],
|
|
209
|
+
[chalk.white('Average'), chalk.blue(`${avg}ms`)]
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
console.log(statsTable.toString());
|
|
213
|
+
|
|
214
|
+
if (results.failures > 0) {
|
|
215
|
+
console.log(chalk.red(`\n β οΈ ${results.failures} requests failed (HTTP 4xx/5xx or Network Error).\n`));
|
|
216
|
+
} else {
|
|
217
|
+
console.log(chalk.green(`\n β
All requests completed successfully.\n`));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
resolve();
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// Bootstrap workers
|
|
224
|
+
const initialWorkers = Math.min(concurrency, totalRequests);
|
|
225
|
+
for (let i = 0; i < initialWorkers; i++) {
|
|
226
|
+
executeNext();
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
package/src/checker.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validates a response against a set of rules
|
|
5
|
+
* @param {Object} response - The Axios response object
|
|
6
|
+
* @param {Object} rules - Rules like { status, has: [], match: { key: value } }
|
|
7
|
+
* @returns {Object} { passed: boolean, results: [] }
|
|
8
|
+
*/
|
|
9
|
+
export function validateResponse(response, rules) {
|
|
10
|
+
const results = [];
|
|
11
|
+
let allPassed = true;
|
|
12
|
+
|
|
13
|
+
// 1. Status Check
|
|
14
|
+
if (rules.status) {
|
|
15
|
+
const expected = parseInt(rules.status);
|
|
16
|
+
const actual = response.status;
|
|
17
|
+
const passed = expected === actual;
|
|
18
|
+
results.push({
|
|
19
|
+
label: 'Status Code',
|
|
20
|
+
expected,
|
|
21
|
+
actual,
|
|
22
|
+
passed
|
|
23
|
+
});
|
|
24
|
+
if (!passed) allPassed = false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 2. Body Presence Check (has keys)
|
|
28
|
+
if (rules.has && rules.has.length > 0) {
|
|
29
|
+
const body = response.data || {};
|
|
30
|
+
rules.has.forEach(key => {
|
|
31
|
+
let actual = getDeep(body, key);
|
|
32
|
+
let passed = actual !== undefined;
|
|
33
|
+
|
|
34
|
+
// Smart Array Handling:
|
|
35
|
+
// If root is an Array and user checks for 'data' (or any key) that's missing,
|
|
36
|
+
// we check if the array itself belongs to the "presence" check.
|
|
37
|
+
if (!passed && Array.isArray(body)) {
|
|
38
|
+
// If the user is checking for existence on a root array,
|
|
39
|
+
// we'll pass if the array is not empty.
|
|
40
|
+
passed = body.length > 0;
|
|
41
|
+
actual = passed ? `Array(${body.length})` : 'Empty Array';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
results.push({
|
|
45
|
+
label: `Field Presence [${key}]`,
|
|
46
|
+
expected: 'Exists',
|
|
47
|
+
actual: passed ? (actual === undefined ? 'Found' : actual) : 'Missing',
|
|
48
|
+
passed
|
|
49
|
+
});
|
|
50
|
+
if (!passed) allPassed = false;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 3. Value Match Check
|
|
55
|
+
if (rules.match && Object.keys(rules.match).length > 0) {
|
|
56
|
+
const body = response.data || {};
|
|
57
|
+
for (const [key, expected] of Object.entries(rules.match)) {
|
|
58
|
+
const actual = getDeep(body, key);
|
|
59
|
+
const passed = String(actual) === String(expected);
|
|
60
|
+
results.push({
|
|
61
|
+
label: `Value Match [${key}]`,
|
|
62
|
+
expected,
|
|
63
|
+
actual: actual !== undefined ? actual : 'undefined',
|
|
64
|
+
passed
|
|
65
|
+
});
|
|
66
|
+
if (!passed) allPassed = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { passed: allPassed, results };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getDeep(obj, path) {
|
|
74
|
+
if (!obj) return undefined;
|
|
75
|
+
const keys = path.split(/[.[\]]+/).filter(Boolean);
|
|
76
|
+
return keys.reduce((acc, key) => acc && acc[key], obj);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function showCheckResult(validation) {
|
|
80
|
+
console.log(chalk.cyan('\n π§ͺ Test Results:'));
|
|
81
|
+
|
|
82
|
+
validation.results.forEach(res => {
|
|
83
|
+
const icon = res.passed ? chalk.green('β') : chalk.red('β');
|
|
84
|
+
const color = res.passed ? chalk.white : chalk.red;
|
|
85
|
+
|
|
86
|
+
console.log(` ${icon} ${res.label}`);
|
|
87
|
+
if (!res.passed) {
|
|
88
|
+
console.log(chalk.gray(` Expected: ${res.expected}`));
|
|
89
|
+
console.log(chalk.gray(` Actual: ${res.actual}`));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (validation.passed) {
|
|
94
|
+
console.log(chalk.green.bold('\n β¨ ALL TESTS PASSED! \n'));
|
|
95
|
+
} else {
|
|
96
|
+
console.log(chalk.red.bold('\n β SOME TESTS FAILED \n'));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
package/src/executor.js
CHANGED
|
@@ -4,7 +4,7 @@ import ora from 'ora';
|
|
|
4
4
|
import { saveInteraction, loadEnv, saveEnv } from './storage.js';
|
|
5
5
|
import { formatResponse } from './formatter.js';
|
|
6
6
|
|
|
7
|
-
function applyEnvReplacements(data, envVars, usedKeys = null) {
|
|
7
|
+
export function applyEnvReplacements(data, envVars, usedKeys = null) {
|
|
8
8
|
if (typeof data === 'string') {
|
|
9
9
|
return data.replace(/\{\{(.+?)\}\}/g, (match, key) => {
|
|
10
10
|
if (envVars[key] !== undefined) {
|
|
@@ -89,6 +89,12 @@ export async function executeRequest(method, url, options = {}) {
|
|
|
89
89
|
// Let's at least strip the drive letter root
|
|
90
90
|
pathPart = url.replace(/^[a-zA-Z]:/, '').replace(/\\/g, '/');
|
|
91
91
|
if (!pathPart.startsWith('/')) pathPart = '/' + pathPart;
|
|
92
|
+
|
|
93
|
+
// Hard fix for Git Bash root expansion "C:/Program Files/Git/" -> "/"
|
|
94
|
+
const lowerPath = pathPart.toLowerCase();
|
|
95
|
+
if (lowerPath === '/program files/git/' || lowerPath === '/program files/git') {
|
|
96
|
+
pathPart = '/';
|
|
97
|
+
}
|
|
92
98
|
}
|
|
93
99
|
}
|
|
94
100
|
// 3. No leading slash but doesn't look like a host (no dots, no protocol)
|
|
@@ -211,6 +217,7 @@ export async function executeRequest(method, url, options = {}) {
|
|
|
211
217
|
|
|
212
218
|
console.log(chalk.gray('\n πΎ Interaction saved:'), chalk.white(interactionId));
|
|
213
219
|
|
|
220
|
+
return response;
|
|
214
221
|
} catch (error) {
|
|
215
222
|
const duration = Date.now() - startTime;
|
|
216
223
|
spinner.fail(chalk.red('Request failed'));
|
package/src/replay.js
CHANGED
|
@@ -5,13 +5,26 @@ import { getAllInteractions, loadInteraction } from './storage.js';
|
|
|
5
5
|
import { formatResponse } from './formatter.js';
|
|
6
6
|
|
|
7
7
|
export async function listInteractions(options) {
|
|
8
|
-
|
|
8
|
+
let interactions = getAllInteractions();
|
|
9
9
|
const limit = parseInt(options.limit) || 10;
|
|
10
10
|
const offset = parseInt(options.offset) || 0;
|
|
11
11
|
|
|
12
|
+
// Apply Filters
|
|
13
|
+
if (options.date) {
|
|
14
|
+
interactions = interactions.filter(int => int.id.startsWith(options.date));
|
|
15
|
+
}
|
|
16
|
+
if (options.url) {
|
|
17
|
+
interactions = interactions.filter(int => int.request.url.toLowerCase().includes(options.url.toLowerCase()));
|
|
18
|
+
}
|
|
19
|
+
if (options.status) {
|
|
20
|
+
interactions = interactions.filter(int => String(int.response.status) === String(options.status));
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
if (interactions.length === 0) {
|
|
13
|
-
console.log(chalk.yellow('\n No interactions found.'));
|
|
14
|
-
|
|
24
|
+
console.log(chalk.yellow('\n No matching interactions found.'));
|
|
25
|
+
if (!options.date && !options.url && !options.status) {
|
|
26
|
+
console.log(chalk.gray(' Run a request first: '), chalk.white('kiroo POST https://api.example.com/endpoint\n'));
|
|
27
|
+
}
|
|
15
28
|
return;
|
|
16
29
|
}
|
|
17
30
|
|
package/src/storage.js
CHANGED
|
@@ -19,6 +19,11 @@ export function ensureKirooDir() {
|
|
|
19
19
|
if (!existsSync(ENV_FILE)) {
|
|
20
20
|
writeFileSync(ENV_FILE, JSON.stringify({ current: 'default', environments: { default: {} } }, null, 2));
|
|
21
21
|
}
|
|
22
|
+
|
|
23
|
+
const GITIGNORE_FILE = join(KIROO_DIR, '.gitignore');
|
|
24
|
+
if (!existsSync(GITIGNORE_FILE)) {
|
|
25
|
+
writeFileSync(GITIGNORE_FILE, 'env.json\n');
|
|
26
|
+
}
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
export async function saveInteraction(interaction) {
|