#!/usr/bin/env python3 """ MarkiTect Installer This script provides an easy way to install MarkiTect and make it available system-wide. It handles virtual environment creation, dependency installation, and creates symbolic links to make the commands available from anywhere. Usage: python install.py [options] Options: --prefix PATH Installation prefix (default: ~/.local) --system Install system-wide (requires sudo, uses /usr/local) --venv-dir PATH Custom virtual environment directory --no-symlinks Don't create symbolic links (manual PATH setup required) --force Force reinstallation over existing installation --dev Install in development mode with test dependencies --check Check if MarkiTect is already installed --uninstall Uninstall MarkiTect --help Show this help message """ import os import sys import subprocess import shutil import argparse from pathlib import Path import tempfile class MarkiTectInstaller: """MarkiTect installation manager.""" def __init__(self, prefix=None, system=False, venv_dir=None, force=False, dev=False): self.system = system self.force = force self.dev = dev # Determine installation paths if system: self.prefix = Path("/usr/local") self.bin_dir = self.prefix / "bin" self.venv_dir = Path(venv_dir) if venv_dir else self.prefix / "lib" / "markitect" else: self.prefix = Path(prefix) if prefix else Path.home() / ".local" self.bin_dir = self.prefix / "bin" self.venv_dir = Path(venv_dir) if venv_dir else self.prefix / "lib" / "markitect" self.project_dir = Path(__file__).parent.absolute() def check_requirements(self): """Check system requirements.""" print("๐Ÿ” Checking system requirements...") # Check Python version if sys.version_info < (3, 8): print("โŒ Python 3.8 or higher is required") sys.exit(1) print(f"โœ… Python {sys.version.split()[0]} found") # Check if pip is available try: subprocess.run([sys.executable, "-m", "pip", "--version"], check=True, capture_output=True) print("โœ… pip is available") except subprocess.CalledProcessError: print("โŒ pip is not available. Please install pip first.") sys.exit(1) # Check if git is available (optional) try: subprocess.run(["git", "--version"], check=True, capture_output=True) print("โœ… git is available") except (subprocess.CalledProcessError, FileNotFoundError): print("โš ๏ธ git is not available (optional for version info)") def check_existing_installation(self): """Check if MarkiTect is already installed.""" # Check for existing venv if self.venv_dir.exists(): print(f"๐Ÿ“ Existing installation found at {self.venv_dir}") return True # Check for existing binaries markitect_bin = self.bin_dir / "markitect" if markitect_bin.exists(): print(f"๐Ÿ“ Existing binary found at {markitect_bin}") return True return False def create_directories(self): """Create necessary directories.""" print(f"๐Ÿ“ Creating directories...") if self.system and not os.access(self.prefix, os.W_OK): print("โŒ System installation requires sudo privileges") print(" Please run with sudo or choose a different installation prefix") sys.exit(1) self.prefix.mkdir(parents=True, exist_ok=True) self.bin_dir.mkdir(parents=True, exist_ok=True) print(f"โœ… Created directories in {self.prefix}") def create_virtual_environment(self): """Create and set up virtual environment.""" print(f"๐Ÿ Creating virtual environment at {self.venv_dir}") if self.venv_dir.exists(): if self.force: print(f"๐Ÿ—‘๏ธ Removing existing installation...") shutil.rmtree(self.venv_dir) else: print("โŒ Virtual environment already exists. Use --force to overwrite.") sys.exit(1) # Create virtual environment subprocess.run([ sys.executable, "-m", "venv", str(self.venv_dir) ], check=True) # Get paths to venv executables if sys.platform == "win32": venv_python = self.venv_dir / "Scripts" / "python.exe" venv_pip = self.venv_dir / "Scripts" / "pip.exe" else: venv_python = self.venv_dir / "bin" / "python" venv_pip = self.venv_dir / "bin" / "pip" # Upgrade pip print("๐Ÿ“ฆ Upgrading pip...") subprocess.run([ str(venv_pip), "install", "--upgrade", "pip", "setuptools", "wheel" ], check=True) return venv_python, venv_pip def install_markitect(self, venv_python, venv_pip): """Install MarkiTect in the virtual environment.""" print("๐Ÿ“ฆ Installing MarkiTect...") install_cmd = [str(venv_pip), "install"] if self.dev: print("๐Ÿ› ๏ธ Installing in development mode with test dependencies...") # Install in editable mode from current directory install_cmd.extend(["-e", str(self.project_dir)]) # Install test dependencies subprocess.run(install_cmd, check=True) subprocess.run([ str(venv_pip), "install", "pytest", "pytest-cov", "black", "flake8", "mypy" ], check=True) else: # Install from current directory install_cmd.append(str(self.project_dir)) subprocess.run(install_cmd, check=True) print("โœ… MarkiTect installed successfully") def create_symlinks(self, no_symlinks=False): """Create symbolic links for global access.""" if no_symlinks: print("โš ๏ธ Skipping symbolic link creation") self.show_manual_setup() return print("๐Ÿ”— Creating symbolic links...") # Get venv bin directory if sys.platform == "win32": venv_bin = self.venv_dir / "Scripts" exe_suffix = ".exe" else: venv_bin = self.venv_dir / "bin" exe_suffix = "" # Commands to link commands = ["markitect", "tddai", "issue"] for cmd in commands: src = venv_bin / f"{cmd}{exe_suffix}" dst = self.bin_dir / cmd if src.exists(): # Remove existing symlink/file if dst.exists() or dst.is_symlink(): dst.unlink() # Create symlink try: dst.symlink_to(src) print(f"โœ… Created symlink: {dst} -> {src}") except OSError: # Fallback: create wrapper script self.create_wrapper_script(dst, src) else: print(f"โš ๏ธ Command {cmd} not found in virtual environment") def create_wrapper_script(self, dst, src): """Create a wrapper script when symlinks aren't available.""" print(f"๐Ÿ”ง Creating wrapper script: {dst}") if sys.platform == "win32": # Windows batch file dst = dst.with_suffix(".bat") content = f'@echo off\n"{src}" %*\n' else: # Unix shell script content = f'#!/bin/bash\nexec "{src}" "$@"\n' dst.write_text(content) if sys.platform != "win32": os.chmod(dst, 0o755) def show_manual_setup(self): """Show manual PATH setup instructions.""" print("\n๐Ÿ“‹ Manual Setup Instructions:") print("=" * 50) print(f"Add the following to your PATH environment variable:") print(f" {self.venv_dir / 'bin'}") print() print("For bash/zsh, add this line to ~/.bashrc or ~/.zshrc:") print(f' export PATH="{self.venv_dir / "bin"}:$PATH"') print() def test_installation(self): """Test the installation.""" print("๐Ÿงช Testing installation...") # Test markitect command try: markitect_bin = self.bin_dir / "markitect" if not markitect_bin.exists(): # Try direct venv path if sys.platform == "win32": markitect_bin = self.venv_dir / "Scripts" / "markitect.exe" else: markitect_bin = self.venv_dir / "bin" / "markitect" result = subprocess.run([ str(markitect_bin), "version", "--short" ], capture_output=True, text=True, check=True) version = result.stdout.strip() print(f"โœ… MarkiTect installed successfully - version {version}") return True except (subprocess.CalledProcessError, FileNotFoundError) as e: print(f"โŒ Installation test failed: {e}") return False def uninstall(self): """Uninstall MarkiTect.""" print("๐Ÿ—‘๏ธ Uninstalling MarkiTect...") removed_something = False # Remove virtual environment if self.venv_dir.exists(): print(f"๐Ÿ—‘๏ธ Removing virtual environment: {self.venv_dir}") shutil.rmtree(self.venv_dir) removed_something = True # Remove symlinks commands = ["markitect", "tddai", "issue"] for cmd in commands: for bin_path in [self.bin_dir / cmd, self.bin_dir / f"{cmd}.bat"]: if bin_path.exists() or bin_path.is_symlink(): print(f"๐Ÿ—‘๏ธ Removing: {bin_path}") bin_path.unlink() removed_something = True if removed_something: print("โœ… MarkiTect uninstalled successfully") else: print("โš ๏ธ No MarkiTect installation found") def install(self, no_symlinks=False): """Perform the complete installation.""" print("๐Ÿš€ Installing MarkiTect") print("=" * 50) self.check_requirements() if not self.force and self.check_existing_installation(): print("โŒ MarkiTect is already installed. Use --force to reinstall.") sys.exit(1) self.create_directories() venv_python, venv_pip = self.create_virtual_environment() self.install_markitect(venv_python, venv_pip) self.create_symlinks(no_symlinks) print() if self.test_installation(): print("๐ŸŽ‰ Installation completed successfully!") print() print("You can now use MarkiTect from anywhere:") print(" markitect --help") print(" markitect version") print(" tddai --help") print(" issue --help") else: print("โš ๏ธ Installation completed but tests failed") self.show_manual_setup() def check_installation_status(self): """Check current installation status.""" print("๐Ÿ” MarkiTect Installation Status") print("=" * 50) # Check virtual environment if self.venv_dir.exists(): print(f"โœ… Virtual environment: {self.venv_dir}") else: print(f"โŒ Virtual environment: Not found at {self.venv_dir}") # Check binaries commands = ["markitect", "tddai", "issue"] for cmd in commands: bin_path = self.bin_dir / cmd if bin_path.exists(): print(f"โœ… {cmd}: {bin_path}") else: print(f"โŒ {cmd}: Not found at {bin_path}") # Try to get version try: result = subprocess.run([ "markitect", "version", "--short" ], capture_output=True, text=True) if result.returncode == 0: version = result.stdout.strip() print(f"โœ… Working installation: version {version}") else: print("โŒ Installation found but not working") except FileNotFoundError: print("โŒ markitect command not available in PATH") def main(): parser = argparse.ArgumentParser( description="MarkiTect Installer", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__.split('\n\n')[1] # Show usage from docstring ) parser.add_argument("--prefix", type=Path, help="Installation prefix (default: ~/.local)") parser.add_argument("--system", action="store_true", help="Install system-wide (requires sudo)") parser.add_argument("--venv-dir", type=Path, help="Custom virtual environment directory") parser.add_argument("--no-symlinks", action="store_true", help="Don't create symbolic links") parser.add_argument("--force", action="store_true", help="Force reinstallation") parser.add_argument("--dev", action="store_true", help="Install in development mode") parser.add_argument("--check", action="store_true", help="Check installation status") parser.add_argument("--uninstall", action="store_true", help="Uninstall MarkiTect") args = parser.parse_args() # Create installer instance installer = MarkiTectInstaller( prefix=args.prefix, system=args.system, venv_dir=args.venv_dir, force=args.force, dev=args.dev ) # Handle different actions if args.check: installer.check_installation_status() elif args.uninstall: installer.uninstall() else: installer.install(no_symlinks=args.no_symlinks) if __name__ == "__main__": main()