#!/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 os import sys import json import time import getpass import requests import argparse import subprocess from pathlib import Path from urllib.parse import urlencode # 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()