git-watchtower 1.6.0 → 1.7.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/bin/git-watchtower.js +89 -9
- package/package.json +6 -1
- package/sounds/README.md +34 -0
- package/src/casino/index.js +721 -0
- package/src/casino/sounds.js +245 -0
- package/src/cli/args.js +239 -0
- package/src/config/loader.js +329 -0
- package/src/config/schema.js +305 -0
- package/src/git/branch.js +428 -0
- package/src/git/commands.js +416 -0
- package/src/git/pr.js +111 -0
- package/src/git/remote.js +127 -0
- package/src/index.js +179 -0
- package/src/polling/engine.js +157 -0
- package/src/server/process.js +329 -0
- package/src/server/static.js +95 -0
- package/src/state/store.js +527 -0
- package/src/telemetry/analytics.js +142 -0
- package/src/telemetry/config.js +123 -0
- package/src/telemetry/index.js +93 -0
- package/src/ui/actions.js +425 -0
- package/src/ui/ansi.js +498 -0
- package/src/ui/keybindings.js +198 -0
- package/src/ui/renderer.js +1326 -0
- package/src/utils/async.js +219 -0
- package/src/utils/browser.js +40 -0
- package/src/utils/errors.js +490 -0
- package/src/utils/gitignore.js +174 -0
- package/src/utils/sound.js +33 -0
- package/src/utils/time.js +27 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standardized error classes for Git Watchtower
|
|
3
|
+
* Provides consistent error handling across the application
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Base error class for Git Watchtower
|
|
8
|
+
* @extends Error
|
|
9
|
+
*/
|
|
10
|
+
class AppError extends Error {
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} message - Error message
|
|
13
|
+
* @param {string} [code] - Error code for programmatic handling
|
|
14
|
+
* @param {Object} [details] - Additional error details
|
|
15
|
+
*/
|
|
16
|
+
constructor(message, code = 'APP_ERROR', details = {}) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'AppError';
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.details = details;
|
|
21
|
+
this.timestamp = new Date();
|
|
22
|
+
|
|
23
|
+
// Capture stack trace (V8 specific)
|
|
24
|
+
if (Error.captureStackTrace) {
|
|
25
|
+
Error.captureStackTrace(this, this.constructor);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a user-friendly message for display
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
toUserMessage() {
|
|
34
|
+
return this.message;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Serialize error for logging
|
|
39
|
+
* @returns {Object}
|
|
40
|
+
*/
|
|
41
|
+
toJSON() {
|
|
42
|
+
return {
|
|
43
|
+
name: this.name,
|
|
44
|
+
code: this.code,
|
|
45
|
+
message: this.message,
|
|
46
|
+
details: this.details,
|
|
47
|
+
timestamp: this.timestamp.toISOString(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Error class for Git-related operations
|
|
54
|
+
* @extends AppError
|
|
55
|
+
*/
|
|
56
|
+
class GitError extends AppError {
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} message - Error message
|
|
59
|
+
* @param {string} [code] - Git error code
|
|
60
|
+
* @param {Object} [details] - Additional details including command, stderr
|
|
61
|
+
*/
|
|
62
|
+
constructor(message, code = 'GIT_ERROR', details = {}) {
|
|
63
|
+
super(message, code, details);
|
|
64
|
+
this.name = 'GitError';
|
|
65
|
+
this.command = details.command || null;
|
|
66
|
+
this.stderr = details.stderr || null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if error is due to network issues
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
isNetworkError() {
|
|
74
|
+
const networkPatterns = [
|
|
75
|
+
'Could not resolve host',
|
|
76
|
+
'Connection refused',
|
|
77
|
+
'Connection timed out',
|
|
78
|
+
'Network is unreachable',
|
|
79
|
+
'fatal: unable to access',
|
|
80
|
+
'SSL certificate problem',
|
|
81
|
+
];
|
|
82
|
+
return networkPatterns.some(
|
|
83
|
+
(pattern) =>
|
|
84
|
+
this.message.includes(pattern) ||
|
|
85
|
+
(this.stderr && this.stderr.includes(pattern))
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if error is due to authentication
|
|
91
|
+
* @returns {boolean}
|
|
92
|
+
*/
|
|
93
|
+
isAuthError() {
|
|
94
|
+
const authPatterns = [
|
|
95
|
+
'Authentication failed',
|
|
96
|
+
'Permission denied',
|
|
97
|
+
'Invalid username or password',
|
|
98
|
+
'could not read Username',
|
|
99
|
+
'fatal: Authentication',
|
|
100
|
+
];
|
|
101
|
+
return authPatterns.some(
|
|
102
|
+
(pattern) =>
|
|
103
|
+
this.message.includes(pattern) ||
|
|
104
|
+
(this.stderr && this.stderr.includes(pattern))
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if error is due to merge conflicts
|
|
110
|
+
* @returns {boolean}
|
|
111
|
+
*/
|
|
112
|
+
isMergeConflict() {
|
|
113
|
+
const conflictPatterns = [
|
|
114
|
+
'CONFLICT',
|
|
115
|
+
'Automatic merge failed',
|
|
116
|
+
'fix conflicts',
|
|
117
|
+
'Merge conflict',
|
|
118
|
+
];
|
|
119
|
+
return conflictPatterns.some(
|
|
120
|
+
(pattern) =>
|
|
121
|
+
this.message.includes(pattern) ||
|
|
122
|
+
(this.stderr && this.stderr.includes(pattern))
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if error is due to dirty working directory
|
|
128
|
+
* @returns {boolean}
|
|
129
|
+
*/
|
|
130
|
+
isDirtyWorkingDir() {
|
|
131
|
+
const dirtyPatterns = [
|
|
132
|
+
'Your local changes',
|
|
133
|
+
'uncommitted changes',
|
|
134
|
+
'Please commit your changes',
|
|
135
|
+
'overwritten by checkout',
|
|
136
|
+
];
|
|
137
|
+
return dirtyPatterns.some(
|
|
138
|
+
(pattern) =>
|
|
139
|
+
this.message.includes(pattern) ||
|
|
140
|
+
(this.stderr && this.stderr.includes(pattern))
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
toUserMessage() {
|
|
145
|
+
if (this.isNetworkError()) {
|
|
146
|
+
return 'Network error - check your connection';
|
|
147
|
+
}
|
|
148
|
+
if (this.isAuthError()) {
|
|
149
|
+
return 'Authentication failed - check credentials';
|
|
150
|
+
}
|
|
151
|
+
if (this.isMergeConflict()) {
|
|
152
|
+
return 'Merge conflict - resolve conflicts first';
|
|
153
|
+
}
|
|
154
|
+
if (this.isDirtyWorkingDir()) {
|
|
155
|
+
return 'Uncommitted changes - commit or stash first';
|
|
156
|
+
}
|
|
157
|
+
return this.message;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Create GitError from exec callback error
|
|
162
|
+
* @param {Error} error - Original error
|
|
163
|
+
* @param {string} command - Git command that was executed
|
|
164
|
+
* @param {string} [stderr] - Standard error output
|
|
165
|
+
* @returns {GitError}
|
|
166
|
+
*/
|
|
167
|
+
static fromExecError(error, command, stderr = '') {
|
|
168
|
+
const message = stderr || error.message;
|
|
169
|
+
let code = 'GIT_ERROR';
|
|
170
|
+
|
|
171
|
+
// Determine specific error code
|
|
172
|
+
// @ts-ignore - Node.js ExecException has killed and code properties
|
|
173
|
+
if (error.killed) {
|
|
174
|
+
code = 'GIT_TIMEOUT';
|
|
175
|
+
// @ts-ignore - Node.js ExecException has code property
|
|
176
|
+
} else if (error.code === 'ENOENT') {
|
|
177
|
+
code = 'GIT_NOT_FOUND';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return new GitError(message, code, { command, stderr });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Error class for configuration-related issues
|
|
186
|
+
* @extends AppError
|
|
187
|
+
*/
|
|
188
|
+
class ConfigError extends AppError {
|
|
189
|
+
/**
|
|
190
|
+
* @param {string} message - Error message
|
|
191
|
+
* @param {string} [code] - Config error code
|
|
192
|
+
* @param {Object} [details] - Additional details
|
|
193
|
+
*/
|
|
194
|
+
constructor(message, code = 'CONFIG_ERROR', details = {}) {
|
|
195
|
+
super(message, code, details);
|
|
196
|
+
this.name = 'ConfigError';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Create ConfigError for missing config
|
|
201
|
+
* @param {string} configPath - Path to missing config
|
|
202
|
+
* @returns {ConfigError}
|
|
203
|
+
*/
|
|
204
|
+
static missing(configPath) {
|
|
205
|
+
return new ConfigError(
|
|
206
|
+
`Configuration file not found: ${configPath}`,
|
|
207
|
+
'CONFIG_NOT_FOUND',
|
|
208
|
+
{ path: configPath }
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Create ConfigError for invalid config
|
|
214
|
+
* @param {string} reason - Why config is invalid
|
|
215
|
+
* @param {Object} [details] - Validation details
|
|
216
|
+
* @returns {ConfigError}
|
|
217
|
+
*/
|
|
218
|
+
static invalid(reason, details = {}) {
|
|
219
|
+
return new ConfigError(
|
|
220
|
+
`Invalid configuration: ${reason}`,
|
|
221
|
+
'CONFIG_INVALID',
|
|
222
|
+
details
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Create ConfigError for parse error
|
|
228
|
+
* @param {Error} parseError - Original parse error
|
|
229
|
+
* @returns {ConfigError}
|
|
230
|
+
*/
|
|
231
|
+
static parseError(parseError) {
|
|
232
|
+
return new ConfigError(
|
|
233
|
+
`Failed to parse configuration: ${parseError.message}`,
|
|
234
|
+
'CONFIG_PARSE_ERROR',
|
|
235
|
+
{ originalError: parseError.message }
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Error class for server-related issues
|
|
242
|
+
* @extends AppError
|
|
243
|
+
*/
|
|
244
|
+
class ServerError extends AppError {
|
|
245
|
+
/**
|
|
246
|
+
* @param {string} message - Error message
|
|
247
|
+
* @param {string} [code] - Server error code
|
|
248
|
+
* @param {Object} [details] - Additional details
|
|
249
|
+
*/
|
|
250
|
+
constructor(message, code = 'SERVER_ERROR', details = {}) {
|
|
251
|
+
super(message, code, details);
|
|
252
|
+
this.name = 'ServerError';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Create ServerError for port in use
|
|
257
|
+
* @param {number} port - The port that's in use
|
|
258
|
+
* @returns {ServerError}
|
|
259
|
+
*/
|
|
260
|
+
static portInUse(port) {
|
|
261
|
+
return new ServerError(
|
|
262
|
+
`Port ${port} is already in use`,
|
|
263
|
+
'PORT_IN_USE',
|
|
264
|
+
{ port }
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Create ServerError for process crash
|
|
270
|
+
* @param {string} command - The command that crashed
|
|
271
|
+
* @param {number} exitCode - Exit code
|
|
272
|
+
* @returns {ServerError}
|
|
273
|
+
*/
|
|
274
|
+
static processCrashed(command, exitCode) {
|
|
275
|
+
return new ServerError(
|
|
276
|
+
`Server process crashed with exit code ${exitCode}`,
|
|
277
|
+
'PROCESS_CRASHED',
|
|
278
|
+
{ command, exitCode }
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Create ServerError for start failure
|
|
284
|
+
* @param {string} command - The command that failed to start
|
|
285
|
+
* @param {string} reason - Failure reason
|
|
286
|
+
* @returns {ServerError}
|
|
287
|
+
*/
|
|
288
|
+
static startFailed(command, reason) {
|
|
289
|
+
return new ServerError(
|
|
290
|
+
`Failed to start server: ${reason}`,
|
|
291
|
+
'START_FAILED',
|
|
292
|
+
{ command, reason }
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Error class for validation errors
|
|
299
|
+
* @extends AppError
|
|
300
|
+
*/
|
|
301
|
+
class ValidationError extends AppError {
|
|
302
|
+
/**
|
|
303
|
+
* @param {string} message - Error message
|
|
304
|
+
* @param {string} field - Field that failed validation
|
|
305
|
+
* @param {*} value - Invalid value
|
|
306
|
+
*/
|
|
307
|
+
constructor(message, field, value) {
|
|
308
|
+
super(message, 'VALIDATION_ERROR', { field, value });
|
|
309
|
+
this.name = 'ValidationError';
|
|
310
|
+
this.field = field;
|
|
311
|
+
this.value = value;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Create ValidationError for invalid branch name
|
|
316
|
+
* @param {string} name - Invalid branch name
|
|
317
|
+
* @returns {ValidationError}
|
|
318
|
+
*/
|
|
319
|
+
static invalidBranchName(name) {
|
|
320
|
+
return new ValidationError(
|
|
321
|
+
`Invalid branch name: "${name}"`,
|
|
322
|
+
'branchName',
|
|
323
|
+
name
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Create ValidationError for invalid port
|
|
329
|
+
* @param {*} port - Invalid port value
|
|
330
|
+
* @returns {ValidationError}
|
|
331
|
+
*/
|
|
332
|
+
static invalidPort(port) {
|
|
333
|
+
return new ValidationError(
|
|
334
|
+
`Invalid port: ${port}. Must be a number between 1 and 65535`,
|
|
335
|
+
'port',
|
|
336
|
+
port
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Standardized error handler
|
|
343
|
+
* Provides consistent error handling across the application
|
|
344
|
+
*/
|
|
345
|
+
class ErrorHandler {
|
|
346
|
+
/**
|
|
347
|
+
* @param {Object} [options]
|
|
348
|
+
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
349
|
+
* @param {Function} [options.onError] - Callback for all errors
|
|
350
|
+
*/
|
|
351
|
+
constructor(options = {}) {
|
|
352
|
+
this.debug = options.debug || process.env.DEBUG === 'true';
|
|
353
|
+
this.onError = options.onError || null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Handle an error and return a user-friendly message
|
|
358
|
+
* @param {Error} error - The error to handle
|
|
359
|
+
* @param {string} context - Context where error occurred
|
|
360
|
+
* @returns {{ message: string, severity: 'error' | 'warning' | 'info' }}
|
|
361
|
+
*/
|
|
362
|
+
handle(error, context = 'unknown') {
|
|
363
|
+
// Log in debug mode
|
|
364
|
+
if (this.debug) {
|
|
365
|
+
console.error(`[${context}]`, error);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Call error callback if provided
|
|
369
|
+
if (this.onError) {
|
|
370
|
+
this.onError(error, context);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Determine severity and message
|
|
374
|
+
if (error instanceof AppError) {
|
|
375
|
+
return {
|
|
376
|
+
message: error.toUserMessage(),
|
|
377
|
+
severity: this.getSeverity(error),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Handle standard errors
|
|
382
|
+
return {
|
|
383
|
+
message: error.message || 'An unexpected error occurred',
|
|
384
|
+
severity: 'error',
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Determine error severity
|
|
390
|
+
* @param {AppError} error
|
|
391
|
+
* @returns {'error' | 'warning' | 'info'}
|
|
392
|
+
*/
|
|
393
|
+
getSeverity(error) {
|
|
394
|
+
// Network errors are warnings (transient)
|
|
395
|
+
if (error instanceof GitError && error.isNetworkError()) {
|
|
396
|
+
return 'warning';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Config errors are usually user-fixable
|
|
400
|
+
if (error instanceof ConfigError) {
|
|
401
|
+
return 'warning';
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return 'error';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Check if error is retryable
|
|
409
|
+
* @param {Error} error
|
|
410
|
+
* @returns {boolean}
|
|
411
|
+
*/
|
|
412
|
+
isRetryable(error) {
|
|
413
|
+
if (error instanceof GitError) {
|
|
414
|
+
return error.isNetworkError();
|
|
415
|
+
}
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ============================================================================
|
|
421
|
+
// Standalone error classifiers (work on raw error message strings)
|
|
422
|
+
// ============================================================================
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Check if an error message indicates an authentication failure
|
|
426
|
+
* @param {string} errorMessage - Error message to check
|
|
427
|
+
* @returns {boolean}
|
|
428
|
+
*/
|
|
429
|
+
function isAuthError(errorMessage) {
|
|
430
|
+
const authErrors = [
|
|
431
|
+
'Authentication failed',
|
|
432
|
+
'could not read Username',
|
|
433
|
+
'could not read Password',
|
|
434
|
+
'Permission denied',
|
|
435
|
+
'invalid credentials',
|
|
436
|
+
'authorization failed',
|
|
437
|
+
'fatal: Authentication',
|
|
438
|
+
'HTTP 401',
|
|
439
|
+
'HTTP 403',
|
|
440
|
+
];
|
|
441
|
+
const msg = (errorMessage || '').toLowerCase();
|
|
442
|
+
return authErrors.some(err => msg.includes(err.toLowerCase()));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Check if an error message indicates a merge conflict
|
|
447
|
+
* @param {string} errorMessage - Error message to check
|
|
448
|
+
* @returns {boolean}
|
|
449
|
+
*/
|
|
450
|
+
function isMergeConflict(errorMessage) {
|
|
451
|
+
const conflictIndicators = [
|
|
452
|
+
'CONFLICT',
|
|
453
|
+
'Automatic merge failed',
|
|
454
|
+
'fix conflicts',
|
|
455
|
+
'Merge conflict',
|
|
456
|
+
];
|
|
457
|
+
return conflictIndicators.some(ind => (errorMessage || '').includes(ind));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Check if an error message indicates a network error
|
|
462
|
+
* @param {string} errorMessage - Error message to check
|
|
463
|
+
* @returns {boolean}
|
|
464
|
+
*/
|
|
465
|
+
function isNetworkError(errorMessage) {
|
|
466
|
+
const networkErrors = [
|
|
467
|
+
'Could not resolve host',
|
|
468
|
+
'unable to access',
|
|
469
|
+
'Connection refused',
|
|
470
|
+
'Network is unreachable',
|
|
471
|
+
'Connection timed out',
|
|
472
|
+
'Failed to connect',
|
|
473
|
+
'no route to host',
|
|
474
|
+
'Temporary failure in name resolution',
|
|
475
|
+
];
|
|
476
|
+
const msg = (errorMessage || '').toLowerCase();
|
|
477
|
+
return networkErrors.some(err => msg.includes(err.toLowerCase()));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
module.exports = {
|
|
481
|
+
AppError,
|
|
482
|
+
GitError,
|
|
483
|
+
ConfigError,
|
|
484
|
+
ServerError,
|
|
485
|
+
ValidationError,
|
|
486
|
+
ErrorHandler,
|
|
487
|
+
isAuthError,
|
|
488
|
+
isMergeConflict,
|
|
489
|
+
isNetworkError,
|
|
490
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gitignore pattern parsing and file filtering utilities
|
|
3
|
+
* Used by the file watcher to ignore .git directory and .gitignore patterns
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Convert a gitignore pattern to a RegExp
|
|
11
|
+
* Supports basic gitignore syntax: *, **, ?, negation (!), directory markers (/)
|
|
12
|
+
* @param {string} pattern - The gitignore pattern
|
|
13
|
+
* @returns {RegExp|null} - The compiled regex or null if pattern is invalid/negation
|
|
14
|
+
*/
|
|
15
|
+
function gitignorePatternToRegex(pattern) {
|
|
16
|
+
// Handle negation (we'll filter these out separately)
|
|
17
|
+
if (pattern.startsWith('!')) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Handle directory-only patterns (ending with /)
|
|
22
|
+
const dirOnly = pattern.endsWith('/');
|
|
23
|
+
if (dirOnly) {
|
|
24
|
+
pattern = pattern.slice(0, -1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Handle patterns starting with / (anchored to root)
|
|
28
|
+
const anchored = pattern.startsWith('/');
|
|
29
|
+
if (anchored) {
|
|
30
|
+
pattern = pattern.slice(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Escape special regex characters except * and ?
|
|
34
|
+
let regexStr = pattern
|
|
35
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
36
|
+
// Convert **/ to a placeholder (matches any path prefix including empty)
|
|
37
|
+
.replace(/\*\*\//g, '{{GLOBSTAR_SLASH}}')
|
|
38
|
+
// Convert ** to a placeholder (matches any path)
|
|
39
|
+
.replace(/\*\*/g, '{{GLOBSTAR}}')
|
|
40
|
+
// Convert * to match anything except /
|
|
41
|
+
.replace(/\*/g, '[^/]*')
|
|
42
|
+
// Convert ? to match single character except /
|
|
43
|
+
.replace(/\?/g, '[^/]')
|
|
44
|
+
// Convert globstar placeholders back
|
|
45
|
+
// {{GLOBSTAR_SLASH}} matches "any/path/" or empty string
|
|
46
|
+
.replace(/\{\{GLOBSTAR_SLASH\}\}/g, '(.*\\/)?')
|
|
47
|
+
// {{GLOBSTAR}} matches any path including slashes
|
|
48
|
+
.replace(/\{\{GLOBSTAR\}\}/g, '.*');
|
|
49
|
+
|
|
50
|
+
// Build the final regex
|
|
51
|
+
if (anchored) {
|
|
52
|
+
regexStr = '^' + regexStr;
|
|
53
|
+
} else {
|
|
54
|
+
// Match anywhere in path
|
|
55
|
+
regexStr = '(^|/)' + regexStr;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (dirOnly) {
|
|
59
|
+
regexStr = regexStr + '(/|$)';
|
|
60
|
+
} else {
|
|
61
|
+
regexStr = regexStr + '($|/)';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
return new RegExp(regexStr);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse a .gitignore file and return an array of compiled regex patterns
|
|
73
|
+
* @param {string} gitignorePath - Path to the .gitignore file
|
|
74
|
+
* @returns {RegExp[]} - Array of compiled regex patterns
|
|
75
|
+
*/
|
|
76
|
+
function parseGitignoreFile(gitignorePath) {
|
|
77
|
+
const patterns = [];
|
|
78
|
+
|
|
79
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
80
|
+
return patterns;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
85
|
+
const lines = content.split('\n');
|
|
86
|
+
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
const trimmed = line.trim();
|
|
89
|
+
// Skip empty lines and comments
|
|
90
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const regex = gitignorePatternToRegex(trimmed);
|
|
95
|
+
if (regex) {
|
|
96
|
+
patterns.push(regex);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
// Silently continue if we can't read .gitignore
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return patterns;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Load gitignore patterns from multiple possible locations
|
|
108
|
+
* @param {string[]} searchPaths - Array of directories to search for .gitignore
|
|
109
|
+
* @returns {RegExp[]} - Array of compiled regex patterns
|
|
110
|
+
*/
|
|
111
|
+
function loadGitignorePatterns(searchPaths) {
|
|
112
|
+
for (const searchPath of searchPaths) {
|
|
113
|
+
const gitignorePath = path.join(searchPath, '.gitignore');
|
|
114
|
+
const patterns = parseGitignoreFile(gitignorePath);
|
|
115
|
+
if (patterns.length > 0) {
|
|
116
|
+
return patterns;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if a filename matches the .git directory
|
|
124
|
+
* @param {string} filename - The filename to check
|
|
125
|
+
* @returns {boolean} - True if the file is in the .git directory
|
|
126
|
+
*/
|
|
127
|
+
function isGitDirectory(filename) {
|
|
128
|
+
if (filename === '.git' || filename.startsWith('.git/') || filename.startsWith('.git\\')) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Normalize path separators for cross-platform support
|
|
133
|
+
const normalizedPath = filename.replace(/\\/g, '/');
|
|
134
|
+
|
|
135
|
+
// Check if path contains .git directory anywhere
|
|
136
|
+
if (normalizedPath.includes('/.git/') || normalizedPath.includes('/.git')) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check if a file path should be ignored by the file watcher
|
|
145
|
+
* @param {string} filename - The filename to check
|
|
146
|
+
* @param {RegExp[]} ignorePatterns - Array of compiled gitignore patterns
|
|
147
|
+
* @returns {boolean} - True if the file should be ignored
|
|
148
|
+
*/
|
|
149
|
+
function shouldIgnoreFile(filename, ignorePatterns = []) {
|
|
150
|
+
// Always ignore .git directory
|
|
151
|
+
if (isGitDirectory(filename)) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Normalize path separators for cross-platform support
|
|
156
|
+
const normalizedPath = filename.replace(/\\/g, '/');
|
|
157
|
+
|
|
158
|
+
// Check against gitignore patterns
|
|
159
|
+
for (const pattern of ignorePatterns) {
|
|
160
|
+
if (pattern.test(normalizedPath)) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = {
|
|
169
|
+
gitignorePatternToRegex,
|
|
170
|
+
parseGitignoreFile,
|
|
171
|
+
loadGitignorePatterns,
|
|
172
|
+
isGitDirectory,
|
|
173
|
+
shouldIgnoreFile,
|
|
174
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform system sound playback
|
|
3
|
+
* @module utils/sound
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { exec } = require('child_process');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Play a system notification sound (non-blocking).
|
|
10
|
+
* Cross-platform: macOS (afplay), Linux (paplay/aplay), Windows (terminal bell).
|
|
11
|
+
* @param {object} [options]
|
|
12
|
+
* @param {string} [options.cwd] - Working directory for exec
|
|
13
|
+
*/
|
|
14
|
+
function playSound(options = {}) {
|
|
15
|
+
const { platform } = process;
|
|
16
|
+
const cwd = options.cwd || process.cwd();
|
|
17
|
+
|
|
18
|
+
if (platform === 'darwin') {
|
|
19
|
+
exec('afplay /System/Library/Sounds/Pop.aiff 2>/dev/null', { cwd });
|
|
20
|
+
} else if (platform === 'linux') {
|
|
21
|
+
exec(
|
|
22
|
+
'paplay /usr/share/sounds/freedesktop/stereo/message-new-instant.oga 2>/dev/null || ' +
|
|
23
|
+
'paplay /usr/share/sounds/freedesktop/stereo/complete.oga 2>/dev/null || ' +
|
|
24
|
+
'aplay /usr/share/sounds/sound-icons/prompt.wav 2>/dev/null || ' +
|
|
25
|
+
'printf "\\a"',
|
|
26
|
+
{ cwd }
|
|
27
|
+
);
|
|
28
|
+
} else {
|
|
29
|
+
process.stdout.write('\x07');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { playSound };
|