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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modforge-cli
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: ModForge-CLI — a Modrinth-based Minecraft modpack builder
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "modforge-cli"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "ModForge-CLI — a Modrinth-based Minecraft modpack builder"
5
5
  authors = [{ name = "Frank1o3", email = "jahdy1o3@gmail.com" }]
6
6
  license = { text = "MIT" }
@@ -1,5 +1,5 @@
1
1
  """
2
2
  Auto-generated file. DO NOT EDIT.
3
3
  """
4
- __version__ = "0.2.0"
4
+ __version__ = "0.2.1"
5
5
  __author__ = "Frank1o3"
@@ -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.18.4"
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
- # Map loader names to their dependency keys
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
- "dependencies": {"minecraft": mc, loader_key: loader_version},
195
- "files": [], # Only for overrides, not mods
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 update modrinth.index.json dependencies.
82
+ Download all mods and register them in modrinth.index.json.
79
83
 
80
- Note: Modrinth launchers auto-download mods based on dependencies.
81
- We download to mods/ folder for local use, but the index only needs
82
- the version IDs in dependencies, not file paths.
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
- # Update dependencies section with correct loader version
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
- # Skip if already downloaded and hash matches
175
- if dest.exists():
176
- existing_hash = hashlib.sha1(dest.read_bytes()).hexdigest()
177
- if existing_hash == primary_file["hashes"]["sha1"]:
178
- console.print(f"[dim]✓ {primary_file['filename']} (cached)[/dim]")
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) # Delete corrupted file
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