cli.py (7013B)
1 import typer 2 from rich.console import Console 3 from rich.syntax import Syntax 4 from rich.table import Table 5 from rich.prompt import Prompt 6 from typing import Optional 7 8 # Import the version from __init__.py 9 from . import __version__ 10 11 from .core import ( 12 execute_remote_code, 13 search_repository, 14 get_folder_contents, 15 download_file, 16 download_folder, 17 install_tool, 18 login_github, 19 add_bookmark, 20 list_bookmarks 21 ) 22 from .network import RateLimitError, fetch_url_content, fetch_gist_content, convert_to_raw_url 23 from .utils import print_error, print_info, print_warning, print_success, ConfigManager 24 25 app = typer.Typer(help="Githrun: Run Python code from GitHub instantly.") 26 bookmark_app = typer.Typer(help="Manage bookmarks.") 27 app.add_typer(bookmark_app, name="bookmark") 28 29 console = Console() 30 31 # --- VERSION HANDLING --- 32 33 def version_callback(value: bool): 34 if value: 35 console.print(f"Githrun version: [bold cyan]{__version__}[/bold cyan]") 36 raise typer.Exit() 37 38 @app.callback() 39 def main( 40 version: bool = typer.Option( 41 None, 42 "--version", 43 "-v", 44 help="Show the application's version and exit.", 45 callback=version_callback, 46 is_eager=True 47 ) 48 ): 49 """ 50 Githrun: Run Python code from GitHub instantly. 51 """ 52 pass 53 54 # --- MAIN COMMANDS --- 55 56 @app.command() 57 def login(token: str = typer.Argument(..., help="Your GitHub Personal Access Token.")): 58 """ 59 Authenticate with GitHub to increase rate limits and access private repos. 60 """ 61 login_github(token) 62 print_success("GitHub token saved successfully!") 63 64 @app.command() 65 def install( 66 url: str = typer.Argument(..., help="URL of the script."), 67 name: str = typer.Option(..., "--name", "-n", help="Name of the command tool.") 68 ): 69 """ 70 Install a remote script as a local command-line tool. 71 """ 72 try: 73 msg = install_tool(url, name) 74 print_success(msg) 75 except Exception as e: 76 print_error(str(e)) 77 78 @app.command() 79 def run( 80 url: str = typer.Argument(..., help="GitHub URL, Gist URL, or Bookmark Name."), 81 inspect: bool = typer.Option(False, "--inspect", "-i", help="Print code without running."), 82 yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."), 83 auto_install: bool = typer.Option(False, "--auto-install", help="Auto-install missing dependencies.") 84 ): 85 """Download and execute a remote Python script.""" 86 try: 87 # Check bookmarks 88 b_url = ConfigManager.get_bookmark(url) 89 if b_url: 90 print_info(f"Resolved bookmark '{url}' to: {b_url}") 91 url = b_url 92 93 if inspect: 94 raw = convert_to_raw_url(url) 95 if raw.startswith("gist:"): 96 content = fetch_gist_content(raw.split(":")[1]) 97 else: 98 content = fetch_url_content(raw) 99 if content: 100 console.print(Syntax(content, "python", theme="monokai", line_numbers=True)) 101 return 102 103 if not yes: 104 console.print(f"\n[bold yellow]SECURITY WARNING:[/bold yellow] Running remote code from:") 105 console.print(f"[underline]{url}[/underline]\n") 106 if not typer.confirm("Execute this script?"): 107 raise typer.Exit() 108 109 execute_remote_code(url, auto_install=auto_install) 110 111 except RateLimitError: 112 print_error("GitHub API Rate Limit Exceeded (60 reqs/hr).") 113 if typer.confirm("Would you like to add an API Key to increase limits?"): 114 token = Prompt.ask("Enter GitHub Token") 115 login_github(token) 116 print_success("Token saved! Please try running the command again.") 117 except Exception as e: 118 print_error(str(e)) 119 raise typer.Exit(1) 120 121 # --- BOOKMARK COMMANDS --- 122 123 @bookmark_app.command("add") 124 def bookmark_add(name: str, url: str): 125 """Save a URL as a bookmark.""" 126 add_bookmark(name, url) 127 print_success(f"Bookmark '{name}' added.") 128 129 @bookmark_app.command("list") 130 def bookmark_list(): 131 """List all saved bookmarks.""" 132 bookmarks = list_bookmarks() 133 if not bookmarks: 134 print_warning("No bookmarks found.") 135 return 136 137 table = Table(title="Bookmarks") 138 table.add_column("Name", style="cyan") 139 table.add_column("URL", style="green") 140 for name, url in bookmarks.items(): 141 table.add_row(name, url) 142 console.print(table) 143 144 # --- EXISTING COMMANDS (UPDATED) --- 145 146 @app.command() 147 def find( 148 repo_url: str = typer.Argument(..., help="GitHub repository URL."), 149 query: str = typer.Argument(..., help="Search query (filename)."), 150 interactive: bool = typer.Option(True, help="Enable interactive selection.") 151 ): 152 """Search for files in a repo.""" 153 try: 154 with console.status("Scanning..."): 155 from .core import search_repository 156 results = search_repository(repo_url, query) 157 158 if not results: 159 print_warning("No results.") 160 return 161 162 table = Table(title=f"Results for {query}") 163 table.add_column("#", style="yellow") 164 table.add_column("Path", style="cyan") 165 166 for idx, r in enumerate(results): 167 table.add_row(str(idx+1), r['path']) 168 console.print(table) 169 170 if interactive: 171 choice = Prompt.ask("Select file # to Run (or 'q')") 172 if choice.isdigit(): 173 idx = int(choice) - 1 174 if 0 <= idx < len(results): 175 execute_remote_code(results[idx]['raw_url']) 176 177 except RateLimitError: 178 print_error("Rate Limit Hit.") 179 print_info("Use 'githrun login <token>' to fix this.") 180 except Exception as e: 181 print_error(str(e)) 182 183 @app.command() 184 def download( 185 url: str = typer.Argument(..., help="GitHub file or folder URL."), 186 output: Optional[str] = typer.Option(None, "--output", "-o", help="Output path.") 187 ): 188 """Download file or folder.""" 189 try: 190 from .core import download_folder, download_file 191 if "/tree/" in url: 192 path = download_folder(url, output) 193 print_success(f"Downloaded folder: {path}") 194 else: 195 path = download_file(url, output) 196 print_success(f"Downloaded file: {path}") 197 except Exception as e: 198 print_error(str(e)) 199 200 @app.command() 201 def show(url: str = typer.Argument(..., help="GitHub folder URL.")): 202 """Show folder contents.""" 203 try: 204 from .core import get_folder_contents 205 items = get_folder_contents(url) 206 207 if not items: 208 print_error("Empty folder or invalid URL.") 209 return 210 211 table = Table(title="Contents") 212 table.add_column("Name") 213 table.add_column("Type") 214 215 for item in items: 216 itype = "Dir" if item['type'] == 'dir' else "File" 217 table.add_row(item['name'], itype) 218 console.print(table) 219 except Exception as e: 220 print_error(str(e)) 221 222 if __name__ == "__main__": 223 app()