@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.
- package/CHANGELOG.md +25 -9
- package/README.md +215 -106
- package/bin/autopilot.js +1 -1
- package/docs/CONFIGURATION.md +103 -103
- package/docs/DESIGN_PRINCIPLES.md +114 -114
- package/docs/TEAM-MODE.md +51 -51
- package/docs/TROUBLESHOOTING.md +21 -21
- package/package.json +75 -69
- package/src/commands/config.js +110 -110
- package/src/commands/dashboard.mjs +151 -151
- package/src/commands/doctor.js +127 -153
- package/src/commands/init.js +8 -9
- package/src/commands/insights.js +237 -237
- package/src/commands/leaderboard.js +116 -116
- package/src/commands/pause.js +18 -18
- package/src/commands/preset.js +121 -121
- package/src/commands/resume.js +17 -17
- package/src/commands/start.js +41 -41
- package/src/commands/status.js +73 -39
- package/src/commands/stop.js +58 -50
- package/src/commands/undo.js +84 -84
- package/src/config/defaults.js +23 -16
- package/src/config/ignore.js +14 -31
- package/src/config/loader.js +80 -80
- package/src/core/commit.js +45 -52
- package/src/core/commitMessageGenerator.js +130 -0
- package/src/core/configValidator.js +92 -0
- package/src/core/events.js +110 -110
- package/src/core/focus.js +2 -1
- package/src/core/gemini.js +15 -15
- package/src/core/git.js +29 -2
- package/src/core/history.js +69 -69
- package/src/core/notifier.js +61 -0
- package/src/core/retryQueue.js +152 -0
- package/src/core/safety.js +224 -210
- package/src/core/state.js +69 -71
- package/src/core/watcher.js +193 -66
- package/src/index.js +70 -70
- package/src/utils/banner.js +6 -6
- package/src/utils/crypto.js +18 -18
- package/src/utils/identity.js +41 -41
- package/src/utils/logger.js +86 -68
- package/src/utils/paths.js +62 -62
- package/src/utils/process.js +141 -141
package/src/core/safety.js
CHANGED
|
@@ -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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
*
|
|
147
|
-
* @param {
|
|
148
|
-
* @
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
await git.
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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.
|
|
12
|
-
this.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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;
|