@mintlify/cli 4.0.1421 → 4.0.1423

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.
@@ -0,0 +1,599 @@
1
+ import { addLog, ErrorLog, SpinnerLog, removeLastLog } from '@mintlify/previewing';
2
+ import chalk from 'chalk';
3
+ import { Text } from 'ink';
4
+ import type { Argv } from 'yargs';
5
+
6
+ import { getConfigValue } from '../config.js';
7
+ import { terminate } from '../helpers.js';
8
+ import { subdomainMiddleware } from '../middlewares/subdomainMiddleware.js';
9
+ import {
10
+ getAssistantThread,
11
+ getBucketThreads,
12
+ getBuckets,
13
+ getConversations,
14
+ getFeedback,
15
+ getFeedbackByPage,
16
+ getKpi,
17
+ getSearches,
18
+ } from './client.js';
19
+ import { formatLocalDate } from './dates.js';
20
+ import {
21
+ filterConversationResponse,
22
+ filterFeedbackByPageResponse,
23
+ filterFeedbackResponse,
24
+ filterSearchResponse,
25
+ } from './filters.js';
26
+ import { num, truncate } from './format.js';
27
+ import { formatBarChart, formatOutput, resolveFormat, type OutputFormat } from './output.js';
28
+
29
+ const withSubdomain = <T extends object>(yargs: Argv<T>) =>
30
+ yargs.option('subdomain', {
31
+ type: 'string' as const,
32
+ description: 'Documentation subdomain (default: mint config set subdomain)',
33
+ });
34
+
35
+ function defaultFrom(): string {
36
+ const configured = getConfigValue('dateFrom');
37
+ if (configured) return configured;
38
+ const d = new Date();
39
+ d.setDate(d.getDate() - 7);
40
+ return formatLocalDate(d);
41
+ }
42
+
43
+ function defaultTo(): string {
44
+ return getConfigValue('dateTo') ?? formatLocalDate(new Date());
45
+ }
46
+
47
+ const withDates = <T extends object>(yargs: Argv<T>) =>
48
+ yargs
49
+ .option('from', {
50
+ type: 'string' as const,
51
+ default: defaultFrom(),
52
+ description: 'Start date (YYYY-MM-DD)',
53
+ })
54
+ .option('to', {
55
+ type: 'string' as const,
56
+ default: defaultTo(),
57
+ description: 'End date (YYYY-MM-DD)',
58
+ });
59
+
60
+ const withFormat = <T extends object>(yargs: Argv<T>) =>
61
+ yargs.option('format', {
62
+ type: 'string' as const,
63
+ choices: ['table', 'plain', 'json', 'graph'] as const,
64
+ description: 'Output format (table=pretty, plain=pipeable, json=raw)',
65
+ });
66
+
67
+ const withAll = <T extends object>(yargs: Argv<T>) => withFormat(withDates(withSubdomain(yargs)));
68
+
69
+ function output(format: OutputFormat, text: string) {
70
+ if (format === 'table') {
71
+ addLog(<Text>{text}</Text>);
72
+ } else {
73
+ process.stdout.write(text + '\n');
74
+ }
75
+ }
76
+
77
+ export const analyticsBuilder = (yargs: Argv) =>
78
+ yargs
79
+ .middleware(subdomainMiddleware)
80
+ .command(
81
+ 'stats',
82
+ 'display KPI numbers (views, visitors, searches)',
83
+ (yargs) =>
84
+ withAll(yargs).option('page', {
85
+ type: 'string',
86
+ description: 'Filter to a specific page path',
87
+ }),
88
+ async (argv) => {
89
+ const format = resolveFormat(argv);
90
+ try {
91
+ if (format === 'table') addLog(<SpinnerLog message="Fetching analytics..." />);
92
+ const kpi = await getKpi(
93
+ { dateFrom: argv.from, dateTo: argv.to, page: argv.page },
94
+ argv.subdomain
95
+ );
96
+ if (format === 'table') removeLastLog();
97
+
98
+ if (format === 'json') {
99
+ output(format, JSON.stringify(kpi, null, 2));
100
+ await terminate(0);
101
+ return;
102
+ }
103
+
104
+ if (format === 'plain') {
105
+ const lines = [
106
+ ['METRIC', 'HUMAN', 'AGENT', 'TOTAL'].join('\t'),
107
+ ['Views', kpi.humanViews, kpi.agentViews, kpi.humanViews + kpi.agentViews].join('\t'),
108
+ [
109
+ 'Visitors',
110
+ kpi.humanVisitors,
111
+ kpi.agentVisitors,
112
+ kpi.humanVisitors + kpi.agentVisitors,
113
+ ].join('\t'),
114
+ ['Searches', kpi.humanSearches, '', kpi.humanSearches].join('\t'),
115
+ ['Feedback', kpi.humanFeedback, '', kpi.humanFeedback].join('\t'),
116
+ [
117
+ 'Assistant',
118
+ kpi.humanAssistant,
119
+ kpi.agentMcpSearches,
120
+ kpi.humanAssistant + kpi.agentMcpSearches,
121
+ ].join('\t'),
122
+ ];
123
+ output(format, lines.join('\n'));
124
+ await terminate(0);
125
+ return;
126
+ }
127
+
128
+ if (format === 'graph') {
129
+ const label = argv.subdomain ?? '';
130
+ const lines: string[] = [];
131
+ lines.push(chalk.bold(`\nAnalytics \u2014 ${label} (${argv.from} to ${argv.to})\n`));
132
+ lines.push(chalk.bold(' Human vs Agent\n'));
133
+ lines.push(
134
+ formatBarChart([
135
+ { label: 'Human Views', value: kpi.humanViews, color: 'cyan' },
136
+ { label: 'Agent Views', value: kpi.agentViews, color: 'magenta' },
137
+ { label: 'Human Visitors', value: kpi.humanVisitors, color: 'cyan' },
138
+ { label: 'Agent Visitors', value: kpi.agentVisitors, color: 'magenta' },
139
+ ])
140
+ );
141
+ lines.push(chalk.bold('\n\n Engagement\n'));
142
+ lines.push(
143
+ formatBarChart([
144
+ { label: 'Searches', value: kpi.humanSearches, color: 'yellow' },
145
+ { label: 'Feedback', value: kpi.humanFeedback, color: 'green' },
146
+ { label: 'Assistant (web)', value: kpi.humanAssistant, color: 'blue' },
147
+ { label: 'Assistant (API)', value: kpi.agentMcpSearches, color: 'magenta' },
148
+ ])
149
+ );
150
+ output('table', lines.join('\n'));
151
+ await terminate(0);
152
+ return;
153
+ }
154
+
155
+ const lines: string[] = [];
156
+ const label = argv.subdomain ?? '';
157
+ lines.push(chalk.bold(`\nAnalytics \u2014 ${label} (${argv.from} to ${argv.to})\n`));
158
+
159
+ if (argv.page) {
160
+ lines.push(` Page: ${argv.page}\n`);
161
+ }
162
+
163
+ lines.push(chalk.bold(' Views'));
164
+ lines.push(` Human: ${chalk.cyan(num(kpi.humanViews).padStart(8))}`);
165
+ lines.push(` Agent: ${chalk.magenta(num(kpi.agentViews).padStart(8))}`);
166
+ lines.push(` Total: ${num(kpi.humanViews + kpi.agentViews).padStart(8)}`);
167
+
168
+ lines.push(chalk.bold('\n Visitors'));
169
+ lines.push(` Human: ${chalk.cyan(num(kpi.humanVisitors).padStart(8))}`);
170
+ lines.push(` Agent: ${chalk.magenta(num(kpi.agentVisitors).padStart(8))}`);
171
+ lines.push(` Total: ${num(kpi.humanVisitors + kpi.agentVisitors).padStart(8)}`);
172
+
173
+ lines.push(`\n Searches: ${chalk.bold(num(kpi.humanSearches))}`);
174
+ lines.push(` Feedback: ${chalk.bold(num(kpi.humanFeedback))}`);
175
+ lines.push(
176
+ ` Assistant: ${chalk.bold(num(kpi.humanAssistant))} web, ${chalk.bold(num(kpi.agentMcpSearches))} API`
177
+ );
178
+
179
+ output(format, lines.join('\n'));
180
+ await terminate(0);
181
+ } catch (err) {
182
+ if (format === 'table') removeLastLog();
183
+ addLog(<ErrorLog message={err instanceof Error ? err.message : 'unknown error'} />);
184
+ await terminate(1);
185
+ }
186
+ }
187
+ )
188
+ .command(
189
+ 'search',
190
+ 'display search analytics',
191
+ (yargs) =>
192
+ withAll(yargs)
193
+ .option('query', { type: 'string', description: 'Filter by search query substring' })
194
+ .option('page', { type: 'string', description: 'Filter by top clicked page' }),
195
+ async (argv) => {
196
+ const format = resolveFormat(argv);
197
+ try {
198
+ if (format === 'table') addLog(<SpinnerLog message="Fetching search analytics..." />);
199
+ const data = await getSearches({ dateFrom: argv.from, dateTo: argv.to }, argv.subdomain);
200
+ if (format === 'table') removeLastLog();
201
+
202
+ const filteredData = filterSearchResponse(data, {
203
+ query: argv.query,
204
+ page: argv.page,
205
+ });
206
+ const rows = filteredData.searches;
207
+
208
+ const headers = ['Query', 'Hits', 'CTR', 'Top Clicked Page', 'Last Searched'];
209
+ const tableRows = rows.map((r) => [
210
+ truncate(r.searchQuery, 30),
211
+ num(r.hits),
212
+ r.ctr.toFixed(1) + '%',
213
+ truncate(r.topClickedPage || '\u2014', 30),
214
+ r.lastSearchedAt.slice(0, 10),
215
+ ]);
216
+
217
+ if (format === 'json') {
218
+ output(format, JSON.stringify(filteredData, null, 2));
219
+ } else if (format === 'graph') {
220
+ const label = argv.subdomain ?? '';
221
+ const lines: string[] = [];
222
+ lines.push(
223
+ chalk.bold(`\nSearch Queries \u2014 ${label} (${argv.from} to ${argv.to})\n`)
224
+ );
225
+ lines.push(
226
+ formatBarChart(
227
+ rows.slice(0, 20).map((r) => ({
228
+ label: truncate(r.searchQuery, 25),
229
+ value: r.hits,
230
+ color: 'yellow',
231
+ }))
232
+ )
233
+ );
234
+ output('table', lines.join('\n'));
235
+ } else if (format === 'plain') {
236
+ output(format, formatOutput(format, headers, tableRows, filteredData));
237
+ } else {
238
+ const label = argv.subdomain ?? '';
239
+ const lines: string[] = [];
240
+ lines.push(
241
+ chalk.bold(`\nSearch Analytics \u2014 ${label} (${argv.from} to ${argv.to})`)
242
+ );
243
+ lines.push(`Total Searches: ${chalk.bold(num(data.totalSearches))}\n`);
244
+ lines.push(formatOutput(format, headers, tableRows, filteredData));
245
+ output(format, lines.join('\n'));
246
+ }
247
+ await terminate(0);
248
+ } catch (err) {
249
+ if (format === 'table') removeLastLog();
250
+ addLog(<ErrorLog message={err instanceof Error ? err.message : 'unknown error'} />);
251
+ await terminate(1);
252
+ }
253
+ }
254
+ )
255
+ .command(
256
+ 'feedback',
257
+ 'display feedback analytics',
258
+ (yargs) =>
259
+ withAll(yargs)
260
+ .option('type', {
261
+ type: 'string',
262
+ choices: ['code', 'page'] as const,
263
+ description: 'Feedback type: code snippets or page-level aggregation',
264
+ })
265
+ .option('page', { type: 'string', description: 'Filter to a specific page path' }),
266
+ async (argv) => {
267
+ const format = resolveFormat(argv);
268
+ try {
269
+ if (format === 'table') addLog(<SpinnerLog message="Fetching feedback..." />);
270
+
271
+ if (argv.type === 'page') {
272
+ const data = await getFeedbackByPage(
273
+ { dateFrom: argv.from, dateTo: argv.to },
274
+ argv.subdomain
275
+ );
276
+ if (format === 'table') removeLastLog();
277
+
278
+ const filteredData = filterFeedbackByPageResponse(data, argv.page);
279
+ const rows = filteredData.feedback;
280
+
281
+ const headers = ['Path', 'Thumbs Up', 'Thumbs Down', 'Code', 'Total'];
282
+ const tableRows = rows.map((r) => [
283
+ truncate(r.path, 40),
284
+ num(r.thumbsUp),
285
+ num(r.thumbsDown),
286
+ num(r.code),
287
+ num(r.total),
288
+ ]);
289
+
290
+ if (format === 'json') {
291
+ output(format, JSON.stringify(filteredData, null, 2));
292
+ } else if (format === 'graph') {
293
+ const label = argv.subdomain ?? '';
294
+ const lines: string[] = [];
295
+ lines.push(
296
+ chalk.bold(`\nFeedback by Page \u2014 ${label} (${argv.from} to ${argv.to})\n`)
297
+ );
298
+ lines.push(
299
+ formatBarChart(
300
+ rows.slice(0, 20).map((r) => ({
301
+ label: truncate(r.path, 25),
302
+ value: r.total,
303
+ color: 'green',
304
+ }))
305
+ )
306
+ );
307
+ output('table', lines.join('\n'));
308
+ } else {
309
+ const label = argv.subdomain ?? '';
310
+ const lines: string[] = [];
311
+ if (format === 'table')
312
+ lines.push(chalk.bold(`\nFeedback \u2014 ${label} (${argv.from} to ${argv.to})\n`));
313
+ lines.push(formatOutput(format, headers, tableRows, filteredData));
314
+ if (format === 'table' && data.hasMore)
315
+ lines.push(chalk.dim('\n (more results available)'));
316
+ output(format, lines.join('\n'));
317
+ }
318
+ } else {
319
+ const source = argv.type === 'code' ? 'code_snippet' : undefined;
320
+ const data = await getFeedback(
321
+ { dateFrom: argv.from, dateTo: argv.to, source },
322
+ argv.subdomain
323
+ );
324
+ if (format === 'table') removeLastLog();
325
+
326
+ const filteredData = filterFeedbackResponse(data, argv.page);
327
+ const rows = filteredData.feedback;
328
+
329
+ const headers = ['ID', 'Path', 'Status', 'Source', 'Comment', 'Created'];
330
+ const tableRows = rows.map((r) => [
331
+ r.id.slice(0, 8),
332
+ truncate(r.path, 30),
333
+ r.status,
334
+ r.source,
335
+ truncate(r.comment ?? '\u2014', 30),
336
+ r.createdAt?.slice(0, 10) ?? '\u2014',
337
+ ]);
338
+
339
+ if (format === 'json') {
340
+ output(format, JSON.stringify(filteredData, null, 2));
341
+ } else {
342
+ const label = argv.subdomain ?? '';
343
+ const lines: string[] = [];
344
+ if (format === 'table')
345
+ lines.push(chalk.bold(`\nFeedback \u2014 ${label} (${argv.from} to ${argv.to})\n`));
346
+ lines.push(formatOutput(format, headers, tableRows, filteredData));
347
+ if (format === 'table' && data.hasMore)
348
+ lines.push(chalk.dim('\n (more results available)'));
349
+ output(format, lines.join('\n'));
350
+ }
351
+ }
352
+ await terminate(0);
353
+ } catch (err) {
354
+ if (format === 'table') removeLastLog();
355
+ addLog(<ErrorLog message={err instanceof Error ? err.message : 'unknown error'} />);
356
+ await terminate(1);
357
+ }
358
+ }
359
+ )
360
+ .command('conversation', 'view assistant conversation analytics', (yargs) =>
361
+ yargs
362
+ .command(
363
+ 'list',
364
+ 'list assistant conversations',
365
+ (yargs) =>
366
+ withAll(yargs).option('page', {
367
+ type: 'string',
368
+ description: 'Filter conversations mentioning this page in sources',
369
+ }),
370
+ async (argv) => {
371
+ const format = resolveFormat(argv);
372
+ try {
373
+ if (format === 'table') addLog(<SpinnerLog message="Fetching conversations..." />);
374
+ const data = await getConversations(
375
+ { dateFrom: argv.from, dateTo: argv.to },
376
+ argv.subdomain
377
+ );
378
+ if (format === 'table') removeLastLog();
379
+
380
+ const filteredData = filterConversationResponse(data, argv.page);
381
+ const conversations = filteredData.conversations;
382
+
383
+ const headers = ['ID', 'Timestamp', 'Query', 'Category'];
384
+ const tableRows = conversations.map((c) => [
385
+ c.id,
386
+ c.timestamp.slice(0, 19).replace('T', ' '),
387
+ truncate(c.query, 40),
388
+ c.queryCategory || '\u2014',
389
+ ]);
390
+
391
+ if (format === 'json') {
392
+ output(format, JSON.stringify(filteredData, null, 2));
393
+ } else {
394
+ const label = argv.subdomain ?? '';
395
+ const lines: string[] = [];
396
+ if (format === 'table')
397
+ lines.push(
398
+ chalk.bold(`\nConversations \u2014 ${label} (${argv.from} to ${argv.to})\n`)
399
+ );
400
+ lines.push(formatOutput(format, headers, tableRows, filteredData));
401
+ if (format === 'table' && data.hasMore)
402
+ lines.push(chalk.dim('\n (more results available)'));
403
+ output(format, lines.join('\n'));
404
+ }
405
+ await terminate(0);
406
+ } catch (err) {
407
+ if (format === 'table') removeLastLog();
408
+ addLog(<ErrorLog message={err instanceof Error ? err.message : 'unknown error'} />);
409
+ await terminate(1);
410
+ }
411
+ }
412
+ )
413
+ .command(
414
+ 'view <id>',
415
+ 'view a single conversation',
416
+ (yargs) =>
417
+ withFormat(withSubdomain(yargs)).positional('id', {
418
+ type: 'string',
419
+ demandOption: true,
420
+ description: 'Conversation ID',
421
+ }),
422
+ async (argv) => {
423
+ const format = resolveFormat(argv);
424
+ try {
425
+ if (format === 'table') addLog(<SpinnerLog message="Fetching conversation..." />);
426
+
427
+ const thread = await getAssistantThread(argv.id, argv.subdomain);
428
+
429
+ if (format === 'table') removeLastLog();
430
+
431
+ if (format === 'json' || format === 'plain') {
432
+ output(format, JSON.stringify(thread, null, 2));
433
+ } else {
434
+ const lines: string[] = [];
435
+ lines.push(chalk.bold(`\nConversation ${thread.threadId}\n`));
436
+ lines.push(` Created: ${thread.createdAt}`);
437
+ for (const message of thread.messages) {
438
+ const label = message.role === 'user' ? 'User' : 'Assistant';
439
+ lines.push(chalk.bold(`\n ${label}:`));
440
+ for (const line of message.content.split('\n')) {
441
+ lines.push(` ${line}`);
442
+ }
443
+ }
444
+ output(format, lines.join('\n'));
445
+ }
446
+ await terminate(0);
447
+ } catch (err) {
448
+ if (format === 'table') removeLastLog();
449
+ addLog(<ErrorLog message={err instanceof Error ? err.message : 'unknown error'} />);
450
+ await terminate(1);
451
+ }
452
+ }
453
+ )
454
+ .command('buckets', 'view conversation category buckets', (yargs) =>
455
+ yargs
456
+ .command(
457
+ 'list',
458
+ 'list conversation buckets',
459
+ (yargs) => withAll(yargs),
460
+ async (argv) => {
461
+ const format = resolveFormat(argv);
462
+ try {
463
+ if (format === 'table')
464
+ addLog(<SpinnerLog message="Fetching conversation buckets..." />);
465
+ const data = await getBuckets(
466
+ { dateFrom: argv.from, dateTo: argv.to },
467
+ argv.subdomain
468
+ );
469
+ if (format === 'table') removeLastLog();
470
+
471
+ const headers = ['ID', 'Label', 'Count', 'Last Asked'];
472
+ const tableRows = data.data.map((b) => [
473
+ b.id.slice(0, 12),
474
+ truncate(b.questionSummary, 50),
475
+ num(b.size),
476
+ b.lastAsked ? b.lastAsked.slice(0, 10) : '\u2014',
477
+ ]);
478
+
479
+ if (format === 'json') {
480
+ output(format, JSON.stringify(data, null, 2));
481
+ } else if (format === 'graph') {
482
+ const label = argv.subdomain ?? '';
483
+ const lines: string[] = [];
484
+ lines.push(
485
+ chalk.bold(
486
+ `\nConversation Buckets \u2014 ${label} (${argv.from} to ${argv.to})\n`
487
+ )
488
+ );
489
+ lines.push(
490
+ formatBarChart(
491
+ data.data.slice(0, 20).map((b) => ({
492
+ label: truncate(b.questionSummary, 30),
493
+ value: b.size,
494
+ color: 'blue',
495
+ }))
496
+ )
497
+ );
498
+ output('table', lines.join('\n'));
499
+ } else {
500
+ const label = argv.subdomain ?? '';
501
+ const lines: string[] = [];
502
+ if (format === 'table')
503
+ lines.push(
504
+ chalk.bold(
505
+ `\nConversation Buckets \u2014 ${label} (${argv.from} to ${argv.to})\n`
506
+ )
507
+ );
508
+ lines.push(formatOutput(format, headers, tableRows, data));
509
+ if (format === 'table')
510
+ lines.push(chalk.dim(`\n Total: ${data.pagination.total}`));
511
+ output(format, lines.join('\n'));
512
+ }
513
+ await terminate(0);
514
+ } catch (err) {
515
+ if (format === 'table') removeLastLog();
516
+ addLog(
517
+ <ErrorLog message={err instanceof Error ? err.message : 'unknown error'} />
518
+ );
519
+ await terminate(1);
520
+ }
521
+ }
522
+ )
523
+ .command(
524
+ 'view <id>',
525
+ 'view conversations in a bucket',
526
+ (yargs) =>
527
+ withAll(yargs)
528
+ .option('limit', {
529
+ type: 'number',
530
+ description: 'Maximum threads to return (1-100)',
531
+ })
532
+ .option('cursor', {
533
+ type: 'string',
534
+ description: 'Pagination cursor from a previous response',
535
+ })
536
+ .positional('id', {
537
+ type: 'string',
538
+ demandOption: true,
539
+ description: 'Bucket ID',
540
+ }),
541
+ async (argv) => {
542
+ const format = resolveFormat(argv);
543
+ try {
544
+ if (format === 'table')
545
+ addLog(<SpinnerLog message="Fetching bucket conversations..." />);
546
+ const data = await getBucketThreads(
547
+ argv.id,
548
+ {
549
+ dateFrom: argv.from,
550
+ dateTo: argv.to,
551
+ limit: argv.limit,
552
+ cursor: argv.cursor,
553
+ },
554
+ argv.subdomain
555
+ );
556
+ if (format === 'table') removeLastLog();
557
+
558
+ if (format === 'json') {
559
+ output(format, JSON.stringify(data, null, 2));
560
+ } else {
561
+ const headers = ['Thread ID', 'Query', 'Length', 'Feedback', 'Created'];
562
+ const tableRows = data.data.map((t) => [
563
+ t.id.slice(0, 12),
564
+ truncate(t.firstUserMessage || '\u2014', 40),
565
+ num(t.length),
566
+ `+${t.feedback.up} -${t.feedback.down}`,
567
+ t.createdAt.slice(0, 10),
568
+ ]);
569
+ const lines: string[] = [];
570
+ if (format === 'table') lines.push(chalk.bold(`\nBucket ${argv.id}\n`));
571
+ lines.push(formatOutput(format, headers, tableRows, data));
572
+ if (
573
+ format === 'table' &&
574
+ data.pagination.hasMore &&
575
+ data.pagination.nextCursor
576
+ ) {
577
+ lines.push(
578
+ chalk.dim(
579
+ `\n More results available. Run again with --cursor ${data.pagination.nextCursor}`
580
+ )
581
+ );
582
+ }
583
+ output(format, lines.join('\n'));
584
+ }
585
+ await terminate(0);
586
+ } catch (err) {
587
+ if (format === 'table') removeLastLog();
588
+ addLog(
589
+ <ErrorLog message={err instanceof Error ? err.message : 'unknown error'} />
590
+ );
591
+ await terminate(1);
592
+ }
593
+ }
594
+ )
595
+ .demandCommand(1, 'specify a subcommand: list or view')
596
+ )
597
+ .demandCommand(1, 'specify a subcommand: list, view, or buckets')
598
+ )
599
+ .demandCommand(1, 'specify a subcommand: stats, search, feedback, or conversation');
@@ -0,0 +1,99 @@
1
+ import chalk from 'chalk';
2
+
3
+ import { isAI } from '../helpers.js';
4
+
5
+ export type OutputFormat = 'table' | 'plain' | 'json' | 'graph';
6
+
7
+ export function resolveFormat(argv: { format?: string }): OutputFormat {
8
+ if (
9
+ argv.format === 'table' ||
10
+ argv.format === 'plain' ||
11
+ argv.format === 'json' ||
12
+ argv.format === 'graph'
13
+ )
14
+ return argv.format;
15
+ if (isAI()) return 'json';
16
+ return 'plain';
17
+ }
18
+
19
+ export function formatPlainTable(headers: string[], rows: string[][]): string {
20
+ if (rows.length === 0) return '';
21
+
22
+ const colWidths = headers.map((h, i) =>
23
+ Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length))
24
+ );
25
+
26
+ const headerLine = headers.map((h, i) => h.toUpperCase().padEnd(colWidths[i]!)).join('\t');
27
+ const bodyLines = rows.map((row) => row.map((cell, i) => cell.padEnd(colWidths[i]!)).join('\t'));
28
+
29
+ return [headerLine, ...bodyLines].join('\n');
30
+ }
31
+
32
+ export function formatPrettyTable(headers: string[], rows: string[][]): string {
33
+ if (rows.length === 0) return chalk.dim(' No data found.');
34
+
35
+ const colWidths = headers.map((h, i) =>
36
+ Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length))
37
+ );
38
+
39
+ const headerLine = headers.map((h, i) => chalk.bold(h.padEnd(colWidths[i]!))).join(' ');
40
+ const separator = chalk.dim(colWidths.map((w) => '\u2500'.repeat(w)).join('\u2500\u2500'));
41
+ const bodyLines = rows.map((row) => row.map((cell, i) => cell.padEnd(colWidths[i]!)).join(' '));
42
+
43
+ return [headerLine, separator, ...bodyLines].join('\n');
44
+ }
45
+
46
+ function gradientBar(len: number, rgb: [number, number, number]): string {
47
+ let result = '';
48
+ for (let i = 0; i < len; i++) {
49
+ const t = len === 1 ? 1 : i / (len - 1);
50
+ const dim = 0.3 + t * 0.7;
51
+ const r = Math.round(rgb[0] * dim);
52
+ const g = Math.round(rgb[1] * dim);
53
+ const b = Math.round(rgb[2] * dim);
54
+ result += chalk.rgb(r, g, b)('\u2588');
55
+ }
56
+ return result;
57
+ }
58
+
59
+ const COLOR_MAP: Record<string, [number, number, number]> = {
60
+ cyan: [0, 255, 255],
61
+ magenta: [255, 0, 255],
62
+ yellow: [255, 255, 0],
63
+ green: [0, 255, 100],
64
+ blue: [80, 140, 255],
65
+ red: [255, 80, 80],
66
+ };
67
+
68
+ export function formatBarChart(
69
+ items: { label: string; value: number; color?: string }[],
70
+ opts: { maxWidth?: number } = {}
71
+ ): string {
72
+ if (items.length === 0) return chalk.dim(' No data found.');
73
+
74
+ const maxWidth = opts.maxWidth ?? 40;
75
+ const maxVal = Math.max(...items.map((i) => i.value), 1);
76
+ const maxLabel = Math.max(...items.map((i) => i.label.length));
77
+ const maxValStr = Math.max(...items.map((i) => i.value.toLocaleString('en-US').length));
78
+
79
+ return items
80
+ .map((item) => {
81
+ const barLen = Math.round((item.value / maxVal) * maxWidth);
82
+ const rgb = COLOR_MAP[item.color ?? 'cyan'] ?? COLOR_MAP.cyan!;
83
+ const bar = barLen > 0 ? gradientBar(barLen, rgb) : '';
84
+ const pad = ' '.repeat(maxWidth - barLen);
85
+ return ` ${item.label.padEnd(maxLabel)} ${bar}${pad} ${item.value.toLocaleString('en-US').padStart(maxValStr)}`;
86
+ })
87
+ .join('\n');
88
+ }
89
+
90
+ export function formatOutput(
91
+ format: OutputFormat,
92
+ headers: string[],
93
+ rows: string[][],
94
+ jsonData: unknown
95
+ ): string {
96
+ if (format === 'json') return JSON.stringify(jsonData, null, 2);
97
+ if (format === 'plain') return formatPlainTable(headers, rows);
98
+ return formatPrettyTable(headers, rows);
99
+ }