@traisetech/autopilot 0.1.3 → 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 CHANGED
@@ -15,3 +15,53 @@
15
15
  - Per-repo config and ignore rules
16
16
 
17
17
  Built by Praise Masunga (PraiseTechzw).
18
+ # Changelog
19
+
20
+ All notable changes to this project will be documented in this file.
21
+ This project follows Semantic Versioning (https://semver.org).
22
+
23
+ ---
24
+
25
+ ## [0.1.4] – 2026-02-01
26
+
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.
30
+ - Fixed a critical CLI crash where `autopilot start` failed due to miswired Commander action handlers.
31
+ - Improved command registration to ensure all CLI commands are correctly bound and validated at runtime.
32
+ - Prevented undefined command handlers from causing runtime exceptions.
33
+
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`.
38
+ - Pre-publish verification pipeline to block publishing broken builds.
39
+ - CLI smoke tests to ensure core commands (`init`, `start`, `status`, `doctor`) do not crash.
40
+ - Test-only dry-run mode for watcher to allow safe automated testing.
41
+ - Additional configuration and commit logic unit tests.
42
+
43
+ ### Changed
44
+ - Standardized command exports across all CLI commands.
45
+ - Improved error messages for misconfigured or invalid commands.
46
+ - Strengthened release hygiene and stability guarantees.
47
+
48
+ ### Developer Experience
49
+ - Added `prepublishOnly` guard to prevent accidental publishing of failing builds.
50
+ - Improved Windows compatibility during testing and CLI execution.
51
+
52
+ ---
53
+
54
+ ## [0.1.3] – 2026-01-31
55
+
56
+ ### Added
57
+ - Initial public release of Autopilot CLI.
58
+ - Intelligent Git automation with smart commit messages.
59
+ - Background watcher with debouncing and safety rails.
60
+ - Branch protection and remote-ahead detection.
61
+ - `doctor` command for environment diagnostics.
62
+ - Per-project configuration via `.autopilotrc.json`.
63
+
64
+ ---
65
+
66
+ ## [0.1.0] – Initial Development
67
+ - Core architecture and foundational CLI commands.
package/README.md CHANGED
@@ -1,210 +1,2 @@
1
- # 🚀 Autopilot CLI
2
1
 
3
- <div align="center">
4
-
5
- ![Autopilot Logo](https://img.shields.io/badge/Autopilot-CLI-blue?style=for-the-badge&logo=git&logoColor=white)
6
-
7
- **Intelligent Git automation that commits and pushes your code, so you can focus on building.**
8
-
9
- [![npm version](https://img.shields.io/npm/v/autopilot-cli?style=flat-square&color=success)](https://www.npmjs.com/package/autopilot-cli)
10
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
11
- [![Node Version](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen?style=flat-square)](https://nodejs.org)
12
- [![Downloads](https://img.shields.io/npm/dm/autopilot-cli?style=flat-square&color=blue)](https://www.npmjs.com/package/autopilot-cli)
13
- [![GitHub Stars](https://img.shields.io/github/stars/PraiseTechzw/autopilot-cli?style=flat-square&color=gold)](https://github.com/PraiseTechzw/autopilot-cli/stargazers)
14
- [![Build Status](https://img.shields.io/github/actions/workflow/status/PraiseTechzw/autopilot-cli/ci.yml?style=flat-square)](https://github.com/PraiseTechzw/autopilot-cli/actions)
15
- [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
16
-
17
- **Built by [Praise Masunga](https://github.com/PraiseTechzw) (PraiseTechzw)**
18
-
19
- [Features](#-features) • [Quick Start](#-quick-start) • [Configuration](#-configuration) • [Commands](#-commands) • [Safety](#-safety-features)
20
-
21
- </div>
22
-
23
- ---
24
-
25
- ## 📖 Table of Contents
26
-
27
- - [Why Autopilot?](#-why-autopilot)
28
- - [Features](#-features)
29
- - [Quick Start](#-quick-start)
30
- - [Installation](#-installation)
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
- <table>
43
- <tr>
44
- <td width="50%">
45
-
46
- ### ❌ Before Autopilot
47
-
48
- ```bash
49
- # Every. Single. Time.
50
- git add .
51
- git commit -m "update stuff"
52
- git push
53
-
54
- # Repeat 50+ times a day...
55
- # Lose focus on coding
56
- # Forget to commit
57
- # Inconsistent messages
58
- ```
59
-
60
- </td>
61
- <td width="50%">
62
-
63
- ### ✅ With Autopilot
64
-
65
- ```bash
66
- # One time setup
67
- autopilot init
68
- autopilot start
69
-
70
- # That's it!
71
- # Focus on coding
72
- # Auto-commits with smart messages
73
- # Never lose work again
74
- ```
75
-
76
- </td>
77
- </tr>
78
- </table>
79
-
80
- ---
81
-
82
- ## ✨ Features
83
-
84
- - **🧠 Smart Commits**: Generates professional conventional commit messages automatically.
85
- - **⚡ Watcher Engine**: Real-time file monitoring with smart debouncing using `chokidar`.
86
- - **🛡️ Safety First**: Blocks commits on protected branches and checks remote status.
87
- - **🔄 Automated Flow**: Fetches, stages, commits, and pushes (optional) automatically.
88
- - **⚙️ Zero Config**: Works out of the box, but fully configurable via `.autopilotrc.json`.
89
- - **🩺 Self-Healing**: Includes a `doctor` command to diagnose and fix issues.
90
-
91
- ---
92
-
93
- ## 🚀 Quick Start
94
-
95
- ### Installation
96
-
97
- ```bash
98
- # Install globally via npm
99
- npm install -g autopilot-cli
100
- ```
101
-
102
- ### Usage
103
-
104
- ```bash
105
- # 1. Initialize in your project
106
- cd my-project
107
- autopilot init
108
-
109
- # 2. Start the background watcher
110
- autopilot start
111
-
112
- # 3. Check status
113
- autopilot status
114
-
115
- # 4. Stop when done
116
- autopilot stop
117
- ```
118
-
119
- ---
120
-
121
- ## 💻 Commands
122
-
123
- | Command | Description |
124
- |---------|-------------|
125
- | `autopilot init` | Initializes configuration and ignore files in the current directory. |
126
- | `autopilot start` | Starts the background watcher daemon. |
127
- | `autopilot stop` | Stops the running watcher daemon. |
128
- | `autopilot status` | Shows the current status of the watcher process. |
129
- | `autopilot doctor` | Runs diagnostics to verify environment and configuration. |
130
- | `autopilot --help` | Displays help information. |
131
-
132
- ---
133
-
134
- ## ⚙️ Configuration
135
-
136
- Autopilot uses a `.autopilotrc.json` file for configuration.
137
-
138
- ```json
139
- {
140
- "minInterval": 30,
141
- "autoPush": true,
142
- "blockedBranches": ["main", "production"],
143
- "requireChecks": false,
144
- "ignore": [
145
- "*.log",
146
- "temp/",
147
- "dist/",
148
- "node_modules"
149
- ]
150
- }
151
- ```
152
-
153
- | Option | Type | Default | Description |
154
- |--------|------|---------|-------------|
155
- | `minInterval` | `number` | `30` | Minimum seconds between commits. |
156
- | `autoPush` | `boolean` | `true` | Whether to push changes automatically after commit. |
157
- | `blockedBranches` | `array` | `[]` | List of branches to disable auto-commit on. |
158
- | `requireChecks` | `boolean` | `false` | Run custom checks before committing. |
159
- | `ignore` | `array` | `[]` | Additional glob patterns to ignore. |
160
-
161
- ---
162
-
163
- ## 🛡️ Safety Features
164
-
165
- Autopilot includes several safety mechanisms to prevent accidents:
166
-
167
- 1. **Branch Protection**: Will not run on branches listed in `blockedBranches`.
168
- 2. **Remote Sync**: Checks if local branch is behind remote before acting.
169
- 3. **Debouncing**: Waits for file changes to settle before committing.
170
- 4. **PID Management**: Ensures only one instance runs per repository.
171
-
172
- ---
173
-
174
- ## 🔧 Troubleshooting
175
-
176
- If you encounter issues, run the doctor command:
177
-
178
- ```bash
179
- autopilot doctor
180
- ```
181
-
182
- This will check for:
183
- - Git repository status
184
- - Configuration validity
185
- - Node.js version
186
- - Permissions
187
-
188
- ---
189
-
190
- ## 🤝 Contributing
191
-
192
- Contributions are welcome! Please feel free to submit a Pull Request.
193
-
194
- 1. Fork the repository
195
- 2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
196
- 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
197
- 4. Push to the branch (`git push origin feature/AmazingFeature`)
198
- 5. Open a Pull Request
199
-
200
- ---
201
-
202
- ## 📜 License
203
-
204
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
205
-
206
- ---
207
-
208
- <div align="center">
209
- <b>Built with ❤️ by <a href="https://github.com/PraiseTechzw">Praise Masunga (PraiseTechzw)</a></b>
210
- </div>
2
+ <!-- Autopilot test run: 2026-02-01 -->
package/bin/autopilot.js CHANGED
@@ -1,12 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { Command } = require('commander');
4
- const { initRepo } = require('../src/commands/init');
5
- const { startWatcher } = require('../src/commands/start');
6
- const { stopWatcher } = require('../src/commands/stop');
7
- const { statusWatcher } = require('../src/commands/status');
8
- const { doctor } = require('../src/commands/doctor');
4
+ const initRepo = require('../src/commands/init');
5
+ const startWatcher = require('../src/commands/start');
6
+ const stopWatcher = require('../src/commands/stop');
7
+ const statusWatcher = require('../src/commands/status');
8
+ const doctor = require('../src/commands/doctor');
9
9
  const pkg = require('../package.json');
10
+ const logger = require('../src/utils/logger');
11
+
12
+ // Validate command handlers
13
+ const commands = {
14
+ init: initRepo,
15
+ start: startWatcher,
16
+ stop: stopWatcher,
17
+ status: statusWatcher,
18
+ doctor: doctor
19
+ };
20
+
21
+ // Runtime assertion to prevent wiring errors
22
+ Object.entries(commands).forEach(([name, handler]) => {
23
+ if (typeof handler !== 'function') {
24
+ console.error(`\n❌ FATAL ERROR: Command handler for '${name}' is not a function.`);
25
+ console.error(`Received: ${typeof handler}`);
26
+ console.error('Please report this issue to the maintainer.\n');
27
+ process.exit(1);
28
+ }
29
+ });
10
30
 
11
31
  const program = new Command();
12
32
 
@@ -1,40 +1,21 @@
1
- # Troubleshooting
2
-
3
- **Built by Praise Masunga (PraiseTechzw)**
4
-
5
- ---
6
-
7
- ## Autopilot won’t start
8
-
9
- - Ensure you are inside a git repository
10
- - Run `autopilot doctor`
11
- - Remove stale PID if needed: delete `.autopilot.pid`
12
-
13
- ---
14
-
15
- ## It won’t commit
16
-
17
- Common reasons:
18
- - You are on a blocked branch (`blockBranches`)
19
- - Remote is ahead (pull first)
20
- - `requireChecks` is enabled and a check failed
21
- - No changes detected by git
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",
3
+ "version": "0.1.6",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -32,12 +32,11 @@
32
32
  "main": "src/index.js",
33
33
  "scripts": {
34
34
  "dev": "node bin/autopilot.js",
35
- "lint": "echo \"No lint configured\"",
36
35
  "test": "node --test",
37
- "verify": "node bin/autopilot.js doctor",
38
- "release:patch": "npm test && echo \"⚠️ Ensure CHANGELOG.md is updated\" && npm version patch && git push --follow-tags",
39
- "release:minor": "npm test && echo \"⚠️ Ensure CHANGELOG.md is updated\" && npm version minor && git push --follow-tags",
40
- "release:major": "npm test && echo \"⚠️ Ensure CHANGELOG.md is updated\" && npm version major && git push --follow-tags"
36
+ "lint": "node -c bin/autopilot.js && node -c src/index.js",
37
+ "verify": "node bin/autopilot.js --help && npm run test && node bin/autopilot.js doctor",
38
+ "prepublishOnly": "npm run verify",
39
+ "release:patch": "npm run verify && npm version patch && git push --follow-tags && echo \"\n🚀 Ready to publish! Run: npm publish --access public\""
41
40
  },
42
41
  "engines": {
43
42
  "node": ">=18.0.0"
@@ -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:');
@@ -89,4 +126,4 @@ async function initRepo() {
89
126
  }
90
127
  }
91
128
 
92
- module.exports = { initRepo };
129
+ module.exports = initRepo;
@@ -22,6 +22,8 @@ const DEFAULT_IGNORE_PATTERNS = [
22
22
  '.env.*',
23
23
  'coverage/',
24
24
  '*.log',
25
+ 'autopilot.log',
26
+ '.autopilot.pid',
25
27
  '.DS_Store',
26
28
  '.git/',
27
29
  '.idea/',
@@ -1,37 +1,136 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
- const logger = require('../utils/logger');
4
-
5
- const readIgnoreFile = async (repoPath) => {
6
- const ignorePath = path.join(repoPath, '.autopilot-ignore');
7
- try {
8
- if (await fs.pathExists(ignorePath)) {
9
- const content = await fs.readFile(ignorePath, 'utf-8');
10
- return content
11
- .split('\n')
12
- .map((line) => line.trim())
13
- .filter((line) => line && !line.startsWith('#'));
14
- }
15
- } catch (error) {
16
- logger.debug(`Error reading ignore file: ${error.message}`);
17
- }
18
- return [];
19
- };
20
-
21
- const createIgnoreFile = async (repoPath, patterns = []) => {
22
- const ignorePath = path.join(repoPath, '.autopilot-ignore');
23
- try {
24
- const content =
25
- `# Autopilot Ignore File\n# Add patterns to exclude from autopilot watching\n\n` +
26
- patterns.join('\n');
27
- await fs.writeFile(ignorePath, content);
28
- logger.success('Created .autopilot-ignore file');
29
- } catch (error) {
30
- logger.error(`Failed to create ignore file: ${error.message}`);
31
- }
32
- };
33
-
34
- module.exports = {
35
- readIgnoreFile,
36
- createIgnoreFile,
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
+ };
@@ -1,47 +1,47 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
- const logger = require('../utils/logger');
4
- const defaults = require('./defaults');
5
- const { getConfigPath } = require('../utils/paths');
6
-
7
- const loadConfig = async (repoPath) => {
8
- const configPath = getConfigPath(repoPath);
9
- try {
10
- if (await fs.pathExists(configPath)) {
11
- const config = await fs.readJson(configPath);
12
- logger.debug(`Loaded config from ${configPath}`);
13
- return { ...defaults, ...config };
14
- }
15
- } catch (error) {
16
- logger.warn(`Error loading config: ${error.message}`);
17
- }
18
- return defaults;
19
- };
20
-
21
- const saveConfig = async (repoPath, config) => {
22
- const configPath = getConfigPath(repoPath);
23
- try {
24
- await fs.writeJson(configPath, config, { spaces: 2 });
25
- logger.success(`Config saved to ${configPath}`);
26
- } catch (error) {
27
- logger.error(`Failed to save config: ${error.message}`);
28
- }
29
- };
30
-
31
- const createDefaultConfig = async (repoPath) => {
32
- const configPath = getConfigPath(repoPath);
33
- try {
34
- if (!(await fs.pathExists(configPath))) {
35
- await fs.writeJson(configPath, defaults, { spaces: 2 });
36
- logger.success(`Created default config at ${configPath}`);
37
- }
38
- } catch (error) {
39
- logger.error(`Failed to create config: ${error.message}`);
40
- }
41
- };
42
-
43
- module.exports = {
44
- loadConfig,
45
- saveConfig,
46
- createDefaultConfig,
47
- };
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const logger = require('../utils/logger');
4
+ const { DEFAULT_CONFIG } = require('./defaults');
5
+ const { getConfigPath } = require('../utils/paths');
6
+
7
+ const loadConfig = async (repoPath) => {
8
+ const configPath = getConfigPath(repoPath);
9
+ try {
10
+ if (await fs.pathExists(configPath)) {
11
+ const config = await fs.readJson(configPath);
12
+ logger.debug(`Loaded config from ${configPath}`);
13
+ return { ...DEFAULT_CONFIG, ...config };
14
+ }
15
+ } catch (error) {
16
+ logger.warn(`Error loading config: ${error.message}`);
17
+ }
18
+ return { ...DEFAULT_CONFIG };
19
+ };
20
+
21
+ const saveConfig = async (repoPath, config) => {
22
+ const configPath = getConfigPath(repoPath);
23
+ try {
24
+ await fs.writeJson(configPath, config, { spaces: 2 });
25
+ logger.success(`Config saved to ${configPath}`);
26
+ } catch (error) {
27
+ logger.error(`Failed to save config: ${error.message}`);
28
+ }
29
+ };
30
+
31
+ const createDefaultConfig = async (repoPath) => {
32
+ const configPath = getConfigPath(repoPath);
33
+ try {
34
+ if (!(await fs.pathExists(configPath))) {
35
+ await fs.writeJson(configPath, DEFAULT_CONFIG, { spaces: 2 });
36
+ logger.success(`Created default config at ${configPath}`);
37
+ }
38
+ } catch (error) {
39
+ logger.error(`Failed to create config: ${error.message}`);
40
+ }
41
+ };
42
+
43
+ module.exports = {
44
+ loadConfig,
45
+ saveConfig,
46
+ createDefaultConfig,
47
+ };
@@ -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
- this.logVerbose('Starting Autopilot watcher...');
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
- // 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
- ];
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: finalIgnored,
79
+ ignored: ignoredFilter,
71
80
  ignoreInitial: true,
72
81
  persistent: true,
73
82
  awaitWriteFinish: {
@@ -91,9 +100,18 @@ class Watcher {
91
100
  logger.success(`Autopilot is watching ${this.repoPath}`);
92
101
  logger.info(`Logs: ${this.logFilePath}`);
93
102
 
103
+ // Test Mode Support
104
+ if (process.env.AUTOPILOT_TEST_MODE) {
105
+ logger.warn('TEST MODE: Running in dry-run mode for 8 seconds...');
106
+ setTimeout(async () => {
107
+ logger.info('TEST MODE: Auto-stopping watcher...');
108
+ await this.stop();
109
+ process.exit(0);
110
+ }, 8000);
111
+ }
112
+
94
113
  } catch (error) {
95
114
  logger.error(`Failed to start watcher: ${error.message}`);
96
- this.logVerbose(`Start error: ${error.stack}`);
97
115
  await this.stop();
98
116
  }
99
117
  }
@@ -127,10 +145,15 @@ class Watcher {
127
145
  onFsEvent(type, filePath) {
128
146
  if (this.isProcessing) return;
129
147
 
130
- // Double check ignore (safety net)
131
- if (filePath.includes('.git') || filePath.endsWith('autopilot.log')) return;
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
+ }
132
155
 
133
- this.logVerbose(`File event: ${type} ${filePath}`);
156
+ this.logVerbose(`File event: ${type} ${relativePath}`);
134
157
  this.scheduleProcess();
135
158
  }
136
159
 
@@ -144,11 +167,22 @@ class Watcher {
144
167
  clearTimeout(this.debounceTimer);
145
168
  }
146
169
 
170
+ logger.debug('Debounce fired. Waiting...');
171
+
147
172
  this.debounceTimer = setTimeout(() => {
148
173
  this.processChanges();
149
174
  }, debounceMs);
150
175
  }
151
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
+
152
186
  /**
153
187
  * Main processing loop
154
188
  */
@@ -157,153 +191,84 @@ class Watcher {
157
191
  this.isProcessing = true;
158
192
 
159
193
  try {
194
+ logger.debug('Checking git status...');
195
+
160
196
  // 1. Min interval check
161
197
  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');
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) {
165
210
  return;
166
211
  }
167
212
 
168
- // 2. Safety: Branch check
213
+ // 3. Safety: Branch check
169
214
  const branch = await git.getBranch(this.repoPath);
170
215
  if (this.config?.blockedBranches?.includes(branch)) {
171
- logger.warn(`Current branch '${branch}' is blocked. Skipping.`);
216
+ logger.warn(`Skip commit: Branch '${branch}' is blocked`);
172
217
  return;
173
218
  }
174
219
 
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;
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}`);
182
231
  }
183
232
 
184
- // 4. Safety: Custom checks
233
+ // 5. Safety: Custom checks
185
234
  if (this.config?.requireChecks) {
186
235
  const checksPassed = await this.runChecks();
187
236
  if (!checksPassed) {
188
- logger.warn('Checks failed. Skipping commit.');
237
+ logger.warn('Skip commit: Checks failed');
189
238
  return;
190
239
  }
191
240
  }
192
241
 
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`);
242
+ // 6. Commit
243
+ logger.info('Committing changes...');
201
244
 
202
- // Add all
245
+ // Add all changes
203
246
  await git.addAll(this.repoPath);
204
247
 
205
248
  // 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}`);
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');
229
263
  }
230
264
 
231
265
  } catch (error) {
232
266
  logger.error(`Process error: ${error.message}`);
233
- this.logVerbose(`Process exception: ${error.stack}`);
234
267
  } finally {
235
268
  this.isProcessing = false;
236
269
  this.debounceTimer = null;
237
270
  }
238
271
  }
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
272
  }
308
273
 
309
274
  module.exports = Watcher;
@@ -12,6 +12,16 @@ const logger = {
12
12
  console.log(`ℹ️ ${message}`);
13
13
  },
14
14
 
15
+ /**
16
+ * Log debug message (only visible if DEBUG env var is set)
17
+ * @param {string} message - Message to log
18
+ */
19
+ debug: (message) => {
20
+ if (process.env.DEBUG) {
21
+ console.log(`🔍 ${message}`);
22
+ }
23
+ },
24
+
15
25
  /**
16
26
  * Log success message
17
27
  * @param {string} message - Message to log