@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/CHANGELOG.md +56 -6
- package/README.md +202 -215
- package/bin/autopilot.js +9 -1
- package/docs/DESIGN_PRINCIPLES.md +114 -0
- package/package.json +69 -69
- package/src/commands/config.js +110 -0
- package/src/commands/dashboard.mjs +22 -15
- package/src/commands/doctor.js +14 -4
- package/src/commands/init.js +29 -7
- package/src/commands/insights.js +52 -46
- package/src/commands/leaderboard.js +49 -10
- package/src/config/defaults.js +5 -2
- package/src/config/ignore.js +10 -10
- package/src/config/loader.js +36 -10
- package/src/core/commit.js +41 -6
- package/src/core/events.js +105 -0
- package/src/core/git.js +41 -3
- package/src/core/grok.js +109 -0
- package/src/core/safety.js +6 -0
- package/src/core/watcher.js +30 -2
- package/src/index.js +8 -2
- package/src/utils/crypto.js +18 -0
- package/src/utils/identity.js +41 -0
- package/src/utils/logger.js +7 -7
- package/src/utils/paths.js +3 -0
package/package.json
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@traisetech/autopilot",
|
|
3
|
-
"version": "2.
|
|
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
|
|
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 &&
|
|
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
|
|
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
|
-
|
|
41
|
-
|
|
41
|
+
setStatus('paused');
|
|
42
|
+
setPausedState(stateManager.getState());
|
|
42
43
|
} else if (currentPid) {
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
setStatus('running');
|
|
45
|
+
setPausedState(null);
|
|
45
46
|
} else {
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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.');
|
package/src/commands/init.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
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(
|
|
161
|
-
|
|
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
|
-
|
|
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'
|
package/src/commands/insights.js
CHANGED
|
@@ -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
|
|
11
|
+
'--pretty=format:===C===%H|%an|%ad|%s|%b===E===',
|
|
14
12
|
'--date=iso',
|
|
15
13
|
'--numstat'
|
|
16
14
|
]);
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
if (!stdout) return [];
|
|
17
|
+
|
|
19
18
|
const commits = [];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (!
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|