@traisetech/autopilot 2.3.0 → 2.5.0
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/CHANGELOG.md +28 -1
- package/README.md +215 -202
- package/bin/autopilot.js +9 -2
- package/docs/CONFIGURATION.md +103 -103
- package/docs/DESIGN_PRINCIPLES.md +114 -114
- package/docs/TEAM-MODE.md +51 -51
- package/docs/TROUBLESHOOTING.md +21 -21
- package/package.json +75 -69
- package/src/commands/config.js +110 -110
- package/src/commands/dashboard.mjs +151 -151
- package/src/commands/doctor.js +127 -153
- package/src/commands/guide.js +63 -0
- package/src/commands/init.js +8 -9
- package/src/commands/insights.js +237 -237
- package/src/commands/leaderboard.js +116 -116
- package/src/commands/pause.js +18 -18
- package/src/commands/preset.js +121 -121
- package/src/commands/resume.js +17 -17
- package/src/commands/start.js +41 -41
- package/src/commands/status.js +73 -39
- package/src/commands/stop.js +58 -50
- package/src/commands/undo.js +84 -84
- package/src/config/defaults.js +23 -16
- package/src/config/ignore.js +14 -31
- package/src/config/loader.js +80 -80
- package/src/core/commit.js +45 -52
- package/src/core/commitMessageGenerator.js +130 -0
- package/src/core/configValidator.js +92 -0
- package/src/core/events.js +110 -110
- package/src/core/focus.js +2 -1
- package/src/core/gemini.js +15 -15
- package/src/core/git.js +29 -2
- package/src/core/history.js +69 -69
- package/src/core/notifier.js +61 -0
- package/src/core/retryQueue.js +152 -0
- package/src/core/safety.js +224 -210
- package/src/core/state.js +69 -71
- package/src/core/watcher.js +193 -66
- package/src/index.js +70 -70
- package/src/utils/banner.js +6 -6
- package/src/utils/crypto.js +18 -18
- package/src/utils/identity.js +41 -41
- package/src/utils/logger.js +86 -68
- package/src/utils/paths.js +62 -62
- package/src/utils/process.js +141 -141
package/src/commands/insights.js
CHANGED
|
@@ -1,237 +1,237 @@
|
|
|
1
|
-
const fs = require('fs-extra');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const logger = require('../utils/logger');
|
|
4
|
-
const git = require('../core/git');
|
|
5
|
-
const { createObjectCsvWriter } = require('csv-writer');
|
|
6
|
-
|
|
7
|
-
async function getGitStats(repoPath) {
|
|
8
|
-
try {
|
|
9
|
-
const { stdout } = await git.runGit(repoPath, [
|
|
10
|
-
'log',
|
|
11
|
-
'--pretty=format:===C===%H|%an|%ad|%s|%b===E===',
|
|
12
|
-
'--date=iso',
|
|
13
|
-
'--numstat'
|
|
14
|
-
]);
|
|
15
|
-
|
|
16
|
-
if (!stdout) return [];
|
|
17
|
-
|
|
18
|
-
const commits = [];
|
|
19
|
-
const rawCommits = stdout.split('===C===').filter(Boolean);
|
|
20
|
-
|
|
21
|
-
for (const raw of rawCommits) {
|
|
22
|
-
const [metadataPlusBody, ...statsParts] = raw.split('===E===');
|
|
23
|
-
if (!metadataPlusBody) continue;
|
|
24
|
-
|
|
25
|
-
const [hash, author, dateStr, subject, ...bodyParts] = metadataPlusBody.trim().split('|');
|
|
26
|
-
const body = bodyParts.join('|'); // Rejoin in case body had pipes
|
|
27
|
-
|
|
28
|
-
// Trust Verification: Only process autopilot commits
|
|
29
|
-
if (!body.includes('Autopilot-Commit: true')) continue;
|
|
30
|
-
|
|
31
|
-
const commit = {
|
|
32
|
-
hash,
|
|
33
|
-
author,
|
|
34
|
-
date: new Date(dateStr),
|
|
35
|
-
message: `${subject}\n${body}`.trim(),
|
|
36
|
-
files: [],
|
|
37
|
-
additions: 0,
|
|
38
|
-
deletions: 0
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const statsText = statsParts.join('===E===').trim();
|
|
42
|
-
if (statsText) {
|
|
43
|
-
const statLines = statsText.split('\n');
|
|
44
|
-
for (const line of statLines) {
|
|
45
|
-
const [add, del, file] = line.trim().split(/\s+/);
|
|
46
|
-
if (file) {
|
|
47
|
-
const additions = parseInt(add) || 0;
|
|
48
|
-
const deletions = parseInt(del) || 0;
|
|
49
|
-
commit.files.push({ file, additions, deletions });
|
|
50
|
-
commit.additions += additions;
|
|
51
|
-
commit.deletions += deletions;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
commits.push(commit);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return commits;
|
|
59
|
-
} catch (error) {
|
|
60
|
-
logger.error(`Failed to analyze git history: ${error.message}`);
|
|
61
|
-
return [];
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function calculateMetrics(commits) {
|
|
66
|
-
const stats = {
|
|
67
|
-
totalCommits: commits.length,
|
|
68
|
-
totalFilesChanged: new Set(),
|
|
69
|
-
totalAdditions: 0,
|
|
70
|
-
totalDeletions: 0,
|
|
71
|
-
commitsByDay: {},
|
|
72
|
-
commitsByHour: {},
|
|
73
|
-
authors: {},
|
|
74
|
-
streak: { current: 0, max: 0 },
|
|
75
|
-
topFiles: {},
|
|
76
|
-
quality: {
|
|
77
|
-
conventional: 0,
|
|
78
|
-
issuesReferenced: 0,
|
|
79
|
-
avgLength: 0,
|
|
80
|
-
score: 0
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const dates = new Set();
|
|
85
|
-
let totalMessageLength = 0;
|
|
86
|
-
|
|
87
|
-
commits.forEach(c => {
|
|
88
|
-
// Aggregates
|
|
89
|
-
stats.totalAdditions += c.additions;
|
|
90
|
-
stats.totalDeletions += c.deletions;
|
|
91
|
-
c.files.forEach(f => {
|
|
92
|
-
stats.totalFilesChanged.add(f.file);
|
|
93
|
-
stats.topFiles[f.file] = (stats.topFiles[f.file] || 0) + 1;
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// Time analysis
|
|
97
|
-
const dateStr = c.date.toISOString().split('T')[0];
|
|
98
|
-
const hour = c.date.getHours();
|
|
99
|
-
|
|
100
|
-
stats.commitsByDay[dateStr] = (stats.commitsByDay[dateStr] || 0) + 1;
|
|
101
|
-
stats.commitsByHour[hour] = (stats.commitsByHour[hour] || 0) + 1;
|
|
102
|
-
dates.add(dateStr);
|
|
103
|
-
|
|
104
|
-
// Quality
|
|
105
|
-
const conventionalRegex = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?: .+/;
|
|
106
|
-
if (conventionalRegex.test(c.message)) stats.quality.conventional++;
|
|
107
|
-
if (/#\d+/.test(c.message)) stats.quality.issuesReferenced++;
|
|
108
|
-
totalMessageLength += c.message.length;
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// Calculate Averages
|
|
112
|
-
stats.totalFilesCount = stats.totalFilesChanged.size;
|
|
113
|
-
stats.quality.avgLength = commits.length ? Math.round(totalMessageLength / commits.length) : 0;
|
|
114
|
-
|
|
115
|
-
// Calculate Score (0-100)
|
|
116
|
-
// 40% Conventional, 30% Message Length (>30 chars), 30% Consistency
|
|
117
|
-
const convScore = commits.length ? (stats.quality.conventional / commits.length) * 40 : 0;
|
|
118
|
-
const lenScore = Math.min(stats.quality.avgLength / 50, 1) * 30; // Cap at 50 chars
|
|
119
|
-
const consistencyScore = 30; // Placeholder for now
|
|
120
|
-
stats.quality.score = Math.round(convScore + lenScore + consistencyScore);
|
|
121
|
-
|
|
122
|
-
// Streak Calculation
|
|
123
|
-
const sortedDates = Array.from(dates).sort();
|
|
124
|
-
let currentStreak = 0;
|
|
125
|
-
let maxStreak = 0;
|
|
126
|
-
let lastDate = null;
|
|
127
|
-
|
|
128
|
-
sortedDates.forEach(date => {
|
|
129
|
-
const d = new Date(date);
|
|
130
|
-
if (!lastDate) {
|
|
131
|
-
currentStreak = 1;
|
|
132
|
-
} else {
|
|
133
|
-
const diffTime = Math.abs(d - lastDate);
|
|
134
|
-
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
135
|
-
if (diffDays === 1) {
|
|
136
|
-
currentStreak++;
|
|
137
|
-
} else {
|
|
138
|
-
maxStreak = Math.max(maxStreak, currentStreak);
|
|
139
|
-
currentStreak = 1;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
lastDate = d;
|
|
143
|
-
});
|
|
144
|
-
stats.streak.max = Math.max(maxStreak, currentStreak);
|
|
145
|
-
|
|
146
|
-
// Check if streak is active (last commit today or yesterday)
|
|
147
|
-
const today = new Date().toISOString().split('T')[0];
|
|
148
|
-
const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0];
|
|
149
|
-
const lastCommitDate = sortedDates[sortedDates.length - 1];
|
|
150
|
-
|
|
151
|
-
if (lastCommitDate === today || lastCommitDate === yesterday) {
|
|
152
|
-
stats.streak.current = currentStreak;
|
|
153
|
-
} else {
|
|
154
|
-
stats.streak.current = 0;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return stats;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async function insights(options) {
|
|
161
|
-
try {
|
|
162
|
-
const repoPath = options.cwd || process.cwd();
|
|
163
|
-
logger.info('Analyzing repository history...');
|
|
164
|
-
|
|
165
|
-
const commits = await getGitStats(repoPath);
|
|
166
|
-
if (commits.length === 0) {
|
|
167
|
-
logger.warn('No git history found.');
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const metrics = calculateMetrics(commits);
|
|
172
|
-
|
|
173
|
-
if (options.format === 'json') {
|
|
174
|
-
console.log(JSON.stringify(metrics, null, 2));
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Display Report
|
|
179
|
-
console.log('');
|
|
180
|
-
logger.section('📈 Project Analytics');
|
|
181
|
-
console.log(`Total Commits: ${metrics.totalCommits}`);
|
|
182
|
-
console.log(`Files Changed: ${metrics.totalFilesCount}`);
|
|
183
|
-
console.log(`Lines Added: ${metrics.totalAdditions}`);
|
|
184
|
-
console.log(`Lines Deleted: ${metrics.totalDeletions}`);
|
|
185
|
-
console.log(`Current Streak: ${metrics.streak.current} days (Max: ${metrics.streak.max})`);
|
|
186
|
-
|
|
187
|
-
// Find most productive hour
|
|
188
|
-
const productiveHour = Object.entries(metrics.commitsByHour)
|
|
189
|
-
.sort(([, a], [, b]) => b - a)[0];
|
|
190
|
-
console.log(`Peak Productivity: ${productiveHour ? productiveHour[0] + ':00' : 'N/A'}`);
|
|
191
|
-
|
|
192
|
-
console.log('');
|
|
193
|
-
logger.section('💎 Quality Score: ' + metrics.quality.score + '/100');
|
|
194
|
-
console.log(`Conventional Commits: ${Math.round((metrics.quality.conventional / metrics.totalCommits) * 100)}%`);
|
|
195
|
-
console.log(`Avg Message Length: ${metrics.quality.avgLength} chars`);
|
|
196
|
-
console.log(`Issues Referenced: ${metrics.quality.issuesReferenced}`);
|
|
197
|
-
|
|
198
|
-
if (metrics.quality.score < 50) {
|
|
199
|
-
logger.warn('Suggestion: Use conventional commits (feat:, fix:) to improve score.');
|
|
200
|
-
} else if (metrics.quality.score > 80) {
|
|
201
|
-
logger.success('Great job! High quality commit history.');
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// CSV Export
|
|
205
|
-
if (options.export === 'csv') {
|
|
206
|
-
const csvPath = path.join(repoPath, 'autopilot-insights.csv');
|
|
207
|
-
const csvWriter = createObjectCsvWriter({
|
|
208
|
-
path: csvPath,
|
|
209
|
-
header: [
|
|
210
|
-
{ id: 'hash', title: 'Hash' },
|
|
211
|
-
{ id: 'date', title: 'Date' },
|
|
212
|
-
{ id: 'author', title: 'Author' },
|
|
213
|
-
{ id: 'message', title: 'Message' },
|
|
214
|
-
{ id: 'additions', title: 'Additions' },
|
|
215
|
-
{ id: 'deletions', title: 'Deletions' }
|
|
216
|
-
]
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
const records = commits.map(c => ({
|
|
220
|
-
hash: c.hash,
|
|
221
|
-
date: c.date.toISOString(),
|
|
222
|
-
author: c.author,
|
|
223
|
-
message: c.message,
|
|
224
|
-
additions: c.additions,
|
|
225
|
-
deletions: c.deletions
|
|
226
|
-
}));
|
|
227
|
-
|
|
228
|
-
await csvWriter.writeRecords(records);
|
|
229
|
-
logger.success(`Exported insights to ${csvPath}`);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
} catch (error) {
|
|
233
|
-
logger.error(`Failed to generate insights: ${error.message}`);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
module.exports = { insights, getGitStats, calculateMetrics };
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const logger = require('../utils/logger');
|
|
4
|
+
const git = require('../core/git');
|
|
5
|
+
const { createObjectCsvWriter } = require('csv-writer');
|
|
6
|
+
|
|
7
|
+
async function getGitStats(repoPath) {
|
|
8
|
+
try {
|
|
9
|
+
const { stdout } = await git.runGit(repoPath, [
|
|
10
|
+
'log',
|
|
11
|
+
'--pretty=format:===C===%H|%an|%ad|%s|%b===E===',
|
|
12
|
+
'--date=iso',
|
|
13
|
+
'--numstat'
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
if (!stdout) return [];
|
|
17
|
+
|
|
18
|
+
const commits = [];
|
|
19
|
+
const rawCommits = stdout.split('===C===').filter(Boolean);
|
|
20
|
+
|
|
21
|
+
for (const raw of rawCommits) {
|
|
22
|
+
const [metadataPlusBody, ...statsParts] = raw.split('===E===');
|
|
23
|
+
if (!metadataPlusBody) continue;
|
|
24
|
+
|
|
25
|
+
const [hash, author, dateStr, subject, ...bodyParts] = metadataPlusBody.trim().split('|');
|
|
26
|
+
const body = bodyParts.join('|'); // Rejoin in case body had pipes
|
|
27
|
+
|
|
28
|
+
// Trust Verification: Only process autopilot commits
|
|
29
|
+
if (!body.includes('Autopilot-Commit: true')) continue;
|
|
30
|
+
|
|
31
|
+
const commit = {
|
|
32
|
+
hash,
|
|
33
|
+
author,
|
|
34
|
+
date: new Date(dateStr),
|
|
35
|
+
message: `${subject}\n${body}`.trim(),
|
|
36
|
+
files: [],
|
|
37
|
+
additions: 0,
|
|
38
|
+
deletions: 0
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const statsText = statsParts.join('===E===').trim();
|
|
42
|
+
if (statsText) {
|
|
43
|
+
const statLines = statsText.split('\n');
|
|
44
|
+
for (const line of statLines) {
|
|
45
|
+
const [add, del, file] = line.trim().split(/\s+/);
|
|
46
|
+
if (file) {
|
|
47
|
+
const additions = parseInt(add) || 0;
|
|
48
|
+
const deletions = parseInt(del) || 0;
|
|
49
|
+
commit.files.push({ file, additions, deletions });
|
|
50
|
+
commit.additions += additions;
|
|
51
|
+
commit.deletions += deletions;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
commits.push(commit);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return commits;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
logger.error(`Failed to analyze git history: ${error.message}`);
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function calculateMetrics(commits) {
|
|
66
|
+
const stats = {
|
|
67
|
+
totalCommits: commits.length,
|
|
68
|
+
totalFilesChanged: new Set(),
|
|
69
|
+
totalAdditions: 0,
|
|
70
|
+
totalDeletions: 0,
|
|
71
|
+
commitsByDay: {},
|
|
72
|
+
commitsByHour: {},
|
|
73
|
+
authors: {},
|
|
74
|
+
streak: { current: 0, max: 0 },
|
|
75
|
+
topFiles: {},
|
|
76
|
+
quality: {
|
|
77
|
+
conventional: 0,
|
|
78
|
+
issuesReferenced: 0,
|
|
79
|
+
avgLength: 0,
|
|
80
|
+
score: 0
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const dates = new Set();
|
|
85
|
+
let totalMessageLength = 0;
|
|
86
|
+
|
|
87
|
+
commits.forEach(c => {
|
|
88
|
+
// Aggregates
|
|
89
|
+
stats.totalAdditions += c.additions;
|
|
90
|
+
stats.totalDeletions += c.deletions;
|
|
91
|
+
c.files.forEach(f => {
|
|
92
|
+
stats.totalFilesChanged.add(f.file);
|
|
93
|
+
stats.topFiles[f.file] = (stats.topFiles[f.file] || 0) + 1;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Time analysis
|
|
97
|
+
const dateStr = c.date.toISOString().split('T')[0];
|
|
98
|
+
const hour = c.date.getHours();
|
|
99
|
+
|
|
100
|
+
stats.commitsByDay[dateStr] = (stats.commitsByDay[dateStr] || 0) + 1;
|
|
101
|
+
stats.commitsByHour[hour] = (stats.commitsByHour[hour] || 0) + 1;
|
|
102
|
+
dates.add(dateStr);
|
|
103
|
+
|
|
104
|
+
// Quality
|
|
105
|
+
const conventionalRegex = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?: .+/;
|
|
106
|
+
if (conventionalRegex.test(c.message)) stats.quality.conventional++;
|
|
107
|
+
if (/#\d+/.test(c.message)) stats.quality.issuesReferenced++;
|
|
108
|
+
totalMessageLength += c.message.length;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Calculate Averages
|
|
112
|
+
stats.totalFilesCount = stats.totalFilesChanged.size;
|
|
113
|
+
stats.quality.avgLength = commits.length ? Math.round(totalMessageLength / commits.length) : 0;
|
|
114
|
+
|
|
115
|
+
// Calculate Score (0-100)
|
|
116
|
+
// 40% Conventional, 30% Message Length (>30 chars), 30% Consistency
|
|
117
|
+
const convScore = commits.length ? (stats.quality.conventional / commits.length) * 40 : 0;
|
|
118
|
+
const lenScore = Math.min(stats.quality.avgLength / 50, 1) * 30; // Cap at 50 chars
|
|
119
|
+
const consistencyScore = 30; // Placeholder for now
|
|
120
|
+
stats.quality.score = Math.round(convScore + lenScore + consistencyScore);
|
|
121
|
+
|
|
122
|
+
// Streak Calculation
|
|
123
|
+
const sortedDates = Array.from(dates).sort();
|
|
124
|
+
let currentStreak = 0;
|
|
125
|
+
let maxStreak = 0;
|
|
126
|
+
let lastDate = null;
|
|
127
|
+
|
|
128
|
+
sortedDates.forEach(date => {
|
|
129
|
+
const d = new Date(date);
|
|
130
|
+
if (!lastDate) {
|
|
131
|
+
currentStreak = 1;
|
|
132
|
+
} else {
|
|
133
|
+
const diffTime = Math.abs(d - lastDate);
|
|
134
|
+
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
135
|
+
if (diffDays === 1) {
|
|
136
|
+
currentStreak++;
|
|
137
|
+
} else {
|
|
138
|
+
maxStreak = Math.max(maxStreak, currentStreak);
|
|
139
|
+
currentStreak = 1;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
lastDate = d;
|
|
143
|
+
});
|
|
144
|
+
stats.streak.max = Math.max(maxStreak, currentStreak);
|
|
145
|
+
|
|
146
|
+
// Check if streak is active (last commit today or yesterday)
|
|
147
|
+
const today = new Date().toISOString().split('T')[0];
|
|
148
|
+
const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0];
|
|
149
|
+
const lastCommitDate = sortedDates[sortedDates.length - 1];
|
|
150
|
+
|
|
151
|
+
if (lastCommitDate === today || lastCommitDate === yesterday) {
|
|
152
|
+
stats.streak.current = currentStreak;
|
|
153
|
+
} else {
|
|
154
|
+
stats.streak.current = 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return stats;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function insights(options) {
|
|
161
|
+
try {
|
|
162
|
+
const repoPath = options.cwd || process.cwd();
|
|
163
|
+
logger.info('Analyzing repository history...');
|
|
164
|
+
|
|
165
|
+
const commits = await getGitStats(repoPath);
|
|
166
|
+
if (commits.length === 0) {
|
|
167
|
+
logger.warn('No git history found.');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const metrics = calculateMetrics(commits);
|
|
172
|
+
|
|
173
|
+
if (options.format === 'json') {
|
|
174
|
+
console.log(JSON.stringify(metrics, null, 2));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Display Report
|
|
179
|
+
console.log('');
|
|
180
|
+
logger.section('📈 Project Analytics');
|
|
181
|
+
console.log(`Total Commits: ${metrics.totalCommits}`);
|
|
182
|
+
console.log(`Files Changed: ${metrics.totalFilesCount}`);
|
|
183
|
+
console.log(`Lines Added: ${metrics.totalAdditions}`);
|
|
184
|
+
console.log(`Lines Deleted: ${metrics.totalDeletions}`);
|
|
185
|
+
console.log(`Current Streak: ${metrics.streak.current} days (Max: ${metrics.streak.max})`);
|
|
186
|
+
|
|
187
|
+
// Find most productive hour
|
|
188
|
+
const productiveHour = Object.entries(metrics.commitsByHour)
|
|
189
|
+
.sort(([, a], [, b]) => b - a)[0];
|
|
190
|
+
console.log(`Peak Productivity: ${productiveHour ? productiveHour[0] + ':00' : 'N/A'}`);
|
|
191
|
+
|
|
192
|
+
console.log('');
|
|
193
|
+
logger.section('💎 Quality Score: ' + metrics.quality.score + '/100');
|
|
194
|
+
console.log(`Conventional Commits: ${Math.round((metrics.quality.conventional / metrics.totalCommits) * 100)}%`);
|
|
195
|
+
console.log(`Avg Message Length: ${metrics.quality.avgLength} chars`);
|
|
196
|
+
console.log(`Issues Referenced: ${metrics.quality.issuesReferenced}`);
|
|
197
|
+
|
|
198
|
+
if (metrics.quality.score < 50) {
|
|
199
|
+
logger.warn('Suggestion: Use conventional commits (feat:, fix:) to improve score.');
|
|
200
|
+
} else if (metrics.quality.score > 80) {
|
|
201
|
+
logger.success('Great job! High quality commit history.');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// CSV Export
|
|
205
|
+
if (options.export === 'csv') {
|
|
206
|
+
const csvPath = path.join(repoPath, 'autopilot-insights.csv');
|
|
207
|
+
const csvWriter = createObjectCsvWriter({
|
|
208
|
+
path: csvPath,
|
|
209
|
+
header: [
|
|
210
|
+
{ id: 'hash', title: 'Hash' },
|
|
211
|
+
{ id: 'date', title: 'Date' },
|
|
212
|
+
{ id: 'author', title: 'Author' },
|
|
213
|
+
{ id: 'message', title: 'Message' },
|
|
214
|
+
{ id: 'additions', title: 'Additions' },
|
|
215
|
+
{ id: 'deletions', title: 'Deletions' }
|
|
216
|
+
]
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const records = commits.map(c => ({
|
|
220
|
+
hash: c.hash,
|
|
221
|
+
date: c.date.toISOString(),
|
|
222
|
+
author: c.author,
|
|
223
|
+
message: c.message,
|
|
224
|
+
additions: c.additions,
|
|
225
|
+
deletions: c.deletions
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
await csvWriter.writeRecords(records);
|
|
229
|
+
logger.success(`Exported insights to ${csvPath}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
} catch (error) {
|
|
233
|
+
logger.error(`Failed to generate insights: ${error.message}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = { insights, getGitStats, calculateMetrics };
|