@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.
@@ -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;
@@ -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-pro',
18
- interactive: false
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
 
@@ -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
- return 'chore: update changes';
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
- return await generateAICommitMessage(diffContent, config.ai.apiKey);
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
 
@@ -5,15 +5,17 @@
5
5
 
6
6
  const logger = require('../utils/logger');
7
7
 
8
- const GEMINI_API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent';
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 response = await fetch(`${GEMINI_API_URL}?key=${apiKey}`, {
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
- * @returns {Promise<boolean>}
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(`${GEMINI_API_URL}?key=${apiKey}`, {
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
- return response.ok;
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;