@traisetech/autopilot 0.1.8 → 2.0.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 +29 -0
- package/README.md +72 -90
- package/bin/autopilot.js +59 -1
- package/docs/CONFIGURATION.md +100 -82
- package/docs/TEAM-MODE.md +51 -0
- package/package.json +14 -4
- package/src/commands/dashboard.mjs +142 -0
- package/src/commands/init.js +57 -26
- package/src/commands/insights.js +231 -90
- package/src/commands/leaderboard.js +70 -0
- package/src/commands/pause.js +18 -0
- package/src/commands/preset.js +121 -0
- package/src/commands/resume.js +17 -0
- package/src/commands/undo.js +84 -0
- package/src/config/defaults.js +15 -3
- package/src/core/commit.js +31 -6
- package/src/core/gemini.js +20 -8
- package/src/core/git.js +80 -1
- package/src/core/history.js +69 -0
- package/src/core/safety.js +204 -38
- package/src/core/state.js +71 -0
- package/src/core/watcher.js +54 -11
- package/src/index.js +34 -0
- package/src/utils/logger.js +9 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Presets Command
|
|
3
|
+
* Built by Praise Masunga (PraiseTechzw)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs-extra');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const logger = require('../utils/logger');
|
|
9
|
+
const { getConfigPath } = require('../utils/paths');
|
|
10
|
+
const { DEFAULT_CONFIG } = require('../config/defaults');
|
|
11
|
+
|
|
12
|
+
const PRESETS = {
|
|
13
|
+
'safe-team': {
|
|
14
|
+
description: 'Safe configuration for team collaboration',
|
|
15
|
+
config: {
|
|
16
|
+
teamMode: true,
|
|
17
|
+
pullBeforePush: true,
|
|
18
|
+
conflictStrategy: 'abort',
|
|
19
|
+
preventSecrets: true,
|
|
20
|
+
commitMessageMode: 'smart',
|
|
21
|
+
debounceSeconds: 30,
|
|
22
|
+
minSecondsBetweenCommits: 300
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
'solo-speed': {
|
|
26
|
+
description: 'Fast-paced configuration for solo developers',
|
|
27
|
+
config: {
|
|
28
|
+
teamMode: false,
|
|
29
|
+
pullBeforePush: false,
|
|
30
|
+
commitMessageMode: 'simple',
|
|
31
|
+
debounceSeconds: 5,
|
|
32
|
+
minSecondsBetweenCommits: 60,
|
|
33
|
+
autoPush: true
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
'strict-ci': {
|
|
37
|
+
description: 'Strict configuration ensuring quality checks pass',
|
|
38
|
+
config: {
|
|
39
|
+
requireChecks: true,
|
|
40
|
+
checks: ['npm test', 'npm run lint'],
|
|
41
|
+
preventSecrets: true,
|
|
42
|
+
preCommitChecks: {
|
|
43
|
+
secrets: true,
|
|
44
|
+
fileSize: true,
|
|
45
|
+
lint: true,
|
|
46
|
+
test: true
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
async function listPresets() {
|
|
53
|
+
logger.section('📋 Available Workflow Presets');
|
|
54
|
+
|
|
55
|
+
Object.entries(PRESETS).forEach(([name, preset]) => {
|
|
56
|
+
console.log(`\n ${logger.colors.cyan(name)}`);
|
|
57
|
+
console.log(` ${preset.description}`);
|
|
58
|
+
});
|
|
59
|
+
console.log('');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function applyPreset(name) {
|
|
63
|
+
if (!name) {
|
|
64
|
+
logger.error('Please specify a preset name.');
|
|
65
|
+
await listPresets();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const preset = PRESETS[name];
|
|
70
|
+
if (!preset) {
|
|
71
|
+
logger.error(`Preset '${name}' not found.`);
|
|
72
|
+
await listPresets();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const repoPath = process.cwd();
|
|
77
|
+
const configPath = getConfigPath(repoPath);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Read existing config or use defaults
|
|
81
|
+
let currentConfig = DEFAULT_CONFIG;
|
|
82
|
+
if (await fs.pathExists(configPath)) {
|
|
83
|
+
currentConfig = await fs.readJson(configPath);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Merge preset config
|
|
87
|
+
const newConfig = {
|
|
88
|
+
...currentConfig,
|
|
89
|
+
...preset.config
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
await fs.writeJson(configPath, newConfig, { spaces: 2 });
|
|
93
|
+
logger.success(`Applied preset '${name}' successfully!`);
|
|
94
|
+
logger.info(`Updated .autopilotrc.json with ${Object.keys(preset.config).length} settings.`);
|
|
95
|
+
|
|
96
|
+
} catch (error) {
|
|
97
|
+
logger.error(`Failed to apply preset: ${error.message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function presetCommand(command, name) {
|
|
102
|
+
switch (command) {
|
|
103
|
+
case 'list':
|
|
104
|
+
await listPresets();
|
|
105
|
+
break;
|
|
106
|
+
case 'apply':
|
|
107
|
+
await applyPreset(name);
|
|
108
|
+
break;
|
|
109
|
+
default:
|
|
110
|
+
// If first arg is a known preset name, treat it as apply
|
|
111
|
+
if (PRESETS[command]) {
|
|
112
|
+
await applyPreset(command);
|
|
113
|
+
} else {
|
|
114
|
+
logger.error(`Unknown command or preset: ${command}`);
|
|
115
|
+
console.log('Usage: autopilot preset [list|apply] <name>');
|
|
116
|
+
console.log(' autopilot preset <name>');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = presetCommand;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const logger = require('../utils/logger');
|
|
2
|
+
const StateManager = require('../core/state');
|
|
3
|
+
|
|
4
|
+
function resumeCommand() {
|
|
5
|
+
const root = process.cwd();
|
|
6
|
+
const stateManager = new StateManager(root);
|
|
7
|
+
|
|
8
|
+
if (!stateManager.isPaused()) {
|
|
9
|
+
logger.warn('Autopilot is already running.');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
stateManager.resume();
|
|
14
|
+
logger.success('Autopilot resumed.');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = resumeCommand;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const logger = require('../utils/logger');
|
|
2
|
+
const HistoryManager = require('../core/history');
|
|
3
|
+
const git = require('../core/git');
|
|
4
|
+
|
|
5
|
+
async function undoCommand(options) {
|
|
6
|
+
const root = process.cwd();
|
|
7
|
+
const historyManager = new HistoryManager(root);
|
|
8
|
+
const count = options.count ? parseInt(options.count, 10) : 1;
|
|
9
|
+
|
|
10
|
+
if (isNaN(count) || count < 1) {
|
|
11
|
+
logger.error('Invalid count. Must be a positive integer.');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
logger.info(`Attempting to undo last ${count > 1 ? count + ' commits' : 'commit'}...`);
|
|
16
|
+
|
|
17
|
+
// Check for dirty working directory
|
|
18
|
+
const hasChanges = await git.hasChanges(root);
|
|
19
|
+
if (hasChanges) {
|
|
20
|
+
logger.warn('Working directory is dirty. Undo might be unsafe.');
|
|
21
|
+
// In a real interactive CLI, we might ask for confirmation here.
|
|
22
|
+
// For now, we proceed with caution or abort if strictly required.
|
|
23
|
+
// We'll assume soft reset is safe for unpushed, revert for pushed.
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let undoneCount = 0;
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < count; i++) {
|
|
29
|
+
const lastCommit = historyManager.getLastCommit();
|
|
30
|
+
|
|
31
|
+
if (!lastCommit) {
|
|
32
|
+
logger.warn('No more Autopilot commits found in history.');
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Verify commit exists in git
|
|
37
|
+
const exists = await git.commitExists(root, lastCommit.hash);
|
|
38
|
+
if (!exists) {
|
|
39
|
+
logger.warn(`Commit ${lastCommit.hash} not found in git history. Removing from Autopilot history.`);
|
|
40
|
+
historyManager.removeLastCommit();
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check if pushed (simplified: if remote branch contains it)
|
|
45
|
+
// For simplicity in this phase, we'll try soft reset first if it's HEAD
|
|
46
|
+
// If it's not HEAD (e.g. manual commits happened on top), we must revert.
|
|
47
|
+
|
|
48
|
+
const headHash = await git.getLatestCommitHash(root);
|
|
49
|
+
|
|
50
|
+
if (headHash === lastCommit.hash) {
|
|
51
|
+
// It's the latest commit, we can try soft reset
|
|
52
|
+
logger.info(`Resetting (soft) ${lastCommit.hash} (${lastCommit.message})...`);
|
|
53
|
+
const result = await git.resetSoft(root, 'HEAD~1');
|
|
54
|
+
if (result.ok) {
|
|
55
|
+
historyManager.removeLastCommit();
|
|
56
|
+
undoneCount++;
|
|
57
|
+
logger.success(`Undid ${lastCommit.hash}`);
|
|
58
|
+
} else {
|
|
59
|
+
logger.error(`Failed to reset ${lastCommit.hash}: ${result.stderr}`);
|
|
60
|
+
break; // Stop on error
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
// It's buried, or pushed (safer to revert in mixed scenarios)
|
|
64
|
+
logger.info(`Reverting ${lastCommit.hash} (${lastCommit.message})...`);
|
|
65
|
+
const result = await git.revert(root, lastCommit.hash);
|
|
66
|
+
if (result.ok) {
|
|
67
|
+
historyManager.removeLastCommit();
|
|
68
|
+
undoneCount++;
|
|
69
|
+
logger.success(`Reverted ${lastCommit.hash}`);
|
|
70
|
+
} else {
|
|
71
|
+
logger.error(`Failed to revert ${lastCommit.hash}: ${result.stderr}`);
|
|
72
|
+
break; // Stop on error
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (undoneCount > 0) {
|
|
78
|
+
logger.success(`Successfully undid ${undoneCount} commit(s).`);
|
|
79
|
+
} else {
|
|
80
|
+
logger.info('No commits were undone.');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = undoCommand;
|
package/src/config/defaults.js
CHANGED
|
@@ -13,9 +13,21 @@ const DEFAULT_CONFIG = {
|
|
|
13
13
|
commitMessageMode: 'smart', // smart | simple | ai
|
|
14
14
|
ai: {
|
|
15
15
|
enabled: false,
|
|
16
|
-
apiKey: '',
|
|
17
|
-
model: 'gemini-
|
|
18
|
-
interactive:
|
|
16
|
+
apiKey: '', // Store in env var or secure config
|
|
17
|
+
model: 'gemini-2.5-flash',
|
|
18
|
+
interactive: true
|
|
19
|
+
},
|
|
20
|
+
// Phase 1: Team Mode
|
|
21
|
+
teamMode: false,
|
|
22
|
+
pullBeforePush: true,
|
|
23
|
+
conflictStrategy: 'abort',
|
|
24
|
+
maxUnpushedCommits: 5,
|
|
25
|
+
// Phase 1: Pre-commit checks
|
|
26
|
+
preCommitChecks: {
|
|
27
|
+
secrets: true,
|
|
28
|
+
fileSize: true,
|
|
29
|
+
lint: false,
|
|
30
|
+
test: null
|
|
19
31
|
}
|
|
20
32
|
};
|
|
21
33
|
|
package/src/core/commit.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const logger = require('../utils/logger');
|
|
8
8
|
const { generateAICommitMessage } = require('./gemini');
|
|
9
|
+
const HistoryManager = require('./history');
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Generate a conventional commit message based on diff analysis
|
|
@@ -15,20 +16,44 @@ const { generateAICommitMessage } = require('./gemini');
|
|
|
15
16
|
* @returns {Promise<string>} Conventional commit message
|
|
16
17
|
*/
|
|
17
18
|
async function generateCommitMessage(files, diffContent, config = {}) {
|
|
19
|
+
let message = '';
|
|
18
20
|
if (!files || files.length === 0) {
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// AI Mode
|
|
23
|
-
if (config.commitMessageMode === 'ai' && config.ai?.enabled && config.ai?.apiKey) {
|
|
21
|
+
message = 'chore: update changes';
|
|
22
|
+
} else if (config.commitMessageMode === 'ai' && config.ai?.enabled && config.ai?.apiKey) {
|
|
23
|
+
// AI Mode
|
|
24
24
|
try {
|
|
25
25
|
logger.info('Generating AI commit message...');
|
|
26
|
-
|
|
26
|
+
message = await generateAICommitMessage(diffContent, config.ai.apiKey);
|
|
27
27
|
} catch (error) {
|
|
28
28
|
logger.warn('AI generation failed, falling back to smart generation.');
|
|
29
|
+
message = generateSmartCommitMessage(files, diffContent);
|
|
29
30
|
}
|
|
31
|
+
} else {
|
|
32
|
+
// Smart Mode (Default)
|
|
33
|
+
message = generateSmartCommitMessage(files, diffContent);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Prepend [autopilot] tag for traceability (Phase 1 req)
|
|
37
|
+
const finalMessage = `[autopilot] ${message}`;
|
|
38
|
+
|
|
39
|
+
// Record history
|
|
40
|
+
try {
|
|
41
|
+
const root = process.cwd();
|
|
42
|
+
const historyManager = new HistoryManager(root);
|
|
43
|
+
// We don't have the hash yet, but we will update it or store it after commit
|
|
44
|
+
// Actually, we should probably return just the message here and let the caller (watcher) handle history.
|
|
45
|
+
// However, the prompt says "Tag all commits with [autopilot] prefix".
|
|
46
|
+
} catch (err) {
|
|
47
|
+
// ignore history errors here
|
|
30
48
|
}
|
|
31
49
|
|
|
50
|
+
return finalMessage;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Smart generation logic extracted
|
|
55
|
+
*/
|
|
56
|
+
function generateSmartCommitMessage(files, diffContent) {
|
|
32
57
|
// 1. Parse Diff for deep analysis
|
|
33
58
|
const diffAnalysis = parseDiff(diffContent);
|
|
34
59
|
|
package/src/core/gemini.js
CHANGED
|
@@ -5,15 +5,17 @@
|
|
|
5
5
|
|
|
6
6
|
const logger = require('../utils/logger');
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const BASE_API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/';
|
|
9
|
+
const DEFAULT_MODEL = 'gemini-2.5-flash';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Generate a commit message using Gemini API
|
|
12
13
|
* @param {string} diff - The git diff content
|
|
13
14
|
* @param {string} apiKey - Google Gemini API Key
|
|
15
|
+
* @param {string} [model] - Gemini Model ID (default: gemini-2.5-flash)
|
|
14
16
|
* @returns {Promise<string>} Generated commit message
|
|
15
17
|
*/
|
|
16
|
-
async function generateAICommitMessage(diff, apiKey) {
|
|
18
|
+
async function generateAICommitMessage(diff, apiKey, model = DEFAULT_MODEL) {
|
|
17
19
|
if (!diff || !diff.trim()) {
|
|
18
20
|
return 'chore: update changes';
|
|
19
21
|
}
|
|
@@ -39,7 +41,8 @@ ${truncatedDiff}
|
|
|
39
41
|
`;
|
|
40
42
|
|
|
41
43
|
try {
|
|
42
|
-
const
|
|
44
|
+
const url = `${BASE_API_URL}${model}:generateContent?key=${apiKey}`;
|
|
45
|
+
const response = await fetch(url, {
|
|
43
46
|
method: 'POST',
|
|
44
47
|
headers: {
|
|
45
48
|
'Content-Type': 'application/json',
|
|
@@ -84,12 +87,14 @@ ${truncatedDiff}
|
|
|
84
87
|
/**
|
|
85
88
|
* Validate Gemini API Key
|
|
86
89
|
* @param {string} apiKey
|
|
87
|
-
* @
|
|
90
|
+
* @param {string} [model] - Gemini Model ID (default: gemini-2.5-flash)
|
|
91
|
+
* @returns {Promise<{valid: boolean, error?: string}>}
|
|
88
92
|
*/
|
|
89
|
-
async function validateApiKey(apiKey) {
|
|
93
|
+
async function validateApiKey(apiKey, model = DEFAULT_MODEL) {
|
|
90
94
|
try {
|
|
95
|
+
const url = `${BASE_API_URL}${model}:generateContent?key=${apiKey}`;
|
|
91
96
|
// Simple test call with empty prompt
|
|
92
|
-
const response = await fetch(
|
|
97
|
+
const response = await fetch(url, {
|
|
93
98
|
method: 'POST',
|
|
94
99
|
headers: { 'Content-Type': 'application/json' },
|
|
95
100
|
body: JSON.stringify({
|
|
@@ -97,9 +102,16 @@ async function validateApiKey(apiKey) {
|
|
|
97
102
|
generationConfig: { maxOutputTokens: 1 }
|
|
98
103
|
})
|
|
99
104
|
});
|
|
100
|
-
|
|
105
|
+
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
const errorData = await response.json().catch(() => ({}));
|
|
108
|
+
const errorMessage = errorData.error?.message || response.statusText;
|
|
109
|
+
return { valid: false, error: errorMessage };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { valid: true };
|
|
101
113
|
} catch (e) {
|
|
102
|
-
return false;
|
|
114
|
+
return { valid: false, error: e.message };
|
|
103
115
|
}
|
|
104
116
|
}
|
|
105
117
|
|
package/src/core/git.js
CHANGED
|
@@ -104,6 +104,21 @@ async function fetch(root) {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Run a generic git command
|
|
109
|
+
* @param {string} root - Repository root path
|
|
110
|
+
* @param {string[]} args - Git arguments
|
|
111
|
+
* @returns {Promise<{ok: boolean, stdout: string, stderr: string}>} Result object
|
|
112
|
+
*/
|
|
113
|
+
async function runGit(root, args) {
|
|
114
|
+
try {
|
|
115
|
+
const { stdout, stderr } = await execa('git', args, { cwd: root });
|
|
116
|
+
return { ok: true, stdout, stderr };
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return { ok: false, stdout: '', stderr: error.message };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
107
122
|
/**
|
|
108
123
|
* Check if remote is ahead/behind
|
|
109
124
|
* @param {string} root - Repository root path
|
|
@@ -167,6 +182,65 @@ async function getDiff(root, staged = true) {
|
|
|
167
182
|
}
|
|
168
183
|
}
|
|
169
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Revert a specific commit
|
|
187
|
+
* @param {string} root - Repository root path
|
|
188
|
+
* @param {string} hash - Commit hash
|
|
189
|
+
* @returns {Promise<{ok: boolean, stdout: string, stderr: string}>} Result object
|
|
190
|
+
*/
|
|
191
|
+
async function revert(root, hash) {
|
|
192
|
+
try {
|
|
193
|
+
const { stdout, stderr } = await execa('git', ['revert', '--no-edit', hash], { cwd: root });
|
|
194
|
+
return { ok: true, stdout, stderr };
|
|
195
|
+
} catch (error) {
|
|
196
|
+
return { ok: false, stdout: '', stderr: error.message };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Reset to a specific commit (soft reset)
|
|
202
|
+
* @param {string} root - Repository root path
|
|
203
|
+
* @param {string} target - Target commitish (e.g. HEAD~1)
|
|
204
|
+
* @returns {Promise<{ok: boolean, stdout: string, stderr: string}>} Result object
|
|
205
|
+
*/
|
|
206
|
+
async function resetSoft(root, target) {
|
|
207
|
+
try {
|
|
208
|
+
const { stdout, stderr } = await execa('git', ['reset', '--soft', target], { cwd: root });
|
|
209
|
+
return { ok: true, stdout, stderr };
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return { ok: false, stdout: '', stderr: error.message };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check if a commit exists
|
|
217
|
+
* @param {string} root - Repository root path
|
|
218
|
+
* @param {string} hash - Commit hash
|
|
219
|
+
* @returns {Promise<boolean>} True if exists
|
|
220
|
+
*/
|
|
221
|
+
async function commitExists(root, hash) {
|
|
222
|
+
try {
|
|
223
|
+
await execa('git', ['cat-file', '-e', `${hash}^{commit}`], { cwd: root });
|
|
224
|
+
return true;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get the latest commit hash
|
|
232
|
+
* @param {string} root - Repository root path
|
|
233
|
+
* @returns {Promise<string|null>} Commit hash or null
|
|
234
|
+
*/
|
|
235
|
+
async function getLatestCommitHash(root) {
|
|
236
|
+
try {
|
|
237
|
+
const { stdout } = await execa('git', ['rev-parse', 'HEAD'], { cwd: root });
|
|
238
|
+
return stdout.trim();
|
|
239
|
+
} catch (error) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
170
244
|
module.exports = {
|
|
171
245
|
getBranch,
|
|
172
246
|
hasChanges,
|
|
@@ -174,7 +248,12 @@ module.exports = {
|
|
|
174
248
|
addAll,
|
|
175
249
|
commit,
|
|
176
250
|
fetch,
|
|
251
|
+
runGit,
|
|
177
252
|
isRemoteAhead,
|
|
178
253
|
push,
|
|
179
|
-
getDiff
|
|
254
|
+
getDiff,
|
|
255
|
+
revert,
|
|
256
|
+
resetSoft,
|
|
257
|
+
commitExists,
|
|
258
|
+
getLatestCommitHash
|
|
180
259
|
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { getAutopilotHome } = require('../utils/paths');
|
|
4
|
+
const logger = require('../utils/logger');
|
|
5
|
+
|
|
6
|
+
const HISTORY_FILE = 'history.json';
|
|
7
|
+
|
|
8
|
+
class HistoryManager {
|
|
9
|
+
constructor(repoPath) {
|
|
10
|
+
this.repoPath = repoPath;
|
|
11
|
+
this.historyDir = path.join(repoPath, '.autopilot');
|
|
12
|
+
this.historyFile = path.join(this.historyDir, HISTORY_FILE);
|
|
13
|
+
this.init();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
init() {
|
|
17
|
+
fs.ensureDirSync(this.historyDir);
|
|
18
|
+
if (!fs.existsSync(this.historyFile)) {
|
|
19
|
+
fs.writeJsonSync(this.historyFile, { commits: [] }, { spaces: 2 });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getHistory() {
|
|
24
|
+
try {
|
|
25
|
+
const data = fs.readJsonSync(this.historyFile);
|
|
26
|
+
return data.commits || [];
|
|
27
|
+
} catch (error) {
|
|
28
|
+
logger.error('Failed to read history:', error.message);
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
addCommit(commitData) {
|
|
34
|
+
try {
|
|
35
|
+
const history = this.getHistory();
|
|
36
|
+
history.unshift({
|
|
37
|
+
...commitData,
|
|
38
|
+
timestamp: new Date().toISOString()
|
|
39
|
+
});
|
|
40
|
+
// Keep only last 100 commits
|
|
41
|
+
if (history.length > 100) history.length = 100;
|
|
42
|
+
|
|
43
|
+
fs.writeJsonSync(this.historyFile, { commits: history }, { spaces: 2 });
|
|
44
|
+
} catch (error) {
|
|
45
|
+
logger.error('Failed to write history:', error.message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getLastCommit() {
|
|
50
|
+
const history = this.getHistory();
|
|
51
|
+
return history[0];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
removeLastCommit() {
|
|
55
|
+
try {
|
|
56
|
+
const history = this.getHistory();
|
|
57
|
+
if (history.length === 0) return null;
|
|
58
|
+
|
|
59
|
+
const removed = history.shift();
|
|
60
|
+
fs.writeJsonSync(this.historyFile, { commits: history }, { spaces: 2 });
|
|
61
|
+
return removed;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
logger.error('Failed to update history:', error.message);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = HistoryManager;
|