Sync from development - prepare for v0.4.0

This commit is contained in:
Omni
2026-02-25 17:40:43 +00:00
parent 2eb54b9a36
commit 805718222a
324 changed files with 4914 additions and 4567 deletions

View File

@@ -91,12 +91,17 @@ class ConfigureModlistCommand:
try:
# Build configuration context from args
context = self._build_context_from_args(args)
# Use legacy implementation for now - will migrate to backend services later
result = self._execute_legacy_configuration(context)
logger.info("Finished non-interactive modlist configuration")
return 0 if result is not True else 1
if not getattr(args, 'skip_confirmation', False) and context.get('install_dir'):
from jackify.backend.handlers.modlist_install_cli_ttw import prompt_ttw_if_eligible
prompt_ttw_if_eligible(context['install_dir'], context.get('modlist_name') or '')
return 0 if result is True else 1
except Exception as e:
logger.error(f"Failed to configure modlist: {e}")
@@ -156,4 +161,4 @@ class ConfigureModlistCommand:
# The _configure_new_modlist method already calls run_modlist_configuration_phase internally
# So we don't need to call it again here
return result
return result

View File

@@ -0,0 +1,89 @@
"""
Setup Mod Organizer 2 Command
CLI interface for downloading and configuring a standalone MO2 instance.
"""
import logging
from pathlib import Path
from jackify.backend.services.mo2_setup_service import MO2SetupService, _is_dangerous_path
from jackify.shared.colors import COLOR_PROMPT, COLOR_RESET, COLOR_INFO, COLOR_SUCCESS, COLOR_ERROR
logger = logging.getLogger(__name__)
class SetupMO2Command:
"""CLI command for standalone MO2 setup"""
def run(self):
"""Execute the MO2 setup workflow"""
print(f"\n{COLOR_INFO}=== Setup Mod Organizer 2 ==={COLOR_RESET}\n")
print("Downloads the latest MO2 release, adds it to Steam, and configures a Proton prefix.")
print("Steam will be restarted during this process.\n")
# Install directory
default_dir = str(Path.home() / "ModOrganizer2")
dir_input = input(
f"{COLOR_PROMPT}Installation directory [{default_dir}]: {COLOR_RESET}"
).strip()
install_dir = Path(dir_input) if dir_input else Path(default_dir)
# Danger check
if _is_dangerous_path(install_dir):
print(f"{COLOR_ERROR}Refusing to install to a dangerous directory: {install_dir}{COLOR_RESET}")
input(f"\n{COLOR_PROMPT}Press Enter to continue...{COLOR_RESET}")
return
# Non-empty directory warning
if install_dir.exists() and any(install_dir.iterdir()):
print(f"\n{COLOR_ERROR}[WARN] Directory is not empty: {install_dir}{COLOR_RESET}")
confirm = input(
f"{COLOR_PROMPT}Files may be overwritten. Continue anyway? (y/N): {COLOR_RESET}"
).strip().lower()
if confirm != 'y':
print("Cancelled.")
input(f"\n{COLOR_PROMPT}Press Enter to continue...{COLOR_RESET}")
return
# Shortcut name
default_name = "Mod Organizer 2"
name_input = input(
f"{COLOR_PROMPT}Steam shortcut name [{default_name}]: {COLOR_RESET}"
).strip()
shortcut_name = name_input if name_input else default_name
# Confirm
print(f"\n{COLOR_INFO}Install directory: {install_dir}{COLOR_RESET}")
print(f"{COLOR_INFO}Shortcut name: {shortcut_name}{COLOR_RESET}")
confirm = input(
f"\n{COLOR_PROMPT}Proceed? (Y/n): {COLOR_RESET}"
).strip().lower()
if confirm == 'n':
print("Cancelled.")
input(f"\n{COLOR_PROMPT}Press Enter to continue...{COLOR_RESET}")
return
print(f"\n{COLOR_INFO}Starting MO2 setup...{COLOR_RESET}\n")
def _progress(msg: str):
print(f"{COLOR_INFO} {msg}{COLOR_RESET}")
service = MO2SetupService()
success, app_id, error_msg = service.setup_mo2(
install_dir=install_dir,
shortcut_name=shortcut_name,
progress_callback=_progress,
)
if success:
print(f"\n{COLOR_SUCCESS}{'='*60}{COLOR_RESET}")
print(f"{COLOR_SUCCESS}MO2 setup complete!{COLOR_RESET}")
print(f"{COLOR_SUCCESS}{'='*60}{COLOR_RESET}")
print(f"{COLOR_INFO}Steam AppID: {app_id}{COLOR_RESET}")
print(f"{COLOR_INFO}Launch Mod Organizer 2 from your Steam library.{COLOR_RESET}")
else:
print(f"\n{COLOR_ERROR}MO2 setup failed: {error_msg}{COLOR_RESET}")
print(f"{COLOR_INFO}Check logs for details.{COLOR_RESET}")
input(f"\n{COLOR_PROMPT}Press Enter to continue...{COLOR_RESET}")

View File

@@ -33,11 +33,9 @@ from jackify.backend.handlers.filesystem_handler import FileSystemHandler
from jackify.backend.handlers.path_handler import PathHandler
from jackify.backend.handlers.shortcut_handler import ShortcutHandler
from jackify.backend.handlers.menu_handler import MenuHandler
from jackify.backend.handlers.mo2_handler import MO2Handler
logger = logging.getLogger(__name__)
class JackifyCLI:
"""Main application class for Jackify CLI Frontend"""
@@ -92,10 +90,6 @@ class JackifyCLI:
self.selected_modlist = None
self.setup_complete = False
def _debug_print(self, message):
"""Print debug message only if debug mode is enabled"""
if hasattr(self, '_debug_mode') and self._debug_mode:
logger.debug(message)
def _configure_logging_early(self):
"""Configure logging to be quiet during initialization, will be adjusted after arg parsing"""
@@ -113,22 +107,40 @@ class JackifyCLI:
"""Configure final logging level based on parsed arguments"""
# Use the existing LoggingHandler for proper log rotation
from jackify.backend.handlers.logging_handler import LoggingHandler
from jackify.shared.paths import get_jackify_logs_dir
# Set up CLI-specific logging with rotation
logging_handler = LoggingHandler()
logging_handler.rotate_log_for_logger('jackify-cli', 'Modlist_Install_workflow_cli.log')
cli_logger = logging_handler.setup_logger('jackify-cli', 'Modlist_Install_workflow_cli.log')
# Keep CLI logging in the canonical modlist workflow log file.
logging_handler.rotate_log_for_logger('jackify-cli', 'Modlist_Install_workflow.log')
cli_logger = logging_handler.setup_logger('jackify-cli', 'Modlist_Install_workflow.log')
# Remove legacy CLI log artifact if present (old naming path no longer used).
try:
legacy_cli_log = get_jackify_logs_dir() / "Modlist_Install_workflow_cli.log"
if legacy_cli_log.exists():
legacy_cli_log.unlink()
except Exception:
pass
# Configure logging level
if self.args.debug:
cli_logger.setLevel(logging.DEBUG)
root_level = logging.DEBUG
print("Debug logging enabled for console and file")
elif self.args.verbose:
cli_logger.setLevel(logging.INFO)
root_level = logging.INFO
print("Verbose logging enabled for console and file")
else:
# Keep it at WARNING level for clean startup
# Keep console clean in normal mode; details remain in workflow log.
cli_logger.setLevel(logging.WARNING)
root_level = logging.ERROR
root_logger = logging.getLogger()
root_logger.setLevel(root_level)
for handler in root_logger.handlers:
handler.setLevel(root_level)
def _is_steamdeck(self):
"""Check if running on Steam Deck"""
@@ -192,7 +204,7 @@ class JackifyCLI:
def _check_for_updates_on_startup(self):
"""Check for updates on startup in background thread"""
try:
self._debug_print("Checking for updates on startup...")
logger.debug("Checking for updates on startup...")
def update_check_callback(update_info):
"""Handle update check results"""
@@ -207,15 +219,15 @@ class JackifyCLI:
print(f"\nTo update, run: jackify --update")
print("Or visit: https://github.com/Omni-guides/Jackify/releases")
else:
self._debug_print("No updates available")
logger.debug("No updates available")
except Exception as e:
self._debug_print(f"Error showing update info: {e}")
logger.debug(f"Error showing update info: {e}")
# Check for updates in background
self.backend_services['update_service'].check_for_updates_async(update_check_callback)
except Exception as e:
self._debug_print(f"Error checking for updates on startup: {e}")
logger.debug(f"Error checking for updates on startup: {e}")
# Continue anyway - don't block startup on update check errors
def _handle_update(self):
@@ -326,7 +338,6 @@ class JackifyCLI:
self.menu_handler = self.menu # Alias for backend compatibility
# Add MO2 handler to the menu handler for additional tasks menu
self.menu.mo2_handler = MO2Handler(self.menu)
# Set steamdeck attribute that menus expect
self.steamdeck = self.system_info.is_steamdeck
@@ -359,24 +370,24 @@ class JackifyCLI:
# Now that we have args, configure logging properly
self._configure_logging_final()
self._debug_print('Initializing Jackify CLI Frontend')
self._debug_print('JackifyCLI.run() called')
self._debug_print(f'Parsed args: {self.args}')
logger.debug('Initializing Jackify CLI Frontend')
logger.debug('JackifyCLI.run() called')
logger.debug(f'Parsed args: {self.args}')
# Handle update functionality
if getattr(self.args, 'update', False):
self._debug_print('Entering update workflow')
logger.debug('Entering update workflow')
return self._handle_update()
# Handle legacy restart-steam functionality (temporary)
if getattr(self.args, 'restart_steam', False):
self._debug_print('Entering restart_steam workflow')
logger.debug('Entering restart_steam workflow')
return self._handle_restart_steam()
# Handle install-modlist top-level functionality
if getattr(self.args, 'install_modlist', False):
self._debug_print('Entering install_modlist workflow')
logger.debug('Entering install_modlist workflow')
return self.commands['install_modlist'].execute_top_level(self.args)
# Handle subcommands
@@ -514,12 +525,10 @@ class JackifyCLI:
command_instance.run()
return 0
def main():
"""Legacy main function (not used in new structure)"""
pass
if __name__ == "__main__":
# Do not call directly -- use __main__.py
print("Please use: python -m jackify.frontends.cli")

View File

@@ -37,8 +37,10 @@ class AdditionalMenuHandler:
print(f" {COLOR_ACTION}→ Install TTW using TTW_Linux_Installer{COLOR_RESET}")
print(f"{COLOR_SELECTION}3.{COLOR_RESET} Install Wabbajack Application")
print(f" {COLOR_ACTION}→ Downloads and configures the Wabbajack app itself (via Proton){COLOR_RESET}")
print(f"{COLOR_SELECTION}4.{COLOR_RESET} Setup Mod Organizer 2")
print(f" {COLOR_ACTION}→ Download and configure a standalone MO2 instance{COLOR_RESET}")
print(f"{COLOR_SELECTION}0.{COLOR_RESET} Return to Main Menu")
selection = input(f"\n{COLOR_PROMPT}Enter your selection (0-3): {COLOR_RESET}").strip()
selection = input(f"\n{COLOR_PROMPT}Enter your selection (0-4): {COLOR_RESET}").strip()
if selection.lower() == 'q': # Allow 'q' to re-display menu
continue
@@ -48,21 +50,14 @@ class AdditionalMenuHandler:
self._execute_ttw_install(cli_instance)
elif selection == "3":
self._execute_install_wabbajack(cli_instance)
elif selection == "4":
self._execute_setup_mo2(cli_instance)
elif selection == "0":
break
else:
print("Invalid selection. Please try again.")
time.sleep(1)
def _execute_legacy_install_mo2(self, cli_instance):
"""LEGACY BRIDGE: Execute MO2 installation"""
# LEGACY BRIDGE: Use legacy imports until backend migration complete
if hasattr(cli_instance, 'menu') and hasattr(cli_instance.menu, 'mo2_handler'):
cli_instance.menu.mo2_handler.install_mo2()
else:
print(f"{COLOR_INFO}MO2 handler not available - this will be implemented in Phase 2.3{COLOR_RESET}")
input("\nPress Enter to continue...")
def _execute_legacy_recovery_menu(self, cli_instance):
"""LEGACY BRIDGE: Execute recovery menu"""
# Handled by RecoveryMenuHandler
@@ -314,3 +309,12 @@ class AdditionalMenuHandler:
if self.logger:
self.logger.debug("AdditionalMenuHandler: Executing Install Wabbajack command")
command.run()
def _execute_setup_mo2(self, cli_instance):
"""Execute standalone MO2 setup"""
from jackify.frontends.cli.commands.setup_mo2 import SetupMO2Command
command = SetupMO2Command()
if self.logger:
self.logger.debug("AdditionalMenuHandler: Executing Setup MO2 command")
command.run()