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/README.md +386 -293
- package/bin/kiroo.js +412 -288
- package/package.json +2 -1
- package/src/analyze.js +568 -0
- package/src/bench.js +11 -4
- package/src/checker.js +26 -9
- package/src/config.js +109 -0
- package/src/deterministic.js +22 -0
- package/src/env.js +31 -3
- package/src/executor.js +18 -1
- package/src/export.js +560 -93
- package/src/formatter.js +18 -6
- package/src/init.js +80 -48
- package/src/lingo.js +55 -36
- package/src/proxy.js +140 -0
- package/src/replay.js +5 -4
- package/src/run.js +246 -0
- package/src/sanitizer.js +100 -0
- package/src/snapshot.js +76 -19
- package/src/stats.js +15 -5
- package/src/storage.js +223 -142
package/bin/kiroo.js
CHANGED
|
@@ -1,288 +1,412 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Command } from 'commander';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import { executeRequest } from '../src/executor.js';
|
|
6
|
-
import { listInteractions, replayInteraction } from '../src/replay.js';
|
|
7
|
-
import { saveSnapshot, compareSnapshots, listSnapshots } from '../src/snapshot.js';
|
|
8
|
-
import { setEnv, setVar, deleteVar, listEnv } from '../src/env.js';
|
|
9
|
-
import { showGraph } from '../src/graph.js';
|
|
10
|
-
import { validateResponse, showCheckResult } from '../src/checker.js';
|
|
11
|
-
import { initProject } from '../src/init.js';
|
|
12
|
-
import { showStats } from '../src/stats.js';
|
|
13
|
-
import { handleImport } from '../src/import.js';
|
|
14
|
-
import { clearAllInteractions } from '../src/storage.js';
|
|
15
|
-
import { runBenchmark } from '../src/bench.js';
|
|
16
|
-
import { editInteraction } from '../src/edit.js';
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
.
|
|
124
|
-
.
|
|
125
|
-
.action(async (
|
|
126
|
-
await
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
//
|
|
130
|
-
program
|
|
131
|
-
.command('
|
|
132
|
-
.description('
|
|
133
|
-
.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
.
|
|
141
|
-
.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
.
|
|
148
|
-
.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
.
|
|
155
|
-
.
|
|
156
|
-
.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.
|
|
179
|
-
.
|
|
180
|
-
.action((
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
.
|
|
226
|
-
.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
.
|
|
257
|
-
.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
.
|
|
266
|
-
.
|
|
267
|
-
.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { executeRequest } from '../src/executor.js';
|
|
6
|
+
import { listInteractions, replayInteraction } from '../src/replay.js';
|
|
7
|
+
import { saveSnapshot, compareSnapshots, listSnapshots } from '../src/snapshot.js';
|
|
8
|
+
import { setEnv, setVar, deleteVar, listEnv } from '../src/env.js';
|
|
9
|
+
import { showGraph } from '../src/graph.js';
|
|
10
|
+
import { validateResponse, showCheckResult } from '../src/checker.js';
|
|
11
|
+
import { initProject } from '../src/init.js';
|
|
12
|
+
import { showStats } from '../src/stats.js';
|
|
13
|
+
import { handleImport } from '../src/import.js';
|
|
14
|
+
import { clearAllInteractions, scrubStoredData } from '../src/storage.js';
|
|
15
|
+
import { runBenchmark } from '../src/bench.js';
|
|
16
|
+
import { editInteraction } from '../src/edit.js';
|
|
17
|
+
import { exportInteractions } from '../src/export.js';
|
|
18
|
+
import { runProxy } from '../src/proxy.js';
|
|
19
|
+
import { analyzeSnapshots } from '../src/analyze.js';
|
|
20
|
+
import { runSnapshot } from '../src/run.js';
|
|
21
|
+
|
|
22
|
+
const program = new Command();
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.name('kiroo')
|
|
26
|
+
.description('Git for API interactions. Record, replay, snapshot, and diff your APIs.')
|
|
27
|
+
.version('0.9.5')
|
|
28
|
+
.showSuggestionAfterError()
|
|
29
|
+
.option('--lang <language>', 'Translate output to specified language (e.g., hi, es, fr)');
|
|
30
|
+
|
|
31
|
+
// Init command
|
|
32
|
+
program
|
|
33
|
+
.command('init')
|
|
34
|
+
.description('Initialize Kiroo in current directory')
|
|
35
|
+
.addHelpText('after', `
|
|
36
|
+
Examples:
|
|
37
|
+
$ kiroo init`)
|
|
38
|
+
.action(async () => {
|
|
39
|
+
await initProject();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
// Check command (Zero-Code Testing)
|
|
44
|
+
program
|
|
45
|
+
.command('check <url>')
|
|
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`)
|
|
51
|
+
.option('-m, --method <method>', 'HTTP method (GET, POST, etc.)', 'GET')
|
|
52
|
+
// ... (keep options as is)
|
|
53
|
+
.option('-H, --header <header...>', 'Add custom headers')
|
|
54
|
+
.option('-d, --data <data>', 'Request body (JSON or shorthand)')
|
|
55
|
+
.option('--status <code...>', 'Expected HTTP status code')
|
|
56
|
+
.option('--has <fields...>', 'Comma-separated list of expected fields in JSON response')
|
|
57
|
+
.option('--match <matches...>', 'Expected field values (e.g., status=active)')
|
|
58
|
+
.action(async (url, options) => {
|
|
59
|
+
// ...
|
|
60
|
+
// Execute request
|
|
61
|
+
const opts = program.opts();
|
|
62
|
+
const response = await executeRequest(options.method || 'GET', url, {
|
|
63
|
+
header: options.header,
|
|
64
|
+
data: options.data,
|
|
65
|
+
lang: opts.lang
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (!response) {
|
|
69
|
+
console.error(chalk.red('\n ✗ No response received to validate.'));
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Parse matches: ["key1=val1", "key2=val2"] -> { key1: val1, key2: val2 }
|
|
74
|
+
const matchObj = {};
|
|
75
|
+
if (options.match) {
|
|
76
|
+
options.match.forEach(m => {
|
|
77
|
+
const [k, ...v] = m.split('=');
|
|
78
|
+
if (k) matchObj[k] = v.join('=');
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Parse has: ["id,name"] or ["id", "name"] -> ["id", "name"]
|
|
83
|
+
const hasFields = options.has ? options.has.flatMap(h => h.split(',')).map(f => f.trim()) : [];
|
|
84
|
+
|
|
85
|
+
// Construct rules
|
|
86
|
+
const rules = {
|
|
87
|
+
status: Array.isArray(options.status) ? options.status[0] : options.status,
|
|
88
|
+
has: hasFields,
|
|
89
|
+
match: matchObj
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Validate
|
|
93
|
+
const validation = validateResponse(response, rules);
|
|
94
|
+
await showCheckResult(validation, opts.lang);
|
|
95
|
+
|
|
96
|
+
if (!validation.passed) {
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// HTTP methods as commands
|
|
102
|
+
['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].forEach(method => {
|
|
103
|
+
program
|
|
104
|
+
.command(`${method.toLowerCase()} <url>`)
|
|
105
|
+
.alias(method)
|
|
106
|
+
.description(`Execute ${method} request and store interaction`)
|
|
107
|
+
.option('-H, --header <headers...>', 'Headers (key:value)')
|
|
108
|
+
.option('-d, --data <data>', 'Request body (JSON string or key=value pairs)')
|
|
109
|
+
.option('-s, --save <pairs...>', 'Extract values from response to env (key=path.to.data)')
|
|
110
|
+
.action(async (url, options) => {
|
|
111
|
+
const opts = program.opts();
|
|
112
|
+
await executeRequest(method, url, { ...options, lang: opts.lang });
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// List interactions
|
|
117
|
+
program
|
|
118
|
+
.command('list')
|
|
119
|
+
.description('List all stored interactions')
|
|
120
|
+
.option('-n, --limit <number>', 'Number of interactions to show', '10')
|
|
121
|
+
.option('-o, --offset <number>', 'Offset for pagination', '0')
|
|
122
|
+
.option('--date <date>', 'Filter by date (YYYY-MM-DD)')
|
|
123
|
+
.option('--url <url>', 'Filter by URL path')
|
|
124
|
+
.option('--status <status>', 'Filter by HTTP status')
|
|
125
|
+
.action(async (options) => {
|
|
126
|
+
await listInteractions(options);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Replay interaction
|
|
130
|
+
program
|
|
131
|
+
.command('replay <id>')
|
|
132
|
+
.description('Replay a stored interaction')
|
|
133
|
+
.action(async (id) => {
|
|
134
|
+
await replayInteraction(id);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Edit interaction
|
|
138
|
+
program
|
|
139
|
+
.command('edit <id>')
|
|
140
|
+
.description('Edit an interaction in your text editor and quickly replay it')
|
|
141
|
+
.action(async (id) => {
|
|
142
|
+
await editInteraction(id);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Export interactions
|
|
146
|
+
program
|
|
147
|
+
.command('export')
|
|
148
|
+
.description('Export stored interactions to Postman or OpenAPI')
|
|
149
|
+
.option('-f, --format <format>', 'Export format: postman|openapi', 'postman')
|
|
150
|
+
.option('-o, --out <filename>', 'Output JSON filename')
|
|
151
|
+
.option('--title <title>', 'OpenAPI title (openapi format only)')
|
|
152
|
+
.option('--api-version <version>', 'OpenAPI version (openapi format only)')
|
|
153
|
+
.option('--server <url>', 'OpenAPI server URL override')
|
|
154
|
+
.option('--path-prefix <prefix>', 'Only include endpoints that start with this path')
|
|
155
|
+
.option('--min-samples <number>', 'Only include operations seen at least N times')
|
|
156
|
+
.action((options) => {
|
|
157
|
+
exportInteractions(options);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Bench command (Load Testing)
|
|
161
|
+
program
|
|
162
|
+
.command('bench <url>')
|
|
163
|
+
.description('Run a basic load test against an endpoint')
|
|
164
|
+
.option('-m, --method <method>', 'HTTP method (GET, POST, etc.)', 'GET')
|
|
165
|
+
.option('-n, --number <number>', 'Number of total requests to send', '10')
|
|
166
|
+
.option('-c, --concurrent <number>', 'Number of concurrent requests', '1')
|
|
167
|
+
.option('-H, --header <header...>', 'Add custom headers')
|
|
168
|
+
.option('-v, --verbose', 'Show detailed output for every request')
|
|
169
|
+
.option('-d, --data <data>', 'Request body')
|
|
170
|
+
.action(async (url, options) => {
|
|
171
|
+
const opts = program.opts();
|
|
172
|
+
await runBenchmark(url, { ...options, lang: opts.lang });
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Clear command
|
|
176
|
+
program
|
|
177
|
+
.command('clear')
|
|
178
|
+
.description('Clear all stored interaction history')
|
|
179
|
+
.option('-f, --force', 'Force clear without confirmation')
|
|
180
|
+
.action(async (options) => {
|
|
181
|
+
if (!options.force) {
|
|
182
|
+
const inquirer = (await import('inquirer')).default;
|
|
183
|
+
const { confirm } = await inquirer.prompt([
|
|
184
|
+
{
|
|
185
|
+
type: 'confirm',
|
|
186
|
+
name: 'confirm',
|
|
187
|
+
message: chalk.red('Are you sure you want to clear all history?'),
|
|
188
|
+
default: false
|
|
189
|
+
}
|
|
190
|
+
]);
|
|
191
|
+
if (!confirm) return;
|
|
192
|
+
}
|
|
193
|
+
clearAllInteractions();
|
|
194
|
+
console.log(chalk.green('\n ✨ History cleared successfully.\n'));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
program
|
|
198
|
+
.command('scrub')
|
|
199
|
+
.description('Redact sensitive data in stored interactions and snapshots')
|
|
200
|
+
.option('--dry-run', 'Show what would be changed without modifying files')
|
|
201
|
+
.action((options) => {
|
|
202
|
+
const summary = scrubStoredData({ dryRun: !!options.dryRun });
|
|
203
|
+
|
|
204
|
+
console.log(chalk.cyan('\n 🧼 Scrub summary'));
|
|
205
|
+
console.log(chalk.gray(` Interactions: ${summary.interactions.updated}/${summary.interactions.scanned} updated`));
|
|
206
|
+
console.log(chalk.gray(` Snapshots: ${summary.snapshots.updated}/${summary.snapshots.scanned} updated`));
|
|
207
|
+
|
|
208
|
+
if (summary.totalUpdated === 0) {
|
|
209
|
+
console.log(chalk.green('\n ✅ No sensitive changes needed.\n'));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (summary.dryRun) {
|
|
214
|
+
console.log(chalk.yellow('\n ⚠️ Dry run only. Re-run without --dry-run to apply changes.\n'));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
console.log(chalk.green('\n ✅ Sensitive fields were redacted successfully.\n'));
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Environment commands
|
|
222
|
+
const env = program.command('env').description('Environment management');
|
|
223
|
+
|
|
224
|
+
env
|
|
225
|
+
.command('use <name>')
|
|
226
|
+
.description('Switch to a specific environment')
|
|
227
|
+
.action((name) => setEnv(name));
|
|
228
|
+
|
|
229
|
+
env
|
|
230
|
+
.command('list')
|
|
231
|
+
.description('List environments and variables')
|
|
232
|
+
.action(() => listEnv());
|
|
233
|
+
|
|
234
|
+
env
|
|
235
|
+
.command('set <key> <value>')
|
|
236
|
+
.description('Set a variable in current environment')
|
|
237
|
+
.action((key, value) => setVar(key, value));
|
|
238
|
+
|
|
239
|
+
env
|
|
240
|
+
.command('rm <key>')
|
|
241
|
+
.description('Remove a variable from current environment')
|
|
242
|
+
.action((key) => deleteVar(key));
|
|
243
|
+
|
|
244
|
+
// Snapshot commands
|
|
245
|
+
const snapshot = program.command('snapshot').description('Snapshot management');
|
|
246
|
+
|
|
247
|
+
snapshot
|
|
248
|
+
.command('save <tag>')
|
|
249
|
+
.description('Save current state as snapshot')
|
|
250
|
+
.action(async (tag) => {
|
|
251
|
+
await saveSnapshot(tag);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
snapshot
|
|
255
|
+
.command('list')
|
|
256
|
+
.description('List all snapshots')
|
|
257
|
+
.action(async () => {
|
|
258
|
+
await listSnapshots();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
snapshot
|
|
262
|
+
.command('compare <tag1> <tag2>')
|
|
263
|
+
.description('Compare two snapshots')
|
|
264
|
+
.option('--analyze', 'Run semantic analysis right after structural compare')
|
|
265
|
+
.option('--ai', 'When used with --analyze, include AI summary')
|
|
266
|
+
.option('--model <model>', 'Model override for analyze --ai mode')
|
|
267
|
+
.option('--max-tokens <number>', 'Max completion tokens for analyze --ai mode')
|
|
268
|
+
.option('--fail-on <severity>', 'Severity threshold for analyze mode (low|medium|high|critical)')
|
|
269
|
+
.action(async (tag1, tag2, options) => {
|
|
270
|
+
const opts = program.opts();
|
|
271
|
+
await compareSnapshots(tag1, tag2, opts.lang);
|
|
272
|
+
if (options.analyze) {
|
|
273
|
+
await analyzeSnapshots(tag1, tag2, {
|
|
274
|
+
ai: !!options.ai,
|
|
275
|
+
model: options.model,
|
|
276
|
+
maxTokens: options.maxTokens,
|
|
277
|
+
failOn: options.failOn,
|
|
278
|
+
lang: opts.lang,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
});
|
|
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
|
+
|
|
297
|
+
program
|
|
298
|
+
.command('analyze <tag1> <tag2>')
|
|
299
|
+
.description('Semantic blast-radius analysis between two snapshots')
|
|
300
|
+
.option('--json', 'Output structured JSON report')
|
|
301
|
+
.option('--ai', 'Generate Groq-powered impact summary (uses GROQ_API_KEY)')
|
|
302
|
+
.option('--model <model>', 'Groq model override')
|
|
303
|
+
.option('--max-tokens <number>', 'Max completion tokens for AI summary')
|
|
304
|
+
.option('--fail-on <severity>', 'Exit non-zero when severity >= threshold (low|medium|high|critical)')
|
|
305
|
+
.action(async (tag1, tag2, options) => {
|
|
306
|
+
const opts = program.opts();
|
|
307
|
+
await analyzeSnapshots(tag1, tag2, {
|
|
308
|
+
...options,
|
|
309
|
+
lang: opts.lang
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Graph command
|
|
314
|
+
program
|
|
315
|
+
.command('graph')
|
|
316
|
+
.description('Show visual dependency graph of API interactions')
|
|
317
|
+
.action(async () => {
|
|
318
|
+
await showGraph();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Import command
|
|
322
|
+
program
|
|
323
|
+
.command('import')
|
|
324
|
+
.description('Import a request from a cURL command (opens editor if no command is provided)')
|
|
325
|
+
.allowUnknownOption()
|
|
326
|
+
.action(async (_, command) => {
|
|
327
|
+
if (command.args.length > 0) {
|
|
328
|
+
// Pass tokens directly to handleImport
|
|
329
|
+
await handleImport(command.args);
|
|
330
|
+
} else {
|
|
331
|
+
const inquirer = (await import('inquirer')).default;
|
|
332
|
+
const response = await inquirer.prompt([
|
|
333
|
+
{
|
|
334
|
+
type: 'editor',
|
|
335
|
+
name: 'curl',
|
|
336
|
+
message: 'Paste your cURL command here (opens your default editor):',
|
|
337
|
+
validate: (input) => input.trim().length > 0 || 'Please enter a cURL command'
|
|
338
|
+
}
|
|
339
|
+
]);
|
|
340
|
+
await handleImport(response.curl);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Graph command
|
|
345
|
+
/*
|
|
346
|
+
program
|
|
347
|
+
.command('graph')
|
|
348
|
+
.description('Show API dependency graph')
|
|
349
|
+
.action(async () => {
|
|
350
|
+
await showGraph();
|
|
351
|
+
});
|
|
352
|
+
*/
|
|
353
|
+
|
|
354
|
+
// Proxy command
|
|
355
|
+
program
|
|
356
|
+
.command('proxy')
|
|
357
|
+
.description('Start a time-travel proxy to automatically record interactions')
|
|
358
|
+
.requiredOption('-t, --target <url>', 'Target URL to proxy requests to')
|
|
359
|
+
.option('-p, --port <port>', 'Port to listen on', '8080')
|
|
360
|
+
.action(async (options) => {
|
|
361
|
+
await runProxy(options.target, options);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Stats command
|
|
365
|
+
program
|
|
366
|
+
.command('stats')
|
|
367
|
+
.alias('stat')
|
|
368
|
+
.description('Show usage statistics and bottlenecks')
|
|
369
|
+
.addHelpText('after', `
|
|
370
|
+
Examples:
|
|
371
|
+
$ kiroo stats
|
|
372
|
+
$ kiroo stats --lang hi`)
|
|
373
|
+
.action(async () => {
|
|
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
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Error handling
|
|
397
|
+
program.exitOverride();
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
await program.parseAsync(process.argv);
|
|
401
|
+
} catch (err) {
|
|
402
|
+
if (err.code === 'commander.help' || err.code === 'commander.version' || err.message === '(outputHelp)') {
|
|
403
|
+
process.exit(0);
|
|
404
|
+
} else if (err.code === 'commander.unknownCommand' || err.code === 'commander.missingArgument') {
|
|
405
|
+
// Commander already printed the error and suggestion, just exit
|
|
406
|
+
process.exit(1);
|
|
407
|
+
} else {
|
|
408
|
+
// For other unexpected errors, we print our own formatted message
|
|
409
|
+
console.error(chalk.red('\n ✗ Error:'), err.message, '\n');
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
}
|