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,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async utilities for Git Watchtower
|
|
3
|
+
* Provides Mutex, timeout wrapper, and retry with exponential backoff
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Simple mutex for preventing concurrent operations
|
|
8
|
+
* Use this to prevent race conditions in polling and server operations
|
|
9
|
+
*/
|
|
10
|
+
class Mutex {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.locked = false;
|
|
13
|
+
this.queue = [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Acquire the lock. Returns a promise that resolves when lock is acquired.
|
|
18
|
+
* @returns {Promise<void>}
|
|
19
|
+
*/
|
|
20
|
+
async acquire() {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
if (!this.locked) {
|
|
23
|
+
this.locked = true;
|
|
24
|
+
resolve();
|
|
25
|
+
} else {
|
|
26
|
+
this.queue.push(resolve);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Release the lock. If there are waiting acquirers, the next one gets the lock.
|
|
33
|
+
*/
|
|
34
|
+
release() {
|
|
35
|
+
if (this.queue.length > 0) {
|
|
36
|
+
const next = this.queue.shift();
|
|
37
|
+
next();
|
|
38
|
+
} else {
|
|
39
|
+
this.locked = false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Execute a function while holding the lock
|
|
45
|
+
* @template T
|
|
46
|
+
* @param {() => Promise<T>} fn - Async function to execute
|
|
47
|
+
* @returns {Promise<T>}
|
|
48
|
+
*/
|
|
49
|
+
async withLock(fn) {
|
|
50
|
+
await this.acquire();
|
|
51
|
+
try {
|
|
52
|
+
return await fn();
|
|
53
|
+
} finally {
|
|
54
|
+
this.release();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if the mutex is currently locked
|
|
60
|
+
* @returns {boolean}
|
|
61
|
+
*/
|
|
62
|
+
isLocked() {
|
|
63
|
+
return this.locked;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get the number of waiters in the queue
|
|
68
|
+
* @returns {number}
|
|
69
|
+
*/
|
|
70
|
+
getQueueLength() {
|
|
71
|
+
return this.queue.length;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Wrap a promise with a timeout
|
|
77
|
+
* @template T
|
|
78
|
+
* @param {Promise<T>} promise - The promise to wrap
|
|
79
|
+
* @param {number} ms - Timeout in milliseconds
|
|
80
|
+
* @param {string} [message] - Custom timeout error message
|
|
81
|
+
* @returns {Promise<T>}
|
|
82
|
+
* @throws {Error} If the timeout is exceeded
|
|
83
|
+
*/
|
|
84
|
+
function withTimeout(promise, ms, message = 'Operation timed out') {
|
|
85
|
+
let timeoutId;
|
|
86
|
+
|
|
87
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
88
|
+
timeoutId = setTimeout(() => {
|
|
89
|
+
reject(new Error(message));
|
|
90
|
+
}, ms);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return Promise.race([promise, timeoutPromise]).finally(() => {
|
|
94
|
+
clearTimeout(timeoutId);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Retry an async function with exponential backoff
|
|
100
|
+
* @template T
|
|
101
|
+
* @param {() => Promise<T>} fn - Async function to retry
|
|
102
|
+
* @param {Object} [options] - Retry options
|
|
103
|
+
* @param {number} [options.maxAttempts=3] - Maximum number of attempts
|
|
104
|
+
* @param {number} [options.baseDelay=1000] - Base delay in ms (doubles each retry)
|
|
105
|
+
* @param {number} [options.maxDelay=30000] - Maximum delay between retries
|
|
106
|
+
* @param {(error: Error) => boolean} [options.shouldRetry] - Function to determine if error is retryable
|
|
107
|
+
* @returns {Promise<T>}
|
|
108
|
+
* @throws {Error} The last error if all retries fail
|
|
109
|
+
*/
|
|
110
|
+
async function retry(fn, options = {}) {
|
|
111
|
+
const {
|
|
112
|
+
maxAttempts = 3,
|
|
113
|
+
baseDelay = 1000,
|
|
114
|
+
maxDelay = 30000,
|
|
115
|
+
shouldRetry = () => true,
|
|
116
|
+
} = options;
|
|
117
|
+
|
|
118
|
+
let lastError;
|
|
119
|
+
|
|
120
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
121
|
+
try {
|
|
122
|
+
return await fn();
|
|
123
|
+
} catch (error) {
|
|
124
|
+
lastError = error;
|
|
125
|
+
|
|
126
|
+
// Check if we should retry this error
|
|
127
|
+
if (!shouldRetry(error)) {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Don't wait after the last attempt
|
|
132
|
+
if (attempt < maxAttempts) {
|
|
133
|
+
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
|
|
134
|
+
await sleep(delay);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
throw lastError;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Sleep for a given number of milliseconds
|
|
144
|
+
* @param {number} ms - Milliseconds to sleep
|
|
145
|
+
* @returns {Promise<void>}
|
|
146
|
+
*/
|
|
147
|
+
function sleep(ms) {
|
|
148
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Debounce a function - only execute after delay with no calls
|
|
153
|
+
* @template {(...args: any[]) => void} T
|
|
154
|
+
* @param {T} fn - Function to debounce
|
|
155
|
+
* @param {number} delay - Delay in milliseconds
|
|
156
|
+
* @returns {T & { cancel: () => void }}
|
|
157
|
+
*/
|
|
158
|
+
function debounce(fn, delay) {
|
|
159
|
+
let timeoutId = null;
|
|
160
|
+
|
|
161
|
+
const debounced = (...args) => {
|
|
162
|
+
if (timeoutId) {
|
|
163
|
+
clearTimeout(timeoutId);
|
|
164
|
+
}
|
|
165
|
+
timeoutId = setTimeout(() => {
|
|
166
|
+
fn(...args);
|
|
167
|
+
timeoutId = null;
|
|
168
|
+
}, delay);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
debounced.cancel = () => {
|
|
172
|
+
if (timeoutId) {
|
|
173
|
+
clearTimeout(timeoutId);
|
|
174
|
+
timeoutId = null;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// @ts-ignore - TypeScript can't verify generic function augmentation
|
|
179
|
+
return debounced;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Throttle a function - execute at most once per interval
|
|
184
|
+
* @template {(...args: any[]) => void} T
|
|
185
|
+
* @param {T} fn - Function to throttle
|
|
186
|
+
* @param {number} interval - Minimum interval between calls in milliseconds
|
|
187
|
+
* @returns {T}
|
|
188
|
+
*/
|
|
189
|
+
function throttle(fn, interval) {
|
|
190
|
+
let lastCall = 0;
|
|
191
|
+
let timeoutId = null;
|
|
192
|
+
|
|
193
|
+
// @ts-ignore - TypeScript can't verify generic function return type
|
|
194
|
+
return (...args) => {
|
|
195
|
+
const now = Date.now();
|
|
196
|
+
const timeSinceLastCall = now - lastCall;
|
|
197
|
+
|
|
198
|
+
if (timeSinceLastCall >= interval) {
|
|
199
|
+
lastCall = now;
|
|
200
|
+
fn(...args);
|
|
201
|
+
} else if (!timeoutId) {
|
|
202
|
+
// Schedule a call for when the interval expires
|
|
203
|
+
timeoutId = setTimeout(() => {
|
|
204
|
+
lastCall = Date.now();
|
|
205
|
+
timeoutId = null;
|
|
206
|
+
fn(...args);
|
|
207
|
+
}, interval - timeSinceLastCall);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = {
|
|
213
|
+
Mutex,
|
|
214
|
+
withTimeout,
|
|
215
|
+
retry,
|
|
216
|
+
sleep,
|
|
217
|
+
debounce,
|
|
218
|
+
throttle,
|
|
219
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform browser opening utility
|
|
3
|
+
* @module utils/browser
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { execFile } = require('child_process');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Open a URL in the user's default browser.
|
|
10
|
+
* Cross-platform: macOS (open), Windows (start), Linux (xdg-open).
|
|
11
|
+
* Uses execFile (no shell) to prevent command injection via crafted URLs.
|
|
12
|
+
* @param {string} url - The URL to open
|
|
13
|
+
* @param {function} [onError] - Optional error callback (receives Error)
|
|
14
|
+
*/
|
|
15
|
+
function openInBrowser(url, onError) {
|
|
16
|
+
const platform = process.platform;
|
|
17
|
+
let command;
|
|
18
|
+
let args;
|
|
19
|
+
|
|
20
|
+
if (platform === 'darwin') {
|
|
21
|
+
command = 'open';
|
|
22
|
+
args = [url];
|
|
23
|
+
} else if (platform === 'win32') {
|
|
24
|
+
// On Windows, 'start' is a shell built-in, so we must use cmd.exe.
|
|
25
|
+
// The URL is passed as a separate argument, not interpolated into a string.
|
|
26
|
+
command = 'cmd.exe';
|
|
27
|
+
args = ['/c', 'start', '', url];
|
|
28
|
+
} else {
|
|
29
|
+
command = 'xdg-open';
|
|
30
|
+
args = [url];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
execFile(command, args, (error) => {
|
|
34
|
+
if (error && onError) {
|
|
35
|
+
onError(error);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { openInBrowser };
|