@traisetech/autopilot 0.1.8 → 2.0.1

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.
@@ -1,38 +1,204 @@
1
- const logger = require('../utils/logger');
2
-
3
- const validateConfig = (config) => {
4
- const errors = [];
5
-
6
- if (!config) {
7
- errors.push('Configuration is missing');
8
- return errors;
9
- }
10
-
11
- if (config.commitMessage && typeof config.commitMessage !== 'string') {
12
- errors.push('commitMessage must be a string');
13
- }
14
-
15
- if (config.autoPush && typeof config.autoPush !== 'boolean') {
16
- errors.push('autoPush must be a boolean');
17
- }
18
-
19
- if (config.ignore && !Array.isArray(config.ignore)) {
20
- errors.push('ignore must be an array');
21
- }
22
-
23
- return errors;
24
- };
25
-
26
- const validateBeforeCommit = async (repoPath) => {
27
- const errors = [];
28
-
29
- // Add safety checks before committing
30
- // For example, check for large files, sensitive files, etc.
31
-
32
- return errors;
33
- };
34
-
35
- module.exports = {
36
- validateConfig,
37
- validateBeforeCommit,
38
- };
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
+ try {
60
+ // Get staged files
61
+ const statusObj = await git.getPorcelainStatus(repoPath);
62
+ // Filter only modified/added files (ignore deleted)
63
+ const stagedFiles = statusObj.files.filter(f => f.status !== 'D' && f.status !== ' D');
64
+
65
+ // 1. File Size Check
66
+ if (config.preCommitChecks.fileSize) {
67
+ for (const file of stagedFiles) {
68
+ try {
69
+ const filePath = path.join(repoPath, file.file);
70
+ if (fs.existsSync(filePath)) {
71
+ const stats = await fs.stat(filePath);
72
+ const sizeMb = stats.size / (1024 * 1024);
73
+ if (sizeMb > MAX_FILE_SIZE_MB) {
74
+ errors.push(`File ${file.file} is too large (${sizeMb.toFixed(2)}MB > ${MAX_FILE_SIZE_MB}MB)`);
75
+ }
76
+ }
77
+ } catch (err) {
78
+ // Ignore missing files
79
+ }
80
+ }
81
+ }
82
+
83
+ // 2. Secret Detection
84
+ if (config.preCommitChecks.secrets) {
85
+ for (const file of stagedFiles) {
86
+ try {
87
+ const filePath = path.join(repoPath, file.file);
88
+ if (fs.existsSync(filePath)) {
89
+ // Read first 1MB only for performance
90
+ // Use stream or buffer
91
+ const buffer = Buffer.alloc(1024 * 1024);
92
+ const fd = await fs.open(filePath, 'r');
93
+ const { bytesRead } = await fs.read(fd, buffer, 0, buffer.length, 0);
94
+ await fs.close(fd);
95
+
96
+ const content = buffer.toString('utf8', 0, bytesRead);
97
+
98
+ for (const pattern of SECRET_PATTERNS) {
99
+ if (pattern.regex.test(content)) {
100
+ errors.push(`Possible ${pattern.name} detected in ${file.file}`);
101
+ }
102
+ }
103
+ }
104
+ } catch (err) {
105
+ logger.debug(`Could not read ${file.file} for secret scan: ${err.message}`);
106
+ }
107
+ }
108
+ }
109
+
110
+ // 3. Linting
111
+ if (config.preCommitChecks.lint && config.preCommitChecks.lint !== false) {
112
+ logger.info('Running lint check...');
113
+ try {
114
+ await execa.command('npm run lint', { cwd: repoPath });
115
+ } catch (err) {
116
+ errors.push(`Lint check failed: ${err.shortMessage || err.message}`);
117
+ }
118
+ }
119
+
120
+ // 4. Tests
121
+ if (config.preCommitChecks.test) {
122
+ logger.info('Running tests...');
123
+ try {
124
+ await execa.command(config.preCommitChecks.test, { cwd: repoPath });
125
+ } catch (err) {
126
+ errors.push(`Test check failed: ${err.shortMessage || err.message}`);
127
+ }
128
+ }
129
+
130
+ } catch (error) {
131
+ logger.error(`Validation error: ${error.message}`);
132
+ // If validation crashes, safe to fail open? No, fail closed for safety.
133
+ errors.push(`Validation crashed: ${error.message}`);
134
+ }
135
+
136
+ return { ok: errors.length === 0, errors };
137
+ };
138
+
139
+ /**
140
+ * Handle Team Mode Checks (Pull-Before-Push)
141
+ * @param {string} repoPath
142
+ * @param {object} config
143
+ * @returns {Promise<{ok: boolean, action: 'continue'|'pull'|'abort'}>}
144
+ */
145
+ const checkTeamStatus = async (repoPath, config) => {
146
+ if (!config.teamMode) {
147
+ return { ok: true, action: 'continue' };
148
+ }
149
+
150
+ logger.debug('Running Team Mode checks...');
151
+
152
+ // Check unpushed commits limit
153
+ try {
154
+ const branch = await git.getBranch(repoPath);
155
+ if (!branch) return { ok: false, action: 'abort' }; // No branch?
156
+
157
+ const { stdout: unpushedCount } = await execa('git', ['rev-list', '--count', `origin/${branch}..HEAD`], { cwd: repoPath }).catch(() => ({ stdout: '0' }));
158
+
159
+ if (Number(unpushedCount) > (config.maxUnpushedCommits || 5)) {
160
+ logger.warn(`Too many unpushed commits (${unpushedCount}). Pushing required.`);
161
+ // We might force a push here or just warn
162
+ }
163
+
164
+ // Fetch to see if we are behind
165
+ await git.fetch(repoPath);
166
+ const remoteStatus = await git.isRemoteAhead(repoPath);
167
+
168
+ if (remoteStatus.behind) {
169
+ if (config.pullBeforePush) {
170
+ logger.info('Remote is ahead. Pulling changes...');
171
+ // Try pull --rebase
172
+ try {
173
+ await execa('git', ['pull', '--rebase'], { cwd: repoPath });
174
+ return { ok: true, action: 'continue' };
175
+ } catch (err) {
176
+ logger.error('Pull failed (conflict detected).');
177
+ return { ok: false, action: 'abort' };
178
+ }
179
+ } else {
180
+ return { ok: false, action: 'abort' };
181
+ }
182
+ }
183
+
184
+ // Detect potential conflicts without pulling (git diff --check)
185
+ // Actually git diff --check is for whitespace, not merge conflicts.
186
+ // To check for merge conflicts before pulling is hard without actually merging.
187
+ // But since we did fetch, we can dry-run a merge?
188
+ // For now, relies on pull --rebase failure above.
189
+
190
+ } catch (err) {
191
+ logger.warn(`Team check failed: ${err.message}`);
192
+ // If we can't check, maybe safe to continue locally?
193
+ // But "conflictStrategy": "abort" suggests we should stop.
194
+ if (config.conflictStrategy === 'abort') return { ok: false, action: 'abort' };
195
+ }
196
+
197
+ return { ok: true, action: 'continue' };
198
+ };
199
+
200
+ module.exports = {
201
+ validateConfig,
202
+ validateBeforeCommit,
203
+ checkTeamStatus
204
+ };
@@ -0,0 +1,71 @@
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;
@@ -14,6 +14,9 @@ const { generateCommitMessage } = require('./commit');
14
14
  const { savePid, removePid, registerProcessHandlers } = require('../utils/process');
15
15
  const { loadConfig } = require('../config/loader');
16
16
  const { readIgnoreFile, createIgnoredFilter, normalizePath } = require('../config/ignore');
17
+ const HistoryManager = require('./history');
18
+ const StateManager = require('./state');
19
+ const { validateBeforeCommit, checkTeamStatus } = require('./safety');
17
20
 
18
21
  class Watcher {
19
22
  constructor(repoPath) {
@@ -30,6 +33,8 @@ class Watcher {
30
33
  this.ignorePatterns = [];
31
34
  this.ignoredFilter = null;
32
35
  this.focusEngine = new FocusEngine(repoPath);
36
+ this.historyManager = new HistoryManager(repoPath);
37
+ this.stateManager = new StateManager(repoPath);
33
38
  }
34
39
 
35
40
  logVerbose(message) {
@@ -269,6 +274,13 @@ class Watcher {
269
274
  try {
270
275
  logger.debug('Checking git status...');
271
276
 
277
+ // 0. Pause Check
278
+ if (this.stateManager.isPaused()) {
279
+ const state = this.stateManager.getState();
280
+ logger.debug(`Skipping processing: Autopilot is paused (${state.reason})`);
281
+ return;
282
+ }
283
+
272
284
  // 1. Min interval check
273
285
  const now = Date.now();
274
286
  const minInterval = (this.config?.minSecondsBetweenCommits || 30) * 1000;
@@ -293,15 +305,24 @@ class Watcher {
293
305
  return;
294
306
  }
295
307
 
296
- // 4. Safety: Remote check (fetch -> behind?)
297
- logger.debug('Checking remote status...');
298
- const remoteStatus = await git.isRemoteAhead(this.repoPath);
299
- if (remoteStatus.behind) {
300
- logger.warn('Skip commit: Local branch is behind remote. Please pull changes.');
308
+ // 4. Safety: Team Mode & Remote check
309
+ logger.debug('Checking team/remote status...');
310
+ const teamStatus = await checkTeamStatus(this.repoPath, this.config);
311
+ if (!teamStatus.ok) {
312
+ logger.warn('Skip commit: Team check failed (Remote ahead or conflict).');
313
+ return;
314
+ }
315
+
316
+ // 5. Safety: Pre-commit checks (Validation)
317
+ logger.debug('Running pre-commit validation...');
318
+ const validation = await validateBeforeCommit(this.repoPath, this.config);
319
+ if (!validation.ok) {
320
+ logger.warn('Skip commit: Pre-commit validation failed:');
321
+ validation.errors.forEach(e => logger.error(`- ${e}`));
301
322
  return;
302
323
  }
303
324
 
304
- // 5. Safety: Custom checks
325
+ // 6. Safety: Custom checks (Legacy)
305
326
  if (this.config?.requireChecks) {
306
327
  const checksPassed = await this.runChecks();
307
328
  if (!checksPassed) {
@@ -310,7 +331,7 @@ class Watcher {
310
331
  }
311
332
  }
312
333
 
313
- // 6. Commit
334
+ // 7. Commit
314
335
  logger.info('Committing changes...');
315
336
 
316
337
  // Add all changes
@@ -335,10 +356,32 @@ class Watcher {
335
356
  message = approval.message;
336
357
  }
337
358
 
338
- await git.commit(this.repoPath, message);
339
- this.lastCommitAt = Date.now();
340
- this.focusEngine.onCommit();
341
- logger.success('Commit complete');
359
+ const commitResult = await git.commit(this.repoPath, message);
360
+
361
+ if (commitResult.ok) {
362
+ this.lastCommitAt = Date.now();
363
+ this.focusEngine.onCommit();
364
+
365
+ // Phase 1: Record History
366
+ try {
367
+ // We need the hash of the commit we just made
368
+ const hash = await git.getLatestCommitHash(this.repoPath);
369
+ if (hash) {
370
+ this.historyManager.addCommit({
371
+ hash,
372
+ message,
373
+ files: changedFiles.map(f => f.file)
374
+ });
375
+ }
376
+ } catch (err) {
377
+ logger.error(`Failed to record history: ${err.message}`);
378
+ }
379
+
380
+ logger.success('Commit complete');
381
+ } else {
382
+ logger.error(`Commit failed: ${commitResult.stderr}`);
383
+ return;
384
+ }
342
385
 
343
386
  // 7. Auto-push
344
387
  if (this.config?.autoPush) {
package/src/index.js CHANGED
@@ -3,7 +3,13 @@ const { initRepo } = require('./commands/init');
3
3
  const { startWatcher } = require('./commands/start');
4
4
  const { stopWatcher } = require('./commands/stop');
5
5
  const { statusWatcher } = require('./commands/status');
6
+ const undoCommand = require('./commands/undo');
6
7
  const { doctor } = require('./commands/doctor');
8
+ const { insights } = require('./commands/insights');
9
+ const pauseCommand = require('./commands/pause');
10
+ const resumeCommand = require('./commands/resume');
11
+ const runDashboard = require('./commands/dashboard');
12
+ const { leaderboard } = require('./commands/leaderboard');
7
13
  const pkg = require('../package.json');
8
14
 
9
15
  function run() {
@@ -14,6 +20,12 @@ function run() {
14
20
  .description('Git automation with safety rails')
15
21
  .version(pkg.version, '-v, --version', 'Show version');
16
22
 
23
+ program
24
+ .command('leaderboard')
25
+ .description('View or sync with the global leaderboard')
26
+ .option('--sync', 'Sync your local stats to the leaderboard')
27
+ .action(leaderboard);
28
+
17
29
  program
18
30
  .command('init')
19
31
  .description('Initialize autopilot configuration in repository')
@@ -34,6 +46,27 @@ function run() {
34
46
  .description('Show autopilot watcher status')
35
47
  .action(statusWatcher);
36
48
 
49
+ program
50
+ .command('undo')
51
+ .description('Undo the last Autopilot commit')
52
+ .option('-c, --count <n>', 'Number of commits to undo', '1')
53
+ .action(undoCommand);
54
+
55
+ program
56
+ .command('pause [reason]')
57
+ .description('Pause Autopilot watcher')
58
+ .action(pauseCommand);
59
+
60
+ program
61
+ .command('resume')
62
+ .description('Resume Autopilot watcher')
63
+ .action(resumeCommand);
64
+
65
+ program
66
+ .command('dashboard')
67
+ .description('View real-time Autopilot dashboard')
68
+ .action(runDashboard);
69
+
37
70
  program
38
71
  .command('doctor')
39
72
  .description('Diagnose and validate autopilot setup')
@@ -43,6 +76,7 @@ function run() {
43
76
  .command('insights')
44
77
  .description('View productivity insights and focus analytics')
45
78
  .option('-f, --format <type>', 'Output format (json, text)', 'text')
79
+ .option('-e, --export <type>', 'Export insights (csv)')
46
80
  .action(insights);
47
81
 
48
82
  program
@@ -4,6 +4,15 @@
4
4
  */
5
5
 
6
6
  const logger = {
7
+ colors: {
8
+ cyan: (text) => `\x1b[36m${text}\x1b[0m`,
9
+ green: (text) => `\x1b[32m${text}\x1b[0m`,
10
+ yellow: (text) => `\x1b[33m${text}\x1b[0m`,
11
+ red: (text) => `\x1b[31m${text}\x1b[0m`,
12
+ blue: (text) => `\x1b[34m${text}\x1b[0m`,
13
+ bold: (text) => `\x1b[1m${text}\x1b[0m`
14
+ },
15
+
7
16
  /**
8
17
  * Log informational message
9
18
  * @param {string} message - Message to log