Sync from development - prepare for v0.1.1

This commit is contained in:
Omni
2025-09-15 20:18:13 +01:00
parent 0b6e32beac
commit 70b18004e1
64 changed files with 5142 additions and 1164 deletions

View File

@@ -168,7 +168,7 @@ def main():
print(f"Error: {diagnosis['error']}")
return
print(f"\n📊 Diagnosis Results:")
print(f"\nDiagnosis Results:")
print(f" Average CPU: {diagnosis['avg_cpu']:.1f}% (Range: {diagnosis['min_cpu']:.1f}% - {diagnosis['max_cpu']:.1f}%)")
print(f" Memory usage: {diagnosis['avg_memory_mb']:.1f}MB (Peak: {diagnosis['max_memory_mb']:.1f}MB)")
print(f" Low CPU samples: {diagnosis['low_cpu_samples']}/{diagnosis['samples']} "

View File

@@ -761,7 +761,14 @@ class ModlistHandler:
# Conditionally update binary and working directory paths
# Skip for jackify-engine workflows since paths are already correct
if not getattr(self, 'engine_installed', False):
steam_libraries = [self.steam_library] if self.steam_library else None
# Convert steamapps/common path to library root path
steam_libraries = None
if self.steam_library:
# self.steam_library is steamapps/common, need to go up 2 levels to get library root
steam_library_root = Path(self.steam_library).parent.parent
steam_libraries = [steam_library_root]
self.logger.debug(f"Using Steam library root: {steam_library_root}")
if not self.path_handler.edit_binary_working_paths(
modlist_ini_path=modlist_ini_path_obj,
modlist_dir_path=modlist_dir_path_obj,

View File

@@ -723,13 +723,17 @@ class ModlistInstallCLI:
if chunk == b'\n':
# Complete line - decode and print
line = buffer.decode('utf-8', errors='replace')
print(line, end='')
# Enhance Nexus download errors with modlist context
enhanced_line = self._enhance_nexus_error(line)
print(enhanced_line, end='')
buffer = b''
last_progress_time = time.time()
elif chunk == b'\r':
# Carriage return - decode and print without newline
line = buffer.decode('utf-8', errors='replace')
print(line, end='')
# Enhance Nexus download errors with modlist context
enhanced_line = self._enhance_nexus_error(line)
print(enhanced_line, end='')
sys.stdout.flush()
buffer = b''
last_progress_time = time.time()
@@ -1098,4 +1102,36 @@ class ModlistInstallCLI:
print(f"Nexus API Key: [SET]")
else:
print(f"Nexus API Key: [NOT SET - WILL LIKELY FAIL]")
print(f"{COLOR_INFO}----------------------------------------{COLOR_RESET}")
print(f"{COLOR_INFO}----------------------------------------{COLOR_RESET}")
def _enhance_nexus_error(self, line: str) -> str:
"""
Enhance Nexus download error messages by adding the mod URL for easier troubleshooting.
"""
import re
# Pattern to match Nexus download errors with ModID and FileID
nexus_error_pattern = r"Failed to download '[^']+' from Nexus \(Game: ([^,]+), ModID: (\d+), FileID: \d+\):"
match = re.search(nexus_error_pattern, line)
if match:
game_name = match.group(1)
mod_id = match.group(2)
# Map game names to Nexus URL segments
game_url_map = {
'SkyrimSpecialEdition': 'skyrimspecialedition',
'Skyrim': 'skyrim',
'Fallout4': 'fallout4',
'FalloutNewVegas': 'newvegas',
'Oblivion': 'oblivion',
'Starfield': 'starfield'
}
game_url = game_url_map.get(game_name, game_name.lower())
mod_url = f"https://www.nexusmods.com/{game_url}/mods/{mod_id}"
# Add URL on next line for easier debugging
return f"{line}\n Nexus URL: {mod_url}"
return line

View File

@@ -815,11 +815,12 @@ class PathHandler:
subpath = value_part[idx:].lstrip('/')
correct_steam_lib = None
for lib in steam_libraries:
if (lib / subpath.split('/')[2]).exists():
correct_steam_lib = lib.parent
# Check if the actual game folder exists in this library
if len(subpath.split('/')) > 3 and (lib / subpath.split('/')[2] / subpath.split('/')[3]).exists():
correct_steam_lib = lib
break
if not correct_steam_lib and steam_libraries:
correct_steam_lib = steam_libraries[0].parent
correct_steam_lib = steam_libraries[0]
if correct_steam_lib:
new_binary_path = f"{drive_prefix}/{correct_steam_lib}/{subpath}".replace('\\', '/').replace('//', '/')
else:

View File

@@ -1,141 +0,0 @@
import os
import sys
import json
import requests
import shutil
import tempfile
import time
from pathlib import Path
GITHUB_OWNER = "Omni-guides"
GITHUB_REPO = "Jackify"
ASSET_NAME = "jackify"
CONFIG_DIR = os.path.expanduser("~/.config/jackify")
TOKEN_PATH = os.path.join(CONFIG_DIR, "github_token")
LAST_CHECK_PATH = os.path.join(CONFIG_DIR, "last_update_check.json")
THROTTLE_HOURS = 6
def get_github_token():
if os.path.exists(TOKEN_PATH):
with open(TOKEN_PATH, "r") as f:
return f.read().strip()
return None
def get_latest_release_info():
url = f"https://api.github.com/repos/{GITHUB_OWNER}/{GITHUB_REPO}/releases/latest"
headers = {}
token = get_github_token()
if token:
headers["Authorization"] = f"token {token}"
resp = requests.get(url, headers=headers, verify=True)
if resp.status_code == 200:
return resp.json()
else:
raise RuntimeError(f"Failed to fetch release info: {resp.status_code} {resp.text}")
def get_current_version():
# This should match however Jackify stores its version
try:
from jackify import __version__
return __version__
except ImportError:
return None
def should_check_for_update():
try:
if os.path.exists(LAST_CHECK_PATH):
with open(LAST_CHECK_PATH, "r") as f:
data = json.load(f)
last_check = data.get("last_check", 0)
now = int(time.time())
if now - last_check < THROTTLE_HOURS * 3600:
return False
return True
except Exception as e:
print(f"[WARN] Could not read last update check timestamp: {e}")
return True
def record_update_check():
try:
with open(LAST_CHECK_PATH, "w") as f:
json.dump({"last_check": int(time.time())}, f)
except Exception as e:
print(f"[WARN] Could not write last update check timestamp: {e}")
def check_for_update():
if not should_check_for_update():
return False, None, None
try:
release = get_latest_release_info()
latest_version = release["tag_name"].lstrip("v")
current_version = get_current_version()
if current_version is None:
print("[WARN] Could not determine current version.")
record_update_check()
return False, None, None
if latest_version > current_version:
record_update_check()
return True, latest_version, release
record_update_check()
return False, latest_version, release
except Exception as e:
print(f"[ERROR] Update check failed: {e}")
record_update_check()
return False, None, None
def download_latest_asset(release):
token = get_github_token()
headers = {"Accept": "application/octet-stream"}
if token:
headers["Authorization"] = f"token {token}"
for asset in release["assets"]:
if asset["name"] == ASSET_NAME:
download_url = asset["url"]
resp = requests.get(download_url, headers=headers, stream=True, verify=True)
if resp.status_code == 200:
return resp.content
else:
raise RuntimeError(f"Failed to download asset: {resp.status_code} {resp.text}")
raise RuntimeError(f"Asset '{ASSET_NAME}' not found in release.")
def replace_current_binary(new_binary_bytes):
current_exe = os.path.realpath(sys.argv[0])
backup_path = current_exe + ".bak"
try:
# Write to a temp file first
with tempfile.NamedTemporaryFile(delete=False, dir=os.path.dirname(current_exe)) as tmpf:
tmpf.write(new_binary_bytes)
tmp_path = tmpf.name
# Backup current binary
shutil.copy2(current_exe, backup_path)
# Replace atomically
os.replace(tmp_path, current_exe)
os.chmod(current_exe, 0o755)
print(f"[INFO] Updated binary written to {current_exe}. Backup at {backup_path}.")
return True
except Exception as e:
print(f"[ERROR] Failed to replace binary: {e}")
return False
def main():
if '--update' in sys.argv:
print("Checking for updates...")
update_available, latest_version, release = check_for_update()
if update_available:
print(f"A new version (v{latest_version}) is available. Downloading...")
try:
new_bin = download_latest_asset(release)
if replace_current_binary(new_bin):
print("Update complete! Please restart Jackify.")
else:
print("Update failed during binary replacement.")
except Exception as e:
print(f"[ERROR] Update failed: {e}")
else:
print("You are already running the latest version.")
sys.exit(0)
# For direct CLI testing
if __name__ == "__main__":
main()