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.
- package/CHANGELOG.md +53 -0
- package/README.md +64 -23
- package/bin/claude-setup.js +118 -38
- package/package.json +65 -18
- 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/.claude/hooks/comms/messages_UmaMatrix.json +0 -10
- package/.claude/hooks/comms/messages_VictorSpiral.json +0 -10
- package/.claude/hooks/comms/messages_WendyQuantum.json +0 -10
- package/.claude/hooks/comms/messages_XavierPrism.json +0 -10
- package/templates/get_unread_messages.py +0 -54
- package/templates/send_message.py +0 -75
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub API client for fetching Claude setup files
|
|
3
|
+
* Implements the multi-strategy approach from architecture design
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fetch = require('node-fetch');
|
|
7
|
+
const { EventEmitter } = require('events');
|
|
8
|
+
const { GitHubAPIError } = require('../utils/errors');
|
|
9
|
+
|
|
10
|
+
class GitHubFetcher extends EventEmitter {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
super();
|
|
13
|
+
|
|
14
|
+
this.repository = options.repository || {
|
|
15
|
+
owner: 'screwyforcepush',
|
|
16
|
+
repo: 'claude-code-subagent-bus',
|
|
17
|
+
branch: 'main',
|
|
18
|
+
paths: {
|
|
19
|
+
claudeDir: '.claude',
|
|
20
|
+
claudeMd: 'CLAUDE.md'
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
this.config = {
|
|
25
|
+
baseUrl: 'https://api.github.com',
|
|
26
|
+
rawUrl: 'https://raw.githubusercontent.com',
|
|
27
|
+
timeout: options.timeout || 10000,
|
|
28
|
+
retryCount: options.retryCount || 3,
|
|
29
|
+
retryDelay: options.retryDelay || 1000,
|
|
30
|
+
maxRetryDelay: options.maxRetryDelay || 10000,
|
|
31
|
+
userAgent: options.userAgent || '@claude-code/setup-installer/1.0.0',
|
|
32
|
+
token: options.token || process.env.GITHUB_TOKEN
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
this.rateLimitReset = null;
|
|
36
|
+
this.requestCount = 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Fetch repository files according to FetcherAPI interface
|
|
41
|
+
* @param {string} path - Directory path to fetch
|
|
42
|
+
* @param {Object} options - Fetch options
|
|
43
|
+
* @returns {Promise<FileTree>} Directory structure
|
|
44
|
+
*/
|
|
45
|
+
async fetchDirectory(path, options = {}) {
|
|
46
|
+
const fetchOptions = this._prepareFetchOptions(options);
|
|
47
|
+
|
|
48
|
+
this.emit('progress', {
|
|
49
|
+
task: 'fetch_directory',
|
|
50
|
+
current: 0,
|
|
51
|
+
message: `Fetching directory: ${path}`,
|
|
52
|
+
status: 'running'
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// Strategy 1: GitHub Trees API (fastest for recursive fetch)
|
|
57
|
+
const result = await this._fetchWithTrees(path, fetchOptions);
|
|
58
|
+
|
|
59
|
+
this.emit('progress', {
|
|
60
|
+
task: 'fetch_directory',
|
|
61
|
+
current: 100,
|
|
62
|
+
message: `Successfully fetched ${path}`,
|
|
63
|
+
status: 'complete'
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return result;
|
|
67
|
+
} catch (treesError) {
|
|
68
|
+
console.warn(`Trees API failed: ${treesError.message}`);
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
// Strategy 2: GitHub Contents API (fallback)
|
|
72
|
+
const result = await this._fetchWithContents(path, fetchOptions);
|
|
73
|
+
|
|
74
|
+
this.emit('progress', {
|
|
75
|
+
task: 'fetch_directory',
|
|
76
|
+
current: 100,
|
|
77
|
+
message: `Successfully fetched ${path} via Contents API`,
|
|
78
|
+
status: 'complete'
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return result;
|
|
82
|
+
} catch (contentsError) {
|
|
83
|
+
console.warn(`Contents API failed: ${contentsError.message}`);
|
|
84
|
+
|
|
85
|
+
// Strategy 3: Raw URLs (last resort)
|
|
86
|
+
const result = await this._fetchWithRawUrls(path, fetchOptions);
|
|
87
|
+
|
|
88
|
+
this.emit('progress', {
|
|
89
|
+
task: 'fetch_directory',
|
|
90
|
+
current: 100,
|
|
91
|
+
message: `Successfully fetched ${path} via raw URLs`,
|
|
92
|
+
status: 'complete'
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Fetch individual file according to FetcherAPI interface
|
|
102
|
+
* @param {string} path - File path to fetch
|
|
103
|
+
* @param {Object} options - Fetch options
|
|
104
|
+
* @returns {Promise<FileContent>} File content
|
|
105
|
+
*/
|
|
106
|
+
async fetchFile(path, options = {}) {
|
|
107
|
+
const fetchOptions = this._prepareFetchOptions(options);
|
|
108
|
+
|
|
109
|
+
this.emit('progress', {
|
|
110
|
+
task: 'fetch_file',
|
|
111
|
+
current: 0,
|
|
112
|
+
message: `Fetching file: ${path}`,
|
|
113
|
+
status: 'running'
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
// Try Contents API first for single files
|
|
118
|
+
const result = await this._fetchSingleFileContent(path, fetchOptions);
|
|
119
|
+
|
|
120
|
+
this.emit('progress', {
|
|
121
|
+
task: 'fetch_file',
|
|
122
|
+
current: 100,
|
|
123
|
+
message: `Successfully fetched ${path}`,
|
|
124
|
+
status: 'complete'
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return result;
|
|
128
|
+
} catch (contentsError) {
|
|
129
|
+
console.warn(`Contents API failed for ${path}: ${contentsError.message}`);
|
|
130
|
+
|
|
131
|
+
// Fallback to raw URL
|
|
132
|
+
const result = await this._fetchSingleFileRaw(path, fetchOptions);
|
|
133
|
+
|
|
134
|
+
this.emit('progress', {
|
|
135
|
+
task: 'fetch_file',
|
|
136
|
+
current: 100,
|
|
137
|
+
message: `Successfully fetched ${path} via raw URL`,
|
|
138
|
+
status: 'complete'
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Validate GitHub connection
|
|
147
|
+
* @returns {Promise<boolean>} Connection status
|
|
148
|
+
*/
|
|
149
|
+
async validateConnection() {
|
|
150
|
+
try {
|
|
151
|
+
const url = `${this.config.baseUrl}/repos/${this.repository.owner}/${this.repository.repo}`;
|
|
152
|
+
const response = await this._makeRequest(url, { method: 'HEAD' });
|
|
153
|
+
return response.status === 200;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.warn(`GitHub connection validation failed: ${error.message}`);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Strategy 1: GitHub Trees API for recursive directory fetch
|
|
162
|
+
* @private
|
|
163
|
+
*/
|
|
164
|
+
async _fetchWithTrees(path, options) {
|
|
165
|
+
const { version } = options;
|
|
166
|
+
const url = `${this.config.baseUrl}/repos/${this.repository.owner}/${this.repository.repo}/git/trees/${version}?recursive=1`;
|
|
167
|
+
|
|
168
|
+
const response = await this._makeRequest(url);
|
|
169
|
+
const data = await response.json();
|
|
170
|
+
|
|
171
|
+
if (data.truncated) {
|
|
172
|
+
throw new GitHubAPIError(
|
|
173
|
+
'Repository tree is too large for Trees API',
|
|
174
|
+
{ url, truncated: true }
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Filter and structure tree data
|
|
179
|
+
const claudeFiles = data.tree.filter(item => {
|
|
180
|
+
// Include files in the requested path or exact match for root files
|
|
181
|
+
const isInPath = item.path.startsWith(path + '/') || item.path === path;
|
|
182
|
+
const isBlob = item.type === 'blob';
|
|
183
|
+
const shouldSkip = this._shouldSkipFile(item.path.split('/').pop()) ||
|
|
184
|
+
this._shouldSkipDirectory(item.path.split('/').pop());
|
|
185
|
+
|
|
186
|
+
return isInPath && isBlob && !shouldSkip;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Fetch file contents in parallel batches
|
|
190
|
+
const files = await this._fetchFilesBatch(claudeFiles, options);
|
|
191
|
+
|
|
192
|
+
return this._buildFileTree(path, files);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Strategy 2: GitHub Contents API for directory fetch
|
|
197
|
+
* @private
|
|
198
|
+
*/
|
|
199
|
+
async _fetchWithContents(path, options) {
|
|
200
|
+
const { version } = options;
|
|
201
|
+
const url = `${this.config.baseUrl}/repos/${this.repository.owner}/${this.repository.repo}/contents/${path}?ref=${version}`;
|
|
202
|
+
|
|
203
|
+
const response = await this._makeRequest(url);
|
|
204
|
+
const data = await response.json();
|
|
205
|
+
|
|
206
|
+
if (!Array.isArray(data)) {
|
|
207
|
+
throw new GitHubAPIError(
|
|
208
|
+
`Expected directory but got file: ${path}`,
|
|
209
|
+
{ path, type: data.type }
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const files = [];
|
|
214
|
+
|
|
215
|
+
// Process each item in directory
|
|
216
|
+
for (const item of data) {
|
|
217
|
+
if (item.type === 'file') {
|
|
218
|
+
// Skip binary files and cache files
|
|
219
|
+
if (!this._shouldSkipFile(item.name)) {
|
|
220
|
+
try {
|
|
221
|
+
const content = await this._fetchSingleFileContent(item.path, options);
|
|
222
|
+
files.push(content);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.warn(`Failed to fetch file ${item.path}: ${error.message}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} else if (item.type === 'dir') {
|
|
228
|
+
// Skip __pycache__ and other generated directories
|
|
229
|
+
if (!this._shouldSkipDirectory(item.name)) {
|
|
230
|
+
try {
|
|
231
|
+
// Recursively fetch subdirectories
|
|
232
|
+
const subTree = await this._fetchWithContents(item.path, options);
|
|
233
|
+
files.push(...this._flattenFileTree(subTree));
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.warn(`Failed to fetch directory ${item.path}: ${error.message}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return this._buildFileTree(path, files);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Strategy 3: Raw URLs for direct file access
|
|
246
|
+
* @private
|
|
247
|
+
*/
|
|
248
|
+
async _fetchWithRawUrls(path, options) {
|
|
249
|
+
// Complete .claude directory structure based on actual repository
|
|
250
|
+
const knownFiles = [
|
|
251
|
+
// Root configuration
|
|
252
|
+
'.claude/settings.json',
|
|
253
|
+
'.claude/settings.local.json',
|
|
254
|
+
|
|
255
|
+
// Core agents
|
|
256
|
+
'.claude/agents/core/agent-orchestrator.md',
|
|
257
|
+
'.claude/agents/core/architect.md',
|
|
258
|
+
'.claude/agents/core/business-analyst.md',
|
|
259
|
+
'.claude/agents/core/deep-researcher.md',
|
|
260
|
+
'.claude/agents/core/designer.md',
|
|
261
|
+
'.claude/agents/core/engineer.md',
|
|
262
|
+
'.claude/agents/core/gatekeeper.md',
|
|
263
|
+
'.claude/agents/core/planner.md',
|
|
264
|
+
'.claude/agents/meta-agent-builder.md',
|
|
265
|
+
|
|
266
|
+
// Commands
|
|
267
|
+
'.claude/commands/cook.md',
|
|
268
|
+
'.claude/commands/new-agents.md',
|
|
269
|
+
|
|
270
|
+
// Communication hooks
|
|
271
|
+
'.claude/hooks/comms/get_unread_messages.py',
|
|
272
|
+
'.claude/hooks/comms/register_subagent.py',
|
|
273
|
+
'.claude/hooks/comms/send_message.py',
|
|
274
|
+
'.claude/hooks/comms/update_subagent_completion.py',
|
|
275
|
+
|
|
276
|
+
// Context hooks
|
|
277
|
+
'.claude/hooks/context/repo_map.py',
|
|
278
|
+
|
|
279
|
+
// Observability hooks
|
|
280
|
+
'.claude/hooks/observability/send_event.py',
|
|
281
|
+
|
|
282
|
+
// Root CLAUDE.md
|
|
283
|
+
'CLAUDE.md'
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
const files = [];
|
|
287
|
+
|
|
288
|
+
for (const filePath of knownFiles) {
|
|
289
|
+
if (filePath.startsWith(path) || (path === '.claude' && filePath.startsWith('.claude')) || (path === 'CLAUDE.md' && filePath === 'CLAUDE.md')) {
|
|
290
|
+
try {
|
|
291
|
+
const content = await this._fetchSingleFileRaw(filePath, options);
|
|
292
|
+
files.push(content);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
// Continue on individual file failures for raw URL strategy
|
|
295
|
+
console.warn(`Failed to fetch ${filePath}: ${error.message}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (files.length === 0) {
|
|
301
|
+
throw new GitHubAPIError(
|
|
302
|
+
`No files found using raw URL strategy for path: ${path}`,
|
|
303
|
+
{ path, strategy: 'raw_urls' }
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return this._buildFileTree(path, files);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Fetch single file content via Contents API
|
|
312
|
+
* @private
|
|
313
|
+
*/
|
|
314
|
+
async _fetchSingleFileContent(filePath, options) {
|
|
315
|
+
const { version } = options;
|
|
316
|
+
const url = `${this.config.baseUrl}/repos/${this.repository.owner}/${this.repository.repo}/contents/${filePath}?ref=${version}`;
|
|
317
|
+
|
|
318
|
+
const response = await this._makeRequest(url);
|
|
319
|
+
const data = await response.json();
|
|
320
|
+
|
|
321
|
+
if (data.type !== 'file') {
|
|
322
|
+
throw new GitHubAPIError(
|
|
323
|
+
`Expected file but got ${data.type}: ${filePath}`,
|
|
324
|
+
{ filePath, type: data.type }
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
path: filePath,
|
|
330
|
+
content: data.encoding === 'base64' ?
|
|
331
|
+
Buffer.from(data.content, 'base64').toString('utf-8') :
|
|
332
|
+
data.content,
|
|
333
|
+
encoding: 'utf-8',
|
|
334
|
+
sha: data.sha
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Fetch single file via raw URL
|
|
340
|
+
* @private
|
|
341
|
+
*/
|
|
342
|
+
async _fetchSingleFileRaw(filePath, options) {
|
|
343
|
+
const { version } = options;
|
|
344
|
+
const url = `${this.config.rawUrl}/${this.repository.owner}/${this.repository.repo}/${version}/${filePath}`;
|
|
345
|
+
|
|
346
|
+
const response = await this._makeRequest(url);
|
|
347
|
+
const content = await response.text();
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
path: filePath,
|
|
351
|
+
content: content,
|
|
352
|
+
encoding: 'utf-8',
|
|
353
|
+
sha: null // Not available for raw URLs
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Fetch multiple files in parallel batches
|
|
359
|
+
* @private
|
|
360
|
+
*/
|
|
361
|
+
async _fetchFilesBatch(fileItems, options, batchSize = 5) {
|
|
362
|
+
const files = [];
|
|
363
|
+
|
|
364
|
+
for (let i = 0; i < fileItems.length; i += batchSize) {
|
|
365
|
+
const batch = fileItems.slice(i, i + batchSize);
|
|
366
|
+
|
|
367
|
+
const batchPromises = batch.map(async (item) => {
|
|
368
|
+
try {
|
|
369
|
+
return await this._fetchSingleFileContent(item.path, options);
|
|
370
|
+
} catch (error) {
|
|
371
|
+
console.warn(`Failed to fetch ${item.path}: ${error.message}`);
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const batchResults = await Promise.all(batchPromises);
|
|
377
|
+
files.push(...batchResults.filter(Boolean));
|
|
378
|
+
|
|
379
|
+
// Update progress
|
|
380
|
+
this.emit('progress', {
|
|
381
|
+
task: 'fetch_directory',
|
|
382
|
+
current: Math.min(100, Math.round((i + batchSize) / fileItems.length * 80)), // 80% for fetching
|
|
383
|
+
message: `Fetched ${Math.min(i + batchSize, fileItems.length)}/${fileItems.length} files`,
|
|
384
|
+
status: 'running'
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Rate limiting delay between batches
|
|
388
|
+
if (i + batchSize < fileItems.length) {
|
|
389
|
+
await this._rateLimitDelay();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return files;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Build file tree structure
|
|
398
|
+
* @private
|
|
399
|
+
*/
|
|
400
|
+
_buildFileTree(rootPath, files) {
|
|
401
|
+
const tree = {
|
|
402
|
+
path: rootPath,
|
|
403
|
+
type: 'dir',
|
|
404
|
+
children: [],
|
|
405
|
+
files: files
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// Add file tree structure for compatibility
|
|
409
|
+
const filePaths = files.map(f => f.path);
|
|
410
|
+
tree.children = this._buildTreeStructure(filePaths, rootPath);
|
|
411
|
+
|
|
412
|
+
return tree;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Build hierarchical tree structure
|
|
417
|
+
* @private
|
|
418
|
+
*/
|
|
419
|
+
_buildTreeStructure(filePaths, rootPath) {
|
|
420
|
+
const tree = [];
|
|
421
|
+
const pathMap = new Map();
|
|
422
|
+
|
|
423
|
+
filePaths.forEach(filePath => {
|
|
424
|
+
const relativePath = filePath.startsWith(rootPath) ?
|
|
425
|
+
filePath.slice(rootPath.length + 1) : filePath;
|
|
426
|
+
|
|
427
|
+
const parts = relativePath.split('/');
|
|
428
|
+
let currentPath = rootPath;
|
|
429
|
+
|
|
430
|
+
parts.forEach((part, index) => {
|
|
431
|
+
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
|
432
|
+
|
|
433
|
+
if (!pathMap.has(currentPath)) {
|
|
434
|
+
const node = {
|
|
435
|
+
path: currentPath,
|
|
436
|
+
type: index === parts.length - 1 ? 'file' : 'dir',
|
|
437
|
+
sha: null
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
pathMap.set(currentPath, node);
|
|
441
|
+
|
|
442
|
+
if (index === 0) {
|
|
443
|
+
tree.push(node);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
return tree;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Flatten file tree to file list
|
|
454
|
+
* @private
|
|
455
|
+
*/
|
|
456
|
+
_flattenFileTree(tree) {
|
|
457
|
+
if (tree.files) {
|
|
458
|
+
return tree.files;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const files = [];
|
|
462
|
+
if (tree.children) {
|
|
463
|
+
tree.children.forEach(child => {
|
|
464
|
+
if (child.type === 'file') {
|
|
465
|
+
files.push(child);
|
|
466
|
+
} else {
|
|
467
|
+
files.push(...this._flattenFileTree(child));
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return files;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Make HTTP request with retry logic and rate limiting
|
|
477
|
+
* @private
|
|
478
|
+
*/
|
|
479
|
+
async _makeRequest(url, options = {}) {
|
|
480
|
+
const requestOptions = {
|
|
481
|
+
timeout: this.config.timeout,
|
|
482
|
+
headers: {
|
|
483
|
+
'User-Agent': this.config.userAgent,
|
|
484
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
485
|
+
...options.headers
|
|
486
|
+
},
|
|
487
|
+
...options
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
if (this.config.token) {
|
|
491
|
+
requestOptions.headers['Authorization'] = `token ${this.config.token}`;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return await this._executeWithRetry(async () => {
|
|
495
|
+
// Check rate limit before request
|
|
496
|
+
await this._checkRateLimit();
|
|
497
|
+
|
|
498
|
+
this.requestCount++;
|
|
499
|
+
const response = await fetch(url, requestOptions);
|
|
500
|
+
|
|
501
|
+
// Handle rate limiting
|
|
502
|
+
if (response.status === 403) {
|
|
503
|
+
const rateLimitReset = response.headers.get('x-ratelimit-reset');
|
|
504
|
+
if (rateLimitReset) {
|
|
505
|
+
this.rateLimitReset = parseInt(rateLimitReset) * 1000;
|
|
506
|
+
throw new GitHubAPIError(
|
|
507
|
+
'GitHub API rate limit exceeded',
|
|
508
|
+
{ resetTime: this.rateLimitReset, status: 403, headers: response.headers }
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (!response.ok) {
|
|
514
|
+
throw new GitHubAPIError(
|
|
515
|
+
`GitHub API request failed: ${response.status} ${response.statusText}`,
|
|
516
|
+
{ url, status: response.status, statusText: response.statusText }
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return response;
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Execute function with exponential backoff retry
|
|
526
|
+
* @private
|
|
527
|
+
*/
|
|
528
|
+
async _executeWithRetry(fn, attempt = 1) {
|
|
529
|
+
try {
|
|
530
|
+
return await fn();
|
|
531
|
+
} catch (error) {
|
|
532
|
+
if (attempt >= this.config.retryCount) {
|
|
533
|
+
throw error;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (error.code === 'GITHUB_RATE_LIMIT') {
|
|
537
|
+
// Wait for rate limit reset
|
|
538
|
+
const delay = this.rateLimitReset ?
|
|
539
|
+
Math.max(this.rateLimitReset - Date.now(), 0) :
|
|
540
|
+
this._calculateRetryDelay(attempt);
|
|
541
|
+
|
|
542
|
+
console.warn(`Rate limited, waiting ${delay}ms before retry ${attempt + 1}/${this.config.retryCount}`);
|
|
543
|
+
await this._sleep(delay);
|
|
544
|
+
} else {
|
|
545
|
+
// Exponential backoff for other errors
|
|
546
|
+
const delay = this._calculateRetryDelay(attempt);
|
|
547
|
+
console.warn(`Request failed, retrying in ${delay}ms (attempt ${attempt + 1}/${this.config.retryCount}): ${error.message}`);
|
|
548
|
+
await this._sleep(delay);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return this._executeWithRetry(fn, attempt + 1);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Calculate exponential backoff delay
|
|
557
|
+
* @private
|
|
558
|
+
*/
|
|
559
|
+
_calculateRetryDelay(attempt) {
|
|
560
|
+
const baseDelay = this.config.retryDelay * Math.pow(2, attempt - 1);
|
|
561
|
+
const jitter = Math.random() * 0.1 * baseDelay; // 10% jitter
|
|
562
|
+
return Math.min(baseDelay + jitter, this.config.maxRetryDelay);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Check and wait for rate limit if necessary
|
|
567
|
+
* @private
|
|
568
|
+
*/
|
|
569
|
+
async _checkRateLimit() {
|
|
570
|
+
if (this.rateLimitReset && Date.now() < this.rateLimitReset) {
|
|
571
|
+
const delay = this.rateLimitReset - Date.now();
|
|
572
|
+
console.warn(`Rate limit active, waiting ${delay}ms`);
|
|
573
|
+
await this._sleep(delay);
|
|
574
|
+
this.rateLimitReset = null;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Rate limiting delay between requests
|
|
580
|
+
* @private
|
|
581
|
+
*/
|
|
582
|
+
async _rateLimitDelay() {
|
|
583
|
+
// GitHub allows 5000 requests/hour for authenticated users
|
|
584
|
+
// Be conservative with 1 request per 100ms
|
|
585
|
+
await this._sleep(100);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Sleep utility
|
|
590
|
+
* @private
|
|
591
|
+
*/
|
|
592
|
+
_sleep(ms) {
|
|
593
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Prepare fetch options with defaults
|
|
598
|
+
* @private
|
|
599
|
+
*/
|
|
600
|
+
_prepareFetchOptions(options) {
|
|
601
|
+
return {
|
|
602
|
+
version: options.version || this.repository.branch,
|
|
603
|
+
useCache: options.useCache || false,
|
|
604
|
+
retryCount: options.retryCount || this.config.retryCount,
|
|
605
|
+
timeout: options.timeout || this.config.timeout
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Check if file should be skipped during fetch
|
|
611
|
+
* @private
|
|
612
|
+
*/
|
|
613
|
+
_shouldSkipFile(filename) {
|
|
614
|
+
const skipPatterns = [
|
|
615
|
+
/\.pyc$/, // Python bytecode
|
|
616
|
+
/\.pyo$/, // Python optimized bytecode
|
|
617
|
+
/\.pyd$/, // Python extension modules
|
|
618
|
+
/\.DS_Store$/, // macOS metadata
|
|
619
|
+
/Thumbs\.db$/, // Windows thumbnails
|
|
620
|
+
/\.git/, // Git files
|
|
621
|
+
/\.svn/, // SVN files
|
|
622
|
+
/~$/, // Backup files
|
|
623
|
+
/\.tmp$/, // Temporary files
|
|
624
|
+
/\.temp$/ // Temporary files
|
|
625
|
+
];
|
|
626
|
+
|
|
627
|
+
return skipPatterns.some(pattern => pattern.test(filename));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Check if directory should be skipped during fetch
|
|
632
|
+
* @private
|
|
633
|
+
*/
|
|
634
|
+
_shouldSkipDirectory(dirname) {
|
|
635
|
+
const skipDirs = [
|
|
636
|
+
'__pycache__', // Python cache
|
|
637
|
+
'.git', // Git directory
|
|
638
|
+
'.svn', // SVN directory
|
|
639
|
+
'node_modules', // Node.js modules
|
|
640
|
+
'.vscode', // VS Code settings
|
|
641
|
+
'.idea', // IntelliJ settings
|
|
642
|
+
'.DS_Store', // macOS metadata
|
|
643
|
+
'Thumbs.db' // Windows thumbnails
|
|
644
|
+
];
|
|
645
|
+
|
|
646
|
+
return skipDirs.includes(dirname);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Main function for backwards compatibility
|
|
652
|
+
* @param {string} gitRef - Git reference (branch/tag/commit)
|
|
653
|
+
* @param {Object} options - Additional options
|
|
654
|
+
* @returns {Promise<Object>} Repository files
|
|
655
|
+
*/
|
|
656
|
+
async function fetchRepository(gitRef = 'main', options = {}) {
|
|
657
|
+
const fetcher = new GitHubFetcher(options);
|
|
658
|
+
|
|
659
|
+
const [claudeDir, claudeMd] = await Promise.all([
|
|
660
|
+
fetcher.fetchDirectory('.claude', { version: gitRef }),
|
|
661
|
+
fetcher.fetchFile('CLAUDE.md', { version: gitRef })
|
|
662
|
+
]);
|
|
663
|
+
|
|
664
|
+
return {
|
|
665
|
+
claudeDirectory: claudeDir,
|
|
666
|
+
claudeFile: claudeMd,
|
|
667
|
+
files: [...(claudeDir.files || []), claudeMd]
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
module.exports = {
|
|
672
|
+
GitHubFetcher,
|
|
673
|
+
fetchRepository,
|
|
674
|
+
// Backwards compatibility
|
|
675
|
+
fetchClaudeSetup: fetchRepository
|
|
676
|
+
};
|