Files
Jackify/jackify/frontends/cli/commands/install_modlist.py
Omni cd591c14e3 Initial public release v0.1.0 - Linux Wabbajack Modlist Application
Jackify provides native Linux support for Wabbajack modlist installation
   and management with automated Steam integration and Proton configuration.

   Key Features:
   - Almost Native Linux implementation (texconv.exe run via proton)
   - Automated Steam shortcut creation and Proton prefix management
   - Both CLI and GUI interfaces, with Steam Deck optimization

   Supported Games:
   - Skyrim Special Edition
   - Fallout 4
   - Fallout New Vegas
   - Oblivion, Starfield, Enderal, and diverse other games

   Technical Architecture:
   - Clean separation between frontend and backend services
   - Powered by jackify-engine 0.3.x for Wabbajack-matching modlist installation
2025-09-05 20:46:24 +01:00

363 lines
14 KiB
Python

"""
Install Modlist Command
CLI command for installing modlists.
Extracted from the original jackify-cli.py.
"""
import os
import logging
from typing import Optional
# Import the backend services we'll need
from jackify.backend.models.modlist import ModlistContext
from jackify.shared.colors import COLOR_INFO, COLOR_ERROR, COLOR_RESET
logger = logging.getLogger(__name__)
class InstallModlistCommand:
"""Handler for the install-modlist CLI command."""
def __init__(self, backend_services, system_info):
"""Initialize with backend services.
Args:
backend_services: Dictionary of backend service instances
system_info: System information (steamdeck flag, etc.)
"""
self.backend_services = backend_services
self.system_info = system_info
def add_top_level_args(self, parser):
"""Add top-level install-modlist arguments to the main parser.
Args:
parser: The main ArgumentParser
"""
parser.add_argument(
"--install-modlist",
action="store_true",
help="Enable modlist install/list feature (for GUI integration)"
)
parser.add_argument(
"--list-modlists",
action="store_true",
help="List available modlists for a game type (with --install-modlist)"
)
parser.add_argument(
"--install",
action="store_true",
help="Install a modlist non-interactively (with --install-modlist)"
)
parser.add_argument(
"--game-type",
type=str,
default=None,
help="Game type to filter modlists (skyrim, fallout4, falloutnv, oblivion, starfield, oblivion_remastered, other)"
)
parser.add_argument(
"--modlist-value",
type=str,
help="Modlist identifier for online modlists"
)
def add_parser(self, subparsers):
"""Add the install-modlist subcommand parser.
Args:
subparsers: The ArgumentParser subparsers object
"""
install_modlist_parser = subparsers.add_parser(
"install-modlist",
help="Install or list available modlists"
)
install_modlist_parser.add_argument(
"--list",
action="store_true",
help="List available modlists for a game type"
)
install_modlist_parser.add_argument(
"--game-type",
type=str,
default=None,
help="Game type to filter modlists (skyrim, fallout4, falloutnv, oblivion, starfield, oblivion_remastered, other)"
)
return install_modlist_parser
def execute_top_level(self, args) -> int:
"""Execute top-level install-modlist functionality.
Args:
args: Parsed command-line arguments
Returns:
Exit code (0 for success, 1 for failure)
"""
if getattr(args, 'list_modlists', False):
return self.list_modlists(args)
elif getattr(args, 'install', False):
return self.install_modlist_auto(args)
else:
print(f"{COLOR_ERROR}No valid install-modlist operation specified{COLOR_RESET}")
return 1
def execute_subcommand(self, args) -> int:
"""Execute the install-modlist subcommand.
Args:
args: Parsed command-line arguments
Returns:
Exit code (0 for success, 1 for failure)
"""
if getattr(args, 'list', False):
return self.list_modlists(args)
else:
# Default behavior: run interactive modlist installation
logger.info("Starting interactive modlist installation via subcommand")
try:
# Use the working ModlistInstallCLI for interactive installation
from jackify.backend.core.modlist_operations import ModlistInstallCLI
# Use new SystemInfo pattern
modlist_cli = ModlistInstallCLI(self.system_info)
# Run interactive discovery phase
context = modlist_cli.run_discovery_phase()
if context:
# Run configuration phase (installation + Steam setup)
modlist_cli.configuration_phase()
logger.info("Interactive modlist installation completed successfully")
return 0
else:
logger.info("Modlist installation cancelled by user")
return 1
except Exception as e:
logger.error(f"Failed to install modlist: {e}")
print(f"{COLOR_ERROR}Installation failed: {e}{COLOR_RESET}")
return 1
def list_modlists(self, args) -> int:
"""List available modlists for a game type.
Args:
args: Parsed command-line arguments
Returns:
Exit code (0 for success, 1 for failure)
"""
logger.info("Listing available modlists")
try:
# Use legacy implementation for now - will migrate to backend services later
result = self._execute_legacy_list_modlists(args)
return 0
except Exception as e:
logger.error(f"Failed to list modlists: {e}")
print(f"{COLOR_ERROR}Failed to list modlists: {e}{COLOR_RESET}")
return 1
def install_modlist_auto(self, args) -> int:
"""Install a modlist non-interactively.
Args:
args: Parsed command-line arguments
Returns:
Exit code (0 for success, 1 for failure)
"""
logger.info("Starting non-interactive modlist installation")
try:
# Build context from args
context = self._build_install_context_from_args(args)
# Validate required fields
if not self._validate_install_context(context):
return 1
# Use legacy implementation for now - will migrate to backend services later
result = self._execute_legacy_install(context)
logger.info("Finished non-interactive modlist installation")
return result
except Exception as e:
logger.error(f"Failed to install modlist: {e}")
print(f"{COLOR_ERROR}Installation failed: {e}{COLOR_RESET}")
return 1
def _build_install_context_from_args(self, args) -> dict:
"""Build installation context from command arguments.
Args:
args: Parsed command-line arguments
Returns:
Context dictionary
"""
return {
'modlist_name': getattr(args, 'modlist_name', None),
'install_dir': getattr(args, 'install_dir', None),
'download_dir': getattr(args, 'download_dir', None),
'nexus_api_key': os.environ.get('NEXUS_API_KEY'),
'game_type': getattr(args, 'game_type', None),
'modlist_value': getattr(args, 'modlist_value', None),
'skip_confirmation': True,
'resolution': getattr(args, 'resolution', None),
}
def _validate_install_context(self, context: dict) -> bool:
"""Validate installation context.
Args:
context: Installation context dictionary
Returns:
True if valid, False otherwise
"""
is_gui_mode = os.environ.get('JACKIFY_GUI_MODE') == '1'
required_keys = ['modlist_name', 'install_dir', 'download_dir', 'nexus_api_key', 'game_type']
missing = [k for k in required_keys if not context.get(k)]
if is_gui_mode and missing:
print(f"ERROR: Missing required arguments for GUI workflow: {', '.join(missing)}")
print("This workflow must be fully non-interactive. Please report this as a bug if you see this message.")
return False
return True
def _execute_legacy_list_modlists(self, args):
"""Execute list modlists using backend implementation.
Args:
args: Parsed command-line arguments
"""
# Import backend services
from jackify.backend.core.modlist_operations import ModlistInstallCLI
# Use new SystemInfo pattern
modlist_cli = ModlistInstallCLI(self.system_info)
# Get all modlists from engine
raw_modlists = modlist_cli.get_all_modlists_from_engine()
# Group by game type as in original CLI
game_type_map = {
'skyrim': ['Skyrim', 'Skyrim Special Edition'],
'fallout4': ['Fallout 4'],
'falloutnv': ['Fallout New Vegas'],
'oblivion': ['Oblivion'],
'starfield': ['Starfield'],
'oblivion_remastered': ['Oblivion Remastered', 'OblivionRemastered'],
'other': None
}
grouped_modlists = {k: [] for k in game_type_map}
for m_info in raw_modlists: # m_info is like {'id': ..., 'game': ...}
found_category = False
for cat_key, cat_keywords in game_type_map.items():
if cat_key == 'other':
continue
if cat_keywords:
for keyword in cat_keywords:
if keyword.lower() in m_info.get('game', '').lower():
grouped_modlists[cat_key].append(m_info)
found_category = True
break
if found_category:
break
if not found_category:
grouped_modlists['other'].append(m_info)
# Output modlists for the requested game type
game_type = (getattr(args, 'game_type', '') or '').lower()
if game_type and game_type in grouped_modlists:
for m in grouped_modlists[game_type]:
print(m.get('id', ''))
else:
# Output all modlists
for cat_key in ['skyrim', 'fallout4', 'falloutnv', 'oblivion', 'starfield', 'oblivion_remastered', 'other']:
for m in grouped_modlists[cat_key]:
print(m.get('id', ''))
def _execute_legacy_install(self, context: dict) -> int:
"""Execute installation using backend implementation.
Args:
context: Installation context dictionary
Returns:
Exit code
"""
# Import backend services
from jackify.backend.core.modlist_operations import ModlistInstallCLI
from jackify.shared.colors import COLOR_WARNING, COLOR_PROMPT
# Use new SystemInfo pattern
modlist_cli = ModlistInstallCLI(self.system_info)
# Detect game type and check support
game_type = None
wabbajack_file_path = context.get('wabbajack_file_path')
modlist_info = context.get('modlist_info')
if wabbajack_file_path:
game_type = modlist_cli.detect_game_type(wabbajack_file_path=wabbajack_file_path)
elif modlist_info:
game_type = modlist_cli.detect_game_type(modlist_info=modlist_info)
elif context.get('game_type'):
game_type = context['game_type']
# Check if game is supported
if game_type and not modlist_cli.check_game_support(game_type):
# Show unsupported game warning
supported_games = modlist_cli.wabbajack_parser.get_supported_games_display_names()
supported_games_str = ", ".join(supported_games)
print(f"\n{COLOR_WARNING}Game Support Notice{COLOR_RESET}")
print(f"{COLOR_WARNING}While any modlist can be downloaded with Jackify, the post-install configuration can only be automatically applied to: {supported_games_str}.{COLOR_RESET}")
print(f"{COLOR_WARNING}We are working to add more automated support in future releases!{COLOR_RESET}")
# Ask for confirmation to continue
response = input(f"{COLOR_PROMPT}Click Enter to continue with the modlist installation, or type 'cancel' to abort: {COLOR_RESET}").strip().lower()
if response == 'cancel':
print("[INFO] Modlist installation cancelled by user.")
return 1
is_gui_mode = os.environ.get('JACKIFY_GUI_MODE') == '1'
if is_gui_mode:
confirmed_context = modlist_cli.run_discovery_phase(context_override=context)
if confirmed_context:
# For unsupported games, skip post-install configuration
if game_type and not modlist_cli.check_game_support(game_type):
print(f"{COLOR_WARNING}Modlist installation completed successfully.{COLOR_RESET}")
print(f"{COLOR_WARNING}Note: Post-install configuration was skipped for unsupported game type: {game_type}{COLOR_RESET}")
return 0
else:
modlist_cli.configuration_phase()
return 0
else:
print("[INFO] Modlist installation cancelled or not confirmed.")
return 1
else:
# CLI mode: allow interactive prompts as before
confirmed_context = modlist_cli.run_discovery_phase(context_override=context)
if confirmed_context:
# For unsupported games, skip post-install configuration
if game_type and not modlist_cli.check_game_support(game_type):
print(f"{COLOR_WARNING}Modlist installation completed successfully.{COLOR_RESET}")
print(f"{COLOR_WARNING}Note: Post-install configuration was skipped for unsupported game type: {game_type}{COLOR_RESET}")
return 0
else:
modlist_cli.configuration_phase()
return 0
else:
print("[INFO] Modlist installation cancelled or not confirmed.")
return 1