@traisetech/autopilot 0.1.3
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 +17 -0
- package/LICENSE +22 -0
- package/README.md +210 -0
- package/bin/autopilot.js +48 -0
- package/docs/ARCHITECTURE.md +534 -0
- package/docs/CONFIGURATION.md +82 -0
- package/docs/CONTRIBUTING.md +47 -0
- package/docs/DESIGN_DELIVERY.md +441 -0
- package/docs/DESIGN_SUMMARY.md +61 -0
- package/docs/EXTENDING.md +69 -0
- package/docs/SAFETY-FEATURES.md +56 -0
- package/docs/START_HERE.md +41 -0
- package/docs/TROUBLESHOOTING.md +40 -0
- package/package.json +59 -0
- package/src/commands/doctor.js +121 -0
- package/src/commands/init.js +92 -0
- package/src/commands/start.js +41 -0
- package/src/commands/status.js +56 -0
- package/src/commands/stop.js +50 -0
- package/src/config/defaults.js +34 -0
- package/src/config/ignore.js +37 -0
- package/src/config/loader.js +47 -0
- package/src/core/commit.js +116 -0
- package/src/core/git.js +154 -0
- package/src/core/safety.js +38 -0
- package/src/core/watcher.js +309 -0
- package/src/index.js +50 -0
- package/src/utils/banner.js +6 -0
- package/src/utils/logger.js +49 -0
- package/src/utils/paths.js +59 -0
- package/src/utils/process.js +141 -0
package/src/core/git.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git helper module - Clean, testable Git operations
|
|
3
|
+
* Built by Praise Masunga (PraiseTechzw)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { execa } = require('execa');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get current branch name
|
|
10
|
+
* @param {string} root - Repository root path
|
|
11
|
+
* @returns {Promise<string|null>} Branch name or null on error
|
|
12
|
+
*/
|
|
13
|
+
async function getBranch(root) {
|
|
14
|
+
try {
|
|
15
|
+
const { stdout } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: root });
|
|
16
|
+
return stdout.trim();
|
|
17
|
+
} catch (error) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if repository has uncommitted changes
|
|
24
|
+
* @param {string} root - Repository root path
|
|
25
|
+
* @returns {Promise<boolean>} True if there are changes
|
|
26
|
+
*/
|
|
27
|
+
async function hasChanges(root) {
|
|
28
|
+
try {
|
|
29
|
+
const { stdout } = await execa('git', ['status', '--porcelain'], { cwd: root });
|
|
30
|
+
return stdout.trim().length > 0;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get porcelain status - parsed list of changed files
|
|
38
|
+
* @param {string} root - Repository root path
|
|
39
|
+
* @returns {Promise<{ok: boolean, files: string[], raw: string}>} Status object
|
|
40
|
+
*/
|
|
41
|
+
async function getPorcelainStatus(root) {
|
|
42
|
+
try {
|
|
43
|
+
const { stdout } = await execa('git', ['status', '--porcelain'], { cwd: root });
|
|
44
|
+
const raw = stdout.trim();
|
|
45
|
+
|
|
46
|
+
if (!raw) {
|
|
47
|
+
return { ok: true, files: [], raw: '' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const files = raw
|
|
51
|
+
.split(/\r?\n/)
|
|
52
|
+
.map(line => line.slice(3).trim()); // Remove status prefix (XY + space)
|
|
53
|
+
|
|
54
|
+
return { ok: true, files, raw };
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return { ok: false, files: [], raw: error.message };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Stage all changes (git add -A)
|
|
62
|
+
* @param {string} root - Repository root path
|
|
63
|
+
* @returns {Promise<{ok: boolean, stdout: string, stderr: string}>} Result object
|
|
64
|
+
*/
|
|
65
|
+
async function addAll(root) {
|
|
66
|
+
try {
|
|
67
|
+
const { stdout, stderr } = await execa('git', ['add', '-A'], { cwd: root });
|
|
68
|
+
return { ok: true, stdout, stderr };
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return { ok: false, stdout: '', stderr: error.message };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Commit staged changes
|
|
76
|
+
* @param {string} root - Repository root path
|
|
77
|
+
* @param {string} message - Commit message
|
|
78
|
+
* @returns {Promise<{ok: boolean, stdout: string, stderr: string}>} Result object
|
|
79
|
+
*/
|
|
80
|
+
async function commit(root, message) {
|
|
81
|
+
try {
|
|
82
|
+
const { stdout, stderr } = await execa('git', ['commit', '-m', message], { cwd: root });
|
|
83
|
+
return { ok: true, stdout, stderr };
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return { ok: false, stdout: '', stderr: error.message };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Fetch updates from remote
|
|
91
|
+
* @param {string} root - Repository root path
|
|
92
|
+
* @returns {Promise<{ok: boolean, stdout: string, stderr: string}>} Result object
|
|
93
|
+
*/
|
|
94
|
+
async function fetch(root) {
|
|
95
|
+
try {
|
|
96
|
+
const { stdout, stderr } = await execa('git', ['fetch'], { cwd: root });
|
|
97
|
+
return { ok: true, stdout, stderr };
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return { ok: false, stdout: '', stderr: error.message };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check if remote is ahead/behind
|
|
105
|
+
* @param {string} root - Repository root path
|
|
106
|
+
* @returns {Promise<{ok: boolean, ahead: boolean, behind: boolean, raw: string}>} Status object
|
|
107
|
+
*/
|
|
108
|
+
async function isRemoteAhead(root) {
|
|
109
|
+
try {
|
|
110
|
+
const branch = await getBranch(root);
|
|
111
|
+
if (!branch) return { ok: false, ahead: false, behind: false, raw: 'No branch' };
|
|
112
|
+
|
|
113
|
+
// Ensure we have latest info
|
|
114
|
+
await fetch(root);
|
|
115
|
+
|
|
116
|
+
const { stdout } = await execa('git', ['rev-list', '--left-right', '--count', `${branch}...origin/${branch}`], { cwd: root });
|
|
117
|
+
const [aheadCount, behindCount] = stdout.trim().split(/\s+/).map(Number);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
ok: true,
|
|
121
|
+
ahead: aheadCount > 0,
|
|
122
|
+
behind: behindCount > 0,
|
|
123
|
+
raw: stdout.trim()
|
|
124
|
+
};
|
|
125
|
+
} catch (error) {
|
|
126
|
+
return { ok: false, ahead: false, behind: false, raw: error.message };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Push changes to remote
|
|
132
|
+
* @param {string} root - Repository root path
|
|
133
|
+
* @param {string} branch - Branch to push
|
|
134
|
+
* @returns {Promise<{ok: boolean, stdout: string, stderr: string}>} Result object
|
|
135
|
+
*/
|
|
136
|
+
async function push(root, branch) {
|
|
137
|
+
try {
|
|
138
|
+
const { stdout, stderr } = await execa('git', ['push', 'origin', branch], { cwd: root });
|
|
139
|
+
return { ok: true, stdout, stderr };
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return { ok: false, stdout: '', stderr: error.message };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = {
|
|
146
|
+
getBranch,
|
|
147
|
+
hasChanges,
|
|
148
|
+
getPorcelainStatus,
|
|
149
|
+
addAll,
|
|
150
|
+
commit,
|
|
151
|
+
fetch,
|
|
152
|
+
isRemoteAhead,
|
|
153
|
+
push,
|
|
154
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autopilot Watcher Engine
|
|
3
|
+
* Built by Praise Masunga (PraiseTechzw)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs-extra');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const chokidar = require('chokidar');
|
|
9
|
+
const { execa } = require('execa');
|
|
10
|
+
const logger = require('../utils/logger');
|
|
11
|
+
const git = require('./git');
|
|
12
|
+
const { generateCommitMessage } = require('./commit');
|
|
13
|
+
const { getIgnorePath } = require('../utils/paths');
|
|
14
|
+
const { savePid, removePid, registerProcessHandlers } = require('../utils/process');
|
|
15
|
+
const { loadConfig } = require('../config/loader');
|
|
16
|
+
|
|
17
|
+
class Watcher {
|
|
18
|
+
constructor(repoPath) {
|
|
19
|
+
this.repoPath = repoPath;
|
|
20
|
+
this.config = null;
|
|
21
|
+
this.watcher = null;
|
|
22
|
+
this.isWatching = false;
|
|
23
|
+
this.isProcessing = false;
|
|
24
|
+
this.debounceTimer = null;
|
|
25
|
+
this.lastCommitAt = 0;
|
|
26
|
+
this.logFilePath = path.join(repoPath, 'autopilot.log');
|
|
27
|
+
this.ignorePatterns = [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Initialize and start the watcher
|
|
32
|
+
*/
|
|
33
|
+
async start() {
|
|
34
|
+
try {
|
|
35
|
+
if (this.isWatching) {
|
|
36
|
+
logger.warn('Watcher is already running');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Initialize environment
|
|
41
|
+
await fs.ensureFile(this.logFilePath);
|
|
42
|
+
await savePid(this.repoPath);
|
|
43
|
+
|
|
44
|
+
this.logVerbose('Starting Autopilot watcher...');
|
|
45
|
+
|
|
46
|
+
// Load configuration
|
|
47
|
+
await this.reloadConfig();
|
|
48
|
+
await this.reloadIgnore();
|
|
49
|
+
|
|
50
|
+
// Initial safety check
|
|
51
|
+
const currentBranch = await git.getBranch(this.repoPath);
|
|
52
|
+
if (currentBranch && this.config.blockedBranches?.includes(currentBranch)) {
|
|
53
|
+
logger.error(`Branch '${currentBranch}' is blocked in config. Stopping.`);
|
|
54
|
+
this.logVerbose(`Blocked branch detected: ${currentBranch}`);
|
|
55
|
+
await this.stop();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Combine defaults + loaded patterns
|
|
60
|
+
const finalIgnored = [
|
|
61
|
+
...this.ignorePatterns,
|
|
62
|
+
/(^|[\/\\])\.git([\/\\]|$)/, // Regex for .git folder
|
|
63
|
+
'**/autopilot.log',
|
|
64
|
+
'**/.autopilot.pid',
|
|
65
|
+
'node_modules' // Sensible default
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// Start Chokidar
|
|
69
|
+
this.watcher = chokidar.watch(this.repoPath, {
|
|
70
|
+
ignored: finalIgnored,
|
|
71
|
+
ignoreInitial: true,
|
|
72
|
+
persistent: true,
|
|
73
|
+
awaitWriteFinish: {
|
|
74
|
+
stabilityThreshold: 1000,
|
|
75
|
+
pollInterval: 100,
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
this.watcher
|
|
80
|
+
.on('add', (path) => this.onFsEvent('add', path))
|
|
81
|
+
.on('change', (path) => this.onFsEvent('change', path))
|
|
82
|
+
.on('unlink', (path) => this.onFsEvent('unlink', path))
|
|
83
|
+
.on('error', (error) => this.handleError(error));
|
|
84
|
+
|
|
85
|
+
// Handle process signals
|
|
86
|
+
registerProcessHandlers(async () => {
|
|
87
|
+
await this.stop();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
this.isWatching = true;
|
|
91
|
+
logger.success(`Autopilot is watching ${this.repoPath}`);
|
|
92
|
+
logger.info(`Logs: ${this.logFilePath}`);
|
|
93
|
+
|
|
94
|
+
} catch (error) {
|
|
95
|
+
logger.error(`Failed to start watcher: ${error.message}`);
|
|
96
|
+
this.logVerbose(`Start error: ${error.stack}`);
|
|
97
|
+
await this.stop();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Stop the watcher
|
|
103
|
+
*/
|
|
104
|
+
async stop() {
|
|
105
|
+
try {
|
|
106
|
+
if (this.debounceTimer) {
|
|
107
|
+
clearTimeout(this.debounceTimer);
|
|
108
|
+
this.debounceTimer = null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (this.watcher) {
|
|
112
|
+
await this.watcher.close();
|
|
113
|
+
this.watcher = null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
await removePid(this.repoPath);
|
|
117
|
+
this.isWatching = false;
|
|
118
|
+
logger.info('Watcher stopped');
|
|
119
|
+
} catch (error) {
|
|
120
|
+
logger.error(`Error stopping watcher: ${error.message}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Handle filesystem events
|
|
126
|
+
*/
|
|
127
|
+
onFsEvent(type, filePath) {
|
|
128
|
+
if (this.isProcessing) return;
|
|
129
|
+
|
|
130
|
+
// Double check ignore (safety net)
|
|
131
|
+
if (filePath.includes('.git') || filePath.endsWith('autopilot.log')) return;
|
|
132
|
+
|
|
133
|
+
this.logVerbose(`File event: ${type} ${filePath}`);
|
|
134
|
+
this.scheduleProcess();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Schedule processing with debounce
|
|
139
|
+
*/
|
|
140
|
+
scheduleProcess() {
|
|
141
|
+
const debounceMs = (this.config?.debounceSeconds || 5) * 1000;
|
|
142
|
+
|
|
143
|
+
if (this.debounceTimer) {
|
|
144
|
+
clearTimeout(this.debounceTimer);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.debounceTimer = setTimeout(() => {
|
|
148
|
+
this.processChanges();
|
|
149
|
+
}, debounceMs);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Main processing loop
|
|
154
|
+
*/
|
|
155
|
+
async processChanges() {
|
|
156
|
+
if (this.isProcessing) return;
|
|
157
|
+
this.isProcessing = true;
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
// 1. Min interval check
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
const minInterval = (this.config?.minIntervalSeconds || 30) * 1000;
|
|
163
|
+
if (now - this.lastCommitAt < minInterval) {
|
|
164
|
+
this.logVerbose('Skipping: Minimum interval not met');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 2. Safety: Branch check
|
|
169
|
+
const branch = await git.getBranch(this.repoPath);
|
|
170
|
+
if (this.config?.blockedBranches?.includes(branch)) {
|
|
171
|
+
logger.warn(`Current branch '${branch}' is blocked. Skipping.`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 3. Safety: Remote check (fetch -> behind?)
|
|
176
|
+
this.logVerbose('Checking remote status...');
|
|
177
|
+
const remoteStatus = await git.isRemoteAhead(this.repoPath);
|
|
178
|
+
if (remoteStatus.behind) {
|
|
179
|
+
logger.warn('Local branch is behind remote. Please pull changes.');
|
|
180
|
+
this.logVerbose('Behind remote. Pausing auto-commit.');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 4. Safety: Custom checks
|
|
185
|
+
if (this.config?.requireChecks) {
|
|
186
|
+
const checksPassed = await this.runChecks();
|
|
187
|
+
if (!checksPassed) {
|
|
188
|
+
logger.warn('Checks failed. Skipping commit.');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 5. Flow: Status -> Add -> Commit -> Push
|
|
194
|
+
const status = await git.getPorcelainStatus(this.repoPath);
|
|
195
|
+
if (!status.ok || status.files.length === 0) {
|
|
196
|
+
this.logVerbose('No changes to commit');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this.logVerbose(`Detecting ${status.files.length} changed files`);
|
|
201
|
+
|
|
202
|
+
// Add all
|
|
203
|
+
await git.addAll(this.repoPath);
|
|
204
|
+
|
|
205
|
+
// Generate message
|
|
206
|
+
const message = generateCommitMessage(status.files);
|
|
207
|
+
this.logVerbose(`Generated message: ${message}`);
|
|
208
|
+
|
|
209
|
+
// Commit
|
|
210
|
+
const commitResult = await git.commit(this.repoPath, message);
|
|
211
|
+
if (commitResult.ok) {
|
|
212
|
+
logger.success(`Committed: ${message}`);
|
|
213
|
+
this.lastCommitAt = Date.now();
|
|
214
|
+
|
|
215
|
+
// Push if enabled
|
|
216
|
+
if (this.config?.autoPush) {
|
|
217
|
+
logger.info('Pushing to remote...');
|
|
218
|
+
const pushResult = await git.push(this.repoPath, branch);
|
|
219
|
+
if (pushResult.ok) {
|
|
220
|
+
logger.success('Pushed successfully');
|
|
221
|
+
} else {
|
|
222
|
+
logger.error(`Push failed: ${pushResult.stderr}`);
|
|
223
|
+
this.logVerbose(`Push error: ${pushResult.stderr}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
logger.error(`Commit failed: ${commitResult.stderr}`);
|
|
228
|
+
this.logVerbose(`Commit error: ${commitResult.stderr}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
} catch (error) {
|
|
232
|
+
logger.error(`Process error: ${error.message}`);
|
|
233
|
+
this.logVerbose(`Process exception: ${error.stack}`);
|
|
234
|
+
} finally {
|
|
235
|
+
this.isProcessing = false;
|
|
236
|
+
this.debounceTimer = null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Run user-defined checks
|
|
242
|
+
*/
|
|
243
|
+
async runChecks() {
|
|
244
|
+
const checks = this.config?.checks || [];
|
|
245
|
+
if (checks.length === 0) return true;
|
|
246
|
+
|
|
247
|
+
this.logVerbose(`Running checks: ${checks.join(', ')}`);
|
|
248
|
+
|
|
249
|
+
for (const cmd of checks) {
|
|
250
|
+
try {
|
|
251
|
+
logger.info(`Running check: ${cmd}`);
|
|
252
|
+
await execa(cmd, { cwd: this.repoPath, shell: true });
|
|
253
|
+
} catch (error) {
|
|
254
|
+
logger.error(`Check failed: ${cmd}`);
|
|
255
|
+
this.logVerbose(`Check output: ${error.message}`);
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Reload configuration
|
|
264
|
+
*/
|
|
265
|
+
async reloadConfig() {
|
|
266
|
+
this.config = await loadConfig(this.repoPath);
|
|
267
|
+
this.logVerbose('Config reloaded');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Reload ignore patterns
|
|
272
|
+
*/
|
|
273
|
+
async reloadIgnore() {
|
|
274
|
+
const ignorePath = getIgnorePath(this.repoPath);
|
|
275
|
+
this.ignorePatterns = [];
|
|
276
|
+
|
|
277
|
+
if (await fs.pathExists(ignorePath)) {
|
|
278
|
+
try {
|
|
279
|
+
const content = await fs.readFile(ignorePath, 'utf-8');
|
|
280
|
+
const lines = content.split('\n')
|
|
281
|
+
.map(l => l.trim())
|
|
282
|
+
.filter(l => l && !l.startsWith('#'));
|
|
283
|
+
|
|
284
|
+
this.ignorePatterns.push(...lines);
|
|
285
|
+
this.logVerbose(`Loaded ${lines.length} ignore patterns`);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
logger.warn(`Failed to load ignore file: ${error.message}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Also load ignore patterns from config json if they exist
|
|
292
|
+
if (this.config?.ignore && Array.isArray(this.config.ignore)) {
|
|
293
|
+
this.ignorePatterns.push(...this.config.ignore);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
handleError(error) {
|
|
298
|
+
logger.error(`Watcher error: ${error.message}`);
|
|
299
|
+
this.logVerbose(`Watcher error: ${error.stack}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
logVerbose(message) {
|
|
303
|
+
const timestamp = new Date().toISOString();
|
|
304
|
+
const logLine = `[${timestamp}] ${message}\n`;
|
|
305
|
+
fs.appendFile(this.logFilePath, logLine).catch(() => {});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
module.exports = Watcher;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const { initRepo } = require('./commands/init');
|
|
3
|
+
const { startWatcher } = require('./commands/start');
|
|
4
|
+
const { stopWatcher } = require('./commands/stop');
|
|
5
|
+
const { statusWatcher } = require('./commands/status');
|
|
6
|
+
const { doctor } = require('./commands/doctor');
|
|
7
|
+
const pkg = require('../package.json');
|
|
8
|
+
|
|
9
|
+
function run() {
|
|
10
|
+
const program = new Command();
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.name('autopilot')
|
|
14
|
+
.description('Git automation with safety rails')
|
|
15
|
+
.version(pkg.version, '-v, --version', 'Show version');
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.command('init')
|
|
19
|
+
.description('Initialize autopilot configuration in repository')
|
|
20
|
+
.action(initRepo);
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.command('start')
|
|
24
|
+
.description('Start autopilot watcher in foreground')
|
|
25
|
+
.action(startWatcher);
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command('stop')
|
|
29
|
+
.description('Stop the running autopilot watcher')
|
|
30
|
+
.action(stopWatcher);
|
|
31
|
+
|
|
32
|
+
program
|
|
33
|
+
.command('status')
|
|
34
|
+
.description('Show autopilot watcher status')
|
|
35
|
+
.action(statusWatcher);
|
|
36
|
+
|
|
37
|
+
program
|
|
38
|
+
.command('doctor')
|
|
39
|
+
.description('Diagnose and validate autopilot setup')
|
|
40
|
+
.action(doctor);
|
|
41
|
+
|
|
42
|
+
program
|
|
43
|
+
.addHelpText('after', '\nBuilt by Praise Masunga (PraiseTechzw)')
|
|
44
|
+
.addHelpCommand(true, 'Show help for command')
|
|
45
|
+
.showHelpAfterError('(add --help for command information)');
|
|
46
|
+
|
|
47
|
+
program.parse(process.argv);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { run };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple logger utility for consistent CLI output
|
|
3
|
+
* Built by Praise Masunga (PraiseTechzw)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const logger = {
|
|
7
|
+
/**
|
|
8
|
+
* Log informational message
|
|
9
|
+
* @param {string} message - Message to log
|
|
10
|
+
*/
|
|
11
|
+
info: (message) => {
|
|
12
|
+
console.log(`ā¹ļø ${message}`);
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Log success message
|
|
17
|
+
* @param {string} message - Message to log
|
|
18
|
+
*/
|
|
19
|
+
success: (message) => {
|
|
20
|
+
console.log(`ā
${message}`);
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Log warning message
|
|
25
|
+
* @param {string} message - Message to log
|
|
26
|
+
*/
|
|
27
|
+
warn: (message) => {
|
|
28
|
+
console.warn(`ā ļø ${message}`);
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Log error message
|
|
33
|
+
* @param {string} message - Message to log
|
|
34
|
+
*/
|
|
35
|
+
error: (message) => {
|
|
36
|
+
console.error(`ā ${message}`);
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Log section header
|
|
41
|
+
* @param {string} title - Section title
|
|
42
|
+
*/
|
|
43
|
+
section: (title) => {
|
|
44
|
+
console.log(`\n${title}`);
|
|
45
|
+
console.log('ā'.repeat(50));
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
module.exports = logger;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path utilities for Autopilot
|
|
3
|
+
* Built by Praise Masunga (PraiseTechzw)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const fs = require('fs-extra');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get path to config file in repository
|
|
12
|
+
* @param {string} repoPath - Repository path
|
|
13
|
+
* @returns {string} Config file path
|
|
14
|
+
*/
|
|
15
|
+
const getConfigPath = (repoPath) => {
|
|
16
|
+
return path.join(repoPath, '.autopilotrc.json');
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get path to ignore file in repository
|
|
21
|
+
* @param {string} repoPath - Repository path
|
|
22
|
+
* @returns {string} Ignore file path
|
|
23
|
+
*/
|
|
24
|
+
const getIgnorePath = (repoPath) => {
|
|
25
|
+
return path.join(repoPath, '.autopilotignore');
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get path to .git directory
|
|
30
|
+
* @param {string} repoPath - Repository path
|
|
31
|
+
* @returns {string} .git directory path
|
|
32
|
+
*/
|
|
33
|
+
const getGitPath = (repoPath) => {
|
|
34
|
+
return path.join(repoPath, '.git');
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get global config directory
|
|
39
|
+
* @returns {string} Config directory path
|
|
40
|
+
*/
|
|
41
|
+
const getConfigDir = () => {
|
|
42
|
+
return path.join(os.homedir(), '.autopilot');
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Ensure config directory exists
|
|
47
|
+
* @returns {Promise<void>}
|
|
48
|
+
*/
|
|
49
|
+
const ensureConfigDir = async () => {
|
|
50
|
+
await fs.ensureDir(getConfigDir());
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
getConfigPath,
|
|
55
|
+
getIgnorePath,
|
|
56
|
+
getGitPath,
|
|
57
|
+
getConfigDir,
|
|
58
|
+
ensureConfigDir,
|
|
59
|
+
};
|