claude-comms 1.0.0 → 1.0.2
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/CHANGELOG.md +53 -0
- package/README.md +64 -23
- package/bin/claude-setup.js +118 -38
- package/package.json +66 -19
- package/src/cli/index.js +30 -0
- package/src/cli/interfaces.js +386 -0
- package/src/cli/parser.js +158 -0
- package/src/cli/prompts.js +317 -0
- package/src/cli/runner.js +436 -0
- package/src/fetcher/github.js +676 -0
- package/src/index.js +221 -0
- package/src/installer/dependency-check.js +19 -0
- package/src/installer/file-writer.js +648 -0
- package/src/orchestrator/installer.js +849 -0
- package/src/orchestrator/main.js +267 -0
- package/src/orchestrator/mocks/index.js +388 -0
- package/src/utils/config.js +430 -0
- package/src/utils/constants.js +397 -0
- package/src/utils/errors.js +361 -0
- package/src/utils/index.js +46 -0
- package/src/utils/logger.js +177 -0
- package/src/utils/platform.js +362 -0
- package/src/utils/retry.js +358 -0
- package/templates/settings.local.json +1 -0
- package/templates/get_unread_messages.py +0 -54
- package/templates/send_message.py +0 -75
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform detection and cross-platform utilities
|
|
3
|
+
* Handles OS detection, path normalization, and platform-specific operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const fs = require('fs').promises;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Platform information and utilities
|
|
12
|
+
*/
|
|
13
|
+
class Platform {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.platform = os.platform();
|
|
16
|
+
this.arch = os.arch();
|
|
17
|
+
this.release = os.release();
|
|
18
|
+
this.homedir = os.homedir();
|
|
19
|
+
this.tmpdir = os.tmpdir();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if running on Windows
|
|
24
|
+
*/
|
|
25
|
+
isWindows() {
|
|
26
|
+
return this.platform === 'win32';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if running on macOS
|
|
31
|
+
*/
|
|
32
|
+
isMacOS() {
|
|
33
|
+
return this.platform === 'darwin';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if running on Linux
|
|
38
|
+
*/
|
|
39
|
+
isLinux() {
|
|
40
|
+
return this.platform === 'linux';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if running on Unix-like system (Linux or macOS)
|
|
45
|
+
*/
|
|
46
|
+
isUnix() {
|
|
47
|
+
return this.isLinux() || this.isMacOS();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get platform-specific line ending
|
|
52
|
+
*/
|
|
53
|
+
getLineEnding() {
|
|
54
|
+
return this.isWindows() ? '\r\n' : '\n';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get platform-specific path separator
|
|
59
|
+
*/
|
|
60
|
+
getPathSeparator() {
|
|
61
|
+
return path.sep;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Normalize path for current platform
|
|
66
|
+
*/
|
|
67
|
+
normalizePath(inputPath) {
|
|
68
|
+
if (!inputPath) return inputPath;
|
|
69
|
+
return path.normalize(inputPath);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve path relative to home directory
|
|
74
|
+
*/
|
|
75
|
+
resolveHome(inputPath) {
|
|
76
|
+
if (!inputPath) return inputPath;
|
|
77
|
+
if (inputPath.startsWith('~/') || inputPath === '~') {
|
|
78
|
+
return path.join(this.homedir, inputPath.slice(2));
|
|
79
|
+
}
|
|
80
|
+
return inputPath;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get absolute path, resolving ~ and relative paths
|
|
85
|
+
*/
|
|
86
|
+
getAbsolutePath(inputPath, relativeTo = process.cwd()) {
|
|
87
|
+
if (!inputPath) return relativeTo;
|
|
88
|
+
|
|
89
|
+
// Resolve home directory
|
|
90
|
+
let resolved = this.resolveHome(inputPath);
|
|
91
|
+
|
|
92
|
+
// Make absolute if relative
|
|
93
|
+
if (!path.isAbsolute(resolved)) {
|
|
94
|
+
resolved = path.resolve(relativeTo, resolved);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return this.normalizePath(resolved);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if path is safe (prevent directory traversal)
|
|
102
|
+
*/
|
|
103
|
+
isSafePath(inputPath, basePath) {
|
|
104
|
+
try {
|
|
105
|
+
const absolute = this.getAbsolutePath(inputPath);
|
|
106
|
+
const baseAbsolute = this.getAbsolutePath(basePath);
|
|
107
|
+
const relative = path.relative(baseAbsolute, absolute);
|
|
108
|
+
|
|
109
|
+
// Check if path escapes base directory
|
|
110
|
+
return !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get user's shell information
|
|
118
|
+
*/
|
|
119
|
+
getShellInfo() {
|
|
120
|
+
const shell = process.env.SHELL || (this.isWindows() ? 'cmd' : 'bash');
|
|
121
|
+
const shellName = path.basename(shell);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
path: shell,
|
|
125
|
+
name: shellName,
|
|
126
|
+
isInteractive: !!process.stdout.isTTY
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Check if running in CI environment
|
|
132
|
+
*/
|
|
133
|
+
isCI() {
|
|
134
|
+
return !!(
|
|
135
|
+
process.env.CI ||
|
|
136
|
+
process.env.CONTINUOUS_INTEGRATION ||
|
|
137
|
+
process.env.BUILD_NUMBER ||
|
|
138
|
+
process.env.GITHUB_ACTIONS ||
|
|
139
|
+
process.env.TRAVIS ||
|
|
140
|
+
process.env.CIRCLECI
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get available disk space for a path
|
|
146
|
+
*/
|
|
147
|
+
async getDiskSpace(targetPath) {
|
|
148
|
+
try {
|
|
149
|
+
const stats = await fs.statfs ? fs.statfs(targetPath) : null;
|
|
150
|
+
if (stats) {
|
|
151
|
+
return {
|
|
152
|
+
free: stats.bavail * stats.bsize,
|
|
153
|
+
total: stats.blocks * stats.bsize
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
// Fallback: return null if can't determine
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Check if directory is writable
|
|
164
|
+
*/
|
|
165
|
+
async isWritable(dirPath) {
|
|
166
|
+
try {
|
|
167
|
+
const testFile = path.join(dirPath, '.write-test-' + Date.now());
|
|
168
|
+
await fs.writeFile(testFile, 'test');
|
|
169
|
+
await fs.unlink(testFile);
|
|
170
|
+
return true;
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get platform-specific configuration directory
|
|
178
|
+
*/
|
|
179
|
+
getConfigDir(appName = 'claude-code') {
|
|
180
|
+
if (this.isWindows()) {
|
|
181
|
+
return path.join(process.env.APPDATA || this.homedir, appName);
|
|
182
|
+
} else if (this.isMacOS()) {
|
|
183
|
+
return path.join(this.homedir, 'Library', 'Application Support', appName);
|
|
184
|
+
} else {
|
|
185
|
+
return path.join(process.env.XDG_CONFIG_HOME || path.join(this.homedir, '.config'), appName);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get platform-specific cache directory
|
|
191
|
+
*/
|
|
192
|
+
getCacheDir(appName = 'claude-code') {
|
|
193
|
+
if (this.isWindows()) {
|
|
194
|
+
return path.join(process.env.LOCALAPPDATA || this.tmpdir, appName);
|
|
195
|
+
} else if (this.isMacOS()) {
|
|
196
|
+
return path.join(this.homedir, 'Library', 'Caches', appName);
|
|
197
|
+
} else {
|
|
198
|
+
return path.join(process.env.XDG_CACHE_HOME || path.join(this.homedir, '.cache'), appName);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get platform-specific executable extension
|
|
204
|
+
*/
|
|
205
|
+
getExecutableExtension() {
|
|
206
|
+
return this.isWindows() ? '.exe' : '';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Find executable in PATH
|
|
211
|
+
*/
|
|
212
|
+
async findExecutable(name) {
|
|
213
|
+
const extension = this.getExecutableExtension();
|
|
214
|
+
const execName = name + extension;
|
|
215
|
+
|
|
216
|
+
// Check current directory first
|
|
217
|
+
try {
|
|
218
|
+
const localPath = path.join(process.cwd(), execName);
|
|
219
|
+
await fs.access(localPath, fs.constants.F_OK | fs.constants.X_OK);
|
|
220
|
+
return localPath;
|
|
221
|
+
} catch {
|
|
222
|
+
// Continue to PATH search
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Search in PATH
|
|
226
|
+
const pathEnv = process.env.PATH || '';
|
|
227
|
+
const pathDirs = pathEnv.split(path.delimiter);
|
|
228
|
+
|
|
229
|
+
for (const dir of pathDirs) {
|
|
230
|
+
if (!dir) continue;
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const execPath = path.join(dir, execName);
|
|
234
|
+
await fs.access(execPath, fs.constants.F_OK | fs.constants.X_OK);
|
|
235
|
+
return execPath;
|
|
236
|
+
} catch {
|
|
237
|
+
// Continue searching
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Check if a command is available
|
|
246
|
+
*/
|
|
247
|
+
async hasCommand(command) {
|
|
248
|
+
return (await this.findExecutable(command)) !== null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get environment variable with platform-specific defaults
|
|
253
|
+
*/
|
|
254
|
+
getEnvVar(name, defaultValue = null, options = {}) {
|
|
255
|
+
const value = process.env[name];
|
|
256
|
+
|
|
257
|
+
if (value !== undefined) {
|
|
258
|
+
if (options.type === 'boolean') {
|
|
259
|
+
return ['true', '1', 'yes', 'on'].includes(value.toLowerCase());
|
|
260
|
+
}
|
|
261
|
+
if (options.type === 'number') {
|
|
262
|
+
const num = Number(value);
|
|
263
|
+
return isNaN(num) ? defaultValue : num;
|
|
264
|
+
}
|
|
265
|
+
if (options.type === 'array') {
|
|
266
|
+
const separator = options.separator || (this.isWindows() ? ';' : ':');
|
|
267
|
+
return value.split(separator).filter(Boolean);
|
|
268
|
+
}
|
|
269
|
+
return value;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return defaultValue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get platform-specific temp file path
|
|
277
|
+
*/
|
|
278
|
+
getTempFilePath(prefix = 'claude-installer', extension = '') {
|
|
279
|
+
const filename = `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}${extension}`;
|
|
280
|
+
return path.join(this.tmpdir, filename);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Convert file content for platform (handle line endings)
|
|
285
|
+
*/
|
|
286
|
+
convertFileContent(content, targetPlatform = null) {
|
|
287
|
+
const target = targetPlatform || this.platform;
|
|
288
|
+
const targetEnding = target === 'win32' ? '\r\n' : '\n';
|
|
289
|
+
|
|
290
|
+
// Normalize all line endings to \n first, then convert to target
|
|
291
|
+
return content.replace(/\r\n|\r|\n/g, '\n').replace(/\n/g, targetEnding);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Get platform summary for debugging
|
|
296
|
+
*/
|
|
297
|
+
getSummary() {
|
|
298
|
+
return {
|
|
299
|
+
platform: this.platform,
|
|
300
|
+
arch: this.arch,
|
|
301
|
+
release: this.release,
|
|
302
|
+
nodeVersion: process.version,
|
|
303
|
+
isCI: this.isCI(),
|
|
304
|
+
shell: this.getShellInfo(),
|
|
305
|
+
homedir: this.homedir,
|
|
306
|
+
configDir: this.getConfigDir(),
|
|
307
|
+
cacheDir: this.getCacheDir()
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Singleton platform instance
|
|
314
|
+
*/
|
|
315
|
+
const platform = new Platform();
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Utility functions using the singleton
|
|
319
|
+
*/
|
|
320
|
+
const platformUtils = {
|
|
321
|
+
// Platform checks
|
|
322
|
+
isWindows: () => platform.isWindows(),
|
|
323
|
+
isMacOS: () => platform.isMacOS(),
|
|
324
|
+
isLinux: () => platform.isLinux(),
|
|
325
|
+
isUnix: () => platform.isUnix(),
|
|
326
|
+
isCI: () => platform.isCI(),
|
|
327
|
+
|
|
328
|
+
// Path utilities
|
|
329
|
+
normalizePath: (path) => platform.normalizePath(path),
|
|
330
|
+
resolveHome: (path) => platform.resolveHome(path),
|
|
331
|
+
getAbsolutePath: (path, relativeTo) => platform.getAbsolutePath(path, relativeTo),
|
|
332
|
+
isSafePath: (path, basePath) => platform.isSafePath(path, basePath),
|
|
333
|
+
|
|
334
|
+
// System utilities
|
|
335
|
+
getShellInfo: () => platform.getShellInfo(),
|
|
336
|
+
getDiskSpace: (path) => platform.getDiskSpace(path),
|
|
337
|
+
isWritable: (path) => platform.isWritable(path),
|
|
338
|
+
|
|
339
|
+
// Executable utilities
|
|
340
|
+
findExecutable: (name) => platform.findExecutable(name),
|
|
341
|
+
hasCommand: (command) => platform.hasCommand(command),
|
|
342
|
+
|
|
343
|
+
// Directory utilities
|
|
344
|
+
getConfigDir: (appName) => platform.getConfigDir(appName),
|
|
345
|
+
getCacheDir: (appName) => platform.getCacheDir(appName),
|
|
346
|
+
getTempFilePath: (prefix, extension) => platform.getTempFilePath(prefix, extension),
|
|
347
|
+
|
|
348
|
+
// Environment utilities
|
|
349
|
+
getEnvVar: (name, defaultValue, options) => platform.getEnvVar(name, defaultValue, options),
|
|
350
|
+
|
|
351
|
+
// Content utilities
|
|
352
|
+
getLineEnding: () => platform.getLineEnding(),
|
|
353
|
+
convertFileContent: (content, targetPlatform) => platform.convertFileContent(content, targetPlatform),
|
|
354
|
+
|
|
355
|
+
// Debug utilities
|
|
356
|
+
getSummary: () => platform.getSummary(),
|
|
357
|
+
|
|
358
|
+
// Access to Platform class
|
|
359
|
+
Platform
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
module.exports = platformUtils;
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry utility with exponential backoff logic
|
|
3
|
+
* Provides resilient operation patterns for network and file system operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { ErrorFactory } = require('./errors');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default retry configuration
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
12
|
+
maxAttempts: 3,
|
|
13
|
+
baseDelay: 1000, // 1 second base delay
|
|
14
|
+
maxDelay: 30000, // 30 seconds max delay
|
|
15
|
+
backoffFactor: 2, // Exponential factor
|
|
16
|
+
jitter: true, // Add randomness to prevent thundering herd
|
|
17
|
+
jitterFactor: 0.1, // 10% jitter
|
|
18
|
+
retryCondition: null, // Function to determine if error is retryable
|
|
19
|
+
onRetry: null // Callback function called before each retry
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Retry an async operation with exponential backoff
|
|
24
|
+
*/
|
|
25
|
+
async function retry(operation, options = {}) {
|
|
26
|
+
const config = { ...DEFAULT_RETRY_CONFIG, ...options };
|
|
27
|
+
|
|
28
|
+
let lastError;
|
|
29
|
+
let attempt = 0;
|
|
30
|
+
|
|
31
|
+
while (attempt < config.maxAttempts) {
|
|
32
|
+
attempt++;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const result = await operation(attempt);
|
|
36
|
+
return result;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
lastError = error;
|
|
39
|
+
|
|
40
|
+
// Check if we should retry this error
|
|
41
|
+
if (!shouldRetry(error, config.retryCondition, attempt, config.maxAttempts)) {
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Don't delay after the last attempt
|
|
46
|
+
if (attempt >= config.maxAttempts) {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Calculate delay with exponential backoff and jitter
|
|
51
|
+
const delay = calculateDelay(attempt, config);
|
|
52
|
+
|
|
53
|
+
// Call retry callback if provided
|
|
54
|
+
if (config.onRetry) {
|
|
55
|
+
await config.onRetry(error, attempt, delay);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Wait before next attempt
|
|
59
|
+
await sleep(delay);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// All attempts exhausted
|
|
64
|
+
throw ErrorFactory.fromCaught(lastError, `Failed after ${config.maxAttempts} attempts`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Determine if an error should trigger a retry
|
|
69
|
+
*/
|
|
70
|
+
function shouldRetry(error, retryCondition, attempt, maxAttempts) {
|
|
71
|
+
// Don't retry if we've exhausted attempts
|
|
72
|
+
if (attempt >= maxAttempts) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Use custom retry condition if provided
|
|
77
|
+
if (retryCondition) {
|
|
78
|
+
return retryCondition(error, attempt);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Default retry conditions
|
|
82
|
+
if (error.isRecoverable && error.isRecoverable()) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Network and GitHub API errors are generally retryable
|
|
87
|
+
const retryableTypes = ['NetworkError', 'GitHubAPIError'];
|
|
88
|
+
if (retryableTypes.includes(error.constructor.name)) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Rate limit errors should be retried
|
|
93
|
+
if (error.code === 'GITHUB_RATE_LIMIT' || error.code === 'NETWORK_TIMEOUT') {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Server errors (5xx) should be retried
|
|
98
|
+
if (error.status >= 500 && error.status < 600) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ECONNRESET, ETIMEDOUT, ENOTFOUND are typically temporary
|
|
103
|
+
const retryableCodes = ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED'];
|
|
104
|
+
if (error.code && retryableCodes.includes(error.code)) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Calculate delay with exponential backoff and jitter
|
|
113
|
+
*/
|
|
114
|
+
function calculateDelay(attempt, config) {
|
|
115
|
+
// Base exponential backoff: baseDelay * (backoffFactor ^ (attempt - 1))
|
|
116
|
+
let delay = config.baseDelay * Math.pow(config.backoffFactor, attempt - 1);
|
|
117
|
+
|
|
118
|
+
// Cap at max delay
|
|
119
|
+
delay = Math.min(delay, config.maxDelay);
|
|
120
|
+
|
|
121
|
+
// Add jitter to prevent thundering herd
|
|
122
|
+
if (config.jitter) {
|
|
123
|
+
const jitterRange = delay * config.jitterFactor;
|
|
124
|
+
const jitterOffset = (Math.random() - 0.5) * 2 * jitterRange;
|
|
125
|
+
delay = Math.max(0, delay + jitterOffset);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return Math.floor(delay);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Sleep for specified milliseconds
|
|
133
|
+
*/
|
|
134
|
+
function sleep(ms) {
|
|
135
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Retry wrapper for common operations
|
|
140
|
+
*/
|
|
141
|
+
class RetryWrapper {
|
|
142
|
+
constructor(config = {}) {
|
|
143
|
+
this.config = { ...DEFAULT_RETRY_CONFIG, ...config };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Retry an HTTP fetch operation
|
|
148
|
+
*/
|
|
149
|
+
async fetch(url, options = {}) {
|
|
150
|
+
return retry(async (_attempt) => {
|
|
151
|
+
const response = await fetch(url, {
|
|
152
|
+
...options,
|
|
153
|
+
headers: {
|
|
154
|
+
'User-Agent': '@claude-code/setup-installer',
|
|
155
|
+
...options.headers
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Check for HTTP errors
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
162
|
+
error.status = response.status;
|
|
163
|
+
error.response = response;
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return response;
|
|
168
|
+
}, {
|
|
169
|
+
...this.config,
|
|
170
|
+
retryCondition: (error, _attempt) => {
|
|
171
|
+
// Retry on network errors and 5xx server errors
|
|
172
|
+
return error.status >= 500 || !error.status;
|
|
173
|
+
},
|
|
174
|
+
onRetry: (error, attempt, delay) => {
|
|
175
|
+
if (this.config.logger) {
|
|
176
|
+
this.config.logger.warn(
|
|
177
|
+
`HTTP request failed (attempt ${attempt}), retrying in ${delay}ms: ${error.message}`,
|
|
178
|
+
{ url, attempt, delay }
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Retry a file system operation
|
|
187
|
+
*/
|
|
188
|
+
async fileOperation(operation, operationName = 'file operation') {
|
|
189
|
+
return retry(operation, {
|
|
190
|
+
...this.config,
|
|
191
|
+
retryCondition: (error, _attempt) => {
|
|
192
|
+
// Retry on temporary file system errors
|
|
193
|
+
const retryableCodes = ['EMFILE', 'ENFILE', 'EAGAIN', 'EBUSY'];
|
|
194
|
+
return error.code && retryableCodes.includes(error.code);
|
|
195
|
+
},
|
|
196
|
+
onRetry: (error, attempt, delay) => {
|
|
197
|
+
if (this.config.logger) {
|
|
198
|
+
this.config.logger.warn(
|
|
199
|
+
`${operationName} failed (attempt ${attempt}), retrying in ${delay}ms: ${error.message}`,
|
|
200
|
+
{ operation: operationName, attempt, delay }
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Retry with custom configuration
|
|
209
|
+
*/
|
|
210
|
+
async withConfig(operation, customConfig) {
|
|
211
|
+
const mergedConfig = { ...this.config, ...customConfig };
|
|
212
|
+
return retry(operation, mergedConfig);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Pre-configured retry wrappers for common scenarios
|
|
218
|
+
*/
|
|
219
|
+
const retryPresets = {
|
|
220
|
+
/**
|
|
221
|
+
* GitHub API operations with rate limit handling
|
|
222
|
+
*/
|
|
223
|
+
github: new RetryWrapper({
|
|
224
|
+
maxAttempts: 5,
|
|
225
|
+
baseDelay: 2000,
|
|
226
|
+
maxDelay: 60000,
|
|
227
|
+
retryCondition: (error, _attempt) => {
|
|
228
|
+
// Always retry rate limits with longer delays
|
|
229
|
+
if (error.code === 'GITHUB_RATE_LIMIT') {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
// Retry server errors and network issues
|
|
233
|
+
return error.status >= 500 || !error.status;
|
|
234
|
+
}
|
|
235
|
+
}),
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Network operations with fast retry
|
|
239
|
+
*/
|
|
240
|
+
network: new RetryWrapper({
|
|
241
|
+
maxAttempts: 3,
|
|
242
|
+
baseDelay: 500,
|
|
243
|
+
maxDelay: 5000,
|
|
244
|
+
retryCondition: (error, _attempt) => {
|
|
245
|
+
const networkCodes = ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED'];
|
|
246
|
+
return error.code && networkCodes.includes(error.code);
|
|
247
|
+
}
|
|
248
|
+
}),
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* File system operations with quick retry
|
|
252
|
+
*/
|
|
253
|
+
filesystem: new RetryWrapper({
|
|
254
|
+
maxAttempts: 3,
|
|
255
|
+
baseDelay: 100,
|
|
256
|
+
maxDelay: 1000,
|
|
257
|
+
jitterFactor: 0.2,
|
|
258
|
+
retryCondition: (error, _attempt) => {
|
|
259
|
+
const fsCodes = ['EMFILE', 'ENFILE', 'EAGAIN', 'EBUSY'];
|
|
260
|
+
return error.code && fsCodes.includes(error.code);
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Circuit breaker pattern for failing operations
|
|
267
|
+
*/
|
|
268
|
+
class CircuitBreaker {
|
|
269
|
+
constructor(options = {}) {
|
|
270
|
+
this.failureThreshold = options.failureThreshold || 5;
|
|
271
|
+
this.resetTimeout = options.resetTimeout || 30000;
|
|
272
|
+
this.monitoringPeriod = options.monitoringPeriod || 60000;
|
|
273
|
+
|
|
274
|
+
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
|
|
275
|
+
this.failures = 0;
|
|
276
|
+
this.lastFailureTime = null;
|
|
277
|
+
this.successCount = 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Execute operation through circuit breaker
|
|
282
|
+
*/
|
|
283
|
+
async execute(operation) {
|
|
284
|
+
if (this.state === 'OPEN') {
|
|
285
|
+
if (Date.now() - this.lastFailureTime > this.resetTimeout) {
|
|
286
|
+
this.state = 'HALF_OPEN';
|
|
287
|
+
this.successCount = 0;
|
|
288
|
+
} else {
|
|
289
|
+
throw ErrorFactory.network('Circuit breaker is OPEN - too many failures');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const result = await operation();
|
|
295
|
+
|
|
296
|
+
if (this.state === 'HALF_OPEN') {
|
|
297
|
+
this.successCount++;
|
|
298
|
+
if (this.successCount >= 3) {
|
|
299
|
+
this.reset();
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
this.failures = 0;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return result;
|
|
306
|
+
} catch (error) {
|
|
307
|
+
this.recordFailure();
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Record a failure and potentially open the circuit
|
|
314
|
+
*/
|
|
315
|
+
recordFailure() {
|
|
316
|
+
this.failures++;
|
|
317
|
+
this.lastFailureTime = Date.now();
|
|
318
|
+
|
|
319
|
+
if (this.failures >= this.failureThreshold) {
|
|
320
|
+
this.state = 'OPEN';
|
|
321
|
+
} else if (this.state === 'HALF_OPEN') {
|
|
322
|
+
this.state = 'OPEN';
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Reset circuit breaker to closed state
|
|
328
|
+
*/
|
|
329
|
+
reset() {
|
|
330
|
+
this.state = 'CLOSED';
|
|
331
|
+
this.failures = 0;
|
|
332
|
+
this.lastFailureTime = null;
|
|
333
|
+
this.successCount = 0;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Get current circuit breaker status
|
|
338
|
+
*/
|
|
339
|
+
getStatus() {
|
|
340
|
+
return {
|
|
341
|
+
state: this.state,
|
|
342
|
+
failures: this.failures,
|
|
343
|
+
lastFailureTime: this.lastFailureTime,
|
|
344
|
+
successCount: this.successCount
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
module.exports = {
|
|
350
|
+
retry,
|
|
351
|
+
RetryWrapper,
|
|
352
|
+
CircuitBreaker,
|
|
353
|
+
retryPresets,
|
|
354
|
+
calculateDelay,
|
|
355
|
+
shouldRetry,
|
|
356
|
+
sleep,
|
|
357
|
+
DEFAULT_RETRY_CONFIG
|
|
358
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{\n \"observability\": {\n \"server_url\": \"http://localhost:3001\",\n \"enabled\": true,\n \"auto_start\": false\n },\n \"hooks\": {\n \"enabled\": true,\n \"python_path\": \"python3\",\n \"uv_path\": \"uv\",\n \"timeout\": 30\n },\n \"agents\": {\n \"max_concurrent\": 10,\n \"timeout\": 300,\n \"retry_attempts\": 3\n },\n \"communication\": {\n \"message_retention_hours\": 24,\n \"broadcast_enabled\": true\n },\n \"development\": {\n \"verbose_logging\": false,\n \"debug_mode\": false\n }\n}
|