claude-comms 1.0.2 → 1.0.4
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 +78 -37
- package/package.json +4 -3
- package/src/fetcher/github.js +437 -21
- package/src/orchestrator/installer.js +27 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,53 +1,94 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
All notable changes to
|
|
3
|
+
All notable changes to the `@claude-code/setup-installer` package will be documented in this file.
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [1.0.
|
|
8
|
+
## [1.0.3] - 2025-01-15
|
|
9
9
|
|
|
10
|
-
###
|
|
11
|
-
- **BREAKING**: Replaced mock implementations with real GitHubFetcher and FileWriter
|
|
12
|
-
- Integrated FrankPulse's path handling fixes for cross-platform compatibility
|
|
13
|
-
- Fixed dependency injection in `_createDependencies()` to use real implementations
|
|
14
|
-
- Enhanced installation flow to properly fetch from GitHub repository
|
|
15
|
-
- Fixed API interface compatibility between orchestrator and real modules
|
|
10
|
+
### 🚀 Major Performance Improvements
|
|
16
11
|
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
- End-to-end installation testing framework
|
|
12
|
+
#### GitHub API Rate Limiting Fix
|
|
13
|
+
- **CRITICAL FIX**: Implemented GitHub tarball fetching to reduce API requests from 21+ individual calls to 1 single tarball download
|
|
14
|
+
- **Performance**: Installation now completes in <10 seconds (previously up to 575+ seconds due to rate limiting)
|
|
15
|
+
- **Reliability**: Eliminates GitHub API rate limit issues for most users
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
17
|
+
#### Retry Logic Improvements
|
|
18
|
+
- **Rate Limit Handling**: Capped retry delays at maximum 30 seconds (previously unlimited)
|
|
19
|
+
- **Fail-Fast**: Automatically fails installations with >5 minute rate limit waits, suggesting GitHub token usage
|
|
20
|
+
- **User Experience**: Added countdown display for rate limit waits >5 seconds
|
|
21
|
+
- **Smart Delays**: Improved exponential backoff with reasonable caps
|
|
27
22
|
|
|
28
|
-
###
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
- All 21+ files from .claude directory structure are now correctly installed
|
|
23
|
+
### ✨ New Features
|
|
24
|
+
- **Tarball Extraction**: New `fetchAsTarball()` method for optimal GitHub repository fetching
|
|
25
|
+
- **Fallback Strategy**: Maintains compatibility with existing API methods as fallback
|
|
26
|
+
- **Progress Tracking**: Enhanced progress indicators during tarball download and extraction
|
|
33
27
|
|
|
34
|
-
|
|
28
|
+
### 🔧 Technical Improvements
|
|
29
|
+
- **Single Request**: Tarball approach uses 1 GitHub API request vs 21+ individual file requests
|
|
30
|
+
- **File Structure**: Complete `.claude` directory and `CLAUDE.md` extraction from tarball
|
|
31
|
+
- **Error Handling**: Better error messages with actionable suggestions (GitHub token setup)
|
|
32
|
+
- **Memory Efficiency**: Temporary file extraction with automatic cleanup
|
|
35
33
|
|
|
36
|
-
###
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
34
|
+
### 🐛 Bug Fixes
|
|
35
|
+
- Fixed CLAUDE.md path handling in tarball extraction
|
|
36
|
+
- Resolved rate limiting countdown display
|
|
37
|
+
- Improved error messaging for network failures
|
|
38
|
+
- Fixed retry delay calculations exceeding maximum thresholds
|
|
41
39
|
|
|
42
|
-
###
|
|
43
|
-
-
|
|
44
|
-
-
|
|
40
|
+
### 🛡️ Security & Reliability
|
|
41
|
+
- Enhanced input validation for GitHub API responses
|
|
42
|
+
- Secure temporary file handling during tarball extraction
|
|
43
|
+
- Improved error boundary handling for network failures
|
|
44
|
+
- Rate limit detection and graceful degradation
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
### 📋 Testing
|
|
47
|
+
- Added comprehensive tarball validation test suite
|
|
48
|
+
- Performance benchmarking for installation timing
|
|
49
|
+
- Retry logic validation with cap testing
|
|
50
|
+
- Cross-platform compatibility verification
|
|
47
51
|
|
|
48
|
-
###
|
|
52
|
+
### 🔄 Migration Guide
|
|
53
|
+
No breaking changes. Existing installations will automatically benefit from:
|
|
54
|
+
- Faster installation times
|
|
55
|
+
- Reduced rate limiting issues
|
|
56
|
+
- Better error messages
|
|
57
|
+
- Enhanced reliability
|
|
58
|
+
|
|
59
|
+
Users experiencing rate limits should set `GITHUB_TOKEN` environment variable for increased API limits (5,000/hour vs 60/hour).
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## [1.0.2] - 2025-01-14
|
|
64
|
+
|
|
65
|
+
### Features
|
|
66
|
+
- Initial NPX installer implementation
|
|
67
|
+
- GitHub API integration with multiple fallback strategies
|
|
68
|
+
- CLI interface with progress indicators
|
|
69
|
+
- Cross-platform support (Windows, macOS, Linux)
|
|
70
|
+
|
|
71
|
+
### Dependencies
|
|
72
|
+
- `commander` for CLI parsing
|
|
73
|
+
- `chalk` for colored output
|
|
74
|
+
- `ora` for progress indicators
|
|
75
|
+
- `node-fetch` for HTTP requests
|
|
76
|
+
- `fs-extra` for file operations
|
|
77
|
+
- `tar` for tarball extraction
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## [1.0.1] - 2025-01-13
|
|
82
|
+
|
|
83
|
+
### Features
|
|
84
|
+
- Core package structure
|
|
85
|
+
- Basic file installation logic
|
|
86
|
+
- GitHub API integration foundation
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## [1.0.0] - 2025-01-12
|
|
91
|
+
|
|
92
|
+
### Features
|
|
49
93
|
- Initial release
|
|
50
|
-
- NPX installer
|
|
51
|
-
- Multi-agent orchestration framework
|
|
52
|
-
- GitHub integration for fetching setup files
|
|
53
|
-
- File writer with backup and rollback capabilities
|
|
94
|
+
- Basic NPX installer prototype
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-comms",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "NPX installer for Claude Code hooks and observability setup",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -69,7 +69,8 @@
|
|
|
69
69
|
"chalk": "^4.1.2",
|
|
70
70
|
"ora": "^5.4.1",
|
|
71
71
|
"node-fetch": "^2.6.11",
|
|
72
|
-
"fs-extra": "^11.1.1"
|
|
72
|
+
"fs-extra": "^11.1.1",
|
|
73
|
+
"tar": "^6.2.0"
|
|
73
74
|
},
|
|
74
75
|
"files": [
|
|
75
76
|
"bin/",
|
|
@@ -81,4 +82,4 @@
|
|
|
81
82
|
"publishConfig": {
|
|
82
83
|
"access": "public"
|
|
83
84
|
}
|
|
84
|
-
}
|
|
85
|
+
}
|
package/src/fetcher/github.js
CHANGED
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const fetch = require('node-fetch');
|
|
7
|
+
const tar = require('tar');
|
|
7
8
|
const { EventEmitter } = require('events');
|
|
8
9
|
const { GitHubAPIError } = require('../utils/errors');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const os = require('os');
|
|
9
13
|
|
|
10
14
|
class GitHubFetcher extends EventEmitter {
|
|
11
15
|
constructor(options = {}) {
|
|
@@ -142,6 +146,55 @@ class GitHubFetcher extends EventEmitter {
|
|
|
142
146
|
}
|
|
143
147
|
}
|
|
144
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Fetch repository as tarball (single request, fastest approach)
|
|
151
|
+
* @param {Object} options - Fetch options
|
|
152
|
+
* @returns {Promise<Object>} Repository files extracted from tarball
|
|
153
|
+
*/
|
|
154
|
+
async fetchAsTarball(options = {}) {
|
|
155
|
+
const fetchOptions = this._prepareFetchOptions(options);
|
|
156
|
+
|
|
157
|
+
this.emit('progress', {
|
|
158
|
+
task: 'fetch_tarball',
|
|
159
|
+
current: 0,
|
|
160
|
+
message: 'Downloading repository tarball...',
|
|
161
|
+
status: 'running'
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// Download tarball from GitHub
|
|
166
|
+
const tarballBuffer = await this._downloadTarball(fetchOptions);
|
|
167
|
+
|
|
168
|
+
this.emit('progress', {
|
|
169
|
+
task: 'fetch_tarball',
|
|
170
|
+
current: 50,
|
|
171
|
+
message: 'Extracting files from tarball...',
|
|
172
|
+
status: 'running'
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Extract and filter files
|
|
176
|
+
const extractedFiles = await this._extractTarballFiles(tarballBuffer, fetchOptions);
|
|
177
|
+
|
|
178
|
+
this.emit('progress', {
|
|
179
|
+
task: 'fetch_tarball',
|
|
180
|
+
current: 100,
|
|
181
|
+
message: `Successfully extracted ${Object.keys(extractedFiles).length} file groups`,
|
|
182
|
+
status: 'complete'
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return extractedFiles;
|
|
186
|
+
|
|
187
|
+
} catch (error) {
|
|
188
|
+
this.emit('progress', {
|
|
189
|
+
task: 'fetch_tarball',
|
|
190
|
+
current: 0,
|
|
191
|
+
message: `Tarball fetch failed: ${error.message}`,
|
|
192
|
+
status: 'failed'
|
|
193
|
+
});
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
145
198
|
/**
|
|
146
199
|
* Validate GitHub connection
|
|
147
200
|
* @returns {Promise<boolean>} Connection status
|
|
@@ -503,10 +556,12 @@ class GitHubFetcher extends EventEmitter {
|
|
|
503
556
|
const rateLimitReset = response.headers.get('x-ratelimit-reset');
|
|
504
557
|
if (rateLimitReset) {
|
|
505
558
|
this.rateLimitReset = parseInt(rateLimitReset) * 1000;
|
|
506
|
-
|
|
559
|
+
const error = new GitHubAPIError(
|
|
507
560
|
'GitHub API rate limit exceeded',
|
|
508
561
|
{ resetTime: this.rateLimitReset, status: 403, headers: response.headers }
|
|
509
562
|
);
|
|
563
|
+
error.code = 'GITHUB_RATE_LIMIT';
|
|
564
|
+
throw error;
|
|
510
565
|
}
|
|
511
566
|
}
|
|
512
567
|
|
|
@@ -534,13 +589,26 @@ class GitHubFetcher extends EventEmitter {
|
|
|
534
589
|
}
|
|
535
590
|
|
|
536
591
|
if (error.code === 'GITHUB_RATE_LIMIT') {
|
|
537
|
-
//
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
592
|
+
// Check if we should fail fast
|
|
593
|
+
this._shouldFailFastOnRateLimit();
|
|
594
|
+
|
|
595
|
+
// Calculate smart rate limit delay
|
|
596
|
+
const delay = this._calculateRateLimitDelay();
|
|
597
|
+
|
|
598
|
+
if (delay > 0) {
|
|
599
|
+
console.warn(`Rate limit exceeded. Waiting ${Math.round(delay / 1000)}s before retry ${attempt + 1}/${this.config.retryCount}`);
|
|
600
|
+
|
|
601
|
+
if (!this.config.token) {
|
|
602
|
+
console.warn('💡 Tip: Set GITHUB_TOKEN environment variable to increase rate limits (5,000/hour vs 60/hour)');
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Show countdown for longer waits
|
|
606
|
+
if (delay > 5000) {
|
|
607
|
+
await this._waitWithCountdown(delay);
|
|
608
|
+
} else {
|
|
609
|
+
await this._sleep(delay);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
544
612
|
} else {
|
|
545
613
|
// Exponential backoff for other errors
|
|
546
614
|
const delay = this._calculateRetryDelay(attempt);
|
|
@@ -562,15 +630,88 @@ class GitHubFetcher extends EventEmitter {
|
|
|
562
630
|
return Math.min(baseDelay + jitter, this.config.maxRetryDelay);
|
|
563
631
|
}
|
|
564
632
|
|
|
633
|
+
/**
|
|
634
|
+
* Calculate smart rate limit delay with reasonable caps
|
|
635
|
+
* @private
|
|
636
|
+
*/
|
|
637
|
+
_calculateRateLimitDelay() {
|
|
638
|
+
if (!this.rateLimitReset) {
|
|
639
|
+
return 0;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const now = Date.now();
|
|
643
|
+
const resetTime = this.rateLimitReset;
|
|
644
|
+
const timeUntilReset = resetTime - now;
|
|
645
|
+
|
|
646
|
+
// If reset time is in the past, no delay needed
|
|
647
|
+
if (timeUntilReset <= 0) {
|
|
648
|
+
this.rateLimitReset = null;
|
|
649
|
+
return 0;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Cap rate limit delays at 30 seconds maximum
|
|
653
|
+
const cappedDelay = Math.min(timeUntilReset, 30000);
|
|
654
|
+
|
|
655
|
+
// If original delay would be over 5 minutes, suggest failing fast
|
|
656
|
+
if (timeUntilReset > 5 * 60 * 1000) {
|
|
657
|
+
throw new GitHubAPIError(
|
|
658
|
+
`Rate limit reset time too far in future (${Math.round(timeUntilReset / 1000)}s). Consider using a GitHub token or try again later.`,
|
|
659
|
+
{
|
|
660
|
+
resetTime: this.rateLimitReset,
|
|
661
|
+
delaySeconds: Math.round(timeUntilReset / 1000),
|
|
662
|
+
suggestion: 'Use GITHUB_TOKEN environment variable to increase rate limits'
|
|
663
|
+
}
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return cappedDelay;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Check if we should fail fast on rate limit
|
|
672
|
+
* @private
|
|
673
|
+
*/
|
|
674
|
+
_shouldFailFastOnRateLimit() {
|
|
675
|
+
if (!this.rateLimitReset) {
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const timeUntilReset = this.rateLimitReset - Date.now();
|
|
680
|
+
|
|
681
|
+
// Fail fast if rate limit won't reset for more than 5 minutes
|
|
682
|
+
if (timeUntilReset > 5 * 60 * 1000) {
|
|
683
|
+
throw new GitHubAPIError(
|
|
684
|
+
`Rate limit reset time too far in future (${Math.round(timeUntilReset / 1000)}s). Installation cancelled.`,
|
|
685
|
+
{
|
|
686
|
+
resetTime: this.rateLimitReset,
|
|
687
|
+
delaySeconds: Math.round(timeUntilReset / 1000),
|
|
688
|
+
suggestion: 'Use GITHUB_TOKEN environment variable or try again later'
|
|
689
|
+
}
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
|
|
565
696
|
/**
|
|
566
697
|
* Check and wait for rate limit if necessary
|
|
567
698
|
* @private
|
|
568
699
|
*/
|
|
569
700
|
async _checkRateLimit() {
|
|
570
701
|
if (this.rateLimitReset && Date.now() < this.rateLimitReset) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
702
|
+
// Use smart rate limit delay calculation
|
|
703
|
+
const delay = this._calculateRateLimitDelay();
|
|
704
|
+
|
|
705
|
+
if (delay > 0) {
|
|
706
|
+
console.warn(`Rate limit active, waiting ${Math.round(delay / 1000)}s`);
|
|
707
|
+
|
|
708
|
+
if (delay > 5000) {
|
|
709
|
+
await this._waitWithCountdown(delay);
|
|
710
|
+
} else {
|
|
711
|
+
await this._sleep(delay);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
574
715
|
this.rateLimitReset = null;
|
|
575
716
|
}
|
|
576
717
|
}
|
|
@@ -593,6 +734,44 @@ class GitHubFetcher extends EventEmitter {
|
|
|
593
734
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
594
735
|
}
|
|
595
736
|
|
|
737
|
+
/**
|
|
738
|
+
* Wait with countdown display for better user experience
|
|
739
|
+
* @private
|
|
740
|
+
*/
|
|
741
|
+
async _waitWithCountdown(totalMs) {
|
|
742
|
+
const intervalMs = 1000; // Update every second
|
|
743
|
+
let remainingMs = totalMs;
|
|
744
|
+
|
|
745
|
+
while (remainingMs > 0) {
|
|
746
|
+
const remainingSeconds = Math.ceil(remainingMs / 1000);
|
|
747
|
+
process.stdout.write(`\r⏳ Rate limit wait: ${remainingSeconds}s remaining...`);
|
|
748
|
+
|
|
749
|
+
const sleepTime = Math.min(intervalMs, remainingMs);
|
|
750
|
+
await this._sleep(sleepTime);
|
|
751
|
+
remainingMs -= sleepTime;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
process.stdout.write('\r✅ Rate limit wait complete.\n');
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Wait for rate limit with user feedback
|
|
759
|
+
* @private
|
|
760
|
+
*/
|
|
761
|
+
async _waitForRateLimit() {
|
|
762
|
+
const delay = this._calculateRateLimitDelay();
|
|
763
|
+
|
|
764
|
+
if (delay > 0) {
|
|
765
|
+
console.warn(`Rate limit active, waiting ${Math.round(delay / 1000)}s`);
|
|
766
|
+
|
|
767
|
+
if (delay > 5000) {
|
|
768
|
+
await this._waitWithCountdown(delay);
|
|
769
|
+
} else {
|
|
770
|
+
await this._sleep(delay);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
596
775
|
/**
|
|
597
776
|
* Prepare fetch options with defaults
|
|
598
777
|
* @private
|
|
@@ -645,6 +824,218 @@ class GitHubFetcher extends EventEmitter {
|
|
|
645
824
|
|
|
646
825
|
return skipDirs.includes(dirname);
|
|
647
826
|
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Download repository tarball from GitHub
|
|
830
|
+
* @private
|
|
831
|
+
*/
|
|
832
|
+
async _downloadTarball(options) {
|
|
833
|
+
const { version } = options;
|
|
834
|
+
const url = `${this.config.baseUrl}/repos/${this.repository.owner}/${this.repository.repo}/tarball/${version}`;
|
|
835
|
+
|
|
836
|
+
const response = await this._makeRequest(url);
|
|
837
|
+
|
|
838
|
+
if (!response.ok) {
|
|
839
|
+
throw new GitHubAPIError(
|
|
840
|
+
`Failed to download tarball: ${response.status} ${response.statusText}`,
|
|
841
|
+
{ url, status: response.status }
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Download tarball as buffer
|
|
846
|
+
const buffer = await response.buffer();
|
|
847
|
+
|
|
848
|
+
if (buffer.length === 0) {
|
|
849
|
+
throw new GitHubAPIError(
|
|
850
|
+
'Downloaded tarball is empty',
|
|
851
|
+
{ url, size: buffer.length }
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
return buffer;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Extract relevant files from tarball buffer
|
|
860
|
+
* @private
|
|
861
|
+
*/
|
|
862
|
+
async _extractTarballFiles(tarballBuffer, _options) {
|
|
863
|
+
return new Promise((resolve, reject) => {
|
|
864
|
+
const extractedFiles = {
|
|
865
|
+
'.claude': {
|
|
866
|
+
path: '.claude',
|
|
867
|
+
type: 'dir',
|
|
868
|
+
children: [],
|
|
869
|
+
files: []
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
const claudeMdFile = {
|
|
874
|
+
path: 'CLAUDE.md',
|
|
875
|
+
content: null,
|
|
876
|
+
encoding: 'utf-8'
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
// Create temporary directory for extraction
|
|
880
|
+
const tempDir = path.join(os.tmpdir(), `claude-extract-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
881
|
+
|
|
882
|
+
// Track files we want to keep
|
|
883
|
+
const wantedFiles = new Map();
|
|
884
|
+
let rootDirName = null;
|
|
885
|
+
|
|
886
|
+
const extractStream = tar.extract({
|
|
887
|
+
cwd: tempDir,
|
|
888
|
+
filter: (filePath, entry) => {
|
|
889
|
+
// Debug logging
|
|
890
|
+
console.log(`DEBUG: Processing ${filePath} (type: ${entry.type})`);
|
|
891
|
+
|
|
892
|
+
// First entry is usually the root directory (e.g., "screwyforcepush-claude-code-subagent-bus-abc123/")
|
|
893
|
+
if (!rootDirName && entry.type === 'Directory') {
|
|
894
|
+
rootDirName = filePath;
|
|
895
|
+
console.log(`DEBUG: Set root directory to: ${rootDirName}`);
|
|
896
|
+
return false; // Don't extract the root dir itself
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Skip if this doesn't belong to our repository root
|
|
900
|
+
if (rootDirName && !filePath.startsWith(rootDirName + '/')) {
|
|
901
|
+
return false;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Remove root directory prefix to get actual file path
|
|
905
|
+
const actualPath = rootDirName ? filePath.slice(rootDirName.length + 1) : filePath;
|
|
906
|
+
|
|
907
|
+
// Only extract files we care about
|
|
908
|
+
const isClaudeFile = actualPath.startsWith('.claude/') || actualPath === '.claude';
|
|
909
|
+
const isClaudeMd = actualPath === 'CLAUDE.md';
|
|
910
|
+
|
|
911
|
+
console.log(`DEBUG: ${actualPath} -> claude: ${isClaudeFile}, md: ${isClaudeMd}, type: ${entry.type}`);
|
|
912
|
+
|
|
913
|
+
if ((isClaudeFile || isClaudeMd) && entry.type === 'File') {
|
|
914
|
+
wantedFiles.set(filePath, actualPath);
|
|
915
|
+
console.log(`DEBUG: Will extract: ${filePath} -> ${actualPath}`);
|
|
916
|
+
return true;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Also allow .claude directories to be processed for their children
|
|
920
|
+
if (isClaudeFile && entry.type === 'Directory') {
|
|
921
|
+
return true;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
extractStream.on('error', (error) => {
|
|
929
|
+
reject(new GitHubAPIError(
|
|
930
|
+
`Tarball extraction failed: ${error.message}`,
|
|
931
|
+
{ error: error.message }
|
|
932
|
+
));
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
extractStream.on('finish', async () => {
|
|
936
|
+
try {
|
|
937
|
+
// Process extracted files
|
|
938
|
+
for (const [extractedPath, actualPath] of wantedFiles) {
|
|
939
|
+
const fullPath = path.join(tempDir, extractedPath);
|
|
940
|
+
|
|
941
|
+
try {
|
|
942
|
+
const stats = await fs.promises.stat(fullPath);
|
|
943
|
+
|
|
944
|
+
if (stats.isFile()) {
|
|
945
|
+
const content = await fs.promises.readFile(fullPath, 'utf-8');
|
|
946
|
+
|
|
947
|
+
if (actualPath === 'CLAUDE.md') {
|
|
948
|
+
claudeMdFile.content = content;
|
|
949
|
+
claudeMdFile.path = 'CLAUDE.md';
|
|
950
|
+
extractedFiles['CLAUDE.md'] = claudeMdFile;
|
|
951
|
+
} else if (actualPath.startsWith('.claude/')) {
|
|
952
|
+
// Add to .claude files
|
|
953
|
+
const relativePath = actualPath.slice('.claude/'.length);
|
|
954
|
+
const fileObj = {
|
|
955
|
+
path: actualPath,
|
|
956
|
+
content: content,
|
|
957
|
+
encoding: 'utf-8'
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
extractedFiles['.claude'].files.push(fileObj);
|
|
961
|
+
|
|
962
|
+
// Build directory structure for compatibility
|
|
963
|
+
this._addToDirectoryStructure(extractedFiles['.claude'], relativePath, fileObj);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
} catch (fileError) {
|
|
967
|
+
// Skip files that can't be read
|
|
968
|
+
console.warn(`Warning: Could not read extracted file ${fullPath}: ${fileError.message}`);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Clean up temp directory
|
|
973
|
+
try {
|
|
974
|
+
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
|
975
|
+
} catch (cleanupError) {
|
|
976
|
+
// Log but don't fail
|
|
977
|
+
console.warn(`Warning: Could not clean up temp directory ${tempDir}: ${cleanupError.message}`);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
resolve(extractedFiles);
|
|
981
|
+
|
|
982
|
+
} catch (processingError) {
|
|
983
|
+
reject(new GitHubAPIError(
|
|
984
|
+
`File processing failed: ${processingError.message}`,
|
|
985
|
+
{ error: processingError.message }
|
|
986
|
+
));
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
// Create temp directory and start extraction
|
|
991
|
+
fs.promises.mkdir(tempDir, { recursive: true })
|
|
992
|
+
.then(() => {
|
|
993
|
+
// Pipe tarball buffer to extraction stream
|
|
994
|
+
const { Readable } = require('stream');
|
|
995
|
+
const readable = new Readable();
|
|
996
|
+
readable.push(tarballBuffer);
|
|
997
|
+
readable.push(null);
|
|
998
|
+
readable.pipe(extractStream);
|
|
999
|
+
})
|
|
1000
|
+
.catch(reject);
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Add file to directory structure for compatibility with existing code
|
|
1006
|
+
* @private
|
|
1007
|
+
*/
|
|
1008
|
+
_addToDirectoryStructure(claudeDir, relativePath, fileObj) {
|
|
1009
|
+
const parts = relativePath.split('/');
|
|
1010
|
+
let currentLevel = claudeDir.children;
|
|
1011
|
+
let currentPath = '.claude';
|
|
1012
|
+
|
|
1013
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1014
|
+
const part = parts[i];
|
|
1015
|
+
currentPath = `${currentPath}/${part}`;
|
|
1016
|
+
|
|
1017
|
+
if (i === parts.length - 1) {
|
|
1018
|
+
// This is the file
|
|
1019
|
+
currentLevel.push({
|
|
1020
|
+
path: currentPath,
|
|
1021
|
+
type: 'file',
|
|
1022
|
+
sha: null
|
|
1023
|
+
});
|
|
1024
|
+
} else {
|
|
1025
|
+
// This is a directory
|
|
1026
|
+
let dirEntry = currentLevel.find(item => item.path === currentPath && item.type === 'dir');
|
|
1027
|
+
if (!dirEntry) {
|
|
1028
|
+
dirEntry = {
|
|
1029
|
+
path: currentPath,
|
|
1030
|
+
type: 'dir',
|
|
1031
|
+
children: []
|
|
1032
|
+
};
|
|
1033
|
+
currentLevel.push(dirEntry);
|
|
1034
|
+
}
|
|
1035
|
+
currentLevel = dirEntry.children;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
648
1039
|
}
|
|
649
1040
|
|
|
650
1041
|
/**
|
|
@@ -656,16 +1047,41 @@ class GitHubFetcher extends EventEmitter {
|
|
|
656
1047
|
async function fetchRepository(gitRef = 'main', options = {}) {
|
|
657
1048
|
const fetcher = new GitHubFetcher(options);
|
|
658
1049
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
1050
|
+
try {
|
|
1051
|
+
// Strategy 1: Tarball approach (fastest, single request)
|
|
1052
|
+
console.log('Attempting tarball fetch strategy...');
|
|
1053
|
+
const tarballFiles = await fetcher.fetchAsTarball({ version: gitRef });
|
|
1054
|
+
|
|
1055
|
+
return {
|
|
1056
|
+
claudeDirectory: tarballFiles['.claude'],
|
|
1057
|
+
claudeFile: tarballFiles['CLAUDE.md'],
|
|
1058
|
+
files: [...(tarballFiles['.claude'].files || []), tarballFiles['CLAUDE.md']]
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
} catch (tarballError) {
|
|
1062
|
+
console.warn(`Tarball fetch failed: ${tarballError.message}`);
|
|
1063
|
+
console.log('Falling back to individual file fetch strategy...');
|
|
1064
|
+
|
|
1065
|
+
try {
|
|
1066
|
+
// Strategy 2: Individual file fetch (fallback)
|
|
1067
|
+
const [claudeDir, claudeMd] = await Promise.all([
|
|
1068
|
+
fetcher.fetchDirectory('.claude', { version: gitRef }),
|
|
1069
|
+
fetcher.fetchFile('CLAUDE.md', { version: gitRef })
|
|
1070
|
+
]);
|
|
1071
|
+
|
|
1072
|
+
return {
|
|
1073
|
+
claudeDirectory: claudeDir,
|
|
1074
|
+
claudeFile: claudeMd,
|
|
1075
|
+
files: [...(claudeDir.files || []), claudeMd]
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
} catch (fallbackError) {
|
|
1079
|
+
console.error('Both tarball and individual fetch strategies failed');
|
|
1080
|
+
throw new Error(
|
|
1081
|
+
`All fetch strategies failed. Tarball: ${tarballError.message}. Individual: ${fallbackError.message}`
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
669
1085
|
}
|
|
670
1086
|
|
|
671
1087
|
module.exports = {
|
|
@@ -241,6 +241,32 @@ class Installer extends EventEmitter {
|
|
|
241
241
|
this.logger.info(`Fetching files from version: ${this.options.version}`);
|
|
242
242
|
|
|
243
243
|
try {
|
|
244
|
+
// Try tarball approach first if fetcher supports it
|
|
245
|
+
if (this.fetcher.fetchAsTarball) {
|
|
246
|
+
this.logger.info('Using optimized tarball fetch strategy');
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const fetchedFiles = await this.fetcher.fetchAsTarball({
|
|
250
|
+
version: this.options.version,
|
|
251
|
+
useCache: this.options.cache,
|
|
252
|
+
retryCount: 3
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Calculate total files for progress tracking
|
|
256
|
+
this.progress.total = this._countFiles(fetchedFiles);
|
|
257
|
+
|
|
258
|
+
this.logger.info(`Fetched ${this.progress.total} files successfully via tarball`);
|
|
259
|
+
return fetchedFiles;
|
|
260
|
+
|
|
261
|
+
} catch (tarballError) {
|
|
262
|
+
this.logger.warn(`Tarball fetch failed: ${tarballError.message}`);
|
|
263
|
+
this.logger.info('Falling back to individual file fetch strategy');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Fallback to individual file fetching
|
|
268
|
+
this.logger.info('Using individual file fetch strategy');
|
|
269
|
+
|
|
244
270
|
// Fetch .claude directory structure
|
|
245
271
|
const claudeFiles = await this.fetcher.fetchDirectory('.claude', {
|
|
246
272
|
version: this.options.version,
|
|
@@ -262,7 +288,7 @@ class Installer extends EventEmitter {
|
|
|
262
288
|
// Calculate total files for progress tracking
|
|
263
289
|
this.progress.total = this._countFiles(fetchedFiles);
|
|
264
290
|
|
|
265
|
-
this.logger.info(`Fetched ${this.progress.total} files successfully`);
|
|
291
|
+
this.logger.info(`Fetched ${this.progress.total} files successfully via individual fetch`);
|
|
266
292
|
return fetchedFiles;
|
|
267
293
|
|
|
268
294
|
} catch (error) {
|