From b6d179442b884f46c271734a56077f7d2c696ca0 Mon Sep 17 00:00:00 2001
From: Stoppedwumm <129097720+Stoppedwumm@users.noreply.github.com>
Date: Sat, 3 May 2025 13:56:31 +0000
Subject: [PATCH 01/19] =?UTF-8?q?F=C3=BCge=20die=20Benutzeroberfl=C3=A4che?=
=?UTF-8?q?=20f=C3=BCr=20die=20Konsole=20und=20das=20Mod-Dashboard=20hinzu?=
=?UTF-8?q?,=20implementiere=20die=20Nachrichten-=20und=20Fehlerverarbeitu?=
=?UTF-8?q?ng?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ui/chrome/console.html | 22 ++++++++++
ui/chrome/css/main.css | 27 ++++++++++++
ui/chrome/index.html | 22 ++++++++++
ui/chrome/js/console.js | 18 ++++++++
ui/chrome/js/moddashboard.js | 0
ui/chrome/moddashboard/index.html | 28 ++++++++++++
ui/externalScripts/console_preload.js | 14 ++++++
ui/externalScripts/main_preload.js | 7 +++
ui_index.cjs | 63 +++++++++++++++++++++++++++
9 files changed, 201 insertions(+)
create mode 100644 ui/chrome/console.html
create mode 100644 ui/chrome/css/main.css
create mode 100644 ui/chrome/index.html
create mode 100644 ui/chrome/js/console.js
create mode 100644 ui/chrome/js/moddashboard.js
create mode 100644 ui/chrome/moddashboard/index.html
create mode 100644 ui/externalScripts/console_preload.js
create mode 100644 ui/externalScripts/main_preload.js
create mode 100644 ui_index.cjs
diff --git a/ui/chrome/console.html b/ui/chrome/console.html
new file mode 100644
index 0000000..4d370d4
--- /dev/null
+++ b/ui/chrome/console.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+ Console
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/chrome/css/main.css b/ui/chrome/css/main.css
new file mode 100644
index 0000000..6b0d48d
--- /dev/null
+++ b/ui/chrome/css/main.css
@@ -0,0 +1,27 @@
+body {
+ background-color: #ffffff;
+ font-family: Arial, sans-serif;
+ color: #333;
+}
+
+#terminal {
+ width: 100%;
+ height: 300px; /* Adjusted height for better visibility */
+ overflow-y: auto;
+ background-color: #000;
+ color: #fff;
+ font-family: monospace;
+ border: 1px solid #ccc;
+ padding: 10px;
+ box-sizing: border-box;
+ margin-top: 20px; /* Added spacing from other elements */
+}
+
+#terminal pre {
+ margin: 0;
+ padding: 0;
+}
+
+#terminal pre .error {
+ color: red;
+}
\ No newline at end of file
diff --git a/ui/chrome/index.html b/ui/chrome/index.html
new file mode 100644
index 0000000..97151b5
--- /dev/null
+++ b/ui/chrome/index.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+ MC Launcher
+
+
+
+
+
+
+ Mod Dashboard
+
+
+
\ No newline at end of file
diff --git a/ui/chrome/js/console.js b/ui/chrome/js/console.js
new file mode 100644
index 0000000..f58f3e2
--- /dev/null
+++ b/ui/chrome/js/console.js
@@ -0,0 +1,18 @@
+mcAPI.onMessage((msg) => {
+ const c = document.getElementById('terminal');
+ const newLine = document.createElement('pre');
+ console.log(msg);
+ newLine.innerText = msg;
+ c.appendChild(newLine);
+ c.scrollTop = c.scrollHeight;
+});
+
+mcAPI.onError((msg) => {
+ const c = document.getElementById('terminal');
+ const newLine = document.createElement('pre');
+ console.error(msg);
+ newLine.innerText = msg;
+ newLine.className = 'error'
+ c.appendChild(newLine);
+ c.scrollTop = c.scrollHeight;
+});
\ No newline at end of file
diff --git a/ui/chrome/js/moddashboard.js b/ui/chrome/js/moddashboard.js
new file mode 100644
index 0000000..e69de29
diff --git a/ui/chrome/moddashboard/index.html b/ui/chrome/moddashboard/index.html
new file mode 100644
index 0000000..3aa0c47
--- /dev/null
+++ b/ui/chrome/moddashboard/index.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+ Mod Dashboard
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/externalScripts/console_preload.js b/ui/externalScripts/console_preload.js
new file mode 100644
index 0000000..3690c58
--- /dev/null
+++ b/ui/externalScripts/console_preload.js
@@ -0,0 +1,14 @@
+const {ipcRenderer,contextBridge} = require('electron');
+
+contextBridge.exposeInMainWorld('mcAPI', {
+ onMessage: (callback) => {
+ ipcRenderer.on('msg', (event, arg) => {
+ callback(arg);
+ });
+ },
+ onError: (callback) => {
+ ipcRenderer.on('error', (event, arg) => {
+ callback(arg);
+ });
+ }
+});
\ No newline at end of file
diff --git a/ui/externalScripts/main_preload.js b/ui/externalScripts/main_preload.js
new file mode 100644
index 0000000..74c0498
--- /dev/null
+++ b/ui/externalScripts/main_preload.js
@@ -0,0 +1,7 @@
+const {ipcRenderer,contextBridge} = require('electron');
+
+contextBridge.exposeInMainWorld('mcAPI', {
+ launch: () => {
+ ipcRenderer.send('launch');
+ }
+})
\ No newline at end of file
diff --git a/ui_index.cjs b/ui_index.cjs
new file mode 100644
index 0000000..96c8006
--- /dev/null
+++ b/ui_index.cjs
@@ -0,0 +1,63 @@
+const { app, BrowserWindow, ipcMain } = require('electron');
+const path = require('path');
+const fs = require('fs');
+const { spawn } = require('child_process');
+const mainIndex = path.join(__dirname, 'index.js')
+
+const createWindow = () => {
+ const win = new BrowserWindow({
+ width: 800,
+ height: 600,
+ webPreferences: {
+ preload: path.join(__dirname, 'ui', 'externalScripts', 'main_preload.js')
+ }
+ });
+
+ win.loadFile(path.join(__dirname, 'ui', 'chrome', 'index.html'));
+}
+
+const createConsole = () => {
+ const consoleWin = new BrowserWindow({
+ width: 800,
+ height: 600,
+ webPreferences: {
+ preload: path.join(__dirname, 'ui', 'externalScripts', 'console_preload.js')
+ }
+ });
+
+ consoleWin.loadFile(path.join(__dirname, 'ui', 'chrome', 'console.html'));
+ return consoleWin
+}
+
+app.whenReady().then(() => {
+ ipcMain.on('launch', (event, arg) => {
+ let consoleWin = createConsole();
+ const process = spawn('node', [mainIndex]);
+
+ process.stdout.on('data', (data) => {
+ consoleWin.webContents.send('msg', String(data)); // Send to renderer
+ console.log(`stdout: ${data}`);
+ });
+
+ process.stderr.on('data', (data) => {
+ consoleWin.webContents.send('error', String(data)); // Send to renderer
+ console.error(`stderr: ${data}`);
+ });
+
+ process.on('close', (code) => {
+ console.log(`child process exited with code ${code}`);
+ });
+
+ process.on('error', (error) => {
+ console.error(`Error: ${error}`);
+ });
+
+ process.on('exit', (code) => {
+ console.log(`Process exited with code: ${code}`);
+ event.reply('process-exit', code);
+ consoleWin.close();
+ consoleWin = null;
+ });
+ });
+ createWindow();
+});
\ No newline at end of file
From 60ca56538ece7dc4a1e5298e69d97794598528df Mon Sep 17 00:00:00 2001
From: StoppedwummSites <150438484+StoppedwummSites@users.noreply.github.com>
Date: Tue, 6 May 2025 09:25:04 +0200
Subject: [PATCH 02/19] Port to python
---
.gitignore | 3 +-
1.21.1.json | 56 ---
__main__.py | 1002 +++++++++++++++++++++++++++++++++++++++++++++++++++
java.py | 428 ++++++++++++++++++++++
replacer.py | 68 ++++
5 files changed, 1500 insertions(+), 57 deletions(-)
create mode 100644 __main__.py
create mode 100644 java.py
create mode 100644 replacer.py
diff --git a/.gitignore b/.gitignore
index 6393361..874c3ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,5 @@ config.json
java-runtime
.DS_Store
dist
-build
\ No newline at end of file
+build
+__pycache__
\ No newline at end of file
diff --git a/1.21.1.json b/1.21.1.json
index 20b4bc2..fcfa979 100644
--- a/1.21.1.json
+++ b/1.21.1.json
@@ -49,62 +49,6 @@
"--height",
"${resolution_height}"
]
- },
- {
- "rules": [
- {
- "action": "allow",
- "features": {
- "has_quick_plays_support": true
- }
- }
- ],
- "value": [
- "--quickPlayPath",
- "${quickPlayPath}"
- ]
- },
- {
- "rules": [
- {
- "action": "allow",
- "features": {
- "is_quick_play_singleplayer": true
- }
- }
- ],
- "value": [
- "--quickPlaySingleplayer",
- "${quickPlaySingleplayer}"
- ]
- },
- {
- "rules": [
- {
- "action": "allow",
- "features": {
- "is_quick_play_multiplayer": true
- }
- }
- ],
- "value": [
- "--quickPlayMultiplayer",
- "${quickPlayMultiplayer}"
- ]
- },
- {
- "rules": [
- {
- "action": "allow",
- "features": {
- "is_quick_play_realms": true
- }
- }
- ],
- "value": [
- "--quickPlayRealms",
- "${quickPlayRealms}"
- ]
}
],
"jvm": [
diff --git a/__main__.py b/__main__.py
new file mode 100644
index 0000000..f8fc65b
--- /dev/null
+++ b/__main__.py
@@ -0,0 +1,1002 @@
+# main.py
+import os
+import platform
+import pathlib
+import json
+import hashlib
+import asyncio
+import logging
+import sys
+import zipfile
+import shutil # For copytree and rmtree
+from typing import Dict, Any, List, Optional, Union
+
+import aiohttp
+import aiofiles
+import aiofiles.os
+from tqdm.asyncio import tqdm # Use tqdm's async version
+
+# --- Local Imports ---
+try:
+ # Assuming java.py and replacer.py are in the same directory
+ from java import download_java
+ from replacer import replace_text
+except ImportError as e:
+ print(f"Error importing local modules (java.py, replacer.py): {e}")
+ print("Please ensure these files are in the same directory as main.py.")
+ sys.exit(1)
+
+# --- Logging Setup ---
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+log = logging.getLogger(__name__)
+
+# --- Constants and Configuration ---
+DEFAULT_VERSION_MANIFEST = 'neoforge-21.1.162.json' # Default if not in launcher_config
+SCRIPT_DIR = pathlib.Path(__file__).parent.resolve()
+
+# Load launcher_config.json and patch paths
+try:
+ with open(SCRIPT_DIR / "launcher_config.json", 'r') as f:
+ launcher_config_raw = json.load(f)
+except FileNotFoundError:
+ log.error("launcher_config.json not found in the script directory.")
+ sys.exit(1)
+except json.JSONDecodeError as e:
+ log.error(f"Error parsing launcher_config.json: {e}")
+ sys.exit(1)
+
+# Patch launcher config immediately (replace_text is synchronous)
+launcher_config = {}
+for key, value in launcher_config_raw.items():
+ launcher_config[key] = replace_text(value, {':thisdir:': str(SCRIPT_DIR)})
+
+log.info(f"Launcher config: {json.dumps(launcher_config, indent=2)}")
+
+# Main target version manifest filename from launcher_config or default
+TARGET_VERSION_MANIFEST_FILENAME = launcher_config.get('version', DEFAULT_VERSION_MANIFEST)
+
+# Load user config (config.json) if it exists
+cfg = {}
+try:
+ config_path = SCRIPT_DIR / 'config.json'
+ if config_path.exists():
+ # Reading small config at start is often acceptable synchronously.
+ with open(config_path, 'r', encoding='utf-8') as f:
+ cfg = json.load(f)
+except json.JSONDecodeError as e:
+ log.warning(f"Could not parse config.json: {e}. Using defaults.")
+except Exception as e:
+ log.warning(f"Could not read config.json: {e}. Using defaults.")
+
+# --- Authentication Details ---
+AUTH_PLAYER_NAME = cfg.get("auth_player_name") if cfg.get("auth_player_name") else 'Player'
+AUTH_UUID = cfg.get("auth_uuid") if cfg.get("auth_uuid") else '00000000-0000-0000-0000-000000000000'
+AUTH_ACCESS_TOKEN = cfg.get("auth_access_token") if cfg.get("auth_access_token") else '00000000000000000000000000000000'
+AUTH_XUID = cfg.get("auth_xuid") if cfg.get("auth_xuid") else '0'
+USER_TYPE = 'msa'
+
+# --- Directories ---
+BASE_PATH = pathlib.Path(launcher_config.get('basepath', SCRIPT_DIR / '.mc_launcher_data'))
+MINECRAFT_DIR = BASE_PATH / launcher_config.get('path', '.minecraft')
+VERSIONS_DIR = MINECRAFT_DIR / 'versions'
+LIBRARIES_DIR = MINECRAFT_DIR / 'libraries'
+ASSETS_DIR = MINECRAFT_DIR / 'assets'
+ASSET_INDEXES_DIR = ASSETS_DIR / 'indexes'
+ASSET_OBJECTS_DIR = ASSETS_DIR / 'objects'
+LAUNCHER_PROFILES_PATH = MINECRAFT_DIR / 'launcher_profiles.json'
+CLIENT_STORAGE_PATH = MINECRAFT_DIR / 'client_storage.json'
+BACKUP_PATH_BASE = SCRIPT_DIR / '.minecraft_autobackup'
+JAVA_INSTALL_DIR = SCRIPT_DIR / 'java-runtime'
+
+# --- Helper Functions ---
+
+async def get_file_sha1(file_path: pathlib.Path) -> str:
+ """Calculates the SHA1 hash of a file asynchronously."""
+ sha1_hash = hashlib.sha1()
+ try:
+ async with aiofiles.open(file_path, 'rb') as f:
+ while True:
+ chunk = await f.read(8192)
+ if not chunk:
+ break
+ sha1_hash.update(chunk)
+ return sha1_hash.hexdigest()
+ except FileNotFoundError:
+ raise FileNotFoundError(f"File not found for SHA1 calculation: {file_path}")
+ except Exception as e:
+ raise RuntimeError(f"Error calculating SHA1 for {file_path}: {e}")
+
+async def file_exists(file_path: pathlib.Path) -> bool:
+ """Checks if a file exists asynchronously."""
+ try:
+ stats = await aiofiles.os.stat(file_path)
+ # Check if it's a regular file using stat results
+ return stats.st_mode & 0o100000 != 0
+ except OSError: # Includes FileNotFoundError
+ return False
+
+# Shared aiohttp session
+AIOHTTP_SESSION = None
+
+async def get_session():
+ global AIOHTTP_SESSION
+ if AIOHTTP_SESSION is None or AIOHTTP_SESSION.closed:
+ AIOHTTP_SESSION = aiohttp.ClientSession()
+ return AIOHTTP_SESSION
+
+async def close_session():
+ global AIOHTTP_SESSION
+ if AIOHTTP_SESSION and not AIOHTTP_SESSION.closed:
+ await AIOHTTP_SESSION.close()
+ AIOHTTP_SESSION = None
+
+async def download_file(
+ url: str,
+ dest_path: pathlib.Path,
+ expected_sha1: Optional[str],
+ force_download: bool = False,
+ pbar: Optional[tqdm] = None # Progress bar instance passed in
+) -> bool:
+ """Downloads a file asynchronously, verifies SHA1 hash."""
+ dir_path = dest_path.parent
+ await aiofiles.os.makedirs(dir_path, exist_ok=True)
+
+ needs_download = force_download
+ exists = await file_exists(dest_path)
+
+ if exists and not force_download:
+ if expected_sha1:
+ try:
+ current_sha1 = await get_file_sha1(dest_path)
+ if current_sha1.lower() == expected_sha1.lower():
+ if pbar: pbar.update(1) # Update progress even if skipped
+ return False # No download needed
+ else:
+ log.warning(f"SHA1 mismatch for existing file {dest_path.name}. Expected {expected_sha1}, got {current_sha1}. Redownloading.")
+ needs_download = True
+ except Exception as hash_error:
+ log.warning(f"Warning: Could not hash existing file {dest_path}. Redownloading. Error: {hash_error}")
+ needs_download = True
+ else:
+ if pbar: pbar.update(1)
+ return False # File exists, no hash check, no download needed
+ elif not exists:
+ needs_download = True
+
+ if not needs_download:
+ if pbar: pbar.update(1) # Ensure progress updates if no download needed
+ return False
+
+ session = await get_session()
+ try:
+ async with session.get(url) as response:
+ if not response.ok:
+ # Clean up potentially corrupted file before throwing
+ try:
+ if await aiofiles.os.path.exists(dest_path): await aiofiles.os.remove(dest_path)
+ except OSError: pass # Ignore deletion errors
+ raise aiohttp.ClientResponseError(
+ response.request_info, response.history,
+ status=response.status, message=f"Failed to download {url}: {response.reason}", headers=response.headers
+ )
+ # Stream download to file
+ async with aiofiles.open(dest_path, 'wb') as f:
+ async for chunk in response.content.iter_chunked(8192):
+ await f.write(chunk)
+
+ if expected_sha1:
+ downloaded_sha1 = await get_file_sha1(dest_path)
+ if downloaded_sha1.lower() != expected_sha1.lower():
+ raise ValueError(f"SHA1 mismatch for {dest_path.name}. Expected {expected_sha1}, got {downloaded_sha1}")
+
+ if pbar: pbar.update(1) # Update progress after successful download
+ return True # Download occurred
+
+ except Exception as error:
+ log.error(f"Error downloading {url}: {error}")
+ # Clean up potentially incomplete file
+ try:
+ if await aiofiles.os.path.exists(dest_path): await aiofiles.os.remove(dest_path)
+ except OSError: pass
+ if pbar: pbar.close() # Stop progress bar on error
+ raise # Re-throw to stop the process
+
+# Sync zip extraction (run in executor)
+def _extract_zip_sync(jar_path: pathlib.Path, extract_to_dir: pathlib.Path):
+ try:
+ with zipfile.ZipFile(jar_path, 'r') as zip_ref:
+ for member in zip_ref.infolist():
+ # Skip directories and META-INF
+ if not member.is_dir() and not member.filename.upper().startswith('META-INF/'):
+ try:
+ # Preserve directory structure within the zip
+ zip_ref.extract(member, extract_to_dir)
+ except Exception as extract_error:
+ log.warning(f"Warning: Could not extract {member.filename} from {jar_path.name}. Error: {extract_error}")
+ except zipfile.BadZipFile:
+ log.error(f"Failed to read zip file (BadZipFile): {jar_path}")
+ raise
+ except Exception as e:
+ log.error(f"Failed to process zip file {jar_path}: {e}")
+ raise
+
+async def extract_natives(jar_path: pathlib.Path, extract_to_dir: pathlib.Path):
+ """Extracts native libraries from a JAR file asynchronously."""
+ await aiofiles.os.makedirs(extract_to_dir, exist_ok=True)
+ loop = asyncio.get_running_loop()
+ # Run the synchronous extraction function in a thread pool executor
+ await loop.run_in_executor(None, _extract_zip_sync, jar_path, extract_to_dir)
+
+
+def get_os_name() -> str:
+ """Gets the current OS name ('windows', 'osx', 'linux')."""
+ system = platform.system()
+ if system == 'Windows': return 'windows'
+ elif system == 'Darwin': return 'osx'
+ elif system == 'Linux': return 'linux'
+ else: raise OSError(f"Unsupported platform: {system}")
+
+def get_arch_name() -> str:
+ """Gets the current architecture name ('x64', 'x86', 'arm64', 'arm32')."""
+ machine = platform.machine().lower()
+ if machine in ['amd64', 'x86_64']: return 'x64'
+ elif machine in ['i386', 'i686']: return 'x86'
+ elif machine in ['arm64', 'aarch64']: return 'arm64'
+ elif machine.startswith('arm') and '64' not in machine: return 'arm32'
+ else:
+ # Fallback or error for less common architectures
+ log.warning(f"Unsupported architecture: {platform.machine()}. Falling back to 'x64'. This might cause issues.")
+ return 'x64'
+
+
+# --- Rule Processing ---
+
+def check_rule(rule: Optional[Dict[str, Any]]) -> bool:
+ """
+ Checks if a *single rule* permits an item based on the current environment.
+ Returns True if the rule permits inclusion, False otherwise.
+ """
+ if not rule or 'action' not in rule:
+ return True # Default allow if no rule/action specified
+
+ action = rule.get('action', 'allow')
+ applies = True # Assume the condition matches unless proven otherwise
+
+ # Check OS condition
+ if 'os' in rule and isinstance(rule['os'], dict):
+ os_rule = rule['os']
+ current_os = get_os_name()
+ current_arch = get_arch_name()
+ if 'name' in os_rule and os_rule['name'] != current_os:
+ applies = False
+ if applies and 'arch' in os_rule and os_rule['arch'] != current_arch:
+ applies = False
+ # Version check omitted for simplicity
+
+ # Check features condition
+ if applies and 'features' in rule and isinstance(rule['features'], dict):
+ features_rule = rule['features']
+ # Get feature flags from config or use defaults
+ is_demo = cfg.get('demo', False)
+ has_custom_res = True # Assume true like JS example, maybe get from cfg?
+
+ # Check specific features mentioned in the rule.
+ # If *any* specified feature condition is NOT met, 'applies' becomes False.
+ if 'is_demo_user' in features_rule:
+ if features_rule['is_demo_user'] != is_demo:
+ applies = False
+ if applies and 'has_custom_resolution' in features_rule:
+ if features_rule['has_custom_resolution'] != has_custom_res:
+ applies = False
+ # Add other feature checks here based on rule.features keys similarly...
+
+ # Evaluate the rule's final outcome based on action and condition match
+ if action == 'allow':
+ # Allow action: Item permitted only if conditions apply
+ return applies
+ elif action == 'disallow':
+ # Disallow action: Item permitted only if conditions *do not* apply
+ return not applies
+ else:
+ log.warning(f"Unknown rule action: {action}. Defaulting to allow.")
+ return True
+
+def check_item_rules(rules: Optional[List[Dict[str, Any]]]) -> bool:
+ """
+ Checks if an item (library/argument) should be included based on its rules array.
+ Implements the logic: Disallow if *any* rule prevents inclusion.
+ """
+ if not rules:
+ return True # No rules, always include (default allow)
+
+ # Assume allowed unless a rule forbids it
+ allowed = True
+ for rule in rules:
+ # check_rule returns True if this specific rule *permits* inclusion
+ if not check_rule(rule):
+ # If check_rule is False, this rule prevents inclusion
+ allowed = False
+ # log.debug(f"Item disallowed by rule: {rule}") # Optional debug
+ break # No need to check further rules, it's disallowed
+
+ return allowed
+
+# --- End Rule Processing ---
+
+
+async def ensure_launcher_profiles(current_version_id: str):
+ """Ensures launcher_profiles.json exists and contains basic info."""
+ log.info('Checking launcher profiles...')
+ profile_name = f"custom-{current_version_id}"
+ # Generate profile key from UUID (consistent with vanilla launcher)
+ auth_profile_key = AUTH_UUID.replace('-', '')
+ account_key = f"account-{auth_profile_key}" # Used in newer profile formats
+
+ # Basic structure
+ profiles_data = {
+ "profiles": {
+ profile_name: {
+ # "created": datetime.now().isoformat(), # Optional
+ # "icon": "Furnace", # Optional
+ "lastUsed": "1970-01-01T00:00:00.000Z", # Needs update on launch
+ "lastVersionId": current_version_id,
+ "name": profile_name,
+ "type": "custom"
+ # "javaArgs": "-Xmx2G", # Optional
+ # "gameDir": str(MINECRAFT_DIR) # Optional
+ }
+ },
+ "authenticationDatabase": {
+ account_key: {
+ "accessToken": AUTH_ACCESS_TOKEN,
+ "profiles": {
+ auth_profile_key: {
+ "displayName": AUTH_PLAYER_NAME,
+ "playerUUID": AUTH_UUID,
+ "userId": AUTH_XUID,
+ # "texture": "..." # Optional base64 skin/cape
+ }
+ },
+ "username": AUTH_PLAYER_NAME, # Often email for MSA
+ "properties": [], # Optional user properties
+ # "remoteId": "...", # Xbox user ID
+ }
+ },
+ "settings": {
+ # "locale": "en-us",
+ # "showMenu": True,
+ # ... other launcher settings
+ },
+ "selectedUser": {
+ "account": account_key,
+ "profile": auth_profile_key
+ },
+ "version": 4 # Common version number for this format
+ }
+ try:
+ # Ensure directory exists first
+ await aiofiles.os.makedirs(LAUNCHER_PROFILES_PATH.parent, exist_ok=True)
+ # Write the file asynchronously
+ async with aiofiles.open(LAUNCHER_PROFILES_PATH, 'w', encoding='utf-8') as f:
+ await f.write(json.dumps(profiles_data, indent=2))
+ log.info(f"Created/updated {LAUNCHER_PROFILES_PATH}")
+ except Exception as error:
+ log.error(f"Failed to write {LAUNCHER_PROFILES_PATH}: {error}")
+ raise RuntimeError("Could not write launcher profiles file.")
+
+
+async def load_manifest(filename: str) -> Dict[str, Any]:
+ """Loads a JSON manifest file from the script's directory asynchronously."""
+ file_path = SCRIPT_DIR / filename
+ log.info(f"Loading manifest: {filename}")
+ try:
+ async with aiofiles.open(file_path, 'r', encoding='utf-8') as f:
+ content = await f.read()
+ return json.loads(content)
+ except FileNotFoundError:
+ log.error(f"Manifest file not found: {file_path}")
+ raise
+ except json.JSONDecodeError as e:
+ log.error(f"Failed to parse manifest {filename}: {e}")
+ raise ValueError(f"Invalid JSON in manifest: {filename}")
+ except Exception as e:
+ log.error(f"Failed to load manifest {filename}: {e}")
+ raise
+
+def merge_manifests(target_manifest: Dict[str, Any], base_manifest: Dict[str, Any]) -> Dict[str, Any]:
+ """Merges two version manifests (target inheriting from base)."""
+ target_id = target_manifest.get('id', 'unknown-target')
+ base_id = base_manifest.get('id', 'unknown-base')
+ log.info(f"Merging manifests: {target_id} inheriting from {base_id}")
+
+ # Combine libraries: Use a dictionary keyed by 'name' for overrides.
+ combined_libraries_map = {}
+ for lib in base_manifest.get('libraries', []):
+ if 'name' in lib: combined_libraries_map[lib['name']] = lib
+ for lib in target_manifest.get('libraries', []):
+ if 'name' in lib: combined_libraries_map[lib['name']] = lib # Overwrite
+
+ # Combine arguments: Append target args to base args.
+ base_args = base_manifest.get('arguments', {}) or {}
+ target_args = target_manifest.get('arguments', {}) or {}
+ combined_arguments = {
+ "game": (base_args.get('game', []) or []) + (target_args.get('game', []) or []),
+ "jvm": (base_args.get('jvm', []) or []) + (target_args.get('jvm', []) or [])
+ }
+
+ # Construct merged manifest, prioritizing target values.
+ merged = {
+ "id": target_manifest.get('id'),
+ "time": target_manifest.get('time'),
+ "releaseTime": target_manifest.get('releaseTime'),
+ "type": target_manifest.get('type'),
+ "mainClass": target_manifest.get('mainClass'), # Target overrides
+ "assetIndex": target_manifest.get('assetIndex', base_manifest.get('assetIndex')), # Prefer target
+ "assets": target_manifest.get('assets', base_manifest.get('assets')), # Asset ID string
+ "downloads": base_manifest.get('downloads'), # Use base downloads (client.jar etc.)
+ "javaVersion": target_manifest.get('javaVersion', base_manifest.get('javaVersion')), # Prefer target
+ "libraries": list(combined_libraries_map.values()), # Convert dict values back to list
+ "arguments": combined_arguments,
+ "logging": target_manifest.get('logging', base_manifest.get('logging')), # Prefer target
+ "complianceLevel": target_manifest.get('complianceLevel', base_manifest.get('complianceLevel')),
+ "minimumLauncherVersion": target_manifest.get('minimumLauncherVersion', base_manifest.get('minimumLauncherVersion'))
+ }
+ # Clean up None values from .get() fallbacks
+ return {k: v for k, v in merged.items() if v is not None}
+
+# Sync backup function (to be run in executor)
+def _create_backup_sync(source_dir: pathlib.Path, backup_dir_base: pathlib.Path):
+ """Copies source to backup dir, zips it, then removes backup dir."""
+ backup_dir = backup_dir_base # Directory to copy into first
+ backup_zip_path = backup_dir_base.with_suffix('.zip')
+
+ log.info(f"Starting backup copy from {source_dir} to {backup_dir}")
+ # Remove old backup directory/zip if they exist
+ if backup_zip_path.exists():
+ log.debug(f"Removing existing backup zip: {backup_zip_path}")
+ backup_zip_path.unlink()
+ if backup_dir.exists():
+ log.debug(f"Removing existing intermediate backup dir: {backup_dir}")
+ shutil.rmtree(backup_dir)
+
+ # Copy the entire directory
+ shutil.copytree(source_dir, backup_dir, dirs_exist_ok=True)
+ log.info(f"Backup copy complete. Zipping {backup_dir} to {backup_zip_path}")
+
+ # Zip the backup directory
+ with zipfile.ZipFile(backup_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
+ for root, _, files in os.walk(backup_dir):
+ for file in files:
+ file_path = pathlib.Path(root) / file
+ # Arcname is the path inside the zip file relative to backup_dir
+ arcname = file_path.relative_to(backup_dir)
+ zipf.write(file_path, arcname)
+ log.info(f"Backup zip created at {backup_zip_path}")
+
+ # Remove the temporary backup directory after zipping
+ shutil.rmtree(backup_dir)
+ log.info(f"Removed intermediate backup directory {backup_dir}")
+
+# --- Main Execution ---
+async def main():
+ try:
+ # 1. Load Target Manifest
+ target_manifest = await load_manifest(TARGET_VERSION_MANIFEST_FILENAME)
+ target_version_id = target_manifest.get('id')
+ if not target_version_id:
+ raise ValueError(f"Target manifest {TARGET_VERSION_MANIFEST_FILENAME} is missing required 'id' field.")
+
+ # 2. Load Base Manifest if needed and Merge
+ final_manifest = target_manifest
+ if 'inheritsFrom' in target_manifest:
+ base_version_id = target_manifest['inheritsFrom']
+ base_manifest_filename = f"{base_version_id}.json"
+ try:
+ base_manifest = await load_manifest(base_manifest_filename)
+ final_manifest = merge_manifests(target_manifest, base_manifest)
+ except (FileNotFoundError, ValueError, KeyError, Exception) as e:
+ log.error(f"Could not load or merge base manifest '{base_manifest_filename}' specified in {TARGET_VERSION_MANIFEST_FILENAME}: {e}")
+ sys.exit(1)
+ else:
+ log.info(f"Manifest {target_version_id} does not inherit from another version.")
+
+ # --- Use finalManifest for all subsequent steps ---
+ version_id = final_manifest.get('id') # Should be the target ID after merge
+ if not version_id:
+ raise ValueError("Final merged manifest is missing the 'id' field.")
+
+ version_dir = VERSIONS_DIR / version_id
+ natives_dir = version_dir / f"{version_id}-natives"
+
+ log.info(f"Preparing Minecraft {version_id}...")
+ os_name = get_os_name()
+ arch_name = get_arch_name()
+ log.info(f"Detected OS: {os_name}, Arch: {arch_name}")
+
+ # 4. Ensure Directories and Launcher Profiles
+ log.info(f"Ensuring base directory exists: {MINECRAFT_DIR}")
+ # Create all necessary directories asynchronously and concurrently
+ await asyncio.gather(
+ aiofiles.os.makedirs(MINECRAFT_DIR, exist_ok=True),
+ aiofiles.os.makedirs(VERSIONS_DIR, exist_ok=True),
+ aiofiles.os.makedirs(LIBRARIES_DIR, exist_ok=True),
+ aiofiles.os.makedirs(ASSETS_DIR, exist_ok=True),
+ aiofiles.os.makedirs(version_dir, exist_ok=True),
+ aiofiles.os.makedirs(natives_dir, exist_ok=True),
+ aiofiles.os.makedirs(ASSET_INDEXES_DIR, exist_ok=True),
+ aiofiles.os.makedirs(ASSET_OBJECTS_DIR, exist_ok=True)
+ )
+ # Create/update launcher profiles
+ await ensure_launcher_profiles(version_id)
+
+ # 5. Copy *Target* Version Manifest JSON to Version Directory
+ target_manifest_source_path = SCRIPT_DIR / TARGET_VERSION_MANIFEST_FILENAME
+ dest_manifest_path = version_dir / f"{version_id}.json"
+ try:
+ log.info(f"Copying {target_manifest_source_path.name} to {dest_manifest_path}")
+ # Run synchronous copy in executor to avoid blocking
+ await asyncio.get_running_loop().run_in_executor(
+ None, shutil.copyfile, target_manifest_source_path, dest_manifest_path
+ )
+ except Exception as error:
+ log.error(f"Failed to copy target version manifest: {error}")
+ raise RuntimeError(f"Could not copy version manifest file: {target_manifest_source_path}")
+
+ # 6. Download Client JAR
+ log.info('Checking client JAR...')
+ client_info = final_manifest.get('downloads', {}).get('client')
+ if not (client_info and 'url' in client_info and 'sha1' in client_info):
+ raise ValueError(f"Merged manifest for {version_id} is missing client download information (url, sha1).")
+ client_jar_path = version_dir / f"{version_id}.jar"
+ # Use tqdm context for the single download (async with not supported, manual create/close)
+ client_pbar = tqdm(total=1, desc="Client JAR", unit="file", leave=False)
+ try:
+ await download_file(client_info['url'], client_jar_path, client_info['sha1'], pbar=client_pbar)
+ finally:
+ client_pbar.close()
+
+
+ # 7. Prepare Library List
+ log.info('Processing library list...')
+ libraries_to_process = []
+ classpath_entries_set = {str(client_jar_path)} # Use a set to avoid duplicates
+ native_library_paths = []
+
+ for lib in final_manifest.get('libraries', []):
+ # Check rules for the entire library entry FIRST
+ if not check_item_rules(lib.get('rules')):
+ # log.debug(f"Skipping library due to overall rules: {lib.get('name', 'N/A')}")
+ continue
+
+ lib_name = lib.get('name', 'unknown-library')
+ downloads = lib.get('downloads', {})
+ artifact = downloads.get('artifact')
+ classifiers = downloads.get('classifiers', {})
+ natives_rules = lib.get('natives', {}) # Legacy natives mapping
+
+ # --- Determine Native Classifier ---
+ native_classifier_key = None
+ native_info = None
+ # Check 'natives' mapping first (less common now)
+ if os_name in natives_rules:
+ raw_classifier = natives_rules[os_name]
+ # Replace ${arch} - Python needs specific replacement logic
+ arch_replace = '64' if arch_name == 'x64' else ('32' if arch_name == 'x86' else arch_name)
+ potential_key = raw_classifier.replace('${arch}', arch_replace)
+ if potential_key in classifiers:
+ native_classifier_key = potential_key
+ native_info = classifiers[native_classifier_key]
+ # log.debug(f"Found native classifier via 'natives' rule: {native_classifier_key}")
+
+ # Check standard 'classifiers' if not found via 'natives'
+ if not native_info and classifiers:
+ # Construct potential keys based on current OS/Arch
+ potential_keys = [
+ f"natives-{os_name}-{arch_name}",
+ f"natives-{os_name}",
+ ]
+ for key in potential_keys:
+ if key in classifiers:
+ native_classifier_key = key
+ native_info = classifiers[key]
+ # log.debug(f"Found native classifier via standard key: {key}")
+ break
+
+ # --- Add Main Artifact ---
+ if artifact and artifact.get('path') and artifact.get('url'):
+ # Rules specific to the artifact itself are not standard in manifests.
+ # We rely on the top-level library rules check done earlier.
+ lib_path = LIBRARIES_DIR / artifact['path']
+ libraries_to_process.append({
+ "name": lib_name,
+ "url": artifact['url'],
+ "path": lib_path,
+ "sha1": artifact.get('sha1'),
+ "is_native": False,
+ })
+ classpath_entries_set.add(str(lib_path)) # Add non-native to classpath
+
+ # --- Add Native Artifact ---
+ if native_info and native_info.get('path') and native_info.get('url'):
+ # Again, rely on top-level library rules. Classifier-specific rules aren't standard.
+ native_path = LIBRARIES_DIR / native_info['path']
+ libraries_to_process.append({
+ "name": f"{lib_name}:{native_classifier_key}", # Include classifier in name for clarity
+ "url": native_info['url'],
+ "path": native_path,
+ "sha1": native_info.get('sha1'),
+ "is_native": True,
+ })
+ native_library_paths.append(native_path) # Keep track of native JARs for extraction
+
+ # Download Libraries (Corrected tqdm usage)
+ log.info(f"Downloading {len(libraries_to_process)} library files...")
+ lib_pbar = tqdm(total=len(libraries_to_process), desc="Libraries", unit="file", leave=False)
+ try:
+ download_tasks = [
+ download_file(lib_info['url'], lib_info['path'], lib_info['sha1'], pbar=lib_pbar)
+ for lib_info in libraries_to_process
+ ]
+ await asyncio.gather(*download_tasks)
+ finally:
+ lib_pbar.close()
+ log.info('Library download check complete.')
+
+ classpath_entries = list(classpath_entries_set) # Convert classpath set back to list
+
+ # 8. Extract Natives
+ log.info('Extracting native libraries...')
+ # Clear existing natives directory first
+ try:
+ if await aiofiles.os.path.isdir(natives_dir):
+ log.debug(f"Removing existing natives directory: {natives_dir}")
+ # Run synchronous rmtree in executor
+ await asyncio.get_running_loop().run_in_executor(None, shutil.rmtree, natives_dir)
+ await aiofiles.os.makedirs(natives_dir, exist_ok=True)
+ except Exception as err:
+ log.warning(f"Could not clear/recreate natives directory {natives_dir}: {err}. Extraction might fail or use old files.")
+
+ if native_library_paths:
+ # Corrected tqdm usage for natives
+ native_pbar = tqdm(total=len(native_library_paths), desc="Natives", unit="file", leave=False)
+ try:
+ extract_tasks = []
+ for native_jar_path in native_library_paths:
+ # Define task within loop to capture correct native_jar_path
+ async def extract_task(jar_path, pbar_instance):
+ try:
+ await extract_natives(jar_path, natives_dir)
+ except Exception as e:
+ log.error(f"\nFailed to extract natives from: {jar_path.name}: {e}")
+ # Decide if you want to raise or just log
+ # raise # Uncomment to stop on first extraction error
+ finally:
+ pbar_instance.update(1)
+
+ extract_tasks.append(extract_task(native_jar_path, native_pbar))
+ await asyncio.gather(*extract_tasks)
+ finally:
+ native_pbar.close() # Ensure pbar is closed
+ else:
+ log.info("No native libraries to extract for this platform.")
+ log.info('Native extraction complete.')
+
+
+ # 9. Download Assets
+ log.info('Checking assets...')
+ asset_index_info = final_manifest.get('assetIndex')
+ if not (asset_index_info and 'id' in asset_index_info and 'url' in asset_index_info and 'sha1' in asset_index_info):
+ raise ValueError(f"Merged manifest for {version_id} is missing asset index information (id, url, sha1).")
+
+ asset_index_id = asset_index_info['id']
+ asset_index_filename = f"{asset_index_id}.json"
+ asset_index_path = ASSET_INDEXES_DIR / asset_index_filename
+
+ # Download asset index (Corrected tqdm usage)
+ idx_pbar = tqdm(total=1, desc="Asset Index", unit="file", leave=False)
+ try:
+ await download_file(asset_index_info['url'], asset_index_path, asset_index_info['sha1'], pbar=idx_pbar)
+ finally:
+ idx_pbar.close()
+
+ # Load asset index content
+ try:
+ async with aiofiles.open(asset_index_path, 'r', encoding='utf-8') as f:
+ asset_index_content = json.loads(await f.read())
+ except Exception as e:
+ raise RuntimeError(f"Failed to read downloaded asset index {asset_index_path}: {e}")
+
+ asset_objects = asset_index_content.get('objects', {})
+ total_assets = len(asset_objects)
+ log.info(f"Checking {total_assets} asset files listed in index {asset_index_id}...")
+
+ # Download assets (Corrected tqdm usage)
+ asset_pbar = tqdm(total=total_assets, desc="Assets", unit="file", leave=False)
+ try:
+ asset_download_tasks = []
+ for asset_key, asset_details in asset_objects.items():
+ asset_hash = asset_details.get('hash')
+ if not asset_hash:
+ log.warning(f"Asset '{asset_key}' is missing hash in index, skipping.")
+ asset_pbar.update(1); continue # Still count it towards progress
+
+ hash_prefix = asset_hash[:2]
+ asset_subdir = ASSET_OBJECTS_DIR / hash_prefix
+ asset_filepath = asset_subdir / asset_hash
+ # Standard Minecraft asset download URL structure
+ asset_url = f"https://resources.download.minecraft.net/{hash_prefix}/{asset_hash}"
+ asset_download_tasks.append(
+ download_file(asset_url, asset_filepath, asset_hash, pbar=asset_pbar)
+ )
+ await asyncio.gather(*asset_download_tasks)
+ finally:
+ asset_pbar.close() # Ensure pbar is closed
+ log.info('Asset check complete.')
+
+ # 10. Download and Setup Java Runtime
+ java_version_info = final_manifest.get('javaVersion')
+ if not (java_version_info and 'majorVersion' in java_version_info):
+ log.warning("Manifest does not specify Java major version. Attempting default.")
+ required_java_major = DEFAULT_JAVA_VERSION # Use default from java.py
+ else:
+ required_java_major = java_version_info['majorVersion']
+
+ log.info(f"Checking/Installing Java {required_java_major}...")
+ java_executable = await download_java(
+ version=required_java_major,
+ destination_dir_str=str(JAVA_INSTALL_DIR) # Pass as string
+ # imageType='jre' # Optionally force JRE
+ )
+
+ if not java_executable:
+ log.error(f"Failed to obtain a suitable Java {required_java_major} executable.")
+ log.error(f"Ensure Java {required_java_major} is installed and accessible, or allow the script to download it to {JAVA_INSTALL_DIR}.")
+ sys.exit(1)
+ log.info(f"Using Java executable: {java_executable}")
+
+
+ # 11. Handle Client Storage (for NeoForge setup tracking)
+ log.info("Loading client storage...")
+ client_storage = {}
+ try:
+ if await aiofiles.os.path.exists(CLIENT_STORAGE_PATH):
+ async with aiofiles.open(CLIENT_STORAGE_PATH, 'r', encoding='utf-8') as f:
+ client_storage = json.loads(await f.read())
+ else:
+ # Initialize if file doesn't exist
+ client_storage = {"setupNeoForge": []} # Start with empty list
+ except json.JSONDecodeError as e:
+ log.warning(f"Failed to load or parse {CLIENT_STORAGE_PATH}: {e}. Reinitializing.")
+ client_storage = {"setupNeoForge": []} # Reinitialize on error
+ except Exception as e:
+ log.error(f"Error handling {CLIENT_STORAGE_PATH}: {e}. Reinitializing.")
+ client_storage = {"setupNeoForge": []}
+
+ # Ensure setupNeoForge exists and is a list (migration from old boolean)
+ if "setupNeoForge" not in client_storage or not isinstance(client_storage.get("setupNeoForge"), list):
+ client_storage["setupNeoForge"] = []
+
+
+ # 12. Run NeoForge Installer if necessary
+ needs_neoforge_setup = False
+ if version_id.startswith("neoforge-") and version_id not in client_storage.get("setupNeoForge", []):
+ needs_neoforge_setup = True
+ neoforge_installer_jar = SCRIPT_DIR / 'neoinstaller.jar'
+ if not await aiofiles.os.path.isfile(neoforge_installer_jar):
+ log.warning(f"NeoForge version detected ({version_id}), but neoinstaller.jar not found at {neoforge_installer_jar}. Skipping automatic setup.")
+ needs_neoforge_setup = False # Cannot perform setup
+
+ if needs_neoforge_setup:
+ log.info(f"Setting up NeoForge for {version_id}...")
+ # Command structure: java -jar neoinstaller.jar --install-client
+ setup_command_args = [
+ java_executable,
+ "-jar",
+ str(neoforge_installer_jar),
+ "--install-client",
+ str(MINECRAFT_DIR) # Pass the .minecraft dir path
+ ]
+
+ log.info(f"Running NeoForge setup command: {' '.join(setup_command_args)}")
+ # Run the installer process
+ process = await asyncio.create_subprocess_exec(
+ *setup_command_args,
+ stdout=asyncio.subprocess.PIPE,
+ stderr=asyncio.subprocess.PIPE,
+ cwd=str(SCRIPT_DIR) # Run from script dir to find installer jar easily?
+ )
+
+ stdout, stderr = await process.communicate()
+
+ if stdout: log.info("NeoForge Installer Output:\n" + stdout.decode(errors='ignore'))
+ if stderr: log.error("NeoForge Installer Errors:\n" + stderr.decode(errors='ignore'))
+
+ if process.returncode == 0:
+ log.info("NeoForge setup completed successfully.")
+ # Update client storage
+ client_storage["setupNeoForge"].append(version_id) # Assumes it's a list
+ try:
+ async with aiofiles.open(CLIENT_STORAGE_PATH, 'w', encoding='utf-8') as f:
+ await f.write(json.dumps(client_storage, indent=2))
+ except Exception as e:
+ log.error(f"Failed to update {CLIENT_STORAGE_PATH} after NeoForge setup: {e}")
+ else:
+ log.error(f"NeoForge setup failed with exit code {process.returncode}. Minecraft might not launch correctly.")
+ # Decide if you want to exit here or try launching anyway
+ # sys.exit(1)
+
+
+ # 13. Construct Launch Command
+ log.info('Constructing launch command...')
+ classpath_separator = os.pathsep # Use ';' for Windows, ':' for Linux/macOS
+ classpath_string = classpath_separator.join(classpath_entries)
+
+ # Argument Placeholder Replacements
+ replacements = {
+ '${natives_directory}': str(natives_dir),
+ '${library_directory}': str(LIBRARIES_DIR),
+ '${classpath_separator}': classpath_separator,
+ '${launcher_name}': 'CustomPythonLauncher',
+ '${launcher_version}': '1.0',
+ '${classpath}': classpath_string,
+ '${auth_player_name}': AUTH_PLAYER_NAME,
+ '${version_name}': version_id,
+ '${game_directory}': str(MINECRAFT_DIR),
+ '${assets_root}': str(ASSETS_DIR),
+ '${assets_index_name}': asset_index_id, # Use the ID from assetIndex
+ '${auth_uuid}': AUTH_UUID,
+ '${auth_access_token}': AUTH_ACCESS_TOKEN,
+ '${clientid}': 'N/A', # Placeholder
+ '${auth_xuid}': AUTH_XUID,
+ '${user_type}': USER_TYPE,
+ '${version_type}': final_manifest.get('type', 'release'), # Use manifest type
+ '${resolution_width}': cfg.get('resolution_width', '854'),
+ '${resolution_height}': cfg.get('resolution_height', '480'),
+ }
+
+ def replace_placeholders(arg_template: str) -> str:
+ """Replaces all placeholders in a single argument string."""
+ replaced_arg = arg_template
+ for key, value in replacements.items():
+ replaced_arg = replaced_arg.replace(key, value)
+ return replaced_arg
+
+ # --- Process JVM Arguments (Using corrected rule logic) ---
+ jvm_args = []
+ for arg_entry in final_manifest.get('arguments', {}).get('jvm', []):
+ arg_values_to_add = [] # List to hold processed args for this entry
+ rules = None
+ process_this_entry = True
+
+ if isinstance(arg_entry, str):
+ # Simple string argument, implicitly allowed (no rules)
+ arg_values_to_add.append(replace_placeholders(arg_entry))
+ elif isinstance(arg_entry, dict):
+ # Argument object with potential rules
+ rules = arg_entry.get('rules')
+ # Check rules BEFORE processing value
+ if not check_item_rules(rules):
+ # log.debug(f"Skipping JVM arg object due to rules: {arg_entry.get('value', '')}")
+ process_this_entry = False # Skip this whole dict entry
+ else:
+ # Rules allow, now process the value(s)
+ value_from_dict = arg_entry.get('value')
+ if isinstance(value_from_dict, list):
+ arg_values_to_add.extend(replace_placeholders(val) for val in value_from_dict)
+ elif isinstance(value_from_dict, str):
+ arg_values_to_add.append(replace_placeholders(value_from_dict))
+ else:
+ log.warning(f"Unsupported value type in JVM arg object: {value_from_dict}")
+ process_this_entry = False
+ else:
+ log.warning(f"Unsupported JVM argument format: {arg_entry}")
+ process_this_entry = False # Skip unknown format
+
+ # Add processed arguments if the entry was allowed and processed
+ if process_this_entry:
+ for arg in arg_values_to_add:
+ # Basic quoting for -D properties with spaces
+ if arg.startswith("-D") and "=" in arg:
+ key, value = arg.split("=", 1)
+ if " " in value and not (value.startswith('"') and value.endswith('"')):
+ arg = f'{key}="{value}"'
+ jvm_args.append(arg)
+
+ # --- Process Game Arguments (Using corrected rule logic) ---
+ game_args = []
+ for arg_entry in final_manifest.get('arguments', {}).get('game', []):
+ arg_values_to_add = []
+ rules = None
+ process_this_entry = True
+
+ if isinstance(arg_entry, str):
+ arg_values_to_add.append(replace_placeholders(arg_entry))
+ elif isinstance(arg_entry, dict):
+ rules = arg_entry.get('rules')
+ if not check_item_rules(rules):
+ # log.debug(f"Skipping game arg object due to rules: {arg_entry.get('value', '')}")
+ process_this_entry = False
+ else:
+ value_from_dict = arg_entry.get('value')
+ if isinstance(value_from_dict, list):
+ arg_values_to_add.extend(replace_placeholders(val) for val in value_from_dict)
+ elif isinstance(value_from_dict, str):
+ arg_values_to_add.append(replace_placeholders(value_from_dict))
+ else:
+ log.warning(f"Unsupported value type in game arg object: {value_from_dict}")
+ process_this_entry = False
+ else:
+ log.warning(f"Unsupported game argument format: {arg_entry}")
+ process_this_entry = False
+
+ if process_this_entry:
+ game_args.extend(arg_values_to_add)
+
+
+ # 14. Launch Minecraft
+ main_class = final_manifest.get('mainClass')
+ if not main_class:
+ raise ValueError("Final manifest is missing the 'mainClass' required for launch.")
+
+ final_launch_args = [
+ java_executable,
+ *jvm_args,
+ main_class,
+ *game_args,
+ ]
+
+ log.info("Attempting to launch Minecraft...")
+ # Optionally log the full command for debugging, but be careful with tokens
+ # log.debug(f"Launch command: {' '.join(final_launch_args)}")
+
+ # Run the Minecraft process
+ mc_process = await asyncio.create_subprocess_exec(
+ *final_launch_args,
+ stdout=sys.stdout, # Redirect child stdout to parent's stdout
+ stderr=sys.stderr, # Redirect child stderr to parent's stderr
+ cwd=MINECRAFT_DIR # Set the working directory to .minecraft
+ )
+
+ log.info(f"Minecraft process started (PID: {mc_process.pid}). Waiting for exit...")
+
+ # Wait for the process to complete
+ return_code = await mc_process.wait()
+
+ # 15. Post-Launch Actions (Backup)
+ log.info(f"Minecraft process exited with code {return_code}.")
+
+ # Perform backup if configured
+ if cfg.get("backup", False):
+ log.info("Backup requested. Creating backup...")
+ try:
+ loop = asyncio.get_running_loop()
+ # Run the synchronous backup function in the executor
+ await loop.run_in_executor(None, _create_backup_sync, MINECRAFT_DIR, BACKUP_PATH_BASE)
+ log.info("Backup process completed.")
+ except Exception as backup_error:
+ log.error(f"Failed to create backup: {backup_error}", exc_info=True)
+ else:
+ log.info("Backup disabled in config.")
+
+ except Exception as e:
+ log.exception("--- An error occurred during setup or launch ---")
+ # Optionally add more specific error handling or cleanup
+ sys.exit(1)
+ finally:
+ # Ensure the shared aiohttp session is closed on exit or error
+ await close_session()
+
+
+# --- Script Entry Point ---
+if __name__ == "__main__":
+ try:
+ asyncio.run(main())
+ except KeyboardInterrupt:
+ log.info("Launch cancelled by user.")
+ # Ensure session is closed if KeyboardInterrupt happens before finally block in main
+ # Running close_session within a new asyncio.run context
+ try:
+ asyncio.run(close_session())
+ except RuntimeError: # Can happen if loop is already closed
+ pass
+ # Note: SystemExit from sys.exit() will also terminate the script here
\ No newline at end of file
diff --git a/java.py b/java.py
new file mode 100644
index 0000000..21f9349
--- /dev/null
+++ b/java.py
@@ -0,0 +1,428 @@
+import os
+import platform
+import pathlib
+import hashlib
+import asyncio
+import logging
+import zipfile
+import tarfile
+import tempfile
+import secrets # For random hex bytes
+import aiohttp
+import aiofiles
+import aiofiles.os
+
+# Configure logging
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+log = logging.getLogger(__name__)
+
+# --- Configuration ---
+ADOPTIUM_API_BASE = 'https://api.adoptium.net/v3'
+DEFAULT_JAVA_VERSION = 17
+DEFAULT_IMAGE_TYPE = 'jdk'
+
+# --- Helper Functions ---
+
+def get_api_os_arch():
+ """Maps Python platform/machine to Adoptium API values."""
+ system = platform.system()
+ machine = platform.machine()
+
+ api_os = None
+ api_arch = None
+
+ if system == 'Windows':
+ api_os = 'windows'
+ elif system == 'Darwin':
+ api_os = 'mac'
+ elif system == 'Linux':
+ api_os = 'linux'
+ else:
+ log.error(f"Unsupported operating system: {system}")
+ return None
+
+ machine = machine.lower()
+ if machine in ['amd64', 'x86_64']:
+ api_arch = 'x64'
+ elif machine in ['arm64', 'aarch64']:
+ api_arch = 'aarch64'
+ # Add other mappings if needed (e.g., x86, arm32)
+ # elif machine in ['i386', 'i686']:
+ # api_arch = 'x86'
+ # elif machine.startswith('armv7'):
+ # api_arch = 'arm'
+ else:
+ log.error(f"Unsupported architecture: {machine}")
+ return None
+
+ return {"os": api_os, "arch": api_arch}
+
+
+# --- Corrected find_java_executable Function ---
+async def find_java_executable(extract_dir: pathlib.Path, system: str) -> pathlib.Path | None:
+ """
+ Finds the path to the Java executable within the specified directory.
+ Checks standard locations based on OS. Corrected scandir usage.
+ """
+ log.info(f"[find_java_executable] Searching in: {extract_dir}")
+ try:
+ # Use await for checking the main directory existence (could involve I/O)
+ if not await aiofiles.os.path.isdir(extract_dir):
+ log.warning(f"[find_java_executable] Provided path is not a directory: {extract_dir}")
+ return None
+
+ potential_sub_dir = None
+ log.debug(f"[find_java_executable] Scanning for subdirectory in {extract_dir}...")
+ try:
+ # --- CORRECTED USAGE: Use synchronous os.scandir ---
+ # No await, no async for. Standard for loop.
+ for entry in os.scandir(extract_dir):
+ log.debug(f"[find_java_executable] Found entry: {entry.path} (Name: {entry.name})")
+ try:
+ # entry.is_dir() is synchronous
+ is_dir = entry.is_dir()
+ log.debug(f"[find_java_executable] Is '{entry.name}' a directory? {is_dir}")
+ if is_dir:
+ potential_sub_dir = pathlib.Path(entry.path)
+ log.info(f"[find_java_executable] Found potential Java subdirectory: {potential_sub_dir}")
+ break # Assume first directory found is the right one
+ except OSError as scandir_entry_error:
+ log.warning(f"[find_java_executable] Could not check directory status for {entry.path}: {scandir_entry_error}")
+ continue
+ except OSError as e:
+ log.warning(f"[find_java_executable] Could not scan directory {extract_dir}: {e}")
+ # Continue trying base directory paths even if scan fails
+
+ # Determine which directory to check: the found subdir or the base extract dir
+ base_dir_to_check = potential_sub_dir if potential_sub_dir else extract_dir
+ log.info(f"[find_java_executable] Selected base directory for final check: {base_dir_to_check}")
+
+ java_executable_path = None
+ if system == 'Windows':
+ java_executable_path = base_dir_to_check / 'bin' / 'java.exe'
+ elif system == 'Darwin':
+ java_executable_path = base_dir_to_check / 'Contents' / 'Home' / 'bin' / 'java'
+ else: # Linux
+ java_executable_path = base_dir_to_check / 'bin' / 'java'
+
+ log.info(f"[find_java_executable] Constructed full path to check: {java_executable_path}")
+
+ # --- Check 1: File Existence (Use await for aiofiles.os.path) ---
+ try:
+ is_file = await aiofiles.os.path.isfile(java_executable_path)
+ log.info(f"[find_java_executable] Check 1: Does path exist as a file? {is_file}")
+ except Exception as e_isfile:
+ log.error(f"[find_java_executable] Error checking if path is file: {e_isfile}")
+ return None
+
+ if is_file:
+ # --- Check 2: Execute Permission (Use synchronous os.access) ---
+ try:
+ is_executable = os.access(java_executable_path, os.X_OK)
+ log.info(f"[find_java_executable] Check 2: Is file executable (os.X_OK)? {is_executable}")
+ if is_executable:
+ log.info(f"[find_java_executable] Success! Found accessible executable: {java_executable_path.resolve()}")
+ return java_executable_path.resolve()
+ else:
+ log.warning(f"[find_java_executable] File found but not executable: {java_executable_path}")
+ return None
+ except Exception as e_access:
+ log.error(f"[find_java_executable] Error checking execute permission with os.access: {e_access}")
+ return None
+ else:
+ log.warning(f"[find_java_executable] Executable path not found or is not a file.")
+ # Fallback logic (only runs if potential_sub_dir was found but failed the check above)
+ if potential_sub_dir and base_dir_to_check == potential_sub_dir:
+ log.info(f"[find_java_executable] Retrying search directly in base directory: {extract_dir}")
+ fallback_path = None
+ if system == 'Windows':
+ fallback_path = extract_dir / 'bin' / 'java.exe'
+ elif system == 'Darwin':
+ fallback_path = extract_dir / 'Contents' / 'Home' / 'bin' / 'java'
+ else:
+ fallback_path = extract_dir / 'bin' / 'java'
+
+ if fallback_path:
+ log.info(f"[find_java_executable] Checking fallback path: {fallback_path}")
+ try:
+ # Use await for async file check
+ fb_is_file = await aiofiles.os.path.isfile(fallback_path)
+ log.info(f"[find_java_executable] Fallback Check 1: Exists as file? {fb_is_file}")
+ if fb_is_file:
+ # Use sync permission check
+ fb_is_executable = os.access(fallback_path, os.X_OK)
+ log.info(f"[find_java_executable] Fallback Check 2: Is executable? {fb_is_executable}")
+ if fb_is_executable:
+ log.info(f"[find_java_executable] Success on fallback! Found: {fallback_path.resolve()}")
+ return fallback_path.resolve()
+ else:
+ log.warning(f"[find_java_executable] Fallback file found but not executable.")
+ else:
+ log.warning(f"[find_java_executable] Fallback path not found or not a file.")
+ except Exception as e_fb:
+ log.error(f"[find_java_executable] Error during fallback check: {e_fb}")
+
+ log.warning(f"[find_java_executable] Could not find executable via primary or fallback paths.")
+ return None
+
+ except Exception as e:
+ log.exception(f"[find_java_executable] Unexpected error searching in {extract_dir}: {e}") # Use log.exception to get traceback
+ return None
+
+
+# Function to run synchronous extraction in a separate thread
+def _extract_zip(zip_data: bytes, dest_path: pathlib.Path):
+ import io
+ with io.BytesIO(zip_data) as zip_buffer:
+ with zipfile.ZipFile(zip_buffer, 'r') as zip_ref:
+ zip_ref.extractall(dest_path)
+
+def _extract_tar(tar_path: pathlib.Path, dest_path: pathlib.Path):
+ # Check if the tar file exists before trying to open it
+ if not tar_path.is_file():
+ log.error(f"Tar file not found for extraction: {tar_path}")
+ raise FileNotFoundError(f"Tar file not found: {tar_path}")
+ try:
+ with tarfile.open(tar_path, "r:gz") as tar_ref:
+ # tarfile doesn't have a built-in strip_components like the command line
+ # We need to manually filter members or extract carefully
+ # For simplicity here, we assume strip=1 behaviour is desired and
+ # hope the find_java_executable handles the structure.
+ # A more robust solution would iterate members and adjust paths.
+ tar_ref.extractall(path=dest_path) # This might create a top-level dir
+ except tarfile.ReadError as e:
+ log.error(f"Error reading tar file {tar_path}: {e}")
+ raise
+ except Exception as e:
+ log.error(f"Unexpected error during tar extraction from {tar_path}: {e}")
+ raise
+
+# --- Main Exported Function ---
+async def download_java(
+ version: int = DEFAULT_JAVA_VERSION,
+ destination_dir_str: str | None = None,
+ image_type: str = DEFAULT_IMAGE_TYPE,
+ vendor: str = 'eclipse',
+ jvm_impl: str = 'hotspot',
+) -> str | None:
+ """
+ Downloads and extracts a standalone Java runtime/JDK if not already present.
+
+ Args:
+ version: The major Java version (e.g., 11, 17, 21).
+ destination_dir_str: Directory for Java. If None, a temporary dir is used.
+ **Crucially, if this directory already contains a valid executable, download will be skipped.**
+ image_type: Type of Java package ('jdk' or 'jre').
+ vendor: The build vendor (usually 'eclipse' for Temurin).
+ jvm_impl: The JVM implementation.
+
+ Returns:
+ The absolute path to the Java executable as a string if successful, otherwise None.
+ """
+
+ if destination_dir_str is None:
+ # Use mkdtemp for a secure temporary directory if none provided
+ # Note: This temporary directory won't persist across runs.
+ # A fixed path is usually better for caching.
+ # Running sync mkdtemp in executor to avoid blocking
+ loop = asyncio.get_running_loop()
+ temp_dir_str = await loop.run_in_executor(None, tempfile.mkdtemp, f"downloaded-java-{secrets.token_hex(4)}-")
+ destination_dir = pathlib.Path(temp_dir_str)
+ log.info(f"No destination directory provided, using temporary directory: {destination_dir}")
+ else:
+ destination_dir = pathlib.Path(destination_dir_str).resolve()
+
+
+ platform_info = get_api_os_arch()
+ if not platform_info:
+ return None
+ api_os = platform_info["os"]
+ api_arch = platform_info["arch"]
+ current_system = platform.system()
+
+ # --- Check if Java executable already exists ---
+ log.info(f"Checking for existing Java executable in: {destination_dir}")
+ try:
+ existing_java_path = await find_java_executable(destination_dir, current_system)
+ if existing_java_path:
+ log.info(f"Valid Java executable already found at: {existing_java_path}. Skipping download.")
+ return str(existing_java_path)
+ else:
+ log.info(f"Existing Java executable not found or installation is incomplete in {destination_dir}.")
+ except Exception as check_error:
+ # Log the exception details if the check itself fails
+ log.exception(f"Error during pre-check for existing Java in {destination_dir}: {check_error}. Assuming download is needed.")
+ # --- End Check ---
+
+ log.info('Proceeding with Java download and extraction process...')
+ api_url = f"{ADOPTIUM_API_BASE}/binary/latest/{version}/ga/{api_os}/{api_arch}/{image_type}/{jvm_impl}/normal/{vendor}"
+ log.info(f"Attempting to download Java {version} ({image_type}) for {api_os}-{api_arch} from Adoptium API.")
+
+ download_url = None
+ archive_type = None
+
+ async with aiohttp.ClientSession() as session:
+ try:
+ log.info(f"Fetching download details (HEAD request) from: {api_url}")
+ # Use allow_redirects=True and get the final URL
+ async with session.head(api_url, allow_redirects=True) as head_response:
+ head_response.raise_for_status() # Raise exception for bad status codes (4xx, 5xx)
+ download_url = str(head_response.url) # Get the final URL after redirects
+
+ if not download_url:
+ raise ValueError("Could not resolve download URL after redirects.")
+
+ if download_url.endswith('.zip'):
+ archive_type = 'zip'
+ elif download_url.endswith('.tar.gz'):
+ archive_type = 'tar.gz'
+ else:
+ # Guess based on OS if extension is missing (less reliable)
+ archive_type = 'zip' if api_os == 'windows' else 'tar.gz'
+
+ log.info(f"Resolved download URL: {download_url}")
+ log.info(f"Detected archive type: {archive_type}")
+
+ # Ensure destination directory exists
+ # Use await aiofiles.os.makedirs
+ await aiofiles.os.makedirs(destination_dir, exist_ok=True)
+ log.info(f"Ensured destination directory exists: {destination_dir}")
+
+ log.info('Starting download...')
+ async with session.get(download_url) as response:
+ response.raise_for_status()
+ file_data = await response.read()
+ log.info('Download complete.')
+
+ log.info(f"Extracting {archive_type} archive to {destination_dir}...")
+ loop = asyncio.get_running_loop()
+
+ if archive_type == 'zip':
+ # Run synchronous zip extraction in a thread
+ await loop.run_in_executor(None, _extract_zip, file_data, destination_dir)
+ else: # tar.gz
+ # Write to a temporary file first for tarfile
+ temp_tar_path = None
+ # Use a context manager for the temporary file
+ # Running sync tempfile operations in executor
+ fd, temp_tar_path_str = await loop.run_in_executor(None, tempfile.mkstemp, ".tar.gz", "java-dl-")
+ os.close(fd) # Close descriptor from mkstemp
+ temp_tar_path = pathlib.Path(temp_tar_path_str)
+
+ try:
+ log.debug(f"Saving temporary tar archive to: {temp_tar_path}")
+ async with aiofiles.open(temp_tar_path, 'wb') as f:
+ await f.write(file_data)
+ log.debug(f"Temporary archive saved successfully.")
+
+ # Run synchronous tar extraction in a thread
+ log.debug(f"Starting tar extraction from {temp_tar_path}...")
+ await loop.run_in_executor(None, _extract_tar, temp_tar_path, destination_dir)
+ log.debug('Extraction using tar complete.')
+
+ finally:
+ # Clean up temporary tar file using await aiofiles.os.remove
+ if temp_tar_path and await aiofiles.os.path.exists(temp_tar_path):
+ try:
+ await aiofiles.os.remove(temp_tar_path)
+ log.debug(f"Temporary file {temp_tar_path} deleted.")
+ except OSError as e:
+ log.warning(f"Could not delete temporary tar file {temp_tar_path}: {e}")
+ elif temp_tar_path:
+ log.debug(f"Temporary file {temp_tar_path} did not exist for deletion.")
+
+ log.info('Extraction complete.')
+
+ # --- Find executable AFTER extraction ---
+ # Add a small delay, just in case of filesystem flush issues (optional)
+ # await asyncio.sleep(0.5)
+ log.info("Re-checking for Java executable after extraction...")
+ java_path = await find_java_executable(destination_dir, current_system)
+
+ if java_path:
+ log.info(f"Java executable successfully found after extraction at: {java_path}")
+ return str(java_path)
+ else:
+ log.error('Extraction seemed successful, but failed to find Java executable at the expected location afterwards.')
+ log.error(f"Please double-check the contents of {destination_dir} and the logic in find_java_executable for platform {current_system}.")
+ # Log directory contents for debugging
+ try:
+ log.error(f"Contents of {destination_dir}: {os.listdir(destination_dir)}")
+ # Check potential subdir contents too
+ for item in os.listdir(destination_dir):
+ item_path = destination_dir / item
+ if item_path.is_dir():
+ log.error(f"Contents of {item_path}: {os.listdir(item_path)}")
+ bin_path = item_path / 'bin'
+ if bin_path.is_dir():
+ log.error(f"Contents of {bin_path}: {os.listdir(bin_path)}")
+ break # Show first subdir found
+ except Exception as list_err:
+ log.error(f"Could not list directory contents for debugging: {list_err}")
+ return None
+
+ except aiohttp.ClientResponseError as e:
+ log.error(f"HTTP Error downloading Java: {e.status} {e.message}")
+ if e.status == 404:
+ log.error(f"Could not find a build for Java {version} ({image_type}) for {api_os}-{api_arch}. Check Adoptium website for availability.")
+ else:
+ log.error(f"Response Headers: {e.headers}")
+ # Try reading response body if available (might be large)
+ try:
+ error_body = await e.response.text()
+ log.error(f"Response Body (partial): {error_body[:500]}")
+ except Exception:
+ pass # Ignore if body can't be read
+
+ log.error(f"Java download/extraction failed. Directory {destination_dir} may be incomplete.")
+ return None
+ except Exception as error:
+ log.exception(f"An unexpected error occurred during download/extraction: {error}")
+ log.error(f"Java download/extraction failed. Directory {destination_dir} may be incomplete.")
+ return None
+
+# Example usage (optional, can be run with `python java.py`)
+if __name__ == "__main__":
+ async def run_test():
+ print("Testing Java Downloader...")
+ # Define a test destination directory
+ test_dest = pathlib.Path("./test-java-runtime").resolve()
+ print(f"Will attempt to download Java to: {test_dest}")
+
+ # Clean up previous test run if exists
+ if test_dest.exists():
+ import shutil
+ print("Removing previous test directory...")
+ shutil.rmtree(test_dest)
+
+ java_exe_path = await download_java(
+ version=21, # Specify version
+ destination_dir_str=str(test_dest),
+ image_type='jdk'
+ )
+
+ if java_exe_path:
+ print(f"\nSuccess! Java executable path: {java_exe_path}")
+ # Try running java -version
+ try:
+ print("\nRunning java -version:")
+ proc = await asyncio.create_subprocess_exec(
+ java_exe_path,
+ "-version",
+ stdout=asyncio.subprocess.PIPE,
+ stderr=asyncio.subprocess.PIPE
+ )
+ stdout, stderr = await proc.communicate()
+ # java -version often prints to stderr
+ print("Exit Code:", proc.returncode)
+ if stderr:
+ print("Output (stderr):\n", stderr.decode())
+ if stdout: # Just in case it prints to stdout
+ print("Output (stdout):\n", stdout.decode())
+
+ except Exception as e:
+ print(f"Error running java -version: {e}")
+ else:
+ print("\nFailed to download or find Java executable.")
+
+ asyncio.run(run_test())
\ No newline at end of file
diff --git a/replacer.py b/replacer.py
new file mode 100644
index 0000000..16a53f0
--- /dev/null
+++ b/replacer.py
@@ -0,0 +1,68 @@
+import logging
+
+# Configure logging (optional, but good practice)
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+log = logging.getLogger(__name__)
+
+def replace_text(value: str, replacements: dict) -> str:
+ """
+ Replaces all occurrences of specified substrings within a string.
+ Does not use regular expressions.
+
+ Args:
+ value: The original string to perform replacements on.
+ replacements: A dictionary where keys are the substrings
+ to find and values are the strings to
+ replace them with.
+
+ Returns:
+ The string with all specified replacements made.
+ Returns the original value if it's not a string
+ or if replacements is not a valid dictionary.
+ """
+ if not isinstance(value, str):
+ log.warning("replace_text: Input 'value' is not a string. Returning original value.")
+ return value
+
+ if not isinstance(replacements, dict):
+ log.warning("replace_text: Input 'replacements' is not a valid dictionary. Returning original value.")
+ return value
+
+ modified_value = value
+
+ # Iterate through each key-value pair in the replacements dictionary
+ for search_string, replace_string in replacements.items():
+ # Ensure both search and replace values are strings for safety
+ if isinstance(search_string, str) and isinstance(replace_string, str):
+ # Use str.replace() to replace all occurrences
+ modified_value = modified_value.replace(search_string, replace_string)
+ else:
+ log.warning(f"replace_text: Skipping replacement for key '{search_string}' as either key or value is not a string.")
+
+ return modified_value
+
+# Example Usage (matches the JS comment example)
+# if __name__ == "__main__":
+# import os
+# __dirname = os.path.abspath('.') # Simulate __dirname for example
+
+# original_config = {
+# "executable": ":thisdir:/bin/launcher",
+# "configFile": "/etc/config.conf",
+# "logPath": ":thisdir:/logs/app.log",
+# "tempDir": "/tmp",
+# "description": "Uses :thisdir: multiple :thisdir: times."
+# }
+
+# replacements = {":thisdir:": __dirname}
+
+# patched_config = {}
+# for key, value in original_config.items():
+# patched_config[key] = replace_text(value, replacements) # Note: Not async in Python
+
+# print("Original Config:", original_config)
+# print("Patched Config:", patched_config)
+
+# # Expected Output (assuming __dirname = '/path/to/current/directory'):
+# # Original Config: {'executable': ':thisdir:/bin/launcher', 'configFile': '/etc/config.conf', 'logPath': ':thisdir:/logs/app.log', 'tempDir': '/tmp', 'description': 'Uses :thisdir: multiple :thisdir: times.'}
+# # Patched Config: {'executable': '/path/to/current/directory/bin/launcher', 'configFile': '/etc/config.conf', 'logPath': '/path/to/current/directory/logs/app.log', 'tempDir': '/tmp', 'description': 'Uses /path/to/current/directory multiple /path/to/current/directory times.'}
\ No newline at end of file
From 3e45d4685e97eb8b95f731b6f323e9b593eb3817 Mon Sep 17 00:00:00 2001
From: StoppedwummSites <150438484+StoppedwummSites@users.noreply.github.com>
Date: Tue, 6 May 2025 12:08:02 +0200
Subject: [PATCH 03/19] Remove python port
---
__main__.py | 1002 ---------------------------------------------------
java.py | 428 ----------------------
replacer.py | 68 ----
3 files changed, 1498 deletions(-)
delete mode 100644 __main__.py
delete mode 100644 java.py
delete mode 100644 replacer.py
diff --git a/__main__.py b/__main__.py
deleted file mode 100644
index f8fc65b..0000000
--- a/__main__.py
+++ /dev/null
@@ -1,1002 +0,0 @@
-# main.py
-import os
-import platform
-import pathlib
-import json
-import hashlib
-import asyncio
-import logging
-import sys
-import zipfile
-import shutil # For copytree and rmtree
-from typing import Dict, Any, List, Optional, Union
-
-import aiohttp
-import aiofiles
-import aiofiles.os
-from tqdm.asyncio import tqdm # Use tqdm's async version
-
-# --- Local Imports ---
-try:
- # Assuming java.py and replacer.py are in the same directory
- from java import download_java
- from replacer import replace_text
-except ImportError as e:
- print(f"Error importing local modules (java.py, replacer.py): {e}")
- print("Please ensure these files are in the same directory as main.py.")
- sys.exit(1)
-
-# --- Logging Setup ---
-logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
-log = logging.getLogger(__name__)
-
-# --- Constants and Configuration ---
-DEFAULT_VERSION_MANIFEST = 'neoforge-21.1.162.json' # Default if not in launcher_config
-SCRIPT_DIR = pathlib.Path(__file__).parent.resolve()
-
-# Load launcher_config.json and patch paths
-try:
- with open(SCRIPT_DIR / "launcher_config.json", 'r') as f:
- launcher_config_raw = json.load(f)
-except FileNotFoundError:
- log.error("launcher_config.json not found in the script directory.")
- sys.exit(1)
-except json.JSONDecodeError as e:
- log.error(f"Error parsing launcher_config.json: {e}")
- sys.exit(1)
-
-# Patch launcher config immediately (replace_text is synchronous)
-launcher_config = {}
-for key, value in launcher_config_raw.items():
- launcher_config[key] = replace_text(value, {':thisdir:': str(SCRIPT_DIR)})
-
-log.info(f"Launcher config: {json.dumps(launcher_config, indent=2)}")
-
-# Main target version manifest filename from launcher_config or default
-TARGET_VERSION_MANIFEST_FILENAME = launcher_config.get('version', DEFAULT_VERSION_MANIFEST)
-
-# Load user config (config.json) if it exists
-cfg = {}
-try:
- config_path = SCRIPT_DIR / 'config.json'
- if config_path.exists():
- # Reading small config at start is often acceptable synchronously.
- with open(config_path, 'r', encoding='utf-8') as f:
- cfg = json.load(f)
-except json.JSONDecodeError as e:
- log.warning(f"Could not parse config.json: {e}. Using defaults.")
-except Exception as e:
- log.warning(f"Could not read config.json: {e}. Using defaults.")
-
-# --- Authentication Details ---
-AUTH_PLAYER_NAME = cfg.get("auth_player_name") if cfg.get("auth_player_name") else 'Player'
-AUTH_UUID = cfg.get("auth_uuid") if cfg.get("auth_uuid") else '00000000-0000-0000-0000-000000000000'
-AUTH_ACCESS_TOKEN = cfg.get("auth_access_token") if cfg.get("auth_access_token") else '00000000000000000000000000000000'
-AUTH_XUID = cfg.get("auth_xuid") if cfg.get("auth_xuid") else '0'
-USER_TYPE = 'msa'
-
-# --- Directories ---
-BASE_PATH = pathlib.Path(launcher_config.get('basepath', SCRIPT_DIR / '.mc_launcher_data'))
-MINECRAFT_DIR = BASE_PATH / launcher_config.get('path', '.minecraft')
-VERSIONS_DIR = MINECRAFT_DIR / 'versions'
-LIBRARIES_DIR = MINECRAFT_DIR / 'libraries'
-ASSETS_DIR = MINECRAFT_DIR / 'assets'
-ASSET_INDEXES_DIR = ASSETS_DIR / 'indexes'
-ASSET_OBJECTS_DIR = ASSETS_DIR / 'objects'
-LAUNCHER_PROFILES_PATH = MINECRAFT_DIR / 'launcher_profiles.json'
-CLIENT_STORAGE_PATH = MINECRAFT_DIR / 'client_storage.json'
-BACKUP_PATH_BASE = SCRIPT_DIR / '.minecraft_autobackup'
-JAVA_INSTALL_DIR = SCRIPT_DIR / 'java-runtime'
-
-# --- Helper Functions ---
-
-async def get_file_sha1(file_path: pathlib.Path) -> str:
- """Calculates the SHA1 hash of a file asynchronously."""
- sha1_hash = hashlib.sha1()
- try:
- async with aiofiles.open(file_path, 'rb') as f:
- while True:
- chunk = await f.read(8192)
- if not chunk:
- break
- sha1_hash.update(chunk)
- return sha1_hash.hexdigest()
- except FileNotFoundError:
- raise FileNotFoundError(f"File not found for SHA1 calculation: {file_path}")
- except Exception as e:
- raise RuntimeError(f"Error calculating SHA1 for {file_path}: {e}")
-
-async def file_exists(file_path: pathlib.Path) -> bool:
- """Checks if a file exists asynchronously."""
- try:
- stats = await aiofiles.os.stat(file_path)
- # Check if it's a regular file using stat results
- return stats.st_mode & 0o100000 != 0
- except OSError: # Includes FileNotFoundError
- return False
-
-# Shared aiohttp session
-AIOHTTP_SESSION = None
-
-async def get_session():
- global AIOHTTP_SESSION
- if AIOHTTP_SESSION is None or AIOHTTP_SESSION.closed:
- AIOHTTP_SESSION = aiohttp.ClientSession()
- return AIOHTTP_SESSION
-
-async def close_session():
- global AIOHTTP_SESSION
- if AIOHTTP_SESSION and not AIOHTTP_SESSION.closed:
- await AIOHTTP_SESSION.close()
- AIOHTTP_SESSION = None
-
-async def download_file(
- url: str,
- dest_path: pathlib.Path,
- expected_sha1: Optional[str],
- force_download: bool = False,
- pbar: Optional[tqdm] = None # Progress bar instance passed in
-) -> bool:
- """Downloads a file asynchronously, verifies SHA1 hash."""
- dir_path = dest_path.parent
- await aiofiles.os.makedirs(dir_path, exist_ok=True)
-
- needs_download = force_download
- exists = await file_exists(dest_path)
-
- if exists and not force_download:
- if expected_sha1:
- try:
- current_sha1 = await get_file_sha1(dest_path)
- if current_sha1.lower() == expected_sha1.lower():
- if pbar: pbar.update(1) # Update progress even if skipped
- return False # No download needed
- else:
- log.warning(f"SHA1 mismatch for existing file {dest_path.name}. Expected {expected_sha1}, got {current_sha1}. Redownloading.")
- needs_download = True
- except Exception as hash_error:
- log.warning(f"Warning: Could not hash existing file {dest_path}. Redownloading. Error: {hash_error}")
- needs_download = True
- else:
- if pbar: pbar.update(1)
- return False # File exists, no hash check, no download needed
- elif not exists:
- needs_download = True
-
- if not needs_download:
- if pbar: pbar.update(1) # Ensure progress updates if no download needed
- return False
-
- session = await get_session()
- try:
- async with session.get(url) as response:
- if not response.ok:
- # Clean up potentially corrupted file before throwing
- try:
- if await aiofiles.os.path.exists(dest_path): await aiofiles.os.remove(dest_path)
- except OSError: pass # Ignore deletion errors
- raise aiohttp.ClientResponseError(
- response.request_info, response.history,
- status=response.status, message=f"Failed to download {url}: {response.reason}", headers=response.headers
- )
- # Stream download to file
- async with aiofiles.open(dest_path, 'wb') as f:
- async for chunk in response.content.iter_chunked(8192):
- await f.write(chunk)
-
- if expected_sha1:
- downloaded_sha1 = await get_file_sha1(dest_path)
- if downloaded_sha1.lower() != expected_sha1.lower():
- raise ValueError(f"SHA1 mismatch for {dest_path.name}. Expected {expected_sha1}, got {downloaded_sha1}")
-
- if pbar: pbar.update(1) # Update progress after successful download
- return True # Download occurred
-
- except Exception as error:
- log.error(f"Error downloading {url}: {error}")
- # Clean up potentially incomplete file
- try:
- if await aiofiles.os.path.exists(dest_path): await aiofiles.os.remove(dest_path)
- except OSError: pass
- if pbar: pbar.close() # Stop progress bar on error
- raise # Re-throw to stop the process
-
-# Sync zip extraction (run in executor)
-def _extract_zip_sync(jar_path: pathlib.Path, extract_to_dir: pathlib.Path):
- try:
- with zipfile.ZipFile(jar_path, 'r') as zip_ref:
- for member in zip_ref.infolist():
- # Skip directories and META-INF
- if not member.is_dir() and not member.filename.upper().startswith('META-INF/'):
- try:
- # Preserve directory structure within the zip
- zip_ref.extract(member, extract_to_dir)
- except Exception as extract_error:
- log.warning(f"Warning: Could not extract {member.filename} from {jar_path.name}. Error: {extract_error}")
- except zipfile.BadZipFile:
- log.error(f"Failed to read zip file (BadZipFile): {jar_path}")
- raise
- except Exception as e:
- log.error(f"Failed to process zip file {jar_path}: {e}")
- raise
-
-async def extract_natives(jar_path: pathlib.Path, extract_to_dir: pathlib.Path):
- """Extracts native libraries from a JAR file asynchronously."""
- await aiofiles.os.makedirs(extract_to_dir, exist_ok=True)
- loop = asyncio.get_running_loop()
- # Run the synchronous extraction function in a thread pool executor
- await loop.run_in_executor(None, _extract_zip_sync, jar_path, extract_to_dir)
-
-
-def get_os_name() -> str:
- """Gets the current OS name ('windows', 'osx', 'linux')."""
- system = platform.system()
- if system == 'Windows': return 'windows'
- elif system == 'Darwin': return 'osx'
- elif system == 'Linux': return 'linux'
- else: raise OSError(f"Unsupported platform: {system}")
-
-def get_arch_name() -> str:
- """Gets the current architecture name ('x64', 'x86', 'arm64', 'arm32')."""
- machine = platform.machine().lower()
- if machine in ['amd64', 'x86_64']: return 'x64'
- elif machine in ['i386', 'i686']: return 'x86'
- elif machine in ['arm64', 'aarch64']: return 'arm64'
- elif machine.startswith('arm') and '64' not in machine: return 'arm32'
- else:
- # Fallback or error for less common architectures
- log.warning(f"Unsupported architecture: {platform.machine()}. Falling back to 'x64'. This might cause issues.")
- return 'x64'
-
-
-# --- Rule Processing ---
-
-def check_rule(rule: Optional[Dict[str, Any]]) -> bool:
- """
- Checks if a *single rule* permits an item based on the current environment.
- Returns True if the rule permits inclusion, False otherwise.
- """
- if not rule or 'action' not in rule:
- return True # Default allow if no rule/action specified
-
- action = rule.get('action', 'allow')
- applies = True # Assume the condition matches unless proven otherwise
-
- # Check OS condition
- if 'os' in rule and isinstance(rule['os'], dict):
- os_rule = rule['os']
- current_os = get_os_name()
- current_arch = get_arch_name()
- if 'name' in os_rule and os_rule['name'] != current_os:
- applies = False
- if applies and 'arch' in os_rule and os_rule['arch'] != current_arch:
- applies = False
- # Version check omitted for simplicity
-
- # Check features condition
- if applies and 'features' in rule and isinstance(rule['features'], dict):
- features_rule = rule['features']
- # Get feature flags from config or use defaults
- is_demo = cfg.get('demo', False)
- has_custom_res = True # Assume true like JS example, maybe get from cfg?
-
- # Check specific features mentioned in the rule.
- # If *any* specified feature condition is NOT met, 'applies' becomes False.
- if 'is_demo_user' in features_rule:
- if features_rule['is_demo_user'] != is_demo:
- applies = False
- if applies and 'has_custom_resolution' in features_rule:
- if features_rule['has_custom_resolution'] != has_custom_res:
- applies = False
- # Add other feature checks here based on rule.features keys similarly...
-
- # Evaluate the rule's final outcome based on action and condition match
- if action == 'allow':
- # Allow action: Item permitted only if conditions apply
- return applies
- elif action == 'disallow':
- # Disallow action: Item permitted only if conditions *do not* apply
- return not applies
- else:
- log.warning(f"Unknown rule action: {action}. Defaulting to allow.")
- return True
-
-def check_item_rules(rules: Optional[List[Dict[str, Any]]]) -> bool:
- """
- Checks if an item (library/argument) should be included based on its rules array.
- Implements the logic: Disallow if *any* rule prevents inclusion.
- """
- if not rules:
- return True # No rules, always include (default allow)
-
- # Assume allowed unless a rule forbids it
- allowed = True
- for rule in rules:
- # check_rule returns True if this specific rule *permits* inclusion
- if not check_rule(rule):
- # If check_rule is False, this rule prevents inclusion
- allowed = False
- # log.debug(f"Item disallowed by rule: {rule}") # Optional debug
- break # No need to check further rules, it's disallowed
-
- return allowed
-
-# --- End Rule Processing ---
-
-
-async def ensure_launcher_profiles(current_version_id: str):
- """Ensures launcher_profiles.json exists and contains basic info."""
- log.info('Checking launcher profiles...')
- profile_name = f"custom-{current_version_id}"
- # Generate profile key from UUID (consistent with vanilla launcher)
- auth_profile_key = AUTH_UUID.replace('-', '')
- account_key = f"account-{auth_profile_key}" # Used in newer profile formats
-
- # Basic structure
- profiles_data = {
- "profiles": {
- profile_name: {
- # "created": datetime.now().isoformat(), # Optional
- # "icon": "Furnace", # Optional
- "lastUsed": "1970-01-01T00:00:00.000Z", # Needs update on launch
- "lastVersionId": current_version_id,
- "name": profile_name,
- "type": "custom"
- # "javaArgs": "-Xmx2G", # Optional
- # "gameDir": str(MINECRAFT_DIR) # Optional
- }
- },
- "authenticationDatabase": {
- account_key: {
- "accessToken": AUTH_ACCESS_TOKEN,
- "profiles": {
- auth_profile_key: {
- "displayName": AUTH_PLAYER_NAME,
- "playerUUID": AUTH_UUID,
- "userId": AUTH_XUID,
- # "texture": "..." # Optional base64 skin/cape
- }
- },
- "username": AUTH_PLAYER_NAME, # Often email for MSA
- "properties": [], # Optional user properties
- # "remoteId": "...", # Xbox user ID
- }
- },
- "settings": {
- # "locale": "en-us",
- # "showMenu": True,
- # ... other launcher settings
- },
- "selectedUser": {
- "account": account_key,
- "profile": auth_profile_key
- },
- "version": 4 # Common version number for this format
- }
- try:
- # Ensure directory exists first
- await aiofiles.os.makedirs(LAUNCHER_PROFILES_PATH.parent, exist_ok=True)
- # Write the file asynchronously
- async with aiofiles.open(LAUNCHER_PROFILES_PATH, 'w', encoding='utf-8') as f:
- await f.write(json.dumps(profiles_data, indent=2))
- log.info(f"Created/updated {LAUNCHER_PROFILES_PATH}")
- except Exception as error:
- log.error(f"Failed to write {LAUNCHER_PROFILES_PATH}: {error}")
- raise RuntimeError("Could not write launcher profiles file.")
-
-
-async def load_manifest(filename: str) -> Dict[str, Any]:
- """Loads a JSON manifest file from the script's directory asynchronously."""
- file_path = SCRIPT_DIR / filename
- log.info(f"Loading manifest: {filename}")
- try:
- async with aiofiles.open(file_path, 'r', encoding='utf-8') as f:
- content = await f.read()
- return json.loads(content)
- except FileNotFoundError:
- log.error(f"Manifest file not found: {file_path}")
- raise
- except json.JSONDecodeError as e:
- log.error(f"Failed to parse manifest {filename}: {e}")
- raise ValueError(f"Invalid JSON in manifest: {filename}")
- except Exception as e:
- log.error(f"Failed to load manifest {filename}: {e}")
- raise
-
-def merge_manifests(target_manifest: Dict[str, Any], base_manifest: Dict[str, Any]) -> Dict[str, Any]:
- """Merges two version manifests (target inheriting from base)."""
- target_id = target_manifest.get('id', 'unknown-target')
- base_id = base_manifest.get('id', 'unknown-base')
- log.info(f"Merging manifests: {target_id} inheriting from {base_id}")
-
- # Combine libraries: Use a dictionary keyed by 'name' for overrides.
- combined_libraries_map = {}
- for lib in base_manifest.get('libraries', []):
- if 'name' in lib: combined_libraries_map[lib['name']] = lib
- for lib in target_manifest.get('libraries', []):
- if 'name' in lib: combined_libraries_map[lib['name']] = lib # Overwrite
-
- # Combine arguments: Append target args to base args.
- base_args = base_manifest.get('arguments', {}) or {}
- target_args = target_manifest.get('arguments', {}) or {}
- combined_arguments = {
- "game": (base_args.get('game', []) or []) + (target_args.get('game', []) or []),
- "jvm": (base_args.get('jvm', []) or []) + (target_args.get('jvm', []) or [])
- }
-
- # Construct merged manifest, prioritizing target values.
- merged = {
- "id": target_manifest.get('id'),
- "time": target_manifest.get('time'),
- "releaseTime": target_manifest.get('releaseTime'),
- "type": target_manifest.get('type'),
- "mainClass": target_manifest.get('mainClass'), # Target overrides
- "assetIndex": target_manifest.get('assetIndex', base_manifest.get('assetIndex')), # Prefer target
- "assets": target_manifest.get('assets', base_manifest.get('assets')), # Asset ID string
- "downloads": base_manifest.get('downloads'), # Use base downloads (client.jar etc.)
- "javaVersion": target_manifest.get('javaVersion', base_manifest.get('javaVersion')), # Prefer target
- "libraries": list(combined_libraries_map.values()), # Convert dict values back to list
- "arguments": combined_arguments,
- "logging": target_manifest.get('logging', base_manifest.get('logging')), # Prefer target
- "complianceLevel": target_manifest.get('complianceLevel', base_manifest.get('complianceLevel')),
- "minimumLauncherVersion": target_manifest.get('minimumLauncherVersion', base_manifest.get('minimumLauncherVersion'))
- }
- # Clean up None values from .get() fallbacks
- return {k: v for k, v in merged.items() if v is not None}
-
-# Sync backup function (to be run in executor)
-def _create_backup_sync(source_dir: pathlib.Path, backup_dir_base: pathlib.Path):
- """Copies source to backup dir, zips it, then removes backup dir."""
- backup_dir = backup_dir_base # Directory to copy into first
- backup_zip_path = backup_dir_base.with_suffix('.zip')
-
- log.info(f"Starting backup copy from {source_dir} to {backup_dir}")
- # Remove old backup directory/zip if they exist
- if backup_zip_path.exists():
- log.debug(f"Removing existing backup zip: {backup_zip_path}")
- backup_zip_path.unlink()
- if backup_dir.exists():
- log.debug(f"Removing existing intermediate backup dir: {backup_dir}")
- shutil.rmtree(backup_dir)
-
- # Copy the entire directory
- shutil.copytree(source_dir, backup_dir, dirs_exist_ok=True)
- log.info(f"Backup copy complete. Zipping {backup_dir} to {backup_zip_path}")
-
- # Zip the backup directory
- with zipfile.ZipFile(backup_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
- for root, _, files in os.walk(backup_dir):
- for file in files:
- file_path = pathlib.Path(root) / file
- # Arcname is the path inside the zip file relative to backup_dir
- arcname = file_path.relative_to(backup_dir)
- zipf.write(file_path, arcname)
- log.info(f"Backup zip created at {backup_zip_path}")
-
- # Remove the temporary backup directory after zipping
- shutil.rmtree(backup_dir)
- log.info(f"Removed intermediate backup directory {backup_dir}")
-
-# --- Main Execution ---
-async def main():
- try:
- # 1. Load Target Manifest
- target_manifest = await load_manifest(TARGET_VERSION_MANIFEST_FILENAME)
- target_version_id = target_manifest.get('id')
- if not target_version_id:
- raise ValueError(f"Target manifest {TARGET_VERSION_MANIFEST_FILENAME} is missing required 'id' field.")
-
- # 2. Load Base Manifest if needed and Merge
- final_manifest = target_manifest
- if 'inheritsFrom' in target_manifest:
- base_version_id = target_manifest['inheritsFrom']
- base_manifest_filename = f"{base_version_id}.json"
- try:
- base_manifest = await load_manifest(base_manifest_filename)
- final_manifest = merge_manifests(target_manifest, base_manifest)
- except (FileNotFoundError, ValueError, KeyError, Exception) as e:
- log.error(f"Could not load or merge base manifest '{base_manifest_filename}' specified in {TARGET_VERSION_MANIFEST_FILENAME}: {e}")
- sys.exit(1)
- else:
- log.info(f"Manifest {target_version_id} does not inherit from another version.")
-
- # --- Use finalManifest for all subsequent steps ---
- version_id = final_manifest.get('id') # Should be the target ID after merge
- if not version_id:
- raise ValueError("Final merged manifest is missing the 'id' field.")
-
- version_dir = VERSIONS_DIR / version_id
- natives_dir = version_dir / f"{version_id}-natives"
-
- log.info(f"Preparing Minecraft {version_id}...")
- os_name = get_os_name()
- arch_name = get_arch_name()
- log.info(f"Detected OS: {os_name}, Arch: {arch_name}")
-
- # 4. Ensure Directories and Launcher Profiles
- log.info(f"Ensuring base directory exists: {MINECRAFT_DIR}")
- # Create all necessary directories asynchronously and concurrently
- await asyncio.gather(
- aiofiles.os.makedirs(MINECRAFT_DIR, exist_ok=True),
- aiofiles.os.makedirs(VERSIONS_DIR, exist_ok=True),
- aiofiles.os.makedirs(LIBRARIES_DIR, exist_ok=True),
- aiofiles.os.makedirs(ASSETS_DIR, exist_ok=True),
- aiofiles.os.makedirs(version_dir, exist_ok=True),
- aiofiles.os.makedirs(natives_dir, exist_ok=True),
- aiofiles.os.makedirs(ASSET_INDEXES_DIR, exist_ok=True),
- aiofiles.os.makedirs(ASSET_OBJECTS_DIR, exist_ok=True)
- )
- # Create/update launcher profiles
- await ensure_launcher_profiles(version_id)
-
- # 5. Copy *Target* Version Manifest JSON to Version Directory
- target_manifest_source_path = SCRIPT_DIR / TARGET_VERSION_MANIFEST_FILENAME
- dest_manifest_path = version_dir / f"{version_id}.json"
- try:
- log.info(f"Copying {target_manifest_source_path.name} to {dest_manifest_path}")
- # Run synchronous copy in executor to avoid blocking
- await asyncio.get_running_loop().run_in_executor(
- None, shutil.copyfile, target_manifest_source_path, dest_manifest_path
- )
- except Exception as error:
- log.error(f"Failed to copy target version manifest: {error}")
- raise RuntimeError(f"Could not copy version manifest file: {target_manifest_source_path}")
-
- # 6. Download Client JAR
- log.info('Checking client JAR...')
- client_info = final_manifest.get('downloads', {}).get('client')
- if not (client_info and 'url' in client_info and 'sha1' in client_info):
- raise ValueError(f"Merged manifest for {version_id} is missing client download information (url, sha1).")
- client_jar_path = version_dir / f"{version_id}.jar"
- # Use tqdm context for the single download (async with not supported, manual create/close)
- client_pbar = tqdm(total=1, desc="Client JAR", unit="file", leave=False)
- try:
- await download_file(client_info['url'], client_jar_path, client_info['sha1'], pbar=client_pbar)
- finally:
- client_pbar.close()
-
-
- # 7. Prepare Library List
- log.info('Processing library list...')
- libraries_to_process = []
- classpath_entries_set = {str(client_jar_path)} # Use a set to avoid duplicates
- native_library_paths = []
-
- for lib in final_manifest.get('libraries', []):
- # Check rules for the entire library entry FIRST
- if not check_item_rules(lib.get('rules')):
- # log.debug(f"Skipping library due to overall rules: {lib.get('name', 'N/A')}")
- continue
-
- lib_name = lib.get('name', 'unknown-library')
- downloads = lib.get('downloads', {})
- artifact = downloads.get('artifact')
- classifiers = downloads.get('classifiers', {})
- natives_rules = lib.get('natives', {}) # Legacy natives mapping
-
- # --- Determine Native Classifier ---
- native_classifier_key = None
- native_info = None
- # Check 'natives' mapping first (less common now)
- if os_name in natives_rules:
- raw_classifier = natives_rules[os_name]
- # Replace ${arch} - Python needs specific replacement logic
- arch_replace = '64' if arch_name == 'x64' else ('32' if arch_name == 'x86' else arch_name)
- potential_key = raw_classifier.replace('${arch}', arch_replace)
- if potential_key in classifiers:
- native_classifier_key = potential_key
- native_info = classifiers[native_classifier_key]
- # log.debug(f"Found native classifier via 'natives' rule: {native_classifier_key}")
-
- # Check standard 'classifiers' if not found via 'natives'
- if not native_info and classifiers:
- # Construct potential keys based on current OS/Arch
- potential_keys = [
- f"natives-{os_name}-{arch_name}",
- f"natives-{os_name}",
- ]
- for key in potential_keys:
- if key in classifiers:
- native_classifier_key = key
- native_info = classifiers[key]
- # log.debug(f"Found native classifier via standard key: {key}")
- break
-
- # --- Add Main Artifact ---
- if artifact and artifact.get('path') and artifact.get('url'):
- # Rules specific to the artifact itself are not standard in manifests.
- # We rely on the top-level library rules check done earlier.
- lib_path = LIBRARIES_DIR / artifact['path']
- libraries_to_process.append({
- "name": lib_name,
- "url": artifact['url'],
- "path": lib_path,
- "sha1": artifact.get('sha1'),
- "is_native": False,
- })
- classpath_entries_set.add(str(lib_path)) # Add non-native to classpath
-
- # --- Add Native Artifact ---
- if native_info and native_info.get('path') and native_info.get('url'):
- # Again, rely on top-level library rules. Classifier-specific rules aren't standard.
- native_path = LIBRARIES_DIR / native_info['path']
- libraries_to_process.append({
- "name": f"{lib_name}:{native_classifier_key}", # Include classifier in name for clarity
- "url": native_info['url'],
- "path": native_path,
- "sha1": native_info.get('sha1'),
- "is_native": True,
- })
- native_library_paths.append(native_path) # Keep track of native JARs for extraction
-
- # Download Libraries (Corrected tqdm usage)
- log.info(f"Downloading {len(libraries_to_process)} library files...")
- lib_pbar = tqdm(total=len(libraries_to_process), desc="Libraries", unit="file", leave=False)
- try:
- download_tasks = [
- download_file(lib_info['url'], lib_info['path'], lib_info['sha1'], pbar=lib_pbar)
- for lib_info in libraries_to_process
- ]
- await asyncio.gather(*download_tasks)
- finally:
- lib_pbar.close()
- log.info('Library download check complete.')
-
- classpath_entries = list(classpath_entries_set) # Convert classpath set back to list
-
- # 8. Extract Natives
- log.info('Extracting native libraries...')
- # Clear existing natives directory first
- try:
- if await aiofiles.os.path.isdir(natives_dir):
- log.debug(f"Removing existing natives directory: {natives_dir}")
- # Run synchronous rmtree in executor
- await asyncio.get_running_loop().run_in_executor(None, shutil.rmtree, natives_dir)
- await aiofiles.os.makedirs(natives_dir, exist_ok=True)
- except Exception as err:
- log.warning(f"Could not clear/recreate natives directory {natives_dir}: {err}. Extraction might fail or use old files.")
-
- if native_library_paths:
- # Corrected tqdm usage for natives
- native_pbar = tqdm(total=len(native_library_paths), desc="Natives", unit="file", leave=False)
- try:
- extract_tasks = []
- for native_jar_path in native_library_paths:
- # Define task within loop to capture correct native_jar_path
- async def extract_task(jar_path, pbar_instance):
- try:
- await extract_natives(jar_path, natives_dir)
- except Exception as e:
- log.error(f"\nFailed to extract natives from: {jar_path.name}: {e}")
- # Decide if you want to raise or just log
- # raise # Uncomment to stop on first extraction error
- finally:
- pbar_instance.update(1)
-
- extract_tasks.append(extract_task(native_jar_path, native_pbar))
- await asyncio.gather(*extract_tasks)
- finally:
- native_pbar.close() # Ensure pbar is closed
- else:
- log.info("No native libraries to extract for this platform.")
- log.info('Native extraction complete.')
-
-
- # 9. Download Assets
- log.info('Checking assets...')
- asset_index_info = final_manifest.get('assetIndex')
- if not (asset_index_info and 'id' in asset_index_info and 'url' in asset_index_info and 'sha1' in asset_index_info):
- raise ValueError(f"Merged manifest for {version_id} is missing asset index information (id, url, sha1).")
-
- asset_index_id = asset_index_info['id']
- asset_index_filename = f"{asset_index_id}.json"
- asset_index_path = ASSET_INDEXES_DIR / asset_index_filename
-
- # Download asset index (Corrected tqdm usage)
- idx_pbar = tqdm(total=1, desc="Asset Index", unit="file", leave=False)
- try:
- await download_file(asset_index_info['url'], asset_index_path, asset_index_info['sha1'], pbar=idx_pbar)
- finally:
- idx_pbar.close()
-
- # Load asset index content
- try:
- async with aiofiles.open(asset_index_path, 'r', encoding='utf-8') as f:
- asset_index_content = json.loads(await f.read())
- except Exception as e:
- raise RuntimeError(f"Failed to read downloaded asset index {asset_index_path}: {e}")
-
- asset_objects = asset_index_content.get('objects', {})
- total_assets = len(asset_objects)
- log.info(f"Checking {total_assets} asset files listed in index {asset_index_id}...")
-
- # Download assets (Corrected tqdm usage)
- asset_pbar = tqdm(total=total_assets, desc="Assets", unit="file", leave=False)
- try:
- asset_download_tasks = []
- for asset_key, asset_details in asset_objects.items():
- asset_hash = asset_details.get('hash')
- if not asset_hash:
- log.warning(f"Asset '{asset_key}' is missing hash in index, skipping.")
- asset_pbar.update(1); continue # Still count it towards progress
-
- hash_prefix = asset_hash[:2]
- asset_subdir = ASSET_OBJECTS_DIR / hash_prefix
- asset_filepath = asset_subdir / asset_hash
- # Standard Minecraft asset download URL structure
- asset_url = f"https://resources.download.minecraft.net/{hash_prefix}/{asset_hash}"
- asset_download_tasks.append(
- download_file(asset_url, asset_filepath, asset_hash, pbar=asset_pbar)
- )
- await asyncio.gather(*asset_download_tasks)
- finally:
- asset_pbar.close() # Ensure pbar is closed
- log.info('Asset check complete.')
-
- # 10. Download and Setup Java Runtime
- java_version_info = final_manifest.get('javaVersion')
- if not (java_version_info and 'majorVersion' in java_version_info):
- log.warning("Manifest does not specify Java major version. Attempting default.")
- required_java_major = DEFAULT_JAVA_VERSION # Use default from java.py
- else:
- required_java_major = java_version_info['majorVersion']
-
- log.info(f"Checking/Installing Java {required_java_major}...")
- java_executable = await download_java(
- version=required_java_major,
- destination_dir_str=str(JAVA_INSTALL_DIR) # Pass as string
- # imageType='jre' # Optionally force JRE
- )
-
- if not java_executable:
- log.error(f"Failed to obtain a suitable Java {required_java_major} executable.")
- log.error(f"Ensure Java {required_java_major} is installed and accessible, or allow the script to download it to {JAVA_INSTALL_DIR}.")
- sys.exit(1)
- log.info(f"Using Java executable: {java_executable}")
-
-
- # 11. Handle Client Storage (for NeoForge setup tracking)
- log.info("Loading client storage...")
- client_storage = {}
- try:
- if await aiofiles.os.path.exists(CLIENT_STORAGE_PATH):
- async with aiofiles.open(CLIENT_STORAGE_PATH, 'r', encoding='utf-8') as f:
- client_storage = json.loads(await f.read())
- else:
- # Initialize if file doesn't exist
- client_storage = {"setupNeoForge": []} # Start with empty list
- except json.JSONDecodeError as e:
- log.warning(f"Failed to load or parse {CLIENT_STORAGE_PATH}: {e}. Reinitializing.")
- client_storage = {"setupNeoForge": []} # Reinitialize on error
- except Exception as e:
- log.error(f"Error handling {CLIENT_STORAGE_PATH}: {e}. Reinitializing.")
- client_storage = {"setupNeoForge": []}
-
- # Ensure setupNeoForge exists and is a list (migration from old boolean)
- if "setupNeoForge" not in client_storage or not isinstance(client_storage.get("setupNeoForge"), list):
- client_storage["setupNeoForge"] = []
-
-
- # 12. Run NeoForge Installer if necessary
- needs_neoforge_setup = False
- if version_id.startswith("neoforge-") and version_id not in client_storage.get("setupNeoForge", []):
- needs_neoforge_setup = True
- neoforge_installer_jar = SCRIPT_DIR / 'neoinstaller.jar'
- if not await aiofiles.os.path.isfile(neoforge_installer_jar):
- log.warning(f"NeoForge version detected ({version_id}), but neoinstaller.jar not found at {neoforge_installer_jar}. Skipping automatic setup.")
- needs_neoforge_setup = False # Cannot perform setup
-
- if needs_neoforge_setup:
- log.info(f"Setting up NeoForge for {version_id}...")
- # Command structure: java -jar neoinstaller.jar --install-client
- setup_command_args = [
- java_executable,
- "-jar",
- str(neoforge_installer_jar),
- "--install-client",
- str(MINECRAFT_DIR) # Pass the .minecraft dir path
- ]
-
- log.info(f"Running NeoForge setup command: {' '.join(setup_command_args)}")
- # Run the installer process
- process = await asyncio.create_subprocess_exec(
- *setup_command_args,
- stdout=asyncio.subprocess.PIPE,
- stderr=asyncio.subprocess.PIPE,
- cwd=str(SCRIPT_DIR) # Run from script dir to find installer jar easily?
- )
-
- stdout, stderr = await process.communicate()
-
- if stdout: log.info("NeoForge Installer Output:\n" + stdout.decode(errors='ignore'))
- if stderr: log.error("NeoForge Installer Errors:\n" + stderr.decode(errors='ignore'))
-
- if process.returncode == 0:
- log.info("NeoForge setup completed successfully.")
- # Update client storage
- client_storage["setupNeoForge"].append(version_id) # Assumes it's a list
- try:
- async with aiofiles.open(CLIENT_STORAGE_PATH, 'w', encoding='utf-8') as f:
- await f.write(json.dumps(client_storage, indent=2))
- except Exception as e:
- log.error(f"Failed to update {CLIENT_STORAGE_PATH} after NeoForge setup: {e}")
- else:
- log.error(f"NeoForge setup failed with exit code {process.returncode}. Minecraft might not launch correctly.")
- # Decide if you want to exit here or try launching anyway
- # sys.exit(1)
-
-
- # 13. Construct Launch Command
- log.info('Constructing launch command...')
- classpath_separator = os.pathsep # Use ';' for Windows, ':' for Linux/macOS
- classpath_string = classpath_separator.join(classpath_entries)
-
- # Argument Placeholder Replacements
- replacements = {
- '${natives_directory}': str(natives_dir),
- '${library_directory}': str(LIBRARIES_DIR),
- '${classpath_separator}': classpath_separator,
- '${launcher_name}': 'CustomPythonLauncher',
- '${launcher_version}': '1.0',
- '${classpath}': classpath_string,
- '${auth_player_name}': AUTH_PLAYER_NAME,
- '${version_name}': version_id,
- '${game_directory}': str(MINECRAFT_DIR),
- '${assets_root}': str(ASSETS_DIR),
- '${assets_index_name}': asset_index_id, # Use the ID from assetIndex
- '${auth_uuid}': AUTH_UUID,
- '${auth_access_token}': AUTH_ACCESS_TOKEN,
- '${clientid}': 'N/A', # Placeholder
- '${auth_xuid}': AUTH_XUID,
- '${user_type}': USER_TYPE,
- '${version_type}': final_manifest.get('type', 'release'), # Use manifest type
- '${resolution_width}': cfg.get('resolution_width', '854'),
- '${resolution_height}': cfg.get('resolution_height', '480'),
- }
-
- def replace_placeholders(arg_template: str) -> str:
- """Replaces all placeholders in a single argument string."""
- replaced_arg = arg_template
- for key, value in replacements.items():
- replaced_arg = replaced_arg.replace(key, value)
- return replaced_arg
-
- # --- Process JVM Arguments (Using corrected rule logic) ---
- jvm_args = []
- for arg_entry in final_manifest.get('arguments', {}).get('jvm', []):
- arg_values_to_add = [] # List to hold processed args for this entry
- rules = None
- process_this_entry = True
-
- if isinstance(arg_entry, str):
- # Simple string argument, implicitly allowed (no rules)
- arg_values_to_add.append(replace_placeholders(arg_entry))
- elif isinstance(arg_entry, dict):
- # Argument object with potential rules
- rules = arg_entry.get('rules')
- # Check rules BEFORE processing value
- if not check_item_rules(rules):
- # log.debug(f"Skipping JVM arg object due to rules: {arg_entry.get('value', '')}")
- process_this_entry = False # Skip this whole dict entry
- else:
- # Rules allow, now process the value(s)
- value_from_dict = arg_entry.get('value')
- if isinstance(value_from_dict, list):
- arg_values_to_add.extend(replace_placeholders(val) for val in value_from_dict)
- elif isinstance(value_from_dict, str):
- arg_values_to_add.append(replace_placeholders(value_from_dict))
- else:
- log.warning(f"Unsupported value type in JVM arg object: {value_from_dict}")
- process_this_entry = False
- else:
- log.warning(f"Unsupported JVM argument format: {arg_entry}")
- process_this_entry = False # Skip unknown format
-
- # Add processed arguments if the entry was allowed and processed
- if process_this_entry:
- for arg in arg_values_to_add:
- # Basic quoting for -D properties with spaces
- if arg.startswith("-D") and "=" in arg:
- key, value = arg.split("=", 1)
- if " " in value and not (value.startswith('"') and value.endswith('"')):
- arg = f'{key}="{value}"'
- jvm_args.append(arg)
-
- # --- Process Game Arguments (Using corrected rule logic) ---
- game_args = []
- for arg_entry in final_manifest.get('arguments', {}).get('game', []):
- arg_values_to_add = []
- rules = None
- process_this_entry = True
-
- if isinstance(arg_entry, str):
- arg_values_to_add.append(replace_placeholders(arg_entry))
- elif isinstance(arg_entry, dict):
- rules = arg_entry.get('rules')
- if not check_item_rules(rules):
- # log.debug(f"Skipping game arg object due to rules: {arg_entry.get('value', '')}")
- process_this_entry = False
- else:
- value_from_dict = arg_entry.get('value')
- if isinstance(value_from_dict, list):
- arg_values_to_add.extend(replace_placeholders(val) for val in value_from_dict)
- elif isinstance(value_from_dict, str):
- arg_values_to_add.append(replace_placeholders(value_from_dict))
- else:
- log.warning(f"Unsupported value type in game arg object: {value_from_dict}")
- process_this_entry = False
- else:
- log.warning(f"Unsupported game argument format: {arg_entry}")
- process_this_entry = False
-
- if process_this_entry:
- game_args.extend(arg_values_to_add)
-
-
- # 14. Launch Minecraft
- main_class = final_manifest.get('mainClass')
- if not main_class:
- raise ValueError("Final manifest is missing the 'mainClass' required for launch.")
-
- final_launch_args = [
- java_executable,
- *jvm_args,
- main_class,
- *game_args,
- ]
-
- log.info("Attempting to launch Minecraft...")
- # Optionally log the full command for debugging, but be careful with tokens
- # log.debug(f"Launch command: {' '.join(final_launch_args)}")
-
- # Run the Minecraft process
- mc_process = await asyncio.create_subprocess_exec(
- *final_launch_args,
- stdout=sys.stdout, # Redirect child stdout to parent's stdout
- stderr=sys.stderr, # Redirect child stderr to parent's stderr
- cwd=MINECRAFT_DIR # Set the working directory to .minecraft
- )
-
- log.info(f"Minecraft process started (PID: {mc_process.pid}). Waiting for exit...")
-
- # Wait for the process to complete
- return_code = await mc_process.wait()
-
- # 15. Post-Launch Actions (Backup)
- log.info(f"Minecraft process exited with code {return_code}.")
-
- # Perform backup if configured
- if cfg.get("backup", False):
- log.info("Backup requested. Creating backup...")
- try:
- loop = asyncio.get_running_loop()
- # Run the synchronous backup function in the executor
- await loop.run_in_executor(None, _create_backup_sync, MINECRAFT_DIR, BACKUP_PATH_BASE)
- log.info("Backup process completed.")
- except Exception as backup_error:
- log.error(f"Failed to create backup: {backup_error}", exc_info=True)
- else:
- log.info("Backup disabled in config.")
-
- except Exception as e:
- log.exception("--- An error occurred during setup or launch ---")
- # Optionally add more specific error handling or cleanup
- sys.exit(1)
- finally:
- # Ensure the shared aiohttp session is closed on exit or error
- await close_session()
-
-
-# --- Script Entry Point ---
-if __name__ == "__main__":
- try:
- asyncio.run(main())
- except KeyboardInterrupt:
- log.info("Launch cancelled by user.")
- # Ensure session is closed if KeyboardInterrupt happens before finally block in main
- # Running close_session within a new asyncio.run context
- try:
- asyncio.run(close_session())
- except RuntimeError: # Can happen if loop is already closed
- pass
- # Note: SystemExit from sys.exit() will also terminate the script here
\ No newline at end of file
diff --git a/java.py b/java.py
deleted file mode 100644
index 21f9349..0000000
--- a/java.py
+++ /dev/null
@@ -1,428 +0,0 @@
-import os
-import platform
-import pathlib
-import hashlib
-import asyncio
-import logging
-import zipfile
-import tarfile
-import tempfile
-import secrets # For random hex bytes
-import aiohttp
-import aiofiles
-import aiofiles.os
-
-# Configure logging
-logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
-log = logging.getLogger(__name__)
-
-# --- Configuration ---
-ADOPTIUM_API_BASE = 'https://api.adoptium.net/v3'
-DEFAULT_JAVA_VERSION = 17
-DEFAULT_IMAGE_TYPE = 'jdk'
-
-# --- Helper Functions ---
-
-def get_api_os_arch():
- """Maps Python platform/machine to Adoptium API values."""
- system = platform.system()
- machine = platform.machine()
-
- api_os = None
- api_arch = None
-
- if system == 'Windows':
- api_os = 'windows'
- elif system == 'Darwin':
- api_os = 'mac'
- elif system == 'Linux':
- api_os = 'linux'
- else:
- log.error(f"Unsupported operating system: {system}")
- return None
-
- machine = machine.lower()
- if machine in ['amd64', 'x86_64']:
- api_arch = 'x64'
- elif machine in ['arm64', 'aarch64']:
- api_arch = 'aarch64'
- # Add other mappings if needed (e.g., x86, arm32)
- # elif machine in ['i386', 'i686']:
- # api_arch = 'x86'
- # elif machine.startswith('armv7'):
- # api_arch = 'arm'
- else:
- log.error(f"Unsupported architecture: {machine}")
- return None
-
- return {"os": api_os, "arch": api_arch}
-
-
-# --- Corrected find_java_executable Function ---
-async def find_java_executable(extract_dir: pathlib.Path, system: str) -> pathlib.Path | None:
- """
- Finds the path to the Java executable within the specified directory.
- Checks standard locations based on OS. Corrected scandir usage.
- """
- log.info(f"[find_java_executable] Searching in: {extract_dir}")
- try:
- # Use await for checking the main directory existence (could involve I/O)
- if not await aiofiles.os.path.isdir(extract_dir):
- log.warning(f"[find_java_executable] Provided path is not a directory: {extract_dir}")
- return None
-
- potential_sub_dir = None
- log.debug(f"[find_java_executable] Scanning for subdirectory in {extract_dir}...")
- try:
- # --- CORRECTED USAGE: Use synchronous os.scandir ---
- # No await, no async for. Standard for loop.
- for entry in os.scandir(extract_dir):
- log.debug(f"[find_java_executable] Found entry: {entry.path} (Name: {entry.name})")
- try:
- # entry.is_dir() is synchronous
- is_dir = entry.is_dir()
- log.debug(f"[find_java_executable] Is '{entry.name}' a directory? {is_dir}")
- if is_dir:
- potential_sub_dir = pathlib.Path(entry.path)
- log.info(f"[find_java_executable] Found potential Java subdirectory: {potential_sub_dir}")
- break # Assume first directory found is the right one
- except OSError as scandir_entry_error:
- log.warning(f"[find_java_executable] Could not check directory status for {entry.path}: {scandir_entry_error}")
- continue
- except OSError as e:
- log.warning(f"[find_java_executable] Could not scan directory {extract_dir}: {e}")
- # Continue trying base directory paths even if scan fails
-
- # Determine which directory to check: the found subdir or the base extract dir
- base_dir_to_check = potential_sub_dir if potential_sub_dir else extract_dir
- log.info(f"[find_java_executable] Selected base directory for final check: {base_dir_to_check}")
-
- java_executable_path = None
- if system == 'Windows':
- java_executable_path = base_dir_to_check / 'bin' / 'java.exe'
- elif system == 'Darwin':
- java_executable_path = base_dir_to_check / 'Contents' / 'Home' / 'bin' / 'java'
- else: # Linux
- java_executable_path = base_dir_to_check / 'bin' / 'java'
-
- log.info(f"[find_java_executable] Constructed full path to check: {java_executable_path}")
-
- # --- Check 1: File Existence (Use await for aiofiles.os.path) ---
- try:
- is_file = await aiofiles.os.path.isfile(java_executable_path)
- log.info(f"[find_java_executable] Check 1: Does path exist as a file? {is_file}")
- except Exception as e_isfile:
- log.error(f"[find_java_executable] Error checking if path is file: {e_isfile}")
- return None
-
- if is_file:
- # --- Check 2: Execute Permission (Use synchronous os.access) ---
- try:
- is_executable = os.access(java_executable_path, os.X_OK)
- log.info(f"[find_java_executable] Check 2: Is file executable (os.X_OK)? {is_executable}")
- if is_executable:
- log.info(f"[find_java_executable] Success! Found accessible executable: {java_executable_path.resolve()}")
- return java_executable_path.resolve()
- else:
- log.warning(f"[find_java_executable] File found but not executable: {java_executable_path}")
- return None
- except Exception as e_access:
- log.error(f"[find_java_executable] Error checking execute permission with os.access: {e_access}")
- return None
- else:
- log.warning(f"[find_java_executable] Executable path not found or is not a file.")
- # Fallback logic (only runs if potential_sub_dir was found but failed the check above)
- if potential_sub_dir and base_dir_to_check == potential_sub_dir:
- log.info(f"[find_java_executable] Retrying search directly in base directory: {extract_dir}")
- fallback_path = None
- if system == 'Windows':
- fallback_path = extract_dir / 'bin' / 'java.exe'
- elif system == 'Darwin':
- fallback_path = extract_dir / 'Contents' / 'Home' / 'bin' / 'java'
- else:
- fallback_path = extract_dir / 'bin' / 'java'
-
- if fallback_path:
- log.info(f"[find_java_executable] Checking fallback path: {fallback_path}")
- try:
- # Use await for async file check
- fb_is_file = await aiofiles.os.path.isfile(fallback_path)
- log.info(f"[find_java_executable] Fallback Check 1: Exists as file? {fb_is_file}")
- if fb_is_file:
- # Use sync permission check
- fb_is_executable = os.access(fallback_path, os.X_OK)
- log.info(f"[find_java_executable] Fallback Check 2: Is executable? {fb_is_executable}")
- if fb_is_executable:
- log.info(f"[find_java_executable] Success on fallback! Found: {fallback_path.resolve()}")
- return fallback_path.resolve()
- else:
- log.warning(f"[find_java_executable] Fallback file found but not executable.")
- else:
- log.warning(f"[find_java_executable] Fallback path not found or not a file.")
- except Exception as e_fb:
- log.error(f"[find_java_executable] Error during fallback check: {e_fb}")
-
- log.warning(f"[find_java_executable] Could not find executable via primary or fallback paths.")
- return None
-
- except Exception as e:
- log.exception(f"[find_java_executable] Unexpected error searching in {extract_dir}: {e}") # Use log.exception to get traceback
- return None
-
-
-# Function to run synchronous extraction in a separate thread
-def _extract_zip(zip_data: bytes, dest_path: pathlib.Path):
- import io
- with io.BytesIO(zip_data) as zip_buffer:
- with zipfile.ZipFile(zip_buffer, 'r') as zip_ref:
- zip_ref.extractall(dest_path)
-
-def _extract_tar(tar_path: pathlib.Path, dest_path: pathlib.Path):
- # Check if the tar file exists before trying to open it
- if not tar_path.is_file():
- log.error(f"Tar file not found for extraction: {tar_path}")
- raise FileNotFoundError(f"Tar file not found: {tar_path}")
- try:
- with tarfile.open(tar_path, "r:gz") as tar_ref:
- # tarfile doesn't have a built-in strip_components like the command line
- # We need to manually filter members or extract carefully
- # For simplicity here, we assume strip=1 behaviour is desired and
- # hope the find_java_executable handles the structure.
- # A more robust solution would iterate members and adjust paths.
- tar_ref.extractall(path=dest_path) # This might create a top-level dir
- except tarfile.ReadError as e:
- log.error(f"Error reading tar file {tar_path}: {e}")
- raise
- except Exception as e:
- log.error(f"Unexpected error during tar extraction from {tar_path}: {e}")
- raise
-
-# --- Main Exported Function ---
-async def download_java(
- version: int = DEFAULT_JAVA_VERSION,
- destination_dir_str: str | None = None,
- image_type: str = DEFAULT_IMAGE_TYPE,
- vendor: str = 'eclipse',
- jvm_impl: str = 'hotspot',
-) -> str | None:
- """
- Downloads and extracts a standalone Java runtime/JDK if not already present.
-
- Args:
- version: The major Java version (e.g., 11, 17, 21).
- destination_dir_str: Directory for Java. If None, a temporary dir is used.
- **Crucially, if this directory already contains a valid executable, download will be skipped.**
- image_type: Type of Java package ('jdk' or 'jre').
- vendor: The build vendor (usually 'eclipse' for Temurin).
- jvm_impl: The JVM implementation.
-
- Returns:
- The absolute path to the Java executable as a string if successful, otherwise None.
- """
-
- if destination_dir_str is None:
- # Use mkdtemp for a secure temporary directory if none provided
- # Note: This temporary directory won't persist across runs.
- # A fixed path is usually better for caching.
- # Running sync mkdtemp in executor to avoid blocking
- loop = asyncio.get_running_loop()
- temp_dir_str = await loop.run_in_executor(None, tempfile.mkdtemp, f"downloaded-java-{secrets.token_hex(4)}-")
- destination_dir = pathlib.Path(temp_dir_str)
- log.info(f"No destination directory provided, using temporary directory: {destination_dir}")
- else:
- destination_dir = pathlib.Path(destination_dir_str).resolve()
-
-
- platform_info = get_api_os_arch()
- if not platform_info:
- return None
- api_os = platform_info["os"]
- api_arch = platform_info["arch"]
- current_system = platform.system()
-
- # --- Check if Java executable already exists ---
- log.info(f"Checking for existing Java executable in: {destination_dir}")
- try:
- existing_java_path = await find_java_executable(destination_dir, current_system)
- if existing_java_path:
- log.info(f"Valid Java executable already found at: {existing_java_path}. Skipping download.")
- return str(existing_java_path)
- else:
- log.info(f"Existing Java executable not found or installation is incomplete in {destination_dir}.")
- except Exception as check_error:
- # Log the exception details if the check itself fails
- log.exception(f"Error during pre-check for existing Java in {destination_dir}: {check_error}. Assuming download is needed.")
- # --- End Check ---
-
- log.info('Proceeding with Java download and extraction process...')
- api_url = f"{ADOPTIUM_API_BASE}/binary/latest/{version}/ga/{api_os}/{api_arch}/{image_type}/{jvm_impl}/normal/{vendor}"
- log.info(f"Attempting to download Java {version} ({image_type}) for {api_os}-{api_arch} from Adoptium API.")
-
- download_url = None
- archive_type = None
-
- async with aiohttp.ClientSession() as session:
- try:
- log.info(f"Fetching download details (HEAD request) from: {api_url}")
- # Use allow_redirects=True and get the final URL
- async with session.head(api_url, allow_redirects=True) as head_response:
- head_response.raise_for_status() # Raise exception for bad status codes (4xx, 5xx)
- download_url = str(head_response.url) # Get the final URL after redirects
-
- if not download_url:
- raise ValueError("Could not resolve download URL after redirects.")
-
- if download_url.endswith('.zip'):
- archive_type = 'zip'
- elif download_url.endswith('.tar.gz'):
- archive_type = 'tar.gz'
- else:
- # Guess based on OS if extension is missing (less reliable)
- archive_type = 'zip' if api_os == 'windows' else 'tar.gz'
-
- log.info(f"Resolved download URL: {download_url}")
- log.info(f"Detected archive type: {archive_type}")
-
- # Ensure destination directory exists
- # Use await aiofiles.os.makedirs
- await aiofiles.os.makedirs(destination_dir, exist_ok=True)
- log.info(f"Ensured destination directory exists: {destination_dir}")
-
- log.info('Starting download...')
- async with session.get(download_url) as response:
- response.raise_for_status()
- file_data = await response.read()
- log.info('Download complete.')
-
- log.info(f"Extracting {archive_type} archive to {destination_dir}...")
- loop = asyncio.get_running_loop()
-
- if archive_type == 'zip':
- # Run synchronous zip extraction in a thread
- await loop.run_in_executor(None, _extract_zip, file_data, destination_dir)
- else: # tar.gz
- # Write to a temporary file first for tarfile
- temp_tar_path = None
- # Use a context manager for the temporary file
- # Running sync tempfile operations in executor
- fd, temp_tar_path_str = await loop.run_in_executor(None, tempfile.mkstemp, ".tar.gz", "java-dl-")
- os.close(fd) # Close descriptor from mkstemp
- temp_tar_path = pathlib.Path(temp_tar_path_str)
-
- try:
- log.debug(f"Saving temporary tar archive to: {temp_tar_path}")
- async with aiofiles.open(temp_tar_path, 'wb') as f:
- await f.write(file_data)
- log.debug(f"Temporary archive saved successfully.")
-
- # Run synchronous tar extraction in a thread
- log.debug(f"Starting tar extraction from {temp_tar_path}...")
- await loop.run_in_executor(None, _extract_tar, temp_tar_path, destination_dir)
- log.debug('Extraction using tar complete.')
-
- finally:
- # Clean up temporary tar file using await aiofiles.os.remove
- if temp_tar_path and await aiofiles.os.path.exists(temp_tar_path):
- try:
- await aiofiles.os.remove(temp_tar_path)
- log.debug(f"Temporary file {temp_tar_path} deleted.")
- except OSError as e:
- log.warning(f"Could not delete temporary tar file {temp_tar_path}: {e}")
- elif temp_tar_path:
- log.debug(f"Temporary file {temp_tar_path} did not exist for deletion.")
-
- log.info('Extraction complete.')
-
- # --- Find executable AFTER extraction ---
- # Add a small delay, just in case of filesystem flush issues (optional)
- # await asyncio.sleep(0.5)
- log.info("Re-checking for Java executable after extraction...")
- java_path = await find_java_executable(destination_dir, current_system)
-
- if java_path:
- log.info(f"Java executable successfully found after extraction at: {java_path}")
- return str(java_path)
- else:
- log.error('Extraction seemed successful, but failed to find Java executable at the expected location afterwards.')
- log.error(f"Please double-check the contents of {destination_dir} and the logic in find_java_executable for platform {current_system}.")
- # Log directory contents for debugging
- try:
- log.error(f"Contents of {destination_dir}: {os.listdir(destination_dir)}")
- # Check potential subdir contents too
- for item in os.listdir(destination_dir):
- item_path = destination_dir / item
- if item_path.is_dir():
- log.error(f"Contents of {item_path}: {os.listdir(item_path)}")
- bin_path = item_path / 'bin'
- if bin_path.is_dir():
- log.error(f"Contents of {bin_path}: {os.listdir(bin_path)}")
- break # Show first subdir found
- except Exception as list_err:
- log.error(f"Could not list directory contents for debugging: {list_err}")
- return None
-
- except aiohttp.ClientResponseError as e:
- log.error(f"HTTP Error downloading Java: {e.status} {e.message}")
- if e.status == 404:
- log.error(f"Could not find a build for Java {version} ({image_type}) for {api_os}-{api_arch}. Check Adoptium website for availability.")
- else:
- log.error(f"Response Headers: {e.headers}")
- # Try reading response body if available (might be large)
- try:
- error_body = await e.response.text()
- log.error(f"Response Body (partial): {error_body[:500]}")
- except Exception:
- pass # Ignore if body can't be read
-
- log.error(f"Java download/extraction failed. Directory {destination_dir} may be incomplete.")
- return None
- except Exception as error:
- log.exception(f"An unexpected error occurred during download/extraction: {error}")
- log.error(f"Java download/extraction failed. Directory {destination_dir} may be incomplete.")
- return None
-
-# Example usage (optional, can be run with `python java.py`)
-if __name__ == "__main__":
- async def run_test():
- print("Testing Java Downloader...")
- # Define a test destination directory
- test_dest = pathlib.Path("./test-java-runtime").resolve()
- print(f"Will attempt to download Java to: {test_dest}")
-
- # Clean up previous test run if exists
- if test_dest.exists():
- import shutil
- print("Removing previous test directory...")
- shutil.rmtree(test_dest)
-
- java_exe_path = await download_java(
- version=21, # Specify version
- destination_dir_str=str(test_dest),
- image_type='jdk'
- )
-
- if java_exe_path:
- print(f"\nSuccess! Java executable path: {java_exe_path}")
- # Try running java -version
- try:
- print("\nRunning java -version:")
- proc = await asyncio.create_subprocess_exec(
- java_exe_path,
- "-version",
- stdout=asyncio.subprocess.PIPE,
- stderr=asyncio.subprocess.PIPE
- )
- stdout, stderr = await proc.communicate()
- # java -version often prints to stderr
- print("Exit Code:", proc.returncode)
- if stderr:
- print("Output (stderr):\n", stderr.decode())
- if stdout: # Just in case it prints to stdout
- print("Output (stdout):\n", stdout.decode())
-
- except Exception as e:
- print(f"Error running java -version: {e}")
- else:
- print("\nFailed to download or find Java executable.")
-
- asyncio.run(run_test())
\ No newline at end of file
diff --git a/replacer.py b/replacer.py
deleted file mode 100644
index 16a53f0..0000000
--- a/replacer.py
+++ /dev/null
@@ -1,68 +0,0 @@
-import logging
-
-# Configure logging (optional, but good practice)
-logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
-log = logging.getLogger(__name__)
-
-def replace_text(value: str, replacements: dict) -> str:
- """
- Replaces all occurrences of specified substrings within a string.
- Does not use regular expressions.
-
- Args:
- value: The original string to perform replacements on.
- replacements: A dictionary where keys are the substrings
- to find and values are the strings to
- replace them with.
-
- Returns:
- The string with all specified replacements made.
- Returns the original value if it's not a string
- or if replacements is not a valid dictionary.
- """
- if not isinstance(value, str):
- log.warning("replace_text: Input 'value' is not a string. Returning original value.")
- return value
-
- if not isinstance(replacements, dict):
- log.warning("replace_text: Input 'replacements' is not a valid dictionary. Returning original value.")
- return value
-
- modified_value = value
-
- # Iterate through each key-value pair in the replacements dictionary
- for search_string, replace_string in replacements.items():
- # Ensure both search and replace values are strings for safety
- if isinstance(search_string, str) and isinstance(replace_string, str):
- # Use str.replace() to replace all occurrences
- modified_value = modified_value.replace(search_string, replace_string)
- else:
- log.warning(f"replace_text: Skipping replacement for key '{search_string}' as either key or value is not a string.")
-
- return modified_value
-
-# Example Usage (matches the JS comment example)
-# if __name__ == "__main__":
-# import os
-# __dirname = os.path.abspath('.') # Simulate __dirname for example
-
-# original_config = {
-# "executable": ":thisdir:/bin/launcher",
-# "configFile": "/etc/config.conf",
-# "logPath": ":thisdir:/logs/app.log",
-# "tempDir": "/tmp",
-# "description": "Uses :thisdir: multiple :thisdir: times."
-# }
-
-# replacements = {":thisdir:": __dirname}
-
-# patched_config = {}
-# for key, value in original_config.items():
-# patched_config[key] = replace_text(value, replacements) # Note: Not async in Python
-
-# print("Original Config:", original_config)
-# print("Patched Config:", patched_config)
-
-# # Expected Output (assuming __dirname = '/path/to/current/directory'):
-# # Original Config: {'executable': ':thisdir:/bin/launcher', 'configFile': '/etc/config.conf', 'logPath': ':thisdir:/logs/app.log', 'tempDir': '/tmp', 'description': 'Uses :thisdir: multiple :thisdir: times.'}
-# # Patched Config: {'executable': '/path/to/current/directory/bin/launcher', 'configFile': '/etc/config.conf', 'logPath': '/path/to/current/directory/logs/app.log', 'tempDir': '/tmp', 'description': 'Uses /path/to/current/directory multiple /path/to/current/directory times.'}
\ No newline at end of file
From feaa4c2e558965e200646db6e8dfea170212c09b Mon Sep 17 00:00:00 2001
From: StoppedwummSites <150438484+StoppedwummSites@users.noreply.github.com>
Date: Tue, 6 May 2025 12:10:20 +0200
Subject: [PATCH 04/19] Add package
---
package-lock.json | 3 +++
package.json | 5 ++++-
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/package-lock.json b/package-lock.json
index 5706696..6f45c91 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,6 +18,9 @@
"uuid": "^11.1.0",
"windows-shortcuts": "^0.1.6"
},
+ "bin": {
+ "minecraft": "npm run start"
+ },
"devDependencies": {
"electron": "^36.1.0",
"electron-builder": "^26.0.12"
diff --git a/package.json b/package.json
index e1dff39..36566c0 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"type": "module",
"main": "index.js",
"scripts": {
- "start": "node .",
+ "start": "electron ui_index.cjs",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
@@ -23,5 +23,8 @@
"devDependencies": {
"electron": "^36.1.0",
"electron-builder": "^26.0.12"
+ },
+ "bin": {
+ "minecraft": "electron ui_index.cjs"
}
}
From 26be77c90794e0f546ef415c1eb411e60c478abb Mon Sep 17 00:00:00 2001
From: StoppedwummSites <150438484+StoppedwummSites@users.noreply.github.com>
Date: Tue, 6 May 2025 12:17:31 +0200
Subject: [PATCH 05/19] Add test
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 36566c0..9f65368 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
"main": "index.js",
"scripts": {
"start": "electron ui_index.cjs",
- "test": "echo \"Error: no test specified\" && exit 1"
+ "test": "node ."
},
"dependencies": {
"@xterm/xterm": "^5.5.0",
From dce9a3e1348de37442fe193174268222cea24ca3 Mon Sep 17 00:00:00 2001
From: StoppedwummSites <150438484+StoppedwummSites@users.noreply.github.com>
Date: Wed, 7 May 2025 12:26:24 +0200
Subject: [PATCH 06/19] Add Vitest for testing and implement initial API tests
- Updated package.json to include Vitest as a dev dependency.
- Created api.test.js to add tests for the API, including checks for OS name, rule validation, Java API, manifest handling, and miscellaneous functions.
- Ensured tests cover both defined methods and expected behaviors.
---
.gitignore | 3 +-
api.test.js | 66 +++
index.js | 26 +-
package-lock.json | 1414 ++++++++++++++++++++++++++++++++++++++++++++-
package.json | 3 +-
5 files changed, 1502 insertions(+), 10 deletions(-)
create mode 100644 api.test.js
diff --git a/.gitignore b/.gitignore
index 874c3ea..08be659 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,4 +9,5 @@ java-runtime
.DS_Store
dist
build
-__pycache__
\ No newline at end of file
+__pycache__
+test.txt
\ No newline at end of file
diff --git a/api.test.js b/api.test.js
new file mode 100644
index 0000000..82ad637
--- /dev/null
+++ b/api.test.js
@@ -0,0 +1,66 @@
+// windows only test
+import index from "./index.js"
+import * as java from "./java.js"
+import { describe, it, expect } from "vitest"
+import fs from "fs/promises"
+
+describe("API exists", () => {
+ it("Should be defined", () => {
+ expect(index).toBeDefined()
+ })
+})
+
+describe("API methods", () => {
+ it("Can get OS name", () => {
+ expect(index.getOSName()).toBeDefined()
+ expect(index.getOSName()).eq("windows")
+ })
+ it("Can check rules", () => {
+ expect(index.checkRule({
+ "action": "allow",
+ "os": {
+ "name": "osx"
+ }
+ })).eq(false)
+ expect(index.checkRule({
+ "action": "allow",
+ "os": {
+ "name": "linux"
+ }
+ })).eq(false)
+ expect(index.checkRule({
+ "action": "allow",
+ "os": {
+ "name": "windows"
+ }
+ })).eq(true)
+ })
+})
+
+describe("Java API", () => {
+ it("Should be defined", () => {
+ expect(java.downloadJava).toBeDefined()
+ expect(java.downloadJava).toBeTypeOf("function")
+ })
+})
+
+describe("Can handle manifest", () => {
+ it("Should be defined", async () => {
+ expect(await index.loadManifest("neoforge-21.1.162.json")).toBeDefined()
+ })
+ it("Should merge Manifests", async () => {
+ expect(await index.mergeManifests(await index.loadManifest("neoforge-21.1.162.json"), await index.loadManifest("1.21.1.json"))).toBeDefined()
+ expect(await index.mergeManifests(await index.loadManifest("neoforge-21.1.162.json"), await index.loadManifest("1.21.1.json"))).toBeTypeOf("object")
+ })
+})
+
+describe("Misc functions", () => {
+ it("Should be defined", async () => {
+ expect(index.downloadFile).toBeDefined()
+ await fs.rm("test.txt", { force: true })
+ const download = await index.downloadFile("https://example.com", "test.txt")
+ expect(download).toBeDefined()
+ expect(download).toBeTypeOf("boolean")
+ expect(download).eq(true)
+ })
+})
\ No newline at end of file
diff --git a/index.js b/index.js
index 635c1ec..9beab18 100644
--- a/index.js
+++ b/index.js
@@ -823,8 +823,24 @@ async function main() {
} // End of main function
-main().catch(error => {
- console.error("\n--- An error occurred during setup or launch ---");
- console.error(error);
- process.exit(1);
-});
\ No newline at end of file
+const entryPointScript = process.argv[1].split(path.sep).pop();
+if (entryPointScript === __filename || entryPointScript === __dirname.split(path.sep).pop()) {
+ main().catch(error => {
+ console.error("\n--- An error occurred during setup or launch ---");
+ console.error(error);
+ process.exit(1);
+ });
+}
+
+export default {
+ downloadFile,
+ extractNatives,
+ getOSName,
+ getArchName,
+ checkRule,
+ checkItemRules,
+ ensureLauncherProfiles,
+ loadManifest,
+ mergeManifests,
+ main
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 6f45c91..ee4693d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,11 +19,12 @@
"windows-shortcuts": "^0.1.6"
},
"bin": {
- "minecraft": "npm run start"
+ "minecraft": "electron ui_index.cjs"
},
"devDependencies": {
"electron": "^36.1.0",
- "electron-builder": "^26.0.12"
+ "electron-builder": "^26.0.12",
+ "vitest": "^3.1.3"
}
},
"node_modules/@develar/schema-utils": {
@@ -750,6 +751,431 @@
"node": ">= 10.0.0"
}
},
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
+ "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz",
+ "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz",
+ "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz",
+ "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz",
+ "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz",
+ "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz",
+ "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz",
+ "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz",
+ "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz",
+ "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz",
+ "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz",
+ "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz",
+ "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz",
+ "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz",
+ "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz",
+ "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz",
+ "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz",
+ "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz",
+ "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz",
+ "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz",
+ "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz",
+ "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz",
+ "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz",
+ "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz",
+ "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@gar/promisify": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
@@ -872,6 +1298,13 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@malept/cross-spawn-promise": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz",
@@ -1016,6 +1449,286 @@
"node": ">=14"
}
},
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz",
+ "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz",
+ "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz",
+ "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz",
+ "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz",
+ "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz",
+ "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz",
+ "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz",
+ "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz",
+ "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz",
+ "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz",
+ "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz",
+ "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz",
+ "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz",
+ "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz",
+ "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz",
+ "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz",
+ "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz",
+ "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz",
+ "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz",
+ "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
"node_modules/@sindresorhus/is": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
@@ -1075,6 +1788,13 @@
"@types/ms": "*"
}
},
+ "node_modules/@types/estree": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
+ "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/fs-extra": {
"version": "9.0.13",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
@@ -1160,6 +1880,119 @@
"@types/node": "*"
}
},
+ "node_modules/@vitest/expect": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.3.tgz",
+ "integrity": "sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "3.1.3",
+ "@vitest/utils": "3.1.3",
+ "chai": "^5.2.0",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.3.tgz",
+ "integrity": "sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "3.1.3",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0 || ^6.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.3.tgz",
+ "integrity": "sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.3.tgz",
+ "integrity": "sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "3.1.3",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.3.tgz",
+ "integrity": "sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.1.3",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.3.tgz",
+ "integrity": "sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^3.0.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.3.tgz",
+ "integrity": "sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.1.3",
+ "loupe": "^3.1.3",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
"node_modules/@xmldom/xmldom": {
"version": "0.8.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
@@ -1498,6 +2331,16 @@
"node": ">=0.8"
}
},
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/astral-regex": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
@@ -1732,6 +2575,16 @@
"node": ">= 10.0.0"
}
},
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/cacache": {
"version": "16.1.3",
"resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz",
@@ -1933,6 +2786,23 @@
"node": ">= 0.4"
}
},
+ "node_modules/chai": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz",
+ "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -1950,6 +2820,16 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/check-error": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
+ "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
"node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
@@ -2292,6 +3172,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/defaults": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
@@ -2815,6 +3705,13 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
@@ -2848,7 +3745,48 @@
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
"dev": true,
"license": "MIT",
- "optional": true
+ "optional": true
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
+ "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.4",
+ "@esbuild/android-arm": "0.25.4",
+ "@esbuild/android-arm64": "0.25.4",
+ "@esbuild/android-x64": "0.25.4",
+ "@esbuild/darwin-arm64": "0.25.4",
+ "@esbuild/darwin-x64": "0.25.4",
+ "@esbuild/freebsd-arm64": "0.25.4",
+ "@esbuild/freebsd-x64": "0.25.4",
+ "@esbuild/linux-arm": "0.25.4",
+ "@esbuild/linux-arm64": "0.25.4",
+ "@esbuild/linux-ia32": "0.25.4",
+ "@esbuild/linux-loong64": "0.25.4",
+ "@esbuild/linux-mips64el": "0.25.4",
+ "@esbuild/linux-ppc64": "0.25.4",
+ "@esbuild/linux-riscv64": "0.25.4",
+ "@esbuild/linux-s390x": "0.25.4",
+ "@esbuild/linux-x64": "0.25.4",
+ "@esbuild/netbsd-arm64": "0.25.4",
+ "@esbuild/netbsd-x64": "0.25.4",
+ "@esbuild/openbsd-arm64": "0.25.4",
+ "@esbuild/openbsd-x64": "0.25.4",
+ "@esbuild/sunos-x64": "0.25.4",
+ "@esbuild/win32-arm64": "0.25.4",
+ "@esbuild/win32-ia32": "0.25.4",
+ "@esbuild/win32-x64": "0.25.4"
+ }
},
"node_modules/escalade": {
"version": "3.2.0",
@@ -2874,6 +3812,26 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz",
+ "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/exponential-backoff": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz",
@@ -2937,6 +3895,21 @@
"pend": "~1.2.0"
}
},
+ "node_modules/fdir": {
+ "version": "6.4.4",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
+ "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
@@ -3115,6 +4088,21 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -3813,6 +4801,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/loupe": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz",
+ "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
@@ -3843,6 +4838,16 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/magic-string": {
+ "version": "0.30.17",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+ "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0"
+ }
+ },
"node_modules/make-fetch-happen": {
"version": "10.2.1",
"resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz",
@@ -4273,6 +5278,25 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
"node_modules/negotiator": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
@@ -4561,6 +5585,23 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
+ "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
"node_modules/pe-library": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz",
@@ -4583,6 +5624,26 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/plist": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
@@ -4598,6 +5659,35 @@
"node": ">=10.4.0"
}
},
+ "node_modules/postcss": {
+ "version": "8.5.3",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
+ "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.8",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
"node_modules/postject": {
"version": "1.0.0-alpha.6",
"resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz",
@@ -4845,6 +5935,46 @@
"node": ">=8.0"
}
},
+ "node_modules/rollup": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz",
+ "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.7"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.40.2",
+ "@rollup/rollup-android-arm64": "4.40.2",
+ "@rollup/rollup-darwin-arm64": "4.40.2",
+ "@rollup/rollup-darwin-x64": "4.40.2",
+ "@rollup/rollup-freebsd-arm64": "4.40.2",
+ "@rollup/rollup-freebsd-x64": "4.40.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.40.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.40.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.40.2",
+ "@rollup/rollup-linux-arm64-musl": "4.40.2",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.40.2",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.40.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.40.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.40.2",
+ "@rollup/rollup-linux-x64-gnu": "4.40.2",
+ "@rollup/rollup-linux-x64-musl": "4.40.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.40.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.40.2",
+ "@rollup/rollup-win32-x64-msvc": "4.40.2",
+ "fsevents": "~2.3.2"
+ }
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -4948,6 +6078,13 @@
"node": ">=8"
}
},
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
@@ -5061,6 +6198,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
@@ -5112,6 +6259,13 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/stat-mode": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz",
@@ -5122,6 +6276,13 @@
"node": ">= 6"
}
},
+ "node_modules/std-env": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
+ "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -5344,6 +6505,67 @@
"semver": "bin/semver"
}
},
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
+ "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinypool": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz",
+ "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+ "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
+ "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/tmp": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
@@ -5498,6 +6720,175 @@
"node": ">=0.6.0"
}
},
+ "node_modules/vite": {
+ "version": "6.3.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
+ "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.3.tgz",
+ "integrity": "sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.4.0",
+ "es-module-lexer": "^1.7.0",
+ "pathe": "^2.0.3",
+ "vite": "^5.0.0 || ^6.0.0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.3.tgz",
+ "integrity": "sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "3.1.3",
+ "@vitest/mocker": "3.1.3",
+ "@vitest/pretty-format": "^3.1.3",
+ "@vitest/runner": "3.1.3",
+ "@vitest/snapshot": "3.1.3",
+ "@vitest/spy": "3.1.3",
+ "@vitest/utils": "3.1.3",
+ "chai": "^5.2.0",
+ "debug": "^4.4.0",
+ "expect-type": "^1.2.1",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3",
+ "std-env": "^3.9.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.2",
+ "tinyglobby": "^0.2.13",
+ "tinypool": "^1.0.2",
+ "tinyrainbow": "^2.0.0",
+ "vite": "^5.0.0 || ^6.0.0",
+ "vite-node": "3.1.3",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/debug": "^4.1.12",
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "@vitest/browser": "3.1.3",
+ "@vitest/ui": "3.1.3",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/debug": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
@@ -5533,6 +6924,23 @@
"node": ">= 8"
}
},
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/windows-shortcuts": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/windows-shortcuts/-/windows-shortcuts-0.1.6.tgz",
diff --git a/package.json b/package.json
index 9f65368..44c6457 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,8 @@
},
"devDependencies": {
"electron": "^36.1.0",
- "electron-builder": "^26.0.12"
+ "electron-builder": "^26.0.12",
+ "vitest": "^3.1.3"
},
"bin": {
"minecraft": "electron ui_index.cjs"
From 208aebbc0af68aa50f81043b12bad14162f6f463 Mon Sep 17 00:00:00 2001
From: StoppedwummSites <150438484+StoppedwummSites@users.noreply.github.com>
Date: Wed, 7 May 2025 12:32:10 +0200
Subject: [PATCH 07/19] Update downloadFile test to use a valid URL
---
api.test.js | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/api.test.js b/api.test.js
index 82ad637..4f7ae9a 100644
--- a/api.test.js
+++ b/api.test.js
@@ -57,8 +57,10 @@ describe("Can handle manifest", () => {
describe("Misc functions", () => {
it("Should be defined", async () => {
expect(index.downloadFile).toBeDefined()
+ })
+ it("Should download file", async () => {
await fs.rm("test.txt", { force: true })
- const download = await index.downloadFile("https://example.com", "test.txt")
+ const download = await index.downloadFile("https://sample-files.com/downloads/documents/txt/simple.txt", "test.txt")
expect(download).toBeDefined()
expect(download).toBeTypeOf("boolean")
expect(download).eq(true)
From e1d4d8097280fb08cafb8c9ae71c9eb051a798fc Mon Sep 17 00:00:00 2001
From: StoppedwummSites <150438484+StoppedwummSites@users.noreply.github.com>
Date: Wed, 7 May 2025 12:37:45 +0200
Subject: [PATCH 08/19] Remove unused dependencies from package.json and
package-lock.json
---
package-lock.json | 16 +---------------
package.json | 4 +---
2 files changed, 2 insertions(+), 18 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index ee4693d..4d63a31 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,14 +9,12 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
- "@xterm/xterm": "^5.5.0",
"adm-zip": "^0.5.16",
"axios": "^1.9.0",
"cli-progress": "^3.12.0",
"node-fetch": "^3.3.2",
"tar": "^7.4.3",
- "uuid": "^11.1.0",
- "windows-shortcuts": "^0.1.6"
+ "uuid": "^11.1.0"
},
"bin": {
"minecraft": "electron ui_index.cjs"
@@ -2003,12 +2001,6 @@
"node": ">=10.0.0"
}
},
- "node_modules/@xterm/xterm": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
- "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
- "license": "MIT"
- },
"node_modules/7zip-bin": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz",
@@ -6941,12 +6933,6 @@
"node": ">=8"
}
},
- "node_modules/windows-shortcuts": {
- "version": "0.1.6",
- "resolved": "https://registry.npmjs.org/windows-shortcuts/-/windows-shortcuts-0.1.6.tgz",
- "integrity": "sha512-kjkb3Hmmmg7jwnOb+29AOmoEEA1L/JeLsMOYovpLxYpuc+fN0R+pr8sMwep3JFhUZloxyw1XTzq8n3HugXkqBA==",
- "license": "MIT"
- },
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
diff --git a/package.json b/package.json
index 44c6457..f33c048 100644
--- a/package.json
+++ b/package.json
@@ -11,14 +11,12 @@
"test": "node ."
},
"dependencies": {
- "@xterm/xterm": "^5.5.0",
"adm-zip": "^0.5.16",
"axios": "^1.9.0",
"cli-progress": "^3.12.0",
"node-fetch": "^3.3.2",
"tar": "^7.4.3",
- "uuid": "^11.1.0",
- "windows-shortcuts": "^0.1.6"
+ "uuid": "^11.1.0"
},
"devDependencies": {
"electron": "^36.1.0",
From fdb8a05086f68ccf485b38eea9a89aedf3d79686 Mon Sep 17 00:00:00 2001
From: StoppedwummSites <150438484+StoppedwummSites@users.noreply.github.com>
Date: Wed, 7 May 2025 12:39:24 +0200
Subject: [PATCH 09/19] Add GitHub Actions workflow for ViTest testing
---
.github/workflows/test.yml | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
create mode 100644 .github/workflows/test.yml
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..53a97f2
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,19 @@
+name: Test with ViTest
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+jobs:
+ test:
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
+ with:
+ node-version: "16"
+ cache: "npm"
+ - run: npm ci
+ - run: npx vitest --run --coverage
+ env:
+ CI: true
\ No newline at end of file
From 6843adaa342c9f15bf7faaf2455cecb676d6d35a Mon Sep 17 00:00:00 2001
From: StoppedwummSites <150438484+StoppedwummSites@users.noreply.github.com>
Date: Wed, 7 May 2025 12:42:45 +0200
Subject: [PATCH 10/19] Update GitHub Actions workflow to target gui-preview
branch and upgrade Node.js version to 22
---
.github/workflows/test.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 53a97f2..a287751 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,9 +1,9 @@
name: Test with ViTest
on:
push:
- branches: [main]
+ branches: [gui-preview]
pull_request:
- branches: [main]
+ branches: [gui-preview]
jobs:
test:
runs-on: windows-latest
@@ -11,7 +11,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
- node-version: "16"
+ node-version: "22"
cache: "npm"
- run: npm ci
- run: npx vitest --run --coverage
From 43c55cd057d25837d5d70cf17ac550f3741b2664 Mon Sep 17 00:00:00 2001
From: StoppedwummSites <150438484+StoppedwummSites@users.noreply.github.com>
Date: Wed, 7 May 2025 12:44:25 +0200
Subject: [PATCH 11/19] Add Vitest coverage dependency to package.json and
package-lock.json
---
package-lock.json | 318 ++++++++++++++++++++++++++++++++++++++++++++++
package.json | 1 +
2 files changed, 319 insertions(+)
diff --git a/package-lock.json b/package-lock.json
index 4d63a31..06962d6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,11 +20,86 @@
"minecraft": "electron ui_index.cjs"
},
"devDependencies": {
+ "@vitest/coverage-v8": "^3.1.3",
"electron": "^36.1.0",
"electron-builder": "^26.0.12",
"vitest": "^3.1.3"
}
},
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz",
+ "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.1"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz",
+ "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@develar/schema-utils": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
@@ -1296,6 +1371,51 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
+ "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/set-array": "^1.2.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
@@ -1303,6 +1423,17 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.25",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
"node_modules/@malept/cross-spawn-promise": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz",
@@ -1878,6 +2009,39 @@
"@types/node": "*"
}
},
+ "node_modules/@vitest/coverage-v8": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.3.tgz",
+ "integrity": "sha512-cj76U5gXCl3g88KSnf80kof6+6w+K4BjOflCl7t6yRJPDuCrHtVu0SgNYOUARJOL5TI8RScDbm5x4s1/P9bvpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.3.0",
+ "@bcoe/v8-coverage": "^1.0.2",
+ "debug": "^4.4.0",
+ "istanbul-lib-coverage": "^3.2.2",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-lib-source-maps": "^5.0.6",
+ "istanbul-reports": "^3.1.7",
+ "magic-string": "^0.30.17",
+ "magicast": "^0.3.5",
+ "std-env": "^3.9.0",
+ "test-exclude": "^7.0.1",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/browser": "3.1.3",
+ "vitest": "3.1.3"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@vitest/expect": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.3.tgz",
@@ -4385,6 +4549,13 @@
"node": ">=10"
}
},
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
@@ -4628,6 +4799,60 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
+ "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.23",
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
+ "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
@@ -4840,6 +5065,47 @@
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
+ "node_modules/magicast": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
+ "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.25.4",
+ "@babel/types": "^7.25.4",
+ "source-map-js": "^1.2.0"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "7.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
+ "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/make-fetch-happen": {
"version": "10.2.1",
"resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz",
@@ -6477,6 +6743,58 @@
"rimraf": "bin.js"
}
},
+ "node_modules/test-exclude": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz",
+ "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^10.4.1",
+ "minimatch": "^9.0.4"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/test-exclude/node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/test-exclude/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/tiny-async-pool": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz",
diff --git a/package.json b/package.json
index f33c048..0f7d538 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"uuid": "^11.1.0"
},
"devDependencies": {
+ "@vitest/coverage-v8": "^3.1.3",
"electron": "^36.1.0",
"electron-builder": "^26.0.12",
"vitest": "^3.1.3"
From 041871b94a12636004bb4652c95bf6dfd30a95e7 Mon Sep 17 00:00:00 2001
From: StoppedwummSites <150438484+StoppedwummSites@users.noreply.github.com>
Date: Wed, 7 May 2025 12:55:08 +0200
Subject: [PATCH 12/19] Update .gitignore to include coverage directory and add
@vitest/ui dependency in package.json and package-lock.json
---
.gitignore | 3 +-
package-lock.json | 79 +++++++++++++++++++++++++++++++++++++++++++++++
package.json | 1 +
3 files changed, 82 insertions(+), 1 deletion(-)
diff --git a/.gitignore b/.gitignore
index 08be659..9511d3c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,4 +10,5 @@ java-runtime
dist
build
__pycache__
-test.txt
\ No newline at end of file
+test.txt
+coverage
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 06962d6..6202fdd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,6 +21,7 @@
},
"devDependencies": {
"@vitest/coverage-v8": "^3.1.3",
+ "@vitest/ui": "^3.1.3",
"electron": "^36.1.0",
"electron-builder": "^26.0.12",
"vitest": "^3.1.3"
@@ -1578,6 +1579,13 @@
"node": ">=14"
}
},
+ "node_modules/@polka/url": {
+ "version": "1.0.0-next.29",
+ "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
+ "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz",
@@ -2140,6 +2148,28 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/@vitest/ui": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.1.3.tgz",
+ "integrity": "sha512-IipSzX+8DptUdXN/GWq3hq5z18MwnpphYdOMm0WndkRGYELzfq7NDP8dMpZT7JGW1uXFrIGxOW2D0Xi++ulByg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "3.1.3",
+ "fflate": "^0.8.2",
+ "flatted": "^3.3.3",
+ "pathe": "^2.0.3",
+ "sirv": "^3.0.1",
+ "tinyglobby": "^0.2.13",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "vitest": "3.1.3"
+ }
+ },
"node_modules/@vitest/utils": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.3.tgz",
@@ -4089,6 +4119,13 @@
"node": "^12.20 || >= 14.13"
}
},
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/filelist": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@@ -4112,6 +4149,13 @@
"node": ">=10"
}
},
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
@@ -5529,6 +5573,16 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/mrmime": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
+ "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -6376,6 +6430,21 @@
"node": ">=10"
}
},
+ "node_modules/sirv": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz",
+ "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@polka/url": "^1.0.0-next.24",
+ "mrmime": "^2.0.0",
+ "totalist": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/slice-ansi": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
@@ -6896,6 +6965,16 @@
"tmp": "^0.2.0"
}
},
+ "node_modules/totalist": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
+ "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/truncate-utf8-bytes": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
diff --git a/package.json b/package.json
index 0f7d538..d0be05b 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
},
"devDependencies": {
"@vitest/coverage-v8": "^3.1.3",
+ "@vitest/ui": "^3.1.3",
"electron": "^36.1.0",
"electron-builder": "^26.0.12",
"vitest": "^3.1.3"
From fa816b8c0a4bc55aa2308fe5ddfa9e24335032b3 Mon Sep 17 00:00:00 2001
From: StoppedwummSites <150438484+StoppedwummSites@users.noreply.github.com>
Date: Fri, 9 May 2025 08:10:18 +0200
Subject: [PATCH 13/19] Add support for UI launch in the launcher and entry
point script
---
index.js | 4 ++--
ui_index.cjs | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/index.js b/index.js
index 9beab18..c0f749b 100644
--- a/index.js
+++ b/index.js
@@ -822,9 +822,9 @@ async function main() {
} // End of main function
-
+// --ui adds fix to the launcher (when the ui launches, it returns ui_index.cjs as the entry point script, which will trigger module logic, which is now fixed)
const entryPointScript = process.argv[1].split(path.sep).pop();
-if (entryPointScript === __filename || entryPointScript === __dirname.split(path.sep).pop()) {
+if (entryPointScript === __filename || entryPointScript === __dirname.split(path.sep).pop() || (process.argv.length == 3 && process.argv[2] == "--ui")) {
main().catch(error => {
console.error("\n--- An error occurred during setup or launch ---");
console.error(error);
diff --git a/ui_index.cjs b/ui_index.cjs
index 96c8006..d648f71 100644
--- a/ui_index.cjs
+++ b/ui_index.cjs
@@ -32,7 +32,7 @@ const createConsole = () => {
app.whenReady().then(() => {
ipcMain.on('launch', (event, arg) => {
let consoleWin = createConsole();
- const process = spawn('node', [mainIndex]);
+ const process = spawn('node', [mainIndex, "--ui"]);
process.stdout.on('data', (data) => {
consoleWin.webContents.send('msg', String(data)); // Send to renderer
From 033eb43beaa1fe47deca06186ca8d137e9c5689e Mon Sep 17 00:00:00 2001
From: StoppedwummSites <150438484+StoppedwummSites@users.noreply.github.com>
Date: Fri, 9 May 2025 09:16:59 +0200
Subject: [PATCH 14/19] Implement search functionality in mod dashboard and
update script type to module
---
ui/chrome/js/moddashboard.js | 23 +++++++++++++++++++++++
ui/chrome/moddashboard/index.html | 2 +-
2 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/ui/chrome/js/moddashboard.js b/ui/chrome/js/moddashboard.js
index e69de29..851b4d5 100644
--- a/ui/chrome/js/moddashboard.js
+++ b/ui/chrome/js/moddashboard.js
@@ -0,0 +1,23 @@
+document.getElementById("search").addEventListener("click", async function() {
+ let search = document.getElementById("searchText").value;
+ let searchUrl = "https://mc-backend-six.vercel.app/api/search?q=" + encodeURIComponent(search)
+ const searchResult = await fetch(searchUrl)
+ const data = await searchResult.json()
+ let result = data.hits
+ let resultDiv = document.getElementById("result")
+ resultDiv.innerHTML = ""
+ for (var i = 0; i < result.length; i++) {
+ let project = result[i]
+ let projectDiv = document.createElement("div")
+ projectDiv.className = "project"
+ projectDiv.innerHTML = `
+
+ ${project.title}
+ ${project.description}
+ Downloads: ${project.downloads}
+ Follows: ${project.follows}
+ View on Modrinth
+ `
+ resultDiv.appendChild(projectDiv)
+ }
+});
\ No newline at end of file
diff --git a/ui/chrome/moddashboard/index.html b/ui/chrome/moddashboard/index.html
index 3aa0c47..f70eeb9 100644
--- a/ui/chrome/moddashboard/index.html
+++ b/ui/chrome/moddashboard/index.html
@@ -24,5 +24,5 @@ Mod Details
-
+
\ No newline at end of file
From 68ddc1003c7b52d4d3e19a0db315443c34efdc9c Mon Sep 17 00:00:00 2001
From: StoppedwummSites <150438484+StoppedwummSites@users.noreply.github.com>
Date: Fri, 9 May 2025 09:19:30 +0200
Subject: [PATCH 15/19] Refactor mod dashboard layout by removing mod list and
details sections, and enhancing search functionality.
---
ui/chrome/moddashboard/index.html | 19 +++++++------------
1 file changed, 7 insertions(+), 12 deletions(-)
diff --git a/ui/chrome/moddashboard/index.html b/ui/chrome/moddashboard/index.html
index f70eeb9..c7aa512 100644
--- a/ui/chrome/moddashboard/index.html
+++ b/ui/chrome/moddashboard/index.html
@@ -11,18 +11,13 @@
Mod Dashboard
-
-
+
+
+
+
+
+
+
\ No newline at end of file
From 9d0881ebdea437a482f5ccfb75e62cc48755d9f2 Mon Sep 17 00:00:00 2001
From: StoppedwummSites <150438484+StoppedwummSites@users.noreply.github.com>
Date: Fri, 9 May 2025 10:35:11 +0200
Subject: [PATCH 16/19] Add install button and functionality to mod dashboard
for easier mod installation
---
ui/chrome/css/main.css | 18 ++++++++++++++++++
ui/chrome/js/moddashboard.js | 1 +
ui/chrome/moddashboard/index.html | 16 +++++++++++++++-
3 files changed, 34 insertions(+), 1 deletion(-)
diff --git a/ui/chrome/css/main.css b/ui/chrome/css/main.css
index 6b0d48d..fdfa898 100644
--- a/ui/chrome/css/main.css
+++ b/ui/chrome/css/main.css
@@ -24,4 +24,22 @@ body {
#terminal pre .error {
color: red;
+}
+
+.project {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-top: 20px;
+ /** Add an outer border to the project section */
+ border: 1px solid #ccc;
+}
+
+.project h3 {
+ margin-bottom: 10px;
+}
+
+.project p {
+ margin: 0;
+ font-size: 14px;
}
\ No newline at end of file
diff --git a/ui/chrome/js/moddashboard.js b/ui/chrome/js/moddashboard.js
index 851b4d5..ef326f6 100644
--- a/ui/chrome/js/moddashboard.js
+++ b/ui/chrome/js/moddashboard.js
@@ -17,6 +17,7 @@ document.getElementById("search").addEventListener("click", async function() {
Downloads: ${project.downloads}
Follows: ${project.follows}
View on Modrinth
+
`
resultDiv.appendChild(projectDiv)
}
diff --git a/ui/chrome/moddashboard/index.html b/ui/chrome/moddashboard/index.html
index c7aa512..ffb29db 100644
--- a/ui/chrome/moddashboard/index.html
+++ b/ui/chrome/moddashboard/index.html
@@ -1,11 +1,24 @@
+
Mod Dashboard
+
+
Mod Dashboard
@@ -16,8 +29,9 @@ Mod Dashboard
-
+
+
\ No newline at end of file
From 66010044687f9644a89fe316b2cf8b242ca25b01 Mon Sep 17 00:00:00 2001
From: StoppedwummSites <150438484+StoppedwummSites@users.noreply.github.com>
Date: Fri, 9 May 2025 11:01:47 +0200
Subject: [PATCH 17/19] Implement mod downloading functionality and enhance
dependency handling in mod dashboard
---
ui/chrome/moddashboard/index.html | 34 ++++++++++++++++++++++++------
ui/externalScripts/main_preload.js | 3 +++
ui_index.cjs | 28 +++++++++++++++++++-----
3 files changed, 53 insertions(+), 12 deletions(-)
diff --git a/ui/chrome/moddashboard/index.html b/ui/chrome/moddashboard/index.html
index ffb29db..c354274 100644
--- a/ui/chrome/moddashboard/index.html
+++ b/ui/chrome/moddashboard/index.html
@@ -8,19 +8,39 @@
diff --git a/ui/externalScripts/main_preload.js b/ui/externalScripts/main_preload.js
index 74c0498..6d2c0e3 100644
--- a/ui/externalScripts/main_preload.js
+++ b/ui/externalScripts/main_preload.js
@@ -3,5 +3,8 @@ const {ipcRenderer,contextBridge} = require('electron');
contextBridge.exposeInMainWorld('mcAPI', {
launch: () => {
ipcRenderer.send('launch');
+ },
+ downloadToModsFolder: (url) => {
+ ipcRenderer.send('downloadToModsFolder', url);
}
})
\ No newline at end of file
diff --git a/ui_index.cjs b/ui_index.cjs
index d648f71..ee515b8 100644
--- a/ui_index.cjs
+++ b/ui_index.cjs
@@ -33,25 +33,25 @@ app.whenReady().then(() => {
ipcMain.on('launch', (event, arg) => {
let consoleWin = createConsole();
const process = spawn('node', [mainIndex, "--ui"]);
-
+
process.stdout.on('data', (data) => {
consoleWin.webContents.send('msg', String(data)); // Send to renderer
console.log(`stdout: ${data}`);
});
-
+
process.stderr.on('data', (data) => {
consoleWin.webContents.send('error', String(data)); // Send to renderer
console.error(`stderr: ${data}`);
});
-
+
process.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});
-
+
process.on('error', (error) => {
console.error(`Error: ${error}`);
});
-
+
process.on('exit', (code) => {
console.log(`Process exited with code: ${code}`);
event.reply('process-exit', code);
@@ -59,5 +59,23 @@ app.whenReady().then(() => {
consoleWin = null;
});
});
+ ipcMain.on("downloadToModsFolder", (event, url) => {
+ fetch(url)
+ .then(response => {
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+ return response.arrayBuffer();
+ })
+ .then(buffer => {
+ const modsFolder = path.join(__dirname, ".minecraft", 'mods');
+ if (!fs.existsSync(modsFolder)) {
+ fs.mkdirSync(modsFolder);
+ }
+ const filePath = path.join(modsFolder, decodeURIComponent(path.basename(url)));
+ fs.writeFileSync(filePath, Buffer.from(buffer));
+ event.reply('download-complete', filePath);
+ })
+ });
createWindow();
});
\ No newline at end of file
From 43da4a45b5a6bb93fda0b72f910886a94278dbeb Mon Sep 17 00:00:00 2001
From: StoppedwummSites <150438484+StoppedwummSites@users.noreply.github.com>
Date: Fri, 9 May 2025 11:02:24 +0200
Subject: [PATCH 18/19] Fix body margin and padding for consistent layout in
main.css
---
ui/chrome/css/main.css | 2 ++
1 file changed, 2 insertions(+)
diff --git a/ui/chrome/css/main.css b/ui/chrome/css/main.css
index fdfa898..70d9893 100644
--- a/ui/chrome/css/main.css
+++ b/ui/chrome/css/main.css
@@ -2,6 +2,8 @@ body {
background-color: #ffffff;
font-family: Arial, sans-serif;
color: #333;
+ margin: 0;
+ padding: 20px;
}
#terminal {
From 6494a36451d59eee69934fc584bb23abd433b10c Mon Sep 17 00:00:00 2001
From: StoppedwummSites <150438484+StoppedwummSites@users.noreply.github.com>
Date: Fri, 9 May 2025 11:04:22 +0200
Subject: [PATCH 19/19] Replace deprecated fs.rmdir with fs.rm for removing old
backup in main function
---
index.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/index.js b/index.js
index c0f749b..afccbbf 100644
--- a/index.js
+++ b/index.js
@@ -812,7 +812,7 @@ async function main() {
});
})
// removing old backup
- await fs.rmdir(BACKUP_PATH, { recursive: true, force: true });
+ await fs.rm(BACKUP_PATH, { recursive: true, force: true });
} catch (error) {
console.error(`Failed to create backup: ${error}`);