@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/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@traisetech/autopilot",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"files": [
|
|
8
|
+
"bin",
|
|
9
|
+
"src",
|
|
10
|
+
"docs",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"CHANGELOG.md"
|
|
14
|
+
],
|
|
15
|
+
"description": "An intelligent CLI tool that automatically commits and pushes your code so you can focus on building.",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"git",
|
|
18
|
+
"automation",
|
|
19
|
+
"autocommit",
|
|
20
|
+
"autopush",
|
|
21
|
+
"developer-tools",
|
|
22
|
+
"cli",
|
|
23
|
+
"productivity",
|
|
24
|
+
"git-workflow"
|
|
25
|
+
],
|
|
26
|
+
"author": "Praise Masunga (PraiseTechzw)",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"type": "commonjs",
|
|
29
|
+
"bin": {
|
|
30
|
+
"autopilot": "bin/autopilot.js"
|
|
31
|
+
},
|
|
32
|
+
"main": "src/index.js",
|
|
33
|
+
"scripts": {
|
|
34
|
+
"dev": "node bin/autopilot.js",
|
|
35
|
+
"lint": "echo \"No lint configured\"",
|
|
36
|
+
"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"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18.0.0"
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/PraiseTechzw/autopilot-cli.git"
|
|
48
|
+
},
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/PraiseTechzw/autopilot-cli/issues"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/PraiseTechzw/autopilot-cli#readme",
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"chokidar": "^5.0.0",
|
|
55
|
+
"commander": "^14.0.3",
|
|
56
|
+
"execa": "^9.6.1",
|
|
57
|
+
"fs-extra": "^11.3.3"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: doctor
|
|
3
|
+
* Diagnoses potential issues with the repository and environment
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs-extra');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const process = require('process');
|
|
9
|
+
const { execa } = require('execa');
|
|
10
|
+
const logger = require('../utils/logger');
|
|
11
|
+
const git = require('../core/git');
|
|
12
|
+
|
|
13
|
+
const doctor = async () => {
|
|
14
|
+
const repoPath = process.cwd();
|
|
15
|
+
let issues = 0;
|
|
16
|
+
|
|
17
|
+
logger.section('Autopilot Doctor');
|
|
18
|
+
logger.info('Diagnosing environment...');
|
|
19
|
+
|
|
20
|
+
// 1. Check Git installation
|
|
21
|
+
try {
|
|
22
|
+
const { stdout } = await execa('git', ['--version']);
|
|
23
|
+
logger.success(`Git installed: ${stdout.trim()}`);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
logger.error('Git is not installed or not in PATH.');
|
|
26
|
+
issues++;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 2. Check valid repository
|
|
30
|
+
try {
|
|
31
|
+
const { stdout } = await execa('git', ['rev-parse', '--is-inside-work-tree'], { cwd: repoPath });
|
|
32
|
+
if (stdout.trim() === 'true') {
|
|
33
|
+
logger.success('Valid Git repository detected');
|
|
34
|
+
} else {
|
|
35
|
+
logger.error('Not inside a Git repository.');
|
|
36
|
+
issues++;
|
|
37
|
+
return; // Stop further checks if not a repo
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
logger.error('Not inside a Git repository.');
|
|
41
|
+
issues++;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 3. Check remote origin
|
|
46
|
+
try {
|
|
47
|
+
const { stdout } = await execa('git', ['remote', 'get-url', 'origin'], { cwd: repoPath });
|
|
48
|
+
const remoteUrl = stdout.trim();
|
|
49
|
+
logger.success(`Remote 'origin' found: ${remoteUrl}`);
|
|
50
|
+
|
|
51
|
+
// Check remote type
|
|
52
|
+
if (remoteUrl.startsWith('http')) {
|
|
53
|
+
logger.warn('Remote uses HTTPS. Ensure credential helper is configured for non-interactive push.');
|
|
54
|
+
} else if (remoteUrl.startsWith('git@') || remoteUrl.startsWith('ssh://')) {
|
|
55
|
+
logger.success('Remote uses SSH (recommended).');
|
|
56
|
+
} else {
|
|
57
|
+
logger.info('Remote uses unknown protocol.');
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
logger.warn('No remote \'origin\' configured. Auto-push will fail.');
|
|
61
|
+
issues++;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 4. Check branch name
|
|
65
|
+
const branch = await git.getBranch(repoPath);
|
|
66
|
+
if (branch) {
|
|
67
|
+
logger.info(`Current branch: ${branch}`);
|
|
68
|
+
if (branch === 'main' || branch === 'master') {
|
|
69
|
+
logger.warn('You are on the main/master branch. It is recommended to work on feature branches.');
|
|
70
|
+
issues++;
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
logger.error('Could not detect current branch.');
|
|
74
|
+
issues++;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 5. Check .env in .gitignore
|
|
78
|
+
const envPath = path.join(repoPath, '.env');
|
|
79
|
+
if (await fs.pathExists(envPath)) {
|
|
80
|
+
try {
|
|
81
|
+
// Check if ignored by git
|
|
82
|
+
await execa('git', ['check-ignore', '.env'], { cwd: repoPath });
|
|
83
|
+
logger.success('.env is properly ignored.');
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// Exit code 1 means not ignored
|
|
86
|
+
logger.warn('SECURITY WARNING: .env file exists but is NOT ignored by git!');
|
|
87
|
+
logger.info('Add .env to your .gitignore file immediately.');
|
|
88
|
+
issues++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 6. Check remote status (ahead/behind)
|
|
93
|
+
try {
|
|
94
|
+
const remoteStatus = await git.isRemoteAhead(repoPath);
|
|
95
|
+
if (remoteStatus.ok) {
|
|
96
|
+
if (remoteStatus.behind) {
|
|
97
|
+
logger.warn('Your branch is behind remote. You should pull changes before starting autopilot.');
|
|
98
|
+
issues++;
|
|
99
|
+
} else if (remoteStatus.ahead) {
|
|
100
|
+
logger.info('Your branch is ahead of remote. Autopilot will push these changes.');
|
|
101
|
+
} else {
|
|
102
|
+
logger.success('Branch is up to date with remote.');
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
// Could be no upstream configured, which is fine for local-only initially
|
|
106
|
+
logger.info('Could not check remote status (upstream might not be set).');
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
logger.info('Skipping remote status check.');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Summary
|
|
113
|
+
console.log(''); // newline
|
|
114
|
+
if (issues === 0) {
|
|
115
|
+
logger.success('Diagnosis complete. No issues found. You are ready to fly! ✈️');
|
|
116
|
+
} else {
|
|
117
|
+
logger.warn(`Diagnosis complete. Found ${issues} potential issue(s).`);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
module.exports = doctor;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autopilot init command - Initialize repository configuration
|
|
3
|
+
* Built by Praise Masunga (PraiseTechzw)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs-extra');
|
|
7
|
+
const logger = require('../utils/logger');
|
|
8
|
+
const { getConfigPath, getIgnorePath, getGitPath } = require('../utils/paths');
|
|
9
|
+
const { DEFAULT_CONFIG, DEFAULT_IGNORE_PATTERNS } = require('../config/defaults');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Verify current directory is a git repository
|
|
13
|
+
* @param {string} repoPath - Path to check
|
|
14
|
+
* @returns {boolean} True if git repo
|
|
15
|
+
*/
|
|
16
|
+
function isGitRepo(repoPath) {
|
|
17
|
+
const gitPath = getGitPath(repoPath);
|
|
18
|
+
return fs.existsSync(gitPath);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create .autopilotignore file with safe defaults
|
|
23
|
+
* @param {string} repoPath - Repository path
|
|
24
|
+
* @returns {Promise<boolean>} True if created, false if already exists
|
|
25
|
+
*/
|
|
26
|
+
async function createIgnoreFile(repoPath) {
|
|
27
|
+
const ignorePath = getIgnorePath(repoPath);
|
|
28
|
+
|
|
29
|
+
if (fs.existsSync(ignorePath)) {
|
|
30
|
+
logger.info('.autopilotignore already exists');
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await fs.writeFile(ignorePath, DEFAULT_IGNORE_PATTERNS, 'utf8');
|
|
35
|
+
logger.success('Created .autopilotignore');
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create .autopilotrc.json file with default configuration
|
|
41
|
+
* @param {string} repoPath - Repository path
|
|
42
|
+
* @returns {Promise<boolean>} True if created, false if already exists
|
|
43
|
+
*/
|
|
44
|
+
async function createConfigFile(repoPath) {
|
|
45
|
+
const configPath = getConfigPath(repoPath);
|
|
46
|
+
|
|
47
|
+
if (fs.existsSync(configPath)) {
|
|
48
|
+
logger.info('.autopilotrc.json already exists');
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await fs.writeJson(configPath, DEFAULT_CONFIG, { spaces: 2 });
|
|
53
|
+
logger.success('Created .autopilotrc.json');
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Initialize Autopilot in current repository
|
|
59
|
+
*/
|
|
60
|
+
async function initRepo() {
|
|
61
|
+
try {
|
|
62
|
+
const repoPath = process.cwd();
|
|
63
|
+
|
|
64
|
+
logger.section('🚀 Autopilot Init');
|
|
65
|
+
logger.info('Built by Praise Masunga (PraiseTechzw)');
|
|
66
|
+
logger.info('Initializing git automation...');
|
|
67
|
+
|
|
68
|
+
// Verify git repository
|
|
69
|
+
if (!isGitRepo(repoPath)) {
|
|
70
|
+
logger.error('Not a git repository. Please run this inside a git repo.');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
logger.success('Git repository detected');
|
|
75
|
+
|
|
76
|
+
// Create files
|
|
77
|
+
await createIgnoreFile(repoPath);
|
|
78
|
+
await createConfigFile(repoPath);
|
|
79
|
+
|
|
80
|
+
logger.section('✨ Initialization Complete');
|
|
81
|
+
logger.info('Next steps:');
|
|
82
|
+
logger.info(' 1. Review .autopilotrc.json to customize settings');
|
|
83
|
+
logger.info(' 2. Review .autopilotignore to adjust ignore patterns');
|
|
84
|
+
logger.info(' 3. Run "autopilot start" to begin watching');
|
|
85
|
+
|
|
86
|
+
} catch (error) {
|
|
87
|
+
logger.error(`Initialization failed: ${error.message}`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { initRepo };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: start
|
|
3
|
+
* Starts the Autopilot watcher in the foreground
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const process = require('process');
|
|
7
|
+
const logger = require('../utils/logger');
|
|
8
|
+
const Watcher = require('../core/watcher');
|
|
9
|
+
const { getRunningPid } = require('../utils/process');
|
|
10
|
+
|
|
11
|
+
const start = async (options) => {
|
|
12
|
+
const repoPath = process.cwd();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
// Check if already running
|
|
16
|
+
const runningPid = await getRunningPid(repoPath);
|
|
17
|
+
if (runningPid) {
|
|
18
|
+
logger.warn(`Autopilot is already running (PID: ${runningPid})`);
|
|
19
|
+
logger.info('Run "autopilot stop" to stop the current instance.');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Initialize watcher
|
|
24
|
+
const watcher = new Watcher(repoPath);
|
|
25
|
+
|
|
26
|
+
logger.section('Starting Autopilot');
|
|
27
|
+
logger.info('Press Ctrl+C to stop, or run "autopilot stop" in another terminal.');
|
|
28
|
+
|
|
29
|
+
// Start watching
|
|
30
|
+
await watcher.start();
|
|
31
|
+
|
|
32
|
+
// Keep process alive is handled by chokidar being persistent
|
|
33
|
+
// The watcher handles process signals for cleanup
|
|
34
|
+
|
|
35
|
+
} catch (error) {
|
|
36
|
+
logger.error(`Failed to start autopilot: ${error.message}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
module.exports = start;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: status
|
|
3
|
+
* Checks the status of the Autopilot watcher
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs-extra');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const process = require('process');
|
|
9
|
+
const logger = require('../utils/logger');
|
|
10
|
+
const { getRunningPid } = require('../utils/process');
|
|
11
|
+
|
|
12
|
+
const status = async () => {
|
|
13
|
+
const repoPath = process.cwd();
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const pid = await getRunningPid(repoPath);
|
|
17
|
+
|
|
18
|
+
logger.section('Autopilot Status');
|
|
19
|
+
|
|
20
|
+
if (pid) {
|
|
21
|
+
logger.success(`Status: Running`);
|
|
22
|
+
logger.info(`PID: ${pid}`);
|
|
23
|
+
} else {
|
|
24
|
+
logger.warn('Status: Not Running');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Show recent logs if available
|
|
28
|
+
const logPath = path.join(repoPath, 'autopilot.log');
|
|
29
|
+
if (await fs.pathExists(logPath)) {
|
|
30
|
+
logger.section('Recent Logs');
|
|
31
|
+
try {
|
|
32
|
+
const logs = await fs.readFile(logPath, 'utf-8');
|
|
33
|
+
const lines = logs.trim().split('\n');
|
|
34
|
+
const lastLines = lines.slice(-5); // Show last 5 lines
|
|
35
|
+
|
|
36
|
+
if (lastLines.length > 0) {
|
|
37
|
+
lastLines.forEach(line => console.log(line));
|
|
38
|
+
} else {
|
|
39
|
+
console.log('(Log file is empty)');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
logger.info(`\nFull log: ${logPath}`);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
logger.error(`Could not read log file: ${error.message}`);
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
logger.info('No log file found.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
} catch (error) {
|
|
51
|
+
logger.error(`Error checking status: ${error.message}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
module.exports = status;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: stop
|
|
3
|
+
* Stops the running Autopilot instance
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const process = require('process');
|
|
7
|
+
const logger = require('../utils/logger');
|
|
8
|
+
const { getRunningPid, removePid } = require('../utils/process');
|
|
9
|
+
|
|
10
|
+
const stop = async () => {
|
|
11
|
+
const repoPath = process.cwd();
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const pid = await getRunningPid(repoPath);
|
|
15
|
+
|
|
16
|
+
if (!pid) {
|
|
17
|
+
logger.info('Autopilot is not running.');
|
|
18
|
+
// Clean up stale PID file just in case
|
|
19
|
+
await removePid(repoPath);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
logger.info(`Stopping Autopilot (PID: ${pid})...`);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Send SIGTERM to the process
|
|
27
|
+
process.kill(pid, 'SIGTERM');
|
|
28
|
+
|
|
29
|
+
logger.success('Autopilot stopped successfully.');
|
|
30
|
+
|
|
31
|
+
// Cleanup PID file
|
|
32
|
+
await removePid(repoPath);
|
|
33
|
+
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error.code === 'ESRCH') {
|
|
36
|
+
logger.warn('Process not found (stale PID file). Cleaning up...');
|
|
37
|
+
await removePid(repoPath);
|
|
38
|
+
logger.success('Cleaned up stale lock file.');
|
|
39
|
+
} else {
|
|
40
|
+
logger.error(`Failed to stop process: ${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
} catch (error) {
|
|
45
|
+
logger.error(`Error stopping autopilot: ${error.message}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
module.exports = stop;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default configuration values for Autopilot
|
|
3
|
+
* Built by Praise Masunga (PraiseTechzw)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CONFIG = {
|
|
7
|
+
debounceSeconds: 20,
|
|
8
|
+
minSecondsBetweenCommits: 180,
|
|
9
|
+
autoPush: true,
|
|
10
|
+
blockBranches: ['main', 'master'],
|
|
11
|
+
requireChecks: false,
|
|
12
|
+
checks: [],
|
|
13
|
+
commitMessageMode: 'smart', // smart | simple
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const DEFAULT_IGNORE_PATTERNS = [
|
|
17
|
+
'node_modules/',
|
|
18
|
+
'dist/',
|
|
19
|
+
'build/',
|
|
20
|
+
'.next/',
|
|
21
|
+
'.env',
|
|
22
|
+
'.env.*',
|
|
23
|
+
'coverage/',
|
|
24
|
+
'*.log',
|
|
25
|
+
'.DS_Store',
|
|
26
|
+
'.git/',
|
|
27
|
+
'.idea/',
|
|
28
|
+
'.vscode/'
|
|
29
|
+
].join('\n');
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
DEFAULT_CONFIG,
|
|
33
|
+
DEFAULT_IGNORE_PATTERNS,
|
|
34
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +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
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart commit message generator
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate a conventional commit message based on changed files
|
|
7
|
+
* @param {string[]} files - Array of changed file paths
|
|
8
|
+
* @returns {string} Conventional commit message
|
|
9
|
+
*/
|
|
10
|
+
function generateCommitMessage(files) {
|
|
11
|
+
if (!files || files.length === 0) {
|
|
12
|
+
return 'chore: update changes';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const categories = {
|
|
16
|
+
fix: false,
|
|
17
|
+
feat: false,
|
|
18
|
+
docs: false,
|
|
19
|
+
test: false,
|
|
20
|
+
chore: false, // Generic chore (unknown files)
|
|
21
|
+
config: false, // Specific config files
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
for (const file of files) {
|
|
25
|
+
const lowerFile = file.toLowerCase();
|
|
26
|
+
const normalized = lowerFile.replace(/\\/g, '/');
|
|
27
|
+
let matched = false;
|
|
28
|
+
|
|
29
|
+
// Fix detection (highest priority)
|
|
30
|
+
if (
|
|
31
|
+
normalized.includes('/fix/') ||
|
|
32
|
+
normalized.includes('fix-') ||
|
|
33
|
+
normalized.includes('-fix') ||
|
|
34
|
+
normalized.includes('bugfix') ||
|
|
35
|
+
normalized.includes('hotfix') ||
|
|
36
|
+
normalized.includes('error') ||
|
|
37
|
+
normalized.includes('exception') ||
|
|
38
|
+
normalized.endsWith('error.js') ||
|
|
39
|
+
normalized.endsWith('error.ts')
|
|
40
|
+
) {
|
|
41
|
+
categories.fix = true;
|
|
42
|
+
matched = true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Docs detection
|
|
46
|
+
if (normalized.endsWith('.md') || normalized.endsWith('.txt')) {
|
|
47
|
+
categories.docs = true;
|
|
48
|
+
matched = true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Test detection
|
|
52
|
+
if (
|
|
53
|
+
normalized.includes('.test.') ||
|
|
54
|
+
normalized.includes('.spec.') ||
|
|
55
|
+
normalized.includes('__tests__')
|
|
56
|
+
) {
|
|
57
|
+
categories.test = true;
|
|
58
|
+
matched = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Feat detection - src/ changes that aren't tests
|
|
62
|
+
if (
|
|
63
|
+
(normalized.startsWith('src/') || normalized.includes('/src/')) &&
|
|
64
|
+
!categories.test // Ensure we don't count tests as features if they happen to be in src
|
|
65
|
+
) {
|
|
66
|
+
categories.feat = true;
|
|
67
|
+
matched = true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Config files detection
|
|
71
|
+
if (
|
|
72
|
+
normalized === 'package.json' ||
|
|
73
|
+
normalized.endsWith('.yml') ||
|
|
74
|
+
normalized.endsWith('.yaml') ||
|
|
75
|
+
normalized.endsWith('.json') ||
|
|
76
|
+
normalized.endsWith('.config.js') ||
|
|
77
|
+
normalized.endsWith('.config.ts') ||
|
|
78
|
+
normalized.includes('.github/') ||
|
|
79
|
+
normalized.includes('.gitignore') ||
|
|
80
|
+
normalized.includes('.editorconfig')
|
|
81
|
+
) {
|
|
82
|
+
categories.config = true;
|
|
83
|
+
matched = true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Default to chore for other files
|
|
87
|
+
if (!matched) {
|
|
88
|
+
categories.chore = true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Priority order: fix > feat > docs > test > chore
|
|
93
|
+
if (categories.fix) {
|
|
94
|
+
return 'fix: resolve issues';
|
|
95
|
+
}
|
|
96
|
+
if (categories.feat) {
|
|
97
|
+
return 'feat: add new features';
|
|
98
|
+
}
|
|
99
|
+
if (categories.docs) {
|
|
100
|
+
return 'docs: update documentation';
|
|
101
|
+
}
|
|
102
|
+
if (categories.test) {
|
|
103
|
+
return 'test: update tests';
|
|
104
|
+
}
|
|
105
|
+
// "chore" category splits into explicit config vs generic changes
|
|
106
|
+
if (categories.config) {
|
|
107
|
+
return 'chore: update configuration';
|
|
108
|
+
}
|
|
109
|
+
if (categories.chore) {
|
|
110
|
+
return 'chore: update changes';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return 'chore: update changes';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = { generateCommitMessage };
|