kiroo 0.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiroo",
3
- "version": "0.8.0",
3
+ "version": "0.9.5",
4
4
  "description": "Git for API interactions. Record, replay, snapshot, and diff your APIs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,6 +42,7 @@
42
42
  "cli-table3": "^0.6.3",
43
43
  "commander": "^12.0.0",
44
44
  "dotenv": "^17.3.1",
45
+ "http-proxy": "^1.18.1",
45
46
  "inquirer": "^9.2.15",
46
47
  "js-yaml": "^4.1.0",
47
48
  "lingo.dev": "^0.133.1",
package/src/analyze.js ADDED
@@ -0,0 +1,568 @@
1
+ import axios from 'axios';
2
+ import chalk from 'chalk';
3
+ import { loadKirooConfig } from './config.js';
4
+ import { stableJSONStringify } from './deterministic.js';
5
+ import { loadSnapshotData } from './storage.js';
6
+ import { translateText } from './lingo.js';
7
+ import { getEnvVar } from './env.js';
8
+
9
+ const SEVERITY_RANK = {
10
+ none: 0,
11
+ low: 1,
12
+ medium: 2,
13
+ high: 3,
14
+ critical: 4
15
+ };
16
+
17
+ const DEFAULT_MODEL_PRIORITY = [
18
+ 'qwen/qwen3-32b',
19
+ 'moonshotai/kimi-k2-instruct-0905',
20
+ 'openai/gpt-oss-20b'
21
+ ];
22
+
23
+ function getPath(urlStr) {
24
+ try {
25
+ const urlObj = new URL(urlStr);
26
+ return urlObj.pathname;
27
+ } catch {
28
+ return urlStr.startsWith('/') ? urlStr : `/${urlStr}`;
29
+ }
30
+ }
31
+
32
+ function endpointKey(method, path) {
33
+ return `${String(method || '').toUpperCase()} ${path}`;
34
+ }
35
+
36
+ function normalizeFieldName(name) {
37
+ return String(name || '').replace(/[_-]/g, '').toLowerCase();
38
+ }
39
+
40
+ function isObject(val) {
41
+ return val !== null && typeof val === 'object' && !Array.isArray(val);
42
+ }
43
+
44
+ function compareStatus(beforeStatus, afterStatus) {
45
+ if (beforeStatus === afterStatus) return null;
46
+
47
+ const before2xx = beforeStatus >= 200 && beforeStatus < 300;
48
+ const before4xx5xx = beforeStatus >= 400;
49
+ const after3xx = afterStatus >= 300 && afterStatus < 400;
50
+ const after4xx5xx = afterStatus >= 400;
51
+
52
+ let severity = 'medium';
53
+ let breaking = true;
54
+
55
+ if (before2xx && afterStatus === 304) {
56
+ severity = 'low';
57
+ breaking = false;
58
+ } else if (before2xx && after3xx) {
59
+ severity = 'low';
60
+ breaking = false;
61
+ } else if (before2xx && after4xx5xx) {
62
+ severity = 'high';
63
+ breaking = true;
64
+ } else if (before4xx5xx && afterStatus >= 200 && afterStatus < 300) {
65
+ severity = 'low';
66
+ breaking = false;
67
+ }
68
+
69
+ return {
70
+ type: 'status_changed',
71
+ path: 'response.status',
72
+ severity,
73
+ breaking,
74
+ message: `Status changed from ${beforeStatus} to ${afterStatus}`
75
+ };
76
+ }
77
+
78
+ function compareBodies(beforeValue, afterValue, path = '') {
79
+ const issues = [];
80
+
81
+ if (beforeValue === null && afterValue === null) return issues;
82
+ if (beforeValue === null && afterValue !== null) {
83
+ issues.push({
84
+ type: 'field_type_changed',
85
+ path: path || 'root',
86
+ severity: 'medium',
87
+ breaking: false,
88
+ message: `Type changed from null to ${Array.isArray(afterValue) ? 'array' : typeof afterValue}`
89
+ });
90
+ return issues;
91
+ }
92
+ if (beforeValue !== null && afterValue === null) {
93
+ issues.push({
94
+ type: 'field_type_changed',
95
+ path: path || 'root',
96
+ severity: 'medium',
97
+ breaking: false,
98
+ message: `Type changed from ${Array.isArray(beforeValue) ? 'array' : typeof beforeValue} to null`
99
+ });
100
+ return issues;
101
+ }
102
+
103
+ const beforeType = Array.isArray(beforeValue) ? 'array' : typeof beforeValue;
104
+ const afterType = Array.isArray(afterValue) ? 'array' : typeof afterValue;
105
+
106
+ if (beforeType !== afterType) {
107
+ issues.push({
108
+ type: 'field_type_changed',
109
+ path: path || 'root',
110
+ severity: 'high',
111
+ breaking: true,
112
+ message: `Type changed from ${beforeType} to ${afterType}`
113
+ });
114
+ return issues;
115
+ }
116
+
117
+ if (isObject(beforeValue) && isObject(afterValue)) {
118
+ const beforeKeys = Object.keys(beforeValue).sort((a, b) => a.localeCompare(b));
119
+ const afterKeys = Object.keys(afterValue).sort((a, b) => a.localeCompare(b));
120
+
121
+ const removed = beforeKeys.filter((key) => !afterKeys.includes(key));
122
+ const added = afterKeys.filter((key) => !beforeKeys.includes(key));
123
+ const matchedRemoved = new Set();
124
+ const matchedAdded = new Set();
125
+
126
+ for (const removedKey of removed) {
127
+ const removedNorm = normalizeFieldName(removedKey);
128
+ const addCandidate = added.find((addedKey) => !matchedAdded.has(addedKey) && normalizeFieldName(addedKey) === removedNorm);
129
+
130
+ if (addCandidate) {
131
+ matchedRemoved.add(removedKey);
132
+ matchedAdded.add(addCandidate);
133
+ issues.push({
134
+ type: 'field_rename_candidate',
135
+ path: path || 'root',
136
+ severity: 'medium',
137
+ breaking: true,
138
+ message: `Possible rename: "${removedKey}" -> "${addCandidate}"`
139
+ });
140
+ }
141
+ }
142
+
143
+ for (const removedKey of removed) {
144
+ if (matchedRemoved.has(removedKey)) continue;
145
+ const removedPath = path ? `${path}.${removedKey}` : removedKey;
146
+ issues.push({
147
+ type: 'field_removed',
148
+ path: removedPath,
149
+ severity: 'high',
150
+ breaking: true,
151
+ message: `Field removed: ${removedPath}`
152
+ });
153
+ }
154
+
155
+ for (const addedKey of added) {
156
+ if (matchedAdded.has(addedKey)) continue;
157
+ const addedPath = path ? `${path}.${addedKey}` : addedKey;
158
+ issues.push({
159
+ type: 'field_added',
160
+ path: addedPath,
161
+ severity: 'low',
162
+ breaking: false,
163
+ message: `Field added: ${addedPath}`
164
+ });
165
+ }
166
+
167
+ for (const sharedKey of beforeKeys.filter((key) => afterKeys.includes(key))) {
168
+ const childPath = path ? `${path}.${sharedKey}` : sharedKey;
169
+ issues.push(...compareBodies(beforeValue[sharedKey], afterValue[sharedKey], childPath));
170
+ }
171
+
172
+ return issues;
173
+ }
174
+
175
+ if (Array.isArray(beforeValue) && Array.isArray(afterValue)) {
176
+ if (beforeValue.length > 0 && afterValue.length > 0) {
177
+ const itemPath = `${path || 'root'}[0]`;
178
+ issues.push(...compareBodies(beforeValue[0], afterValue[0], itemPath));
179
+ }
180
+ }
181
+
182
+ return issues;
183
+ }
184
+
185
+ function updateSummaryFromIssue(summary, issue) {
186
+ summary.totalIssues += 1;
187
+ summary.bySeverity[issue.severity] += 1;
188
+ summary.byType[issue.type] = (summary.byType[issue.type] || 0) + 1;
189
+ }
190
+
191
+ function ensureEndpoint(reportMap, method, path) {
192
+ const key = endpointKey(method, path);
193
+ if (!reportMap.has(key)) {
194
+ reportMap.set(key, {
195
+ method: String(method || '').toUpperCase(),
196
+ path,
197
+ issues: [],
198
+ highestSeverity: 'none'
199
+ });
200
+ }
201
+ return reportMap.get(key);
202
+ }
203
+
204
+ function addIssue(reportMap, summary, method, path, issue) {
205
+ const endpoint = ensureEndpoint(reportMap, method, path);
206
+ const isDuplicate = endpoint.issues.some((existing) =>
207
+ existing.type === issue.type &&
208
+ existing.path === issue.path &&
209
+ existing.message === issue.message &&
210
+ existing.severity === issue.severity
211
+ );
212
+ if (isDuplicate) {
213
+ return;
214
+ }
215
+
216
+ endpoint.issues.push(issue);
217
+ if (SEVERITY_RANK[issue.severity] > SEVERITY_RANK[endpoint.highestSeverity]) {
218
+ endpoint.highestSeverity = issue.severity;
219
+ }
220
+ updateSummaryFromIssue(summary, issue);
221
+ }
222
+
223
+ function compareInteractionsForAnalysis(beforeInteractions, afterInteractions) {
224
+ const reportMap = new Map();
225
+ const summary = {
226
+ totalEndpoints: 0,
227
+ totalIssues: 0,
228
+ bySeverity: {
229
+ low: 0,
230
+ medium: 0,
231
+ high: 0,
232
+ critical: 0
233
+ },
234
+ byType: {}
235
+ };
236
+
237
+ const sortedBefore = [...beforeInteractions].sort((a, b) => endpointKey(a.method, getPath(a.url)).localeCompare(endpointKey(b.method, getPath(b.url))));
238
+ const sortedAfter = [...afterInteractions].sort((a, b) => endpointKey(a.method, getPath(a.url)).localeCompare(endpointKey(b.method, getPath(b.url))));
239
+ const consumedBeforeIndexes = new Set();
240
+
241
+ for (const after of sortedAfter) {
242
+ const method = String(after.method || '').toUpperCase();
243
+ const path = getPath(after.url || '');
244
+ const candidates = sortedBefore
245
+ .map((item, index) => ({ item, index }))
246
+ .filter(({ item, index }) => !consumedBeforeIndexes.has(index) && String(item.method || '').toUpperCase() === method && getPath(item.url || '') === path);
247
+
248
+ const match = candidates.find(({ item }) => item.id && after.id && item.id === after.id) || candidates[0];
249
+
250
+ if (!match) {
251
+ addIssue(reportMap, summary, method, path, {
252
+ type: 'endpoint_added',
253
+ path,
254
+ severity: 'low',
255
+ breaking: false,
256
+ message: 'Endpoint added in target snapshot'
257
+ });
258
+ continue;
259
+ }
260
+
261
+ consumedBeforeIndexes.add(match.index);
262
+ const before = match.item;
263
+ const statusIssue = compareStatus(before.response?.status, after.response?.status);
264
+ if (statusIssue) {
265
+ addIssue(reportMap, summary, method, path, statusIssue);
266
+ }
267
+
268
+ if (before.response?.body !== undefined && after.response?.body !== undefined) {
269
+ const bodyIssues = compareBodies(before.response.body, after.response.body);
270
+ for (const issue of bodyIssues) {
271
+ addIssue(reportMap, summary, method, path, issue);
272
+ }
273
+ }
274
+ }
275
+
276
+ sortedBefore.forEach((before, index) => {
277
+ if (consumedBeforeIndexes.has(index)) return;
278
+ const method = String(before.method || '').toUpperCase();
279
+ const path = getPath(before.url || '');
280
+ addIssue(reportMap, summary, method, path, {
281
+ type: 'endpoint_removed',
282
+ path,
283
+ severity: 'high',
284
+ breaking: true,
285
+ message: 'Endpoint removed from target snapshot'
286
+ });
287
+ });
288
+
289
+ const endpoints = Array.from(reportMap.values())
290
+ .sort((a, b) => {
291
+ const severityDiff = SEVERITY_RANK[b.highestSeverity] - SEVERITY_RANK[a.highestSeverity];
292
+ if (severityDiff !== 0) return severityDiff;
293
+ if (a.method !== b.method) return a.method.localeCompare(b.method);
294
+ return a.path.localeCompare(b.path);
295
+ })
296
+ .map((endpoint) => ({
297
+ ...endpoint,
298
+ issues: endpoint.issues.sort((a, b) => {
299
+ const severityDiff = SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity];
300
+ if (severityDiff !== 0) return severityDiff;
301
+ if (a.type !== b.type) return a.type.localeCompare(b.type);
302
+ return String(a.path || '').localeCompare(String(b.path || ''));
303
+ })
304
+ }));
305
+
306
+ summary.totalEndpoints = endpoints.length;
307
+ const highestSeverity = endpoints.reduce((highest, endpoint) => (
308
+ SEVERITY_RANK[endpoint.highestSeverity] > SEVERITY_RANK[highest] ? endpoint.highestSeverity : highest
309
+ ), 'none');
310
+
311
+ return {
312
+ summary,
313
+ highestSeverity,
314
+ endpoints
315
+ };
316
+ }
317
+
318
+ export function analyzeSnapshotData(sourceSnapshot, targetSnapshot) {
319
+ const sourceInteractions = sourceSnapshot?.interactions || [];
320
+ const targetInteractions = targetSnapshot?.interactions || [];
321
+ const analysis = compareInteractionsForAnalysis(sourceInteractions, targetInteractions);
322
+
323
+ return {
324
+ generatedAt: new Date().toISOString(),
325
+ source: {
326
+ tag: sourceSnapshot?.tag || 'source',
327
+ timestamp: sourceSnapshot?.timestamp
328
+ },
329
+ target: {
330
+ tag: targetSnapshot?.tag || 'target',
331
+ timestamp: targetSnapshot?.timestamp
332
+ },
333
+ summary: analysis.summary,
334
+ highestSeverity: analysis.highestSeverity,
335
+ endpoints: analysis.endpoints
336
+ };
337
+ }
338
+
339
+ function colorForSeverity(severity) {
340
+ if (severity === 'critical') return chalk.redBright;
341
+ if (severity === 'high') return chalk.red;
342
+ if (severity === 'medium') return chalk.yellow;
343
+ if (severity === 'low') return chalk.blue;
344
+ return chalk.gray;
345
+ }
346
+
347
+ function shouldFail(report, failOnSeverity) {
348
+ const threshold = String(failOnSeverity || '').toLowerCase();
349
+ if (!SEVERITY_RANK[threshold]) return false;
350
+ return SEVERITY_RANK[report.highestSeverity] >= SEVERITY_RANK[threshold];
351
+ }
352
+
353
+ async function maybeTranslate(text, lang) {
354
+ if (!lang) return text;
355
+ return translateText(text, lang);
356
+ }
357
+
358
+ async function requestGroqSummary(report, { model, maxTokens, apiKey }) {
359
+ const endpointPreview = report.endpoints.slice(0, 30).map((endpoint) => ({
360
+ method: endpoint.method,
361
+ path: endpoint.path,
362
+ highestSeverity: endpoint.highestSeverity,
363
+ issues: endpoint.issues.slice(0, 6).map((issue) => ({
364
+ type: issue.type,
365
+ severity: issue.severity,
366
+ message: issue.message
367
+ }))
368
+ }));
369
+
370
+ const prompt = [
371
+ 'You are a senior API reviewer.',
372
+ 'Return concise output only as bullet points.',
373
+ 'Rules:',
374
+ '- Maximum 5 bullet points',
375
+ '- Each bullet must be <= 14 words',
376
+ '- Focus on impact + risk + actionable fix',
377
+ '- Do not include headings or paragraphs',
378
+ '',
379
+ stableJSONStringify({
380
+ sourceTag: report.source.tag,
381
+ targetTag: report.target.tag,
382
+ summary: report.summary,
383
+ highestSeverity: report.highestSeverity,
384
+ endpoints: endpointPreview
385
+ }, 2)
386
+ ].join('\n');
387
+
388
+ const response = await axios.post(
389
+ 'https://api.groq.com/openai/v1/chat/completions',
390
+ {
391
+ model,
392
+ temperature: 0.2,
393
+ max_tokens: maxTokens,
394
+ messages: [
395
+ { role: 'system', content: 'You are an API contract analysis assistant.' },
396
+ { role: 'user', content: prompt }
397
+ ]
398
+ },
399
+ {
400
+ headers: {
401
+ Authorization: `Bearer ${apiKey}`,
402
+ 'Content-Type': 'application/json'
403
+ }
404
+ }
405
+ );
406
+
407
+ const text = response.data?.choices?.[0]?.message?.content;
408
+ if (!text) {
409
+ throw new Error(`No summary text returned by Groq model ${model}`);
410
+ }
411
+ return text.trim();
412
+ }
413
+
414
+ function toConciseBullets(rawText, report) {
415
+ let cleanedText = String(rawText || '');
416
+ cleanedText = cleanedText.replace(/<think>[\s\S]*?<\/think>/gi, ' ');
417
+ if (cleanedText.includes('<think>')) {
418
+ cleanedText = cleanedText.split('<think>')[0];
419
+ }
420
+
421
+ const lines = cleanedText
422
+ .split('\n')
423
+ .map((line) => line.trim())
424
+ .filter(Boolean);
425
+
426
+ const normalized = lines
427
+ .map((line) => line.replace(/^[-*โ€ข\d.)\s]+/, '').trim())
428
+ .filter((line) => !/^<\/?think>$/i.test(line))
429
+ .filter((line) => !/^(okay|let'?s|first,|then,|on the flip side|i need to)/i.test(line))
430
+ .filter(Boolean);
431
+
432
+ if (normalized.length === 0 || normalized.every((line) => line.length < 8)) {
433
+ return fallbackBullets(report);
434
+ }
435
+
436
+ const bullets = normalized
437
+ .slice(0, 5)
438
+ .map((line) => {
439
+ const compact = line.replace(/\s+/g, ' ');
440
+ const clipped = compact.length > 100 ? `${compact.slice(0, 97)}...` : compact;
441
+ return `- ${clipped}`;
442
+ });
443
+
444
+ return bullets;
445
+ }
446
+
447
+ function fallbackBullets(report) {
448
+ const s = report.summary;
449
+ const bullets = [];
450
+ bullets.push(`- Highest severity: ${String(report.highestSeverity).toUpperCase()}.`);
451
+ bullets.push(`- High/Critical issues: ${s.bySeverity.high + s.bySeverity.critical}.`);
452
+ bullets.push(`- Removed endpoints: ${s.byType.endpoint_removed || 0}.`);
453
+ bullets.push(`- Status changes: ${s.byType.status_changed || 0}.`);
454
+ bullets.push('- Action: patch clients first, then add compatibility aliases.');
455
+ return bullets;
456
+ }
457
+
458
+ export function resolveGroqApiKey() {
459
+ return getEnvVar('GROQ_API_KEY');
460
+ }
461
+
462
+ async function generateAiSummary(report, options) {
463
+ const apiKey = resolveGroqApiKey();
464
+ if (!apiKey) {
465
+ throw new Error('GROQ_API_KEY not found in .kiroo/env.json. Run "kiroo env set GROQ_API_KEY <your_key>" or re-run "kiroo init".');
466
+ }
467
+
468
+ const modelCandidates = options.model
469
+ ? [options.model]
470
+ : (options.modelPriority?.length ? options.modelPriority : DEFAULT_MODEL_PRIORITY);
471
+
472
+ const failures = [];
473
+ for (const model of modelCandidates) {
474
+ try {
475
+ const summary = await requestGroqSummary(report, {
476
+ model,
477
+ maxTokens: options.maxTokens,
478
+ apiKey
479
+ });
480
+ return { summary, model };
481
+ } catch (error) {
482
+ failures.push(`${model}: ${error.message}`);
483
+ }
484
+ }
485
+
486
+ throw new Error(`Groq summary failed for all candidate models. ${failures.join(' | ')}`);
487
+ }
488
+
489
+ export async function analyzeSnapshots(tag1, tag2, options = {}) {
490
+ try {
491
+ const config = loadKirooConfig();
492
+ const failOnSeverity = options.failOn || null;
493
+ const maxTokens = Number.parseInt(options.maxTokens, 10) || config.settings?.analysis?.maxCompletionTokens || 900;
494
+ const modelPriority = config.settings?.analysis?.modelPriority || DEFAULT_MODEL_PRIORITY;
495
+
496
+ const sourceSnapshot = loadSnapshotData(tag1);
497
+ const targetSnapshot = loadSnapshotData(tag2);
498
+ const report = analyzeSnapshotData(sourceSnapshot, targetSnapshot);
499
+
500
+ if (options.json) {
501
+ console.log(stableJSONStringify(report));
502
+ } else {
503
+ let header = '๐Ÿง  Blast Radius Analysis';
504
+ if (options.lang) header = await translateText(header, options.lang);
505
+ console.log(chalk.cyan(`\n ${header}`));
506
+ console.log(chalk.gray(` Source: ${tag1}`));
507
+ console.log(chalk.gray(` Target: ${tag2}\n`));
508
+
509
+ const shownEndpoints = report.endpoints.slice(0, 6);
510
+ for (const endpoint of shownEndpoints) {
511
+ let severityLabel = endpoint.highestSeverity.toUpperCase();
512
+ if (options.lang) severityLabel = await translateText(severityLabel, options.lang);
513
+ const severityColor = colorForSeverity(endpoint.highestSeverity);
514
+ console.log(` ${severityColor(severityLabel)} ${chalk.white(endpoint.method)} ${chalk.gray(endpoint.path)}`);
515
+
516
+ for (const issue of endpoint.issues.slice(0, 4)) {
517
+ const issueColor = colorForSeverity(issue.severity);
518
+ let issueMsg = issue.message;
519
+ if (options.lang) issueMsg = await translateText(issueMsg, options.lang);
520
+ console.log(` - ${issueColor(issue.severity)} ${issueMsg}`);
521
+ }
522
+ if (endpoint.issues.length > 4) {
523
+ console.log(chalk.gray(` - ... +${endpoint.issues.length - 4} more issues`));
524
+ }
525
+ }
526
+ if (report.endpoints.length > shownEndpoints.length) {
527
+ console.log(chalk.gray(` ... +${report.endpoints.length - shownEndpoints.length} more endpoints`));
528
+ }
529
+
530
+ const summaryLine = `Issues: ${report.summary.totalIssues} | Endpoints: ${report.summary.totalEndpoints} | Highest severity: ${report.highestSeverity.toUpperCase()}`;
531
+ const translatedSummary = await maybeTranslate(summaryLine, options.lang);
532
+ console.log(chalk.cyan(`\n ${translatedSummary}`));
533
+ }
534
+
535
+ if (options.ai) {
536
+ const ai = await generateAiSummary(report, {
537
+ model: options.model,
538
+ modelPriority,
539
+ maxTokens
540
+ });
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
+ }
556
+ }
557
+
558
+ if (failOnSeverity && shouldFail(report, failOnSeverity)) {
559
+ console.error(chalk.red(`\n โœ— Analysis failed threshold (${failOnSeverity}). Highest severity is ${report.highestSeverity}.`));
560
+ process.exit(1);
561
+ }
562
+
563
+ console.log(chalk.green('\n โœ… Analysis complete.\n'));
564
+ } catch (error) {
565
+ console.error(chalk.red('\n โœ— Analysis failed:'), error.message, '\n');
566
+ process.exit(1);
567
+ }
568
+ }
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
- console.log('\n ' + chalk.blue.bold('๐Ÿš€ Benchmark Results'));
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
- console.log(chalk.red(`\n โš ๏ธ ${results.failures} requests failed (HTTP 4xx/5xx or Network Error).\n`));
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
- console.log(chalk.green(`\n โœ… All requests completed successfully.\n`));
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
- export function showCheckResult(validation) {
80
- console.log(chalk.cyan('\n ๐Ÿงช Test Results:'));
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.forEach(res => {
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
- console.log(` ${icon} ${res.label}`);
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
- console.log(chalk.gray(` Expected: ${res.expected}`));
89
- console.log(chalk.gray(` Actual: ${res.actual}`));
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
- console.log(chalk.green.bold('\n โœจ ALL TESTS PASSED! \n'));
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
- console.log(chalk.red.bold('\n โŒ SOME TESTS FAILED \n'));
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
  }