modforge-cli 0.2.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {modforge_cli-0.2.0 → modforge_cli-0.2.1}/PKG-INFO +1 -1
- {modforge_cli-0.2.0 → modforge_cli-0.2.1}/pyproject.toml +1 -1
- {modforge_cli-0.2.0 → modforge_cli-0.2.1}/src/modforge_cli/__version__.py +1 -1
- {modforge_cli-0.2.0 → modforge_cli-0.2.1}/src/modforge_cli/cli.py +6 -8
- {modforge_cli-0.2.0 → modforge_cli-0.2.1}/src/modforge_cli/core/downloader.py +41 -42
- {modforge_cli-0.2.0 → modforge_cli-0.2.1}/src/modforge_cli/core/utils.py +2 -2
- {modforge_cli-0.2.0 → modforge_cli-0.2.1}/LICENSE +0 -0
- {modforge_cli-0.2.0 → modforge_cli-0.2.1}/README.md +0 -0
- {modforge_cli-0.2.0 → modforge_cli-0.2.1}/src/modforge_cli/__init__.py +0 -0
- {modforge_cli-0.2.0 → modforge_cli-0.2.1}/src/modforge_cli/__main__.py +0 -0
- {modforge_cli-0.2.0 → modforge_cli-0.2.1}/src/modforge_cli/api/__init__.py +0 -0
- {modforge_cli-0.2.0 → modforge_cli-0.2.1}/src/modforge_cli/api/modrinth.py +0 -0
- {modforge_cli-0.2.0 → modforge_cli-0.2.1}/src/modforge_cli/core/__init__.py +0 -0
- {modforge_cli-0.2.0 → modforge_cli-0.2.1}/src/modforge_cli/core/models.py +0 -0
- {modforge_cli-0.2.0 → modforge_cli-0.2.1}/src/modforge_cli/core/policy.py +0 -0
- {modforge_cli-0.2.0 → modforge_cli-0.2.1}/src/modforge_cli/core/resolver.py +0 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
|
-
import logging
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
import shutil
|
|
6
5
|
import subprocess
|
|
@@ -46,7 +45,7 @@ app = typer.Typer(
|
|
|
46
45
|
console = Console()
|
|
47
46
|
|
|
48
47
|
# Configuration
|
|
49
|
-
FABRIC_LOADER_VERSION = "0.
|
|
48
|
+
FABRIC_LOADER_VERSION = "0.16.9"
|
|
50
49
|
CONFIG_PATH = Path.home() / ".config" / "ModForge-CLI"
|
|
51
50
|
REGISTRY_PATH = CONFIG_PATH / "registry.json"
|
|
52
51
|
MODRINTH_API = CONFIG_PATH / "modrinth_api.json"
|
|
@@ -114,6 +113,7 @@ def main_callback(
|
|
|
114
113
|
|
|
115
114
|
if verbose:
|
|
116
115
|
# Enable verbose logging
|
|
116
|
+
import logging
|
|
117
117
|
|
|
118
118
|
logging.basicConfig(
|
|
119
119
|
level=logging.DEBUG,
|
|
@@ -176,8 +176,8 @@ def setup(
|
|
|
176
176
|
manifest = Manifest(name=name, minecraft=mc, loader=loader, loader_version=loader_version)
|
|
177
177
|
(pack_dir / "ModForge-CLI.json").write_text(manifest.model_dump_json(indent=4))
|
|
178
178
|
|
|
179
|
-
# Create Modrinth index
|
|
180
|
-
#
|
|
179
|
+
# Create Modrinth index following official format
|
|
180
|
+
# See: https://docs.modrinth.com/docs/modpacks/format/
|
|
181
181
|
loader_key_map = {
|
|
182
182
|
"fabric": "fabric-loader",
|
|
183
183
|
"quilt": "quilt-loader",
|
|
@@ -191,8 +191,8 @@ def setup(
|
|
|
191
191
|
"game": "minecraft",
|
|
192
192
|
"versionId": "1.0.0",
|
|
193
193
|
"name": name,
|
|
194
|
-
"
|
|
195
|
-
"
|
|
194
|
+
"files": [], # Will be populated during build
|
|
195
|
+
"dependencies": {loader_key: loader_version, "minecraft": mc},
|
|
196
196
|
}
|
|
197
197
|
(pack_dir / "modrinth.index.json").write_text(json.dumps(index_data, indent=2))
|
|
198
198
|
|
|
@@ -367,7 +367,6 @@ def export(pack_name: str | None = None) -> None:
|
|
|
367
367
|
|
|
368
368
|
if not installer.exists():
|
|
369
369
|
console.print("[yellow]Downloading Fabric installer...[/yellow]")
|
|
370
|
-
|
|
371
370
|
|
|
372
371
|
urllib.request.urlretrieve(FABRIC_INSTALLER_URL, installer)
|
|
373
372
|
|
|
@@ -492,7 +491,6 @@ def doctor() -> None:
|
|
|
492
491
|
|
|
493
492
|
# Check Java
|
|
494
493
|
try:
|
|
495
|
-
|
|
496
494
|
result = subprocess.run(["java", "-version"], capture_output=True, text=True, check=True)
|
|
497
495
|
console.print("[green]✓[/green] Java installed")
|
|
498
496
|
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
@@ -34,6 +34,10 @@ class ModDownloader:
|
|
|
34
34
|
|
|
35
35
|
self.index = json.loads(index_file.read_text())
|
|
36
36
|
|
|
37
|
+
# Ensure files array exists
|
|
38
|
+
if "files" not in self.index:
|
|
39
|
+
self.index["files"] = []
|
|
40
|
+
|
|
37
41
|
def _select_compatible_version(self, versions: list[dict]) -> dict | None:
|
|
38
42
|
"""
|
|
39
43
|
Select the most appropriate version based on:
|
|
@@ -75,11 +79,11 @@ class ModDownloader:
|
|
|
75
79
|
|
|
76
80
|
async def download_all(self, project_ids: Iterable[str]) -> None:
|
|
77
81
|
"""
|
|
78
|
-
Download all mods and
|
|
82
|
+
Download all mods and register them in modrinth.index.json.
|
|
79
83
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
The index format follows Modrinth's standard:
|
|
85
|
+
- files: array of all mods with hashes, URLs, and metadata
|
|
86
|
+
- dependencies: MC version and loader version
|
|
83
87
|
"""
|
|
84
88
|
tasks = [self._download_project(pid) for pid in project_ids]
|
|
85
89
|
|
|
@@ -95,35 +99,9 @@ class ModDownloader:
|
|
|
95
99
|
await coro
|
|
96
100
|
progress.advance(task_id)
|
|
97
101
|
|
|
98
|
-
#
|
|
99
|
-
self._update_dependencies()
|
|
102
|
+
# Write updated index
|
|
100
103
|
self.index_file.write_text(json.dumps(self.index, indent=2))
|
|
101
104
|
|
|
102
|
-
def _update_dependencies(self) -> None:
|
|
103
|
-
"""
|
|
104
|
-
Ensure dependencies section has correct MC version and loader.
|
|
105
|
-
This is what launchers use to setup the game.
|
|
106
|
-
"""
|
|
107
|
-
if "dependencies" not in self.index:
|
|
108
|
-
self.index["dependencies"] = {}
|
|
109
|
-
|
|
110
|
-
# Set Minecraft version
|
|
111
|
-
self.index["dependencies"]["minecraft"] = self.mc_version
|
|
112
|
-
|
|
113
|
-
# Set loader (fabric-loader, forge, quilt-loader, neoforge)
|
|
114
|
-
loader_key_map = {
|
|
115
|
-
"fabric": "fabric-loader",
|
|
116
|
-
"quilt": "quilt-loader",
|
|
117
|
-
"forge": "forge",
|
|
118
|
-
"neoforge": "neoforge",
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
loader_key = loader_key_map.get(self.loader.lower(), self.loader.lower())
|
|
122
|
-
|
|
123
|
-
# Use "*" to let launcher pick latest compatible version
|
|
124
|
-
# Or you can specify exact version if available
|
|
125
|
-
self.index["dependencies"][loader_key] = "*"
|
|
126
|
-
|
|
127
105
|
async def _download_project(self, project_id: str) -> None:
|
|
128
106
|
# 1. Fetch all versions for this project
|
|
129
107
|
url = self.api.project_versions(project_id)
|
|
@@ -171,17 +149,21 @@ class ModDownloader:
|
|
|
171
149
|
# 4. Download file to mods/ directory
|
|
172
150
|
dest = self.output_dir / primary_file["filename"]
|
|
173
151
|
|
|
174
|
-
#
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return
|
|
180
|
-
else:
|
|
181
|
-
console.print(
|
|
182
|
-
f"[yellow]Re-downloading {primary_file['filename']} (hash mismatch)[/yellow]"
|
|
183
|
-
)
|
|
152
|
+
# Check if already registered in index
|
|
153
|
+
existing_entry = next(
|
|
154
|
+
(f for f in self.index["files"] if f["path"] == f"mods/{primary_file['filename']}"),
|
|
155
|
+
None,
|
|
156
|
+
)
|
|
184
157
|
|
|
158
|
+
if existing_entry:
|
|
159
|
+
# Verify hash matches
|
|
160
|
+
if dest.exists():
|
|
161
|
+
existing_hash = hashlib.sha1(dest.read_bytes()).hexdigest()
|
|
162
|
+
if existing_hash == primary_file["hashes"]["sha1"]:
|
|
163
|
+
console.print(f"[dim]✓ {primary_file['filename']} (cached)[/dim]")
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
# Download the file
|
|
185
167
|
try:
|
|
186
168
|
async with self.session.get(primary_file["url"]) as r:
|
|
187
169
|
if r.status != 200:
|
|
@@ -197,14 +179,31 @@ class ModDownloader:
|
|
|
197
179
|
|
|
198
180
|
# 5. Verify hash
|
|
199
181
|
sha1 = hashlib.sha1(data).hexdigest()
|
|
182
|
+
sha512 = hashlib.sha512(data).hexdigest()
|
|
183
|
+
|
|
200
184
|
if sha1 != primary_file["hashes"]["sha1"]:
|
|
201
|
-
dest.unlink(missing_ok=True)
|
|
185
|
+
dest.unlink(missing_ok=True)
|
|
202
186
|
raise RuntimeError(
|
|
203
187
|
f"Hash mismatch for {primary_file['filename']}\n"
|
|
204
188
|
f" Expected: {primary_file['hashes']['sha1']}\n"
|
|
205
189
|
f" Got: {sha1}"
|
|
206
190
|
)
|
|
207
191
|
|
|
192
|
+
# 6. Register in index (Modrinth format)
|
|
193
|
+
file_entry = {
|
|
194
|
+
"path": f"mods/{primary_file['filename']}",
|
|
195
|
+
"hashes": {"sha1": sha1, "sha512": sha512},
|
|
196
|
+
"env": {"client": "required", "server": "required"},
|
|
197
|
+
"downloads": [primary_file["url"]],
|
|
198
|
+
"fileSize": primary_file["size"],
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# Remove existing entry if present (update scenario)
|
|
202
|
+
self.index["files"] = [f for f in self.index["files"] if f["path"] != file_entry["path"]]
|
|
203
|
+
|
|
204
|
+
# Add new entry
|
|
205
|
+
self.index["files"].append(file_entry)
|
|
206
|
+
|
|
208
207
|
console.print(
|
|
209
208
|
f"[green]✓[/green] {primary_file['filename']} "
|
|
210
209
|
f"[dim](v{version.get('version_number')}, {self.loader})[/dim]"
|
|
@@ -159,8 +159,8 @@ def install_fabric(
|
|
|
159
159
|
f"Fabric installation failed:\n{e.stderr}\n\n"
|
|
160
160
|
f"Make sure Java is installed and accessible."
|
|
161
161
|
) from e
|
|
162
|
-
except FileNotFoundError:
|
|
163
|
-
raise RuntimeError("Java not found. Please install Java 17 or higher and try again.")
|
|
162
|
+
except FileNotFoundError as e:
|
|
163
|
+
raise RuntimeError("Java not found. Please install Java 17 or higher and try again.") from e
|
|
164
164
|
|
|
165
165
|
|
|
166
166
|
def detect_install_method() -> str:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|