mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-17 12:37:45 +02:00
Sync from development - prepare for v0.3.0
This commit is contained in:
451
jackify/backend/handlers/modlist_install_cli_discovery.py
Normal file
451
jackify/backend/handlers/modlist_install_cli_discovery.py
Normal file
@@ -0,0 +1,451 @@
|
||||
"""Discovery phase methods for ModlistInstallCLI (Mixin)."""
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict
|
||||
|
||||
from .config_handler import ConfigHandler
|
||||
from .ui_colors import (
|
||||
COLOR_PROMPT,
|
||||
COLOR_RESET,
|
||||
COLOR_INFO,
|
||||
COLOR_ERROR,
|
||||
COLOR_SUCCESS,
|
||||
COLOR_WARNING,
|
||||
COLOR_SELECTION,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ModlistInstallCLIDiscoveryMixin:
|
||||
"""Mixin providing discovery phase methods."""
|
||||
|
||||
def run_discovery_phase(self, context_override=None) -> Optional[Dict]:
|
||||
"""
|
||||
Run the discovery phase: prompt for all required info, and validate inputs.
|
||||
Returns a context dict with all collected info, or None if cancelled.
|
||||
Accepts context_override for pre-filled values (e.g., for Tuxborn/machineid flow).
|
||||
"""
|
||||
self.logger.info("Starting modlist discovery phase (restored logic).")
|
||||
from .modlist_install_cli import get_jackify_engine_path
|
||||
|
||||
print(f"\n{COLOR_PROMPT}--- Wabbajack Modlist Install: Discovery Phase ---{COLOR_RESET}")
|
||||
|
||||
if context_override:
|
||||
self.context.update(context_override)
|
||||
if 'resolution' in context_override:
|
||||
self.context['resolution'] = context_override['resolution']
|
||||
else:
|
||||
self.context = {}
|
||||
|
||||
is_gui_mode = os.environ.get('JACKIFY_GUI_MODE') == '1'
|
||||
# Only require game_type for non-Tuxborn workflows
|
||||
if self.context.get('machineid'):
|
||||
required_keys = ['modlist_name', 'install_dir', 'download_dir', 'nexus_api_key']
|
||||
else:
|
||||
required_keys = ['modlist_name', 'install_dir', 'download_dir', 'nexus_api_key', 'game_type']
|
||||
has_modlist = self.context.get('modlist_value') or self.context.get('machineid')
|
||||
missing = [k for k in required_keys if not self.context.get(k)]
|
||||
if is_gui_mode:
|
||||
if missing or not has_modlist:
|
||||
self.logger.error(f"Missing required arguments for GUI workflow: {', '.join(missing)}")
|
||||
if not has_modlist:
|
||||
self.logger.error("Missing modlist_value or machineid for GUI workflow.")
|
||||
self.logger.error("This workflow must be fully non-interactive. Please report this as a bug if you see this message.")
|
||||
return None
|
||||
self.logger.info("All required context present in GUI mode, skipping prompts.")
|
||||
return self.context
|
||||
|
||||
# Get engine path using the helper
|
||||
engine_executable = get_jackify_engine_path()
|
||||
self.logger.debug(f"Engine executable path: {engine_executable}")
|
||||
|
||||
if not os.path.exists(engine_executable):
|
||||
print(f"{COLOR_ERROR}Error: jackify-install-engine not found at expected location.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}Expected: {engine_executable}{COLOR_RESET}")
|
||||
return None
|
||||
|
||||
engine_dir = os.path.dirname(engine_executable)
|
||||
|
||||
# 1. Prompt for modlist source (unless using machineid from context_override)
|
||||
if 'machineid' not in self.context:
|
||||
print("\n" + "-" * 28) # Separator
|
||||
print(f"{COLOR_PROMPT}How would you like to select your modlist?{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}1.{COLOR_RESET} Select from a list of available modlists")
|
||||
print(f"{COLOR_SELECTION}2.{COLOR_RESET} Provide the path to a .wabbajack file on disk")
|
||||
print(f"{COLOR_SELECTION}0.{COLOR_RESET} Cancel and return to previous menu")
|
||||
source_choice = input(f"{COLOR_PROMPT}Enter your selection (0-2): {COLOR_RESET}").strip()
|
||||
self.logger.debug(f"User selected modlist source option: {source_choice}")
|
||||
|
||||
if source_choice == '1':
|
||||
self.context['modlist_source_type'] = 'online_list'
|
||||
print(f"\n{COLOR_INFO}Fetching available modlists... This may take a moment.{COLOR_RESET}")
|
||||
try:
|
||||
env = os.environ.copy()
|
||||
env["DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"] = "1"
|
||||
self.logger.info("Setting DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 for jackify-engine process.")
|
||||
|
||||
# Use the engine path from the helper function, but the command structure from restored.
|
||||
engine_executable_path_for_subprocess = get_jackify_engine_path()
|
||||
command = [engine_executable_path_for_subprocess, 'list-modlists', '--show-all-sizes', '--show-machine-url']
|
||||
self.logger.info(f"Executing command: {' '.join(command)} in CWD: {engine_dir}")
|
||||
|
||||
# check=True as in restored logic
|
||||
result = subprocess.run(
|
||||
command,
|
||||
capture_output=True, text=True, check=True,
|
||||
env=env, cwd=engine_dir
|
||||
)
|
||||
|
||||
# self.logger.debug(f"Engine stdout (raw):\n{result.stdout}") # COMMENTED OUT - too verbose
|
||||
|
||||
lines = result.stdout.splitlines()
|
||||
|
||||
# Parse new format: [STATUS] Modlist Name - Game - Download|Install|Total - MachineURL
|
||||
# STATUS indicators: [DOWN], [NSFW], or both [DOWN] [NSFW]
|
||||
raw_modlists_from_engine = []
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('Loading') or line.startswith('Loaded'):
|
||||
continue
|
||||
|
||||
# Extract status indicators
|
||||
status_down = '[DOWN]' in line
|
||||
status_nsfw = '[NSFW]' in line
|
||||
|
||||
# Remove status indicators to get clean line
|
||||
clean_line = line.replace('[DOWN]', '').replace('[NSFW]', '').strip()
|
||||
|
||||
# Split on ' - ' to get: [Modlist Name, Game, Sizes, MachineURL]
|
||||
parts = clean_line.split(' - ')
|
||||
if len(parts) != 4:
|
||||
continue # Skip malformed lines
|
||||
|
||||
modlist_name = parts[0].strip()
|
||||
game_name = parts[1].strip()
|
||||
sizes_str = parts[2].strip()
|
||||
machine_url = parts[3].strip()
|
||||
|
||||
# Parse sizes: "Download|Install|Total" (e.g., "203GB|130GB|333GB")
|
||||
size_parts = sizes_str.split('|')
|
||||
if len(size_parts) != 3:
|
||||
continue # Skip if sizes don't match expected format
|
||||
|
||||
download_size = size_parts[0].strip()
|
||||
install_size = size_parts[1].strip()
|
||||
total_size = size_parts[2].strip()
|
||||
|
||||
# Skip if any required data is missing
|
||||
if not modlist_name or not game_name or not machine_url:
|
||||
continue
|
||||
|
||||
raw_modlists_from_engine.append({
|
||||
'id': modlist_name, # Use modlist name as ID for compatibility
|
||||
'name': modlist_name,
|
||||
'game': game_name,
|
||||
'download_size': download_size,
|
||||
'install_size': install_size,
|
||||
'total_size': total_size,
|
||||
'machine_url': machine_url, # Store machine URL for installation
|
||||
'status_down': status_down,
|
||||
'status_nsfw': status_nsfw
|
||||
})
|
||||
|
||||
self.logger.info(f"Scraped {len(raw_modlists_from_engine)} modlists after revised regex and filtering logic.")
|
||||
|
||||
if not raw_modlists_from_engine:
|
||||
print(f"{COLOR_WARNING}No modlists found after applying revised regex and filtering logic.{COLOR_RESET}")
|
||||
return None
|
||||
|
||||
# EXACT game_type_map and grouping logic from restored file
|
||||
game_type_map = {
|
||||
'1': ('Skyrim', ['Skyrim', 'Skyrim Special Edition']),
|
||||
'2': ('Fallout 4', ['Fallout 4']),
|
||||
'3': ('Fallout New Vegas', ['Fallout New Vegas']),
|
||||
'4': ('Oblivion', ['Oblivion']),
|
||||
'5': ('Other Games', None) # Using None as in restored for keyword list
|
||||
}
|
||||
|
||||
grouped_modlists = {k: [] for k in game_type_map}
|
||||
|
||||
for m_info in raw_modlists_from_engine: # m_info is like {'id': ..., 'game': ...}
|
||||
found_category = False
|
||||
for cat_key, (cat_label, cat_keywords) in game_type_map.items():
|
||||
if cat_key == '5': # Skip 'Other Games' for direct matching initially
|
||||
continue
|
||||
if cat_keywords: # Ensure there are keywords to check (handles 'Other Games' with None)
|
||||
for keyword in cat_keywords:
|
||||
if keyword.lower() in m_info['game'].lower():
|
||||
grouped_modlists[cat_key].append(m_info)
|
||||
found_category = True
|
||||
break # Found category for this modlist
|
||||
if found_category:
|
||||
break # Move to next modlist
|
||||
if not found_category:
|
||||
grouped_modlists['5'].append(m_info) # Add to 'Other Games'
|
||||
|
||||
selected_modlist_info = None # Will store {'id': ..., 'game': ...}
|
||||
while not selected_modlist_info:
|
||||
print(f"\n{COLOR_PROMPT}Select a game category:{COLOR_RESET}")
|
||||
|
||||
category_display_map = {} # Maps displayed number to actual game_type_map key
|
||||
display_idx = 1
|
||||
# Iterate in a defined order for consistent menu
|
||||
for cat_key_ordered in ['1','2','3','4','5']:
|
||||
if cat_key_ordered in grouped_modlists and grouped_modlists[cat_key_ordered]: # Only show if non-empty
|
||||
cat_label = game_type_map[cat_key_ordered][0]
|
||||
print(f" {COLOR_SELECTION}{display_idx}.{COLOR_RESET} {cat_label} ({len(grouped_modlists[cat_key_ordered])} modlists)")
|
||||
category_display_map[str(display_idx)] = cat_key_ordered
|
||||
display_idx += 1
|
||||
|
||||
if display_idx == 1: # No categories had any modlists
|
||||
print(f"{COLOR_WARNING}No modlists found to display after grouping. Engine output might be empty or filtered entirely.{COLOR_RESET}")
|
||||
return None
|
||||
|
||||
print(f" {COLOR_SELECTION}0.{COLOR_RESET} Cancel")
|
||||
|
||||
game_cat_choice = input(f"{COLOR_PROMPT}Enter selection: {COLOR_RESET}").strip()
|
||||
if game_cat_choice == '0':
|
||||
self.logger.info("User cancelled game category selection.")
|
||||
return None
|
||||
|
||||
actual_cat_key = category_display_map.get(game_cat_choice)
|
||||
if not actual_cat_key:
|
||||
print(f"{COLOR_ERROR}Invalid selection. Please try again.{COLOR_RESET}")
|
||||
continue
|
||||
|
||||
# modlist_group_for_game is a list of dicts like {'id': ..., 'game': ...}
|
||||
modlist_group_for_game = sorted(grouped_modlists[actual_cat_key], key=lambda x: x['id'].lower())
|
||||
|
||||
print(f"\n{COLOR_SUCCESS}Available Modlists for {game_type_map[actual_cat_key][0]}:{COLOR_RESET}")
|
||||
for idx, m_detail in enumerate(modlist_group_for_game, 1):
|
||||
if actual_cat_key == '5': # 'Other Games' category
|
||||
print(f" {COLOR_SELECTION}{idx}.{COLOR_RESET} {m_detail['id']} ({m_detail['game']})")
|
||||
else:
|
||||
print(f" {COLOR_SELECTION}{idx}.{COLOR_RESET} {m_detail['id']}")
|
||||
print(f" {COLOR_SELECTION}0.{COLOR_RESET} Back to game categories")
|
||||
|
||||
while True:
|
||||
mod_choice_idx_str = input(f"{COLOR_PROMPT}Select modlist (or 0): {COLOR_RESET}").strip()
|
||||
if mod_choice_idx_str == '0':
|
||||
break
|
||||
if mod_choice_idx_str.isdigit():
|
||||
mod_idx = int(mod_choice_idx_str) - 1
|
||||
if 0 <= mod_idx < len(modlist_group_for_game):
|
||||
selected_modlist_info = modlist_group_for_game[mod_idx]
|
||||
self.context['modlist_source'] = 'identifier'
|
||||
# Use machine_url for installation, display name for suggestions
|
||||
self.context['modlist_value'] = selected_modlist_info.get('machine_url', selected_modlist_info['id'])
|
||||
self.context['modlist_game'] = selected_modlist_info['game']
|
||||
self.context['modlist_name_suggestion'] = selected_modlist_info['id'].split('/')[-1]
|
||||
self.logger.info(f"User selected online modlist: {selected_modlist_info}")
|
||||
break
|
||||
else:
|
||||
print(f"{COLOR_ERROR}Invalid modlist number.{COLOR_RESET}")
|
||||
else:
|
||||
print(f"{COLOR_ERROR}Invalid input. Please enter a number.{COLOR_RESET}")
|
||||
if selected_modlist_info:
|
||||
break
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
self.logger.error(f"list-modlists failed. Code: {e.returncode}")
|
||||
if e.stdout: self.logger.error(f"Engine stdout:\n{e.stdout}")
|
||||
if e.stderr: self.logger.error(f"Engine stderr:\n{e.stderr}")
|
||||
print(f"{COLOR_ERROR}Failed to fetch modlist list. Engine error (Code: {e.returncode}).{COLOR_RESET}")
|
||||
return None
|
||||
except FileNotFoundError:
|
||||
self.logger.error(f"Engine not found: {engine_executable_path_for_subprocess}")
|
||||
print(f"{COLOR_ERROR}Critical error: jackify-install-engine not found.{COLOR_RESET}")
|
||||
return None
|
||||
except Exception as e:
|
||||
self.logger.error(f"Unexpected error fetching modlists: {e}", exc_info=True)
|
||||
print(f"{COLOR_ERROR}Unexpected error fetching modlists: {e}{COLOR_RESET}")
|
||||
return None
|
||||
|
||||
elif source_choice == '2':
|
||||
self.context['modlist_source_type'] = 'local_file'
|
||||
print(f"\n{COLOR_PROMPT}Please provide the path to your .wabbajack file (tab-completion supported).{COLOR_RESET}")
|
||||
modlist_path = self.menu_handler.get_existing_file_path(
|
||||
prompt_message="Enter the path to your .wabbajack file (or 'q' to cancel):",
|
||||
extension_filter=".wabbajack", # Ensure this is the exact filter used by the method
|
||||
no_header=True # To avoid re-printing a header if get_existing_file_path has one
|
||||
)
|
||||
if modlist_path is None: # Assumes get_existing_file_path returns None on cancel/'q'
|
||||
self.logger.info("User cancelled .wabbajack file selection.")
|
||||
print(f"{COLOR_INFO}Cancelled by user.{COLOR_RESET}")
|
||||
return None
|
||||
|
||||
self.context['modlist_source'] = 'path' # For install command
|
||||
self.context['modlist_value'] = str(modlist_path)
|
||||
# Suggest a name based on the file
|
||||
self.context['modlist_name_suggestion'] = Path(modlist_path).stem
|
||||
self.logger.info(f"User selected local .wabbajack file: {modlist_path}")
|
||||
|
||||
elif source_choice == '0':
|
||||
self.logger.info("User cancelled modlist source selection.")
|
||||
print(f"{COLOR_INFO}Returning to previous menu.{COLOR_RESET}")
|
||||
return None
|
||||
else:
|
||||
self.logger.warning(f"Invalid modlist source choice: {source_choice}")
|
||||
print(f"{COLOR_ERROR}Invalid selection. Please try again.{COLOR_RESET}")
|
||||
return self.run_discovery_phase() # Re-prompt
|
||||
|
||||
# --- Prompts for install_dir, download_dir, modlist_name, api_key ---
|
||||
# It will use self.context['modlist_name_suggestion'] if available.
|
||||
|
||||
# 2. Prompt for modlist name (skip if 'modlist_name' already in context from override)
|
||||
if 'modlist_name' not in self.context or not self.context['modlist_name']:
|
||||
default_name = self.context.get('modlist_name_suggestion', 'MyModlist')
|
||||
print("\n" + "-" * 28)
|
||||
print(f"{COLOR_PROMPT}Enter a name for this modlist installation in Steam.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}(This will be the shortcut name. Default: {default_name}){COLOR_RESET}")
|
||||
modlist_name_input = input(f"{COLOR_PROMPT}Modlist Name (or 'q' to cancel): {COLOR_RESET}").strip()
|
||||
if not modlist_name_input: # User hit enter for default
|
||||
modlist_name = default_name
|
||||
elif modlist_name_input.lower() == 'q':
|
||||
self.logger.info("User cancelled at modlist name prompt.")
|
||||
return None
|
||||
else:
|
||||
modlist_name = modlist_name_input
|
||||
self.context['modlist_name'] = modlist_name
|
||||
self.logger.debug(f"Modlist name set to: {self.context['modlist_name']}")
|
||||
|
||||
# 3. Prompt for install directory
|
||||
if 'install_dir' not in self.context:
|
||||
# Use configurable base directory
|
||||
config_handler = ConfigHandler()
|
||||
base_install_dir = Path(config_handler.get_modlist_install_base_dir())
|
||||
default_install_dir = base_install_dir / self.context['modlist_name']
|
||||
print("\n" + "-" * 28)
|
||||
print(f"{COLOR_PROMPT}Enter the main installation directory for '{self.context['modlist_name']}'.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}(Default: {default_install_dir}){COLOR_RESET}")
|
||||
install_dir_path = self.menu_handler.get_directory_path(
|
||||
prompt_message=f"{COLOR_PROMPT}Install directory (or 'q' to cancel, Enter for default): {COLOR_RESET}",
|
||||
default_path=default_install_dir,
|
||||
create_if_missing=True,
|
||||
no_header=True
|
||||
)
|
||||
if install_dir_path is None:
|
||||
self.logger.info("User cancelled at install directory prompt.")
|
||||
return None
|
||||
self.context['install_dir'] = install_dir_path
|
||||
self.logger.debug(f"Install directory context set to: {self.context['install_dir']}")
|
||||
|
||||
# 4. Prompt for download directory
|
||||
if 'download_dir' not in self.context:
|
||||
# Use configurable base directory for downloads
|
||||
config_handler = ConfigHandler()
|
||||
base_download_dir = Path(config_handler.get_modlist_downloads_base_dir())
|
||||
default_download_dir = base_download_dir / self.context['modlist_name']
|
||||
|
||||
print("\n" + "-" * 28)
|
||||
print(f"{COLOR_PROMPT}Enter the downloads directory for modlist archives.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}(Default: {default_download_dir}){COLOR_RESET}")
|
||||
download_dir_path = self.menu_handler.get_directory_path(
|
||||
prompt_message=f"{COLOR_PROMPT}Download directory (or 'q' to cancel, Enter for default): {COLOR_RESET}",
|
||||
default_path=default_download_dir,
|
||||
create_if_missing=True,
|
||||
no_header=True
|
||||
)
|
||||
if download_dir_path is None:
|
||||
self.logger.info("User cancelled at download directory prompt.")
|
||||
return None
|
||||
self.context['download_dir'] = download_dir_path
|
||||
self.logger.debug(f"Download directory context set to: {self.context['download_dir']}")
|
||||
|
||||
# 5. Get Nexus authentication (OAuth or API key)
|
||||
if 'nexus_api_key' not in self.context:
|
||||
from jackify.backend.services.nexus_auth_service import NexusAuthService
|
||||
auth_service = NexusAuthService()
|
||||
|
||||
# Get current auth status
|
||||
authenticated, method, username = auth_service.get_auth_status()
|
||||
|
||||
if authenticated:
|
||||
# Already authenticated - use existing auth
|
||||
if method == 'oauth':
|
||||
print("\n" + "-" * 28)
|
||||
print(f"{COLOR_SUCCESS}Nexus Authentication: Authorized via OAuth{COLOR_RESET}")
|
||||
if username:
|
||||
print(f"{COLOR_INFO}Logged in as: {username}{COLOR_RESET}")
|
||||
elif method == 'api_key':
|
||||
print("\n" + "-" * 28)
|
||||
print(f"{COLOR_INFO}Nexus Authentication: Using API Key (Legacy){COLOR_RESET}")
|
||||
|
||||
# Get valid token/key and OAuth state for engine auto-refresh
|
||||
api_key, oauth_info = auth_service.get_auth_for_engine()
|
||||
if api_key:
|
||||
self.context['nexus_api_key'] = api_key
|
||||
self.context['nexus_oauth_info'] = oauth_info # For engine auto-refresh
|
||||
else:
|
||||
# Auth expired or invalid - prompt to set up
|
||||
print(f"\n{COLOR_WARNING}Your authentication has expired or is invalid.{COLOR_RESET}")
|
||||
authenticated = False
|
||||
|
||||
if not authenticated:
|
||||
# Not authenticated - offer to set up OAuth
|
||||
print("\n" + "-" * 28)
|
||||
print(f"{COLOR_WARNING}Nexus Mods authentication is required for downloading mods.{COLOR_RESET}")
|
||||
print(f"\n{COLOR_PROMPT}Would you like to authorize with Nexus now?{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}This will open your browser for secure OAuth authorization.{COLOR_RESET}")
|
||||
|
||||
authorize = input(f"{COLOR_PROMPT}Authorize now? [Y/n]: {COLOR_RESET}").strip().lower()
|
||||
|
||||
if authorize in ('', 'y', 'yes'):
|
||||
# Launch OAuth authorization
|
||||
print(f"\n{COLOR_INFO}Starting OAuth authorization...{COLOR_RESET}")
|
||||
print(f"{COLOR_WARNING}Your browser will open shortly.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}Note: Your browser may ask permission to open 'xdg-open' or{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}Jackify's protocol handler - please click 'Open' or 'Allow'.{COLOR_RESET}")
|
||||
|
||||
def show_message(msg):
|
||||
print(f"\n{COLOR_INFO}{msg}{COLOR_RESET}")
|
||||
|
||||
success = auth_service.authorize_oauth(show_browser_message_callback=show_message)
|
||||
|
||||
if success:
|
||||
print(f"\n{COLOR_SUCCESS}OAuth authorization successful!{COLOR_RESET}")
|
||||
_, _, username = auth_service.get_auth_status()
|
||||
if username:
|
||||
print(f"{COLOR_INFO}Authorized as: {username}{COLOR_RESET}")
|
||||
|
||||
api_key, oauth_info = auth_service.get_auth_for_engine()
|
||||
if api_key:
|
||||
self.context['nexus_api_key'] = api_key
|
||||
self.context['nexus_oauth_info'] = oauth_info # For engine auto-refresh
|
||||
else:
|
||||
print(f"{COLOR_ERROR}Failed to retrieve auth token after authorization.{COLOR_RESET}")
|
||||
return None
|
||||
else:
|
||||
print(f"\n{COLOR_ERROR}OAuth authorization failed.{COLOR_RESET}")
|
||||
return None
|
||||
else:
|
||||
# User declined OAuth - cancelled
|
||||
print(f"\n{COLOR_INFO}Authorization required to proceed. Installation cancelled.{COLOR_RESET}")
|
||||
self.logger.info("User declined Nexus authorization.")
|
||||
return None
|
||||
self.logger.debug(f"Nexus authentication configured for engine.")
|
||||
|
||||
# Display summary and confirm
|
||||
self._display_summary() # Ensure this method exists or implement it
|
||||
if self.context.get('skip_confirmation'):
|
||||
confirm = 'y'
|
||||
else:
|
||||
confirm = input(f"{COLOR_PROMPT}Proceed with installation using these settings? (y/N): {COLOR_RESET}").strip().lower()
|
||||
if confirm != 'y':
|
||||
self.logger.info("User cancelled at final confirmation.")
|
||||
print(f"{COLOR_INFO}Installation cancelled by user.{COLOR_RESET}")
|
||||
return None
|
||||
|
||||
self.logger.info("Discovery phase complete.") # Log completion first
|
||||
|
||||
# Create a copy of the context for logging, so we don't alter the original
|
||||
context_for_logging = self.context.copy()
|
||||
if 'nexus_api_key' in context_for_logging and context_for_logging['nexus_api_key'] is not None:
|
||||
context_for_logging['nexus_api_key'] = "[REDACTED]" # Redact the API key for logging
|
||||
|
||||
self.logger.info(f"Context: {context_for_logging}") # Log the redacted context
|
||||
return self.context
|
||||
|
||||
Reference in New Issue
Block a user