kiroo 0.3.4 β 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 +80 -3
- package/package.json +1 -1
- package/src/bench.js +229 -0
- package/src/checker.js +99 -0
- package/src/executor.js +23 -6
- package/src/graph.js +75 -0
- package/src/replay.js +16 -3
- package/src/snapshot.js +16 -4
- package/src/storage.js +7 -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
|
@@ -6,18 +6,20 @@ import { executeRequest } from '../src/executor.js';
|
|
|
6
6
|
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.
|
|
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')
|
|
@@ -129,6 +198,14 @@ snapshot
|
|
|
129
198
|
await compareSnapshots(tag1, tag2);
|
|
130
199
|
});
|
|
131
200
|
|
|
201
|
+
// Graph command
|
|
202
|
+
program
|
|
203
|
+
.command('graph')
|
|
204
|
+
.description('Show visual dependency graph of API interactions')
|
|
205
|
+
.action(async () => {
|
|
206
|
+
await showGraph();
|
|
207
|
+
});
|
|
208
|
+
|
|
132
209
|
// Import command
|
|
133
210
|
program
|
|
134
211
|
.command('import')
|
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,16 +4,20 @@ 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) {
|
|
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) {
|
|
11
|
+
if (usedKeys) usedKeys.add(key);
|
|
12
|
+
return envVars[key];
|
|
13
|
+
}
|
|
14
|
+
return match;
|
|
11
15
|
});
|
|
12
16
|
}
|
|
13
17
|
if (typeof data === 'object' && data !== null) {
|
|
14
18
|
const newData = Array.isArray(data) ? [] : {};
|
|
15
19
|
for (const key in data) {
|
|
16
|
-
newData[key] = applyEnvReplacements(data[key], envVars);
|
|
20
|
+
newData[key] = applyEnvReplacements(data[key], envVars, usedKeys);
|
|
17
21
|
}
|
|
18
22
|
return newData;
|
|
19
23
|
}
|
|
@@ -52,8 +56,11 @@ export async function executeRequest(method, url, options = {}) {
|
|
|
52
56
|
const env = loadEnv();
|
|
53
57
|
const currentEnvVars = env.environments[env.current] || {};
|
|
54
58
|
|
|
59
|
+
const usedKeys = new Set();
|
|
60
|
+
const savedKeys = [];
|
|
61
|
+
|
|
55
62
|
// Apply replacements to URL
|
|
56
|
-
url = applyEnvReplacements(url, currentEnvVars);
|
|
63
|
+
url = applyEnvReplacements(url, currentEnvVars, usedKeys);
|
|
57
64
|
|
|
58
65
|
// Auto-BaseURL logic
|
|
59
66
|
if (currentEnvVars.baseUrl) {
|
|
@@ -82,6 +89,12 @@ export async function executeRequest(method, url, options = {}) {
|
|
|
82
89
|
// Let's at least strip the drive letter root
|
|
83
90
|
pathPart = url.replace(/^[a-zA-Z]:/, '').replace(/\\/g, '/');
|
|
84
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
|
+
}
|
|
85
98
|
}
|
|
86
99
|
}
|
|
87
100
|
// 3. No leading slash but doesn't look like a host (no dots, no protocol)
|
|
@@ -103,7 +116,7 @@ export async function executeRequest(method, url, options = {}) {
|
|
|
103
116
|
options.header.forEach(h => {
|
|
104
117
|
const [key, ...valueParts] = h.split(':');
|
|
105
118
|
const headerValue = valueParts.join(':').trim();
|
|
106
|
-
headers[key.trim()] = applyEnvReplacements(headerValue, currentEnvVars);
|
|
119
|
+
headers[key.trim()] = applyEnvReplacements(headerValue, currentEnvVars, usedKeys);
|
|
107
120
|
});
|
|
108
121
|
}
|
|
109
122
|
|
|
@@ -112,7 +125,7 @@ export async function executeRequest(method, url, options = {}) {
|
|
|
112
125
|
if (options.data) {
|
|
113
126
|
let rawData = options.data;
|
|
114
127
|
// Apply replacements to raw data string before parsing
|
|
115
|
-
rawData = applyEnvReplacements(rawData, currentEnvVars);
|
|
128
|
+
rawData = applyEnvReplacements(rawData, currentEnvVars, usedKeys);
|
|
116
129
|
|
|
117
130
|
try {
|
|
118
131
|
body = JSON.parse(rawData);
|
|
@@ -175,6 +188,7 @@ export async function executeRequest(method, url, options = {}) {
|
|
|
175
188
|
const val = getDeep(response, responsePath);
|
|
176
189
|
if (val !== undefined) {
|
|
177
190
|
env.environments[env.current][envKey] = val;
|
|
191
|
+
savedKeys.push(envKey);
|
|
178
192
|
console.log(chalk.cyan(` β¨ Saved to env:`), chalk.white(`${envKey}=${val}`));
|
|
179
193
|
} else {
|
|
180
194
|
console.log(chalk.yellow(` β οΈ Could not find path '${responsePath}' in response`));
|
|
@@ -197,10 +211,13 @@ export async function executeRequest(method, url, options = {}) {
|
|
|
197
211
|
data: response.data,
|
|
198
212
|
},
|
|
199
213
|
duration,
|
|
214
|
+
saves: savedKeys,
|
|
215
|
+
uses: Array.from(usedKeys)
|
|
200
216
|
});
|
|
201
217
|
|
|
202
218
|
console.log(chalk.gray('\n πΎ Interaction saved:'), chalk.white(interactionId));
|
|
203
219
|
|
|
220
|
+
return response;
|
|
204
221
|
} catch (error) {
|
|
205
222
|
const duration = Date.now() - startTime;
|
|
206
223
|
spinner.fail(chalk.red('Request failed'));
|
package/src/graph.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getAllInteractions } from './storage.js';
|
|
3
|
+
|
|
4
|
+
export async function showGraph() {
|
|
5
|
+
const interactions = getAllInteractions().reverse(); // Chronological order
|
|
6
|
+
|
|
7
|
+
if (interactions.length === 0) {
|
|
8
|
+
console.log(chalk.yellow('\n No interactions recorded yet.'));
|
|
9
|
+
console.log(chalk.gray(' Run some requests to see the dependency graph!\n'));
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// 1. Map: Variable -> Provider Interaction (Method + Path)
|
|
14
|
+
const variableProviders = {};
|
|
15
|
+
|
|
16
|
+
// 2. Interaction nodes with their connections
|
|
17
|
+
const nodes = [];
|
|
18
|
+
|
|
19
|
+
const getPath = (urlStr) => {
|
|
20
|
+
try {
|
|
21
|
+
const urlObj = new URL(urlStr);
|
|
22
|
+
return urlObj.pathname;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return urlStr.startsWith('http') ? urlStr : (urlStr.startsWith('/') ? urlStr : '/' + urlStr);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
interactions.forEach(int => {
|
|
29
|
+
const path = getPath(int.request.url);
|
|
30
|
+
const method = int.request.method;
|
|
31
|
+
const saves = int.metadata.saves || [];
|
|
32
|
+
const uses = int.metadata.uses || [];
|
|
33
|
+
|
|
34
|
+
// Track which variables this interaction provides
|
|
35
|
+
saves.forEach(v => {
|
|
36
|
+
variableProviders[v] = { method, path };
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
nodes.push({ method, path, saves, uses });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
console.log(chalk.cyan('\n πΈοΈ API Dependency Graph:'));
|
|
43
|
+
console.log(chalk.gray(' (Shows how data flows between endpoints)\n'));
|
|
44
|
+
|
|
45
|
+
// Simple visualization
|
|
46
|
+
const seenPaths = new Set();
|
|
47
|
+
|
|
48
|
+
nodes.forEach((node, idx) => {
|
|
49
|
+
const nodeLabel = `${chalk.white(node.method)} ${chalk.gray(node.path)}`;
|
|
50
|
+
|
|
51
|
+
// Find dependencies based on 'uses'
|
|
52
|
+
const dependencies = node.uses.map(v => {
|
|
53
|
+
const provider = variableProviders[v];
|
|
54
|
+
return provider ? `[${v}] from ${provider.method} ${provider.path}` : null;
|
|
55
|
+
}).filter(Boolean);
|
|
56
|
+
|
|
57
|
+
// Render the node
|
|
58
|
+
if (dependencies.length > 0) {
|
|
59
|
+
console.log(` ${chalk.blue('β¬')} ${nodeLabel}`);
|
|
60
|
+
dependencies.forEach((dep, depIdx) => {
|
|
61
|
+
const branchToken = depIdx === dependencies.length - 1 ? 'ββ' : 'ββ';
|
|
62
|
+
console.log(` ${chalk.gray(branchToken)} ${chalk.yellow('uses')} ${dep}`);
|
|
63
|
+
});
|
|
64
|
+
} else {
|
|
65
|
+
console.log(` ${chalk.green('β')} ${nodeLabel}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (node.saves.length > 0) {
|
|
69
|
+
const saveToken = node.uses.length > 0 ? ' β' : ' ';
|
|
70
|
+
console.log(`${saveToken} ${chalk.magenta('β³')} ${chalk.gray('saves')} ${chalk.white(node.saves.join(', '))}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log('');
|
|
74
|
+
});
|
|
75
|
+
}
|
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/snapshot.js
CHANGED
|
@@ -59,15 +59,27 @@ export async function compareSnapshots(tag1, tag2) {
|
|
|
59
59
|
const results = [];
|
|
60
60
|
let breakingChanges = 0;
|
|
61
61
|
|
|
62
|
-
//
|
|
62
|
+
// Helper to get path from URL string
|
|
63
|
+
const getPath = (urlStr) => {
|
|
64
|
+
try {
|
|
65
|
+
const urlObj = new URL(urlStr);
|
|
66
|
+
return urlObj.pathname;
|
|
67
|
+
} catch (e) {
|
|
68
|
+
// If it's already a path or invalid full URL, return as is
|
|
69
|
+
return urlStr.startsWith('http') ? urlStr : (urlStr.startsWith('/') ? urlStr : '/' + urlStr);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Domain-agnostic comparison: match by Path and Method
|
|
63
74
|
s2.interactions.forEach(int2 => {
|
|
64
|
-
const
|
|
75
|
+
const path2 = getPath(int2.url);
|
|
76
|
+
const int1 = s1.interactions.find(i => getPath(i.url) === path2 && i.method === int2.method);
|
|
65
77
|
|
|
66
78
|
if (!int1) {
|
|
67
79
|
results.push({
|
|
68
80
|
type: 'NEW',
|
|
69
81
|
method: int2.method,
|
|
70
|
-
url:
|
|
82
|
+
url: path2,
|
|
71
83
|
msg: chalk.blue('New interaction added')
|
|
72
84
|
});
|
|
73
85
|
return;
|
|
@@ -97,7 +109,7 @@ export async function compareSnapshots(tag1, tag2) {
|
|
|
97
109
|
results.push({
|
|
98
110
|
type: 'CHANGE',
|
|
99
111
|
method: int2.method,
|
|
100
|
-
url:
|
|
112
|
+
url: path2,
|
|
101
113
|
msg: diffs.join('\n ')
|
|
102
114
|
});
|
|
103
115
|
}
|
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) {
|
|
@@ -39,6 +44,8 @@ export async function saveInteraction(interaction) {
|
|
|
39
44
|
response: interaction.response,
|
|
40
45
|
metadata: {
|
|
41
46
|
duration_ms: interaction.duration,
|
|
47
|
+
saves: interaction.saves || [], // Variables saved from this response
|
|
48
|
+
uses: interaction.uses || [], // Variables used in this request
|
|
42
49
|
},
|
|
43
50
|
};
|
|
44
51
|
|