Files
markitect-main/install.py
tegwick 8e6ba272ca feat: implement markitect installer with version/release commands (issue #80)
- Add comprehensive version information system with git integration
- Add `markitect version` and `markitect release` commands with multiple output formats
- Add global `--version` flag for quick version checking
- Create Python installer script with advanced options (install.py)
- Create shell installer wrapper for easy installation (install.sh)
- Add comprehensive installation documentation (INSTALL.md)
- Support user and system-wide installations with virtual environments
- Include development mode installation with test dependencies
- Add installation status checking and uninstall functionality

Commands added:
- `markitect --version` - Quick version display
- `markitect version [--short]` - Detailed version information
- `markitect release [--format text|json|yaml]` - Release information

Installer features:
- Automatic virtual environment creation
- Symbolic link management for global access
- Custom installation paths and prefixes
- Development mode with test dependencies
- Installation validation and troubleshooting

Resolves #80

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-03 05:47:02 +02:00

388 lines
14 KiB
Python

#!/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()