claudekit-cli 1.0.0 → 1.1.0
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/.github/workflows/ci.yml +2 -0
- package/CHANGELOG.md +19 -0
- package/CLAUDE.md +7 -0
- package/README.md +61 -3
- package/biome.json +3 -0
- package/dist/index.js +102 -0
- package/package.json +1 -1
- package/src/commands/version.ts +135 -0
- package/src/index.ts +11 -0
- package/src/types.ts +7 -0
- package/tests/commands/version.test.ts +297 -0
- package/.claude/agents/brainstormer.md +0 -96
- package/.claude/agents/code-reviewer.md +0 -141
- package/.claude/agents/copywriter.md +0 -108
- package/.claude/agents/database-admin.md +0 -86
- package/.claude/agents/debugger.md +0 -124
- package/.claude/agents/docs-manager.md +0 -115
- package/.claude/agents/git-manager.md +0 -60
- package/.claude/agents/journal-writer.md +0 -111
- package/.claude/agents/planner.md +0 -87
- package/.claude/agents/project-manager.md +0 -113
- package/.claude/agents/researcher.md +0 -173
- package/.claude/agents/scout.md +0 -123
- package/.claude/agents/tester.md +0 -95
- package/.claude/agents/ui-ux-designer.md +0 -206
- package/.claude/commands/bootstrap.md +0 -104
- package/.claude/commands/brainstorm.md +0 -67
- package/.claude/commands/content/enhance.md +0 -13
- package/.claude/commands/content/fast.md +0 -11
- package/.claude/commands/content/good.md +0 -13
- package/.claude/commands/cook.md +0 -19
- package/.claude/commands/debug.md +0 -10
- package/.claude/commands/design/3d.md +0 -65
- package/.claude/commands/design/describe.md +0 -13
- package/.claude/commands/design/fast.md +0 -19
- package/.claude/commands/design/good.md +0 -23
- package/.claude/commands/design/screenshot.md +0 -23
- package/.claude/commands/design/video.md +0 -23
- package/.claude/commands/docs/init.md +0 -13
- package/.claude/commands/docs/summarize.md +0 -10
- package/.claude/commands/docs/update.md +0 -21
- package/.claude/commands/fix/ci.md +0 -11
- package/.claude/commands/fix/fast.md +0 -12
- package/.claude/commands/fix/hard.md +0 -18
- package/.claude/commands/fix/logs.md +0 -16
- package/.claude/commands/fix/test.md +0 -18
- package/.claude/commands/fix/types.md +0 -10
- package/.claude/commands/git/cm.md +0 -5
- package/.claude/commands/git/cp.md +0 -4
- package/.claude/commands/integrate/polar.md +0 -42
- package/.claude/commands/plan/ci.md +0 -12
- package/.claude/commands/plan/two.md +0 -13
- package/.claude/commands/plan.md +0 -10
- package/.claude/commands/scout.md +0 -29
- package/.claude/commands/test.md +0 -7
- package/.claude/commands/watzup.md +0 -8
- package/.claude/hooks/telegram_notify.sh +0 -136
- package/.claude/send-discord.sh +0 -64
- package/.claude/settings.json +0 -7
- package/.claude/statusline.sh +0 -143
- package/.claude/workflows/development-rules.md +0 -80
- package/.claude/workflows/documentation-management.md +0 -28
- package/.claude/workflows/orchestration-protocol.md +0 -16
- package/.claude/workflows/primary-workflow.md +0 -41
- package/.opencode/agent/code-reviewer.md +0 -141
- package/.opencode/agent/debugger.md +0 -74
- package/.opencode/agent/docs-manager.md +0 -119
- package/.opencode/agent/git-manager.md +0 -60
- package/.opencode/agent/planner-researcher.md +0 -100
- package/.opencode/agent/planner.md +0 -87
- package/.opencode/agent/project-manager.md +0 -113
- package/.opencode/agent/researcher.md +0 -173
- package/.opencode/agent/solution-brainstormer.md +0 -89
- package/.opencode/agent/system-architecture.md +0 -192
- package/.opencode/agent/tester.md +0 -96
- package/.opencode/agent/ui-ux-designer.md +0 -203
- package/.opencode/agent/ui-ux-developer.md +0 -97
- package/.opencode/command/cook.md +0 -7
- package/.opencode/command/debug.md +0 -10
- package/.opencode/command/design/3d.md +0 -65
- package/.opencode/command/design/fast.md +0 -18
- package/.opencode/command/design/good.md +0 -21
- package/.opencode/command/design/screenshot.md +0 -22
- package/.opencode/command/design/video.md +0 -22
- package/.opencode/command/docs/init.md +0 -11
- package/.opencode/command/docs/summarize.md +0 -10
- package/.opencode/command/docs/update.md +0 -18
- package/.opencode/command/fix/ci.md +0 -8
- package/.opencode/command/fix/fast.md +0 -11
- package/.opencode/command/fix/hard.md +0 -15
- package/.opencode/command/fix/logs.md +0 -16
- package/.opencode/command/fix/test.md +0 -18
- package/.opencode/command/fix/types.md +0 -10
- package/.opencode/command/git/cm.md +0 -5
- package/.opencode/command/git/cp.md +0 -4
- package/.opencode/command/plan/ci.md +0 -12
- package/.opencode/command/plan/two.md +0 -13
- package/.opencode/command/plan.md +0 -10
- package/.opencode/command/test.md +0 -7
- package/.opencode/command/watzup.md +0 -8
- package/docs/code-standards.md +0 -1128
- package/docs/codebase-summary.md +0 -821
- package/docs/github-setup.md +0 -176
- package/docs/project-pdr.md +0 -739
- package/docs/system-architecture.md +0 -950
- package/docs/tech-stack.md +0 -290
- package/plans/251008-claudekit-cli-implementation-plan.md +0 -1469
- package/plans/reports/251008-from-code-reviewer-to-developer-review-report.md +0 -864
- package/plans/reports/251008-from-tester-to-developer-test-summary-report.md +0 -409
- package/plans/reports/251008-researcher-download-extraction-report.md +0 -1377
- package/plans/reports/251008-researcher-github-api-report.md +0 -1339
- package/plans/research/251008-cli-frameworks-bun-research.md +0 -1051
- package/plans/templates/bug-fix-template.md +0 -69
- package/plans/templates/feature-implementation-template.md +0 -84
- package/plans/templates/refactor-template.md +0 -82
- package/plans/templates/template-usage-guide.md +0 -58
|
@@ -1,1339 +0,0 @@
|
|
|
1
|
-
# Research Report: GitHub API Integration for Downloading Private Repository Releases
|
|
2
|
-
|
|
3
|
-
## Executive Summary
|
|
4
|
-
|
|
5
|
-
This research provides a comprehensive analysis of GitHub API integration methods for downloading releases from private repositories. The recommended approach is to use **Octokit.js** (the official GitHub SDK) with **fine-grained Personal Access Tokens (PATs)** for authentication, implementing proper error handling with exponential backoff retry strategies. For CLI applications, detecting and utilizing existing GitHub CLI (`gh`) authentication provides the best user experience, falling back to PAT prompts when needed.
|
|
6
|
-
|
|
7
|
-
**Key Recommendations:**
|
|
8
|
-
- Use Octokit.js v22+ for comprehensive GitHub API interactions with built-in TypeScript support
|
|
9
|
-
- Implement fine-grained PATs with minimal required permissions for enhanced security
|
|
10
|
-
- Use system credential stores (via keytar or OS-specific solutions) for secure token storage
|
|
11
|
-
- Implement retry logic with exponential backoff for rate limit handling
|
|
12
|
-
- Stream large files using Node.js streams with progress tracking
|
|
13
|
-
|
|
14
|
-
**Research Conducted:** January 2025
|
|
15
|
-
**Sources Consulted:** 25+
|
|
16
|
-
**Date Range of Materials:** 2024-2025
|
|
17
|
-
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
## Research Methodology
|
|
21
|
-
|
|
22
|
-
### Sources Consulted
|
|
23
|
-
- Official GitHub REST API documentation
|
|
24
|
-
- Octokit.js official repository and documentation
|
|
25
|
-
- Stack Overflow technical discussions (2024-2025)
|
|
26
|
-
- GitHub community discussions
|
|
27
|
-
- Security best practices articles and guides
|
|
28
|
-
- npm package comparisons and documentation
|
|
29
|
-
|
|
30
|
-
### Key Search Terms Used
|
|
31
|
-
- GitHub REST API releases private repository authentication
|
|
32
|
-
- Octokit download release assets
|
|
33
|
-
- Fine-grained personal access token
|
|
34
|
-
- GitHub CLI integration Node.js
|
|
35
|
-
- API rate limiting best practices
|
|
36
|
-
- Secure token storage Node.js
|
|
37
|
-
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
## Key Findings
|
|
41
|
-
|
|
42
|
-
### 1. Technology Overview
|
|
43
|
-
|
|
44
|
-
#### GitHub REST API for Releases
|
|
45
|
-
|
|
46
|
-
The GitHub REST API provides comprehensive endpoints for managing releases:
|
|
47
|
-
|
|
48
|
-
**Core Endpoints:**
|
|
49
|
-
- `GET /repos/{owner}/{repo}/releases` - List all releases (supports pagination)
|
|
50
|
-
- `GET /repos/{owner}/{repo}/releases/latest` - Get the latest non-prerelease release
|
|
51
|
-
- `GET /repos/{owner}/{repo}/releases/tags/{tag}` - Get release by tag name
|
|
52
|
-
- `GET /repos/{owner}/{repo}/releases/{release_id}` - Get specific release
|
|
53
|
-
- `GET /repos/{owner}/{repo}/releases/assets/{asset_id}` - Get release asset
|
|
54
|
-
|
|
55
|
-
**Authentication Requirements:**
|
|
56
|
-
- Public repositories: No authentication required for listing releases
|
|
57
|
-
- Private repositories: Requires fine-grained PAT or classic PAT with `repo` scope
|
|
58
|
-
- GitHub Apps: Requires "Contents" repository permissions (read access minimum)
|
|
59
|
-
|
|
60
|
-
**Important API Behaviors:**
|
|
61
|
-
- Information about published releases is available to everyone
|
|
62
|
-
- Draft releases are only visible to users with push access
|
|
63
|
-
- The `/releases/latest` endpoint returns the most recent non-prerelease, non-draft release sorted by `created_at`
|
|
64
|
-
- Release lists don't have guaranteed sort order; client-side sorting recommended
|
|
65
|
-
|
|
66
|
-
#### Release Assets Download
|
|
67
|
-
|
|
68
|
-
**Two Methods for Downloading Assets:**
|
|
69
|
-
|
|
70
|
-
1. **Browser Method:** Use `browser_download_url` from the asset metadata
|
|
71
|
-
2. **API Method:** Request the asset endpoint with `Accept: application/octet-stream` header
|
|
72
|
-
|
|
73
|
-
```javascript
|
|
74
|
-
// Get asset metadata
|
|
75
|
-
GET /repos/{owner}/{repo}/releases/assets/{asset_id}
|
|
76
|
-
|
|
77
|
-
// Download asset binary
|
|
78
|
-
GET /repos/{owner}/{repo}/releases/assets/{asset_id}
|
|
79
|
-
Headers: { Accept: "application/octet-stream" }
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
**Response Codes:**
|
|
83
|
-
- `200` - Success (asset data returned)
|
|
84
|
-
- `302` - Redirect to download URL
|
|
85
|
-
- `404` - Asset not found or insufficient permissions
|
|
86
|
-
|
|
87
|
-
---
|
|
88
|
-
|
|
89
|
-
### 2. Current State & Trends (2024-2025)
|
|
90
|
-
|
|
91
|
-
#### Fine-Grained Personal Access Tokens (Recommended)
|
|
92
|
-
|
|
93
|
-
Introduced in 2023, fine-grained PATs are now the recommended authentication method:
|
|
94
|
-
|
|
95
|
-
**Key Advantages:**
|
|
96
|
-
- Granular permissions (50+ specific permissions available)
|
|
97
|
-
- Repository-specific access (can limit to single repo)
|
|
98
|
-
- Organization or user-scoped tokens
|
|
99
|
-
- Mandatory expiration dates (max 1 year)
|
|
100
|
-
- Organization approval workflows available
|
|
101
|
-
|
|
102
|
-
**For Private Repository Access:**
|
|
103
|
-
```
|
|
104
|
-
Required Permissions:
|
|
105
|
-
- Contents: Read (minimum for cloning/downloading)
|
|
106
|
-
- Contents: Read + Write (for pushing changes)
|
|
107
|
-
- Metadata: Read (automatically included)
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
**Current Limitations (2025):**
|
|
111
|
-
- SSO support still in development
|
|
112
|
-
- Outside collaborator support incomplete
|
|
113
|
-
- Some legacy APIs still require classic PATs
|
|
114
|
-
|
|
115
|
-
#### GitHub CLI Integration Trend
|
|
116
|
-
|
|
117
|
-
The official GitHub CLI (`gh`) has become the preferred authentication method for CLI tools:
|
|
118
|
-
|
|
119
|
-
**Detection and Usage:**
|
|
120
|
-
```bash
|
|
121
|
-
# Check if gh CLI is authenticated
|
|
122
|
-
gh auth status --json hosts
|
|
123
|
-
|
|
124
|
-
# Get current token
|
|
125
|
-
gh auth token
|
|
126
|
-
|
|
127
|
-
# Programmatic authentication
|
|
128
|
-
export GH_TOKEN="your-token"
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
**Benefits:**
|
|
132
|
-
- Secure token storage in system credential store
|
|
133
|
-
- No need for interactive prompts
|
|
134
|
-
- Automatic token refresh
|
|
135
|
-
- Supports SSO and 2FA workflows
|
|
136
|
-
|
|
137
|
-
---
|
|
138
|
-
|
|
139
|
-
### 3. Best Practices
|
|
140
|
-
|
|
141
|
-
#### Authentication Strategy
|
|
142
|
-
|
|
143
|
-
**Recommended Multi-Tier Approach:**
|
|
144
|
-
|
|
145
|
-
1. **Check for GitHub CLI Authentication First**
|
|
146
|
-
```javascript
|
|
147
|
-
import { execSync } from 'child_process';
|
|
148
|
-
|
|
149
|
-
function getGitHubToken() {
|
|
150
|
-
try {
|
|
151
|
-
// Try to get token from gh CLI
|
|
152
|
-
const token = execSync('gh auth token', { encoding: 'utf-8' }).trim();
|
|
153
|
-
return token;
|
|
154
|
-
} catch (error) {
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
2. **Check Environment Variables**
|
|
161
|
-
```javascript
|
|
162
|
-
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
3. **Prompt User for Token** (last resort)
|
|
166
|
-
```javascript
|
|
167
|
-
import { input, password } from '@inquirer/prompts';
|
|
168
|
-
|
|
169
|
-
const token = await password({
|
|
170
|
-
message: 'Enter your GitHub Personal Access Token:',
|
|
171
|
-
mask: '*'
|
|
172
|
-
});
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
4. **Store Token Securely** (if user agrees)
|
|
176
|
-
- Use OS credential store (keytar for cross-platform)
|
|
177
|
-
- Encrypt tokens before file storage
|
|
178
|
-
- Never store in plain text config files
|
|
179
|
-
|
|
180
|
-
#### Token Security Best Practices
|
|
181
|
-
|
|
182
|
-
**Storage:**
|
|
183
|
-
- ✅ Use OS credential stores (macOS Keychain, Windows Credential Vault, Linux Secret Service)
|
|
184
|
-
- ✅ Encrypt tokens if file storage is necessary
|
|
185
|
-
- ✅ Use environment-specific .env files (with .gitignore)
|
|
186
|
-
- ❌ Never commit tokens to git repositories
|
|
187
|
-
- ❌ Avoid storing in process.env in production
|
|
188
|
-
- ❌ Don't log tokens or include in error messages
|
|
189
|
-
|
|
190
|
-
**Token Management:**
|
|
191
|
-
- Set minimal required permissions (principle of least privilege)
|
|
192
|
-
- Use fine-grained PATs with repository-specific access
|
|
193
|
-
- Set expiration dates (30-90 days recommended)
|
|
194
|
-
- Rotate tokens regularly
|
|
195
|
-
- Revoke tokens immediately when compromised
|
|
196
|
-
- Monitor token usage through GitHub audit logs
|
|
197
|
-
|
|
198
|
-
**Code Implementation:**
|
|
199
|
-
```javascript
|
|
200
|
-
// GOOD: Token from secure source
|
|
201
|
-
const token = await getSecureToken();
|
|
202
|
-
const octokit = new Octokit({ auth: token });
|
|
203
|
-
|
|
204
|
-
// BAD: Hardcoded token
|
|
205
|
-
const octokit = new Octokit({ auth: 'ghp_hardcodedtoken123' });
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
#### Library Selection
|
|
209
|
-
|
|
210
|
-
**Octokit.js - Recommended for GitHub API**
|
|
211
|
-
|
|
212
|
-
**Pros:**
|
|
213
|
-
- Official GitHub SDK with comprehensive API coverage
|
|
214
|
-
- Built-in error handling for GitHub-specific errors
|
|
215
|
-
- Automatic retry with rate limit handling
|
|
216
|
-
- TypeScript support with full type definitions
|
|
217
|
-
- Active maintenance and updates
|
|
218
|
-
- Works in browsers, Node.js, and Deno
|
|
219
|
-
|
|
220
|
-
**Cons:**
|
|
221
|
-
- Larger bundle size vs. raw Axios
|
|
222
|
-
- Learning curve for plugin system
|
|
223
|
-
- Requires Node.js 18+ for latest version
|
|
224
|
-
|
|
225
|
-
**When to Choose Alternatives:**
|
|
226
|
-
- **Axios:** Multi-API integration (not just GitHub), lightweight needs
|
|
227
|
-
- **node-fetch:** Minimal dependencies, simple HTTP requests
|
|
228
|
-
- **Got:** Advanced HTTP features, custom retry logic
|
|
229
|
-
|
|
230
|
-
---
|
|
231
|
-
|
|
232
|
-
### 4. Security Considerations
|
|
233
|
-
|
|
234
|
-
#### Token Permission Scoping
|
|
235
|
-
|
|
236
|
-
**Minimum Permissions for Release Downloads:**
|
|
237
|
-
```
|
|
238
|
-
Fine-Grained PAT:
|
|
239
|
-
- Repository access: Select specific repo(s)
|
|
240
|
-
- Permissions:
|
|
241
|
-
✓ Contents: Read
|
|
242
|
-
✓ Metadata: Read (auto-included)
|
|
243
|
-
|
|
244
|
-
Classic PAT:
|
|
245
|
-
- Scopes: repo (full repository access)
|
|
246
|
-
Note: Classic tokens are "all or nothing" - less secure
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
#### Common Security Pitfalls
|
|
250
|
-
|
|
251
|
-
**1. Token Exposure in Logs**
|
|
252
|
-
```javascript
|
|
253
|
-
// BAD: Token in error logs
|
|
254
|
-
console.error('Failed with token:', token);
|
|
255
|
-
|
|
256
|
-
// GOOD: Sanitized logging
|
|
257
|
-
console.error('Failed to authenticate with GitHub');
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
**2. Token in Environment Variables (Production)**
|
|
261
|
-
```javascript
|
|
262
|
-
// RISKY: process.env can be accessed by all dependencies
|
|
263
|
-
const token = process.env.GITHUB_TOKEN;
|
|
264
|
-
|
|
265
|
-
// BETTER: Load from secure storage
|
|
266
|
-
const token = await loadFromCredentialStore();
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
**3. Insufficient HTTPS Verification**
|
|
270
|
-
```javascript
|
|
271
|
-
// BAD: Disabling SSL verification
|
|
272
|
-
const octokit = new Octokit({
|
|
273
|
-
auth: token,
|
|
274
|
-
request: { rejectUnauthorized: false }
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
// GOOD: Always verify SSL
|
|
278
|
-
const octokit = new Octokit({ auth: token });
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
#### Handling Private Repository 404 Errors
|
|
282
|
-
|
|
283
|
-
Private repositories return `404` for both "not found" and "no permission" scenarios:
|
|
284
|
-
|
|
285
|
-
```javascript
|
|
286
|
-
try {
|
|
287
|
-
const release = await octokit.rest.repos.getLatest({
|
|
288
|
-
owner: 'user',
|
|
289
|
-
repo: 'private-repo'
|
|
290
|
-
});
|
|
291
|
-
} catch (error) {
|
|
292
|
-
if (error.status === 404) {
|
|
293
|
-
// Could be: repo doesn't exist, repo is private, or insufficient permissions
|
|
294
|
-
console.error('Repository not found or insufficient permissions');
|
|
295
|
-
// Prompt for authentication or verify token scopes
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
---
|
|
301
|
-
|
|
302
|
-
### 5. Performance Insights
|
|
303
|
-
|
|
304
|
-
#### Rate Limiting
|
|
305
|
-
|
|
306
|
-
**GitHub API Rate Limits (2025):**
|
|
307
|
-
|
|
308
|
-
| Authentication Type | Requests/Hour | Notes |
|
|
309
|
-
|-------------------|---------------|-------|
|
|
310
|
-
| Unauthenticated | 60 | Per IP address |
|
|
311
|
-
| Personal Access Token | 5,000 | Per token |
|
|
312
|
-
| GitHub App (installation) | 15,000+ | Scales with repos/users |
|
|
313
|
-
| OAuth App | 5,000 | Per token |
|
|
314
|
-
|
|
315
|
-
**Rate Limit Headers:**
|
|
316
|
-
```javascript
|
|
317
|
-
const response = await octokit.request('GET /repos/{owner}/{repo}/releases');
|
|
318
|
-
|
|
319
|
-
console.log({
|
|
320
|
-
limit: response.headers['x-ratelimit-limit'], // Max requests per hour
|
|
321
|
-
remaining: response.headers['x-ratelimit-remaining'], // Requests left
|
|
322
|
-
reset: response.headers['x-ratelimit-reset'] // Reset time (Unix epoch)
|
|
323
|
-
});
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
**Handling Rate Limits:**
|
|
327
|
-
```javascript
|
|
328
|
-
import { retry } from '@octokit/plugin-retry';
|
|
329
|
-
import { throttling } from '@octokit/plugin-throttling';
|
|
330
|
-
|
|
331
|
-
const MyOctokit = Octokit.plugin(retry, throttling);
|
|
332
|
-
|
|
333
|
-
const octokit = new MyOctokit({
|
|
334
|
-
auth: token,
|
|
335
|
-
throttle: {
|
|
336
|
-
onRateLimit: (retryAfter, options, octokit, retryCount) => {
|
|
337
|
-
console.warn(`Rate limit hit, retrying after ${retryAfter}s`);
|
|
338
|
-
if (retryCount < 3) return true; // Retry
|
|
339
|
-
return false;
|
|
340
|
-
},
|
|
341
|
-
onSecondaryRateLimit: (retryAfter, options, octokit) => {
|
|
342
|
-
console.warn('Secondary rate limit hit');
|
|
343
|
-
return true;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
});
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
#### Streaming Large Files
|
|
350
|
-
|
|
351
|
-
**Best Practice: Stream to Disk**
|
|
352
|
-
```javascript
|
|
353
|
-
import fs from 'fs';
|
|
354
|
-
import stream from 'stream';
|
|
355
|
-
import { promisify } from 'util';
|
|
356
|
-
|
|
357
|
-
const pipeline = promisify(stream.pipeline);
|
|
358
|
-
|
|
359
|
-
async function downloadAsset(octokit, owner, repo, assetId, outputPath) {
|
|
360
|
-
const response = await octokit.request(
|
|
361
|
-
'GET /repos/{owner}/{repo}/releases/assets/{asset_id}',
|
|
362
|
-
{
|
|
363
|
-
owner,
|
|
364
|
-
repo,
|
|
365
|
-
asset_id: assetId,
|
|
366
|
-
headers: {
|
|
367
|
-
accept: 'application/octet-stream'
|
|
368
|
-
},
|
|
369
|
-
// Return raw response for streaming
|
|
370
|
-
request: { responseType: 'stream' }
|
|
371
|
-
}
|
|
372
|
-
);
|
|
373
|
-
|
|
374
|
-
await pipeline(
|
|
375
|
-
response.data,
|
|
376
|
-
fs.createWriteStream(outputPath)
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
**With Progress Tracking:**
|
|
382
|
-
```javascript
|
|
383
|
-
import ProgressBar from 'progress';
|
|
384
|
-
|
|
385
|
-
async function downloadWithProgress(octokit, owner, repo, assetId, outputPath) {
|
|
386
|
-
// First, get asset metadata for size
|
|
387
|
-
const asset = await octokit.rest.repos.getReleaseAsset({
|
|
388
|
-
owner,
|
|
389
|
-
repo,
|
|
390
|
-
asset_id: assetId
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
const totalSize = asset.data.size;
|
|
394
|
-
const progressBar = new ProgressBar('Downloading [:bar] :percent :etas', {
|
|
395
|
-
total: totalSize,
|
|
396
|
-
width: 40
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
const response = await octokit.request(
|
|
400
|
-
'GET /repos/{owner}/{repo}/releases/assets/{asset_id}',
|
|
401
|
-
{
|
|
402
|
-
owner,
|
|
403
|
-
repo,
|
|
404
|
-
asset_id: assetId,
|
|
405
|
-
headers: { accept: 'application/octet-stream' },
|
|
406
|
-
request: { responseType: 'stream' }
|
|
407
|
-
}
|
|
408
|
-
);
|
|
409
|
-
|
|
410
|
-
const writer = fs.createWriteStream(outputPath);
|
|
411
|
-
|
|
412
|
-
response.data.on('data', (chunk) => {
|
|
413
|
-
progressBar.tick(chunk.length);
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
await pipeline(response.data, writer);
|
|
417
|
-
}
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
---
|
|
421
|
-
|
|
422
|
-
## Comparative Analysis
|
|
423
|
-
|
|
424
|
-
### Library Comparison: Octokit vs. Axios vs. Got
|
|
425
|
-
|
|
426
|
-
| Feature | Octokit.js | Axios | Got |
|
|
427
|
-
|---------|-----------|-------|-----|
|
|
428
|
-
| **GitHub API Native** | ✅ Official SDK | ❌ Generic HTTP | ❌ Generic HTTP |
|
|
429
|
-
| **TypeScript Support** | ✅ Full types | ✅ Via @types | ✅ Built-in |
|
|
430
|
-
| **Auto Rate Limiting** | ✅ Plugin available | ❌ Manual | ❌ Manual |
|
|
431
|
-
| **Error Handling** | ✅ GitHub-specific | ⚠️ Generic | ⚠️ Generic |
|
|
432
|
-
| **Bundle Size** | ~200KB | ~15KB | ~50KB |
|
|
433
|
-
| **Learning Curve** | Medium | Low | Low |
|
|
434
|
-
| **Retry Logic** | ✅ Plugin | ⚠️ Manual/Libraries | ✅ Built-in |
|
|
435
|
-
| **Streaming** | ✅ Supported | ✅ Supported | ✅ Supported |
|
|
436
|
-
| **Best For** | GitHub APIs | Multi-API | Advanced HTTP |
|
|
437
|
-
|
|
438
|
-
**Recommendation:** Use **Octokit.js** for GitHub-specific integrations due to superior error handling, built-in rate limiting, and GitHub-specific features.
|
|
439
|
-
|
|
440
|
-
### Authentication Method Comparison
|
|
441
|
-
|
|
442
|
-
| Method | Security | UX | Setup Complexity | Best For |
|
|
443
|
-
|--------|----------|----|--------------------|----------|
|
|
444
|
-
| **GitHub CLI Detection** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | CLI tools |
|
|
445
|
-
| **Fine-Grained PAT** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | Production apps |
|
|
446
|
-
| **Classic PAT** | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Legacy systems |
|
|
447
|
-
| **OAuth Apps** | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | Web apps |
|
|
448
|
-
| **GitHub Apps** | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐ | Enterprise |
|
|
449
|
-
|
|
450
|
-
---
|
|
451
|
-
|
|
452
|
-
## Implementation Recommendations
|
|
453
|
-
|
|
454
|
-
### Quick Start Guide
|
|
455
|
-
|
|
456
|
-
**1. Install Dependencies**
|
|
457
|
-
```bash
|
|
458
|
-
npm install octokit
|
|
459
|
-
# Optional: For secure storage
|
|
460
|
-
npm install keytar
|
|
461
|
-
# Optional: For CLI prompts
|
|
462
|
-
npm install @inquirer/prompts
|
|
463
|
-
# Optional: For progress bars
|
|
464
|
-
npm install progress
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
**2. Basic Setup**
|
|
468
|
-
```javascript
|
|
469
|
-
import { Octokit } from 'octokit';
|
|
470
|
-
|
|
471
|
-
// Initialize with token
|
|
472
|
-
const octokit = new Octokit({
|
|
473
|
-
auth: process.env.GITHUB_TOKEN
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
// Test connection
|
|
477
|
-
const { data: user } = await octokit.rest.users.getAuthenticated();
|
|
478
|
-
console.log(`Authenticated as: ${user.login}`);
|
|
479
|
-
```
|
|
480
|
-
|
|
481
|
-
**3. List Releases**
|
|
482
|
-
```javascript
|
|
483
|
-
async function listReleases(owner, repo) {
|
|
484
|
-
const { data: releases } = await octokit.rest.repos.listReleases({
|
|
485
|
-
owner,
|
|
486
|
-
repo,
|
|
487
|
-
per_page: 10
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
return releases.map(r => ({
|
|
491
|
-
name: r.name,
|
|
492
|
-
tag: r.tag_name,
|
|
493
|
-
published: r.published_at,
|
|
494
|
-
assets: r.assets.length
|
|
495
|
-
}));
|
|
496
|
-
}
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
**4. Get Latest Release**
|
|
500
|
-
```javascript
|
|
501
|
-
async function getLatestRelease(owner, repo) {
|
|
502
|
-
try {
|
|
503
|
-
const { data: release } = await octokit.rest.repos.getLatestRelease({
|
|
504
|
-
owner,
|
|
505
|
-
repo
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
return {
|
|
509
|
-
version: release.tag_name,
|
|
510
|
-
assets: release.assets.map(a => ({
|
|
511
|
-
id: a.id,
|
|
512
|
-
name: a.name,
|
|
513
|
-
size: a.size,
|
|
514
|
-
downloadUrl: a.browser_download_url
|
|
515
|
-
}))
|
|
516
|
-
};
|
|
517
|
-
} catch (error) {
|
|
518
|
-
if (error.status === 404) {
|
|
519
|
-
throw new Error('No releases found or repository is private');
|
|
520
|
-
}
|
|
521
|
-
throw error;
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
**5. Download Release Asset**
|
|
527
|
-
```javascript
|
|
528
|
-
import fs from 'fs';
|
|
529
|
-
import { pipeline } from 'stream/promises';
|
|
530
|
-
|
|
531
|
-
async function downloadAsset(owner, repo, assetId, outputPath) {
|
|
532
|
-
// Get asset details first
|
|
533
|
-
const { data: asset } = await octokit.rest.repos.getReleaseAsset({
|
|
534
|
-
owner,
|
|
535
|
-
repo,
|
|
536
|
-
asset_id: assetId
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
console.log(`Downloading ${asset.name} (${asset.size} bytes)...`);
|
|
540
|
-
|
|
541
|
-
// Download with streaming
|
|
542
|
-
const response = await octokit.request(
|
|
543
|
-
'GET /repos/{owner}/{repo}/releases/assets/{asset_id}',
|
|
544
|
-
{
|
|
545
|
-
owner,
|
|
546
|
-
repo,
|
|
547
|
-
asset_id: assetId,
|
|
548
|
-
headers: {
|
|
549
|
-
accept: 'application/octet-stream'
|
|
550
|
-
},
|
|
551
|
-
request: {
|
|
552
|
-
responseType: 'stream'
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
);
|
|
556
|
-
|
|
557
|
-
await pipeline(
|
|
558
|
-
response.data,
|
|
559
|
-
fs.createWriteStream(outputPath)
|
|
560
|
-
);
|
|
561
|
-
|
|
562
|
-
console.log(`Downloaded to ${outputPath}`);
|
|
563
|
-
}
|
|
564
|
-
```
|
|
565
|
-
|
|
566
|
-
---
|
|
567
|
-
|
|
568
|
-
### Code Examples
|
|
569
|
-
|
|
570
|
-
#### Complete Authentication Flow
|
|
571
|
-
|
|
572
|
-
```javascript
|
|
573
|
-
import { Octokit } from 'octokit';
|
|
574
|
-
import { execSync } from 'child_process';
|
|
575
|
-
import { password } from '@inquirer/prompts';
|
|
576
|
-
import keytar from 'keytar';
|
|
577
|
-
|
|
578
|
-
const SERVICE_NAME = 'my-github-cli-tool';
|
|
579
|
-
const ACCOUNT_NAME = 'github-token';
|
|
580
|
-
|
|
581
|
-
class GitHubAuthManager {
|
|
582
|
-
constructor() {
|
|
583
|
-
this.octokit = null;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
async getToken() {
|
|
587
|
-
// 1. Try GitHub CLI
|
|
588
|
-
const ghToken = this.tryGitHubCLI();
|
|
589
|
-
if (ghToken) {
|
|
590
|
-
console.log('✓ Using GitHub CLI authentication');
|
|
591
|
-
return ghToken;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// 2. Try environment variable
|
|
595
|
-
const envToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
596
|
-
if (envToken) {
|
|
597
|
-
console.log('✓ Using token from environment');
|
|
598
|
-
return envToken;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// 3. Try stored credential
|
|
602
|
-
const storedToken = await this.getStoredToken();
|
|
603
|
-
if (storedToken) {
|
|
604
|
-
console.log('✓ Using stored credential');
|
|
605
|
-
return storedToken;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// 4. Prompt user
|
|
609
|
-
const promptedToken = await this.promptForToken();
|
|
610
|
-
|
|
611
|
-
// 5. Optionally store for future use
|
|
612
|
-
await this.storeToken(promptedToken);
|
|
613
|
-
|
|
614
|
-
return promptedToken;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
tryGitHubCLI() {
|
|
618
|
-
try {
|
|
619
|
-
const token = execSync('gh auth token', {
|
|
620
|
-
encoding: 'utf-8',
|
|
621
|
-
stdio: ['pipe', 'pipe', 'ignore'] // Suppress errors
|
|
622
|
-
}).trim();
|
|
623
|
-
|
|
624
|
-
// Verify it's a valid token format
|
|
625
|
-
if (token.startsWith('ghp_') || token.startsWith('gho_')) {
|
|
626
|
-
return token;
|
|
627
|
-
}
|
|
628
|
-
} catch {
|
|
629
|
-
// gh CLI not available or not authenticated
|
|
630
|
-
}
|
|
631
|
-
return null;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
async getStoredToken() {
|
|
635
|
-
try {
|
|
636
|
-
return await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
637
|
-
} catch (error) {
|
|
638
|
-
console.warn('Could not access credential store:', error.message);
|
|
639
|
-
return null;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
async promptForToken() {
|
|
644
|
-
console.log('\nGitHub authentication required.');
|
|
645
|
-
console.log('Create a token at: https://github.com/settings/tokens');
|
|
646
|
-
console.log('Required scopes: repo (for private repositories)\n');
|
|
647
|
-
|
|
648
|
-
const token = await password({
|
|
649
|
-
message: 'Enter your GitHub Personal Access Token:',
|
|
650
|
-
mask: '*',
|
|
651
|
-
validate: (value) => {
|
|
652
|
-
if (!value) return 'Token is required';
|
|
653
|
-
if (!value.startsWith('ghp_') && !value.startsWith('gho_')) {
|
|
654
|
-
return 'Invalid token format';
|
|
655
|
-
}
|
|
656
|
-
return true;
|
|
657
|
-
}
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
return token;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
async storeToken(token) {
|
|
664
|
-
const { confirm } = await import('@inquirer/prompts');
|
|
665
|
-
|
|
666
|
-
const shouldStore = await confirm({
|
|
667
|
-
message: 'Save token for future use? (stored securely in system keychain)',
|
|
668
|
-
default: true
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
if (shouldStore) {
|
|
672
|
-
try {
|
|
673
|
-
await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, token);
|
|
674
|
-
console.log('✓ Token saved securely');
|
|
675
|
-
} catch (error) {
|
|
676
|
-
console.warn('Could not save token:', error.message);
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
async deleteStoredToken() {
|
|
682
|
-
try {
|
|
683
|
-
await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
684
|
-
console.log('✓ Stored token deleted');
|
|
685
|
-
} catch (error) {
|
|
686
|
-
console.warn('Could not delete token:', error.message);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
async initialize() {
|
|
691
|
-
const token = await this.getToken();
|
|
692
|
-
this.octokit = new Octokit({ auth: token });
|
|
693
|
-
|
|
694
|
-
// Verify authentication
|
|
695
|
-
try {
|
|
696
|
-
const { data: user } = await this.octokit.rest.users.getAuthenticated();
|
|
697
|
-
console.log(`✓ Authenticated as: ${user.login}\n`);
|
|
698
|
-
return this.octokit;
|
|
699
|
-
} catch (error) {
|
|
700
|
-
if (error.status === 401) {
|
|
701
|
-
// Invalid token - clear stored credentials
|
|
702
|
-
await this.deleteStoredToken();
|
|
703
|
-
throw new Error('Invalid token. Please try again.');
|
|
704
|
-
}
|
|
705
|
-
throw error;
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// Usage
|
|
711
|
-
const authManager = new GitHubAuthManager();
|
|
712
|
-
const octokit = await authManager.initialize();
|
|
713
|
-
```
|
|
714
|
-
|
|
715
|
-
#### Error Handling with Retry Logic
|
|
716
|
-
|
|
717
|
-
```javascript
|
|
718
|
-
import { Octokit } from 'octokit';
|
|
719
|
-
import pRetry from 'p-retry';
|
|
720
|
-
|
|
721
|
-
class GitHubReleaseManager {
|
|
722
|
-
constructor(token) {
|
|
723
|
-
this.octokit = new Octokit({ auth: token });
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
async withRetry(operation, options = {}) {
|
|
727
|
-
const defaultOptions = {
|
|
728
|
-
retries: 3,
|
|
729
|
-
factor: 2,
|
|
730
|
-
minTimeout: 1000,
|
|
731
|
-
maxTimeout: 10000,
|
|
732
|
-
onFailedAttempt: (error) => {
|
|
733
|
-
console.warn(
|
|
734
|
-
`Attempt ${error.attemptNumber} failed. ${error.retriesLeft} retries left.`
|
|
735
|
-
);
|
|
736
|
-
|
|
737
|
-
// Check for rate limiting
|
|
738
|
-
if (error.response?.headers) {
|
|
739
|
-
const resetTime = error.response.headers['x-ratelimit-reset'];
|
|
740
|
-
if (resetTime) {
|
|
741
|
-
const waitTime = (resetTime * 1000) - Date.now();
|
|
742
|
-
if (waitTime > 0) {
|
|
743
|
-
console.log(`Rate limited. Waiting ${Math.ceil(waitTime / 1000)}s...`);
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
};
|
|
749
|
-
|
|
750
|
-
return pRetry(operation, { ...defaultOptions, ...options });
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
async getLatestRelease(owner, repo) {
|
|
754
|
-
return this.withRetry(async () => {
|
|
755
|
-
try {
|
|
756
|
-
const { data } = await this.octokit.rest.repos.getLatestRelease({
|
|
757
|
-
owner,
|
|
758
|
-
repo
|
|
759
|
-
});
|
|
760
|
-
return data;
|
|
761
|
-
} catch (error) {
|
|
762
|
-
// Handle specific error cases
|
|
763
|
-
if (error.status === 404) {
|
|
764
|
-
// Don't retry 404s - repository doesn't exist or no releases
|
|
765
|
-
throw new pRetry.AbortError(
|
|
766
|
-
`No releases found for ${owner}/${repo}. ` +
|
|
767
|
-
'Repository may be private or have no releases.'
|
|
768
|
-
);
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
if (error.status === 403) {
|
|
772
|
-
// Rate limited or forbidden - do retry
|
|
773
|
-
throw error;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
if (error.status === 401) {
|
|
777
|
-
// Authentication failed - don't retry
|
|
778
|
-
throw new pRetry.AbortError('Authentication failed. Check your token.');
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// Retry on network errors or 5xx
|
|
782
|
-
throw error;
|
|
783
|
-
}
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
async downloadAsset(owner, repo, assetId, outputPath) {
|
|
788
|
-
return this.withRetry(async () => {
|
|
789
|
-
const response = await this.octokit.request(
|
|
790
|
-
'GET /repos/{owner}/{repo}/releases/assets/{asset_id}',
|
|
791
|
-
{
|
|
792
|
-
owner,
|
|
793
|
-
repo,
|
|
794
|
-
asset_id: assetId,
|
|
795
|
-
headers: { accept: 'application/octet-stream' },
|
|
796
|
-
request: { responseType: 'stream' }
|
|
797
|
-
}
|
|
798
|
-
);
|
|
799
|
-
|
|
800
|
-
await pipeline(
|
|
801
|
-
response.data,
|
|
802
|
-
fs.createWriteStream(outputPath)
|
|
803
|
-
);
|
|
804
|
-
|
|
805
|
-
return outputPath;
|
|
806
|
-
}, {
|
|
807
|
-
// More retries for downloads due to potential network issues
|
|
808
|
-
retries: 5
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
```
|
|
813
|
-
|
|
814
|
-
#### Complete CLI Tool Example
|
|
815
|
-
|
|
816
|
-
```javascript
|
|
817
|
-
#!/usr/bin/env node
|
|
818
|
-
import { Octokit } from 'octokit';
|
|
819
|
-
import { Command } from 'commander';
|
|
820
|
-
import { GitHubAuthManager } from './auth.js';
|
|
821
|
-
import { GitHubReleaseManager } from './releases.js';
|
|
822
|
-
import ora from 'ora';
|
|
823
|
-
import chalk from 'chalk';
|
|
824
|
-
|
|
825
|
-
const program = new Command();
|
|
826
|
-
|
|
827
|
-
program
|
|
828
|
-
.name('gh-release-downloader')
|
|
829
|
-
.description('Download releases from GitHub repositories')
|
|
830
|
-
.version('1.0.0');
|
|
831
|
-
|
|
832
|
-
program
|
|
833
|
-
.command('download')
|
|
834
|
-
.description('Download the latest release from a repository')
|
|
835
|
-
.argument('<repository>', 'Repository in format owner/repo')
|
|
836
|
-
.option('-t, --tag <tag>', 'Download specific tag (default: latest)')
|
|
837
|
-
.option('-a, --asset <pattern>', 'Asset name pattern to download')
|
|
838
|
-
.option('-o, --output <path>', 'Output directory', './downloads')
|
|
839
|
-
.action(async (repository, options) => {
|
|
840
|
-
const [owner, repo] = repository.split('/');
|
|
841
|
-
|
|
842
|
-
if (!owner || !repo) {
|
|
843
|
-
console.error(chalk.red('Invalid repository format. Use: owner/repo'));
|
|
844
|
-
process.exit(1);
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
const spinner = ora('Authenticating with GitHub...').start();
|
|
848
|
-
|
|
849
|
-
try {
|
|
850
|
-
// Authenticate
|
|
851
|
-
const authManager = new GitHubAuthManager();
|
|
852
|
-
const octokit = await authManager.initialize();
|
|
853
|
-
spinner.succeed('Authenticated');
|
|
854
|
-
|
|
855
|
-
const releaseManager = new GitHubReleaseManager(octokit.auth);
|
|
856
|
-
|
|
857
|
-
// Get release
|
|
858
|
-
spinner.start(`Fetching release information...`);
|
|
859
|
-
const release = options.tag
|
|
860
|
-
? await releaseManager.getReleaseByTag(owner, repo, options.tag)
|
|
861
|
-
: await releaseManager.getLatestRelease(owner, repo);
|
|
862
|
-
|
|
863
|
-
spinner.succeed(`Found release: ${release.tag_name}`);
|
|
864
|
-
|
|
865
|
-
// Find matching asset
|
|
866
|
-
const assets = release.assets;
|
|
867
|
-
if (assets.length === 0) {
|
|
868
|
-
console.log(chalk.yellow('No assets found in this release'));
|
|
869
|
-
return;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
let asset;
|
|
873
|
-
if (options.asset) {
|
|
874
|
-
asset = assets.find(a => a.name.includes(options.asset));
|
|
875
|
-
if (!asset) {
|
|
876
|
-
console.error(chalk.red(`No asset matching "${options.asset}" found`));
|
|
877
|
-
console.log('\nAvailable assets:');
|
|
878
|
-
assets.forEach(a => console.log(` - ${a.name}`));
|
|
879
|
-
process.exit(1);
|
|
880
|
-
}
|
|
881
|
-
} else if (assets.length === 1) {
|
|
882
|
-
asset = assets[0];
|
|
883
|
-
} else {
|
|
884
|
-
const { select } = await import('@inquirer/prompts');
|
|
885
|
-
const assetName = await select({
|
|
886
|
-
message: 'Select asset to download:',
|
|
887
|
-
choices: assets.map(a => ({
|
|
888
|
-
name: `${a.name} (${(a.size / 1024 / 1024).toFixed(2)} MB)`,
|
|
889
|
-
value: a.name
|
|
890
|
-
}))
|
|
891
|
-
});
|
|
892
|
-
asset = assets.find(a => a.name === assetName);
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// Download asset
|
|
896
|
-
const outputPath = `${options.output}/${asset.name}`;
|
|
897
|
-
spinner.start(`Downloading ${asset.name}...`);
|
|
898
|
-
|
|
899
|
-
await releaseManager.downloadAsset(
|
|
900
|
-
owner,
|
|
901
|
-
repo,
|
|
902
|
-
asset.id,
|
|
903
|
-
outputPath
|
|
904
|
-
);
|
|
905
|
-
|
|
906
|
-
spinner.succeed(chalk.green(`Downloaded to ${outputPath}`));
|
|
907
|
-
|
|
908
|
-
} catch (error) {
|
|
909
|
-
spinner.fail(chalk.red('Error: ' + error.message));
|
|
910
|
-
|
|
911
|
-
if (error.status === 404) {
|
|
912
|
-
console.log('\nPossible issues:');
|
|
913
|
-
console.log(' - Repository does not exist');
|
|
914
|
-
console.log(' - Repository is private and token lacks permissions');
|
|
915
|
-
console.log(' - No releases published');
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
process.exit(1);
|
|
919
|
-
}
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
program
|
|
923
|
-
.command('list')
|
|
924
|
-
.description('List releases from a repository')
|
|
925
|
-
.argument('<repository>', 'Repository in format owner/repo')
|
|
926
|
-
.option('-n, --limit <number>', 'Number of releases to show', '10')
|
|
927
|
-
.action(async (repository, options) => {
|
|
928
|
-
const [owner, repo] = repository.split('/');
|
|
929
|
-
|
|
930
|
-
const spinner = ora('Fetching releases...').start();
|
|
931
|
-
|
|
932
|
-
try {
|
|
933
|
-
const authManager = new GitHubAuthManager();
|
|
934
|
-
const octokit = await authManager.initialize();
|
|
935
|
-
spinner.stop();
|
|
936
|
-
|
|
937
|
-
const { data: releases } = await octokit.rest.repos.listReleases({
|
|
938
|
-
owner,
|
|
939
|
-
repo,
|
|
940
|
-
per_page: parseInt(options.limit)
|
|
941
|
-
});
|
|
942
|
-
|
|
943
|
-
console.log(chalk.bold(`\nReleases for ${owner}/${repo}:\n`));
|
|
944
|
-
|
|
945
|
-
releases.forEach((release, index) => {
|
|
946
|
-
console.log(chalk.cyan(`${index + 1}. ${release.tag_name}`) +
|
|
947
|
-
(release.prerelease ? chalk.yellow(' (pre-release)') : ''));
|
|
948
|
-
console.log(` Published: ${new Date(release.published_at).toLocaleDateString()}`);
|
|
949
|
-
console.log(` Assets: ${release.assets.length}`);
|
|
950
|
-
if (release.assets.length > 0) {
|
|
951
|
-
release.assets.forEach(asset => {
|
|
952
|
-
console.log(` - ${asset.name} (${(asset.size / 1024 / 1024).toFixed(2)} MB)`);
|
|
953
|
-
});
|
|
954
|
-
}
|
|
955
|
-
console.log('');
|
|
956
|
-
});
|
|
957
|
-
|
|
958
|
-
} catch (error) {
|
|
959
|
-
spinner.fail(chalk.red('Error: ' + error.message));
|
|
960
|
-
process.exit(1);
|
|
961
|
-
}
|
|
962
|
-
});
|
|
963
|
-
|
|
964
|
-
program.parse();
|
|
965
|
-
```
|
|
966
|
-
|
|
967
|
-
---
|
|
968
|
-
|
|
969
|
-
### Common Pitfalls
|
|
970
|
-
|
|
971
|
-
#### 1. **Incorrect Accept Header for Downloads**
|
|
972
|
-
|
|
973
|
-
```javascript
|
|
974
|
-
// ❌ WRONG - Gets asset metadata, not the file
|
|
975
|
-
const asset = await octokit.rest.repos.getReleaseAsset({
|
|
976
|
-
owner: 'user',
|
|
977
|
-
repo: 'repo',
|
|
978
|
-
asset_id: 123
|
|
979
|
-
});
|
|
980
|
-
|
|
981
|
-
// ✅ CORRECT - Downloads the actual file
|
|
982
|
-
const asset = await octokit.request(
|
|
983
|
-
'GET /repos/{owner}/{repo}/releases/assets/{asset_id}',
|
|
984
|
-
{
|
|
985
|
-
owner: 'user',
|
|
986
|
-
repo: 'repo',
|
|
987
|
-
asset_id: 123,
|
|
988
|
-
headers: {
|
|
989
|
-
accept: 'application/octet-stream'
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
);
|
|
993
|
-
```
|
|
994
|
-
|
|
995
|
-
#### 2. **Not Handling Rate Limits**
|
|
996
|
-
|
|
997
|
-
```javascript
|
|
998
|
-
// ❌ WRONG - Will fail when rate limited
|
|
999
|
-
for (const repo of repos) {
|
|
1000
|
-
const releases = await octokit.rest.repos.listReleases({
|
|
1001
|
-
owner,
|
|
1002
|
-
repo
|
|
1003
|
-
});
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
// ✅ CORRECT - Check rate limits and implement backoff
|
|
1007
|
-
import pLimit from 'p-limit';
|
|
1008
|
-
|
|
1009
|
-
const limit = pLimit(5); // Max 5 concurrent requests
|
|
1010
|
-
|
|
1011
|
-
const results = await Promise.all(
|
|
1012
|
-
repos.map(repo =>
|
|
1013
|
-
limit(async () => {
|
|
1014
|
-
try {
|
|
1015
|
-
return await octokit.rest.repos.listReleases({ owner, repo });
|
|
1016
|
-
} catch (error) {
|
|
1017
|
-
if (error.status === 403) {
|
|
1018
|
-
// Check rate limit headers and wait
|
|
1019
|
-
const resetTime = error.response.headers['x-ratelimit-reset'];
|
|
1020
|
-
const waitMs = (resetTime * 1000) - Date.now();
|
|
1021
|
-
if (waitMs > 0) {
|
|
1022
|
-
await new Promise(resolve => setTimeout(resolve, waitMs));
|
|
1023
|
-
return await octokit.rest.repos.listReleases({ owner, repo });
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
throw error;
|
|
1027
|
-
}
|
|
1028
|
-
})
|
|
1029
|
-
)
|
|
1030
|
-
);
|
|
1031
|
-
```
|
|
1032
|
-
|
|
1033
|
-
#### 3. **Loading Large Files into Memory**
|
|
1034
|
-
|
|
1035
|
-
```javascript
|
|
1036
|
-
// ❌ WRONG - Loads entire file into memory
|
|
1037
|
-
const { data } = await octokit.request(
|
|
1038
|
-
'GET /repos/{owner}/{repo}/releases/assets/{asset_id}',
|
|
1039
|
-
{
|
|
1040
|
-
owner,
|
|
1041
|
-
repo,
|
|
1042
|
-
asset_id: assetId,
|
|
1043
|
-
headers: { accept: 'application/octet-stream' }
|
|
1044
|
-
}
|
|
1045
|
-
);
|
|
1046
|
-
fs.writeFileSync('output.zip', data); // Memory intensive!
|
|
1047
|
-
|
|
1048
|
-
// ✅ CORRECT - Stream to disk
|
|
1049
|
-
const { data: stream } = await octokit.request(
|
|
1050
|
-
'GET /repos/{owner}/{repo}/releases/assets/{asset_id}',
|
|
1051
|
-
{
|
|
1052
|
-
owner,
|
|
1053
|
-
repo,
|
|
1054
|
-
asset_id: assetId,
|
|
1055
|
-
headers: { accept: 'application/octet-stream' },
|
|
1056
|
-
request: { responseType: 'stream' }
|
|
1057
|
-
}
|
|
1058
|
-
);
|
|
1059
|
-
|
|
1060
|
-
await pipeline(stream, fs.createWriteStream('output.zip'));
|
|
1061
|
-
```
|
|
1062
|
-
|
|
1063
|
-
#### 4. **Insecure Token Storage**
|
|
1064
|
-
|
|
1065
|
-
```javascript
|
|
1066
|
-
// ❌ WRONG - Plain text storage
|
|
1067
|
-
fs.writeFileSync('.github-token', token);
|
|
1068
|
-
|
|
1069
|
-
// ❌ WRONG - Logged in errors
|
|
1070
|
-
console.error('Auth failed with token:', token);
|
|
1071
|
-
|
|
1072
|
-
// ✅ CORRECT - Use system credential store
|
|
1073
|
-
import keytar from 'keytar';
|
|
1074
|
-
await keytar.setPassword('my-app', 'github-token', token);
|
|
1075
|
-
|
|
1076
|
-
// ✅ CORRECT - Sanitized logging
|
|
1077
|
-
console.error('Auth failed: Invalid or expired token');
|
|
1078
|
-
```
|
|
1079
|
-
|
|
1080
|
-
#### 5. **Not Verifying Token Permissions**
|
|
1081
|
-
|
|
1082
|
-
```javascript
|
|
1083
|
-
// ❌ WRONG - Assume token has correct permissions
|
|
1084
|
-
const release = await octokit.rest.repos.getLatestRelease({ owner, repo });
|
|
1085
|
-
|
|
1086
|
-
// ✅ CORRECT - Verify and provide helpful errors
|
|
1087
|
-
try {
|
|
1088
|
-
const release = await octokit.rest.repos.getLatestRelease({ owner, repo });
|
|
1089
|
-
} catch (error) {
|
|
1090
|
-
if (error.status === 404) {
|
|
1091
|
-
console.error(
|
|
1092
|
-
'Cannot access repository. Please ensure:\n' +
|
|
1093
|
-
'1. Repository exists and is spelled correctly\n' +
|
|
1094
|
-
'2. Your token has "repo" scope for private repositories\n' +
|
|
1095
|
-
'3. You have access to this repository'
|
|
1096
|
-
);
|
|
1097
|
-
} else if (error.status === 401) {
|
|
1098
|
-
console.error('Token is invalid or expired. Please authenticate again.');
|
|
1099
|
-
}
|
|
1100
|
-
throw error;
|
|
1101
|
-
}
|
|
1102
|
-
```
|
|
1103
|
-
|
|
1104
|
-
---
|
|
1105
|
-
|
|
1106
|
-
## Resources & References
|
|
1107
|
-
|
|
1108
|
-
### Official Documentation
|
|
1109
|
-
|
|
1110
|
-
- [GitHub REST API - Releases](https://docs.github.com/en/rest/releases/releases) - Official releases endpoint documentation
|
|
1111
|
-
- [GitHub REST API - Release Assets](https://docs.github.com/en/rest/releases/assets) - Asset download documentation
|
|
1112
|
-
- [Managing Personal Access Tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) - PAT management guide
|
|
1113
|
-
- [Fine-Grained PATs](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) - Fine-grained token creation
|
|
1114
|
-
- [GitHub CLI Manual](https://cli.github.com/manual/) - Official gh CLI documentation
|
|
1115
|
-
- [Octokit.js Documentation](https://github.com/octokit/octokit.js) - Official Octokit SDK
|
|
1116
|
-
- [Rate Limits for REST API](https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api) - Rate limiting details
|
|
1117
|
-
|
|
1118
|
-
### Recommended Tutorials
|
|
1119
|
-
|
|
1120
|
-
- [Introducing Fine-Grained Personal Access Tokens](https://github.blog/security/application-security/introducing-fine-grained-personal-access-tokens-for-github/) - GitHub Blog post on fine-grained PATs
|
|
1121
|
-
- [How to Download Files in Node.js with Streams](https://www.digitalocean.com/community/tutorials/how-to-work-with-files-using-streams-in-node-js) - DigitalOcean streaming guide
|
|
1122
|
-
- [Building Resilient Systems with Retry Mechanisms](https://medium.com/@devharshgupta.com/building-resilient-systems-with-api-retry-mechanisms-in-node-js-a-guide-to-handling-failure-d6d9021b172a) - Retry pattern implementation
|
|
1123
|
-
- [Node.js Security Best Practices](https://www.nodejs-security.com/blog/do-not-use-secrets-in-environment-variables-and-here-is-how-to-do-it-better) - Secure credential handling
|
|
1124
|
-
|
|
1125
|
-
### Community Resources
|
|
1126
|
-
|
|
1127
|
-
- [Stack Overflow - GitHub API Tag](https://stackoverflow.com/questions/tagged/github-api) - Community Q&A
|
|
1128
|
-
- [GitHub Community Discussions](https://github.com/orgs/community/discussions) - Official GitHub community
|
|
1129
|
-
- [Octokit.js Issues](https://github.com/octokit/octokit.js/issues) - Bug reports and feature requests
|
|
1130
|
-
|
|
1131
|
-
### npm Packages
|
|
1132
|
-
|
|
1133
|
-
**Core Libraries:**
|
|
1134
|
-
- [`octokit`](https://www.npmjs.com/package/octokit) - Official GitHub SDK
|
|
1135
|
-
- [`@octokit/rest`](https://www.npmjs.com/package/@octokit/rest) - REST API client
|
|
1136
|
-
- [`@octokit/plugin-retry`](https://www.npmjs.com/package/@octokit/plugin-retry) - Retry plugin
|
|
1137
|
-
- [`@octokit/plugin-throttling`](https://www.npmjs.com/package/@octokit/plugin-throttling) - Rate limit handling
|
|
1138
|
-
|
|
1139
|
-
**Utility Libraries:**
|
|
1140
|
-
- [`p-retry`](https://www.npmjs.com/package/p-retry) - Promise retry with exponential backoff
|
|
1141
|
-
- [`keytar`](https://www.npmjs.com/package/keytar) - Native password/credential storage
|
|
1142
|
-
- [`@inquirer/prompts`](https://www.npmjs.com/package/@inquirer/prompts) - CLI prompts
|
|
1143
|
-
- [`progress`](https://www.npmjs.com/package/progress) - Progress bars
|
|
1144
|
-
- [`ora`](https://www.npmjs.com/package/ora) - Elegant terminal spinners
|
|
1145
|
-
- [`chalk`](https://www.npmjs.com/package/chalk) - Terminal string styling
|
|
1146
|
-
- [`commander`](https://www.npmjs.com/package/commander) - CLI framework
|
|
1147
|
-
|
|
1148
|
-
### Further Reading
|
|
1149
|
-
|
|
1150
|
-
**Advanced Topics:**
|
|
1151
|
-
- [GitHub Apps vs OAuth Apps](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/differences-between-github-apps-and-oauth-apps) - Authentication comparison
|
|
1152
|
-
- [GraphQL API](https://docs.github.com/en/graphql) - Alternative to REST for complex queries
|
|
1153
|
-
- [GitHub Actions Integration](https://docs.github.com/en/actions/security-guides/automatic-token-authentication) - CI/CD authentication
|
|
1154
|
-
- [Webhooks](https://docs.github.com/en/webhooks) - Real-time release notifications
|
|
1155
|
-
|
|
1156
|
-
**Security Deep Dives:**
|
|
1157
|
-
- [GitHub Token Security Best Practices](https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning) - Secret scanning
|
|
1158
|
-
- [Credential Storage on Different Platforms](https://github.com/atom/node-keytar#platform-notes) - OS-specific details
|
|
1159
|
-
- [OWASP Secrets Management](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html) - General security guidance
|
|
1160
|
-
|
|
1161
|
-
---
|
|
1162
|
-
|
|
1163
|
-
## Appendices
|
|
1164
|
-
|
|
1165
|
-
### A. Glossary
|
|
1166
|
-
|
|
1167
|
-
| Term | Definition |
|
|
1168
|
-
|------|------------|
|
|
1169
|
-
| **Personal Access Token (PAT)** | A token that functions like a password for GitHub API authentication |
|
|
1170
|
-
| **Fine-Grained PAT** | New token type with granular, repository-specific permissions |
|
|
1171
|
-
| **Classic PAT** | Legacy token type with broad, scope-based permissions |
|
|
1172
|
-
| **Release** | A packaged version of software at a specific point in time |
|
|
1173
|
-
| **Release Asset** | A file attached to a release (binary, source code, etc.) |
|
|
1174
|
-
| **Rate Limiting** | GitHub's mechanism to prevent API abuse by limiting requests |
|
|
1175
|
-
| **Octokit** | Official GitHub SDK for JavaScript/TypeScript |
|
|
1176
|
-
| **Exponential Backoff** | Retry strategy where wait time increases exponentially |
|
|
1177
|
-
| **Streaming** | Processing data in chunks rather than loading all into memory |
|
|
1178
|
-
| **gh CLI** | Official GitHub command-line tool |
|
|
1179
|
-
| **OAuth** | Authorization framework for delegated access |
|
|
1180
|
-
| **GitHub App** | Server-to-server integration with GitHub |
|
|
1181
|
-
| **Scope** | Permission level granted to a token |
|
|
1182
|
-
| **Secondary Rate Limit** | Additional limits on rapid, resource-intensive requests |
|
|
1183
|
-
|
|
1184
|
-
### B. Token Permission Matrix
|
|
1185
|
-
|
|
1186
|
-
#### Fine-Grained PAT Permissions for Common Operations
|
|
1187
|
-
|
|
1188
|
-
| Operation | Required Permissions | Notes |
|
|
1189
|
-
|-----------|---------------------|-------|
|
|
1190
|
-
| List public releases | None (unauthenticated) | - |
|
|
1191
|
-
| List private releases | Contents: Read, Metadata: Read | Repository must be accessible |
|
|
1192
|
-
| Download public asset | None (unauthenticated) | Can use browser_download_url |
|
|
1193
|
-
| Download private asset | Contents: Read, Metadata: Read | Must use API with token |
|
|
1194
|
-
| Create release | Contents: Write | Also needs repo access |
|
|
1195
|
-
| Delete release | Contents: Write | Requires write access |
|
|
1196
|
-
| List repos | None or Metadata: Read | Depends on visibility |
|
|
1197
|
-
|
|
1198
|
-
#### Classic PAT Scopes
|
|
1199
|
-
|
|
1200
|
-
| Scope | Access Level | Use Case |
|
|
1201
|
-
|-------|-------------|----------|
|
|
1202
|
-
| `repo` | Full repository access | Private repo operations |
|
|
1203
|
-
| `public_repo` | Public repository access | Public repo operations |
|
|
1204
|
-
| `repo:status` | Commit status access | CI/CD integrations |
|
|
1205
|
-
| `repo_deployment` | Deployment status | Deployment tools |
|
|
1206
|
-
| `read:packages` | Package read access | Package downloads |
|
|
1207
|
-
|
|
1208
|
-
### C. Error Code Reference
|
|
1209
|
-
|
|
1210
|
-
| Status Code | Meaning | Common Causes | Solution |
|
|
1211
|
-
|------------|---------|---------------|----------|
|
|
1212
|
-
| 200 | Success | - | - |
|
|
1213
|
-
| 302 | Redirect | Asset download redirect | Follow redirect |
|
|
1214
|
-
| 401 | Unauthorized | Invalid/expired token | Refresh or recreate token |
|
|
1215
|
-
| 403 | Forbidden | Rate limit or insufficient permissions | Check rate limits, verify scopes |
|
|
1216
|
-
| 404 | Not Found | Repo doesn't exist, private, or no releases | Verify repo name, check permissions |
|
|
1217
|
-
| 422 | Validation Failed | Invalid parameters | Check request parameters |
|
|
1218
|
-
| 500 | Server Error | GitHub server issue | Retry with backoff |
|
|
1219
|
-
| 502/503 | Service Unavailable | GitHub temporarily down | Retry with backoff |
|
|
1220
|
-
|
|
1221
|
-
### D. TypeScript Type Definitions
|
|
1222
|
-
|
|
1223
|
-
```typescript
|
|
1224
|
-
// Core types for GitHub release operations
|
|
1225
|
-
import { Octokit } from 'octokit';
|
|
1226
|
-
|
|
1227
|
-
interface ReleaseAsset {
|
|
1228
|
-
id: number;
|
|
1229
|
-
name: string;
|
|
1230
|
-
size: number;
|
|
1231
|
-
browser_download_url: string;
|
|
1232
|
-
content_type: string;
|
|
1233
|
-
created_at: string;
|
|
1234
|
-
updated_at: string;
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
interface Release {
|
|
1238
|
-
id: number;
|
|
1239
|
-
tag_name: string;
|
|
1240
|
-
name: string | null;
|
|
1241
|
-
body: string | null;
|
|
1242
|
-
draft: boolean;
|
|
1243
|
-
prerelease: boolean;
|
|
1244
|
-
created_at: string;
|
|
1245
|
-
published_at: string | null;
|
|
1246
|
-
assets: ReleaseAsset[];
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
interface AuthConfig {
|
|
1250
|
-
token?: string;
|
|
1251
|
-
useGitHubCLI?: boolean;
|
|
1252
|
-
useEnvVar?: boolean;
|
|
1253
|
-
promptIfMissing?: boolean;
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
interface DownloadOptions {
|
|
1257
|
-
owner: string;
|
|
1258
|
-
repo: string;
|
|
1259
|
-
assetId: number;
|
|
1260
|
-
outputPath: string;
|
|
1261
|
-
onProgress?: (transferred: number, total: number) => void;
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
type AuthMethod = 'github-cli' | 'env-var' | 'stored' | 'prompt';
|
|
1265
|
-
|
|
1266
|
-
interface AuthResult {
|
|
1267
|
-
method: AuthMethod;
|
|
1268
|
-
token: string;
|
|
1269
|
-
username?: string;
|
|
1270
|
-
}
|
|
1271
|
-
```
|
|
1272
|
-
|
|
1273
|
-
### E. Environment Variables Reference
|
|
1274
|
-
|
|
1275
|
-
```bash
|
|
1276
|
-
# GitHub API Authentication
|
|
1277
|
-
GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx # Personal Access Token
|
|
1278
|
-
GH_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx # Alternative name (used by gh CLI)
|
|
1279
|
-
|
|
1280
|
-
# GitHub API Configuration
|
|
1281
|
-
GITHUB_API_URL=https://api.github.com # Custom API endpoint (for Enterprise)
|
|
1282
|
-
GITHUB_ENTERPRISE=true # Enable enterprise mode
|
|
1283
|
-
|
|
1284
|
-
# Proxy Configuration (if needed)
|
|
1285
|
-
HTTP_PROXY=http://proxy.example.com:8080
|
|
1286
|
-
HTTPS_PROXY=http://proxy.example.com:8080
|
|
1287
|
-
NO_PROXY=localhost,127.0.0.1
|
|
1288
|
-
|
|
1289
|
-
# Application Configuration
|
|
1290
|
-
LOG_LEVEL=info # Logging level
|
|
1291
|
-
DOWNLOAD_DIR=./downloads # Default download directory
|
|
1292
|
-
MAX_RETRIES=3 # Maximum retry attempts
|
|
1293
|
-
RETRY_DELAY=1000 # Initial retry delay (ms)
|
|
1294
|
-
```
|
|
1295
|
-
|
|
1296
|
-
### F. CLI Command Reference
|
|
1297
|
-
|
|
1298
|
-
```bash
|
|
1299
|
-
# GitHub CLI Authentication
|
|
1300
|
-
gh auth login # Interactive login
|
|
1301
|
-
gh auth login --with-token # Login with token from stdin
|
|
1302
|
-
gh auth status # Check auth status
|
|
1303
|
-
gh auth token # Print current token
|
|
1304
|
-
gh auth logout # Logout
|
|
1305
|
-
|
|
1306
|
-
# GitHub CLI Release Operations
|
|
1307
|
-
gh release list --repo owner/repo # List releases
|
|
1308
|
-
gh release view v1.0.0 # View release details
|
|
1309
|
-
gh release download v1.0.0 # Download release assets
|
|
1310
|
-
gh release download --pattern "*.zip" # Download matching assets
|
|
1311
|
-
|
|
1312
|
-
# Using gh API directly
|
|
1313
|
-
gh api repos/owner/repo/releases # List releases via API
|
|
1314
|
-
gh api repos/owner/repo/releases/latest # Get latest release
|
|
1315
|
-
```
|
|
1316
|
-
|
|
1317
|
-
---
|
|
1318
|
-
|
|
1319
|
-
## Conclusion
|
|
1320
|
-
|
|
1321
|
-
This research provides a comprehensive foundation for implementing GitHub release download functionality in Node.js applications. The recommended stack is:
|
|
1322
|
-
|
|
1323
|
-
1. **Octokit.js** for GitHub API interactions
|
|
1324
|
-
2. **Fine-grained Personal Access Tokens** for authentication
|
|
1325
|
-
3. **GitHub CLI detection** for seamless user experience
|
|
1326
|
-
4. **System credential stores** for secure token storage
|
|
1327
|
-
5. **Exponential backoff retry** for resilience
|
|
1328
|
-
6. **Streaming downloads** for performance
|
|
1329
|
-
|
|
1330
|
-
The code examples provided are production-ready and follow current best practices as of 2025. Security considerations have been prioritized throughout, with emphasis on minimal permissions, secure storage, and proper error handling.
|
|
1331
|
-
|
|
1332
|
-
For CLI tool development, the authentication flow that checks GitHub CLI → environment variables → stored credentials → user prompt provides the best user experience while maintaining security standards.
|
|
1333
|
-
|
|
1334
|
-
---
|
|
1335
|
-
|
|
1336
|
-
**Document Version:** 1.0
|
|
1337
|
-
**Last Updated:** October 8, 2025
|
|
1338
|
-
**Research Conducted By:** AI Research Assistant
|
|
1339
|
-
**Next Review Date:** January 2026
|