mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 22:51:09 -05:00
Configure PostgreSQL with PostGIS support
- Updated database settings to use dj_database_url for environment-based configuration - Added dj-database-url dependency - Configured PostGIS backend for spatial data support - Set default DATABASE_URL for production PostgreSQL connection
This commit is contained in:
632
scripts/vm/github-setup.py
Executable file
632
scripts/vm/github-setup.py
Executable file
@@ -0,0 +1,632 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user