#!/usr/bin/env python3 """ PyPI Build and Upload Script for SuperClaude Framework Handles building, validation, and uploading to PyPI with proper error handling """ import os import sys import shutil import subprocess import argparse from pathlib import Path from typing import Tuple, List, Optional # Project root PROJECT_ROOT = Path(__file__).parent.parent DIST_DIR = PROJECT_ROOT / "dist" BUILD_DIR = PROJECT_ROOT / "build" def run_command(cmd: List[str], description: str) -> Tuple[bool, str]: """Run a command and return success status and output""" print(f"๐Ÿ”„ {description}...") try: result = subprocess.run( cmd, capture_output=True, text=True, cwd=PROJECT_ROOT, check=True ) print(f"โœ… {description} completed successfully") return True, result.stdout except subprocess.CalledProcessError as e: print(f"โŒ {description} failed:") print(f" Exit code: {e.returncode}") print(f" Error: {e.stderr}") return False, e.stderr except Exception as e: print(f"โŒ {description} failed with exception: {e}") return False, str(e) def clean_build_artifacts(): """Clean previous build artifacts""" artifacts = [DIST_DIR, BUILD_DIR, PROJECT_ROOT / "superclaude.egg-info"] for artifact in artifacts: if artifact.exists(): print(f"๐Ÿงน Removing {artifact}") if artifact.is_dir(): shutil.rmtree(artifact) else: artifact.unlink() def install_build_tools() -> bool: """Install required build tools""" tools = ["build", "twine"] for tool in tools: success, _ = run_command( [sys.executable, "-m", "pip", "install", "--upgrade", tool], f"Installing {tool}", ) if not success: return False return True def validate_project_structure() -> bool: """Validate project structure before building""" required_files = [ "pyproject.toml", "README.md", "LICENSE", "superclaude/__init__.py", "superclaude/__main__.py", "setup/__init__.py", ] print("๐Ÿ” Validating project structure...") for file_path in required_files: full_path = PROJECT_ROOT / file_path if not full_path.exists(): print(f"โŒ Missing required file: {file_path}") return False # Check if version is consistent try: from superclaude import __version__ print(f"๐Ÿ“ฆ Package version: {__version__}") except ImportError as e: print(f"โŒ Could not import version from SuperClaude: {e}") return False print("โœ… Project structure validation passed") return True def build_package() -> bool: """Build the package""" return run_command( [sys.executable, "-m", "build"], "Building package distributions" )[0] def validate_distribution() -> bool: """Validate the built distribution""" if not DIST_DIR.exists(): print("โŒ Distribution directory does not exist") return False dist_files = list(DIST_DIR.glob("*")) if not dist_files: print("โŒ No distribution files found") return False print(f"๐Ÿ“ฆ Found distribution files:") for file in dist_files: print(f" - {file.name}") # Check with twine return run_command( [sys.executable, "-m", "twine", "check"] + [str(f) for f in dist_files], "Validating distributions with twine", )[0] def upload_to_testpypi() -> bool: """Upload to TestPyPI for testing""" dist_files = list(DIST_DIR.glob("*")) return run_command( [sys.executable, "-m", "twine", "upload", "--repository", "testpypi"] + [str(f) for f in dist_files], "Uploading to TestPyPI", )[0] def upload_to_pypi() -> bool: """Upload to production PyPI""" dist_files = list(DIST_DIR.glob("*")) # Check if we have API token in environment if os.getenv("PYPI_API_TOKEN"): cmd = [ sys.executable, "-m", "twine", "upload", "--username", "__token__", "--password", os.getenv("PYPI_API_TOKEN"), ] + [str(f) for f in dist_files] else: # Fall back to .pypirc configuration cmd = [sys.executable, "-m", "twine", "upload"] + [str(f) for f in dist_files] return run_command(cmd, "Uploading to PyPI")[0] def test_installation_from_testpypi() -> bool: """Test installation from TestPyPI""" print("๐Ÿงช Testing installation from TestPyPI...") print(" Note: This will install in a separate process") success, output = run_command( [ sys.executable, "-m", "pip", "install", "--index-url", "https://test.pypi.org/simple/", "--extra-index-url", "https://pypi.org/simple/", "SuperClaude", "--force-reinstall", "--no-deps", ], "Installing from TestPyPI", ) if success: print("โœ… Test installation successful") # Try to import the package try: import superclaude print(f"โœ… Package import successful, version: {superclaude.__version__}") return True except ImportError as e: print(f"โŒ Package import failed: {e}") return False return False def main(): """Main execution function""" parser = argparse.ArgumentParser(description="Build and upload SuperClaude to PyPI") parser.add_argument( "--testpypi", action="store_true", help="Upload to TestPyPI instead of PyPI" ) parser.add_argument( "--test-install", action="store_true", help="Test installation from TestPyPI" ) parser.add_argument( "--skip-build", action="store_true", help="Skip build step (use existing dist)" ) parser.add_argument( "--skip-validation", action="store_true", help="Skip validation steps" ) parser.add_argument( "--clean", action="store_true", help="Only clean build artifacts" ) args = parser.parse_args() # Change to project root os.chdir(PROJECT_ROOT) if args.clean: clean_build_artifacts() return print("๐Ÿš€ SuperClaude PyPI Build and Upload Script") print(f"๐Ÿ“ Working directory: {PROJECT_ROOT}") # Step 1: Clean previous builds clean_build_artifacts() # Step 2: Install build tools if not install_build_tools(): print("โŒ Failed to install build tools") sys.exit(1) # Step 3: Validate project structure if not args.skip_validation and not validate_project_structure(): print("โŒ Project structure validation failed") sys.exit(1) # Step 4: Build package if not args.skip_build: if not build_package(): print("โŒ Package build failed") sys.exit(1) # Step 5: Validate distribution if not args.skip_validation and not validate_distribution(): print("โŒ Distribution validation failed") sys.exit(1) # Step 6: Upload if args.testpypi: if not upload_to_testpypi(): print("โŒ Upload to TestPyPI failed") sys.exit(1) # Test installation if requested if args.test_install: if not test_installation_from_testpypi(): print("โŒ Test installation failed") sys.exit(1) else: # Confirm production upload response = input( "๐Ÿšจ Upload to production PyPI? This cannot be undone! (yes/no): " ) if response.lower() != "yes": print("โŒ Upload cancelled") sys.exit(1) if not upload_to_pypi(): print("โŒ Upload to PyPI failed") sys.exit(1) print("โœ… All operations completed successfully!") if __name__ == "__main__": main()