mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-08 03:37:44 +02:00
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
This commit is contained in:
5
jackify/frontends/cli/commands/__init__.py
Normal file
5
jackify/frontends/cli/commands/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
CLI Commands
|
||||
|
||||
Individual command implementations for the CLI interface.
|
||||
"""
|
||||
159
jackify/frontends/cli/commands/configure_modlist.py
Normal file
159
jackify/frontends/cli/commands/configure_modlist.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
Configure Modlist Command
|
||||
|
||||
CLI command for configuring a modlist post-install.
|
||||
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.configuration import ConfigurationContext
|
||||
from jackify.shared.colors import COLOR_INFO, COLOR_ERROR, COLOR_RESET
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigureModlistCommand:
|
||||
"""Handler for the configure-modlist CLI command."""
|
||||
|
||||
def __init__(self, backend_services):
|
||||
"""Initialize with backend services.
|
||||
|
||||
Args:
|
||||
backend_services: Dictionary of backend service instances
|
||||
"""
|
||||
self.backend_services = backend_services
|
||||
self.test_mode = False # TODO: Get from global config
|
||||
|
||||
def add_parser(self, subparsers):
|
||||
"""Add the configure-modlist subcommand parser.
|
||||
|
||||
Args:
|
||||
subparsers: The ArgumentParser subparsers object
|
||||
"""
|
||||
configure_modlist_parser = subparsers.add_parser(
|
||||
"configure-modlist",
|
||||
help="Configure a modlist post-install (for GUI integration)"
|
||||
)
|
||||
configure_modlist_parser.add_argument(
|
||||
"--modlist-name",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Name of the modlist to configure (Steam shortcut name)"
|
||||
)
|
||||
configure_modlist_parser.add_argument(
|
||||
"--install-dir",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Install directory of the modlist"
|
||||
)
|
||||
configure_modlist_parser.add_argument(
|
||||
"--download-dir",
|
||||
type=str,
|
||||
help="Downloads directory (optional)"
|
||||
)
|
||||
configure_modlist_parser.add_argument(
|
||||
"--nexus-api-key",
|
||||
type=str,
|
||||
help="Nexus API key (optional)"
|
||||
)
|
||||
configure_modlist_parser.add_argument(
|
||||
"--mo2-exe-path",
|
||||
type=str,
|
||||
help="Path to ModOrganizer.exe (for AppID lookup)"
|
||||
)
|
||||
configure_modlist_parser.add_argument(
|
||||
"--resolution",
|
||||
type=str,
|
||||
help="Resolution to set (optional)"
|
||||
)
|
||||
configure_modlist_parser.add_argument(
|
||||
"--skip-confirmation",
|
||||
action='store_true',
|
||||
help="Skip confirmation prompts"
|
||||
)
|
||||
return configure_modlist_parser
|
||||
|
||||
def execute(self, args) -> int:
|
||||
"""Execute the configure-modlist command.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, 1 for failure)
|
||||
"""
|
||||
logger.info("Starting non-interactive modlist configuration (CLI mode)")
|
||||
|
||||
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
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to configure modlist: {e}")
|
||||
print(f"{COLOR_ERROR}Configuration failed: {e}{COLOR_RESET}")
|
||||
return 1
|
||||
|
||||
def _build_context_from_args(self, args) -> dict:
|
||||
"""Build context dictionary 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': getattr(args, 'nexus_api_key', os.environ.get('NEXUS_API_KEY')),
|
||||
'mo2_exe_path': getattr(args, 'mo2_exe_path', None),
|
||||
'resolution': getattr(args, 'resolution', None),
|
||||
'skip_confirmation': getattr(args, 'skip_confirmation', False),
|
||||
'modlist_value': getattr(args, 'modlist_value', None),
|
||||
'modlist_source': getattr(args, 'modlist_source', None),
|
||||
}
|
||||
|
||||
def _execute_legacy_configuration(self, context: dict):
|
||||
"""Execute configuration using legacy implementation.
|
||||
|
||||
This is a temporary bridge - will be replaced with backend service calls.
|
||||
|
||||
Args:
|
||||
context: Configuration context dictionary
|
||||
|
||||
Returns:
|
||||
Result from legacy configuration
|
||||
"""
|
||||
# Import backend services
|
||||
from jackify.backend.handlers.menu_handler import ModlistMenuHandler
|
||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
|
||||
# Create legacy handler instances
|
||||
config_handler = ConfigHandler()
|
||||
modlist_menu = ModlistMenuHandler(
|
||||
config_handler=config_handler,
|
||||
test_mode=self.test_mode
|
||||
)
|
||||
|
||||
# Execute legacy configuration workflow
|
||||
# The _configure_new_modlist method already handles Steam restart, manual steps, and configuration
|
||||
result = modlist_menu._configure_new_modlist(
|
||||
default_modlist_dir=context['install_dir'],
|
||||
default_modlist_name=context['modlist_name']
|
||||
)
|
||||
|
||||
# The _configure_new_modlist method already calls run_modlist_configuration_phase internally
|
||||
# So we don't need to call it again here
|
||||
|
||||
return result
|
||||
363
jackify/frontends/cli/commands/install_modlist.py
Normal file
363
jackify/frontends/cli/commands/install_modlist.py
Normal file
@@ -0,0 +1,363 @@
|
||||
"""
|
||||
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
|
||||
247
jackify/frontends/cli/commands/tuxborn.py
Normal file
247
jackify/frontends/cli/commands/tuxborn.py
Normal file
@@ -0,0 +1,247 @@
|
||||
"""
|
||||
Tuxborn Command
|
||||
|
||||
CLI command for the Tuxborn Automatic Installer.
|
||||
Extracted from the original jackify-cli.py.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from pathlib import Path
|
||||
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 TuxbornCommand:
|
||||
"""Handler for the tuxborn-auto 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_args(self, parser):
|
||||
"""Add tuxborn-auto arguments to the main parser.
|
||||
|
||||
Args:
|
||||
parser: The main ArgumentParser
|
||||
"""
|
||||
parser.add_argument(
|
||||
"--tuxborn-auto",
|
||||
action="store_true",
|
||||
help="Run the Tuxborn Automatic Installer non-interactively (for GUI integration)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--install-dir",
|
||||
type=str,
|
||||
help="Install directory for Tuxborn (required with --tuxborn-auto)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--download-dir",
|
||||
type=str,
|
||||
help="Downloads directory for Tuxborn (required with --tuxborn-auto)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--modlist-name",
|
||||
type=str,
|
||||
default="Tuxborn",
|
||||
help="Modlist name (optional, defaults to 'Tuxborn')"
|
||||
)
|
||||
|
||||
def execute(self, args) -> int:
|
||||
"""Execute the tuxborn-auto command.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, 1 for failure)
|
||||
"""
|
||||
logger.info("Starting Tuxborn Automatic Installer (GUI integration mode)")
|
||||
|
||||
try:
|
||||
# Set up logging redirection (copied from original)
|
||||
self._setup_tee_logging()
|
||||
|
||||
# Build context from args
|
||||
context = self._build_context_from_args(args)
|
||||
|
||||
# Validate required fields
|
||||
if not self._validate_context(context):
|
||||
return 1
|
||||
|
||||
# Use legacy implementation for now - will migrate to backend services later
|
||||
result = self._execute_legacy_tuxborn(context)
|
||||
|
||||
logger.info("Finished Tuxborn Automatic Installer")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to run Tuxborn installer: {e}")
|
||||
print(f"{COLOR_ERROR}Tuxborn installation failed: {e}{COLOR_RESET}")
|
||||
return 1
|
||||
finally:
|
||||
# Restore stdout/stderr
|
||||
self._restore_stdout_stderr()
|
||||
|
||||
def _build_context_from_args(self, args) -> dict:
|
||||
"""Build context dictionary from command arguments.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
|
||||
Returns:
|
||||
Context dictionary
|
||||
"""
|
||||
install_dir = getattr(args, 'install_dir', None)
|
||||
download_dir = getattr(args, 'download_dir', None)
|
||||
modlist_name = getattr(args, 'modlist_name', 'Tuxborn')
|
||||
machineid = 'Tuxborn/Tuxborn'
|
||||
|
||||
# Try to get API key from saved config first, then environment variable
|
||||
from jackify.backend.services.api_key_service import APIKeyService
|
||||
api_key_service = APIKeyService()
|
||||
api_key = api_key_service.get_saved_api_key()
|
||||
if not api_key:
|
||||
api_key = os.environ.get('NEXUS_API_KEY')
|
||||
|
||||
resolution = getattr(args, 'resolution', None)
|
||||
mo2_exe_path = getattr(args, 'mo2_exe_path', None)
|
||||
skip_confirmation = True # Always true in GUI mode
|
||||
|
||||
context = {
|
||||
'machineid': machineid,
|
||||
'modlist_name': modlist_name,
|
||||
'install_dir': install_dir,
|
||||
'download_dir': download_dir,
|
||||
'nexus_api_key': api_key,
|
||||
'skip_confirmation': skip_confirmation,
|
||||
'resolution': resolution,
|
||||
'mo2_exe_path': mo2_exe_path,
|
||||
}
|
||||
|
||||
# PATCH: Always set modlist_value and modlist_source for Tuxborn workflow
|
||||
context['modlist_value'] = 'Tuxborn/Tuxborn'
|
||||
context['modlist_source'] = 'identifier'
|
||||
|
||||
return context
|
||||
|
||||
def _validate_context(self, context: dict) -> bool:
|
||||
"""Validate Tuxborn context.
|
||||
|
||||
Args:
|
||||
context: Tuxborn context dictionary
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
required_keys = ['modlist_name', 'install_dir', 'download_dir', 'nexus_api_key']
|
||||
missing = [k for k in required_keys if not context.get(k)]
|
||||
|
||||
if missing:
|
||||
print(f"{COLOR_ERROR}Missing required arguments for --tuxborn-auto.\\n"
|
||||
f"--install-dir, --download-dir, and NEXUS_API_KEY (env, 32+ chars) are required.{COLOR_RESET}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _setup_tee_logging(self):
|
||||
"""Set up TEE logging (copied from original implementation)."""
|
||||
import shutil
|
||||
|
||||
# TEE logging setup & log rotation (copied from original)
|
||||
class TeeStdout:
|
||||
def __init__(self, *files):
|
||||
self.files = files
|
||||
def write(self, data):
|
||||
for f in self.files:
|
||||
f.write(data)
|
||||
f.flush()
|
||||
def flush(self):
|
||||
for f in self.files:
|
||||
f.flush()
|
||||
|
||||
log_dir = Path.home() / "Jackify" / "logs"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
workflow_log_path = log_dir / "tuxborn_workflow.log"
|
||||
|
||||
# Log rotation: keep last 3 logs, 1KB each (for testing)
|
||||
max_logs = 3
|
||||
max_size = 1024 # 1KB for testing
|
||||
if workflow_log_path.exists() and workflow_log_path.stat().st_size > max_size:
|
||||
for i in range(max_logs, 0, -1):
|
||||
prev = log_dir / f"tuxborn_workflow.log.{i-1}" if i > 1 else workflow_log_path
|
||||
dest = log_dir / f"tuxborn_workflow.log.{i}"
|
||||
if prev.exists():
|
||||
if dest.exists():
|
||||
dest.unlink()
|
||||
prev.rename(dest)
|
||||
|
||||
self.workflow_log = open(workflow_log_path, 'a')
|
||||
self.orig_stdout, self.orig_stderr = sys.stdout, sys.stderr
|
||||
sys.stdout = TeeStdout(sys.stdout, self.workflow_log)
|
||||
sys.stderr = TeeStdout(sys.stderr, self.workflow_log)
|
||||
|
||||
def _restore_stdout_stderr(self):
|
||||
"""Restore original stdout/stderr."""
|
||||
if hasattr(self, 'orig_stdout'):
|
||||
sys.stdout = self.orig_stdout
|
||||
sys.stderr = self.orig_stderr
|
||||
if hasattr(self, 'workflow_log'):
|
||||
self.workflow_log.close()
|
||||
|
||||
def _execute_legacy_tuxborn(self, context: dict) -> int:
|
||||
"""Execute Tuxborn using legacy implementation.
|
||||
|
||||
Args:
|
||||
context: Tuxborn context dictionary
|
||||
|
||||
Returns:
|
||||
Exit code
|
||||
"""
|
||||
# Import backend services
|
||||
from jackify.backend.core.modlist_operations import ModlistInstallCLI
|
||||
from jackify.backend.handlers.menu_handler import MenuHandler
|
||||
|
||||
# Create legacy handler instances
|
||||
menu_handler = MenuHandler()
|
||||
modlist_cli = ModlistInstallCLI(
|
||||
menu_handler=menu_handler,
|
||||
steamdeck=self.system_info.get('is_steamdeck', False)
|
||||
)
|
||||
|
||||
confirmed_context = modlist_cli.run_discovery_phase(context_override=context)
|
||||
if confirmed_context:
|
||||
menu_handler.logger.info("Tuxborn discovery confirmed by GUI. Proceeding to configuration/installation.")
|
||||
modlist_cli.configuration_phase()
|
||||
|
||||
# Handle GUI integration prompts (copied from original)
|
||||
print('[PROMPT:RESTART_STEAM]')
|
||||
if os.environ.get('JACKIFY_GUI_MODE'):
|
||||
input() # Wait for GUI to send confirmation, no CLI prompt
|
||||
else:
|
||||
answer = input('Restart Steam automatically now? (Y/n): ')
|
||||
# ... handle answer as before ...
|
||||
|
||||
print('[PROMPT:MANUAL_STEPS]')
|
||||
if os.environ.get('JACKIFY_GUI_MODE'):
|
||||
input() # Wait for GUI to send confirmation, no CLI prompt
|
||||
else:
|
||||
input('Once you have completed ALL the steps above, press Enter to continue...')
|
||||
|
||||
return 0
|
||||
else:
|
||||
menu_handler.logger.info("Tuxborn discovery/confirmation cancelled or failed (GUI mode).")
|
||||
print(f"{COLOR_INFO}Tuxborn installation cancelled or not confirmed.{COLOR_RESET}")
|
||||
return 1
|
||||
Reference in New Issue
Block a user