@traisetech/autopilot 2.0.0 ā 2.1.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 +32 -5
- package/README.md +201 -214
- package/bin/autopilot.js +9 -1
- package/docs/DESIGN_PRINCIPLES.md +58 -0
- package/package.json +69 -69
- package/src/commands/config.js +110 -0
- package/src/commands/dashboard.mjs +13 -8
- package/src/commands/init.js +29 -7
- package/src/commands/insights.js +72 -32
- package/src/commands/leaderboard.js +47 -7
- 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 +38 -1
- package/src/core/grok.js +109 -0
- package/src/core/safety.js +6 -0
- package/src/core/watcher.js +30 -2
- package/src/utils/crypto.js +18 -0
- package/src/utils/identity.js +41 -0
- 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.0",
|
|
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';
|
|
@@ -8,7 +8,10 @@ import path from 'path';
|
|
|
8
8
|
import StateManager from '../core/state.js';
|
|
9
9
|
import git from '../core/git.js';
|
|
10
10
|
import HistoryManager from '../core/history.js';
|
|
11
|
-
import
|
|
11
|
+
import processUtils from '../utils/process.js';
|
|
12
|
+
|
|
13
|
+
const { useState, useEffect } = React;
|
|
14
|
+
const { getRunningPid } = processUtils;
|
|
12
15
|
|
|
13
16
|
const e = React.createElement;
|
|
14
17
|
|
|
@@ -29,7 +32,7 @@ const Dashboard = () => {
|
|
|
29
32
|
const fetchData = async () => {
|
|
30
33
|
try {
|
|
31
34
|
// 1. Check process status
|
|
32
|
-
const currentPid = await
|
|
35
|
+
const currentPid = await getRunningPid(root);
|
|
33
36
|
setPid(currentPid);
|
|
34
37
|
|
|
35
38
|
// 2. Check Paused State
|
|
@@ -122,11 +125,13 @@ const Dashboard = () => {
|
|
|
122
125
|
// Pending Changes
|
|
123
126
|
e(Box, { flexDirection: "column", marginBottom: 1 },
|
|
124
127
|
e(Text, { underline: true }, `Pending Changes (${pendingFiles.length})`),
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
e(Box, { flexDirection: "column" },
|
|
129
|
+
pendingFiles.length === 0 ?
|
|
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}`)
|
|
133
|
+
)
|
|
134
|
+
),
|
|
130
135
|
pendingFiles.length > 5 && e(Text, { color: "gray" }, ` ...and ${pendingFiles.length - 5} more`)
|
|
131
136
|
),
|
|
132
137
|
|
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
|
@@ -7,47 +7,87 @@ const { createObjectCsvWriter } = require('csv-writer');
|
|
|
7
7
|
async function getGitStats(repoPath) {
|
|
8
8
|
try {
|
|
9
9
|
// Get commit log with stats
|
|
10
|
-
//
|
|
10
|
+
// We use custom delimiters to safely parse multi-line bodies and stats
|
|
11
11
|
const { stdout } = await git.runGit(repoPath, [
|
|
12
12
|
'log',
|
|
13
|
-
'--pretty=format
|
|
13
|
+
'--pretty=format:====COMMIT====%n%H|%an|%ad|%s|%b%n====BODY_END====',
|
|
14
14
|
'--date=iso',
|
|
15
15
|
'--numstat'
|
|
16
16
|
]);
|
|
17
17
|
|
|
18
|
-
const lines = stdout.split('\n');
|
|
19
18
|
const commits = [];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
19
|
+
const rawCommits = stdout.split('====COMMIT====');
|
|
20
|
+
|
|
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
|
|
48
55
|
}
|
|
56
|
+
|
|
57
|
+
// TODO: Verify Signature (Optional but recommended for strict mode)
|
|
58
|
+
// const signature = extractTrailer(body, 'Autopilot-Signature');
|
|
59
|
+
// if (!verifySignature(signature, ...)) continue;
|
|
60
|
+
|
|
61
|
+
const commit = {
|
|
62
|
+
hash,
|
|
63
|
+
author,
|
|
64
|
+
date: new Date(dateStr),
|
|
65
|
+
message: subject + '\n' + body,
|
|
66
|
+
files: [],
|
|
67
|
+
additions: 0,
|
|
68
|
+
deletions: 0
|
|
69
|
+
};
|
|
70
|
+
|
|
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
|
+
|
|
82
|
+
commit.files.push({ file, additions, deletions });
|
|
83
|
+
commit.additions += additions;
|
|
84
|
+
commit.deletions += deletions;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
commits.push(commit);
|
|
49
90
|
}
|
|
50
|
-
if (currentCommit) commits.push(currentCommit);
|
|
51
91
|
|
|
52
92
|
return commits;
|
|
53
93
|
} catch (error) {
|
|
@@ -1,10 +1,40 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
1
3
|
const { getGitStats, calculateMetrics } = require('./insights');
|
|
2
4
|
const logger = require('../utils/logger');
|
|
3
5
|
const open = require('open');
|
|
6
|
+
const crypto = require('crypto');
|
|
4
7
|
|
|
5
8
|
// Default API URL (can be overridden by config)
|
|
6
9
|
const DEFAULT_API_URL = 'http://localhost:3000';
|
|
7
10
|
|
|
11
|
+
async function calculateFocusTime(repoPath) {
|
|
12
|
+
const logPath = path.join(repoPath, 'autopilot.log');
|
|
13
|
+
if (!await fs.pathExists(logPath)) return 0;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const content = await fs.readFile(logPath, 'utf8');
|
|
17
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
18
|
+
let totalMs = 0;
|
|
19
|
+
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
try {
|
|
22
|
+
const entry = JSON.parse(line);
|
|
23
|
+
if (entry.type === 'FOCUS_SESSION_END' && entry.totalActiveMs) {
|
|
24
|
+
totalMs += entry.totalActiveMs;
|
|
25
|
+
}
|
|
26
|
+
} catch (e) {
|
|
27
|
+
// ignore bad lines
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return Math.round(totalMs / 60000); // minutes
|
|
32
|
+
} catch (error) {
|
|
33
|
+
logger.warn(`Failed to parse autopilot.log: ${error.message}`);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
8
38
|
async function leaderboard(options) {
|
|
9
39
|
const apiUrl = process.env.AUTOPILOT_API_URL || DEFAULT_API_URL;
|
|
10
40
|
|
|
@@ -12,6 +42,7 @@ async function leaderboard(options) {
|
|
|
12
42
|
await syncLeaderboard(apiUrl, options);
|
|
13
43
|
} else {
|
|
14
44
|
logger.info(`Opening leaderboard at ${apiUrl}/leaderboard...`);
|
|
45
|
+
const { default: open } = await import('open');
|
|
15
46
|
await open(`${apiUrl}/leaderboard`);
|
|
16
47
|
}
|
|
17
48
|
}
|
|
@@ -34,19 +65,28 @@ async function syncLeaderboard(apiUrl, options) {
|
|
|
34
65
|
const { stdout: username } = await git.runGit(repoPath, ['config', 'user.name']);
|
|
35
66
|
const { stdout: email } = await git.runGit(repoPath, ['config', 'user.email']);
|
|
36
67
|
|
|
68
|
+
const userEmail = email.trim() || 'unknown';
|
|
69
|
+
const userName = username.trim() || 'Anonymous';
|
|
70
|
+
|
|
71
|
+
// Anonymize ID using hash
|
|
72
|
+
const userId = crypto.createHash('sha256').update(userEmail).digest('hex').substring(0, 12);
|
|
73
|
+
|
|
74
|
+
// Get focus time from logs (or fallback to git stats proxy)
|
|
75
|
+
const logFocusMinutes = await calculateFocusTime(repoPath);
|
|
76
|
+
const gitFocusMinutes = Math.round(metrics.totalAdditions / 10);
|
|
77
|
+
const focusMinutes = logFocusMinutes > 0 ? logFocusMinutes : gitFocusMinutes;
|
|
78
|
+
|
|
37
79
|
const stats = {
|
|
38
|
-
id:
|
|
39
|
-
username:
|
|
80
|
+
id: userId,
|
|
81
|
+
username: userName, // Display name (can be public)
|
|
40
82
|
score: metrics.quality.score * 100 + metrics.totalCommits * 10, // Example scoring
|
|
41
83
|
commits: metrics.totalCommits,
|
|
42
|
-
focusMinutes:
|
|
84
|
+
focusMinutes: focusMinutes,
|
|
43
85
|
streak: metrics.streak.current
|
|
44
86
|
};
|
|
45
87
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
logger.info(`Syncing stats for ${stats.username}...`);
|
|
88
|
+
logger.info(`Syncing stats for ${stats.username} (ID: ${userId})...`);
|
|
89
|
+
logger.info('Note: Only metrics are shared. No code or file contents are transmitted.');
|
|
50
90
|
|
|
51
91
|
const response = await fetch(`${apiUrl}/api/leaderboard/sync`, {
|
|
52
92
|
method: 'POST',
|
package/src/config/defaults.js
CHANGED
|
@@ -13,8 +13,11 @@ const DEFAULT_CONFIG = {
|
|
|
13
13
|
commitMessageMode: 'smart', // smart | simple | ai
|
|
14
14
|
ai: {
|
|
15
15
|
enabled: false,
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
provider: 'gemini', // gemini | grok
|
|
17
|
+
apiKey: '',
|
|
18
|
+
grokApiKey: '',
|
|
19
|
+
model: 'gemini-2.5-flash', // default for gemini
|
|
20
|
+
grokModel: 'grok-beta', // default for grok
|
|
18
21
|
interactive: true
|
|
19
22
|
},
|
|
20
23
|
// Phase 1: Team Mode
|
package/src/config/ignore.js
CHANGED
|
@@ -79,20 +79,20 @@ const createIgnoredFilter = (repoPath, userPatterns = []) => {
|
|
|
79
79
|
];
|
|
80
80
|
|
|
81
81
|
return (absolutePath) => {
|
|
82
|
-
// 1.
|
|
83
|
-
|
|
82
|
+
// 1. Get relative path safely using path.relative
|
|
83
|
+
// This handles Windows casing and separators correctly
|
|
84
|
+
const relativeRaw = path.relative(repoPath, absolutePath);
|
|
84
85
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
relativePath = normalizedAbs.slice(normalizedRepoPath.length);
|
|
89
|
-
if (relativePath.startsWith('/')) {
|
|
90
|
-
relativePath = relativePath.slice(1);
|
|
91
|
-
}
|
|
86
|
+
// If outside repo, ignore (or handle differently? Chokidar usually stays inside)
|
|
87
|
+
if (relativeRaw.startsWith('..') || path.isAbsolute(relativeRaw)) {
|
|
88
|
+
return false;
|
|
92
89
|
}
|
|
93
90
|
|
|
91
|
+
// Normalize to forward slashes for matching
|
|
92
|
+
const relativePath = normalizePath(relativeRaw);
|
|
93
|
+
|
|
94
94
|
// Handle root path case
|
|
95
|
-
if (!relativePath) return false;
|
|
95
|
+
if (!relativePath || relativePath === '.') return false;
|
|
96
96
|
|
|
97
97
|
// 3. Check critical matches
|
|
98
98
|
const parts = relativePath.split('/');
|