kiroo 0.9.0 → 0.9.5
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 +19 -1
- package/bin/kiroo.js +64 -13
- package/package.json +1 -1
- package/src/analyze.js +27 -9
- package/src/bench.js +11 -4
- package/src/checker.js +26 -9
- package/src/executor.js +1 -1
- package/src/export.js +62 -5
- package/src/formatter.js +18 -6
- package/src/lingo.js +24 -1
- package/src/proxy.js +20 -0
- package/src/run.js +246 -0
- package/src/stats.js +15 -5
package/README.md
CHANGED
|
@@ -141,7 +141,7 @@ Initialize Kiroo in your current project.
|
|
|
141
141
|
kiroo init
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
-
### `kiroo get/post/put/delete <url>`
|
|
144
|
+
### `kiroo get/post/put/delete/patch <url>`
|
|
145
145
|
Execute and record an API interaction.
|
|
146
146
|
- **Description**: Performs an HTTP request, displays the response, and saves it to history.
|
|
147
147
|
- **Prerequisites**: Access to the URL (or a `baseUrl` set in the environment).
|
|
@@ -154,6 +154,7 @@ Execute and record an API interaction.
|
|
|
154
154
|
- **Example**:
|
|
155
155
|
```bash
|
|
156
156
|
kiroo post /api/auth/login -d "email=user@test.com password=123" --save token=data.token
|
|
157
|
+
kiroo patch /api/profile -d "name=UpdatedName"
|
|
157
158
|
```
|
|
158
159
|
|
|
159
160
|
### `kiroo list`
|
|
@@ -271,9 +272,15 @@ Snapshot management.
|
|
|
271
272
|
- `list`: List all saved snapshots.
|
|
272
273
|
- `compare <tag1> <tag2>`: Detect structural changes between two states.
|
|
273
274
|
- `compare <tag1> <tag2> --analyze`: Structural compare + semantic severity in one run.
|
|
275
|
+
- `run <tag>`: Execute all interactions in a snapshot sequentially (supports auth token chaining).
|
|
276
|
+
- **Options for `run`**:
|
|
277
|
+
- `-v, --verbose`: Show response preview for each request.
|
|
278
|
+
- `--fail-fast`: Stop immediately on first failed interaction.
|
|
279
|
+
- `--timeout <ms>`: Request timeout in milliseconds (Default: 30000).
|
|
274
280
|
- **Example**:
|
|
275
281
|
```bash
|
|
276
282
|
kiroo snapshot compare v1.stable current
|
|
283
|
+
kiroo snapshot run v1.stable --fail-fast
|
|
277
284
|
```
|
|
278
285
|
|
|
279
286
|
### `kiroo analyze <tag1> <tag2>`
|
|
@@ -349,6 +356,17 @@ Wipe history.
|
|
|
349
356
|
kiroo clear --force
|
|
350
357
|
```
|
|
351
358
|
|
|
359
|
+
### `kiroo help [command]`
|
|
360
|
+
Get command-level help directly in CLI.
|
|
361
|
+
- **Description**: Shows global help or detailed help for a specific command.
|
|
362
|
+
- **Arguments**:
|
|
363
|
+
- `command`: (Optional) Command name to inspect.
|
|
364
|
+
- **Example**:
|
|
365
|
+
```bash
|
|
366
|
+
kiroo help
|
|
367
|
+
kiroo help bench
|
|
368
|
+
```
|
|
369
|
+
|
|
352
370
|
---
|
|
353
371
|
|
|
354
372
|
## 🤝 Contributing
|
package/bin/kiroo.js
CHANGED
|
@@ -17,19 +17,24 @@ import { editInteraction } from '../src/edit.js';
|
|
|
17
17
|
import { exportInteractions } from '../src/export.js';
|
|
18
18
|
import { runProxy } from '../src/proxy.js';
|
|
19
19
|
import { analyzeSnapshots } from '../src/analyze.js';
|
|
20
|
+
import { runSnapshot } from '../src/run.js';
|
|
20
21
|
|
|
21
22
|
const program = new Command();
|
|
22
23
|
|
|
23
24
|
program
|
|
24
25
|
.name('kiroo')
|
|
25
26
|
.description('Git for API interactions. Record, replay, snapshot, and diff your APIs.')
|
|
26
|
-
.version('0.
|
|
27
|
+
.version('0.9.5')
|
|
28
|
+
.showSuggestionAfterError()
|
|
27
29
|
.option('--lang <language>', 'Translate output to specified language (e.g., hi, es, fr)');
|
|
28
30
|
|
|
29
31
|
// Init command
|
|
30
32
|
program
|
|
31
33
|
.command('init')
|
|
32
34
|
.description('Initialize Kiroo in current directory')
|
|
35
|
+
.addHelpText('after', `
|
|
36
|
+
Examples:
|
|
37
|
+
$ kiroo init`)
|
|
33
38
|
.action(async () => {
|
|
34
39
|
await initProject();
|
|
35
40
|
});
|
|
@@ -39,17 +44,25 @@ program
|
|
|
39
44
|
program
|
|
40
45
|
.command('check <url>')
|
|
41
46
|
.description('Execute a request and validate the response against rules')
|
|
47
|
+
.addHelpText('after', `
|
|
48
|
+
Examples:
|
|
49
|
+
$ kiroo check /api/users --status 200 --has id,email
|
|
50
|
+
$ kiroo check http://api.example.com/login --match status=active`)
|
|
42
51
|
.option('-m, --method <method>', 'HTTP method (GET, POST, etc.)', 'GET')
|
|
52
|
+
// ... (keep options as is)
|
|
43
53
|
.option('-H, --header <header...>', 'Add custom headers')
|
|
44
54
|
.option('-d, --data <data>', 'Request body (JSON or shorthand)')
|
|
45
55
|
.option('--status <code...>', 'Expected HTTP status code')
|
|
46
56
|
.option('--has <fields...>', 'Comma-separated list of expected fields in JSON response')
|
|
47
57
|
.option('--match <matches...>', 'Expected field values (e.g., status=active)')
|
|
48
58
|
.action(async (url, options) => {
|
|
59
|
+
// ...
|
|
49
60
|
// Execute request
|
|
61
|
+
const opts = program.opts();
|
|
50
62
|
const response = await executeRequest(options.method || 'GET', url, {
|
|
51
63
|
header: options.header,
|
|
52
64
|
data: options.data,
|
|
65
|
+
lang: opts.lang
|
|
53
66
|
});
|
|
54
67
|
|
|
55
68
|
if (!response) {
|
|
@@ -78,13 +91,12 @@ program
|
|
|
78
91
|
|
|
79
92
|
// Validate
|
|
80
93
|
const validation = validateResponse(response, rules);
|
|
81
|
-
showCheckResult(validation);
|
|
94
|
+
await showCheckResult(validation, opts.lang);
|
|
82
95
|
|
|
83
96
|
if (!validation.passed) {
|
|
84
97
|
process.exit(1);
|
|
85
98
|
}
|
|
86
99
|
});
|
|
87
|
-
// sk_live_p7BWJjsYlKmauBOjiEeiLRuu4DokkBWsgYne_E6osTo
|
|
88
100
|
|
|
89
101
|
// HTTP methods as commands
|
|
90
102
|
['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].forEach(method => {
|
|
@@ -96,7 +108,8 @@ program
|
|
|
96
108
|
.option('-d, --data <data>', 'Request body (JSON string or key=value pairs)')
|
|
97
109
|
.option('-s, --save <pairs...>', 'Extract values from response to env (key=path.to.data)')
|
|
98
110
|
.action(async (url, options) => {
|
|
99
|
-
|
|
111
|
+
const opts = program.opts();
|
|
112
|
+
await executeRequest(method, url, { ...options, lang: opts.lang });
|
|
100
113
|
});
|
|
101
114
|
});
|
|
102
115
|
|
|
@@ -155,7 +168,8 @@ program
|
|
|
155
168
|
.option('-v, --verbose', 'Show detailed output for every request')
|
|
156
169
|
.option('-d, --data <data>', 'Request body')
|
|
157
170
|
.action(async (url, options) => {
|
|
158
|
-
|
|
171
|
+
const opts = program.opts();
|
|
172
|
+
await runBenchmark(url, { ...options, lang: opts.lang });
|
|
159
173
|
});
|
|
160
174
|
|
|
161
175
|
// Clear command
|
|
@@ -266,6 +280,20 @@ snapshot
|
|
|
266
280
|
}
|
|
267
281
|
});
|
|
268
282
|
|
|
283
|
+
snapshot
|
|
284
|
+
.command('run <tag>')
|
|
285
|
+
.description('Execute all interactions from a snapshot sequentially (auto-chains auth tokens)')
|
|
286
|
+
.option('-v, --verbose', 'Show response body preview for each request')
|
|
287
|
+
.option('--fail-fast', 'Exit immediately on first failure')
|
|
288
|
+
.option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
|
|
289
|
+
.action(async (tag, options) => {
|
|
290
|
+
await runSnapshot(tag, {
|
|
291
|
+
verbose: !!options.verbose,
|
|
292
|
+
failFast: !!options.failFast,
|
|
293
|
+
timeout: parseInt(options.timeout, 10) || 30000,
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
269
297
|
program
|
|
270
298
|
.command('analyze <tag1> <tag2>')
|
|
271
299
|
.description('Semantic blast-radius analysis between two snapshots')
|
|
@@ -336,9 +364,33 @@ program
|
|
|
336
364
|
// Stats command
|
|
337
365
|
program
|
|
338
366
|
.command('stats')
|
|
339
|
-
.
|
|
367
|
+
.alias('stat')
|
|
368
|
+
.description('Show usage statistics and bottlenecks')
|
|
369
|
+
.addHelpText('after', `
|
|
370
|
+
Examples:
|
|
371
|
+
$ kiroo stats
|
|
372
|
+
$ kiroo stats --lang hi`)
|
|
340
373
|
.action(async () => {
|
|
341
|
-
|
|
374
|
+
const opts = program.opts();
|
|
375
|
+
await showStats({ lang: opts.lang });
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Help command
|
|
379
|
+
program
|
|
380
|
+
.command('help [command]')
|
|
381
|
+
.description('Display help for a command')
|
|
382
|
+
.action((cmd) => {
|
|
383
|
+
if (cmd) {
|
|
384
|
+
const subCommand = program.commands.find(c => c.name() === cmd || c.aliases().includes(cmd));
|
|
385
|
+
if (subCommand) {
|
|
386
|
+
subCommand.help();
|
|
387
|
+
} else {
|
|
388
|
+
console.error(chalk.red(`\n ✗ Unknown command: ${cmd}`));
|
|
389
|
+
program.help();
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
program.help();
|
|
393
|
+
}
|
|
342
394
|
});
|
|
343
395
|
|
|
344
396
|
// Error handling
|
|
@@ -347,15 +399,14 @@ program.exitOverride();
|
|
|
347
399
|
try {
|
|
348
400
|
await program.parseAsync(process.argv);
|
|
349
401
|
} catch (err) {
|
|
350
|
-
if (err.code === 'commander.help' || err.message === '(outputHelp)') {
|
|
351
|
-
// Help was requested, exit normally
|
|
402
|
+
if (err.code === 'commander.help' || err.code === 'commander.version' || err.message === '(outputHelp)') {
|
|
352
403
|
process.exit(0);
|
|
353
|
-
} else if (err.code === 'commander.unknownCommand') {
|
|
354
|
-
|
|
355
|
-
console.log(chalk.gray(' Run'), chalk.white('kiroo --help'), chalk.gray('for usage information.\n'));
|
|
404
|
+
} else if (err.code === 'commander.unknownCommand' || err.code === 'commander.missingArgument') {
|
|
405
|
+
// Commander already printed the error and suggestion, just exit
|
|
356
406
|
process.exit(1);
|
|
357
407
|
} else {
|
|
358
|
-
|
|
408
|
+
// For other unexpected errors, we print our own formatted message
|
|
409
|
+
console.error(chalk.red('\n ✗ Error:'), err.message, '\n');
|
|
359
410
|
process.exit(1);
|
|
360
411
|
}
|
|
361
412
|
}
|
package/package.json
CHANGED
package/src/analyze.js
CHANGED
|
@@ -500,18 +500,25 @@ export async function analyzeSnapshots(tag1, tag2, options = {}) {
|
|
|
500
500
|
if (options.json) {
|
|
501
501
|
console.log(stableJSONStringify(report));
|
|
502
502
|
} else {
|
|
503
|
-
|
|
503
|
+
let header = '🧠 Blast Radius Analysis';
|
|
504
|
+
if (options.lang) header = await translateText(header, options.lang);
|
|
505
|
+
console.log(chalk.cyan(`\n ${header}`));
|
|
504
506
|
console.log(chalk.gray(` Source: ${tag1}`));
|
|
505
507
|
console.log(chalk.gray(` Target: ${tag2}\n`));
|
|
506
508
|
|
|
507
509
|
const shownEndpoints = report.endpoints.slice(0, 6);
|
|
508
510
|
for (const endpoint of shownEndpoints) {
|
|
511
|
+
let severityLabel = endpoint.highestSeverity.toUpperCase();
|
|
512
|
+
if (options.lang) severityLabel = await translateText(severityLabel, options.lang);
|
|
509
513
|
const severityColor = colorForSeverity(endpoint.highestSeverity);
|
|
510
|
-
console.log(` ${severityColor(
|
|
511
|
-
|
|
514
|
+
console.log(` ${severityColor(severityLabel)} ${chalk.white(endpoint.method)} ${chalk.gray(endpoint.path)}`);
|
|
515
|
+
|
|
516
|
+
for (const issue of endpoint.issues.slice(0, 4)) {
|
|
512
517
|
const issueColor = colorForSeverity(issue.severity);
|
|
513
|
-
|
|
514
|
-
|
|
518
|
+
let issueMsg = issue.message;
|
|
519
|
+
if (options.lang) issueMsg = await translateText(issueMsg, options.lang);
|
|
520
|
+
console.log(` - ${issueColor(issue.severity)} ${issueMsg}`);
|
|
521
|
+
}
|
|
515
522
|
if (endpoint.issues.length > 4) {
|
|
516
523
|
console.log(chalk.gray(` - ... +${endpoint.issues.length - 4} more issues`));
|
|
517
524
|
}
|
|
@@ -531,10 +538,21 @@ export async function analyzeSnapshots(tag1, tag2, options = {}) {
|
|
|
531
538
|
modelPriority,
|
|
532
539
|
maxTokens
|
|
533
540
|
});
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
541
|
+
let aiSectionTitle = `🤖 AI Summary (${ai.model})`;
|
|
542
|
+
if (options.lang) aiSectionTitle = await translateText(aiSectionTitle, options.lang);
|
|
543
|
+
console.log(chalk.magenta(`\n ${aiSectionTitle}`));
|
|
544
|
+
|
|
545
|
+
const bullets = toConciseBullets(ai.summary, report);
|
|
546
|
+
for (const bullet of bullets) {
|
|
547
|
+
let finalBullet = bullet;
|
|
548
|
+
if (options.lang) {
|
|
549
|
+
// Translate the text part of the bullet
|
|
550
|
+
const bulletText = bullet.replace(/^- /, '');
|
|
551
|
+
const translatedBullet = await translateText(bulletText, options.lang);
|
|
552
|
+
finalBullet = `- ${translatedBullet}`;
|
|
553
|
+
}
|
|
554
|
+
console.log(` ${finalBullet}`);
|
|
555
|
+
}
|
|
538
556
|
}
|
|
539
557
|
|
|
540
558
|
if (failOnSeverity && shouldFail(report, failOnSeverity)) {
|
package/src/bench.js
CHANGED
|
@@ -4,6 +4,7 @@ import axios from 'axios';
|
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import { loadEnv } from './storage.js';
|
|
6
6
|
import { applyEnvReplacements } from './executor.js';
|
|
7
|
+
import { translateText } from './lingo.js';
|
|
7
8
|
|
|
8
9
|
export async function runBenchmark(url, options) {
|
|
9
10
|
const envData = loadEnv();
|
|
@@ -177,7 +178,7 @@ export async function runBenchmark(url, options) {
|
|
|
177
178
|
}
|
|
178
179
|
};
|
|
179
180
|
|
|
180
|
-
const finalizeBenchmark = () => {
|
|
181
|
+
const finalizeBenchmark = async () => {
|
|
181
182
|
const totalTime = Date.now() - startTime;
|
|
182
183
|
if (!isVerbose) spinner.stop();
|
|
183
184
|
|
|
@@ -191,7 +192,9 @@ export async function runBenchmark(url, options) {
|
|
|
191
192
|
avg = Math.round(results.times.reduce((a, b) => a + b, 0) / results.times.length);
|
|
192
193
|
}
|
|
193
194
|
|
|
194
|
-
|
|
195
|
+
let resultTitle = '🚀 Benchmark Results';
|
|
196
|
+
if (options.lang) resultTitle = await translateText(resultTitle, options.lang);
|
|
197
|
+
console.log('\n ' + chalk.blue.bold(resultTitle));
|
|
195
198
|
console.log(' ' + chalk.gray(`${method} ${targetUrl}\n`));
|
|
196
199
|
|
|
197
200
|
const statsTable = new Table({
|
|
@@ -212,9 +215,13 @@ export async function runBenchmark(url, options) {
|
|
|
212
215
|
console.log(statsTable.toString());
|
|
213
216
|
|
|
214
217
|
if (results.failures > 0) {
|
|
215
|
-
|
|
218
|
+
let errorMsg = `⚠️ ${results.failures} requests failed (HTTP 4xx/5xx or Network Error).`;
|
|
219
|
+
if (options.lang) errorMsg = await translateText(errorMsg, options.lang);
|
|
220
|
+
console.log(chalk.red(`\n ${errorMsg}\n`));
|
|
216
221
|
} else {
|
|
217
|
-
|
|
222
|
+
let successMsg = '✅ All requests completed successfully.';
|
|
223
|
+
if (options.lang) successMsg = await translateText(successMsg, options.lang);
|
|
224
|
+
console.log(chalk.green(`\n ${successMsg}\n`));
|
|
218
225
|
}
|
|
219
226
|
|
|
220
227
|
resolve();
|
package/src/checker.js
CHANGED
|
@@ -76,24 +76,41 @@ function getDeep(obj, path) {
|
|
|
76
76
|
return keys.reduce((acc, key) => acc && acc[key], obj);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
import { translateText } from './lingo.js';
|
|
80
|
+
|
|
81
|
+
export async function showCheckResult(validation, lang) {
|
|
82
|
+
let title = '🧪 Test Results:';
|
|
83
|
+
if (lang) title = await translateText(title, lang);
|
|
84
|
+
console.log(chalk.cyan(`\n ${title}`));
|
|
81
85
|
|
|
82
|
-
validation.results
|
|
86
|
+
for (const res of validation.results) {
|
|
83
87
|
const icon = res.passed ? chalk.green('✓') : chalk.red('✗');
|
|
84
88
|
const color = res.passed ? chalk.white : chalk.red;
|
|
85
89
|
|
|
86
|
-
|
|
90
|
+
let label = res.label;
|
|
91
|
+
if (lang) label = await translateText(label, lang);
|
|
92
|
+
console.log(` ${icon} ${label}`);
|
|
93
|
+
|
|
87
94
|
if (!res.passed) {
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
let expectedLabel = 'Expected:';
|
|
96
|
+
let actualLabel = 'Actual:';
|
|
97
|
+
if (lang) {
|
|
98
|
+
expectedLabel = await translateText(expectedLabel, lang);
|
|
99
|
+
actualLabel = await translateText(actualLabel, lang);
|
|
100
|
+
}
|
|
101
|
+
console.log(chalk.gray(` ${expectedLabel} ${res.expected}`));
|
|
102
|
+
console.log(chalk.gray(` ${actualLabel} ${res.actual}`));
|
|
90
103
|
}
|
|
91
|
-
}
|
|
104
|
+
}
|
|
92
105
|
|
|
93
106
|
if (validation.passed) {
|
|
94
|
-
|
|
107
|
+
let msg = '✨ ALL TESTS PASSED!';
|
|
108
|
+
if (lang) msg = await translateText(msg, lang);
|
|
109
|
+
console.log(chalk.green.bold(`\n ${msg} \n`));
|
|
95
110
|
} else {
|
|
96
|
-
|
|
111
|
+
let msg = '❌ SOME TESTS FAILED';
|
|
112
|
+
if (lang) msg = await translateText(msg, lang);
|
|
113
|
+
console.log(chalk.red.bold(`\n ${msg} \n`));
|
|
97
114
|
process.exit(1);
|
|
98
115
|
}
|
|
99
116
|
}
|
package/src/executor.js
CHANGED
|
@@ -177,7 +177,7 @@ export async function executeRequest(method, url, options = {}) {
|
|
|
177
177
|
spinner.succeed(chalk.green(`${response.status} ${response.statusText}`) + chalk.gray(` (${duration}ms)`));
|
|
178
178
|
|
|
179
179
|
// Format and display response
|
|
180
|
-
console.log(formatResponse(response));
|
|
180
|
+
console.log(await formatResponse(response, options.lang));
|
|
181
181
|
|
|
182
182
|
// Handle --save option
|
|
183
183
|
if (options.save) {
|
package/src/export.js
CHANGED
|
@@ -96,10 +96,12 @@ function getPathAndOrigin(rawUrl) {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
function inferSchema(value) {
|
|
99
|
-
if (value === null) return { nullable: true };
|
|
99
|
+
if (value === null) return { type: 'object', nullable: true };
|
|
100
100
|
if (Array.isArray(value)) {
|
|
101
101
|
if (value.length === 0) return { type: 'array', items: {} };
|
|
102
|
-
|
|
102
|
+
// Only infer from first 5 items to avoid noise from duplicates
|
|
103
|
+
const sample = value.slice(0, 5);
|
|
104
|
+
const mergedItem = sample.map((item) => inferSchema(item)).reduce(mergeSchemas, {});
|
|
103
105
|
return { type: 'array', items: mergedItem };
|
|
104
106
|
}
|
|
105
107
|
if (value && typeof value === 'object') {
|
|
@@ -253,6 +255,37 @@ function hasHeader(headers, name) {
|
|
|
253
255
|
return Object.keys(headers || {}).some((k) => k.toLowerCase() === name.toLowerCase());
|
|
254
256
|
}
|
|
255
257
|
|
|
258
|
+
const SENSITIVE_PATTERNS = [
|
|
259
|
+
{ regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, replace: '<REDACTED_EMAIL>' },
|
|
260
|
+
{ regex: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, replace: '<REDACTED_JWT>' },
|
|
261
|
+
{ regex: /\b(sk_live_|pk_live_|sk_test_|pk_test_)[A-Za-z0-9_-]{10,}/g, replace: '<REDACTED_KEY>' },
|
|
262
|
+
{ regex: /\b(gsk_|ghp_|gho_|glpat-)[A-Za-z0-9_-]{10,}/g, replace: '<REDACTED_KEY>' },
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
function redactValue(val) {
|
|
266
|
+
if (typeof val !== 'string') return val;
|
|
267
|
+
let result = val;
|
|
268
|
+
for (const { regex, replace } of SENSITIVE_PATTERNS) {
|
|
269
|
+
result = result.replace(regex, replace);
|
|
270
|
+
}
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function truncateAndRedact(value, maxArrayItems = 3) {
|
|
275
|
+
if (value === null || value === undefined) return value;
|
|
276
|
+
if (typeof value === 'string') return redactValue(value);
|
|
277
|
+
if (typeof value !== 'object') return value;
|
|
278
|
+
if (Array.isArray(value)) {
|
|
279
|
+
const sliced = value.slice(0, maxArrayItems);
|
|
280
|
+
return sliced.map((item) => truncateAndRedact(item, maxArrayItems));
|
|
281
|
+
}
|
|
282
|
+
const result = {};
|
|
283
|
+
for (const [k, v] of Object.entries(value)) {
|
|
284
|
+
result[k] = truncateAndRedact(v, maxArrayItems);
|
|
285
|
+
}
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
|
|
256
289
|
function buildOpenApiSpec(interactions, options = {}) {
|
|
257
290
|
const title = options.title || 'Kiroo Traffic API';
|
|
258
291
|
const version = options.apiVersion || '1.0.0';
|
|
@@ -368,7 +401,7 @@ function buildOpenApiSpec(interactions, options = {}) {
|
|
|
368
401
|
content: {
|
|
369
402
|
[mimeType]: {
|
|
370
403
|
schema: mergedBodySchema,
|
|
371
|
-
example: op.requestBodies[0]
|
|
404
|
+
example: truncateAndRedact(op.requestBodies[0])
|
|
372
405
|
}
|
|
373
406
|
}
|
|
374
407
|
};
|
|
@@ -377,17 +410,41 @@ function buildOpenApiSpec(interactions, options = {}) {
|
|
|
377
410
|
const sortedStatuses = Array.from(op.responses.keys()).sort((a, b) => a.localeCompare(b));
|
|
378
411
|
sortedStatuses.forEach((status) => {
|
|
379
412
|
const res = op.responses.get(status);
|
|
413
|
+
const statusNum = parseInt(status, 10);
|
|
414
|
+
|
|
415
|
+
// Meaningful description based on status code
|
|
416
|
+
const description = statusNum === 200 ? 'Successful response'
|
|
417
|
+
: statusNum === 201 ? 'Resource created'
|
|
418
|
+
: statusNum === 204 ? 'No content'
|
|
419
|
+
: statusNum === 304 ? 'Not modified'
|
|
420
|
+
: statusNum === 400 ? 'Bad request'
|
|
421
|
+
: statusNum === 401 ? 'Unauthorized'
|
|
422
|
+
: statusNum === 403 ? 'Forbidden'
|
|
423
|
+
: statusNum === 404 ? 'Not found'
|
|
424
|
+
: statusNum === 409 ? 'Conflict'
|
|
425
|
+
: statusNum === 500 ? 'Internal server error'
|
|
426
|
+
: `Response ${status}`;
|
|
427
|
+
|
|
428
|
+
// 204 and 304 typically have no body
|
|
429
|
+
if (statusNum === 204 || statusNum === 304) {
|
|
430
|
+
operation.responses[status] = { description };
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
380
434
|
const mimeType = Array.from(res.mimeTypes)[0] || 'application/json';
|
|
381
435
|
const schema = res.bodies.length > 0
|
|
382
436
|
? res.bodies.map((b) => inferSchema(b)).reduce(mergeSchemas, {})
|
|
383
437
|
: {};
|
|
384
438
|
|
|
439
|
+
// Recursively truncate arrays and redact sensitive data in examples
|
|
440
|
+
let example = res.example !== undefined ? truncateAndRedact(res.example) : undefined;
|
|
441
|
+
|
|
385
442
|
operation.responses[status] = {
|
|
386
|
-
description
|
|
443
|
+
description,
|
|
387
444
|
content: {
|
|
388
445
|
[mimeType]: {
|
|
389
446
|
schema,
|
|
390
|
-
...(
|
|
447
|
+
...(example !== undefined ? { example } : {})
|
|
391
448
|
}
|
|
392
449
|
}
|
|
393
450
|
};
|
package/src/formatter.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import { translateText, translateResponseData } from './lingo.js';
|
|
4
|
+
|
|
5
|
+
export async function formatResponse(response, lang) {
|
|
4
6
|
const lines = [];
|
|
5
7
|
|
|
6
8
|
// Status
|
|
@@ -21,7 +23,10 @@ export function formatResponse(response) {
|
|
|
21
23
|
.slice(0, 3);
|
|
22
24
|
|
|
23
25
|
if (headers.length > 0) {
|
|
24
|
-
|
|
26
|
+
let headersLabel = ' Headers:';
|
|
27
|
+
if (lang) headersLabel = await translateText(headersLabel, lang);
|
|
28
|
+
lines.push(chalk.gray(headersLabel));
|
|
29
|
+
|
|
25
30
|
headers.forEach(([key, value]) => {
|
|
26
31
|
const displayValue = typeof value === 'string' && value.length > 50
|
|
27
32
|
? value.substring(0, 50) + '...'
|
|
@@ -33,17 +38,24 @@ export function formatResponse(response) {
|
|
|
33
38
|
|
|
34
39
|
// Body
|
|
35
40
|
if (response.data) {
|
|
36
|
-
|
|
41
|
+
let responseLabel = ' Response:';
|
|
42
|
+
if (lang) responseLabel = await translateText(responseLabel, lang);
|
|
43
|
+
lines.push(chalk.gray(responseLabel));
|
|
44
|
+
|
|
45
|
+
let displayData = response.data;
|
|
46
|
+
if (lang) {
|
|
47
|
+
displayData = await translateResponseData(response.data, lang);
|
|
48
|
+
}
|
|
37
49
|
|
|
38
|
-
if (typeof
|
|
50
|
+
if (typeof displayData === 'object') {
|
|
39
51
|
// Pretty print JSON
|
|
40
|
-
const json = JSON.stringify(
|
|
52
|
+
const json = JSON.stringify(displayData, null, 2);
|
|
41
53
|
json.split('\n').forEach(line => {
|
|
42
54
|
lines.push(chalk.cyan(` ${line}`));
|
|
43
55
|
});
|
|
44
56
|
} else {
|
|
45
57
|
// Plain text
|
|
46
|
-
const text = String(
|
|
58
|
+
const text = String(displayData);
|
|
47
59
|
const preview = text.length > 500 ? text.substring(0, 500) + '...' : text;
|
|
48
60
|
lines.push(chalk.white(` ${preview}`));
|
|
49
61
|
}
|
package/src/lingo.js
CHANGED
|
@@ -15,6 +15,7 @@ function getLingoEngine() {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export async function translateText(text, targetLang) {
|
|
18
|
+
if (!text || typeof text !== 'string' || text.trim() === '') return text;
|
|
18
19
|
const engine = getLingoEngine();
|
|
19
20
|
if (!engine) return text;
|
|
20
21
|
|
|
@@ -26,7 +27,29 @@ export async function translateText(text, targetLang) {
|
|
|
26
27
|
});
|
|
27
28
|
return result;
|
|
28
29
|
} catch (error) {
|
|
29
|
-
console.log(chalk.red(`\n ⚠️ Translation failed: ${error.message}`));
|
|
30
|
+
// console.log(chalk.red(`\n ⚠️ Translation failed: ${error.message}`));
|
|
30
31
|
return text;
|
|
31
32
|
}
|
|
32
33
|
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Recursively translates strings within an object or array
|
|
37
|
+
*/
|
|
38
|
+
export async function translateResponseData(data, targetLang) {
|
|
39
|
+
if (!data) return data;
|
|
40
|
+
if (typeof data === 'string') {
|
|
41
|
+
return await translateText(data, targetLang);
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(data)) {
|
|
44
|
+
return await Promise.all(data.map(item => translateResponseData(item, targetLang)));
|
|
45
|
+
}
|
|
46
|
+
if (typeof data === 'object') {
|
|
47
|
+
const translated = {};
|
|
48
|
+
for (const [key, value] of Object.entries(data)) {
|
|
49
|
+
// Don't translate keys, just values
|
|
50
|
+
translated[key] = await translateResponseData(value, targetLang);
|
|
51
|
+
}
|
|
52
|
+
return translated;
|
|
53
|
+
}
|
|
54
|
+
return data;
|
|
55
|
+
}
|
package/src/proxy.js
CHANGED
|
@@ -91,8 +91,28 @@ export async function runProxy(targetHost, options = {}) {
|
|
|
91
91
|
});
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
// Add CORS headers to proxied responses
|
|
95
|
+
proxy.on('proxyRes', function (proxyRes, req, res) {
|
|
96
|
+
proxyRes.headers['access-control-allow-origin'] = req.headers.origin || '*';
|
|
97
|
+
proxyRes.headers['access-control-allow-credentials'] = 'true';
|
|
98
|
+
});
|
|
99
|
+
|
|
94
100
|
const server = http.createServer((req, res) => {
|
|
95
101
|
req.kirooStartTime = Date.now();
|
|
102
|
+
|
|
103
|
+
// Handle CORS preflight
|
|
104
|
+
if (req.method === 'OPTIONS') {
|
|
105
|
+
res.writeHead(204, {
|
|
106
|
+
'Access-Control-Allow-Origin': req.headers.origin || '*',
|
|
107
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
|
108
|
+
'Access-Control-Allow-Headers': req.headers['access-control-request-headers'] || '*',
|
|
109
|
+
'Access-Control-Allow-Credentials': 'true',
|
|
110
|
+
'Access-Control-Max-Age': '86400',
|
|
111
|
+
});
|
|
112
|
+
res.end();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
96
116
|
let bodyChunks = [];
|
|
97
117
|
|
|
98
118
|
req.on('data', (chunk) => {
|
package/src/run.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { loadSnapshotData, loadEnv } from './storage.js';
|
|
5
|
+
|
|
6
|
+
const REDACTED_RE = /<REDACTED[^>]*>/i;
|
|
7
|
+
|
|
8
|
+
function isRedacted(val) {
|
|
9
|
+
return typeof val === 'string' && REDACTED_RE.test(val.trim());
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Sort interactions chronologically (oldest first) so login
|
|
14
|
+
* runs before authenticated requests.
|
|
15
|
+
*/
|
|
16
|
+
function sortChronologically(interactions) {
|
|
17
|
+
return [...interactions].sort((a, b) => {
|
|
18
|
+
// IDs are timestamps like "2026-03-15T08-10-57-031Z"
|
|
19
|
+
const idA = a.id || '';
|
|
20
|
+
const idB = b.id || '';
|
|
21
|
+
return idA.localeCompare(idB);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Deduplicate redundant interactions (e.g. 10 identical GET /api/projects 304s).
|
|
27
|
+
* Keeps unique method+path combinations, but allows duplicates for different
|
|
28
|
+
* status codes or non-cacheable methods (POST/PUT/DELETE).
|
|
29
|
+
*/
|
|
30
|
+
function deduplicateInteractions(interactions) {
|
|
31
|
+
const seen = new Map();
|
|
32
|
+
return interactions.filter((int) => {
|
|
33
|
+
const method = String(int.method || int.request?.method || 'GET').toUpperCase();
|
|
34
|
+
// Always keep non-GET (side-effectful) methods
|
|
35
|
+
if (method !== 'GET') return true;
|
|
36
|
+
|
|
37
|
+
const url = int.url || int.request?.url || '/';
|
|
38
|
+
const status = int.response?.status || 0;
|
|
39
|
+
const key = `${method}|${url}|${status}`;
|
|
40
|
+
if (seen.has(key)) return false;
|
|
41
|
+
seen.set(key, true);
|
|
42
|
+
return true;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Run all interactions from a snapshot sequentially.
|
|
48
|
+
* - Sorted oldest-first so login precedes authenticated requests
|
|
49
|
+
* - Deduplicates redundant 304 GET requests
|
|
50
|
+
* - Auto-chains tokens from auth responses
|
|
51
|
+
* - Resolves <REDACTED> headers/body from env vars
|
|
52
|
+
*/
|
|
53
|
+
export async function runSnapshot(tag, options = {}) {
|
|
54
|
+
let snapshot;
|
|
55
|
+
try {
|
|
56
|
+
snapshot = loadSnapshotData(tag);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error(chalk.red(`\n ✗ Snapshot not found: ${tag}`));
|
|
59
|
+
console.log(chalk.gray(` Run 'kiroo snapshot list' to see available snapshots.\n`));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const raw = snapshot.interactions || [];
|
|
64
|
+
if (raw.length === 0) {
|
|
65
|
+
console.log(chalk.yellow(`\n ⚠️ Snapshot "${tag}" has no interactions.\n`));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const env = loadEnv();
|
|
70
|
+
const envVars = env.environments[env.current] || {};
|
|
71
|
+
const baseUrl = envVars.baseUrl || '';
|
|
72
|
+
|
|
73
|
+
// Sort chronologically, then deduplicate
|
|
74
|
+
const sorted = sortChronologically(raw);
|
|
75
|
+
const interactions = deduplicateInteractions(sorted);
|
|
76
|
+
|
|
77
|
+
// Captured variables during run (auto-chained)
|
|
78
|
+
const captured = {};
|
|
79
|
+
|
|
80
|
+
console.log(chalk.cyan(`\n ▶ Running snapshot: ${chalk.white(tag)}`));
|
|
81
|
+
console.log(chalk.gray(` ${interactions.length} interactions (${raw.length - interactions.length} duplicates skipped)\n`));
|
|
82
|
+
|
|
83
|
+
let passed = 0;
|
|
84
|
+
let failed = 0;
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < interactions.length; i++) {
|
|
87
|
+
const int = interactions[i];
|
|
88
|
+
const method = String(int.method || int.request?.method || 'GET').toUpperCase();
|
|
89
|
+
let url = int.url || int.request?.url || '/';
|
|
90
|
+
const headers = { ...(int.request?.headers || {}) };
|
|
91
|
+
let body = int.request?.body ? JSON.parse(JSON.stringify(int.request.body)) : undefined;
|
|
92
|
+
|
|
93
|
+
// Remove internal/hop-by-hop headers
|
|
94
|
+
for (const h of ['host', 'content-length', 'connection', 'accept-encoding', 'if-none-match',
|
|
95
|
+
'sec-ch-ua', 'sec-ch-ua-mobile', 'sec-ch-ua-platform', 'sec-fetch-dest', 'sec-fetch-mode',
|
|
96
|
+
'sec-fetch-site', 'dnt', 'origin', 'referer', 'user-agent']) {
|
|
97
|
+
delete headers[h];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Resolve URL — if relative, prepend baseUrl
|
|
101
|
+
if (url.startsWith('/') && baseUrl) {
|
|
102
|
+
const normalized = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
103
|
+
url = normalized + url;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Fix redacted auth headers
|
|
107
|
+
for (const [key, val] of Object.entries(headers)) {
|
|
108
|
+
if (isRedacted(String(val))) {
|
|
109
|
+
const k = key.toLowerCase();
|
|
110
|
+
if (k === 'authorization') {
|
|
111
|
+
const token = captured.token || envVars.token;
|
|
112
|
+
if (token) headers[key] = token.startsWith('Bearer') ? token : `Bearer ${token}`;
|
|
113
|
+
else delete headers[key];
|
|
114
|
+
} else if (k === 'x-api-key' || k === 'api-key') {
|
|
115
|
+
const apiKey = captured.apiKey || envVars.sk || envVars.apiKey || envVars['x-api-key'];
|
|
116
|
+
if (apiKey) headers[key] = apiKey;
|
|
117
|
+
else delete headers[key];
|
|
118
|
+
} else {
|
|
119
|
+
delete headers[key];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Fix redacted body fields (e.g. password)
|
|
125
|
+
if (body && typeof body === 'object') {
|
|
126
|
+
for (const [key, val] of Object.entries(body)) {
|
|
127
|
+
if (isRedacted(String(val))) {
|
|
128
|
+
// Try to find from env vars (password, secret, etc.)
|
|
129
|
+
const envVal = envVars[key] || envVars[key.toLowerCase()];
|
|
130
|
+
if (envVal) {
|
|
131
|
+
body[key] = envVal;
|
|
132
|
+
}
|
|
133
|
+
// If no env val found, leave <REDACTED> — will fail but at least user knows
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Replace {{var}} placeholders
|
|
139
|
+
const allVars = { ...envVars, ...captured };
|
|
140
|
+
url = replaceVars(url, allVars);
|
|
141
|
+
if (typeof body === 'string') body = replaceVars(body, allVars);
|
|
142
|
+
if (body && typeof body === 'object') body = replaceVarsDeep(body, allVars);
|
|
143
|
+
for (const [key, val] of Object.entries(headers)) {
|
|
144
|
+
if (typeof val === 'string') headers[key] = replaceVars(val, allVars);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Execute
|
|
148
|
+
const shortUrl = url.replace(/^https?:\/\/[^/]+/, '');
|
|
149
|
+
const label = `[${i + 1}/${interactions.length}] ${method} ${shortUrl}`;
|
|
150
|
+
const spinner = ora({ text: chalk.gray(label), spinner: 'dots' }).start();
|
|
151
|
+
const start = Date.now();
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const res = await axios({
|
|
155
|
+
method: method.toLowerCase(),
|
|
156
|
+
url,
|
|
157
|
+
headers,
|
|
158
|
+
data: body,
|
|
159
|
+
timeout: options.timeout || 30000,
|
|
160
|
+
validateStatus: () => true,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const duration = Date.now() - start;
|
|
164
|
+
const status = res.status;
|
|
165
|
+
const statusColor = status >= 400 ? chalk.red : chalk.green;
|
|
166
|
+
|
|
167
|
+
spinner.stopAndPersist({
|
|
168
|
+
symbol: status >= 400 ? chalk.red('✗') : chalk.green('✓'),
|
|
169
|
+
text: `${statusColor(`${status}`)} ${chalk.gray(`${method} ${shortUrl}`)} ${chalk.dim(`${duration}ms`)}`,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Auto-capture tokens from auth-like responses
|
|
173
|
+
if (res.data && typeof res.data === 'object') {
|
|
174
|
+
const data = res.data;
|
|
175
|
+
for (const field of ['token', 'accessToken', 'access_token', 'jwt', 'authToken', 'auth_token']) {
|
|
176
|
+
if (data[field] && typeof data[field] === 'string') {
|
|
177
|
+
captured.token = data[field];
|
|
178
|
+
if (options.verbose) console.log(chalk.dim(` 🔑 Captured ${field}`));
|
|
179
|
+
}
|
|
180
|
+
if (data.data && typeof data.data === 'object' && data.data[field]) {
|
|
181
|
+
captured.token = data.data[field];
|
|
182
|
+
if (options.verbose) console.log(chalk.dim(` 🔑 Captured data.${field}`));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const field of ['apiKey', 'api_key', 'key', 'publishableKey']) {
|
|
186
|
+
if (data[field] && typeof data[field] === 'string') {
|
|
187
|
+
captured.apiKey = data[field];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (options.verbose && res.data) {
|
|
193
|
+
const bodyStr = typeof res.data === 'object' ? JSON.stringify(res.data, null, 2) : String(res.data);
|
|
194
|
+
const lines = bodyStr.split('\n');
|
|
195
|
+
const preview = lines.length > 6 ? lines.slice(0, 6).join('\n') + '\n ...' : lines.join('\n');
|
|
196
|
+
console.log(chalk.gray(` ↳ ${preview.split('\n').join('\n ')}`));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (status < 400) passed++;
|
|
200
|
+
else failed++;
|
|
201
|
+
|
|
202
|
+
} catch (err) {
|
|
203
|
+
const duration = Date.now() - start;
|
|
204
|
+
spinner.stopAndPersist({
|
|
205
|
+
symbol: chalk.red('✗'),
|
|
206
|
+
text: `${chalk.red('ERR')} ${chalk.gray(`${method} ${shortUrl}`)} ${chalk.dim(`${duration}ms`)} ${chalk.red(err.code || err.message)}`,
|
|
207
|
+
});
|
|
208
|
+
failed++;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Summary
|
|
213
|
+
console.log(chalk.cyan(`\n ── Run Complete ──`));
|
|
214
|
+
console.log(chalk.white(` Snapshot: ${tag}`));
|
|
215
|
+
console.log(chalk.green(` Passed: ${passed}`));
|
|
216
|
+
if (failed > 0) console.log(chalk.red(` Failed: ${failed}`));
|
|
217
|
+
console.log(chalk.gray(` Total: ${interactions.length}\n`));
|
|
218
|
+
|
|
219
|
+
if (captured.token) {
|
|
220
|
+
console.log(chalk.dim(` 🔑 Auto-captured token from auth response`));
|
|
221
|
+
console.log('');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (failed > 0 && options.failFast) {
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function replaceVars(str, vars) {
|
|
230
|
+
return str.replace(/\{\{(.+?)\}\}/g, (match, key) => {
|
|
231
|
+
return vars[key] !== undefined ? vars[key] : match;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function replaceVarsDeep(obj, vars) {
|
|
236
|
+
if (typeof obj === 'string') return replaceVars(obj, vars);
|
|
237
|
+
if (Array.isArray(obj)) return obj.map(item => replaceVarsDeep(item, vars));
|
|
238
|
+
if (obj && typeof obj === 'object') {
|
|
239
|
+
const result = {};
|
|
240
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
241
|
+
result[k] = replaceVarsDeep(v, vars);
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
return obj;
|
|
246
|
+
}
|
package/src/stats.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import Table from 'cli-table3';
|
|
3
3
|
import { getAllInteractions } from './storage.js';
|
|
4
|
+
import { translateText } from './lingo.js';
|
|
4
5
|
|
|
5
|
-
export function showStats() {
|
|
6
|
+
export async function showStats(options = {}) {
|
|
7
|
+
const lang = options.lang;
|
|
6
8
|
const interactions = getAllInteractions();
|
|
7
9
|
|
|
8
10
|
if (interactions.length === 0) {
|
|
@@ -11,7 +13,9 @@ export function showStats() {
|
|
|
11
13
|
return;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
let title = '📊 Kiroo Analytics Dashboard';
|
|
17
|
+
if (lang) title = await translateText(title, lang);
|
|
18
|
+
console.log(chalk.cyan.bold(`\n ${title}\n`));
|
|
15
19
|
|
|
16
20
|
// 1. General Metrics
|
|
17
21
|
const total = interactions.length;
|
|
@@ -26,7 +30,9 @@ export function showStats() {
|
|
|
26
30
|
{ [chalk.white('Avg. Duration')]: chalk.white(avgDuration + 'ms') }
|
|
27
31
|
);
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
let generalHeader = '● General Performance';
|
|
34
|
+
if (lang) generalHeader = await translateText(generalHeader, lang);
|
|
35
|
+
console.log(chalk.white.bold(` ${generalHeader}`));
|
|
30
36
|
console.log(generalTable.toString());
|
|
31
37
|
|
|
32
38
|
// 2. Method Distribution
|
|
@@ -45,7 +51,9 @@ export function showStats() {
|
|
|
45
51
|
methodTable.push([chalk.white(method), chalk.white(count), chalk.gray(percentage)]);
|
|
46
52
|
});
|
|
47
53
|
|
|
48
|
-
|
|
54
|
+
let distHeader = '● Request Distribution';
|
|
55
|
+
if (lang) distHeader = await translateText(distHeader, lang);
|
|
56
|
+
console.log(chalk.white.bold(`\n ${distHeader}`));
|
|
49
57
|
console.log(methodTable.toString());
|
|
50
58
|
|
|
51
59
|
// 3. Slowest Endpoints
|
|
@@ -67,7 +75,9 @@ export function showStats() {
|
|
|
67
75
|
]);
|
|
68
76
|
});
|
|
69
77
|
|
|
70
|
-
|
|
78
|
+
let slowHeader = '● Top 5 Slowest Endpoints (Bottlenecks)';
|
|
79
|
+
if (lang) slowHeader = await translateText(slowHeader, lang);
|
|
80
|
+
console.log(chalk.white.bold(`\n ${slowHeader}`));
|
|
71
81
|
console.log(slowTable.toString());
|
|
72
82
|
console.log('');
|
|
73
83
|
}
|