262 lines
8.0 KiB
Python
262 lines
8.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
First-run warning system for autonomous-dev plugin.
|
|
|
|
Interactive warning system for opt-out consent on first /auto-implement run.
|
|
|
|
Features:
|
|
- Displays first-run warning about automatic git operations
|
|
- Prompts user for consent (Y/n, defaults to yes)
|
|
- Records user choice in state file
|
|
- Skips warning in non-interactive sessions
|
|
- Graceful error handling
|
|
|
|
Date: 2025-11-11
|
|
Issue: #61 (Enable Zero Manual Git Operations by Default)
|
|
Agent: implementer
|
|
|
|
See error-handling-patterns skill for exception hierarchy and error handling best practices.
|
|
|
|
|
|
Design Patterns:
|
|
See library-design-patterns skill for standardized design patterns.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Import user state manager (standard pattern from project libraries)
|
|
try:
|
|
from .user_state_manager import (
|
|
UserStateManager,
|
|
is_first_run,
|
|
DEFAULT_STATE_FILE
|
|
)
|
|
except ImportError:
|
|
# Direct script execution - add lib dir to path
|
|
lib_dir = Path(__file__).parent.resolve()
|
|
sys.path.insert(0, str(lib_dir))
|
|
from user_state_manager import (
|
|
UserStateManager,
|
|
is_first_run,
|
|
DEFAULT_STATE_FILE
|
|
)
|
|
|
|
|
|
# Exception hierarchy pattern from error-handling-patterns skill:
|
|
# BaseException -> Exception -> AutonomousDevError -> DomainError(BaseException) -> SpecificError
|
|
class FirstRunWarningError(Exception):
|
|
"""Exception raised for first-run warning errors."""
|
|
pass
|
|
|
|
|
|
def render_warning() -> str:
|
|
"""
|
|
Render first-run warning message.
|
|
|
|
Returns:
|
|
Formatted warning message with user prompt
|
|
"""
|
|
warning = """
|
|
╔══════════════════════════════════════════════════════════════╗
|
|
║ ║
|
|
║ 🚀 Zero Manual Git Operations (NEW DEFAULT) ║
|
|
║ ║
|
|
║ Automatic git operations enabled after /auto-implement: ║
|
|
║ ║
|
|
║ ✓ automatic commit with conventional commit message ║
|
|
║ ✓ automatic push to remote ║
|
|
║ ✓ automatic pull request creation ║
|
|
║ ║
|
|
║ HOW TO OPT OUT: ║
|
|
║ ║
|
|
║ Add to .env file: ║
|
|
║ AUTO_GIT_ENABLED=false ║
|
|
║ ║
|
|
║ Or disable specific operations: ║
|
|
║ AUTO_GIT_PUSH=false # Disable push ║
|
|
║ AUTO_GIT_PR=false # Disable PR creation ║
|
|
║ ║
|
|
║ See docs/GIT-AUTOMATION.md for details ║
|
|
║ ║
|
|
╚══════════════════════════════════════════════════════════════╝
|
|
|
|
Do you want to enable automatic git operations? (Y/n): """
|
|
|
|
return warning
|
|
|
|
|
|
def parse_user_input(user_input: str) -> bool:
|
|
"""
|
|
Parse user input for consent.
|
|
|
|
Accepts: 'yes', 'y', 'Y', 'YES', '' (empty = yes)
|
|
Rejects: 'no', 'n', 'N', 'NO'
|
|
|
|
Args:
|
|
user_input: User input string
|
|
|
|
Returns:
|
|
True if accepted, False if rejected
|
|
|
|
Raises:
|
|
FirstRunWarningError: If input is invalid
|
|
"""
|
|
# Strip whitespace
|
|
user_input = user_input.strip()
|
|
|
|
# Empty input defaults to yes
|
|
if not user_input:
|
|
return True
|
|
|
|
# Check for yes
|
|
if user_input.lower() in {'yes', 'y'}:
|
|
return True
|
|
|
|
# Check for no
|
|
if user_input.lower() in {'no', 'n'}:
|
|
return False
|
|
|
|
# Invalid input
|
|
raise FirstRunWarningError(
|
|
f"Invalid input: '{user_input}'. Please enter 'yes' or 'no' (or press Enter for yes)."
|
|
)
|
|
|
|
|
|
def is_interactive_session() -> bool:
|
|
"""
|
|
Detect if running in an interactive session.
|
|
|
|
Returns:
|
|
True if interactive, False otherwise
|
|
"""
|
|
# Check if in CI environment
|
|
if os.environ.get("CI"):
|
|
return False
|
|
|
|
# Check if stdin is a TTY
|
|
try:
|
|
return sys.stdin.isatty()
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def show_first_run_warning(
|
|
state_file: Path = DEFAULT_STATE_FILE,
|
|
max_retries: int = 3
|
|
) -> bool:
|
|
"""
|
|
Show first-run warning and prompt user for consent.
|
|
|
|
Args:
|
|
state_file: Path to state file
|
|
max_retries: Maximum number of retry attempts for invalid input
|
|
|
|
Returns:
|
|
True if user accepts, False if user rejects
|
|
|
|
Raises:
|
|
FirstRunWarningError: If max retries exceeded or interrupted
|
|
"""
|
|
# Skip in non-interactive sessions
|
|
if not is_interactive_session():
|
|
# Default to True (opt-out model)
|
|
record_user_choice(accepted=True, state_file=state_file)
|
|
return True
|
|
|
|
# Display warning (print to sys.stdout explicitly for tests)
|
|
warning = render_warning()
|
|
sys.stdout.write(warning)
|
|
sys.stdout.flush()
|
|
|
|
# Prompt for input with retries
|
|
retry_count = 0
|
|
while retry_count < max_retries:
|
|
try:
|
|
user_input = input()
|
|
accepted = parse_user_input(user_input)
|
|
|
|
# Record choice
|
|
record_user_choice(accepted=accepted, state_file=state_file)
|
|
|
|
return accepted
|
|
|
|
except FirstRunWarningError as e:
|
|
retry_count += 1
|
|
if retry_count >= max_retries:
|
|
raise FirstRunWarningError(
|
|
f"Maximum retries exceeded. Please run /auto-implement again and enter 'yes' or 'no'."
|
|
)
|
|
sys.stdout.write(f"\n{e}\n")
|
|
sys.stdout.write("Do you want to enable automatic git operations? (Y/n): ")
|
|
sys.stdout.flush()
|
|
|
|
except KeyboardInterrupt:
|
|
raise FirstRunWarningError("Interrupted by user")
|
|
|
|
except EOFError:
|
|
# End of input - default to yes
|
|
record_user_choice(accepted=True, state_file=state_file)
|
|
return True
|
|
|
|
# Should not reach here
|
|
raise FirstRunWarningError("Unexpected error in first-run warning")
|
|
|
|
|
|
def record_user_choice(accepted: bool, state_file: Path = DEFAULT_STATE_FILE) -> None:
|
|
"""
|
|
Record user choice in state file.
|
|
|
|
Args:
|
|
accepted: True if user accepted, False if rejected
|
|
state_file: Path to state file
|
|
|
|
Raises:
|
|
FirstRunWarningError: If recording fails
|
|
"""
|
|
try:
|
|
manager = UserStateManager(state_file)
|
|
manager.set_preference("auto_git_enabled", accepted)
|
|
manager.record_first_run_complete()
|
|
manager.save()
|
|
except Exception as e:
|
|
raise FirstRunWarningError(f"Failed to record user choice: {e}")
|
|
|
|
|
|
def should_show_warning(state_file: Path = DEFAULT_STATE_FILE) -> bool:
|
|
"""
|
|
Determine whether to show first-run warning.
|
|
|
|
Skips warning if:
|
|
- Not first run (user already made a choice)
|
|
- AUTO_GIT_ENABLED env var is set (user already configured)
|
|
- Non-interactive session (can't prompt for input)
|
|
|
|
Args:
|
|
state_file: Path to state file
|
|
|
|
Returns:
|
|
True if warning should be shown, False otherwise
|
|
"""
|
|
# Skip if env var is already set
|
|
if os.environ.get("AUTO_GIT_ENABLED") is not None:
|
|
return False
|
|
|
|
# Skip in non-interactive sessions
|
|
if not is_interactive_session():
|
|
return False
|
|
|
|
# Show if first run
|
|
return is_first_run(state_file)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# CLI test
|
|
try:
|
|
result = show_first_run_warning()
|
|
print(f"\nUser choice: {'Accepted' if result else 'Rejected'}")
|
|
except FirstRunWarningError as e:
|
|
print(f"Error: {e}")
|
|
sys.exit(1)
|