#!/usr/bin/env python3 """ ThrillWiki GitHub PAT Setup Helper Interactive script for setting up GitHub Personal Access Tokens with proper validation and integration with the automation system. Features: - Guided GitHub PAT creation process - Token validation and permission checking - Integration with existing github-auth.py patterns - Clear instructions for PAT scope requirements - Secure token storage with proper file permissions """ import sys import getpass import requests import argparse import subprocess from pathlib import Path # Configuration SCRIPT_DIR = Path(__file__).parent PROJECT_DIR = SCRIPT_DIR.parent.parent CONFIG_SCRIPT = SCRIPT_DIR / "automation-config.sh" GITHUB_AUTH_SCRIPT = PROJECT_DIR / "scripts" / "github-auth.py" TOKEN_FILE = PROJECT_DIR / ".github-pat" # GitHub API Configuration GITHUB_API_BASE = "https://api.github.com" REQUEST_TIMEOUT = 30 # Token scope requirements for different use cases TOKEN_SCOPES = { "public": { "description": "Public repositories only", "scopes": ["public_repo"], "note": "Suitable for public repositories and basic automation", }, "private": { "description": "Private repositories access", "scopes": ["repo"], "note": "Required for private repositories and full automation features", }, "full": { "description": "Full automation capabilities", "scopes": ["repo", "workflow", "read:org"], "note": "Recommended for complete automation setup with GitHub Actions", }, } class Colors: """ANSI color codes for terminal output""" RED = "\033[0;31m" GREEN = "\033[0;32m" YELLOW = "\033[1;33m" BLUE = "\033[0;34m" PURPLE = "\033[0;35m" CYAN = "\033[0;36m" BOLD = "\033[1m" NC = "\033[0m" # No Color def print_colored(message, color=Colors.NC): """Print colored message to terminal""" print(f"{color}{message}{Colors.NC}") def print_error(message): """Print error message""" print_colored(f"❌ Error: {message}", Colors.RED) def print_success(message): """Print success message""" print_colored(f"✅ {message}", Colors.GREEN) def print_warning(message): """Print warning message""" print_colored(f"⚠️ Warning: {message}", Colors.YELLOW) def print_info(message): """Print info message""" print_colored(f"ℹ️ {message}", Colors.BLUE) def print_step(step, total, message): """Print step progress""" print_colored(f"\n[{step}/{total}] {message}", Colors.CYAN) def validate_token_format(token): """Validate GitHub token format""" if not token: return False # GitHub token patterns patterns = [ lambda t: t.startswith("ghp_") and len(t) >= 40, # Classic PAT lambda t: t.startswith("github_pat_") and len(t) >= 50, # Fine-grained PAT lambda t: t.startswith("gho_") and len(t) >= 40, # OAuth token lambda t: t.startswith("ghu_") and len(t) >= 40, # User token lambda t: t.startswith("ghs_") and len(t) >= 40, # Server token ] return any(pattern(token) for pattern in patterns) def test_github_token(token, timeout=REQUEST_TIMEOUT): """Test GitHub token by making API call""" if not token: return False, "No token provided" try: headers = { "Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", } response = requests.get( f"{GITHUB_API_BASE}/user", headers=headers, timeout=timeout ) if response.status_code == 200: user_data = response.json() return ( True, f"Valid token for user: { user_data.get( 'login', 'unknown')}", ) elif response.status_code == 401: return False, "Invalid or expired token" elif response.status_code == 403: return False, "Token lacks required permissions" else: return ( False, f"API request failed with HTTP { response.status_code}", ) except requests.exceptions.RequestException as e: return False, f"Network error: {str(e)}" def get_token_permissions(token, timeout=REQUEST_TIMEOUT): """Get token permissions and scopes""" if not token: return None, "No token provided" try: headers = { "Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", } # Get user info and check token in response headers response = requests.get( f"{GITHUB_API_BASE}/user", headers=headers, timeout=timeout ) if response.status_code == 200: scopes = response.headers.get("X-OAuth-Scopes", "").split(", ") scopes = [scope.strip() for scope in scopes if scope.strip()] return scopes, None else: return ( None, f"Failed to get permissions: HTTP { response.status_code}", ) except requests.exceptions.RequestException as e: return None, f"Network error: {str(e)}" def check_repository_access(token, repo_url=None, timeout=REQUEST_TIMEOUT): """Check if token can access the repository""" if not token: return False, "No token provided" # Try to determine repository from git remote if not repo_url: try: result = subprocess.run( ["git", "remote", "get-url", "origin"], cwd=PROJECT_DIR, capture_output=True, text=True, timeout=10, ) if result.returncode == 0: repo_url = result.stdout.strip() except (subprocess.TimeoutExpired, FileNotFoundError): pass if not repo_url: return None, "Could not determine repository URL" # Extract owner/repo from URL if "github.com" in repo_url: # Handle both SSH and HTTPS URLs if repo_url.startswith("git@github.com:"): repo_path = repo_url.replace("git@github.com:", "").replace(".git", "") elif "github.com/" in repo_url: repo_path = repo_url.split("github.com/")[-1].replace(".git", "") else: return None, "Could not parse repository URL" try: headers = { "Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", } response = requests.get( f"{GITHUB_API_BASE}/repos/{repo_path}", headers=headers, timeout=timeout, ) if response.status_code == 200: repo_data = response.json() return ( True, f"Access confirmed for { repo_data.get( 'full_name', repo_path)}", ) elif response.status_code == 404: return False, "Repository not found or no access" elif response.status_code == 403: return False, "Access denied - insufficient permissions" else: return ( False, f"Access check failed: HTTP { response.status_code}", ) except requests.exceptions.RequestException as e: return None, f"Network error: {str(e)}" return None, "Not a GitHub repository" def show_pat_instructions(): """Show detailed PAT creation instructions""" print_colored("\n" + "=" * 60, Colors.BOLD) print_colored("GitHub Personal Access Token (PAT) Setup Guide", Colors.BOLD) print_colored("=" * 60, Colors.BOLD) print("\n🔐 Why do you need a GitHub PAT?") print(" • Access private repositories") print(" • Avoid GitHub API rate limits") print(" • Enable automated repository operations") print(" • Secure authentication without passwords") print("\n📋 Step-by-step PAT creation:") print(" 1. Go to: https://github.com/settings/tokens") print(" 2. Click 'Generate new token' → 'Generate new token (classic)'") print(" 3. Enter a descriptive note: 'ThrillWiki Automation'") print(" 4. Set expiration (recommended: 90 days for security)") print(" 5. Select appropriate scopes:") print("\n🎯 Recommended scope configurations:") for scope_type, config in TOKEN_SCOPES.items(): print(f"\n {scope_type.upper()} REPOSITORIES:") print(f" • Description: {config['description']}") print(f" • Required scopes: {', '.join(config['scopes'])}") print(f" • Note: {config['note']}") print("\n⚡ Quick setup for most users:") print(" • Select 'repo' scope for full repository access") print(" • This enables all automation features") print("\n🔒 Security best practices:") print(" • Use descriptive token names") print(" • Set reasonable expiration dates") print(" • Regenerate tokens regularly") print(" • Never share tokens in public") print(" • Delete unused tokens immediately") print("\n📱 After creating your token:") print(" • Copy the token immediately (it won't be shown again)") print(" • Return to this script and paste it when prompted") print(" • The script will validate and securely store your token") def interactive_token_setup(): """Interactive token setup process""" print_colored("\n🚀 ThrillWiki GitHub PAT Setup", Colors.BOLD) print_colored("================================", Colors.BOLD) # Check if token already exists if TOKEN_FILE.exists(): try: existing_token = TOKEN_FILE.read_text().strip() if existing_token: print_info("Existing GitHub token found") # Test existing token valid, message = test_github_token(existing_token) if valid: print_success(f"Current token is valid: {message}") choice = ( input("\nDo you want to replace the existing token? (y/N): ") .strip() .lower() ) if choice not in ["y", "yes"]: print_info("Keeping existing token") return True else: print_warning(f"Current token is invalid: {message}") print_info("Setting up new token...") except Exception as e: print_warning(f"Could not read existing token: {e}") # Show instructions print("\n" + "=" * 50) choice = ( input("Do you want to see PAT creation instructions? (Y/n): ").strip().lower() ) if choice not in ["n", "no"]: show_pat_instructions() # Get token from user print_step(1, 3, "Enter your GitHub Personal Access Token") print("📋 Please paste your GitHub PAT below:") print(" (Input will be hidden for security)") while True: try: token = getpass.getpass("GitHub PAT: ").strip() if not token: print_error("No token entered. Please try again.") continue # Validate format if not validate_token_format(token): print_error( "Invalid token format. GitHub tokens should start with 'ghp_', 'github_pat_', etc." ) retry = input("Try again? (Y/n): ").strip().lower() if retry in ["n", "no"]: return False continue break except KeyboardInterrupt: print("\nSetup cancelled by user") return False # Test token print_step(2, 3, "Validating GitHub token") print("🔍 Testing token with GitHub API...") valid, message = test_github_token(token) if not valid: print_error(f"Token validation failed: {message}") return False print_success(message) # Check permissions print("🔐 Checking token permissions...") scopes, error = get_token_permissions(token) if error: print_warning(f"Could not check permissions: {error}") else: print_success( f"Token scopes: {', '.join(scopes) if scopes else 'None detected'}" ) # Check for recommended scopes has_repo = "repo" in scopes or "public_repo" in scopes if not has_repo: print_warning("Token may lack repository access permissions") # Check repository access print("📁 Checking repository access...") access, access_message = check_repository_access(token) if access is True: print_success(access_message) elif access is False: print_warning(access_message) else: print_info(access_message or "Repository access check skipped") # Store token print_step(3, 3, "Storing GitHub token securely") try: # Backup existing token if it exists if TOKEN_FILE.exists(): backup_file = TOKEN_FILE.with_suffix(".backup") TOKEN_FILE.rename(backup_file) print_info(f"Existing token backed up to: {backup_file}") # Write new token TOKEN_FILE.write_text(token) TOKEN_FILE.chmod(0o600) # Read/write for owner only print_success(f"Token stored securely in: {TOKEN_FILE}") # Try to update configuration via config script try: if CONFIG_SCRIPT.exists(): subprocess.run( [ "bash", "-c", f'source {CONFIG_SCRIPT} && store_github_token "{token}"', ], check=False, capture_output=True, ) print_success("Token added to automation configuration") except Exception as e: print_warning(f"Could not update automation config: {e}") print_success("GitHub PAT setup completed successfully!") return True except Exception as e: print_error(f"Failed to store token: {e}") return False def validate_existing_token(): """Validate existing GitHub token""" print_colored("\n🔍 GitHub Token Validation", Colors.BOLD) print_colored("===========================", Colors.BOLD) if not TOKEN_FILE.exists(): print_error("No GitHub token file found") print_info(f"Expected location: {TOKEN_FILE}") return False try: token = TOKEN_FILE.read_text().strip() if not token: print_error("Token file is empty") return False print_info("Validating stored token...") # Format validation if not validate_token_format(token): print_error("Token format is invalid") return False print_success("Token format is valid") # API validation valid, message = test_github_token(token) if not valid: print_error(f"Token validation failed: {message}") return False print_success(message) # Check permissions scopes, error = get_token_permissions(token) if error: print_warning(f"Could not check permissions: {error}") else: print_success( f"Token scopes: { ', '.join(scopes) if scopes else 'None detected'}" ) # Check repository access access, access_message = check_repository_access(token) if access is True: print_success(access_message) elif access is False: print_warning(access_message) else: print_info(access_message or "Repository access check inconclusive") print_success("Token validation completed") return True except Exception as e: print_error(f"Error reading token: {e}") return False def remove_token(): """Remove stored GitHub token""" print_colored("\n🗑️ GitHub Token Removal", Colors.BOLD) print_colored("=========================", Colors.BOLD) if not TOKEN_FILE.exists(): print_info("No GitHub token file found") return True try: # Backup before removal backup_file = TOKEN_FILE.with_suffix(".removed") TOKEN_FILE.rename(backup_file) print_success(f"Token removed and backed up to: {backup_file}") # Try to remove from config try: if CONFIG_SCRIPT.exists(): subprocess.run( [ "bash", "-c", f"source {CONFIG_SCRIPT} && remove_github_token", ], check=False, capture_output=True, ) print_success("Token removed from automation configuration") except Exception as e: print_warning(f"Could not update automation config: {e}") print_success("GitHub token removed successfully") return True except Exception as e: print_error(f"Error removing token: {e}") return False def show_token_status(): """Show current token status""" print_colored("\n📊 GitHub Token Status", Colors.BOLD) print_colored("======================", Colors.BOLD) # Check token file print(f"📁 Token file: {TOKEN_FILE}") if TOKEN_FILE.exists(): print_success("Token file exists") # Check permissions perms = oct(TOKEN_FILE.stat().st_mode)[-3:] if perms == "600": print_success(f"File permissions: {perms} (secure)") else: print_warning(f"File permissions: {perms} (should be 600)") # Quick validation try: token = TOKEN_FILE.read_text().strip() if token: if validate_token_format(token): print_success("Token format is valid") # Quick API test valid, message = test_github_token(token, timeout=10) if valid: print_success(f"Token is valid: {message}") else: print_error(f"Token is invalid: {message}") else: print_error("Token format is invalid") else: print_error("Token file is empty") except Exception as e: print_error(f"Error reading token: {e}") else: print_warning("Token file not found") # Check config integration print(f"\n⚙️ Configuration: {CONFIG_SCRIPT}") if CONFIG_SCRIPT.exists(): print_success("Configuration script available") else: print_warning("Configuration script not found") # Check existing GitHub auth script print(f"\n🔐 GitHub auth script: {GITHUB_AUTH_SCRIPT}") if GITHUB_AUTH_SCRIPT.exists(): print_success("GitHub auth script available") else: print_warning("GitHub auth script not found") def main(): """Main CLI interface""" parser = argparse.ArgumentParser( description="ThrillWiki GitHub PAT Setup Helper", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: %(prog)s setup # Interactive token setup %(prog)s validate # Validate existing token %(prog)s status # Show token status %(prog)s remove # Remove stored token %(prog)s --help # Show this help For detailed PAT creation instructions, run: %(prog)s setup """, ) parser.add_argument( "command", choices=["setup", "validate", "status", "remove", "help"], help="Command to execute", ) parser.add_argument( "--token", help="GitHub token to validate (for validate command)" ) parser.add_argument( "--force", action="store_true", help="Force operation without prompts" ) if len(sys.argv) == 1: parser.print_help() sys.exit(1) args = parser.parse_args() try: if args.command == "setup": success = interactive_token_setup() sys.exit(0 if success else 1) elif args.command == "validate": if args.token: # Validate provided token print_info("Validating provided token...") if validate_token_format(args.token): valid, message = test_github_token(args.token) if valid: print_success(message) sys.exit(0) else: print_error(message) sys.exit(1) else: print_error("Invalid token format") sys.exit(1) else: # Validate existing token success = validate_existing_token() sys.exit(0 if success else 1) elif args.command == "status": show_token_status() sys.exit(0) elif args.command == "remove": if not args.force: confirm = ( input("Are you sure you want to remove the GitHub token? (y/N): ") .strip() .lower() ) if confirm not in ["y", "yes"]: print_info("Operation cancelled") sys.exit(0) success = remove_token() sys.exit(0 if success else 1) elif args.command == "help": parser.print_help() sys.exit(0) except KeyboardInterrupt: print("\nOperation cancelled by user") sys.exit(1) except Exception as e: print_error(f"Unexpected error: {e}") sys.exit(1) if __name__ == "__main__": main()