@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/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 };