kiroo 0.7.4 โ†’ 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
@@ -51,12 +51,13 @@ Coming from a browser? Don't type a single header.
51
51
 
52
52
  ## โœจ Features that WOW
53
53
 
54
- ### ๐ŸŸข **Git-Native Testing**
55
- Capture a **Snapshot** of your entire API state and compare versions to detect breaking changes instantly.
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.
56
58
  ```bash
57
59
  kiroo snapshot save v1-stable
58
- # ... make changes ...
59
- kiroo snapshot compare v1-stable current
60
+ kiroo --lang hi snapshot compare v1-stable current
60
61
  ```
61
62
 
62
63
  ### ๐ŸŒ **Variable Chaining**
@@ -74,6 +75,20 @@ kiroo post /api/user -d "name=Yash email=yash@kiroo.io role=admin"
74
75
 
75
76
  ---
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.
80
+ ```bash
81
+ kiroo check /api/login -m POST -d "user=yash pass=123" --status 200 --has token
82
+ ```
83
+
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
88
+ ```
89
+
90
+ ---
91
+
77
92
  ## ๐Ÿš€ Quick Start
78
93
 
79
94
  ### 1. Installation
@@ -141,6 +156,16 @@ Re-run a specific interaction.
141
156
  kiroo replay 2026-03-10T14-30-05-123Z
142
157
  ```
143
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
+
144
169
  ### `kiroo check <url>`
145
170
  Zero-Code Testing engine.
146
171
  - **Description**: Executes a request and runs assertions on the response. Exits with code 1 on failure.
@@ -217,6 +242,16 @@ Snapshot management.
217
242
  kiroo snapshot compare v1.stable current
218
243
  ```
219
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
+
220
255
  ### `kiroo env`
221
256
  Environment & Variable management.
222
257
  - **Commands**:
package/bin/kiroo.js CHANGED
@@ -13,13 +13,16 @@ import { showStats } from '../src/stats.js';
13
13
  import { handleImport } from '../src/import.js';
14
14
  import { clearAllInteractions } from '../src/storage.js';
15
15
  import { runBenchmark } from '../src/bench.js';
16
+ import { editInteraction } from '../src/edit.js';
17
+ import { exportToPostman } from '../src/export.js';
16
18
 
17
19
  const program = new Command();
18
20
 
19
21
  program
20
22
  .name('kiroo')
21
23
  .description('Git for API interactions. Record, replay, snapshot, and diff your APIs.')
22
- .version('0.7.4');
24
+ .version('0.8.0')
25
+ .option('--lang <language>', 'Translate output to specified language (e.g., hi, es, fr)');
23
26
 
24
27
  // Init command
25
28
  program
@@ -115,6 +118,23 @@ program
115
118
  await replayInteraction(id);
116
119
  });
117
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
+
118
138
  // Bench command (Load Testing)
119
139
  program
120
140
  .command('bench <url>')
@@ -195,7 +215,8 @@ snapshot
195
215
  .command('compare <tag1> <tag2>')
196
216
  .description('Compare two snapshots')
197
217
  .action(async (tag1, tag2) => {
198
- await compareSnapshots(tag1, tag2);
218
+ const opts = program.opts();
219
+ await compareSnapshots(tag1, tag2, opts.lang);
199
220
  });
200
221
 
201
222
  // Graph command
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiroo",
3
- "version": "0.7.4",
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/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/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/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