@traisetech/autopilot 2.3.0 → 2.5.0
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 +28 -1
- package/README.md +215 -202
- package/bin/autopilot.js +9 -2
- package/docs/CONFIGURATION.md +103 -103
- package/docs/DESIGN_PRINCIPLES.md +114 -114
- package/docs/TEAM-MODE.md +51 -51
- package/docs/TROUBLESHOOTING.md +21 -21
- package/package.json +75 -69
- package/src/commands/config.js +110 -110
- package/src/commands/dashboard.mjs +151 -151
- package/src/commands/doctor.js +127 -153
- package/src/commands/guide.js +63 -0
- package/src/commands/init.js +8 -9
- package/src/commands/insights.js +237 -237
- package/src/commands/leaderboard.js +116 -116
- package/src/commands/pause.js +18 -18
- package/src/commands/preset.js +121 -121
- package/src/commands/resume.js +17 -17
- package/src/commands/start.js +41 -41
- package/src/commands/status.js +73 -39
- package/src/commands/stop.js +58 -50
- package/src/commands/undo.js +84 -84
- package/src/config/defaults.js +23 -16
- package/src/config/ignore.js +14 -31
- package/src/config/loader.js +80 -80
- package/src/core/commit.js +45 -52
- package/src/core/commitMessageGenerator.js +130 -0
- package/src/core/configValidator.js +92 -0
- package/src/core/events.js +110 -110
- package/src/core/focus.js +2 -1
- package/src/core/gemini.js +15 -15
- package/src/core/git.js +29 -2
- package/src/core/history.js +69 -69
- package/src/core/notifier.js +61 -0
- package/src/core/retryQueue.js +152 -0
- package/src/core/safety.js +224 -210
- package/src/core/state.js +69 -71
- package/src/core/watcher.js +193 -66
- package/src/index.js +70 -70
- package/src/utils/banner.js +6 -6
- package/src/utils/crypto.js +18 -18
- package/src/utils/identity.js +41 -41
- package/src/utils/logger.js +86 -68
- package/src/utils/paths.js +62 -62
- package/src/utils/process.js +141 -141
package/src/utils/identity.js
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
const fs = require('fs-extra');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const crypto = require('crypto');
|
|
4
|
-
const { getConfigDir } = require('./paths');
|
|
5
|
-
const logger = require('./logger');
|
|
6
|
-
|
|
7
|
-
const getIdentityPath = () => path.join(getConfigDir(), 'identity.json');
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Get or create the anonymous user identity
|
|
11
|
-
* @returns {Promise<{id: string, created: number}>} Identity object
|
|
12
|
-
*/
|
|
13
|
-
async function getIdentity() {
|
|
14
|
-
try {
|
|
15
|
-
const identityPath = getIdentityPath();
|
|
16
|
-
|
|
17
|
-
if (await fs.pathExists(identityPath)) {
|
|
18
|
-
return await fs.readJson(identityPath);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Create new identity
|
|
22
|
-
const identity = {
|
|
23
|
-
id: crypto.randomUUID(),
|
|
24
|
-
created: Date.now()
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
await fs.ensureDir(getConfigDir());
|
|
28
|
-
await fs.writeJson(identityPath, identity, { spaces: 2 });
|
|
29
|
-
logger.debug(`Created new anonymous identity: ${identity.id}`);
|
|
30
|
-
|
|
31
|
-
return identity;
|
|
32
|
-
} catch (error) {
|
|
33
|
-
logger.error(`Failed to manage identity: ${error.message}`);
|
|
34
|
-
// Fallback to memory-only ID if filesystem fails
|
|
35
|
-
return { id: crypto.randomUUID(), created: Date.now() };
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
module.exports = {
|
|
40
|
-
getIdentity
|
|
41
|
-
};
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { getConfigDir } = require('./paths');
|
|
5
|
+
const logger = require('./logger');
|
|
6
|
+
|
|
7
|
+
const getIdentityPath = () => path.join(getConfigDir(), 'identity.json');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get or create the anonymous user identity
|
|
11
|
+
* @returns {Promise<{id: string, created: number}>} Identity object
|
|
12
|
+
*/
|
|
13
|
+
async function getIdentity() {
|
|
14
|
+
try {
|
|
15
|
+
const identityPath = getIdentityPath();
|
|
16
|
+
|
|
17
|
+
if (await fs.pathExists(identityPath)) {
|
|
18
|
+
return await fs.readJson(identityPath);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Create new identity
|
|
22
|
+
const identity = {
|
|
23
|
+
id: crypto.randomUUID(),
|
|
24
|
+
created: Date.now()
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
await fs.ensureDir(getConfigDir());
|
|
28
|
+
await fs.writeJson(identityPath, identity, { spaces: 2 });
|
|
29
|
+
logger.debug(`Created new anonymous identity: ${identity.id}`);
|
|
30
|
+
|
|
31
|
+
return identity;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
logger.error(`Failed to manage identity: ${error.message}`);
|
|
34
|
+
// Fallback to memory-only ID if filesystem fails
|
|
35
|
+
return { id: crypto.randomUUID(), created: Date.now() };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
getIdentity
|
|
41
|
+
};
|
package/src/utils/logger.js
CHANGED
|
@@ -1,68 +1,86 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
},
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
console.log(
|
|
64
|
-
|
|
65
|
-
},
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const LOG_FILENAME = '.autopilot.log';
|
|
5
|
+
const MAX_LOG_SIZE = 500 * 1024; // 500KB
|
|
6
|
+
const KEEP_LINES = 200;
|
|
7
|
+
|
|
8
|
+
let targetPath = null;
|
|
9
|
+
|
|
10
|
+
const logger = {
|
|
11
|
+
colors: {
|
|
12
|
+
cyan: (text) => `\x1b[36m${text}\x1b[0m`,
|
|
13
|
+
green: (text) => `\x1b[32m${text}\x1b[0m`,
|
|
14
|
+
yellow: (text) => `\x1b[33m${text}\x1b[0m`,
|
|
15
|
+
red: (text) => `\x1b[31m${text}\x1b[0m`,
|
|
16
|
+
blue: (text) => `\x1b[34m${text}\x1b[0m`,
|
|
17
|
+
bold: (text) => `\x1b[1m${text}\x1b[0m`
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
setTargetPath: (dir) => {
|
|
21
|
+
targetPath = dir;
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
_writeToFile: (message) => {
|
|
25
|
+
if (!targetPath) return; // Only log to file if target path is set
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const logPath = path.join(targetPath, LOG_FILENAME);
|
|
29
|
+
const timestamp = new Date().toISOString();
|
|
30
|
+
const line = `[${timestamp}] ${message}\n`;
|
|
31
|
+
|
|
32
|
+
// Check size for rotation - only occasionally to avoid heavy I/O
|
|
33
|
+
// We'll trust append for speed and only rotate if we happen to check
|
|
34
|
+
if (Math.random() < 0.05 && fs.existsSync(logPath)) {
|
|
35
|
+
const stats = fs.statSync(logPath);
|
|
36
|
+
if (stats.size > MAX_LOG_SIZE) {
|
|
37
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
38
|
+
const lines = content.split('\n');
|
|
39
|
+
const keptLines = lines.slice(-KEEP_LINES).join('\n');
|
|
40
|
+
fs.writeFileSync(logPath, keptLines + '\n');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fs.appendFileSync(logPath, line);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
// Silent fail
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
info: (message) => {
|
|
51
|
+
console.log(`${logger.colors.cyan('ℹ️')} ${message}`);
|
|
52
|
+
logger._writeToFile(`INFO: ${message}`);
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
debug: (message) => {
|
|
56
|
+
if (process.env.DEBUG) {
|
|
57
|
+
console.log(`${logger.colors.blue('🔍')} ${message}`);
|
|
58
|
+
}
|
|
59
|
+
logger._writeToFile(`DEBUG: ${message}`);
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
success: (message) => {
|
|
63
|
+
console.log(`${logger.colors.green('✅')} ${message}`);
|
|
64
|
+
logger._writeToFile(`SUCCESS: ${message}`);
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
warn: (message) => {
|
|
68
|
+
console.warn(`${logger.colors.yellow('⚠️')} ${message}`);
|
|
69
|
+
logger._writeToFile(`WARN: ${message}`);
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
error: (message) => {
|
|
73
|
+
console.error(`${logger.colors.red('❌')} ${message}`);
|
|
74
|
+
logger._writeToFile(`ERROR: ${message}`);
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
section: (title) => {
|
|
78
|
+
const sep = '─'.repeat(50);
|
|
79
|
+
console.log(`\n${logger.colors.bold(logger.colors.cyan(title))}`);
|
|
80
|
+
console.log(logger.colors.cyan(sep));
|
|
81
|
+
logger._writeToFile(`SECTION: ${title}`);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
module.exports = logger;
|
|
86
|
+
|
package/src/utils/paths.js
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Path utilities for Autopilot
|
|
3
|
-
* Built by Praise Masunga (PraiseTechzw)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const os = require('os');
|
|
8
|
-
const fs = require('fs-extra');
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Get path to config file in repository
|
|
12
|
-
* @param {string} repoPath - Repository path
|
|
13
|
-
* @returns {string} Config file path
|
|
14
|
-
*/
|
|
15
|
-
const getConfigPath = (repoPath) => {
|
|
16
|
-
return path.join(repoPath, '.autopilotrc.json');
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Get path to ignore file in repository
|
|
21
|
-
* @param {string} repoPath - Repository path
|
|
22
|
-
* @returns {string} Ignore file path
|
|
23
|
-
*/
|
|
24
|
-
const getIgnorePath = (repoPath) => {
|
|
25
|
-
return path.join(repoPath, '.autopilotignore');
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Get path to .git directory
|
|
30
|
-
* @param {string} repoPath - Repository path
|
|
31
|
-
* @returns {string} .git directory path
|
|
32
|
-
*/
|
|
33
|
-
const getGitPath = (repoPath) => {
|
|
34
|
-
return path.join(repoPath, '.git');
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Get global config directory
|
|
39
|
-
* @returns {string} Config directory path
|
|
40
|
-
*/
|
|
41
|
-
const getConfigDir = () => {
|
|
42
|
-
if (process.env.AUTOPILOT_CONFIG_DIR) {
|
|
43
|
-
return process.env.AUTOPILOT_CONFIG_DIR;
|
|
44
|
-
}
|
|
45
|
-
return path.join(os.homedir(), '.autopilot');
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Ensure config directory exists
|
|
50
|
-
* @returns {Promise<void>}
|
|
51
|
-
*/
|
|
52
|
-
const ensureConfigDir = async () => {
|
|
53
|
-
await fs.ensureDir(getConfigDir());
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
module.exports = {
|
|
57
|
-
getConfigPath,
|
|
58
|
-
getIgnorePath,
|
|
59
|
-
getGitPath,
|
|
60
|
-
getConfigDir,
|
|
61
|
-
ensureConfigDir,
|
|
62
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Path utilities for Autopilot
|
|
3
|
+
* Built by Praise Masunga (PraiseTechzw)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const fs = require('fs-extra');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get path to config file in repository
|
|
12
|
+
* @param {string} repoPath - Repository path
|
|
13
|
+
* @returns {string} Config file path
|
|
14
|
+
*/
|
|
15
|
+
const getConfigPath = (repoPath) => {
|
|
16
|
+
return path.join(repoPath, '.autopilotrc.json');
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get path to ignore file in repository
|
|
21
|
+
* @param {string} repoPath - Repository path
|
|
22
|
+
* @returns {string} Ignore file path
|
|
23
|
+
*/
|
|
24
|
+
const getIgnorePath = (repoPath) => {
|
|
25
|
+
return path.join(repoPath, '.autopilotignore');
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get path to .git directory
|
|
30
|
+
* @param {string} repoPath - Repository path
|
|
31
|
+
* @returns {string} .git directory path
|
|
32
|
+
*/
|
|
33
|
+
const getGitPath = (repoPath) => {
|
|
34
|
+
return path.join(repoPath, '.git');
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get global config directory
|
|
39
|
+
* @returns {string} Config directory path
|
|
40
|
+
*/
|
|
41
|
+
const getConfigDir = () => {
|
|
42
|
+
if (process.env.AUTOPILOT_CONFIG_DIR) {
|
|
43
|
+
return process.env.AUTOPILOT_CONFIG_DIR;
|
|
44
|
+
}
|
|
45
|
+
return path.join(os.homedir(), '.autopilot');
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Ensure config directory exists
|
|
50
|
+
* @returns {Promise<void>}
|
|
51
|
+
*/
|
|
52
|
+
const ensureConfigDir = async () => {
|
|
53
|
+
await fs.ensureDir(getConfigDir());
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
module.exports = {
|
|
57
|
+
getConfigPath,
|
|
58
|
+
getIgnorePath,
|
|
59
|
+
getGitPath,
|
|
60
|
+
getConfigDir,
|
|
61
|
+
ensureConfigDir,
|
|
62
|
+
};
|
package/src/utils/process.js
CHANGED
|
@@ -1,141 +1,141 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Process management utilities
|
|
3
|
-
* Built by Praise Masunga (PraiseTechzw)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const fs = require('fs-extra');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
const logger = require('./logger');
|
|
9
|
-
const { ensureConfigDir } = require('./paths');
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Get path to PID file for a repository or global
|
|
13
|
-
* @param {string} [repoPath] - Repository path
|
|
14
|
-
* @returns {string} PID file path
|
|
15
|
-
*/
|
|
16
|
-
const getPidPath = (repoPath) => {
|
|
17
|
-
if (repoPath) {
|
|
18
|
-
return path.join(repoPath, '.autopilot.pid');
|
|
19
|
-
}
|
|
20
|
-
// Fallback to global pid if needed, though mostly used per-repo
|
|
21
|
-
return path.join(require('os').homedir(), '.autopilot', 'autopilot.pid');
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Save current process PID to file
|
|
26
|
-
* @param {string} repoPath - Repository path
|
|
27
|
-
*/
|
|
28
|
-
const savePid = async (repoPath) => {
|
|
29
|
-
try {
|
|
30
|
-
const pidPath = getPidPath(repoPath);
|
|
31
|
-
await fs.writeFile(pidPath, process.pid.toString());
|
|
32
|
-
} catch (error) {
|
|
33
|
-
logger.error(`Failed to save PID file: ${error.message}`);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Remove PID file
|
|
39
|
-
* @param {string} repoPath - Repository path
|
|
40
|
-
*/
|
|
41
|
-
const removePid = async (repoPath) => {
|
|
42
|
-
try {
|
|
43
|
-
const pidPath = getPidPath(repoPath);
|
|
44
|
-
if (await fs.pathExists(pidPath)) {
|
|
45
|
-
await fs.remove(pidPath);
|
|
46
|
-
}
|
|
47
|
-
} catch (error) {
|
|
48
|
-
// Ignore errors during cleanup
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Check if a process is running
|
|
54
|
-
* @param {number} pid - Process ID
|
|
55
|
-
* @returns {boolean} True if running
|
|
56
|
-
*/
|
|
57
|
-
const isProcessRunning = (pid) => {
|
|
58
|
-
try {
|
|
59
|
-
process.kill(pid, 0);
|
|
60
|
-
return true;
|
|
61
|
-
} catch (e) {
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Read PID from file and check if running
|
|
68
|
-
* @param {string} repoPath - Repository path
|
|
69
|
-
* @returns {Promise<number|null>} PID if running, null otherwise
|
|
70
|
-
*/
|
|
71
|
-
const getRunningPid = async (repoPath) => {
|
|
72
|
-
try {
|
|
73
|
-
const pidPath = getPidPath(repoPath);
|
|
74
|
-
if (await fs.pathExists(pidPath)) {
|
|
75
|
-
const pid = parseInt(await fs.readFile(pidPath, 'utf-8'), 10);
|
|
76
|
-
if (isProcessRunning(pid)) {
|
|
77
|
-
return pid;
|
|
78
|
-
}
|
|
79
|
-
// Stale PID file
|
|
80
|
-
await removePid(repoPath);
|
|
81
|
-
}
|
|
82
|
-
return null;
|
|
83
|
-
} catch (error) {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Register process signal handlers for graceful shutdown
|
|
90
|
-
* @param {Function} cleanupFn - Async cleanup function to run on exit
|
|
91
|
-
*/
|
|
92
|
-
const registerProcessHandlers = (cleanupFn) => {
|
|
93
|
-
let cleaningUp = false;
|
|
94
|
-
|
|
95
|
-
const handleSignal = async (signal) => {
|
|
96
|
-
if (cleaningUp) return;
|
|
97
|
-
cleaningUp = true;
|
|
98
|
-
|
|
99
|
-
logger.info(`Received ${signal}, shutting down...`);
|
|
100
|
-
|
|
101
|
-
try {
|
|
102
|
-
if (cleanupFn) {
|
|
103
|
-
await cleanupFn();
|
|
104
|
-
}
|
|
105
|
-
} catch (error) {
|
|
106
|
-
logger.error(`Error during cleanup: ${error.message}`);
|
|
107
|
-
} finally {
|
|
108
|
-
process.exit(0);
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
process.on('SIGINT', () => handleSignal('SIGINT'));
|
|
113
|
-
process.on('SIGTERM', () => handleSignal('SIGTERM'));
|
|
114
|
-
|
|
115
|
-
// Handle uncaught errors to try to cleanup if possible
|
|
116
|
-
process.on('uncaughtException', async (error) => {
|
|
117
|
-
logger.error(`Uncaught Exception: ${error.message}`);
|
|
118
|
-
logger.error(error.stack);
|
|
119
|
-
if (!cleaningUp) {
|
|
120
|
-
cleaningUp = true;
|
|
121
|
-
try {
|
|
122
|
-
if (cleanupFn) await cleanupFn();
|
|
123
|
-
} catch (e) {
|
|
124
|
-
// Ignore
|
|
125
|
-
}
|
|
126
|
-
process.exit(1);
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
process.on('unhandledRejection', async (reason) => {
|
|
131
|
-
logger.error(`Unhandled Rejection: ${reason}`);
|
|
132
|
-
});
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
module.exports = {
|
|
136
|
-
savePid,
|
|
137
|
-
removePid,
|
|
138
|
-
getRunningPid,
|
|
139
|
-
isProcessRunning,
|
|
140
|
-
registerProcessHandlers
|
|
141
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Process management utilities
|
|
3
|
+
* Built by Praise Masunga (PraiseTechzw)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs-extra');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const logger = require('./logger');
|
|
9
|
+
const { ensureConfigDir } = require('./paths');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get path to PID file for a repository or global
|
|
13
|
+
* @param {string} [repoPath] - Repository path
|
|
14
|
+
* @returns {string} PID file path
|
|
15
|
+
*/
|
|
16
|
+
const getPidPath = (repoPath) => {
|
|
17
|
+
if (repoPath) {
|
|
18
|
+
return path.join(repoPath, '.autopilot.pid');
|
|
19
|
+
}
|
|
20
|
+
// Fallback to global pid if needed, though mostly used per-repo
|
|
21
|
+
return path.join(require('os').homedir(), '.autopilot', 'autopilot.pid');
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Save current process PID to file
|
|
26
|
+
* @param {string} repoPath - Repository path
|
|
27
|
+
*/
|
|
28
|
+
const savePid = async (repoPath) => {
|
|
29
|
+
try {
|
|
30
|
+
const pidPath = getPidPath(repoPath);
|
|
31
|
+
await fs.writeFile(pidPath, process.pid.toString());
|
|
32
|
+
} catch (error) {
|
|
33
|
+
logger.error(`Failed to save PID file: ${error.message}`);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Remove PID file
|
|
39
|
+
* @param {string} repoPath - Repository path
|
|
40
|
+
*/
|
|
41
|
+
const removePid = async (repoPath) => {
|
|
42
|
+
try {
|
|
43
|
+
const pidPath = getPidPath(repoPath);
|
|
44
|
+
if (await fs.pathExists(pidPath)) {
|
|
45
|
+
await fs.remove(pidPath);
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
// Ignore errors during cleanup
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if a process is running
|
|
54
|
+
* @param {number} pid - Process ID
|
|
55
|
+
* @returns {boolean} True if running
|
|
56
|
+
*/
|
|
57
|
+
const isProcessRunning = (pid) => {
|
|
58
|
+
try {
|
|
59
|
+
process.kill(pid, 0);
|
|
60
|
+
return true;
|
|
61
|
+
} catch (e) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Read PID from file and check if running
|
|
68
|
+
* @param {string} repoPath - Repository path
|
|
69
|
+
* @returns {Promise<number|null>} PID if running, null otherwise
|
|
70
|
+
*/
|
|
71
|
+
const getRunningPid = async (repoPath) => {
|
|
72
|
+
try {
|
|
73
|
+
const pidPath = getPidPath(repoPath);
|
|
74
|
+
if (await fs.pathExists(pidPath)) {
|
|
75
|
+
const pid = parseInt(await fs.readFile(pidPath, 'utf-8'), 10);
|
|
76
|
+
if (isProcessRunning(pid)) {
|
|
77
|
+
return pid;
|
|
78
|
+
}
|
|
79
|
+
// Stale PID file
|
|
80
|
+
await removePid(repoPath);
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Register process signal handlers for graceful shutdown
|
|
90
|
+
* @param {Function} cleanupFn - Async cleanup function to run on exit
|
|
91
|
+
*/
|
|
92
|
+
const registerProcessHandlers = (cleanupFn) => {
|
|
93
|
+
let cleaningUp = false;
|
|
94
|
+
|
|
95
|
+
const handleSignal = async (signal) => {
|
|
96
|
+
if (cleaningUp) return;
|
|
97
|
+
cleaningUp = true;
|
|
98
|
+
|
|
99
|
+
logger.info(`Received ${signal}, shutting down...`);
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
if (cleanupFn) {
|
|
103
|
+
await cleanupFn();
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
logger.error(`Error during cleanup: ${error.message}`);
|
|
107
|
+
} finally {
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
process.on('SIGINT', () => handleSignal('SIGINT'));
|
|
113
|
+
process.on('SIGTERM', () => handleSignal('SIGTERM'));
|
|
114
|
+
|
|
115
|
+
// Handle uncaught errors to try to cleanup if possible
|
|
116
|
+
process.on('uncaughtException', async (error) => {
|
|
117
|
+
logger.error(`Uncaught Exception: ${error.message}`);
|
|
118
|
+
logger.error(error.stack);
|
|
119
|
+
if (!cleaningUp) {
|
|
120
|
+
cleaningUp = true;
|
|
121
|
+
try {
|
|
122
|
+
if (cleanupFn) await cleanupFn();
|
|
123
|
+
} catch (e) {
|
|
124
|
+
// Ignore
|
|
125
|
+
}
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
process.on('unhandledRejection', async (reason) => {
|
|
131
|
+
logger.error(`Unhandled Rejection: ${reason}`);
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
savePid,
|
|
137
|
+
removePid,
|
|
138
|
+
getRunningPid,
|
|
139
|
+
isProcessRunning,
|
|
140
|
+
registerProcessHandlers
|
|
141
|
+
};
|