@traisetech/autopilot 2.0.1 → 2.1.1

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/package.json CHANGED
@@ -1,69 +1,69 @@
1
- {
2
- "name": "@traisetech/autopilot",
3
- "version": "2.0.1",
4
- "publishConfig": {
5
- "access": "public"
6
- },
7
- "files": [
8
- "bin",
9
- "src",
10
- "docs",
11
- "README.md",
12
- "LICENSE",
13
- "CHANGELOG.md"
14
- ],
15
- "description": "An intelligent CLI tool that automatically commits and pushes your code so you can focus on building.",
16
- "keywords": [
17
- "git",
18
- "automation",
19
- "autocommit",
20
- "autopush",
21
- "developer-tools",
22
- "cli",
23
- "productivity",
24
- "git-workflow"
25
- ],
26
- "author": "Praise Masunga (PraiseTechzw)",
27
- "license": "MIT",
28
- "type": "commonjs",
29
- "bin": {
30
- "autopilot": "bin/autopilot.js"
31
- },
32
- "main": "src/index.js",
33
- "scripts": {
34
- "dev": "node bin/autopilot.js",
35
- "test": "node --test",
36
- "lint": "node -c bin/autopilot.js && node -c src/index.js",
37
- "verify": "node bin/autopilot.js --help && node bin/autopilot.js doctor && node --test",
38
- "prepublishOnly": "npm run verify",
39
- "release:patch": "npm run verify && npm version patch && git push --follow-tags && echo \"\nšŸš€ Release initiated! GitHub Action will publish to NPM.\"",
40
- "release:minor": "npm run verify && npm version minor && git push --follow-tags && echo \"\nšŸš€ Release initiated! GitHub Action will publish to NPM.\"",
41
- "release:major": "npm run verify && npm version major && git push --follow-tags && echo \"\nšŸš€ Release initiated! GitHub Action will publish to NPM.\""
42
- },
43
- "engines": {
44
- "node": ">=18.0.0"
45
- },
46
- "repository": {
47
- "type": "git",
48
- "url": "git+https://github.com/PraiseTechzw/autopilot-cli.git"
49
- },
50
- "bugs": {
51
- "url": "https://github.com/PraiseTechzw/autopilot-cli/issues"
52
- },
53
- "homepage": "https://github.com/PraiseTechzw/autopilot-cli#readme",
54
- "dependencies": {
55
- "@traisetech/autopilot": "^0.1.7",
56
- "chokidar": "^3.6.0",
57
- "commander": "^14.0.3",
58
- "csv-writer": "^1.6.0",
59
- "execa": "^5.1.1",
60
- "fs-extra": "^11.3.3",
61
- "ink": "^6.6.0",
62
- "ink-big-text": "^2.0.0",
63
- "ink-gradient": "^4.0.0",
64
- "ink-spinner": "^5.0.0",
65
- "open": "^11.0.0",
66
- "prop-types": "^15.8.1",
67
- "react": "^19.2.4"
68
- }
69
- }
1
+ {
2
+ "name": "@traisetech/autopilot",
3
+ "version": "2.1.1",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "files": [
8
+ "bin",
9
+ "src",
10
+ "docs",
11
+ "README.md",
12
+ "LICENSE",
13
+ "CHANGELOG.md"
14
+ ],
15
+ "description": "An intelligent Git automation CLI that safely commits and pushes your code so you can focus on building.",
16
+ "keywords": [
17
+ "git",
18
+ "automation",
19
+ "autocommit",
20
+ "autopush",
21
+ "developer-tools",
22
+ "cli",
23
+ "productivity",
24
+ "git-workflow"
25
+ ],
26
+ "author": "Praise Masunga (PraiseTechzw)",
27
+ "license": "MIT",
28
+ "type": "commonjs",
29
+ "bin": {
30
+ "autopilot": "bin/autopilot.js"
31
+ },
32
+ "main": "src/index.js",
33
+ "scripts": {
34
+ "dev": "node bin/autopilot.js",
35
+ "test": "node --test --test-concurrency=1",
36
+ "lint": "node -c bin/autopilot.js && node -c src/index.js",
37
+ "verify": "node bin/autopilot.js --help && node bin/autopilot.js doctor && npm test",
38
+ "prepublishOnly": "npm run verify",
39
+ "release:patch": "npm run verify && npm version patch && git push --follow-tags && echo \"\nšŸš€ Release initiated! GitHub Action will publish to NPM.\"",
40
+ "release:minor": "npm run verify && npm version minor && git push --follow-tags && echo \"\nšŸš€ Release initiated! GitHub Action will publish to NPM.\"",
41
+ "release:major": "npm run verify && npm version major && git push --follow-tags && echo \"\nšŸš€ Release initiated! GitHub Action will publish to NPM.\""
42
+ },
43
+ "engines": {
44
+ "node": ">=18.0.0"
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/PraiseTechzw/autopilot-cli.git"
49
+ },
50
+ "bugs": {
51
+ "url": "https://github.com/PraiseTechzw/autopilot-cli/issues"
52
+ },
53
+ "homepage": "https://github.com/PraiseTechzw/autopilot-cli#readme",
54
+ "dependencies": {
55
+ "@traisetech/autopilot": "^0.1.7",
56
+ "chokidar": "^3.6.0",
57
+ "commander": "^14.0.3",
58
+ "csv-writer": "^1.6.0",
59
+ "execa": "^5.1.1",
60
+ "fs-extra": "^11.3.3",
61
+ "ink": "^6.6.0",
62
+ "ink-big-text": "^2.0.0",
63
+ "ink-gradient": "^4.0.0",
64
+ "ink-spinner": "^5.0.0",
65
+ "open": "^11.0.0",
66
+ "prop-types": "^15.8.1",
67
+ "react": "^19.2.4"
68
+ }
69
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Config Command
3
+ * View and modify Autopilot configuration
4
+ */
5
+
6
+ const logger = require('../utils/logger');
7
+ const { loadConfig, saveConfig, getGlobalConfigPath } = require('../config/loader');
8
+ const fs = require('fs-extra');
9
+ const { getConfigPath } = require('../utils/paths');
10
+
11
+ /**
12
+ * Helper to get value by dot notation
13
+ */
14
+ const getByDot = (obj, path) => {
15
+ return path.split('.').reduce((acc, part) => acc && acc[part], obj);
16
+ };
17
+
18
+ /**
19
+ * Helper to set value by dot notation
20
+ */
21
+ const setByDot = (obj, path, value) => {
22
+ const parts = path.split('.');
23
+ const last = parts.pop();
24
+ const target = parts.reduce((acc, part) => {
25
+ if (!acc[part]) acc[part] = {};
26
+ return acc[part];
27
+ }, obj);
28
+ target[last] = value;
29
+ };
30
+
31
+ /**
32
+ * Parse value string to appropriate type
33
+ */
34
+ const parseValue = (val) => {
35
+ if (val === 'true') return true;
36
+ if (val === 'false') return false;
37
+ if (!isNaN(val) && val.trim() !== '') return Number(val);
38
+ return val;
39
+ };
40
+
41
+ async function config(cmd, key, value, options) {
42
+ const repoPath = options?.cwd || process.cwd();
43
+ const isGlobal = options?.global || false;
44
+
45
+ if (cmd === 'list') {
46
+ // If list --global, show only global config?
47
+ // Or just show effective config?
48
+ // Let's show effective config, maybe annotated if we had time.
49
+ // But for now, just loadConfig which merges them.
50
+ const currentConfig = await loadConfig(repoPath);
51
+ console.log(JSON.stringify(currentConfig, null, 2));
52
+ if (isGlobal) {
53
+ logger.info('(Note: Showing effective merged config. Use --global to set global values.)');
54
+ }
55
+ return;
56
+ }
57
+
58
+ if (cmd === 'get') {
59
+ if (!key) {
60
+ logger.error('Usage: autopilot config get <key>');
61
+ return;
62
+ }
63
+ // Get always shows effective value
64
+ const currentConfig = await loadConfig(repoPath);
65
+ const val = getByDot(currentConfig, key);
66
+ if (val === undefined) {
67
+ logger.warn(`Key '${key}' not set`);
68
+ } else {
69
+ console.log(typeof val === 'object' ? JSON.stringify(val, null, 2) : val);
70
+ }
71
+ return;
72
+ }
73
+
74
+ if (cmd === 'set') {
75
+ if (!key || value === undefined) {
76
+ logger.error('Usage: autopilot config set <key> <value>');
77
+ return;
78
+ }
79
+
80
+ const typedValue = parseValue(value);
81
+
82
+ // We need to read the specific file (local or global) to avoid merging defaults into it
83
+ let rawConfig = {};
84
+ let configPath;
85
+
86
+ if (isGlobal) {
87
+ configPath = getGlobalConfigPath();
88
+ } else {
89
+ configPath = getConfigPath(repoPath);
90
+ }
91
+
92
+ if (await fs.pathExists(configPath)) {
93
+ try {
94
+ rawConfig = await fs.readJson(configPath);
95
+ } catch (e) {
96
+ // Ignore read errors, start fresh
97
+ }
98
+ }
99
+
100
+ setByDot(rawConfig, key, typedValue);
101
+
102
+ await saveConfig(repoPath, rawConfig, isGlobal);
103
+ logger.success(`Set ${key} = ${typedValue} ${isGlobal ? '(Global)' : '(Local)'}`);
104
+ return;
105
+ }
106
+
107
+ logger.error('Unknown config command. Use list, get, or set.');
108
+ }
109
+
110
+ module.exports = config;
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect } from 'react';
1
+ import React from 'react';
2
2
  import { render, Box, Text, useInput, useApp } from 'ink';
3
3
  import Gradient from 'ink-gradient';
4
4
  import BigText from 'ink-big-text';
@@ -10,6 +10,7 @@ import git from '../core/git.js';
10
10
  import HistoryManager from '../core/history.js';
11
11
  import processUtils from '../utils/process.js';
12
12
 
13
+ const { useState, useEffect } = React;
13
14
  const { getRunningPid } = processUtils;
14
15
 
15
16
  const e = React.createElement;
@@ -18,7 +19,7 @@ const e = React.createElement;
18
19
  const Dashboard = () => {
19
20
  const { exit } = useApp();
20
21
  const root = process.cwd();
21
-
22
+
22
23
  const [status, setStatus] = useState('loading');
23
24
  const [pid, setPid] = useState(null);
24
25
  const [lastCommit, setLastCommit] = useState(null);
@@ -33,18 +34,18 @@ const Dashboard = () => {
33
34
  // 1. Check process status
34
35
  const currentPid = await getRunningPid(root);
35
36
  setPid(currentPid);
36
-
37
+
37
38
  // 2. Check Paused State
38
39
  const stateManager = new StateManager(root);
39
40
  if (stateManager.isPaused()) {
40
- setStatus('paused');
41
- setPausedState(stateManager.getState());
41
+ setStatus('paused');
42
+ setPausedState(stateManager.getState());
42
43
  } else if (currentPid) {
43
- setStatus('running');
44
- setPausedState(null);
44
+ setStatus('running');
45
+ setPausedState(null);
45
46
  } else {
46
- setStatus('stopped');
47
- setPausedState(null);
47
+ setStatus('stopped');
48
+ setPausedState(null);
48
49
  }
49
50
 
50
51
  // 3. Last Commit
@@ -55,7 +56,7 @@ const Dashboard = () => {
55
56
  // 4. Pending Files
56
57
  const statusObj = await git.getPorcelainStatus(root);
57
58
  if (statusObj.ok) {
58
- setPendingFiles(statusObj.files);
59
+ setPendingFiles(statusObj.files);
59
60
  }
60
61
 
61
62
  // 5. Today Stats (Simple count from history)
@@ -124,11 +125,13 @@ const Dashboard = () => {
124
125
  // Pending Changes
125
126
  e(Box, { flexDirection: "column", marginBottom: 1 },
126
127
  e(Text, { underline: true }, `Pending Changes (${pendingFiles.length})`),
127
- pendingFiles.length === 0 ?
128
- e(Text, { color: "gray" }, "No pending changes") :
129
- pendingFiles.slice(0, 5).map((f, i) =>
130
- e(Text, { key: i, color: "yellow" }, ` ${f.status} ${f.file}`)
131
- ),
128
+ e(Box, { flexDirection: "column" },
129
+ pendingFiles.length === 0 ?
130
+ e(Text, { color: "gray" }, "No pending changes") :
131
+ pendingFiles.slice(0, 5).map((f, idx) =>
132
+ e(Text, { key: `${f.file}-${idx}`, color: "yellow" }, ` ${f.status} ${f.file}`)
133
+ )
134
+ ),
132
135
  pendingFiles.length > 5 && e(Text, { color: "gray" }, ` ...and ${pendingFiles.length - 5} more`)
133
136
  ),
134
137
 
@@ -140,5 +143,9 @@ const Dashboard = () => {
140
143
  };
141
144
 
142
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
+ }
143
150
  render(e(Dashboard));
144
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.');
@@ -9,7 +9,8 @@ const readline = require('readline');
9
9
  const logger = require('../utils/logger');
10
10
  const { getConfigPath, getIgnorePath, getGitPath } = require('../utils/paths');
11
11
  const { DEFAULT_CONFIG, DEFAULT_IGNORE_PATTERNS } = require('../config/defaults');
12
- const { validateApiKey } = require('../core/gemini');
12
+ const gemini = require('../core/gemini');
13
+ const grok = require('../core/grok');
13
14
 
14
15
  function askQuestion(query) {
15
16
  if (!process.stdin.isTTY) {
@@ -137,17 +138,27 @@ async function initRepo() {
137
138
  const useTeamMode = teamMode.toLowerCase() === 'y';
138
139
 
139
140
  // Phase 3: AI Configuration
140
- const enableAI = await askQuestion('Enable AI commit messages (Gemini)? [y/N]: ');
141
+ const enableAI = await askQuestion('Enable AI commit messages? [y/N]: ');
141
142
  let useAI = enableAI.toLowerCase() === 'y';
142
143
 
143
144
  let apiKey = '';
145
+ let grokApiKey = '';
146
+ let provider = 'gemini';
144
147
  let interactive = false;
145
148
 
146
149
  if (useAI) {
150
+ // Select Provider
151
+ const providerAns = await askQuestion('Select AI Provider (gemini/grok) [gemini]: ');
152
+ provider = providerAns.toLowerCase() === 'grok' ? 'grok' : 'gemini';
153
+
147
154
  while (true) {
148
- apiKey = await askQuestion('Enter your Google Gemini API Key: ');
155
+ const keyPrompt = provider === 'grok'
156
+ ? 'Enter your xAI Grok API Key: '
157
+ : 'Enter your Google Gemini API Key: ';
158
+
159
+ const keyInput = await askQuestion(keyPrompt);
149
160
 
150
- if (!apiKey) {
161
+ if (!keyInput) {
151
162
  logger.warn('API Key cannot be empty if AI is enabled.');
152
163
  const retry = await askQuestion('Try again? (n to disable AI) [Y/n]: ');
153
164
  if (retry.toLowerCase() === 'n') {
@@ -157,11 +168,18 @@ async function initRepo() {
157
168
  continue;
158
169
  }
159
170
 
160
- logger.info('Verifying API Key...');
161
- const result = await validateApiKey(apiKey);
171
+ logger.info(`Verifying ${provider} API Key...`);
172
+ let result;
173
+ if (provider === 'grok') {
174
+ result = await grok.validateGrokApiKey(keyInput);
175
+ } else {
176
+ result = await gemini.validateApiKey(keyInput);
177
+ }
162
178
 
163
179
  if (result.valid) {
164
180
  logger.success('API Key verified successfully! ✨');
181
+ if (provider === 'grok') grokApiKey = keyInput;
182
+ else apiKey = keyInput;
165
183
  break;
166
184
  } else {
167
185
  logger.warn(`API Key validation failed: ${result.error}`);
@@ -173,6 +191,8 @@ async function initRepo() {
173
191
  break;
174
192
  } else if (choice === 'p') {
175
193
  logger.warn('Proceeding with potentially invalid API key.');
194
+ if (provider === 'grok') grokApiKey = keyInput;
195
+ else apiKey = keyInput;
176
196
  break;
177
197
  }
178
198
  // Default is retry (loop)
@@ -189,8 +209,10 @@ async function initRepo() {
189
209
  teamMode: useTeamMode,
190
210
  ai: {
191
211
  enabled: useAI,
212
+ provider: provider,
192
213
  apiKey: apiKey,
193
- model: 'gemini-2.5-flash',
214
+ grokApiKey: grokApiKey,
215
+ model: provider === 'grok' ? 'grok-beta' : 'gemini-2.5-flash',
194
216
  interactive: interactive
195
217
  },
196
218
  commitMessageMode: useAI ? 'ai' : 'smart'
@@ -6,48 +6,54 @@ const { createObjectCsvWriter } = require('csv-writer');
6
6
 
7
7
  async function getGitStats(repoPath) {
8
8
  try {
9
- // Get commit log with stats
10
- // Format: hash|author|date|subject|body
11
9
  const { stdout } = await git.runGit(repoPath, [
12
10
  'log',
13
- '--pretty=format:%H|%an|%ad|%s',
11
+ '--pretty=format:===C===%H|%an|%ad|%s|%b===E===',
14
12
  '--date=iso',
15
13
  '--numstat'
16
14
  ]);
17
15
 
18
- const lines = stdout.split('\n');
16
+ if (!stdout) return [];
17
+
19
18
  const commits = [];
20
- let currentCommit = null;
21
-
22
- // Parse git log output
23
- for (const line of lines) {
24
- if (!line.trim()) continue;
25
-
26
- // Check if line is a commit header (hash|author|date|subject)
27
- // Hashes are 40 chars hex.
28
- const parts = line.split('|');
29
- if (parts.length >= 4 && /^[0-9a-f]{40}$/.test(parts[0])) {
30
- if (currentCommit) commits.push(currentCommit);
31
- currentCommit = {
32
- hash: parts[0],
33
- author: parts[1],
34
- date: new Date(parts[2]),
35
- message: parts.slice(3).join('|'),
36
- files: [],
37
- additions: 0,
38
- deletions: 0
39
- };
40
- } else if (currentCommit && /^\d+\s+\d+\s+/.test(line)) {
41
- // Stat line: "10 5 src/file.js"
42
- const [add, del, file] = line.split(/\s+/);
43
- const additions = parseInt(add) || 0;
44
- const deletions = parseInt(del) || 0;
45
- currentCommit.files.push({ file, additions, deletions });
46
- currentCommit.additions += additions;
47
- currentCommit.deletions += deletions;
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
+ }
48
54
  }
55
+ commits.push(commit);
49
56
  }
50
- if (currentCommit) commits.push(currentCommit);
51
57
 
52
58
  return commits;
53
59
  } catch (error) {
@@ -90,7 +96,7 @@ function calculateMetrics(commits) {
90
96
  // Time analysis
91
97
  const dateStr = c.date.toISOString().split('T')[0];
92
98
  const hour = c.date.getHours();
93
-
99
+
94
100
  stats.commitsByDay[dateStr] = (stats.commitsByDay[dateStr] || 0) + 1;
95
101
  stats.commitsByHour[hour] = (stats.commitsByHour[hour] || 0) + 1;
96
102
  dates.add(dateStr);
@@ -105,7 +111,7 @@ function calculateMetrics(commits) {
105
111
  // Calculate Averages
106
112
  stats.totalFilesCount = stats.totalFilesChanged.size;
107
113
  stats.quality.avgLength = commits.length ? Math.round(totalMessageLength / commits.length) : 0;
108
-
114
+
109
115
  // Calculate Score (0-100)
110
116
  // 40% Conventional, 30% Message Length (>30 chars), 30% Consistency
111
117
  const convScore = commits.length ? (stats.quality.conventional / commits.length) * 40 : 0;
@@ -125,7 +131,7 @@ function calculateMetrics(commits) {
125
131
  currentStreak = 1;
126
132
  } else {
127
133
  const diffTime = Math.abs(d - lastDate);
128
- const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
134
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
129
135
  if (diffDays === 1) {
130
136
  currentStreak++;
131
137
  } else {
@@ -136,12 +142,12 @@ function calculateMetrics(commits) {
136
142
  lastDate = d;
137
143
  });
138
144
  stats.streak.max = Math.max(maxStreak, currentStreak);
139
-
145
+
140
146
  // Check if streak is active (last commit today or yesterday)
141
147
  const today = new Date().toISOString().split('T')[0];
142
148
  const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0];
143
149
  const lastCommitDate = sortedDates[sortedDates.length - 1];
144
-
150
+
145
151
  if (lastCommitDate === today || lastCommitDate === yesterday) {
146
152
  stats.streak.current = currentStreak;
147
153
  } else {
@@ -177,10 +183,10 @@ async function insights(options) {
177
183
  console.log(`Lines Added: ${metrics.totalAdditions}`);
178
184
  console.log(`Lines Deleted: ${metrics.totalDeletions}`);
179
185
  console.log(`Current Streak: ${metrics.streak.current} days (Max: ${metrics.streak.max})`);
180
-
186
+
181
187
  // Find most productive hour
182
188
  const productiveHour = Object.entries(metrics.commitsByHour)
183
- .sort(([,a], [,b]) => b - a)[0];
189
+ .sort(([, a], [, b]) => b - a)[0];
184
190
  console.log(`Peak Productivity: ${productiveHour ? productiveHour[0] + ':00' : 'N/A'}`);
185
191
 
186
192
  console.log('');
@@ -201,12 +207,12 @@ async function insights(options) {
201
207
  const csvWriter = createObjectCsvWriter({
202
208
  path: csvPath,
203
209
  header: [
204
- {id: 'hash', title: 'Hash'},
205
- {id: 'date', title: 'Date'},
206
- {id: 'author', title: 'Author'},
207
- {id: 'message', title: 'Message'},
208
- {id: 'additions', title: 'Additions'},
209
- {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' }
210
216
  ]
211
217
  });
212
218