Initial commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
IMPLEMENTATION_PLAN.md
|
||||
220
.opencode/skills/gh-celebs/SKILL.md
Normal file
220
.opencode/skills/gh-celebs/SKILL.md
Normal file
@@ -0,0 +1,220 @@
|
||||
---
|
||||
name: gh-celebs
|
||||
description: This skill should be used when the user asks to "search GitHub repositories", "find popular GitHub repos", "look up GitHub projects by stars", "find the most starred repos", or needs to discover popular open-source projects. Make sure to use this skill whenever the user mentions GitHub repository search, popular repositories, star count rankings, or wants to use the gh-celebs CLI tool. Also use for updating cached repository data or configuring the tool with GitHub tokens for higher API rate limits.
|
||||
---
|
||||
|
||||
# gh-celebs Skill
|
||||
|
||||
Execute GitHub repository searches using the gh-celebs CLI tool to find popular repositories by star count.
|
||||
|
||||
## Overview
|
||||
|
||||
gh-celebs is a fast, non-interactive CLI tool for searching GitHub repositories by popularity (stars). It uses a local SQLite database that learns and grows as you search.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Common Commands
|
||||
|
||||
```bash
|
||||
# Search for top React repositories
|
||||
gh-celebs react
|
||||
|
||||
# Get top 10 machine learning repos
|
||||
gh-celebs "machine learning" --limit 10
|
||||
|
||||
# JSON output for processing
|
||||
gh-celebs "rust web framework" --limit 20 --json
|
||||
|
||||
# Update all cached repositories
|
||||
gh-celebs update
|
||||
|
||||
# Update a specific repository
|
||||
gh-celebs update facebook/react
|
||||
```
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### Direct Search
|
||||
|
||||
When the user asks to find popular repositories, translate their request into a gh-celebs command:
|
||||
|
||||
- "Find me popular React repos" → `gh-celebs react --limit 5`
|
||||
- "Show me the top 15 Python projects" → `gh-celebs python --limit 15`
|
||||
- "What are the most starred Go projects?" → `gh-celebs go --limit 10`
|
||||
|
||||
### Output Format Selection
|
||||
|
||||
**Use plain text (default) when:**
|
||||
- User wants to see results immediately
|
||||
- Displaying results in conversation
|
||||
- User asks "show me" or "find me"
|
||||
|
||||
**Use JSON (--json) when:**
|
||||
- User needs to process/filter results
|
||||
- User mentions "export", "save", or "output to file"
|
||||
- User wants programmatic access to results
|
||||
- User mentions filtering, sorting, or extracting data
|
||||
|
||||
### Limit Management
|
||||
|
||||
Default to --limit 5 for simple requests. Use higher limits when:
|
||||
- User explicitly asks for more ("top 20", "first 50")
|
||||
- User wants comprehensive results
|
||||
- JSON output requested (implies processing)
|
||||
|
||||
### Search Query Construction
|
||||
|
||||
Construct queries based on user intent:
|
||||
|
||||
| User Request | Query |
|
||||
|-------------|-------|
|
||||
| "React" | `react` |
|
||||
| "React web framework" | `react web framework` |
|
||||
| "Machine learning in Python" | `machine learning python` |
|
||||
| "Rust CLI tools" | `rust cli` |
|
||||
|
||||
Always wrap multi-word queries in quotes when passing to gh-celebs.
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
### Check Installation
|
||||
|
||||
Before executing commands, verify gh-celebs is installed:
|
||||
|
||||
```bash
|
||||
gh-celebs --help
|
||||
```
|
||||
|
||||
If not installed, see `references/setup-guide.md` for installation instructions.
|
||||
|
||||
### Configuration
|
||||
|
||||
Check for config file at `~/.gh-celebs/config.toml`:
|
||||
|
||||
```bash
|
||||
ls ~/.gh-celebs/config.toml
|
||||
```
|
||||
|
||||
If missing, offer to set up configuration. See `references/setup-guide.md` for details on GitHub token configuration for higher API rate limits.
|
||||
|
||||
## Update Commands
|
||||
|
||||
### Update All Cached Repos
|
||||
|
||||
When user asks to "update" or "refresh" their database:
|
||||
|
||||
```bash
|
||||
gh-celebs update
|
||||
```
|
||||
|
||||
### Update Specific Repository
|
||||
|
||||
When user mentions a specific repo:
|
||||
|
||||
```bash
|
||||
gh-celebs update owner/repo-name
|
||||
```
|
||||
|
||||
Examples:
|
||||
- `gh-celebs update facebook/react`
|
||||
- `gh-celebs update rust-lang/rust`
|
||||
|
||||
## Rate Limits
|
||||
|
||||
gh-celebs uses the GitHub Search API with these limits:
|
||||
- **Unauthenticated**: 10 requests per minute
|
||||
- **Authenticated**: 30 requests per minute
|
||||
|
||||
The tool displays remaining API calls: `[X/10]` or `[X/30]`
|
||||
|
||||
If rate limited, see `references/troubleshooting.md`.
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Exit Codes
|
||||
|
||||
- `0`: Success
|
||||
- `1`: General error
|
||||
- `2`: Rate limited
|
||||
|
||||
### Handling Errors
|
||||
|
||||
1. If command fails with rate limit (exit code 2):
|
||||
- Check if user has GitHub token configured
|
||||
- Suggest adding token to `~/.gh-celebs/config.toml`
|
||||
- See `references/troubleshooting.md`
|
||||
|
||||
2. If database errors occur:
|
||||
- Suggest running `gh-celebs update` to refresh
|
||||
- Check disk space at `~/.gh-celebs/`
|
||||
|
||||
3. If no results found:
|
||||
- Try broader search terms
|
||||
- Check spelling
|
||||
- Try related keywords
|
||||
|
||||
## References
|
||||
|
||||
- `references/setup-guide.md` - Installation and configuration
|
||||
- `references/examples.md` - Common search patterns and examples
|
||||
- `references/troubleshooting.md` - Rate limits, errors, and solutions
|
||||
- `scripts/check-install.sh` - Verify gh-celebs installation
|
||||
- `scripts/setup-config.sh` - Initialize configuration with GitHub token
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Database Location
|
||||
|
||||
- **Path**: `~/.gh-celebs/repos.db`
|
||||
- **Config**: `~/.gh-celebs/config.toml`
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. Search local database first
|
||||
2. If insufficient results, query GitHub API
|
||||
3. Cache API results to database
|
||||
4. Merge and sort by stars
|
||||
5. Return top N results
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Query Specificity**: More specific queries yield better results
|
||||
2. **Rate Limit Awareness**: Check remaining API calls in output
|
||||
3. **Regular Updates**: Run `gh-celebs update` periodically for fresh data
|
||||
4. **Token Configuration**: Set up GitHub token for higher rate limits
|
||||
5. **Output Format**: Use JSON for scripting, plain text for display
|
||||
|
||||
## Example Workflows
|
||||
|
||||
### Finding Libraries for a Project
|
||||
|
||||
```bash
|
||||
# Search for React UI libraries
|
||||
gh-celebs "react ui component" --limit 20
|
||||
|
||||
# Get JSON output for filtering
|
||||
gh-celebs "react ui component" --limit 20 --json > ui-libs.json
|
||||
```
|
||||
|
||||
### Keeping Data Fresh
|
||||
|
||||
```bash
|
||||
# Update all cached repos weekly
|
||||
gh-celebs update
|
||||
|
||||
# Check rate limits
|
||||
gh-celebs "test" --limit 1
|
||||
```
|
||||
|
||||
### Exploring Popular Tech Stacks
|
||||
|
||||
```bash
|
||||
# Top JavaScript frameworks
|
||||
gh-celebs "javascript framework" --limit 10
|
||||
|
||||
# Popular Rust tools
|
||||
gh-celebs "rust cli" --limit 15
|
||||
|
||||
# Python ML libraries
|
||||
gh-celebs "python machine learning" --limit 20
|
||||
```
|
||||
311
.opencode/skills/gh-celebs/references/examples.md
Normal file
311
.opencode/skills/gh-celebs/references/examples.md
Normal file
@@ -0,0 +1,311 @@
|
||||
# gh-celebs Examples
|
||||
|
||||
Common search patterns and real-world usage examples.
|
||||
|
||||
## Basic Searches
|
||||
|
||||
### Popular JavaScript Frameworks
|
||||
|
||||
```bash
|
||||
# Top 5 React repositories (default)
|
||||
gh-celebs react
|
||||
|
||||
# Top 10 Vue projects
|
||||
gh-celebs vue --limit 10
|
||||
|
||||
# Angular ecosystem
|
||||
gh-celebs angular --limit 8
|
||||
```
|
||||
|
||||
### Language-Specific Searches
|
||||
|
||||
```bash
|
||||
# Rust projects
|
||||
gh-celebs rust --limit 10
|
||||
|
||||
# Go (Golang) tools
|
||||
gh-celebs golang --limit 15
|
||||
|
||||
# Python data science
|
||||
gh-celebs "python data" --limit 12
|
||||
|
||||
# TypeScript utilities
|
||||
gh-celebs typescript --limit 20
|
||||
```
|
||||
|
||||
### Topic-Based Searches
|
||||
|
||||
```bash
|
||||
# Machine learning
|
||||
gh-celebs "machine learning" --limit 10
|
||||
|
||||
# CLI tools
|
||||
gh-celebs "cli tool" --limit 15
|
||||
|
||||
# Web frameworks
|
||||
gh-celebs "web framework" --limit 10
|
||||
|
||||
# Static site generators
|
||||
gh-celebs "static site" --limit 10
|
||||
```
|
||||
|
||||
## Advanced Searches
|
||||
|
||||
### Multi-Word Queries
|
||||
|
||||
Always quote multi-word searches:
|
||||
|
||||
```bash
|
||||
# Good
|
||||
gh-celebs "react native" --limit 10
|
||||
gh-celebs "vs code" --limit 5
|
||||
|
||||
# Bad (interprets as separate arguments)
|
||||
# gh-celebs react native --limit 10
|
||||
```
|
||||
|
||||
### Combining Terms
|
||||
|
||||
```bash
|
||||
# React + testing
|
||||
gh-celebs "react testing" --limit 10
|
||||
|
||||
# Docker + orchestration
|
||||
gh-celebs "docker kubernetes" --limit 8
|
||||
|
||||
# Database + specific language
|
||||
gh-celebs "postgres rust" --limit 5
|
||||
```
|
||||
|
||||
### JSON Output Examples
|
||||
|
||||
```bash
|
||||
# Save to file for processing
|
||||
gh-celebs "react" --limit 20 --json > react-repos.json
|
||||
|
||||
# Pretty print with jq
|
||||
gh-celebs "api framework" --limit 10 --json | jq '.results[] | {name: .full_name, stars}'
|
||||
|
||||
# Count languages in results
|
||||
gh-celebs "web framework" --limit 50 --json | jq '[.results[].language] | unique | length'
|
||||
```
|
||||
|
||||
## Domain-Specific Examples
|
||||
|
||||
### Frontend Development
|
||||
|
||||
```bash
|
||||
# CSS frameworks
|
||||
gh-celebs "css framework" --limit 10
|
||||
|
||||
# Component libraries
|
||||
gh-celebs "component library" --limit 15
|
||||
|
||||
# State management
|
||||
gh-celebs "state management" --limit 8
|
||||
|
||||
# Build tools
|
||||
gh-celebs "bundler" --limit 10
|
||||
```
|
||||
|
||||
### Backend Development
|
||||
|
||||
```bash
|
||||
# API frameworks
|
||||
gh-celebs "api framework" --limit 15
|
||||
|
||||
# Authentication libraries
|
||||
gh-celebs "authentication" --limit 10
|
||||
|
||||
# Database tools
|
||||
gh-celebs "database" --limit 12
|
||||
|
||||
# GraphQL tools
|
||||
gh-celebs "graphql" --limit 10
|
||||
```
|
||||
|
||||
### DevOps & Infrastructure
|
||||
|
||||
```bash
|
||||
# CI/CD tools
|
||||
gh-celebs "ci cd" --limit 10
|
||||
|
||||
# Infrastructure as Code
|
||||
gh-celebs "terraform" --limit 8
|
||||
|
||||
# Monitoring tools
|
||||
gh-celebs "monitoring" --limit 10
|
||||
|
||||
# Container orchestration
|
||||
gh-celebs "kubernetes" --limit 15
|
||||
```
|
||||
|
||||
### Data Science & AI
|
||||
|
||||
```bash
|
||||
# Deep learning frameworks
|
||||
gh-celebs "deep learning" --limit 10
|
||||
|
||||
# NLP libraries
|
||||
gh-celebs "natural language" --limit 12
|
||||
|
||||
# Data visualization
|
||||
gh-celebs "data visualization" --limit 10
|
||||
|
||||
# Jupyter notebooks
|
||||
gh-celebs "jupyter" --limit 8
|
||||
```
|
||||
|
||||
## Practical Workflows
|
||||
|
||||
### Comparing Frameworks
|
||||
|
||||
```bash
|
||||
# Save multiple results for comparison
|
||||
gh-celebs "react" --limit 5 --json > react.json
|
||||
gh-celebs "vue" --limit 5 --json > vue.json
|
||||
gh-celebs "angular" --limit 5 --json > angular.json
|
||||
|
||||
# Compare with jq
|
||||
cat react.json | jq '.results[0].stars'
|
||||
cat vue.json | jq '.results[0].stars'
|
||||
```
|
||||
|
||||
### Finding Alternatives
|
||||
|
||||
```bash
|
||||
# jQuery alternatives
|
||||
gh-celebs "jquery alternative" --limit 10
|
||||
|
||||
# Bootstrap alternatives
|
||||
gh-celebs "css framework" --limit 15
|
||||
|
||||
# Express alternatives
|
||||
gh-celebs "nodejs framework" --limit 10
|
||||
```
|
||||
|
||||
### Discovering New Tools
|
||||
|
||||
```bash
|
||||
# Recently popular
|
||||
gh-celebs "new framework" --limit 10
|
||||
|
||||
# Trending topics
|
||||
gh-celebs "emerging tech" --limit 8
|
||||
|
||||
# Experimental projects
|
||||
gh-celebs "experimental" --limit 10
|
||||
```
|
||||
|
||||
### Quick Reference
|
||||
|
||||
```bash
|
||||
# Top projects overall
|
||||
gh-celebs "" --limit 5
|
||||
|
||||
# Top this year (approximate)
|
||||
gh-celebs "created:2026" --limit 10
|
||||
|
||||
# Most popular by specific owner
|
||||
gh-celebs "facebook/" --limit 10
|
||||
gh-celebs "google/" --limit 10
|
||||
```
|
||||
|
||||
## Update Examples
|
||||
|
||||
### Refresh All Data
|
||||
|
||||
```bash
|
||||
# Update all cached repositories
|
||||
gh-celebs update
|
||||
```
|
||||
|
||||
### Update Specific Projects
|
||||
|
||||
```bash
|
||||
# Update popular frameworks
|
||||
gh-celebs update facebook/react
|
||||
gh-celebs update vuejs/vue
|
||||
gh-celebs update angular/angular
|
||||
|
||||
# Update tools you use
|
||||
gh-celebs update cli/cli
|
||||
gh-celebs update rust-lang/cargo
|
||||
```
|
||||
|
||||
### Scheduled Updates
|
||||
|
||||
```bash
|
||||
# Add to crontab for weekly updates
|
||||
0 9 * * 1 gh-celebs update
|
||||
```
|
||||
|
||||
## Error Handling Examples
|
||||
|
||||
### Testing Rate Limits
|
||||
|
||||
```bash
|
||||
# Quick test that doesn't use API calls
|
||||
gh-celebs update facebook/react
|
||||
```
|
||||
|
||||
### Checking Database
|
||||
|
||||
```bash
|
||||
# Search that uses local cache
|
||||
gh-celebs "react" --limit 100
|
||||
```
|
||||
|
||||
## Sample Output
|
||||
|
||||
### Plain Text
|
||||
|
||||
```
|
||||
Searching for "react"...
|
||||
API: [8/10 calls remaining]
|
||||
|
||||
1. facebook/react (220,473 ⭐) - Updated 2026-01-15
|
||||
https://github.com/facebook/react
|
||||
A declarative, efficient, and flexible JavaScript library for building user interfaces.
|
||||
|
||||
✓ URL copied to clipboard: https://github.com/facebook/react
|
||||
```
|
||||
|
||||
### JSON Output
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "react",
|
||||
"limit": 5,
|
||||
"api_remaining": 8,
|
||||
"api_limit": 10,
|
||||
"results": [
|
||||
{
|
||||
"rank": 1,
|
||||
"full_name": "facebook/react",
|
||||
"stars": 220473,
|
||||
"html_url": "https://github.com/facebook/react",
|
||||
"description": "A declarative, efficient, and flexible JavaScript library...",
|
||||
"language": "JavaScript"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Tips & Tricks
|
||||
|
||||
1. **Start Broad**: Use simple terms first, then narrow down
|
||||
2. **Check Cache**: Repeated searches use local database, not API
|
||||
3. **JSON for Scripts**: Use `--json` flag for automation
|
||||
4. **Update Regularly**: Run `gh-celebs update` weekly for fresh data
|
||||
5. **Token Benefits**: Configure GitHub token for 3x rate limit
|
||||
|
||||
## Common Patterns
|
||||
|
||||
| Goal | Command |
|
||||
|------|---------|
|
||||
| Find top 5 | `gh-celebs <query>` |
|
||||
| Find top N | `gh-celebs <query> --limit N` |
|
||||
| Process results | `gh-celebs <query> --json` |
|
||||
| Update all | `gh-celebs update` |
|
||||
| Update one | `gh-celebs update owner/repo` |
|
||||
211
.opencode/skills/gh-celebs/references/setup-guide.md
Normal file
211
.opencode/skills/gh-celebs/references/setup-guide.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# gh-celebs Setup Guide
|
||||
|
||||
Complete installation and configuration instructions for the gh-celebs CLI tool.
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Rust toolchain (1.70+)
|
||||
- GitHub account (optional, for higher rate limits)
|
||||
|
||||
### Install via Cargo
|
||||
|
||||
```bash
|
||||
cargo install gh-celebs
|
||||
```
|
||||
|
||||
### Verify Installation
|
||||
|
||||
```bash
|
||||
gh-celebs --help
|
||||
```
|
||||
|
||||
Expected output shows version and available commands.
|
||||
|
||||
### Uninstall
|
||||
|
||||
```bash
|
||||
cargo uninstall gh-celebs
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Config File Location
|
||||
|
||||
**Linux/macOS:**
|
||||
```
|
||||
~/.gh-celebs/config.toml
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
%USERPROFILE%\.gh-celebs\config.toml
|
||||
```
|
||||
|
||||
### Creating Config Directory
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.gh-celebs
|
||||
```
|
||||
|
||||
### GitHub Token (Optional but Recommended)
|
||||
|
||||
Generate a token for higher rate limits (30/min vs 10/min).
|
||||
|
||||
#### Step 1: Generate Token
|
||||
|
||||
1. Go to https://github.com/settings/tokens
|
||||
2. Click "Generate new token (classic)"
|
||||
3. Give it a name like "gh-celebs"
|
||||
4. **No scopes required** (token is only for rate limit increase)
|
||||
5. Click "Generate token"
|
||||
6. Copy the token immediately
|
||||
|
||||
#### Step 2: Add to Config
|
||||
|
||||
Create `~/.gh-celebs/config.toml`:
|
||||
|
||||
```toml
|
||||
[github]
|
||||
token = "ghp_your_token_here"
|
||||
```
|
||||
|
||||
#### Step 3: Test Token
|
||||
|
||||
```bash
|
||||
gh-celebs test --limit 1
|
||||
```
|
||||
|
||||
Check output for `[X/30]` instead of `[X/10]`.
|
||||
|
||||
### Token Security
|
||||
|
||||
- Never commit tokens to version control
|
||||
- Use `.gitignore` for config files
|
||||
- Tokens have no expiration by default
|
||||
- Rotate tokens periodically
|
||||
|
||||
## Database Location
|
||||
|
||||
The SQLite database is automatically created at:
|
||||
|
||||
```
|
||||
~/.gh-celebs/repos.db
|
||||
```
|
||||
|
||||
### Backup Database
|
||||
|
||||
```bash
|
||||
cp ~/.gh-celebs/repos.db ~/.gh-celebs/repos.db.backup
|
||||
```
|
||||
|
||||
### Reset Database
|
||||
|
||||
To start fresh:
|
||||
|
||||
```bash
|
||||
rm ~/.gh-celebs/repos.db
|
||||
```
|
||||
|
||||
Next search will recreate the database.
|
||||
|
||||
### Database Size
|
||||
|
||||
Typical sizes:
|
||||
- Empty: ~24KB
|
||||
- 1,000 repos: ~500KB
|
||||
- 10,000 repos: ~5MB
|
||||
|
||||
## Troubleshooting Installation
|
||||
|
||||
### Cargo Not Found
|
||||
|
||||
**Problem:** `command not found: cargo`
|
||||
|
||||
**Solution:** Install Rust:
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
```
|
||||
|
||||
### Build Errors
|
||||
|
||||
**Problem:** Compilation fails
|
||||
|
||||
**Solutions:**
|
||||
1. Update Rust: `rustup update`
|
||||
2. Clear cache: `cargo clean`
|
||||
3. Reinstall: `cargo install gh-celebs --force`
|
||||
|
||||
### Permission Denied
|
||||
|
||||
**Problem:** Cannot write to `~/.gh-celebs/`
|
||||
|
||||
**Solution:** Check directory permissions:
|
||||
```bash
|
||||
ls -la ~/.gh-celebs/
|
||||
chmod 755 ~/.gh-celebs/
|
||||
```
|
||||
|
||||
### Windows-Specific Issues
|
||||
|
||||
**Problem:** Path not recognized
|
||||
|
||||
**Solution:** Add to PATH manually:
|
||||
1. Find cargo bin: `cargo --print home`\bin
|
||||
2. Add to System Environment Variables
|
||||
3. Restart terminal
|
||||
|
||||
## Platform-Specific Notes
|
||||
|
||||
### macOS
|
||||
|
||||
Install cargo via Homebrew if needed:
|
||||
```bash
|
||||
brew install rust
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
Most distributions include rust in package manager:
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt install cargo
|
||||
|
||||
# Fedora
|
||||
sudo dnf install cargo
|
||||
|
||||
# Arch
|
||||
sudo pacman -S rust
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
Use Git Bash or PowerShell. Git Bash provides better compatibility with Unix-style paths.
|
||||
|
||||
## Post-Installation Checklist
|
||||
|
||||
- [ ] gh-celebs command available in PATH
|
||||
- [ ] Database directory created (`~/.gh-celebs/`)
|
||||
- [ ] Test search works: `gh-celebs test --limit 1`
|
||||
- [ ] (Optional) GitHub token configured
|
||||
- [ ] (Optional) Token verified: `[X/30]` in output
|
||||
|
||||
## Next Steps
|
||||
|
||||
After installation:
|
||||
|
||||
1. Run your first search: `gh-celebs "your topic"`
|
||||
2. Check `references/examples.md` for usage patterns
|
||||
3. See `references/troubleshooting.md` for common issues
|
||||
|
||||
## Updating gh-celebs
|
||||
|
||||
To update to latest version:
|
||||
|
||||
```bash
|
||||
cargo install gh-celebs --force
|
||||
```
|
||||
|
||||
This preserves your database and config.
|
||||
469
.opencode/skills/gh-celebs/references/troubleshooting.md
Normal file
469
.opencode/skills/gh-celebs/references/troubleshooting.md
Normal file
@@ -0,0 +1,469 @@
|
||||
# gh-celebs Troubleshooting
|
||||
|
||||
Common issues, errors, and their solutions.
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### Understanding Rate Limits
|
||||
|
||||
GitHub Search API limits:
|
||||
- **Unauthenticated**: 10 requests per minute
|
||||
- **Authenticated**: 30 requests per minute
|
||||
|
||||
Rate limit status appears in output as `[X/10]` or `[X/30]`.
|
||||
|
||||
### Rate Limit Exceeded (Exit Code 2)
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: Rate limit exceeded. Try again in 60 seconds.
|
||||
API: [0/10 calls remaining]
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Wait 60 seconds** for reset
|
||||
2. **Configure GitHub token** for 3x limits
|
||||
3. **Use cached results** from local database
|
||||
|
||||
### Configuring GitHub Token
|
||||
|
||||
See `references/setup-guide.md` for detailed token setup.
|
||||
|
||||
Quick setup:
|
||||
```bash
|
||||
mkdir -p ~/.gh-celebs
|
||||
cat > ~/.gh-celebs/config.toml << 'EOF'
|
||||
[github]
|
||||
token = "your_github_token_here"
|
||||
EOF
|
||||
```
|
||||
|
||||
### Checking Rate Limit Status
|
||||
|
||||
```bash
|
||||
# Make any search to see rate limit
|
||||
git celebs test --limit 1
|
||||
```
|
||||
|
||||
## Database Issues
|
||||
|
||||
### Database Locked
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: database is locked
|
||||
```
|
||||
|
||||
**Causes:**
|
||||
- Multiple gh-celebs processes running
|
||||
- Database corruption
|
||||
- Disk space issues
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Kill other gh-celebs processes:
|
||||
```bash
|
||||
ps aux | grep gh-celebs
|
||||
kill -9 <pid>
|
||||
```
|
||||
|
||||
2. Restart with fresh database:
|
||||
```bash
|
||||
mv ~/.gh-celebs/repos.db ~/.gh-celebs/repos.db.backup
|
||||
git celebs "test" --limit 1 # Recreates database
|
||||
```
|
||||
|
||||
### Database Corruption
|
||||
|
||||
**Symptoms:**
|
||||
- Unexpected crashes
|
||||
- Missing data
|
||||
- SQLite errors
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Backup and reset
|
||||
mv ~/.gh-celebs/repos.db ~/.gh-celebs/repos.db.corrupted
|
||||
# New database created automatically on next search
|
||||
```
|
||||
|
||||
### Disk Space Issues
|
||||
|
||||
**Check database size:**
|
||||
```bash
|
||||
ls -lh ~/.gh-celebs/repos.db
|
||||
```
|
||||
|
||||
**Typical sizes:**
|
||||
- 1,000 repos: ~500KB
|
||||
- 10,000 repos: ~5MB
|
||||
- 50,000 repos: ~25MB
|
||||
|
||||
**Clean up old data:**
|
||||
```bash
|
||||
rm ~/.gh-celebs/repos.db
|
||||
```
|
||||
|
||||
## Network Issues
|
||||
|
||||
### Connection Timeouts
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: request timed out
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Check internet connection:
|
||||
```bash
|
||||
ping github.com
|
||||
```
|
||||
|
||||
2. Retry with smaller limit:
|
||||
```bash
|
||||
gh-celebs "react" --limit 5
|
||||
```
|
||||
|
||||
3. Use cached results only:
|
||||
```bash
|
||||
# Search uses local database
|
||||
gh-celebs "react" --limit 100
|
||||
```
|
||||
|
||||
### SSL/Certificate Errors
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: SSL certificate problem
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Update system certificates:
|
||||
```bash
|
||||
# macOS
|
||||
brew install ca-certificates
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo update-ca-certificates
|
||||
|
||||
# RHEL/CentOS
|
||||
sudo update-ca-trust
|
||||
```
|
||||
|
||||
2. Check system time:
|
||||
```bash
|
||||
date
|
||||
```
|
||||
|
||||
## Search Issues
|
||||
|
||||
### No Results Found
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Searching for "xyz123abc"...
|
||||
No results found.
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Check spelling
|
||||
2. Try broader terms:
|
||||
```bash
|
||||
# Instead of
|
||||
gh-celebs "react-hooks-library"
|
||||
|
||||
# Try
|
||||
gh-celebs "react hooks"
|
||||
```
|
||||
|
||||
3. Check GitHub search works:
|
||||
```bash
|
||||
# Visit https://github.com/search?q=your+query
|
||||
```
|
||||
|
||||
### Unexpected Results
|
||||
|
||||
**Cause:** Local database may have stale data
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Update all cached repos
|
||||
gh-celebs update
|
||||
|
||||
# Or search with higher limit to get fresh API results
|
||||
gh-celebs "react" --limit 50
|
||||
```
|
||||
|
||||
## Configuration Issues
|
||||
|
||||
### Config File Not Found
|
||||
|
||||
**Symptoms:**
|
||||
- Token not recognized
|
||||
- Rate limits still at 10/min
|
||||
|
||||
**Check:**
|
||||
```bash
|
||||
ls -la ~/.gh-celebs/config.toml
|
||||
cat ~/.gh-celebs/config.toml
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
mkdir -p ~/.gh-celebs
|
||||
cat > ~/.gh-celebs/config.toml << 'EOF'
|
||||
[github]
|
||||
token = "your_token"
|
||||
EOF
|
||||
```
|
||||
|
||||
### Invalid Token
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: Bad credentials
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify token at https://github.com/settings/tokens
|
||||
2. Generate new token if expired
|
||||
3. Check token format: `ghp_xxxxxxxx`
|
||||
|
||||
### Permission Errors
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: Permission denied
|
||||
```
|
||||
|
||||
**Check directory permissions:**
|
||||
```bash
|
||||
ls -la ~/.gh-celebs/
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
chmod 755 ~/.gh-celebs/
|
||||
chmod 644 ~/.gh-celebs/config.toml
|
||||
```
|
||||
|
||||
## Update Command Issues
|
||||
|
||||
### Update Fails for Specific Repo
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: Repository not found: owner/repo
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Check repository exists:
|
||||
```bash
|
||||
# Visit https://github.com/owner/repo
|
||||
```
|
||||
|
||||
2. Verify exact name:
|
||||
```bash
|
||||
gh-celebs "owner/repo-name" --limit 1
|
||||
```
|
||||
|
||||
3. Remove from database (if renamed/deleted):
|
||||
```bash
|
||||
# Manual SQLite removal required
|
||||
sqlite3 ~/.gh-celebs/repos.db "DELETE FROM repos WHERE full_name='owner/repo';"
|
||||
```
|
||||
|
||||
### Bulk Update Hangs
|
||||
|
||||
**Symptoms:**
|
||||
- `gh-celebs update` takes very long
|
||||
- No progress indication
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Check number of cached repos:
|
||||
```bash
|
||||
sqlite3 ~/.gh-celebs/repos.db "SELECT COUNT(*) FROM repos;"
|
||||
```
|
||||
|
||||
2. Update in batches:
|
||||
```bash
|
||||
# Update specific repos instead
|
||||
gh-celebs update facebook/react
|
||||
gh-celebs update vuejs/vue
|
||||
```
|
||||
|
||||
3. Wait for rate limit reset if needed
|
||||
|
||||
## Platform-Specific Issues
|
||||
|
||||
### Windows Path Issues
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: path not found
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Use forward slashes or double backslashes:
|
||||
```bash
|
||||
# Good
|
||||
gh-celebs "query"
|
||||
|
||||
# Not needed - tool handles paths internally
|
||||
```
|
||||
|
||||
2. Check environment variables:
|
||||
```powershell
|
||||
$env:USERPROFILE
|
||||
```
|
||||
|
||||
### macOS Gatekeeper
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: cannot be opened because the developer cannot be verified
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Allow in Security & Privacy settings
|
||||
# Or install via cargo instead of binary
|
||||
```
|
||||
|
||||
### Linux Permission Denied
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: Permission denied
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Check if cargo bin is in PATH
|
||||
echo $PATH | grep cargo
|
||||
|
||||
# If not, add to ~/.bashrc:
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
```
|
||||
|
||||
## Performance Issues
|
||||
|
||||
### Slow Searches
|
||||
|
||||
**Causes:**
|
||||
- Large database
|
||||
- Network latency
|
||||
- Rate limiting delays
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Check database size:
|
||||
```bash
|
||||
ls -lh ~/.gh-celebs/repos.db
|
||||
```
|
||||
|
||||
2. Vacuum database:
|
||||
```bash
|
||||
sqlite3 ~/.gh-celebs/repos.db "VACUUM;"
|
||||
```
|
||||
|
||||
3. Use smaller limits:
|
||||
```bash
|
||||
gh-celebs "react" --limit 5
|
||||
```
|
||||
|
||||
### High Memory Usage
|
||||
|
||||
**Cause:** Large result sets
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Process in smaller batches
|
||||
gh-celebs "react" --limit 20 --json | jq '.results[]'
|
||||
```
|
||||
|
||||
## Exit Codes Reference
|
||||
|
||||
| Code | Meaning | Action |
|
||||
|------|---------|--------|
|
||||
| 0 | Success | None needed |
|
||||
| 1 | General error | Check error message |
|
||||
| 2 | Rate limited | Wait or add token |
|
||||
| 130 | Interrupted (Ctrl+C) | Retry if needed |
|
||||
|
||||
## Getting Help
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable verbose output:
|
||||
```bash
|
||||
RUST_LOG=debug gh-celebs "react" --limit 5
|
||||
```
|
||||
|
||||
### Check Installation
|
||||
|
||||
```bash
|
||||
gh-celebs --version
|
||||
gh-celebs --help
|
||||
```
|
||||
|
||||
### Community Resources
|
||||
|
||||
- GitHub Issues: Report bugs
|
||||
- GitHub Discussions: Ask questions
|
||||
- Documentation: See README.md
|
||||
|
||||
## Prevention
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Configure token early** - Avoid rate limit headaches
|
||||
2. **Regular updates** - Run `gh-celebs update` weekly
|
||||
3. **Backup database** - Before major updates
|
||||
4. **Monitor disk space** - Database grows with usage
|
||||
5. **Use specific queries** - Better results, fewer API calls
|
||||
|
||||
### Health Check Script
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
echo "gh-celebs Health Check"
|
||||
echo "======================"
|
||||
|
||||
# Check installation
|
||||
if command -v gh-celebs &> /dev/null; then
|
||||
echo "✓ gh-celebs installed"
|
||||
gh-celebs --version
|
||||
else
|
||||
echo "✗ gh-celebs not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check database
|
||||
if [ -f ~/.gh-celebs/repos.db ]; then
|
||||
echo "✓ Database exists"
|
||||
ls -lh ~/.gh-celebs/repos.db
|
||||
else
|
||||
echo "✗ Database not found"
|
||||
fi
|
||||
|
||||
# Check config
|
||||
if [ -f ~/.gh-celebs/config.toml ]; then
|
||||
echo "✓ Config file exists"
|
||||
else
|
||||
echo "⚠ Config file not found (optional)"
|
||||
fi
|
||||
|
||||
# Test search
|
||||
echo ""
|
||||
echo "Testing search..."
|
||||
gh-celebs "test" --limit 1
|
||||
```
|
||||
93
.opencode/skills/gh-celebs/scripts/check-install.sh
Executable file
93
.opencode/skills/gh-celebs/scripts/check-install.sh
Executable file
@@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
# check-install.sh - Verify gh-celebs installation and configuration
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo "Checking gh-celebs installation..."
|
||||
echo "=================================="
|
||||
echo ""
|
||||
|
||||
# Check if gh-celebs is installed
|
||||
if command -v gh-celebs &> /dev/null; then
|
||||
echo -e "${GREEN}✓${NC} gh-celebs is installed"
|
||||
gh-celebs --version 2>/dev/null || echo " Version: unknown"
|
||||
else
|
||||
echo -e "${RED}✗${NC} gh-celebs is not installed"
|
||||
echo ""
|
||||
echo "To install:"
|
||||
echo " cargo install gh-celebs"
|
||||
echo ""
|
||||
echo "Prerequisites:"
|
||||
echo " - Rust toolchain (cargo)"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Check database directory
|
||||
DB_DIR="$HOME/.gh-celebs"
|
||||
if [ -d "$DB_DIR" ]; then
|
||||
echo -e "${GREEN}✓${NC} Database directory exists: $DB_DIR"
|
||||
|
||||
# Check database file
|
||||
if [ -f "$DB_DIR/repos.db" ]; then
|
||||
DB_SIZE=$(ls -lh "$DB_DIR/repos.db" | awk '{print $5}')
|
||||
echo -e "${GREEN}✓${NC} Database file exists ($DB_SIZE)"
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} Database file not found (will be created on first search)"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} Database directory not found"
|
||||
echo " Creating: $DB_DIR"
|
||||
mkdir -p "$DB_DIR"
|
||||
echo -e "${GREEN}✓${NC} Created database directory"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Check config file
|
||||
CONFIG_FILE="$DB_DIR/config.toml"
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
echo -e "${GREEN}✓${NC} Config file exists"
|
||||
|
||||
# Check if token is configured
|
||||
if grep -q "token" "$CONFIG_FILE" 2>/dev/null; then
|
||||
echo -e "${GREEN}✓${NC} GitHub token configured"
|
||||
echo " Rate limit: 30 requests/minute"
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} No GitHub token configured"
|
||||
echo " Rate limit: 10 requests/minute"
|
||||
echo " To increase: Add token to ~/.gh-celebs/config.toml"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} Config file not found (optional)"
|
||||
echo " Location: ~/.gh-celebs/config.toml"
|
||||
echo " Token increases rate limit from 10 to 30/minute"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Test search functionality
|
||||
echo "Testing search functionality..."
|
||||
if gh-celebs "test" --limit 1 &> /dev/null; then
|
||||
echo -e "${GREEN}✓${NC} Search functionality works"
|
||||
else
|
||||
echo -e "${RED}✗${NC} Search test failed"
|
||||
echo " This might be due to:"
|
||||
echo " - Network connectivity issues"
|
||||
echo " - Rate limiting"
|
||||
echo " - GitHub API issues"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=================================="
|
||||
echo "Check complete!"
|
||||
|
||||
exit 0
|
||||
110
.opencode/skills/gh-celebs/scripts/setup-config.sh
Executable file
110
.opencode/skills/gh-celebs/scripts/setup-config.sh
Executable file
@@ -0,0 +1,110 @@
|
||||
#!/bin/bash
|
||||
# setup-config.sh - Initialize gh-celebs configuration with optional GitHub token
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Config directory and file
|
||||
CONFIG_DIR="$HOME/.gh-celebs"
|
||||
CONFIG_FILE="$CONFIG_DIR/config.toml"
|
||||
|
||||
echo "gh-celebs Configuration Setup"
|
||||
echo "============================="
|
||||
echo ""
|
||||
|
||||
# Create config directory
|
||||
if [ ! -d "$CONFIG_DIR" ]; then
|
||||
echo "Creating config directory..."
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
echo -e "${GREEN}✓${NC} Created: $CONFIG_DIR"
|
||||
else
|
||||
echo -e "${GREEN}✓${NC} Config directory exists: $CONFIG_DIR"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Check if config already exists
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
echo -e "${YELLOW}⚠${NC} Config file already exists: $CONFIG_FILE"
|
||||
read -p "Overwrite existing config? (y/N): " -n 1 -r
|
||||
echo ""
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Setup cancelled."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}GitHub Token Setup (Optional)${NC}"
|
||||
echo "=============================="
|
||||
echo ""
|
||||
echo "A GitHub token increases API rate limits:"
|
||||
echo " Without token: 10 requests/minute"
|
||||
echo " With token: 30 requests/minute"
|
||||
echo ""
|
||||
echo "To generate a token:"
|
||||
echo " 1. Visit: https://github.com/settings/tokens"
|
||||
echo " 2. Click 'Generate new token (classic)'"
|
||||
echo " 3. Give it a name (e.g., 'gh-celebs')"
|
||||
echo " 4. No scopes required (just for rate limit increase)"
|
||||
echo " 5. Copy the generated token"
|
||||
echo ""
|
||||
|
||||
# Ask for token
|
||||
read -p "Enter GitHub token (press Enter to skip): " token
|
||||
|
||||
# Create config file
|
||||
echo ""
|
||||
echo "Creating config file..."
|
||||
|
||||
if [ -n "$token" ]; then
|
||||
# Create config with token
|
||||
cat > "$CONFIG_FILE" << EOF
|
||||
[github]
|
||||
token = "$token"
|
||||
EOF
|
||||
echo -e "${GREEN}✓${NC} Config file created with token"
|
||||
echo " Rate limit: 30 requests/minute"
|
||||
else
|
||||
# Create empty config template
|
||||
cat > "$CONFIG_FILE" << 'EOF'
|
||||
[github]
|
||||
# Uncomment and add your token for higher rate limits
|
||||
# token = "ghp_your_token_here"
|
||||
EOF
|
||||
echo -e "${GREEN}✓${NC} Config file created (no token)"
|
||||
echo " Rate limit: 10 requests/minute"
|
||||
echo " To add token later, edit: $CONFIG_FILE"
|
||||
fi
|
||||
|
||||
# Set permissions
|
||||
chmod 600 "$CONFIG_FILE"
|
||||
echo " Permissions: 600 (owner read/write only)"
|
||||
|
||||
echo ""
|
||||
echo "=============================="
|
||||
echo -e "${GREEN}Setup complete!${NC}"
|
||||
echo ""
|
||||
echo "Config file: $CONFIG_FILE"
|
||||
echo ""
|
||||
|
||||
# Test the configuration
|
||||
echo "Testing configuration..."
|
||||
if command -v gh-celebs &> /dev/null; then
|
||||
if gh-celebs "test" --limit 1 &> /dev/null; then
|
||||
echo -e "${GREEN}✓${NC} Configuration test passed"
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} Test failed (may be network/rate limit)"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} gh-celebs not installed"
|
||||
echo " Install with: cargo install gh-celebs"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
1846
Cargo.lock
generated
Normal file
1846
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
Normal file
20
Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "gh-celebs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["gh-celebs"]
|
||||
description = "A fast CLI tool for searching GitHub repositories by popularity"
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.44", features = ["full"] }
|
||||
rusqlite = { version = "0.34", features = ["bundled", "chrono"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
anyhow = "1.0"
|
||||
directories = "6.0"
|
||||
toml = "0.8"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
urlencoding = "2.1"
|
||||
30
src/cli.rs
Normal file
30
src/cli.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "gh-celebs")]
|
||||
#[command(about = "A fast CLI tool for searching GitHub repositories by popularity")]
|
||||
#[command(version)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Commands>,
|
||||
|
||||
#[arg(help = "Search query", value_name = "QUERY")]
|
||||
pub query: Option<String>,
|
||||
|
||||
#[arg(short, long, default_value_t = 5, help = "Number of results to return")]
|
||||
pub limit: usize,
|
||||
|
||||
#[arg(long, help = "Output results as JSON")]
|
||||
pub json: bool,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
#[command(about = "Update cached repositories")]
|
||||
Update {
|
||||
#[arg(
|
||||
help = "Repository to update (format: owner/repo). If not specified, updates all cached repos."
|
||||
)]
|
||||
repo: Option<String>,
|
||||
},
|
||||
}
|
||||
53
src/config.rs
Normal file
53
src/config.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
pub github: GitHubConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct GitHubConfig {
|
||||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load(config_path: &Path) -> Result<Self> {
|
||||
if !config_path.exists() {
|
||||
return Ok(Self::default());
|
||||
}
|
||||
|
||||
let content = std::fs::read_to_string(config_path)
|
||||
.with_context(|| format!("Failed to read config file at {:?}", config_path))?;
|
||||
|
||||
let config: Config = toml::from_str(&content)
|
||||
.with_context(|| format!("Failed to parse config file at {:?}", config_path))?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn save(&self, config_path: &Path) -> Result<()> {
|
||||
let content = toml::to_string_pretty(self).context("Failed to serialize config")?;
|
||||
|
||||
if let Some(parent) = config_path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create config directory at {:?}", parent))?;
|
||||
}
|
||||
|
||||
std::fs::write(config_path, content)
|
||||
.with_context(|| format!("Failed to write config file at {:?}", config_path))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
github: GitHubConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
170
src/db.rs
Normal file
170
src/db.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
use crate::models::Repo;
|
||||
use anyhow::{Context, Result};
|
||||
use rusqlite::{params, Connection};
|
||||
use std::path::Path;
|
||||
|
||||
pub struct Database {
|
||||
conn: Connection,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub fn new(db_path: &Path) -> Result<Self> {
|
||||
let conn = Connection::open(db_path)
|
||||
.with_context(|| format!("Failed to open database at {:?}", db_path))?;
|
||||
|
||||
let db = Self { conn };
|
||||
db.init_schema()?;
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
fn init_schema(&self) -> Result<()> {
|
||||
self.conn
|
||||
.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS repos (
|
||||
id INTEGER PRIMARY KEY,
|
||||
full_name TEXT UNIQUE NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
owner TEXT NOT NULL,
|
||||
description TEXT,
|
||||
stars INTEGER NOT NULL,
|
||||
language TEXT,
|
||||
html_url TEXT NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_stars ON repos(stars DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_full_name ON repos(full_name);",
|
||||
)
|
||||
.context("Failed to initialize database schema")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn search(&self, query: &str, limit: i64) -> Result<Vec<Repo>> {
|
||||
let search_pattern = format!("%{}%", query);
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, full_name, name, owner, description, stars, language, html_url, updated_at
|
||||
FROM repos
|
||||
WHERE full_name LIKE ?1 OR description LIKE ?1
|
||||
ORDER BY stars DESC
|
||||
LIMIT ?2",
|
||||
)?;
|
||||
|
||||
let repos = stmt
|
||||
.query_map(params![&search_pattern, limit], |row| {
|
||||
Ok(Repo {
|
||||
id: row.get(0)?,
|
||||
full_name: row.get(1)?,
|
||||
name: row.get(2)?,
|
||||
owner: row.get(3)?,
|
||||
description: row.get(4)?,
|
||||
stars: row.get(5)?,
|
||||
language: row.get(6)?,
|
||||
html_url: row.get(7)?,
|
||||
updated_at: row.get(8)?,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.context("Failed to collect search results")?;
|
||||
|
||||
Ok(repos)
|
||||
}
|
||||
|
||||
pub fn insert_or_update(&self, repo: &Repo) -> Result<()> {
|
||||
self.conn.execute(
|
||||
"INSERT INTO repos (id, full_name, name, owner, description, stars, language, html_url, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
|
||||
ON CONFLICT(full_name) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
owner = excluded.owner,
|
||||
description = excluded.description,
|
||||
stars = excluded.stars,
|
||||
language = excluded.language,
|
||||
html_url = excluded.html_url,
|
||||
updated_at = excluded.updated_at",
|
||||
params![
|
||||
repo.id,
|
||||
&repo.full_name,
|
||||
&repo.name,
|
||||
&repo.owner,
|
||||
repo.description.as_ref(),
|
||||
repo.stars,
|
||||
repo.language.as_ref(),
|
||||
&repo.html_url,
|
||||
repo.updated_at,
|
||||
],
|
||||
).context("Failed to insert or update repository")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_by_full_name(&self, full_name: &str) -> Result<Option<Repo>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, full_name, name, owner, description, stars, language, html_url, updated_at
|
||||
FROM repos
|
||||
WHERE full_name = ?1",
|
||||
)?;
|
||||
|
||||
let result = stmt.query_row(params![full_name], |row| {
|
||||
Ok(Repo {
|
||||
id: row.get(0)?,
|
||||
full_name: row.get(1)?,
|
||||
name: row.get(2)?,
|
||||
owner: row.get(3)?,
|
||||
description: row.get(4)?,
|
||||
stars: row.get(5)?,
|
||||
language: row.get(6)?,
|
||||
html_url: row.get(7)?,
|
||||
updated_at: row.get(8)?,
|
||||
})
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(repo) => Ok(Some(repo)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_all(&self) -> Result<Vec<Repo>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, full_name, name, owner, description, stars, language, html_url, updated_at
|
||||
FROM repos
|
||||
ORDER BY stars DESC",
|
||||
)?;
|
||||
|
||||
let repos = stmt
|
||||
.query_map([], |row| {
|
||||
Ok(Repo {
|
||||
id: row.get(0)?,
|
||||
full_name: row.get(1)?,
|
||||
name: row.get(2)?,
|
||||
owner: row.get(3)?,
|
||||
description: row.get(4)?,
|
||||
stars: row.get(5)?,
|
||||
language: row.get(6)?,
|
||||
html_url: row.get(7)?,
|
||||
updated_at: row.get(8)?,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.context("Failed to collect all repositories")?;
|
||||
|
||||
Ok(repos)
|
||||
}
|
||||
|
||||
pub fn update_stars(&self, full_name: &str, stars: i64) -> Result<()> {
|
||||
self.conn
|
||||
.execute(
|
||||
"UPDATE repos SET stars = ?1, updated_at = CURRENT_TIMESTAMP WHERE full_name = ?2",
|
||||
params![stars, full_name],
|
||||
)
|
||||
.context("Failed to update star count")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn count(&self) -> Result<i64> {
|
||||
let count: i64 = self
|
||||
.conn
|
||||
.query_row("SELECT COUNT(*) FROM repos", [], |row| row.get(0))?;
|
||||
Ok(count)
|
||||
}
|
||||
}
|
||||
119
src/github.rs
Normal file
119
src/github.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use crate::models::{GitHubRepo, GitHubSearchResponse, RateLimitInfo};
|
||||
use anyhow::{Context, Result};
|
||||
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, AUTHORIZATION, USER_AGENT};
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct GitHubClient {
|
||||
client: reqwest::Client,
|
||||
token: Option<String>,
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
impl GitHubClient {
|
||||
pub fn new(token: Option<String>) -> Result<Self> {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(USER_AGENT, HeaderValue::from_static("gh-celebs"));
|
||||
headers.insert(ACCEPT, HeaderValue::from_static("application/vnd.github.v3+json"));
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.default_headers(headers)
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
token,
|
||||
base_url: "https://api.github.com".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn build_request(&self, url: String) -> reqwest::RequestBuilder {
|
||||
let mut request = self.client.get(&url);
|
||||
if let Some(token) = &self.token {
|
||||
request = request.header(AUTHORIZATION, format!("Bearer {}", token));
|
||||
}
|
||||
request
|
||||
}
|
||||
|
||||
fn extract_rate_limit(headers: &HeaderMap) -> RateLimitInfo {
|
||||
let remaining = headers
|
||||
.get("x-ratelimit-remaining")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
let limit = headers
|
||||
.get("x-ratelimit-limit")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(10);
|
||||
|
||||
RateLimitInfo { remaining, limit }
|
||||
}
|
||||
|
||||
pub async fn search_repositories(
|
||||
&self,
|
||||
query: &str,
|
||||
limit: usize,
|
||||
) -> Result<(Vec<GitHubRepo>, RateLimitInfo)> {
|
||||
let url = format!(
|
||||
"{}/search/repositories?q={}+sort:stars&order=desc&per_page={}",
|
||||
self.base_url,
|
||||
urlencoding::encode(query),
|
||||
limit
|
||||
);
|
||||
|
||||
let response = self.build_request(url)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send request to GitHub API")?;
|
||||
|
||||
let headers = response.headers().clone();
|
||||
let rate_limit = Self::extract_rate_limit(&headers);
|
||||
|
||||
if response.status().is_success() {
|
||||
let search_response: GitHubSearchResponse = response
|
||||
.json()
|
||||
.await
|
||||
.context("Failed to parse GitHub API response")?;
|
||||
|
||||
Ok((search_response.items, rate_limit))
|
||||
} else if response.status().as_u16() == 403 || response.status().as_u16() == 429 {
|
||||
anyhow::bail!("Rate limited by GitHub API. Please wait a moment and try again.");
|
||||
} else {
|
||||
let status = response.status();
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
anyhow::bail!("GitHub API error: {} - {}", status, text);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_repository(&self, owner: &str, repo: &str) -> Result<(GitHubRepo, RateLimitInfo)> {
|
||||
let url = format!("{}/repos/{}/{}", self.base_url, owner, repo);
|
||||
|
||||
let response = self.build_request(url)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send request to GitHub API")?;
|
||||
|
||||
let headers = response.headers().clone();
|
||||
let rate_limit = Self::extract_rate_limit(&headers);
|
||||
|
||||
if response.status().is_success() {
|
||||
let repo: GitHubRepo = response
|
||||
.json()
|
||||
.await
|
||||
.context("Failed to parse GitHub API response")?;
|
||||
|
||||
Ok((repo, rate_limit))
|
||||
} else if response.status().as_u16() == 404 {
|
||||
anyhow::bail!("Repository {}/{} not found", owner, repo);
|
||||
} else if response.status().as_u16() == 403 || response.status().as_u16() == 429 {
|
||||
anyhow::bail!("Rate limited by GitHub API. Please wait a moment and try again.");
|
||||
} else {
|
||||
let status = response.status();
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
anyhow::bail!("GitHub API error: {} - {}", status, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/main.rs
Normal file
90
src/main.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
mod cli;
|
||||
mod config;
|
||||
mod db;
|
||||
mod github;
|
||||
mod models;
|
||||
mod output;
|
||||
mod search;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use directories::ProjectDirs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::cli::{Cli, Commands};
|
||||
use crate::config::Config;
|
||||
use crate::db::Database;
|
||||
use crate::github::GitHubClient;
|
||||
use crate::output::OutputFormatter;
|
||||
use crate::search::SearchEngine;
|
||||
|
||||
fn get_data_dir() -> Result<PathBuf> {
|
||||
let proj_dirs = ProjectDirs::from("com", "gh-celebs", "gh-celebs")
|
||||
.context("Failed to determine project directories")?;
|
||||
Ok(proj_dirs.data_dir().to_path_buf())
|
||||
}
|
||||
|
||||
fn get_config_path() -> Result<PathBuf> {
|
||||
let proj_dirs = ProjectDirs::from("com", "gh-celebs", "gh-celebs")
|
||||
.context("Failed to determine project directories")?;
|
||||
Ok(proj_dirs.config_dir().join("config.toml"))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let data_dir = get_data_dir()?;
|
||||
std::fs::create_dir_all(&data_dir)
|
||||
.with_context(|| format!("Failed to create data directory at {:?}", data_dir))?;
|
||||
|
||||
let db_path = data_dir.join("repos.db");
|
||||
let db = Database::new(&db_path)?;
|
||||
|
||||
let config_path = get_config_path()?;
|
||||
if let Some(config_dir) = config_path.parent() {
|
||||
std::fs::create_dir_all(config_dir)
|
||||
.with_context(|| format!("Failed to create config directory at {:?}", config_dir))?;
|
||||
}
|
||||
let config = Config::load(&config_path)?;
|
||||
|
||||
let github = GitHubClient::new(config.github.token)?;
|
||||
let search_engine = SearchEngine::new(db, github);
|
||||
|
||||
match cli.command {
|
||||
Some(Commands::Update { repo }) => {
|
||||
if let Some(repo_name) = repo {
|
||||
let rate_limit = search_engine.update_single(&repo_name).await?;
|
||||
if rate_limit.remaining < 3 {
|
||||
eprintln!("Warning: Rate limit running low ({} remaining)", rate_limit.remaining);
|
||||
}
|
||||
} else {
|
||||
let rate_limit = search_engine.update_all().await?;
|
||||
if rate_limit.remaining < 3 {
|
||||
eprintln!("Warning: Rate limit running low ({} remaining)", rate_limit.remaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let query = cli.query.ok_or_else(|| {
|
||||
anyhow::anyhow!("Query is required when not using a subcommand")
|
||||
})?;
|
||||
|
||||
let response = search_engine.search(&query, cli.limit).await?;
|
||||
|
||||
if cli.json {
|
||||
let json = OutputFormatter::format_json(&response)?;
|
||||
println!("{}", json);
|
||||
} else {
|
||||
let text = OutputFormatter::format_text(&response)?;
|
||||
println!("{}", text);
|
||||
}
|
||||
|
||||
if response.api_remaining < 3 {
|
||||
eprintln!("Warning: GitHub API rate limit running low ({} remaining)", response.api_remaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
78
src/models.rs
Normal file
78
src/models.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Repo {
|
||||
pub id: i64,
|
||||
pub full_name: String,
|
||||
pub name: String,
|
||||
pub owner: String,
|
||||
pub description: Option<String>,
|
||||
pub stars: i64,
|
||||
pub language: Option<String>,
|
||||
pub html_url: String,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GitHubRepo {
|
||||
pub id: i64,
|
||||
pub full_name: String,
|
||||
pub name: String,
|
||||
pub owner: Owner,
|
||||
pub description: Option<String>,
|
||||
pub stargazers_count: i64,
|
||||
pub language: Option<String>,
|
||||
pub html_url: String,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Owner {
|
||||
pub login: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GitHubSearchResponse {
|
||||
pub total_count: i64,
|
||||
pub items: Vec<GitHubRepo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SearchResult {
|
||||
pub rank: usize,
|
||||
#[serde(flatten)]
|
||||
pub repo: Repo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SearchResponse {
|
||||
pub query: String,
|
||||
pub limit: usize,
|
||||
pub api_remaining: i64,
|
||||
pub api_limit: i64,
|
||||
pub total_cached: i64,
|
||||
pub results: Vec<SearchResult>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RateLimitInfo {
|
||||
pub remaining: i64,
|
||||
pub limit: i64,
|
||||
}
|
||||
|
||||
impl From<GitHubRepo> for Repo {
|
||||
fn from(gh_repo: GitHubRepo) -> Self {
|
||||
Self {
|
||||
id: gh_repo.id,
|
||||
full_name: gh_repo.full_name.clone(),
|
||||
name: gh_repo.name,
|
||||
owner: gh_repo.owner.login,
|
||||
description: gh_repo.description,
|
||||
stars: gh_repo.stargazers_count,
|
||||
language: gh_repo.language,
|
||||
html_url: gh_repo.html_url,
|
||||
updated_at: gh_repo.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/output.rs
Normal file
76
src/output.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use crate::models::{SearchResponse, SearchResult};
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::Utc;
|
||||
|
||||
pub struct OutputFormatter;
|
||||
|
||||
impl OutputFormatter {
|
||||
pub fn format_text(response: &SearchResponse) -> Result<String> {
|
||||
let mut output = String::new();
|
||||
|
||||
output.push_str(&format!("Searching for \"{}\"...\n", response.query));
|
||||
output.push_str(&format!(
|
||||
"API: [{}/{} calls remaining]\n\n",
|
||||
response.api_remaining, response.api_limit
|
||||
));
|
||||
|
||||
if response.results.is_empty() {
|
||||
output.push_str("No results found.\n");
|
||||
return Ok(output);
|
||||
}
|
||||
|
||||
for result in &response.results {
|
||||
output.push_str(&Self::format_repo_text(result));
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn format_repo_text(result: &SearchResult) -> String {
|
||||
let repo = &result.repo;
|
||||
let stars_formatted = Self::format_stars(repo.stars);
|
||||
let updated = Self::format_date(repo.updated_at);
|
||||
|
||||
let mut output = String::new();
|
||||
output.push_str(&format!(
|
||||
"{}. {} ({} ⭐) - Updated {}\n",
|
||||
result.rank, repo.full_name, stars_formatted, updated
|
||||
));
|
||||
output.push_str(&format!(" {}\n", repo.html_url));
|
||||
|
||||
if let Some(desc) = &repo.description {
|
||||
let truncated = Self::truncate_description(desc, 80);
|
||||
output.push_str(&format!(" {}\n", truncated));
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn format_stars(stars: i64) -> String {
|
||||
if stars >= 1_000_000 {
|
||||
format!("{:.1}M", stars as f64 / 1_000_000.0)
|
||||
} else if stars >= 1_000 {
|
||||
format!("{:.1}k", stars as f64 / 1_000.0)
|
||||
} else {
|
||||
stars.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn format_date(date: chrono::DateTime<Utc>) -> String {
|
||||
date.format("%Y-%m-%d").to_string()
|
||||
}
|
||||
|
||||
fn truncate_description(desc: &str, max_len: usize) -> String {
|
||||
if desc.chars().count() <= max_len {
|
||||
desc.to_string()
|
||||
} else {
|
||||
let truncated: String = desc.chars().take(max_len).collect();
|
||||
format!("{}...", truncated)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_json(response: &SearchResponse) -> Result<String> {
|
||||
serde_json::to_string_pretty(response).context("Failed to serialize response to JSON")
|
||||
}
|
||||
}
|
||||
132
src/search.rs
Normal file
132
src/search.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use crate::db::Database;
|
||||
use crate::github::GitHubClient;
|
||||
use crate::models::{RateLimitInfo, Repo, SearchResponse, SearchResult};
|
||||
use anyhow::Result;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct SearchEngine {
|
||||
db: Database,
|
||||
github: GitHubClient,
|
||||
}
|
||||
|
||||
impl SearchEngine {
|
||||
pub fn new(db: Database, github: GitHubClient) -> Self {
|
||||
Self { db, github }
|
||||
}
|
||||
|
||||
pub async fn search(
|
||||
&self,
|
||||
query: &str,
|
||||
limit: usize,
|
||||
) -> Result<SearchResponse> {
|
||||
let local_results = self.db.search(query, limit as i64)?;
|
||||
let total_cached = self.db.count()?;
|
||||
|
||||
let mut all_repos: HashMap<String, Repo> = HashMap::new();
|
||||
|
||||
for repo in &local_results {
|
||||
all_repos.insert(repo.full_name.clone(), repo.clone());
|
||||
}
|
||||
|
||||
let mut rate_limit = RateLimitInfo {
|
||||
remaining: 10,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
if local_results.len() < limit {
|
||||
match self.github.search_repositories(query, limit).await {
|
||||
Ok((api_repos, api_rate_limit)) => {
|
||||
rate_limit = api_rate_limit;
|
||||
|
||||
for gh_repo in api_repos {
|
||||
let repo: Repo = gh_repo.into();
|
||||
self.db.insert_or_update(&repo)?;
|
||||
all_repos.insert(repo.full_name.clone(), repo);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Failed to fetch from GitHub API: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut results: Vec<Repo> = all_repos.into_values().collect();
|
||||
results.sort_by(|a, b| b.stars.cmp(&a.stars));
|
||||
results.truncate(limit);
|
||||
|
||||
let search_results: Vec<SearchResult> = results
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(idx, repo)| SearchResult {
|
||||
rank: idx + 1,
|
||||
repo,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(SearchResponse {
|
||||
query: query.to_string(),
|
||||
limit,
|
||||
api_remaining: rate_limit.remaining,
|
||||
api_limit: rate_limit.limit,
|
||||
total_cached,
|
||||
results: search_results,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn update_all(&self) -> Result<RateLimitInfo> {
|
||||
let repos = self.db.get_all()?;
|
||||
let total = repos.len();
|
||||
|
||||
let mut last_rate_limit = RateLimitInfo {
|
||||
remaining: 10,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
println!("Updating {} repositories...", total);
|
||||
|
||||
for (idx, repo) in repos.iter().enumerate() {
|
||||
let parts: Vec<&str> = repo.full_name.split('/').collect();
|
||||
if parts.len() != 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
print!("\rUpdating {}/{}: {}", idx + 1, total, repo.full_name);
|
||||
|
||||
match self.github.get_repository(parts[0], parts[1]).await {
|
||||
Ok((gh_repo, rate_limit)) => {
|
||||
last_rate_limit = rate_limit;
|
||||
self.db.update_stars(&repo.full_name, gh_repo.stargazers_count)?;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("\nWarning: Failed to update {}: {}", repo.full_name, e);
|
||||
}
|
||||
}
|
||||
|
||||
if last_rate_limit.remaining < 3 {
|
||||
println!("\nWarning: Rate limit running low ({} remaining)", last_rate_limit.remaining);
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n✓ Updated {} repositories", total);
|
||||
Ok(last_rate_limit)
|
||||
}
|
||||
|
||||
pub async fn update_single(&self, full_name: &str) -> Result<RateLimitInfo> {
|
||||
let parts: Vec<&str> = full_name.split('/').collect();
|
||||
if parts.len() != 2 {
|
||||
anyhow::bail!("Invalid repository format. Use 'owner/repo'");
|
||||
}
|
||||
|
||||
let (owner, name) = (parts[0], parts[1]);
|
||||
|
||||
let (gh_repo, rate_limit) = self.github.get_repository(owner, name).await?;
|
||||
let repo: Repo = gh_repo.into();
|
||||
|
||||
self.db.insert_or_update(&repo)?;
|
||||
|
||||
println!("✓ Updated {}", full_name);
|
||||
println!(" Stars: {}", repo.stars);
|
||||
|
||||
Ok(rate_limit)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user