commit d36633ab6ce5477d80ebb053bb6b3ffe67b7edd4
parent f0524ed15a6b81e5d7e796528cccfe97bcd8ca42
Author: Amit Dutta <amitdutta4255@gmail.com>
Date: Thu, 5 Feb 2026 14:57:30 +0530
Change name from pyrgo to githrun
Diffstat:
13 files changed, 547 insertions(+), 547 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
@@ -1,13 +1,13 @@
-# Contributing to Pyrgo
+# Contributing to Githrun
First off, thanks for taking the time to contribute!
-The following is a set of guidelines for contributing to Pyrgo. These are just guidelines, not rules. Use your best judgment and feel free to propose changes to this document in a pull request.
+The following is a set of guidelines for contributing to Githrun. These are just guidelines, not rules. Use your best judgment and feel free to propose changes to this document in a pull request.
## How Can I Contribute?
### Reporting Bugs
-This section guides you through submitting a bug report for Pyrgo. Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports.
+This section guides you through submitting a bug report for Githrun. Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports.
- **Use a clear and descriptive title** for the issue to identify the problem.
- **Describe the exact steps which reproduce the problem** in as many details as possible.
@@ -18,8 +18,8 @@ This section guides you through submitting a bug report for Pyrgo. Following the
1. **Fork the repository** on GitHub.
2. **Clone your fork** locally:
```bash
- git clone [https://github.com/your-username/pyrgo.git](https://github.com/your-username/pyrgo.git)
- cd pyrgo
+ git clone [https://github.com/your-username/githrun.git](https://github.com/your-username/githrun.git)
+ cd githrun
```
3. **Create a virtual environment** to keep dependencies isolated:
```bash
@@ -42,7 +42,7 @@ This section guides you through submitting a bug report for Pyrgo. Following the
```bash
git push origin feature/amazing-feature
```
-8. **Open a Pull Request** on the main Pyrgo repository.
+8. **Open a Pull Request** on the main Githrun repository.
## Style Guide
- We follow **PEP 8** standards.
diff --git a/README.md b/README.md
@@ -1,10 +1,10 @@
-# Pyrgo
+# Githrun
> The Swiss Army Knife for Remote Python Execution.
-Pyrgo is a powerful CLI tool and Python library that lets you run, explore, and install Python code directly from GitHub and Gists. It handles dependencies, private repositories, and even turns remote scripts into local command-line tools.
+Githrun is a powerful CLI tool and Python library that lets you run, explore, and install Python code directly from GitHub and Gists. It handles dependencies, private repositories, and even turns remote scripts into local command-line tools.
-[](https://badge.fury.io/py/pyrgo)
+[](https://badge.fury.io/py/githrun)
[](https://opensource.org/licenses/MIT)
## Features
@@ -12,7 +12,7 @@ Pyrgo is a powerful CLI tool and Python library that lets you run, explore, and
* **Remote Execution**: Run scripts from GitHub or Gist URLs instantly.
* **Auto-Dependency Management**: Automatically creates temporary virtual environments and installs missing packages using the `--auto-install` flag.
* **Private Repo Access**: Authenticate securely with GitHub tokens to access private code and increase API rate limits.
-* **Bookmarks**: Save long URLs as short aliases (e.g., `pyrgo run clean-db`).
+* **Bookmarks**: Save long URLs as short aliases (e.g., `githrun run clean-db`).
* **Tool Installation**: Install remote scripts as permanent local CLI commands available in your system path.
* **Recursive Downloads**: Download entire folders or specific sub-directories from a repository.
* **Interactive Search**: Search for files in a repo and run them immediately from the results.
@@ -23,7 +23,7 @@ Pyrgo is a powerful CLI tool and Python library that lets you run, explore, and
## Installation
```bash
-pip install pyrgo
+pip install githrun
```
---
@@ -35,69 +35,69 @@ Execute a script directly from a URL.
**Basic Execution:**
```bash
-pyrgo run [https://github.com/user/repo/blob/main/script.py](https://github.com/user/repo/blob/main/script.py)
+githrun run [https://github.com/user/repo/blob/main/script.py](https://github.com/user/repo/blob/main/script.py)
```
**Run Gists:**
```bash
-pyrgo run [https://gist.github.com/user/1234567890abcdef](https://gist.github.com/user/1234567890abcdef)
+githrun run [https://gist.github.com/user/1234567890abcdef](https://gist.github.com/user/1234567890abcdef)
```
**Auto-Install Dependencies:**
If a remote script requires packages you do not have installed (e.g., pandas, requests), use this flag to run it in an isolated environment:
```bash
-pyrgo run [https://github.com/user/repo/blob/main/data.py](https://github.com/user/repo/blob/main/data.py) --auto-install
+githrun run [https://github.com/user/repo/blob/main/data.py](https://github.com/user/repo/blob/main/data.py) --auto-install
```
**Inspect Code:**
View the source code with syntax highlighting before running it (Safety Check):
```bash
-pyrgo run [https://github.com/user/repo/blob/main/script.py](https://github.com/user/repo/blob/main/script.py) --inspect
+githrun run [https://github.com/user/repo/blob/main/script.py](https://github.com/user/repo/blob/main/script.py) --inspect
```
### 2. Authentication (Private Repos & Rate Limits)
GitHub limits unauthenticated requests to 60 per hour. Login to increase this limit to 5,000 and access private repositories.
```bash
-pyrgo login ghp_YourPersonalAccessToken...
+githrun login ghp_YourPersonalAccessToken...
```
-*The token is stored securely in `~/.pyrgo/config.json`.*
+*The token is stored securely in `~/.githrun/config.json`.*
### 3. Bookmarks
Stop copy-pasting long URLs. Save them once, run them anywhere.
**Add a Bookmark:**
```bash
-pyrgo bookmark add clean-db [https://github.com/user/repo/blob/main/utils/cleanup.py](https://github.com/user/repo/blob/main/utils/cleanup.py)
+githrun bookmark add clean-db [https://github.com/user/repo/blob/main/utils/cleanup.py](https://github.com/user/repo/blob/main/utils/cleanup.py)
```
**Run a Bookmark:**
```bash
-pyrgo run clean-db
+githrun run clean-db
```
**List Bookmarks:**
```bash
-pyrgo bookmark list
+githrun bookmark list
```
### 4. Install as a Tool
Turn a remote Python script into a command you can run from anywhere in your terminal.
```bash
-pyrgo install [https://github.com/user/repo/blob/main/my-tool.py](https://github.com/user/repo/blob/main/my-tool.py) --name mytool
+githrun install [https://github.com/user/repo/blob/main/my-tool.py](https://github.com/user/repo/blob/main/my-tool.py) --name mytool
```
-* **Windows:** Creates a `.bat` file in `~/.pyrgo/bin`.
-* **Linux/Mac:** Creates an executable shim in `~/.pyrgo/bin`.
-* *Note: You must add `~/.pyrgo/bin` to your system PATH.*
+* **Windows:** Creates a `.bat` file in `~/.githrun/bin`.
+* **Linux/Mac:** Creates an executable shim in `~/.githrun/bin`.
+* *Note: You must add `~/.githrun/bin` to your system PATH.*
### 5. Find & Search
Search for files inside a remote repository without cloning it.
```bash
# Search for files containing "config"
-pyrgo find [https://github.com/user/repo](https://github.com/user/repo) "config"
+githrun find [https://github.com/user/repo](https://github.com/user/repo) "config"
```
*This command is interactive. You can select a result number to run it immediately.*
@@ -106,52 +106,52 @@ Download artifacts to your local machine.
**Download a single file:**
```bash
-pyrgo download [https://github.com/user/repo/blob/main/script.py](https://github.com/user/repo/blob/main/script.py)
+githrun download [https://github.com/user/repo/blob/main/script.py](https://github.com/user/repo/blob/main/script.py)
```
**Download a specific folder (Recursive):**
```bash
-pyrgo download [https://github.com/user/repo/tree/main/src/utils](https://github.com/user/repo/tree/main/src/utils) --output ./local_utils
+githrun download [https://github.com/user/repo/tree/main/src/utils](https://github.com/user/repo/tree/main/src/utils) --output ./local_utils
```
### 7. Show Folder Contents
List files in a remote directory to understand the structure.
```bash
-pyrgo show [https://github.com/user/repo/tree/main/src](https://github.com/user/repo/tree/main/src)
+githrun show [https://github.com/user/repo/tree/main/src](https://github.com/user/repo/tree/main/src)
```
---
## Python API Usage
-You can use Pyrgo inside your own Python scripts.
+You can use Githrun inside your own Python scripts.
```python
-import pyrgo
+import githrun
# 1. Search a repository
-results = pyrgo.search_repository("[https://github.com/user/repo](https://github.com/user/repo)", "test")
+results = githrun.search_repository("[https://github.com/user/repo](https://github.com/user/repo)", "test")
for item in results:
print(item['path'], item['raw_url'])
# 2. Download a file
-pyrgo.download_file("[https://github.com/user/repo/blob/main/script.py](https://github.com/user/repo/blob/main/script.py)", output_path="script.py")
+githrun.download_file("[https://github.com/user/repo/blob/main/script.py](https://github.com/user/repo/blob/main/script.py)", output_path="script.py")
# 3. Download a full folder
-pyrgo.download_folder("[https://github.com/user/repo/tree/main/src](https://github.com/user/repo/tree/main/src)")
+githrun.download_folder("[https://github.com/user/repo/tree/main/src](https://github.com/user/repo/tree/main/src)")
# 4. Execute code programmatically
-exit_code = pyrgo.execute_remote_code("[https://github.com/user/repo/blob/main/script.py](https://github.com/user/repo/blob/main/script.py)", args=["--verbose"])
+exit_code = githrun.execute_remote_code("[https://github.com/user/repo/blob/main/script.py](https://github.com/user/repo/blob/main/script.py)", args=["--verbose"])
```
## Configuration
-Pyrgo stores configuration and cache files in your home directory:
+Githrun stores configuration and cache files in your home directory:
-* **Config:** `~/.pyrgo/config.json` (Tokens, Bookmarks)
-* **Cache:** `~/.pyrgo/cache/` (API responses)
-* **Binaries:** `~/.pyrgo/bin/` (Installed tools)
+* **Config:** `~/.githrun/config.json` (Tokens, Bookmarks)
+* **Cache:** `~/.githrun/cache/` (API responses)
+* **Binaries:** `~/.githrun/bin/` (Installed tools)
## License
diff --git a/SECURITY.md b/SECURITY.md
@@ -26,4 +26,4 @@ For critical vulnerabilities that require immediate attention (e.g., remote code
* We aim to send a fix or a detailed plan within 72 hours.
* Please allow us time to patch the issue before disclosing it publicly.
-Thank you for helping keep Pyrgo safe!-
\ No newline at end of file
+Thank you for helping keep Githrun safe!+
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
@@ -1,10 +1,10 @@
[tool.poetry]
-name = "pyrgo"
+name = "githrun"
version = "0.1.0"
description = "A CLI tool to run Python scripts directly from GitHub URLs"
authors = ["Amit Dutta <mail@amit.is-a.dev>"]
readme = "README.md"
-packages = [{include = "pyrgo", from = "src"}]
+packages = [{include = "githrun", from = "src"}]
[tool.poetry.dependencies]
python = "^3.9"
@@ -16,7 +16,7 @@ requests = "^2.31.0"
rich = "^13.7.0"
[tool.poetry.scripts]
-pyrgo = "pyrgo.cli:app"
+githrun = "githrun.cli:app"
[build-system]
requires = ["poetry-core"]
diff --git a/src/pyrgo/__init__.py b/src/githrun/__init__.py
diff --git a/src/githrun/cli.py b/src/githrun/cli.py
@@ -0,0 +1,197 @@
+import typer
+from rich.console import Console
+from rich.syntax import Syntax
+from rich.table import Table
+from rich.prompt import Prompt
+from typing import Optional
+
+from .core import (
+ execute_remote_code,
+ search_repository,
+ get_folder_contents,
+ download_file,
+ download_folder,
+ install_tool,
+ login_github,
+ add_bookmark,
+ list_bookmarks
+)
+from .network import RateLimitError, fetch_url_content, fetch_gist_content, convert_to_raw_url
+from .utils import print_error, print_info, print_warning, print_success, ConfigManager
+
+app = typer.Typer(help="Githrun: Run Python code from GitHub instantly.")
+bookmark_app = typer.Typer(help="Manage bookmarks.")
+app.add_typer(bookmark_app, name="bookmark")
+
+console = Console()
+
+# --- MAIN COMMANDS ---
+
+@app.command()
+def login(token: str = typer.Argument(..., help="Your GitHub Personal Access Token.")):
+ """
+ Authenticate with GitHub to increase rate limits and access private repos.
+ """
+ login_github(token)
+ print_success("GitHub token saved successfully!")
+
+@app.command()
+def install(
+ url: str = typer.Argument(..., help="URL of the script."),
+ name: str = typer.Option(..., "--name", "-n", help="Name of the command tool.")
+):
+ """
+ Install a remote script as a local command-line tool.
+ """
+ try:
+ msg = install_tool(url, name)
+ print_success(msg)
+ except Exception as e:
+ print_error(str(e))
+
+@app.command()
+def run(
+ url: str = typer.Argument(..., help="GitHub URL, Gist URL, or Bookmark Name."),
+ inspect: bool = typer.Option(False, "--inspect", "-i", help="Print code without running."),
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
+ auto_install: bool = typer.Option(False, "--auto-install", help="Auto-install missing dependencies.")
+):
+ """Download and execute a remote Python script."""
+ try:
+ # Check bookmarks
+ b_url = ConfigManager.get_bookmark(url)
+ if b_url:
+ print_info(f"Resolved bookmark '{url}' to: {b_url}")
+ url = b_url
+
+ if inspect:
+ raw = convert_to_raw_url(url)
+ if raw.startswith("gist:"):
+ content = fetch_gist_content(raw.split(":")[1])
+ else:
+ content = fetch_url_content(raw)
+ if content:
+ console.print(Syntax(content, "python", theme="monokai", line_numbers=True))
+ return
+
+ if not yes:
+ console.print(f"\n[bold yellow]SECURITY WARNING:[/bold yellow] Running remote code from:")
+ console.print(f"[underline]{url}[/underline]\n")
+ if not typer.confirm("Execute this script?"):
+ raise typer.Exit()
+
+ execute_remote_code(url, auto_install=auto_install)
+
+ except RateLimitError:
+ print_error("GitHub API Rate Limit Exceeded (60 reqs/hr).")
+ if typer.confirm("Would you like to add an API Key to increase limits?"):
+ token = Prompt.ask("Enter GitHub Token")
+ login_github(token)
+ print_success("Token saved! Please try running the command again.")
+ except Exception as e:
+ print_error(str(e))
+ raise typer.Exit(1)
+
+# --- BOOKMARK COMMANDS ---
+
+@bookmark_app.command("add")
+def bookmark_add(name: str, url: str):
+ """Save a URL as a bookmark."""
+ add_bookmark(name, url)
+ print_success(f"Bookmark '{name}' added.")
+
+@bookmark_app.command("list")
+def bookmark_list():
+ """List all saved bookmarks."""
+ bookmarks = list_bookmarks()
+ if not bookmarks:
+ print_warning("No bookmarks found.")
+ return
+
+ table = Table(title="Bookmarks")
+ table.add_column("Name", style="cyan")
+ table.add_column("URL", style="green")
+ for name, url in bookmarks.items():
+ table.add_row(name, url)
+ console.print(table)
+
+# --- EXISTING COMMANDS (UPDATED) ---
+
+@app.command()
+def find(
+ repo_url: str = typer.Argument(..., help="GitHub repository URL."),
+ query: str = typer.Argument(..., help="Search query (filename)."),
+ interactive: bool = typer.Option(True, help="Enable interactive selection.")
+):
+ """Search for files in a repo."""
+ try:
+ with console.status("Scanning..."):
+ from .core import search_repository
+ results = search_repository(repo_url, query)
+
+ if not results:
+ print_warning("No results.")
+ return
+
+ table = Table(title=f"Results for {query}")
+ table.add_column("#", style="yellow")
+ table.add_column("Path", style="cyan")
+
+ for idx, r in enumerate(results):
+ table.add_row(str(idx+1), r['path'])
+ console.print(table)
+
+ if interactive:
+ choice = Prompt.ask("Select file # to Run (or 'q')")
+ if choice.isdigit():
+ idx = int(choice) - 1
+ if 0 <= idx < len(results):
+ execute_remote_code(results[idx]['raw_url'])
+
+ except RateLimitError:
+ print_error("Rate Limit Hit.")
+ print_info("Use 'githrun login <token>' to fix this.")
+ except Exception as e:
+ print_error(str(e))
+
+@app.command()
+def download(
+ url: str = typer.Argument(..., help="GitHub file or folder URL."),
+ output: Optional[str] = typer.Option(None, "--output", "-o", help="Output path.")
+):
+ """Download file or folder."""
+ try:
+ from .core import download_folder, download_file
+ if "/tree/" in url:
+ path = download_folder(url, output)
+ print_success(f"Downloaded folder: {path}")
+ else:
+ path = download_file(url, output)
+ print_success(f"Downloaded file: {path}")
+ except Exception as e:
+ print_error(str(e))
+
+@app.command()
+def show(url: str = typer.Argument(..., help="GitHub folder URL.")):
+ """Show folder contents."""
+ try:
+ from .core import get_folder_contents
+ items = get_folder_contents(url)
+
+ if not items:
+ print_error("Empty folder or invalid URL.")
+ return
+
+ table = Table(title="Contents")
+ table.add_column("Name")
+ table.add_column("Type")
+
+ for item in items:
+ itype = "Dir" if item['type'] == 'dir' else "File"
+ table.add_row(item['name'], itype)
+ console.print(table)
+ except Exception as e:
+ print_error(str(e))
+
+if __name__ == "__main__":
+ app()+
\ No newline at end of file
diff --git a/src/pyrgo/core.py b/src/githrun/core.py
diff --git a/src/pyrgo/network.py b/src/githrun/network.py
diff --git a/src/githrun/utils.py b/src/githrun/utils.py
@@ -0,0 +1,183 @@
+import os
+import sys
+import tempfile
+import json
+import time
+import shutil
+import venv
+import platform
+import subprocess
+import importlib.util
+from pathlib import Path
+from typing import Optional, Dict
+from contextlib import contextmanager
+from rich.console import Console
+
+console = Console()
+
+# Paths
+APP_DIR = Path.home() / ".githrun"
+CACHE_DIR = APP_DIR / "cache"
+BIN_DIR = APP_DIR / "bin"
+CONFIG_FILE = APP_DIR / "config.json"
+CACHE_DURATION = 600 # 10 minutes
+
+def ensure_dirs():
+ APP_DIR.mkdir(exist_ok=True)
+ CACHE_DIR.mkdir(exist_ok=True)
+ BIN_DIR.mkdir(exist_ok=True)
+
+class ConfigManager:
+ """Manages persistent configuration (Tokens, Bookmarks)."""
+
+ @staticmethod
+ def load() -> Dict:
+ ensure_dirs()
+ if not CONFIG_FILE.exists():
+ return {"api_key": None, "bookmarks": {}}
+ try:
+ with open(CONFIG_FILE, "r") as f:
+ return json.load(f)
+ except:
+ return {"api_key": None, "bookmarks": {}}
+
+ @staticmethod
+ def save(data: Dict):
+ ensure_dirs()
+ with open(CONFIG_FILE, "w") as f:
+ json.dump(data, f, indent=4)
+
+ @staticmethod
+ def get_api_key() -> Optional[str]:
+ # 1. Check Environment Variable
+ if os.environ.get("GITHUB_TOKEN"):
+ return os.environ["GITHUB_TOKEN"]
+
+ # 2. Check .env file in current directory
+ env_path = Path.cwd() / ".env"
+ if env_path.exists():
+ try:
+ with open(env_path, "r") as f:
+ for line in f:
+ if line.strip().startswith("GITHUB_TOKEN="):
+ return line.split("=", 1)[1].strip().strip('"').strip("'")
+ except:
+ pass
+
+ # 3. Check Global Config
+ config = ConfigManager.load()
+ return config.get("api_key")
+
+ @staticmethod
+ def set_api_key(key: str):
+ config = ConfigManager.load()
+ config["api_key"] = key
+ ConfigManager.save(config)
+
+ @staticmethod
+ def add_bookmark(name: str, url: str):
+ config = ConfigManager.load()
+ if "bookmarks" not in config:
+ config["bookmarks"] = {}
+ config["bookmarks"][name] = url
+ ConfigManager.save(config)
+
+ @staticmethod
+ def get_bookmark(name: str) -> Optional[str]:
+ config = ConfigManager.load()
+ return config.get("bookmarks", {}).get(name)
+
+ @staticmethod
+ def list_bookmarks() -> Dict[str, str]:
+ return ConfigManager.load().get("bookmarks", {})
+
+# Cache Logic
+def load_cache(key: str):
+ ensure_dirs()
+ cache_file = CACHE_DIR / f"{key}.json"
+ if cache_file.exists():
+ try:
+ with open(cache_file, "r", encoding="utf-8") as f:
+ data = json.load(f)
+ if time.time() - data["timestamp"] < CACHE_DURATION:
+ return data["content"]
+ except:
+ pass
+ return None
+
+def save_cache(key: str, content: any):
+ ensure_dirs()
+ cache_file = CACHE_DIR / f"{key}.json"
+ try:
+ with open(cache_file, "w", encoding="utf-8") as f:
+ json.dump({"timestamp": time.time(), "content": content}, f)
+ except:
+ pass
+
+# Dependency & Venv Logic
+def check_package_installed(package_name: str) -> bool:
+ if package_name in sys.builtin_module_names:
+ return True
+ try:
+ return importlib.util.find_spec(package_name) is not None
+ except Exception:
+ return False
+
+class VenvManager:
+ """Manages temporary virtual environments for auto-installing dependencies."""
+
+ def __init__(self):
+ self.temp_dir = tempfile.mkdtemp(prefix="githrun_env_")
+ self.venv_dir = Path(self.temp_dir)
+ self.python_exe = self._get_python_exe()
+ self.pip_exe = self._get_pip_exe()
+
+ def _get_python_exe(self):
+ if platform.system() == "Windows":
+ return self.venv_dir / "Scripts" / "python.exe"
+ return self.venv_dir / "bin" / "python"
+
+ def _get_pip_exe(self):
+ if platform.system() == "Windows":
+ return self.venv_dir / "Scripts" / "pip.exe"
+ return self.venv_dir / "bin" / "pip"
+
+ def create(self):
+ """Creates the virtual environment."""
+ console.print("[blue]Creating temporary virtual environment...[/blue]")
+ venv.create(self.venv_dir, with_pip=True)
+
+ def install(self, packages: list):
+ """Installs packages into the venv."""
+ if not packages:
+ return
+ console.print(f"[blue]Installing dependencies: {', '.join(packages)}...[/blue]")
+ subprocess.check_call([str(self.pip_exe), "install"] + packages, stdout=subprocess.DEVNULL)
+
+ def cleanup(self):
+ """Deletes the virtual environment."""
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
+
+# Standard Helpers
+def print_error(msg: str):
+ console.print(f"[bold red]Error:[/bold red] {msg}")
+
+def print_info(msg: str):
+ console.print(f"[bold blue]Info:[/bold blue] {msg}")
+
+def print_success(msg: str):
+ console.print(f"[bold green]Success:[/bold green] {msg}")
+
+def print_warning(msg: str):
+ console.print(f"[bold yellow]Warning:[/bold yellow] {msg}")
+
+@contextmanager
+def temp_python_file(content: str):
+ fd, path = tempfile.mkstemp(suffix=".py", text=True)
+ try:
+ with os.fdopen(fd, 'w', encoding='utf-8') as tmp:
+ tmp.write(content)
+ yield path
+ finally:
+ if os.path.exists(path):
+ os.remove(path)+
\ No newline at end of file
diff --git a/src/pyrgo/cli.py b/src/pyrgo/cli.py
@@ -1,197 +0,0 @@
-import typer
-from rich.console import Console
-from rich.syntax import Syntax
-from rich.table import Table
-from rich.prompt import Prompt
-from typing import Optional
-
-from .core import (
- execute_remote_code,
- search_repository,
- get_folder_contents,
- download_file,
- download_folder,
- install_tool,
- login_github,
- add_bookmark,
- list_bookmarks
-)
-from .network import RateLimitError, fetch_url_content, fetch_gist_content, convert_to_raw_url
-from .utils import print_error, print_info, print_warning, print_success, ConfigManager
-
-app = typer.Typer(help="Pyrgo: Run Python code from GitHub instantly.")
-bookmark_app = typer.Typer(help="Manage bookmarks.")
-app.add_typer(bookmark_app, name="bookmark")
-
-console = Console()
-
-# --- MAIN COMMANDS ---
-
-@app.command()
-def login(token: str = typer.Argument(..., help="Your GitHub Personal Access Token.")):
- """
- Authenticate with GitHub to increase rate limits and access private repos.
- """
- login_github(token)
- print_success("GitHub token saved successfully!")
-
-@app.command()
-def install(
- url: str = typer.Argument(..., help="URL of the script."),
- name: str = typer.Option(..., "--name", "-n", help="Name of the command tool.")
-):
- """
- Install a remote script as a local command-line tool.
- """
- try:
- msg = install_tool(url, name)
- print_success(msg)
- except Exception as e:
- print_error(str(e))
-
-@app.command()
-def run(
- url: str = typer.Argument(..., help="GitHub URL, Gist URL, or Bookmark Name."),
- inspect: bool = typer.Option(False, "--inspect", "-i", help="Print code without running."),
- yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
- auto_install: bool = typer.Option(False, "--auto-install", help="Auto-install missing dependencies.")
-):
- """Download and execute a remote Python script."""
- try:
- # Check bookmarks
- b_url = ConfigManager.get_bookmark(url)
- if b_url:
- print_info(f"Resolved bookmark '{url}' to: {b_url}")
- url = b_url
-
- if inspect:
- raw = convert_to_raw_url(url)
- if raw.startswith("gist:"):
- content = fetch_gist_content(raw.split(":")[1])
- else:
- content = fetch_url_content(raw)
- if content:
- console.print(Syntax(content, "python", theme="monokai", line_numbers=True))
- return
-
- if not yes:
- console.print(f"\n[bold yellow]SECURITY WARNING:[/bold yellow] Running remote code from:")
- console.print(f"[underline]{url}[/underline]\n")
- if not typer.confirm("Execute this script?"):
- raise typer.Exit()
-
- execute_remote_code(url, auto_install=auto_install)
-
- except RateLimitError:
- print_error("GitHub API Rate Limit Exceeded (60 reqs/hr).")
- if typer.confirm("Would you like to add an API Key to increase limits?"):
- token = Prompt.ask("Enter GitHub Token")
- login_github(token)
- print_success("Token saved! Please try running the command again.")
- except Exception as e:
- print_error(str(e))
- raise typer.Exit(1)
-
-# --- BOOKMARK COMMANDS ---
-
-@bookmark_app.command("add")
-def bookmark_add(name: str, url: str):
- """Save a URL as a bookmark."""
- add_bookmark(name, url)
- print_success(f"Bookmark '{name}' added.")
-
-@bookmark_app.command("list")
-def bookmark_list():
- """List all saved bookmarks."""
- bookmarks = list_bookmarks()
- if not bookmarks:
- print_warning("No bookmarks found.")
- return
-
- table = Table(title="Bookmarks")
- table.add_column("Name", style="cyan")
- table.add_column("URL", style="green")
- for name, url in bookmarks.items():
- table.add_row(name, url)
- console.print(table)
-
-# --- EXISTING COMMANDS (UPDATED) ---
-
-@app.command()
-def find(
- repo_url: str = typer.Argument(..., help="GitHub repository URL."),
- query: str = typer.Argument(..., help="Search query (filename)."),
- interactive: bool = typer.Option(True, help="Enable interactive selection.")
-):
- """Search for files in a repo."""
- try:
- with console.status("Scanning..."):
- from .core import search_repository
- results = search_repository(repo_url, query)
-
- if not results:
- print_warning("No results.")
- return
-
- table = Table(title=f"Results for {query}")
- table.add_column("#", style="yellow")
- table.add_column("Path", style="cyan")
-
- for idx, r in enumerate(results):
- table.add_row(str(idx+1), r['path'])
- console.print(table)
-
- if interactive:
- choice = Prompt.ask("Select file # to Run (or 'q')")
- if choice.isdigit():
- idx = int(choice) - 1
- if 0 <= idx < len(results):
- execute_remote_code(results[idx]['raw_url'])
-
- except RateLimitError:
- print_error("Rate Limit Hit.")
- print_info("Use 'pyrgo login <token>' to fix this.")
- except Exception as e:
- print_error(str(e))
-
-@app.command()
-def download(
- url: str = typer.Argument(..., help="GitHub file or folder URL."),
- output: Optional[str] = typer.Option(None, "--output", "-o", help="Output path.")
-):
- """Download file or folder."""
- try:
- from .core import download_folder, download_file
- if "/tree/" in url:
- path = download_folder(url, output)
- print_success(f"Downloaded folder: {path}")
- else:
- path = download_file(url, output)
- print_success(f"Downloaded file: {path}")
- except Exception as e:
- print_error(str(e))
-
-@app.command()
-def show(url: str = typer.Argument(..., help="GitHub folder URL.")):
- """Show folder contents."""
- try:
- from .core import get_folder_contents
- items = get_folder_contents(url)
-
- if not items:
- print_error("Empty folder or invalid URL.")
- return
-
- table = Table(title="Contents")
- table.add_column("Name")
- table.add_column("Type")
-
- for item in items:
- itype = "Dir" if item['type'] == 'dir' else "File"
- table.add_row(item['name'], itype)
- console.print(table)
- except Exception as e:
- print_error(str(e))
-
-if __name__ == "__main__":
- app()-
\ No newline at end of file
diff --git a/src/pyrgo/utils.py b/src/pyrgo/utils.py
@@ -1,183 +0,0 @@
-import os
-import sys
-import tempfile
-import json
-import time
-import shutil
-import venv
-import platform
-import subprocess
-import importlib.util
-from pathlib import Path
-from typing import Optional, Dict
-from contextlib import contextmanager
-from rich.console import Console
-
-console = Console()
-
-# Paths
-APP_DIR = Path.home() / ".pyrgo"
-CACHE_DIR = APP_DIR / "cache"
-BIN_DIR = APP_DIR / "bin"
-CONFIG_FILE = APP_DIR / "config.json"
-CACHE_DURATION = 600 # 10 minutes
-
-def ensure_dirs():
- APP_DIR.mkdir(exist_ok=True)
- CACHE_DIR.mkdir(exist_ok=True)
- BIN_DIR.mkdir(exist_ok=True)
-
-class ConfigManager:
- """Manages persistent configuration (Tokens, Bookmarks)."""
-
- @staticmethod
- def load() -> Dict:
- ensure_dirs()
- if not CONFIG_FILE.exists():
- return {"api_key": None, "bookmarks": {}}
- try:
- with open(CONFIG_FILE, "r") as f:
- return json.load(f)
- except:
- return {"api_key": None, "bookmarks": {}}
-
- @staticmethod
- def save(data: Dict):
- ensure_dirs()
- with open(CONFIG_FILE, "w") as f:
- json.dump(data, f, indent=4)
-
- @staticmethod
- def get_api_key() -> Optional[str]:
- # 1. Check Environment Variable
- if os.environ.get("GITHUB_TOKEN"):
- return os.environ["GITHUB_TOKEN"]
-
- # 2. Check .env file in current directory
- env_path = Path.cwd() / ".env"
- if env_path.exists():
- try:
- with open(env_path, "r") as f:
- for line in f:
- if line.strip().startswith("GITHUB_TOKEN="):
- return line.split("=", 1)[1].strip().strip('"').strip("'")
- except:
- pass
-
- # 3. Check Global Config
- config = ConfigManager.load()
- return config.get("api_key")
-
- @staticmethod
- def set_api_key(key: str):
- config = ConfigManager.load()
- config["api_key"] = key
- ConfigManager.save(config)
-
- @staticmethod
- def add_bookmark(name: str, url: str):
- config = ConfigManager.load()
- if "bookmarks" not in config:
- config["bookmarks"] = {}
- config["bookmarks"][name] = url
- ConfigManager.save(config)
-
- @staticmethod
- def get_bookmark(name: str) -> Optional[str]:
- config = ConfigManager.load()
- return config.get("bookmarks", {}).get(name)
-
- @staticmethod
- def list_bookmarks() -> Dict[str, str]:
- return ConfigManager.load().get("bookmarks", {})
-
-# Cache Logic
-def load_cache(key: str):
- ensure_dirs()
- cache_file = CACHE_DIR / f"{key}.json"
- if cache_file.exists():
- try:
- with open(cache_file, "r", encoding="utf-8") as f:
- data = json.load(f)
- if time.time() - data["timestamp"] < CACHE_DURATION:
- return data["content"]
- except:
- pass
- return None
-
-def save_cache(key: str, content: any):
- ensure_dirs()
- cache_file = CACHE_DIR / f"{key}.json"
- try:
- with open(cache_file, "w", encoding="utf-8") as f:
- json.dump({"timestamp": time.time(), "content": content}, f)
- except:
- pass
-
-# Dependency & Venv Logic
-def check_package_installed(package_name: str) -> bool:
- if package_name in sys.builtin_module_names:
- return True
- try:
- return importlib.util.find_spec(package_name) is not None
- except Exception:
- return False
-
-class VenvManager:
- """Manages temporary virtual environments for auto-installing dependencies."""
-
- def __init__(self):
- self.temp_dir = tempfile.mkdtemp(prefix="pyrgo_env_")
- self.venv_dir = Path(self.temp_dir)
- self.python_exe = self._get_python_exe()
- self.pip_exe = self._get_pip_exe()
-
- def _get_python_exe(self):
- if platform.system() == "Windows":
- return self.venv_dir / "Scripts" / "python.exe"
- return self.venv_dir / "bin" / "python"
-
- def _get_pip_exe(self):
- if platform.system() == "Windows":
- return self.venv_dir / "Scripts" / "pip.exe"
- return self.venv_dir / "bin" / "pip"
-
- def create(self):
- """Creates the virtual environment."""
- console.print("[blue]Creating temporary virtual environment...[/blue]")
- venv.create(self.venv_dir, with_pip=True)
-
- def install(self, packages: list):
- """Installs packages into the venv."""
- if not packages:
- return
- console.print(f"[blue]Installing dependencies: {', '.join(packages)}...[/blue]")
- subprocess.check_call([str(self.pip_exe), "install"] + packages, stdout=subprocess.DEVNULL)
-
- def cleanup(self):
- """Deletes the virtual environment."""
- shutil.rmtree(self.temp_dir, ignore_errors=True)
-
-# Standard Helpers
-def print_error(msg: str):
- console.print(f"[bold red]Error:[/bold red] {msg}")
-
-def print_info(msg: str):
- console.print(f"[bold blue]Info:[/bold blue] {msg}")
-
-def print_success(msg: str):
- console.print(f"[bold green]Success:[/bold green] {msg}")
-
-def print_warning(msg: str):
- console.print(f"[bold yellow]Warning:[/bold yellow] {msg}")
-
-@contextmanager
-def temp_python_file(content: str):
- fd, path = tempfile.mkstemp(suffix=".py", text=True)
- try:
- with os.fdopen(fd, 'w', encoding='utf-8') as tmp:
- tmp.write(content)
- yield path
- finally:
- if os.path.exists(path):
- os.remove(path)-
\ No newline at end of file
diff --git a/test/test_githrun.py b/test/test_githrun.py
@@ -0,0 +1,121 @@
+import os
+import sys
+import shutil
+import githrun
+from rich.console import Console
+from rich.panel import Panel
+
+console = Console()
+
+# --- CONFIGURATION ---
+# We use the repo you mentioned in the chat for testing
+TEST_REPO = "https://github.com/notamitgamer/adpkg"
+TEST_FILE = "https://github.com/notamitgamer/adpkg/blob/main/test1.py"
+TEST_FOLDER = "https://github.com/notamitgamer/adpkg/tree/main/"
+DOWNLOAD_TARGET = "test_download_artifact.py"
+
+def print_header(title):
+ console.print(Panel(f"[bold white]{title}[/bold white]", style="bold blue"))
+
+def test_cli_command(cmd, description):
+ console.print(f"\n[bold cyan]🔹 Testing CLI: {description}[/bold cyan]")
+ console.print(f"[dim]Command: {cmd}[/dim]")
+
+ exit_code = os.system(cmd)
+
+ if exit_code == 0:
+ console.print("[bold green]✔ Passed[/bold green]")
+ return True
+ else:
+ console.print("[bold red]✘ Failed[/bold red]")
+ return False
+
+def test_python_api():
+ print_header("Testing Python Source Code API")
+
+ # 1. Search Repository
+ console.print("\n[bold cyan]🔹 API: githrun.search_repository()[/bold cyan]")
+ try:
+ results = githrun.search_repository(TEST_REPO, "test1.py")
+ if results:
+ console.print(f"[green]✔ Success: Found {len(results)} matches.[/green]")
+ for item in results[:1]: # Show first match
+ console.print(f" Found: {item['path']} ({item['raw_url']})")
+ else:
+ console.print("[yellow]âš Warning: No results found (API limit might be hit).[/yellow]")
+ except Exception as e:
+ console.print(f"[bold red]✘ Error:[/bold red] {e}")
+
+ # 2. Get Folder Contents
+ console.print("\n[bold cyan]🔹 API: githrun.get_folder_contents()[/bold cyan]")
+ try:
+ items = githrun.get_folder_contents(TEST_FOLDER)
+ if items:
+ console.print(f"[green]✔ Success: Fetched {len(items)} items from folder.[/green]")
+ else:
+ console.print("[yellow]âš Warning: Folder appears empty or API error.[/yellow]")
+ except Exception as e:
+ console.print(f"[bold red]✘ Error:[/bold red] {e}")
+
+ # 3. Download File
+ console.print("\n[bold cyan]🔹 API: githrun.download_file()[/bold cyan]")
+ try:
+ path = githrun.download_file(TEST_FILE, output_path="api_download_test.py")
+ if os.path.exists(path):
+ console.print(f"[green]✔ Success: File downloaded to {path}[/green]")
+ os.remove(path) # Cleanup
+ else:
+ console.print("[bold red]✘ Failed: File not found after download call.[/bold red]")
+ except Exception as e:
+ console.print(f"[bold red]✘ Error:[/bold red] {e}")
+
+def main():
+ # Ensure we are in the right directory or package is installed
+ if not shutil.which("githrun"):
+ console.print("[bold red]CRITICAL: 'githrun' command not found.[/bold red]")
+ console.print("Please run [bold]pip install .[/bold] in the root directory first.")
+ return
+
+ print_header("Starting githrun Feature Tests")
+
+ # --- CLI TESTS ---
+
+ # 1. Help Command
+ test_cli_command("githrun --help", "Help Menu")
+
+ # 2. Run Command (Non-interactive)
+ # --yes skips the confirmation prompt
+ test_cli_command(f"githrun run {TEST_FILE} --yes", "Run Remote Script")
+
+ # 3. Inspect Mode
+ # Should print code but not run it
+ test_cli_command(f"githrun run {TEST_FILE} --inspect", "Inspect Code")
+
+ # 4. Show Folder
+ test_cli_command(f"githrun show {TEST_FOLDER}", "Show Folder Contents")
+
+ # 5. Find File (Non-interactive)
+ # Typer converts `interactive=True` to a flag `--no-interactive`
+ test_cli_command(f"githrun find {TEST_REPO} test --no-interactive", "Find File")
+
+ # 6. Download
+ test_cli_command(f"githrun download {TEST_FILE} -o {DOWNLOAD_TARGET}", "Download File")
+ if os.path.exists(DOWNLOAD_TARGET):
+ console.print("[dim] Verifying file existence... OK (Deleting now)[/dim]")
+ os.remove(DOWNLOAD_TARGET)
+
+ # 7. Bookmarks
+ test_cli_command(f"githrun bookmark add test-bm {TEST_FILE}", "Add Bookmark")
+ test_cli_command("githrun bookmark list", "List Bookmarks")
+ test_cli_command("githrun run test-bm --yes", "Run from Bookmark")
+
+ # --- API TESTS ---
+ test_python_api()
+
+ print_header("Test Suite Completed")
+
+if __name__ == "__main__":
+ try:
+ main()
+ except KeyboardInterrupt:
+ console.print("\n[bold red]Tests aborted by user.[/bold red]")+
\ No newline at end of file
diff --git a/test/test_pyrgo.py b/test/test_pyrgo.py
@@ -1,121 +0,0 @@
-import os
-import sys
-import shutil
-import pyrgo
-from rich.console import Console
-from rich.panel import Panel
-
-console = Console()
-
-# --- CONFIGURATION ---
-# We use the repo you mentioned in the chat for testing
-TEST_REPO = "https://github.com/notamitgamer/adpkg"
-TEST_FILE = "https://github.com/notamitgamer/adpkg/blob/main/test1.py"
-TEST_FOLDER = "https://github.com/notamitgamer/adpkg/tree/main/"
-DOWNLOAD_TARGET = "test_download_artifact.py"
-
-def print_header(title):
- console.print(Panel(f"[bold white]{title}[/bold white]", style="bold blue"))
-
-def test_cli_command(cmd, description):
- console.print(f"\n[bold cyan]🔹 Testing CLI: {description}[/bold cyan]")
- console.print(f"[dim]Command: {cmd}[/dim]")
-
- exit_code = os.system(cmd)
-
- if exit_code == 0:
- console.print("[bold green]✔ Passed[/bold green]")
- return True
- else:
- console.print("[bold red]✘ Failed[/bold red]")
- return False
-
-def test_python_api():
- print_header("Testing Python Source Code API")
-
- # 1. Search Repository
- console.print("\n[bold cyan]🔹 API: pyrgo.search_repository()[/bold cyan]")
- try:
- results = pyrgo.search_repository(TEST_REPO, "test1.py")
- if results:
- console.print(f"[green]✔ Success: Found {len(results)} matches.[/green]")
- for item in results[:1]: # Show first match
- console.print(f" Found: {item['path']} ({item['raw_url']})")
- else:
- console.print("[yellow]âš Warning: No results found (API limit might be hit).[/yellow]")
- except Exception as e:
- console.print(f"[bold red]✘ Error:[/bold red] {e}")
-
- # 2. Get Folder Contents
- console.print("\n[bold cyan]🔹 API: pyrgo.get_folder_contents()[/bold cyan]")
- try:
- items = pyrgo.get_folder_contents(TEST_FOLDER)
- if items:
- console.print(f"[green]✔ Success: Fetched {len(items)} items from folder.[/green]")
- else:
- console.print("[yellow]âš Warning: Folder appears empty or API error.[/yellow]")
- except Exception as e:
- console.print(f"[bold red]✘ Error:[/bold red] {e}")
-
- # 3. Download File
- console.print("\n[bold cyan]🔹 API: pyrgo.download_file()[/bold cyan]")
- try:
- path = pyrgo.download_file(TEST_FILE, output_path="api_download_test.py")
- if os.path.exists(path):
- console.print(f"[green]✔ Success: File downloaded to {path}[/green]")
- os.remove(path) # Cleanup
- else:
- console.print("[bold red]✘ Failed: File not found after download call.[/bold red]")
- except Exception as e:
- console.print(f"[bold red]✘ Error:[/bold red] {e}")
-
-def main():
- # Ensure we are in the right directory or package is installed
- if not shutil.which("pyrgo"):
- console.print("[bold red]CRITICAL: 'pyrgo' command not found.[/bold red]")
- console.print("Please run [bold]pip install .[/bold] in the root directory first.")
- return
-
- print_header("Starting Pyrgo Feature Tests")
-
- # --- CLI TESTS ---
-
- # 1. Help Command
- test_cli_command("pyrgo --help", "Help Menu")
-
- # 2. Run Command (Non-interactive)
- # --yes skips the confirmation prompt
- test_cli_command(f"pyrgo run {TEST_FILE} --yes", "Run Remote Script")
-
- # 3. Inspect Mode
- # Should print code but not run it
- test_cli_command(f"pyrgo run {TEST_FILE} --inspect", "Inspect Code")
-
- # 4. Show Folder
- test_cli_command(f"pyrgo show {TEST_FOLDER}", "Show Folder Contents")
-
- # 5. Find File (Non-interactive)
- # Typer converts `interactive=True` to a flag `--no-interactive`
- test_cli_command(f"pyrgo find {TEST_REPO} test --no-interactive", "Find File")
-
- # 6. Download
- test_cli_command(f"pyrgo download {TEST_FILE} -o {DOWNLOAD_TARGET}", "Download File")
- if os.path.exists(DOWNLOAD_TARGET):
- console.print("[dim] Verifying file existence... OK (Deleting now)[/dim]")
- os.remove(DOWNLOAD_TARGET)
-
- # 7. Bookmarks
- test_cli_command(f"pyrgo bookmark add test-bm {TEST_FILE}", "Add Bookmark")
- test_cli_command("pyrgo bookmark list", "List Bookmarks")
- test_cli_command("pyrgo run test-bm --yes", "Run from Bookmark")
-
- # --- API TESTS ---
- test_python_api()
-
- print_header("Test Suite Completed")
-
-if __name__ == "__main__":
- try:
- main()
- except KeyboardInterrupt:
- console.print("\n[bold red]Tests aborted by user.[/bold red]")-
\ No newline at end of file