@traisetech/autopilot 0.1.4 → 0.1.6
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 +5 -0
- package/README.md +1 -226
- package/docs/TROUBLESHOOTING.md +21 -40
- package/package.json +2 -2
- package/src/commands/init.js +37 -0
- package/src/config/defaults.js +2 -0
- package/src/config/ignore.js +136 -37
- package/src/core/watcher.js +90 -135
package/CHANGELOG.md
CHANGED
|
@@ -25,11 +25,16 @@ This project follows Semantic Versioning (https://semver.org).
|
|
|
25
25
|
## [0.1.4] – 2026-02-01
|
|
26
26
|
|
|
27
27
|
### Fixed
|
|
28
|
+
- **Windows Compatibility**: Fixed critical issue where absolute paths on Windows caused ignore rules to fail.
|
|
29
|
+
- **Watcher Noise**: Fixed infinite commit loops caused by `.vscode/time-analytics.json` and self-logging.
|
|
28
30
|
- Fixed a critical CLI crash where `autopilot start` failed due to miswired Commander action handlers.
|
|
29
31
|
- Improved command registration to ensure all CLI commands are correctly bound and validated at runtime.
|
|
30
32
|
- Prevented undefined command handlers from causing runtime exceptions.
|
|
31
33
|
|
|
32
34
|
### Added
|
|
35
|
+
- **Release Gates**: Added `npm run verify` and `prepublishOnly` hooks to prevent broken releases.
|
|
36
|
+
- **Integration Tests**: Added full end-to-end test suite using `node:test`.
|
|
37
|
+
- **Smart Init**: `autopilot init` now automatically adds `autopilot.log` to `.gitignore`.
|
|
33
38
|
- Pre-publish verification pipeline to block publishing broken builds.
|
|
34
39
|
- CLI smoke tests to ensure core commands (`init`, `start`, `status`, `doctor`) do not crash.
|
|
35
40
|
- Test-only dry-run mode for watcher to allow safe automated testing.
|
package/README.md
CHANGED
|
@@ -1,227 +1,2 @@
|
|
|
1
|
-
# 🚀 Autopilot
|
|
2
1
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-

|
|
6
|
-
|
|
7
|
-
**Intelligent Git automation that commits and pushes your code, so you can focus on building.**
|
|
8
|
-
|
|
9
|
-
[](https://www.npmjs.com/package/@traisetech/autopilot)
|
|
10
|
-
[](https://opensource.org/licenses/MIT)
|
|
11
|
-
[](https://nodejs.org)
|
|
12
|
-
[](https://www.npmjs.com/package/@traisetech/autopilot)
|
|
13
|
-
[](https://github.com/PraiseTechzw/autopilot/stargazers)
|
|
14
|
-
[](https://github.com/PraiseTechzw/autopilot/actions)
|
|
15
|
-
[](http://makeapullrequest.com)
|
|
16
|
-
|
|
17
|
-
**Built by [Praise Masunga](https://github.com/PraiseTechzw) (PraiseTechzw)**
|
|
18
|
-
|
|
19
|
-
[Features](#-features) • [Installation](#-installation) • [Quick Start](#-quick-start) • [Configuration](#-configuration) • [Commands](#-commands)
|
|
20
|
-
|
|
21
|
-
</div>
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## 📖 Table of Contents
|
|
26
|
-
|
|
27
|
-
- [Why Autopilot?](#-why-autopilot)
|
|
28
|
-
- [Features](#-features)
|
|
29
|
-
- [Installation](#-installation)
|
|
30
|
-
- [Quick Start](#-quick-start)
|
|
31
|
-
- [Commands](#-commands)
|
|
32
|
-
- [Configuration](#-configuration)
|
|
33
|
-
- [Safety Features](#-safety-features)
|
|
34
|
-
- [Troubleshooting](#-troubleshooting)
|
|
35
|
-
- [Contributing](#-contributing)
|
|
36
|
-
- [License](#-license)
|
|
37
|
-
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
## 🎯 Why Autopilot?
|
|
41
|
-
|
|
42
|
-
Autopilot is designed for developers who want to stay in the flow. It solves manual Git workflow fatigue by handling the repetitive cycle of staging, committing, and pushing changes, allowing you to focus entirely on writing code.
|
|
43
|
-
|
|
44
|
-
<table>
|
|
45
|
-
<tr>
|
|
46
|
-
<td width="50%">
|
|
47
|
-
|
|
48
|
-
### ❌ Before Autopilot
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
# Every. Single. Time.
|
|
52
|
-
git add .
|
|
53
|
-
git commit -m "update stuff"
|
|
54
|
-
git push
|
|
55
|
-
|
|
56
|
-
# Repeat 50+ times a day...
|
|
57
|
-
# Lose focus on coding
|
|
58
|
-
# Forget to commit
|
|
59
|
-
# Inconsistent messages
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
</td>
|
|
63
|
-
<td width="50%">
|
|
64
|
-
|
|
65
|
-
### ✅ With Autopilot
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
# One time setup
|
|
69
|
-
autopilot init
|
|
70
|
-
autopilot start
|
|
71
|
-
|
|
72
|
-
# That's it!
|
|
73
|
-
# Focus on coding
|
|
74
|
-
# Auto-commits with smart messages
|
|
75
|
-
# Never lose work again
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
</td>
|
|
79
|
-
</tr>
|
|
80
|
-
</table>
|
|
81
|
-
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
## ✨ Features
|
|
85
|
-
|
|
86
|
-
- **🧠 Smart Commits**: Generates professional conventional commit messages automatically.
|
|
87
|
-
- **⚡ Watcher Engine**: Real-time file monitoring with smart debouncing using `chokidar`.
|
|
88
|
-
- **🛡️ Safety First**: Blocks commits on protected branches and checks remote status.
|
|
89
|
-
- **🔄 Automated Flow**: Fetches, stages, commits, and pushes (optional) automatically.
|
|
90
|
-
- **⚙️ Zero Config**: Works out of the box, but fully configurable via `.autopilotrc.json`.
|
|
91
|
-
- **🩺 Self-Healing**: Includes a `doctor` command to diagnose and fix issues.
|
|
92
|
-
|
|
93
|
-
---
|
|
94
|
-
|
|
95
|
-
## ⬇️ Installation
|
|
96
|
-
|
|
97
|
-
Install Autopilot globally using npm:
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
npm install -g @traisetech/autopilot
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
## 🚀 Quick Start
|
|
106
|
-
|
|
107
|
-
### 1. Initialize
|
|
108
|
-
Navigate to your Git repository and initialize Autopilot:
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
cd my-project
|
|
112
|
-
autopilot init
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
### 2. Start Watching
|
|
116
|
-
Start the background daemon to begin monitoring your files:
|
|
117
|
-
|
|
118
|
-
```bash
|
|
119
|
-
autopilot start
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
### 3. Check Status
|
|
123
|
-
Verify that Autopilot is running:
|
|
124
|
-
|
|
125
|
-
```bash
|
|
126
|
-
autopilot status
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### 4. Stop
|
|
130
|
-
When you're done for the day:
|
|
131
|
-
|
|
132
|
-
```bash
|
|
133
|
-
autopilot stop
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
---
|
|
137
|
-
|
|
138
|
-
## 💻 Commands
|
|
139
|
-
|
|
140
|
-
| Command | Description |
|
|
141
|
-
|---------|-------------|
|
|
142
|
-
| `autopilot init` | Initializes configuration and ignore files in the current directory. |
|
|
143
|
-
| `autopilot start` | Starts the background watcher daemon. |
|
|
144
|
-
| `autopilot stop` | Stops the running watcher daemon. |
|
|
145
|
-
| `autopilot status` | Shows the current status of the watcher process. |
|
|
146
|
-
| `autopilot doctor` | Runs diagnostics to verify environment and configuration. |
|
|
147
|
-
| `autopilot --help` | Displays help information. |
|
|
148
|
-
|
|
149
|
-
---
|
|
150
|
-
|
|
151
|
-
## ⚙️ Configuration
|
|
152
|
-
|
|
153
|
-
Autopilot uses a `.autopilotrc.json` file for configuration. It is created automatically when you run `autopilot init`.
|
|
154
|
-
|
|
155
|
-
```json
|
|
156
|
-
{
|
|
157
|
-
"minInterval": 30,
|
|
158
|
-
"autoPush": true,
|
|
159
|
-
"blockedBranches": ["main", "production"],
|
|
160
|
-
"requireChecks": false,
|
|
161
|
-
"ignore": [
|
|
162
|
-
"*.log",
|
|
163
|
-
"temp/",
|
|
164
|
-
"dist/",
|
|
165
|
-
"node_modules"
|
|
166
|
-
]
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
| Option | Type | Default | Description |
|
|
171
|
-
|--------|------|---------|-------------|
|
|
172
|
-
| `minInterval` | `number` | `30` | Minimum seconds between commits. |
|
|
173
|
-
| `autoPush` | `boolean` | `true` | Whether to push changes automatically after commit. |
|
|
174
|
-
| `blockedBranches` | `array` | `[]` | List of branches to disable auto-commit on. |
|
|
175
|
-
| `requireChecks` | `boolean` | `false` | Run custom checks before committing. |
|
|
176
|
-
| `ignore` | `array` | `[]` | Additional glob patterns to ignore. |
|
|
177
|
-
|
|
178
|
-
---
|
|
179
|
-
|
|
180
|
-
## 🛡️ Safety Features
|
|
181
|
-
|
|
182
|
-
Autopilot includes several safety mechanisms to prevent accidents:
|
|
183
|
-
|
|
184
|
-
1. **Branch Protection**: Will not run on branches listed in `blockedBranches`.
|
|
185
|
-
2. **Remote Sync**: Checks if local branch is behind remote before acting.
|
|
186
|
-
3. **Debouncing**: Waits for file changes to settle before committing.
|
|
187
|
-
4. **PID Management**: Ensures only one instance runs per repository.
|
|
188
|
-
|
|
189
|
-
---
|
|
190
|
-
|
|
191
|
-
## 🔧 Troubleshooting
|
|
192
|
-
|
|
193
|
-
If you encounter issues, run the doctor command:
|
|
194
|
-
|
|
195
|
-
```bash
|
|
196
|
-
autopilot doctor
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
This will check for:
|
|
200
|
-
- Git repository status
|
|
201
|
-
- Configuration validity
|
|
202
|
-
- Node.js version
|
|
203
|
-
- Permissions
|
|
204
|
-
|
|
205
|
-
---
|
|
206
|
-
|
|
207
|
-
## 🤝 Contributing
|
|
208
|
-
|
|
209
|
-
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
210
|
-
|
|
211
|
-
1. Fork the repository
|
|
212
|
-
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
|
213
|
-
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
|
214
|
-
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
|
215
|
-
5. Open a Pull Request
|
|
216
|
-
|
|
217
|
-
---
|
|
218
|
-
|
|
219
|
-
## 📜 License
|
|
220
|
-
|
|
221
|
-
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
222
|
-
|
|
223
|
-
---
|
|
224
|
-
|
|
225
|
-
<div align="center">
|
|
226
|
-
<b>Built by <a href="https://github.com/PraiseTechzw">Praise Masunga (PraiseTechzw)</a></b>
|
|
227
|
-
</div>
|
|
2
|
+
<!-- Autopilot test run: 2026-02-01 -->
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -1,40 +1,21 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## It won’t push
|
|
26
|
-
|
|
27
|
-
- Verify `autoPush` is true
|
|
28
|
-
- Check your `origin` remote and authentication
|
|
29
|
-
- Ensure you have access to the remote
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
## Logs
|
|
34
|
-
|
|
35
|
-
- `autopilot.log` is created in the repo root
|
|
36
|
-
- `autopilot status` shows the last log line
|
|
37
|
-
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
**Built by Praise Masunga (PraiseTechzw)**
|
|
1
|
+
# TROUBLESHOOTING
|
|
2
|
+
|
|
3
|
+
## Common Issues
|
|
4
|
+
|
|
5
|
+
### Watcher Loop / High CPU Usage (Windows)
|
|
6
|
+
If Autopilot seems to be constantly triggering or consuming high CPU on Windows, it is likely due to "noisy" files that change frequently but should be ignored.
|
|
7
|
+
|
|
8
|
+
**Common Culprits:**
|
|
9
|
+
- `.vscode/time-analytics.json`: Some VS Code extensions write to this file constantly.
|
|
10
|
+
- `autopilot.log`: If the watcher logs to a file that it is also watching, it creates a feedback loop.
|
|
11
|
+
|
|
12
|
+
**Solution:**
|
|
13
|
+
Autopilot v0.1.4+ includes enhanced Windows path handling to automatically ignore these files. If you still see issues:
|
|
14
|
+
1. Ensure `.vscode/` and `*.log` are in your `.autopilotignore` file.
|
|
15
|
+
2. Run `autopilot doctor` to verify your configuration.
|
|
16
|
+
|
|
17
|
+
### "git.status is not a function"
|
|
18
|
+
This was a known issue in older versions. Please upgrade to the latest version.
|
|
19
|
+
|
|
20
|
+
### Permissions Errors
|
|
21
|
+
Ensure you have write access to the repository and that no other process has locked the files (common on Windows with anti-virus software).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@traisetech/autopilot",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"dev": "node bin/autopilot.js",
|
|
35
35
|
"test": "node --test",
|
|
36
36
|
"lint": "node -c bin/autopilot.js && node -c src/index.js",
|
|
37
|
-
"verify": "
|
|
37
|
+
"verify": "node bin/autopilot.js --help && npm run test && node bin/autopilot.js doctor",
|
|
38
38
|
"prepublishOnly": "npm run verify",
|
|
39
39
|
"release:patch": "npm run verify && npm version patch && git push --follow-tags && echo \"\n🚀 Ready to publish! Run: npm publish --access public\""
|
|
40
40
|
},
|
package/src/commands/init.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const fs = require('fs-extra');
|
|
7
|
+
const path = require('path');
|
|
7
8
|
const logger = require('../utils/logger');
|
|
8
9
|
const { getConfigPath, getIgnorePath, getGitPath } = require('../utils/paths');
|
|
9
10
|
const { DEFAULT_CONFIG, DEFAULT_IGNORE_PATTERNS } = require('../config/defaults');
|
|
@@ -54,6 +55,41 @@ async function createConfigFile(repoPath) {
|
|
|
54
55
|
return true;
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Update .gitignore with Autopilot specific files
|
|
60
|
+
* @param {string} repoPath
|
|
61
|
+
*/
|
|
62
|
+
async function updateGitIgnore(repoPath) {
|
|
63
|
+
const gitIgnorePath = path.join(repoPath, '.gitignore');
|
|
64
|
+
const toIgnore = ['autopilot.log', '.autopilot.pid'];
|
|
65
|
+
let content = '';
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
if (await fs.pathExists(gitIgnorePath)) {
|
|
69
|
+
content = await fs.readFile(gitIgnorePath, 'utf-8');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const lines = content.split('\n').map(l => l.trim());
|
|
73
|
+
const newLines = [];
|
|
74
|
+
let added = false;
|
|
75
|
+
|
|
76
|
+
for (const item of toIgnore) {
|
|
77
|
+
if (!lines.includes(item)) {
|
|
78
|
+
newLines.push(item);
|
|
79
|
+
added = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (added) {
|
|
84
|
+
const newContent = content + (content && !content.endsWith('\n') ? '\n' : '') + newLines.join('\n') + '\n';
|
|
85
|
+
await fs.writeFile(gitIgnorePath, newContent);
|
|
86
|
+
logger.success('Updated .gitignore');
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
logger.warn(`Could not update .gitignore: ${error.message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
57
93
|
/**
|
|
58
94
|
* Initialize Autopilot in current repository
|
|
59
95
|
*/
|
|
@@ -76,6 +112,7 @@ async function initRepo() {
|
|
|
76
112
|
// Create files
|
|
77
113
|
await createIgnoreFile(repoPath);
|
|
78
114
|
await createConfigFile(repoPath);
|
|
115
|
+
await updateGitIgnore(repoPath);
|
|
79
116
|
|
|
80
117
|
logger.section('✨ Initialization Complete');
|
|
81
118
|
logger.info('Next steps:');
|
package/src/config/defaults.js
CHANGED
package/src/config/ignore.js
CHANGED
|
@@ -1,37 +1,136 @@
|
|
|
1
|
-
const fs = require('fs-extra');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const logger = require('../utils/logger');
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const logger = require('../utils/logger');
|
|
4
|
+
const { getIgnorePath } = require('../utils/paths');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Standardize path to use forward slashes
|
|
8
|
+
* @param {string} p - Path to normalize
|
|
9
|
+
* @returns {string} Normalized path
|
|
10
|
+
*/
|
|
11
|
+
const normalizePath = (p) => p.split(path.sep).join('/');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Read ignore file patterns
|
|
15
|
+
* @param {string} repoPath
|
|
16
|
+
* @returns {Promise<string[]>} Array of ignore patterns
|
|
17
|
+
*/
|
|
18
|
+
const readIgnoreFile = async (repoPath) => {
|
|
19
|
+
const ignorePath = getIgnorePath(repoPath);
|
|
20
|
+
try {
|
|
21
|
+
if (await fs.pathExists(ignorePath)) {
|
|
22
|
+
const content = await fs.readFile(ignorePath, 'utf-8');
|
|
23
|
+
return content
|
|
24
|
+
.split('\n')
|
|
25
|
+
.map((line) => line.trim())
|
|
26
|
+
.filter((line) => line && !line.startsWith('#'));
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
logger.debug(`Error reading ignore file: ${error.message}`);
|
|
30
|
+
}
|
|
31
|
+
return [];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create ignore file
|
|
36
|
+
*/
|
|
37
|
+
const createIgnoreFile = async (repoPath, patterns = []) => {
|
|
38
|
+
const ignorePath = getIgnorePath(repoPath);
|
|
39
|
+
try {
|
|
40
|
+
const content =
|
|
41
|
+
`# Autopilot Ignore File\n# Add patterns to exclude from autopilot watching\n\n` +
|
|
42
|
+
patterns.join('\n');
|
|
43
|
+
await fs.writeFile(ignorePath, content);
|
|
44
|
+
logger.success('Created .autopilotignore file');
|
|
45
|
+
} catch (error) {
|
|
46
|
+
logger.error(`Failed to create ignore file: ${error.message}`);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create a filter function for Chokidar
|
|
52
|
+
* @param {string} repoPath - Root of the repository
|
|
53
|
+
* @param {string[]} userPatterns - Custom ignore patterns
|
|
54
|
+
* @returns {function} Filter function (path => boolean)
|
|
55
|
+
*/
|
|
56
|
+
const createIgnoredFilter = (repoPath, userPatterns = []) => {
|
|
57
|
+
const normalizedRepoPath = normalizePath(repoPath);
|
|
58
|
+
|
|
59
|
+
// Always ignore these critical paths to prevent loops and noise
|
|
60
|
+
const criticalIgnores = [
|
|
61
|
+
'.git',
|
|
62
|
+
'node_modules',
|
|
63
|
+
'.vscode',
|
|
64
|
+
'.idea',
|
|
65
|
+
'dist',
|
|
66
|
+
'build',
|
|
67
|
+
'coverage',
|
|
68
|
+
'.next'
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const criticalFiles = [
|
|
72
|
+
'autopilot.log',
|
|
73
|
+
'.autopilot.pid',
|
|
74
|
+
'.DS_Store'
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
return (absolutePath) => {
|
|
78
|
+
// 1. Normalize paths
|
|
79
|
+
const normalizedAbs = normalizePath(absolutePath);
|
|
80
|
+
|
|
81
|
+
// 2. Get relative path
|
|
82
|
+
let relativePath = normalizedAbs;
|
|
83
|
+
if (normalizedAbs.startsWith(normalizedRepoPath)) {
|
|
84
|
+
relativePath = normalizedAbs.slice(normalizedRepoPath.length);
|
|
85
|
+
if (relativePath.startsWith('/')) {
|
|
86
|
+
relativePath = relativePath.slice(1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Handle root path case
|
|
91
|
+
if (!relativePath) return false;
|
|
92
|
+
|
|
93
|
+
// 3. Check critical directory prefixes
|
|
94
|
+
// We check if any path segment matches a critical ignore
|
|
95
|
+
const parts = relativePath.split('/');
|
|
96
|
+
for (const part of parts) {
|
|
97
|
+
if (criticalIgnores.includes(part)) return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 4. Check file extensions and exact matches
|
|
101
|
+
if (relativePath.endsWith('.log')) return true;
|
|
102
|
+
if (criticalFiles.some(f => relativePath.endsWith(f))) return true;
|
|
103
|
+
|
|
104
|
+
// 5. Check user patterns (simple prefix/suffix matching for now)
|
|
105
|
+
// For robust glob support without adding dependencies, we rely on basic checks
|
|
106
|
+
// Most users use simple dir/ or *.ext patterns
|
|
107
|
+
for (const pattern of userPatterns) {
|
|
108
|
+
// Remove leading/trailing slashes for comparison
|
|
109
|
+
const cleanPattern = pattern.replace(/^\/+|\/+$/g, '');
|
|
110
|
+
|
|
111
|
+
// Directory match (e.g., "temp/")
|
|
112
|
+
if (pattern.endsWith('/') && (relativePath === cleanPattern || relativePath.startsWith(cleanPattern + '/'))) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Extension match (e.g., "*.tmp")
|
|
117
|
+
if (pattern.startsWith('*.') && relativePath.endsWith(pattern.slice(1))) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Exact match
|
|
122
|
+
if (relativePath === cleanPattern) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return false;
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
readIgnoreFile,
|
|
133
|
+
createIgnoreFile,
|
|
134
|
+
createIgnoredFilter,
|
|
135
|
+
normalizePath
|
|
136
|
+
};
|
package/src/core/watcher.js
CHANGED
|
@@ -6,13 +6,12 @@
|
|
|
6
6
|
const fs = require('fs-extra');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const chokidar = require('chokidar');
|
|
9
|
-
const { execa } = require('execa');
|
|
10
9
|
const logger = require('../utils/logger');
|
|
11
10
|
const git = require('./git');
|
|
12
11
|
const { generateCommitMessage } = require('./commit');
|
|
13
|
-
const { getIgnorePath } = require('../utils/paths');
|
|
14
12
|
const { savePid, removePid, registerProcessHandlers } = require('../utils/process');
|
|
15
13
|
const { loadConfig } = require('../config/loader');
|
|
14
|
+
const { readIgnoreFile, createIgnoredFilter, normalizePath } = require('../config/ignore');
|
|
16
15
|
|
|
17
16
|
class Watcher {
|
|
18
17
|
constructor(repoPath) {
|
|
@@ -27,6 +26,23 @@ class Watcher {
|
|
|
27
26
|
this.ignorePatterns = [];
|
|
28
27
|
}
|
|
29
28
|
|
|
29
|
+
logVerbose(message) {
|
|
30
|
+
// Helper to log debug messages to file/stdout if needed
|
|
31
|
+
// For now, we use logger.debug which writes to stdout if DEBUG is set
|
|
32
|
+
// We also want to ensure we don't create a loop by logging to the file we watch
|
|
33
|
+
// But since we ignore autopilot.log, it should be fine.
|
|
34
|
+
logger.debug(message);
|
|
35
|
+
// TODO: Append to log file if configured
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async reloadConfig() {
|
|
39
|
+
this.config = await loadConfig(this.repoPath);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async reloadIgnore() {
|
|
43
|
+
this.ignorePatterns = await readIgnoreFile(this.repoPath);
|
|
44
|
+
}
|
|
45
|
+
|
|
30
46
|
/**
|
|
31
47
|
* Initialize and start the watcher
|
|
32
48
|
*/
|
|
@@ -41,7 +57,7 @@ class Watcher {
|
|
|
41
57
|
await fs.ensureFile(this.logFilePath);
|
|
42
58
|
await savePid(this.repoPath);
|
|
43
59
|
|
|
44
|
-
|
|
60
|
+
logger.info('Starting Autopilot watcher...');
|
|
45
61
|
|
|
46
62
|
// Load configuration
|
|
47
63
|
await this.reloadConfig();
|
|
@@ -51,23 +67,16 @@ class Watcher {
|
|
|
51
67
|
const currentBranch = await git.getBranch(this.repoPath);
|
|
52
68
|
if (currentBranch && this.config.blockedBranches?.includes(currentBranch)) {
|
|
53
69
|
logger.error(`Branch '${currentBranch}' is blocked in config. Stopping.`);
|
|
54
|
-
this.logVerbose(`Blocked branch detected: ${currentBranch}`);
|
|
55
70
|
await this.stop();
|
|
56
71
|
return;
|
|
57
72
|
}
|
|
58
73
|
|
|
59
|
-
//
|
|
60
|
-
const
|
|
61
|
-
...this.ignorePatterns,
|
|
62
|
-
/(^|[\/\\])\.git([\/\\]|$)/, // Regex for .git folder
|
|
63
|
-
'**/autopilot.log',
|
|
64
|
-
'**/.autopilot.pid',
|
|
65
|
-
'node_modules' // Sensible default
|
|
66
|
-
];
|
|
74
|
+
// Create robust ignore filter
|
|
75
|
+
const ignoredFilter = createIgnoredFilter(this.repoPath, this.ignorePatterns);
|
|
67
76
|
|
|
68
|
-
// Start Chokidar
|
|
77
|
+
// Start Chokidar with function-based ignore
|
|
69
78
|
this.watcher = chokidar.watch(this.repoPath, {
|
|
70
|
-
ignored:
|
|
79
|
+
ignored: ignoredFilter,
|
|
71
80
|
ignoreInitial: true,
|
|
72
81
|
persistent: true,
|
|
73
82
|
awaitWriteFinish: {
|
|
@@ -93,17 +102,16 @@ class Watcher {
|
|
|
93
102
|
|
|
94
103
|
// Test Mode Support
|
|
95
104
|
if (process.env.AUTOPILOT_TEST_MODE) {
|
|
96
|
-
logger.warn('TEST MODE: Running in dry-run mode for
|
|
105
|
+
logger.warn('TEST MODE: Running in dry-run mode for 8 seconds...');
|
|
97
106
|
setTimeout(async () => {
|
|
98
107
|
logger.info('TEST MODE: Auto-stopping watcher...');
|
|
99
108
|
await this.stop();
|
|
100
109
|
process.exit(0);
|
|
101
|
-
},
|
|
110
|
+
}, 8000);
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
} catch (error) {
|
|
105
114
|
logger.error(`Failed to start watcher: ${error.message}`);
|
|
106
|
-
this.logVerbose(`Start error: ${error.stack}`);
|
|
107
115
|
await this.stop();
|
|
108
116
|
}
|
|
109
117
|
}
|
|
@@ -137,10 +145,15 @@ class Watcher {
|
|
|
137
145
|
onFsEvent(type, filePath) {
|
|
138
146
|
if (this.isProcessing) return;
|
|
139
147
|
|
|
140
|
-
//
|
|
141
|
-
|
|
148
|
+
// Normalize path relative to repo for logging and checks
|
|
149
|
+
const relativePath = normalizePath(path.relative(this.repoPath, filePath));
|
|
150
|
+
|
|
151
|
+
// Double check ignore (safety net) - although chokidar should catch most
|
|
152
|
+
if (relativePath.includes('.git/') || relativePath.endsWith('autopilot.log') || relativePath.includes('.vscode/')) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
142
155
|
|
|
143
|
-
this.logVerbose(`File event: ${type} ${
|
|
156
|
+
this.logVerbose(`File event: ${type} ${relativePath}`);
|
|
144
157
|
this.scheduleProcess();
|
|
145
158
|
}
|
|
146
159
|
|
|
@@ -154,11 +167,22 @@ class Watcher {
|
|
|
154
167
|
clearTimeout(this.debounceTimer);
|
|
155
168
|
}
|
|
156
169
|
|
|
170
|
+
logger.debug('Debounce fired. Waiting...');
|
|
171
|
+
|
|
157
172
|
this.debounceTimer = setTimeout(() => {
|
|
158
173
|
this.processChanges();
|
|
159
174
|
}, debounceMs);
|
|
160
175
|
}
|
|
161
176
|
|
|
177
|
+
handleError(error) {
|
|
178
|
+
logger.error(`Watcher error: ${error.message}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async runChecks() {
|
|
182
|
+
// Placeholder for custom checks
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
162
186
|
/**
|
|
163
187
|
* Main processing loop
|
|
164
188
|
*/
|
|
@@ -167,153 +191,84 @@ class Watcher {
|
|
|
167
191
|
this.isProcessing = true;
|
|
168
192
|
|
|
169
193
|
try {
|
|
194
|
+
logger.debug('Checking git status...');
|
|
195
|
+
|
|
170
196
|
// 1. Min interval check
|
|
171
197
|
const now = Date.now();
|
|
172
|
-
const minInterval = (this.config?.
|
|
173
|
-
if (now - this.lastCommitAt < minInterval) {
|
|
174
|
-
|
|
198
|
+
const minInterval = (this.config?.minSecondsBetweenCommits || 180) * 1000;
|
|
199
|
+
if (this.lastCommitAt > 0 && now - this.lastCommitAt < minInterval) {
|
|
200
|
+
logger.debug(`Skip commit: Minimum interval not met (${Math.round((minInterval - (now - this.lastCommitAt))/1000)}s remaining)`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 2. Check if dirty
|
|
205
|
+
const statusObj = await git.getPorcelainStatus(this.repoPath);
|
|
206
|
+
const isDirty = statusObj.ok && statusObj.files.length > 0;
|
|
207
|
+
logger.debug(`Git dirty: ${isDirty}`);
|
|
208
|
+
|
|
209
|
+
if (!isDirty) {
|
|
175
210
|
return;
|
|
176
211
|
}
|
|
177
212
|
|
|
178
|
-
//
|
|
213
|
+
// 3. Safety: Branch check
|
|
179
214
|
const branch = await git.getBranch(this.repoPath);
|
|
180
215
|
if (this.config?.blockedBranches?.includes(branch)) {
|
|
181
|
-
logger.warn(`
|
|
216
|
+
logger.warn(`Skip commit: Branch '${branch}' is blocked`);
|
|
182
217
|
return;
|
|
183
218
|
}
|
|
184
219
|
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
220
|
+
// 4. Safety: Remote check (fetch -> behind?)
|
|
221
|
+
logger.debug('Checking remote status...');
|
|
222
|
+
// Note: isRemoteAhead might need network, timeout safely?
|
|
223
|
+
try {
|
|
224
|
+
const remoteStatus = await git.isRemoteAhead(this.repoPath);
|
|
225
|
+
if (remoteStatus.behind) {
|
|
226
|
+
logger.warn('Skip commit: Local branch is behind remote. Please pull changes.');
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
} catch (e) {
|
|
230
|
+
logger.debug(`Remote check failed (offline?): ${e.message}`);
|
|
192
231
|
}
|
|
193
232
|
|
|
194
|
-
//
|
|
233
|
+
// 5. Safety: Custom checks
|
|
195
234
|
if (this.config?.requireChecks) {
|
|
196
235
|
const checksPassed = await this.runChecks();
|
|
197
236
|
if (!checksPassed) {
|
|
198
|
-
logger.warn('Checks failed
|
|
237
|
+
logger.warn('Skip commit: Checks failed');
|
|
199
238
|
return;
|
|
200
239
|
}
|
|
201
240
|
}
|
|
202
241
|
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
if (!status.ok || status.files.length === 0) {
|
|
206
|
-
this.logVerbose('No changes to commit');
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
this.logVerbose(`Detecting ${status.files.length} changed files`);
|
|
242
|
+
// 6. Commit
|
|
243
|
+
logger.info('Committing changes...');
|
|
211
244
|
|
|
212
|
-
// Add all
|
|
245
|
+
// Add all changes
|
|
213
246
|
await git.addAll(this.repoPath);
|
|
214
247
|
|
|
215
248
|
// Generate message
|
|
216
|
-
const
|
|
217
|
-
this.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
logger.success('Pushed successfully');
|
|
231
|
-
} else {
|
|
232
|
-
logger.error(`Push failed: ${pushResult.stderr}`);
|
|
233
|
-
this.logVerbose(`Push error: ${pushResult.stderr}`);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
} else {
|
|
237
|
-
logger.error(`Commit failed: ${commitResult.stderr}`);
|
|
238
|
-
this.logVerbose(`Commit error: ${commitResult.stderr}`);
|
|
249
|
+
const changedFiles = statusObj.files;
|
|
250
|
+
const message = this.config?.commitMessageMode === 'simple'
|
|
251
|
+
? 'chore: auto-commit changes'
|
|
252
|
+
: generateCommitMessage(changedFiles);
|
|
253
|
+
|
|
254
|
+
await git.commit(this.repoPath, message);
|
|
255
|
+
this.lastCommitAt = Date.now();
|
|
256
|
+
logger.success('Commit done');
|
|
257
|
+
|
|
258
|
+
// 7. Auto-push
|
|
259
|
+
if (this.config?.autoPush) {
|
|
260
|
+
logger.info('Pushing to remote...');
|
|
261
|
+
await git.push(this.repoPath);
|
|
262
|
+
logger.success('Push complete');
|
|
239
263
|
}
|
|
240
264
|
|
|
241
265
|
} catch (error) {
|
|
242
266
|
logger.error(`Process error: ${error.message}`);
|
|
243
|
-
this.logVerbose(`Process exception: ${error.stack}`);
|
|
244
267
|
} finally {
|
|
245
268
|
this.isProcessing = false;
|
|
246
269
|
this.debounceTimer = null;
|
|
247
270
|
}
|
|
248
271
|
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Run user-defined checks
|
|
252
|
-
*/
|
|
253
|
-
async runChecks() {
|
|
254
|
-
const checks = this.config?.checks || [];
|
|
255
|
-
if (checks.length === 0) return true;
|
|
256
|
-
|
|
257
|
-
this.logVerbose(`Running checks: ${checks.join(', ')}`);
|
|
258
|
-
|
|
259
|
-
for (const cmd of checks) {
|
|
260
|
-
try {
|
|
261
|
-
logger.info(`Running check: ${cmd}`);
|
|
262
|
-
await execa(cmd, { cwd: this.repoPath, shell: true });
|
|
263
|
-
} catch (error) {
|
|
264
|
-
logger.error(`Check failed: ${cmd}`);
|
|
265
|
-
this.logVerbose(`Check output: ${error.message}`);
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return true;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Reload configuration
|
|
274
|
-
*/
|
|
275
|
-
async reloadConfig() {
|
|
276
|
-
this.config = await loadConfig(this.repoPath);
|
|
277
|
-
this.logVerbose('Config reloaded');
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Reload ignore patterns
|
|
282
|
-
*/
|
|
283
|
-
async reloadIgnore() {
|
|
284
|
-
const ignorePath = getIgnorePath(this.repoPath);
|
|
285
|
-
this.ignorePatterns = [];
|
|
286
|
-
|
|
287
|
-
if (await fs.pathExists(ignorePath)) {
|
|
288
|
-
try {
|
|
289
|
-
const content = await fs.readFile(ignorePath, 'utf-8');
|
|
290
|
-
const lines = content.split('\n')
|
|
291
|
-
.map(l => l.trim())
|
|
292
|
-
.filter(l => l && !l.startsWith('#'));
|
|
293
|
-
|
|
294
|
-
this.ignorePatterns.push(...lines);
|
|
295
|
-
this.logVerbose(`Loaded ${lines.length} ignore patterns`);
|
|
296
|
-
} catch (error) {
|
|
297
|
-
logger.warn(`Failed to load ignore file: ${error.message}`);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Also load ignore patterns from config json if they exist
|
|
302
|
-
if (this.config?.ignore && Array.isArray(this.config.ignore)) {
|
|
303
|
-
this.ignorePatterns.push(...this.config.ignore);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
handleError(error) {
|
|
308
|
-
logger.error(`Watcher error: ${error.message}`);
|
|
309
|
-
this.logVerbose(`Watcher error: ${error.stack}`);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
logVerbose(message) {
|
|
313
|
-
const timestamp = new Date().toISOString();
|
|
314
|
-
const logLine = `[${timestamp}] ${message}\n`;
|
|
315
|
-
fs.appendFile(this.logFilePath, logLine).catch(() => {});
|
|
316
|
-
}
|
|
317
272
|
}
|
|
318
273
|
|
|
319
274
|
module.exports = Watcher;
|