@traisetech/autopilot 2.3.0 → 2.5.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/README.md +215 -202
  3. package/bin/autopilot.js +9 -2
  4. package/docs/CONFIGURATION.md +103 -103
  5. package/docs/DESIGN_PRINCIPLES.md +114 -114
  6. package/docs/TEAM-MODE.md +51 -51
  7. package/docs/TROUBLESHOOTING.md +21 -21
  8. package/package.json +75 -69
  9. package/src/commands/config.js +110 -110
  10. package/src/commands/dashboard.mjs +151 -151
  11. package/src/commands/doctor.js +127 -153
  12. package/src/commands/guide.js +63 -0
  13. package/src/commands/init.js +8 -9
  14. package/src/commands/insights.js +237 -237
  15. package/src/commands/leaderboard.js +116 -116
  16. package/src/commands/pause.js +18 -18
  17. package/src/commands/preset.js +121 -121
  18. package/src/commands/resume.js +17 -17
  19. package/src/commands/start.js +41 -41
  20. package/src/commands/status.js +73 -39
  21. package/src/commands/stop.js +58 -50
  22. package/src/commands/undo.js +84 -84
  23. package/src/config/defaults.js +23 -16
  24. package/src/config/ignore.js +14 -31
  25. package/src/config/loader.js +80 -80
  26. package/src/core/commit.js +45 -52
  27. package/src/core/commitMessageGenerator.js +130 -0
  28. package/src/core/configValidator.js +92 -0
  29. package/src/core/events.js +110 -110
  30. package/src/core/focus.js +2 -1
  31. package/src/core/gemini.js +15 -15
  32. package/src/core/git.js +29 -2
  33. package/src/core/history.js +69 -69
  34. package/src/core/notifier.js +61 -0
  35. package/src/core/retryQueue.js +152 -0
  36. package/src/core/safety.js +224 -210
  37. package/src/core/state.js +69 -71
  38. package/src/core/watcher.js +193 -66
  39. package/src/index.js +70 -70
  40. package/src/utils/banner.js +6 -6
  41. package/src/utils/crypto.js +18 -18
  42. package/src/utils/identity.js +41 -41
  43. package/src/utils/logger.js +86 -68
  44. package/src/utils/paths.js +62 -62
  45. package/src/utils/process.js +141 -141
@@ -1,56 +1,90 @@
1
1
  /**
2
- * Command: status
3
- * Checks the status of the Autopilot watcher
2
+ * Autopilot status command
3
+ * Built by Praise Masunga (PraiseTechzw)
4
4
  */
5
5
 
6
6
  const fs = require('fs-extra');
7
7
  const path = require('path');
8
- const process = require('process');
9
- const logger = require('../utils/logger');
10
- const { getRunningPid } = require('../utils/process');
8
+ // const { formatDistanceToNow } = require('date-fns'); // Removed unused dependency
9
+ const git = require('../core/git');
10
+ const safety = require('../core/safety');
11
11
 
12
- const status = async () => {
13
- const repoPath = process.cwd();
12
+ async function status() {
13
+ const root = process.cwd();
14
+ const statePath = path.join(root, '.autopilot-state.json');
15
+ const queuePath = path.join(root, '.autopilot-queue.json');
14
16
 
15
- try {
16
- const pid = await getRunningPid(repoPath);
17
+ console.log('\n Autopilot status');
18
+ console.log(' ─────────────────────────────');
17
19
 
18
- logger.section('Autopilot Status');
20
+ if (!fs.existsSync(statePath)) {
21
+ console.error('Status: Not Running');
22
+ console.log(' ─────────────────────────────');
23
+ return;
24
+ }
19
25
 
26
+ try {
27
+ const state = fs.readJsonSync(statePath);
28
+ const pid = state.pid;
29
+ let alive = false;
30
+
20
31
  if (pid) {
21
- logger.success(`Status: Running`);
22
- logger.info(`PID: ${pid}`);
23
- } else {
24
- logger.warn('Status: Not Running');
25
- }
26
-
27
- // Show recent logs if available
28
- const logPath = path.join(repoPath, 'autopilot.log');
29
- if (await fs.pathExists(logPath)) {
30
- logger.section('Recent Logs');
31
32
  try {
32
- const logs = await fs.readFile(logPath, 'utf-8');
33
- const lines = logs.trim().split('\n');
34
- const lastLines = lines.slice(-5); // Show last 5 lines
35
-
36
- if (lastLines.length > 0) {
37
- lastLines.forEach(line => console.log(line));
38
- } else {
39
- console.log('(Log file is empty)');
40
- }
41
-
42
- logger.info(`\nFull log: ${logPath}`);
43
- } catch (error) {
44
- logger.error(`Could not read log file: ${error.message}`);
33
+ process.kill(pid, 0);
34
+ alive = true;
35
+ } catch (e) {
36
+ alive = false;
45
37
  }
46
- } else {
47
- logger.info('No log file found.');
48
38
  }
49
39
 
50
- } catch (error) {
51
- logger.error(`Error checking status: ${error.message}`);
52
- process.exit(1);
40
+ const branch = state.branch || await git.getBranch(root) || 'unknown';
41
+ // Check if protected
42
+ // Read config to get protected branches
43
+ const configPath = path.join(root, '.autopilotrc.json');
44
+ let config = {};
45
+ if (fs.existsSync(configPath)) {
46
+ config = fs.readJsonSync(configPath);
47
+ }
48
+ const isProtected = safety.isProtectedBranch(branch, config);
49
+
50
+ const relativeTime = (ts) => {
51
+ if (!ts) return 'never';
52
+ const diff = Date.now() - ts;
53
+ const mins = Math.floor(diff / 60000);
54
+ if (mins < 1) return 'just now';
55
+ if (mins < 60) return `${mins} min ago`;
56
+ const hours = Math.floor(mins / 60);
57
+ if (hours < 24) return `${hours}h ago`;
58
+ return `${Math.floor(hours / 24)} days ago`;
59
+ };
60
+
61
+ const uptime = () => {
62
+ if (!state.startedAt) return 'unknown';
63
+ const diff = Date.now() - state.startedAt;
64
+ const hours = Math.floor(diff / 3600000);
65
+ const mins = Math.floor((diff % 3600000) / 60000);
66
+ return `${hours}h ${mins}m`;
67
+ };
68
+
69
+ let queueLength = 0;
70
+ if (fs.existsSync(queuePath)) {
71
+ const queue = fs.readJsonSync(queuePath);
72
+ queueLength = queue.length;
73
+ }
74
+
75
+ console.log(` State: ${alive ? (state.status || 'watching') : 'stopped'}`);
76
+ console.log(` Branch: ${branch}${isProtected ? ' (PROTECTED — push blocked)' : ''}`);
77
+ console.log(` Last commit: ${state.lastCommitHash ? state.lastCommitHash.substring(0, 7) : 'none'} — "${state.lastCommitMessage || 'none'}" (${relativeTime(state.lastCommitAt)})`);
78
+ console.log(` Last push: ${state.lastPushHash ? state.lastPushHash.substring(0, 7) : 'none'} — ${state.lastPushStatus || 'none'} (${relativeTime(state.lastPushAt)})`);
79
+ console.log(` Pending queue: ${queueLength} jobs waiting to push`);
80
+ console.log(` Conflicts: ${state.conflicts || 'none detected'}`);
81
+ console.log(` Watching: ${state.watchPath || root}`);
82
+ console.log(` Uptime: ${uptime()}`);
83
+ } catch (err) {
84
+ console.log(` Error: Could not read state: ${err.message}`);
53
85
  }
54
- };
86
+
87
+ console.log(' ─────────────────────────────');
88
+ }
55
89
 
56
90
  module.exports = status;
@@ -1,50 +1,58 @@
1
- /**
2
- * Command: stop
3
- * Stops the running Autopilot instance
4
- */
5
-
6
- const process = require('process');
7
- const logger = require('../utils/logger');
8
- const { getRunningPid, removePid } = require('../utils/process');
9
-
10
- const stop = async () => {
11
- const repoPath = process.cwd();
12
-
13
- try {
14
- const pid = await getRunningPid(repoPath);
15
-
16
- if (!pid) {
17
- logger.info('Autopilot is not running.');
18
- // Clean up stale PID file just in case
19
- await removePid(repoPath);
20
- return;
21
- }
22
-
23
- logger.info(`Stopping Autopilot (PID: ${pid})...`);
24
-
25
- try {
26
- // Send SIGTERM to the process
27
- process.kill(pid, 'SIGTERM');
28
-
29
- logger.success('Autopilot stopped successfully.');
30
-
31
- // Cleanup PID file
32
- await removePid(repoPath);
33
-
34
- } catch (error) {
35
- if (error.code === 'ESRCH') {
36
- logger.warn('Process not found (stale PID file). Cleaning up...');
37
- await removePid(repoPath);
38
- logger.success('Cleaned up stale lock file.');
39
- } else {
40
- logger.error(`Failed to stop process: ${error.message}`);
41
- }
42
- }
43
-
44
- } catch (error) {
45
- logger.error(`Error stopping autopilot: ${error.message}`);
46
- process.exit(1);
47
- }
48
- };
49
-
50
- module.exports = stop;
1
+ /**
2
+ * Command: stop
3
+ * Stops the running Autopilot instance
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const process = require('process');
9
+ const logger = require('../utils/logger');
10
+ const { getRunningPid, removePid } = require('../utils/process');
11
+
12
+ const stop = async () => {
13
+ const repoPath = process.cwd();
14
+
15
+ try {
16
+ const pid = await getRunningPid(repoPath);
17
+
18
+ if (!pid) {
19
+ logger.info('Autopilot is not running.');
20
+ // Clean up stale PID file just in case
21
+ await removePid(repoPath);
22
+ return;
23
+ }
24
+
25
+ logger.info(`Stopping Autopilot (PID: ${pid})...`);
26
+
27
+ try {
28
+ // Send SIGTERM to the process
29
+ process.kill(pid, 'SIGTERM');
30
+
31
+ logger.success('Autopilot stopped successfully.');
32
+
33
+ // Cleanup files
34
+ await removePid(repoPath);
35
+
36
+ const statePath = path.join(repoPath, '.autopilot-state.json');
37
+ const logPath = path.join(repoPath, '.autopilot.log');
38
+
39
+ if (fs.existsSync(statePath)) fs.unlinkSync(statePath);
40
+ if (fs.existsSync(logPath)) fs.unlinkSync(logPath);
41
+
42
+ } catch (error) {
43
+ if (error.code === 'ESRCH') {
44
+ logger.warn('Process not found (stale PID file). Cleaning up...');
45
+ await removePid(repoPath);
46
+ logger.success('Cleaned up stale lock file.');
47
+ } else {
48
+ logger.error(`Failed to stop process: ${error.message}`);
49
+ }
50
+ }
51
+
52
+ } catch (error) {
53
+ logger.error(`Error stopping autopilot: ${error.message}`);
54
+ process.exit(1);
55
+ }
56
+ };
57
+
58
+ module.exports = stop;
@@ -1,84 +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;
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;
@@ -4,29 +4,34 @@
4
4
  */
5
5
 
6
6
  const DEFAULT_CONFIG = {
7
- debounceSeconds: 20,
7
+ watchPath: '.',
8
+ debounceMs: 20000,
9
+ aiProvider: 'grok',
10
+ aiApiKey: '',
11
+ ai: {
12
+ enabled: true,
13
+ provider: 'grok',
14
+ apiKey: '',
15
+ grokApiKey: '',
16
+ interactive: true,
17
+ model: 'grok-beta'
18
+ },
19
+ protectedBranches: ['main', 'master', 'production', 'prod', 'release'],
20
+ allowPushToProtected: false,
21
+ notificationsEnabled: true,
22
+ maxRetryAttempts: 5,
23
+ ignorePaths: ['.git', 'node_modules', '.autopilot/', '.autopilot-state.json', '.autopilot.log', '.autopilot-queue.json'],
24
+
25
+ // Legacy/Internal
8
26
  minSecondsBetweenCommits: 180,
9
27
  autoPush: true,
10
- blockedBranches: ['main', 'master'],
11
28
  requireChecks: false,
12
29
  checks: [],
13
- commitMessageMode: 'ai', // Default to AI for zero-config
14
- ai: {
15
- enabled: true, // Enabled by default
16
- provider: 'grok', // Grok is the default for our system keys
17
- apiKey: '',
18
- grokApiKey: '',
19
- model: 'grok-beta',
20
- grokModel: 'grok-beta',
21
- interactive: true // Prompt user to review AI messages by default
22
- },
23
-
24
- // Phase 1: Team Mode
30
+ commitMessageMode: 'ai',
25
31
  teamMode: false,
26
32
  pullBeforePush: true,
27
33
  conflictStrategy: 'abort',
28
34
  maxUnpushedCommits: 5,
29
- // Phase 1: Pre-commit checks
30
35
  preCommitChecks: {
31
36
  secrets: true,
32
37
  fileSize: true,
@@ -44,8 +49,10 @@ const DEFAULT_IGNORE_PATTERNS = [
44
49
  '.env.*',
45
50
  'coverage/',
46
51
  '*.log',
47
- 'autopilot.log',
52
+ '.autopilot.log',
48
53
  '.autopilot.pid',
54
+ '.autopilot-state.json',
55
+ '.autopilot/',
49
56
  '.DS_Store',
50
57
  '.git/',
51
58
  '.idea/',
@@ -54,9 +54,9 @@ const createIgnoreFile = async (repoPath, patterns = []) => {
54
54
  * @returns {function} Filter function (path => boolean)
55
55
  */
56
56
  const createIgnoredFilter = (repoPath, userPatterns = []) => {
57
- const normalizedRepoPath = normalizePath(repoPath);
57
+ const normalizedRepoPath = normalizePath(repoPath).toLowerCase();
58
58
 
59
- // Critical ignores that are ALWAYS enforced
59
+ // Critical ignores that are ALWAYS enforced (lowercase for comparison)
60
60
  const criticalPrefixes = [
61
61
  '.git',
62
62
  'node_modules',
@@ -71,7 +71,7 @@ const createIgnoredFilter = (repoPath, userPatterns = []) => {
71
71
  const criticalFiles = [
72
72
  'autopilot.log',
73
73
  '.autopilot.pid',
74
- '.DS_Store'
74
+ '.ds_store'
75
75
  ];
76
76
 
77
77
  const criticalExtensions = [
@@ -79,18 +79,18 @@ const createIgnoredFilter = (repoPath, userPatterns = []) => {
79
79
  ];
80
80
 
81
81
  return (absolutePath) => {
82
- // 1. Get relative path safely using path.relative
83
- // This handles Windows casing and separators correctly
84
- const relativeRaw = path.relative(repoPath, absolutePath);
82
+ // 1. Normalize and lowercase for consistent matching
83
+ const normalizedTarget = normalizePath(absolutePath).toLowerCase();
85
84
 
86
- // If outside repo, ignore (or handle differently? Chokidar usually stays inside)
87
- if (relativeRaw.startsWith('..') || path.isAbsolute(relativeRaw)) {
88
- return false;
85
+ // 2. Determine relative path
86
+ let relativePath;
87
+ if (normalizedTarget.startsWith(normalizedRepoPath)) {
88
+ relativePath = normalizedTarget.slice(normalizedRepoPath.length).replace(/^\/+/, '');
89
+ } else {
90
+ // Fallback to path.relative if startsWith fails for some reason
91
+ relativePath = normalizePath(path.relative(repoPath, absolutePath)).toLowerCase();
89
92
  }
90
93
 
91
- // Normalize to forward slashes for matching
92
- const relativePath = normalizePath(relativeRaw);
93
-
94
94
  // Handle root path case
95
95
  if (!relativePath || relativePath === '.') return false;
96
96
 
@@ -111,30 +111,13 @@ const createIgnoredFilter = (repoPath, userPatterns = []) => {
111
111
  if (filename.endsWith(ext)) return true;
112
112
  }
113
113
 
114
- // 4. Check user patterns (simple glob-like)
115
- // TODO: Use micromatch if more complex patterns needed, but for now simple matching
116
- // Note: chokidar handles globs in its 'ignored' option if passed as array,
117
- // but here we are providing a function.
118
-
119
- // We can rely on chokidar's glob handling if we pass array, but we are returning a function.
120
- // If we want to support user globs in this function, we'd need micromatch.
121
- // However, Chokidar accepts an array of strings/globs/functions.
122
- // We should probably rely on Chokidar for user patterns if possible,
123
- // BUT the requirement says "Apply ignore rules in TWO places: a) chokidar ignored function b) internal ignore matcher".
124
- // So we must handle it here too.
125
-
126
- // Simple implementation for user patterns:
114
+ // 4. Check user patterns
127
115
  for (const pattern of userPatterns) {
128
- // Remove leading slash for matching relative path
129
- const cleanPattern = pattern.replace(/^\/+/, '');
116
+ const cleanPattern = pattern.toLowerCase().replace(/^\/+/, '');
130
117
 
131
- // Exact match
132
118
  if (relativePath === cleanPattern) return true;
133
-
134
- // Directory match
135
119
  if (relativePath.startsWith(cleanPattern + '/')) return true;
136
120
 
137
- // Extension match (*.log)
138
121
  if (cleanPattern.startsWith('*.')) {
139
122
  const ext = cleanPattern.slice(1);
140
123
  if (filename.endsWith(ext)) return true;