Skip to content

Commit 0f8c4eb

Browse files
committed
feat(cli): add dev activity and dev owners commands with indexed author contributions
- Add 'dev activity' command showing most active files with commit counts and complexity - Add 'dev owners' command with developer specialization breakdown - Index author contributions during 'dev index' for 35x speedup (17.5s → 0.5s) - Add file_authors table to MetricsStore schema - Implement batched git operations (1 call vs N file calls) - Refactor metrics commands to top-level for better UX - Add compact table format with factual summaries - Add GitHub handle resolution for developer identification - Remove deprecated 'dev metrics' command Performance improvements: - Batched git log reduces overhead from O(N files) to O(1) - Indexed author data enables offline queries - No git access needed after initial indexing Breaking changes: - 'dev metrics activity' → 'dev activity' - 'dev metrics ownership' → 'dev owners' - 'dev metrics size' removed (redundant)
1 parent a22632d commit 0f8c4eb

File tree

13 files changed

+831
-272
lines changed

13 files changed

+831
-272
lines changed

.changeset/huge-release-v0-8-0.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
"@lytics/dev-agent": minor
3+
"@lytics/dev-agent-cli": minor
4+
"@lytics/dev-agent-core": minor
5+
"@lytics/dev-agent-mcp": minor
6+
"@lytics/dev-agent-subagents": minor
7+
"@lytics/dev-agent-types": minor
8+
---
9+
10+
## 🎉 v0.8.0 - Major Feature Release
11+
12+
This release includes 33 commits with significant new features, performance improvements, and architectural enhancements.
13+
14+
### 🚀 Major Features
15+
16+
- **`dev map` command** - Visualize codebase structure with component counts, exports, and hot paths (224x performance improvement!)
17+
- **`dev activity` command** - Show most active files with commit counts, recency, and complexity
18+
- **`dev owners` command** - Developer specialization breakdown with file-level ownership
19+
- **Author contribution indexing** - Indexed during `dev index` for 35x faster ownership queries
20+
- **Service layer architecture** - 7 services with dependency injection for better testability
21+
- **MetricsStore with SQLite** - Persistent code analytics with `file_authors` table
22+
- **Code metadata system** - Factual metrics replacing risk scoring
23+
- **Change frequency analysis** - Git activity tracking and hotspot identification
24+
- **Stats comparison & export** - Historical metrics analysis
25+
26+
### 🎨 CLI/UX Improvements
27+
28+
- **Compact table format** for metrics commands with factual summaries
29+
- **Top-level commands** - `dev activity` and `dev owners` (refactored from `dev metrics`)
30+
- Enhanced `dev stats` output with 10x performance boost
31+
- Enhanced `dev git stats` with clean, scannable format
32+
- Enhanced `dev compact`, `dev clean`, and MCP command outputs
33+
- Modernized CLI with compact, user-friendly formatting
34+
- Comprehensive help text with examples and use cases
35+
- Visual indicators (🔥 for hotspots, ✏️ for activity)
36+
- GitHub handle resolution for developer identification
37+
38+
### 🏗️ Architecture & Quality
39+
40+
- Service-oriented architecture with dependency injection
41+
- Circular dependency resolution via shared types package
42+
- Complete Zod validation across all 9 MCP adapters and external boundaries
43+
- Kero logger integration throughout
44+
- SearchService refactor for better code reuse
45+
- Improved error handling and messaging
46+
47+
### ⚡ Performance Optimizations
48+
49+
- **`dev map`**: 224x speedup (103s → 0.46s)
50+
- Added `getAll()` method for fast scans without semantic search
51+
- Added `skipEmbedder` option for read-only operations
52+
- Added `getBasicStats()` to avoid expensive git enrichment
53+
- **`dev owners`**: 35x speedup (17.5s → 0.5s)
54+
- Batched git operations during indexing (1 call vs N file calls)
55+
- Author contributions stored in `file_authors` table
56+
- Offline capability - no git access needed after indexing
57+
- **`dev stats`**: 10x speedup via direct JSON reads
58+
59+
### 🐛 Bug Fixes
60+
61+
- Fixed component count overflow in map generation (2.4B → 3.7K)
62+
- Fixed detailed stats persistence in indexer
63+
- Fixed ENOBUFS issues
64+
65+
### 📚 Documentation
66+
67+
- Updated website for v0.7.0 features
68+
- TypeScript standards with Zod validation examples
69+
- Workflow documentation with commit checkpoints
70+
- Enhanced CLI help text across all commands
71+
72+
### 🧪 Testing
73+
74+
- All 1,918 tests passing
75+
- Added comprehensive test coverage for new features
76+
- Mock updates for new `getAll()` method
77+
78+
This release represents a significant step forward in usability, performance, and code quality. Special thanks to all contributors!
79+

packages/cli/src/cli.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import chalk from 'chalk';
44
import { Command } from 'commander';
5+
import { activityCommand } from './commands/activity.js';
56
import { cleanCommand } from './commands/clean.js';
67
import { compactCommand } from './commands/compact.js';
78
import { dashboardCommand } from './commands/dashboard.js';
@@ -12,7 +13,7 @@ import { indexCommand } from './commands/index.js';
1213
import { initCommand } from './commands/init.js';
1314
import { mapCommand } from './commands/map.js';
1415
import { mcpCommand } from './commands/mcp.js';
15-
import { metricsCommand } from './commands/metrics.js';
16+
import { ownersCommand } from './commands/owners.js';
1617
import { planCommand } from './commands/plan.js';
1718
import { searchCommand } from './commands/search.js';
1819
import { statsCommand } from './commands/stats.js';
@@ -41,7 +42,8 @@ program.addCommand(gitCommand);
4142
program.addCommand(mapCommand);
4243
program.addCommand(updateCommand);
4344
program.addCommand(statsCommand);
44-
program.addCommand(metricsCommand);
45+
program.addCommand(ownersCommand);
46+
program.addCommand(activityCommand);
4547
program.addCommand(dashboardCommand);
4648
program.addCommand(compactCommand);
4749
program.addCommand(cleanCommand);
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/**
2+
* Activity command - Show most active files by commit frequency
3+
*/
4+
5+
import * as path from 'node:path';
6+
import {
7+
type FileMetrics,
8+
getMostActive,
9+
getStoragePath,
10+
MetricsStore,
11+
} from '@lytics/dev-agent-core';
12+
import chalk from 'chalk';
13+
import { Command } from 'commander';
14+
import { loadConfig } from '../utils/config.js';
15+
import { logger } from '../utils/logger.js';
16+
17+
/**
18+
* Format relative time (e.g., "2 days ago", "today")
19+
*/
20+
function formatRelativeTime(date: Date): string {
21+
const now = new Date();
22+
const diffMs = now.getTime() - date.getTime();
23+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
24+
25+
if (diffDays === 0) return 'today';
26+
if (diffDays === 1) return 'yesterday';
27+
if (diffDays < 7) return `${diffDays}d ago`;
28+
if (diffDays < 30) return `${Math.floor(diffDays / 7)}w ago`;
29+
if (diffDays < 365) return `${Math.floor(diffDays / 30)}mo ago`;
30+
return `${Math.floor(diffDays / 365)}y ago`;
31+
}
32+
33+
/**
34+
* Format files as a compact table
35+
*/
36+
function formatFileMetricsTable(files: FileMetrics[]): string {
37+
if (files.length === 0) return '';
38+
39+
// Calculate column widths
40+
const maxPathLen = Math.max(...files.map((f) => f.filePath.length), 40);
41+
const pathWidth = Math.min(maxPathLen, 55);
42+
43+
// Header
44+
let output = chalk.bold(
45+
`${'FILE'.padEnd(pathWidth)} ${'COMMITS'.padStart(7)} ${'LOC'.padStart(6)} ${'AUTHORS'.padStart(7)} ${'LAST CHANGE'}\n`
46+
);
47+
48+
// Separator line
49+
output += chalk.dim(`${'─'.repeat(pathWidth + 2 + 7 + 2 + 6 + 2 + 7 + 2 + 12)}\n`);
50+
51+
// Rows
52+
for (const file of files) {
53+
// Truncate path if too long
54+
let displayPath = file.filePath;
55+
if (displayPath.length > pathWidth) {
56+
displayPath = `...${displayPath.slice(-(pathWidth - 3))}`;
57+
}
58+
displayPath = displayPath.padEnd(pathWidth);
59+
60+
const commits = String(file.commitCount).padStart(7);
61+
const loc = String(file.linesOfCode).padStart(6);
62+
63+
// Author count with emoji
64+
const authorIcon = file.authorCount === 1 ? ' 👤' : file.authorCount === 2 ? ' 👥' : '👥👥';
65+
const authors = `${String(file.authorCount).padStart(5)}${authorIcon}`;
66+
67+
// Relative time
68+
const lastChange = file.lastModified ? formatRelativeTime(file.lastModified) : 'unknown';
69+
70+
output += `${chalk.dim(displayPath)} ${chalk.cyan(commits)} ${chalk.yellow(loc)} ${chalk.green(authors)} ${chalk.gray(lastChange)}\n`;
71+
}
72+
73+
return output;
74+
}
75+
76+
/**
77+
* Generate summary insights
78+
*/
79+
function generateActivitySummary(files: FileMetrics[]): string[] {
80+
const insights: string[] = [];
81+
const highChurn = files.filter((f) => f.commitCount >= 10).length;
82+
const singleAuthor = files.filter((f) => f.authorCount === 1).length;
83+
84+
if (highChurn > 0) {
85+
insights.push(`${highChurn} file${highChurn > 1 ? 's' : ''} changed 10+ times this month`);
86+
}
87+
if (singleAuthor > 0 && singleAuthor === files.length) {
88+
insights.push(`All files have single author`);
89+
} else if (singleAuthor > files.length / 2) {
90+
insights.push(`${singleAuthor}/${files.length} files have single author`);
91+
}
92+
93+
return insights;
94+
}
95+
96+
/**
97+
* Activity command - Show most active files
98+
*/
99+
export const activityCommand = new Command('activity')
100+
.description('Show most active files by commit frequency')
101+
.option('-n, --limit <number>', 'Number of files to show', '10')
102+
.option('--json', 'Output as JSON', false)
103+
.action(async (options) => {
104+
try {
105+
const config = await loadConfig();
106+
if (!config) {
107+
logger.error('No config found. Run "dev init" first.');
108+
process.exit(1);
109+
}
110+
111+
const repositoryPath = config.repository?.path || config.repositoryPath || process.cwd();
112+
const storagePath = await getStoragePath(path.resolve(repositoryPath));
113+
const metricsDbPath = path.join(storagePath, 'metrics.db');
114+
115+
const store = new MetricsStore(metricsDbPath);
116+
const latestSnapshot = store.getLatestSnapshot(path.resolve(repositoryPath));
117+
118+
if (!latestSnapshot) {
119+
logger.warn('No metrics found. Index your repository first with "dev index".');
120+
store.close();
121+
process.exit(0);
122+
}
123+
124+
const limit = Number.parseInt(options.limit, 10);
125+
const files = getMostActive(store, latestSnapshot.id, limit);
126+
127+
// Get total count for context
128+
const allFiles = store.getCodeMetadata({ snapshotId: latestSnapshot.id, limit: 10000 });
129+
const totalWithActivity = allFiles.filter((f) => (f.commitCount || 0) >= 5).length;
130+
131+
store.close();
132+
133+
if (files.length === 0) {
134+
logger.warn('No file metrics available.');
135+
process.exit(0);
136+
}
137+
138+
// JSON output for programmatic use
139+
if (options.json) {
140+
console.log(JSON.stringify({ files, totalWithActivity }, null, 2));
141+
return;
142+
}
143+
144+
// Human-readable table output
145+
console.log('');
146+
console.log(
147+
chalk.bold.cyan(`📊 Most Active Files (${totalWithActivity} total with 5+ commits)`)
148+
);
149+
console.log('');
150+
console.log(formatFileMetricsTable(files));
151+
console.log('');
152+
153+
// Add summary
154+
const summary = generateActivitySummary(files);
155+
if (summary.length > 0) {
156+
console.log(chalk.dim('Summary:'));
157+
for (const insight of summary) {
158+
console.log(chalk.dim(` • ${insight}`));
159+
}
160+
}
161+
162+
console.log('');
163+
} catch (error) {
164+
logger.error(
165+
`Failed to get activity metrics: ${error instanceof Error ? error.message : String(error)}`
166+
);
167+
process.exit(1);
168+
}
169+
});

packages/cli/src/commands/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { execSync } from 'node:child_process';
22
import { existsSync } from 'node:fs';
3-
import { join } from 'node:path';
3+
import { join, resolve } from 'node:path';
44
import {
55
AsyncEventBus,
66
ensureStorageDirectory,
@@ -67,7 +67,7 @@ export const indexCommand = new Command('index')
6767
const spinner = ora('Checking prerequisites...').start();
6868

6969
try {
70-
const resolvedRepoPath = repositoryPath;
70+
const resolvedRepoPath = resolve(repositoryPath);
7171

7272
// Check prerequisites upfront
7373
const isGitRepo = isGitRepository(resolvedRepoPath);
@@ -139,6 +139,11 @@ export const indexCommand = new Command('index')
139139
if (event.codeMetadata && event.codeMetadata.length > 0) {
140140
metricsStore.appendCodeMetadata(snapshotId, event.codeMetadata);
141141
}
142+
143+
// Store file author contributions if available
144+
if (event.authorContributions && event.authorContributions.size > 0) {
145+
metricsStore.appendFileAuthors(snapshotId, event.authorContributions);
146+
}
142147
} catch (error) {
143148
// Log error but don't fail indexing - metrics are non-critical
144149
logger.error(

0 commit comments

Comments
 (0)