Skip to main content

scripts-coditect-git-helper

#!/usr/bin/env python3 """

title: "Custom Exceptions" component_type: script version: "1.0.0" audience: contributor status: stable summary: "AZ1.AI CODITECT Git Helper" keywords: ['coditect', 'git', 'helper', 'validation'] tokens: ~500 created: 2025-12-22 updated: 2025-12-22 script_name: "coditect-git-helper.py" language: python executable: true usage: "python3 scripts/coditect-git-helper.py [options]" python_version: "3.10+" dependencies: [] modifies_files: false network_access: false requires_auth: false

AZ1.AI CODITECT Git Helper

Copyright © 2025 AZ1.AI INC. All Rights Reserved. Developed by Hal Casteel, Founder/CEO/CTO, AZ1.AI INC.

Purpose: Automated Git workflow management for non-Git-expert users

  • Automatic session management with branches
  • Regular auto-commits during work sessions
  • Branch management (create, switch, merge)
  • Pull request creation
  • Sync with GitHub (push/pull)
  • Safe, beginner-friendly Git operations """

import os import sys import subprocess import json import logging from pathlib import Path from datetime import datetime from typing import Optional, List, Dict, Any import time

Custom Exceptions

class GitHelperError(Exception): """Base exception for Git Helper errors.""" pass

class GitOperationError(GitHelperError): """Raised when git operation fails.""" pass

class GitHubOperationError(GitHelperError): """Raised when GitHub operation fails.""" pass

class SessionError(GitHelperError): """Raised when session management fails.""" pass

class ValidationError(GitHelperError): """Raised when input validation fails.""" pass

class NetworkError(GitHelperError): """Raised when network operations fail.""" pass

class ConfigurationError(GitHelperError): """Raised when configuration is invalid.""" pass

Colors

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 setup_logging(project_dir: Path) -> logging.Logger: """ Setup dual logging to both file and stdout.

Args:
project_dir: Project directory path

Returns:
Configured logger instance
"""
# Create logs directory
logs_dir = project_dir / 'logs'
logs_dir.mkdir(exist_ok=True)

# Create log file with timestamp
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
log_file = logs_dir / f'coditect-git-helper_{timestamp}.log'

# Configure logger
logger = logging.getLogger('GitHelper')
logger.setLevel(logging.DEBUG)

# File handler - detailed logging
file_handler = logging.FileHandler(log_file)
file_handler.setLevel(logging.DEBUG)
file_formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
file_handler.setFormatter(file_formatter)

# Console handler - user-friendly logging
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
console_formatter = logging.Formatter('%(message)s')
console_handler.setFormatter(console_formatter)

# Add handlers
logger.addHandler(file_handler)
logger.addHandler(console_handler)

logger.info(f"Logging initialized: {log_file}")

return logger

def retry_operation(max_retries: int = 3, delay: float = 1.0, backoff: float = 2.0): """ Decorator for retrying operations with exponential backoff.

Args:
max_retries: Maximum number of retry attempts
delay: Initial delay in seconds
backoff: Backoff multiplier for each retry

Returns:
Decorated function with retry logic
"""
def decorator(func):
def wrapper(*args, **kwargs):
current_delay = delay
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except (NetworkError, subprocess.CalledProcessError) as e:
if attempt == max_retries - 1:
raise
logger = logging.getLogger('GitHelper')
logger.warning(
f"Attempt {attempt + 1}/{max_retries} failed: {e}. "
f"Retrying in {current_delay:.1f}s..."
)
time.sleep(current_delay)
current_delay *= backoff
return func(*args, **kwargs)
return wrapper
return decorator

class GitHelper: """Automated Git workflow manager"""

def __init__(self, project_dir: Path = None):
self.project_dir = project_dir or Path.cwd()
self.config_file = self.project_dir / ".coditect-git-config.json"
self.session_file = self.project_dir / ".coditect-session.json"
self.session_backup_file = self.project_dir / ".coditect-session-backup.json"

# Initialize logger
self.logger = setup_logging(self.project_dir)

# Load configuration and session with error handling
try:
self.config = self.load_config()
self.session = self.load_session()
self.logger.debug("GitHelper initialized successfully")
except Exception as e:
self.logger.error(f"Failed to initialize GitHelper: {e}")
raise ConfigurationError(f"Initialization failed: {e}")

def load_config(self) -> Dict[str, Any]:
"""Load Git helper configuration"""
if self.config_file.exists():
with open(self.config_file, 'r') as f:
return json.load(f)

# Default configuration
return {
'auto_commit_interval': 15, # minutes
'auto_push_interval': 30, # minutes
'branch_naming_pattern': '{date}-{description}',
'commit_message_template': '{action}: {description}\n\nSession: {session_id}\nTimestamp: {timestamp}',
'main_branch': 'main',
'enable_auto_sync': True,
'enable_branch_per_session': True,
'enable_auto_cleanup': True
}

def save_config(self):
"""Save configuration with error handling."""
try:
with open(self.config_file, 'w') as f:
json.dump(self.config, f, indent=2)
self.logger.debug("Configuration saved successfully")
except Exception as e:
self.logger.error(f"Failed to save configuration: {e}")
raise ConfigurationError(f"Failed to save configuration: {e}")

def load_session(self) -> Dict[str, Any]:
"""Load current session with backup recovery."""
try:
if self.session_file.exists():
with open(self.session_file, 'r') as f:
session = json.load(f)
self.logger.debug("Session loaded successfully")
return session
except Exception as e:
self.logger.warning(f"Failed to load session file: {e}")
# Try to recover from backup
if self.session_backup_file.exists():
try:
self.logger.info("Attempting to recover from session backup...")
with open(self.session_backup_file, 'r') as f:
session = json.load(f)
self.logger.info("Session recovered from backup")
return session
except Exception as backup_error:
self.logger.error(f"Failed to recover from backup: {backup_error}")

return {}

def save_session(self):
"""Save session state with backup."""
try:
# Create backup of existing session if it exists
if self.session_file.exists():
import shutil
shutil.copy(self.session_file, self.session_backup_file)
self.logger.debug("Session backup created")

# Save new session state
with open(self.session_file, 'w') as f:
json.dump(self.session, f, indent=2)
self.logger.debug("Session saved successfully")
except Exception as e:
self.logger.error(f"Failed to save session: {e}")
raise SessionError(f"Failed to save session: {e}")

def run_git(self, args: List[str], check=True, capture_output=True) -> subprocess.CompletedProcess:
"""
Run git command with error handling and logging.

Args:
args: Git command arguments
check: Raise exception on non-zero exit
capture_output: Capture stdout/stderr

Returns:
CompletedProcess result

Raises:
GitOperationError: If git command fails
"""
try:
self.logger.debug(f"Running git command: {' '.join(args)}")
result = subprocess.run(
['git'] + args,
cwd=self.project_dir,
check=check,
capture_output=capture_output,
text=True
)
self.logger.debug(f"Git command completed: {result.returncode}")
return result
except subprocess.CalledProcessError as e:
if not check:
return e
error_msg = f"Git command failed: {' '.join(args)}"
self.logger.error(error_msg)
self.logger.error(f"Exit code: {e.returncode}")
self.logger.error(f"Error output: {e.stderr}")
print(f"{Colors.RED}✗ {error_msg}{Colors.END}")
print(f"{Colors.RED} Error: {e.stderr}{Colors.END}")
raise GitOperationError(f"{error_msg}: {e.stderr}")
except Exception as e:
error_msg = f"Unexpected error running git command: {e}"
self.logger.error(error_msg)
raise GitOperationError(error_msg)

def is_git_repo(self) -> bool:
"""Check if directory is a Git repository"""
return (self.project_dir / ".git").exists()

def get_current_branch(self) -> str:
"""Get current Git branch"""
result = self.run_git(['branch', '--show-current'])
return result.stdout.strip()

def get_remote_url(self) -> Optional[str]:
"""Get remote repository URL"""
try:
result = self.run_git(['remote', 'get-url', 'origin'], check=False)
if result.returncode == 0:
return result.stdout.strip()
except:
pass
return None

def has_uncommitted_changes(self) -> bool:
"""Check for uncommitted changes"""
result = self.run_git(['status', '--porcelain'])
return bool(result.stdout.strip())

def has_unpushed_commits(self) -> bool:
"""Check for unpushed commits"""
try:
# Check if remote tracking branch exists
self.run_git(['rev-parse', '--abbrev-ref', '@{u}'])

# Compare local with remote
result = self.run_git(['rev-list', '@{u}..HEAD', '--count'])
return int(result.stdout.strip()) > 0
except:
# No remote tracking branch
return True

def initialize_repo(self):
"""Initialize Git repository if needed"""
print(f"{Colors.BLUE}▶ Initializing Git repository...{Colors.END}")

if not self.is_git_repo():
self.run_git(['init'])
self.run_git(['branch', '-m', self.config['main_branch']])
print(f"{Colors.GREEN}✓ Git repository initialized{Colors.END}")

# Create initial .gitignore
self.create_gitignore()
else:
print(f"{Colors.GREEN}✓ Git repository already initialized{Colors.END}")

def create_gitignore(self):
"""Create project .gitignore from CODITECT template"""
gitignore_path = self.project_dir / ".gitignore"

# Try to use CODITECT universal template if available
template_path = self.project_dir / ".coditect" / "templates" / "gitignore-universal-template"

if template_path.exists():
# Copy universal template
import shutil
shutil.copy(template_path, gitignore_path)
print(f"{Colors.GREEN}✓ Created .gitignore from CODITECT universal template{Colors.END}")
elif not gitignore_path.exists():
# Fallback to basic gitignore
gitignore_content = """# CODITECT Session Files

.coditect-session.json .coditect-git-config.json

Python

pycache/ *.py[cod] *$py.class *.so .Python venv/ env/

Node

node_modules/ npm-debug.log*

IDEs

.vscode/ .idea/ *.swp *.swo

OS

.DS_Store Thumbs.db

Build outputs

dist/ build/ *.egg-info/

Logs

*.log logs/

Environment

.env .env.local """ with open(gitignore_path, 'w') as f: f.write(gitignore_content) print(f"{Colors.GREEN}✓ Created basic .gitignore{Colors.END}")

def setup_remote(self, remote_url: str):
"""Setup GitHub remote"""
print(f"{Colors.BLUE}▶ Setting up GitHub remote...{Colors.END}")

current_remote = self.get_remote_url()

if current_remote:
if current_remote == remote_url:
print(f"{Colors.GREEN}✓ Remote already configured{Colors.END}")
else:
print(f"{Colors.YELLOW}⚠ Updating remote URL{Colors.END}")
self.run_git(['remote', 'set-url', 'origin', remote_url])
else:
self.run_git(['remote', 'add', 'origin', remote_url])
print(f"{Colors.GREEN}✓ Remote 'origin' added{Colors.END}")

def start_session(self, description: str = "work session"):
"""Start new work session"""
print(f"\n{Colors.BLUE}{Colors.BOLD}▶ Starting work session...{Colors.END}\n")

# Generate session ID
session_id = datetime.now().strftime("%Y%m%d-%H%M%S")

# Create session branch if enabled
branch_name = self.config['main_branch']
if self.config.get('enable_branch_per_session', True):
date_str = datetime.now().strftime("%Y-%m-%d")
branch_name = f"session/{date_str}-{description.replace(' ', '-')}"

# Create and checkout branch
current_branch = self.get_current_branch()
if current_branch != branch_name:
try:
self.run_git(['checkout', '-b', branch_name])
print(f"{Colors.GREEN}✓ Created session branch: {branch_name}{Colors.END}")
except:
# Branch might already exist
self.run_git(['checkout', branch_name])
print(f"{Colors.GREEN}✓ Switched to branch: {branch_name}{Colors.END}")

# Store session info
self.session = {
'session_id': session_id,
'description': description,
'branch': branch_name,
'started_at': datetime.now().isoformat(),
'last_commit': None,
'last_push': None,
'commit_count': 0,
'push_count': 0
}
self.save_session()

print(f"{Colors.GREEN}✓ Session started: {session_id}{Colors.END}")
print(f"{Colors.CYAN} Branch: {branch_name}{Colors.END}")
print(f"{Colors.CYAN} Description: {description}{Colors.END}\n")

return session_id

def auto_commit(self, message: str = None):
"""Perform automatic commit"""
if not self.has_uncommitted_changes():
print(f"{Colors.YELLOW}ℹ No changes to commit{Colors.END}")
return False

# Generate commit message
if not message:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
session_id = self.session.get('session_id', 'unknown')

message = f"""Auto-save: Work in progress

Session: {session_id} Timestamp: {timestamp}

🤖 Generated with AZ1.AI CODITECT Git Helper

Co-Authored-By: Claude noreply@anthropic.com"""

    # Stage all changes
self.run_git(['add', '-A'])

# Commit
self.run_git(['commit', '-m', message])

# Update session
self.session['last_commit'] = datetime.now().isoformat()
self.session['commit_count'] = self.session.get('commit_count', 0) + 1
self.save_session()

print(f"{Colors.GREEN}✓ Auto-committed changes (commit #{self.session['commit_count']}){Colors.END}")
return True

@retry_operation(max_retries=3, delay=2.0)
def auto_push(self):
"""Perform automatic push to remote with retry logic."""
if not self.has_unpushed_commits():
print(f"{Colors.YELLOW}ℹ No commits to push{Colors.END}")
self.logger.info("No commits to push")
return False

branch = self.get_current_branch()

try:
self.logger.info(f"Pushing to origin/{branch}...")
# Push with --set-upstream for new branches
self.run_git(['push', '-u', 'origin', branch])

# Update session
self.session['last_push'] = datetime.now().isoformat()
self.session['push_count'] = self.session.get('push_count', 0) + 1
self.save_session()

print(f"{Colors.GREEN}✓ Pushed to origin/{branch} (push #{self.session['push_count']}){Colors.END}")
self.logger.info(f"Push successful (push #{self.session['push_count']})")
return True

except GitOperationError as e:
error_msg = f"Push failed: {e}"
self.logger.error(error_msg)
print(f"{Colors.RED}✗ {error_msg}{Colors.END}")
print(f"{Colors.YELLOW}⚠ You may need to authenticate with GitHub{Colors.END}")
print(f"{Colors.YELLOW} Run: gh auth login{Colors.END}")
raise NetworkError(error_msg)

@retry_operation(max_retries=3, delay=2.0)
def sync_with_remote(self):
"""Pull latest changes from remote with retry logic."""
print(f"{Colors.BLUE}▶ Syncing with remote...{Colors.END}")
self.logger.info("Starting sync with remote...")

branch = self.get_current_branch()

try:
# Fetch latest
self.logger.debug("Fetching from origin...")
self.run_git(['fetch', 'origin'])

# Pull with rebase to keep clean history
self.logger.debug(f"Pulling from origin/{branch} with rebase...")
self.run_git(['pull', '--rebase', 'origin', branch])

print(f"{Colors.GREEN}✓ Synced with origin/{branch}{Colors.END}")
self.logger.info("Sync completed successfully")
return True

except GitOperationError as e:
self.logger.warning(f"Could not sync with remote: {e}")
print(f"{Colors.YELLOW}⚠ Could not sync with remote (may not exist yet){Colors.END}")
return False

def create_pr(self, title: str, description: str = None):
"""Create pull request using GitHub CLI"""
print(f"\n{Colors.BLUE}{Colors.BOLD}▶ Creating pull request...{Colors.END}\n")

# Check if gh CLI is installed
try:
subprocess.run(['gh', '--version'], check=True, capture_output=True)
except FileNotFoundError:
print(f"{Colors.RED}✗ GitHub CLI (gh) not installed{Colors.END}")
print(f"{Colors.YELLOW} Install: brew install gh{Colors.END}")
return False

# Ensure changes are pushed
if self.has_uncommitted_changes():
print(f"{Colors.YELLOW}⚠ Committing changes before creating PR...{Colors.END}")
self.auto_commit(f"{title}\n\n{description or ''}")

if self.has_unpushed_commits():
print(f"{Colors.YELLOW}⚠ Pushing changes before creating PR...{Colors.END}")
self.auto_push()

# Create PR
branch = self.get_current_branch()
base_branch = self.config['main_branch']

pr_body = description or f"Pull request for {branch}"

try:
result = subprocess.run(
['gh', 'pr', 'create',
'--title', title,
'--body', pr_body,
'--base', base_branch],
cwd=self.project_dir,
check=True,
capture_output=True,
text=True
)

pr_url = result.stdout.strip()
print(f"{Colors.GREEN}✓ Pull request created!{Colors.END}")
print(f"{Colors.CYAN} URL: {pr_url}{Colors.END}")

return True

except subprocess.CalledProcessError as e:
print(f"{Colors.RED}✗ PR creation failed: {e.stderr}{Colors.END}")
return False

def end_session(self, merge_to_main: bool = False, create_pr_flag: bool = False):
"""End work session"""
print(f"\n{Colors.BLUE}{Colors.BOLD}▶ Ending work session...{Colors.END}\n")

# Final commit
if self.has_uncommitted_changes():
print(f"{Colors.YELLOW}⚠ Committing final changes...{Colors.END}")
self.auto_commit("End of work session - final commit")

# Final push
if self.has_unpushed_commits():
print(f"{Colors.YELLOW}⚠ Pushing final changes...{Colors.END}")
self.auto_push()

# Create PR if requested
if create_pr_flag:
session_desc = self.session.get('description', 'work session')
self.create_pr(
title=f"Session: {session_desc}",
description=f"Work from session {self.session.get('session_id')}"
)

# Merge to main if requested
if merge_to_main:
self.merge_session_to_main()

# Session summary
duration = None
if 'started_at' in self.session:
start = datetime.fromisoformat(self.session['started_at'])
duration = datetime.now() - start

print(f"\n{Colors.GREEN}{Colors.BOLD}Session Summary:{Colors.END}")
print(f" Session ID: {self.session.get('session_id')}")
print(f" Duration: {duration}")
print(f" Commits: {self.session.get('commit_count', 0)}")
print(f" Pushes: {self.session.get('push_count', 0)}")
print(f" Branch: {self.session.get('branch')}\n")

# Clear session
if self.session_file.exists():
self.session_file.unlink()
self.session = {}

def merge_session_to_main(self):
"""Merge current branch to main with rollback on failure."""
print(f"{Colors.BLUE}▶ Merging to {self.config['main_branch']}...{Colors.END}")
self.logger.info("Starting merge to main branch...")

current_branch = self.get_current_branch()
main_branch = self.config['main_branch']

if current_branch == main_branch:
print(f"{Colors.YELLOW}ℹ Already on {main_branch}{Colors.END}")
self.logger.info("Already on main branch")
return True

# Store original branch for rollback
original_branch = current_branch

try:
# Switch to main
self.logger.debug(f"Switching to {main_branch}...")
self.run_git(['checkout', main_branch])

# Pull latest
self.logger.debug("Pulling latest changes...")
self.run_git(['pull', 'origin', main_branch], check=False)

# Get main branch HEAD for rollback
main_head_before = self.run_git(['rev-parse', 'HEAD']).stdout.strip()
self.logger.debug(f"Main branch HEAD before merge: {main_head_before}")

# Merge session branch
self.logger.info(f"Merging {current_branch} into {main_branch}...")
self.run_git(['merge', '--no-ff', current_branch, '-m', f'Merge session: {current_branch}'])

# Push merged changes
self.logger.debug("Pushing merged changes...")
self.run_git(['push', 'origin', main_branch])

print(f"{Colors.GREEN}✓ Merged {current_branch} → {main_branch}{Colors.END}")
self.logger.info("Merge completed successfully")

# Cleanup session branch if configured
if self.config.get('enable_auto_cleanup', True):
self.run_git(['branch', '-d', current_branch])
print(f"{Colors.GREEN}✓ Cleaned up local branch: {current_branch}{Colors.END}")
self.logger.info(f"Cleaned up branch: {current_branch}")

return True

except GitOperationError as e:
error_msg = f"Merge failed: {e}"
self.logger.error(error_msg)
print(f"{Colors.RED}✗ {error_msg}{Colors.END}")
print(f"{Colors.YELLOW}⚠ Attempting rollback...{Colors.END}")

# Rollback merge
try:
self.logger.info("Rolling back merge...")
# Abort merge if in progress
self.run_git(['merge', '--abort'], check=False)

# Reset to HEAD before merge if we're still on main
current = self.get_current_branch()
if current == main_branch and 'main_head_before' in locals():
self.run_git(['reset', '--hard', main_head_before])
self.logger.info("Rolled back to state before merge")

# Switch back to original branch
self.run_git(['checkout', original_branch])
print(f"{Colors.YELLOW}✓ Rolled back to {original_branch}{Colors.END}")
self.logger.info(f"Switched back to {original_branch}")

print(f"{Colors.YELLOW}⚠ You may need to resolve conflicts manually{Colors.END}")
print(f"{Colors.YELLOW} Merge conflicts can be resolved with:{Colors.END}")
print(f"{Colors.YELLOW} git checkout {main_branch}{Colors.END}")
print(f"{Colors.YELLOW} git merge {original_branch}{Colors.END}")
print(f"{Colors.YELLOW} # Resolve conflicts{Colors.END}")
print(f"{Colors.YELLOW} git add .{Colors.END}")
print(f"{Colors.YELLOW} git commit{Colors.END}")

except Exception as rollback_error:
self.logger.error(f"Rollback failed: {rollback_error}")
print(f"{Colors.RED}✗ Rollback failed: {rollback_error}{Colors.END}")
print(f"{Colors.YELLOW}⚠ Manual intervention required{Colors.END}")

return False

def status(self):
"""Show Git status with session info"""
print(f"\n{Colors.BLUE}{Colors.BOLD}Git & Session Status{Colors.END}\n")

# Git repository info
if self.is_git_repo():
print(f"{Colors.GREEN}✓ Git repository initialized{Colors.END}")
print(f" Branch: {Colors.CYAN}{self.get_current_branch()}{Colors.END}")

remote = self.get_remote_url()
if remote:
print(f" Remote: {Colors.CYAN}{remote}{Colors.END}")
else:
print(f"{Colors.RED}✗ Not a Git repository{Colors.END}")
return

# Changes status
if self.has_uncommitted_changes():
print(f"\n{Colors.YELLOW}⚠ Uncommitted changes detected{Colors.END}")
result = self.run_git(['status', '--short'])
print(result.stdout)
else:
print(f"\n{Colors.GREEN}✓ Working tree clean{Colors.END}")

# Unpushed commits
if self.has_unpushed_commits():
result = self.run_git(['log', '@{u}..HEAD', '--oneline'], check=False)
if result.returncode == 0:
commit_count = len(result.stdout.strip().split('\n'))
print(f"\n{Colors.YELLOW}⚠ {commit_count} unpushed commit(s){Colors.END}")
else:
print(f"{Colors.GREEN}✓ All commits pushed{Colors.END}")

# Session info
if self.session:
print(f"\n{Colors.BOLD}Active Session:{Colors.END}")
print(f" Session ID: {self.session.get('session_id')}")
print(f" Description: {self.session.get('description')}")
print(f" Branch: {self.session.get('branch')}")
print(f" Commits: {self.session.get('commit_count', 0)}")
print(f" Pushes: {self.session.get('push_count', 0)}")
else:
print(f"\n{Colors.YELLOW}ℹ No active session{Colors.END}")

def interactive_menu(self):
"""Interactive menu for Git operations"""
while True:
print(f"\n{Colors.CYAN}{Colors.BOLD}═══ CODITECT Git Helper ═══{Colors.END}\n")
print("1. Start work session")
print("2. Commit changes")
print("3. Push to GitHub")
print("4. Sync with remote")
print("5. Create pull request")
print("6. End session")
print("7. Show status")
print("8. Exit")

choice = input(f"\n{Colors.YELLOW}Select option [1-8]: {Colors.END}").strip()

if choice == '1':
desc = input("Session description: ").strip() or "work session"
self.start_session(desc)

elif choice == '2':
msg = input("Commit message (optional): ").strip() or None
self.auto_commit(msg)

elif choice == '3':
self.auto_push()

elif choice == '4':
self.sync_with_remote()

elif choice == '5':
title = input("PR title: ").strip()
desc = input("PR description (optional): ").strip() or None
self.create_pr(title, desc)

elif choice == '6':
create_pr = input("Create pull request? (y/n): ").lower() == 'y'
merge = input("Merge to main? (y/n): ").lower() == 'y'
self.end_session(merge_to_main=merge, create_pr_flag=create_pr)

elif choice == '7':
self.status()

elif choice == '8':
print(f"\n{Colors.GREEN}Goodbye!{Colors.END}\n")
break

else:
print(f"{Colors.RED}Invalid option{Colors.END}")

def main(): """ Entry point with error handling and exit codes.

Exit codes:
0: Success
1: General error
2: Validation error
3: Git operation error
4: GitHub operation error
5: Network error
130: User cancelled (Ctrl+C)
"""
import argparse

parser = argparse.ArgumentParser(
description='CODITECT Git Helper - Automated Git workflow management',
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument('command', nargs='?', choices=[
'init', 'start', 'commit', 'push', 'sync', 'pr', 'end', 'status', 'menu'
], help='Command to execute')
parser.add_argument('--message', '-m', help='Commit message')
parser.add_argument('--description', '-d', help='Session/PR description')
parser.add_argument('--title', '-t', help='PR title')
parser.add_argument('--merge', action='store_true', help='Merge to main on end')
parser.add_argument('--remote', '-r', help='GitHub remote URL')
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')

args = parser.parse_args()

try:
# Initialize helper
helper = GitHelper()

# Set logging level
if args.verbose:
helper.logger.setLevel(logging.DEBUG)

# Execute command
if not args.command or args.command == 'menu':
helper.interactive_menu()

elif args.command == 'init':
helper.initialize_repo()
if args.remote:
helper.setup_remote(args.remote)

elif args.command == 'start':
desc = args.description or 'work session'
if not desc.strip():
raise ValidationError("Session description cannot be empty")
helper.start_session(desc)

elif args.command == 'commit':
helper.auto_commit(args.message)

elif args.command == 'push':
helper.auto_push()

elif args.command == 'sync':
helper.sync_with_remote()

elif args.command == 'pr':
title = args.title or input("PR title: ")
if not title.strip():
raise ValidationError("PR title cannot be empty")
helper.create_pr(title, args.description)

elif args.command == 'end':
helper.end_session(merge_to_main=args.merge)

elif args.command == 'status':
helper.status()

helper.logger.info("Command completed successfully")
return 0

except KeyboardInterrupt:
print(f"\n{Colors.YELLOW}⚠ Operation cancelled by user{Colors.END}")
return 130

except ValidationError as e:
print(f"{Colors.RED}✗ Validation error: {e}{Colors.END}")
logger = logging.getLogger('GitHelper')
logger.error(f"Validation error: {e}")
return 2

except GitOperationError as e:
print(f"{Colors.RED}✗ Git operation failed: {e}{Colors.END}")
logger = logging.getLogger('GitHelper')
logger.error(f"Git operation error: {e}")
return 3

except GitHubOperationError as e:
print(f"{Colors.RED}✗ GitHub operation failed: {e}{Colors.END}")
print(f"{Colors.YELLOW} Check GitHub CLI authentication: gh auth login{Colors.END}")
logger = logging.getLogger('GitHelper')
logger.error(f"GitHub operation error: {e}")
return 4

except NetworkError as e:
print(f"{Colors.RED}✗ Network error: {e}{Colors.END}")
print(f"{Colors.YELLOW} Check your internet connection{Colors.END}")
logger = logging.getLogger('GitHelper')
logger.error(f"Network error: {e}")
return 5

except (SessionError, ConfigurationError) as e:
print(f"{Colors.RED}✗ Error: {e}{Colors.END}")
logger = logging.getLogger('GitHelper')
logger.error(f"Error: {e}")
return 1

except Exception as e:
print(f"{Colors.RED}✗ Unexpected error: {e}{Colors.END}")
logger = logging.getLogger('GitHelper')
logger.exception("Unexpected error occurred")
return 1

if name == 'main': sys.exit(main())