claude-comms 1.0.1 → 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.
@@ -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}