@traisetech/autopilot 2.1.0 → 2.2.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 CHANGED
@@ -1,8 +1,68 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.2.0] - 2026-02-14
4
+
5
+ ### Added
6
+ - **Automatic Leaderboard Sync**:
7
+ - Watcher auto-syncs stats after commit or push.
8
+ - Uses site API with anonymized ID and metrics only.
9
+ - **Durable Backend Storage**:
10
+ - Website API backed by Supabase for persistent leaderboard and event telemetry.
11
+ - **Events API**:
12
+ - CLI emits `push_success` events with commit hash and identity.
13
+ - Website ingests and stores normalized payloads.
14
+
15
+ ### Improved
16
+ - **Config Consistency**:
17
+ - Standardized `blockedBranches` with backward-compat mapping.
18
+ - **AI Network Resilience**:
19
+ - Added request timeouts to Gemini/Grok; doctor validates connectivity.
20
+ - **Programmatic Imports**:
21
+ - Fixed command imports wiring in `src/index.js`.
22
+
23
+ ### Docs/Website
24
+ - **OG Image & Favicon**:
25
+ - Added `public/og-image.svg` and `public/favicon.svg`.
26
+ - Corrected manifest path to `/manifest.webmanifest`.
27
+ - **Foreground Watcher Wording**:
28
+ - Updated homepage and commands to reflect foreground behavior.
29
+
30
+ ### Tests
31
+ - **Grok Test Coverage**:
32
+ - Added parity tests mirroring Gemini scenarios.
33
+
3
34
  All notable changes to this project will be documented in this file.
4
35
  This project follows [Semantic Versioning](https://semver.org).
5
36
 
37
+ ## [2.1.1] - 2026-02-11
38
+
39
+ ### Added
40
+ - **Global Leaderboard Sync**:
41
+ - New `autopilot leaderboard --sync` command to share productivity metrics with the global community.
42
+ - Implemented secure, anonymized data transmission (metrics only, no code).
43
+ - **Dynamic Leaderboard Dashboard**:
44
+ - Completely redesigned `autopilot-docs` leaderboard with real-time analytics.
45
+ - Real-time aggregation of global commits, focus hours, and active streaks.
46
+ - Premium glassmorphism UI with live ranking updates.
47
+
48
+ ### Improved
49
+ - **CLI Aesthetics**:
50
+ - Integrated full ANSI color support for the `logger` utility for more professional output.
51
+ - Improved visual hierarchy with bold section headers and color-coded status icons.
52
+ - **Diagnostics**:
53
+ - Enhanced `doctor` command to intelligently check for `credential.helper` validation on HTTPS remotes.
54
+ - **Insights Portability**:
55
+ - Refined Git log parsing to be more robust across different Git versions and configurations.
56
+
57
+ ### Fixed
58
+ - **Dashboard Stability**:
59
+ - Fixed "Duplicate Key" warning in the interactive React Ink dashboard.
60
+ - Added TTY-detection to prevent crashes in non-interactive environments (CI/CD, background).
61
+ - Fixed ESM/CommonJS compatibility issues using dynamic `import()` for the dashboard.
62
+ - **Test Integrity**:
63
+ - Implemented `AUTOPILOT_TEST_MODE` bypass for automated dashboard verification.
64
+ - Fixed cross-test contamination and EBUSY errors on Windows platforms.
65
+
6
66
  ## [2.1.0] - 2026-02-08
7
67
 
8
68
  ### Added
package/bin/autopilot.js CHANGED
@@ -13,6 +13,7 @@ const { leaderboard } = require('../src/commands/leaderboard');
13
13
  const doctor = require('../src/commands/doctor');
14
14
  const presetCommand = require('../src/commands/preset');
15
15
  const configCommand = require('../src/commands/config');
16
+ const interactiveCommand = require('../src/commands/interactive');
16
17
  const pkg = require('../package.json');
17
18
  const logger = require('../src/utils/logger');
18
19
  const { checkForUpdate } = require('../src/utils/update-check');
@@ -30,7 +31,8 @@ const commands = {
30
31
  leaderboard: leaderboard,
31
32
  doctor: doctor,
32
33
  preset: presetCommand,
33
- config: configCommand
34
+ config: configCommand,
35
+ interactive: interactiveCommand
34
36
  };
35
37
 
36
38
  // Runtime assertion to prevent wiring errors
@@ -122,6 +124,12 @@ program
122
124
  .option('-g, --global', 'Use global configuration')
123
125
  .action(configCommand);
124
126
 
127
+ program
128
+ .command('interactive [on|off]')
129
+ .description('Toggle AI Safety Mode (on = prompt, off = automated)')
130
+ .option('-g, --global', 'Set the preference globally')
131
+ .action(interactiveCommand);
132
+
125
133
  program
126
134
  .command('doctor')
127
135
  .description('Diagnose and validate autopilot setup')
@@ -49,7 +49,7 @@ This document reflects the current `.autopilotrc.json` options.
49
49
  - **Default:** true
50
50
  - **Description:** Push to `origin/<branch>` after commit.
51
51
 
52
- ### `blockBranches`
52
+ ### `blockedBranches`
53
53
  - **Type:** string[]
54
54
  - **Default:** `["main", "master"]`
55
55
  - **Description:** Branches where auto-commit is disabled.
@@ -65,24 +65,27 @@ This document reflects the current `.autopilotrc.json` options.
65
65
  - **Description:** Shell commands executed sequentially when `requireChecks` is true.
66
66
 
67
67
  ### `commitMessageMode`
68
- - **Type:** `"smart" | "simple"`
68
+ - **Type:** `"smart" | "simple" | "ai"`
69
69
  - **Default:** `"smart"`
70
- - **Description:** Smart uses file-based conventional commit messages; simple uses `chore: update changes`.
70
+ - **Description:**
71
+ - smart: file/diff-based conventional messages
72
+ - simple: `chore: update changes`
73
+ - ai: uses configured AI provider (Gemini or Grok)
71
74
 
72
75
  ### `teamMode`
73
76
  - **Type:** boolean
74
77
  - **Default:** `false`
75
78
  - **Description:** Enables pull-before-push and stricter conflict handling. Recommended for collaborative environments.
76
79
 
77
- ### `maxFileSizeMB`
78
- - **Type:** number
79
- - **Default:** `50`
80
- - **Description:** Prevents committing files larger than this size (in MB).
80
+ ### `preCommitChecks.fileSize`
81
+ - **Type:** boolean
82
+ - **Default:** `true`
83
+ - **Description:** Prevent commits containing files larger than 50MB.
81
84
 
82
- ### `preventSecrets`
85
+ ### `preCommitChecks.secrets`
83
86
  - **Type:** boolean
84
87
  - **Default:** `true`
85
- - **Description:** Scans staged files for common secret patterns (AWS keys, GitHub tokens) before committing.
88
+ - **Description:** Secret scan for common key/token patterns before committing.
86
89
 
87
90
  ---
88
91
 
@@ -40,19 +40,75 @@ All automation is **reversible** via `autopilot undo`.
40
40
  - **AI (Gemini / Grok)** is an assistant, never an authority.
41
41
  - AI output must be reviewable, overridable, and optional.
42
42
 
43
+ ## Privacy & Local-First Design
44
+
45
+ **Privacy Guarantees:**
46
+ - Your source code **never** leaves your machine
47
+ - No code diffs are transmitted externally
48
+ - No file contents are sent to remote servers
49
+ - AI commit message generation happens with metadata only (file paths, line counts, not actual code)
50
+
51
+ **Local-First Architecture:**
52
+ - Works 100% offline (except for git push operations)
53
+ - No authentication to external services required
54
+ - All data stored locally in your project
55
+ - Configuration is local and version-controllable
56
+
43
57
  ## Leaderboard & Metrics
44
58
 
45
59
  - Metrics are derived **only** from local Git activity created by Autopilot.
46
60
  - **No raw code, diffs, or file contents are ever transmitted.**
47
61
  - Leaderboard data is:
48
- - opt-in
62
+ - opt-in (disabled by default)
49
63
  - anonymized or pseudonymous
50
64
  - explainable (users know exactly what is counted)
65
+ - aggregate only (commit counts, focus time, streak days)
66
+
67
+ **What gets synced (if opted in):**
68
+ - ✅ Commit counts
69
+ - ✅ Focus time duration
70
+ - ✅ Streak days
71
+ - ✅ Anonymized username/identifier
72
+
73
+ **What never gets synced:**
74
+ - ❌ Source code
75
+ - ❌ File names or paths
76
+ - ❌ Commit messages
77
+ - ❌ Repository names
78
+ - ❌ File diffs or changes
79
+
80
+ ## User Experience Philosophy
81
+
82
+ **When in Doubt: Pause, Explain, Wait**
83
+
84
+ - Ambiguous situations should trigger clear, actionable error messages
85
+ - Users should always understand what Autopilot is doing and why
86
+ - Status messages should be informative without being verbose
87
+ - Configuration should have sensible defaults but be fully customizable
88
+
89
+ **Trust Through Transparency:**
90
+ - Every action Autopilot takes should be logged
91
+ - Users should be able to audit what happened and when
92
+ - The system should explain its decisions in plain language
93
+ - Documentation should be honest about limitations
94
+
95
+ ## Development Guidelines
96
+
97
+ **For Contributors:**
98
+ - Any feature must pass the "trust test" - would you trust this with your production code?
99
+ - Prefer explicit over implicit behavior
100
+ - Add clear error messages for every failure case
101
+ - Document why, not just what
102
+ - Test edge cases extensively, especially around git state
103
+
104
+ **For AI Integration:**
105
+ - AI should enhance, not replace, developer judgment
106
+ - All AI suggestions must be reviewable before commit
107
+ - Provide escape hatches for AI-generated content
108
+ - Log AI usage for transparency
109
+ - Allow disabling AI features entirely
51
110
 
52
- ## Philosophy
111
+ ---
53
112
 
54
- - Autopilot exists to protect developer flow, not replace developer judgment.
55
- - **When in doubt: pause, explain, wait.**
113
+ *Any feature (including Grok or leaderboards) must pass this test: Does it maintain developer trust, safety, and control?*
56
114
 
57
- ---
58
- *Any feature (including Grok or leaderboards) must pass this test.*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@traisetech/autopilot",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -19,7 +19,7 @@ const e = React.createElement;
19
19
  const Dashboard = () => {
20
20
  const { exit } = useApp();
21
21
  const root = process.cwd();
22
-
22
+
23
23
  const [status, setStatus] = useState('loading');
24
24
  const [pid, setPid] = useState(null);
25
25
  const [lastCommit, setLastCommit] = useState(null);
@@ -34,18 +34,18 @@ const Dashboard = () => {
34
34
  // 1. Check process status
35
35
  const currentPid = await getRunningPid(root);
36
36
  setPid(currentPid);
37
-
37
+
38
38
  // 2. Check Paused State
39
39
  const stateManager = new StateManager(root);
40
40
  if (stateManager.isPaused()) {
41
- setStatus('paused');
42
- setPausedState(stateManager.getState());
41
+ setStatus('paused');
42
+ setPausedState(stateManager.getState());
43
43
  } else if (currentPid) {
44
- setStatus('running');
45
- setPausedState(null);
44
+ setStatus('running');
45
+ setPausedState(null);
46
46
  } else {
47
- setStatus('stopped');
48
- setPausedState(null);
47
+ setStatus('stopped');
48
+ setPausedState(null);
49
49
  }
50
50
 
51
51
  // 3. Last Commit
@@ -56,7 +56,7 @@ const Dashboard = () => {
56
56
  // 4. Pending Files
57
57
  const statusObj = await git.getPorcelainStatus(root);
58
58
  if (statusObj.ok) {
59
- setPendingFiles(statusObj.files);
59
+ setPendingFiles(statusObj.files);
60
60
  }
61
61
 
62
62
  // 5. Today Stats (Simple count from history)
@@ -126,10 +126,10 @@ const Dashboard = () => {
126
126
  e(Box, { flexDirection: "column", marginBottom: 1 },
127
127
  e(Text, { underline: true }, `Pending Changes (${pendingFiles.length})`),
128
128
  e(Box, { flexDirection: "column" },
129
- pendingFiles.length === 0 ?
129
+ pendingFiles.length === 0 ?
130
130
  e(Text, { color: "gray" }, "No pending changes") :
131
- pendingFiles.slice(0, 5).map((f) =>
132
- e(Text, { key: f.file, color: "yellow" }, ` ${f.status} ${f.file}`)
131
+ pendingFiles.slice(0, 5).map((f, idx) =>
132
+ e(Text, { key: `${f.file}-${idx}`, color: "yellow" }, ` ${f.status} ${f.file}`)
133
133
  )
134
134
  ),
135
135
  pendingFiles.length > 5 && e(Text, { color: "gray" }, ` ...and ${pendingFiles.length - 5} more`)
@@ -143,5 +143,9 @@ const Dashboard = () => {
143
143
  };
144
144
 
145
145
  export default function runDashboard() {
146
+ if (!process.stdin.isTTY && !process.env.AUTOPILOT_TEST_MODE) {
147
+ console.error('Error: Dashboard requires an interactive terminal (TTY).');
148
+ process.exit(1);
149
+ }
146
150
  render(e(Dashboard));
147
151
  }
@@ -13,7 +13,7 @@ const git = require('../core/git');
13
13
  const doctor = async () => {
14
14
  const repoPath = process.cwd();
15
15
  let issues = 0;
16
-
16
+
17
17
  logger.section('Autopilot Doctor');
18
18
  logger.info('Diagnosing environment...');
19
19
 
@@ -50,7 +50,17 @@ const doctor = async () => {
50
50
 
51
51
  // Check remote type
52
52
  if (remoteUrl.startsWith('http')) {
53
- logger.warn('Remote uses HTTPS. Ensure credential helper is configured for non-interactive push.');
53
+ let hasHelper = false;
54
+ try {
55
+ const { stdout: helper } = await execa('git', ['config', '--get', 'credential.helper'], { cwd: repoPath });
56
+ if (helper.trim()) hasHelper = true;
57
+ } catch (e) { /* ignore */ }
58
+
59
+ if (hasHelper) {
60
+ logger.success('Remote uses HTTPS with credential helper configured.');
61
+ } else {
62
+ logger.warn('Remote uses HTTPS. Ensure credential helper is configured for non-interactive push.');
63
+ }
54
64
  } else if (remoteUrl.startsWith('git@') || remoteUrl.startsWith('ssh://')) {
55
65
  logger.success('Remote uses SSH (recommended).');
56
66
  } else {
@@ -102,8 +112,8 @@ const doctor = async () => {
102
112
  logger.success('Branch is up to date with remote.');
103
113
  }
104
114
  } else {
105
- // Could be no upstream configured, which is fine for local-only initially
106
- logger.info('Could not check remote status (upstream might not be set).');
115
+ // Could be no upstream configured, which is fine for local-only initially
116
+ logger.info('Could not check remote status (upstream might not be set).');
107
117
  }
108
118
  } catch (error) {
109
119
  logger.info('Skipping remote status check.');
@@ -116,6 +126,34 @@ const doctor = async () => {
116
126
  } else {
117
127
  logger.warn(`Diagnosis complete. Found ${issues} potential issue(s).`);
118
128
  }
129
+
130
+ // 7. AI Connectivity (if enabled)
131
+ try {
132
+ const { loadConfig } = require('../config/loader');
133
+ const config = await loadConfig(repoPath);
134
+ if (config?.ai?.enabled) {
135
+ logger.section('AI Connectivity');
136
+ if (config.ai.provider === 'grok') {
137
+ const { validateGrokApiKey } = require('../core/grok');
138
+ const result = await validateGrokApiKey(config.ai.grokApiKey);
139
+ if (result.valid) logger.success('Grok API reachable and key looks valid.');
140
+ else {
141
+ logger.warn(`Grok API check failed: ${result.error}`);
142
+ issues++;
143
+ }
144
+ } else {
145
+ const { validateApiKey } = require('../core/gemini');
146
+ const result = await validateApiKey(config.ai.apiKey);
147
+ if (result.valid) logger.success('Gemini API reachable and key looks valid.');
148
+ else {
149
+ logger.warn(`Gemini API check failed: ${result.error}`);
150
+ issues++;
151
+ }
152
+ }
153
+ }
154
+ } catch (error) {
155
+ // ignore AI check failures
156
+ }
119
157
  };
120
158
 
121
159
  module.exports = doctor;
@@ -137,19 +137,21 @@ async function initRepo() {
137
137
  const teamMode = await askQuestion('Enable team mode? (pull before push) [y/N]: ');
138
138
  const useTeamMode = teamMode.toLowerCase() === 'y';
139
139
 
140
- // Phase 3: AI Configuration
141
- const enableAI = await askQuestion('Enable AI commit messages? [y/N]: ');
142
- let useAI = enableAI.toLowerCase() === 'y';
140
+ // Phase 3: AI Configuration (Zero-Config)
141
+ logger.info('\n🤖 AI Commit Messages are ENABLED by default (Zero-Config).');
142
+ const customAI = await askQuestion('Would you like to use your own AI API keys instead? [y/N]: ');
143
143
 
144
+ let useAI = true;
144
145
  let apiKey = '';
145
146
  let grokApiKey = '';
146
- let provider = 'gemini';
147
- let interactive = false;
147
+ let provider = 'grok';
148
+ let interactive = DEFAULT_CONFIG.ai.interactive;
149
+
148
150
 
149
- if (useAI) {
151
+ if (customAI.toLowerCase() === 'y') {
150
152
  // Select Provider
151
- const providerAns = await askQuestion('Select AI Provider (gemini/grok) [gemini]: ');
152
- provider = providerAns.toLowerCase() === 'grok' ? 'grok' : 'gemini';
153
+ const providerAns = await askQuestion('Select AI Provider (gemini/grok) [grok]: ');
154
+ provider = providerAns.toLowerCase() === 'gemini' ? 'gemini' : 'grok';
153
155
 
154
156
  while (true) {
155
157
  const keyPrompt = provider === 'grok'
@@ -159,16 +161,15 @@ async function initRepo() {
159
161
  const keyInput = await askQuestion(keyPrompt);
160
162
 
161
163
  if (!keyInput) {
162
- logger.warn('API Key cannot be empty if AI is enabled.');
163
- const retry = await askQuestion('Try again? (n to disable AI) [Y/n]: ');
164
+ logger.warn('Custom API Key cannot be empty. Falling back to System AI.');
165
+ const retry = await askQuestion('Try again with custom key? (n to use System AI) [Y/n]: ');
164
166
  if (retry.toLowerCase() === 'n') {
165
- useAI = false;
166
167
  break;
167
168
  }
168
169
  continue;
169
170
  }
170
171
 
171
- logger.info(`Verifying ${provider} API Key...`);
172
+ logger.info(`Verifying custom ${provider} API Key...`);
172
173
  let result;
173
174
  if (provider === 'grok') {
174
175
  result = await grok.validateGrokApiKey(keyInput);
@@ -177,34 +178,33 @@ async function initRepo() {
177
178
  }
178
179
 
179
180
  if (result.valid) {
180
- logger.success('API Key verified successfully! ✨');
181
+ logger.success('Custom API Key verified successfully! ✨');
181
182
  if (provider === 'grok') grokApiKey = keyInput;
182
183
  else apiKey = keyInput;
183
184
  break;
184
185
  } else {
185
186
  logger.warn(`API Key validation failed: ${result.error}`);
186
- const retry = await askQuestion('Try again? (n to disable AI, p to proceed anyway) [Y/n/p]: ');
187
+ const retry = await askQuestion('Try again? (n to use System AI, p to proceed anyway) [Y/n/p]: ');
187
188
  const choice = retry.toLowerCase();
188
189
 
189
190
  if (choice === 'n') {
190
- useAI = false;
191
191
  break;
192
192
  } else if (choice === 'p') {
193
- logger.warn('Proceeding with potentially invalid API key.');
193
+ logger.warn('Proceeding with custom API key.');
194
194
  if (provider === 'grok') grokApiKey = keyInput;
195
195
  else apiKey = keyInput;
196
196
  break;
197
197
  }
198
- // Default is retry (loop)
199
198
  }
200
199
  }
201
200
 
202
- if (useAI) {
203
- const interactiveAns = await askQuestion('Review AI messages before committing? [y/N]: ');
204
- interactive = interactiveAns.toLowerCase() === 'y';
205
- }
201
+ const interactiveAns = await askQuestion('Review AI messages before committing? [y/N]: ');
202
+ interactive = interactiveAns.toLowerCase() === 'y';
203
+ } else {
204
+ logger.info('Using System AI (Zero-Config mode). ✨');
206
205
  }
207
206
 
207
+
208
208
  const overrides = {
209
209
  teamMode: useTeamMode,
210
210
  ai: {
@@ -6,86 +6,52 @@ const { createObjectCsvWriter } = require('csv-writer');
6
6
 
7
7
  async function getGitStats(repoPath) {
8
8
  try {
9
- // Get commit log with stats
10
- // We use custom delimiters to safely parse multi-line bodies and stats
11
9
  const { stdout } = await git.runGit(repoPath, [
12
10
  'log',
13
- '--pretty=format:====COMMIT====%n%H|%an|%ad|%s|%b%n====BODY_END====',
11
+ '--pretty=format:===C===%H|%an|%ad|%s|%b===E===',
14
12
  '--date=iso',
15
13
  '--numstat'
16
14
  ]);
17
15
 
16
+ if (!stdout) return [];
17
+
18
18
  const commits = [];
19
- const rawCommits = stdout.split('====COMMIT====');
19
+ const rawCommits = stdout.split('===C===').filter(Boolean);
20
20
 
21
21
  for (const raw of rawCommits) {
22
- if (!raw.trim()) continue;
23
-
24
- const [metadataPart, statsPart] = raw.split('====BODY_END====');
25
- if (!metadataPart) continue;
26
-
27
- const lines = metadataPart.trim().split('\n');
28
- const header = lines[0]; // hash|author|date|subject|body_start...
29
- // The body might continue on next lines if %b has newlines.
30
- // Actually, my format puts %b starting on the first line.
31
- // But let's be safer: split header by | first 4 times only.
32
-
33
- // header format: hash|author|date|subject|rest...
34
- // But wait, if body has newlines, "lines" array has them.
35
-
36
- // Let's reconstruct the full message body
37
- const fullMetadata = metadataPart.trim();
38
- const firstPipe = fullMetadata.indexOf('|');
39
- const secondPipe = fullMetadata.indexOf('|', firstPipe + 1);
40
- const thirdPipe = fullMetadata.indexOf('|', secondPipe + 1);
41
- const fourthPipe = fullMetadata.indexOf('|', thirdPipe + 1);
42
-
43
- if (firstPipe === -1 || fourthPipe === -1) continue;
44
-
45
- const hash = fullMetadata.substring(0, firstPipe);
46
- const author = fullMetadata.substring(firstPipe + 1, secondPipe);
47
- const dateStr = fullMetadata.substring(secondPipe + 1, thirdPipe);
48
- const subject = fullMetadata.substring(thirdPipe + 1, fourthPipe);
49
- const body = fullMetadata.substring(fourthPipe + 1);
50
-
51
- // TRUST VERIFICATION
52
- // Check for Autopilot trailers
53
- if (!body.includes('Autopilot-Commit: true')) {
54
- continue; // Skip non-autopilot commits
55
- }
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
56
27
 
57
- // TODO: Verify Signature (Optional but recommended for strict mode)
58
- // const signature = extractTrailer(body, 'Autopilot-Signature');
59
- // if (!verifySignature(signature, ...)) continue;
28
+ // Trust Verification: Only process autopilot commits
29
+ if (!body.includes('Autopilot-Commit: true')) continue;
60
30
 
61
31
  const commit = {
62
32
  hash,
63
33
  author,
64
34
  date: new Date(dateStr),
65
- message: subject + '\n' + body,
35
+ message: `${subject}\n${body}`.trim(),
66
36
  files: [],
67
37
  additions: 0,
68
38
  deletions: 0
69
39
  };
70
40
 
71
- // Parse Stats
72
- if (statsPart) {
73
- const statLines = statsPart.trim().split('\n');
74
- for (const statLine of statLines) {
75
- if (!statLine.trim()) continue;
76
- const parts = statLine.split(/\s+/);
77
- if (parts.length >= 3) {
78
- const additions = parseInt(parts[0]) || 0;
79
- const deletions = parseInt(parts[1]) || 0;
80
- const file = parts.slice(2).join(' '); // handle spaces in filenames
81
-
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;
82
49
  commit.files.push({ file, additions, deletions });
83
50
  commit.additions += additions;
84
51
  commit.deletions += deletions;
85
52
  }
86
53
  }
87
54
  }
88
-
89
55
  commits.push(commit);
90
56
  }
91
57
 
@@ -130,7 +96,7 @@ function calculateMetrics(commits) {
130
96
  // Time analysis
131
97
  const dateStr = c.date.toISOString().split('T')[0];
132
98
  const hour = c.date.getHours();
133
-
99
+
134
100
  stats.commitsByDay[dateStr] = (stats.commitsByDay[dateStr] || 0) + 1;
135
101
  stats.commitsByHour[hour] = (stats.commitsByHour[hour] || 0) + 1;
136
102
  dates.add(dateStr);
@@ -145,7 +111,7 @@ function calculateMetrics(commits) {
145
111
  // Calculate Averages
146
112
  stats.totalFilesCount = stats.totalFilesChanged.size;
147
113
  stats.quality.avgLength = commits.length ? Math.round(totalMessageLength / commits.length) : 0;
148
-
114
+
149
115
  // Calculate Score (0-100)
150
116
  // 40% Conventional, 30% Message Length (>30 chars), 30% Consistency
151
117
  const convScore = commits.length ? (stats.quality.conventional / commits.length) * 40 : 0;
@@ -165,7 +131,7 @@ function calculateMetrics(commits) {
165
131
  currentStreak = 1;
166
132
  } else {
167
133
  const diffTime = Math.abs(d - lastDate);
168
- const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
134
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
169
135
  if (diffDays === 1) {
170
136
  currentStreak++;
171
137
  } else {
@@ -176,12 +142,12 @@ function calculateMetrics(commits) {
176
142
  lastDate = d;
177
143
  });
178
144
  stats.streak.max = Math.max(maxStreak, currentStreak);
179
-
145
+
180
146
  // Check if streak is active (last commit today or yesterday)
181
147
  const today = new Date().toISOString().split('T')[0];
182
148
  const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0];
183
149
  const lastCommitDate = sortedDates[sortedDates.length - 1];
184
-
150
+
185
151
  if (lastCommitDate === today || lastCommitDate === yesterday) {
186
152
  stats.streak.current = currentStreak;
187
153
  } else {
@@ -217,10 +183,10 @@ async function insights(options) {
217
183
  console.log(`Lines Added: ${metrics.totalAdditions}`);
218
184
  console.log(`Lines Deleted: ${metrics.totalDeletions}`);
219
185
  console.log(`Current Streak: ${metrics.streak.current} days (Max: ${metrics.streak.max})`);
220
-
186
+
221
187
  // Find most productive hour
222
188
  const productiveHour = Object.entries(metrics.commitsByHour)
223
- .sort(([,a], [,b]) => b - a)[0];
189
+ .sort(([, a], [, b]) => b - a)[0];
224
190
  console.log(`Peak Productivity: ${productiveHour ? productiveHour[0] + ':00' : 'N/A'}`);
225
191
 
226
192
  console.log('');
@@ -241,12 +207,12 @@ async function insights(options) {
241
207
  const csvWriter = createObjectCsvWriter({
242
208
  path: csvPath,
243
209
  header: [
244
- {id: 'hash', title: 'Hash'},
245
- {id: 'date', title: 'Date'},
246
- {id: 'author', title: 'Author'},
247
- {id: 'message', title: 'Message'},
248
- {id: 'additions', title: 'Additions'},
249
- {id: 'deletions', title: 'Deletions'}
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' }
250
216
  ]
251
217
  });
252
218