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/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 { exportToPostman } from '../src/export.js';
18
-
19
- const program = new Command();
20
-
21
- program
22
- .name('kiroo')
23
- .description('Git for API interactions. Record, replay, snapshot, and diff your APIs.')
24
- .version('0.8.0')
25
- .option('--lang <language>', 'Translate output to specified language (e.g., hi, es, fr)');
26
-
27
- // Init command
28
- program
29
- .command('init')
30
- .description('Initialize Kiroo in current directory')
31
- .action(async () => {
32
- await initProject();
33
- });
34
-
35
- // Check command (Zero-Code Testing)
36
- program
37
- .command('check <url>')
38
- .description('Execute a request and validate the response against rules')
39
- .option('-m, --method <method>', 'HTTP method (GET, POST, etc.)', 'GET')
40
- .option('-H, --header <header...>', 'Add custom headers')
41
- .option('-d, --data <data>', 'Request body (JSON or shorthand)')
42
- .option('--status <code...>', 'Expected HTTP status code')
43
- .option('--has <fields...>', 'Comma-separated list of expected fields in JSON response')
44
- .option('--match <matches...>', 'Expected field values (e.g., status=active)')
45
- .action(async (url, options) => {
46
- // Execute request
47
- const response = await executeRequest(options.method || 'GET', url, {
48
- header: options.header,
49
- data: options.data,
50
- });
51
-
52
- if (!response) {
53
- console.error(chalk.red('\n ✗ No response received to validate.'));
54
- process.exit(1);
55
- }
56
-
57
- // Parse matches: ["key1=val1", "key2=val2"] -> { key1: val1, key2: val2 }
58
- const matchObj = {};
59
- if (options.match) {
60
- options.match.forEach(m => {
61
- const [k, ...v] = m.split('=');
62
- if (k) matchObj[k] = v.join('=');
63
- });
64
- }
65
-
66
- // Parse has: ["id,name"] or ["id", "name"] -> ["id", "name"]
67
- const hasFields = options.has ? options.has.flatMap(h => h.split(',')).map(f => f.trim()) : [];
68
-
69
- // Construct rules
70
- const rules = {
71
- status: Array.isArray(options.status) ? options.status[0] : options.status,
72
- has: hasFields,
73
- match: matchObj
74
- };
75
-
76
- // Validate
77
- const validation = validateResponse(response, rules);
78
- showCheckResult(validation);
79
-
80
- if (!validation.passed) {
81
- process.exit(1);
82
- }
83
- });
84
- // sk_live_p7BWJjsYlKmauBOjiEeiLRuu4DokkBWsgYne_E6osTo
85
-
86
- // HTTP methods as commands
87
- ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].forEach(method => {
88
- program
89
- .command(`${method.toLowerCase()} <url>`)
90
- .alias(method)
91
- .description(`Execute ${method} request and store interaction`)
92
- .option('-H, --header <headers...>', 'Headers (key:value)')
93
- .option('-d, --data <data>', 'Request body (JSON string or key=value pairs)')
94
- .option('-s, --save <pairs...>', 'Extract values from response to env (key=path.to.data)')
95
- .action(async (url, options) => {
96
- await executeRequest(method, url, options);
97
- });
98
- });
99
-
100
- // List interactions
101
- program
102
- .command('list')
103
- .description('List all stored interactions')
104
- .option('-n, --limit <number>', 'Number of interactions to show', '10')
105
- .option('-o, --offset <number>', 'Offset for pagination', '0')
106
- .option('--date <date>', 'Filter by date (YYYY-MM-DD)')
107
- .option('--url <url>', 'Filter by URL path')
108
- .option('--status <status>', 'Filter by HTTP status')
109
- .action(async (options) => {
110
- await listInteractions(options);
111
- });
112
-
113
- // Replay interaction
114
- program
115
- .command('replay <id>')
116
- .description('Replay a stored interaction')
117
- .action(async (id) => {
118
- await replayInteraction(id);
119
- });
120
-
121
- // Edit interaction
122
- program
123
- .command('edit <id>')
124
- .description('Edit an interaction in your text editor and quickly replay it')
125
- .action(async (id) => {
126
- await editInteraction(id);
127
- });
128
-
129
- // Export interactions
130
- program
131
- .command('export')
132
- .description('Export all stored interactions to a Postman Collection')
133
- .option('-o, --out <filename>', 'Output JSON filename', 'kiroo-collection.json')
134
- .action((options) => {
135
- exportToPostman(options.out);
136
- });
137
-
138
- // Bench command (Load Testing)
139
- program
140
- .command('bench <url>')
141
- .description('Run a basic load test against an endpoint')
142
- .option('-m, --method <method>', 'HTTP method (GET, POST, etc.)', 'GET')
143
- .option('-n, --number <number>', 'Number of total requests to send', '10')
144
- .option('-c, --concurrent <number>', 'Number of concurrent requests', '1')
145
- .option('-H, --header <header...>', 'Add custom headers')
146
- .option('-v, --verbose', 'Show detailed output for every request')
147
- .option('-d, --data <data>', 'Request body')
148
- .action(async (url, options) => {
149
- await runBenchmark(url, options);
150
- });
151
-
152
- // Clear command
153
- program
154
- .command('clear')
155
- .description('Clear all stored interaction history')
156
- .option('-f, --force', 'Force clear without confirmation')
157
- .action(async (options) => {
158
- if (!options.force) {
159
- const inquirer = (await import('inquirer')).default;
160
- const { confirm } = await inquirer.prompt([
161
- {
162
- type: 'confirm',
163
- name: 'confirm',
164
- message: chalk.red('Are you sure you want to clear all history?'),
165
- default: false
166
- }
167
- ]);
168
- if (!confirm) return;
169
- }
170
- clearAllInteractions();
171
- console.log(chalk.green('\n ✨ History cleared successfully.\n'));
172
- });
173
-
174
- // Environment commands
175
- const env = program.command('env').description('Environment management');
176
-
177
- env
178
- .command('use <name>')
179
- .description('Switch to a specific environment')
180
- .action((name) => setEnv(name));
181
-
182
- env
183
- .command('list')
184
- .description('List environments and variables')
185
- .action(() => listEnv());
186
-
187
- env
188
- .command('set <key> <value>')
189
- .description('Set a variable in current environment')
190
- .action((key, value) => setVar(key, value));
191
-
192
- env
193
- .command('rm <key>')
194
- .description('Remove a variable from current environment')
195
- .action((key) => deleteVar(key));
196
-
197
- // Snapshot commands
198
- const snapshot = program.command('snapshot').description('Snapshot management');
199
-
200
- snapshot
201
- .command('save <tag>')
202
- .description('Save current state as snapshot')
203
- .action(async (tag) => {
204
- await saveSnapshot(tag);
205
- });
206
-
207
- snapshot
208
- .command('list')
209
- .description('List all snapshots')
210
- .action(async () => {
211
- await listSnapshots();
212
- });
213
-
214
- snapshot
215
- .command('compare <tag1> <tag2>')
216
- .description('Compare two snapshots')
217
- .action(async (tag1, tag2) => {
218
- const opts = program.opts();
219
- await compareSnapshots(tag1, tag2, opts.lang);
220
- });
221
-
222
- // Graph command
223
- program
224
- .command('graph')
225
- .description('Show visual dependency graph of API interactions')
226
- .action(async () => {
227
- await showGraph();
228
- });
229
-
230
- // Import command
231
- program
232
- .command('import')
233
- .description('Import a request from a cURL command (opens editor if no command is provided)')
234
- .allowUnknownOption()
235
- .action(async (_, command) => {
236
- if (command.args.length > 0) {
237
- // Pass tokens directly to handleImport
238
- await handleImport(command.args);
239
- } else {
240
- const inquirer = (await import('inquirer')).default;
241
- const response = await inquirer.prompt([
242
- {
243
- type: 'editor',
244
- name: 'curl',
245
- message: 'Paste your cURL command here (opens your default editor):',
246
- validate: (input) => input.trim().length > 0 || 'Please enter a cURL command'
247
- }
248
- ]);
249
- await handleImport(response.curl);
250
- }
251
- });
252
-
253
- // Graph command
254
- /*
255
- program
256
- .command('graph')
257
- .description('Show API dependency graph')
258
- .action(async () => {
259
- await showGraph();
260
- });
261
- */
262
-
263
- // Stats command
264
- program
265
- .command('stats')
266
- .description('Show usage statistics')
267
- .action(async () => {
268
- await showStats();
269
- });
270
-
271
- // Error handling
272
- program.exitOverride();
273
-
274
- try {
275
- await program.parseAsync(process.argv);
276
- } catch (err) {
277
- if (err.code === 'commander.help' || err.message === '(outputHelp)') {
278
- // Help was requested, exit normally
279
- process.exit(0);
280
- } else if (err.code === 'commander.unknownCommand') {
281
- console.error(chalk.red(`\n ✗ Unknown command: ${err.message}\n`));
282
- console.log(chalk.gray(' Run'), chalk.white('kiroo --help'), chalk.gray('for usage information.\n'));
283
- process.exit(1);
284
- } else {
285
- console.error(chalk.red('\n ✗ Error:'), err.message, `(${err.code})`, '\n');
286
- process.exit(1);
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
+ }