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.
@@ -0,0 +1,430 @@
1
+ /**
2
+ * Configuration management for NPX installer
3
+ * Handles default settings, user preferences, and environment-based configuration
4
+ */
5
+
6
+ const path = require('path');
7
+ const fs = require('fs').promises;
8
+ const { ErrorFactory } = require('./errors');
9
+ const platform = require('./platform');
10
+
11
+ /**
12
+ * Configuration manager with defaults, validation, and persistence
13
+ */
14
+ class Config {
15
+ constructor(options = {}) {
16
+ this.configPath = options.configPath || null;
17
+ this.defaults = this.getDefaults();
18
+ this.config = { ...this.defaults };
19
+ this.loaded = false;
20
+ }
21
+
22
+ /**
23
+ * Get default configuration values
24
+ */
25
+ getDefaults() {
26
+ return {
27
+ // Repository settings
28
+ repository: {
29
+ owner: 'alexsavage',
30
+ name: 'claude-code-hooks-multi-agent-observability',
31
+ branch: 'main',
32
+ baseUrl: 'https://api.github.com',
33
+ rawUrl: 'https://raw.githubusercontent.com'
34
+ },
35
+
36
+ // Installation settings
37
+ installation: {
38
+ targetDir: process.cwd(),
39
+ createBackup: true,
40
+ overwriteExisting: false,
41
+ skipExistingFiles: false,
42
+ preservePermissions: true,
43
+ createDirectories: true
44
+ },
45
+
46
+ // GitHub API settings
47
+ github: {
48
+ token: null,
49
+ timeout: 30000,
50
+ retryAttempts: 3,
51
+ retryDelay: 1000,
52
+ userAgent: '@claude-code/setup-installer'
53
+ },
54
+
55
+ // File operations
56
+ files: {
57
+ excludePatterns: ['.git', 'node_modules', '.DS_Store'],
58
+ includePatterns: ['.claude/**/*', 'CLAUDE.md'],
59
+ encoding: 'utf8',
60
+ lineEnding: 'auto', // auto, lf, crlf
61
+ maxFileSize: 10 * 1024 * 1024 // 10MB
62
+ },
63
+
64
+ // Logging settings
65
+ logging: {
66
+ level: 'INFO',
67
+ silent: false,
68
+ timestamps: false,
69
+ colors: true
70
+ },
71
+
72
+ // Dependency checking
73
+ dependencies: {
74
+ checkPython: true,
75
+ checkUv: true,
76
+ pythonVersion: '3.8',
77
+ uvVersion: '0.1.0'
78
+ },
79
+
80
+ // Performance settings
81
+ performance: {
82
+ concurrentDownloads: 5,
83
+ chunkSize: 64 * 1024, // 64KB
84
+ progressUpdateInterval: 100,
85
+ enableCache: true,
86
+ cacheExpiry: 3600000 // 1 hour
87
+ },
88
+
89
+ // Security settings
90
+ security: {
91
+ validateChecksums: false,
92
+ allowUnsafeOperations: false,
93
+ restrictPaths: true,
94
+ maxPathDepth: 10
95
+ }
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Load configuration from file
101
+ */
102
+ async load(configPath = null) {
103
+ const targetPath = configPath || this.configPath;
104
+
105
+ if (!targetPath) {
106
+ this.loaded = true;
107
+ return this.config;
108
+ }
109
+
110
+ try {
111
+ const content = await fs.readFile(targetPath, 'utf8');
112
+ const fileConfig = JSON.parse(content);
113
+
114
+ // Merge with defaults
115
+ this.config = this.mergeDeep(this.defaults, fileConfig);
116
+ this.loaded = true;
117
+
118
+ // Validate configuration
119
+ this.validate();
120
+
121
+ return this.config;
122
+ } catch (error) {
123
+ if (error.code === 'ENOENT') {
124
+ // File doesn't exist, use defaults
125
+ this.loaded = true;
126
+ return this.config;
127
+ }
128
+
129
+ throw ErrorFactory.configuration(
130
+ `Failed to load configuration from ${targetPath}: ${error.message}`,
131
+ 'configFile',
132
+ { path: targetPath, originalError: error }
133
+ );
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Save configuration to file
139
+ */
140
+ async save(configPath = null) {
141
+ const targetPath = configPath || this.configPath;
142
+
143
+ if (!targetPath) {
144
+ throw ErrorFactory.configuration('No configuration file path specified');
145
+ }
146
+
147
+ try {
148
+ // Create directory if needed
149
+ const dir = path.dirname(targetPath);
150
+ await fs.mkdir(dir, { recursive: true });
151
+
152
+ // Save configuration (exclude defaults to keep file clean)
153
+ const configToSave = this.getDifferences();
154
+ const content = JSON.stringify(configToSave, null, 2);
155
+ await fs.writeFile(targetPath, content, 'utf8');
156
+
157
+ return targetPath;
158
+ } catch (error) {
159
+ throw ErrorFactory.filesystem(
160
+ `Failed to save configuration to ${targetPath}: ${error.message}`,
161
+ { path: targetPath, originalError: error }
162
+ );
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Get configuration value with dot notation support
168
+ */
169
+ get(key, defaultValue = undefined) {
170
+ const keys = key.split('.');
171
+ let value = this.config;
172
+
173
+ for (const k of keys) {
174
+ if (value && typeof value === 'object' && k in value) {
175
+ value = value[k];
176
+ } else {
177
+ return defaultValue;
178
+ }
179
+ }
180
+
181
+ return value;
182
+ }
183
+
184
+ /**
185
+ * Set configuration value with dot notation support
186
+ */
187
+ set(key, value) {
188
+ const keys = key.split('.');
189
+ const lastKey = keys.pop();
190
+ let target = this.config;
191
+
192
+ // Navigate to parent object
193
+ for (const k of keys) {
194
+ if (!(k in target) || typeof target[k] !== 'object') {
195
+ target[k] = {};
196
+ }
197
+ target = target[k];
198
+ }
199
+
200
+ // Set value
201
+ target[lastKey] = value;
202
+ return this;
203
+ }
204
+
205
+ /**
206
+ * Update configuration with partial object
207
+ */
208
+ update(updates) {
209
+ this.config = this.mergeDeep(this.config, updates);
210
+ return this;
211
+ }
212
+
213
+ /**
214
+ * Reset configuration to defaults
215
+ */
216
+ reset() {
217
+ this.config = { ...this.defaults };
218
+ return this;
219
+ }
220
+
221
+ /**
222
+ * Get differences from defaults (for saving)
223
+ */
224
+ getDifferences() {
225
+ return this.getDifferencesBetween(this.defaults, this.config);
226
+ }
227
+
228
+ /**
229
+ * Get differences between two objects
230
+ */
231
+ getDifferencesBetween(base, current, result = {}) {
232
+ for (const key in current) {
233
+ if (!(key in base)) {
234
+ // New key
235
+ result[key] = current[key];
236
+ } else if (typeof current[key] === 'object' && current[key] !== null && !Array.isArray(current[key])) {
237
+ // Nested object
238
+ const nested = this.getDifferencesBetween(base[key], current[key]);
239
+ if (Object.keys(nested).length > 0) {
240
+ result[key] = nested;
241
+ }
242
+ } else if (JSON.stringify(current[key]) !== JSON.stringify(base[key])) {
243
+ // Different value
244
+ result[key] = current[key];
245
+ }
246
+ }
247
+ return result;
248
+ }
249
+
250
+ /**
251
+ * Deep merge objects
252
+ */
253
+ mergeDeep(target, source) {
254
+ const result = { ...target };
255
+
256
+ for (const key in source) {
257
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
258
+ result[key] = this.mergeDeep(result[key] || {}, source[key]);
259
+ } else {
260
+ result[key] = source[key];
261
+ }
262
+ }
263
+
264
+ return result;
265
+ }
266
+
267
+ /**
268
+ * Validate configuration values
269
+ */
270
+ validate() {
271
+ const errors = [];
272
+
273
+ // Validate repository settings
274
+ if (!this.config.repository.owner) {
275
+ errors.push('repository.owner is required');
276
+ }
277
+ if (!this.config.repository.name) {
278
+ errors.push('repository.name is required');
279
+ }
280
+
281
+ // Validate installation settings
282
+ if (!this.config.installation.targetDir) {
283
+ errors.push('installation.targetDir is required');
284
+ }
285
+
286
+ // Validate GitHub settings
287
+ if (this.config.github.timeout < 1000) {
288
+ errors.push('github.timeout must be at least 1000ms');
289
+ }
290
+ if (this.config.github.retryAttempts < 0 || this.config.github.retryAttempts > 10) {
291
+ errors.push('github.retryAttempts must be between 0 and 10');
292
+ }
293
+
294
+ // Validate file settings
295
+ if (this.config.files.maxFileSize < 1024) {
296
+ errors.push('files.maxFileSize must be at least 1KB');
297
+ }
298
+
299
+ // Validate performance settings
300
+ if (this.config.performance.concurrentDownloads < 1 || this.config.performance.concurrentDownloads > 20) {
301
+ errors.push('performance.concurrentDownloads must be between 1 and 20');
302
+ }
303
+
304
+ if (errors.length > 0) {
305
+ throw ErrorFactory.validation(
306
+ `Configuration validation failed: ${errors.join(', ')}`,
307
+ 'config',
308
+ { errors }
309
+ );
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Apply environment variable overrides
315
+ */
316
+ applyEnvironmentOverrides() {
317
+ const envMappings = {
318
+ 'CLAUDE_GITHUB_TOKEN': 'github.token',
319
+ 'CLAUDE_TARGET_DIR': 'installation.targetDir',
320
+ 'CLAUDE_LOG_LEVEL': 'logging.level',
321
+ 'CLAUDE_SKIP_DEPS': 'dependencies.checkPython',
322
+ 'CLAUDE_REPO_BRANCH': 'repository.branch'
323
+ };
324
+
325
+ for (const [envVar, configKey] of Object.entries(envMappings)) {
326
+ const value = process.env[envVar];
327
+ if (value !== undefined) {
328
+ // Convert string values appropriately
329
+ let convertedValue = value;
330
+ if (value === 'true') convertedValue = true;
331
+ else if (value === 'false') convertedValue = false;
332
+ else if (/^\d+$/.test(value)) convertedValue = parseInt(value, 10);
333
+
334
+ this.set(configKey, convertedValue);
335
+ }
336
+ }
337
+
338
+ return this;
339
+ }
340
+
341
+ /**
342
+ * Get configuration summary for debugging
343
+ */
344
+ getSummary() {
345
+ return {
346
+ loaded: this.loaded,
347
+ configPath: this.configPath,
348
+ repository: `${this.config.repository.owner}/${this.config.repository.name}@${this.config.repository.branch}`,
349
+ targetDir: this.config.installation.targetDir,
350
+ logLevel: this.config.logging.level,
351
+ hasGitHubToken: !!this.config.github.token,
352
+ differences: Object.keys(this.getDifferences()).length
353
+ };
354
+ }
355
+
356
+ /**
357
+ * Create default configuration file template
358
+ */
359
+ static createTemplate(filePath) {
360
+ const template = {
361
+ $schema: 'https://raw.githubusercontent.com/alexsavage/claude-code-hooks-multi-agent-observability/main/packages/setup-installer/config-schema.json',
362
+ repository: {
363
+ branch: 'main'
364
+ },
365
+ installation: {
366
+ createBackup: true,
367
+ overwriteExisting: false
368
+ },
369
+ github: {
370
+ token: '${GITHUB_TOKEN}'
371
+ },
372
+ logging: {
373
+ level: 'INFO'
374
+ }
375
+ };
376
+
377
+ return {
378
+ path: filePath,
379
+ content: JSON.stringify(template, null, 2),
380
+ instructions: [
381
+ 'Edit this file to customize the installation',
382
+ 'Set GITHUB_TOKEN environment variable for API rate limits',
383
+ 'Use \'auto\' for platform-specific defaults',
384
+ 'Remove unused sections to keep defaults'
385
+ ]
386
+ };
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Create a configured instance with common patterns
392
+ */
393
+ function createConfig(options = {}) {
394
+ // Determine config file path
395
+ let configPath = options.configPath;
396
+
397
+ if (!configPath && options.useGlobal) {
398
+ configPath = path.join(platform.getConfigDir(), 'installer.json');
399
+ } else if (!configPath && options.useLocal) {
400
+ configPath = path.join(process.cwd(), '.claude-installer.json');
401
+ }
402
+
403
+ const config = new Config({ configPath });
404
+
405
+ // Apply environment overrides if requested
406
+ if (options.useEnvironment !== false) {
407
+ config.applyEnvironmentOverrides();
408
+ }
409
+
410
+ return config;
411
+ }
412
+
413
+ /**
414
+ * Quick access to common configuration patterns
415
+ */
416
+ const configUtils = {
417
+ // Create configurations
418
+ create: (options) => createConfig(options),
419
+ createGlobal: () => createConfig({ useGlobal: true }),
420
+ createLocal: () => createConfig({ useLocal: true }),
421
+ createMemory: () => new Config(),
422
+
423
+ // Template utilities
424
+ createTemplate: (filePath) => Config.createTemplate(filePath),
425
+
426
+ // Direct access to Config class
427
+ Config
428
+ };
429
+
430
+ module.exports = configUtils;