@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,210 +1,224 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
- const execa = require('execa');
4
- const logger = require('../utils/logger');
5
- const git = require('./git');
6
-
7
- // Regex patterns for common secrets
8
- const SECRET_PATTERNS = [
9
- { name: 'AWS Access Key', regex: /(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}/ },
10
- { name: 'AWS Secret Key', regex: /(?<![A-Za-z0-9/+=])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=])/ },
11
- { name: 'GitHub Token', regex: /(gh[pousr]_[a-zA-Z0-9]{36,255})/ },
12
- { name: 'Stripe Secret Key', regex: /(sk_live_[0-9a-zA-Z]{24})/ },
13
- { name: 'Google API Key', regex: /AIza[0-9A-Za-z\\-_]{35}/ },
14
- { name: 'Bearer Token', regex: /Bearer [a-zA-Z0-9\-\._~\+\/]+=*/ },
15
- { name: 'Generic Private Key', regex: /-----BEGIN PRIVATE KEY-----/ }
16
- ];
17
-
18
- const MAX_FILE_SIZE_MB = 50;
19
-
20
- /**
21
- * Validate configuration
22
- */
23
- const validateConfig = (config) => {
24
- const errors = [];
25
-
26
- if (!config) {
27
- errors.push('Configuration is missing');
28
- return errors;
29
- }
30
-
31
- if (config.commitMessage && typeof config.commitMessage !== 'string') {
32
- errors.push('commitMessage must be a string');
33
- }
34
-
35
- if (config.autoPush && typeof config.autoPush !== 'boolean') {
36
- errors.push('autoPush must be a boolean');
37
- }
38
-
39
- if (config.ignore && !Array.isArray(config.ignore)) {
40
- errors.push('ignore must be an array');
41
- }
42
-
43
- return errors;
44
- };
45
-
46
- /**
47
- * Run pre-commit checks on staged files
48
- * @param {string} repoPath
49
- * @param {object} config
50
- * @returns {Promise<{ok: boolean, errors: string[]}>}
51
- */
52
- const validateBeforeCommit = async (repoPath, config) => {
53
- const errors = [];
54
-
55
- if (!config.preCommitChecks) {
56
- return { ok: true, errors: [] };
57
- }
58
-
59
- // 0. Merge/Rebase Safety Check (Hard Guarantee)
60
- const isMerge = await git.isMergeInProgress(repoPath);
61
- if (isMerge) {
62
- return { ok: false, errors: ['Repository is in a merge/rebase state. Autopilot paused for safety.'] };
63
- }
64
-
65
- try {
66
- // Get staged files
67
- const statusObj = await git.getPorcelainStatus(repoPath);
68
- // Filter only modified/added files (ignore deleted)
69
- const stagedFiles = statusObj.files.filter(f => f.status !== 'D' && f.status !== ' D');
70
-
71
- // 1. File Size Check
72
- if (config.preCommitChecks.fileSize) {
73
- for (const file of stagedFiles) {
74
- try {
75
- const filePath = path.join(repoPath, file.file);
76
- if (fs.existsSync(filePath)) {
77
- const stats = await fs.stat(filePath);
78
- const sizeMb = stats.size / (1024 * 1024);
79
- if (sizeMb > MAX_FILE_SIZE_MB) {
80
- errors.push(`File ${file.file} is too large (${sizeMb.toFixed(2)}MB > ${MAX_FILE_SIZE_MB}MB)`);
81
- }
82
- }
83
- } catch (err) {
84
- // Ignore missing files
85
- }
86
- }
87
- }
88
-
89
- // 2. Secret Detection
90
- if (config.preCommitChecks.secrets) {
91
- for (const file of stagedFiles) {
92
- try {
93
- const filePath = path.join(repoPath, file.file);
94
- if (fs.existsSync(filePath)) {
95
- // Read first 1MB only for performance
96
- // Use stream or buffer
97
- const buffer = Buffer.alloc(1024 * 1024);
98
- const fd = await fs.open(filePath, 'r');
99
- const { bytesRead } = await fs.read(fd, buffer, 0, buffer.length, 0);
100
- await fs.close(fd);
101
-
102
- const content = buffer.toString('utf8', 0, bytesRead);
103
-
104
- for (const pattern of SECRET_PATTERNS) {
105
- if (pattern.regex.test(content)) {
106
- errors.push(`Possible ${pattern.name} detected in ${file.file}`);
107
- }
108
- }
109
- }
110
- } catch (err) {
111
- logger.debug(`Could not read ${file.file} for secret scan: ${err.message}`);
112
- }
113
- }
114
- }
115
-
116
- // 3. Linting
117
- if (config.preCommitChecks.lint && config.preCommitChecks.lint !== false) {
118
- logger.info('Running lint check...');
119
- try {
120
- await execa.command('npm run lint', { cwd: repoPath });
121
- } catch (err) {
122
- errors.push(`Lint check failed: ${err.shortMessage || err.message}`);
123
- }
124
- }
125
-
126
- // 4. Tests
127
- if (config.preCommitChecks.test) {
128
- logger.info('Running tests...');
129
- try {
130
- await execa.command(config.preCommitChecks.test, { cwd: repoPath });
131
- } catch (err) {
132
- errors.push(`Test check failed: ${err.shortMessage || err.message}`);
133
- }
134
- }
135
-
136
- } catch (error) {
137
- logger.error(`Validation error: ${error.message}`);
138
- // If validation crashes, safe to fail open? No, fail closed for safety.
139
- errors.push(`Validation crashed: ${error.message}`);
140
- }
141
-
142
- return { ok: errors.length === 0, errors };
143
- };
144
-
145
- /**
146
- * Handle Team Mode Checks (Pull-Before-Push)
147
- * @param {string} repoPath
148
- * @param {object} config
149
- * @returns {Promise<{ok: boolean, action: 'continue'|'pull'|'abort'}>}
150
- */
151
- const checkTeamStatus = async (repoPath, config) => {
152
- if (!config.teamMode) {
153
- return { ok: true, action: 'continue' };
154
- }
155
-
156
- logger.debug('Running Team Mode checks...');
157
-
158
- // Check unpushed commits limit
159
- try {
160
- const branch = await git.getBranch(repoPath);
161
- if (!branch) return { ok: false, action: 'abort' }; // No branch?
162
-
163
- const { stdout: unpushedCount } = await execa('git', ['rev-list', '--count', `origin/${branch}..HEAD`], { cwd: repoPath }).catch(() => ({ stdout: '0' }));
164
-
165
- if (Number(unpushedCount) > (config.maxUnpushedCommits || 5)) {
166
- logger.warn(`Too many unpushed commits (${unpushedCount}). Pushing required.`);
167
- // We might force a push here or just warn
168
- }
169
-
170
- // Fetch to see if we are behind
171
- await git.fetch(repoPath);
172
- const remoteStatus = await git.isRemoteAhead(repoPath);
173
-
174
- if (remoteStatus.behind) {
175
- if (config.pullBeforePush) {
176
- logger.info('Remote is ahead. Pulling changes...');
177
- // Try pull --rebase
178
- try {
179
- await execa('git', ['pull', '--rebase'], { cwd: repoPath });
180
- return { ok: true, action: 'continue' };
181
- } catch (err) {
182
- logger.error('Pull failed (conflict detected).');
183
- return { ok: false, action: 'abort' };
184
- }
185
- } else {
186
- return { ok: false, action: 'abort' };
187
- }
188
- }
189
-
190
- // Detect potential conflicts without pulling (git diff --check)
191
- // Actually git diff --check is for whitespace, not merge conflicts.
192
- // To check for merge conflicts before pulling is hard without actually merging.
193
- // But since we did fetch, we can dry-run a merge?
194
- // For now, relies on pull --rebase failure above.
195
-
196
- } catch (err) {
197
- logger.warn(`Team check failed: ${err.message}`);
198
- // If we can't check, maybe safe to continue locally?
199
- // But "conflictStrategy": "abort" suggests we should stop.
200
- if (config.conflictStrategy === 'abort') return { ok: false, action: 'abort' };
201
- }
202
-
203
- return { ok: true, action: 'continue' };
204
- };
205
-
206
- module.exports = {
207
- validateConfig,
208
- validateBeforeCommit,
209
- checkTeamStatus
210
- };
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const execa = require('execa');
4
+ const logger = require('../utils/logger');
5
+ const git = require('./git');
6
+
7
+ // Regex patterns for common secrets
8
+ const SECRET_PATTERNS = [
9
+ { name: 'AWS Access Key', regex: /(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}/ },
10
+ { name: 'AWS Secret Key', regex: /(?<![A-Za-z0-9/+=])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=])/ },
11
+ { name: 'GitHub Token', regex: /(gh[pousr]_[a-zA-Z0-9]{36,255})/ },
12
+ { name: 'Stripe Secret Key', regex: /(sk_live_[0-9a-zA-Z]{24})/ },
13
+ { name: 'Google API Key', regex: /AIza[0-9A-Za-z\\-_]{35}/ },
14
+ { name: 'Bearer Token', regex: /Bearer [a-zA-Z0-9\-\._~\+\/]+=*/ },
15
+ { name: 'Generic Private Key', regex: /-----BEGIN PRIVATE KEY-----/ }
16
+ ];
17
+
18
+ const MAX_FILE_SIZE_MB = 50;
19
+
20
+ /**
21
+ * Validate configuration
22
+ */
23
+ const validateConfig = (config) => {
24
+ const errors = [];
25
+
26
+ if (!config) {
27
+ errors.push('Configuration is missing');
28
+ return errors;
29
+ }
30
+
31
+ if (config.commitMessage && typeof config.commitMessage !== 'string') {
32
+ errors.push('commitMessage must be a string');
33
+ }
34
+
35
+ if (config.autoPush && typeof config.autoPush !== 'boolean') {
36
+ errors.push('autoPush must be a boolean');
37
+ }
38
+
39
+ if (config.ignore && !Array.isArray(config.ignore)) {
40
+ errors.push('ignore must be an array');
41
+ }
42
+
43
+ return errors;
44
+ };
45
+
46
+ /**
47
+ * Run pre-commit checks on staged files
48
+ * @param {string} repoPath
49
+ * @param {object} config
50
+ * @returns {Promise<{ok: boolean, errors: string[]}>}
51
+ */
52
+ const validateBeforeCommit = async (repoPath, config) => {
53
+ const errors = [];
54
+
55
+ if (!config.preCommitChecks) {
56
+ return { ok: true, errors: [] };
57
+ }
58
+
59
+ // 0. Merge/Rebase Safety Check (Hard Guarantee)
60
+ const isMerge = await git.isMergeInProgress(repoPath);
61
+ if (isMerge) {
62
+ return { ok: false, errors: ['Repository is in a merge/rebase state. Autopilot paused for safety.'] };
63
+ }
64
+
65
+ try {
66
+ // Get staged files
67
+ const statusObj = await git.getPorcelainStatus(repoPath);
68
+ // Filter only modified/added files (ignore deleted)
69
+ const stagedFiles = statusObj.files.filter(f => f.status !== 'D' && f.status !== ' D');
70
+
71
+ // 1. File Size Check
72
+ if (config.preCommitChecks.fileSize) {
73
+ for (const file of stagedFiles) {
74
+ try {
75
+ const filePath = path.join(repoPath, file.file);
76
+ if (fs.existsSync(filePath)) {
77
+ const stats = await fs.stat(filePath);
78
+ const sizeMb = stats.size / (1024 * 1024);
79
+ if (sizeMb > MAX_FILE_SIZE_MB) {
80
+ errors.push(`File ${file.file} is too large (${sizeMb.toFixed(2)}MB > ${MAX_FILE_SIZE_MB}MB)`);
81
+ }
82
+ }
83
+ } catch (err) {
84
+ // Ignore missing files
85
+ }
86
+ }
87
+ }
88
+
89
+ // 2. Secret Detection
90
+ if (config.preCommitChecks.secrets) {
91
+ for (const file of stagedFiles) {
92
+ try {
93
+ const filePath = path.join(repoPath, file.file);
94
+ const stats = await fs.stat(filePath);
95
+ if (stats.isFile()) {
96
+ const buffer = Buffer.alloc(1024 * 1024);
97
+ const fd = await fs.open(filePath, 'r');
98
+ const { bytesRead } = await fs.read(fd, buffer, 0, buffer.length, 0);
99
+ await fs.close(fd);
100
+
101
+ const content = buffer.toString('utf8', 0, bytesRead);
102
+
103
+ for (const pattern of SECRET_PATTERNS) {
104
+ if (pattern.regex.test(content)) {
105
+ errors.push(`Possible ${pattern.name} detected in ${file.file}`);
106
+ }
107
+ }
108
+ }
109
+ } catch (err) {
110
+ logger.debug(`Could not read ${file.file} for secret scan: ${err.message}`);
111
+ }
112
+ }
113
+ }
114
+
115
+ // 3. Linting
116
+ if (config.preCommitChecks.lint && config.preCommitChecks.lint !== false) {
117
+ logger.info('Running lint check...');
118
+ try {
119
+ await execa.command('npm run lint', { cwd: repoPath });
120
+ } catch (err) {
121
+ errors.push(`Lint check failed: ${err.shortMessage || err.message}`);
122
+ }
123
+ }
124
+
125
+ // 4. Tests
126
+ if (config.preCommitChecks.test) {
127
+ logger.info('Running tests...');
128
+ try {
129
+ await execa.command(config.preCommitChecks.test, { cwd: repoPath });
130
+ } catch (err) {
131
+ errors.push(`Test check failed: ${err.shortMessage || err.message}`);
132
+ }
133
+ }
134
+
135
+ } catch (error) {
136
+ logger.error(`Validation error: ${error.message}`);
137
+ // If validation crashes, safe to fail open? No, fail closed for safety.
138
+ errors.push(`Validation crashed: ${error.message}`);
139
+ }
140
+
141
+ return { ok: errors.length === 0, errors };
142
+ };
143
+
144
+ /**
145
+ * Handle Team Mode Checks (Pull-Before-Push)
146
+ * @param {string} repoPath
147
+ * @param {object} config
148
+ * @returns {Promise<{ok: boolean, action: 'continue'|'pull'|'abort'}>}
149
+ */
150
+ const checkTeamStatus = async (repoPath, config) => {
151
+ if (!config.teamMode) {
152
+ return { ok: true, action: 'continue' };
153
+ }
154
+
155
+ logger.debug('Running Team Mode checks...');
156
+
157
+ // Check unpushed commits limit
158
+ try {
159
+ const branch = await git.getBranch(repoPath);
160
+ if (!branch) return { ok: false, action: 'abort' }; // No branch?
161
+
162
+ const { stdout: unpushedCount } = await execa('git', ['rev-list', '--count', `origin/${branch}..HEAD`], { cwd: repoPath }).catch(() => ({ stdout: '0' }));
163
+
164
+ if (Number(unpushedCount) > (config.maxUnpushedCommits || 5)) {
165
+ logger.warn(`Too many unpushed commits (${unpushedCount}). Pushing required.`);
166
+ // We might force a push here or just warn
167
+ }
168
+
169
+ // Fetch to see if we are behind
170
+ await git.fetch(repoPath);
171
+ const remoteStatus = await git.isRemoteAhead(repoPath);
172
+
173
+ if (remoteStatus.behind) {
174
+ if (config.pullBeforePush) {
175
+ logger.info('Remote is ahead. Pulling changes...');
176
+ // Try pull --rebase
177
+ try {
178
+ await execa('git', ['pull', '--rebase'], { cwd: repoPath });
179
+ return { ok: true, action: 'continue' };
180
+ } catch (err) {
181
+ logger.error('Pull failed (conflict detected).');
182
+ return { ok: false, action: 'abort' };
183
+ }
184
+ } else {
185
+ return { ok: false, action: 'abort' };
186
+ }
187
+ }
188
+
189
+ // Detect potential conflicts without pulling (git diff --check)
190
+ // Actually git diff --check is for whitespace, not merge conflicts.
191
+ // To check for merge conflicts before pulling is hard without actually merging.
192
+ // But since we did fetch, we can dry-run a merge?
193
+ // For now, relies on pull --rebase failure above.
194
+
195
+ } catch (err) {
196
+ logger.warn(`Team check failed: ${err.message}`);
197
+ // If we can't check, maybe safe to continue locally?
198
+ // But "conflictStrategy": "abort" suggests we should stop.
199
+ if (config.conflictStrategy === 'abort') return { ok: false, action: 'abort' };
200
+ }
201
+
202
+ return { ok: true, action: 'continue' };
203
+ };
204
+
205
+ /**
206
+ * Check if the current branch is protected
207
+ * @param {string} branchName
208
+ * @param {object} config
209
+ * @returns {boolean}
210
+ */
211
+ const isProtectedBranch = (branchName, config) => {
212
+ const defaultProtected = ["main", "master", "production", "prod", "release"];
213
+ const userProtected = config.protectedBranches || [];
214
+ const protectedBranches = [...new Set([...defaultProtected, ...userProtected])];
215
+
216
+ return protectedBranches.includes(branchName);
217
+ };
218
+
219
+ module.exports = {
220
+ validateConfig,
221
+ validateBeforeCommit,
222
+ checkTeamStatus,
223
+ isProtectedBranch
224
+ };
package/src/core/state.js CHANGED
@@ -1,71 +1,69 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
- const logger = require('../utils/logger');
4
- const { getAutopilotHome } = require('../utils/paths');
5
-
6
- const STATE_FILE = 'state.json';
7
-
8
- class StateManager {
9
- constructor(repoPath) {
10
- this.repoPath = repoPath;
11
- this.stateDir = path.join(repoPath, '.autopilot');
12
- this.stateFile = path.join(this.stateDir, STATE_FILE);
13
- this.init();
14
- }
15
-
16
- init() {
17
- fs.ensureDirSync(this.stateDir);
18
- if (!fs.existsSync(this.stateFile)) {
19
- this.reset();
20
- }
21
- }
22
-
23
- getState() {
24
- if (!fs.existsSync(this.stateFile)) {
25
- return { status: 'running' };
26
- }
27
- try {
28
- return fs.readJsonSync(this.stateFile);
29
- } catch (error) {
30
- logger.error('Failed to read state:', error.message);
31
- return { status: 'running' }; // Default
32
- }
33
- }
34
-
35
- setState(newState) {
36
- try {
37
- const currentState = this.getState();
38
- const updatedState = { ...currentState, ...newState };
39
- fs.writeJsonSync(this.stateFile, updatedState, { spaces: 2 });
40
- } catch (error) {
41
- logger.error('Failed to write state:', error.message);
42
- }
43
- }
44
-
45
- reset() {
46
- this.setState({
47
- status: 'running', // running | paused
48
- reason: null,
49
- pausedAt: null
50
- });
51
- }
52
-
53
- pause(reason) {
54
- this.setState({
55
- status: 'paused',
56
- reason: reason || 'User paused',
57
- pausedAt: new Date().toISOString()
58
- });
59
- }
60
-
61
- resume() {
62
- this.reset();
63
- }
64
-
65
- isPaused() {
66
- const state = this.getState();
67
- return state.status === 'paused';
68
- }
69
- }
70
-
71
- module.exports = StateManager;
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const logger = require('../utils/logger');
4
+ const { getAutopilotHome } = require('../utils/paths');
5
+
6
+ const STATE_FILE = '.autopilot-state.json';
7
+
8
+ class StateManager {
9
+ constructor(repoPath) {
10
+ this.repoPath = repoPath;
11
+ this.stateFile = path.join(repoPath, STATE_FILE);
12
+ this.init();
13
+ }
14
+
15
+ init() {
16
+ if (!fs.existsSync(this.stateFile)) {
17
+ this.reset();
18
+ }
19
+ }
20
+
21
+ getState() {
22
+ if (!fs.existsSync(this.stateFile)) {
23
+ return { status: 'running' };
24
+ }
25
+ try {
26
+ return fs.readJsonSync(this.stateFile);
27
+ } catch (error) {
28
+ logger.error('Failed to read state:', error.message);
29
+ return { status: 'running' }; // Default
30
+ }
31
+ }
32
+
33
+ setState(newState) {
34
+ try {
35
+ const currentState = this.getState();
36
+ const updatedState = { ...currentState, ...newState };
37
+ fs.writeJsonSync(this.stateFile, updatedState, { spaces: 2 });
38
+ } catch (error) {
39
+ logger.error('Failed to write state:', error.message);
40
+ }
41
+ }
42
+
43
+ reset() {
44
+ this.setState({
45
+ status: 'running', // running | paused
46
+ reason: null,
47
+ pausedAt: null
48
+ });
49
+ }
50
+
51
+ pause(reason) {
52
+ this.setState({
53
+ status: 'paused',
54
+ reason: reason || 'User paused',
55
+ pausedAt: new Date().toISOString()
56
+ });
57
+ }
58
+
59
+ resume() {
60
+ this.reset();
61
+ }
62
+
63
+ isPaused() {
64
+ const state = this.getState();
65
+ return state.status === 'paused';
66
+ }
67
+ }
68
+
69
+ module.exports = StateManager;