@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.
- package/CLAUDE.md +344 -0
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/chrome-extension/README.md +41 -0
- package/chrome-extension/background.js +3917 -0
- package/chrome-extension/chrome-session-manager.js +706 -0
- package/chrome-extension/content.css +181 -0
- package/chrome-extension/content.js +3022 -0
- package/chrome-extension/data-buffer.js +435 -0
- package/chrome-extension/dom-tracker.js +411 -0
- package/chrome-extension/extension-config.js +78 -0
- package/chrome-extension/firebase-client.js +278 -0
- package/chrome-extension/firebase-config.js +32 -0
- package/chrome-extension/firebase-config.module.js +22 -0
- package/chrome-extension/firebase-config.module.template.js +27 -0
- package/chrome-extension/firebase-config.template.js +36 -0
- package/chrome-extension/frame-capture.js +407 -0
- package/chrome-extension/icon128.png +1 -0
- package/chrome-extension/icon16.png +1 -0
- package/chrome-extension/icon48.png +1 -0
- package/chrome-extension/license-helper.js +181 -0
- package/chrome-extension/logger.js +23 -0
- package/chrome-extension/manifest.json +73 -0
- package/chrome-extension/network-tracker.js +510 -0
- package/chrome-extension/offscreen.html +10 -0
- package/chrome-extension/options.html +203 -0
- package/chrome-extension/options.js +282 -0
- package/chrome-extension/pako.min.js +2 -0
- package/chrome-extension/performance-monitor.js +533 -0
- package/chrome-extension/pii-redactor.js +405 -0
- package/chrome-extension/popup.html +532 -0
- package/chrome-extension/popup.js +2446 -0
- package/chrome-extension/upload-manager.js +323 -0
- package/chrome-extension/web-vitals.iife.js +1 -0
- package/config/api-keys.json +11 -0
- package/config/chrome-pilot-config.json +45 -0
- package/package.json +126 -0
- package/scripts/cleanup-processes.js +109 -0
- package/scripts/config-manager.js +280 -0
- package/scripts/generate-extension-config.js +53 -0
- package/scripts/setup-security.js +64 -0
- package/src/capture/architecture.js +426 -0
- package/src/capture/error-handling-tests.md +38 -0
- package/src/capture/error-handling-types.ts +360 -0
- package/src/capture/index.js +508 -0
- package/src/capture/interfaces.js +625 -0
- package/src/capture/memory-manager.js +713 -0
- package/src/capture/types.js +342 -0
- package/src/chrome-controller.js +2658 -0
- package/src/cli.js +19 -0
- package/src/config-loader.js +303 -0
- package/src/database.js +2178 -0
- package/src/firebase-license-manager.js +462 -0
- package/src/firebase-privacy-guard.js +397 -0
- package/src/http-server.js +1516 -0
- package/src/index-direct.js +157 -0
- package/src/index-modular.js +219 -0
- package/src/index-monolithic-backup.js +2230 -0
- package/src/index.js +305 -0
- package/src/legacy/chrome-controller-old.js +1406 -0
- package/src/legacy/index-express.js +625 -0
- package/src/legacy/index-old.js +977 -0
- package/src/legacy/routes.js +260 -0
- package/src/legacy/shared-storage.js +101 -0
- package/src/logger.js +10 -0
- package/src/mcp/handlers/chrome-tool-handler.js +306 -0
- package/src/mcp/handlers/element-tool-handler.js +51 -0
- package/src/mcp/handlers/frame-tool-handler.js +957 -0
- package/src/mcp/handlers/request-handler.js +104 -0
- package/src/mcp/handlers/workflow-tool-handler.js +636 -0
- package/src/mcp/server.js +68 -0
- package/src/mcp/tools/index.js +701 -0
- package/src/middleware/auth.js +371 -0
- package/src/middleware/security.js +267 -0
- package/src/port-discovery.js +258 -0
- package/src/routes/admin.js +182 -0
- package/src/services/browser-daemon.js +494 -0
- package/src/services/chrome-service.js +375 -0
- package/src/services/failover-manager.js +412 -0
- package/src/services/git-safety-service.js +675 -0
- package/src/services/heartbeat-manager.js +200 -0
- package/src/services/http-client.js +195 -0
- package/src/services/process-manager.js +318 -0
- package/src/services/process-tracker.js +574 -0
- package/src/services/profile-manager.js +449 -0
- package/src/services/project-manager.js +415 -0
- package/src/services/session-manager.js +497 -0
- package/src/services/session-registry.js +491 -0
- package/src/services/unified-session-manager.js +678 -0
- package/src/shared-storage-old.js +267 -0
- package/src/standalone-server.js +53 -0
- package/src/utils/extension-path.js +145 -0
- package/src/utils.js +187 -0
- package/src/validation/log-transformer.js +125 -0
- 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
|
+
}
|