kiroo 0.4.0 β†’ 0.8.0

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,75 @@
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.
32
+
33
+ ---
34
+
35
+ ## πŸ“Š Insights & Performance Dashboard (`kiroo stats`)
36
+
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.
26
41
 
27
42
  ---
28
43
 
29
- ## ✨ Core Capabilities
44
+ ## πŸ”Œ Instant cURL Import (`kiroo import`)
30
45
 
31
- ### πŸ”΄ **Auto-Recording**
32
- Every request made through Kiroo is automatically saved. No more manual exports from Postman.
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 Diffing & Translating**
55
+ Capture a **Snapshot** of your entire API state and compare versions.
56
+ - **Deep Structural Diffs**: Recursively tracks nested schema changes and silent datatype overrides.
57
+ - **Lingo.dev Translation**: Instantly localize breaking change alerts natively in your terminal.
33
58
  ```bash
34
- kiroo post {{baseUrl}}/users -d "name=Yash email=yash@example.com"
59
+ kiroo snapshot save v1-stable
60
+ kiroo --lang hi snapshot compare v1-stable current
35
61
  ```
36
62
 
37
- ### πŸ”„ **Replay Engine**
38
- Re-run any past interaction instantly and see if the backend behavior has changed.
63
+ ### 🌍 **Variable Chaining**
64
+ Chain requests like a pro. Save a token from one response and inject it into the next.
39
65
  ```bash
40
- kiroo replay <interaction-id>
66
+ kiroo post /login --save jwt=data.token
67
+ kiroo get /users -H "Authorization: Bearer {{jwt}}"
41
68
  ```
42
69
 
43
- ### 🌍 **Smart Environments & Variables**
44
- Stop copy-pasting tokens. Chain requests together dynamically.
70
+ ### ⌨️ **Shorthand JSON Parser**
71
+ Forget escaping quotes. Type JSON like a human.
45
72
  ```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}}"
73
+ kiroo post /api/user -d "name=Yash email=yash@kiroo.io role=admin"
51
74
  ```
52
75
 
53
- ### πŸ“Έ **Snapshot System & Diff Engine**
54
- Capture the "Status Quo" of your API and detect **Breaking Changes** during refactors.
76
+ ---
77
+
78
+ ### πŸ§ͺ **Zero-Code Testing Framework**
79
+ Turn your terminal into an automated test runner. Validate responses on the fly without writing a single line of JS.
55
80
  ```bash
56
- # Before refactor
57
- kiroo snapshot save v1-stable
81
+ kiroo check /api/login -m POST -d "user=yash pass=123" --status 200 --has token
82
+ ```
58
83
 
59
- # After refactor
60
- kiroo snapshot compare v1-stable current
84
+ ### πŸš€ **Local Load Benchmarking**
85
+ Stress test endpoints instantly. Simulates massive concurrency and environment-variable-injected workloads to locate latency limits.
86
+ ```bash
87
+ kiroo bench /api/reports -n 1000 -c 50 -v
61
88
  ```
62
89
 
63
90
  ---
@@ -66,53 +93,192 @@ kiroo snapshot compare v1-stable current
66
93
 
67
94
  ### 1. Installation
68
95
  ```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
96
+ npm install -g kiroo
76
97
  ```
77
98
 
78
- ### 2. Initialize
99
+ ### 2. Initialization
79
100
  ```bash
80
101
  kiroo init
81
102
  ```
82
103
 
83
- ### 3. Basic Request
104
+ ### 3. Record Your First Request
84
105
  ```bash
85
- kiroo env set baseUrl http://localhost:3000
86
- kiroo get {{baseUrl}}/health
106
+ kiroo get https://api.github.com/users/yash-pouranik
87
107
  ```
88
108
 
89
109
  ---
90
110
 
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
- ```
111
+ ## πŸ“š Full Command Documentation
112
+
113
+ ### `kiroo init`
114
+ Initialize Kiroo in your current project.
115
+ - **Description**: Creates the `.kiroo/` directory structure and a default `env.json`.
116
+ - **Prerequisites**: None. Run once per project.
117
+ - **Example**:
118
+ ```bash
119
+ kiroo init
120
+ ```
121
+
122
+ ### `kiroo get/post/put/delete <url>`
123
+ Execute and record an API interaction.
124
+ - **Description**: Performs an HTTP request, displays the response, and saves it to history.
125
+ - **Prerequisites**: Access to the URL (or a `baseUrl` set in the environment).
126
+ - **Arguments**:
127
+ - `url`: The endpoint (Absolute URL or relative path if `baseUrl` exists).
128
+ - **Options**:
129
+ - `-H, --header <key:value>`: Add custom headers.
130
+ - `-d, --data <data>`: Request body (JSON or shorthand `key=val`).
131
+ - `-s, --save <key=path>`: Save response data to environment variables.
132
+ - **Example**:
133
+ ```bash
134
+ kiroo post /api/auth/login -d "email=user@test.com password=123" --save token=data.token
135
+ ```
136
+
137
+ ### `kiroo list`
138
+ View your interaction history.
139
+ - **Description**: Displays a paginated list of all recorded requests.
140
+ - **Arguments**: None.
141
+ - **Options**:
142
+ - `-n, --limit <number>`: How many records to show (Default: 10).
143
+ - `-o, --offset <number>`: How many records to skip (Default: 0).
144
+ - **Example**:
145
+ ```bash
146
+ kiroo list -n 20
147
+ ```
148
+
149
+ ### `kiroo replay <id>`
150
+ Re-run a specific interaction.
151
+ - **Description**: Fetches the original request from history and executes it again.
152
+ - **Arguments**:
153
+ - `id`: The timestamp ID of the interaction (found via `kiroo list`).
154
+ - **Example**:
155
+ ```bash
156
+ kiroo replay 2026-03-10T14-30-05-123Z
157
+ ```
158
+
159
+ ### `kiroo edit <id>`
160
+ Quick Refinement. Edit an interaction on the fly and replay it.
161
+ - **Description**: Opens the stored interaction JSON in your default system editor (VS Code, Nano, Vim, etc.). Edit the headers, body, or URL, save, and close. Kiroo immediately replays the updated request.
162
+ - **Arguments**:
163
+ - `id`: The timestamp ID of the interaction.
164
+ - **Example**:
165
+ ```bash
166
+ kiroo edit 2026-03-10T14-30-05-123Z
167
+ ```
168
+
169
+ ### `kiroo check <url>`
170
+ Zero-Code Testing engine.
171
+ - **Description**: Executes a request and runs assertions on the response. Exits with code 1 on failure.
172
+ - **Prerequisites**: Access to the URL.
173
+ - **Arguments**:
174
+ - `url`: The endpoint.
175
+ - **Options**:
176
+ - `-m, --method <method>`: HTTP method (GET, POST, etc. Default: GET).
177
+ - `-H, --header <key:value>`: Add custom headers.
178
+ - `-d, --data <data>`: Request body.
179
+ - `--status <numbers>`: Expected HTTP status code.
180
+ - `--has <fields>`: Comma-separated list of expected fields in JSON.
181
+ - `--match <key=val>`: Exact value matching for JSON fields.
182
+ - **Example**:
183
+ ```bash
184
+ # Check if login is successful
185
+ kiroo check /api/login -m POST -d "user=yash pass=123" --status 200 --has token
186
+ ```
187
+
188
+ ### `kiroo bench <url>`
189
+ Local load testing and benchmarking.
190
+ - **Description**: Sends multiple concurrent HTTP requests to measure endpoint performance (Latency, RPS, Error Rate).
191
+ - **Prerequisites**: Access to the URL.
192
+ - **Arguments**:
193
+ - `url`: The endpoint (supports Auto-BaseURL).
194
+ - **Options**:
195
+ - `-m, --method <method>`: HTTP method (GET, POST, etc. Default: GET).
196
+ - `-n, --number <number>`: Total requests to send (Default: 10).
197
+ - `-c, --concurrent <number>`: Concurrent workers (Default: 1).
198
+ - `-v, --verbose`: Show the HTTP status, response time, and truncated response body for every individual request instead of a single progress spinner.
199
+ - `-H, --header <key:value>`: Add custom headers.
200
+ - `-d, --data <data>`: Request body.
201
+ - **Example**:
202
+ ```bash
203
+ # Send 100 requests in batches of 10
204
+ kiroo bench /api/projects -n 100 -c 10
205
+ ```
206
+
207
+ ### `kiroo graph`
208
+ Visualize API dependencies.
209
+ - **Description**: Generates a tree view showing how data flows between endpoints via saved/used variables.
210
+ - **Prerequisites**: Recorded interactions that use `--save` and `{{variable}}`.
211
+ - **Example**:
212
+ ```bash
213
+ kiroo graph
214
+ ```
215
+
216
+ ### `kiroo stats`
217
+ Analytics dashboard.
218
+ - **Description**: Shows performance metrics, success rates, and identify slow endpoints.
219
+ - **Example**:
220
+ ```bash
221
+ kiroo stats
222
+ ```
223
+
224
+ ### `kiroo import`
225
+ Import from cURL.
226
+ - **Description**: Converts a cURL command into a Kiroo interaction. Opens an interactive editor if no argument is provided.
227
+ - **Arguments**:
228
+ - `curl`: (Optional) The raw cURL string in quotes.
229
+ - **Example**:
230
+ ```bash
231
+ kiroo import "curl https://api.exa.com -H 'Auth: 123'"
232
+ ```
233
+
234
+ ### `kiroo snapshot`
235
+ Snapshot management.
236
+ - **Commands**:
237
+ - `save <tag>`: Save current history as a versioned state.
238
+ - `list`: List all saved snapshots.
239
+ - `compare <tag1> <tag2>`: Detect breaking changes between two states.
240
+ - **Example**:
241
+ ```bash
242
+ kiroo snapshot compare v1.stable current
243
+ ```
244
+
245
+ ### `kiroo export`
246
+ Team Compatibility. Export to Postman.
247
+ - **Description**: Converts all stored Kiroo interactions and responses into a standard Postman Collection v2.1.0 format (`.json`) for seamless GUI import.
248
+ - **Options**:
249
+ - `-o, --out <filename>`: The output filename (Default: `kiroo-collection.json`).
250
+ - **Example**:
251
+ ```bash
252
+ kiroo export --out my_api_collection.json
253
+ ```
254
+
255
+ ### `kiroo env`
256
+ Environment & Variable management.
257
+ - **Commands**:
258
+ - `list`: View all environments and their variables.
259
+ - `use <name>`: Switch active environment (e.g., `prod`, `local`).
260
+ - `set <key> <value>`: Set a variable in the active environment.
261
+ - `rm <key>`: Remove a variable.
262
+ - **Example**:
263
+ ```bash
264
+ kiroo env set baseUrl https://api.myapp.com
265
+ ```
266
+
267
+ ### `kiroo clear`
268
+ Wipe history.
269
+ - **Description**: Deletes all recorded interactions to start fresh.
270
+ - **Options**:
271
+ - `-f, --force`: Clear without a confirmation prompt.
272
+ - **Example**:
273
+ ```bash
274
+ kiroo clear --force
275
+ ```
104
276
 
105
277
  ---
106
278
 
107
- ## 🎯 Comparison
279
+ ## 🀝 Contributing
108
280
 
109
- | Feature | Postman / Insomnia | Bruno | **Kiroo** |
110
- | :--- | :---: | :---: | :---: |
111
- | **CLI-First** | ❌ | ⚠️ | βœ… |
112
- | **Git-Native** | ❌ | βœ… | βœ… |
113
- | **Auto-Recording** | ❌ | ❌ | βœ… |
114
- | **Built-in Replay** | ❌ | ❌ | βœ… |
115
- | **Variable Chaining** | ⚠️ | ⚠️ | βœ… |
281
+ Kiroo is an open-source project and we love contributions! Check out our [Contribution Guidelines](CONTRIBUTING.md).
116
282
 
117
283
  ---
118
284
 
@@ -123,5 +289,5 @@ Distributed under the MIT License. See `LICENSE` for more information.
123
289
  ---
124
290
 
125
291
  <div align="center">
126
- Built with ❀️ for Developers by <a href="https://github.com/yash-pouranik">Yash Pouranik</a>
292
+ Built with ❀️ for Developers
127
293
  </div>
package/bin/kiroo.js CHANGED
@@ -7,17 +7,22 @@ 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';
16
+ import { editInteraction } from '../src/edit.js';
17
+ import { exportToPostman } from '../src/export.js';
14
18
 
15
19
  const program = new Command();
16
20
 
17
21
  program
18
22
  .name('kiroo')
19
23
  .description('Git for API interactions. Record, replay, snapshot, and diff your APIs.')
20
- .version('0.4.0');
24
+ .version('0.8.0')
25
+ .option('--lang <language>', 'Translate output to specified language (e.g., hi, es, fr)');
21
26
 
22
27
  // Init command
23
28
  program
@@ -26,6 +31,56 @@ program
26
31
  .action(async () => {
27
32
  await initProject();
28
33
  });
34
+
35
+ // Check command (Zero-Code Testing)
36
+ program
37
+ .command('check <url>')
38
+ .description('Execute a request and validate the response against rules')
39
+ .option('-m, --method <method>', 'HTTP method (GET, POST, etc.)', 'GET')
40
+ .option('-H, --header <header...>', 'Add custom headers')
41
+ .option('-d, --data <data>', 'Request body (JSON or shorthand)')
42
+ .option('--status <code...>', 'Expected HTTP status code')
43
+ .option('--has <fields...>', 'Comma-separated list of expected fields in JSON response')
44
+ .option('--match <matches...>', 'Expected field values (e.g., status=active)')
45
+ .action(async (url, options) => {
46
+ // Execute request
47
+ const response = await executeRequest(options.method || 'GET', url, {
48
+ header: options.header,
49
+ data: options.data,
50
+ });
51
+
52
+ if (!response) {
53
+ console.error(chalk.red('\n βœ— No response received to validate.'));
54
+ process.exit(1);
55
+ }
56
+
57
+ // Parse matches: ["key1=val1", "key2=val2"] -> { key1: val1, key2: val2 }
58
+ const matchObj = {};
59
+ if (options.match) {
60
+ options.match.forEach(m => {
61
+ const [k, ...v] = m.split('=');
62
+ if (k) matchObj[k] = v.join('=');
63
+ });
64
+ }
65
+
66
+ // Parse has: ["id,name"] or ["id", "name"] -> ["id", "name"]
67
+ const hasFields = options.has ? options.has.flatMap(h => h.split(',')).map(f => f.trim()) : [];
68
+
69
+ // Construct rules
70
+ const rules = {
71
+ status: Array.isArray(options.status) ? options.status[0] : options.status,
72
+ has: hasFields,
73
+ match: matchObj
74
+ };
75
+
76
+ // Validate
77
+ const validation = validateResponse(response, rules);
78
+ showCheckResult(validation);
79
+
80
+ if (!validation.passed) {
81
+ process.exit(1);
82
+ }
83
+ });
29
84
  // sk_live_p7BWJjsYlKmauBOjiEeiLRuu4DokkBWsgYne_E6osTo
30
85
 
31
86
  // HTTP methods as commands
@@ -47,7 +102,10 @@ program
47
102
  .command('list')
48
103
  .description('List all stored interactions')
49
104
  .option('-n, --limit <number>', 'Number of interactions to show', '10')
50
- .option('-o, --offset <number>', 'Number of interactions to skip', '0')
105
+ .option('-o, --offset <number>', 'Offset for pagination', '0')
106
+ .option('--date <date>', 'Filter by date (YYYY-MM-DD)')
107
+ .option('--url <url>', 'Filter by URL path')
108
+ .option('--status <status>', 'Filter by HTTP status')
51
109
  .action(async (options) => {
52
110
  await listInteractions(options);
53
111
  });
@@ -60,6 +118,37 @@ program
60
118
  await replayInteraction(id);
61
119
  });
62
120
 
121
+ // Edit interaction
122
+ program
123
+ .command('edit <id>')
124
+ .description('Edit an interaction in your text editor and quickly replay it')
125
+ .action(async (id) => {
126
+ await editInteraction(id);
127
+ });
128
+
129
+ // Export interactions
130
+ program
131
+ .command('export')
132
+ .description('Export all stored interactions to a Postman Collection')
133
+ .option('-o, --out <filename>', 'Output JSON filename', 'kiroo-collection.json')
134
+ .action((options) => {
135
+ exportToPostman(options.out);
136
+ });
137
+
138
+ // Bench command (Load Testing)
139
+ program
140
+ .command('bench <url>')
141
+ .description('Run a basic load test against an endpoint')
142
+ .option('-m, --method <method>', 'HTTP method (GET, POST, etc.)', 'GET')
143
+ .option('-n, --number <number>', 'Number of total requests to send', '10')
144
+ .option('-c, --concurrent <number>', 'Number of concurrent requests', '1')
145
+ .option('-H, --header <header...>', 'Add custom headers')
146
+ .option('-v, --verbose', 'Show detailed output for every request')
147
+ .option('-d, --data <data>', 'Request body')
148
+ .action(async (url, options) => {
149
+ await runBenchmark(url, options);
150
+ });
151
+
63
152
  // Clear command
64
153
  program
65
154
  .command('clear')
@@ -126,7 +215,8 @@ snapshot
126
215
  .command('compare <tag1> <tag2>')
127
216
  .description('Compare two snapshots')
128
217
  .action(async (tag1, tag2) => {
129
- await compareSnapshots(tag1, tag2);
218
+ const opts = program.opts();
219
+ await compareSnapshots(tag1, tag2, opts.lang);
130
220
  });
131
221
 
132
222
  // Graph command
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiroo",
3
- "version": "0.4.0",
3
+ "version": "0.8.0",
4
4
  "description": "Git for API interactions. Record, replay, snapshot, and diff your APIs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,8 +41,10 @@
41
41
  "chalk": "^5.3.0",
42
42
  "cli-table3": "^0.6.3",
43
43
  "commander": "^12.0.0",
44
+ "dotenv": "^17.3.1",
44
45
  "inquirer": "^9.2.15",
45
46
  "js-yaml": "^4.1.0",
47
+ "lingo.dev": "^0.133.1",
46
48
  "ora": "^8.0.1"
47
49
  },
48
50
  "engines": {
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/edit.js ADDED
@@ -0,0 +1,85 @@
1
+ import { readFileSync, writeFileSync, unlinkSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { execSync } from 'child_process';
4
+ import chalk from 'chalk';
5
+ import { loadInteraction } from './storage.js';
6
+ import { replayInteraction } from './replay.js';
7
+
8
+ export async function editInteraction(id) {
9
+ try {
10
+ const interaction = loadInteraction(id);
11
+ if (!interaction) {
12
+ console.log(chalk.red(`\n βœ— Interaction not found: ${id}\n`));
13
+ return;
14
+ }
15
+
16
+ // Step 2: Generate temporary editable JSON
17
+ const tmpFileName = `.kiroo/tmp_edit_${id}.json`;
18
+ const editableData = {
19
+ method: interaction.request.method,
20
+ url: interaction.request.url,
21
+ headers: interaction.request.headers || {},
22
+ data: interaction.request.body || null
23
+ };
24
+
25
+ writeFileSync(tmpFileName, JSON.stringify(editableData, null, 2));
26
+
27
+ // Step 3: Launch Editor
28
+ const editor = process.env.EDITOR || 'code'; // Fallbacks: code, nano, vim, notepad...
29
+
30
+ console.log(chalk.cyan(`\n πŸ“ Opening interaction in your editor (${editor})...`));
31
+ console.log(chalk.gray(` Save and close the file to automatically replay the request.`));
32
+
33
+ try {
34
+ // In windows, 'code -w' waits for VS Code to close.
35
+ // If notepad, just 'notepad' blocks until closed.
36
+ // If using fallback, we try standard blocking call.
37
+ const launchCmd = editor === 'code' ? 'code -w' : editor;
38
+ execSync(`${launchCmd} ${tmpFileName}`, { stdio: 'inherit' });
39
+ } catch (e) {
40
+ console.log(chalk.red(`\n βœ— Failed to open editor '${editor}'.`));
41
+ console.log(chalk.gray(` Please specify a valid editor via EDITOR environment variable (e.g. EDITOR=nano kiroo edit).`));
42
+ try { unlinkSync(tmpFileName); } catch(err){}
43
+ return;
44
+ }
45
+
46
+ // Step 4: Editor closed, read updated data
47
+ let updatedDataStr;
48
+ try {
49
+ updatedDataStr = readFileSync(tmpFileName, 'utf-8');
50
+ } catch (e) {
51
+ console.log(chalk.red('\n βœ— Could not read the edited file. Editing cancelled.\n'));
52
+ return;
53
+ }
54
+
55
+ let updatedData;
56
+ try {
57
+ updatedData = JSON.parse(updatedDataStr);
58
+ } catch (e) {
59
+ console.log(chalk.red('\n βœ— Invalid JSON syntax in edited file. Editing cancelled.\n'));
60
+ try { unlinkSync(tmpFileName); } catch(err){}
61
+ return;
62
+ }
63
+
64
+ // Step 5: Merge and Rewrite
65
+ interaction.request.method = updatedData.method;
66
+ interaction.request.url = updatedData.url;
67
+ interaction.request.headers = updatedData.headers;
68
+ interaction.request.body = updatedData.data;
69
+
70
+ // Save it over the original file
71
+ const originalPath = join('.kiroo', 'interactions', `${id}.json`);
72
+ writeFileSync(originalPath, JSON.stringify(interaction, null, 2));
73
+
74
+ // Cleanup tmp file
75
+ try { unlinkSync(tmpFileName); } catch(err){}
76
+
77
+ console.log(chalk.green(`\n βœ… Interaction updated. Replaying now...\n`));
78
+
79
+ // Step 6: Trigger replay
80
+ await replayInteraction(id);
81
+
82
+ } catch (error) {
83
+ console.error(chalk.red(`\n βœ— Edit Failed: ${error.message}\n`));
84
+ }
85
+ }
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/export.js ADDED
@@ -0,0 +1,93 @@
1
+ import { writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import chalk from 'chalk';
4
+ import { getAllInteractions } from './storage.js';
5
+
6
+ export function exportToPostman(outFileName) {
7
+ try {
8
+ const interactions = getAllInteractions();
9
+
10
+ if (interactions.length === 0) {
11
+ console.log(chalk.yellow('\n ⚠️ No interactions found to export.'));
12
+ console.log(chalk.gray(' Run some requests first before exporting.\n'));
13
+ return;
14
+ }
15
+
16
+ const postmanCollection = {
17
+ info: {
18
+ name: `Kiroo Export - ${new Date().toISOString().split('T')[0]}`,
19
+ schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
20
+ },
21
+ item: interactions.map(int => {
22
+ // Map Headers
23
+ const headerList = Object.entries(int.request.headers || {}).map(([key, value]) => ({
24
+ key,
25
+ value: value.toString(),
26
+ type: "text"
27
+ }));
28
+
29
+ // Format body
30
+ let rawBody = '';
31
+ if (int.request.body) {
32
+ rawBody = typeof int.request.body === 'object'
33
+ ? JSON.stringify(int.request.body, null, 2)
34
+ : int.request.body.toString();
35
+ }
36
+
37
+ // Response Body
38
+ let resBodyStr = '';
39
+ if (int.response.body) {
40
+ resBodyStr = typeof int.response.body === 'object'
41
+ ? JSON.stringify(int.response.body, null, 2)
42
+ : int.response.body.toString();
43
+ }
44
+
45
+ return {
46
+ name: `[${int.request.method}] ${int.request.url}`,
47
+ request: {
48
+ method: int.request.method.toUpperCase(),
49
+ header: headerList,
50
+ url: {
51
+ raw: int.request.url
52
+ },
53
+ ...(rawBody ? {
54
+ body: {
55
+ mode: "raw",
56
+ raw: rawBody,
57
+ options: {
58
+ raw: { language: "json" }
59
+ }
60
+ }
61
+ } : {})
62
+ },
63
+ response: [
64
+ {
65
+ name: "Saved Example from Kiroo",
66
+ originalRequest: {
67
+ method: int.request.method.toUpperCase(),
68
+ header: headerList,
69
+ url: { raw: int.request.url }
70
+ },
71
+ status: "Saved Response",
72
+ code: int.response.status,
73
+ _postman_previewlanguage: "json",
74
+ header: [],
75
+ cookie: [],
76
+ body: resBodyStr
77
+ }
78
+ ]
79
+ };
80
+ })
81
+ };
82
+
83
+ const outputPath = join(process.cwd(), outFileName);
84
+ writeFileSync(outputPath, JSON.stringify(postmanCollection, null, 2));
85
+
86
+ console.log(chalk.green(`\n βœ… Collection exported successfully!`));
87
+ console.log(chalk.gray(` Saved to: ${outputPath}`));
88
+ console.log(chalk.magenta(` You can now import this file directly into Postman/Insomnia.\n`));
89
+
90
+ } catch (error) {
91
+ console.error(chalk.red('\n βœ— Export failed:'), error.message, '\n');
92
+ }
93
+ }
package/src/lingo.js ADDED
@@ -0,0 +1,36 @@
1
+ import { LingoDotDevEngine } from "lingo.dev/sdk";
2
+ import chalk from "chalk";
3
+ import { loadEnv } from "./storage.js";
4
+
5
+ function getLingoEngine() {
6
+ const envData = loadEnv();
7
+ const currentEnvVars = envData.environments[envData.current] || {};
8
+
9
+ // Prioritize process.env, fallback to kiroo environments
10
+ const apiKey = currentEnvVars.LINGODOTDEV_API_KEY;
11
+
12
+ if (!apiKey) {
13
+ console.log(chalk.yellow(`\n ⚠️ LINGODOTDEV_API_KEY not found.`));
14
+ console.log(chalk.gray(`run 'kiroo env set LINGODOTDEV_API_KEY <your_key>'\n`));
15
+ return null;
16
+ }
17
+
18
+ return new LingoDotDevEngine({ apiKey });
19
+ }
20
+
21
+ export async function translateText(text, targetLang) {
22
+ const engine = getLingoEngine();
23
+ if (!engine) return text;
24
+
25
+ try {
26
+ const result = await engine.localizeText(text, {
27
+ sourceLocale: 'en',
28
+ targetLocale: targetLang,
29
+ fast: true
30
+ });
31
+ return result;
32
+ } catch (error) {
33
+ console.log(chalk.red(`\n ⚠️ Translation failed: ${error.message}`));
34
+ return text;
35
+ }
36
+ }
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
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import Table from 'cli-table3';
3
3
  import { getAllInteractions, saveSnapshotData, getAllSnapshots, loadSnapshotData } from './storage.js';
4
+ import { translateText } from './lingo.js';
4
5
 
5
6
  export async function saveSnapshot(tag) {
6
7
  const interactions = getAllInteractions();
@@ -49,12 +50,15 @@ export async function listSnapshots() {
49
50
  console.log('');
50
51
  }
51
52
 
52
- export async function compareSnapshots(tag1, tag2) {
53
+ export async function compareSnapshots(tag1, tag2, lang) {
53
54
  try {
54
55
  const s1 = loadSnapshotData(tag1);
55
56
  const s2 = loadSnapshotData(tag2);
56
57
 
57
58
  console.log(chalk.cyan(`\n πŸ” Comparing Snapshots:`), chalk.white(tag1), chalk.gray('vs'), chalk.white(tag2));
59
+ if (lang) {
60
+ console.log(chalk.magenta(` 🌍 Translating output to: ${chalk.white(lang.toUpperCase())} using Lingo.dev...`));
61
+ }
58
62
 
59
63
  const results = [];
60
64
  let breakingChanges = 0;
@@ -93,18 +97,62 @@ export async function compareSnapshots(tag1, tag2) {
93
97
  breakingChanges++;
94
98
  }
95
99
 
96
- // Deep field comparison (very basic for MVP)
97
- if (typeof int1.response.body === 'object' && typeof int2.response.body === 'object' && int1.response.body !== null && int2.response.body !== null) {
98
- const keys1 = Object.keys(int1.response.body);
99
- const keys2 = Object.keys(int2.response.body);
100
+ // Helper for deep structural comparison
101
+ const deepCompare = (val1, val2, path = '') => {
102
+ const changes = [];
100
103
 
101
- const removed = keys1.filter(k => !keys2.includes(k));
102
- if (removed.length > 0) {
103
- diffs.push(`Fields removed: ${chalk.red(removed.join(', '))}`);
104
- breakingChanges++;
104
+ // Handle nulls
105
+ if (val1 === null && val2 !== null) return [{ path, msg: `type changed from null to ${typeof val2}`, breaking: false }];
106
+ if (val1 !== null && val2 === null) return [{ path, msg: `type changed from ${typeof val1} to null`, breaking: false }];
107
+ if (val1 === null && val2 === null) return changes;
108
+
109
+ const type1 = Array.isArray(val1) ? 'array' : typeof val1;
110
+ const type2 = Array.isArray(val2) ? 'array' : typeof val2;
111
+
112
+ if (type1 !== type2) {
113
+ changes.push({ path, msg: `type changed from ${chalk.yellow(type1)} to ${chalk.yellow(type2)}`, breaking: true });
114
+ return changes;
105
115
  }
106
- }
107
-
116
+
117
+ if (type1 === 'object') {
118
+ const keys1 = Object.keys(val1);
119
+ const keys2 = Object.keys(val2);
120
+
121
+ // Check for removed keys (Breaking)
122
+ for (const k of keys1) {
123
+ const currentPath = path ? `${path}.${k}` : k;
124
+ if (!keys2.includes(k)) {
125
+ changes.push({ path: currentPath, msg: `was ${chalk.red('removed')}`, breaking: true });
126
+ } else {
127
+ changes.push(...deepCompare(val1[k], val2[k], currentPath));
128
+ }
129
+ }
130
+
131
+ // Check for added keys (Non-breaking)
132
+ for (const k of keys2) {
133
+ const currentPath = path ? `${path}.${k}` : k;
134
+ if (!keys1.includes(k)) {
135
+ changes.push({ path: currentPath, msg: `was ${chalk.green('added')}`, breaking: false });
136
+ }
137
+ }
138
+ } else if (type1 === 'array') {
139
+ // Array structure validation (check first item schema only if exists)
140
+ if (val1.length > 0 && val2.length > 0) {
141
+ const itemPath = path ? `${path}[0]` : '[0]';
142
+ changes.push(...deepCompare(val1[0], val2[0], itemPath));
143
+ }
144
+ }
145
+
146
+ return changes;
147
+ };
148
+
149
+ if (int1.response.body !== undefined && int2.response.body !== undefined) {
150
+ const structuralChanges = deepCompare(int1.response.body, int2.response.body);
151
+ for (const change of structuralChanges) {
152
+ diffs.push(`${chalk.cyan(change.path || 'root')} ${change.msg}`);
153
+ if (change.breaking) breakingChanges++;
154
+ }
155
+ }
108
156
  if (diffs.length > 0) {
109
157
  results.push({
110
158
  type: 'CHANGE',
@@ -116,19 +164,36 @@ export async function compareSnapshots(tag1, tag2) {
116
164
  });
117
165
 
118
166
  if (results.length === 0) {
119
- console.log(chalk.green('\n βœ… No differences detected. Your API is stable!\n'));
167
+ let finalMsg = 'No differences detected. Your API is stable!';
168
+ if (lang) finalMsg = await translateText(finalMsg, lang);
169
+ console.log(chalk.green(`\n βœ… ${finalMsg}\n`));
120
170
  } else {
121
171
  console.log('');
122
- results.forEach(res => {
172
+
173
+ for (const res of results) {
174
+ let printMsg = res.msg;
175
+ if (lang) {
176
+ // Basic translation hook for individual diff items (stripping ansi)
177
+ const cleanMsg = printMsg.replace(/\x1B\[[0-9;]*m/g, '');
178
+ const translatedMsg = await translateText(cleanMsg, lang);
179
+ printMsg = chalk.yellow('[Translated] ') + translatedMsg;
180
+ }
181
+
123
182
  const symbol = res.type === 'NEW' ? chalk.blue('+') : chalk.yellow('⚠️');
124
183
  console.log(` ${symbol} ${chalk.white(res.method)} ${chalk.gray(res.url)}`);
125
- console.log(` ${res.msg}`);
126
- });
184
+ console.log(` ${printMsg}`);
185
+ }
127
186
 
187
+ let alertMsg = breakingChanges > 0
188
+ ? `Detected ${breakingChanges} potential breaking changes!`
189
+ : `Non-breaking changes detected.`;
190
+
191
+ if (lang) alertMsg = await translateText(alertMsg, lang);
192
+
128
193
  if (breakingChanges > 0) {
129
- console.log(chalk.red(`\n 🚨 Detected ${breakingChanges} potential breaking changes!\n`));
194
+ console.log(chalk.red(`\n 🚨 ${alertMsg}\n`));
130
195
  } else {
131
- console.log(chalk.blue('\n ℹ️ Non-breaking changes detected.\n'));
196
+ console.log(chalk.blue(`\n ℹ️ ${alertMsg}\n`));
132
197
  }
133
198
  }
134
199
 
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) {