Skip to content

Commit 3ff31c7

Browse files
committed
feat(cli): enhance stats and storage output with 10x performance boost
- Optimize dev stats to read JSON directly (0.7s vs 7s, 10x faster) - Add visual progress bars and percentages to language breakdown - Display summary line with repo, files, components, storage, and age - Show git context (branch, commit, remote) in stats output - Add health indicators and actionable next steps - Upgrade dev storage info with clean table format and status icons - Remove logger timestamps from user-facing commands for cleaner output Bug Fixes: - Fix dev update bug where totalDocuments was set to batch size instead of querying actual LanceDB vector count, causing stats to show incorrect totals - Calculate total components from byLanguage sum for accuracy Output Improvements: - Consistent hybrid style across commands (gh CLI + docker inspired) - Tables for structured data with visual status indicators (✓, ⚠, ✗) - Progressive disclosure (important info first) - Human-readable sizes and relative timestamps - Maintained consistency with existing dev gh commands Breaking Changes: - dev stats no longer has 'show' subcommand (stats is now direct command) - Removed --verbose flag (detailed table is now default) Affects: #148 (Dashboard & Visualization epic)
1 parent df19423 commit 3ff31c7

File tree

6 files changed

+767
-508
lines changed

6 files changed

+767
-508
lines changed

packages/cli/src/cli.test.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,9 @@ describe('CLI Structure', () => {
5858
expect(jsonOption).toBeDefined();
5959
});
6060

61-
it('stats command should have show subcommand with json option', () => {
62-
const subcommands = statsCommand.commands;
63-
const showCommand = subcommands.find((cmd) => cmd.name() === 'show');
64-
65-
expect(showCommand).toBeDefined();
66-
67-
if (showCommand) {
68-
const jsonOption = showCommand.options.find((opt) => opt.long === '--json');
69-
expect(jsonOption).toBeDefined();
70-
}
61+
it('stats command should have json option', () => {
62+
const jsonOption = statsCommand.options.find((opt) => opt.long === '--json');
63+
expect(jsonOption).toBeDefined();
7164
});
7265

7366
it('clean command should have force option', () => {

packages/cli/src/commands/gh.ts

Lines changed: 42 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ import { Command } from 'commander';
1111
import ora from 'ora';
1212
import { formatNumber } from '../utils/formatters.js';
1313
import { keroLogger, logger } from '../utils/logger.js';
14-
import { output } from '../utils/output.js';
14+
import {
15+
output,
16+
printGitHubContext,
17+
printGitHubSearchResults,
18+
printGitHubStats,
19+
} from '../utils/output.js';
1520

1621
/**
1722
* Create GitHub indexer with centralized storage
@@ -169,44 +174,15 @@ export const ghCommand = new Command('gh')
169174
limit: options.limit,
170175
});
171176

172-
spinner.succeed(chalk.green(`Found ${results.length} results`));
173-
174-
if (results.length === 0) {
175-
logger.log('');
176-
logger.log(chalk.gray('No results found'));
177-
return;
178-
}
177+
spinner.stop();
179178

180179
// Output results
181180
if (options.json) {
182181
console.log(JSON.stringify(results, null, 2));
183182
return;
184183
}
185184

186-
logger.log('');
187-
for (const result of results) {
188-
const doc = result.document;
189-
const typeEmoji = doc.type === 'issue' ? '🐛' : '🔀';
190-
const stateColor =
191-
doc.state === 'open'
192-
? chalk.green
193-
: doc.state === 'merged'
194-
? chalk.magenta
195-
: chalk.gray;
196-
197-
logger.log(
198-
`${typeEmoji} ${chalk.bold(`#${doc.number}`)} ${doc.title} ${stateColor(`[${doc.state}]`)}`
199-
);
200-
logger.log(
201-
` ${chalk.gray(`Score: ${(result.score * 100).toFixed(0)}%`)} | ${chalk.blue(doc.url)}`
202-
);
203-
204-
if (doc.labels.length > 0) {
205-
logger.log(` Labels: ${doc.labels.map((l: string) => chalk.cyan(l)).join(', ')}`);
206-
}
207-
208-
logger.log('');
209-
}
185+
printGitHubSearchResults(results, query as string);
210186
} catch (error) {
211187
spinner.fail('Search failed');
212188
logger.error((error as Error).message);
@@ -257,46 +233,42 @@ export const ghCommand = new Command('gh')
257233
return;
258234
}
259235

260-
spinner.succeed(chalk.green('Context retrieved'));
236+
spinner.stop();
261237

262238
if (options.json) {
263239
console.log(JSON.stringify(context, null, 2));
264240
return;
265241
}
266242

243+
// Convert context to printable format
267244
const doc = context.document;
268-
const typeEmoji = doc.type === 'issue' ? '🐛' : '🔀';
269-
270-
logger.log('');
271-
logger.log(chalk.bold.cyan(`${typeEmoji} #${doc.number}: ${doc.title}`));
272-
logger.log('');
273-
logger.log(chalk.gray(`${doc.body.substring(0, 200)}...`));
274-
logger.log('');
275-
276-
if (context.relatedIssues.length > 0) {
277-
logger.log(chalk.bold('Related Issues:'));
278-
for (const related of context.relatedIssues) {
279-
logger.log(` 🐛 #${related.number} ${related.title}`);
280-
}
281-
logger.log('');
282-
}
283-
284-
if (context.relatedPRs.length > 0) {
285-
logger.log(chalk.bold('Related PRs:'));
286-
for (const related of context.relatedPRs) {
287-
logger.log(` 🔀 #${related.number} ${related.title}`);
288-
}
289-
logger.log('');
290-
}
291-
292-
if (context.linkedCodeFiles.length > 0) {
293-
logger.log(chalk.bold('Linked Code Files:'));
294-
for (const file of context.linkedCodeFiles) {
295-
const scorePercent = (file.score * 100).toFixed(0);
296-
logger.log(` 📁 ${file.path} (${scorePercent}% match)`);
297-
}
298-
logger.log('');
299-
}
245+
printGitHubContext({
246+
type: doc.type,
247+
number: doc.number,
248+
title: doc.title,
249+
body: doc.body,
250+
state: doc.state,
251+
author: doc.author,
252+
createdAt: doc.createdAt,
253+
updatedAt: doc.updatedAt,
254+
labels: doc.labels,
255+
url: doc.url,
256+
comments: doc.comments,
257+
relatedIssues: context.relatedIssues.map((r) => ({
258+
number: r.number,
259+
title: r.title,
260+
state: r.state,
261+
})),
262+
relatedPRs: context.relatedPRs.map((r) => ({
263+
number: r.number,
264+
title: r.title,
265+
state: r.state,
266+
})),
267+
linkedFiles: context.linkedCodeFiles.map((f) => ({
268+
path: f.path,
269+
score: f.score,
270+
})),
271+
});
300272
} catch (error) {
301273
spinner.fail('Failed to get context');
302274
logger.error((error as Error).message);
@@ -319,45 +291,16 @@ export const ghCommand = new Command('gh')
319291
spinner.stop();
320292

321293
if (!stats) {
322-
logger.log('');
323-
logger.log(chalk.yellow('GitHub data not indexed'));
324-
logger.log('Run "dev gh index" to index');
294+
output.log();
295+
output.warn('GitHub data not indexed');
296+
output.log('Run "dev gh index" to index');
325297
return;
326298
}
327299

328-
logger.log('');
329-
logger.log(chalk.bold.cyan('GitHub Indexing Stats'));
330-
logger.log('');
331-
logger.log(`Repository: ${chalk.cyan(stats.repository)}`);
332-
logger.log(`Total Documents: ${chalk.yellow(stats.totalDocuments)}`);
333-
logger.log('');
334-
335-
logger.log(chalk.bold('By Type:'));
336-
if (stats.byType.issue) {
337-
logger.log(` Issues: ${stats.byType.issue}`);
338-
}
339-
if (stats.byType.pull_request) {
340-
logger.log(` Pull Requests: ${stats.byType.pull_request}`);
341-
}
342-
logger.log('');
343-
344-
logger.log(chalk.bold('By State:'));
345-
if (stats.byState.open) {
346-
logger.log(` ${chalk.green('Open')}: ${stats.byState.open}`);
347-
}
348-
if (stats.byState.closed) {
349-
logger.log(` ${chalk.gray('Closed')}: ${stats.byState.closed}`);
350-
}
351-
if (stats.byState.merged) {
352-
logger.log(` ${chalk.magenta('Merged')}: ${stats.byState.merged}`);
353-
}
354-
logger.log('');
355-
356-
logger.log(`Last Indexed: ${chalk.gray(stats.lastIndexed)}`);
357-
logger.log('');
300+
printGitHubStats(stats);
358301
} catch (error) {
359302
spinner.fail('Failed to get stats');
360-
logger.error((error as Error).message);
303+
output.error((error as Error).message);
361304
process.exit(1);
362305
}
363306
})

0 commit comments

Comments
 (0)