Skip to main content

#!/usr/bin/env python3 """ CODITECT Bootstrap Installer

This script bootstraps the CODITECT installation by:

  1. Checking prerequisites (Python 3.10+, Git)
  2. Installing and authenticating GitHub CLI
  3. Optionally configuring SSH keys
  4. Cloning coditect-core to ~/PROJECTS/
  5. Running the full CODITECT-CORE-INITIAL-SETUP.py

Usage: curl -sL https://storage.googleapis.com/coditect-dist/install.py | python3

Authentication: Uses GitHub CLI (gh) for secure OAuth-based authentication. No personal access tokens required.

Copyright (c) 2025-2026 AZ1.AI Inc. All Rights Reserved. """

import os import sys import subprocess import shutil import platform from pathlib import Path from datetime import datetime

Configuration

REPO_URL = "https://github.com/coditect-ai/coditect-core.git" REPO_NAME = "coditect-core" MIN_PYTHON_VERSION = (3, 10) MIN_GIT_VERSION = "2.30" PROJECTS_DIR = Path.home() / "PROJECTS"

Colors for terminal output

Shared Colors module (consolidates 36 duplicate definitions)

_script_dir = Path(file).parent sys.path.insert(0, str(_script_dir / "core")) from colors import Colors

def print_banner(): """Print the CODITECT installation banner.""" print(f""" {Colors.CYAN}================================================================================ CODITECT BOOTSTRAP INSTALLER ================================================================================{Colors.RESET}

This installer will:

  1. Check prerequisites (Python 3.10+, Git 2.30+)
  2. Set up GitHub CLI and authentication
  3. Optionally configure SSH keys for GitHub
  4. Clone CODITECT to ~/PROJECTS/coditect-core
  5. Run the full installation setup

{Colors.YELLOW}Documentation:{Colors.RESET} https://storage.googleapis.com/coditect-dist/docs/USER-QUICK-START.md {Colors.YELLOW}GitHub CLI Reference:{Colors.RESET} https://storage.googleapis.com/coditect-dist/docs/GITHUB-CLI-USER-MANAGEMENT.md """)

def print_section(title: str): """Print a section header.""" print(f"\n{Colors.CYAN}{'=' * 70}") print(f" {title}") print(f"{'=' * 70}{Colors.RESET}\n")

def print_step(step: int, total: int, message: str): """Print a step indicator.""" print(f"\n{Colors.BLUE}[{step}/{total}]{Colors.RESET} {Colors.BOLD}{message}{Colors.RESET}")

def print_success(message: str): """Print a success message.""" print(f" {Colors.GREEN}✓{Colors.RESET} {message}")

def print_error(message: str): """Print an error message.""" print(f" {Colors.RED}✗{Colors.RESET} {message}")

def print_warning(message: str): """Print a warning message.""" print(f" {Colors.YELLOW}!{Colors.RESET} {message}")

def print_info(message: str): """Print an info message.""" print(f" {Colors.CYAN}ℹ{Colors.RESET} {message}")

def run_command(cmd: list, capture: bool = True, check: bool = True, input_text: str = None) -> subprocess.CompletedProcess: """Run a command and return the result.""" try: result = subprocess.run( cmd, capture_output=capture, text=True, check=check, input=input_text ) return result except subprocess.CalledProcessError as e: return e except FileNotFoundError: return None

def prompt_yes_no(question: str, default: bool = True) -> bool: """Prompt user for yes/no answer.""" suffix = " [Y/n]: " if default else " [y/N]: " answer = input(f" {question}{suffix}").strip().lower() if not answer: return default return answer in ('y', 'yes')

def check_python_version() -> bool: """Check if Python version meets requirements.""" current = sys.version_info[:2] if current >= MIN_PYTHON_VERSION: print_success(f"Python {current[0]}.{current[1]} (required: >= {MIN_PYTHON_VERSION[0]}.{MIN_PYTHON_VERSION[1]})") return True else: print_error(f"Python {current[0]}.{current[1]} - requires >= {MIN_PYTHON_VERSION[0]}.{MIN_PYTHON_VERSION[1]}") return False

def check_git_version() -> bool: """Check if Git version meets requirements.""" result = run_command(["git", "--version"], check=False) if result is None: print_error("Git not found - please install Git") print_info("Run: xcode-select --install") return False

version_str = result.stdout.strip().replace("git version ", "")
parts = version_str.split(".")
if len(parts) >= 2:
version = f"{parts[0]}.{parts[1]}"
if version >= MIN_GIT_VERSION:
print_success(f"Git {version_str} (required: >= {MIN_GIT_VERSION})")
return True

print_error(f"Git {version_str} - requires >= {MIN_GIT_VERSION}")
return False

def check_homebrew() -> bool: """Check if Homebrew is installed.""" result = run_command(["brew", "--version"], check=False) if result is None or result.returncode != 0: return False return True

def install_homebrew() -> bool: """Install Homebrew.""" print_info("Installing Homebrew...") print_info("This may take a few minutes and require your password.")

install_cmd = '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
result = subprocess.run(install_cmd, shell=True)

if result.returncode == 0:
print_success("Homebrew installed successfully")
return True
else:
print_error("Failed to install Homebrew")
return False

def check_gh_cli() -> bool: """Check if GitHub CLI is installed.""" result = run_command(["gh", "--version"], check=False) if result is None or result.returncode != 0: return False version = result.stdout.split('\n')[0] if result.stdout else "unknown" print_success(f"GitHub CLI installed ({version.strip()})") return True

def install_gh_cli() -> bool: """Install GitHub CLI.""" if check_homebrew(): print_info("Installing GitHub CLI via Homebrew...") result = run_command(["brew", "install", "gh"], check=False, capture=False) if result and result.returncode == 0: print_success("GitHub CLI installed successfully") return True

print_error("Could not install GitHub CLI automatically")
print_info("Please install manually:")
print_info(" Option 1: brew install gh")
print_info(" Option 2: Download from https://cli.github.com")
return False

def check_gh_auth() -> bool: """Check if GitHub CLI is authenticated.""" result = run_command(["gh", "auth", "status"], check=False) if result is None: return False return result.returncode == 0

def get_gh_username() -> str: """Get the authenticated GitHub username.""" result = run_command(["gh", "api", "user", "--jq", ".login"], check=False) if result and result.returncode == 0: return result.stdout.strip() return None

def setup_gh_auth() -> bool: """Set up GitHub CLI authentication interactively.""" print_info("Starting GitHub authentication...") print_info("A browser window will open. Log in to GitHub and authorize the CLI.") print()

# Run gh auth login interactively with admin:public_key scope for SSH key management
result = subprocess.run(
["gh", "auth", "login", "--web", "--git-protocol", "https", "-s", "admin:public_key"],
capture_output=False
)

if result.returncode == 0:
username = get_gh_username()
if username:
print_success(f"Authenticated as {username}")
else:
print_success("GitHub authentication complete")
return True
else:
print_error("GitHub authentication failed")
return False

def check_ssh_keys() -> dict: """Check for existing SSH keys.""" ssh_dir = Path.home() / ".ssh" keys = { "ed25519": ssh_dir / "id_ed25519", "rsa": ssh_dir / "id_rsa", "ecdsa": ssh_dir / "id_ecdsa" }

found = {}
for key_type, key_path in keys.items():
if key_path.exists() and (key_path.with_suffix(".pub")).exists():
found[key_type] = key_path

return found

def generate_ssh_key() -> Path: """Generate a new SSH key.""" ssh_dir = Path.home() / ".ssh" ssh_dir.mkdir(mode=0o700, exist_ok=True)

key_path = ssh_dir / "id_ed25519"

if key_path.exists():
print_warning(f"SSH key already exists at {key_path}")
if not prompt_yes_no("Overwrite existing key?", default=False):
return key_path

# Get email for key comment
email = input(" Enter your email for the SSH key (or press Enter to skip): ").strip()
if not email:
email = "coditect-user"

print_info("Generating SSH key (press Enter to accept defaults)...")

# Generate key with empty passphrase for simplicity
result = subprocess.run(
["ssh-keygen", "-t", "ed25519", "-C", email, "-f", str(key_path), "-N", ""],
capture_output=True,
text=True
)

if result.returncode == 0:
print_success(f"SSH key generated at {key_path}")
return key_path
else:
print_error("Failed to generate SSH key")
return None

def start_ssh_agent() -> bool: """Start the SSH agent and add the key.""" # Check if agent is running if "SSH_AUTH_SOCK" not in os.environ: result = run_command(["ssh-agent", "-s"], check=False) if result and result.returncode == 0: # Parse and set environment variables for line in result.stdout.split('\n'): if '=' in line and ';' in line: var = line.split(';')[0] if '=' in var: key, value = var.split('=', 1) os.environ[key] = value return True

def add_ssh_key_to_agent(key_path: Path) -> bool: """Add SSH key to the agent.""" start_ssh_agent() result = run_command(["ssh-add", str(key_path)], check=False) if result and result.returncode == 0: print_success("SSH key added to agent") return True return False

def add_ssh_key_to_github(key_path: Path) -> bool: """Add SSH key to GitHub using gh CLI.""" pub_key = key_path.with_suffix(".pub") if not pub_key.exists(): print_error(f"Public key not found: {pub_key}") return False

hostname = platform.node() or "unknown"
title = f"{hostname} ({datetime.now().strftime('%Y-%m-%d')})"

result = run_command(
["gh", "ssh-key", "add", str(pub_key), "--title", title],
check=False
)

if result and result.returncode == 0:
print_success(f"SSH key added to GitHub as '{title}'")
return True
elif result and "already exists" in (result.stderr or ""):
print_warning("This SSH key is already registered with GitHub")
return True
elif result and "admin:public_key" in (result.stderr or ""):
# Need to refresh auth with SSH key management scope
print_warning("GitHub auth needs additional permissions for SSH key management")
if prompt_yes_no("Refresh GitHub auth to add SSH key permissions?", default=True):
refresh_result = subprocess.run(
["gh", "auth", "refresh", "-h", "github.com", "-s", "admin:public_key"],
capture_output=False
)
if refresh_result.returncode == 0:
# Retry adding the key
return add_ssh_key_to_github(key_path)
print_info("You can add the SSH key manually later:")
print_info(f" gh ssh-key add {pub_key} --title \"{title}\"")
return False
else:
print_error("Failed to add SSH key to GitHub")
if result and result.stderr:
print_info(result.stderr.strip())
return False

def test_ssh_connection() -> bool: """Test SSH connection to GitHub.""" result = run_command( ["ssh", "-T", "git@github.com", "-o", "StrictHostKeyChecking=accept-new"], check=False ) # GitHub returns exit code 1 even on success (it says "shell access not provided") if result: output = (result.stdout or "") + (result.stderr or "") if "successfully authenticated" in output.lower(): print_success("SSH connection to GitHub verified") return True return False

def setup_ssh_keys() -> bool: """Interactive SSH key setup.""" print_section("SSH Key Setup (Optional)")

print(f"""

SSH keys provide secure, passwordless authentication with GitHub. This is {Colors.YELLOW}recommended{Colors.RESET} but not required.

Without SSH keys, you'll use HTTPS with token authentication. """)

if not prompt_yes_no("Would you like to set up SSH keys?", default=True):
print_info("Skipping SSH key setup")
return True

# Check for existing keys
existing_keys = check_ssh_keys()

if existing_keys:
print_success(f"Found existing SSH keys: {', '.join(existing_keys.keys())}")
key_type = list(existing_keys.keys())[0]
key_path = existing_keys[key_type]

if prompt_yes_no(f"Use existing {key_type} key?", default=True):
pass # Use existing key
else:
key_path = generate_ssh_key()
if not key_path:
return False
else:
print_info("No existing SSH keys found")
key_path = generate_ssh_key()
if not key_path:
return False

# Add to agent
add_ssh_key_to_agent(key_path)

# Add to GitHub
if check_gh_auth():
if prompt_yes_no("Add this SSH key to your GitHub account?", default=True):
add_ssh_key_to_github(key_path)

# Test connection
test_ssh_connection()

# Configure git protocol
if prompt_yes_no("Configure Git to use SSH by default?", default=True):
run_command(["gh", "config", "set", "git_protocol", "ssh"], check=False)
print_success("Git configured to use SSH")

return True

def setup_github() -> str: """Complete GitHub setup. Returns auth method used ('gh').""" print_section("GitHub Setup")

# Check for gh CLI
if not check_gh_cli():
print_warning("GitHub CLI (gh) not installed")
print()
print(f" {Colors.CYAN}The GitHub CLI provides secure, browser-based authentication.{Colors.RESET}")
print(f" {Colors.CYAN}No passwords or tokens to manage.{Colors.RESET}")
print()

if check_homebrew():
if prompt_yes_no("Install GitHub CLI now?", default=True):
if not install_gh_cli():
print_error("Could not install GitHub CLI")
print()
print(f" {Colors.YELLOW}Manual installation options:{Colors.RESET}")
print(" 1. brew install gh")
print(" 2. Download from https://cli.github.com")
print()
print(" After installing, run this script again.")
return None
else:
print_info("GitHub CLI is required for installation.")
print_info("Please install it manually and run this script again.")
return None
else:
print_info("Homebrew not found.")
if prompt_yes_no("Install Homebrew? (required for GitHub CLI)", default=True):
if install_homebrew():
if prompt_yes_no("Now install GitHub CLI?", default=True):
if not install_gh_cli():
print_error("Could not install GitHub CLI")
return None
else:
print_info("Please install GitHub CLI and run this script again.")
return None
else:
print_error("Could not install Homebrew")
print()
print(f" {Colors.YELLOW}Manual installation:{Colors.RESET}")
print(" Visit https://brew.sh for Homebrew")
print(" Then: brew install gh")
return None
else:
print()
print(f" {Colors.YELLOW}Install GitHub CLI manually:{Colors.RESET}")
print(" 1. Install Homebrew: https://brew.sh")
print(" 2. Run: brew install gh")
print(" 3. Run this script again")
return None

# Check gh authentication
if check_gh_auth():
username = get_gh_username()
print_success(f"Already authenticated as {username}")
return "gh"
else:
print_warning("GitHub CLI not authenticated")
print()
print(f" {Colors.CYAN}Authentication opens a browser window where you log in to GitHub.{Colors.RESET}")
print(f" {Colors.CYAN}This is secure OAuth - no passwords are stored locally.{Colors.RESET}")
print()

if prompt_yes_no("Authenticate with GitHub now?", default=True):
if setup_gh_auth():
return "gh"
else:
print_error("Authentication failed")
print_info("Try again: gh auth login")
return None
else:
print_info("GitHub authentication is required.")
print_info("Run: gh auth login")
return None

def clone_repository() -> bool: """Clone the CODITECT repository using gh CLI.""" print_section("Cloning CODITECT Repository")

# Create PROJECTS directory
PROJECTS_DIR.mkdir(parents=True, exist_ok=True)
print_success(f"Projects directory: {PROJECTS_DIR}")

repo_path = PROJECTS_DIR / REPO_NAME

# Check if already cloned
if repo_path.exists():
if (repo_path / ".git").exists():
print_warning(f"Repository already exists at {repo_path}")
print_info("Pulling latest changes...")
result = run_command(["git", "-C", str(repo_path), "pull", "--rebase"], check=False)
if result and result.returncode == 0:
print_success("Updated to latest version")
return True
else:
print_warning("Could not update - continuing with existing version")
return True
else:
print_error(f"Directory exists but is not a git repository: {repo_path}")
return False

# Clone using gh CLI
print_info("Cloning repository (this may take a minute)...")

result = run_command(
["gh", "repo", "clone", "coditect-ai/coditect-core", str(repo_path)],
check=False,
capture=False
)

if result and result.returncode == 0:
print_success(f"Cloned to {repo_path}")
return True
else:
print_error("Failed to clone repository")
print()
print(f" {Colors.YELLOW}Troubleshooting:{Colors.RESET}")
print(" - Verify you have access to coditect-ai/coditect-core")
print(" - Check authentication: gh auth status")
print(" - Re-authenticate: gh auth login")
return False

def run_setup_script() -> bool: """Run the full CODITECT setup script.""" print_section("Running CODITECT Setup")

setup_script = PROJECTS_DIR / REPO_NAME / "scripts" / "CODITECT-CORE-INITIAL-SETUP.py"

if not setup_script.exists():
print_error(f"Setup script not found: {setup_script}")
return False

print_info("Starting CODITECT-CORE-INITIAL-SETUP.py...")
print()

# Run the setup script interactively
result = subprocess.run(
[sys.executable, str(setup_script)],
cwd=str(PROJECTS_DIR / REPO_NAME)
)

return result.returncode == 0

def main(): """Main installation flow.""" print_banner()

total_steps = 5

# Step 1: Check prerequisites
print_step(1, total_steps, "Checking prerequisites")

python_ok = check_python_version()
git_ok = check_git_version()

if not python_ok or not git_ok:
print(f"\n{Colors.RED}Prerequisites not met. Please install missing requirements.{Colors.RESET}")
if not python_ok:
print(f"\n Install Python 3.10+: {Colors.CYAN}brew install python@3.12{Colors.RESET}")
if not git_ok:
print(f"\n Install Git: {Colors.CYAN}xcode-select --install{Colors.RESET}")
sys.exit(1)

# Step 2: Set up GitHub CLI and authentication
print_step(2, total_steps, "Setting up GitHub authentication")

if not setup_github():
print(f"\n{Colors.RED}GitHub authentication required to access private repository.{Colors.RESET}")
sys.exit(1)

# Step 3: Set up SSH keys (optional)
print_step(3, total_steps, "SSH key configuration")
setup_ssh_keys()

# Step 4: Clone repository
print_step(4, total_steps, "Cloning CODITECT repository")

if not clone_repository():
sys.exit(1)

# Step 5: Run setup script
print_step(5, total_steps, "Running CODITECT setup")

if not run_setup_script():
print(f"\n{Colors.RED}Setup script encountered an error.{Colors.RESET}")
sys.exit(1)

# Clean up the bootstrap clone (the real install is in the protected location)
bootstrap_clone = PROJECTS_DIR / REPO_NAME
if bootstrap_clone.exists() and bootstrap_clone.is_dir():
print()
print_info("Cleaning up bootstrap files...")
try:
import shutil
shutil.rmtree(bootstrap_clone)
print_success(f"Removed bootstrap clone: {bootstrap_clone}")
print_info(" └─ The real installation is in ~/.coditect (protected location)")
except Exception as e:
print_warning(f"Could not remove bootstrap clone: {e}")
print_info(f" You can manually remove: rm -rf {bootstrap_clone}")

print(f"""

{Colors.GREEN}================================================================================ INSTALLATION COMPLETE! ================================================================================{Colors.RESET}

{Colors.CYAN}Next steps:{Colors.RESET}

  1. Open a new terminal (or run: source ~/.zshrc)
  2. Activate the environment: {Colors.YELLOW}source ~/.coditect/.venv/bin/activate{Colors.RESET}
  3. Start Claude Code and run: {Colors.YELLOW}/orient{Colors.RESET}

{Colors.CYAN}Documentation:{Colors.RESET} https://storage.googleapis.com/coditect-dist/docs/USER-QUICK-START.md

{Colors.CYAN}GitHub CLI Reference:{Colors.RESET} https://storage.googleapis.com/coditect-dist/docs/GITHUB-CLI-USER-MANAGEMENT.md

{Colors.CYAN}Support:{Colors.RESET} support@coditect.ai """)

if name == "main": main()