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 CHANGED
@@ -16,48 +16,60 @@
16
16
 
17
17
  ---
18
18
 
19
- ## πŸ“– Introduction
19
+ ## πŸ“– What is Kiroo?
20
20
 
21
- Kiroo treats your API requests and responses as **first-class versionable artifacts**.
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
- Ever had a production bug that worked fine on your machine? Ever refactored a backend only to find out you broke a critical field 3 days later? Kiroo solves this by letting you **store API interactions in your repository**.
23
+ Stop copy-pasting JSON into Postman. Stop losing your API history. Start versioning it. πŸš€
24
24
 
25
- Every interaction is a structured, reproducibility-focused JSON file that lives in your `.kiroo/` directory.
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
- ## ✨ Core Capabilities
35
+ ## πŸ“Š Insights & Performance Dashboard (`kiroo stats`)
30
36
 
31
- ### πŸ”΄ **Auto-Recording**
32
- Every request made through Kiroo is automatically saved. No more manual exports from Postman.
33
- ```bash
34
- kiroo post {{baseUrl}}/users -d "name=Yash email=yash@example.com"
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
- ### πŸ”„ **Replay Engine**
38
- Re-run any past interaction instantly and see if the backend behavior has changed.
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 replay <interaction-id>
57
+ kiroo snapshot save v1-stable
58
+ # ... make changes ...
59
+ kiroo snapshot compare v1-stable current
41
60
  ```
42
61
 
43
- ### 🌍 **Smart Environments & Variables**
44
- Stop copy-pasting tokens. Chain requests together dynamically.
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
- # Save a token from login
47
- kiroo post /login --save token=data.accessToken
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
- ### πŸ“Έ **Snapshot System & Diff Engine**
54
- Capture the "Status Quo" of your API and detect **Breaking Changes** during refactors.
69
+ ### ⌨️ **Shorthand JSON Parser**
70
+ Forget escaping quotes. Type JSON like a human.
55
71
  ```bash
56
- # Before refactor
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
- # Clone the repo
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. Initialize
84
+ ### 2. Initialization
79
85
  ```bash
80
86
  kiroo init
81
87
  ```
82
88
 
83
- ### 3. Basic Request
89
+ ### 3. Record Your First Request
84
90
  ```bash
85
- kiroo env set baseUrl http://localhost:3000
86
- kiroo get {{baseUrl}}/health
91
+ kiroo get https://api.github.com/users/yash-pouranik
87
92
  ```
88
93
 
89
94
  ---
90
95
 
91
- ## πŸ› οΈ Advanced Workflows
92
-
93
- ### Nested Data Support
94
- Kiroo's shorthand parser understands nested objects and arrays:
95
- ```bash
96
- kiroo put /products/1 -d "reviews[0].stars=5 metadata.isFeatured=true"
97
- ```
98
-
99
- ### Managing Environments
100
- ```bash
101
- kiroo env use prod
102
- kiroo env list
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
- ## 🎯 Comparison
244
+ ## 🀝 Contributing
108
245
 
109
- | Feature | Postman / Insomnia | Bruno | **Kiroo** |
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 by <a href="https://github.com/yash-pouranik">Yash Pouranik</a>
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
- // import { showGraph } from '../src/graph.js';
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.3.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>', 'Number of interactions to skip', '0')
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiroo",
3
- "version": "0.3.4",
3
+ "version": "0.7.4",
4
4
  "description": "Git for API interactions. Record, replay, snapshot, and diff your APIs.",
5
5
  "type": "module",
6
6
  "bin": {
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
- return envVars[key] !== undefined ? envVars[key] : match;
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
- const interactions = getAllInteractions();
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
- console.log(chalk.gray(' Run a request first: '), chalk.white('kiroo POST https://api.example.com/endpoint\n'));
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
- // Simplistic comparison: match by URL and Method
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 int1 = s1.interactions.find(i => i.url === int2.url && i.method === int2.method);
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: int2.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: int2.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