@traisetech/autopilot 2.4.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 (44) hide show
  1. package/CHANGELOG.md +25 -9
  2. package/README.md +215 -106
  3. package/bin/autopilot.js +1 -1
  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/init.js +8 -9
  13. package/src/commands/insights.js +237 -237
  14. package/src/commands/leaderboard.js +116 -116
  15. package/src/commands/pause.js +18 -18
  16. package/src/commands/preset.js +121 -121
  17. package/src/commands/resume.js +17 -17
  18. package/src/commands/start.js +41 -41
  19. package/src/commands/status.js +73 -39
  20. package/src/commands/stop.js +58 -50
  21. package/src/commands/undo.js +84 -84
  22. package/src/config/defaults.js +23 -16
  23. package/src/config/ignore.js +14 -31
  24. package/src/config/loader.js +80 -80
  25. package/src/core/commit.js +45 -52
  26. package/src/core/commitMessageGenerator.js +130 -0
  27. package/src/core/configValidator.js +92 -0
  28. package/src/core/events.js +110 -110
  29. package/src/core/focus.js +2 -1
  30. package/src/core/gemini.js +15 -15
  31. package/src/core/git.js +29 -2
  32. package/src/core/history.js +69 -69
  33. package/src/core/notifier.js +61 -0
  34. package/src/core/retryQueue.js +152 -0
  35. package/src/core/safety.js +224 -210
  36. package/src/core/state.js +69 -71
  37. package/src/core/watcher.js +193 -66
  38. package/src/index.js +70 -70
  39. package/src/utils/banner.js +6 -6
  40. package/src/utils/crypto.js +18 -18
  41. package/src/utils/identity.js +41 -41
  42. package/src/utils/logger.js +86 -68
  43. package/src/utils/paths.js +62 -62
  44. package/src/utils/process.js +141 -141
@@ -1,116 +1,116 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
- const { getGitStats, calculateMetrics } = require('./insights');
4
- const logger = require('../utils/logger');
5
- const crypto = require('crypto');
6
-
7
- // Default API URL (can be overridden by env)
8
- const DEFAULT_API_URL = 'https://autopilot-cli.vercel.app';
9
-
10
- async function calculateFocusTime(repoPath) {
11
- const logPath = path.join(repoPath, 'autopilot.log');
12
- if (!await fs.pathExists(logPath)) return 0;
13
-
14
- try {
15
- const content = await fs.readFile(logPath, 'utf8');
16
- const lines = content.split('\n').filter(l => l.trim());
17
- let totalMs = 0;
18
-
19
- for (const line of lines) {
20
- try {
21
- const entry = JSON.parse(line);
22
- if (entry.type === 'FOCUS_SESSION_END' && entry.totalActiveMs) {
23
- totalMs += entry.totalActiveMs;
24
- }
25
- } catch (e) {
26
- // ignore bad lines
27
- }
28
- }
29
-
30
- return Math.round(totalMs / 60000); // minutes
31
- } catch (error) {
32
- logger.warn(`Failed to parse autopilot.log: ${error.message}`);
33
- return 0;
34
- }
35
- }
36
-
37
- async function leaderboard(options) {
38
- const apiUrl = process.env.AUTOPILOT_API_URL || DEFAULT_API_URL;
39
-
40
- if (options.sync) {
41
- await syncLeaderboard(apiUrl, options);
42
- } else {
43
- logger.info(`Opening leaderboard at ${apiUrl}/leaderboard...`);
44
- const { default: open } = await import('open');
45
- await open(`${apiUrl}/leaderboard`);
46
- }
47
- }
48
-
49
- async function syncLeaderboard(apiUrl, options) {
50
- try {
51
- const repoPath = options.cwd || process.cwd();
52
- logger.info('Calculating stats for leaderboard sync...');
53
-
54
- const commits = await getGitStats(repoPath);
55
- if (commits.length === 0) {
56
- logger.warn('No git history found. Cannot sync stats.');
57
- return;
58
- }
59
-
60
- const metrics = calculateMetrics(commits);
61
-
62
- // Get user info (git config)
63
- const git = require('../core/git');
64
- const { stdout: username } = await git.runGit(repoPath, ['config', 'user.name']);
65
- const { stdout: email } = await git.runGit(repoPath, ['config', 'user.email']);
66
-
67
- const userEmail = email.trim() || 'unknown';
68
- const userName = username.trim() || 'Anonymous';
69
-
70
- // Anonymize ID using hash
71
- const userId = crypto.createHash('sha256').update(userEmail).digest('hex').substring(0, 12);
72
-
73
- // Get focus time from logs (or fallback to git stats proxy)
74
- const logFocusMinutes = await calculateFocusTime(repoPath);
75
- const gitFocusMinutes = Math.round(metrics.totalAdditions / 10);
76
- const focusMinutes = logFocusMinutes > 0 ? logFocusMinutes : gitFocusMinutes;
77
-
78
- const stats = {
79
- id: userId,
80
- username: userName, // Display name (can be public)
81
- score: metrics.quality.score * 100 + metrics.totalCommits * 10, // Example scoring
82
- commits: metrics.totalCommits,
83
- focusMinutes: focusMinutes,
84
- streak: metrics.streak.current
85
- };
86
-
87
- logger.info(`Syncing stats for ${stats.username} (ID: ${userId})...`);
88
- logger.info('Note: Only metrics are shared. No code or file contents are transmitted.');
89
-
90
- const response = await fetch(`${apiUrl}/api/leaderboard/sync`, {
91
- method: 'POST',
92
- headers: { 'Content-Type': 'application/json' },
93
- body: JSON.stringify(stats)
94
- });
95
-
96
- if (!response.ok) {
97
- let errorDetail = '';
98
- try {
99
- const errJson = await response.json();
100
- errorDetail = errJson.details || errJson.error || '';
101
- } catch (e) {
102
- // Not a JSON error
103
- }
104
- throw new Error(`Server responded with ${response.status}${errorDetail ? ': ' + errorDetail : ''}`);
105
- }
106
-
107
- const data = await response.json();
108
- logger.success(`Successfully synced! You are currently ranked #${data.rank}.`);
109
-
110
- } catch (error) {
111
- logger.error(`Failed to sync leaderboard: ${error.message}`);
112
- logger.info('Make sure the docs server is running (npm run dev in autopilot-docs)');
113
- }
114
- }
115
-
116
- module.exports = { leaderboard, syncLeaderboard };
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const { getGitStats, calculateMetrics } = require('./insights');
4
+ const logger = require('../utils/logger');
5
+ const crypto = require('crypto');
6
+
7
+ // Default API URL (can be overridden by env)
8
+ const DEFAULT_API_URL = 'https://autopilot-cli.vercel.app';
9
+
10
+ async function calculateFocusTime(repoPath) {
11
+ const logPath = path.join(repoPath, 'autopilot.log');
12
+ if (!await fs.pathExists(logPath)) return 0;
13
+
14
+ try {
15
+ const content = await fs.readFile(logPath, 'utf8');
16
+ const lines = content.split('\n').filter(l => l.trim());
17
+ let totalMs = 0;
18
+
19
+ for (const line of lines) {
20
+ try {
21
+ const entry = JSON.parse(line);
22
+ if (entry.type === 'FOCUS_SESSION_END' && entry.totalActiveMs) {
23
+ totalMs += entry.totalActiveMs;
24
+ }
25
+ } catch (e) {
26
+ // ignore bad lines
27
+ }
28
+ }
29
+
30
+ return Math.round(totalMs / 60000); // minutes
31
+ } catch (error) {
32
+ logger.warn(`Failed to parse autopilot.log: ${error.message}`);
33
+ return 0;
34
+ }
35
+ }
36
+
37
+ async function leaderboard(options) {
38
+ const apiUrl = process.env.AUTOPILOT_API_URL || DEFAULT_API_URL;
39
+
40
+ if (options.sync) {
41
+ await syncLeaderboard(apiUrl, options);
42
+ } else {
43
+ logger.info(`Opening leaderboard at ${apiUrl}/leaderboard...`);
44
+ const { default: open } = await import('open');
45
+ await open(`${apiUrl}/leaderboard`);
46
+ }
47
+ }
48
+
49
+ async function syncLeaderboard(apiUrl, options) {
50
+ try {
51
+ const repoPath = options.cwd || process.cwd();
52
+ logger.info('Calculating stats for leaderboard sync...');
53
+
54
+ const commits = await getGitStats(repoPath);
55
+ if (commits.length === 0) {
56
+ logger.warn('No git history found. Cannot sync stats.');
57
+ return;
58
+ }
59
+
60
+ const metrics = calculateMetrics(commits);
61
+
62
+ // Get user info (git config)
63
+ const git = require('../core/git');
64
+ const { stdout: username } = await git.runGit(repoPath, ['config', 'user.name']);
65
+ const { stdout: email } = await git.runGit(repoPath, ['config', 'user.email']);
66
+
67
+ const userEmail = email.trim() || 'unknown';
68
+ const userName = username.trim() || 'Anonymous';
69
+
70
+ // Anonymize ID using hash
71
+ const userId = crypto.createHash('sha256').update(userEmail).digest('hex').substring(0, 12);
72
+
73
+ // Get focus time from logs (or fallback to git stats proxy)
74
+ const logFocusMinutes = await calculateFocusTime(repoPath);
75
+ const gitFocusMinutes = Math.round(metrics.totalAdditions / 10);
76
+ const focusMinutes = logFocusMinutes > 0 ? logFocusMinutes : gitFocusMinutes;
77
+
78
+ const stats = {
79
+ id: userId,
80
+ username: userName, // Display name (can be public)
81
+ score: metrics.quality.score * 100 + metrics.totalCommits * 10, // Example scoring
82
+ commits: metrics.totalCommits,
83
+ focusMinutes: focusMinutes,
84
+ streak: metrics.streak.current
85
+ };
86
+
87
+ logger.info(`Syncing stats for ${stats.username} (ID: ${userId})...`);
88
+ logger.info('Note: Only metrics are shared. No code or file contents are transmitted.');
89
+
90
+ const response = await fetch(`${apiUrl}/api/leaderboard/sync`, {
91
+ method: 'POST',
92
+ headers: { 'Content-Type': 'application/json' },
93
+ body: JSON.stringify(stats)
94
+ });
95
+
96
+ if (!response.ok) {
97
+ let errorDetail = '';
98
+ try {
99
+ const errJson = await response.json();
100
+ errorDetail = errJson.details || errJson.error || '';
101
+ } catch (e) {
102
+ // Not a JSON error
103
+ }
104
+ throw new Error(`Server responded with ${response.status}${errorDetail ? ': ' + errorDetail : ''}`);
105
+ }
106
+
107
+ const data = await response.json();
108
+ logger.success(`Successfully synced! You are currently ranked #${data.rank}.`);
109
+
110
+ } catch (error) {
111
+ logger.error(`Failed to sync leaderboard: ${error.message}`);
112
+ logger.info('Make sure the docs server is running (npm run dev in autopilot-docs)');
113
+ }
114
+ }
115
+
116
+ module.exports = { leaderboard, syncLeaderboard };
@@ -1,18 +1,18 @@
1
- const logger = require('../utils/logger');
2
- const StateManager = require('../core/state');
3
-
4
- function pauseCommand(reason) {
5
- const root = process.cwd();
6
- const stateManager = new StateManager(root);
7
-
8
- if (stateManager.isPaused()) {
9
- logger.warn('Autopilot is already paused.');
10
- return;
11
- }
12
-
13
- const pauseReason = typeof reason === 'string' ? reason : 'User paused';
14
- stateManager.pause(pauseReason);
15
- logger.success(`Autopilot paused: "${pauseReason}"`);
16
- }
17
-
18
- module.exports = pauseCommand;
1
+ const logger = require('../utils/logger');
2
+ const StateManager = require('../core/state');
3
+
4
+ function pauseCommand(reason) {
5
+ const root = process.cwd();
6
+ const stateManager = new StateManager(root);
7
+
8
+ if (stateManager.isPaused()) {
9
+ logger.warn('Autopilot is already paused.');
10
+ return;
11
+ }
12
+
13
+ const pauseReason = typeof reason === 'string' ? reason : 'User paused';
14
+ stateManager.pause(pauseReason);
15
+ logger.success(`Autopilot paused: "${pauseReason}"`);
16
+ }
17
+
18
+ module.exports = pauseCommand;
@@ -1,121 +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;
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;
@@ -1,17 +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;
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;
@@ -1,41 +1,41 @@
1
- /**
2
- * Command: start
3
- * Starts the Autopilot watcher in the foreground
4
- */
5
-
6
- const process = require('process');
7
- const logger = require('../utils/logger');
8
- const Watcher = require('../core/watcher');
9
- const { getRunningPid } = require('../utils/process');
10
-
11
- const start = async (options) => {
12
- const repoPath = process.cwd();
13
-
14
- try {
15
- // Check if already running
16
- const runningPid = await getRunningPid(repoPath);
17
- if (runningPid) {
18
- logger.warn(`Autopilot is already running (PID: ${runningPid})`);
19
- logger.info('Run "autopilot stop" to stop the current instance.');
20
- return;
21
- }
22
-
23
- // Initialize watcher
24
- const watcher = new Watcher(repoPath);
25
-
26
- logger.section('Starting Autopilot');
27
- logger.info('Press Ctrl+C to stop, or run "autopilot stop" in another terminal.');
28
-
29
- // Start watching
30
- await watcher.start();
31
-
32
- // Keep process alive is handled by chokidar being persistent
33
- // The watcher handles process signals for cleanup
34
-
35
- } catch (error) {
36
- logger.error(`Failed to start autopilot: ${error.message}`);
37
- process.exit(1);
38
- }
39
- };
40
-
41
- module.exports = start;
1
+ /**
2
+ * Command: start
3
+ * Starts the Autopilot watcher in the foreground
4
+ */
5
+
6
+ const process = require('process');
7
+ const logger = require('../utils/logger');
8
+ const Watcher = require('../core/watcher');
9
+ const { getRunningPid } = require('../utils/process');
10
+
11
+ const start = async (options) => {
12
+ const repoPath = process.cwd();
13
+
14
+ try {
15
+ // Check if already running
16
+ const runningPid = await getRunningPid(repoPath);
17
+ if (runningPid) {
18
+ logger.warn(`Autopilot is already running (PID: ${runningPid})`);
19
+ logger.info('Run "autopilot stop" to stop the current instance.');
20
+ return;
21
+ }
22
+
23
+ // Initialize watcher
24
+ const watcher = new Watcher(repoPath);
25
+
26
+ logger.section('Starting Autopilot');
27
+ logger.info('Press Ctrl+C to stop, or run "autopilot stop" in another terminal.');
28
+
29
+ // Start watching
30
+ await watcher.start();
31
+
32
+ // Keep process alive is handled by chokidar being persistent
33
+ // The watcher handles process signals for cleanup
34
+
35
+ } catch (error) {
36
+ logger.error(`Failed to start autopilot: ${error.message}`);
37
+ process.exit(1);
38
+ }
39
+ };
40
+
41
+ module.exports = start;