@dynamicu/chromedebug-mcp 2.2.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.
Files changed (95) hide show
  1. package/CLAUDE.md +344 -0
  2. package/LICENSE +21 -0
  3. package/README.md +250 -0
  4. package/chrome-extension/README.md +41 -0
  5. package/chrome-extension/background.js +3917 -0
  6. package/chrome-extension/chrome-session-manager.js +706 -0
  7. package/chrome-extension/content.css +181 -0
  8. package/chrome-extension/content.js +3022 -0
  9. package/chrome-extension/data-buffer.js +435 -0
  10. package/chrome-extension/dom-tracker.js +411 -0
  11. package/chrome-extension/extension-config.js +78 -0
  12. package/chrome-extension/firebase-client.js +278 -0
  13. package/chrome-extension/firebase-config.js +32 -0
  14. package/chrome-extension/firebase-config.module.js +22 -0
  15. package/chrome-extension/firebase-config.module.template.js +27 -0
  16. package/chrome-extension/firebase-config.template.js +36 -0
  17. package/chrome-extension/frame-capture.js +407 -0
  18. package/chrome-extension/icon128.png +1 -0
  19. package/chrome-extension/icon16.png +1 -0
  20. package/chrome-extension/icon48.png +1 -0
  21. package/chrome-extension/license-helper.js +181 -0
  22. package/chrome-extension/logger.js +23 -0
  23. package/chrome-extension/manifest.json +73 -0
  24. package/chrome-extension/network-tracker.js +510 -0
  25. package/chrome-extension/offscreen.html +10 -0
  26. package/chrome-extension/options.html +203 -0
  27. package/chrome-extension/options.js +282 -0
  28. package/chrome-extension/pako.min.js +2 -0
  29. package/chrome-extension/performance-monitor.js +533 -0
  30. package/chrome-extension/pii-redactor.js +405 -0
  31. package/chrome-extension/popup.html +532 -0
  32. package/chrome-extension/popup.js +2446 -0
  33. package/chrome-extension/upload-manager.js +323 -0
  34. package/chrome-extension/web-vitals.iife.js +1 -0
  35. package/config/api-keys.json +11 -0
  36. package/config/chrome-pilot-config.json +45 -0
  37. package/package.json +126 -0
  38. package/scripts/cleanup-processes.js +109 -0
  39. package/scripts/config-manager.js +280 -0
  40. package/scripts/generate-extension-config.js +53 -0
  41. package/scripts/setup-security.js +64 -0
  42. package/src/capture/architecture.js +426 -0
  43. package/src/capture/error-handling-tests.md +38 -0
  44. package/src/capture/error-handling-types.ts +360 -0
  45. package/src/capture/index.js +508 -0
  46. package/src/capture/interfaces.js +625 -0
  47. package/src/capture/memory-manager.js +713 -0
  48. package/src/capture/types.js +342 -0
  49. package/src/chrome-controller.js +2658 -0
  50. package/src/cli.js +19 -0
  51. package/src/config-loader.js +303 -0
  52. package/src/database.js +2178 -0
  53. package/src/firebase-license-manager.js +462 -0
  54. package/src/firebase-privacy-guard.js +397 -0
  55. package/src/http-server.js +1516 -0
  56. package/src/index-direct.js +157 -0
  57. package/src/index-modular.js +219 -0
  58. package/src/index-monolithic-backup.js +2230 -0
  59. package/src/index.js +305 -0
  60. package/src/legacy/chrome-controller-old.js +1406 -0
  61. package/src/legacy/index-express.js +625 -0
  62. package/src/legacy/index-old.js +977 -0
  63. package/src/legacy/routes.js +260 -0
  64. package/src/legacy/shared-storage.js +101 -0
  65. package/src/logger.js +10 -0
  66. package/src/mcp/handlers/chrome-tool-handler.js +306 -0
  67. package/src/mcp/handlers/element-tool-handler.js +51 -0
  68. package/src/mcp/handlers/frame-tool-handler.js +957 -0
  69. package/src/mcp/handlers/request-handler.js +104 -0
  70. package/src/mcp/handlers/workflow-tool-handler.js +636 -0
  71. package/src/mcp/server.js +68 -0
  72. package/src/mcp/tools/index.js +701 -0
  73. package/src/middleware/auth.js +371 -0
  74. package/src/middleware/security.js +267 -0
  75. package/src/port-discovery.js +258 -0
  76. package/src/routes/admin.js +182 -0
  77. package/src/services/browser-daemon.js +494 -0
  78. package/src/services/chrome-service.js +375 -0
  79. package/src/services/failover-manager.js +412 -0
  80. package/src/services/git-safety-service.js +675 -0
  81. package/src/services/heartbeat-manager.js +200 -0
  82. package/src/services/http-client.js +195 -0
  83. package/src/services/process-manager.js +318 -0
  84. package/src/services/process-tracker.js +574 -0
  85. package/src/services/profile-manager.js +449 -0
  86. package/src/services/project-manager.js +415 -0
  87. package/src/services/session-manager.js +497 -0
  88. package/src/services/session-registry.js +491 -0
  89. package/src/services/unified-session-manager.js +678 -0
  90. package/src/shared-storage-old.js +267 -0
  91. package/src/standalone-server.js +53 -0
  92. package/src/utils/extension-path.js +145 -0
  93. package/src/utils.js +187 -0
  94. package/src/validation/log-transformer.js +125 -0
  95. package/src/validation/schemas.js +391 -0
@@ -0,0 +1,267 @@
1
+ // Shared storage for recordings across all server instances
2
+ import { promises as fs } from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const STORAGE_FILE = path.join(__dirname, '.recordings-storage.json');
8
+
9
+ class SharedRecordingStorage {
10
+ constructor() {
11
+ this.recordings = new Map();
12
+ this.initialized = false;
13
+ this.initPromise = this.init();
14
+ }
15
+
16
+ async init() {
17
+ await this.loadFromDisk();
18
+ this.initialized = true;
19
+ }
20
+
21
+ async ensureInitialized() {
22
+ if (!this.initialized) {
23
+ await this.initPromise;
24
+ }
25
+ }
26
+
27
+ async loadFromDisk() {
28
+ try {
29
+ const data = await fs.readFile(STORAGE_FILE, 'utf8');
30
+ const parsed = JSON.parse(data);
31
+
32
+ // Convert arrays back to Maps for chunked recordings
33
+ const recordingsArray = parsed.recordings || [];
34
+ this.recordings = new Map(recordingsArray.map(([key, value]) => {
35
+ if (value.type === 'chunked_recording' && Array.isArray(value.chunks)) {
36
+ return [key, {
37
+ ...value,
38
+ chunks: new Map(value.chunks)
39
+ }];
40
+ }
41
+ return [key, value];
42
+ }));
43
+
44
+ console.log(`Loaded ${this.recordings.size} recordings from disk`);
45
+ } catch (error) {
46
+ // File doesn't exist or is invalid, start fresh
47
+ console.log('No existing recordings file, starting fresh');
48
+ }
49
+ }
50
+
51
+ async saveToDisk() {
52
+ try {
53
+ // Convert Maps to arrays for JSON serialization
54
+ const recordingsArray = Array.from(this.recordings.entries()).map(([key, value]) => {
55
+ if (value.type === 'chunked_recording' && value.chunks instanceof Map) {
56
+ return [key, {
57
+ ...value,
58
+ chunks: Array.from(value.chunks.entries())
59
+ }];
60
+ }
61
+ return [key, value];
62
+ });
63
+
64
+ const data = {
65
+ recordings: recordingsArray,
66
+ lastSaved: new Date().toISOString()
67
+ };
68
+ await fs.writeFile(STORAGE_FILE, JSON.stringify(data, null, 2));
69
+ } catch (error) {
70
+ console.error('Error saving recordings to disk:', error);
71
+ }
72
+ }
73
+
74
+ // Removed - no longer storing single recordings
75
+
76
+ async get(id) {
77
+ await this.ensureInitialized();
78
+
79
+ // Try direct lookup first
80
+ let recording = this.recordings.get(id);
81
+ if (recording) return recording;
82
+
83
+ // Try with session_ prefix for legacy chunks
84
+ recording = this.recordings.get(`session_${id}`);
85
+ if (recording) return recording;
86
+
87
+ // Try with frame_session_ prefix for frame captures
88
+ recording = this.recordings.get(`frame_session_${id}`);
89
+ if (recording) return recording;
90
+
91
+ // Also check if the ID itself already has the frame_session_ prefix
92
+ if (id.startsWith('frame_') && !id.startsWith('frame_session_')) {
93
+ // This is a frame session ID, look for it with the full key
94
+ for (const [key, value] of this.recordings.entries()) {
95
+ if (key === id || key === `frame_session_${id}`) {
96
+ return value;
97
+ }
98
+ }
99
+ }
100
+
101
+ return null;
102
+ }
103
+
104
+ async delete(id) {
105
+ await this.ensureInitialized();
106
+
107
+ // Try direct deletion first
108
+ let result = this.recordings.delete(id);
109
+
110
+ // Try with session_ prefix
111
+ if (!result) {
112
+ result = this.recordings.delete(`session_${id}`);
113
+ }
114
+
115
+ // Try with frame_session_ prefix
116
+ if (!result) {
117
+ result = this.recordings.delete(`frame_session_${id}`);
118
+ }
119
+
120
+ if (result) {
121
+ await this.saveToDisk();
122
+ }
123
+
124
+ return result;
125
+ }
126
+
127
+ async list() {
128
+ await this.ensureInitialized();
129
+
130
+ // Only list frame sessions
131
+ const sessions = [];
132
+ for (const [key, value] of this.recordings.entries()) {
133
+ if (key.startsWith('frame_session_') && value.type === 'frame_capture') {
134
+ sessions.push({
135
+ id: key.replace('frame_session_', ''),
136
+ sessionId: value.sessionId,
137
+ totalFrames: value.frames.length,
138
+ timestamp: value.timestamp
139
+ });
140
+ }
141
+ }
142
+
143
+ return sessions.sort((a, b) => b.timestamp - a.timestamp);
144
+ }
145
+
146
+ // Removed chunked recording methods - no longer using chunked storage
147
+
148
+ // Store frame batch for frame capture sessions
149
+ async storeFrameBatch(sessionId, frames) {
150
+ await this.ensureInitialized();
151
+
152
+ const sessionKey = `frame_session_${sessionId}`;
153
+ let session = this.recordings.get(sessionKey);
154
+
155
+ if (!session) {
156
+ // Create new frame session
157
+ session = {
158
+ type: 'frame_capture',
159
+ sessionId: sessionId,
160
+ timestamp: Date.now(),
161
+ frames: []
162
+ };
163
+ this.recordings.set(sessionKey, session);
164
+ }
165
+
166
+ // Add frames to session
167
+ session.frames.push(...frames);
168
+ console.log(`Stored ${frames.length} frames for session ${sessionId}, total: ${session.frames.length}`);
169
+
170
+ // Save to disk
171
+ await this.saveToDisk();
172
+
173
+ return session;
174
+ }
175
+
176
+ // Get frame session info
177
+ async getFrameSessionInfo(sessionId) {
178
+ await this.ensureInitialized();
179
+
180
+ // Try with frame_session_ prefix first
181
+ let sessionKey = `frame_session_${sessionId}`;
182
+ let session = this.recordings.get(sessionKey);
183
+
184
+ // If not found, try without prefix (direct sessionId)
185
+ if (!session) {
186
+ session = this.recordings.get(sessionId);
187
+ }
188
+
189
+ if (!session || session.type !== 'frame_capture') {
190
+ return null;
191
+ }
192
+
193
+ return {
194
+ sessionId: session.sessionId || sessionId,
195
+ totalFrames: session.frames.length,
196
+ timestamp: session.timestamp,
197
+ frameTimestamps: session.frames.map(f => f.timestamp)
198
+ };
199
+ }
200
+
201
+ // Get specific frame from session
202
+ async getFrame(sessionId, frameIndex) {
203
+ await this.ensureInitialized();
204
+
205
+ // Try with frame_session_ prefix first
206
+ let sessionKey = `frame_session_${sessionId}`;
207
+ let session = this.recordings.get(sessionKey);
208
+
209
+ // If not found, try without prefix (direct sessionId)
210
+ if (!session) {
211
+ session = this.recordings.get(sessionId);
212
+ }
213
+
214
+ if (!session || session.type !== 'frame_capture') {
215
+ return null;
216
+ }
217
+
218
+ if (frameIndex < 0 || frameIndex >= session.frames.length) {
219
+ return null;
220
+ }
221
+
222
+ return session.frames[frameIndex];
223
+ }
224
+
225
+ // Get entire frame session
226
+ async getFrameSession(sessionId) {
227
+ await this.ensureInitialized();
228
+
229
+ // Try with frame_session_ prefix first
230
+ let sessionKey = `frame_session_${sessionId}`;
231
+ let session = this.recordings.get(sessionKey);
232
+
233
+ // If not found, try without prefix (direct sessionId)
234
+ if (!session) {
235
+ session = this.recordings.get(sessionId);
236
+ }
237
+
238
+ if (!session || session.type !== 'frame_capture') {
239
+ return null;
240
+ }
241
+
242
+ return session;
243
+ }
244
+
245
+ // Update frame session
246
+ async updateFrameSession(sessionId, updatedSession) {
247
+ await this.ensureInitialized();
248
+
249
+ const sessionKey = `frame_session_${sessionId}`;
250
+ const existingSession = this.recordings.get(sessionKey);
251
+
252
+ if (!existingSession || existingSession.type !== 'frame_capture') {
253
+ throw new Error('Frame session not found');
254
+ }
255
+
256
+ // Update the session
257
+ this.recordings.set(sessionKey, updatedSession);
258
+
259
+ // Save to disk
260
+ await this.saveToDisk();
261
+
262
+ return updatedSession;
263
+ }
264
+ }
265
+
266
+ // Export singleton instance
267
+ export const sharedStorage = new SharedRecordingStorage();
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Chrome Debug HTTP Server
4
+ // This server handles communication with the Chrome extension for frame recording
5
+
6
+ import { startHttpServer, startWebSocketServer } from './http-server.js';
7
+ import { removePortFile } from './port-discovery.js';
8
+
9
+ console.log('╔════════════════════════════════════════════════════════════════╗');
10
+ console.log('║ Chrome Debug HTTP Server ║');
11
+ console.log('║ ║');
12
+ console.log('║ This server enables Chrome extension features like: ║');
13
+ console.log('║ - Frame recording ║');
14
+ console.log('║ - Workflow recording ║');
15
+ console.log('║ - Visual element selection ║');
16
+ console.log('║ ║');
17
+ console.log('║ Keep this running while using Chrome Debug through Claude ║');
18
+ console.log('╚════════════════════════════════════════════════════════════════╝');
19
+ console.log('');
20
+
21
+ async function main() {
22
+ try {
23
+ // Start HTTP server
24
+ const httpPort = await startHttpServer();
25
+ console.log(`✓ HTTP server started on port ${httpPort}`);
26
+
27
+ // Start WebSocket server
28
+ await startWebSocketServer();
29
+ console.log('✓ WebSocket server started');
30
+
31
+ console.log('');
32
+ console.log('Server is ready! Chrome extension features are now available.');
33
+ console.log('Press Ctrl+C to stop the server.');
34
+
35
+ } catch (error) {
36
+ console.error('Failed to start server:', error.message);
37
+ process.exit(1);
38
+ }
39
+ }
40
+
41
+ // Cleanup on exit
42
+ process.on('SIGINT', () => {
43
+ console.log('\nShutting down server...');
44
+ removePortFile();
45
+ process.exit(0);
46
+ });
47
+
48
+ process.on('SIGTERM', () => {
49
+ removePortFile();
50
+ process.exit(0);
51
+ });
52
+
53
+ main().catch(console.error);
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Extension Path Discovery Utility
3
+ *
4
+ * Locates the ChromeDebug extension with flexible fallback logic.
5
+ * Supports bundled extensions, user installations, and custom paths.
6
+ */
7
+
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import os from 'os';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+
16
+ /**
17
+ * Searches for the ChromeDebug extension in multiple locations with fallback logic
18
+ *
19
+ * Search priority:
20
+ * 1. Environment variable: CHROMEDEBUG_EXTENSION_PATH
21
+ * 2. Config file (loaded by caller): config.extensionPath
22
+ * 3. Standard user locations (~/ChromeDebug-Extension, ~/Downloads)
23
+ * 4. Platform-specific app directories
24
+ * 5. Bundled with server (relative path)
25
+ *
26
+ * @param {Object} config - Optional config object with extensionPath property
27
+ * @returns {string|null} Absolute path to extension directory, or null if not found
28
+ */
29
+ export function getExtensionPath(config = {}) {
30
+ const searchPaths = [
31
+ // 1. Environment variable (highest priority)
32
+ process.env.CHROMEDEBUG_EXTENSION_PATH,
33
+
34
+ // 2. Config file setting
35
+ config.extensionPath,
36
+
37
+ // 3. Standard user locations
38
+ path.join(os.homedir(), 'ChromeDebug-Extension'),
39
+ path.join(os.homedir(), 'Documents', 'ChromeDebug-Extension'),
40
+ path.join(os.homedir(), 'Downloads', 'ChromeDebug-Extension'),
41
+
42
+ // 4. Platform-specific app directories
43
+ getPlatformSpecificPath(),
44
+
45
+ // 5. Bundled with npm package (fallback)
46
+ path.resolve(__dirname, '../../chrome-extension')
47
+ ];
48
+
49
+ for (const searchPath of searchPaths) {
50
+ if (!searchPath) continue;
51
+
52
+ const resolvedPath = path.resolve(searchPath);
53
+
54
+ if (validateExtensionPath(resolvedPath)) {
55
+ console.log(`[ExtensionPath] ✓ Found ChromeDebug extension at: ${resolvedPath}`);
56
+ return resolvedPath;
57
+ }
58
+ }
59
+
60
+ console.warn('[ExtensionPath] ⚠ ChromeDebug extension not found in any standard location');
61
+ console.warn('[ExtensionPath] Searched locations:');
62
+ searchPaths.filter(Boolean).forEach(p => console.warn(`[ExtensionPath] - ${p}`));
63
+ console.warn('[ExtensionPath]');
64
+ console.warn('[ExtensionPath] To specify a custom location, use:');
65
+ console.warn('[ExtensionPath] • Environment variable: CHROMEDEBUG_EXTENSION_PATH=/path/to/extension');
66
+ console.warn('[ExtensionPath] • Config file: Set "extensionPath" in config/chrome-pilot-config.json');
67
+
68
+ return null;
69
+ }
70
+
71
+ /**
72
+ * Returns the platform-specific default installation directory
73
+ * @returns {string|null}
74
+ */
75
+ function getPlatformSpecificPath() {
76
+ switch (process.platform) {
77
+ case 'darwin': // macOS
78
+ return '/Applications/ChromeDebug/extension';
79
+ case 'win32': // Windows
80
+ return 'C:\\Program Files\\ChromeDebug\\extension';
81
+ case 'linux':
82
+ return '/opt/chromedebug/extension';
83
+ default:
84
+ return null;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Validates that a path contains a valid ChromeDebug extension
90
+ *
91
+ * Checks:
92
+ * - Directory exists
93
+ * - manifest.json exists
94
+ * - manifest.json is valid JSON
95
+ * - Extension name matches ChromeDebug
96
+ *
97
+ * @param {string} extensionPath - Path to validate
98
+ * @returns {boolean} True if path contains a valid ChromeDebug extension
99
+ */
100
+ export function validateExtensionPath(extensionPath) {
101
+ if (!extensionPath) return false;
102
+
103
+ try {
104
+ const manifestPath = path.join(extensionPath, 'manifest.json');
105
+
106
+ // Check if manifest exists
107
+ if (!fs.existsSync(manifestPath)) {
108
+ return false;
109
+ }
110
+
111
+ // Validate manifest content
112
+ const manifestContent = fs.readFileSync(manifestPath, 'utf8');
113
+ const manifest = JSON.parse(manifestContent);
114
+
115
+ // Verify it's the ChromeDebug extension
116
+ if (!manifest.name?.includes('ChromeDebug')) {
117
+ console.warn(`[ExtensionPath] ⚠ Extension at ${extensionPath} is not ChromeDebug (found: ${manifest.name})`);
118
+ return false;
119
+ }
120
+
121
+ return true;
122
+ } catch (error) {
123
+ // Invalid JSON or filesystem error
124
+ return false;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Gets extension metadata from manifest.json
130
+ * @param {string} extensionPath - Path to extension directory
131
+ * @returns {Object|null} Extension manifest data or null if invalid
132
+ */
133
+ export function getExtensionInfo(extensionPath) {
134
+ if (!validateExtensionPath(extensionPath)) {
135
+ return null;
136
+ }
137
+
138
+ try {
139
+ const manifestPath = path.join(extensionPath, 'manifest.json');
140
+ const manifestContent = fs.readFileSync(manifestPath, 'utf8');
141
+ return JSON.parse(manifestContent);
142
+ } catch (error) {
143
+ return null;
144
+ }
145
+ }
package/src/utils.js ADDED
@@ -0,0 +1,187 @@
1
+ import net from 'net';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+
6
+ export async function findAvailablePort(startPort = 3000, maxPort = 4000) {
7
+ for (let port = startPort; port <= maxPort; port++) {
8
+ if (await isPortAvailable(port)) {
9
+ return port;
10
+ }
11
+ }
12
+ throw new Error(`No available ports found between ${startPort} and ${maxPort}`);
13
+ }
14
+
15
+ /**
16
+ * Find available port with atomic lock coordination for multi-session safety
17
+ * @param {number} startPort - Starting port to check
18
+ * @param {number} maxPort - Maximum port to check
19
+ * @param {string} sessionId - Session identifier for lock ownership
20
+ * @returns {Promise<{port: number, lockFile: string}>} Port and lock file info
21
+ */
22
+ export async function findAvailablePortWithLock(startPort = 3000, maxPort = 4000, sessionId = null) {
23
+ const lockDir = path.join(os.tmpdir(), 'chromedebug-ports');
24
+
25
+ // Ensure lock directory exists
26
+ try {
27
+ await fs.promises.mkdir(lockDir, { recursive: true });
28
+ } catch (error) {
29
+ // Directory might already exist, continue
30
+ }
31
+
32
+ // Clean up stale locks first
33
+ await cleanupStaleLocks(lockDir);
34
+
35
+ for (let port = startPort; port <= maxPort; port++) {
36
+ if (await isPortAvailable(port) && await tryLockPort(port, sessionId, lockDir)) {
37
+ const lockFile = getLockFilePath(port, lockDir);
38
+ return { port, lockFile };
39
+ }
40
+ }
41
+ throw new Error(`No available ports with lock found between ${startPort} and ${maxPort}`);
42
+ }
43
+
44
+ /**
45
+ * Try to atomically lock a port
46
+ * @param {number} port - Port to lock
47
+ * @param {string} sessionId - Session ID for lock ownership
48
+ * @param {string} lockDir - Lock directory path
49
+ * @returns {Promise<boolean>} True if port was successfully locked
50
+ */
51
+ async function tryLockPort(port, sessionId, lockDir) {
52
+ const lockFile = getLockFilePath(port, lockDir);
53
+ const lockData = {
54
+ sessionId: sessionId || process.pid.toString(),
55
+ port,
56
+ timestamp: Date.now(),
57
+ pid: process.pid
58
+ };
59
+
60
+ try {
61
+ // Atomic write with exclusive flag - fails if file exists
62
+ await fs.promises.writeFile(lockFile, JSON.stringify(lockData), { flag: 'wx' });
63
+ return true;
64
+ } catch (error) {
65
+ if (error.code === 'EEXIST') {
66
+ // Lock file exists, check if it's stale
67
+ return await handleExistingLock(lockFile, port);
68
+ }
69
+ return false;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Handle existing lock file - check if stale and clean up
75
+ * @param {string} lockFile - Path to lock file
76
+ * @param {number} port - Port number
77
+ * @returns {Promise<boolean>} True if lock was cleaned up and port is now available
78
+ */
79
+ async function handleExistingLock(lockFile, port) {
80
+ try {
81
+ const lockData = JSON.parse(await fs.promises.readFile(lockFile, 'utf8'));
82
+ const lockAge = Date.now() - lockData.timestamp;
83
+
84
+ // Consider locks older than 5 minutes as stale
85
+ const STALE_LOCK_THRESHOLD = 5 * 60 * 1000;
86
+
87
+ if (lockAge > STALE_LOCK_THRESHOLD) {
88
+ // Try to remove stale lock
89
+ try {
90
+ await fs.promises.unlink(lockFile);
91
+ console.log(`Cleaned up stale port lock for port ${port}`);
92
+ return true;
93
+ } catch (cleanupError) {
94
+ // Another process might have cleaned it up, that's fine
95
+ return false;
96
+ }
97
+ }
98
+
99
+ return false; // Lock is still valid
100
+ } catch (error) {
101
+ // Corrupted lock file, try to remove it
102
+ try {
103
+ await fs.promises.unlink(lockFile);
104
+ return true;
105
+ } catch (cleanupError) {
106
+ return false;
107
+ }
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Clean up all stale lock files
113
+ * @param {string} lockDir - Lock directory path
114
+ */
115
+ async function cleanupStaleLocks(lockDir) {
116
+ try {
117
+ const files = await fs.promises.readdir(lockDir);
118
+ const STALE_LOCK_THRESHOLD = 5 * 60 * 1000;
119
+
120
+ for (const file of files) {
121
+ if (!file.startsWith('port-') || !file.endsWith('.lock')) continue;
122
+
123
+ const lockFile = path.join(lockDir, file);
124
+ try {
125
+ const lockData = JSON.parse(await fs.promises.readFile(lockFile, 'utf8'));
126
+ const lockAge = Date.now() - lockData.timestamp;
127
+
128
+ if (lockAge > STALE_LOCK_THRESHOLD) {
129
+ await fs.promises.unlink(lockFile);
130
+ console.log(`Cleaned up stale lock file: ${file}`);
131
+ }
132
+ } catch (error) {
133
+ // Corrupted lock file, remove it
134
+ try {
135
+ await fs.promises.unlink(lockFile);
136
+ } catch (cleanupError) {
137
+ // Ignore cleanup errors
138
+ }
139
+ }
140
+ }
141
+ } catch (error) {
142
+ // Directory might not exist or be accessible, continue silently
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Release a port lock
148
+ * @param {string} lockFile - Path to lock file to release
149
+ */
150
+ export async function releasePortLock(lockFile) {
151
+ try {
152
+ await fs.promises.unlink(lockFile);
153
+ } catch (error) {
154
+ // Lock might have been cleaned up by another process, that's fine
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Get lock file path for a port
160
+ * @param {number} port - Port number
161
+ * @param {string} lockDir - Lock directory path
162
+ * @returns {string} Lock file path
163
+ */
164
+ function getLockFilePath(port, lockDir) {
165
+ return path.join(lockDir, `port-${port}.lock`);
166
+ }
167
+
168
+ function isPortAvailable(port) {
169
+ return new Promise((resolve) => {
170
+ const server = net.createServer();
171
+
172
+ server.once('error', (err) => {
173
+ if (err.code === 'EADDRINUSE') {
174
+ resolve(false);
175
+ } else {
176
+ resolve(false);
177
+ }
178
+ });
179
+
180
+ server.once('listening', () => {
181
+ server.close();
182
+ resolve(true);
183
+ });
184
+
185
+ server.listen(port);
186
+ });
187
+ }