teleportation-cli 1.0.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/.claude/hooks/config-loader.mjs +93 -0
- package/.claude/hooks/heartbeat.mjs +331 -0
- package/.claude/hooks/notification.mjs +35 -0
- package/.claude/hooks/permission_request.mjs +307 -0
- package/.claude/hooks/post_tool_use.mjs +137 -0
- package/.claude/hooks/pre_tool_use.mjs +451 -0
- package/.claude/hooks/session-register.mjs +274 -0
- package/.claude/hooks/session_end.mjs +256 -0
- package/.claude/hooks/session_start.mjs +308 -0
- package/.claude/hooks/stop.mjs +277 -0
- package/.claude/hooks/user_prompt_submit.mjs +91 -0
- package/LICENSE +21 -0
- package/README.md +243 -0
- package/lib/auth/api-key.js +110 -0
- package/lib/auth/credentials.js +341 -0
- package/lib/backup/manager.js +461 -0
- package/lib/cli/daemon-commands.js +299 -0
- package/lib/cli/index.js +303 -0
- package/lib/cli/session-commands.js +294 -0
- package/lib/cli/snapshot-commands.js +223 -0
- package/lib/cli/worktree-commands.js +291 -0
- package/lib/config/manager.js +306 -0
- package/lib/daemon/lifecycle.js +336 -0
- package/lib/daemon/pid-manager.js +160 -0
- package/lib/daemon/teleportation-daemon.js +2009 -0
- package/lib/handoff/config.js +102 -0
- package/lib/handoff/example.js +152 -0
- package/lib/handoff/git-handoff.js +351 -0
- package/lib/handoff/handoff.js +277 -0
- package/lib/handoff/index.js +25 -0
- package/lib/handoff/session-state.js +238 -0
- package/lib/install/installer.js +555 -0
- package/lib/machine-coders/claude-code-adapter.js +329 -0
- package/lib/machine-coders/example.js +239 -0
- package/lib/machine-coders/gemini-cli-adapter.js +406 -0
- package/lib/machine-coders/index.js +103 -0
- package/lib/machine-coders/interface.js +168 -0
- package/lib/router/classifier.js +251 -0
- package/lib/router/example.js +92 -0
- package/lib/router/index.js +69 -0
- package/lib/router/mech-llms-client.js +277 -0
- package/lib/router/models.js +188 -0
- package/lib/router/router.js +382 -0
- package/lib/session/cleanup.js +100 -0
- package/lib/session/metadata.js +258 -0
- package/lib/session/mute-checker.js +114 -0
- package/lib/session-registry/manager.js +302 -0
- package/lib/snapshot/manager.js +390 -0
- package/lib/utils/errors.js +166 -0
- package/lib/utils/logger.js +148 -0
- package/lib/utils/retry.js +155 -0
- package/lib/worktree/manager.js +301 -0
- package/package.json +66 -0
- package/teleportation-cli.cjs +2987 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import {
|
|
5
|
+
checkDaemonStatus,
|
|
6
|
+
acquirePidLock,
|
|
7
|
+
releasePidLock,
|
|
8
|
+
isProcessRunning
|
|
9
|
+
} from './pid-manager.js';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
|
|
14
|
+
// Path to daemon entry point (relative to lifecycle.js location)
|
|
15
|
+
const DAEMON_SCRIPT = join(__dirname, 'teleportation-daemon.js');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Start the daemon process
|
|
19
|
+
* @param {Object} options - Start options
|
|
20
|
+
* @param {boolean} options.detached - Run daemon as detached process (default: true)
|
|
21
|
+
* @param {boolean} options.silent - Suppress output (default: true)
|
|
22
|
+
* @returns {Promise<{pid: number, success: boolean}>}
|
|
23
|
+
*/
|
|
24
|
+
export async function startDaemon(options = {}) {
|
|
25
|
+
const { detached = true, silent = true } = options;
|
|
26
|
+
|
|
27
|
+
// Check if daemon is already running
|
|
28
|
+
const status = await checkDaemonStatus();
|
|
29
|
+
if (status.running) {
|
|
30
|
+
throw new Error(`Daemon already running with PID ${status.pid}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Clean up stale PID file if exists
|
|
34
|
+
if (status.stale) {
|
|
35
|
+
await releasePidLock(status.pid);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Spawn daemon process
|
|
39
|
+
const child = spawn(
|
|
40
|
+
process.execPath, // Use same Node.js executable
|
|
41
|
+
[DAEMON_SCRIPT],
|
|
42
|
+
{
|
|
43
|
+
detached,
|
|
44
|
+
stdio: silent ? 'ignore' : 'inherit',
|
|
45
|
+
env: {
|
|
46
|
+
...process.env,
|
|
47
|
+
TELEPORTATION_DAEMON: 'true'
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Detach from parent if requested
|
|
53
|
+
if (detached) {
|
|
54
|
+
child.unref();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Wait a moment to ensure process started (increase wait time for CI)
|
|
58
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
59
|
+
|
|
60
|
+
// Verify daemon is running (check multiple times for slow CI)
|
|
61
|
+
let newStatus = await checkDaemonStatus();
|
|
62
|
+
if (!newStatus.running) {
|
|
63
|
+
// Wait a bit more and check again
|
|
64
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
65
|
+
newStatus = await checkDaemonStatus();
|
|
66
|
+
if (!newStatus.running) {
|
|
67
|
+
throw new Error('Daemon failed to start');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
pid: child.pid,
|
|
73
|
+
success: true
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Stop the daemon process
|
|
79
|
+
* @param {Object} options - Stop options
|
|
80
|
+
* @param {number} options.timeout - Timeout in ms for graceful shutdown (default: 5000)
|
|
81
|
+
* @param {boolean} options.force - Force kill if graceful shutdown fails (default: true)
|
|
82
|
+
* @returns {Promise<{success: boolean, forced: boolean}>}
|
|
83
|
+
*/
|
|
84
|
+
export async function stopDaemon(options = {}) {
|
|
85
|
+
const { timeout = 5000, force = true } = options;
|
|
86
|
+
|
|
87
|
+
// Check daemon status
|
|
88
|
+
const status = await checkDaemonStatus();
|
|
89
|
+
if (!status.running) {
|
|
90
|
+
return { success: true, forced: false };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const { pid } = status;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// Send SIGTERM for graceful shutdown
|
|
97
|
+
process.kill(pid, 'SIGTERM');
|
|
98
|
+
|
|
99
|
+
// Wait for process to exit
|
|
100
|
+
const startTime = Date.now();
|
|
101
|
+
while (Date.now() - startTime < timeout) {
|
|
102
|
+
if (!isProcessRunning(pid)) {
|
|
103
|
+
await releasePidLock(pid);
|
|
104
|
+
return { success: true, forced: false };
|
|
105
|
+
}
|
|
106
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Timeout reached, force kill if requested
|
|
110
|
+
if (force) {
|
|
111
|
+
process.kill(pid, 'SIGKILL');
|
|
112
|
+
|
|
113
|
+
// Wait a moment for kill to take effect
|
|
114
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
115
|
+
|
|
116
|
+
if (!isProcessRunning(pid)) {
|
|
117
|
+
await releasePidLock(pid);
|
|
118
|
+
return { success: true, forced: true };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
throw new Error('Failed to kill daemon process');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { success: false, forced: false };
|
|
125
|
+
} catch (err) {
|
|
126
|
+
// Process might have already exited
|
|
127
|
+
if (err.code === 'ESRCH') {
|
|
128
|
+
await releasePidLock(pid);
|
|
129
|
+
return { success: true, forced: false };
|
|
130
|
+
}
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Restart the daemon process
|
|
137
|
+
* @param {Object} options - Restart options
|
|
138
|
+
* @param {number} options.stopTimeout - Timeout for stop operation (default: 5000)
|
|
139
|
+
* @param {boolean} options.force - Force kill on stop timeout (default: true)
|
|
140
|
+
* @returns {Promise<{pid: number, success: boolean, wasRunning: boolean}>}
|
|
141
|
+
*/
|
|
142
|
+
export async function restartDaemon(options = {}) {
|
|
143
|
+
const { stopTimeout = 5000, force = true } = options;
|
|
144
|
+
|
|
145
|
+
// Check if daemon is running
|
|
146
|
+
const status = await checkDaemonStatus();
|
|
147
|
+
const wasRunning = status.running;
|
|
148
|
+
|
|
149
|
+
// Stop daemon if running
|
|
150
|
+
if (wasRunning) {
|
|
151
|
+
await stopDaemon({ timeout: stopTimeout, force });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Start daemon
|
|
155
|
+
const result = await startDaemon();
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
...result,
|
|
159
|
+
wasRunning
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getRelayConfig() {
|
|
164
|
+
return {
|
|
165
|
+
url: process.env.RELAY_API_URL || '',
|
|
166
|
+
key: process.env.RELAY_API_KEY || ''
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function updateSessionDaemonState(sessionId, updates) {
|
|
171
|
+
const { url, key } = getRelayConfig();
|
|
172
|
+
if (!sessionId || !url || !key) return;
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const res = await fetch(`${url}/api/sessions/${encodeURIComponent(sessionId)}/daemon-state`, {
|
|
176
|
+
method: 'PATCH',
|
|
177
|
+
headers: {
|
|
178
|
+
'Content-Type': 'application/json',
|
|
179
|
+
'Authorization': `Bearer ${key}`
|
|
180
|
+
},
|
|
181
|
+
body: JSON.stringify(updates)
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (!res.ok && process.env.DEBUG) {
|
|
185
|
+
console.error('[lifecycle] Failed to update daemon_state:', res.status);
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (process.env.DEBUG) {
|
|
189
|
+
console.error('[lifecycle] Error updating daemon_state:', err.message);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function startDaemonIfNeeded(sessionId, reason = 'manual') {
|
|
195
|
+
const status = await checkDaemonStatus();
|
|
196
|
+
|
|
197
|
+
if (!status.running) {
|
|
198
|
+
await startDaemon();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (sessionId) {
|
|
202
|
+
await updateSessionDaemonState(sessionId, {
|
|
203
|
+
status: 'running',
|
|
204
|
+
started_reason: reason
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function stopDaemonIfNeeded(sessionId, reason = 'manual_stop') {
|
|
210
|
+
const status = await checkDaemonStatus();
|
|
211
|
+
if (!status.running) {
|
|
212
|
+
return { stopped: false, reason: 'not_running' };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// If relay is configured, check if other sessions still have daemon running
|
|
216
|
+
const { url, key } = getRelayConfig();
|
|
217
|
+
if (url && key) {
|
|
218
|
+
try {
|
|
219
|
+
const res = await fetch(`${url}/api/sessions`, {
|
|
220
|
+
headers: { 'Authorization': `Bearer ${key}` }
|
|
221
|
+
});
|
|
222
|
+
if (res.ok) {
|
|
223
|
+
const sessions = await res.json();
|
|
224
|
+
const otherRunning = sessions.some((s) =>
|
|
225
|
+
s.session_id !== sessionId &&
|
|
226
|
+
s.daemon_state &&
|
|
227
|
+
s.daemon_state.status === 'running'
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (otherRunning) {
|
|
231
|
+
return { stopped: false, reason: 'other_sessions_running' };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch (err) {
|
|
235
|
+
// Fail open: if we can't query sessions, we still attempt to stop daemon
|
|
236
|
+
if (process.env.DEBUG) {
|
|
237
|
+
console.error('[lifecycle] Failed to query sessions before stop:', err.message);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Update daemon_state before stopping
|
|
243
|
+
if (sessionId) {
|
|
244
|
+
await updateSessionDaemonState(sessionId, {
|
|
245
|
+
status: 'stopped',
|
|
246
|
+
started_reason: null,
|
|
247
|
+
is_away: false
|
|
248
|
+
// stopped_reason could be added later if DaemonState schema is extended
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const result = await stopDaemon();
|
|
253
|
+
return { stopped: result.success, reason };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get daemon status
|
|
258
|
+
* @returns {Promise<{running: boolean, pid: number|null, uptime: number|null}>}
|
|
259
|
+
*/
|
|
260
|
+
export async function getDaemonStatus() {
|
|
261
|
+
const status = await checkDaemonStatus();
|
|
262
|
+
|
|
263
|
+
// TODO: Add uptime calculation once daemon stores start time
|
|
264
|
+
// For now, we can only report running status and PID
|
|
265
|
+
return {
|
|
266
|
+
running: status.running,
|
|
267
|
+
pid: status.pid,
|
|
268
|
+
uptime: null // Will be implemented when daemon stores start timestamp
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Setup signal handlers for graceful daemon shutdown
|
|
274
|
+
* @param {Function} cleanupCallback - Async function to call before exit
|
|
275
|
+
*/
|
|
276
|
+
export function setupSignalHandlers(cleanupCallback) {
|
|
277
|
+
const handleSignal = async (signal) => {
|
|
278
|
+
console.log(`Received ${signal}, shutting down gracefully...`);
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
// Run cleanup callback
|
|
282
|
+
if (cleanupCallback) {
|
|
283
|
+
await cleanupCallback();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Release PID lock
|
|
287
|
+
await releasePidLock(process.pid);
|
|
288
|
+
|
|
289
|
+
process.exit(0);
|
|
290
|
+
} catch (err) {
|
|
291
|
+
console.error('Error during cleanup:', err);
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// Handle termination signals
|
|
297
|
+
process.on('SIGTERM', () => handleSignal('SIGTERM'));
|
|
298
|
+
process.on('SIGINT', () => handleSignal('SIGINT'));
|
|
299
|
+
|
|
300
|
+
// Handle uncaught errors
|
|
301
|
+
process.on('uncaughtException', async (err) => {
|
|
302
|
+
console.error('Uncaught exception:', err);
|
|
303
|
+
try {
|
|
304
|
+
if (cleanupCallback) {
|
|
305
|
+
await cleanupCallback();
|
|
306
|
+
}
|
|
307
|
+
await releasePidLock(process.pid);
|
|
308
|
+
} catch (cleanupErr) {
|
|
309
|
+
console.error('Error during cleanup:', cleanupErr);
|
|
310
|
+
}
|
|
311
|
+
process.exit(1);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
process.on('unhandledRejection', async (reason) => {
|
|
315
|
+
console.error('Unhandled rejection:', reason);
|
|
316
|
+
try {
|
|
317
|
+
if (cleanupCallback) {
|
|
318
|
+
await cleanupCallback();
|
|
319
|
+
}
|
|
320
|
+
await releasePidLock(process.pid);
|
|
321
|
+
} catch (cleanupErr) {
|
|
322
|
+
console.error('Error during cleanup:', cleanupErr);
|
|
323
|
+
}
|
|
324
|
+
process.exit(1);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export default {
|
|
329
|
+
startDaemon,
|
|
330
|
+
stopDaemon,
|
|
331
|
+
restartDaemon,
|
|
332
|
+
getDaemonStatus,
|
|
333
|
+
setupSignalHandlers,
|
|
334
|
+
startDaemonIfNeeded,
|
|
335
|
+
stopDaemonIfNeeded
|
|
336
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* PID Manager
|
|
7
|
+
* Manages daemon process ID file for ensuring single daemon instance
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const TELEPORTATION_DIR = join(homedir(), '.teleportation');
|
|
11
|
+
const PID_FILE = join(TELEPORTATION_DIR, 'daemon.pid');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if a process with given PID is running
|
|
15
|
+
*/
|
|
16
|
+
export function isProcessRunning(pid) {
|
|
17
|
+
console.log(`[pid-manager] Checking if process ${pid} is running...`);
|
|
18
|
+
// Handle invalid PIDs
|
|
19
|
+
if (typeof pid !== 'number' || pid <= 0) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// Signal 0 checks if process exists without actually sending a signal
|
|
25
|
+
process.kill(pid, 0);
|
|
26
|
+
return true;
|
|
27
|
+
} catch (err) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Read PID from file
|
|
34
|
+
* @returns {Promise<number|null>} PID or null if file doesn't exist or is invalid
|
|
35
|
+
*/
|
|
36
|
+
export async function readPid() {
|
|
37
|
+
console.log(`[pid-manager] Reading PID from ${PID_FILE}...`);
|
|
38
|
+
try {
|
|
39
|
+
const content = await fs.readFile(PID_FILE, 'utf-8');
|
|
40
|
+
const pid = parseInt(content.trim(), 10);
|
|
41
|
+
if (isNaN(pid) || pid <= 0) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return pid;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err.code === 'ENOENT') {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Write PID to file with 600 permissions
|
|
55
|
+
* @param {number} pid - Process ID to write
|
|
56
|
+
*/
|
|
57
|
+
export async function writePid(pid) {
|
|
58
|
+
console.log(`[pid-manager] Writing PID ${pid} to ${PID_FILE}...`);
|
|
59
|
+
// Ensure .teleportation directory exists
|
|
60
|
+
await fs.mkdir(TELEPORTATION_DIR, { recursive: true, mode: 0o700 });
|
|
61
|
+
|
|
62
|
+
// Write PID file with 600 permissions (owner read/write only)
|
|
63
|
+
await fs.writeFile(PID_FILE, String(pid), { mode: 0o600 });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Remove PID file
|
|
68
|
+
*/
|
|
69
|
+
export async function removePid() {
|
|
70
|
+
console.log(`[pid-manager] Removing PID file ${PID_FILE}...`);
|
|
71
|
+
try {
|
|
72
|
+
await fs.unlink(PID_FILE);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (err.code !== 'ENOENT') {
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if daemon is already running
|
|
82
|
+
* @returns {Promise<{running: boolean, pid: number|null, stale: boolean}>}
|
|
83
|
+
*/
|
|
84
|
+
export async function checkDaemonStatus() {
|
|
85
|
+
const pid = await readPid();
|
|
86
|
+
|
|
87
|
+
if (pid === null) {
|
|
88
|
+
return { running: false, pid: null, stale: false };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const running = isProcessRunning(pid);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
running,
|
|
95
|
+
pid,
|
|
96
|
+
stale: !running // PID file exists but process is dead
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Clean up stale PID file (when process doesn't exist)
|
|
102
|
+
* @returns {Promise<boolean>} true if stale PID was removed
|
|
103
|
+
*/
|
|
104
|
+
export async function cleanupStalePid() {
|
|
105
|
+
const status = await checkDaemonStatus();
|
|
106
|
+
|
|
107
|
+
if (status.stale) {
|
|
108
|
+
await removePid();
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Acquire PID lock (write PID file, ensuring no other daemon is running)
|
|
117
|
+
* @param {number} pid - Process ID to write
|
|
118
|
+
* @throws {Error} if another daemon is already running
|
|
119
|
+
*/
|
|
120
|
+
export async function acquirePidLock(pid) {
|
|
121
|
+
console.log(`[pid-manager] Acquiring PID lock for PID ${pid}...`);
|
|
122
|
+
// Clean up any stale PID files first
|
|
123
|
+
await cleanupStalePid();
|
|
124
|
+
|
|
125
|
+
// Check if daemon is running
|
|
126
|
+
const status = await checkDaemonStatus();
|
|
127
|
+
|
|
128
|
+
if (status.running) {
|
|
129
|
+
throw new Error(`Daemon already running with PID ${status.pid}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Write our PID
|
|
133
|
+
await writePid(pid);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Release PID lock (remove PID file if it matches our PID)
|
|
138
|
+
* @param {number} pid - Our process ID
|
|
139
|
+
*/
|
|
140
|
+
export async function releasePidLock(pid) {
|
|
141
|
+
console.log(`[pid-manager] Releasing PID lock for PID ${pid}...`);
|
|
142
|
+
const currentPid = await readPid();
|
|
143
|
+
|
|
144
|
+
// Only remove if PID file matches our process
|
|
145
|
+
if (currentPid === pid) {
|
|
146
|
+
await removePid();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export default {
|
|
151
|
+
isProcessRunning,
|
|
152
|
+
readPid,
|
|
153
|
+
writePid,
|
|
154
|
+
removePid,
|
|
155
|
+
checkDaemonStatus,
|
|
156
|
+
cleanupStalePid,
|
|
157
|
+
acquirePidLock,
|
|
158
|
+
releasePidLock,
|
|
159
|
+
PID_FILE
|
|
160
|
+
};
|