@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,675 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Safety Service - Provides comprehensive Git safety operations for instrumentation
|
|
3
|
+
* Follows ChromeDebug MCP service patterns with robust error handling and rollback capabilities
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Git status validation and working directory analysis
|
|
7
|
+
* - Atomic backup creation with Git stash or file copying
|
|
8
|
+
* - Branch protection to prevent modifications on protected branches
|
|
9
|
+
* - Rollback capabilities for failed operations
|
|
10
|
+
* - Commit state analysis and validation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execSync } from 'child_process';
|
|
14
|
+
import { existsSync, statSync, mkdirSync, copyFileSync, rmSync } from 'fs';
|
|
15
|
+
import { join, resolve, dirname } from 'path';
|
|
16
|
+
import { glob } from 'glob';
|
|
17
|
+
|
|
18
|
+
export class GitSafetyService {
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this.options = {
|
|
21
|
+
timeout: 30000, // 30 seconds for git operations
|
|
22
|
+
protectedBranches: ['main', 'master', 'production', 'develop'],
|
|
23
|
+
maxStashEntries: 10, // Limit stash entries to prevent bloat
|
|
24
|
+
backupRetentionDays: 7,
|
|
25
|
+
...options
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Service state tracking
|
|
29
|
+
this.activeBackups = new Map(); // operationId -> backup info
|
|
30
|
+
this.operationHistory = [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Validates Git repository status and safety for operations
|
|
35
|
+
* @param {string} projectPath - Absolute path to project root
|
|
36
|
+
* @param {Object} options - Validation options
|
|
37
|
+
* @returns {Promise<Object>} Git validation result
|
|
38
|
+
*/
|
|
39
|
+
async validateGitSafety(projectPath, options = {}) {
|
|
40
|
+
try {
|
|
41
|
+
const validationOptions = {
|
|
42
|
+
allowUncommittedChanges: false,
|
|
43
|
+
requireGitRepo: false,
|
|
44
|
+
protectedBranches: this.options.protectedBranches,
|
|
45
|
+
...options
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Basic path validation
|
|
49
|
+
if (!existsSync(projectPath)) {
|
|
50
|
+
return {
|
|
51
|
+
valid: false,
|
|
52
|
+
error: 'Project path does not exist',
|
|
53
|
+
path: projectPath
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check if it's a Git repository
|
|
58
|
+
const gitPath = join(projectPath, '.git');
|
|
59
|
+
const isGitRepo = existsSync(gitPath);
|
|
60
|
+
|
|
61
|
+
if (!isGitRepo) {
|
|
62
|
+
if (validationOptions.requireGitRepo) {
|
|
63
|
+
return {
|
|
64
|
+
valid: false,
|
|
65
|
+
error: 'Git repository required but not found',
|
|
66
|
+
recommendation: 'Initialize git repository with: git init'
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
valid: true,
|
|
72
|
+
isGitRepo: false,
|
|
73
|
+
warning: 'Not a Git repository - manual backup recommended',
|
|
74
|
+
recommendation: 'Initialize git: git init && git add . && git commit -m "Initial commit"'
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Validate Git status
|
|
79
|
+
const gitStatus = await this._getGitStatus(projectPath);
|
|
80
|
+
if (!gitStatus.valid) {
|
|
81
|
+
return gitStatus;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check for uncommitted changes
|
|
85
|
+
if (gitStatus.hasUncommittedChanges && !validationOptions.allowUncommittedChanges) {
|
|
86
|
+
return {
|
|
87
|
+
valid: false,
|
|
88
|
+
error: 'Uncommitted changes detected',
|
|
89
|
+
details: 'Commit or stash changes before proceeding',
|
|
90
|
+
uncommittedFiles: gitStatus.uncommittedFiles,
|
|
91
|
+
recommendation: 'git add . && git commit -m "Pre-instrumentation commit"'
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check protected branches
|
|
96
|
+
const branchCheck = await this._checkProtectedBranch(projectPath, validationOptions.protectedBranches);
|
|
97
|
+
if (!branchCheck.valid) {
|
|
98
|
+
return branchCheck;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check for merge conflicts
|
|
102
|
+
const conflictCheck = await this._checkMergeConflicts(projectPath);
|
|
103
|
+
if (!conflictCheck.valid) {
|
|
104
|
+
return conflictCheck;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
valid: true,
|
|
109
|
+
isGitRepo: true,
|
|
110
|
+
gitStatus,
|
|
111
|
+
branchInfo: branchCheck,
|
|
112
|
+
recommendation: gitStatus.hasUncommittedChanges ?
|
|
113
|
+
'Consider committing changes before instrumentation' :
|
|
114
|
+
'Git repository is ready for safe operations'
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return {
|
|
119
|
+
valid: false,
|
|
120
|
+
error: 'Git safety validation failed',
|
|
121
|
+
details: error.message,
|
|
122
|
+
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Creates a backup using Git stash or file copying
|
|
129
|
+
* @param {string} projectPath - Project root path
|
|
130
|
+
* @param {string} operationId - Unique operation identifier
|
|
131
|
+
* @param {string} strategy - Backup strategy ('git-stash', 'file-copy', 'none')
|
|
132
|
+
* @returns {Promise<Object>} Backup result
|
|
133
|
+
*/
|
|
134
|
+
async createBackup(projectPath, operationId, strategy = 'git-stash') {
|
|
135
|
+
try {
|
|
136
|
+
if (strategy === 'none') {
|
|
137
|
+
return {
|
|
138
|
+
success: true,
|
|
139
|
+
strategy: 'none',
|
|
140
|
+
message: 'No backup created as requested'
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const backupInfo = {
|
|
145
|
+
operationId,
|
|
146
|
+
projectPath: resolve(projectPath),
|
|
147
|
+
strategy,
|
|
148
|
+
timestamp: new Date().toISOString(),
|
|
149
|
+
created: false
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (strategy === 'git-stash') {
|
|
153
|
+
const gitBackup = await this._createGitStashBackup(projectPath, operationId);
|
|
154
|
+
if (gitBackup.success) {
|
|
155
|
+
backupInfo.stashRef = gitBackup.stashRef;
|
|
156
|
+
backupInfo.stashMessage = gitBackup.message;
|
|
157
|
+
backupInfo.created = true;
|
|
158
|
+
} else {
|
|
159
|
+
// Fallback to file copy if git stash fails
|
|
160
|
+
console.warn('Git stash failed, falling back to file copy:', gitBackup.error);
|
|
161
|
+
const fileBackup = await this._createFileCopyBackup(projectPath, operationId);
|
|
162
|
+
backupInfo.strategy = 'file-copy';
|
|
163
|
+
backupInfo.backupPath = fileBackup.backupPath;
|
|
164
|
+
backupInfo.created = fileBackup.success;
|
|
165
|
+
}
|
|
166
|
+
} else if (strategy === 'file-copy') {
|
|
167
|
+
const fileBackup = await this._createFileCopyBackup(projectPath, operationId);
|
|
168
|
+
backupInfo.backupPath = fileBackup.backupPath;
|
|
169
|
+
backupInfo.created = fileBackup.success;
|
|
170
|
+
} else {
|
|
171
|
+
throw new Error(`Unknown backup strategy: ${strategy}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (backupInfo.created) {
|
|
175
|
+
this.activeBackups.set(operationId, backupInfo);
|
|
176
|
+
this.operationHistory.push({
|
|
177
|
+
operationId,
|
|
178
|
+
action: 'backup_created',
|
|
179
|
+
timestamp: new Date().toISOString(),
|
|
180
|
+
strategy
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
success: backupInfo.created,
|
|
186
|
+
operationId,
|
|
187
|
+
strategy: backupInfo.strategy,
|
|
188
|
+
backupInfo: backupInfo.created ? backupInfo : null,
|
|
189
|
+
message: backupInfo.created ?
|
|
190
|
+
`Backup created successfully using ${backupInfo.strategy}` :
|
|
191
|
+
'Backup creation failed'
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return {
|
|
196
|
+
success: false,
|
|
197
|
+
error: 'Backup creation failed',
|
|
198
|
+
details: error.message,
|
|
199
|
+
operationId
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Restores from backup (rollback operation)
|
|
206
|
+
* @param {string} operationId - Operation to rollback
|
|
207
|
+
* @returns {Promise<Object>} Rollback result
|
|
208
|
+
*/
|
|
209
|
+
async rollbackOperation(operationId) {
|
|
210
|
+
try {
|
|
211
|
+
const backupInfo = this.activeBackups.get(operationId);
|
|
212
|
+
if (!backupInfo) {
|
|
213
|
+
return {
|
|
214
|
+
success: false,
|
|
215
|
+
error: 'No backup found for operation',
|
|
216
|
+
operationId
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let rollbackResult;
|
|
221
|
+
|
|
222
|
+
if (backupInfo.strategy === 'git-stash' && backupInfo.stashRef) {
|
|
223
|
+
rollbackResult = await this._rollbackFromGitStash(backupInfo);
|
|
224
|
+
} else if (backupInfo.strategy === 'file-copy' && backupInfo.backupPath) {
|
|
225
|
+
rollbackResult = await this._rollbackFromFileCopy(backupInfo);
|
|
226
|
+
} else {
|
|
227
|
+
throw new Error(`Cannot rollback: invalid backup strategy or missing backup data`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (rollbackResult.success) {
|
|
231
|
+
this.operationHistory.push({
|
|
232
|
+
operationId,
|
|
233
|
+
action: 'rollback_completed',
|
|
234
|
+
timestamp: new Date().toISOString(),
|
|
235
|
+
strategy: backupInfo.strategy
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return rollbackResult;
|
|
240
|
+
|
|
241
|
+
} catch (error) {
|
|
242
|
+
return {
|
|
243
|
+
success: false,
|
|
244
|
+
error: 'Rollback operation failed',
|
|
245
|
+
details: error.message,
|
|
246
|
+
operationId
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Cleans up backup resources for completed operations
|
|
253
|
+
* @param {string} operationId - Operation to clean up
|
|
254
|
+
* @returns {Promise<Object>} Cleanup result
|
|
255
|
+
*/
|
|
256
|
+
async cleanupBackup(operationId) {
|
|
257
|
+
try {
|
|
258
|
+
const backupInfo = this.activeBackups.get(operationId);
|
|
259
|
+
if (!backupInfo) {
|
|
260
|
+
return {
|
|
261
|
+
success: true,
|
|
262
|
+
message: 'No backup to clean up'
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let cleanupResult = { success: false };
|
|
267
|
+
|
|
268
|
+
if (backupInfo.strategy === 'git-stash' && backupInfo.stashRef) {
|
|
269
|
+
cleanupResult = await this._cleanupGitStash(backupInfo);
|
|
270
|
+
} else if (backupInfo.strategy === 'file-copy' && backupInfo.backupPath) {
|
|
271
|
+
cleanupResult = await this._cleanupFileCopy(backupInfo);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (cleanupResult.success) {
|
|
275
|
+
this.activeBackups.delete(operationId);
|
|
276
|
+
this.operationHistory.push({
|
|
277
|
+
operationId,
|
|
278
|
+
action: 'backup_cleaned',
|
|
279
|
+
timestamp: new Date().toISOString(),
|
|
280
|
+
strategy: backupInfo.strategy
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return cleanupResult;
|
|
285
|
+
|
|
286
|
+
} catch (error) {
|
|
287
|
+
return {
|
|
288
|
+
success: false,
|
|
289
|
+
error: 'Cleanup operation failed',
|
|
290
|
+
details: error.message,
|
|
291
|
+
operationId
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Private implementation methods
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Gets comprehensive Git status information
|
|
300
|
+
* @private
|
|
301
|
+
*/
|
|
302
|
+
async _getGitStatus(projectPath) {
|
|
303
|
+
try {
|
|
304
|
+
// Check git status with porcelain format for parsing
|
|
305
|
+
const statusOutput = execSync('git status --porcelain', {
|
|
306
|
+
cwd: projectPath,
|
|
307
|
+
encoding: 'utf8',
|
|
308
|
+
timeout: this.options.timeout
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const uncommittedFiles = statusOutput.trim() ?
|
|
312
|
+
statusOutput.trim().split('\n').map(line => line.substring(3)) : [];
|
|
313
|
+
|
|
314
|
+
// Get current branch
|
|
315
|
+
const branchOutput = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
316
|
+
cwd: projectPath,
|
|
317
|
+
encoding: 'utf8',
|
|
318
|
+
timeout: this.options.timeout
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const currentBranch = branchOutput.trim();
|
|
322
|
+
|
|
323
|
+
// Check if there are staged changes
|
|
324
|
+
const stagedOutput = execSync('git diff --cached --name-only', {
|
|
325
|
+
cwd: projectPath,
|
|
326
|
+
encoding: 'utf8',
|
|
327
|
+
timeout: this.options.timeout
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const stagedFiles = stagedOutput.trim() ? stagedOutput.trim().split('\n') : [];
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
valid: true,
|
|
334
|
+
hasUncommittedChanges: uncommittedFiles.length > 0,
|
|
335
|
+
uncommittedFiles,
|
|
336
|
+
hasStagedChanges: stagedFiles.length > 0,
|
|
337
|
+
stagedFiles,
|
|
338
|
+
currentBranch
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
} catch (error) {
|
|
342
|
+
return {
|
|
343
|
+
valid: false,
|
|
344
|
+
error: 'Failed to get git status',
|
|
345
|
+
details: error.message
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Checks if current branch is protected
|
|
352
|
+
* @private
|
|
353
|
+
*/
|
|
354
|
+
async _checkProtectedBranch(projectPath, protectedBranches) {
|
|
355
|
+
try {
|
|
356
|
+
const branchOutput = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
357
|
+
cwd: projectPath,
|
|
358
|
+
encoding: 'utf8',
|
|
359
|
+
timeout: this.options.timeout
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const currentBranch = branchOutput.trim();
|
|
363
|
+
const isProtected = protectedBranches.includes(currentBranch);
|
|
364
|
+
|
|
365
|
+
if (isProtected) {
|
|
366
|
+
return {
|
|
367
|
+
valid: false,
|
|
368
|
+
error: 'Protected branch detected',
|
|
369
|
+
currentBranch,
|
|
370
|
+
protectedBranches,
|
|
371
|
+
recommendation: `Switch to a feature branch: git checkout -b instrumentation-${Date.now()}`
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
valid: true,
|
|
377
|
+
currentBranch,
|
|
378
|
+
isProtected: false
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
} catch (error) {
|
|
382
|
+
return {
|
|
383
|
+
valid: false,
|
|
384
|
+
error: 'Failed to check branch protection',
|
|
385
|
+
details: error.message
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Checks for merge conflicts
|
|
392
|
+
* @private
|
|
393
|
+
*/
|
|
394
|
+
async _checkMergeConflicts(projectPath) {
|
|
395
|
+
try {
|
|
396
|
+
// Check for merge conflict markers in staged files
|
|
397
|
+
const conflictCheck = execSync('git ls-files -u', {
|
|
398
|
+
cwd: projectPath,
|
|
399
|
+
encoding: 'utf8',
|
|
400
|
+
timeout: this.options.timeout
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const hasConflicts = conflictCheck.trim().length > 0;
|
|
404
|
+
|
|
405
|
+
if (hasConflicts) {
|
|
406
|
+
return {
|
|
407
|
+
valid: false,
|
|
408
|
+
error: 'Merge conflicts detected',
|
|
409
|
+
recommendation: 'Resolve conflicts before proceeding: git status'
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return { valid: true, hasConflicts: false };
|
|
414
|
+
|
|
415
|
+
} catch (error) {
|
|
416
|
+
return {
|
|
417
|
+
valid: false,
|
|
418
|
+
error: 'Failed to check merge conflicts',
|
|
419
|
+
details: error.message
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Creates a Git stash backup
|
|
426
|
+
* @private
|
|
427
|
+
*/
|
|
428
|
+
async _createGitStashBackup(projectPath, operationId) {
|
|
429
|
+
try {
|
|
430
|
+
const stashMessage = `ChromeDebug MCP instrumentation backup - ${operationId} - ${new Date().toISOString()}`;
|
|
431
|
+
|
|
432
|
+
// Create stash including untracked files
|
|
433
|
+
execSync(`git stash push -u -m "${stashMessage}"`, {
|
|
434
|
+
cwd: projectPath,
|
|
435
|
+
encoding: 'utf8',
|
|
436
|
+
timeout: this.options.timeout
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// Get the stash reference
|
|
440
|
+
const stashList = execSync('git stash list --oneline', {
|
|
441
|
+
cwd: projectPath,
|
|
442
|
+
encoding: 'utf8',
|
|
443
|
+
timeout: this.options.timeout
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const stashLines = stashList.trim().split('\n');
|
|
447
|
+
const latestStash = stashLines[0];
|
|
448
|
+
const stashRef = latestStash ? latestStash.split(':')[0] : 'stash@{0}';
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
success: true,
|
|
452
|
+
stashRef,
|
|
453
|
+
message: stashMessage,
|
|
454
|
+
operationId
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
} catch (error) {
|
|
458
|
+
return {
|
|
459
|
+
success: false,
|
|
460
|
+
error: 'Git stash creation failed',
|
|
461
|
+
details: error.message
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Creates a file copy backup
|
|
468
|
+
* @private
|
|
469
|
+
*/
|
|
470
|
+
async _createFileCopyBackup(projectPath, operationId) {
|
|
471
|
+
try {
|
|
472
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
473
|
+
const backupPath = `${projectPath}-backup-${operationId}-${timestamp}`;
|
|
474
|
+
|
|
475
|
+
// Create backup directory
|
|
476
|
+
mkdirSync(backupPath, { recursive: true });
|
|
477
|
+
|
|
478
|
+
// Copy files (excluding node_modules, .git, etc.)
|
|
479
|
+
const patterns = ['**/*'];
|
|
480
|
+
const exclusions = [
|
|
481
|
+
'node_modules/**',
|
|
482
|
+
'.git/**',
|
|
483
|
+
'dist/**',
|
|
484
|
+
'build/**',
|
|
485
|
+
'**/*.log',
|
|
486
|
+
'**/.*cache*/**'
|
|
487
|
+
];
|
|
488
|
+
|
|
489
|
+
const files = await glob(patterns, {
|
|
490
|
+
cwd: projectPath,
|
|
491
|
+
absolute: true,
|
|
492
|
+
ignore: exclusions,
|
|
493
|
+
nodir: true
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
for (const file of files) {
|
|
497
|
+
try {
|
|
498
|
+
const relativePath = file.replace(projectPath, '');
|
|
499
|
+
const destPath = join(backupPath, relativePath);
|
|
500
|
+
const destDir = dirname(destPath);
|
|
501
|
+
|
|
502
|
+
mkdirSync(destDir, { recursive: true });
|
|
503
|
+
copyFileSync(file, destPath);
|
|
504
|
+
} catch (copyError) {
|
|
505
|
+
console.warn(`Failed to copy file ${file}:`, copyError.message);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
success: true,
|
|
511
|
+
backupPath,
|
|
512
|
+
filesCopied: files.length,
|
|
513
|
+
operationId
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
} catch (error) {
|
|
517
|
+
return {
|
|
518
|
+
success: false,
|
|
519
|
+
error: 'File copy backup failed',
|
|
520
|
+
details: error.message
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Rollback from Git stash
|
|
527
|
+
* @private
|
|
528
|
+
*/
|
|
529
|
+
async _rollbackFromGitStash(backupInfo) {
|
|
530
|
+
try {
|
|
531
|
+
// Reset working directory to clean state
|
|
532
|
+
execSync('git reset --hard HEAD', {
|
|
533
|
+
cwd: backupInfo.projectPath,
|
|
534
|
+
timeout: this.options.timeout
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// Apply the stash
|
|
538
|
+
execSync(`git stash apply ${backupInfo.stashRef}`, {
|
|
539
|
+
cwd: backupInfo.projectPath,
|
|
540
|
+
timeout: this.options.timeout
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
success: true,
|
|
545
|
+
message: 'Successfully rolled back from Git stash',
|
|
546
|
+
stashRef: backupInfo.stashRef
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
} catch (error) {
|
|
550
|
+
return {
|
|
551
|
+
success: false,
|
|
552
|
+
error: 'Git stash rollback failed',
|
|
553
|
+
details: error.message
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Rollback from file copy
|
|
560
|
+
* @private
|
|
561
|
+
*/
|
|
562
|
+
async _rollbackFromFileCopy(backupInfo) {
|
|
563
|
+
try {
|
|
564
|
+
// Remove current files and restore from backup
|
|
565
|
+
const files = await glob('**/*', {
|
|
566
|
+
cwd: backupInfo.backupPath,
|
|
567
|
+
absolute: true,
|
|
568
|
+
nodir: true
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
for (const file of files) {
|
|
572
|
+
const relativePath = file.replace(backupInfo.backupPath, '');
|
|
573
|
+
const destPath = join(backupInfo.projectPath, relativePath);
|
|
574
|
+
const destDir = dirname(destPath);
|
|
575
|
+
|
|
576
|
+
mkdirSync(destDir, { recursive: true });
|
|
577
|
+
copyFileSync(file, destPath);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return {
|
|
581
|
+
success: true,
|
|
582
|
+
message: 'Successfully rolled back from file copy',
|
|
583
|
+
filesRestored: files.length
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
} catch (error) {
|
|
587
|
+
return {
|
|
588
|
+
success: false,
|
|
589
|
+
error: 'File copy rollback failed',
|
|
590
|
+
details: error.message
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Cleanup Git stash
|
|
597
|
+
* @private
|
|
598
|
+
*/
|
|
599
|
+
async _cleanupGitStash(backupInfo) {
|
|
600
|
+
try {
|
|
601
|
+
execSync(`git stash drop ${backupInfo.stashRef}`, {
|
|
602
|
+
cwd: backupInfo.projectPath,
|
|
603
|
+
timeout: this.options.timeout
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
return {
|
|
607
|
+
success: true,
|
|
608
|
+
message: 'Git stash cleaned up successfully'
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
} catch (error) {
|
|
612
|
+
return {
|
|
613
|
+
success: false,
|
|
614
|
+
error: 'Git stash cleanup failed',
|
|
615
|
+
details: error.message
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Cleanup file copy backup
|
|
622
|
+
* @private
|
|
623
|
+
*/
|
|
624
|
+
async _cleanupFileCopy(backupInfo) {
|
|
625
|
+
try {
|
|
626
|
+
if (existsSync(backupInfo.backupPath)) {
|
|
627
|
+
rmSync(backupInfo.backupPath, { recursive: true, force: true });
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return {
|
|
631
|
+
success: true,
|
|
632
|
+
message: 'File copy backup cleaned up successfully'
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
} catch (error) {
|
|
636
|
+
return {
|
|
637
|
+
success: false,
|
|
638
|
+
error: 'File copy cleanup failed',
|
|
639
|
+
details: error.message
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Gets service status and active operations
|
|
646
|
+
* @returns {Object} Service status
|
|
647
|
+
*/
|
|
648
|
+
getServiceStatus() {
|
|
649
|
+
return {
|
|
650
|
+
activeBackups: this.activeBackups.size,
|
|
651
|
+
operationHistory: this.operationHistory.slice(-10), // Last 10 operations
|
|
652
|
+
options: this.options
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Gets backup information for an operation
|
|
658
|
+
* @param {string} operationId - Operation ID
|
|
659
|
+
* @returns {Object|null} Backup information
|
|
660
|
+
*/
|
|
661
|
+
getBackupInfo(operationId) {
|
|
662
|
+
return this.activeBackups.get(operationId) || null;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Cleanup service resources
|
|
667
|
+
* @returns {Promise<void>}
|
|
668
|
+
*/
|
|
669
|
+
async cleanup() {
|
|
670
|
+
// Clean up any temporary resources
|
|
671
|
+
this.activeBackups.clear();
|
|
672
|
+
this.operationHistory = [];
|
|
673
|
+
console.log('GitSafetyService cleanup completed');
|
|
674
|
+
}
|
|
675
|
+
}
|